Initial import of Politrack, a self-hosted service that tracks Austrian politicians from federal to municipal level - roles, votes, money flows, statements, meetings, social media and legal proceedings - with every claim linked to its sources. - backend: FastAPI + Postgres/pgvector; adapter framework (fetch -> parse -> load -> enrich) with robots.txt/TDM-aware fetching, a raw-response archive for reparsing, and a claim/evidence model with corroboration - sources: Parliament, Wikidata, EU Parliament/Commission, Council of Ministers, Rechnungshof party reports and donations, procurement, RTR media transparency, political ad libraries, WKStA, news feeds, Wikipedia edits, Bluesky, Mastodon, public Telegram channels, YouTube and X (official API) - web: Next.js frontend in German and English with typed API client - mcp: MCP server for AI agents and RAG - docs: architecture, data model, sources, legal notes, Unraid deployment and contributor guides; Docker Compose setup for Unraid Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
75 lines
3.0 KiB
Python
75 lines
3.0 KiB
Python
"""Curated lists maintained by the operator in config/ (accounts that no official register lists).
|
|
|
|
* config.social_accounts - config/social_accounts.yaml -> social_account rows for persons/organisations,
|
|
e.g. party Telegram channels or Bluesky accounts linked from party websites. The social adapters then
|
|
collect their public posts.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Iterator
|
|
from typing import Any
|
|
|
|
import yaml
|
|
|
|
from politrack.config import get_settings
|
|
from politrack.core import Enricher, RunContext, SourceSpec, register
|
|
from politrack.core.records import OrgRef, PersonRef, SocialAccount
|
|
|
|
CURATED = SourceSpec(
|
|
slug="curated",
|
|
name="Kuratierte Listen (config/)",
|
|
kind="derived",
|
|
homepage="",
|
|
license="operator-maintained",
|
|
access="manual",
|
|
trust=60,
|
|
tos_notes="Entries maintained by the operator; each should name the public page that links the account.",
|
|
)
|
|
|
|
|
|
def person_ref(ctx: RunContext, entry: dict[str, Any]) -> PersonRef | None:
|
|
"""`person: <politrack slug>`, `person: {wikidata: Q..}` / `{parlament: ..}` or `name: <full name>`."""
|
|
person = entry.get("person")
|
|
if isinstance(person, dict):
|
|
return PersonRef(ids={k: str(v) for k, v in person.items()})
|
|
if isinstance(person, str):
|
|
row = ctx.conn.execute("SELECT id FROM person WHERE slug = %s", (person,)).fetchone()
|
|
return PersonRef(ids={"politrack": str(row["id"])}) if row else None
|
|
if entry.get("name"):
|
|
return PersonRef(name=entry["name"])
|
|
return None
|
|
|
|
|
|
def org_ref(entry: dict[str, Any]) -> OrgRef | None:
|
|
"""`org: <slug>` for existing organisations, or `org: {slug, name, kind}` to create one."""
|
|
org = entry.get("org")
|
|
if isinstance(org, str):
|
|
return OrgRef(slug=org, create=False)
|
|
if isinstance(org, dict):
|
|
return OrgRef(slug=org.get("slug"), name=org.get("name"), short_name=org.get("short_name"),
|
|
kind=org.get("kind", "other"), create=bool(org.get("name")))
|
|
return None
|
|
|
|
|
|
@register
|
|
class CuratedSocialAccounts(Enricher):
|
|
spec = CURATED
|
|
name = "config.social_accounts"
|
|
description = "Import social accounts from config/social_accounts.yaml"
|
|
schedule = "5 3 * * *"
|
|
|
|
def run(self, ctx: RunContext) -> Iterator[SocialAccount]:
|
|
path = get_settings().config_dir / "social_accounts.yaml"
|
|
if not path.exists():
|
|
return
|
|
for entry in (yaml.safe_load(path.read_text(encoding="utf-8")) or {}).get("accounts") or []:
|
|
person = person_ref(ctx, entry)
|
|
org = None if person else org_ref(entry)
|
|
if person is None and org is None:
|
|
ctx.stats["entries_without_owner"] += 1
|
|
ctx.log.warning("social_accounts.yaml: no person/org for %s", entry.get("url"))
|
|
continue
|
|
yield SocialAccount(platform=entry["platform"], url=entry["url"], handle=entry.get("handle"),
|
|
person=person, org=org, verified=bool(entry.get("verified")))
|