style: format phase0 router split and fix mypy fixture types
All checks were successful
CI / test (pull_request) Successful in 1m13s
CI / Security audit (bandit) (pull_request) Successful in 34s
CI / lint (pull_request) Successful in 49s
CI / typecheck (pull_request) Successful in 54s
CI / Secret scan (gitleaks) (pull_request) Successful in 31s

This commit is contained in:
Crypto Rug Munch 2026-07-03 02:17:38 +02:00
parent 8731fee018
commit 250f6041b3
8 changed files with 89 additions and 23 deletions

View file

@ -5,6 +5,7 @@ Revises:
Create Date: 2026-07-03 00:00:00.000000 Create Date: 2026-07-03 00:00:00.000000
""" """
from __future__ import annotations from __future__ import annotations
from collections.abc import Sequence from collections.abc import Sequence
@ -58,7 +59,9 @@ def upgrade() -> None:
sa.Column("ts", sa.DateTime(), index=True), sa.Column("ts", sa.DateTime(), index=True),
sa.PrimaryKeyConstraint("id"), sa.PrimaryKeyConstraint("id"),
) )
op.create_index("ix_intel_competitor_ts", "intel_snapshots", ["competitor_id", sa.text("ts DESC")]) op.create_index(
"ix_intel_competitor_ts", "intel_snapshots", ["competitor_id", sa.text("ts DESC")]
)
op.create_table( op.create_table(
"costing_entries", "costing_entries",
sa.Column("id", sa.Integer(), nullable=False), sa.Column("id", sa.Integer(), nullable=False),

5
api.py
View file

@ -81,7 +81,10 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
"""Startup: validate deps. Shutdown: cleanup clients.""" """Startup: validate deps. Shutdown: cleanup clients."""
logger.info("pry_startup", version="3.0.0") logger.info("pry_startup", version="3.0.0")
if not settings.api_key: if not settings.api_key:
logger.warning("pry_api_key_unset", message="PRY_API_KEY is not set; all protected endpoints are publicly accessible") logger.warning(
"pry_api_key_unset",
message="PRY_API_KEY is not set; all protected endpoints are publicly accessible",
)
get_pipeline() # Initialize pipeline get_pipeline() # Initialize pipeline
yield yield
logger.info("pry_shutdown") logger.info("pry_shutdown")

View file

@ -231,7 +231,9 @@ class ObjectField(FieldSpec):
class ApifySchemaBuilder: class ApifySchemaBuilder:
"""Fluent builder for Apify actor input/output schemas.""" """Fluent builder for Apify actor input/output schemas."""
def __init__(self, title: str = "Actor Input", description: str = "", schema_version: int = 1) -> None: def __init__(
self, title: str = "Actor Input", description: str = "", schema_version: int = 1
) -> None:
self.title = title self.title = title
self.description = description self.description = description
self.schema_version = schema_version self.schema_version = schema_version
@ -284,7 +286,13 @@ class ApifySchemaBuilder:
maximum: int | None = None, maximum: int | None = None,
) -> ApifySchemaBuilder: ) -> ApifySchemaBuilder:
field = IntegerField( field = IntegerField(
name, title, description=description, default=default, nullable=nullable, minimum=minimum, maximum=maximum name,
title,
description=description,
default=default,
nullable=nullable,
minimum=minimum,
maximum=maximum,
) )
return self.add_field(field, required=required) return self.add_field(field, required=required)
@ -300,7 +308,13 @@ class ApifySchemaBuilder:
maximum: float | None = None, maximum: float | None = None,
) -> ApifySchemaBuilder: ) -> ApifySchemaBuilder:
field = NumberField( field = NumberField(
name, title, description=description, default=default, nullable=nullable, minimum=minimum, maximum=maximum name,
title,
description=description,
default=default,
nullable=nullable,
minimum=minimum,
maximum=maximum,
) )
return self.add_field(field, required=required) return self.add_field(field, required=required)
@ -313,7 +327,9 @@ class ApifySchemaBuilder:
default: bool | None = None, default: bool | None = None,
nullable: bool = False, nullable: bool = False,
) -> ApifySchemaBuilder: ) -> ApifySchemaBuilder:
field = BooleanField(name, title, description=description, default=default, nullable=nullable) field = BooleanField(
name, title, description=description, default=default, nullable=nullable
)
return self.add_field(field, required=required) return self.add_field(field, required=required)
def add_enum( def add_enum(
@ -326,7 +342,9 @@ class ApifySchemaBuilder:
default: str | None = None, default: str | None = None,
nullable: bool = False, nullable: bool = False,
) -> ApifySchemaBuilder: ) -> ApifySchemaBuilder:
field = EnumField(name, title, enum, description=description, default=default, nullable=nullable) field = EnumField(
name, title, enum, description=description, default=default, nullable=nullable
)
return self.add_field(field, required=required) return self.add_field(field, required=required)
def add_array( def add_array(
@ -365,7 +383,13 @@ class ApifySchemaBuilder:
nested_required: list[str] | None = None, nested_required: list[str] | None = None,
) -> ApifySchemaBuilder: ) -> ApifySchemaBuilder:
field = ObjectField( field = ObjectField(
name, title, properties, description=description, default=default, nullable=nullable, required=nested_required name,
title,
properties,
description=description,
default=default,
nullable=nullable,
required=nested_required,
) )
return self.add_field(field, required=required) return self.add_field(field, required=required)

