Add authenticated global economic calendar with Jin10 fallback
Collect macroeconomic releases, events and holidays through the official browser SDK with resumable history from 2025. Preserve evidence, revisions and observation times, and merge verified series with official fields taking priority. Add category filters, field provenance, coverage and connection status. Verify calendar persistence and deployment recovery in isolated acceptance checks.
This commit is contained in:
1 parent
57424fd661
commit
e840557d22
35 files changed
+6828
-94
No files matched your search
@@ -0,0 +1,72 @@
|
||||
"""Keep provider identities, raw evidence, field observation times and revisions together."""
|
||||
from datetime import date, datetime
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.dialects.postgresql import insert
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from alphapulse.calendar.jin10 import Entry, day_url
|
||||
from alphapulse.calendar.service import record_revision
|
||||
from alphapulse.models import EconomicRelease
|
||||
from alphapulse.models.jin10 import Jin10SyncDay
|
||||
|
||||
|
||||
async def save_entry(session: AsyncSession, entry: Entry, raw_id: int, now: datetime) -> None:
|
||||
row = await session.scalar(select(EconomicRelease).where(
|
||||
EconomicRelease.source == "jin10", EconomicRelease.external_id == entry.identity,
|
||||
).with_for_update())
|
||||
values = {key: dict(value) for key, value in entry.values.items()}
|
||||
meta = values["_jin10"]
|
||||
key = meta["indicator"]
|
||||
if meta["kind"] == "data":
|
||||
current = values[key]
|
||||
old = row.values.get(key, {}) if row else {}
|
||||
observed = {}
|
||||
for field in ("actual", "previous", "forecast", "previous_revised"):
|
||||
if current[field] is not None:
|
||||
observed[field] = old.get("observed_at", {}).get(field, now.isoformat()) if current[field] == old.get(field) else now.isoformat()
|
||||
current["observed_at"] = observed
|
||||
forecast_at = datetime.fromisoformat(observed["forecast"]) if "forecast" in observed else None
|
||||
if forecast_at == now:
|
||||
before = current["actual"] is None
|
||||
else:
|
||||
before = old.get("forecast_before_release", False)
|
||||
current["forecast_before_release"] = bool(before and forecast_at and forecast_at < entry.at and meta["precision"] == "minute")
|
||||
received = meta["kind"] != "data" or values[key]["actual"] is not None
|
||||
if row is None:
|
||||
row = EconomicRelease(source="jin10", external_id=entry.identity, family="jin10", title=entry.title,
|
||||
scheduled_at=entry.at, schedule_status="scheduled", schedule_url=day_url(date.fromisoformat(meta["date"])),
|
||||
release_url=None, period=None, stage=None, schedule_checked_at=now, last_checked_at=now,
|
||||
first_received_at=now if received else None, next_check_at=now, error=None,
|
||||
values=values, revision_no=1)
|
||||
session.add(row)
|
||||
await session.flush()
|
||||
record_revision(session, row, raw_id)
|
||||
return
|
||||
changed = row.values != values or row.scheduled_at != entry.at or row.title != entry.title
|
||||
row.schedule_checked_at = row.last_checked_at = now
|
||||
row.error = None
|
||||
if row.first_received_at is None and received:
|
||||
row.first_received_at = now
|
||||
changed = True
|
||||
if changed:
|
||||
row.title, row.scheduled_at, row.values = entry.title, entry.at, values
|
||||
row.schedule_url = day_url(date.fromisoformat(meta["date"]))
|
||||
row.revision_no += 1
|
||||
record_revision(session, row, raw_id)
|
||||
|
||||
|
||||
async def mark_day(session: AsyncSession, day: date, counts: dict, now: datetime) -> None:
|
||||
stmt = insert(Jin10SyncDay).values(day=day, captured_at=now, counts=counts)
|
||||
await session.execute(stmt.on_conflict_do_update(index_elements=["day"], set_={"captured_at": now, "counts": counts}))
|
||||
|
||||
|
||||
async def next_history_day(session: AsyncSession, start: date, end: date) -> date | None:
|
||||
from datetime import timedelta
|
||||
covered = set(await session.scalars(select(Jin10SyncDay.day).where(Jin10SyncDay.day.between(start, end))))
|
||||
day = start
|
||||
while day <= end:
|
||||
if day not in covered:
|
||||
return day
|
||||
day += timedelta(days=1)
|
||||
return None
|
||||
Reference in new issue
Block a user