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
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
|
@ -58,7 +59,9 @@ def upgrade() -> None:
|
|||
sa.Column("ts", sa.DateTime(), index=True),
|
||||
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(
|
||||
"costing_entries",
|
||||
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."""
|
||||
logger.info("pry_startup", version="3.0.0")
|
||||
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
|
||||
yield
|
||||
logger.info("pry_shutdown")
|
||||
|
|
|
|||
|
|
@ -231,7 +231,9 @@ class ObjectField(FieldSpec):
|
|||
class ApifySchemaBuilder:
|
||||
"""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.description = description
|
||||
self.schema_version = schema_version
|
||||
|
|
@ -284,7 +286,13 @@ class ApifySchemaBuilder:
|
|||
maximum: int | None = None,
|
||||
) -> ApifySchemaBuilder:
|
||||
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)
|
||||
|
||||
|
|
@ -300,7 +308,13 @@ class ApifySchemaBuilder:
|
|||
maximum: float | None = None,
|
||||
) -> ApifySchemaBuilder:
|
||||
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)
|
||||
|
||||
|
|
@ -313,7 +327,9 @@ class ApifySchemaBuilder:
|
|||
default: bool | None = None,
|
||||
nullable: bool = False,
|
||||
) -> 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)
|
||||
|
||||
def add_enum(
|
||||
|
|
@ -326,7 +342,9 @@ class ApifySchemaBuilder:
|
|||
default: str | None = None,
|
||||
nullable: bool = False,
|
||||
) -> 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)
|
||||
|
||||
def add_array(
|
||||
|
|
@ -365,7 +383,13 @@ class ApifySchemaBuilder:
|
|||
nested_required: list[str] | None = None,
|
||||
) -> ApifySchemaBuilder:
|
||||
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)
|
||||
|
||||
|
|
|
|||
1
cli.py
1
cli.py
|
|
@ -196,6 +196,7 @@ def cmd_screenshot(url, output=None, timeout=30):
|
|||
def cmd_migrate():
|
||||
"""Run pending database migrations (alembic upgrade head)."""
|
||||
from db import run_migrations
|
||||
|
||||
run_migrations()
|
||||
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."""
|
||||
monkeypatch.setattr("api.settings.api_key", "secret-pry-key")
|
||||
# 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"})
|
||||
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(
|
||||
"/v1/scrape",
|
||||
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
|
||||
|
||||
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"},
|
||||
|
|
@ -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
|
||||
assert resp.status_code in (200, 402)
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ TestClient. External I/O (HTTP requests, browser automation) is mocked at the
|
|||
deterministic.
|
||||
"""
|
||||
|
||||
from collections.abc import Iterator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -20,14 +21,17 @@ from api import app
|
|||
|
||||
|
||||
@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."""
|
||||
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
|
||||
|
||||
|
||||
@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."""
|
||||
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:
|
||||
schema = {"title": "page title"}
|
||||
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_extractor.extract = AsyncMock(return_value=extracted)
|
||||
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
|
||||
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
|
||||
data = resp.json()
|
||||
|
|
@ -121,7 +132,9 @@ async def test_v1_crawl_success(client: TestClient, mock_scrape_result: dict) ->
|
|||
pages = [mock_scrape_result]
|
||||
with patch("routers.scraping.scraper") as mock_scraper:
|
||||
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
|
||||
data = resp.json()
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
# Licensed under MIT.
|
||||
"""End-to-end tests for the templates router endpoints."""
|
||||
|
||||
from collections.abc import Iterator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
|
@ -14,9 +15,12 @@ from api import app
|
|||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def bypass_x402_middleware() -> None:
|
||||
def bypass_x402_middleware() -> Iterator[None]:
|
||||
"""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
|
||||
|
||||
|
||||
|
|
@ -65,7 +69,10 @@ async def test_get_template_not_found(client: TestClient) -> None:
|
|||
async def test_execute_template(client: TestClient) -> None:
|
||||
result = {"success": True, "data": {"title": "Widget"}}
|
||||
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
|
||||
data = resp.json()
|
||||
|
|
|
|||
|
|
@ -63,10 +63,17 @@ def test_array_and_object_fields() -> 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"]
|
||||
|
||||
|
||||
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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue