1
0
Files
Rihards 9e67e74b1f 0.1.0: Funkciju, darbu un kompetenču reģistrs — 95 iestādes no Nolikumi-as-Code@99b5d2e
Shēma registrs-0.1.xsd; iestades/<VPK ID>/{funkcijas,darbi,kompetence}.xml un registrs/ apvienojums.
656 funkcijas, 1585 darbi, 664 kompetences; VARAM funkciju formulējumi gramatiski saskaņoti (ai_proposed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016igpMJBtapmG6vHE4vSF9q
2026-10-24 23:20:00 +00:00

228 lines
10 KiB
Python

"""FDK reģistrs: Nolikumi-as-Code → iestades/<VPK ID>/{funkcijas,darbi,kompetence}.xml → registrs/{…}.xml.
Noteikumi (vienādi visiem nolikumiem):
• Funkcija — nolikuma punkts ar kind="function" (vai tā apakšpunkts sarakstā, kas sākas ar „… funkcijas:”).
• Darbs — punkts ar kind="task" (vai apakšpunkts sarakstā „… uzdevumus:”).
• Kompetence — punkts ar kind="right" (veids = tiesības) un kompetences nodaļas saraksti, kuru priekšmets ir pati iestāde
(„Ministrija:”, „Birojs atbilstoši kompetencei:”, „Dienests ir tiesīgs:” …); vadītāja, personāla un
struktūras saraksti netiek ņemti.
• Formulējums — ievadteksti no ārējā saraksta ievada uz leju + pats punkts (ārējais ievads, piemēram, „Ministrijai ir šādas
funkcijas:”, tiek izlaists, jo to izsaka ieraksta veids). statuss="auto".
sources/formulejumi.csv var aizstāt formulējumu ar gramatiski saskaņotu (ai_proposed / confirmed).
• ID — F|D|K-<VPK ID>-NNN, pastāvīgs: sources/ids.csv glabā avots → id; jauni punkti saņem nākamo brīvo numuru.
• Svītrotie punkti netiek iekļauti.
python3 tools/build.py --nolikumi ../Nolikumi-as-Code [--only 21-0000 …]
"""
import argparse
import csv
import datetime as dt
import os
import re
import subprocess
import sys
import xml.etree.ElementTree as ET
NS = "{urn:pppa:vpk:nolikums:0.1}"
OUT_NS = "urn:pppa:vpk:fdk:0.1"
TYPES = {"F": ("funkcijas.xml", "Funkcijas", "Funkcija"),
"D": ("darbi.xml", "Darbi", "Darbs"),
"K": ("kompetence.xml", "Kompetences", "Kompetence")}
# kompetences nodaļas saraksti, kas apraksta nevis iestādi, bet tās vadītāju, personālu vai uzbūvi
NOT_INSTITUTION = re.compile(
r"^(ministrs|valsts sekretār|parlamentār|[^:]*priekšniek|[^:]*vadītāj|[^:]*direktor|[^:]*personāl|[^:]*struktūrvienīb|"
r"[^:]*sastāvā|[^:]*finanšu resurs|[^:]*izglītojam|izglītības process|akadēmisk|[^:]*pienākumi|[^:]*komandier)", re.I)
RIGHTS = re.compile(r"tiesīb|tiesīg|\bvar:\s*$", re.I)
DATE = re.compile(r"\b(\d{2})\.(\d{2})\.(\d{4})\.")
def clean(t):
t = " ".join((t or "").split())
t = re.sub(r"\s*[;,]\s*(un|vai)?\s*$", "", t)
return t.rstrip(" .;")
def list_type(lead):
"""Record type of a list by its outermost introductory text."""
s = (lead or "").lower()
if "funkcij" in s:
return "F"
if "uzdevum" in s:
return "D"
if RIGHTS.search(s):
return "Kt"
return None
def read_csv(path):
return list(csv.DictReader(open(path, encoding="utf-8"))) if os.path.exists(path) else []
def git_commit(path):
try:
return subprocess.check_output(["git", "-C", path, "rev-parse", "--short", "HEAD"], text=True).strip()
except Exception:
return os.environ.get("NOLIKUMI_COMMIT", "unknown")
def records_of(root):
"""Yield (type, veids, point, chain_texts, parent_point) for every record in one nolikums."""
out = []
def walk(p, chain, chapter_kind, outer_type, comp_ok, parent_rec, inside):
k = p.get("kind")
text = (p.findtext(NS + "Text") or "").strip()
if k == "deleted" or text.lower().startswith("(svītrots"):
return
rtype = veids = None
if k == "function":
rtype = "F"
elif k == "task":
rtype = "D"
elif k == "right":
rtype, veids = "K", "tiesības"
elif k == "provision":
if outer_type == "F":
rtype = "F"
elif outer_type == "D":
rtype = "D"
elif outer_type == "Kt":
rtype, veids = "K", "tiesības"
elif outer_type == "Kd":
rtype, veids = "K", "darbība"
if rtype:
out.append((rtype, veids, p, list(chain), parent_rec if parent_rec and parent_rec[0] == rtype else None))
kids = p.findall(NS + "Point")
if not kids:
return
# a point with children introduces a list: its text becomes part of the children's formulation
if not inside and k in ("lead", "provision"):
# outermost introductory text — defines the list type, is not repeated in formulations
lt = list_type(text)
if lt is None and chapter_kind == "competence" and text.rstrip().endswith(":") and not NOT_INSTITUTION.match(text):
lt = "Kd" # „Ministrija:”, „Birojs atbilstoši kompetencei:” — iestādes vispārējā kompetence
for c in kids:
walk(c, [], chapter_kind, lt or outer_type, False, None, True)
return
sub = chain + [text]
rec = (rtype, p) if rtype else parent_rec
for c in kids:
walk(c, sub, chapter_kind, outer_type, comp_ok, rec, True)
for ch in root.findall(NS + "Chapter"):
for p in ch.findall(NS + "Point"):
walk(p, [], ch.get("kind"), None, False, None, False)
return out
def formulate(rtype, veids, chain, text, outer_rights):
parts = [" ".join(c.split()) for c in chain] + [clean(text)]
s = " ".join(x for x in parts if x)
s = re.sub(r"\s+", " ", s).strip()
if rtype == "K" and veids == "tiesības" and outer_rights and not s.lower().startswith("tiesīb"):
s = "tiesības " + s
return s
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--nolikumi", required=True)
ap.add_argument("--only", nargs="*")
a = ap.parse_args()
names = {r["vpk_id"]: r["nosaukums"] for r in read_csv("sources/iestades.csv")}
ids = {r["avots"]: r["id"] for r in read_csv("sources/ids.csv")}
over = {r["avots"]: r for r in read_csv("sources/formulejumi.csv")}
used_over = set()
src = f"Nolikumi-as-Code@{git_commit(a.nolikumi)}"
today = dt.date.today().isoformat()
ET.register_namespace("", OUT_NS)
stats = []
files = sorted(f for f in os.listdir(os.path.join(a.nolikumi, "data")) if f.endswith(".xml"))
for fn in files:
root = ET.parse(os.path.join(a.nolikumi, "data", fn)).getroot()
org = root.find(NS + "Institution").get("org")
if a.only and org not in a.only:
continue
if org not in names:
sys.exit(f"{org}: nav sources/iestades.csv")
act = root.find(NS + "Act")
nol = f"MK Nr. {act.findtext(NS + 'Number')}" if act.findtext(NS + "Number") else (act.findtext(NS + "Title") or "")
dates = [act.findtext(NS + "Adopted")]
for note in root.iter(NS + "Note"):
for d, m, y in DATE.findall(note.text or ""):
dates.append(f"{y}-{m}-{d}")
red = max(d for d in dates if d)
recs = records_of(root)
# outermost list intro per record, for the "tiesības" prefix
counters = {}
for v in ids.values():
t, o, n = v[0], v[2:9], int(v[10:])
if o == org:
counters[t] = max(counters.get(t, 0), n)
per = {t: [] for t in TYPES}
rec_id = {}
for rtype, veids, p, chain, parent in recs:
avots = p.get("id")
if avots not in ids:
counters[rtype] = counters.get(rtype, 0) + 1
ids[avots] = f"{rtype}-{org}-{counters[rtype]:03d}"
rid = ids[avots]
if not rid.startswith(rtype + "-"):
sys.exit(f"{avots}: ids.csv saka {rid}, bet punkts tagad ir {rtype} — jāpārskata")
rec_id[p] = rid
text = p.findtext(NS + "Text") or ""
f = formulate(rtype, veids, chain, text, veids == "tiesības" and not chain)
statuss = "auto"
if avots in over:
f, statuss = over[avots]["formulejums"], over[avots]["statuss"]
used_over.add(avots)
attrs = {"id": rid, "iestāde": org, "iestādesNosaukums": names[org], "avots": avots,
"nolikums": nol, "redakcija": red, "statuss": statuss}
if rtype == "K":
attrs = {"id": rid, "veids": veids, **{k: v for k, v in attrs.items() if k != "id"}}
if parent and parent[1] in rec_id:
attrs["vecāks"] = rec_id[parent[1]]
per[rtype].append((attrs, f))
os.makedirs(f"iestades/{org}", exist_ok=True)
for t, (fname, rootname, elname) in TYPES.items():
write(f"iestades/{org}/{fname}", rootname, elname, per[t], src, today, org)
stats.append((org, len(per["F"]), len(per["D"]), len(per["K"])))
# registry = all institution files on disk (also those not rebuilt in this run)
os.makedirs("registrs", exist_ok=True)
for t, (fname, rootname, elname) in TYPES.items():
rows = []
for d in sorted(os.listdir("iestades")):
pth = f"iestades/{d}/{fname}"
if os.path.exists(pth):
for e in ET.parse(pth).getroot():
rows.append(({k: v for k, v in e.attrib.items()}, e.findtext(f"{{{OUT_NS}}}Formulējums")))
write(f"registrs/{fname}", rootname, elname, sorted(rows, key=lambda r: r[0]["id"]), src, today, None)
with open("sources/ids.csv", "w", encoding="utf-8", newline="") as fh:
w = csv.writer(fh)
w.writerow(["avots", "id"])
for k in sorted(ids, key=lambda k: ids[k]):
w.writerow([k, ids[k]])
unused = set(over) - used_over
if unused and not a.only:
sys.exit("formulejumi.csv: avots nav atrasts: " + ", ".join(sorted(unused)))
tot = [sum(s[i] for s in stats) for i in (1, 2, 3)]
print(f"{src}: {len(stats)} iestādes — funkcijas {tot[0]}, darbi {tot[1]}, kompetence {tot[2]}")
for s in stats:
if not (s[1] or s[2]):
print(" bez funkcijām un darbiem:", s[0])
def write(path, rootname, elname, rows, src, today, org):
r = ET.Element(f"{{{OUT_NS}}}{rootname}", {"schemaVersion": "0.1", "avots": src, "ģenerēts": today, **({"iestāde": org} if org else {})})
for attrs, f in rows:
e = ET.SubElement(r, f"{{{OUT_NS}}}{elname}", attrs)
ET.SubElement(e, f"{{{OUT_NS}}}Formulējums").text = f
ET.indent(r, " ")
ET.ElementTree(r).write(path, encoding="UTF-8", xml_declaration=True)
with open(path, "a", encoding="utf-8") as fh:
fh.write("\n")
if __name__ == "__main__":
main()