import logging from contextlib import asynccontextmanager from urllib.parse import urlparse from fastapi import Depends, FastAPI, Header, HTTPException, status from pydantic import BaseModel, Field from rich.console import Console from rich.logging import RichHandler import providers from actions import Recorder from classes.application_result import ApplicationResult from classes.profile import Profile from language import _ from providers._provider import ApplyContext from settings import INTERNAL_API_KEY def setup_logging(): logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)-5s %(name)s: %(message)s", datefmt="%H:%M:%S", handlers=[RichHandler(markup=False, console=Console(width=140), show_time=False, show_path=False)], ) logging.getLogger("flat-apply").setLevel(logging.DEBUG) logging.getLogger("playwright").setLevel(logging.INFO) logger = logging.getLogger("flat-apply") setup_logging() class ProfileModel(BaseModel): salutation: str = "Herr" firstname: str = "" lastname: str = "" email: str = "" telephone: str = "" street: str = "" house_number: str = "" postcode: str = "" city: str = "" is_possessing_wbs: bool = False wbs_type: str = "0" wbs_valid_till: str = "1970-01-01" wbs_rooms: int = 0 wbs_adults: int = 0 wbs_children: int = 0 is_prio_wbs: bool = False immomio_email: str = "" immomio_password: str = "" class ApplyRequest(BaseModel): url: str profile: ProfileModel submit_forms: bool = False application_id: int | None = None # echoed back in logs class ApplyResponse(BaseModel): success: bool message: str provider: str application_id: int | None = None forensics: dict def require_api_key(x_internal_api_key: str | None = Header(default=None)) -> None: if not INTERNAL_API_KEY: raise HTTPException(status_code=503, detail="INTERNAL_API_KEY not configured") if x_internal_api_key != INTERNAL_API_KEY: raise HTTPException(status_code=401, detail="invalid api key") @asynccontextmanager async def lifespan(_app: FastAPI): logger.info("apply ready, providers: %s", sorted(providers.PROVIDERS)) yield app = FastAPI(lifespan=lifespan, title="lazyflat-apply", docs_url=None, redoc_url=None) @app.get("/health") def health(): return {"status": "ok", "providers": sorted(providers.PROVIDERS)} @app.post("/apply", response_model=ApplyResponse, dependencies=[Depends(require_api_key)]) async def apply(req: ApplyRequest): url = req.url.strip() domain = urlparse(url).netloc.lower().removeprefix("www.") logger.info("apply request application_id=%s domain=%s submit=%s", req.application_id, domain, req.submit_forms) recorder = Recorder(url) recorder.step("request.received", detail=f"application_id={req.application_id} domain={domain} submit={req.submit_forms}") if domain not in providers.PROVIDERS: recorder.step("unsupported_provider", "warn", domain) result = ApplicationResult(False, message=_("unsupported_association")) return ApplyResponse( success=False, message=str(result), provider="", application_id=req.application_id, forensics=recorder.to_json(), ) provider = providers.PROVIDERS[domain] profile = Profile.from_dict(req.profile.model_dump()) ctx = ApplyContext(profile=profile, submit_forms=req.submit_forms, recorder=recorder) try: result = await provider.apply_for_flat(url, ctx) logger.info("apply outcome application_id=%s: %r", req.application_id, result) except Exception as e: logger.exception("apply crashed application_id=%s", req.application_id) recorder.step("exception", "error", f"{type(e).__name__}: {e}") result = ApplicationResult(False, f"Script Error:\n{e}") return ApplyResponse( success=result.success, message=str(result), provider=provider.domain, application_id=req.application_id, forensics=recorder.to_json(), )