Spaces:
Running
Running
Commit
·
1d8ff49
1
Parent(s):
e617155
Bug-Fixed
Browse files- Dockerfile +16 -1
- requirements.txt +1 -1
- server.py +6 -5
- 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:
|
|
|
|
| 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 |
-
|
| 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
|
|
|
|
| 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.
|
|
|
|
| 24 |
|
| 25 |
def initialize_driver(self):
|
| 26 |
try:
|
| 27 |
-
self.micro_status("Initializing
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 41 |
-
|
| 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(
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
self.micro_status("Waiting for OTP screen...")
|
| 76 |
-
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 90 |
self.micro_status("Verifying login success...")
|
| 91 |
-
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
|
|
|
|
|
|
|
| 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(
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 133 |
-
|
| 134 |
-
|
|
|
|
| 135 |
time.sleep(3)
|
| 136 |
self.micro_status("Opening transaction details...")
|
| 137 |
-
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
| 140 |
self.micro_status("Adding to Vault...")
|
| 141 |
-
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
| 144 |
try:
|
| 145 |
self.micro_status("Verifying success and saving...")
|
| 146 |
-
company_input = WebDriverWait(self.driver, 10).until(
|
|
|
|
|
|
|
| 147 |
company_input.clear()
|
| 148 |
company_input.send_keys(patient_name)
|
| 149 |
-
WebDriverWait(self.driver, self.DEFAULT_TIMEOUT).until(
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
| 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 |
-
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
|
|
| 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.")
|