1
cli.py
View file

@ -196,6 +196,7 @@ def cmd_screenshot(url, output=None, timeout=30):
def cmd_migrate(): def cmd_migrate():
"""Run pending database migrations (alembic upgrade head).""" """Run pending database migrations (alembic upgrade head)."""
from db import run_migrations from db import run_migrations
run_migrations() run_migrations()
print(f"{GREEN}{NC} Migrations up to date") print(f"{GREEN}{NC} Migrations up to date")

View file

@ -47,11 +47,17 @@ async def test_api_key_required_when_set(client: TestClient, monkeypatch) -> Non
"""Protected endpoints require a valid Bearer PRY_API_KEY.""" """Protected endpoints require a valid Bearer PRY_API_KEY."""
monkeypatch.setattr("api.settings.api_key", "secret-pry-key") monkeypatch.setattr("api.settings.api_key", "secret-pry-key")
# Disable x402 payment gating so we can test auth in isolation. # Disable x402 payment gating so we can test auth in isolation.
with patch("x402_middleware.X402Middleware.__call__", lambda self, scope, receive, send: self.app(scope, receive, send)): with patch(
"x402_middleware.X402Middleware.__call__",
lambda self, scope, receive, send: self.app(scope, receive, send),
):
resp = client.post("/v1/scrape", json={"url": "https://example.com"}) resp = client.post("/v1/scrape", json={"url": "https://example.com"})
assert resp.status_code == 401 assert resp.status_code == 401
with patch("x402_middleware.X402Middleware.__call__", lambda self, scope, receive, send: self.app(scope, receive, send)): with patch(
"x402_middleware.X402Middleware.__call__",
lambda self, scope, receive, send: self.app(scope, receive, send),
):
resp = client.post( resp = client.post(
"/v1/scrape", "/v1/scrape",
json={"url": "https://example.com"}, json={"url": "https://example.com"},
@ -59,7 +65,10 @@ async def test_api_key_required_when_set(client: TestClient, monkeypatch) -> Non
) )
assert resp.status_code == 401 assert resp.status_code == 401
with patch("x402_middleware.X402Middleware.__call__", lambda self, scope, receive, send: self.app(scope, receive, send)): with patch(
"x402_middleware.X402Middleware.__call__",
lambda self, scope, receive, send: self.app(scope, receive, send),
):
resp = client.post( resp = client.post(
"/v1/scrape", "/v1/scrape",
json={"url": "https://example.com"}, json={"url": "https://example.com"},
@ -67,4 +76,3 @@ async def test_api_key_required_when_set(client: TestClient, monkeypatch) -> Non
) )
# auth passes; endpoint returns 402 because we did not mock scraper, which is fine # auth passes; endpoint returns 402 because we did not mock scraper, which is fine
assert resp.status_code in (200, 402) assert resp.status_code in (200, 402)

View file

