Files
crawle-snapp/crawler/flow_runner.py
T
2026-06-15 19:42:03 +03:30

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}