-
Notifications
You must be signed in to change notification settings - Fork 0
refactor: layered scrape API, timeout_phase telemetry, and isolation hardening #45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
8f92933
fix(obs): tag scrape timeouts with queue/boot/work phase
gildesmarais d395779
fix(obs): document timeout_phase and lock engine progress marks
gildesmarais dacb125
test(obs): assert engine marks BOOT before Driver and WORK on tiers
gildesmarais 8cbbac2
refactor(obs): densify timeout_phase progress and tests
gildesmarais f843f28
test(obs): lock request-tier timeout category and phase
gildesmarais 469f13e
fix(runtime): prune orphan scrape dirs on every request
gildesmarais ad913db
refactor(api): restructure into layered packages with FastAPI DI.
gildesmarais 49200d3
refactor(api): remove dead DI/tier params and dedupe service identity
gildesmarais bf7ffde
fix(engine): singleton engine and executor with isolation regression …
gildesmarais 3eb6c67
refactor(config): thread Settings through deps and drop import-time f…
gildesmarais 30d7e43
refactor(api): single-source OpenAPI examples from model instances
gildesmarais a8c9f40
refactor(engine): typed DriverProtocol and centralized capability ada…
gildesmarais ab7534a
refactor(config): nest Sentry settings and simplify env layout
gildesmarais 4e17c62
refactor(schemas): split into enums/request/response modules
gildesmarais c1f3ab6
test: reorganize suite by layer and add scrape bench harness
gildesmarais b8564c8
perf(engine): lazy imports and dedupe browser-tier hot path
gildesmarais e64432d
chore(types): add pyright gate and tighten engine seams
gildesmarais 2e2b83b
docs(agents): document hardening pass architecture conventions
gildesmarais 68798c4
refactor(types): use strict pyright (#46)
gildesmarais 27150d8
chore(cleanup): delete spike script, dead surfaces, and unreachable p…
gildesmarais 671eb2e
refactor(logging): single-source the service logger via get_logger()
gildesmarais b50b08b
refactor(engine): own wall-clock budget math in one module
gildesmarais 2c7ec0b
refactor(api): deepen ScrapeService and thin the scrape route
gildesmarais 96a7ef1
refactor(types): typed Sentry scope seam and single readiness fact
gildesmarais 5398c9f
test: layer the suite and shrink blanket pyright directives
gildesmarais ef4546d
docs: sync AGENTS.md and typing residuals with the refactor
gildesmarais 1b1bbb2
test(api): pin SSRF guardrail at the HTTP seam
gildesmarais 6d3c654
fix(engine): make ENOSPC retry recreatable; pin budget math with units
gildesmarais 61f9193
Merge branch 'main' into fix/timeout-phase-telemetry
gildesmarais 4d7b729
fix(engine): honor submission deadline and harden session/boot isolation
gildesmarais a5f4fa8
fix(api): build routes after OpenAPI configure; live wait_timeout def…
gildesmarais 1bd7f5a
chore(ci): install pyright in requirements-dev and typecheck in CI
gildesmarais File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Botasaurus scrape API application package.""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """HTTP route registration.""" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| """FastAPI dependency providers.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from concurrent.futures import ThreadPoolExecutor | ||
| from typing import Annotated | ||
|
|
||
| from fastapi import Depends, Request | ||
|
|
||
| from app.config import Settings | ||
| from app.domain.scrape_service import ScrapeService | ||
| from app.engine import ScraperEngine | ||
|
|
||
|
|
||
| def get_app_settings(request: Request) -> Settings: | ||
| return request.app.state.settings | ||
|
|
||
|
|
||
| SettingsDep = Annotated[Settings, Depends(get_app_settings)] | ||
|
|
||
|
|
||
| def get_executor(request: Request) -> ThreadPoolExecutor: | ||
| return request.app.state.executor | ||
|
|
||
|
|
||
| ExecutorDep = Annotated[ThreadPoolExecutor, Depends(get_executor)] | ||
|
|
||
|
|
||
| def get_engine(request: Request) -> ScraperEngine: | ||
| return request.app.state.engine | ||
|
|
||
|
|
||
| EngineDep = Annotated[ScraperEngine, Depends(get_engine)] | ||
|
|
||
|
|
||
| def get_scrape_service( | ||
| settings: SettingsDep, | ||
| engine: EngineDep, | ||
| executor: ExecutorDep, | ||
| ) -> ScrapeService: | ||
| return ScrapeService(settings=settings, engine=engine, executor=executor) | ||
|
|
||
|
|
||
| ScrapeServiceDep = Annotated[ScrapeService, Depends(get_scrape_service)] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| """HTTP exception handlers returning scrape error envelopes.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import Any, TypedDict, cast | ||
| from urllib.parse import urlparse | ||
|
|
||
| from fastapi import FastAPI, Request | ||
| from fastapi.exceptions import RequestValidationError | ||
| from fastapi.responses import JSONResponse | ||
|
|
||
| from app.infra.request_id import resolve_request_id | ||
| from app.logging_config import get_logger | ||
| from app.schemas.response import ScrapeError, ScrapeSuccess, validation_error | ||
|
|
||
| logger = get_logger() | ||
|
|
||
| _NON_FIELD_LOC = {"body", "query", "path", "header"} | ||
|
|
||
|
|
||
| class ValidationErrorItem(TypedDict, total=False): | ||
| loc: tuple[Any, ...] | list[Any] | ||
| msg: str | ||
| type: str | ||
| input: Any | ||
|
|
||
|
|
||
| def schema_field_from_loc(loc: tuple[Any, ...] | list[Any]) -> str: | ||
| for part in loc: | ||
| if part not in _NON_FIELD_LOC: | ||
| return str(part) | ||
| return str(loc[-1]) if loc else "unknown" | ||
|
|
||
|
|
||
| def first_schema_field(errors: list[ValidationErrorItem]) -> str: | ||
| if not errors: | ||
| return "unknown" | ||
| return schema_field_from_loc(errors[0].get("loc") or ()) | ||
|
|
||
|
|
||
| def url_from_validation_body(body: Any) -> str: | ||
| if isinstance(body, dict): | ||
| url_value = cast(dict[str, Any], body).get("url") | ||
| if url_value is not None: | ||
| return str(url_value) | ||
| return "" | ||
|
|
||
|
|
||
| def validation_error_message(errors: list[ValidationErrorItem]) -> str: | ||
| if not errors: | ||
| return "Request schema validation failed" | ||
| parts: list[str] = [] | ||
| for err in errors: | ||
| loc = err.get("loc") or () | ||
| field = schema_field_from_loc(loc) | ||
| message = str(err.get("msg") or "invalid") | ||
| parts.append(f"{field}: {message}") | ||
| return "; ".join(parts) | ||
|
|
||
|
|
||
| def json_response( | ||
| body: ScrapeSuccess | ScrapeError, *, status_code: int | ||
| ) -> JSONResponse: | ||
| return JSONResponse(status_code=status_code, content=body.model_dump(mode="json")) | ||
|
|
||
|
|
||
| async def request_schema_validation_handler( | ||
| request: Request, exc: RequestValidationError | ||
| ) -> JSONResponse: | ||
| errors: list[ValidationErrorItem] = list(exc.errors()) # type: ignore[arg-type] | ||
| url = url_from_validation_body(exc.body) | ||
| field = first_schema_field(errors) | ||
| request_id, _ = resolve_request_id( | ||
| request.headers.get("X-Request-Id"), | ||
| host=urlparse(url).hostname if url else None, | ||
| ) | ||
| logger.info( | ||
| "request_schema_422 host=%s field=%s", | ||
| urlparse(url).hostname if url else None, | ||
| field, | ||
| ) | ||
| return json_response( | ||
| validation_error( | ||
| url, | ||
| validation_error_message(errors), | ||
| request_id=request_id, | ||
| ), | ||
| status_code=422, | ||
| ) | ||
|
|
||
|
|
||
| async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONResponse: | ||
| del exc | ||
| logger.exception("unhandled_exception path=%s", request.url.path) | ||
| request_id, _ = resolve_request_id(request.headers.get("X-Request-Id")) | ||
| return json_response( | ||
| validation_error( | ||
| "", | ||
| "Internal server error", | ||
| request_id=request_id, | ||
| ), | ||
| status_code=500, | ||
| ) | ||
|
|
||
|
|
||
| def register_exception_handlers(app: FastAPI) -> None: | ||
| app.add_exception_handler( | ||
| RequestValidationError, | ||
| cast(Any, request_schema_validation_handler), | ||
| ) | ||
| app.add_exception_handler(Exception, unhandled_exception_handler) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.