likumi.lv identifikators (shēmā jau bija, neobligāts) tagad ir katrā likumā — no kataloga 58 failos (katram DataVersion ieraksts); tools/convert.py to raksta HTML avotam. Teksta pilnības skaitā identifikators netiek skaitīts. Vajadzīgs, lai nolikumu atsauces (Nolikumi-as-Code Cite@likumiId) savienotu ar likumu. validate: 58/58 OK. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016igpMJBtapmG6vHE4vSF9q
423 lines
22 KiB
Python
423 lines
22 KiB
Python
"""convert.py — likumi.lv konsolidētā likuma Word (.docx) vai lapa (HTML) → likumi-0.1 XML.
|
|
|
|
Lieto: python3 convert.py <likums.docx|<likumi_id>.html> <izvade.xml>
|
|
HTML lapu un grozījumu lapas lejupielādē: python3 tools/html_source.py fetch <likumi_id> <kešs>
|
|
Strukturē pēc Word stiliem un sarakstu numerācijas (nevis ar MI). Neskaidros gadījumus
|
|
ieraksta verification JSON (`<izvade>.verify.json`), nevis min.
|
|
"""
|
|
import sys, os, re, json, hashlib, datetime
|
|
from lxml import etree
|
|
|
|
NS = "urn:pppa:vpk:likumi:0.1"
|
|
W = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}"
|
|
SUP = str.maketrans("0123456789", "⁰¹²³⁴⁵⁶⁷⁸⁹")
|
|
ROMAN = {"I": 1, "V": 5, "X": 10, "L": 50}
|
|
MONTHS = {"janvār": 1, "februār": 2, "mart": 3, "aprīl": 4, "maij": 5, "jūnij": 6, "jūlij": 7,
|
|
"august": 8, "septembr": 9, "oktobr": 10, "novembr": 11, "decembr": 12}
|
|
|
|
def roman(s):
|
|
v = 0
|
|
for i, c in enumerate(s):
|
|
x = ROMAN[c]; v += -x if i + 1 < len(s) and ROMAN[s[i + 1]] > x else x
|
|
return v
|
|
|
|
def dmy(s): # 03.04.2026 -> 2026-04-03
|
|
d, m, y = s.split("."); return f"{y}-{m}-{d}"
|
|
|
|
def lvdate(s): # 2000.gada 19.oktobrī
|
|
m = re.search(r"(\d{4})\.\s*gada\s+(\d{1,2})\.\s*([a-zēūīāšžčņļķģ]+)", s)
|
|
if not m: return None
|
|
mon = next(v for k, v in MONTHS.items() if m.group(3).startswith(k))
|
|
return f"{m.group(1)}-{mon:02d}-{int(m.group(2)):02d}"
|
|
|
|
def text_of(p):
|
|
"""Paragraph text; superscript digits → Unicode superscript (4¹)."""
|
|
if hasattr(p, "_num"): return p.text # html_source.Para
|
|
out = []
|
|
for r in p.runs:
|
|
t = r.text
|
|
out.append(t.translate(SUP) if r.font.superscript and t.strip().isdigit() else t)
|
|
return re.sub(r"\s+", " ", "".join(out)).strip()
|
|
|
|
def sup_suffix(n): # "4¹" -> ("4","1")
|
|
base = re.match(r"\d+", n).group(0)
|
|
rest = n[len(base):].translate(str.maketrans("⁰¹²³⁴⁵⁶⁷⁸⁹", "0123456789"))
|
|
return base, rest
|
|
|
|
def idpart(prefix, n):
|
|
b, s = sup_suffix(n)
|
|
letter = s[-1].lower() if s[-1:].isalpha() else "" # "644¹⁸A" → art644s18a
|
|
s = s.rstrip("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
|
return f"{prefix}{b}" + (f"s{s}" if s else "") + letter
|
|
|
|
class Numbering:
|
|
def __init__(self, d):
|
|
self.num, self.abs = {}, {}
|
|
el = d.part.numbering_part.element
|
|
for a in el.findall(W + "abstractNum"):
|
|
self.abs[a.get(W + "abstractNumId")] = {l.get(W + "ilvl"): int(l.find(W + "start").get(W + "val")) if l.find(W + "start") is not None else 1 for l in a.findall(W + "lvl")}
|
|
for n in el.findall(W + "num"):
|
|
starts = dict(self.abs.get(n.find(W + "abstractNumId").get(W + "val"), {}))
|
|
for o in n.findall(W + "lvlOverride"):
|
|
so = o.find(W + "startOverride")
|
|
if so is not None: starts[o.get(W + "ilvl")] = int(so.get(W + "val"))
|
|
self.num[str(n.get(W + "numId"))] = starts
|
|
self.count = {}
|
|
|
|
def next(self, numId, ilvl):
|
|
numId = str(numId)
|
|
c = self.count.setdefault(numId, {})
|
|
c[ilvl] = c.get(ilvl, self.num.get(numId, {}).get(str(ilvl), 1) - 1) + 1
|
|
for k in list(c):
|
|
if k > ilvl: del c[k]
|
|
return c[ilvl]
|
|
|
|
def celex(t):
|
|
"""CELEX pirmajam ES aktam tekstā: direktīva 2008/114/EK (L) vai kopš 2015 — direktīva/regula/lēmums (ES) 2022/2557.
|
|
Veco regulu numurs (EK) Nr. 1049/2001 (numurs/gads) netiek minēts — atstāj bez CELEX."""
|
|
for m in re.finditer(r"\((?:ES|ES, Euratom)\)\s*(\d{4})/(\d+)|\b(\d{4})/(\d+)/(?:EK|ES|EEK)\b", t):
|
|
y, n = (m.group(1), m.group(2)) if m.group(1) else (m.group(3), m.group(4))
|
|
before = t[max(0, m.start() - 60):m.start()].lower()
|
|
k = max(("L", before.rfind("direktīv")), ("R", before.rfind("regul")), ("D", before.rfind("lēmum")), key=lambda x: x[1])
|
|
kind = k[0] if k[1] >= 0 else ("L" if m.group(3) else None)
|
|
if kind: return f"3{y}{kind}{int(n):04d}"
|
|
return None
|
|
|
|
|
|
def numpr(p):
|
|
if hasattr(p, "_num"): return p._num # html_source.Para: (drukātais numurs, līmenis)
|
|
pPr = p._p.pPr
|
|
if pPr is None or pPr.numPr is None or pPr.numPr.numId is None: return None
|
|
return pPr.numPr.numId.val, (pPr.numPr.ilvl.val if pPr.numPr.ilvl is not None else 0)
|
|
|
|
NOTE = re.compile(r"^\((Ar grozījumiem|\d{2}\.\d{2}\.\d{4}\.? likuma redakcijā|Pants\b|Daļa\b|Punkts\b|Nodaļ)")
|
|
DELETED = re.compile(r"^\((Izslēgt|izslēgt)")
|
|
ART = re.compile(r"^(\d+)\.?([⁰¹²³⁴⁵⁶⁷⁸⁹]*[A-Z]?)\.?\s*pants(?:\.|(?=\s|$))\s*(.*)$")
|
|
AP_SIG = re.compile(r"^((?:Latvijas Republikas )?Augstākās Padomes (?:priekšsēdētāj|sekretār|priekšsēdētāja vietniek)\S*)\s+(.+)$")
|
|
SECT = re.compile(r"^([A-Z])\s+sadaļa\.?\s*(.*)$") # sadaļa virs nodaļām (PPP likums, kapitāla daļu likums)
|
|
CHAP = re.compile(r"^([IVXL]+)([⁰¹²³⁴⁵⁶⁷⁸⁹]*|\s[A-C](?=\s))\s+nodaļa\.?\s*(.*)$")
|
|
INLINE_PART = re.compile(r"^\((\d+[⁰¹²³⁴⁵⁶⁷⁸⁹]+)\)\s*(.*)$")
|
|
INLINE_POINT = re.compile(r"^(\d+[⁰¹²³⁴⁵⁶⁷⁸⁹]*)\)\s*(.*)$")
|
|
|
|
def E(parent, tag, text=None, **attrs):
|
|
e = etree.SubElement(parent, f"{{{NS}}}{tag}", {k: str(v) for k, v in attrs.items() if v is not None})
|
|
if text is not None: e.text = text
|
|
return e
|
|
|
|
def convert(path, out):
|
|
if path.endswith(".html"):
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import html_source
|
|
ps, nb, raw, src_name, issues = html_source.load(path)
|
|
src_kind = "likumi.lv konsolidētās redakcijas lapas (HTML)"
|
|
pase_adopted = html_source.pase(open(path, encoding="utf-8").read()).get("Pieņemts", "")
|
|
else:
|
|
import docx
|
|
d = docx.Document(path)
|
|
ps = [p for p in d.paragraphs]
|
|
nb = Numbering(d)
|
|
issues = []
|
|
raw = open(path, "rb").read()
|
|
src_name = path.split("/")[-1]
|
|
src_kind = "likumi.lv konsolidētā teksta (Word)"
|
|
pase_adopted = ""
|
|
|
|
# --- galvene (likumi.lv) ---
|
|
lines = [text_of(p) for p in ps]
|
|
head = " ".join(lines[:4])
|
|
issuer = re.search(r"Izdevējs:\s*(\S+)", head).group(1)
|
|
typ = re.search(r"Veids:\s*(\S+)", head).group(1)
|
|
pub = re.search(r"Latvijas Vēstnesis,\s*([\d/]+)", head)
|
|
asof = dmy(re.search(r"Attēlotā redakcija:\s*(\d{2}\.\d{2}\.\d{4})", head).group(1))
|
|
i = 4
|
|
amend_txt = []
|
|
if lines[i].startswith("Grozījumi"):
|
|
i += 1
|
|
while lines[i] and not lines[i].startswith("Saeima ir pieņēmusi"):
|
|
amend_txt.append(lines[i]); i += 1
|
|
while not lines[i]: i += 1
|
|
formula = None
|
|
if lines[i].startswith("Saeima ir pieņēmusi"):
|
|
formula = lines[i]; i += 1
|
|
title = lines[i]; i += 1
|
|
|
|
# --- noslēgums (no beigām) ---
|
|
tail = {}
|
|
j = len(lines) - 1
|
|
while j > i:
|
|
t = lines[j]
|
|
if not t or t.startswith("©"): j -= 1; continue
|
|
if t.startswith("Rīgā"): tail["prom"] = t
|
|
elif re.match(r"^Valsts prezident\S*\s+vietā$", t): tail["sig_for"] = t
|
|
elif re.match(r"^(Valsts prezident|Saeimas priekšsēdētāj)", t): tail["sig"] = t
|
|
elif t.startswith("Likums Saeimā pieņemts"): tail["adopt"] = t
|
|
elif AP_SIG.match(t): tail.setdefault("ap_sigs", []).insert(0, t)
|
|
elif t.startswith("Likums stājas spēkā"): tail["inforce"] = t
|
|
else: break
|
|
j -= 1
|
|
end = j + 1
|
|
adopted = lvdate(tail.get("adopt", ""))
|
|
if not adopted and pase_adopted:
|
|
adopted = dmy(pase_adopted.rstrip("."))
|
|
issues.append(f"Pieņemšanas datums ņemts no likumi.lv pases: {pase_adopted}")
|
|
adopted = adopted or "1900-01-01"
|
|
if adopted == "1900-01-01": issues.append("Pieņemšanas datums nav atrasts")
|
|
doc_id = f"lv-lik-{adopted}"
|
|
# divi likumi, pieņemti vienā dienā: otrais saņem -2 (… -9); izšķir likumi.yaml pēc likumi_id
|
|
lid = os.path.basename(path)[:-5] if path.endswith(".html") else None
|
|
if lid and os.path.exists("likumi.yaml"):
|
|
import yaml
|
|
taken = {e["id"]: str(e.get("likumi_id")) for e in yaml.safe_load(open("likumi.yaml", encoding="utf-8"))}
|
|
base, k = doc_id, 1
|
|
while doc_id in taken and taken[doc_id] != lid:
|
|
k += 1
|
|
if k > 9: raise SystemExit(f"Vairāk nekā 9 likumi pieņemti {adopted}")
|
|
doc_id = f"{base}-{k}"
|
|
if k > 1: issues.append(f"Tajā pašā dienā pieņemts cits likums — ID {doc_id}")
|
|
|
|
root = etree.Element(f"{{{NS}}}Law", nsmap={None: NS}, id=doc_id, schemaVersion="0.1")
|
|
act = E(root, "Act")
|
|
E(act, "Title", title); E(act, "Issuer", issuer); E(act, "Type", typ); E(act, "Adopted", adopted)
|
|
E(act, "Published", "Latvijas Vēstnesis" + (f", {pub.group(1)}" if pub else ""), issue=pub.group(1) if pub else None)
|
|
if "inforce" in tail:
|
|
f = lvdate(tail["inforce"])
|
|
if f: E(act, "InForce", f)
|
|
tv = E(root, "TextVersion", basis="consolidated", asOf=asof)
|
|
for m in re.finditer(r"(\d{2}\.\d{2}\.\d{4})\. likums / (.*?) / Stājas spēkā (\d{2}\.\d{2}\.\d{4})\.", " ".join(amend_txt)):
|
|
E(tv, "Amendment", f"{m.group(1)}. likums", adopted=dmy(m.group(1)), published=m.group(2).strip(), inForce=dmy(m.group(3)))
|
|
src = E(root, "Source")
|
|
E(src, "File", src_name); E(src, "SHA256", hashlib.sha256(raw).hexdigest())
|
|
E(src, "Retrieved", datetime.date.today().isoformat())
|
|
if formula: E(root, "EnactingFormula", formula)
|
|
body = E(root, "Body")
|
|
|
|
# --- korpuss ---
|
|
chap = art = part = point = None
|
|
container = body
|
|
chap_parent = body # sadaļa, ja likumā tādas ir
|
|
tp = eu = None
|
|
mode = "body"
|
|
last = None # pēdējā vienība, kurai pievienot piezīmi
|
|
open_note = None
|
|
chap_no = 0
|
|
pending_chap = None
|
|
used = set()
|
|
parts_seen = []
|
|
|
|
def uid(x):
|
|
if x in used: issues.append(f"Dubults ID {x}")
|
|
used.add(x); return x
|
|
|
|
def new_art(n, heading):
|
|
nonlocal art, part, point, parts_seen
|
|
art = E(container, "Article", id=uid(f"{doc_id}.{idpart('art', n)}"), n=n)
|
|
if heading: E(art, "Heading", heading)
|
|
part = point = None; parts_seen = []
|
|
return art
|
|
|
|
def add_note(t):
|
|
nonlocal open_note
|
|
tgt = last if last is not None else art
|
|
if tgt is None: issues.append(f"Piezīme bez vienības: {t[:60]}"); return
|
|
open_note = E(tgt, "Note", t)
|
|
|
|
for k in range(i, end):
|
|
p = ps[k]; t = lines[k]
|
|
if not t: continue
|
|
# turpinājuma rinda nepabeigtai piezīmei "(Ar grozījumiem ... " bez ")"
|
|
if open_note is not None and open_note.text.count("(") > open_note.text.count(")"):
|
|
open_note.text += " " + t; continue
|
|
open_note = None
|
|
st = p.style.name
|
|
if pending_chap is not None and st.startswith("Heading 1") and not CHAP.match(t):
|
|
E(pending_chap, "Title", t); pending_chap = None; continue
|
|
pending_chap = None
|
|
ms = SECT.match(t)
|
|
if ms and st.startswith("Heading 1"):
|
|
chap_parent = sect = E(body, "Chapter", id=uid(f"{doc_id}.s{ms.group(1)}"), n=ms.group(1), kind="division")
|
|
if ms.group(2): E(sect, "Title", ms.group(2))
|
|
else: pending_chap = sect
|
|
container = sect; mode = "body"; art = last = None; continue
|
|
if st.startswith("Heading 1") or (CHAP.match(t) and len(t) < 160):
|
|
m = CHAP.match(t)
|
|
chap_no = roman(m.group(1)) if m else chap_no + 1
|
|
if not m: issues.append(f"Nodaļai nav numura virsrakstā, pieņemts {chap_no}: {t[:50]}")
|
|
ttl = m.group(3) if m else t
|
|
suf = (m.group(2) or "").strip() if m else ""
|
|
# iestarpināta nodaļa: II¹ → c2.1 (n 2¹); V A → c5.1 (n 5A); V B → c5.2
|
|
sub = (int(suf.translate(str.maketrans("⁰¹²³⁴⁵⁶⁷⁸⁹", "0123456789"))) if suf[:1] in "⁰¹²³⁴⁵⁶⁷⁸⁹" else "ABC".index(suf) + 1) if suf else None
|
|
chap = E(chap_parent, "Chapter", id=uid(f"{doc_id}.c{chap_no}" + (f".{sub}" if sub else "")), n=str(chap_no) + suf,
|
|
kind="general" if chap_no == 1 and "Vispārīg" in ttl else "substantive")
|
|
if ttl: E(chap, "Title", ttl)
|
|
else: pending_chap = chap
|
|
container = chap; mode = "body"; art = last = None; continue
|
|
if t == "Pārejas noteikumi":
|
|
tp = E(root, "TransitionalProvisions", id=uid(f"{doc_id}.tp")); E(tp, "Title", t)
|
|
mode = "tp"; last = None; continue
|
|
if t.startswith("Informatīva atsauce uz Eiropas Savienības"):
|
|
eu = E(root, "EuReference", id=uid(f"{doc_id}.eu")); E(eu, "Title", t)
|
|
mode = "eu"; last = None; continue
|
|
if mode == "eu":
|
|
if numpr(p) is None and len(eu.findall(f"{{{NS}}}Item")) == 0:
|
|
E(eu, "Lead", t); continue
|
|
if numpr(p) is None:
|
|
it = eu.findall(f"{{{NS}}}Item")[-1]; it.find(f"{{{NS}}}Text").text += " " + t; continue
|
|
n = len(eu.findall(f"{{{NS}}}Item")) + 1
|
|
it = E(eu, "Item", id=uid(f"{doc_id}.eu.{n}"))
|
|
cx = celex(t)
|
|
if cx: it.set("celex", cx)
|
|
E(it, "Text", t); continue
|
|
if mode == "tp":
|
|
np_ = numpr(p)
|
|
if np_ and np_[1] == 0:
|
|
n = str(nb.next(*np_))
|
|
last = point = E(tp, "Point", id=uid(f"{doc_id}.tp.{idpart('p', n)}"), n=n,
|
|
kind="deleted" if DELETED.match(t) else "transitional",
|
|
status="deleted" if DELETED.match(t) else None)
|
|
E(point, "Text", t); continue
|
|
m = INLINE_POINT.match(t)
|
|
if (np_ or m) and point is not None:
|
|
if np_ and last is not None and last.getparent() is not tp and last is not point:
|
|
parent = last if INLINE_POINT.match(lines[k - 1] or "") else last.getparent()
|
|
else:
|
|
parent = point
|
|
n = m.group(1) if m and not np_ else str(nb.next(*np_))
|
|
body_t = m.group(2) if m and not np_ else t
|
|
sp = E(parent, "Point", id=uid(f"{parent.get('id')}.{idpart('p', n)}"), n=n)
|
|
E(sp, "Text", body_t); last = sp; continue
|
|
if NOTE.match(t): last = point if point is not None else last; add_note(t); continue
|
|
if point is not None and point.find(f"{{{NS}}}Text") is not None:
|
|
point.find(f"{{{NS}}}Text").text += "\n" + t
|
|
issues.append(f"Pārejas noteikumu {point.get('n')}. punkts: nenumurēta rindkopa pievienota punktam"); continue
|
|
issues.append(f"Pārejas noteikumos neklasificēta rinda: {t[:70]}"); continue
|
|
# --- pants ---
|
|
m = ART.match(t)
|
|
if m and (st.startswith("Heading 2") or numpr(p) is None) and len(m.group(2)) < 200:
|
|
new_art(m.group(1) + m.group(2), m.group(3) or None); last = art; continue
|
|
if art is None and chap is not None and (NOTE.match(t) or DELETED.match(t)):
|
|
nt = etree.Element(f"{{{NS}}}Note"); nt.text = t
|
|
chap.insert(1, nt); open_note = nt; continue
|
|
if art is None:
|
|
issues.append(f"Teksts ārpus panta: {t[:70]}"); continue
|
|
if NOTE.match(t):
|
|
last = art; add_note(t); continue
|
|
np_ = numpr(p)
|
|
mi = INLINE_PART.match(t) if np_ is None else None
|
|
if (np_ and np_[1] == 0) or mi:
|
|
if mi:
|
|
n, txt = mi.group(1), mi.group(2)
|
|
else:
|
|
v = nb.next(*np_); n = str(v); txt = t
|
|
if parts_seen and not getattr(nb, "explicit", False): # Word numerācija "41" pēc "4" = 4¹
|
|
prev = parts_seen[-1]
|
|
pb = sup_suffix(prev)[0]
|
|
if v != int(pb) + 1 and str(v).startswith(pb) and len(str(v)) > len(pb):
|
|
n = pb + str(v)[len(pb):].translate(SUP)
|
|
issues.append(f"{art.get('n')}. pants: numurs {v} interpretēts kā {n}")
|
|
if art.find(f"{{{NS}}}Text") is not None:
|
|
issues.append(f"{art.get('n')}. pantā teksts un daļas sajaukti")
|
|
dele = bool(DELETED.match(txt))
|
|
part = E(art, "Part", id=uid(f"{art.get('id')}.{idpart('d', n)}"), n=n,
|
|
kind="deleted" if dele else None, status="deleted" if dele else None)
|
|
E(part, "Text", txt); parts_seen.append(n); point = None; last = part; continue
|
|
if np_ and np_[1] >= 1:
|
|
parent = part if part is not None else art
|
|
if parent is art and art.find(f"{{{NS}}}Text") is None:
|
|
issues.append(f"{art.get('n')}. pants: punkti bez ievadteksta")
|
|
n = str(nb.next(*np_))
|
|
if np_[1] >= 2 and point is not None: parent = point
|
|
dele = bool(DELETED.match(t))
|
|
pt = E(parent, "Point", id=uid(f"{parent.get('id')}.{idpart('p', n)}"), n=n,
|
|
kind="deleted" if dele else None, status="deleted" if dele else None)
|
|
E(pt, "Text", t)
|
|
if np_[1] == 1: point = pt
|
|
last = pt; continue
|
|
# nenumurēta rindkopa pantā
|
|
if art.find(f"{{{NS}}}Part") is None and art.find(f"{{{NS}}}Text") is None:
|
|
dele = bool(DELETED.match(t))
|
|
E(art, "Text", t)
|
|
if dele: art.set("status", "deleted")
|
|
last = art; continue
|
|
tgt = last if last is not None else art
|
|
tx = tgt.find(f"{{{NS}}}Text")
|
|
if tx is not None:
|
|
tx.text += "\n" + t; issues.append(f"{art.get('n')}. pants: nenumurēta rindkopa pievienota iepriekšējai vienībai")
|
|
else:
|
|
issues.append(f"Neklasificēta rinda: {t[:70]}")
|
|
|
|
# piezīmes jābūt pēc teksta/punktiem: sakārtot
|
|
for el in root.iter(f"{{{NS}}}Article", f"{{{NS}}}Part", f"{{{NS}}}Point"):
|
|
notes = el.findall(f"{{{NS}}}Note")
|
|
for nt in notes: el.remove(nt); el.append(nt)
|
|
if "inforce" in tail: E(root, "InForceStatement", tail["inforce"])
|
|
if "adopt" in tail: E(root, "Adoption", tail["adopt"])
|
|
if "prom" in tail: E(root, "Promulgation", tail["prom"])
|
|
if "sig" in tail:
|
|
role, name = re.match(r"^(Valsts prezident\S*|Saeimas priekšsēdētāj\S*)\s+(.*)$", tail["sig"]).groups()
|
|
if "sig_for" in tail: role += f" ({tail['sig_for']})"
|
|
E(root, "Signatory", name, role=role)
|
|
for t in tail.get("ap_sigs", []):
|
|
role, name = AP_SIG.match(t).groups()
|
|
E(root, "Signatory", name, role=role)
|
|
# pants, kas pieņemts, bet vēl nav spēkā: likumi.lv rāda tikai nosaukumu un piezīmi
|
|
for a in root.iter(f"{{{NS}}}Article"):
|
|
if a.find(f"{{{NS}}}Text") is None and a.find(f"{{{NS}}}Part") is None:
|
|
nts = " ".join(x.text or "" for x in a.findall(f"{{{NS}}}Note"))
|
|
if re.search(r"stājas spēkā \d{2}\.\d{2}\.\d{4}\.? un iekļauts likuma redakcijā", nts):
|
|
a.set("status", "pending"); issues.append(f"{a.get('n')}. pants vēl nav spēkā — bez teksta (status pending)")
|
|
else:
|
|
issues.append(f"{a.get('n')}. pantam nav teksta")
|
|
# likumi.lv identifikators (HTML avots)
|
|
if lid:
|
|
act_el = root.find(f"{{{NS}}}Act")
|
|
if act_el.find(f"{{{NS}}}LikumiId") is None:
|
|
E(act_el, "LikumiId", lid); E(act_el, "Url", f"https://likumi.lv/ta/id/{lid}")
|
|
# pielikumi (tikai HTML avots)
|
|
ann_chars = 0
|
|
for idx, a in enumerate(getattr(nb, "annexes", []), 1):
|
|
mn = re.search(r"(\d+)\.?\s*([⁰¹²³⁴⁵⁶⁷⁸⁹]*)\.?\s*pielikums", a["heading"])
|
|
an = (mn.group(1) + mn.group(2)) if mn else str(idx)
|
|
ann = E(root, "Annex", id=uid(f"{doc_id}.{idpart('ann', an)}"), n=an)
|
|
E(ann, "Heading", a["heading"])
|
|
if a["title"]: E(ann, "Title", a["title"])
|
|
for ln in a["lines"]: E(ann, "Line", ln)
|
|
ann_chars += sum(len(re.sub(r"\s", "", x)) for x in [a["heading"], a["title"] or "", *a["lines"]])
|
|
# viena ES akta atsauce bez saraksta: ievadteikums ir vienīgais punkts
|
|
for eu in root.findall(f"{{{NS}}}EuReference"):
|
|
lead = eu.find(f"{{{NS}}}Lead")
|
|
if lead is not None and not eu.findall(f"{{{NS}}}Item"):
|
|
eu.remove(lead)
|
|
it = E(eu, "Item", id=uid(f"{doc_id}.eu.1"))
|
|
cx = celex(lead.text)
|
|
if cx: it.set("celex", cx)
|
|
E(it, "Text", lead.text)
|
|
# elementu secība saskaņā ar XSD
|
|
order = ["Act", "TextVersion", "Source", "EnactingFormula", "Body", "TransitionalProvisions", "EuReference",
|
|
"InForceStatement", "Adoption", "Promulgation", "Signatory", "Annex", "DataVersion"]
|
|
kids = list(root)
|
|
for c in kids: root.remove(c)
|
|
for name in order:
|
|
for c in kids:
|
|
if etree.QName(c).localname == name: root.append(c)
|
|
dv = E(root, "DataVersion", number=1, date=datetime.date.today().isoformat())
|
|
E(dv, "Change", f"Pirmā versija: konvertēts no {src_kind} ar tools/convert.py")
|
|
|
|
etree.ElementTree(root).write(out, encoding="UTF-8", xml_declaration=True, pretty_print=True)
|
|
# pilnīguma pārbaude: avota teksta zīmes (bez galvenes) pret XML tekstu
|
|
def body_chars(x): # numuri, kas kļūst par atribūtiem (Article@n, Chapter@n), netiek skaitīti
|
|
x = re.sub(r"^\d+\.?[⁰¹²³⁴⁵⁶⁷⁸⁹]*[A-Z]?\.?\s*pants(?:\.|(?=\s|$))", "", x)
|
|
x = re.sub(r"^[IVXL]+([⁰¹²³⁴⁵⁶⁷⁸⁹]*|\s[A-C])?\s+nodaļa\.?", "", x)
|
|
return len(re.sub(r"\s", "", x))
|
|
src_chars = sum(body_chars(x) for x in lines[i - 1:end] if x) + ann_chars
|
|
xml_chars = sum(len(re.sub(r"\s", "", x)) for x in root.itertext()
|
|
if x) - sum(len(re.sub(r"\s", "", e.text or "")) for e in root.iter(f"{{{NS}}}Change", f"{{{NS}}}File", f"{{{NS}}}SHA256", f"{{{NS}}}Retrieved", f"{{{NS}}}Issuer", f"{{{NS}}}Type", f"{{{NS}}}Adopted", f"{{{NS}}}Published", f"{{{NS}}}InForce", f"{{{NS}}}Amendment", f"{{{NS}}}Signatory", f"{{{NS}}}EnactingFormula",
|
|
f"{{{NS}}}Adoption", f"{{{NS}}}InForceStatement", f"{{{NS}}}Promulgation",
|
|
f"{{{NS}}}LikumiId", f"{{{NS}}}Url")) # noslēgums nav avota skaitā
|
|
counts = {k: len(list(root.iter(f"{{{NS}}}{k}"))) for k in ["Chapter", "Article", "Part", "Point", "Note", "Amendment"]}
|
|
rep = {"id": doc_id, "counts": counts, "sourceChars": src_chars, "xmlChars": xml_chars, "issues": issues}
|
|
json.dump(rep, open(out.replace(".xml", ".verify.json"), "w"), ensure_ascii=False, indent=1)
|
|
return rep
|
|
|
|
if __name__ == "__main__":
|
|
print(json.dumps(convert(sys.argv[1], sys.argv[2]), ensure_ascii=False, indent=1))
|