Source code for lalandre_core.utils.regulatory_level

"""
Regulatory level inference from act metadata.

EU financial regulation follows a 3-level hierarchy:
  1 (L1) — Framework legislation (Regulations, Directives) adopted by Parliament/Council
  2 (L2) — Delegated/implementing acts (RTS, ITS) adopted by the Commission
  3 (L3) — Supervisory guidance (Guidelines, Q&A, Recommendations) by ESA (EBA/ESMA/EIOPA)
"""

import re
from typing import Optional

_EURLEX_RE = re.compile(r"^3\d{4}([A-Z])\d{4}$")

_L2_TITLE_PATTERNS = re.compile(
    r"commission\s+(?:delegated|implementing)"
    r"|regulatory\s+technical\s+standard"
    r"|implementing\s+technical\s+standard"
    r"|\bRTS\b|\bITS\b",
    re.IGNORECASE,
)

_ESA_PREFIX_RE = re.compile(r"^E(?:BA|SMA|IOPA)-", re.IGNORECASE)

_ESA_L2_RE = re.compile(r"\bITS\b|\bRTS\b|technical\s+standard", re.IGNORECASE)
_ESA_L3_RE = re.compile(
    r"\bGL\b|guideline|recommendation|Q\s*&\s*A|\bopinion\b",
    re.IGNORECASE,
)

LEVEL_LABELS = {1: "L1", 2: "L2", 3: "L3"}


[docs] def infer_regulatory_level( celex: str, act_type: str, title: Optional[str] = None, form_number: Optional[str] = None, ) -> Optional[int]: """Infer regulatory level from act metadata. Returns ``1`` (L1), ``2`` (L2), ``3`` (L3), or ``None`` (outside scope). """ if not celex: return None title = title or "" form_number = form_number or "" searchable = f"{title} {form_number}" # ── EUR-Lex acts (3YYYYXNNNN) ──────────────────────────────────── m = _EURLEX_RE.match(celex) if m: type_letter = m.group(1) if type_letter in ("R", "L"): # Regulation / Directive # L2 override: Commission delegated/implementing acts if _L2_TITLE_PATTERNS.search(searchable): return 2 return 1 if type_letter == "D": # Decision — typically Commission implementing return 2 if type_letter == "H": # Recommendation return 3 # Other EUR-Lex type letters (rare): conservative return None # ── ESA documents (EBA-*, ESMA-*, EIOPA-*) ────────────────────── if _ESA_PREFIX_RE.match(celex): esa_searchable = f"{celex} {searchable}" if _ESA_L2_RE.search(esa_searchable): return 2 if _ESA_L3_RE.search(esa_searchable): return 3 # Default ESA → L3 (majority of ESA output is guidance) return 3 # ── AMF, Legifrance, others — outside Lamfalussy scope ────────── return None
[docs] def level_to_label(level: Optional[int]) -> Optional[str]: """Convert numeric level to display label: 1→'L1', 2→'L2', 3→'L3'.""" return LEVEL_LABELS.get(level) if level is not None else None