keble-scraper-api 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. keble_scraper_api-0.2.0/.gitignore +16 -0
  2. keble_scraper_api-0.2.0/AiAgentChangesLogs.md +173 -0
  3. keble_scraper_api-0.2.0/PKG-INFO +160 -0
  4. keble_scraper_api-0.2.0/README.md +136 -0
  5. keble_scraper_api-0.2.0/pyproject.toml +48 -0
  6. keble_scraper_api-0.2.0/scripts/__init__.py +1 -0
  7. keble_scraper_api-0.2.0/scripts/reset_pre_release_state.py +9 -0
  8. keble_scraper_api-0.2.0/src/keble_scraper_api/__init__.py +3 -0
  9. keble_scraper_api-0.2.0/src/keble_scraper_api/api/dependencies.py +100 -0
  10. keble_scraper_api-0.2.0/src/keble_scraper_api/api/errors.py +72 -0
  11. keble_scraper_api-0.2.0/src/keble_scraper_api/api/router.py +473 -0
  12. keble_scraper_api-0.2.0/src/keble_scraper_api/api/schemas.py +26 -0
  13. keble_scraper_api-0.2.0/src/keble_scraper_api/application/admin.py +285 -0
  14. keble_scraper_api-0.2.0/src/keble_scraper_api/application/artifacts.py +393 -0
  15. keble_scraper_api-0.2.0/src/keble_scraper_api/application/auth.py +324 -0
  16. keble_scraper_api-0.2.0/src/keble_scraper_api/application/failures.py +427 -0
  17. keble_scraper_api-0.2.0/src/keble_scraper_api/application/fetching.py +72 -0
  18. keble_scraper_api-0.2.0/src/keble_scraper_api/application/jobs.py +891 -0
  19. keble_scraper_api-0.2.0/src/keble_scraper_api/application/proxy_health.py +126 -0
  20. keble_scraper_api-0.2.0/src/keble_scraper_api/application/scheduling.py +24 -0
  21. keble_scraper_api-0.2.0/src/keble_scraper_api/artifact_namespace.py +76 -0
  22. keble_scraper_api-0.2.0/src/keble_scraper_api/container.py +361 -0
  23. keble_scraper_api-0.2.0/src/keble_scraper_api/errors.py +25 -0
  24. keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/artifact_store.py +585 -0
  25. keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/callbacks.py +285 -0
  26. keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/celery_factory.py +63 -0
  27. keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/cloudflare.py +259 -0
  28. keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/credentials.py +75 -0
  29. keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/dispatcher.py +57 -0
  30. keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/http_fetcher.py +351 -0
  31. keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/mongo.py +2345 -0
  32. keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/proxy_router.py +460 -0
  33. keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/request_profiles.py +220 -0
  34. keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/url_policy.py +341 -0
  35. keble_scraper_api-0.2.0/src/keble_scraper_api/main.py +162 -0
  36. keble_scraper_api-0.2.0/src/keble_scraper_api/maintenance/__init__.py +1 -0
  37. keble_scraper_api-0.2.0/src/keble_scraper_api/maintenance/reset_pre_release_state.py +743 -0
  38. keble_scraper_api-0.2.0/src/keble_scraper_api/models.py +473 -0
  39. keble_scraper_api-0.2.0/src/keble_scraper_api/protocols.py +947 -0
  40. keble_scraper_api-0.2.0/src/keble_scraper_api/settings.py +484 -0
  41. keble_scraper_api-0.2.0/src/keble_scraper_api/telemetry.py +109 -0
  42. keble_scraper_api-0.2.0/src/keble_scraper_api/workers/celery_app.py +48 -0
  43. keble_scraper_api-0.2.0/src/keble_scraper_api/workers/cli.py +106 -0
  44. keble_scraper_api-0.2.0/src/keble_scraper_api/workers/runtime.py +78 -0
  45. keble_scraper_api-0.2.0/src/keble_scraper_api/workers/tasks.py +97 -0
  46. keble_scraper_api-0.2.0/tests/integration/test_api.py +350 -0
  47. keble_scraper_api-0.2.0/tests/integration/test_mongo_repository.py +2071 -0
  48. keble_scraper_api-0.2.0/tests/live/test_live_providers.py +543 -0
  49. keble_scraper_api-0.2.0/tests/unit/scripts/test_reset_pre_release_state.py +710 -0
  50. keble_scraper_api-0.2.0/tests/unit/test_artifacts.py +674 -0
  51. keble_scraper_api-0.2.0/tests/unit/test_auth_and_callbacks.py +689 -0
  52. keble_scraper_api-0.2.0/tests/unit/test_celery.py +95 -0
  53. keble_scraper_api-0.2.0/tests/unit/test_cloudflare.py +258 -0
  54. keble_scraper_api-0.2.0/tests/unit/test_http_fetcher.py +425 -0
  55. keble_scraper_api-0.2.0/tests/unit/test_proxy_health.py +207 -0
  56. keble_scraper_api-0.2.0/tests/unit/test_proxy_router.py +423 -0
  57. keble_scraper_api-0.2.0/tests/unit/test_release_workflow.py +73 -0
  58. keble_scraper_api-0.2.0/tests/unit/test_request_profiles.py +141 -0
  59. keble_scraper_api-0.2.0/tests/unit/test_scheduling.py +43 -0
  60. keble_scraper_api-0.2.0/tests/unit/test_settings.py +354 -0
  61. keble_scraper_api-0.2.0/tests/unit/test_telemetry.py +65 -0
  62. keble_scraper_api-0.2.0/tests/unit/test_url_policy.py +139 -0
  63. keble_scraper_api-0.2.0/tests/unit/test_version.py +26 -0
