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.
- keble_scraper_api-0.2.0/.gitignore +16 -0
- keble_scraper_api-0.2.0/AiAgentChangesLogs.md +173 -0
- keble_scraper_api-0.2.0/PKG-INFO +160 -0
- keble_scraper_api-0.2.0/README.md +136 -0
- keble_scraper_api-0.2.0/pyproject.toml +48 -0
- keble_scraper_api-0.2.0/scripts/__init__.py +1 -0
- keble_scraper_api-0.2.0/scripts/reset_pre_release_state.py +9 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/__init__.py +3 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/api/dependencies.py +100 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/api/errors.py +72 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/api/router.py +473 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/api/schemas.py +26 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/application/admin.py +285 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/application/artifacts.py +393 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/application/auth.py +324 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/application/failures.py +427 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/application/fetching.py +72 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/application/jobs.py +891 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/application/proxy_health.py +126 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/application/scheduling.py +24 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/artifact_namespace.py +76 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/container.py +361 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/errors.py +25 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/artifact_store.py +585 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/callbacks.py +285 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/celery_factory.py +63 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/cloudflare.py +259 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/credentials.py +75 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/dispatcher.py +57 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/http_fetcher.py +351 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/mongo.py +2345 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/proxy_router.py +460 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/request_profiles.py +220 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/infrastructure/url_policy.py +341 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/main.py +162 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/maintenance/__init__.py +1 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/maintenance/reset_pre_release_state.py +743 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/models.py +473 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/protocols.py +947 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/settings.py +484 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/telemetry.py +109 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/workers/celery_app.py +48 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/workers/cli.py +106 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/workers/runtime.py +78 -0
- keble_scraper_api-0.2.0/src/keble_scraper_api/workers/tasks.py +97 -0
- keble_scraper_api-0.2.0/tests/integration/test_api.py +350 -0
- keble_scraper_api-0.2.0/tests/integration/test_mongo_repository.py +2071 -0
- keble_scraper_api-0.2.0/tests/live/test_live_providers.py +543 -0
- keble_scraper_api-0.2.0/tests/unit/scripts/test_reset_pre_release_state.py +710 -0
- keble_scraper_api-0.2.0/tests/unit/test_artifacts.py +674 -0
- keble_scraper_api-0.2.0/tests/unit/test_auth_and_callbacks.py +689 -0
- keble_scraper_api-0.2.0/tests/unit/test_celery.py +95 -0
- keble_scraper_api-0.2.0/tests/unit/test_cloudflare.py +258 -0
- keble_scraper_api-0.2.0/tests/unit/test_http_fetcher.py +425 -0
- keble_scraper_api-0.2.0/tests/unit/test_proxy_health.py +207 -0
- keble_scraper_api-0.2.0/tests/unit/test_proxy_router.py +423 -0
- keble_scraper_api-0.2.0/tests/unit/test_release_workflow.py +73 -0
- keble_scraper_api-0.2.0/tests/unit/test_request_profiles.py +141 -0
- keble_scraper_api-0.2.0/tests/unit/test_scheduling.py +43 -0
- keble_scraper_api-0.2.0/tests/unit/test_settings.py +354 -0
- keble_scraper_api-0.2.0/tests/unit/test_telemetry.py +65 -0
- keble_scraper_api-0.2.0/tests/unit/test_url_policy.py +139 -0
- keble_scraper_api-0.2.0/tests/unit/test_version.py +26 -0
|
@@ -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,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
|
+
)
|