Spaces:
Sleeping
Sleeping
Download app.py from Sierpinksi-Project/ISIN: direct link, hf CLI and curl.
- Browser
- Download file 12.4 kB
-
https://huggingface.co/spaces/Sierpinksi-Project/ISIN/resolve/main/app.py
- Command line
-
hf download hf://spaces/Sierpinksi-Project/ISIN/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Sierpinksi-Project/ISIN/resolve/main/app.py
12.4 kB
| from __future__ import annotations | |
| import csv | |
| import io | |
| import random | |
| import re | |
| from functools import lru_cache | |
| import gradio as gr | |
| import requests | |
| # ------------------------------------------------------------ | |
| # ISIN core | |
| # ------------------------------------------------------------ | |
| _ISIN_BODY_RE = re.compile(r"^[A-Z0-9]{11}$") | |
| _ISIN_FULL_RE = re.compile(r"^[A-Z]{2}[A-Z0-9]{9}[0-9]$") | |
| def normalize_isin(s: str) -> str: | |
| return re.sub(r"[\s-]+", "", (s or "")).upper() | |
| def expand_isin_body(body: str) -> str: | |
| """ | |
| Expand an 11-char ISIN body to digits: | |
| A -> 10, ..., Z -> 35, digits stay unchanged. | |
| """ | |
| body = normalize_isin(body) | |
| if not _ISIN_BODY_RE.fullmatch(body): | |
| raise ValueError( | |
| "ISIN-Body muss aus genau 11 alphanumerischen Zeichen bestehen." | |
| ) | |
| out: list[str] = [] | |
| for ch in body: | |
| if ch.isdigit(): | |
| out.append(ch) | |
| else: | |
| out.append(str(ord(ch) - ord("A") + 10)) | |
| return "".join(out) | |
| def luhn_mod10_check_digit_from_expanded(expanded_digits: str) -> str: | |
| """ | |
| Main implementation: | |
| traverse from right to left, double every second digit. | |
| """ | |
| if not expanded_digits.isdigit(): | |
| raise ValueError("expanded_digits muss nur aus Ziffern bestehen.") | |
| total = 0 | |
| double = True # because the check digit will be appended on the right | |
| for ch in reversed(expanded_digits): | |
| n = int(ch) | |
| if double: | |
| n *= 2 | |
| if n > 9: | |
| n -= 9 | |
| total += n | |
| double = not double | |
| return str((10 - (total % 10)) % 10) | |
| def compute_isin_check_digit(body: str) -> str: | |
| expanded = expand_isin_body(body) | |
| return luhn_mod10_check_digit_from_expanded(expanded) | |
| def complete_isin(body: str) -> str: | |
| body = normalize_isin(body) | |
| return body + compute_isin_check_digit(body) | |
| def validate_isin(isin: str) -> bool: | |
| isin = normalize_isin(isin) | |
| if not _ISIN_FULL_RE.fullmatch(isin): | |
| return False | |
| return compute_isin_check_digit(isin[:-1]) == isin[-1] | |
| # ------------------------------------------------------------ | |
| # Independent reference implementation | |
| # (different formulation, same mathematical result) | |
| # ------------------------------------------------------------ | |
| def compute_isin_check_digit_reference(body: str) -> str: | |
| """ | |
| Reference implementation of the same rule, but written independently: | |
| - expand letters | |
| - reason from LEFT using parity of the final full length | |
| """ | |
| digits = [int(ch) for ch in expand_isin_body(body)] | |
| # final full number length = len(digits) + 1 (for the check digit) | |
| full_len = len(digits) + 1 | |
| parity = full_len % 2 | |
| total = 0 | |
| for i, n in enumerate(digits): | |
| if i % 2 == parity: | |
| n *= 2 | |
| if n > 9: | |
| n -= 9 | |
| total += n | |
| return str((10 - (total % 10)) % 10) | |
| def validate_isin_reference(isin: str) -> bool: | |
| isin = normalize_isin(isin) | |
| if not _ISIN_FULL_RE.fullmatch(isin): | |
| return False | |
| return compute_isin_check_digit_reference(isin[:-1]) == isin[-1] | |
| # ------------------------------------------------------------ | |
| # Manual UI helpers | |
| # ------------------------------------------------------------ | |
| def run_manual_isin_tool(user_input: str) -> str: | |
| s = normalize_isin(user_input) | |
| if _ISIN_BODY_RE.fullmatch(s): | |
| full = complete_isin(s) | |
| ref = compute_isin_check_digit_reference(s) | |
| return ( | |
| f"Eingabe erkannt als 11-stelliger ISIN-Body.\n\n" | |
| f"Vollständige ISIN: {full}\n" | |
| f"Prüfziffer: {full[-1]}\n" | |
| f"Referenz-Prüfziffer: {ref}\n" | |
| f"Konsistenz: {'OK' if ref == full[-1] else 'ABWEICHUNG'}" | |
| ) | |
| if _ISIN_FULL_RE.fullmatch(s): | |
| ok_main = validate_isin(s) | |
| ok_ref = validate_isin_reference(s) | |
| return ( | |
| f"Eingabe erkannt als vollständige 12-stellige ISIN.\n\n" | |
| f"ISIN: {s}\n" | |
| f"Validierung (Hauptimplementierung): {'gültig' if ok_main else 'ungültig'}\n" | |
| f"Validierung (Referenzimplementierung): {'gültig' if ok_ref else 'ungültig'}\n" | |
| f"Konsistenz: {'OK' if ok_main == ok_ref else 'ABWEICHUNG'}" | |
| ) | |
| return ( | |
| "Ungültige Eingabe.\n\n" | |
| "Erwartet wird entweder:\n" | |
| "- ein 11-stelliger ISIN-Body (zur Berechnung), oder\n" | |
| "- eine 12-stellige vollständige ISIN (zur Validierung).\n\n" | |
| "Erlaubt sind nur A-Z und 0-9; Leerzeichen und Bindestriche werden ignoriert." | |
| ) | |
| # ------------------------------------------------------------ | |
| # Online dataset self-test | |
| # ------------------------------------------------------------ | |
| REMOTE_SOURCES = { | |
| "NSDL mirror (India ISIN dataset on GitHub)": { | |
| "url": "https://raw.githubusercontent.com/captn3m0/india-isin-data/main/ISIN.csv", | |
| "description": ( | |
| "Öffentliches CSV-Repo mit realen ISINs; gut für externe Regressionstests." | |
| ), | |
| } | |
| } | |
| def _extract_isins_from_csv_text(text: str) -> list[str]: | |
| """ | |
| Parse robustly: | |
| 1) try DictReader and a column named 'isin' | |
| 2) fallback to regex extraction from whole text | |
| """ | |
| out: list[str] = [] | |
| try: | |
| reader = csv.DictReader(io.StringIO(text)) | |
| if reader.fieldnames: | |
| field_map = {f.strip().lower(): f for f in reader.fieldnames if f} | |
| isin_field = field_map.get("isin") | |
| if isin_field: | |
| for row in reader: | |
| candidate = normalize_isin(row.get(isin_field, "")) | |
| if _ISIN_FULL_RE.fullmatch(candidate): | |
| out.append(candidate) | |
| except csv.Error: | |
| pass | |
| if not out: | |
| for candidate in re.findall(r"\b[A-Z]{2}[A-Z0-9]{9}[0-9]\b", text.upper()): | |
| if _ISIN_FULL_RE.fullmatch(candidate): | |
| out.append(candidate) | |
| # de-duplicate but preserve deterministic order | |
| seen = set() | |
| deduped = [] | |
| for x in out: | |
| if x not in seen: | |
| seen.add(x) | |
| deduped.append(x) | |
| return deduped | |
| def fetch_remote_isins(source_name: str) -> tuple[str, ...]: | |
| source = REMOTE_SOURCES[source_name] | |
| resp = requests.get( | |
| source["url"], | |
| timeout=30, | |
| headers={"User-Agent": "ISIN-Validator-Demo/1.0"}, | |
| ) | |
| resp.raise_for_status() | |
| isins = _extract_isins_from_csv_text(resp.text) | |
| if not isins: | |
| raise RuntimeError("Keine gültigen ISINs im Online-Datensatz gefunden.") | |
| return tuple(isins) | |
| def _parse_seed(seed_text: str | None) -> int | None: | |
| s = (seed_text or "").strip() | |
| if not s: | |
| return None | |
| return int(s) | |
| def _render_result_table(rows: list[dict[str, str]], max_rows: int = 1000) -> str: | |
| head = "| # | modus | ISIN | result_main | result_ref | ok |\n|---:|---|---|---|---|---|\n" | |
| body_lines = [] | |
| for row in rows[:max_rows]: | |
| body_lines.append( | |
| f"| {row['idx']} | {row['mode']} | `{row['isin']}` | " | |
| f"`{row['result_main']}` | `{row['result_ref']}` | {row['ok']} |" | |
| ) | |
| tail = "" | |
| if len(rows) > max_rows: | |
| tail = f"\n\n… weitere {len(rows) - max_rows} Zeilen ausgeblendet." | |
| return head + "\n".join(body_lines) + tail | |
| def run_online_self_test( | |
| sample_count: int, | |
| mode: str, | |
| seed_text: str, | |
| source_name: str, | |
| ) -> str: | |
| try: | |
| seed = _parse_seed(seed_text) | |
| rng = random.Random(seed) | |
| dataset = list(fetch_remote_isins(source_name)) | |
| if not dataset: | |
| return "Fehler: Online-Datensatz leer." | |
| n = max(1, min(int(sample_count), len(dataset))) | |
| sample = rng.sample(dataset, k=n) | |
| rows: list[dict[str, str]] = [] | |
| failures: list[dict[str, str]] = [] | |
| for idx, isin in enumerate(sample, start=1): | |
| op = mode | |
| if mode == "mixed": | |
| op = rng.choice(["compute", "verify"]) | |
| if op == "verify": | |
| main = str(validate_isin(isin)) | |
| ref = str(validate_isin_reference(isin)) | |
| ok = (main == "True") and (ref == "True") | |
| else: | |
| # compute | |
| main = complete_isin(isin[:-1]) | |
| ref = isin[:-1] + compute_isin_check_digit_reference(isin[:-1]) | |
| ok = (main == isin) and (ref == isin) | |
| row = { | |
| "idx": str(idx), | |
| "mode": op, | |
| "isin": isin, | |
| "result_main": main, | |
| "result_ref": ref, | |
| "ok": "✅" if ok else "❌", | |
| } | |
| rows.append(row) | |
| if not ok: | |
| failures.append(row) | |
| passed = len(rows) - len(failures) | |
| rate = 100.0 * passed / len(rows) | |
| summary = ( | |
| f"## Online-Selbsttest\n\n" | |
| f"- Quelle: **{source_name}**\n" | |
| f"- Geladene Datensätze: **{len(dataset)}**\n" | |
| f"- Stichprobe: **{len(rows)}**\n" | |
| f"- Modus: **{mode}**\n" | |
| f"- Seed: **{seed if seed is not None else 'zufällig'}**\n" | |
| f"- Bestanden: **{passed}/{len(rows)} ({rate:.1f}%)**\n" | |
| ) | |
| note = ( | |
| "\n**Hinweis:** Dieser Test vergleicht gegen reale Online-ISINs " | |
| "und nutzt zusätzlich eine unabhängig formulierte Referenzimplementierung. " | |
| "Das ist ein starker externer Check, aber kein mathematischer Universalbeweis.\n" | |
| ) | |
| if failures: | |
| fail_block = "\n### Fehlgeschlagene Fälle\n\n" + _render_result_table( | |
| failures, max_rows=1000 | |
| ) | |
| else: | |
| fail_block = "\n### Erste Stichproben\n\n" + _render_result_table( | |
| rows, max_rows=1000 | |
| ) | |
| return summary + note + fail_block | |
| except requests.RequestException as e: | |
| return f"Netzwerkfehler beim Laden des Online-Datensatzes:\n\n`{type(e).__name__}: {e}`" | |
| except Exception as e: | |
| return f"Fehler im Selbsttest:\n\n`{type(e).__name__}: {e}`" | |
| # ------------------------------------------------------------ | |
| # Gradio UI | |
| # ------------------------------------------------------------ | |
| with gr.Blocks() as demo: | |
| gr.Markdown( | |
| """ | |
| # ISIN Rechner / Validator | |
| Diese App kann: | |
| 1. eine ISIN-Prüfziffer berechnen, | |
| 2. eine vollständige ISIN validieren, | |
| 3. einen Online-Selbsttest gegen reale veröffentlichte ISINs ausführen. | |
| """ | |
| ) | |
| with gr.Tab("Manuell"): | |
| with gr.Row(): | |
| manual_input = gr.Textbox( | |
| label="ISIN oder ISIN-Body", | |
| placeholder="z.B. US037833100 oder US0378331005", | |
| lines=1, | |
| ) | |
| manual_button = gr.Button("Berechnen / Validieren") | |
| manual_output = gr.Textbox(label="Ergebnis", lines=10) | |
| manual_button.click( | |
| fn=run_manual_isin_tool, | |
| inputs=manual_input, | |
| outputs=manual_output, | |
| ) | |
| with gr.Tab("Online-Selbsttest"): | |
| gr.Markdown( | |
| """ | |
| Der Selbsttest lädt zufällige echte ISINs aus einem öffentlichen Online-Datensatz, | |
| wählt eine Stichprobe und prüft dann `compute`, `verify` oder beides gemischt. | |
| """ | |
| ) | |
| with gr.Row(): | |
| source_name = gr.Dropdown( | |
| choices=list(REMOTE_SOURCES.keys()), | |
| value=list(REMOTE_SOURCES.keys())[0], | |
| label="Datenquelle", | |
| ) | |
| mode = gr.Dropdown( | |
| choices=["compute", "verify", "mixed"], | |
| value="mixed", | |
| label="Testmodus", | |
| ) | |
| with gr.Row(): | |
| sample_count = gr.Slider( | |
| minimum=1, | |
| maximum=1000, | |
| value=25, | |
| step=1, | |
| label="Anzahl zufälliger ISINs", | |
| ) | |
| seed_text = gr.Textbox( | |
| label="Seed (optional)", | |
| placeholder="z.B. 42", | |
| lines=1, | |
| ) | |
| online_button = gr.Button("Online-Selbsttest starten") | |
| online_output = gr.Markdown() | |
| online_button.click( | |
| fn=run_online_self_test, | |
| inputs=[sample_count, mode, seed_text, source_name], | |
| outputs=online_output, | |
| ) | |
| demo.launch() |