Refactor logging to use Loguru in Playwright scripts for improved logging capabilities
This commit is contained in:
@@ -13,7 +13,7 @@ Notes:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import logging
|
from loguru import logger
|
||||||
import os
|
import os
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@@ -22,12 +22,11 @@ import mimetypes
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from playwright.sync_api import sync_playwright
|
from playwright.sync_api import sync_playwright
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from loguru import logger
|
|
||||||
|
|
||||||
# Load environment variables from .env in the repository root (if present)
|
# Load environment variables from .env in the repository root (if present)
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -46,17 +45,18 @@ def main():
|
|||||||
parser.add_argument('--capture', action='store_true', help='Capture XHR/fetch requests and responses while the browser is open')
|
parser.add_argument('--capture', action='store_true', help='Capture XHR/fetch requests and responses while the browser is open')
|
||||||
parser.add_argument('--capture-output', default='xhr_captured', help='Directory to save captured XHRs')
|
parser.add_argument('--capture-output', default='xhr_captured', help='Directory to save captured XHRs')
|
||||||
parser.add_argument('--capture-wait', type=float, default=6.0, help='Seconds to wait after reload for XHRs to complete when capture starts')
|
parser.add_argument('--capture-wait', type=float, default=6.0, help='Seconds to wait after reload for XHRs to complete when capture starts')
|
||||||
|
parser.add_argument('--capture-types', default='xhr,fetch,document', help='Comma-separated list of resource types to capture (e.g. xhr,fetch,document,image)')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
os.makedirs(args.profile, exist_ok=True)
|
os.makedirs(args.profile, exist_ok=True)
|
||||||
|
|
||||||
with sync_playwright() as p:
|
with sync_playwright() as p:
|
||||||
try:
|
try:
|
||||||
logging.info('Launching Chrome channel with profile %s (headless=%s)', args.profile, args.headless)
|
logger.info('Launching Chrome channel with profile %s (headless=%s)', args.profile, args.headless)
|
||||||
context = p.chromium.launch_persistent_context(user_data_dir=args.profile, channel='chrome', headless=args.headless)
|
context = p.chromium.launch_persistent_context(user_data_dir=args.profile, channel='chrome', headless=args.headless)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error('Failed to launch Chrome channel: %s', e)
|
logger.error(f'Failed to launch Chrome channel: {e}')
|
||||||
logging.info('Falling back to default Chromium persistent context')
|
logger.info('Falling back to default Chromium persistent context')
|
||||||
context = p.chromium.launch_persistent_context(user_data_dir=args.profile, headless=args.headless)
|
context = p.chromium.launch_persistent_context(user_data_dir=args.profile, headless=args.headless)
|
||||||
|
|
||||||
page = context.new_page()
|
page = context.new_page()
|
||||||
@@ -95,7 +95,7 @@ def main():
|
|||||||
|
|
||||||
def on_response(resp):
|
def on_response(resp):
|
||||||
# lightweight debug log
|
# lightweight debug log
|
||||||
logging.debug('Response: %s %s', resp.status, resp.url)
|
logger.debug(f'Response: {resp.status} {resp.url}')
|
||||||
try:
|
try:
|
||||||
req = resp.request
|
req = resp.request
|
||||||
rtype = req.resource_type
|
rtype = req.resource_type
|
||||||
@@ -145,7 +145,7 @@ def main():
|
|||||||
info['response_text'] = None
|
info['response_text'] = None
|
||||||
info['response_body_file'] = path
|
info['response_body_file'] = path
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception('Failed to read response body for %s', req.url)
|
logger.exception(f'Failed to read response body for {req.url}')
|
||||||
|
|
||||||
fname = f"{sanitize_filename(req.url)}_{ts}.json"
|
fname = f"{sanitize_filename(req.url)}_{ts}.json"
|
||||||
out_path = os.path.join(args.capture_output, fname)
|
out_path = os.path.join(args.capture_output, fname)
|
||||||
@@ -154,10 +154,10 @@ def main():
|
|||||||
|
|
||||||
json.dump(info, fh, indent=2)
|
json.dump(info, fh, indent=2)
|
||||||
|
|
||||||
logging.info('Captured %s: %s -> %s', rtype, req.url, out_path)
|
logger.info(f'Captured {rtype}: {req.url} -> {out_path}')
|
||||||
captured.append(out_path)
|
captured.append(out_path)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception('Error in capture on_response')
|
logger.exception('Error in capture on_response')
|
||||||
|
|
||||||
page.on('response', on_response)
|
page.on('response', on_response)
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ def main():
|
|||||||
username = args.username or os.getenv('STATDX_USERNAME')
|
username = args.username or os.getenv('STATDX_USERNAME')
|
||||||
password = args.password or os.getenv('STATDX_PASSWORD')
|
password = args.password or os.getenv('STATDX_PASSWORD')
|
||||||
if username and password:
|
if username and password:
|
||||||
logging.info('Attempting automatic login using provided credentials')
|
logger.info('Attempting automatic login using provided credentials')
|
||||||
# candidate selectors for username and password fields
|
# candidate selectors for username and password fields
|
||||||
user_selectors = ['input[name="username"]', 'input.usernameSelector', 'input[type="email"]', 'input[name*="email" i]', 'input[name*="user" i]', 'input[type="text"]']
|
user_selectors = ['input[name="username"]', 'input.usernameSelector', 'input[type="email"]', 'input[name*="email" i]', 'input[name*="user" i]', 'input[type="text"]']
|
||||||
pass_selectors = ['input[type="password"]', 'input[name="password"]', 'input.passwordSelector']
|
pass_selectors = ['input[type="password"]', 'input[name="password"]', 'input.passwordSelector']
|
||||||
@@ -189,7 +189,7 @@ def main():
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if not user_sel or not pass_sel:
|
if not user_sel or not pass_sel:
|
||||||
logging.warning('Could not auto-detect username/password fields. Skipping automatic login.')
|
logger.warning('Could not auto-detect username/password fields. Skipping automatic login.')
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
page.fill(user_sel, username)
|
page.fill(user_sel, username)
|
||||||
@@ -205,12 +205,12 @@ def main():
|
|||||||
if btn:
|
if btn:
|
||||||
btn.click()
|
btn.click()
|
||||||
clicked = True
|
clicked = True
|
||||||
logging.info('Clicked submit button using selector: %s', s)
|
logger.info(f'Clicked submit button using selector: {s}')
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
if not clicked:
|
if not clicked:
|
||||||
logging.info('No submit button clicked; pressing Enter in password field')
|
logger.info('No submit button clicked; pressing Enter in password field')
|
||||||
page.press(pass_sel, 'Enter')
|
page.press(pass_sel, 'Enter')
|
||||||
|
|
||||||
# wait for either navigation or a post-login selector
|
# wait for either navigation or a post-login selector
|
||||||
@@ -218,19 +218,19 @@ def main():
|
|||||||
if args.post_login_selector:
|
if args.post_login_selector:
|
||||||
page.wait_for_selector(args.post_login_selector, timeout=int(args.wait_after_login * 1000))
|
page.wait_for_selector(args.post_login_selector, timeout=int(args.wait_after_login * 1000))
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.info('Post-login selector not found within timeout')
|
logger.info('Post-login selector not found within timeout')
|
||||||
|
|
||||||
# small extra wait to let JS render
|
# small extra wait to let JS render
|
||||||
sleep(args.wait_after_login)
|
sleep(args.wait_after_login)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception('Automatic login attempt failed')
|
logger.exception('Automatic login attempt failed')
|
||||||
|
|
||||||
if args.wait_for:
|
if args.wait_for:
|
||||||
try:
|
try:
|
||||||
logging.info('Waiting for selector: %s', args.wait_for)
|
logger.info(f'Waiting for selector: {args.wait_for}')
|
||||||
page.wait_for_selector(args.wait_for, timeout=20000)
|
page.wait_for_selector(args.wait_for, timeout=20000)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception('wait_for selector timed out')
|
logger.exception('wait_for selector timed out')
|
||||||
|
|
||||||
# give some time for JS to finish
|
# give some time for JS to finish
|
||||||
page.wait_for_timeout(1500)
|
page.wait_for_timeout(1500)
|
||||||
@@ -238,7 +238,7 @@ def main():
|
|||||||
# Screenshots are disabled per user request
|
# Screenshots are disabled per user request
|
||||||
|
|
||||||
if args.keep_open:
|
if args.keep_open:
|
||||||
logging.info('Leaving browser open for manual inspection. Close the browser window to finish.')
|
logger.info('Leaving browser open for manual inspection. Close the browser window to finish.')
|
||||||
try:
|
try:
|
||||||
# keep running until the user presses Enter in the terminal
|
# keep running until the user presses Enter in the terminal
|
||||||
input('Browser is open. Press Enter here to close and exit...')
|
input('Browser is open. Press Enter here to close and exit...')
|
||||||
@@ -248,7 +248,7 @@ def main():
|
|||||||
try:
|
try:
|
||||||
context.close()
|
context.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception('Error closing context')
|
logger.exception('Error closing context')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ You may need to tweak selectors if the login form uses unusual markup or an SSO
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import logging
|
from loguru import logger
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -21,7 +21,7 @@ TARGET_URL = os.getenv("STATDX_TOPICS_URL", "https://app.statdx.com/")
|
|||||||
if not USERNAME or not PASSWORD:
|
if not USERNAME or not PASSWORD:
|
||||||
raise SystemExit("Please set STATDX_USERNAME and STATDX_PASSWORD in your environment or .env file.")
|
raise SystemExit("Please set STATDX_USERNAME and STATDX_PASSWORD in your environment or .env file.")
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
# using loguru logger
|
||||||
|
|
||||||
|
|
||||||
def run(headless=True, timeout=30000):
|
def run(headless=True, timeout=30000):
|
||||||
@@ -32,16 +32,16 @@ def run(headless=True, timeout=30000):
|
|||||||
context = browser.new_context()
|
context = browser.new_context()
|
||||||
page = context.new_page()
|
page = context.new_page()
|
||||||
|
|
||||||
logging.info("Opening %s", TARGET_URL)
|
logger.info("Opening %s", TARGET_URL)
|
||||||
page.goto(TARGET_URL, wait_until="domcontentloaded", timeout=timeout)
|
page.goto(TARGET_URL, wait_until="domcontentloaded", timeout=timeout)
|
||||||
|
|
||||||
# Heuristics to find fields — adapt per the real login form
|
# Heuristics to find fields — adapt per the real login form
|
||||||
password_selector = "input[type=\"password\"]"
|
password_selector = "input[type=\"password\"]"
|
||||||
try:
|
try:
|
||||||
page.wait_for_selector(password_selector, timeout=8000)
|
page.wait_for_selector(password_selector, timeout=8000)
|
||||||
logging.info("Password field found")
|
logger.info("Password field found")
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.warning("Password field not found quickly; continuing to try common selectors")
|
logger.warning("Password field not found quickly; continuing to try common selectors")
|
||||||
|
|
||||||
# Try to detect username/email field by common selectors
|
# Try to detect username/email field by common selectors
|
||||||
candidate_user_selectors = [
|
candidate_user_selectors = [
|
||||||
@@ -61,18 +61,18 @@ def run(headless=True, timeout=30000):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if not user_sel:
|
if not user_sel:
|
||||||
logging.warning("Could not auto-detect username field; you may need to edit selectors in this script.")
|
logger.warning("Could not auto-detect username field; you may need to edit selectors in this script.")
|
||||||
# fallback: pick first visible text input
|
# fallback: pick first visible text input
|
||||||
inputs = page.query_selector_all("input[type=\"text\"]")
|
inputs = page.query_selector_all("input[type=\"text\"]")
|
||||||
if inputs:
|
if inputs:
|
||||||
user_sel = "input[type=\"text\"]"
|
user_sel = "input[type=\"text\"]"
|
||||||
|
|
||||||
logging.info("Using username selector: %s", user_sel)
|
logger.info("Using username selector: %s", user_sel)
|
||||||
|
|
||||||
if user_sel:
|
if user_sel:
|
||||||
page.fill(user_sel, USERNAME)
|
page.fill(user_sel, USERNAME)
|
||||||
else:
|
else:
|
||||||
logging.error("No username field found. Aborting.")
|
logger.error("No username field found. Aborting.")
|
||||||
browser.close()
|
browser.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ def run(headless=True, timeout=30000):
|
|||||||
if page.query_selector(password_selector):
|
if page.query_selector(password_selector):
|
||||||
page.fill(password_selector, PASSWORD)
|
page.fill(password_selector, PASSWORD)
|
||||||
else:
|
else:
|
||||||
logging.error("No password field found. Aborting.")
|
logger.error("No password field found. Aborting.")
|
||||||
browser.close()
|
browser.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -101,21 +101,21 @@ def run(headless=True, timeout=30000):
|
|||||||
if btn:
|
if btn:
|
||||||
btn.click()
|
btn.click()
|
||||||
clicked = True
|
clicked = True
|
||||||
logging.info("Clicked submit via selector %s", s)
|
logger.info("Clicked submit via selector %s", s)
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not clicked:
|
if not clicked:
|
||||||
logging.warning("No submit button clicked; attempting to press Enter on password field")
|
logger.warning("No submit button clicked; attempting to press Enter on password field")
|
||||||
page.press(password_selector, "Enter")
|
page.press(password_selector, "Enter")
|
||||||
|
|
||||||
# Wait for navigation or the app root element that indicates successful login
|
# Wait for navigation or the app root element that indicates successful login
|
||||||
try:
|
try:
|
||||||
page.wait_for_selector("#ds-app", timeout=20000)
|
page.wait_for_selector("#ds-app", timeout=20000)
|
||||||
logging.info("#ds-app found — likely logged in and app rendered")
|
logger.info("#ds-app found — likely logged in and app rendered")
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.info("Timed out waiting for #ds-app — page content may still be loading or login failed.")
|
logger.info("Timed out waiting for #ds-app — page content may still be loading or login failed.")
|
||||||
|
|
||||||
# Optional: wait a bit for client-side rendering
|
# Optional: wait a bit for client-side rendering
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
@@ -124,7 +124,7 @@ def run(headless=True, timeout=30000):
|
|||||||
out_file = "topics_playwright.html"
|
out_file = "topics_playwright.html"
|
||||||
with open(out_file, "w", encoding="utf-8") as f:
|
with open(out_file, "w", encoding="utf-8") as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
logging.info("Saved rendered HTML to %s", out_file)
|
logger.info("Saved rendered HTML to %s", out_file)
|
||||||
|
|
||||||
browser.close()
|
browser.close()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user