You've already forked Valdibas-Deklaracija-as-Code
Import UAPF package
data/lv-dekl-2026-05-28.xml — A. Kulberga valdības deklarācija; data/lv-mk-2026-05-28.rp.xml — valdības rīcības plāns (MK rīkojums Nr. 449); data/organizacijas.xml, data/valdibas.xml — reģistri. Pārbaudītājs pārbauda, ka deklarācijas un rīcības plāna datnes nosaukums atbilst saknes elementa identifikatoram. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016679RwmHsuTFfxt26wP6rk
188 lines
11 KiB
Python
188 lines
11 KiB
Python
"""Pārbaudītājs: XSD + identifikatoru noteikumi + saites starp datnēm + kārtības ruļļa noteikumi.
|
|
|
|
python3 tools/validate.py [mape ar XML datnēm] (noklusējums: data/)
|
|
|
|
Ziņo: E = kļūda (izejas kods 1), W = brīdinājums, I = informācija.
|
|
"""
|
|
import os, re, sys
|
|
from collections import defaultdict
|
|
from lxml import etree
|
|
|
|
sys.path.insert(0, os.path.dirname(__file__))
|
|
import ids
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
NS = "urn:pppa:cac:valdiba:0.2"
|
|
N = {"v": NS}
|
|
SCHEMAS = {"GovernmentRegistry": "cac-valdiba-kopejie-0.2.xsd", "GovernmentDeclaration": "valdibas-deklaracija-0.2.xsd",
|
|
"GovernmentActionPlan": "valdibas-deklaracijas-ricibas-plans-0.2.xsd",
|
|
"OrganizationRegistry": "organizaciju-registrs-0.2.xsd"}
|
|
OUT = []
|
|
LINKS = [] # (datne, projekts, avots, [(uzdevums, avota uzdevums)])
|
|
|
|
|
|
def rep(level, where, msg): OUT.append((level, where, msg))
|
|
def local(el): return etree.QName(el).localname
|
|
|
|
|
|
def load(folder):
|
|
docs = defaultdict(list)
|
|
for f in sorted(os.listdir(folder)):
|
|
if not f.endswith(".xml"): continue
|
|
p = os.path.join(folder, f)
|
|
try: t = etree.parse(p)
|
|
except etree.XMLSyntaxError as e: rep("E", f, f"XML nav korekts: {e}"); continue
|
|
root = t.getroot(); name = local(root)
|
|
if etree.QName(root).namespace != NS or name not in SCHEMAS: rep("W", f, "nav v0.2 shēmu saimes datne — izlaista"); continue
|
|
sc = etree.XMLSchema(etree.parse(os.path.join(ROOT, "schemas", SCHEMAS[name])))
|
|
if not sc.validate(t):
|
|
for e in sc.error_log: rep("E", f, f"XSD {e.line}: {e.message}")
|
|
if name in ("GovernmentDeclaration", "GovernmentActionPlan") and f != root.get("id") + ".xml": rep("E", f, f"datnes nosaukumam jābūt {root.get('id')}.xml (identifikators)")
|
|
docs[name].append((f, root))
|
|
return docs
|
|
|
|
|
|
def check_registry(f, r):
|
|
govs = {g.get("id"): g for g in r.findall("v:Government", N)}
|
|
orgs = set()
|
|
for gid, g in govs.items():
|
|
vd = g.find("v:ConfidenceVote", N).get("date")
|
|
if not gid.startswith(f"lv-mk-{vd}"): rep("E", f, f"{gid}: ID datums nesakrīt ar uzticības balsojuma datumu {vd}")
|
|
return govs, orgs
|
|
|
|
|
|
def check_orgs(f, r):
|
|
ids_ = {o.get("id"): o for o in r.findall("v:Organization", N)}
|
|
sectors = {s.get("code") for s in r.findall("v:Sector", N)}
|
|
no_reg = 0
|
|
for oid, o in ids_.items():
|
|
sec, kind = oid[:2], o.get("kind")
|
|
if sec not in sectors: rep("E", f, f"{oid}: resors {sec} nav reģistrā")
|
|
if kind in ("ministry", "pm-institution") and not oid.endswith("-0000"): rep("E", f, f"{oid}: resora vadošās institūcijas identifikatoram jābūt {sec}-0000")
|
|
if kind == "subordinate" and o.get("parent", "")[:2] != sec: rep("E", f, f"{oid}: padotības iestāde nav sava resora vadošās institūcijas padotībā")
|
|
if o.get("parent") and o.get("parent") not in ids_: rep("E", f, f"{oid}: augstākā institūcija {o.get('parent')} nav reģistrā")
|
|
if o.get("successor") and o.get("successor") not in ids_: rep("E", f, f"{oid}: pēctecis {o.get('successor')} nav reģistrā")
|
|
if not o.get("regNo") and kind not in ("office", "group"): no_reg += 1
|
|
if r.findtext("v:Metadata/v:Keeper", namespaces=N) not in ids_: rep("E", f, "reģistra uzturētājs nav reģistrā")
|
|
if no_reg: rep("I", f, f"{no_reg} institūcijām nav norādīts reģistrācijas numurs")
|
|
rep("I", f, f"{len(ids_)} ieraksti, {len(sectors)} resori, {sum(1 for o in ids_.values() if o.get('kind') == 'ministry')} ministrijas")
|
|
return set(ids_)
|
|
|
|
|
|
def check_declaration(f, r, govs):
|
|
did = r.get("id"); texts = {}
|
|
for el in r.iter():
|
|
i = el.get("id") if isinstance(el.tag, str) else None
|
|
if i and el is not r and not i.startswith(did + "."): rep("E", f, f"{i}: nesākas ar deklarācijas ID {did}")
|
|
gov = r.find("v:Metadata/v:Government", N); gref = gov.get("ref")
|
|
if gov.get("status") == "confidence_granted" and not gref: rep("E", f, "status confidence_granted, bet nav valdības atsauces")
|
|
if gref and govs:
|
|
if gref not in govs: rep("E", f, f"valdība {gref} nav reģistrā")
|
|
elif (govs[gref].findtext("v:Declaration", namespaces=N) or "") != did: rep("E", f, f"reģistrā valdībai {gref} norādīta cita deklarācija")
|
|
date = r.findtext("v:Metadata/v:Date", namespaces=N)
|
|
if date and not did.startswith(f"lv-dekl-{date}"): rep("W", f, f"ID datums nesakrīt ar dokumenta datumu {date}")
|
|
|
|
def lists(parent_el, parent_id):
|
|
for k, l in enumerate(parent_el.findall("v:List", N), 1):
|
|
if l.get("id") or any(it.get("id") for it in l.findall("v:Item", N)):
|
|
rep("W", f, f"{parent_id}: sarakstiem un saraksta punktiem identifikatoru nepiešķir")
|
|
|
|
def section(s, exp):
|
|
if s.get("id") != exp: rep("E", f, f"{s.get('id')}: sagaidīts {exp} (secība dokumentā)")
|
|
if any(p.get("id") for p in s.findall("v:Paragraph", N)): rep("W", f, f"{exp}: rindkopām identifikatoru nepiešķir")
|
|
lists(s, exp)
|
|
for k, g in enumerate(s.findall("v:Goal", N), 1):
|
|
if g.get("id") != ids.goal(exp, k): rep("E", f, f"{g.get('id')}: sagaidīts {ids.goal(exp, k)}")
|
|
for k, g in enumerate(s.findall("v:Result", N), 1):
|
|
if g.get("id") != ids.result(exp, k): rep("E", f, f"{g.get('id')}: sagaidīts {ids.result(exp, k)}")
|
|
for t in s.findall("v:Task", N):
|
|
num = int(t.get("number")); tid = t.get("id"); idn = int(re.search(r"\.u(\d+)", tid).group(1))
|
|
if not re.fullmatch(re.escape(ids.task(did, num)) + r"(-\d+)?", tid):
|
|
if draft: renumbered.append(num)
|
|
else: rep("E", f, f"{tid}: sagaidīts {ids.task(did, num)} (numurs {num})")
|
|
src = t.get("derivedFrom")
|
|
if src:
|
|
if not derived: rep("E", f, f"{tid}: derivedFrom norādīts, bet nav Metadata/DerivedFrom")
|
|
elif not src.startswith(derived + ".u"): rep("E", f, f"{tid}: derivedFrom {src} nav no {derived}")
|
|
else: links.append((tid, src))
|
|
idnums.append(idn); texts[tid] = t.findtext("v:Text", namespaces=N); lists(t, tid)
|
|
for k, c in enumerate(s.findall("v:Section", N), 1): section(c, ids.section(exp, k, top=False))
|
|
|
|
draft = gov.get("status") == "draft"; derived = r.findtext("v:Metadata/v:DerivedFrom", namespaces=N)
|
|
renumbered, links, idnums = [], [], []
|
|
for k, s in enumerate(r.findall("v:Body/v:Section", N), 1): section(s, ids.section(did, k, top=True))
|
|
if renumbered: rep("I", f, f"projekts: {len(renumbered)} uzdevumiem drukātais numurs atšķiras no identifikatora (pārnumurēts; identifikators nemainās)")
|
|
if derived: LINKS.append((f, did, derived, links))
|
|
nums = sorted(int(t.get("number")) for t in r.iter(f"{{{NS}}}Task"))
|
|
gaps = sorted(set(range(1, (nums[-1] if nums else 0) + 1)) - set(nums))
|
|
if gaps: rep("W", f, f"uzdevumu numerācijā trūkst: {gaps}")
|
|
cp = r.find("v:Composition", N)
|
|
if any(m.get("id") for m in cp.iter(f"{{{NS}}}PrimeMinisterCandidate", f"{{{NS}}}Minister")): rep("W", f, "parakstītājiem identifikatoru nepiešķir")
|
|
rep("I", f, f"{did}: {len(r.findall('.//v:Section', N))} sadaļas, {len(nums)} uzdevumi")
|
|
return did, texts, idnums
|
|
|
|
|
|
def check_plan(f, r, decls, govs, orgs):
|
|
pid, gid, did = r.get("id"), r.get("government"), r.get("declaration")
|
|
if pid != ids.plan(gid): rep("E", f, f"{pid}: jābūt {ids.plan(gid)}")
|
|
if govs:
|
|
if gid not in govs: rep("E", f, f"valdība {gid} nav reģistrā")
|
|
elif govs[gid].findtext("v:Declaration", namespaces=N) != did: rep("E", f, "reģistrā valdībai norādīta cita deklarācija")
|
|
term = govs.get(gid).find("v:TermEnd", N) if govs.get(gid) is not None else None
|
|
term_end = term.get("value") if term is not None else None
|
|
for k, a in enumerate(r.findall("v:Metadata/v:Approval", N), 1):
|
|
if a.get("id") != ids.plan_version(pid, k): rep("E", f, f"{a.get('id')}: sagaidīts {ids.plan_version(pid, k)}")
|
|
tasks = set(decls[did][1]) if did in decls else None
|
|
if tasks is None: rep("W", f, f"deklarācija {did} nav šajā mapē — saite ar uzdevumiem netiek pārbaudīta")
|
|
per_task = defaultdict(int)
|
|
for m in r.findall("v:Measure", N):
|
|
mid, num, t = m.get("id"), m.get("number"), m.get("task")
|
|
if not t.startswith(did + ".u"): rep("E", f, f"{mid}: uzdevums {t} nav no deklarācijas {did}"); continue
|
|
tn = int(re.search(r"\.u(\d+)", t).group(1))
|
|
if tasks is not None and tn not in tasks: rep("E", f, f"{mid}: uzdevums {tn} deklarācijā nav")
|
|
mm = re.fullmatch(r"(\d+)\.(\d+)\.?", num)
|
|
if not mm or int(mm.group(1)) != tn: rep("E", f, f"{mid}: numurs „{num}” neatbilst uzdevumam {tn}")
|
|
elif mid != ids.measure(pid, tn, int(mm.group(2))): rep("E", f, f"{mid}: sagaidīts {ids.measure(pid, tn, int(mm.group(2)))}")
|
|
per_task[tn] += 1
|
|
for o in m.findall("v:Responsible", N) + m.findall("v:CoResponsible", N):
|
|
for c in (o.get("orgs") or "").split():
|
|
if orgs and c not in orgs: rep("E", f, f"{mid}: organizācija {c} nav organizāciju reģistrā")
|
|
for v in (m.find("v:Deadline", N).get("values") or "").split():
|
|
if term_end and v > term_end: rep("E", f, f"{mid}: termiņš {v} pēc valdības pilnvaru beigām {term_end}")
|
|
for p in m.findall("v:Progress", N):
|
|
if p.get("id") != ids.progress(mid, p.get("period")): rep("E", f, f"{p.get('id')}: sagaidīts {ids.progress(mid, p.get('period'))}")
|
|
over = {tn: n for tn, n in sorted(per_task.items()) if n > 5}
|
|
if over: rep("W", f, f"{len(over)} uzdevumiem vairāk nekā pieci pasākumi (kārtības ruļļa 18. punkts): " + ", ".join(f"{tn}. — {n}" for tn, n in over.items()))
|
|
if tasks is not None:
|
|
without = sorted(tasks - set(per_task))
|
|
if without: rep("W", f, f"uzdevumi bez pasākumiem (kārtības ruļļa 20. punkts): {without}")
|
|
rep("I", f, f"{sum(per_task.values())} pasākumi {len(per_task)} uzdevumiem")
|
|
|
|
|
|
def main(folder):
|
|
docs = load(folder)
|
|
govs, orgs = {}, set()
|
|
for f, r in docs["GovernmentRegistry"]: g, o = check_registry(f, r); govs.update(g)
|
|
for f, r in docs["OrganizationRegistry"]: orgs |= check_orgs(f, r)
|
|
decls = {}
|
|
for f, r in docs["GovernmentDeclaration"]:
|
|
did, texts, nums = check_declaration(f, r, govs); decls[did] = (texts, nums)
|
|
for f, did, derived, links in LINKS:
|
|
if derived not in decls: rep("I", f, f"avota deklarācija {derived} nav šajā mapē — saites netiek pārbaudītas"); continue
|
|
src_ids = {ids.task(derived, n) for n in decls[derived][1]}
|
|
bad = [s for _, s in links if re.sub(r"-\d+$", "", s) not in src_ids]
|
|
if bad: rep("E", f, f"derivedFrom norāda uz neesošiem avota uzdevumiem: {bad}")
|
|
taken = {s for _, s in links}
|
|
rep("I", f, f"projekts no {derived}: pārņemti {len(taken)}, jauni {len(decls[did][1]) - len(links)}, izņemti {len(src_ids - taken)}")
|
|
for f, r in docs["GovernmentActionPlan"]: check_plan(f, r, decls, govs, orgs)
|
|
for lvl in "EWI":
|
|
for l, w, m in OUT:
|
|
if l == lvl: print(f"{l} {w}: {m}")
|
|
e = sum(1 for l, *_ in OUT if l == "E"); w = sum(1 for l, *_ in OUT if l == "W")
|
|
print(f"\n{e} kļūdas, {w} brīdinājumi")
|
|
return 1 if e else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else os.path.join(ROOT, "data")))
|