Conversation
- reservation_bot.py: API-based booking bot with slot monitoring - browser_bot.py: Playwright browser automation for form filling - config.py: Environment-based configuration management - requirements.txt: Project dependencies (curl_cffi, playwright) - .env.example: Configuration template - Updated readme.md with comprehensive documentation Educational/theoretical implementation demonstrating: - Web API interaction and scraping - Browser automation with anti-detection - Telegram notification integration
- hybrid_bot.py: Production-ready bot combining fast API monitoring with reliable browser-based booking - Parallel date scanning (~2s for 30 days) - SQLite state persistence (survives restarts) - Smart slot filtering (dates, times, quota) - CAPTCHA detection with manual solving flow - CLI interface with multiple options - Graceful shutdown handling - Docker support: - Dockerfile with Playwright/Chromium - docker-compose.yml for easy deployment - Updated .env.example with all configuration options - Comprehensive README with architecture diagrams
📝 WalkthroughWalkthroughAdds a Kosmos Visa Appointment Bot suite: environment/config templates, three bot implementations (browser, API, hybrid), Docker/Docker Compose setup, dependencies, rewritten README, and SQLite state persistence. Provides slot monitoring, Telegram notifications, optional Playwright auto-booking, and CLI orchestration. Changes
Sequence Diagram(s)sequenceDiagram
participant Bot as HybridBot
participant Monitor as SlotMonitor
participant API as Visa API
participant Notifier as TelegramNotifier
participant Booker as BrowserBooker
participant Portal as Booking Portal
participant DB as SQLite State
rect rgb(230,245,255)
Note over Bot,DB: Initialization
Bot->>DB: Load state (notified slots, successful bookings)
Bot->>Notifier: notify_bot_started()
end
rect rgb(240,255,240)
Note over Bot,API: Monitoring Loop (periodic)
loop Every check_interval
Bot->>Monitor: scan_all_dates()
Monitor->>API: check_date() for each day
API-->>Monitor: return available slots
Monitor->>Monitor: filter_by_time (preferred_times)
Monitor-->>Bot: list[Slot]
end
end
rect rgb(255,250,240)
Note over Bot,Notifier: Notification Flow
Bot->>DB: is_slot_notified(slot_key)?
alt Slot not yet notified
Bot->>DB: mark_slot_notified(slot_key)
Bot->>Notifier: notify_slot_available(slot)
Notifier-->>Notifier: format HTML message
Notifier-->>Telegram: send alert
end
end
rect rgb(255,245,245)
Note over Bot,Portal: Auto-Booking Flow (if enabled & not yet booked)
alt auto_book enabled AND no successful booking yet
Bot->>Booker: book_slot(slot)
Booker->>Booker: setup() - launch browser
Booker->>Portal: navigate_to_booking()
Booker->>Portal: select_appointment_type()
Booker->>Portal: select_date_and_time()
Booker->>Portal: fill_applicant_form()
Booker->>Portal: wait for CAPTCHA
alt CAPTCHA detected
Booker->>Notifier: notify_captcha_required()
Notifier-->>Telegram: send CAPTCHA alert (manual intervention)
Booker->>Booker: wait for manual completion
end
Booker->>Portal: submit_booking()
Portal-->>Booker: confirmation page
Booker->>Booker: extract_confirmation()
Booker->>Booker: take_screenshot()
Booker->>DB: log_successful_booking()
Booker->>Notifier: notify_booking_success(confirmation_id)
Notifier-->>Telegram: send success notification
Booker->>Booker: close() - cleanup browser
else Booking fails
Booker->>DB: log_booking_attempt(status=failed)
Booker->>Notifier: notify_booking_failed(error)
Notifier-->>Telegram: send failure alert
end
end
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes
Pre-merge checks❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (15)
readme.md (3)
20-20: Add language specifiers to fenced code blocks.Markdown best practice: all code blocks should declare their language for syntax highlighting and linting clarity.
🔎 Proposed fixes
-``` +``` kosmos-vize-bot/ ├── hybrid_bot.py # ⭐ RECOMMENDED - Hybrid approach ... -``` +```And similarly for line 92 (the ASCII architecture diagram). For ASCII art without code, you can use:
-``` +```text ┌─────────────────────────────────────────────────────────────────┐ │ HYBRID BOT FLOW │ ... -``` +```textAlso applies to: 92-92
85-85: Remove tautological phrase "CLI interface".The term "CLI" already means "Command-Line Interface"; saying "CLI interface" is redundant. Simplify to "CLI" in the comparison table.
🔎 Proposed fix
-| CLI interface | ✅ | ❌ | ❌ | +| CLI | ✅ | ❌ | ❌ |
222-232: Use heading structure instead of emphasis for section boundary.Lines 221–222 use bold emphasis (
**EDUCATIONAL PURPOSE ONLY**) as a de-facto heading, but Markdown prefers explicit heading syntax for semantic clarity and proper document structure.🔎 Proposed fix
-## Disclaimer - -**EDUCATIONAL PURPOSE ONLY** - -This project demonstrates: +## Disclaimer + +### Educational Purpose Only + +This project demonstrates:browser_bot.py (2)
572-581: Hardcoded PII in example code.The
main()function contains hardcoded example personal data. While labeled as example, this could be accidentally committed with real data or copied without modification.🔎 Proposed fix - load from environment variables
async def main(): """Example usage""" # Applicant information - applicant = ApplicantInfo( - first_name="John", - last_name="Doe", - nationality_number="12345678901", - passport_number="U12345678", - birth_date="1990-01-15", - email="john.doe@example.com", - phone="+905551234567" - ) + applicant = ApplicantInfo( + first_name=os.getenv('APPLICANT_FIRST_NAME', ''), + last_name=os.getenv('APPLICANT_LAST_NAME', ''), + nationality_number=os.getenv('APPLICANT_NATIONALITY_NUMBER', ''), + passport_number=os.getenv('APPLICANT_PASSPORT_NUMBER', ''), + birth_date=os.getenv('APPLICANT_BIRTH_DATE', ''), + email=os.getenv('APPLICANT_EMAIL', ''), + phone=os.getenv('APPLICANT_PHONE', '') + )
180-189: Synchronous blocking call in async context.
requests.getfromcurl_cffiis a synchronous call inside an async method. This blocks the event loop and degrades concurrency.🔎 Proposed fix - run in executor like hybrid_bot.py does
async def check_availability_api(self, date: str) -> List[dict]: """ Check availability using the API (faster than browser) Uses curl_cffi for browser impersonation """ url = f"{self.API_URL}/AppointmentLayouts/GetAppointmentHourQoutaInfo" # ... params and headers setup ... try: - response = requests.get(url, headers=headers, params=params, impersonate="chrome") + loop = asyncio.get_event_loop() + response = await loop.run_in_executor( + None, + lambda: requests.get(url, headers=headers, params=params, impersonate="chrome", timeout=10) + ) if response.status_code == 200:config.py (2)
42-45: IncompleteDealerIdenum compared toreservation_bot.py.
reservation_bot.pydefinesISTANBUL,ANKARA, andIZMIR, but this file only hasISTANBUL. Consider consolidating enum definitions in one place.🔎 Proposed fix
class DealerId(Enum): """Known dealer/location IDs""" ISTANBUL = 1 - # Add more as discovered + ANKARA = 2 + IZMIR = 3 + # Add more as discovered
138-161: Test code logs potentially sensitive applicant names.The
__main__block prints applicant first and last names. While useful for debugging, this could inadvertently expose PII in logs if run with real data.Consider masking or truncating sensitive fields in debug output:
print("\nApplicant Config:") - print(f" Name: {config['applicant'].first_name} {config['applicant'].last_name}") + print(f" Name configured: {bool(config['applicant'].first_name and config['applicant'].last_name)}") print(f" Valid: {config['applicant'].is_valid()}")reservation_bot.py (2)
196-210: Unused variableresponse.The
responsevariable is assigned but never used. Either use it or remove the assignment.🔎 Proposed fix
try: # This endpoint is theoretical - response = self.session.get( + self.session.get( f"{self.BOOKING_PORTAL}/", impersonate="chrome" )
84-97: Mutable class attributeDEFAULT_HEADERSshould useClassVar.As noted by static analysis, mutable class attributes should be annotated with
typing.ClassVarto indicate they are shared across instances.🔎 Proposed fix
+from typing import ClassVar + class KosmosReservationBot: # ... # Common headers to mimic browser - DEFAULT_HEADERS = { + DEFAULT_HEADERS: ClassVar[dict] = { 'accept': 'application/json', # ... }hybrid_bot.py (3)
706-730: Multiple bareexceptclauses silently swallow all errors.The selector iteration loops use bare
except: passwhich hides failures including programming errors. At minimum, log these failures at debug level.🔎 Proposed fix for one instance (apply pattern to others)
# Try to select dealer for selector in selectors['dealer']: try: if await self.page.locator(selector).count() > 0: await self.page.select_option(selector, str(self.config.dealer_id)) break - except: - pass + except Exception as e: + self.logger.debug(f"Selector {selector} failed: {e}")
491-498: f-strings without placeholders are unnecessary.Lines 493-496 use f-strings but contain no interpolated values. Remove the
fprefix.🔎 Proposed fix
def notify_captcha_required(self): """Notify that CAPTCHA needs manual solving""" message = ( - f"⚠️ <b>CAPTCHA DETECTED!</b>\n\n" - f"Please solve the CAPTCHA manually.\n" - f"The browser window should be visible.\n\n" - f"⏱ Waiting up to 2 minutes..." + "⚠️ <b>CAPTCHA DETECTED!</b>\n\n" + "Please solve the CAPTCHA manually.\n" + "The browser window should be visible.\n\n" + "⏱ Waiting up to 2 minutes..." ) self.send(message)
240-240: UseOptional[str]instead of implicitNonedefault.PEP 484 prohibits implicit
Optional. Theerrorparameter should be explicitly typed.🔎 Proposed fix
- def log_booking_attempt(self, slot_key: str, status: str, error: str = None): + def log_booking_attempt(self, slot_key: str, status: str, error: Optional[str] = None):Dockerfile (2)
1-49: Container runs as root user - security concern.Running containers as root increases the attack surface. Consider adding a non-root user.
🔎 Proposed fix
# Create directories for data persistence RUN mkdir -p /app/screenshots /app/data +# Create non-root user +RUN useradd --create-home --shell /bin/bash botuser \ + && chown -R botuser:botuser /app + +USER botuser + # Environment variables ENV PYTHONUNBUFFERED=1 # Default command CMD ["python", "hybrid_bot.py"]
38-40: Glob pattern*.pymay copy unintended files.Using
COPY *.py .copies all Python files from the build context. If there are test files or other scripts not needed in production, they'll be included. Consider being explicit or using.dockerignore.Either list files explicitly:
-COPY *.py . +COPY browser_bot.py config.py hybrid_bot.py reservation_bot.py .Or create a
.dockerignorefile to exclude test files and other non-production code.docker-compose.yml (1)
1-1: Remove theversionkey from docker-compose.yml.Docker Compose V2 ignores the
versionkey and may produce a warning when present. The Compose Specification does not require this key, so it can safely be removed.🔎 Proposed fix
-version: '3.8' - services: kosmos-bot:
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
.env.exampleDockerfilebrowser_bot.pyconfig.pydocker-compose.ymlhybrid_bot.pyreadme.mdrequirements.txtreservation_bot.py
🧰 Additional context used
🧬 Code graph analysis (1)
browser_bot.py (2)
config.py (2)
ApplicationType(17-26)AppointmentTypeId(29-39)reservation_bot.py (6)
ApplicationType(32-34)AppointmentTypeId(37-40)ApplicantInfo(51-61)send_notification(330-346)run(348-428)main(458-489)
🪛 dotenv-linter (4.0.0)
.env.example
[warning] 22-22: [UnorderedKey] The APPLICANT_BIRTH_DATE key should go before the APPLICANT_FIRST_NAME key
(UnorderedKey)
[warning] 23-23: [UnorderedKey] The APPLICANT_EMAIL key should go before the APPLICANT_FIRST_NAME key
(UnorderedKey)
🪛 LanguageTool
readme.md
[style] ~85-~85: This phrase is redundant (‘I’ stands for ‘interface’). Use simply “CLI”.
Context: ...tate persistence | ✅ SQLite | ❌ | ❌ | | CLI interface | ✅ | ❌ | ❌ | | Auto-booking | ✅ | ❌ | ...
(ACRONYM_TAUTOLOGY)
🪛 markdownlint-cli2 (0.18.1)
readme.md
20-20: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
92-92: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
222-222: Emphasis used instead of a heading
(MD036, no-emphasis-as-heading)
🪛 Ruff (0.14.10)
reservation_bot.py
84-97: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
160-160: Consider moving this statement to an else block
(TRY300)
162-162: Do not catch blind exception: Exception
(BLE001)
163-163: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
198-198: Local variable response is assigned to but never used
Remove assignment to unused variable response
(F841)
210-210: Consider moving this statement to an else block
(TRY300)
212-212: Do not catch blind exception: Exception
(BLE001)
213-213: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
326-326: Do not catch blind exception: Exception
(BLE001)
327-327: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
345-345: Do not catch blind exception: Exception
(BLE001)
346-346: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
426-426: Do not catch blind exception: Exception
(BLE001)
427-427: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
hybrid_bot.py
240-240: PEP 484 prohibits implicit Optional
Convert to T | None
(RUF013)
300-312: Mutable class attributes should be annotated with typing.ClassVar
(RUF012)
368-368: Consider moving this statement to an else block
(TRY300)
370-370: Do not catch blind exception: Exception
(BLE001)
371-371: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
466-466: Do not catch blind exception: Exception
(BLE001)
467-467: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
493-493: f-string without any placeholders
Remove extraneous f prefix
(F541)
494-494: f-string without any placeholders
Remove extraneous f prefix
(F541)
495-495: f-string without any placeholders
Remove extraneous f prefix
(F541)
496-496: f-string without any placeholders
Remove extraneous f prefix
(F541)
565-568: Avoid specifying long messages outside the exception class
(TRY003)
660-660: Abstract raise to an inner function
(TRY301)
660-660: Create your own exception
(TRY002)
660-660: Avoid specifying long messages outside the exception class
(TRY003)
673-673: Abstract raise to an inner function
(TRY301)
673-673: Create your own exception
(TRY002)
673-673: Avoid specifying long messages outside the exception class
(TRY003)
675-675: Do not catch blind exception: Exception
(BLE001)
677-677: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
711-711: Do not use bare except
(E722)
711-712: try-except-pass detected, consider logging the exception
(S110)
720-720: Do not use bare except
(E722)
720-721: try-except-pass detected, consider logging the exception
(S110)
729-729: Do not use bare except
(E722)
729-730: try-except-pass detected, consider logging the exception
(S110)
766-766: Do not use bare except
(E722)
766-767: try-except-pass detected, consider logging the exception
(S110)
816-816: Consider moving this statement to an else block
(TRY300)
817-817: Do not use bare except
(E722)
818-818: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
844-844: Do not use bare except
(E722)
844-845: try-except-pass detected, consider logging the exception
(S110)
872-872: Do not use bare except
(E722)
872-873: try-except-pass detected, consider logging the exception
(S110)
882-882: String contains ambiguous ı (LATIN SMALL LETTER DOTLESS I). Did you mean i (LATIN SMALL LETTER I)?
(RUF001)
882-882: String contains ambiguous ı (LATIN SMALL LETTER DOTLESS I). Did you mean i (LATIN SMALL LETTER I)?
(RUF001)
940-940: Unused function argument: sig
(ARG001)
940-940: Unused function argument: frame
(ARG001)
980-980: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1026-1026: Do not catch blind exception: Exception
(BLE001)
1027-1027: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
browser_bot.py
101-101: Avoid specifying long messages outside the exception class
(TRY003)
186-186: Do not catch blind exception: Exception
(BLE001)
187-187: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
255-255: Do not catch blind exception: Exception
(BLE001)
256-256: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
287-287: Do not catch blind exception: Exception
(BLE001)
288-288: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
324-324: Do not catch blind exception: Exception
(BLE001)
363-363: Consider moving this statement to an else block
(TRY300)
364-364: Do not use bare except
(E722)
365-365: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
408-408: String contains ambiguous ı (LATIN SMALL LETTER DOTLESS I). Did you mean i (LATIN SMALL LETTER I)?
(RUF001)
408-408: String contains ambiguous ı (LATIN SMALL LETTER DOTLESS I). Did you mean i (LATIN SMALL LETTER I)?
(RUF001)
438-438: Consider moving this statement to an else block
(TRY300)
440-440: Do not catch blind exception: Exception
(BLE001)
441-441: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
481-481: Consider moving this statement to an else block
(TRY300)
483-483: Do not catch blind exception: Exception
(BLE001)
484-484: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
501-501: Do not catch blind exception: Exception
(BLE001)
502-502: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
🔇 Additional comments (2)
.env.example (1)
1-74: Configuration template is comprehensive and well-documented.Clear sections with helpful comments. Example values are realistic (e.g., appointment types, time formats) and defaults are safe (AUTO_BOOK=false).
Recommend verifying that all variables in this template are actually referenced by the configuration module (src/config.py) and that none are missing. If the config module accepts additional env vars beyond what's listed here, consider documenting them.
readme.md (1)
1-236: Documentation is comprehensive, well-organized, and accurate.The README clearly explains the bot architecture, configuration, deployment, and includes practical examples. Feature descriptions align with the PR objectives (monitoring, Telegram alerts, auto-booking via browser automation, state persistence, Docker support).
Verify that references to bot modules (
hybrid_bot.py,browser_bot.py,reservation_bot.py) exist and that the architecture diagram accurately reflects the implementation before merging.
| class ApplicationType(Enum): | ||
| INDIVIDUAL = 1 | ||
| FAMILY = 2 | ||
|
|
||
|
|
||
| class AppointmentTypeId(Enum): | ||
| STANDARD = 16 | ||
| VIP = 18 | ||
| EEA_AB_SPOUSE = 2339 |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Duplicate enum definitions - import from config.py instead.
ApplicationType and AppointmentTypeId are already defined in config.py with additional helper methods (from_value). This duplication will cause maintenance issues and potential inconsistencies.
🔎 Proposed fix
-class ApplicationType(Enum):
- INDIVIDUAL = 1
- FAMILY = 2
-
-
-class AppointmentTypeId(Enum):
- STANDARD = 16
- VIP = 18
- EEA_AB_SPOUSE = 2339
+from config import ApplicationType, AppointmentTypeIdCommittable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In browser_bot.py around lines 39 to 47, the enums ApplicationType and
AppointmentTypeId are duplicated here but already defined in config.py
(including helper methods like from_value); remove these local enum definitions,
import ApplicationType and AppointmentTypeId from config.py at the top of the
file, update any local references to use the imported names (and use the
from_value helper where appropriate), and clean up any now-unused imports (e.g.,
Enum) to avoid duplication and inconsistencies.
| async def setup_browser(self): | ||
| """Initialize the browser with anti-detection settings""" | ||
| if not PLAYWRIGHT_AVAILABLE: | ||
| raise RuntimeError("Playwright is not installed") | ||
|
|
||
| logger.info("Setting up browser...") | ||
|
|
||
| playwright = await async_playwright().start() | ||
|
|
||
| # Launch browser with stealth settings | ||
| self.browser = await playwright.chromium.launch( | ||
| headless=self.config.headless, | ||
| args=[ | ||
| '--disable-blink-features=AutomationControlled', | ||
| '--no-sandbox', | ||
| '--disable-setuid-sandbox', | ||
| '--disable-dev-shm-usage', | ||
| ] | ||
| ) | ||
|
|
||
| # Create context with realistic viewport and user agent | ||
| self.context = await self.browser.new_context( | ||
| viewport={'width': 1920, 'height': 1080}, | ||
| user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', | ||
| locale='tr-TR', | ||
| timezone_id='Europe/Istanbul', | ||
| ) | ||
|
|
||
| # Add stealth scripts to avoid detection | ||
| await self.context.add_init_script(""" | ||
| // Override navigator.webdriver | ||
| Object.defineProperty(navigator, 'webdriver', { | ||
| get: () => undefined | ||
| }); | ||
|
|
||
| // Override plugins | ||
| Object.defineProperty(navigator, 'plugins', { | ||
| get: () => [1, 2, 3, 4, 5] | ||
| }); | ||
|
|
||
| // Override languages | ||
| Object.defineProperty(navigator, 'languages', { | ||
| get: () => ['tr-TR', 'tr', 'en-US', 'en'] | ||
| }); | ||
| """) | ||
|
|
||
| self.page = await self.context.new_page() | ||
|
|
||
| # Set default timeout | ||
| self.page.set_default_timeout(30000) | ||
|
|
||
| logger.info("Browser setup complete") |
There was a problem hiding this comment.
Playwright instance resource leak.
The playwright instance created at line 105 is never stored or properly closed. When close_browser() is called, it only closes the browser but not the Playwright instance, leading to resource leaks.
🔎 Proposed fix
def __init__(self, applicant: ApplicantInfo, config: BookingConfig):
self.applicant = applicant
self.config = config
self.browser: Optional[Browser] = None
self.context: Optional[BrowserContext] = None
self.page: Optional[Page] = None
self.notified_slots: set = set()
+ self._playwright = None
async def setup_browser(self):
"""Initialize the browser with anti-detection settings"""
if not PLAYWRIGHT_AVAILABLE:
raise RuntimeError("Playwright is not installed")
logger.info("Setting up browser...")
- playwright = await async_playwright().start()
+ self._playwright = await async_playwright().start()
# Launch browser with stealth settings
- self.browser = await playwright.chromium.launch(
+ self.browser = await self._playwright.chromium.launch( async def close_browser(self):
"""Clean up browser resources"""
if self.browser:
await self.browser.close()
+ if self._playwright:
+ await self._playwright.stop()
logger.info("Browser closed")Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 Ruff (0.14.10)
101-101: Avoid specifying long messages outside the exception class
(TRY003)
🤖 Prompt for AI Agents
In browser_bot.py around lines 98 to 149, the Playwright instance created with
async_playwright().start() is not stored or stopped, causing a resource leak;
update setup_browser to assign the returned object to an instance attribute
(e.g. self.playwright) and ensure any cleanup path knows about it, then modify
close_browser to await self.playwright.stop() (after or before closing the
browser/context) and guard with checks so stop is only called if the instance
exists; also ensure exceptions during setup properly close any partially-created
resources by using try/except/finally or async context patterns to stop the
Playwright instance on failure.
| async def navigate_to_booking(self): | ||
| """Navigate to the booking portal""" | ||
| logger.info(f"Navigating to {self.BOOKING_URL}") | ||
|
|
||
| await self.page.goto(self.BOOKING_URL, wait_until='networkidle') | ||
|
|
||
| # Wait for page to fully load | ||
| await self.page.wait_for_load_state('domcontentloaded') | ||
|
|
||
| # Take screenshot for debugging | ||
| await self.page.screenshot(path='screenshots/booking_page.png') | ||
|
|
There was a problem hiding this comment.
Screenshot path may fail if directory doesn't exist.
navigate_to_booking writes to screenshots/ but the directory is only created in run() at line 517. If navigate_to_booking is called directly (e.g., from attempt_booking before run()), this will fail.
🔎 Proposed fix - create directory in `setup_browser`
async def setup_browser(self):
"""Initialize the browser with anti-detection settings"""
if not PLAYWRIGHT_AVAILABLE:
raise RuntimeError("Playwright is not installed")
logger.info("Setting up browser...")
+
+ # Ensure screenshots directory exists
+ os.makedirs('screenshots', exist_ok=True)
playwright = await async_playwright().start()Committable suggestion skipped: line range outside the PR's diff.
| logger.info("CAPTCHA solved!") | ||
| return True | ||
| except: | ||
| logger.error("CAPTCHA solving timeout") | ||
| return False |
There was a problem hiding this comment.
Bare except swallows all exceptions including KeyboardInterrupt and SystemExit.
Use a specific exception type to avoid masking critical errors.
🔎 Proposed fix
try:
await self.page.wait_for_selector(
'.recaptcha-success, [data-captcha-solved="true"]',
timeout=120000 # 2 minutes
)
logger.info("CAPTCHA solved!")
return True
- except:
+ except Exception:
logger.error("CAPTCHA solving timeout")
return False📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| logger.info("CAPTCHA solved!") | |
| return True | |
| except: | |
| logger.error("CAPTCHA solving timeout") | |
| return False | |
| logger.info("CAPTCHA solved!") | |
| return True | |
| except Exception: | |
| logger.error("CAPTCHA solving timeout") | |
| return False |
🧰 Tools
🪛 Ruff (0.14.10)
363-363: Consider moving this statement to an else block
(TRY300)
364-364: Do not use bare except
(E722)
365-365: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
🤖 Prompt for AI Agents
In browser_bot.py around lines 362-366, the bare except is swallowing all
exceptions; change it to catch a specific timeout-related exception (e.g.,
TimeoutException from the library you use) or at minimum catch Exception as e,
log the exception details (logger.exception or logger.error with e) and re-raise
critical exceptions if needed (ensure KeyboardInterrupt/SystemExit are not
caught). Update imports for the chosen exception and replace the bare except
block with the specific/Exception handler that logs the error and returns False.
| dealer_id: int = field(default_factory=lambda: int(os.getenv('DEALER_ID', '1'))) | ||
| application_type: ApplicationType = field( | ||
| default_factory=lambda: ApplicationType.from_value(int(os.getenv('APPLICATION_TYPE', '1'))) | ||
| ) | ||
| appointment_type: AppointmentTypeId = field( | ||
| default_factory=lambda: AppointmentTypeId.from_value(int(os.getenv('APPOINTMENT_TYPE', '16'))) | ||
| ) |
There was a problem hiding this comment.
Missing error handling for integer parsing from environment variables.
If DEALER_ID, APPLICATION_TYPE, APPOINTMENT_TYPE, CHECK_INTERVAL, or DAYS_TO_CHECK contain non-integer values, a ValueError will crash the application during config loading.
🔎 Proposed fix - add safe parsing with defaults
+def _safe_int(value: str, default: int) -> int:
+ """Safely parse integer from string, returning default on failure"""
+ try:
+ return int(value)
+ except (ValueError, TypeError):
+ return default
+
+
@dataclass
class BotConfig:
"""Main bot configuration"""
# Appointment settings
- dealer_id: int = field(default_factory=lambda: int(os.getenv('DEALER_ID', '1')))
+ dealer_id: int = field(default_factory=lambda: _safe_int(os.getenv('DEALER_ID', '1'), 1))
application_type: ApplicationType = field(
- default_factory=lambda: ApplicationType.from_value(int(os.getenv('APPLICATION_TYPE', '1')))
+ default_factory=lambda: ApplicationType.from_value(_safe_int(os.getenv('APPLICATION_TYPE', '1'), 1))
)
appointment_type: AppointmentTypeId = field(
- default_factory=lambda: AppointmentTypeId.from_value(int(os.getenv('APPOINTMENT_TYPE', '16')))
+ default_factory=lambda: AppointmentTypeId.from_value(_safe_int(os.getenv('APPOINTMENT_TYPE', '16'), 16))
)🤖 Prompt for AI Agents
In config.py around lines 85 to 91, environment integer parsing (DEALER_ID,
APPLICATION_TYPE, APPOINTMENT_TYPE and also CHECK_INTERVAL, DAYS_TO_CHECK
elsewhere) is done directly with int(...) which will raise ValueError for
non-integer env values; change this to use a small safe parsing routine or
inline try/except that attempts to int(os.getenv(...)) and falls back to the
intended default on failure, then pass the validated integer into
ApplicationType.from_value and AppointmentTypeId.from_value (and into field
defaults for CHECK_INTERVAL/DAYS_TO_CHECK) so invalid env content won't crash
config loading.
| try: | ||
| # Run in executor to avoid blocking | ||
| loop = asyncio.get_event_loop() | ||
| response = await loop.run_in_executor( | ||
| None, | ||
| lambda: curl_requests.get( | ||
| self.API_URL, | ||
| headers=self.HEADERS, | ||
| params=params, | ||
| impersonate="chrome", | ||
| timeout=10 | ||
| ) | ||
| ) |
There was a problem hiding this comment.
Deprecated asyncio.get_event_loop() usage.
asyncio.get_event_loop() is deprecated in Python 3.10+ when called from a coroutine. Use asyncio.get_running_loop() instead.
🔎 Proposed fix
try:
# Run in executor to avoid blocking
- loop = asyncio.get_event_loop()
+ loop = asyncio.get_running_loop()
response = await loop.run_in_executor(📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try: | |
| # Run in executor to avoid blocking | |
| loop = asyncio.get_event_loop() | |
| response = await loop.run_in_executor( | |
| None, | |
| lambda: curl_requests.get( | |
| self.API_URL, | |
| headers=self.HEADERS, | |
| params=params, | |
| impersonate="chrome", | |
| timeout=10 | |
| ) | |
| ) | |
| try: | |
| # Run in executor to avoid blocking | |
| loop = asyncio.get_running_loop() | |
| response = await loop.run_in_executor( | |
| None, | |
| lambda: curl_requests.get( | |
| self.API_URL, | |
| headers=self.HEADERS, | |
| params=params, | |
| impersonate="chrome", | |
| timeout=10 | |
| ) | |
| ) |
🤖 Prompt for AI Agents
In hybrid_bot.py around lines 330 to 342, the coroutine is calling the
deprecated asyncio.get_event_loop(); replace it with asyncio.get_running_loop()
to obtain the current loop from within a coroutine, e.g., call loop =
asyncio.get_running_loop() before run_in_executor, and keep the rest of the
run_in_executor call unchanged so execution remains off the event loop; no
additional imports are required.
| def _filter_by_time(self, slots: List[Slot]) -> List[Slot]: | ||
| """Filter slots by preferred time ranges""" | ||
| filtered = [] | ||
| for slot in slots: | ||
| for time_range in self.config.preferred_times: | ||
| if '-' in time_range: | ||
| start, end = time_range.split('-') | ||
| if start.strip() <= slot.time <= end.strip(): | ||
| filtered.append(slot) | ||
| break | ||
| return filtered |
There was a problem hiding this comment.
Same string-based time comparison issue as in reservation_bot.py.
The time filtering logic uses string comparison which may fail for non-zero-padded times.
Apply the same fix as suggested for reservation_bot.py - parse times properly before comparison.
🤖 Prompt for AI Agents
In hybrid_bot.py around lines 417 to 427, the time-range filter currently
compares time strings which fails for non-zero-padded times; update it to parse
both the slot.time and each range endpoint into a comparable time value (e.g.,
datetime.time via datetime.strptime or integer minutes since midnight) before
comparing, trim whitespace, handle ranges that may cross midnight by treating
end < start as spanning midnight (compare accordingly), and only append the slot
when the parsed slot time falls within the parsed range.
| curl_cffi>=0.5.0 | ||
| python-dotenv>=1.0.0 | ||
| playwright>=1.40.0 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
git ls-files | head -20Repository: ismkdc/kosmos-vize-bot
Length of output: 200
🏁 Script executed:
# Find all Python files to understand project structure
fd -e py | head -20Repository: ismkdc/kosmos-vize-bot
Length of output: 130
🏁 Script executed:
# Search for imports of the three dependencies to understand usage
rg "import (curl_cffi|python-dotenv|playwright)" -t pyRepository: ismkdc/kosmos-vize-bot
Length of output: 48
🏁 Script executed:
# Also check for 'from' imports
rg "from (curl_cffi|python-dotenv|playwright)" -t pyRepository: ismkdc/kosmos-vize-bot
Length of output: 460
🏁 Script executed:
# Check if there's any version pinning documentation or constraints elsewhere
fd -e txt -e md -e toml -e cfg | xargs rg "playwright|curl_cffi|python-dotenv" 2>/dev/nullRepository: ismkdc/kosmos-vize-bot
Length of output: 240
🌐 Web query:
Playwright 1.40.0 to latest version breaking changes browser automation
💡 Result:
Here are the main breaking changes in Playwright from v1.40.0 onward that can affect browser automation (summary — check the linked release notes for details and exact versions):
- Playwright no longer automatically downloads browser binaries for the standalone packages; use npx playwright install (affects automation scripts that relied on auto-download). (playwright.dev)
- playwright-core binary/name changes (playwright-core binary renamed / CLI behavior changed). Update any scripts that call the old CLI. (playwright.dev)
- route/page.route-related changes: glob URL patterns no longer support ? and []; route.continue() can no longer override the Cookie header (cookies come from the browser store). Adjust route matching and request-modification code. (playwright.dev)
- Events and APIs removed/deprecated impacting automation: browserContext.on('backgroundpage') deprecated; some event behaviors changed (use backgroundPages() etc.). (playwright.dev)
- Input/typing and locator API changes: page.type()/frame.type()/locator.type() were deprecated in favor of locator.fill() / locator.pressSequentially(); some expect/locator assertions tightened (e.g., toBeEditable now throws for non-editable elements). Review tests/scripts using deprecated input APIs and assertions. (playwright.dev)
For a complete, version-by-version list (and browser engine versions), see the Playwright release notes. (playwright.dev)
If you want, I can: (a) produce a patch list of code changes for your repository (need a sample of your tests/scripts), or (b) extract the exact breaking items per intermediate release between 1.40.0 and the current latest. Which would you prefer?
Citations:
- 1: https://playwright.dev/docs/release-notes?utm_source=openai
- 2: https://playwright.dev/docs/release-notes?utm_source=openai
- 3: https://playwright.dev/docs/release-notes?utm_source=openai
- 4: https://playwright.dev/docs/release-notes?utm_source=openai
- 5: https://playwright.dev/docs/release-notes?utm_source=openai
- 6: https://playwright.dev/docs/release-notes?utm_source=openai
🌐 Web query:
curl_cffi version compatibility and breaking changes
💡 Result:
Summary of curl_cffi compatibility and notable breaking changes
-
Python support:
- Minimum Python raised over time (was 3.8 in v0.6; minimum is Python 3.9 since v0.10). [1][4]
-
libcurl / protocol support:
- Bundled libcurl (curl-impersonate) has been upgraded multiple times (e.g., to 8.1.1 in v0.6 and 8.7.1 in v0.7) and HTTP/3 was added in v0.11 — changes in bundled libcurl can affect TLS/HTTP behaviors and platform binary requirements. [2][4]
-
API / behavioral changes you may hit when upgrading:
- AsyncSession.close became an async coroutine (v0.6.1) — code calling it synchronously must be updated. [4]
- Default redirect limit changed (v0.7) — may alter redirect behavior. [3]
- Header strategy changed: empty headers excluded and Expect header no longer sent by default (v0.7). [3]
- Some Windows fingerprint fixes and platform build-target changes across versions (v0.6 → v0.9). [4][2]
-
Fingerprints / impersonation:
- Browser impersonation targets are added/changed frequently; some recent targets (and some commercial-only targets) may be present or omitted between releases — if you rely on a specific fingerprint name, verify it exists in your curl_cffi version or provide custom JA3/headers. [1][2]
-
Packaging / wheels:
- Manylinux/macOS/Win wheel targets expanded; prebuilt wheels include bundled libcurl-impersonate versions — upgrading may change system/GLIBC requirements for the wheel. Check the wheel tags on PyPI for platform/glibc requirements before upgrading. [1]
Where to check details before upgrading
- Official changelog (ReadTheDocs) for version-by-version notes. [3]
- PyPI release history / wheel metadata for platform compatibility. [1]
- GitHub Releases for finer breaking-change descriptions and migration notes. [2]
Sources: PyPI release page and project docs/changelog. [1][2][3][4]
🌐 Web query:
python-dotenv version 1.0.0 stability and API changes
💡 Result:
Summary: python-dotenv 1.0.0 (released Feb 24, 2023) is a stable 1.0 "major" release; it did not introduce large API removals but made Python-compatibility changes and small bug fixes.
Key points
- Release/date: 1.0.0 — 2023-02-24. [1]
- Stability: marked as a production/stable 1.0 release (semantic-versioning); subsequent patch releases (1.0.1 etc.) fixed edge cases. [1][2]
- Notable changes in 1.0.0:
- Dropped support for Python 3.7 and added support for Python 3.12-dev (compatibility change, not an API removal). [1][2]
- Fixed handling for cases where the current working directory does not exist. [1][2]
- API surface: no documented breaking API removals in the 1.0.0 changelog — typical usage (load_dotenv, dotenv_values, set_key/get_key/unset_key, CLI) continued to work; most later changes have been fixes, new Python-version support, or CLI/behavior improvements in minor/patch releases. [1][2]
If you want, I can:
- list any small behavioral changes in functions (load_dotenv, dotenv_values, set_key, etc.) across 0.21.x → 1.0.0 and 1.0.0 → 1.0.1, with exact changelog lines and links.
Sources
[1] python-dotenv changelog / PyPI release notes (1.0.0 entry).
[2] Project changelog mirror (detailed entries for 1.0.0 and later).
Tighten dependency version constraints to prevent breaking changes in automated deployments.
Breaking changes confirmed in Playwright (1.40.0+) and curl_cffi (0.5.0+) could affect bot stability. Playwright has deprecated input APIs (page.type() → locator.fill()), removed browser auto-download, and changed route/assertion behaviors. curl_cffi has altered AsyncSession.close() to async-only (v0.6.1+), changed redirect handling (v0.7+), and shifted header strategies.
Use upper-bound constraints to allow patch updates while preventing breaking minor/major releases:
Recommended version constraints
curl_cffi>=0.5.0,<0.10.0
python-dotenv>=1.0.0,<2.0.0
playwright>=1.40.0,<2.0.0
(Adjust upper bounds based on your tested compatibility; python-dotenv is stable, but curl_cffi and playwright warrant tighter control.)
🤖 Prompt for AI Agents
In requirements.txt around lines 1 to 3, the current unconstrained
lower-bound-only pins allow breaking minor/major releases for curl_cffi and
playwright; update each dependency to include an appropriate upper-bound to
prevent unexpected breaking changes (for example constrain curl_cffi and
playwright to a safe upper bound such as <0.10.0 and <2.0.0 respectively, and
pin python-dotenv to <2.0.0 or another tested upper bound). Edit the three lines
to use range constraints (>=... , <...) per the recommended bounds or your
tested compatibility, so patch updates remain allowed but minor/major breaking
releases are excluded.
| class ApplicationType(Enum): | ||
| INDIVIDUAL = 1 # Bireysel | ||
| FAMILY = 2 # Aile | ||
|
|
||
|
|
||
| class AppointmentTypeId(Enum): | ||
| STANDARD = 16 # Standart | ||
| VIP = 18 # Vip | ||
| EEA_AB_SPOUSE = 2339 # EEA AB Eşi | ||
|
|
||
|
|
||
| class DealerId(Enum): | ||
| """Dealer/Location IDs - These are theoretical values""" | ||
| ISTANBUL = 1 | ||
| ANKARA = 2 | ||
| IZMIR = 3 | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Duplicate enum definitions across multiple files.
ApplicationType, AppointmentTypeId, and DealerId are defined here, in config.py, and in browser_bot.py. This creates maintenance burden and risk of inconsistency.
Consolidate all enum definitions in config.py and import them in other modules:
-class ApplicationType(Enum):
- INDIVIDUAL = 1 # Bireysel
- FAMILY = 2 # Aile
-
-
-class AppointmentTypeId(Enum):
- STANDARD = 16 # Standart
- VIP = 18 # Vip
- EEA_AB_SPOUSE = 2339 # EEA AB Eşi
-
-
-class DealerId(Enum):
- """Dealer/Location IDs - These are theoretical values"""
- ISTANBUL = 1
- ANKARA = 2
- IZMIR = 3
+from config import ApplicationType, AppointmentTypeId, DealerId📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| class ApplicationType(Enum): | |
| INDIVIDUAL = 1 # Bireysel | |
| FAMILY = 2 # Aile | |
| class AppointmentTypeId(Enum): | |
| STANDARD = 16 # Standart | |
| VIP = 18 # Vip | |
| EEA_AB_SPOUSE = 2339 # EEA AB Eşi | |
| class DealerId(Enum): | |
| """Dealer/Location IDs - These are theoretical values""" | |
| ISTANBUL = 1 | |
| ANKARA = 2 | |
| IZMIR = 3 | |
| from config import ApplicationType, AppointmentTypeId, DealerId |
🤖 Prompt for AI Agents
In reservation_bot.py around lines 32 to 48 you define ApplicationType,
AppointmentTypeId, and DealerId which are duplicated in config.py and
browser_bot.py; remove these local enum definitions and instead import the enums
from config.py (e.g., from config import ApplicationType, AppointmentTypeId,
DealerId), update any local references to use the imported names, delete the
duplicate enums from reservation_bot.py, and then remove or replace the
duplicate definitions in other modules so all three enums live only in
config.py; run tests / linter to ensure no unresolved imports remain.
| def time_in_range(slot_time: str, ranges: List[str]) -> bool: | ||
| for time_range in ranges: | ||
| if '-' in time_range: | ||
| start, end = time_range.split('-') | ||
| if start <= slot_time <= end: | ||
| return True | ||
| return False | ||
|
|
||
| filtered = [s for s in filtered if time_in_range(s.time, preferred_times)] |
There was a problem hiding this comment.
String-based time comparison may produce incorrect results.
Comparing times as strings (e.g., start <= slot_time <= end) only works correctly if times are zero-padded (e.g., "09:00" vs "9:00"). Times like "9:00" would incorrectly compare greater than "10:00".
🔎 Proposed fix - parse times properly
def time_in_range(slot_time: str, ranges: List[str]) -> bool:
+ from datetime import datetime
+ def parse_time(t: str) -> datetime:
+ return datetime.strptime(t.strip(), "%H:%M")
+
for time_range in ranges:
if '-' in time_range:
start, end = time_range.split('-')
- if start <= slot_time <= end:
+ try:
+ slot_t = parse_time(slot_time)
+ if parse_time(start) <= slot_t <= parse_time(end):
+ return True
+ except ValueError:
+ continue
- return True
return False🤖 Prompt for AI Agents
In reservation_bot.py around lines 445 to 453, the helper time_in_range
currently compares times as strings which fails for non-zero-padded times;
change it to parse times into a comparable numeric form (e.g., use
datetime.strptime to parse "H:M" or "HH:MM" and compare datetime.time objects,
or convert hours and minutes into total minutes) then perform the comparisons;
ensure you handle invalid formats with a safe fallback and trim whitespace when
splitting ranges; finally use the parsed comparison in the filtered list
comprehension.
Summary by CodeRabbit
New Features
Configuration
Documentation
Chores
✏️ Tip: You can customize this high-level summary in your review settings.