@@ -0,0 +1,16 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .pyright/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ .env
11
+ .env.*
12
+ !.env.example
13
+ frontend/node_modules/
14
+ frontend/dist/
15
+ frontend/playwright-report/
16
+ frontend/test-results/
@@ -0,0 +1,173 @@
1
+ # AI Agent Change Log
2
+
3
+ ## 2026-07-19 20:12 CST
4
+ State: `fix/review2-scraper-recovery`; API `0.2.0` and contract `0.3.0` release
5
+ candidates after the verified local clean break.
6
+
7
+ zma: Continue WP8 through immutable dependency-first releases and final main
8
+ integration.
9
+
10
+ Ai: Advanced API metadata to consume the mandatory retention-revision contract
11
+ and Data Infra `0.9.0`; the runtime includes typed failures, bounded history,
12
+ namespace ownership fencing, and audited repair tooling. Tag/publication remain
13
+ behind full portable, real-Mongo, Pyright, build, clean-install, and image gates.
14
+
15
+ ## 2026-07-19 19:35 CST
16
+ State: `fix/review2-scraper-recovery`; WP8 guarded namespace-reset tooling is
17
+ verified on the topic branch; local execution and coordinated 0.2.0 release remain open.
18
+
19
+ zma:
20
+ - Continue the finalized REVIEW2 plan through audited clean break, complete
21
+ releases, main integration, and runtime proof.
22
+
23
+ Ai:
24
+ - Promoted one canonical artifact-store selector for API/workers/maintenance and
25
+ added a DRY_RUN-first local/test clean-break service plus CLI. APPLY requires
26
+ exact writer, database, Redis, artifact owner, backup/inventory, campaign, and
27
+ audit evidence; it deletes streamed binary keys before fixed Mongo metadata
28
+ and then only the configured Redis namespace.
29
+ - Added five focused tests for authorization, count-only inspection, stable key
30
+ streaming, dependency ordering, replay idempotency, binary-failure metadata
31
+ preservation, adjacent namespace survival, and audit promotion. Ruff, strict
32
+ Pyright, and changed-symbol docstring/prohibited-pattern audits pass.
33
+
34
+ ## 2026-07-17 21:59 CST
35
+ State: `fix/review2-scraper-recovery`; API `0.1.2` consumes contract `0.2.0`
36
+ while its coordinated `0.2.0` release work remains open.
37
+
38
+ zma: Implement all finalized REVIEW2 scraper/provider recovery work without
39
+ raw error strings, false payment inference, unbounded paid retry, or incomplete
40
+ test/release claims.
41
+
42
+ Ai: Moved the API-local error enum to the shared contract, added one total
43
+ fetch/domain-to-failure mapper, and changed append-only attempts, terminal job
44
+ snapshots, Mongo transitions, and callbacks to store typed evidence. Retry
45
+ selection now consumes the failure disposition and dead-letter snapshots retain
46
+ the upstream cause while marking bounded-attempt exhaustion. HTTP envelope,
47
+ lease recovery, Sentry, retention, Shopify adaptation, and the API version/tag
48
+ remain explicitly pending.
49
+
50
+ ## 2026-07-15 19:07 CST
51
+ State: `fix/artifact-access-evidence`; scraper API 0.1.2 remains unchanged.
52
+
53
+ zma: Continue the Shopify rollout audit by proving private object presence,
54
+ retention, and exact reconciliation between object-store reads and Mongo audit
55
+ events without enabling provider traffic.
56
+
57
+ Ai: Added a server-owned audit UUID to every signed OSS/local artifact URL and
58
+ matching immutable audit event, persisted SHA-256 user metadata on new OSS
59
+ objects, and added offline coverage for metadata, signature, local URL, and
60
+ end-to-end service-download correlation. This creates the source contract used
61
+ by typed rollout object-storage evidence; no credential or provider call is
62
+ introduced.
63
+
64
+ ## 2026-07-15 13:18 CST
65
+ State: `feature/local-shared-artifacts`; scraper API 0.1.2 remains unchanged.
66
+
67
+ zma: Support the isolated local ten-store Shopify run with maintainable shared
68
+ artifacts, safe Mac network egress, and correct worker rate behavior.
69
+
70
+ Ai: Added typed development-only local artifact and proxy/DoH settings/adapters,
71
+ kept SSRF validation and TLS/IP pinning intact, and changed Redis route
72
+ admission to reserve asynchronously awaited future timestamps. Unit coverage
73
+ now proves configuration fail-closed behavior, content integrity, DoH parsing,
74
+ direct-proxy trust, and burst pacing without route changes.
75
+
76
+ ## 2026-07-15 10:49 CST
77
+ State: `fix/node24-image-workflows`; API `0.1.2` source is unchanged.
78
+
79
+ zma: Continue the release audit and remove avoidable workflow warnings.
80
+
81
+ Ai: Upgraded the API image publication actions to reviewed Node 24 commits and
82
+ made portable workflow tests reject any action-pin drift.
83
+
84
+ ## 2026-07-15 10:11 CST
85
+ State: `feature/immutable-image-publishing`; API `0.1.2` remains unchanged while
86
+ its release image receives an immutable GHCR publication path.
87
+
88
+ zma: Continue the full rollout audit and close the missing registry/digest gate.
89
+
90
+ Ai: Added annotated-tag/version workflow guards and portable tests proving the
91
+ API image has source-SHA identity, provenance, SBOM, and retained digest evidence.
92
+
93
+ ## 2026-07-15 02:44
94
+ State: v0.1.0 source on `feature/shopify-scraping-stack`; release pending workspace verification.
95
+
96
+ zma: Implement the generalized asynchronous scraper backend, workers, proxy/cost/artifact controls, authentication, and complete test layers required by the Shopify infrastructure plan.
97
+
98
+ Ai: Added typed application ports, secure transport adapters, ACID Mongo lifecycle/outbox and retention workflows, Redis budgets, Celery queue/beat composition, private OSS, signed callbacks, Truth/Google sessions, Sentry, API routes, and deterministic unit/integration/live coverage.
99
+
100
+ ## 2026-07-15 03:39
101
+ State: v0.1.0 source on `feature/shopify-scraping-stack`; Shopify service artifact consumption is implemented and release remains pending the full-stack audit.
102
+
103
+ zma: Continue the complete plan with a usable Shopify-to-scraper artifact contract, additive retention, and full tests.
104
+
105
+ Ai: Added audited service download/pin routes, pin-strengthening ACID behavior, complete public Storefront detail/variant/media query fields, typed validation errors, and unit/integration regressions.
106
+
107
+ ## 2026-07-15 05:50
108
+ State: v0.1.0 source on `feature/shopify-scraping-stack`; safe artifact metadata lookup now completes the Shopify consumer contract.
109
+
110
+ zma: Close the remaining scraper-to-Shopify artifact contract before continuing data-infra integration.
111
+
112
+ Ai: Added a service-authenticated metadata endpoint and application read that returns `ScrapeArtifactRef` only, without granting object bytes; expanded API documentation and offline/integration regressions.
113
+
114
+ ## 2026-07-15 07:42 CST
115
+ State: API `0.1.1` patch on `fix/conservative-bright-cost`; contract remains
116
+ `0.1.0` and the immutable initial tags are retained.
117
+
118
+ zma: Complete the cost-effective Shopify infrastructure without allowing
119
+ temporary vendor promotions to understate proxy spend.
120
+
121
+ Ai: Changed the default Bright reservation from the temporary USD 4/GB coupon
122
+ rate to the official undiscounted USD 8/GB pay-as-you-go baseline; synchronized
123
+ package/OpenAPI/import/Sentry versions, kept Cloudflare at its documented USD
124
+ 0.09/browser-hour rate, rejected zero unit prices, and added price/version tests.
125
+
126
+ ## 2026-07-15 07:58 CST
127
+ State: API `0.1.2` on `fix/docker-build-timeouts`; contract remains `0.1.0`.
128
+
129
+ zma: Finish the infrastructure with exact image proof and no concealed build
130
+ failure.
131
+
132
+ Ai: Added reusable BuildKit `uv` download caches and a 120-second bounded
133
+ timeout to both frozen install stages after two different 30-second PyPI
134
+ timeouts; kept every dependency locked and synchronized release identity.
135
+
136
+ ## 2026-07-16 15:03 CST
137
+ State: `fix/blank-optional-artifact-env`; API version remains unchanged.
138
+
139
+ zma: Make the local scalable Shopify preload runtime use mandatory private OSS
140
+ without retaining obsolete local-artifact settings from an env file.
141
+
142
+ Ai: Added a narrow typed validator that treats blank optional local-artifact
143
+ directory/base-URL values as absent and a unit regression proving a complete
144
+ private OSS backend remains the sole configured artifact store.
145
+
146
+ Ai follow-up: Bound each Mongo database to one persisted object-key namespace
147
+ at startup and reject pre-existing or later prefix drift before job execution,
148
+ closing the content-ID/object-key collision found by the staged Shopify run.
149
+
150
+ ## 2026-07-19 02:36 CST
151
+ State: `fix/review2-scraper-recovery`; API version remains unchanged while
152
+ R2-B-M1 awaits the coordinated REVIEW2 release train.
153
+
154
+ zma: Continue the finalized REVIEW2 plan and replace the unsafe prefix-only
155
+ artifact fence with a source-proven, typed, ACID-safe ownership contract.
156
+
157
+ Ai: Added complete backend/location/prefix identity persistence and a stable
158
+ owner canary in OSS/local storage, rejected legacy rows and all identity drift
159
+ before readiness, and covered cross-database ownership plus backend/bucket
160
+ swaps with offline and isolated real-Mongo regressions.
161
+
162
+ ## 2026-07-19 05:56 CST
163
+ State: `fix/review2-scraper-recovery`; API version remains unchanged while
164
+ R2-B-M11 awaits coordinated Shopify delivery and WP8 release.
165
+
166
+ zma: Continue the finalized REVIEW2 plan with ACID artifact-retention recovery
167
+ and comprehensive edge coverage.
168
+
169
+ Ai: Replaced expiry-strengthening-only pins with monotonic revision-fenced
170
+ policy synchronization. The transaction increments references once, applies
171
+ newer finite or indefinite expiry, ignores stale delivery, accepts exact replay,
172
+ and rejects same-revision policy drift; service audit now records the accepted
173
+ retention revision.
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: keble-scraper-api
3
+ Version: 0.2.0
4
+ Summary: Asynchronous policy-bounded scraping API, workers, proxy router, and artifact service.
5
+ Author-email: zma <bob0103779@gmail.com>
6
+ Requires-Python: <3.14,>=3.13
7
+ Requires-Dist: aiohttp<4,>=3.11
8
+ Requires-Dist: celery<6,>=5.6
9
+ Requires-Dist: cryptography<47,>=45
10
+ Requires-Dist: fastapi<1,>=0.116
11
+ Requires-Dist: google-auth<3,>=2.40
12
+ Requires-Dist: httpx<1,>=0.28
13
+ Requires-Dist: keble-data-infra-contract<1,>=0.9.0
14
+ Requires-Dist: keble-helpers<2,>=1.54.0
15
+ Requires-Dist: keble-scraper-contract==0.3.0
16
+ Requires-Dist: nh3<1,>=0.3
17
+ Requires-Dist: oss2<3,>=2.19
18
+ Requires-Dist: pydantic-settings<3,>=2.10
19
+ Requires-Dist: pymongo<5,>=4.12
20
+ Requires-Dist: redis<7,>=5
21
+ Requires-Dist: sentry-sdk[celery,fastapi]<3,>=2.32
22
+ Requires-Dist: uvicorn[standard]<1,>=0.35
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Keble Scraper API
26
+
27
+ The API package owns FastAPI composition, Mongo lifecycle/index persistence,
28
+ Redis-backed egress budgets and sessions, Celery workers/beat, secure HTTP and
29
+ Cloudflare fetching, private OSS artifacts, HMAC callbacks, authentication,
30
+ auditing, and Sentry instrumentation. It imports `keble-scraper-contract`; the
31
+ contract never imports this package.
32
+
33
+ Version `0.2.0` retains the undiscounted Bright pay-as-you-go baseline for
34
+ pre-dispatch cost reservation so temporary coupon pricing cannot understate
35
+ production spend and releases typed provider recovery, crash-safe callback and
36
+ job budgets, bounded history, artifact-namespace ownership, ordered retention,
37
+ and the guarded first-release reset.
38
+
39
+ The workspace consumes `keble-scraper-contract 0.3.0` as the foundation for
40
+ the API `0.2.0` recovery release. Attempt/job/callback
41
+ persistence stores discriminated typed failure snapshots instead of terminal
42
+ strings, and retry classification uses their finite dispositions. Public HTTP
43
+ errors use the same envelope; mandatory retention revisions prevent delayed pin
44
+ delivery from reversing a newer paid-storage policy.
45
+
46
+ Each job and callback row owns its immutable attempt budget;
47
+ jobs also own a fixed deadline. Lease acquisition and recovery enforce those
48
+ stored values, temporary capacity retains its future horizon, and unexpected
49
+ runtime faults remain leased for expiry recovery rather than becoming a false
50
+ network failure. Cache pointers cannot outlive referenced artifacts, missing
51
+ cache metadata invalidates the pointer before refetch, and expired callback
52
+ leases are recovered or dead-lettered by maintenance.
53
+
54
+ The WP8 clean-break command lives at `scripts/reset_pre_release_state.py` and
55
+ delegates to the package-owned typed maintenance module. DRY_RUN is the default.
56
+ Local/test-only APPLY deletes referenced binaries before their Mongo metadata,
57
+ then the fixed Scraper collection allowlist and exact Redis namespace. It
58
+ requires explicit writer-stop, database, Redis, artifact-owner, snapshot,
59
+ artifact-inventory, campaign, and immutable-audit evidence; failures propagate
60
+ with metadata or RUNNING evidence retained for replay. See `../docs/RUNBOOK.md`.
61
+
62
+ HTTP, Cloudflare, and registered callback transports own reusable connection
63
+ pools closed by `ScraperContainer`. The shared Data Infra Retry-After parser is
64
+ used at every response boundary, with durable due times rounded upward to Mongo
65
+ millisecond precision. Maintenance projects due job ID plus transport policy in
66
+ one indexed read and publishes with configured bounded concurrency.
67
+
68
+ Maintenance also owns bounded historical retirement. Terminal Jobs without a
69
+ callback receive an absolute retention clock immediately; callback Jobs receive
70
+ one only in the same transaction that marks their outbox event delivered.
71
+ Pending, leased, and dead-letter outbox rows therefore cannot be purged. One
72
+ Mongo singleton lease serializes each minute's bounded purge, and the majority
73
+ transaction deletes each expired Job with all immutable Attempt children before
74
+ reporting success. Delivered callbacks and audit facts use independent absolute
75
+ clocks. Defaults are 90 days for Job/Attempt evidence, 30 days for delivered
76
+ callback replay, 400 days for security audit, and 1,000 rows per collection per
77
+ minute; every value is a typed `SCRAPER_*` environment setting.
78
+
79
+ Sentry is initialized explicitly for the API, Celery parent, every worker child,
80
+ and beat. Runtime tags distinguish those processes while credentials, URLs,
81
+ request bodies, and provider payloads stay out of telemetry configuration.
82
+
83
+ Side effects if changes:
84
+ - Shopify must repin and exhaustively map local versus upstream failures;
85
+ - frontend error presentation and OpenAPI generation ship with the API release;
86
+ - pre-production `0.1.x` scraper job rows require the guarded reset/repair step.
87
+
88
+ Runtime commands:
89
+
90
+ ```bash
91
+ uv run --package keble-scraper-api keble-scraper-api
92
+ uv run --package keble-scraper-api keble-scraper-worker fetch
93
+ uv run --package keble-scraper-api keble-scraper-worker render
94
+ uv run --package keble-scraper-api keble-scraper-worker proxy-health
95
+ uv run --package keble-scraper-api keble-scraper-worker maintenance
96
+ uv run --package keble-scraper-api keble-scraper-beat
97
+ ```
98
+
99
+ Service-authenticated consumers may download a verified artifact through the
100
+ audited signed-URL endpoint and synchronize a downstream retention pin with a
101
+ required positive `retentionRevision`. The Mongo transaction increments the
102
+ artifact reference only on first owner-pin creation, applies only newer policy
103
+ revisions, ignores stale deliveries, and rejects a divergent replay at the same
104
+ revision. Both finite archive expiry and indefinite latest retention are valid
105
+ ordered policies.
106
+ The operator and service paths share the same public download route but retain
107
+ separate authorization and audit identities.
108
+
109
+ The download service reserves a server UUID, places it in the immutable audit
110
+ event and the signed URL's `x-keble-audit-id` query, and returns the URL only
111
+ after the audit append succeeds. OSS `GetObject` logs can therefore reconcile
112
+ reads exactly. New OSS objects also carry their canonical SHA-256 as private
113
+ user metadata for bounded integrity checks.
114
+
115
+ For a multi-process local canary without OSS, configure both
116
+ `SCRAPER_LOCAL_ARTIFACT_DIRECTORY` and `SCRAPER_LOCAL_ARTIFACT_BASE_URL`, then
117
+ serve only that isolated directory on localhost. The development adapter keeps
118
+ content-addressed bytes shared between API and Celery workers; staging and
119
+ production reject it and continue to require private OSS.
120
+
121
+ Empty or whitespace-only values for those two optional local-artifact settings
122
+ are treated as absent. This supports explicit Compose overrides when private OSS
123
+ replaces a legacy local-artifact `env_file` configuration; all backend-completeness
124
+ and environment restrictions still apply.
125
+
126
+ The optional OSS endpoint, bucket, access-key ID, and access-key secret are also
127
+ normalized from blank to absent as one typed tuple. Partial or empty production
128
+ configuration fails startup before any external request or artifact write.
129
+
130
+ Startup now applies two independent artifact fences before readiness. Mongo
131
+ persists and compares the complete non-secret backend identity: backend kind,
132
+ deterministic prefix, OSS endpoint/bucket or resolved local directory, and the
133
+ configured owner. The binary store also contains one deterministic owner canary
134
+ inside that prefix. Existing rows without a complete marker, backend/location
135
+ changes, and a second owner all fail closed before workers accept jobs.
136
+
137
+ Set `SCRAPER_ARTIFACT_NAMESPACE_OWNER` in staging and production to a stable,
138
+ non-secret value unique to the Mongo deployment/database that owns the prefix,
139
+ for example `cn-hangzhou/mongo-primary/keble_scraper`. Do not reuse it for a
140
+ second database. Development/test may derive `environment/database` locally.
141
+ The pre-production prefix-only marker is intentionally incompatible and must be
142
+ cleared or explicitly migrated before this release line starts.
143
+
144
+ When a local VPN returns fake-IP DNS answers, configure both
145
+ `SCRAPER_DEVELOPMENT_HTTP_PROXY_URL` and
146
+ `SCRAPER_DEVELOPMENT_DNS_OVER_HTTPS_URL`. The worker validates real DoH A/AAAA
147
+ answers through the explicit proxy, preserves IP pinning, and applies the proxy
148
+ only to the verified `DIRECT` route. The pair is rejected outside
149
+ development/test.
150
+
151
+ Every route admission is atomically assigned a future Redis timestamp derived
152
+ from its requests-per-second policy. A fetch worker asynchronously waits for
153
+ that timestamp, preserving the requested route and job attempt; internal burst
154
+ pressure therefore does not become terminal `ROUTE_UNAVAILABLE` evidence or a
155
+ paid fallback.
156
+
157
+ The repository release workflow builds this package's runtime image only from
158
+ an annotated `keble-scraper-api-vX.Y.Z` tag whose version matches this package.
159
+ GHCR version/source tags are discovery aids; production consumes the digest
160
+ evidence artifact and pins `ghcr.io/keble-ai/keble-scraper-api@sha256:...`.
@@ -0,0 +1,136 @@
1
+ # Keble Scraper API
2
+
3
+ The API package owns FastAPI composition, Mongo lifecycle/index persistence,
4
+ Redis-backed egress budgets and sessions, Celery workers/beat, secure HTTP and
5
+ Cloudflare fetching, private OSS artifacts, HMAC callbacks, authentication,
6
+ auditing, and Sentry instrumentation. It imports `keble-scraper-contract`; the
7
+ contract never imports this package.
8
+
9
+ Version `0.2.0` retains the undiscounted Bright pay-as-you-go baseline for
10
+ pre-dispatch cost reservation so temporary coupon pricing cannot understate
11
+ production spend and releases typed provider recovery, crash-safe callback and
12
+ job budgets, bounded history, artifact-namespace ownership, ordered retention,
13
+ and the guarded first-release reset.
14
+
15
+ The workspace consumes `keble-scraper-contract 0.3.0` as the foundation for
16
+ the API `0.2.0` recovery release. Attempt/job/callback
17
+ persistence stores discriminated typed failure snapshots instead of terminal
18
+ strings, and retry classification uses their finite dispositions. Public HTTP
19
+ errors use the same envelope; mandatory retention revisions prevent delayed pin
20
+ delivery from reversing a newer paid-storage policy.
21
+
22
+ Each job and callback row owns its immutable attempt budget;
23
+ jobs also own a fixed deadline. Lease acquisition and recovery enforce those
24
+ stored values, temporary capacity retains its future horizon, and unexpected
25
+ runtime faults remain leased for expiry recovery rather than becoming a false
26
+ network failure. Cache pointers cannot outlive referenced artifacts, missing
27
+ cache metadata invalidates the pointer before refetch, and expired callback
28
+ leases are recovered or dead-lettered by maintenance.
29
+
30
+ The WP8 clean-break command lives at `scripts/reset_pre_release_state.py` and
31
+ delegates to the package-owned typed maintenance module. DRY_RUN is the default.
32
+ Local/test-only APPLY deletes referenced binaries before their Mongo metadata,
33
+ then the fixed Scraper collection allowlist and exact Redis namespace. It
34
+ requires explicit writer-stop, database, Redis, artifact-owner, snapshot,
35
+ artifact-inventory, campaign, and immutable-audit evidence; failures propagate
36
+ with metadata or RUNNING evidence retained for replay. See `../docs/RUNBOOK.md`.
37
+
38
+ HTTP, Cloudflare, and registered callback transports own reusable connection
39
+ pools closed by `ScraperContainer`. The shared Data Infra Retry-After parser is
40
+ used at every response boundary, with durable due times rounded upward to Mongo
41
+ millisecond precision. Maintenance projects due job ID plus transport policy in
42
+ one indexed read and publishes with configured bounded concurrency.
43
+
44
+ Maintenance also owns bounded historical retirement. Terminal Jobs without a
45
+ callback receive an absolute retention clock immediately; callback Jobs receive
46
+ one only in the same transaction that marks their outbox event delivered.
47
+ Pending, leased, and dead-letter outbox rows therefore cannot be purged. One
48
+ Mongo singleton lease serializes each minute's bounded purge, and the majority
49
+ transaction deletes each expired Job with all immutable Attempt children before
50
+ reporting success. Delivered callbacks and audit facts use independent absolute
51
+ clocks. Defaults are 90 days for Job/Attempt evidence, 30 days for delivered
52
+ callback replay, 400 days for security audit, and 1,000 rows per collection per
53
+ minute; every value is a typed `SCRAPER_*` environment setting.
54
+
55
+ Sentry is initialized explicitly for the API, Celery parent, every worker child,
56
+ and beat. Runtime tags distinguish those processes while credentials, URLs,
57
+ request bodies, and provider payloads stay out of telemetry configuration.
58
+
59
+ Side effects if changes:
60
+ - Shopify must repin and exhaustively map local versus upstream failures;
61
+ - frontend error presentation and OpenAPI generation ship with the API release;
62
+ - pre-production `0.1.x` scraper job rows require the guarded reset/repair step.
63
+
64
+ Runtime commands:
65
+
66
+ ```bash
67
+ uv run --package keble-scraper-api keble-scraper-api
68
+ uv run --package keble-scraper-api keble-scraper-worker fetch
69
+ uv run --package keble-scraper-api keble-scraper-worker render
70
+ uv run --package keble-scraper-api keble-scraper-worker proxy-health
71
+ uv run --package keble-scraper-api keble-scraper-worker maintenance
72
+ uv run --package keble-scraper-api keble-scraper-beat
73
+ ```
74
+
75
+ Service-authenticated consumers may download a verified artifact through the
76
+ audited signed-URL endpoint and synchronize a downstream retention pin with a
77
+ required positive `retentionRevision`. The Mongo transaction increments the
78
+ artifact reference only on first owner-pin creation, applies only newer policy
79
+ revisions, ignores stale deliveries, and rejects a divergent replay at the same
80
+ revision. Both finite archive expiry and indefinite latest retention are valid
81
+ ordered policies.
82
+ The operator and service paths share the same public download route but retain
83
+ separate authorization and audit identities.
84
+
85
+ The download service reserves a server UUID, places it in the immutable audit
86
+ event and the signed URL's `x-keble-audit-id` query, and returns the URL only
87
+ after the audit append succeeds. OSS `GetObject` logs can therefore reconcile
88
+ reads exactly. New OSS objects also carry their canonical SHA-256 as private
89
+ user metadata for bounded integrity checks.
90
+
91
+ For a multi-process local canary without OSS, configure both
92
+ `SCRAPER_LOCAL_ARTIFACT_DIRECTORY` and `SCRAPER_LOCAL_ARTIFACT_BASE_URL`, then
93
+ serve only that isolated directory on localhost. The development adapter keeps
94
+ content-addressed bytes shared between API and Celery workers; staging and
95
+ production reject it and continue to require private OSS.
96
+
97
+ Empty or whitespace-only values for those two optional local-artifact settings
98
+ are treated as absent. This supports explicit Compose overrides when private OSS
99
+ replaces a legacy local-artifact `env_file` configuration; all backend-completeness
100
+ and environment restrictions still apply.
101
+
102
+ The optional OSS endpoint, bucket, access-key ID, and access-key secret are also
103
+ normalized from blank to absent as one typed tuple. Partial or empty production
104
+ configuration fails startup before any external request or artifact write.
105
+
106
+ Startup now applies two independent artifact fences before readiness. Mongo
107
+ persists and compares the complete non-secret backend identity: backend kind,
108
+ deterministic prefix, OSS endpoint/bucket or resolved local directory, and the
109
+ configured owner. The binary store also contains one deterministic owner canary
110
+ inside that prefix. Existing rows without a complete marker, backend/location
111
+ changes, and a second owner all fail closed before workers accept jobs.
112
+
113
+ Set `SCRAPER_ARTIFACT_NAMESPACE_OWNER` in staging and production to a stable,
114
+ non-secret value unique to the Mongo deployment/database that owns the prefix,
115
+ for example `cn-hangzhou/mongo-primary/keble_scraper`. Do not reuse it for a
116
+ second database. Development/test may derive `environment/database` locally.
117
+ The pre-production prefix-only marker is intentionally incompatible and must be
118
+ cleared or explicitly migrated before this release line starts.
119
+
120
+ When a local VPN returns fake-IP DNS answers, configure both
121
+ `SCRAPER_DEVELOPMENT_HTTP_PROXY_URL` and
122
+ `SCRAPER_DEVELOPMENT_DNS_OVER_HTTPS_URL`. The worker validates real DoH A/AAAA
123
+ answers through the explicit proxy, preserves IP pinning, and applies the proxy
124
+ only to the verified `DIRECT` route. The pair is rejected outside
125
+ development/test.
126
+
127
+ Every route admission is atomically assigned a future Redis timestamp derived
128
+ from its requests-per-second policy. A fetch worker asynchronously waits for
129
+ that timestamp, preserving the requested route and job attempt; internal burst
130
+ pressure therefore does not become terminal `ROUTE_UNAVAILABLE` evidence or a
131
+ paid fallback.
132
+
133
+ The repository release workflow builds this package's runtime image only from
134
+ an annotated `keble-scraper-api-vX.Y.Z` tag whose version matches this package.
135
+ GHCR version/source tags are discovery aids; production consumes the digest
136
+ evidence artifact and pins `ghcr.io/keble-ai/keble-scraper-api@sha256:...`.
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "keble-scraper-api"
7
+ version = "0.2.0"
8
+ description = "Asynchronous policy-bounded scraping API, workers, proxy router, and artifact service."
9
+ readme = "README.md"
10
+ requires-python = ">=3.13,<3.14"
11
+ dependencies = [
12
+ "aiohttp>=3.11,<4",
13
+ "celery>=5.6,<6",
14
+ "cryptography>=45,<47",
15
+ "fastapi>=0.116,<1",
16
+ "google-auth>=2.40,<3",
17
+ "httpx>=0.28,<1",
18
+ "keble-data-infra-contract>=0.9.0,<1",
19
+ "keble-helpers>=1.54.0,<2",
20
+ "keble-scraper-contract==0.3.0",
21
+ "nh3>=0.3,<1",
22
+ "oss2>=2.19,<3",
23
+ "pydantic-settings>=2.10,<3",
24
+ "pymongo>=4.12,<5",
25
+ "redis>=5,<7",
26
+ "sentry-sdk[fastapi,celery]>=2.32,<3",
27
+ "uvicorn[standard]>=0.35,<1",
28
+ ]
29
+
30
+ [[project.authors]]
31
+ name = "zma"
32
+ email = "bob0103779@gmail.com"
33
+
34
+ [project.scripts]
35
+ keble-scraper-api = "keble_scraper_api.main:run"
36
+ keble-scraper-worker = "keble_scraper_api.workers.cli:run_worker"
37
+ keble-scraper-beat = "keble_scraper_api.workers.cli:run_beat"
38
+
39
+ [dependency-groups]
40
+ test = [
41
+ "fakeredis[lua]>=2.30,<3",
42
+ "pytest>=8.2,<9",
43
+ "pytest-asyncio>=0.23,<1",
44
+ "respx>=0.22,<1",
45
+ ]
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/keble_scraper_api"]
@@ -0,0 +1 @@
1
+ """Operator-only Scraper maintenance command modules."""
@@ -0,0 +1,9 @@
1
+ """Run the canonical guarded Scraper first-release reset command."""
2
+
3
+ from asyncio import run
4
+
5
+ from keble_scraper_api.maintenance.reset_pre_release_state import amain
6
+
7
+
8
+ if __name__ == "__main__":
9
+ run(amain())
@@ -0,0 +1,3 @@
1
+ """Keble scraper API, workers, persistence, and operations composition."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,100 @@
1
+ """Runtime container binding and shared request authorization dependencies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from uuid import uuid4
6
+
7
+ from fastapi import Request
8
+
9
+ from keble_scraper_contract import OperatorSession
10
+
11
+ from keble_scraper_api.container import ScraperContainer
12
+
13
+
14
+ class ContainerProvider:
15
+ """Bind one lifespan-created service graph to static route definitions.
16
+
17
+ Side effects if changes:
18
+ - every FastAPI dependency resolves process-owned services here.
19
+ """
20
+
21
+ def __init__(self) -> None:
22
+ """Initialize an intentionally unbound provider for OpenAPI creation.
23
+
24
+ Side effects if changes:
25
+ - pre-start request behavior remains fail-closed.
26
+ """
27
+
28
+ self._container: ScraperContainer | None = None
29
+
30
+ def bind(self, *, container: ScraperContainer) -> None:
31
+ """Install exactly one initialized process container.
32
+
33
+ Side effects if changes:
34
+ - route execution becomes available after lifespan startup.
35
+ """
36
+
37
+ if self._container is not None:
38
+ raise RuntimeError("scraper container is already bound")
39
+ self._container = container
40
+
41
+ def clear(self) -> None:
42
+ """Remove the binding before process resources close.
43
+
44
+ Side effects if changes:
45
+ - shutdown requests fail closed.
46
+ """
47
+
48
+ self._container = None
49
+
50
+ def require(self) -> ScraperContainer:
51
+ """Return initialized services or reject pre-start/post-shutdown access.
52
+
53
+ Side effects if changes:
54
+ - every API endpoint dependency.
55
+ """
56
+
57
+ if self._container is None:
58
+ raise RuntimeError("scraper runtime is not initialized")
59
+ return self._container
60
+
61
+
62
+ def request_id(*, request: Request) -> str:
63
+ """Use caller correlation metadata or generate a server request identity.
64
+
65
+ Side effects if changes:
66
+ - audit correlation; never job/idempotency identity.
67
+ """
68
+
69
+ return request.headers.get("x-request-id") or uuid4().hex
70
+
71
+
72
+ def authorize_service(*, request: Request, provider: ContainerProvider) -> str:
73
+ """Authenticate the single Shopify service audience with current/next keys.
74
+
75
+ Side effects if changes:
76
+ - job submit/read/cancel service access.
77
+ """
78
+
79
+ container = provider.require()
80
+ container.service_keys.authorize(
81
+ supplied=request.headers.get("x-service-key")
82
+ )
83
+ return container.settings.service_caller_identity
84
+
85
+
86
+ async def authorize_operator(
87
+ *,
88
+ request: Request,
89
+ provider: ContainerProvider,
90
+ ) -> OperatorSession:
91
+ """Resolve a backend-managed encrypted HttpOnly operator session.
92
+
93
+ Side effects if changes:
94
+ - every admin read/mutation authorization decision.
95
+ """
96
+
97
+ container = provider.require()
98
+ return await container.auth.session_from_cookie(
99
+ token=request.cookies.get(container.settings.session_cookie_name)
100
+ )