Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016679RwmHsuTFfxt26wP6rk
245 lines
12 KiB
Python
245 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""TAP sēdes lapas (kešs no tools/fetch.py) → data/lv-mk-sede-GGGG-MM-DD.xml un sources/<sēde>/*.txt
|
|
|
|
python3 tools/convert.py .cache/<sēdes uuid> [--registry organizacijas.xml] [--pasvaldibas pasvaldibas-atvk.xml] [--valdibas valdibas.xml]
|
|
|
|
Reģistri pēc noklusējuma — no ProcessGit (Organizations_register, Pasvaldibu-registrs, Valdibas-Deklaracija-as-Code).
|
|
Ja sēdes datne jau ir un saturs mainījies (piemēram, publicēts protokols vai mainīts statuss), datu versija palielinās.
|
|
"""
|
|
import datetime as dt
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
import urllib.request
|
|
from xml.sax.saxutils import escape, quoteattr
|
|
|
|
from lxml import etree
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import derive # noqa: E402
|
|
import tap # noqa: E402
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
NS = "urn:pppa:vpk:mk-lemumi:0.1"
|
|
PG = "https://processgit.org/Valsts-Pirmkods"
|
|
REG = {"registry": f"{PG}/Organizations_register/raw/branch/main/organizacijas.xml",
|
|
"pasvaldibas": f"{PG}/Pasvaldibu-registrs/raw/branch/main/data/pasvaldibas-atvk.xml",
|
|
"valdibas": f"{PG}/Valdibas-Deklaracija-as-Code/raw/branch/main/data/valdibas.xml"}
|
|
ROLE = {"org": "institution", "minister": "minister", "atvk": "municipality"}
|
|
|
|
|
|
def load(src):
|
|
if os.path.exists(src):
|
|
return open(src, "rb").read()
|
|
return urllib.request.urlopen(urllib.request.Request(src, headers=tap.UA), timeout=60).read()
|
|
|
|
|
|
def a(name, value):
|
|
return f" {name}={quoteattr(str(value))}" if value not in (None, "", False) else ""
|
|
|
|
|
|
def el(tag, text, **attrs):
|
|
return f"<{tag}{''.join(a(k, v) for k, v in attrs.items())}>{escape(text)}</{tag}>" if text else ""
|
|
|
|
|
|
def government(valdibas_xml, date):
|
|
govs = []
|
|
for g in etree.fromstring(valdibas_xml):
|
|
if isinstance(g.tag, str) and etree.QName(g).localname == "Government":
|
|
cv = next(c for c in g if etree.QName(c).localname == "ConfidenceVote").get("date")
|
|
govs.append((cv, g.get("id")))
|
|
return max(x for x in govs if x[0] <= date)[1]
|
|
|
|
|
|
def org_by_name(org_xml):
|
|
out = {}
|
|
for o in etree.fromstring(org_xml):
|
|
if isinstance(o.tag, str) and etree.QName(o).localname == "Organization" and not o.get("validTo"):
|
|
name = next((c.text for c in o if etree.QName(c).localname == "Name"), "") or ""
|
|
out[re.sub(r"^latvijas republikas ", "", name.lower())] = o.get("id")
|
|
return out
|
|
|
|
|
|
def decision_suffix(label, used):
|
|
s = "pl" if "protokollēmum" in label else "rik" if "Rīkojum" in label else None
|
|
if not s or s in used:
|
|
s = f"doc{len(used) + 1}"
|
|
used.add(s)
|
|
return s
|
|
|
|
|
|
def point_ids(points, prefix, parent_num=None):
|
|
"""Punktu identifikatori: „2.” → p2, „2.1.” → p2.1; „1)” zem p2 → p2.1; nenumurēts → u1, u2 …"""
|
|
seen, u = set(), 0
|
|
for p in points:
|
|
digits = re.findall(r"\d+", p["n"].replace("¹", "").replace("²", ""))
|
|
if digits:
|
|
num = ".".join(digits) if (parent_num is None or ".".join(digits).startswith(parent_num + ".")) else \
|
|
f"{parent_num}.{'.'.join(digits)}"
|
|
pid = f"{prefix}.p{num}"
|
|
else:
|
|
u += 1
|
|
num, pid = parent_num, f"{prefix}.u{u}"
|
|
while pid in seen:
|
|
u += 1
|
|
pid = f"{prefix}.u{u}"
|
|
seen.add(pid)
|
|
p["id"], p["num"] = pid, num
|
|
point_ids(p["children"], prefix, num)
|
|
|
|
|
|
def write_points(points, R, depth=0):
|
|
out = []
|
|
for p in points:
|
|
parties = R.parties(p["text"])
|
|
date, printed = derive.deadline(p["text"])
|
|
kind = derive.kind_of(p["text"], parties, depth > 0)
|
|
party_xml = "".join(el("Party", printed_, role=ROLE[k], org=i if k != "atvk" else None, atvk=i if k == "atvk" else None)
|
|
for k, i, printed_ in parties)
|
|
out.append(f'<Point id="{p["id"]}"{a("n", p["n"]) or " n=" + quoteattr("")} kind="{kind}">' + el("Text", p["text"])
|
|
+ "".join(el("Quote", q) for q in p["quotes"] if q) + party_xml + (el("Deadline", printed, date=date) if date else "")
|
|
+ "".join(write_points(p["children"], R, depth + 1)) + "</Point>")
|
|
return out
|
|
|
|
|
|
def heading_numbers(heading):
|
|
h = " ".join(heading)
|
|
act = re.search(r"rīkojums Nr\.\s*(\d+)", h)
|
|
prot = re.search(r"prot\.\s*Nr\.\s*(\d+)\s+(\d+)\.\s*§", h)
|
|
return (act.group(1) if act else None, prot.group(1) if prot else None, prot.group(2) if prot else None)
|
|
|
|
|
|
def source_text(d):
|
|
lines = list(d["heading"]) + [d["title"] or d["header"]]
|
|
for depth, p in tap.flatten(d["points"]):
|
|
lines.append(" " * depth + (p["n"] + " " if p["n"] else "") + p["text"])
|
|
lines += [" " * depth + " " + q for q in p["quotes"] if q]
|
|
for x in d["annexes"]:
|
|
lines.append(f"[pielikums {x['n']}] {x['text']}")
|
|
lines += d["signatures"]
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def main(argv):
|
|
cache = argv[0]
|
|
opt = {k: (argv[argv.index("--" + k) + 1] if "--" + k in argv else v) for k, v in REG.items()}
|
|
man = json.load(open(os.path.join(cache, "manifest.json"), encoding="utf-8"))
|
|
row, files = man["meeting"], man["files"]
|
|
R = derive.Registers(load(opt["registry"]), load(opt["pasvaldibas"]))
|
|
orgs = org_by_name(load(opt["registry"]))
|
|
date = row["start"][:10]
|
|
mid = f"lv-mk-sede-{date}"
|
|
os.makedirs(os.path.join(ROOT, "data"), exist_ok=True)
|
|
existing = os.path.join(ROOT, "data", mid + ".xml")
|
|
if os.path.exists(existing) and row["uuid"] not in open(existing, encoding="utf-8").read():
|
|
mid += "-2"
|
|
existing = os.path.join(ROOT, "data", mid + ".xml")
|
|
gov = government(load(opt["valdibas"]), date)
|
|
meeting = tap.parse_meeting(open(os.path.join(cache, "meeting.html"), "rb").read())
|
|
title = row["title"] or meeting["title"]
|
|
src_dir = os.path.join("sources", mid)
|
|
os.makedirs(os.path.join(ROOT, src_dir), exist_ok=True)
|
|
|
|
sections, n_points, n_dec = [], 0, 0
|
|
for s in meeting["sections"]:
|
|
items = []
|
|
for it in s["items"]:
|
|
iid = f"{mid}.q{it['n']}"
|
|
api = os.path.join(cache, "api", f"{it['legal_act']}.json") if it["legal_act"] else None
|
|
act_type = resp = None
|
|
if api and os.path.exists(api):
|
|
j = json.load(open(api, encoding="utf-8"))
|
|
resp = tap.norm(j["data"]["attributes"].get("responsible_institution_name"))
|
|
act_type = next((x["attributes"]["name"] for x in j.get("included", []) if x["type"] == "legal_act_types"), None)
|
|
used, decisions = set(), []
|
|
for label, href in it["documents"]:
|
|
m = re.search(r"/structuralizer/data/nodes/([0-9a-f-]{36})/preview", href or "")
|
|
rel = f"nodes/{m.group(1)}.html" if m else None
|
|
if label not in tap.DECISION_DOCS or not rel or rel not in files:
|
|
continue
|
|
d = tap.parse_node(open(os.path.join(cache, rel), "rb").read())
|
|
if not d["points"]:
|
|
continue
|
|
did = f"{iid}.{decision_suffix(label, used)}"
|
|
point_ids(d["points"], did)
|
|
fname = f"q{it['n']}.{did.rsplit('.', 1)[1]}.txt"
|
|
with open(os.path.join(ROOT, src_dir, fname), "w", encoding="utf-8") as f:
|
|
f.write(source_text(d))
|
|
n_points += sum(1 for _ in tap.flatten(d["points"]))
|
|
n_dec += 1
|
|
act_no, prot, par = heading_numbers(d["heading"])
|
|
decisions.append(
|
|
f'<Decision id="{did}" document={quoteattr(label)} textStatus="{"draft" if "projekts" in label else "final"}"'
|
|
f'{a("actNumber", act_no)}{a("protocol", prot)}{a("paragraph", par)}>'
|
|
+ "".join(el("Heading", h) for h in d["heading"]) + el("Title", d["title"]) + el("Url", files[rel]["url"]) + el("SourceFile", f"{src_dir}/{fname}")
|
|
+ el("SHA256", files[rel]["sha256"]) + "".join(write_points(d["points"], R))
|
|
+ "".join(el("Annex", x["text"], n=x["n"]) for x in d["annexes"] if x["text"])
|
|
+ "".join(el("Signatory", x) for x in d["signatures"]) + "</Decision>")
|
|
items.append(
|
|
f'<Item id="{iid}" n="{it["n"]}"{a("tap", it["tap"])}{a("restricted", "true" if it["restricted"] else "")}'
|
|
f' status={quoteattr(it["status"] or "nav norādīts")}'
|
|
+ "".join(a(f, "true") for f in ("addedLate", "movedToA", "postponed") if f in it["flags"]) + ">"
|
|
+ el("Title", it["title"]) + el("ActType", act_type)
|
|
+ (el("Responsible", resp, org=orgs.get(resp.lower().replace("latvijas republikas ", ""))) if resp else "")
|
|
+ el("Presenter", it["presenter"]) + el("Invited", it["invited"])
|
|
+ (f'<LegalAct uuid="{it["legal_act"]}">' + el("Url", f"{tap.BASE}/legal_acts/{it['legal_act']}") + "</LegalAct>"
|
|
if it["legal_act"] else "")
|
|
+ "".join(el("Document", lab, url=(tap.BASE + h if h and h.startswith("/") else h)) for lab, h in it["documents"] if lab and h)
|
|
+ "".join(el("Approval", x) for x in it["approvals"] if x)
|
|
+ el("PublicParticipation", it["participation"]) + "".join(decisions) + "</Item>")
|
|
sections.append(f'<Section n="{s["n"]}" closed="{"true" if "slēgtā daļa" in s["title"] else "false"}">'
|
|
+ el("Title", s["title"]) + "".join(items) + "</Section>")
|
|
|
|
n_items = sum(len(s["items"]) for s in meeting["sections"])
|
|
changes, number = [f"Darba kārtība no TAP: {len(meeting['sections'])} sadaļas, {n_items} jautājumi; "
|
|
f"{n_dec} lēmumu teksti ({n_points} punkti) — projekti, kā pievienoti darba kārtībai."], 1
|
|
prev_versions = ""
|
|
body = "".join(sections)
|
|
if os.path.exists(existing):
|
|
old = etree.parse(existing).getroot()
|
|
N = {"m": NS}
|
|
old_body = "".join(etree.tostring(x, encoding="unicode") for x in old.findall("m:Section", N))
|
|
new_body = "".join(etree.tostring(x, encoding="unicode") for x in
|
|
etree.fromstring(f'<x xmlns="{NS}">{body}</x>').findall("m:Section", N))
|
|
vers = old.findall("m:Metadata/m:DataVersion", N)
|
|
prev_versions = "".join(etree.tostring(v, encoding="unicode").replace(f' xmlns="{NS}"', "") for v in vers)
|
|
old_proto = old.find("m:Metadata/m:Protocol", N)
|
|
if old_body == new_body and (old_proto is not None) == bool(row["protocol"]):
|
|
print(f"{mid}: nav izmaiņu")
|
|
return
|
|
number = max(int(v.get("number")) for v in vers) + 1
|
|
changes = []
|
|
if row["protocol"] and old_proto is None:
|
|
changes.append(f"Publicēts sēdes protokols Nr. {row['protocol']}.")
|
|
if old_body != new_body:
|
|
changes.append("Darba kārtība vai lēmumu teksts atjaunots no TAP.")
|
|
today = dt.date.today().isoformat()
|
|
meta = ("<Metadata>" + el("Title", title) + el("Start", row["start"])
|
|
+ el("Kind", "extraordinary" if "ārkārtas" in (title or "") else "regular")
|
|
+ (f'<Protocol number="{row["protocol"]}">' + el("Url", row.get("protocol_url")) + "</Protocol>" if row["protocol"] else "")
|
|
+ "<Source>" + el("Url", files["meeting.html"]["url"]) + el("SHA256", files["meeting.html"]["sha256"])
|
|
+ el("Retrieved", files["meeting.html"]["retrieved"][:19]) + "</Source>"
|
|
+ f'<DataVersion number="{number}" date="{today}">' + "".join(el("Change", c) for c in changes) + "</DataVersion>"
|
|
+ prev_versions + "</Metadata>")
|
|
xml = (f'<CabinetMeeting xmlns="{NS}" id="{mid}" government="{gov}" schemaVersion="0.1">' + meta + body + "</CabinetMeeting>")
|
|
root = etree.fromstring(xml.encode())
|
|
etree.indent(root, space=" ")
|
|
with open(existing, "wb") as f:
|
|
f.write(etree.tostring(root, xml_declaration=True, encoding="UTF-8") + b"\n")
|
|
viewer = os.path.join(ROOT, "data", "processgit.viewer.json") # ProcessGit skatītājs: jaunā datne sarakstā edit_allow
|
|
if os.path.exists(viewer):
|
|
v = json.load(open(viewer, encoding="utf-8"))
|
|
allow = v["viewers"][0]["edit_allow"]
|
|
if mid + ".xml" not in allow:
|
|
v["viewers"][0]["edit_allow"] = sorted(set(allow + [mid + ".xml"]) - {"lv-mk-sede-*.xml"})
|
|
with open(viewer, "w", encoding="utf-8") as f:
|
|
json.dump(v, f, ensure_ascii=False, indent=2)
|
|
f.write("\n")
|
|
print(f"{existing}: {n_items} jautājumi, {n_dec} lēmumi, {n_points} punkti, datu versija {number}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv[1:])
|