style: format phase0 router split and fix mypy fixture types
All checks were successful
All checks were successful
This commit is contained in:
parent
8731fee018
commit
250f6041b3
8 changed files with 89 additions and 23 deletions
|
|
@ -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
5
api.py
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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
1
cli.py
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue