#!/usr/bin/env python3 """ legislatie_search.py v6 — căutare acte pe legislatie.just.ro (API SOAP, http) + text ÎN VIGOARE și extragere articol (prin lj_core.py, pagini HTML, https). Rulează din orice director: importă lj_core.py din propriul director. python3 legislatie_search.py "OUG 57/2019" # metadate python3 legislatie_search.py "OUG 57/2019" --fetch --articol 485^1 python3 legislatie_search.py "Lege 53/2003" --fetch --grep "concediu de odihn" python3 legislatie_search.py --id 215925 --articol 56 # fără căutare python3 legislatie_search.py "OUG 7/2026" --fetch --api-text # ultim resort, vezi mai jos python3 legislatie_search.py --diagnostic Coduri ieșire: 0 ok | 2 niciun rezultat | 3 HTTPS blocat din acest mediu (ROUTE_BLOCKED) | 4 articol negăsit | 5 pagina fără text | 6 id greșit / eroare portal --api-text: dacă HTTPS e blocat, folosește textul din câmpul `Text` al API-ului SOAP. ATENȚIE: acela e textul PUBLICAT INIȚIAL, fără modificările ulterioare. Se marchează explicit în output și nu se citează ca drept în vigoare. """ import sys import os import re import json import argparse import http.client import xml.etree.ElementTree as ET import uuid import urllib.request import urllib.error import ssl from html.parser import HTMLParser sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import lj_core # noqa: E402 # ── Config ────────────────────────────────────────────────────────────────── SOAP_HOST = "legislatie.just.ro" SOAP_PATH = "/apiws/FreeWebService.svc/SOAP" NS_TEMPURI = "http://tempuri.org/" NS_DC = "http://schemas.datacontract.org/2004/07/FreeWebService" NS_WSA = "http://www.w3.org/2005/08/addressing" CACHE_DIR = lj_core.CACHE_DIR # ── Tipuri de acte: abrevieri -> valoare SearchTitlu ──────────────────────── ACT_TYPE_MAP = { "OUG": "ORDONANTA DE URGENTA", "ORDONANTA DE URGENTA": "ORDONANTA DE URGENTA", "ORDONANȚĂ DE URGENȚĂ": "ORDONANTA DE URGENTA", "OG": "ORDONANTA", "ORDONANTA": "ORDONANTA", "ORDONANȚĂ": "ORDONANTA", "LEGE": "LEGE", "LEGEA": "LEGE", "L": "LEGE", "HG": "HOTARARE", "HOTARARE": "HOTARARE", "HOTĂRÂRE": "HOTARARE", "DECIZIE": "DECIZIE", "DECRET": "DECRET", "ORDIN": "ORDIN", "REGULAMENT": "REGULAMENT", "NORME": "NORME", "INSTRUCTIUNI": "INSTRUCTIUNI", "COD": "COD", } # Abrevieri care implică un emitent anume (preferință, nu filtru strict). AUTO_EMITENT = { "HG": "guvern", "OUG": "guvern", "OG": "guvern", "ORDONANTA DE URGENTA": "guvern", "ORDONANȚĂ DE URGENȚĂ": "guvern", "ORDONANTA": "guvern", "ORDONANȚĂ": "guvern", } def _norm(s: str) -> str: """Normalizează pentru comparații: minuscule, fără diacritice. Atenție: API-ul returnează uneori diacriticele ca „?" (ex. „Camera Deputa?ilor"), așa că eliminăm tot ce nu e literă de bază sau spațiu. """ s = (s or "").lower() for k, v in {"ă": "a", "â": "a", "î": "i", "ș": "s", "ş": "s", "ț": "t", "ţ": "t"}.items(): s = s.replace(k, v) return re.sub(r"[^a-z ]", "", s) def _matches_emitent(result: dict, emitent: str) -> bool: return _norm(emitent) in _norm(result.get("Emitent", "")) def _matches_year(result: dict, year: str) -> bool: for key in ("DataVigoare", "DataActualizare", "Titlu"): if year in result.get(key, ""): return True return False # ── SOAP / căutare (nemodificate față de v1) ──────────────────────────────── def soap_request(action: str, body: str) -> str: """POST SOAP prin urllib (respectă HTTP(S)_PROXY). Încearcă http, apoi https.""" last_err = None for scheme in ("http", "https"): soap_url = f"{scheme}://{SOAP_HOST}{SOAP_PATH}" envelope = ( '' f'' f'' f"{action}" f"urn:uuid:{uuid.uuid4()}" f"{soap_url}" f"" f"{body}" f"" ).encode("utf-8") headers = {"Content-Type": "text/xml; charset=utf-8", "SOAPAction": f'"{action}"', "User-Agent": lj_core.UA} for attempt in range(3): # API-ul are timeout-uri sporadice try: req = urllib.request.Request(soap_url, data=envelope, headers=headers, method="POST") with lj_core.urlopen(req, timeout=60) as resp: return resp.read().decode("utf-8", errors="replace") except urllib.error.HTTPError as e: last_err = e if e.code < 500: break # eroare de cerere: nu insistăm pe schema asta except (urllib.error.URLError, OSError, lj_core.RouteBlocked) as e: last_err = e raise RuntimeError(f"API-ul SOAP nu răspunde (http și https): {last_err}") def get_token() -> str: body = f'' xml_resp = soap_request(f"{NS_TEMPURI}IFreeWebService/GetToken", body) root = ET.fromstring(xml_resp) el = root.find(f".//{{{NS_TEMPURI}}}GetTokenResult") if el is None or not el.text: raise RuntimeError("Nu s-a putut obține token-ul de la API.") return el.text.strip() def search(token, search_titlu=None, search_numar=None, search_an=None, search_text=None, pagina=1, rezultate=10): nil = 'i:nil="true"' d = "d4p1" titlu_xml = f"<{d}:SearchTitlu>{search_titlu}" if search_titlu else f"<{d}:SearchTitlu {nil} />" numar_xml = f"<{d}:SearchNumar>{search_numar}" if search_numar else f"<{d}:SearchNumar {nil} />" an_xml = f"<{d}:SearchAn>{search_an}" if search_an else f"<{d}:SearchAn {nil} />" text_xml = f"<{d}:SearchText>{search_text}" if search_text else f"<{d}:SearchText {nil} />" body = ( f'' f'' f"<{d}:NumarPagina>{pagina}" f"<{d}:RezultatePagina>{rezultate}" f"{an_xml}{numar_xml}{text_xml}{titlu_xml}" f"" f"{token}" f"" ) xml_resp = soap_request(f"{NS_TEMPURI}IFreeWebService/Search", body) root = ET.fromstring(xml_resp) results = [] for legi in root.findall(f".//{{{NS_DC}}}Legi"): entry = {} for child in legi: tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag entry[tag] = (child.text or "").strip() results.append(entry) return results def parse_act_reference(text): text = text.strip() m = re.match(r"^(.+?)\s+(?:nr\.?\s*)?(\d+)\s*/\s*(\d{4})$", text, re.IGNORECASE) if not m: return None tip = m.group(1).strip().upper() return { "SearchTitlu": ACT_TYPE_MAP.get(tip, tip), "SearchNumar": m.group(2), "SearchAn": m.group(3), "_auto_emitent": AUTO_EMITENT.get(tip), } def search_paged(token, search_titlu=None, search_numar=None, search_an=None, search_text=None, emitent=None, max_pages=12, per_page=10): """Parcurge mai multe pagini de rezultate, cu deduplicare. Necesitate: API-ul plafonează RezultatePagina la 10 și NU filtrează după SearchAn, deci actul căutat poate fi pe paginile următoare. Ne oprim devreme dacă am găsit deja un rezultat care trece filtrele (an + emitent). """ seen, collected = set(), [] for pag in range(1, max_pages + 1): page = search(token, search_titlu=search_titlu, search_numar=search_numar, search_an=search_an, search_text=search_text, pagina=pag, rezultate=per_page) new = [r for r in page if r.get("LinkHtml") and r["LinkHtml"] not in seen] if not new: break for r in new: seen.add(r["LinkHtml"]) collected.extend(new) # Fără criterii de filtrare client-side: o singură pagină e de ajuns. if not search_an and not emitent: break # Oprire timpurie: avem deja un rezultat care trece toate filtrele? hit = [r for r in collected if (not search_an or _matches_year(r, search_an)) and (not emitent or _matches_emitent(r, emitent))] if hit: break return collected def format_results(results, top_n=5): if not results: return "Nu s-au găsit rezultate." lines = [] for i, r in enumerate(results[:top_n], 1): titlu = re.sub(r"\s+", " ", r.get("Titlu", "").replace("\ufeff", "").split("EMITENT")[0].strip()) lines.append(f"{i}. {r.get('TipAct', '?')} nr. {r.get('Numar', '?')} — {r.get('Emitent', '?')}") lines.append(f" Titlu: {titlu[:200]}") lines.append(f" Link: {r.get('LinkHtml', 'N/A')}") lines.append(f" Data vigoare: {r.get('DataVigoare', 'N/A')}") lines.append("") return "\n".join(lines) # ── Descărcare text: delegat la lj_core ───────────────────────────────────── def diagnostic(): """Distinge «portalul e picat» de «traseul meu nu poate face HTTPS către portal».""" try: get_token() api = "200" except RuntimeError as e: api = "EROARE: %s" % e try: lj_core.fetch_html("/Public/DetaliiDocumentAfis/5729", timeout=30) html = "200" except lj_core.RouteBlocked as e: html = "EROARE: %s" % e except LookupError as e: html = "EROARE portal: %s" % e print("Diagnostic legislatie.just.ro (python %s, proxy=%s)" % ( sys.version.split()[0], "da" if os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy") else "nu")) print(" API SOAP (http) : %s" % api) print(" pagini HTML (https): %s" % html) api_ok, html_ok = api.startswith("2"), html == "200" if html_ok: print("VERDICT: acces complet. --fetch / lj_core merg din acest mediu.") elif api_ok: print("VERDICT: ROUTE_BLOCKED — căutarea merge, textul NU. Treci la ruta următoare din SKILL.md.") else: print("VERDICT: portalul nu e accesibil deloc de aici (sau e picat). Încearcă altă rută.") return 0 if html_ok else 3 def fetch_act_text(link_html, doc_id=None, out_path=None, no_cache=False, forma_baza=False): """Compatibilitate v5: (cale_fișier, nr_caractere) pentru forma în vigoare.""" meta = lj_core.get_in_force(doc_id or link_html, no_cache=no_cache, forma_baza=forma_baza) path = meta.get("cache") if out_path: with open(out_path, "w", encoding="utf-8") as f: f.write(meta["text"]) path = out_path return path, len(meta["text"]) extract_articol = lj_core.extract_articol # ── CLI ───────────────────────────────────────────────────────────────────── def _print_fetch(meta, args, api_label=None): print(lj_core.header(meta)) if api_label: print(api_label) text = meta["text"] if args.out: with open(args.out, "w", encoding="utf-8") as f: f.write(text) print("SALVAT: %s" % args.out) if args.articol: art = lj_core.extract_articol(text, args.articol) if art is None: print("[!] Articolul %s nu apare în text." % args.articol, file=sys.stderr) return 4 print("\n--- Articolul %s (%s caractere) ---\n%s" % ( lj_core._norm_ref(args.articol), format(len(art), ","), art)) elif args.grep: print("\n" + (lj_core.grep(text, args.grep, args.context) or "(nicio potrivire)")) elif not args.silent: print("\nFolosește --articol , --grep sau grep pe fișierul de mai sus.") return 0 def main(): lj_core.utf8_console() parser = argparse.ArgumentParser(description="Caută/descarcă acte normative pe legislatie.just.ro") parser.add_argument("referinta", nargs="?", help='Referință act, ex: "OUG 57/2019"') parser.add_argument("--tip", help="Tip act (ex: ORDONANTA DE URGENTA, LEGE, HG)") parser.add_argument("--numar", help="Număr act") parser.add_argument("--an", help="An act") parser.add_argument("--text", help="Căutare text liber") parser.add_argument("--emitent", help='Filtru strict de emitent (ex: "Guvernul", "Senatul")') parser.add_argument("--pagini", type=int, default=12, help="Pagini API parcurse la filtrare (implicit 12)") parser.add_argument("--rezultate", type=int, default=10, help="Număr maxim rezultate afișate") parser.add_argument("--json", action="store_true", help="Metadate JSON (fără câmpul Text)") parser.add_argument("--id", help="Sari peste căutare: id sau link DetaliiDocument/") parser.add_argument("--fetch", action="store_true", help="Descarcă textul ÎN VIGOARE al primului rezultat") parser.add_argument("--articol", help="Extrage articolul (ex: 485^1, 12, XLIX, unic); implică --fetch") parser.add_argument("--grep", help="Regex căutat în text; implică --fetch") parser.add_argument("--context", type=int, default=1, help="Linii de context pentru --grep") parser.add_argument("--out", help="Salvează textul și aici") parser.add_argument("--silent", "--info", action="store_true", help="Doar antetul (forma, data), fără text") parser.add_argument("--no-cache", action="store_true", help="Ignoră cache-ul (TTL 6h)") parser.add_argument("--forma-baza", action="store_true", help="Forma publicată inițial, nu cea în vigoare") parser.add_argument("--exact", action="store_true", help="Cu --id: exact consolidarea istorică indicată") parser.add_argument("--api-text", action="store_true", help="Dacă HTTPS e blocat: text din API (FORMA PUBLICATĂ INIȚIAL, marcată ca atare)") parser.add_argument("--diagnostic", action="store_true", help="Testează accesul și spune ce rută merge") parser.add_argument("--version", action="store_true") args = parser.parse_args() if args.version: print("legislatie_search 6 / lj_core", lj_core.VERSION) return 0 if args.diagnostic: return diagnostic() if args.articol or args.grep or args.silent: args.fetch = True if args.id: try: meta = lj_core.get_in_force(args.id, no_cache=args.no_cache, forma_baza=args.forma_baza, exact=args.exact) except lj_core.RouteBlocked as e: print("ROUTE_BLOCKED: %s" % e, file=sys.stderr) return 3 except LookupError as e: print(str(e), file=sys.stderr) return 6 return _print_fetch(meta, args) search_params = {} if args.referinta: parsed = parse_act_reference(args.referinta) search_params = parsed if parsed else {"search_text": args.referinta} else: if args.tip: search_params["search_titlu"] = ACT_TYPE_MAP.get(args.tip.strip().upper(), args.tip.strip().upper()) if args.numar: search_params["search_numar"] = args.numar if args.an: search_params["search_an"] = args.an if args.text: search_params["search_text"] = args.text if not search_params: parser.print_help() return 1 token = get_token() key_map = {"SearchTitlu": "search_titlu", "SearchNumar": "search_numar", "SearchAn": "search_an", "search_titlu": "search_titlu", "search_numar": "search_numar", "search_an": "search_an", "search_text": "search_text"} kwargs = {key_map[k]: v for k, v in search_params.items() if k in key_map} auto_emitent = search_params.get("_auto_emitent") if not auto_emitent and args.tip: auto_emitent = AUTO_EMITENT.get(args.tip.strip().upper()) effective_emitent = args.emitent or auto_emitent results = search_paged(token, emitent=effective_emitent, max_pages=args.pagini, **kwargs) # API-ul NU respectă SearchAn: filtrăm client-side. year = kwargs.get("search_an") if year: filtered = [r for r in results if _matches_year(r, year)] if filtered: results = filtered if effective_emitent: filtered = [r for r in results if _matches_emitent(r, effective_emitent)] if args.emitent or filtered: # explicit = strict; automat = doar dacă nu golește lista results = filtered if not args.fetch: if args.json: print(json.dumps([{k: v for k, v in r.items() if k != "Text"} for r in results], ensure_ascii=False, indent=2)) else: print(format_results(results, top_n=args.rezultate)) return 0 if results else 2 if not results or not results[0].get("LinkHtml"): print("Nu s-au găsit rezultate — nimic de descărcat.", file=sys.stderr) return 2 first = results[0] titlu = re.sub(r"\s+", " ", first.get("Titlu", "").replace("\ufeff", "").split("EMITENT")[0]).strip()[:160] print("REZULTAT: %s nr. %s — %s | %s" % (first.get("TipAct", "?"), first.get("Numar", "?"), first.get("Emitent", "?"), titlu)) try: meta = lj_core.get_in_force(first["LinkHtml"], no_cache=args.no_cache, forma_baza=args.forma_baza) except lj_core.RouteBlocked as e: if not (args.api_text and first.get("Text")): print("ROUTE_BLOCKED: HTTPS către %s nu merge din acest mediu (%s).\n" "Treci la ruta următoare din SKILL.md; ca ultim resort: --api-text." % (SOAP_HOST, e), file=sys.stderr) return 3 raw = first["Text"] text = lj_core.html_to_text(raw) if "<" in raw[:2000] else lj_core.clean_text(raw) meta = {"id": lj_core.doc_id_of(first["LinkHtml"]), "url": first["LinkHtml"], "forma": "PUBLICATĂ INIȚIAL (din API) — NU include modificările ulterioare", "text": text} return _print_fetch(meta, args, api_label=( "ATENȚIE: HTTPS blocat; text din API = forma publicată inițial. Nu îl cita ca drept în " "vigoare decât dacă ai verificat că actul nu a fost modificat.")) except LookupError as e: print(str(e), file=sys.stderr) return 6 return _print_fetch(meta, args) if __name__ == "__main__": sys.exit(main())