Add EP final roll-call votes of Austrian MEPs

Uses the European Parliament's open data (CC BY 4.0) rather than
HowTheyVote, whose dump is ODbL share-alike and behind a robots.txt block.
Also documents the raw archive ownership step for Unraid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
simonandClaude Opus 5.5 committed 2026-11-09 01:18:58 +01:00
1 parent 693c32a5e9
commit cd44d8fbba
14 files changed
+727 -2

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
# Golden fixtures are compared byte for byte; never convert their line endings.
backend/tests/fixtures/** -text
+122
View File
@@ -25,6 +25,8 @@ from politrack.core.records import (
PersonRecord,
PersonRef,
Role,
VoteEvent,
VotePosition,
)
from politrack.text import excerpt, fingerprint, normalize
@@ -191,3 +193,123 @@ class EuropeanParliament(Adapter):
evidence=[Evidence(url=url, doc_type="pdf", title=title, published_at=doc.get("document_date"))],
topics=("Transparenz und Korruption",),
)
LD_JSON = "application/ld+json"
EP_DECISIONS_PAGE = 500
EP_OUTCOME = {"ADOPTED": ("accepted", "angenommen", "adopted"), "REJECTED": ("rejected", "abgelehnt", "rejected")}
EP_VOTERS = (("had_voter_favor", "for", "Stimmte für", "Voted for"),
("had_voter_against", "against", "Stimmte gegen", "Voted against"),
("had_voter_abstention", "abstain", "Enthielt sich bei", "Abstained on"))
EP_INTENDED = (("had_voter_intended_favor", "for"), ("had_voter_intended_against", "against"),
("had_voter_intended_abstention", "abstain"))
def ep_label(value: Any) -> str:
"""German text of a multilingual EP field, falling back to English."""
text = (value.get("de") or value.get("en") or "") if isinstance(value, dict) else (value or "")
return re.sub(r"\s+", " ", str(text)).strip()
def ep_person_ids(values: list[str] | None) -> set[str]:
return {v.rsplit("/", 1)[-1] for v in values or []}
@register
class ParliamentVotes(Adapter):
"""Final roll-call votes (whole texts, no amendments) of the Austrian MEPs known to the database.
Amendment votes are left out on purpose: a sitting has over a hundred of them, which would bury
the final votes on a member's page. The official result counts; corrections MEPs file afterwards are
kept in `data.intended`, because they don't change the result.
"""
spec = EP_SPEC
name = "europarl.votes"
description = "Final roll-call votes of Austrian MEPs in the European Parliament plenary"
schedule = "45 5 * * *"
queue = "scrape"
def start(self, ctx: RunContext) -> Iterator[Request]:
ep_ids = []
if ctx.conn is not None:
ep_ids = sorted(row["value"] for row in ctx.conn.execute(
"SELECT value FROM identifier WHERE entity_type = 'person' AND scheme = 'ep'"))
if not ep_ids:
ctx.stats["no_meps_loaded"] = 1
return
for year in range(int(ctx.params.get("since_year", 2024)), date.today().year + 1):
yield Request(url=f"{EP_API}/meetings", kind="meetings", params={"year": year, "format": LD_JSON, "limit": 200},
meta={"ep_ids": ep_ids})
def parse(self, ctx: RunContext, response: Response) -> Iterator[Any]:
kind = response.request.kind
data = response.json().get("data") or []
meta = response.meta
if kind == "meetings":
today = response.fetched_at.date().isoformat()
for meeting in data: # the list includes future sittings, which have no results yet
if str(meeting.get("activity_date", "9999"))[:10] <= today:
yield Request(url=f"{EP_API}/meetings/{meeting['activity_id']}/vote-results", kind="results",
params={"format": LD_JSON, "limit": 500},
meta={"ep_ids": meta["ep_ids"], "meeting": meeting["activity_id"]})
elif kind == "results":
titles = {item["id"]: ep_label(item.get("activity_label")) for item in data if item.get("activity_date")}
if titles:
yield self.decisions_request(meta["meeting"], 0, meta["ep_ids"], titles)
else:
yield from self.parse_decisions(ctx, response, data)
@staticmethod
def decisions_request(meeting: str, offset: int, ep_ids: list[str], titles: dict[str, str]) -> Request:
return Request(url=f"{EP_API}/meetings/{meeting}/decisions", kind="decisions",
params={"format": LD_JSON, "limit": EP_DECISIONS_PAGE, "offset": offset},
meta={"ep_ids": ep_ids, "meeting": meeting, "offset": offset, "titles": titles})
def parse_decisions(self, ctx: RunContext, response: Response, data: list[dict]) -> Iterator[Any]:
meta = response.meta
austrian = set(meta["ep_ids"])
chamber = OrgRef(slug="europaeisches-parlament")
for decision in data:
if "ROLLCALL" not in str(decision.get("decision_method")) or "decisionAboutId" in decision:
ctx.stats["not_final_rollcall"] += 1
continue
voters = {position: ep_person_ids(decision.get(field)) & austrian for field, position, _, _ in EP_VOTERS}
if not any(voters.values()):
ctx.stats["no_austrian_voters"] += 1
continue
voted_on = date.fromisoformat(decision["activity_date"][:10])
label = ep_label(decision.get("activity_label"))
items = decision.get("inverse_consists_of") or [None]
title = meta["titles"].get(items[0]) or label
stage = ep_label(decision.get("referenceText"))
status = str(decision.get("decision_outcome", "")).rsplit("/", 1)[-1]
result, outcome_de, outcome_en = EP_OUTCOME.get(status, ("unknown", "Ergebnis unbekannt", "outcome unknown"))
counts = {"yes": decision.get("number_of_votes_favor"), "no": decision.get("number_of_votes_against"),
"abstained": decision.get("number_of_votes_abstention")}
tally = f"{counts['yes']} Ja, {counts['no']} Nein, {counts['abstained']} Enthaltungen"
url = f"https://data.europarl.europa.eu/{decision['id']}"
record = next(iter(decision.get("recorded_in_a_realization_of") or []), "")
pv = re.search(r"PV-(\d+)-(\d{4}-\d{2}-\d{2})-RCV", record)
protocol = (f"https://www.europarl.europa.eu/doceo/document/PV-{pv.group(1)}-{pv.group(2)}-RCV_DE.html"
if pv else None)
event_key = fingerprint("ep-vote", decision["id"])
yield VoteEvent(key=event_key, chamber=chamber, title=title, voted_on=voted_on, result=result, stage=stage,
item_ref=label.split(" – ", 1)[0], roll_call=True, url=url, protocol_url=protocol,
raw_text=f"{stage}: {tally}")
intended = {mep: position for field, position in EP_INTENDED for mep in ep_person_ids(decision.get(field))}
for _, position, verb_de, verb_en in EP_VOTERS:
for mep in sorted(voters[position]):
yield Claim(
claim_type="vote", person=PersonRef(ids={"ep": mep}), counterpart_org=chamber, occurred_at=voted_on,
summary_de=f"{verb_de}: {title} (Europäisches Parlament) – {outcome_de}",
summary_en=f"{verb_en}: {title} (European Parliament) – {outcome_en}",
data={"position": position, "result": result, "roll_call": True, "chamber": "EP", **counts,
**({"intended": intended[mep]} if mep in intended else {})},
evidence=[Evidence(url=url, doc_type="api_record", title=title, published_at=voted_on,
quote=f"{stage}: {tally}", locator=f"vote:{decision['id']}")],
topics=("Europäische Union",),
facet=VotePosition(event_key=event_key, position=position),
)
if len(data) == EP_DECISIONS_PAGE:
yield self.decisions_request(meta["meeting"], meta["offset"] + EP_DECISIONS_PAGE, meta["ep_ids"], meta["titles"])
@@ -0,0 +1,223 @@
[
{
"__type__": "VoteEvent",
"chamber": {
"__type__": "OrgRef",
"create": true,
"ids": {},
"kind": "other",
"name": null,
"short_name": null,
"slug": "europaeisches-parlament"
},
"item_ref": "A10-0259/2026",
"key": "3ab02bb907351dfa059987800a98ee3b1e2a77a9",
"protocol_url": "https://www.europarl.europa.eu/doceo/document/PV-10-2026-10-21-RCV_DE.html",
"raw_text": "Entschließungsantrag (gesamter Text): 504 Ja, 63 Nein, 99 Enthaltungen",
"result": "accepted",
"roll_call": true,
"stage": "Entschließungsantrag (gesamter Text)",
"title": "Förderung von Kompetenzen in der Union durch Möglichkeiten der beruflichen Aus- und Weiterbildung in Zeiten des Übergangs",
"url": "https://data.europarl.europa.eu/eli/dl/event/MTG-PL-2026-10-21-DEC-198302",
"voted_on": "2026-10-21"
},
{
"__type__": "Claim",
"claim_type": "vote",
"counterpart_org": {
"__type__": "OrgRef",
"create": true,
"ids": {},
"kind": "other",
"name": null,
"short_name": null,
"slug": "europaeisches-parlament"
},
"counterpart_person": null,
"data": {
"abstained": 99,
"chamber": "EP",
"intended": "against",
"no": 63,
"position": "for",
"result": "accepted",
"roll_call": true,
"yes": 504
},
"evidence": [
{
"__type__": "Evidence",
"confidence": 1.0,
"doc_type": "api_record",
"excerpt": null,
"full_text": null,
"locator": "vote:eli/dl/event/MTG-PL-2026-10-21-DEC-198302",
"meta": {},
"published_at": "2026-10-21",
"quote": "Entschließungsantrag (gesamter Text): 504 Ja, 63 Nein, 99 Enthaltungen",
"source": null,
"stance": "supports",
"title": "Förderung von Kompetenzen in der Union durch Möglichkeiten der beruflichen Aus- und Weiterbildung in Zeiten des Übergangs",
"url": "https://data.europarl.europa.eu/eli/dl/event/MTG-PL-2026-10-21-DEC-198302"
}
],
"facet": {
"__type__": "VotePosition",
"event_key": "3ab02bb907351dfa059987800a98ee3b1e2a77a9",
"party": null,
"position": "for"
},
"key": null,
"occurred_at": "2026-10-21",
"org": null,
"person": {
"__type__": "PersonRef",
"hints": {},
"ids": {
"ep": "257104"
},
"name": null
},
"review": false,
"sensitive": false,
"summary_de": "Stimmte für: Förderung von Kompetenzen in der Union durch Möglichkeiten der beruflichen Aus- und Weiterbildung in Zeiten des Übergangs (Europäisches Parlament) – angenommen",
"summary_en": "Voted for: Förderung von Kompetenzen in der Union durch Möglichkeiten der beruflichen Aus- und Weiterbildung in Zeiten des Übergangs (European Parliament) – adopted",
"topics": [
"Europäische Union"
],
"valid_from": null,
"valid_to": null
},
{
"__type__": "Claim",
"claim_type": "vote",
"counterpart_org": {
"__type__": "OrgRef",
"create": true,
"ids": {},
"kind": "other",
"name": null,
"short_name": null,
"slug": "europaeisches-parlament"
},
"counterpart_person": null,
"data": {
"abstained": 99,
"chamber": "EP",
"no": 63,
"position": "against",
"result": "accepted",
"roll_call": true,
"yes": 504
},
"evidence": [
{
"__type__": "Evidence",
"confidence": 1.0,
"doc_type": "api_record",
"excerpt": null,
"full_text": null,
"locator": "vote:eli/dl/event/MTG-PL-2026-10-21-DEC-198302",
"meta": {},
"published_at": "2026-10-21",
"quote": "Entschließungsantrag (gesamter Text): 504 Ja, 63 Nein, 99 Enthaltungen",
"source": null,
"stance": "supports",
"title": "Förderung von Kompetenzen in der Union durch Möglichkeiten der beruflichen Aus- und Weiterbildung in Zeiten des Übergangs",
"url": "https://data.europarl.europa.eu/eli/dl/event/MTG-PL-2026-10-21-DEC-198302"
}
],
"facet": {
"__type__": "VotePosition",
"event_key": "3ab02bb907351dfa059987800a98ee3b1e2a77a9",
"party": null,
"position": "against"
},
"key": null,
"occurred_at": "2026-10-21",
"org": null,
"person": {
"__type__": "PersonRef",
"hints": {},
"ids": {
"ep": "256996"
},
"name": null
},
"review": false,
"sensitive": false,
"summary_de": "Stimmte gegen: Förderung von Kompetenzen in der Union durch Möglichkeiten der beruflichen Aus- und Weiterbildung in Zeiten des Übergangs (Europäisches Parlament) – angenommen",
"summary_en": "Voted against: Förderung von Kompetenzen in der Union durch Möglichkeiten der beruflichen Aus- und Weiterbildung in Zeiten des Übergangs (European Parliament) – adopted",
"topics": [
"Europäische Union"
],
"valid_from": null,
"valid_to": null
},
{
"__type__": "Claim",
"claim_type": "vote",
"counterpart_org": {
"__type__": "OrgRef",
"create": true,
"ids": {},
"kind": "other",
"name": null,
"short_name": null,
"slug": "europaeisches-parlament"
},
"counterpart_person": null,
"data": {
"abstained": 99,
"chamber": "EP",
"no": 63,
"position": "abstain",
"result": "accepted",
"roll_call": true,
"yes": 504
},
"evidence": [
{
"__type__": "Evidence",
"confidence": 1.0,
"doc_type": "api_record",
"excerpt": null,
"full_text": null,
"locator": "vote:eli/dl/event/MTG-PL-2026-10-21-DEC-198302",
"meta": {},
"published_at": "2026-10-21",
"quote": "Entschließungsantrag (gesamter Text): 504 Ja, 63 Nein, 99 Enthaltungen",
"source": null,
"stance": "supports",
"title": "Förderung von Kompetenzen in der Union durch Möglichkeiten der beruflichen Aus- und Weiterbildung in Zeiten des Übergangs",
"url": "https://data.europarl.europa.eu/eli/dl/event/MTG-PL-2026-10-21-DEC-198302"
}
],
"facet": {
"__type__": "VotePosition",
"event_key": "3ab02bb907351dfa059987800a98ee3b1e2a77a9",
"party": null,
"position": "abstain"
},
"key": null,
"occurred_at": "2026-10-21",
"org": null,
"person": {
"__type__": "PersonRef",
"hints": {},
"ids": {
"ep": "213330"
},
"name": null
},
"review": false,
"sensitive": false,
"summary_de": "Enthielt sich bei: Förderung von Kompetenzen in der Union durch Möglichkeiten der beruflichen Aus- und Weiterbildung in Zeiten des Übergangs (Europäisches Parlament) – angenommen",
"summary_en": "Abstained on: Förderung von Kompetenzen in der Union durch Möglichkeiten der beruflichen Aus- und Weiterbildung in Zeiten des Übergangs (European Parliament) – adopted",
"topics": [
"Europäische Union"
],
"valid_from": null,
"valid_to": null
}
]
+189
View File
@@ -0,0 +1,189 @@
{
"data": [
{
"id": "eli/dl/event/MTG-PL-2026-10-21-DEC-198302",
"type": "Vote",
"activity_date": "2026-10-21",
"activity_id": "MTG-PL-2026-10-21-DEC-198302",
"activity_label": {
"de": "A10-0259/2026 – Brigitte van den Berg – Entschließungsantrag (gesamter Text)",
"en": "A10-0259/2026 – Brigitte van den Berg – Motion for a resolution (as a whole)"
},
"activity_order": "7.0",
"activity_start_date": "2026-10-21T13:49:16+02:00",
"decision_method": "def/ep-decision-methods/VOTE_ELECTRONIC_ROLLCALL",
"decision_outcome": "def/ep-statuses/ADOPTED",
"had_activity_type": "def/ep-activities/PLENARY_OUTCOME",
"had_voter_abstention": [
"person/213330"
],
"had_voter_against": [
"person/256996"
],
"had_voter_favor": [
"person/257104",
"person/125023"
],
"number_of_attendees": 666,
"number_of_votes_abstention": 99,
"number_of_votes_against": 63,
"number_of_votes_favor": 504,
"recorded_in_a_realization_of": [
"eli/dl/doc/PV-10-2026-10-21-RCV-ITM-151"
],
"comment": {
"hu": "Nsz",
"lt": "VB",
"pt": "VN",
"es": "VN",
"cs": "JH",
"bg": "ПГ",
"de": "NA",
"lv": "ATKL",
"sl": "PG",
"mt": "VSI",
"fr": "AN",
"it": "AN",
"hr": "AN",
"da": "AN",
"ro": "AN",
"pl": "gi",
"ga": "VGR",
"nl": "HS",
"fi": "NHÄ",
"et": "NH",
"sk": "HPM",
"el": "OK",
"sv": "ONU",
"en": "RCV"
},
"notation_votingId": "198302",
"referenceText": {
"de": "Entschließungsantrag (gesamter Text)",
"en": "Motion for a resolution (as a whole)"
},
"inverse_consists_of": [
"eli/dl/event/MTG-PL-2026-10-21-VOT-ITM-993266"
],
"had_voter_intended_against": [
"person/257104"
]
},
{
"id": "eli/dl/event/MTG-PL-2026-10-21-DEC-198977",
"type": "Vote",
"activity_date": "2026-10-21",
"activity_id": "MTG-PL-2026-10-21-DEC-198977",
"activity_label": {
"de": "A10-0272/2026 – Nils Ušakovs, Michalis Hadjipantela – nach § 3 – Änd. 70",
"en": "A10-0272/2026 – Nils Ušakovs, Michalis Hadjipantela – After § 3 – Am 70"
},
"activity_order": "13.0",
"activity_start_date": "2026-10-21T13:20:25+02:00",
"decided_on_a_realization_of": [
"eli/dl/doc/A-10-2026-0272-AM-70"
],
"decision_method": "def/ep-decision-methods/VOTE_ELECTRONIC_ROLLCALL",
"decision_outcome": "def/ep-statuses/REJECTED",
"had_activity_type": "def/ep-activities/PLENARY_OUTCOME",
"number_of_attendees": 673,
"number_of_votes_abstention": 41,
"number_of_votes_against": 455,
"number_of_votes_favor": 177,
"recorded_in_a_realization_of": [
"eli/dl/doc/PV-10-2026-10-21-RCV-ITM-023"
],
"responsible_organization_label": {
"et": "PfE",
"hu": "PfE",
"pl": "PfE",
"sl": "PfE",
"el": "PfE",
"ro": "PfE",
"lv": "PfE",
"pt": "PfE",
"sk": "PfE",
"fi": "PfE",
"bg": "PfE",
"en": "PfE",
"fr": "PfE",
"hr": "PfE",
"nl": "PfE",
"mt": "PfE",
"da": "PfE",
"cs": "PfE",
"de": "PfE",
"ga": "PfE",
"it": "PfE",
"sv": "PfE",
"es": "PfE",
"lt": "PfE"
},
"was_motivated_by": [
"eli/dl/event/MTG-PL-2026-10-21-VOT-ITM-993294-RCV-3"
],
"comment": {
"sl": "PG",
"sv": "ONU",
"cs": "JH",
"el": "OK",
"fr": "AN",
"it": "AN",
"hr": "AN",
"da": "AN",
"ro": "AN",
"lv": "ATKL",
"et": "NH",
"es": "VN",
"pt": "VN",
"ga": "VGR",
"bg": "ПГ",
"nl": "HS",
"mt": "VSI",
"pl": "gi",
"fi": "NHÄ",
"en": "RCV",
"de": "NA",
"sk": "HPM",
"hu": "Nsz",
"lt": "VB"
},
"notation_votingId": "198977",
"decisionAboutId": "70",
"referenceText": {
"de": "Nach § 3",
"en": "After § 3"
},
"inverse_consists_of": [
"eli/dl/event/MTG-PL-2026-10-21-VOT-ITM-993294"
],
"had_voter_favor": [
"person/257104",
"person/125023"
]
},
{
"id": "eli/dl/event/MTG-PL-2026-10-21-DEC-199277",
"type": "Vote",
"activity_date": "2026-10-21",
"activity_id": "MTG-PL-2026-10-21-DEC-199277",
"activity_label": {
"de": "Block 16",
"en": "Block 16"
},
"activity_order": "197.0",
"decision_method": "def/ep-decision-methods/VOTE_HAND",
"decision_outcome": "def/ep-statuses/ADOPTED",
"had_activity_type": "def/ep-activities/PLENARY_OUTCOME",
"notation_votingId": "199277",
"decisionAboutId": "707, 708, 709",
"referenceText": {
"de": "Block 16",
"en": "Block 16"
},
"inverse_consists_of": [
"eli/dl/event/MTG-PL-2026-10-21-VOT-ITM-993336"
]
}
]
}
@@ -0,0 +1,25 @@
{
"url": "https://data.europarl.europa.eu/api/v2/meetings/MTG-PL-2026-10-21/decisions",
"kind": "decisions",
"method": "GET",
"params": {
"format": "application/ld+json",
"limit": 500,
"offset": 0
},
"json": null,
"meta": {
"ep_ids": [
"257104",
"256996",
"213330"
],
"meeting": "MTG-PL-2026-10-21",
"offset": 0,
"titles": {
"eli/dl/event/MTG-PL-2026-10-21-VOT-ITM-993266": "Förderung von Kompetenzen in der Union durch Möglichkeiten der beruflichen Aus- und Weiterbildung in Zeiten des Übergangs"
}
},
"content_type": "application/ld+json",
"body_file": "decisions.json"
}
@@ -0,0 +1,42 @@
[
{
"__type__": "Request",
"conditional": true,
"headers": null,
"json": null,
"kind": "results",
"meta": {
"ep_ids": [
"197490"
],
"meeting": "MTG-PL-2025-01-20"
},
"method": "GET",
"params": {
"format": "application/ld+json",
"limit": 500
},
"skip_unchanged": true,
"url": "https://data.europarl.europa.eu/api/v2/meetings/MTG-PL-2025-01-20/vote-results"
},
{
"__type__": "Request",
"conditional": true,
"headers": null,
"json": null,
"kind": "results",
"meta": {
"ep_ids": [
"197490"
],
"meeting": "MTG-PL-2025-01-21"
},
"method": "GET",
"params": {
"format": "application/ld+json",
"limit": 500
},
"skip_unchanged": true,
"url": "https://data.europarl.europa.eu/api/v2/meetings/MTG-PL-2025-01-21/vote-results"
}
]
+34
View File
@@ -0,0 +1,34 @@
{
"data": [
{
"id": "eli/dl/event/MTG-PL-2025-01-20",
"type": "Activity",
"activity_date": "2025-01-20",
"activity_id": "MTG-PL-2025-01-20",
"activity_label": {
"de": "Montag, 20. Januar 2025",
"en": "Monday, 20 January 2025"
}
},
{
"id": "eli/dl/event/MTG-PL-2025-01-21",
"type": "Activity",
"activity_date": "2025-01-21",
"activity_id": "MTG-PL-2025-01-21",
"activity_label": {
"de": "Dienstag, 21. Januar 2025",
"en": "Tuesday, 21 January 2025"
}
},
{
"id": "eli/dl/event/MTG-PL-2026-06-01",
"type": "Activity",
"activity_date": "2026-06-01",
"activity_id": "MTG-PL-2026-06-01",
"activity_label": {
"de": "Dienstag, 21. Januar 2025",
"en": "Tuesday, 21 January 2025"
}
}
]
}
@@ -0,0 +1,18 @@
{
"url": "https://data.europarl.europa.eu/api/v2/meetings",
"kind": "meetings",
"method": "GET",
"params": {
"year": 2025,
"format": "application/ld+json",
"limit": 200
},
"json": null,
"meta": {
"ep_ids": [
"197490"
]
},
"content_type": "application/ld+json",
"body_file": "meetings.json"
}
@@ -0,0 +1,27 @@
[
{
"__type__": "Request",
"conditional": true,
"headers": null,
"json": null,
"kind": "decisions",
"meta": {
"ep_ids": [
"X"
],
"meeting": "MTG-PL-2026-10-21",
"offset": 0,
"titles": {
"eli/dl/event/MTG-PL-2026-10-21-VOT-ITM-993266": "Förderung von Kompetenzen in der Union durch Möglichkeiten der beruflichen Aus- und Weiterbildung in Zeiten des Übergangs"
}
},
"method": "GET",
"params": {
"format": "application/ld+json",
"limit": 500,
"offset": 0
},
"skip_unchanged": true,
"url": "https://data.europarl.europa.eu/api/v2/meetings/MTG-PL-2026-10-21/decisions"
}
]
+20
View File
@@ -0,0 +1,20 @@
{
"data": [
{
"id": "eli/dl/event/MTG-PL-2026-10-21-VOT-ITM-993266",
"activity_date": "2026-10-21",
"activity_id": "MTG-PL-2026-10-21-VOT-ITM-993266",
"activity_label": {
"de": "Förderung von Kompetenzen in der Union durch Möglichkeiten der beruflichen Aus- und Weiterbildung in Zeiten des Übergangs",
"en": "Boosting skills in the Union – opportunities for vocational education and training in times of transition"
}
},
{
"id": "eli/dl/event/no-date",
"activity_id": "no-date",
"activity_label": {
"de": "Eintrag ohne Datum"
}
}
]
}
@@ -0,0 +1,18 @@
{
"url": "https://data.europarl.europa.eu/api/v2/meetings/MTG-PL-2026-10-21/vote-results",
"kind": "results",
"method": "GET",
"params": {
"format": "application/ld+json",
"limit": 500
},
"json": null,
"meta": {
"ep_ids": [
"X"
],
"meeting": "MTG-PL-2026-10-21"
},
"content_type": "application/ld+json",
"body_file": "results.json"
}
+4
View File
@@ -23,6 +23,10 @@
docker compose build
docker compose up -d
```
Docker creates `${DATA_DIR}/raw` as root, but the backend runs as a non-root user, and every fetch
then fails with "Permission denied". Hand it over once after the first `up`:
`chown -R "$(docker run --rm --entrypoint id politrack-backend -u):$(docker run --rm --entrypoint id politrack-backend -g)" "$DATA_DIR/raw"`.
`migrate` runs once (schema, seed data, source registry, job-queue tables), then `api`, `web`, `mcp`
and three workers start:
+1 -1
View File
@@ -32,6 +32,7 @@ Next.js frontend (de/en), Docker Compose for Unraid.
- [x] Social: Bluesky, Mastodon, public Telegram channels, YouTube (key), X via official API (token);
party/institution channels; curated `social_accounts.yaml`
- [x] X deletion sync (`social.x_deletions`): posts gone from X keep a marked record without their text
- [x] EP final roll-call votes of Austrian MEPs (`europarl.votes`)
- [x] Pages: organisations, money explorer, social feed, Wikipedia edits, person network ("follow the money"),
say-vs-vote on roll calls
@@ -45,7 +46,6 @@ Next.js frontend (de/en), Docker Compose for Unraid.
**Data**
- [ ] Full speech texts and interjections from the protocols; committee memberships (list 250)
- [ ] HowTheyVote.eu / EP roll-call votes of Austrian MEPs
- [ ] EU Council participant lists, ministries' press releases, Federal President's schedule
- [ ] Election results and candidate lists; AMA/TAM subsidy recipients (after checking bulk access and terms)
- [ ] Cabinet staff and appointments to state-owned companies (from answers to parliamentary questions)
+2 -1
View File
@@ -45,6 +45,7 @@ Status: ✅ implemented (adapter name in `code`) · 🟡 planned · ⛔ delibera
| Public procurement – data.gv.at "Vergabe-Kerndaten" | **contract awards**: buyer, winner, amount, CPV (eForms) | OGD hub catalogue → per-publisher XML | open data (CC BY 4.0) | ✅ `vergabe.awards` |
| RTR / KommAustria Medientransparenz | **government advertising** and **media subsidies** per payer → medium | open-data API | free re-use, attribution | ✅ `rtr.medientransparenz` |
| European Parliament | Austrian MEPs, roles, **declarations of interests** | open-data/website XML | CC BY 4.0 | ✅ `europarl.meps` |
| – plenary roll calls | **final votes** of Austrian MEPs (whole texts, no amendments), with totals and later corrections | Open Data API v2 (`/meetings/{id}/decisions`) | CC BY 4.0 | ✅ `europarl.votes` |
| European Commission – meetings | meetings of Commissioners/cabinets with **interest representatives** (Austrians and Austrian organisations) | XLSX export | EU re-use policy | ✅ `eu.commission_meetings` |
| Wikipedia (de/en) | anonymous edits from **parliament/government IP ranges** (`config/ip_ranges.yaml`) | MediaWiki API `usercontribs` | CC BY-SA (edit metadata) | ✅ `wikipedia.institution_edits` |
| Google Political Ads Transparency | Austrian political ads: advertiser, spend bracket, impressions | public bundle (zip, CSV) | Google's transparency report terms | ✅ `ads.google` |
@@ -66,7 +67,7 @@ Status: ✅ implemented (adapter name in `code`) · 🟡 planned · ⛔ delibera
|---|---|---|---|---|
| Wikidata | positions incl. Landtage and mayors, party membership, careers, birth dates, **social-media handles**, ids | SPARQL | CC0 | ✅ `wikidata.politicians` |
| GDELT DOC 2.0 | article metadata for politicians' names, attributed to the original outlets | API, ≤ 1 request / 5 s | free | ✅ `gdelt.coverage` (supplement; thin for Austrian outlets) |
| HowTheyVote.eu | EP roll-call votes | CSV dump | check licence | 🟡 |
| HowTheyVote.eu | EP roll-call votes | CSV dump on GitHub (robots.txt blocks its download path), API | ODbL 1.0 (share-alike) | ⛔ not needed: `europarl.votes` takes the same votes from the EP under CC BY 4.0 |
| OpenSanctions | PEP cross-references | bulk JSON | CC BY-NC 4.0 | 🟡 non-commercial only |
| MeineAbgeordneten.at | curated dossiers | – | sui-generis database right (§76c UrhG) | ⛔ scraping → 🟡 ask for a partnership |
| Parteispenden.at | party-finance analysis | – | ask | 🟡 partnership |