@ -11,6 +11,7 @@ TestClient. External I/O (HTTP requests, browser automation) is mocked at the
deterministic. deterministic.
""" """
from collections.abc import Iterator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@ -20,14 +21,17 @@ from api import app
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def bypass_x402_middleware() -> None: def bypass_x402_middleware() -> Iterator[None]:
"""Disable x402 payment gating so scraping endpoints can be tested.""" """Disable x402 payment gating so scraping endpoints can be tested."""
with patch("x402_middleware.X402Middleware.__call__", lambda self, scope, receive, send: self.app(scope, receive, send)): with patch(
"x402_middleware.X402Middleware.__call__",
lambda self, scope, receive, send: self.app(scope, receive, send),
):
yield yield
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def clear_response_cache() -> None: def clear_response_cache() -> Iterator[None]:
"""Clear the shared response cache so tests don't see each other's data.""" """Clear the shared response cache so tests don't see each other's data."""
from deps import cache as response_cache from deps import cache as response_cache
@ -71,7 +75,10 @@ async def test_v1_scrape_success(client: TestClient, mock_scrape_result: dict) -
async def test_v1_scrape_with_json_schema(client: TestClient, mock_scrape_result: dict) -> None: async def test_v1_scrape_with_json_schema(client: TestClient, mock_scrape_result: dict) -> None:
schema = {"title": "page title"} schema = {"title": "page title"}
extracted = {"title": "Example"} extracted = {"title": "Example"}
with patch("routers.scraping.scraper") as mock_scraper, patch("routers.scraping.extractor") as mock_extractor: with (
patch("routers.scraping.scraper") as mock_scraper,
patch("routers.scraping.extractor") as mock_extractor,
):
mock_scraper.scrape = AsyncMock(return_value=mock_scrape_result) mock_scraper.scrape = AsyncMock(return_value=mock_scrape_result)
mock_extractor.extract = AsyncMock(return_value=extracted) mock_extractor.extract = AsyncMock(return_value=extracted)
resp = client.post("/v1/scrape", json={"url": "https://example.com", "jsonSchema": schema}) resp = client.post("/v1/scrape", json={"url": "https://example.com", "jsonSchema": schema})
@ -94,7 +101,11 @@ async def test_v1_detect_block_success(client: TestClient) -> None:
) )
) )
mock_get_client.return_value = mock_client mock_get_client.return_value = mock_client
resp = client.post("/v1/detect-block", content="\"https://example.com\"", headers={"content-type": "application/json"}) resp = client.post(
"/v1/detect-block",
content='"https://example.com"',
headers={"content-type": "application/json"},
)
assert resp.status_code == 200 assert resp.status_code == 200
data = resp.json() data = resp.json()
@ -121,7 +132,9 @@ async def test_v1_crawl_success(client: TestClient, mock_scrape_result: dict) ->
pages = [mock_scrape_result] pages = [mock_scrape_result]
with patch("routers.scraping.scraper") as mock_scraper: with patch("routers.scraping.scraper") as mock_scraper:
mock_scraper.crawl = AsyncMock(return_value=pages) mock_scraper.crawl = AsyncMock(return_value=pages)
resp = client.post("/v1/crawl", json={"url": "https://example.com", "maxPages": 2, "maxDepth": 1}) resp = client.post(
"/v1/crawl", json={"url": "https://example.com", "maxPages": 2, "maxDepth": 1}
)
assert resp.status_code == 200 assert resp.status_code == 200
data = resp.json() data = resp.json()

View file

@ -5,6 +5,7 @@
# Licensed under MIT. # Licensed under MIT.
"""End-to-end tests for the templates router endpoints.""" """End-to-end tests for the templates router endpoints."""
from collections.abc import Iterator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
@ -14,9 +15,12 @@ from api import app
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def bypass_x402_middleware() -> None: def bypass_x402_middleware() -> Iterator[None]:
"""Disable x402 payment gating for template execution tests.""" """Disable x402 payment gating for template execution tests."""
with patch("x402_middleware.X402Middleware.__call__", lambda self, scope, receive, send: self.app(scope, receive, send)): with patch(
"x402_middleware.X402Middleware.__call__",
lambda self, scope, receive, send: self.app(scope, receive, send),
):
yield yield
@ -65,7 +69,10 @@ async def test_get_template_not_found(client: TestClient) -> None:
async def test_execute_template(client: TestClient) -> None: async def test_execute_template(client: TestClient) -> None:
result = {"success": True, "data": {"title": "Widget"}} result = {"success": True, "data": {"title": "Widget"}}
with patch("routers.templates.execute_template", new=AsyncMock(return_value=result)): with patch("routers.templates.execute_template", new=AsyncMock(return_value=result)):
resp = client.post("/v1/templates/execute", json={"template_id": "amazon_product", "url": "https://example.com"}) resp = client.post(
"/v1/templates/execute",
json={"template_id": "amazon_product", "url": "https://example.com"},
)
assert resp.status_code == 200 assert resp.status_code == 200
data = resp.json() data = resp.json()

View file

@ -63,10 +63,17 @@ def test_array_and_object_fields() -> None:
def test_nullable_field() -> None: def test_nullable_field() -> None:
schema = ApifySchemaBuilder().add_string("optional", "Optional", nullable=True).build_input_schema() schema = (
ApifySchemaBuilder().add_string("optional", "Optional", nullable=True).build_input_schema()
)
assert schema["properties"]["optional"]["type"] == ["string", "null"] assert schema["properties"]["optional"]["type"] == ["string", "null"]
def test_prefab() -> None: def test_prefab() -> None:
schema = ApifySchemaBuilder().set_prefab("START_URLS").add_string("url", "URL", required=True).build_input_schema() schema = (
ApifySchemaBuilder()
.set_prefab("START_URLS")
.add_string("url", "URL", required=True)
.build_input_schema()
)
assert schema["prefab"] == "START_URLS" assert schema["prefab"] == "START_URLS"