sonuprasad23 commited on
Commit
1d8ff49
·
1 Parent(s): e617155
Files changed (4) hide show
  1. Dockerfile +16 -1
  2. requirements.txt +1 -1
  3. server.py +6 -5
  4. worker.py +82 -41
Dockerfile CHANGED
@@ -1,3 +1,4 @@
 
1
  FROM python:3.11-slim
2
 
3
  WORKDIR /app
@@ -7,11 +8,25 @@ ENV TZ=Etc/UTC
7
  ENV HOME=/home/appuser
8
  ENV PYTHONUSERBASE=/home/appuser/.local
9
 
 
10
  RUN apt-get update && apt-get install -y --no-install-recommends \
 
 
 
 
11
  chromium \
12
  chromium-driver \
 
 
 
 
 
 
 
 
13
  && rm -rf /var/lib/apt/lists/*
14
 
 
15
  RUN addgroup --system appuser && adduser --system --ingroup appuser appuser
16
  RUN mkdir -p /home/appuser/.local && chown -R appuser:appuser /app /home/appuser
17
 
@@ -26,4 +41,4 @@ COPY --chown=appuser:appuser . .
26
 
27
  EXPOSE 7860
28
 
29
- CMD ["python", "server.py"]
 
1
+ # Dockerfile
2
  FROM python:3.11-slim
3
 
4
  WORKDIR /app
 
8
  ENV HOME=/home/appuser
9
  ENV PYTHONUSERBASE=/home/appuser/.local
10
 
11
+ # System deps + Chromium + ChromeDriver + common headless libs
12
  RUN apt-get update && apt-get install -y --no-install-recommends \
13
+ wget \
14
+ gnupg \
15
+ unzip \
16
+ ca-certificates \
17
  chromium \
18
  chromium-driver \
19
+ libnss3 \
20
+ libxss1 \
21
+ libasound2 \
22
+ libxtst6 \
23
+ libatk-bridge2.0-0 \
24
+ libgtk-3-0 \
25
+ libgbm1 \
26
+ fonts-liberation \
27
  && rm -rf /var/lib/apt/lists/*
28
 
29
+ # Create unprivileged user
30
  RUN addgroup --system appuser && adduser --system --ingroup appuser appuser
31
  RUN mkdir -p /home/appuser/.local && chown -R appuser:appuser /app /home/appuser
32
 
 
41
 
42
  EXPOSE 7860
43
 
44
+ CMD ["python", "server.py"]
requirements.txt CHANGED
@@ -7,4 +7,4 @@ python-dotenv
7
  selenium
8
  google-api-python-client
9
  google-auth-httplib2
10
- google-auth-oauthlib
 
7
  selenium
8
  google-api-python-client
9
  google-auth-httplib2
10
+ google-auth-oauthlib
server.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import eventlet
2
  eventlet.monkey_patch()
3
 
@@ -23,7 +24,7 @@ load_dotenv()
23
 
24
  app = Flask(__name__)
25
  app.config['SECRET_KEY'] = 'secret-key-for-hillside-automation'
26
- FRONTEND_ORIGIN = os.getenv('FRONTEND_URL', 'http://127.0.0.1:5500')
27
  CORS(app, resources={r"/*": {"origins": FRONTEND_ORIGIN}})
28
  socketio = SocketIO(app, cors_allowed_origins=FRONTEND_ORIGIN, async_mode='eventlet')
29
 
@@ -36,7 +37,8 @@ class EmailService:
36
  self.sender_email = os.getenv('EMAIL_SENDER')
37
  self.password = os.getenv('EMAIL_PASSWORD')
38
  self.smtp_server = "smtp.gmail.com"; self.smtp_port = 587
39
- if not self.sender_email or not self.password: print("[Email] WARNING: Email credentials not found in secrets.")
 
40
  def send_report(self, recipients, subject, body, attachments=None):
41
  if not self.sender_email or not self.password: return False
42
  try:
@@ -139,8 +141,7 @@ def handle_init(data):
139
  session_data[session_id] = {'csv_content': data['content'], 'emails': data['emails'], 'filename': data['filename']}
140
  if bot_instance: bot_instance.shutdown()
141
  bot_instance = QuantumBot(socketio, app)
142
-
143
- # --- Definitive Fix: Capture and send detailed error message ---
144
  is_success, error_message = bot_instance.initialize_driver()
145
  if is_success:
146
  emit('bot_initialized')
@@ -174,4 +175,4 @@ if __name__ == '__main__':
174
  print(" Make sure all secrets are set in your Hugging Face Space.")
175
  print(" Listening on http://127.0.0.1:5000")
176
  print("====================================================================")
177
- socketio.run(app, host='0.0.0.0', port=int(os.getenv('PORT', 7860)))
 
1
+ # server.py
2
  import eventlet
3
  eventlet.monkey_patch()
4
 
 
24
 
25
  app = Flask(__name__)
26
  app.config['SECRET_KEY'] = 'secret-key-for-hillside-automation'
27
+ FRONTEND_ORIGIN = os.getenv('FRONTEND_URL', 'http://127.0.0.1:5500')
28
  CORS(app, resources={r"/*": {"origins": FRONTEND_ORIGIN}})
29
  socketio = SocketIO(app, cors_allowed_origins=FRONTEND_ORIGIN, async_mode='eventlet')
30
 
 
37
  self.sender_email = os.getenv('EMAIL_SENDER')
38
  self.password = os.getenv('EMAIL_PASSWORD')
39
  self.smtp_server = "smtp.gmail.com"; self.smtp_port = 587
40
+ if not self.sender_email or not self.password:
41
+ print("[Email] WARNING: Email credentials not found in secrets.")
42
  def send_report(self, recipients, subject, body, attachments=None):
43
  if not self.sender_email or not self.password: return False
44
  try:
 
141
  session_data[session_id] = {'csv_content': data['content'], 'emails': data['emails'], 'filename': data['filename']}
142
  if bot_instance: bot_instance.shutdown()
143
  bot_instance = QuantumBot(socketio, app)
144
+
 
145
  is_success, error_message = bot_instance.initialize_driver()
146
  if is_success:
147
  emit('bot_initialized')
 
175
  print(" Make sure all secrets are set in your Hugging Face Space.")
176
  print(" Listening on http://127.0.0.1:5000")
177
  print("====================================================================")
178
+ socketio.run(app, host='0.0.0.0', port=int(os.getenv('PORT', 7860)))
worker.py CHANGED
@@ -1,17 +1,18 @@
1
- import pandas as pd
2
  import time
3
- import io
4
  import os
5
  import threading
6
  import tempfile
7
  import shutil
 
8
  from selenium import webdriver
9
  from selenium.webdriver.chrome.service import Service as ChromeService
10
  from selenium.webdriver.chrome.options import Options as ChromeOptions
11
  from selenium.webdriver.common.by import By
12
  from selenium.webdriver.support.ui import WebDriverWait
13
  from selenium.webdriver.support import expected_conditions as EC
14
- from selenium.common.exceptions import TimeoutException, NoSuchElementException, ElementNotInteractableException
 
15
 
16
  class QuantumBot:
17
  def __init__(self, socketio, app):
@@ -20,31 +21,39 @@ class QuantumBot:
20
  self.driver = None
21
  self.DEFAULT_TIMEOUT = 30
22
  self.termination_event = threading.Event()
23
- self.temp_dir = None
 
24
 
25
  def initialize_driver(self):
26
  try:
27
- self.micro_status("Initializing isolated headless browser...")
28
  options = ChromeOptions()
 
 
29
  options.binary_location = "/usr/bin/chromium"
30
- user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
31
-
32
- self.temp_dir = tempfile.mkdtemp()
33
 
34
- options.add_argument(f"user-agent={user_agent}")
 
 
 
 
 
 
 
 
35
  options.add_argument("--headless=new")
36
  options.add_argument("--window-size=1920,1080")
 
 
37
  options.add_argument("--no-sandbox")
38
  options.add_argument("--disable-dev-shm-usage")
39
  options.add_argument("--disable-gpu")
40
- options.add_argument(f"--user-data-dir={self.temp_dir}")
41
- options.add_argument('--disable-blink-features=AutomationControlled')
42
- options.add_experimental_option("excludeSwitches", ["enable-automation"])
43
- options.add_experimental_option('useAutomationExtension', False)
44
-
45
  service = ChromeService(executable_path="/usr/bin/chromedriver")
46
  self.driver = webdriver.Chrome(service=service, options=options)
47
-
 
48
  self.driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
49
  'source': "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
50
  })
@@ -69,11 +78,19 @@ class QuantumBot:
69
  self.driver.get("https://gateway.quantumepay.com/")
70
  time.sleep(2)
71
  self.micro_status("Entering credentials...")
72
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Username"))).send_keys(username)
73
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "Password"))).send_keys(password)
74
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
 
 
 
 
 
 
75
  self.micro_status("Waiting for OTP screen...")
76
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.presence_of_element_located((By.ID, "code1")))
 
 
77
  return True, None
78
  except Exception as e:
79
  error_message = f"Error during login: {str(e)}"
@@ -86,9 +103,13 @@ class QuantumBot:
86
  otp_digits = list(otp)
87
  for i in range(6):
88
  self.driver.find_element(By.ID, f"code{i+1}").send_keys(otp_digits[i])
89
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.ID, "login"))).click()
 
 
90
  self.micro_status("Verifying login success...")
91
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']")))
 
 
92
  return True, None
93
  except Exception as e:
94
  error_message = f"Error during OTP submission: {str(e)}"
@@ -117,50 +138,70 @@ class QuantumBot:
117
  try:
118
  self.micro_status(f"Navigating to Void page for '{patient_name}'")
119
  self.driver.get("https://gateway.quantumepay.com/credit-card/void")
120
-
121
  search_successful = False
122
  for attempt in range(15):
123
  try:
124
  self.micro_status(f"Searching for patient (Attempt {attempt + 1})...")
125
- WebDriverWait(self.driver, 2).until(EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'table-wrapper')]")))
126
- search_box = WebDriverWait(self.driver, 2).until(EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']")))
 
 
 
 
127
  search_box.click(); time.sleep(0.5)
128
  search_box.clear(); time.sleep(0.5)
129
  search_box.send_keys(patient_name)
130
  search_successful = True
131
  break
132
- except Exception: time.sleep(1)
133
- if not search_successful: raise Exception("Failed to search for patient.")
134
-
 
135
  time.sleep(3)
136
  self.micro_status("Opening transaction details...")
137
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))).click()
138
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))).click()
139
-
 
 
 
140
  self.micro_status("Adding to Vault...")
141
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))).click()
142
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"))).click()
143
-
 
 
 
144
  try:
145
  self.micro_status("Verifying success and saving...")
146
- company_input = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.NAME, "company_name")))
 
 
147
  company_input.clear()
148
  company_input.send_keys(patient_name)
149
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))).click()
150
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))).click()
 
 
 
 
151
  time.sleep(5)
152
  return 'Done'
153
  except TimeoutException:
154
  self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
155
- WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))).click()
 
 
156
  return 'Bad'
157
  except Exception as e:
158
  print(f"An error occurred while processing {patient_name}: {e}")
159
  return 'Error'
160
-
161
  def shutdown(self):
162
  if self.driver:
163
  self.driver.quit()
164
- if self.temp_dir and os.path.exists(self.temp_dir):
165
- shutil.rmtree(self.temp_dir)
166
- print(f"[Bot] Cleaned up temporary directory: {self.temp_dir}")
 
 
 
1
+ # worker.py
2
  import time
 
3
  import os
4
  import threading
5
  import tempfile
6
  import shutil
7
+
8
  from selenium import webdriver
9
  from selenium.webdriver.chrome.service import Service as ChromeService
10
  from selenium.webdriver.chrome.options import Options as ChromeOptions
11
  from selenium.webdriver.common.by import By
12
  from selenium.webdriver.support.ui import WebDriverWait
13
  from selenium.webdriver.support import expected_conditions as EC
14
+ from selenium.common.exceptions import TimeoutException
15
+
16
 
17
  class QuantumBot:
18
  def __init__(self, socketio, app):
 
21
  self.driver = None
22
  self.DEFAULT_TIMEOUT = 30
23
  self.termination_event = threading.Event()
24
+ self.chrome_user_data_dir = None
25
+ self.chrome_cache_dir = None
26
 
27
  def initialize_driver(self):
28
  try:
29
+ self.micro_status("Initializing headless browser...")
30
  options = ChromeOptions()
31
+
32
+ # Use system Chromium installed in the image
33
  options.binary_location = "/usr/bin/chromium"
 
 
 
34
 
35
+ # Per-run, unique profile and cache to avoid "already in use" locks
36
+ self.chrome_user_data_dir = tempfile.mkdtemp(prefix="chrome-user-data-")
37
+ self.chrome_cache_dir = tempfile.mkdtemp(prefix="chrome-cache-")
38
+ options.add_argument(f"--user-data-dir={self.chrome_user_data_dir}")
39
+ options.add_argument(f"--disk-cache-dir={self.chrome_cache_dir}")
40
+
41
+ # Stable/headless startup flags
42
+ user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36"
43
+ options.add_argument(f"--user-agent={user_agent}")
44
  options.add_argument("--headless=new")
45
  options.add_argument("--window-size=1920,1080")
46
+ options.add_argument("--no-first-run")
47
+ options.add_argument("--no-default-browser-check")
48
  options.add_argument("--no-sandbox")
49
  options.add_argument("--disable-dev-shm-usage")
50
  options.add_argument("--disable-gpu")
51
+ options.add_argument("--disable-features=Translate,AutomationControlled")
52
+
 
 
 
53
  service = ChromeService(executable_path="/usr/bin/chromedriver")
54
  self.driver = webdriver.Chrome(service=service, options=options)
55
+
56
+ # De-emphasize webdriver flag
57
  self.driver.execute_cdp_cmd('Page.addScriptToEvaluateOnNewDocument', {
58
  'source': "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
59
  })
 
78
  self.driver.get("https://gateway.quantumepay.com/")
79
  time.sleep(2)
80
  self.micro_status("Entering credentials...")
81
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
82
+ EC.presence_of_element_located((By.ID, "Username"))
83
+ ).send_keys(username)
84
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
85
+ EC.presence_of_element_located((By.ID, "Password"))
86
+ ).send_keys(password)
87
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
88
+ EC.element_to_be_clickable((By.ID, "login"))
89
+ ).click()
90
  self.micro_status("Waiting for OTP screen...")
91
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
92
+ EC.presence_of_element_located((By.ID, "code1"))
93
+ )
94
  return True, None
95
  except Exception as e:
96
  error_message = f"Error during login: {str(e)}"
 
103
  otp_digits = list(otp)
104
  for i in range(6):
105
  self.driver.find_element(By.ID, f"code{i+1}").send_keys(otp_digits[i])
106
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
107
+ EC.element_to_be_clickable((By.ID, "login"))
108
+ ).click()
109
  self.micro_status("Verifying login success...")
110
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
111
+ EC.element_to_be_clickable((By.XPATH, "//span[text()='Payments']"))
112
+ )
113
  return True, None
114
  except Exception as e:
115
  error_message = f"Error during OTP submission: {str(e)}"
 
138
  try:
139
  self.micro_status(f"Navigating to Void page for '{patient_name}'")
140
  self.driver.get("https://gateway.quantumepay.com/credit-card/void")
 
141
  search_successful = False
142
  for attempt in range(15):
143
  try:
144
  self.micro_status(f"Searching for patient (Attempt {attempt + 1})...")
145
+ WebDriverWait(self.driver, 2).until(
146
+ EC.presence_of_element_located((By.XPATH, "//div[contains(@class, 'table-wrapper')]"))
147
+ )
148
+ search_box = WebDriverWait(self.driver, 2).until(
149
+ EC.element_to_be_clickable((By.XPATH, "//input[@placeholder='Search']"))
150
+ )
151
  search_box.click(); time.sleep(0.5)
152
  search_box.clear(); time.sleep(0.5)
153
  search_box.send_keys(patient_name)
154
  search_successful = True
155
  break
156
+ except Exception:
157
+ time.sleep(1)
158
+ if not search_successful:
159
+ raise Exception("Failed to search for patient.")
160
  time.sleep(3)
161
  self.micro_status("Opening transaction details...")
162
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
163
+ EC.element_to_be_clickable((By.XPATH, f"//tr[contains(., \"{patient_name}\")]//button[@data-v-b6b33fa0]"))
164
+ ).click()
165
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
166
+ EC.element_to_be_clickable((By.LINK_TEXT, "Transaction Detail"))
167
+ ).click()
168
  self.micro_status("Adding to Vault...")
169
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
170
+ EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Add to Vault']"))
171
+ ).click()
172
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
173
+ EC.element_to_be_clickable((By.XPATH, "//div[@class='modal-footer']//button/span[normalize-space()='Confirm']"))
174
+ ).click()
175
  try:
176
  self.micro_status("Verifying success and saving...")
177
+ company_input = WebDriverWait(self.driver, 10).until(
178
+ EC.element_to_be_clickable((By.NAME, "company_name"))
179
+ )
180
  company_input.clear()
181
  company_input.send_keys(patient_name)
182
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
183
+ EC.element_to_be_clickable((By.XPATH, "//button/span[normalize-space()='Save Changes']"))
184
+ ).click()
185
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
186
+ EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Confirm']]"))
187
+ ).click()
188
  time.sleep(5)
189
  return 'Done'
190
  except TimeoutException:
191
  self.micro_status(f"'{patient_name}' is in a bad state, cancelling.")
192
+ WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
193
+ EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space()='Cancel']]"))
194
+ ).click()
195
  return 'Bad'
196
  except Exception as e:
197
  print(f"An error occurred while processing {patient_name}: {e}")
198
  return 'Error'
199
+
200
  def shutdown(self):
201
  if self.driver:
202
  self.driver.quit()
203
+ # Cleanup per-run temp dirs
204
+ for d in [self.chrome_user_data_dir, self.chrome_cache_dir]:
205
+ if d and os.path.exists(d):
206
+ shutil.rmtree(d, ignore_errors=True)
207
+ print(f"[Bot] Cleaned up temporary Chrome profile and cache.")