Spaces:
Sleeping
Sleeping
fix(d1): decouple image capture from D1 push (PR #45) — unfreeze live feed
Browse files- README.md +10 -8
- sec_listener/db.py +5 -1
- sec_listener/worker.py +11 -4
README.md
CHANGED
|
@@ -18,23 +18,25 @@ rebuilds) and caches responses at the edge (CDN + stale-while-revalidate).
|
|
| 18 |
|
| 19 |
## What runs here
|
| 20 |
|
| 21 |
-
One process (`python -m sec_listener.worker`) = SEC listener + Markdown
|
| 22 |
-
+ FastAPI API on port **7860**, which Hugging Face exposes at the Space URL.
|
| 23 |
|
| 24 |
-
##
|
| 25 |
|
| 26 |
| Secret | Purpose |
|
| 27 |
|--------|---------|
|
| 28 |
| `SEC_API_KEY` | Key clients must send as the `X-API-Key` header. Set it, and use the same value in the frontend's `SEC_API_KEY`. |
|
|
|
|
| 29 |
|
| 30 |
## Endpoints
|
| 31 |
|
| 32 |
-
`/health` (open), `/api/ex10?page=&page_size=
|
| 33 |
-
`/api/ex10/{id}`, `/api/stats`, `/api/search?q=`.
|
| 34 |
|
| 35 |
## Notes
|
| 36 |
|
| 37 |
-
- Ships with a seed database for instant content; new filings stream in
|
| 38 |
-
|
| 39 |
-
|
|
|
|
| 40 |
- Public SEC filing data only. Not legal/investment advice. Not affiliated with the SEC.
|
|
|
|
| 18 |
|
| 19 |
## What runs here
|
| 20 |
|
| 21 |
+
One process (`python -m sec_listener.worker`) = SEC listener + Markdown / filing-header /
|
| 22 |
+
image backfill + FastAPI API on port **7860**, which Hugging Face exposes at the Space URL.
|
| 23 |
|
| 24 |
+
## Space secrets
|
| 25 |
|
| 26 |
| Secret | Purpose |
|
| 27 |
|--------|---------|
|
| 28 |
| `SEC_API_KEY` | Key clients must send as the `X-API-Key` header. Set it, and use the same value in the frontend's `SEC_API_KEY`. |
|
| 29 |
+
| `HF_TOKEN` | Enables the parallel HF-dataset mirror, cold-start boot-restore, and scanned-exhibit image capture. |
|
| 30 |
|
| 31 |
## Endpoints
|
| 32 |
|
| 33 |
+
`/health` (open), `/api/ex10?page=&page_size=&form=&cik=&filer=&sort=`,
|
| 34 |
+
`/api/ex10/since?seconds=`, `/api/ex10/{id}`, `/api/facets`, `/api/stats`, `/api/search?q=`.
|
| 35 |
|
| 36 |
## Notes
|
| 37 |
|
| 38 |
+
- Ships with a seed database for instant content; new filings stream in continuously.
|
| 39 |
+
- HF Spaces have **no persistent disk**, so durability comes from the HF dataset mirror:
|
| 40 |
+
on boot the worker restores SQLite from `arthrod/sec-ex10-exhibits`
|
| 41 |
+
(`python -m sec_listener.boot_restore`) and re-mirrors every `SEC_HF_SYNC_INTERVAL` seconds.
|
| 42 |
- Public SEC filing data only. Not legal/investment advice. Not affiliated with the SEC.
|
sec_listener/db.py
CHANGED
|
@@ -200,9 +200,13 @@ class Database:
|
|
| 200 |
conn.commit()
|
| 201 |
|
| 202 |
def update_image_urls(self, exhibit_id: int, urls: list[str]) -> None:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
with self.connect() as conn:
|
| 204 |
conn.execute(
|
| 205 |
-
"UPDATE ex10_exhibits SET image_urls = ? WHERE id = ?",
|
| 206 |
(json.dumps(urls), exhibit_id),
|
| 207 |
)
|
| 208 |
conn.commit()
|
|
|
|
| 200 |
conn.commit()
|
| 201 |
|
| 202 |
def update_image_urls(self, exhibit_id: int, urls: list[str]) -> None:
|
| 203 |
+
# Reset mirrored so a row that was already pushed to D1 on markdown+metadata
|
| 204 |
+
# (image capture is decoupled from the push) is re-queued for a follow-up push,
|
| 205 |
+
# propagating the freshly captured image_urls via the ingest upsert. A row not
|
| 206 |
+
# yet mirrored is unaffected (mirrored stays 0).
|
| 207 |
with self.connect() as conn:
|
| 208 |
conn.execute(
|
| 209 |
+
"UPDATE ex10_exhibits SET image_urls = ?, mirrored = 0 WHERE id = ?",
|
| 210 |
(json.dumps(urls), exhibit_id),
|
| 211 |
)
|
| 212 |
conn.commit()
|
sec_listener/worker.py
CHANGED
|
@@ -199,12 +199,16 @@ class BackfillWorker:
|
|
| 199 |
|
| 200 |
|
| 201 |
async def _d1_push_loop(db: Database, *, url: str, key: str, interval: float,
|
| 202 |
-
require_images: bool =
|
| 203 |
"""Periodically push *finalized* SQLite rows to D1 via the ingest route.
|
| 204 |
|
| 205 |
SQLite stays the working buffer; D1 is authoritative. Failures here never
|
| 206 |
disturb the listener/backfill — rows stay unmirrored and retry next cycle.
|
| 207 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
"""
|
| 209 |
from .d1_sync import push_finalized
|
| 210 |
|
|
@@ -236,11 +240,14 @@ async def _run_all(config: Config):
|
|
| 236 |
|
| 237 |
# Push finalized rows to D1 (authoritative store) via the ingest route —
|
| 238 |
# gated on the shared SEC_API_KEY. Without it, SQLite is the sole store.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
if config.api_key:
|
| 240 |
tasks.append(asyncio.create_task(
|
| 241 |
_d1_push_loop(db, url=config.d1_ingest_url, key=config.api_key,
|
| 242 |
-
interval=float(os.environ.get("SEC_D1_PUSH_INTERVAL", "60")),
|
| 243 |
-
require_images=bool(os.environ.get("HF_TOKEN"))),
|
| 244 |
name="d1-push",
|
| 245 |
))
|
| 246 |
|
|
|
|
| 199 |
|
| 200 |
|
| 201 |
async def _d1_push_loop(db: Database, *, url: str, key: str, interval: float,
|
| 202 |
+
require_images: bool = False, batch: int = 100):
|
| 203 |
"""Periodically push *finalized* SQLite rows to D1 via the ingest route.
|
| 204 |
|
| 205 |
SQLite stays the working buffer; D1 is authoritative. Failures here never
|
| 206 |
disturb the listener/backfill — rows stay unmirrored and retry next cycle.
|
| 207 |
+
|
| 208 |
+
Image capture is **decoupled** from the push: a row finalizes (and pushes) on
|
| 209 |
+
markdown + metadata alone, so a slow/stalled image backfill never freezes the
|
| 210 |
+
live feed. When images are captured later, ``update_image_urls`` re-queues the
|
| 211 |
+
row so a follow-up push propagates them to D1 via the ingest upsert.
|
| 212 |
"""
|
| 213 |
from .d1_sync import push_finalized
|
| 214 |
|
|
|
|
| 240 |
|
| 241 |
# Push finalized rows to D1 (authoritative store) via the ingest route —
|
| 242 |
# gated on the shared SEC_API_KEY. Without it, SQLite is the sole store.
|
| 243 |
+
# Image capture is decoupled from the push (require_images=False default): rows
|
| 244 |
+
# reach D1 on markdown+metadata, and re-push once images arrive (see
|
| 245 |
+
# update_image_urls / _d1_push_loop). This keeps the live feed flowing even when
|
| 246 |
+
# the image backfill is slow or stalled.
|
| 247 |
if config.api_key:
|
| 248 |
tasks.append(asyncio.create_task(
|
| 249 |
_d1_push_loop(db, url=config.d1_ingest_url, key=config.api_key,
|
| 250 |
+
interval=float(os.environ.get("SEC_D1_PUSH_INTERVAL", "60"))),
|
|
|
|
| 251 |
name="d1-push",
|
| 252 |
))
|
| 253 |
|