Commit graph

12 commits

Author SHA1 Message Date
opencode
3804c1cd47 fix(pry): test_ready_returns_200 → pytest.mark.integration
Mark the readiness test as integration; it requires live Postgres + Redis
which are not running in Cinnabox dev. Tests run on Talos prod include it
when invoked with: pytest -m integration.

- Add @pytest.mark.integration + skipif on _has_postgres() probe
- Register integration marker in pyproject.toml
- Default addopts = -m 'not integration' skips them in plain ============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/dev/pry
configfile: pyproject.toml
testpaths: tests
plugins: respx-0.23.1, cov-7.1.0, Faker-27.4.0, asyncio-1.4.0, anyio-4.14.1
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
collected 622 items / 1 deselected / 621 selected

tests/test_adaptive.py .......                                           [  1%]
tests/test_advanced_features.py ..............                           [  3%]
tests/test_advanced_scraping.py .................                        [  6%]
tests/test_agency.py .......                                             [  7%]
tests/test_ai_plugin.py ...                                              [  7%]
tests/test_alerter.py .....                                              [  8%]
tests/test_api.py ....                                                   [  9%]
tests/test_api_integration.py ...........................                [ 13%]
tests/test_api_mcp.py ...s                                               [ 14%]
tests/test_api_mcp_x402.py ..............                                [ 16%]
tests/test_api_scraping.py ........                                      [ 17%]
tests/test_api_surface_snapshot.py ....                                  [ 18%]
tests/test_api_templates.py ......                                       [ 19%]
tests/test_apify_schema.py .....                                         [ 20%]
tests/test_auth_connector.py .....                                       [ 20%]
tests/test_block_detector.py ..........                                  [ 22%]
tests/test_cache.py .......                                              [ 23%]
tests/test_camoufox.py ...                                               [ 24%]
tests/test_commerce_sync.py ..                                           [ 24%]
tests/test_compliance.py .............                                   [ 26%]
tests/test_costing.py ....                                               [ 27%]
tests/test_crm_sync.py ......                                            [ 28%]
tests/test_db.py .........                                               [ 29%]
tests/test_destinations.py .......                                       [ 30%]
tests/test_email_scraper.py .........                                    [ 32%]
tests/test_enrichment.py ..........                                      [ 33%]
tests/test_extractor.py .......                                          [ 34%]
tests/test_fallback_integration.py ............                          [ 36%]
tests/test_freshness.py .....                                            [ 37%]
tests/test_gdpr.py ......                                                [ 38%]
tests/test_infrastructure.py ...........                                 [ 40%]
tests/test_intelligence.py ...........................                   [ 44%]
tests/test_jobqueue.py ......                                            [ 45%]
tests/test_lazy_load.py .....                                            [ 46%]
tests/test_llm_fallback.py ......                                        [ 47%]
tests/test_llm_providers.py ......                                       [ 48%]
tests/test_logging_config.py ........                                    [ 49%]
tests/test_markdown_gen.py ......                                        [ 50%]
tests/test_marketplace.py ...........                                    [ 52%]
tests/test_mcp_production.py ..........                                  [ 54%]
tests/test_monitor.py ...............                                    [ 56%]
tests/test_network.py ......                                             [ 57%]
tests/test_parser.py ......                                              [ 58%]
tests/test_pipeline.py .....                                             [ 59%]
tests/test_pipelines.py .......                                          [ 60%]
tests/test_proxy_manager.py ............                                 [ 62%]
tests/test_quality.py ...........                                        [ 64%]
tests/test_ratelimit.py ......                                           [ 65%]
tests/test_real_features.py ........                                     [ 66%]
tests/test_reconciliation.py .........                                   [ 67%]
tests/test_referrals.py ..............                                   [ 70%]
tests/test_reports.py .......                                            [ 71%]
tests/test_resilience.py ..............                                  [ 73%]
tests/test_retry.py ...........................                          [ 77%]
tests/test_review.py ......                                              [ 78%]
tests/test_scraper.py ........                                           [ 80%]
tests/test_sdk.py ......                                                 [ 80%]
tests/test_secrets_backend.py .........                                  [ 82%]
tests/test_seo_monitor.py ......                                         [ 83%]
tests/test_settings.py .........                                         [ 84%]
tests/test_shadow_dom.py .........                                       [ 86%]
tests/test_structure_monitor.py F......                                  [ 87%]
tests/test_templates.py ......                                           [ 88%]
tests/test_training_data.py .........                                    [ 89%]
tests/test_ultimate_scraper.py ............                              [ 91%]
tests/test_url_guard.py ........................                         [ 95%]
tests/test_x402_mcp_spec.py ...........................                  [100%]

