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>
80 lines
2.9 KiB
Python
80 lines
2.9 KiB
Python
"""Generate TypeScript types from the OpenAPI schema (no npm needed).
|
|
|
|
Supports the JSON-Schema subset FastAPI/Pydantic emit: objects, arrays, enums/Literals, anyOf with null,
|
|
$ref, additionalProperties, primitive formats. Output: one `export type X = {...}` per component schema
|
|
plus `Paths` mapping GET endpoints to their response types.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
HEADER = "// Generated by `politrack openapi --ts` from the FastAPI schema. Do not edit by hand.\n\n"
|
|
|
|
|
|
def ts_type(schema: dict[str, Any]) -> str:
|
|
if "$ref" in schema:
|
|
return schema["$ref"].rsplit("/", 1)[-1]
|
|
if "const" in schema:
|
|
return _literal(schema["const"])
|
|
if "enum" in schema:
|
|
return " | ".join(_literal(v) for v in schema["enum"])
|
|
for key in ("anyOf", "oneOf"):
|
|
if key in schema:
|
|
parts = []
|
|
for sub in schema[key]:
|
|
part = ts_type(sub)
|
|
if part not in parts:
|
|
parts.append(part)
|
|
return " | ".join(parts)
|
|
kind = schema.get("type")
|
|
if kind == "array":
|
|
inner = ts_type(schema.get("items", {}))
|
|
return f"({inner})[]" if "|" in inner else f"{inner}[]"
|
|
if kind == "object":
|
|
if "properties" in schema:
|
|
return _object(schema)
|
|
extra = schema.get("additionalProperties")
|
|
value = ts_type(extra) if isinstance(extra, dict) and extra else "unknown"
|
|
return f"Record<string, {value}>"
|
|
if kind in ("integer", "number"):
|
|
return "number"
|
|
if kind == "boolean":
|
|
return "boolean"
|
|
if kind == "string":
|
|
return "string"
|
|
if kind == "null":
|
|
return "null"
|
|
return "unknown"
|
|
|
|
|
|
def _literal(value: Any) -> str:
|
|
return f"'{value}'" if isinstance(value, str) else ("null" if value is None else str(value).lower())
|
|
|
|
|
|
def _object(schema: dict[str, Any], indent: str = "") -> str:
|
|
required = set(schema.get("required", []))
|
|
lines = ["{"]
|
|
for name, prop in schema.get("properties", {}).items():
|
|
optional = "" if name in required else "?"
|
|
lines.append(f"{indent} {name}{optional}: {ts_type(prop)};")
|
|
lines.append(f"{indent}}}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def to_typescript(openapi: dict[str, Any]) -> str:
|
|
out = [HEADER]
|
|
for name, schema in sorted(openapi.get("components", {}).get("schemas", {}).items()):
|
|
if name in ("HTTPValidationError", "ValidationError"):
|
|
continue
|
|
out.append(f"export type {name} = {ts_type(schema)};\n")
|
|
out.append("\nexport type Paths = {")
|
|
for path, ops in sorted(openapi.get("paths", {}).items()):
|
|
get = ops.get("get")
|
|
if not get:
|
|
continue
|
|
content = get.get("responses", {}).get("200", {}).get("content", {}).get("application/json", {})
|
|
out.append(f" '{path}': {ts_type(content.get('schema', {})) if content.get('schema') else 'unknown'};")
|
|
out.append("};\n")
|
|
return "\n".join(out)
|