CoolFace
Apppublic

bilalkurban/Ownership

sourceHugging Facemitupdated 8d agoView on Hugging Face
0likes
app.py282 linesDownload Raw Back to root
1"""2Malta Finance Ownership Explorer3--------------------------------4Enter a Maltese company name -> resolve it in the GLEIF LEI registry ->5show its ownership chain (direct parent, ultimate parent, subsidiaries)6with the country of each owner.7 8Data source: GLEIF public JSON:API (https://api.gleif.org/api/v1) - free, no key.9"""10 11import requests12import pandas as pd13import gradio as gr14 15BASE = "https://api.gleif.org/api/v1"16HEADERS = {17    "Accept": "application/vnd.api+json",18    "User-Agent": "malta-ownership-explorer/0.1",19}20TIMEOUT = 2521 22# ISO2 -> display name for the countries you'll actually hit. Falls back to the code.23COUNTRY_NAMES = {24    "MT": "Malta", "GB": "United Kingdom", "IE": "Ireland", "LU": "Luxembourg",25    "NL": "Netherlands", "DE": "Germany", "FR": "France", "IT": "Italy",26    "ES": "Spain", "CH": "Switzerland", "CY": "Cyprus", "US": "United States",27    "AE": "United Arab Emirates", "SG": "Singapore", "HK": "Hong Kong",28    "KY": "Cayman Islands", "VG": "British Virgin Islands", "JE": "Jersey",29    "GG": "Guernsey", "IM": "Isle of Man", "LI": "Liechtenstein",30    "BE": "Belgium", "AT": "Austria", "SE": "Sweden", "DK": "Denmark",31    "NO": "Norway", "FI": "Finland", "PL": "Poland", "PT": "Portugal",32    "GR": "Greece", "TR": "Turkiye", "IL": "Israel", "CA": "Canada",33    "AU": "Australia", "JP": "Japan", "CN": "China", "IN": "India",34    "ZA": "South Africa", "BM": "Bermuda", "BS": "Bahamas", "PA": "Panama",35}36 37EXCEPTION_LABELS = {38    "NO_LEI": "Parent exists but has no LEI",39    "NATURAL_PERSONS": "Owned directly by natural persons",40    "NON_CONSOLIDATING": "Parent does not consolidate this entity",41    "NO_KNOWN_PERSON": "No known controlling person",42    "NON_PUBLIC": "Ownership not publicly disclosable",43    "LEGAL_OBSTACLES": "Legal obstacles to disclosure",44    "CONSENT_NOT_OBTAINED": "Parent consent not obtained",45    "BINDING_LEGAL_COMMITMENTS": "Blocked by binding legal commitments",46    "DISCLOSURE_DETRIMENTAL": "Disclosure deemed detrimental",47    "DETRIMENT_NOT_EXCLUDED": "Detriment could not be excluded",48}49 50 51# --------------------------------------------------------------------------- #52# GLEIF client53# --------------------------------------------------------------------------- #54def _get(path, params=None):55    """GET a GLEIF endpoint. Returns parsed JSON, or None for 404/no-content."""56    try:57        r = requests.get(BASE + path, params=params, headers=HEADERS, timeout=TIMEOUT)58    except requests.RequestException as e:59        raise gr.Error(f"Could not reach the GLEIF API: {e}")60    if r.status_code in (204, 404):61        return None62    if r.status_code >= 400:63        return None64    try:65        return r.json()66    except ValueError:67        return None68 69 70def country_name(code):71    if not code:72        return "-"73    return COUNTRY_NAMES.get(code.upper(), code.upper())74 75 76def summarise(record):77    """Flatten one lei-record resource into a plain dict."""78    a = record.get("attributes", {})79    ent = a.get("entity", {}) or {}80    legal_name = (ent.get("legalName") or {}).get("name", "-")81    addr = ent.get("legalAddress") or {}82    return {83        "lei": a.get("lei", "-"),84        "name": legal_name,85        "country_code": addr.get("country", ""),86        "city": addr.get("city", ""),87        "jurisdiction": ent.get("jurisdiction", ""),88        "status": ent.get("status", ""),89        "registration": (a.get("registration") or {}).get("status", ""),90    }91 92 93def search_entities(query, country="MT", limit=15):94    """Find candidate LEI records by legal name, restricted to a country."""95    query = (query or "").strip()96    if not query:97        return []98 99    # An LEI is 20 alphanumeric characters - treat that as a direct lookup.100    if len(query) == 20 and query.isalnum():101        payload = _get(f"/lei-records/{query.upper()}")102        if payload and payload.get("data"):103            return [summarise(payload["data"])]104 105    params = {106        "filter[entity.legalName]": query,107        "page[size]": limit,108        "page[number]": 1,109    }110    if country:111        params["filter[entity.legalAddress.country]"] = country112    payload = _get("/lei-records", params)113    hits = (payload or {}).get("data", []) or []114 115    # Fallback: fuzzy name completion, then resolve the matched LEIs.116    if not hits:117        fuzzy = _get("/fuzzycompletions", {"field": "entity.legalName", "q": query})118        leis = []119        for item in (fuzzy or {}).get("data", [])[: limit * 3]:120            rel = ((item.get("relationships") or {}).get("lei-records") or {}).get("data")121            if isinstance(rel, dict) and rel.get("id"):122                leis.append(rel["id"])123        if leis:124            payload = _get("/lei-records", {"filter[lei]": ",".join(leis[:50]),125                                            "page[size]": 50})126            hits = (payload or {}).get("data", []) or []127            if country:128                hits = [h for h in hits129                        if ((h.get("attributes", {}).get("entity", {}) or {})130                            .get("legalAddress", {}) or {}).get("country") == country]131 132    return [summarise(h) for h in hits[:limit]]133 134 135def ownership_ratio(relationship_payload):136    """Pull a reported ownership percentage out of a relationship record, if any."""137    data = (relationship_payload or {}).get("data")138    if not data:139        return None140    rel = (data.get("attributes") or {}).get("relationship") or {}141    for q in rel.get("quantifiers") or []:142        ratio = q.get("amount", q.get("ratio"))143        if ratio is None:144            continue145        try:146            val = float(ratio)147        except (TypeError, ValueError):148            continue149        return f"{val * 100:.1f}%" if val <= 1 else f"{val:.1f}%"150    return None151 152 153def get_parent(lei, kind):154    """kind is 'direct-parent' or 'ultimate-parent'. Returns (dict|None, note|None)."""155    rec = _get(f"/lei-records/{lei}/{kind}")156    if rec and rec.get("data"):157        row = summarise(rec["data"])158        row["share"] = ownership_ratio(_get(f"/lei-records/{lei}/{kind}-relationship")) or "not reported"159        return row, None160 161    exc = _get(f"/lei-records/{lei}/{kind}-reporting-exception")162    data = (exc or {}).get("data")163    if data:164        reason = (data.get("attributes") or {}).get("reason", "")165        return None, EXCEPTION_LABELS.get(reason, reason or "No parent reported")166    return None, "No parent reported to GLEIF"167 168 169def get_children(lei, limit=25):170    payload = _get(f"/lei-records/{lei}/direct-children", {"page[size]": limit})171    return [summarise(d) for d in (payload or {}).get("data", []) or []]172 173 174# --------------------------------------------------------------------------- #175# Gradio callbacks176# --------------------------------------------------------------------------- #177def do_search(query, country):178    code = "" if country == "Any country" else "MT"179    results = search_entities(query, country=code)180    if not results:181        return (gr.update(choices=[], value=None),182                "No match in the GLEIF registry. Try fewer words, or switch to "183                "**Any country** in case the entity is registered abroad.",184                None, None, {})185 186    choices = [f"{r['name']}  -  {r['lei']}  ({country_name(r['country_code'])})"187               for r in results]188    lookup = {c: r for c, r in zip(choices, results)}189    msg = f"Found **{len(results)}** match(es). Pick one below."190    return gr.update(choices=choices, value=choices[0]), msg, None, None, lookup191 192 193def do_ownership(selection, lookup):194    if not selection or not lookup or selection not in lookup:195        raise gr.Error("Search for a company and select a match first.")196 197    subject = lookup[selection]198    lei = subject["lei"]199 200    direct, direct_note = get_parent(lei, "direct-parent")201    ultimate, ult_note = get_parent(lei, "ultimate-parent")202    children = get_children(lei)203 204    rows = []205    if direct:206        rows.append(["Direct parent", direct["name"], country_name(direct["country_code"]),207                     direct["share"], direct["lei"]])208    else:209        rows.append(["Direct parent", f"- ({direct_note})", "-", "-", "-"])210 211    if ultimate:212        rows.append(["Ultimate parent", ultimate["name"], country_name(ultimate["country_code"]),213                     ultimate["share"], ultimate["lei"]])214    else:215        rows.append(["Ultimate parent", f"- ({ult_note})", "-", "-", "-"])216 217    for c in children:218        rows.append(["Subsidiary", c["name"], country_name(c["country_code"]), "-", c["lei"]])219 220    df = pd.DataFrame(rows, columns=["Relationship", "Entity", "Country",221                                     "Reported share", "LEI"])222 223    card = (224        f"### {subject['name']}\n"225        f"**LEI** `{subject['lei']}`  \n"226        f"**Registered address** {subject['city'] or '-'}, "227        f"{country_name(subject['country_code'])}  \n"228        f"**Jurisdiction** {subject['jurisdiction'] or '-'}  \n"229        f"**Entity status** {subject['status'] or '-'} - "230        f"**LEI status** {subject['registration'] or '-'}\n\n"231        "---\n"232        "*GLEIF publishes accounting-consolidation parents, not a full share register. "233        "Percentages appear only where the entity reported them. For the actual "234        "register of members, see the MBR filings.*"235    )236    return card, df237 238 239DISCLAIMER = """240**What this shows.** Ownership relationships reported to the Global LEI system241(GLEIF) - direct parent, ultimate parent and known subsidiaries, each with its242country of registration.243 244**What it does not show.** A complete shareholder list with percentages. That245lives in the Malta Business Registry's register of members and UBO register,246neither of which has a public API. Where GLEIF has no parent on file, the app247tells you the reported reason (for example, *owned directly by natural persons*).248"""249 250 251with gr.Blocks(title="Malta Finance Ownership Explorer") as demo:252    gr.Markdown("# Malta Finance Ownership Explorer")253    gr.Markdown(DISCLAIMER)254 255    state = gr.State({})256 257    with gr.Row():258        query = gr.Textbox(label="Company name or LEI", scale=3,259                           placeholder="e.g. Bank of Valletta")260        country = gr.Radio(["Malta only", "Any country"], value="Malta only",261                           label="Registered in", scale=1)262        search_btn = gr.Button("Search", variant="primary", scale=1)263 264    status = gr.Markdown()265    matches = gr.Dropdown(label="Matches", choices=[], interactive=True)266    go_btn = gr.Button("Show ownership", variant="primary")267 268    card = gr.Markdown()269    table = gr.Dataframe(label="Ownership chain", wrap=True, interactive=False)270 271    search_btn.click(do_search, [query, country], [matches, status, card, table, state])272    query.submit(do_search, [query, country], [matches, status, card, table, state])273    go_btn.click(do_ownership, [matches, state], [card, table])274 275    gr.Examples(276        examples=[["Bank of Valletta"], ["MeDirect"], ["FIMBank"], ["Lombard Bank"]],277        inputs=query,278    )279 280if __name__ == "__main__":281    demo.launch()282