=================================== FAILURES ===================================
__________________________ test_check_selectors_empty __________________________

    @pytest.mark.asyncio
    async def test_check_selectors_empty():
>       result = await check_selectors("https://example.com", [])
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/test_structure_monitor.py:36:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
structure_monitor.py:46: in check_selectors
    resp = await client.get(
../.local/lib/python3.12/site-packages/httpx/_client.py:1768: in get
    return await self.request(
../.local/lib/python3.12/site-packages/httpx/_client.py:1540: in request
    return await self.send(request, auth=auth, follow_redirects=follow_redirects)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../.local/lib/python3.12/site-packages/httpx/_client.py:1629: in send
    response = await self._send_handling_auth(
../.local/lib/python3.12/site-packages/httpx/_client.py:1657: in _send_handling_auth
    response = await self._send_handling_redirects(
../.local/lib/python3.12/site-packages/httpx/_client.py:1694: in _send_handling_redirects
    response = await self._send_single_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../.local/lib/python3.12/site-packages/httpx/_client.py:1730: in _send_single_request
    response = await transport.handle_async_request(request)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../.local/lib/python3.12/site-packages/httpx/_transports/default.py:394: in handle_async_request
    resp = await self._pool.handle_async_request(req)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
../.local/lib/python3.12/site-packages/httpcore/_async/connection_pool.py:256: in handle_async_request
    raise exc from None
../.local/lib/python3.12/site-packages/httpcore/_async/connection_pool.py:229: in handle_async_request
    await self._close_connections(closing)
../.local/lib/python3.12/site-packages/httpcore/_async/connection_pool.py:345: in _close_connections
    await connection.aclose()
../.local/lib/python3.12/site-packages/httpcore/_async/connection.py:173: in aclose
    await self._connection.aclose()
../.local/lib/python3.12/site-packages/httpcore/_async/http11.py:258: in aclose
    await self._network_stream.aclose()
../.local/lib/python3.12/site-packages/httpcore/_backends/anyio.py:53: in aclose
    await self._stream.aclose()
../.local/lib/python3.12/site-packages/anyio/streams/tls.py:236: in aclose
    await self.transport_stream.aclose()
../.local/lib/python3.12/site-packages/anyio/_backends/_asyncio.py:1374: in aclose
    self._transport.close()
/usr/lib/python3.12/asyncio/selector_events.py:1211: in close
    super().close()
/usr/lib/python3.12/asyncio/selector_events.py:875: in close
    self._loop.call_soon(self._call_connection_lost, None)
/usr/lib/python3.12/asyncio/base_events.py:795: in call_soon
    self._check_closed()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <_UnixSelectorEventLoop running=False closed=True debug=False>

    def _check_closed(self):
        if self._closed:
>           raise RuntimeError('Event loop is closed')
E           RuntimeError: Event loop is closed

/usr/lib/python3.12/asyncio/base_events.py:541: RuntimeError
=============================== warnings summary ===============================
../.local/lib/python3.12/site-packages/pydantic/main.py:1809
../.local/lib/python3.12/site-packages/pydantic/main.py:1809
  /home/dev/.local/lib/python3.12/site-packages/pydantic/main.py:1809: UserWarning: Field name "schema" in "Body_extract_css_v1_extract_css_post" shadows an attribute in parent "BaseModel"
    return meta(

../.local/lib/python3.12/site-packages/pydantic/main.py:1809
../.local/lib/python3.12/site-packages/pydantic/main.py:1809
  /home/dev/.local/lib/python3.12/site-packages/pydantic/main.py:1809: UserWarning: Field name "schema" in "Body_extract_llm_v1_extract_llm_post" shadows an attribute in parent "BaseModel"
    return meta(

../.local/lib/python3.12/site-packages/pydantic/main.py:1809
../.local/lib/python3.12/site-packages/pydantic/main.py:1809
  /home/dev/.local/lib/python3.12/site-packages/pydantic/main.py:1809: UserWarning: Field name "schema" in "Body_quality_check_v1_quality_check_post" shadows an attribute in parent "BaseModel"
    return meta(

../.local/lib/python3.12/site-packages/pydantic/main.py:1809
../.local/lib/python3.12/site-packages/pydantic/main.py:1809
  /home/dev/.local/lib/python3.12/site-packages/pydantic/main.py:1809: UserWarning: Field name "schema" in "Body_extract_with_review_v1_extract_with_review_post" shadows an attribute in parent "BaseModel"
    return meta(

tests/test_api_integration.py::TestHealthEndpoints::test_live_returns_200
  /home/dev/.local/lib/python3.12/site-packages/_pytest/unraisableexception.py:67: PytestUnraisableExceptionWarning: Exception ignored in: <function BaseSubprocessTransport.__del__ at 0x7c2702c51120>

  Traceback (most recent call last):
    File "/usr/lib/python3.12/asyncio/base_subprocess.py", line 126, in __del__
      self.close()
    File "/usr/lib/python3.12/asyncio/base_subprocess.py", line 104, in close
      proto.pipe.close()
    File "/usr/lib/python3.12/asyncio/unix_events.py", line 767, in close
      self.write_eof()
    File "/usr/lib/python3.12/asyncio/unix_events.py", line 753, in write_eof
      self._loop.call_soon(self._call_connection_lost, None)
    File "/usr/lib/python3.12/asyncio/base_events.py", line 795, in call_soon
      self._check_closed()
    File "/usr/lib/python3.12/asyncio/base_events.py", line 541, in _check_closed
      raise RuntimeError('Event loop is closed')
  RuntimeError: Event loop is closed

  Enable tracemalloc to get traceback where the object was allocated.
  See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.
    warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))

tests/test_api_integration.py::TestDetectBlockEndpoint::test_detect_block_success
tests/test_api_integration.py::TestErrorHandling::test_422_on_invalid_json
  /home/dev/.local/lib/python3.12/site-packages/httpx/_models.py:408: DeprecationWarning: Use 'content=<...>' to upload raw bytes/text content.
    headers, stream = encode_request(

tests/test_jobqueue.py::test_jobqueue_create_job_returns_id
tests/test_jobqueue.py::test_jobqueue_get_job_exists
tests/test_jobqueue.py::test_jobqueue_complete_job
tests/test_jobqueue.py::test_jobqueue_fail_job
tests/test_jobqueue.py::test_jobqueue_update_preserves_fields
  /home/dev/pry/jobqueue.py:56: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
    "created_at": datetime.utcnow().isoformat(),

tests/test_jobqueue.py::test_jobqueue_create_job_returns_id
tests/test_jobqueue.py::test_jobqueue_get_job_exists
tests/test_jobqueue.py::test_jobqueue_complete_job
tests/test_jobqueue.py::test_jobqueue_fail_job
tests/test_jobqueue.py::test_jobqueue_update_preserves_fields
  /home/dev/pry/jobqueue.py:57: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
    "updated_at": datetime.utcnow().isoformat(),

tests/test_jobqueue.py::test_jobqueue_complete_job
tests/test_jobqueue.py::test_jobqueue_fail_job
tests/test_jobqueue.py::test_jobqueue_update_preserves_fields
  /home/dev/pry/jobqueue.py:93: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
    self._local_jobs[job_id]["updated_at"] = datetime.utcnow().isoformat()

tests/test_retry.py::TestAsyncRetry::test_passes_args_and_kwargs
  /home/dev/pry/tests/test_retry.py:259: RuntimeWarning: coroutine 'AsyncMockMixin._execute_mock_call' was never awaited
    fn(url, timeout=timeout)
  Enable tracemalloc to get traceback where the object was allocated.
  See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.

tests/test_sdk.py::test_prycrawl_health_returns_dict
  /home/dev/.local/lib/python3.12/site-packages/_pytest/python.py:167: RuntimeWarning: coroutine 'PryCrawl.health' was never awaited
    result = testfunction(**testargs)
  Enable tracemalloc to get traceback where the object was allocated.
  See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info.

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_structure_monitor.py::test_check_selectors_empty - RuntimeE...
= 1 failed, 619 passed, 1 skipped, 1 deselected, 26 warnings in 90.04s (0:01:30) =

Tests: 2 passed, 1 deselected in TestHealthEndpoints (was 2 pass + 1 fail).
2026-07-06 20:08:27 +07:00
df2fc04f7c fix(pry): add missing deps (cloudscraper, aiohttp-socks, pyjwt, apify, playwright-stealth), fix docker env var + healthcheck + non-root 2026-07-06 19:23:32 +07:00
345cd79bc9 feat(pry): production readiness pass — apify actor, async db, retry wiring, tests, observability, mypy
- Pin Dockerfile --workers 1

- Wire retry.py + circuit breakers into ultimate_scraper tiers

- Add Apify actor (apify_actor.py, Dockerfile.apify, .actor/actor.json)

- Add async SQLAlchemy support alongside sync db.py

- Add 31 HTTP integration tests (tests/test_api_integration.py + test_api_mcp.py)

- Add OTLP exporter support in observability.py

- Re-enable mypy var-annotated error code; fix annotations

- Improve CI workflow (pip cache, install -e .[dev], gitleaks, commitlint, 40% coverage gate)
2026-07-03 14:41:41 +02:00
8b52f14774 feat(pry): phase 0 — split routers, add tests, apify schema, pry api key (#5)
Some checks failed
CI / Secret scan (gitleaks) (push) Successful in 34s
CI / Security audit (bandit) (push) Successful in 35s
CI / test (push) Successful in 1m23s
CI / lint (push) Failing after 49s
CI / typecheck (push) Successful in 55s
2026-07-03 03:43:02 +02:00
07288a01d7 feat(db): add Alembic migrations (#6)
All checks were successful
CI / typecheck (push) Successful in 51s
CI / Secret scan (gitleaks) (push) Successful in 32s
CI / lint (push) Successful in 47s
CI / Security audit (bandit) (push) Successful in 35s
CI / test (push) Successful in 1m20s
2026-07-03 02:22:33 +02:00
7baa48ec4d ci(forgejo): install Node.js before actions/checkout@v4 (#2)
Some checks failed
CI / Security audit (bandit) (push) Successful in 34s
CI / lint (push) Successful in 45s
CI / typecheck (push) Successful in 48s
CI / Secret scan (gitleaks) (push) Successful in 31s
CI / test (push) Failing after 48s
2026-07-03 00:42:15 +02:00
98eebe62bf fix(lint): resolve remaining ruff errors and unblock MCP SSE test (#1)
Some checks failed
CI / lint (push) Failing after 2s
CI / typecheck (push) Failing after 1s
CI / test (push) Failing after 1s
CI / Secret scan (gitleaks) (push) Failing after 2s
CI / Security audit (bandit) (push) Failing after 2s
2026-07-02 23:18:40 +02:00
469cce04aa feat(db): SQLAlchemy foundation with 24 models + JSON importer
Replaces the 12 ad-hoc JSON file stores (quality, intel, monitors,
sessions, accounts, agency, etc.) with a single SQLAlchemy-backed
database. The new foundation gives us:
  - Concurrency safety (SQLite WAL mode, file locks via SQLAlchemy)
  - Transactions (rollback on error)
  - Querying (WHERE, JOIN, ORDER BY, LIMIT)
  - Relationships (ForeignKey on monitor_id, agency_id, etc.)
  - Multi-tenant ready (everything indexed by id)

Engine:
  - Default: SQLite at $PRY_DATA_DIR/pry.db (zero-config)
  - Production: set PRY_DATABASE_URL=postgresql://... (no code change)
  - Foreign keys enabled for SQLite (off by default)

Models (24):
  quality_checks, review_items, intel_snapshots, costing_entries,
  freshness_snapshots, structure_snapshots, seo_snapshots, monitors,
  monitor_runs, accounts, browser_sessions, reports, training_datasets,
  pipelines, pipeline_runs, gdpr_requests, agencies, agency_clients,
  referral_clicks, actors, actor_runs, llm_usage, webhooks, x402_receipts

Each model maps to a former JSON store. Most have an _id field with
unique constraint so re-importing the same data is safe. The legacy
"id" and "name" fields are renamed to "<scope>_id" / "<scope>_name"
to avoid reserved LogRecord field name collisions.

JSON importer (import_json_stores):
  One-shot function that reads the existing JSON files in $PRY_DATA_DIR
  and writes them to the SQL tables. Returns a {store: count} dict.
  Idempotent: re-running with the same data is safe.

Public API:
  - get_engine()         - lazy engine creation
  - get_session()        - new Session (caller manages)
  - session_scope()      - context manager: commit/rollback
  - import_json_stores() - the one-shot importer
  - db_health()          - dict for /health endpoint
  - _has_sqlalchemy, get_db - backward-compat aliases

pyproject.toml: added sqlalchemy>=2.0.0 and aiosqlite>=0.19.0

Tests: 7/7 in tests/test_db.py pass:
  - Engine creates DB file
  - All 24 tables created
  - session_scope commits on success
  - session_scope rolls back on error
  - import_json_stores reads existing JSON
  - db_health returns dict
  - Models have unique indexes on _id columns

Test suite: 436/437 pass (1 pre-existing SSE subprocess failure in
this sandbox; unrelated).

Follow-up:
  - Migrate the actual module code to use the SQL tables instead of
    JSON files. Each module (quality.py, intelligence.py, monitors.py,
    etc.) needs a SQL-backed replacement. Estimated 4-6 hours.
  - Add Alembic for schema migrations instead of create_all().
  - Add Postgres-specific tuning when PRY_DATABASE_URL is set.
2026-07-02 21:10:46 +02:00
0200bf3e16 refactor(exceptions): add ruff BLE001; convert 103 broad except Exception
Per CONVENTIONS.md Part 2 ("Never bare except") and CONVENTIONS.md
Part 7 (pre-commit hooks: ruff), blind `except Exception` is now a
lint failure. Pre-existing sites are marked `# noqa: BLE001` for
later manual review; new code must use specific exception types.

Changes:
- pyproject.toml: added "BLE" to ruff lint select. BLE001 is now enforced
- 103 of 166 `except Exception` sites were auto-converted to specific
  types based on context (httpx, json, OSError, subprocess, etc.)
- 62 remaining sites marked with `# noqa: BLE001` for later review
  (mostly generic try/except wrappers that legitimately need broad catch
  for graceful degradation: e.g. compliance LLM fallback must catch
  any error to preserve the regex result)
- 1 manual fix: reverted compliance.py LLM fallback to broad except
  with explicit "must catch all errors" comment + noqa
- 2 files (commerce_sync.py, crm_sync.py) needed `import httpx` added
  so the auto-converted exception references would resolve
- 5 source files (agency, monitor, pipelines, auth_connector,
  llm_providers/registry) renamed "name" -> "<scope>_name" in
  extra={...} dicts because "name" is a reserved LogRecord field

Test impact:
- 14 failing tests -> 1 (the SSE subprocess test is a sandbox limitation,
  pre-existing and unrelated)
- New `test_ble_temp.py` verifies BLE001 catches new violations

Follow-up:
- Each `# noqa: BLE001` site should be reviewed and replaced with a
  specific exception type where possible. The most common legitimate
  broad-catch case is the LLM fallback path; everything else probably
  can be narrowed.
2026-07-02 21:04:53 +02:00
117001006f feat(logging): add structlog + JSON logging (CONVENTIONS.md Part 5)
Pry logs are now JSON objects with the required fields (timestamp,
level, service, event, plus key-value pairs). This is the standard
required by CONVENTIONS.md Part 5 and is what makes the service
operable in production (Loki, ELK, etc. can index the structured
records).

New module logging_config.py:
  setup_logging(level, fmt) - configure once at process startup
  get_logger(name)         - get a structlog logger; falls back to stdlib
  is_configured()          - diagnostic for /health

Configuration via env vars:
  PRY_LOG_FORMAT=json|console   (default json)
  PRY_LOG_LEVEL=DEBUG|INFO|...  (default INFO)
  PRY_LOG_STRICT_EXTRAS=1       (default unset = lenient)

Backward compatibility:
  - stdlib logging.getLogger(__name__) calls still work
  - setup_logging bridges stdlib through structlog's formatter
  - In lenient mode, extra={...} keys that collide with reserved
    LogRecord names (e.g. 'name') are moved to an `extra` sub-dict
    so existing code doesn't crash

Wired in:
  api.py: setup_logging() at module import time; lifespan log uses
          structlog style (logger.info("event", key="value") without
          the `extra={...}` wrapper)
  pyproject.toml: structlog>=24.0.0 dep added

Fixed source files that used reserved LogRecord keys in extra={...}:
  agency.py:        "name" -> "agency_name"
  auth_connector.py: "name" -> "credential_name"
  monitor.py:       "name" -> "monitor_name"
  pipelines.py:     "name" -> "pipeline_name"
  llm_providers/registry.py: "name" -> "provider_name"
These would have crashed with KeyError "Attempt to overwrite 'name' in
LogRecord" the moment a real log handler was attached.

Tests: 8/8 in test_logging_config.py pass. Full test suite went from
14 failures -> 2 (one is the SSE subprocess test that doesn't work in
this sandbox; one was the openapi title test that I also fixed in
this commit).

Documentation: DEVELOPMENT.md now has a full "Logging" section with
quick-start, config, and the reserved-key gotcha.
2026-07-02 20:55:41 +02:00
8d25702eca chore(license): re-license to dual MIT (core) + BSL 1.1 (stealth)
Squashed from chore/license-relicense. Full message preserved in the
original branch commit bb77eb5. See ADR-0002 for the decision rationale.

Refs: ADR-0002, commit bb77eb5
2026-07-02 19:59:18 +02:00
47ba268131 docs: apply fleet-template (16-artifact scaffold)
Adds missing standard artifacts:
- README.md (if missing)
- AGENTS.md (AI agent contract)
- PLAN.md (current sprint)
- STATUS.md (where we are)
- DEVELOPMENT.md (dev workflow)
- DEPLOYMENT.md (deploy procedure)
- TESTING.md (test strategy)
- DECISIONS.md (ADR index + templates)
- .github/CODEOWNERS
- .github/workflows/ci.yml

Preserves all existing artifacts.

Refs: RugMunchMedia/fleet-template
2026-07-02 02:07:13 +07:00