"""Executes a pre-defined sequence of flow steps and records results.""" from __future__ import annotations import time from dataclasses import dataclass, field from typing import Callable import undetected_chromedriver as uc from config import config from crawler.driver import human_delay # A flow step is a callable that receives the driver and returns arbitrary data. StepFn = Callable[[uc.Chrome], dict[str, object]] # Registry — add your custom step functions here. # Keys must match the step names in config.flow_steps. STEP_REGISTRY: dict[str, StepFn] = {} def register_step(name: str) -> Callable[[StepFn], StepFn]: """Decorator to register a function as a named flow step.""" def decorator(fn: StepFn) -> StepFn: STEP_REGISTRY[name] = fn return fn return decorator @dataclass class StepResult: name: str success: bool data: dict[str, object] = field(default_factory=dict) error: str | None = None duration_ms: int = 0 @dataclass class FlowResult: scenario: str identifier: str # phone number or "fresh" steps: list[StepResult] = field(default_factory=list) @property def success(self) -> bool: return all(s.success for s in self.steps) @property def failure_reason(self) -> str | None: for s in self.steps: if not s.success: return f"{s.name}: {s.error}" return None class FlowRunner: def __init__(self, driver: uc.Chrome) -> None: self.driver = driver def run(self, scenario: str, identifier: str) -> FlowResult: result = FlowResult(scenario=scenario, identifier=identifier) for step_name in config.flow_steps: fn = STEP_REGISTRY.get(step_name) if fn is None: result.steps.append( StepResult( name=step_name, success=False, error=f"Step '{step_name}' not found in registry", ) ) break t0 = time.monotonic() try: data = fn(self.driver) duration = int((time.monotonic() - t0) * 1000) result.steps.append(StepResult(name=step_name, success=True, data=data, duration_ms=duration)) human_delay(0.5, 1.5) except Exception as exc: duration = int((time.monotonic() - t0) * 1000) result.steps.append( StepResult(name=step_name, success=False, error=str(exc), duration_ms=duration) ) break # abort remaining steps on first failure return result # --------------------------------------------------------------------------- # Example steps — replace with your actual flow logic # --------------------------------------------------------------------------- @register_step("step_home") def step_home(driver: uc.Chrome) -> dict[str, object]: return {"url": driver.current_url, "title": driver.title} @register_step("step_browse") def step_browse(driver: uc.Chrome) -> dict[str, object]: # Example: navigate to a listing page human_delay(1.0, 2.0) return {"url": driver.current_url}