Files
AlphaPulse/backend/src/alphapulse/core/official_sources.py
T
eezd 89c729092c
Development CI / check-and-build (push) Successful in 2m54s
GitHub Git mirror / mirror (push) Successful in 9s
Development CI / deploy-development (push) Failing after 4m24s
Add resumable US policy news collectors and provenance
Collect official crypto and US policy documents with deterministic screening, full bodies and attachments, bounded retries, and durable history cursors. Preserve publication precision and source identity while keeping new collectors disabled.

Add real Congress, GovInfo, DOJ and Federal Register fixtures, offline capture replay, isolated database validation, and rollout instructions. Validate 982 backend tests, frontend checks, and 57 deployment control tests.
2026-11-04 21:41:04 +08:00

105 lines
5.6 KiB
Python

"""欧洲官方公开订阅和地域;新闻与日历共享来源身份,不共享运行状态。"""
import hashlib
import re
from dataclasses import dataclass
from datetime import UTC, datetime
from email.utils import parsedate_to_datetime
from urllib.parse import urlparse
from xml.etree import ElementTree as ET
EUROSTAT_NEWS = "https://ec.europa.eu/eurostat/news/euro-indicators"
EUROSTAT_FEED = EUROSTAT_NEWS + "?p_p_id=estatsearchportlet_WAR_estatsearchportlet_INSTANCE_OaTpFrwlabNK&p_p_lifecycle=2&p_p_resource_id=atom&_estatsearchportlet_WAR_estatsearchportlet_INSTANCE_OaTpFrwlabNK_pageSize=100&_estatsearchportlet_WAR_estatsearchportlet_INSTANCE_OaTpFrwlabNK_collection=CAT_PREREL"
ONS_FEED = "https://www.ons.gov.uk/releasecalendar?rss&highlight=true&limit=100&page=1&release-type=type-published&sort=date-newest"
FEEDS = {
"ecb": ("https://www.ecb.europa.eu/rss/press.html",),
"eurostat": (EUROSTAT_FEED,),
"boe": ("https://www.bankofengland.co.uk/rss/news", "https://www.bankofengland.co.uk/rss/speeches"),
"ons": (ONS_FEED,),
"destatis": ("https://www.destatis.de/SiteGlobals/Functions/RSSFeed/DE/RSSNewsfeed/Aktuell.xml",),
"insee": ("https://www.insee.fr/fr/flux/3",),
}
REGIONS = {"wscn_news": "CN", "wscn_live": "CN", "coindesk": "GLOBAL",
"bls": "US", "bea": "US", "fed": "US", "umich": "US", "dol": "US", "eia": "US", "adp": "US", "ecb": "EA", "eurostat": "EA", "boe": "GB", "ons": "GB", "destatis": "DE", "insee": "FR", "jin10": "GLOBAL"}
HOSTS = {"ecb": {"www.ecb.europa.eu"}, "eurostat": {"ec.europa.eu"}, "boe": {"www.bankofengland.co.uk"},
"ons": {"www.ons.gov.uk"}, "destatis": {"www.destatis.de"}, "insee": {"www.insee.fr"}}
LANGUAGES = {"destatis": "de", "insee": "fr"}
ATOM = "{http://www.w3.org/2005/Atom}"
@dataclass(frozen=True)
class Entry:
title: str
url: str
at: datetime
summary: str
updated: datetime | None = None
@property
def identity(self) -> str:
return hashlib.sha256(self.url.encode()).hexdigest()
def official_url(source: str, url: str) -> str:
parsed = urlparse(url)
if parsed.scheme != "https" or parsed.hostname not in HOSTS[source]:
raise ValueError(f"{source} 链接不是对应机构的官方 HTTPS 地址")
# ECB 的公开 RSS 链接带重复路径分隔符;身份按同一官方规范地址去重。
if source == "ecb":
url = parsed._replace(path=re.sub(r"/+", "/", parsed.path)).geturl()
return url
def feed_entries(body: bytes, source: str) -> list[Entry]:
try:
root = ET.fromstring(body)
except ET.ParseError as exc:
raise ValueError("官方订阅不是有效的 XML") from exc
if root.tag == "rss" and root.find("channel") is not None:
nodes = root.findall("./channel/item")
elif root.tag == ATOM + "feed":
nodes = root.findall(ATOM + "entry")
else:
raise ValueError("官方响应不是 RSS/Atom 订阅")
result = []
for node in nodes:
if node.tag == ATOM + "entry":
title = node.findtext(ATOM + "title", "").strip()
link = next((n for n in node.findall(ATOM + "link") if n.get("rel", "alternate") == "alternate"), None)
url = link.get("href", "") if link is not None else ""
at = datetime.fromisoformat(node.findtext(ATOM + "published") or node.findtext(ATOM + "updated") or "")
updated = node.findtext(ATOM + "updated")
summary = node.findtext(ATOM + "summary") or node.findtext(ATOM + "content") or ""
else:
title = node.findtext("title", "").strip()
url = node.findtext("link", "").strip()
at = parsedate_to_datetime(node.findtext("pubDate", ""))
updated = None
summary = node.findtext("description", "")
updated_at = datetime.fromisoformat(updated) if updated else None
if not title or at.tzinfo is None or (updated_at is not None and updated_at.tzinfo is None):
raise ValueError("官方订阅缺少标题或带时区的发布/修订时间")
result.append(Entry(title, official_url(source, url), at.astimezone(UTC), summary,
updated_at.astimezone(UTC) if updated_at else None))
return result
def relevant_news(source: str, title: str, url: str) -> bool:
title = title.casefold()
if source == "ecb":
return any(p in url for p in ("/key/", "/inter/", "/accounts/", "/pressconf/")) or "monetary policy decisions" in title
if source == "boe":
return "/speech/" in url or "/monetary-policy-summary-and-minutes/" in url or "bank rate" in title or "monetary policy" in title
if source == "eurostat":
return "inflation" in title or "unemployment" in title or bool(re.search(r"\bgdp\b.*(?:up|down|stable|growth|increased|decreased)|(?:growth|increase|decrease).*\bgdp\b", title))
if source == "ons":
return "time series" not in title and not url.endswith("timeseries") and bool(re.match(r"(?:consumer price inflation, uk:|labour market overview, uk:|uk labour market(?::|\s)|gdp first quarterly estimate, uk:|gdp quarterly national accounts, uk:)", title))
if source == "destatis":
return any(t in title for t in ("verbraucherpreis", "inflationsrate", "bruttoinlandsprodukt", "erwerbstätig", "erwerbslos"))
if source == "insee":
return any(t in title for t in ("prix à la consommation", "taux de chômage", "le pib", "le produit intérieur brut"))
raise ValueError(f"未支持的欧洲新闻来源:{source}")
# US adapters share the existing regional directory.
from alphapulse.core.us_news import SOURCES as US_SOURCES
REGIONS.update({code: "US" for code in US_SOURCES})