Metadata-Version: 2.5
Name: dora-lei-check
Version: 0.2.0
Summary: Check the LEIs in your DORA Register of Information against GLEIF before you submit. Runs locally.
Project-URL: Homepage, https://github.com/kakoullig-hue/dora-lei-check
Project-URL: Issues, https://github.com/kakoullig-hue/dora-lei-check/issues
Author: George Kakoullis
License: MIT
License-File: LICENSE
Keywords: compliance,dora,gleif,lei,regtech,regulatory-reporting,xbrl
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Office/Business :: Financial
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# dora-lei-check

**Find the LEIs that will get your DORA Register of Information rejected — before you submit it, on your own machine.**

```console
pip install dora-lei-check
dora-lei-check register.zip
```

## The problem this solves

A lapsed LEI is perfectly well-formed. It is 20 characters, it passes the ISO 17442 checksum, and it sails through offline XBRL validation. Then you submit, and days later the register comes back:

```
[EBA.VR_71] LEI 984500914ED4D50AZ405 needs to be a valid one according to
the GLEIF databases.
```

The LEI-versus-GLEIF check is an **external lookup**. The ESAs run it on their side, *after* submission, against the live GLEIF register. Until you get the rejection, you are guessing — and each round trip through your NCA burns days you do not have inside a reporting window.

This tool runs that same lookup locally, before you submit.

```console
$ dora-lei-check register.zip

LEI                   VERDICT     GLEIF STATUS      LEGAL NAME
--------------------  ----------  ----------------  ----------------------
254900ZD9J74PNOI5L11  NOT ISSUED  LAPSED            VARANDO LTD
5493001KJTIIGC8Y1R12  OK          ISSUED            Bloomberg Finance L.P.
5493001KJTIIGC8Y1R13  INVALID     invalid checksum  —
984500914ED4D50AZ405  NOT ISSUED  LAPSED            BONOCAP LTD

4 distinct LEI(s): 1 OK, 3 failing, 0 unverified

These will be rejected at submission:
  254900ZD9J74PNOI5L11  NOT ISSUED found in b_05.01.csv:0030  [EBA.VR_77]
  5493001KJTIIGC8Y1R13  INVALID    found in b_05.01.csv:0010  [EBA.VR_71]
  984500914ED4D50AZ405  NOT ISSUED found in b_05.01.csv:0010  [EBA.VR_71]
```

It tells you the rule code you are about to receive, not just that something is wrong.

## Errors it catches before the regulator does

If you are here because you already received one of these, this is the tool that stops it happening again:

| Rule | Message | Where |
|---|---|---|
| `EBA.VR_2` | *LEI [LEI] needs to be a valid one according to the GLEIF databases* | B.01.01, column 0010 |
| `EBA.VR_12` | *LEI [LEI] needs to be a valid one according to the GLEIF databases* | B.01.02, column 0010 |
| `EBA.VR_23` | *LEI [LEI] needs to be a valid one according to the GLEIF databases* | B.01.02, column 0060 |
| `EBA.VR_71` | *LEI [LEI] needs to be a valid one according to the GLEIF databases* | B.05.01, column 0010 |
| `EBA.VR_77` | *LEI [LEI] needs to be a valid one according to the GLEIF databases* | B.05.01, column 0030 |

It also catches the malformed and missing identifiers behind `v8826_m` (*incorrect LEI in table*), `v8891_m` (*wrong LEI format in table*), and `ICTO007` (*no valid or existing LEI has been found for this entity*).

Related codes it does **not** cover yet: `VR_16` (country code versus the LEI certificate), and `VR_72` / `VR_78` (EUID against BRIS). See [what it does not do](#what-it-does-and-does-not-do).

## Why this is worth checking

In the ESAs' dry run, **93.5% of submissions carried at least one data error** and only 6.5% passed all 116 data-quality checks. In the first live cycle, supervisors reported **missing or invalid LEIs in roughly a third of submissions** — among the most common hard-rejection causes. The 2026 cycle tightened the checks further: a register accepted one year can be rejected the next.

Registration status is not static. An LEI that was `ISSUED` when you built the register can be `LAPSED` by the time you submit it, because the provider simply did not renew. Nothing in your file changes. The verdict does.

## Your register never leaves your machine

This matters more than it sounds. A Register of Information is a complete map of your ICT third-party estate — every provider, every contractual arrangement, every criticality assessment. The other pre-submission validators are hosted web tools: you upload that file to someone else's server.

`dora-lei-check` reads the register locally. The only thing that crosses the network is the public 20-character LEI codes, sent to GLEIF's free public API. With `--offline` and GLEIF's daily golden-copy file, nothing crosses the network at all.

## Install

```console
pip install dora-lei-check
```

Python 3.10+. **Zero dependencies** — nothing for your security team to review.

## Use

```console
dora-lei-check register.zip           # a submitted xBRL-CSV package
dora-lei-check ./templates/           # a folder of RoI CSV templates
dora-lei-check b_05.01.csv            # a single template

dora-lei-check register.zip --json    # machine-readable, for pipelines
dora-lei-check register.zip --offline gleif-golden-copy.csv   # no network at all
dora-lei-check register.zip --columns 0010 0030               # narrow the scan
```

| Exit code | Meaning |
|---|---|
| `0` | every LEI is issued and active |
| `1` | at least one LEI will be rejected |
| `2` | some LEIs could not be verified — a network failure is never silently a pass |
| `3` | the register could not be read |

Exit `1` makes it a pre-submission gate in CI. Results are cached on disk, so re-runs during a correction cycle are instant.

### As a library

```python
from pathlib import Path
from dora_lei_check import Report, extract, fetch, judge

report = Report([judge(c, fetch(c.lei)) for c in extract(Path("register.zip"))])
for failure in report.failures:
    print(failure.lei, failure.verdict.value, failure.rules)
```

## What it does and does not do

**Does:** finds every LEI-shaped value anywhere in the register, validates ISO 17442 format and the mod-97 checksum offline, resolves each one against GLEIF, reports anything that is not `ISSUED`, and names the EBA rule code it will trigger.

**Does not:** validate the XBRL itself. Use [Arelle](https://github.com/Arelle/Arelle) for the technical and DPM layers — it is free, excellent, and this is not a replacement for it. Cross-table business rules and EUID-versus-BRIS are not covered.

To be precise about the overlap, because precision is the point: Arelle's `validate/EBA` plugin **does** check LEIs — format and checksum, entirely offline, and only for context entity identifiers. It contains no GLEIF capability whatsoever. A lapsed LEI passes Arelle and fails your submission. That gap is the entire reason this exists.

## Accuracy note

LEIs are located by shape and checksum rather than by a taxonomy field map, so the tool keeps working across EBA taxonomy versions instead of breaking on each release. The trade-off: a 20-character alphanumeric string that is not an LEI could in principle pass the checksum. Every finding is reported with its file, column and row so you can see exactly what was matched, and `--columns` narrows the scan when you want it exact.

Rule codes are only shown when the table and column are unambiguous. A wrong rule code would be worse than none.

## Contributing

If your register failed on something this tool did not catch, [open an issue](https://github.com/kakoullig-hue/dora-lei-check/issues) and say what the rejection said. That feedback decides what gets built next.

## License

MIT.
