105 lines
3.2 KiB
Python
105 lines
3.2 KiB
Python
"""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}
|