stdnum-centroamerica 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stdnum_centroamerica-0.1.0/LICENSE +21 -0
- stdnum_centroamerica-0.1.0/PKG-INFO +290 -0
- stdnum_centroamerica-0.1.0/README.md +254 -0
- stdnum_centroamerica-0.1.0/pyproject.toml +185 -0
- stdnum_centroamerica-0.1.0/pyproject.toml.orig +173 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/__init__.py +67 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_base.py +166 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_data/__init__.py +1 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_data/honduras.py +43 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_data/nicaragua.py +41 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_dates.py +16 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_random.py +28 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_registry.py +184 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_types.py +111 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/contrib/__init__.py +9 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/contrib/_common.py +41 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/contrib/django.py +129 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/contrib/pydantic.py +91 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/contrib/wtforms.py +76 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/cr/__init__.py +64 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/exceptions.py +20 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/gt/__init__.py +103 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/hn/__init__.py +102 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/mrz/__init__.py +408 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/ni/__init__.py +204 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/pa/__init__.py +176 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/py.typed +0 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/sv/__init__.py +7 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/sv/dui.py +37 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/sv/nit.py +64 -0
- stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/sv/nrc.py +28 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Momotombo Devs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stdnum-centroamerica
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Validate, format and compact Central American identity and tax numbers
|
|
5
|
+
Keywords: stdnum,centroamerica,validation,nit,dui,dpi,cedula,ruc,rtn,mrz,guatemala,el-salvador,honduras,nicaragua,costa-rica,panama
|
|
6
|
+
Author: Momotombo Devs
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Framework :: Django
|
|
19
|
+
Classifier: Framework :: FastAPI
|
|
20
|
+
Classifier: Framework :: Flask
|
|
21
|
+
Classifier: Framework :: Pydantic :: 2
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Dist: django>=4.2 ; extra == 'django'
|
|
25
|
+
Requires-Dist: pydantic>=2.6 ; extra == 'pydantic'
|
|
26
|
+
Requires-Dist: wtforms>=3.1 ; extra == 'wtforms'
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Project-URL: Homepage, https://github.com/momotombodevs/stdnum-centroamerica-py
|
|
29
|
+
Project-URL: Source, https://github.com/momotombodevs/stdnum-centroamerica-py
|
|
30
|
+
Project-URL: Issues, https://github.com/momotombodevs/stdnum-centroamerica-py/issues
|
|
31
|
+
Project-URL: Changelog, https://github.com/momotombodevs/stdnum-centroamerica-py/blob/main/CHANGELOG.md
|
|
32
|
+
Provides-Extra: django
|
|
33
|
+
Provides-Extra: pydantic
|
|
34
|
+
Provides-Extra: wtforms
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# stdnum-centroamerica
|
|
38
|
+
|
|
39
|
+
[](https://github.com/momotombodevs/stdnum-centroamerica-py/actions/workflows/ci.yml)
|
|
40
|
+
[](https://pypi.org/project/stdnum-centroamerica/)
|
|
41
|
+
[](pyproject.toml)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
|
|
44
|
+
Validate, format and compact Central American identity and tax document numbers: DPI, NIT, DUI, NRC, RTN, cédulas, RUC, DIMEX and passport MRZs. No dependencies, fully typed, Python 3.11 or later. Works in any project: scripts, FastAPI, Flask, Django, DRF, Celery or notebooks.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
With [uv](https://docs.astral.sh/uv/) (recommended):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
uv add stdnum-centroamerica
|
|
52
|
+
uv add "stdnum-centroamerica[pydantic]" # FastAPI / Pydantic v2 integration
|
|
53
|
+
uv add "stdnum-centroamerica[django]" # Django / DRF integration
|
|
54
|
+
uv add "stdnum-centroamerica[wtforms]" # Flask-WTF / WTForms integration
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
With pip, inside a virtual environment:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
python -m venv .venv
|
|
61
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
62
|
+
python -m pip install stdnum-centroamerica
|
|
63
|
+
python -m pip install "stdnum-centroamerica[django]"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
More options (Poetry, PDM, installing from GitHub, editable mode, hashes) in [docs/installation.md](docs/installation.md).
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
Each document has a `country.document` key (`ni.cedula`, `gt.dpi`, ...) and three operations: validate, format and compact.
|
|
71
|
+
|
|
72
|
+
```pycon
|
|
73
|
+
>>> import stdnum_centroamerica as sca
|
|
74
|
+
|
|
75
|
+
>>> sca.is_valid("ni.cedula", "001-010190-0001N")
|
|
76
|
+
True
|
|
77
|
+
>>> sca.is_valid("ni.ruc", "J0310000000001") # legal entity
|
|
78
|
+
True
|
|
79
|
+
>>> sca.is_valid("ni.ruc", "0000300766011M") # a RUC cannot start with four zeros
|
|
80
|
+
False
|
|
81
|
+
|
|
82
|
+
>>> sca.make("ni.cedula").format("0010101900001n")
|
|
83
|
+
'001-010190-0001N'
|
|
84
|
+
>>> sca.make("ni.cedula").compact("001-010190-0001n")
|
|
85
|
+
'0010101900001N'
|
|
86
|
+
>>> sca.make("gt.dpi").format("1234567890101")
|
|
87
|
+
'1234 56789 0101'
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`validate()` also returns the reason for a failure, which is useful for error messages:
|
|
92
|
+
|
|
93
|
+
```pycon
|
|
94
|
+
>>> result = sca.validate("sv.dui", "12345678-5")
|
|
95
|
+
>>> result.is_valid
|
|
96
|
+
False
|
|
97
|
+
>>> result.error
|
|
98
|
+
<ErrorCode.CHECKSUM: 'checksum'>
|
|
99
|
+
>>> bool(sca.validate("sv.dui", "12345678-4"))
|
|
100
|
+
True
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
| `ErrorCode` | Value | Meaning |
|
|
105
|
+
|-------------|-------|---------|
|
|
106
|
+
| `FORMAT` | `"format"` | wrong length, characters or structure |
|
|
107
|
+
| `COMPONENT` | `"component"` | a component (date, department, municipality, prefix) is out of range |
|
|
108
|
+
| `CHECKSUM` | `"checksum"` | the check digit or letter does not match |
|
|
109
|
+
|
|
110
|
+
`ErrorCode` is a `StrEnum`, so it serializes to JSON as plain text.
|
|
111
|
+
|
|
112
|
+
### Options
|
|
113
|
+
|
|
114
|
+
The Nicaraguan cédula does not check its letter by default, because the CSE does not publish the formula (modulo 23, from unofficial sources). Options are passed as keyword arguments:
|
|
115
|
+
|
|
116
|
+
```pycon
|
|
117
|
+
>>> sca.is_valid("ni.cedula", "001-010190-0001A") # the correct letter is N
|
|
118
|
+
True
|
|
119
|
+
>>> sca.is_valid("ni.cedula", "001-010190-0001A", strict_letter=True)
|
|
120
|
+
False
|
|
121
|
+
>>> sca.is_valid("ni.cedula", "282-010190-0001A", strict_municipality=True) # 282 is not in the list
|
|
122
|
+
False
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The document classes can also be used directly:
|
|
127
|
+
|
|
128
|
+
```pycon
|
|
129
|
+
>>> from stdnum_centroamerica.ni import Cedula
|
|
130
|
+
>>> Cedula(strict_municipality=True).is_valid("001-010190-0001N")
|
|
131
|
+
True
|
|
132
|
+
>>> Cedula.check_letter("0010101900001")
|
|
133
|
+
'N'
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
A misspelled option raises `TypeError` instead of being silently ignored.
|
|
138
|
+
|
|
139
|
+
### Parsing components
|
|
140
|
+
|
|
141
|
+
`parse()` reads the components encoded in the number and returns an immutable object, or `None` if the number is not valid. It is available for `ni.cedula`, `hn.identidad`, `gt.dpi`, `pa.cedula` and the three MRZ formats.
|
|
142
|
+
|
|
143
|
+
```pycon
|
|
144
|
+
>>> cedula = sca.parse("ni.cedula", "001-010190-0001N")
|
|
145
|
+
>>> cedula.municipality_code, cedula.birth_day, cedula.birth_month, cedula.birth_year_two_digits
|
|
146
|
+
(1, 1, 1, 90)
|
|
147
|
+
>>> import datetime
|
|
148
|
+
>>> cedula.birth_year_candidates(datetime.date(2026, 11, 5))
|
|
149
|
+
(1990,)
|
|
150
|
+
|
|
151
|
+
>>> sca.parse("hn.identidad", "0906-1990-00001").municipality_code
|
|
152
|
+
6
|
|
153
|
+
>>> sca.parse("gt.dpi", "1234 56789 0101").department_code
|
|
154
|
+
1
|
|
155
|
+
>>> sca.parse("pa.cedula", "PE-5-687").kind
|
|
156
|
+
<CedulaKind.BORN_ABROAD: 'born_abroad'>
|
|
157
|
+
|
|
158
|
+
>>> mrz = "P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<\nL898902C36UTO7408122F1204159ZE184226B<<<<<10"
|
|
159
|
+
>>> sca.is_valid("mrz.td3", mrz)
|
|
160
|
+
True
|
|
161
|
+
>>> sca.parse("mrz.td3", mrz).surname
|
|
162
|
+
'ERIKSSON'
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The number does not encode the century, so `birth_year_candidates()` returns the possible years (it may be empty: 29/02/01 exists neither in 1901 nor in 2001). For the same reason it cannot be used to decide legal age: someone born in 2010 also gets the candidate 1910.
|
|
167
|
+
|
|
168
|
+
Components come from the structure of the number and are not checked against any registry. **Birth dates and the other components are personal data**: they are hidden from `repr()` so they do not end up in a log by accident, but treat them accordingly (see [docs/security.md](docs/security.md)).
|
|
169
|
+
|
|
170
|
+
### Test data
|
|
171
|
+
|
|
172
|
+
`generate()` returns a synthetic valid number, useful for fixtures and sample data. It is random and may match a real number by chance: do not use it as data about people.
|
|
173
|
+
|
|
174
|
+
```pycon
|
|
175
|
+
>>> sca.is_valid("gt.dpi", sca.generate("gt.dpi"))
|
|
176
|
+
True
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Catalog and custom documents
|
|
181
|
+
|
|
182
|
+
```pycon
|
|
183
|
+
>>> len(sca.keys())
|
|
184
|
+
18
|
|
185
|
+
>>> info = sca.describe()[0]
|
|
186
|
+
>>> info.key, info.country, info.name, info.level
|
|
187
|
+
('gt.nit', 'GT', 'NIT', <Level.COMPLETE: 'complete'>)
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
An unknown key raises `UnknownDocumentError` (with suggestions when it resembles an existing key). `sca.register(key, cls)` adds or replaces a document; the class must implement the `Document` protocol (`is_valid`, `validate`, `format`, `compact`). Call it at application startup.
|
|
192
|
+
|
|
193
|
+
## Supported documents
|
|
194
|
+
|
|
195
|
+
**complete** = validates format and check digit; **format** = validates length, characters and basic ranges.
|
|
196
|
+
|
|
197
|
+
| Country | Document | Key | Level |
|
|
198
|
+
|---------|----------|-----|-------|
|
|
199
|
+
| Guatemala | NIT | `gt.nit` | complete |
|
|
200
|
+
| Guatemala | DPI (CUI) | `gt.dpi` | complete (check digit and department) |
|
|
201
|
+
| El Salvador | DUI | `sv.dui` | complete |
|
|
202
|
+
| El Salvador | NIT | `sv.nit` | complete (also accepts the 9-digit DUI) |
|
|
203
|
+
| El Salvador | NRC | `sv.nrc` | format |
|
|
204
|
+
| Honduras | Número de identidad | `hn.identidad` | format (valid department and municipality) |
|
|
205
|
+
| Honduras | RTN | `hn.rtn` | format |
|
|
206
|
+
| Nicaragua | Cédula | `ni.cedula` | complete (check letter with `strict_letter`, off by default) |
|
|
207
|
+
| Nicaragua | RUC | `ni.ruc` | format |
|
|
208
|
+
| Costa Rica | Cédula física | `cr.cedula_fisica` | format |
|
|
209
|
+
| Costa Rica | Cédula jurídica | `cr.cedula_juridica` | format |
|
|
210
|
+
| Costa Rica | DIMEX | `cr.dimex` | format |
|
|
211
|
+
| Costa Rica | NITE | `cr.nite` | format |
|
|
212
|
+
| Panama | Cédula | `pa.cedula` | format |
|
|
213
|
+
| Panama | RUC | `pa.ruc` | format (the DV is not validated) |
|
|
214
|
+
| International | Passport MRZ (TD3) | `mrz.td3` | complete (ICAO 9303) |
|
|
215
|
+
| International | ID card MRZ (TD1) | `mrz.td1` | complete (ICAO 9303) |
|
|
216
|
+
| International | ID document MRZ (TD2) | `mrz.td2` | complete (ICAO 9303) |
|
|
217
|
+
|
|
218
|
+
Notes:
|
|
219
|
+
|
|
220
|
+
- **Panama:** volume and entry have variable length, so the number can only be read with its hyphens. Use `format()` for the canonical form (`8-926-1601`); `compact()` removes the hyphens.
|
|
221
|
+
- **Honduras:** `hn.identidad` accepts the special codes for naturalized citizens (`0890`) and dual nationality (`0880`).
|
|
222
|
+
- **MRZ:** accepted with or without line breaks. Dates stay as `YYMMDD`, because the century cannot be inferred. Country-specific passport numbers are not validated, only the MRZ.
|
|
223
|
+
- **Nicaragua:** the municipality codes and the check letter come from third parties (the CSE does not publish them), and code 888 has a single source. In Panama, provinces 10 to 13 and the PI and AV suffixes are not official either.
|
|
224
|
+
- The source of every rule, with the date it was consulted, is in [docs/sources.md](docs/sources.md).
|
|
225
|
+
|
|
226
|
+
## Framework integrations
|
|
227
|
+
|
|
228
|
+
The core works in any project with no configuration. The integrations are optional and report errors in each framework's own format. Their messages default to English and can be overridden (see each guide).
|
|
229
|
+
|
|
230
|
+
**FastAPI / Pydantic v2** ([guide](docs/fastapi.md)):
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from typing import Annotated
|
|
234
|
+
from pydantic import BaseModel
|
|
235
|
+
from stdnum_centroamerica.contrib.pydantic import Document
|
|
236
|
+
|
|
237
|
+
Dpi = Annotated[str, Document("gt.dpi")] # validates and stores it compact
|
|
238
|
+
|
|
239
|
+
class Customer(BaseModel):
|
|
240
|
+
dpi: Dpi
|
|
241
|
+
cedula: Annotated[str, Document("ni.cedula", normalize="format", strict_letter=True)]
|
|
242
|
+
# When invalid, FastAPI responds with 422 and type="stdnum_checksum" (or format / component).
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Django and DRF** ([guide](docs/django.md)):
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from stdnum_centroamerica.contrib.django import DocumentFormField, DocumentValidator
|
|
249
|
+
|
|
250
|
+
class Customer(models.Model):
|
|
251
|
+
dpi = models.CharField(max_length=13, validators=[DocumentValidator("gt.dpi")])
|
|
252
|
+
|
|
253
|
+
class CustomerForm(forms.Form):
|
|
254
|
+
dpi = DocumentFormField("gt.dpi") # cleaned_data["dpi"] is compact
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Flask** ([guide](docs/flask.md)):
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
from stdnum_centroamerica.contrib.wtforms import Document
|
|
261
|
+
|
|
262
|
+
class CustomerForm(FlaskForm):
|
|
263
|
+
dpi = StringField("DPI", validators=[DataRequired(), Document("gt.dpi")])
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Complete, tested examples in [examples/](examples/).
|
|
267
|
+
|
|
268
|
+
## Security
|
|
269
|
+
|
|
270
|
+
- No dependencies, no network access and no I/O: the package never stores or logs numbers.
|
|
271
|
+
- Only `str` inputs of up to 256 ASCII characters are accepted; anything else is a `FORMAT` error and no regular expression runs. This rejects digits from other scripts, Unicode hyphens and invisible spaces.
|
|
272
|
+
- Regular expressions run in linear time (no catastrophic backtracking), and the tests check this with adversarial inputs.
|
|
273
|
+
- Instances and data tables are immutable, so they can be shared between threads.
|
|
274
|
+
|
|
275
|
+
Details and recommendations for integrators in [docs/security.md](docs/security.md). To report a vulnerability, see [SECURITY.md](SECURITY.md).
|
|
276
|
+
|
|
277
|
+
## Development
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
uv sync
|
|
281
|
+
uv run pytest # tests (including Hypothesis property tests)
|
|
282
|
+
uv run ruff check . && uv run ruff format --check .
|
|
283
|
+
uv run mypy # strict mode
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the branching model and how to add a document.
|
|
287
|
+
|
|
288
|
+
## License
|
|
289
|
+
|
|
290
|
+
MIT
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# stdnum-centroamerica
|
|
2
|
+
|
|
3
|
+
[](https://github.com/momotombodevs/stdnum-centroamerica-py/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/stdnum-centroamerica/)
|
|
5
|
+
[](pyproject.toml)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
Validate, format and compact Central American identity and tax document numbers: DPI, NIT, DUI, NRC, RTN, cédulas, RUC, DIMEX and passport MRZs. No dependencies, fully typed, Python 3.11 or later. Works in any project: scripts, FastAPI, Flask, Django, DRF, Celery or notebooks.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
With [uv](https://docs.astral.sh/uv/) (recommended):
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
uv add stdnum-centroamerica
|
|
16
|
+
uv add "stdnum-centroamerica[pydantic]" # FastAPI / Pydantic v2 integration
|
|
17
|
+
uv add "stdnum-centroamerica[django]" # Django / DRF integration
|
|
18
|
+
uv add "stdnum-centroamerica[wtforms]" # Flask-WTF / WTForms integration
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
With pip, inside a virtual environment:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python -m venv .venv
|
|
25
|
+
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
|
26
|
+
python -m pip install stdnum-centroamerica
|
|
27
|
+
python -m pip install "stdnum-centroamerica[django]"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
More options (Poetry, PDM, installing from GitHub, editable mode, hashes) in [docs/installation.md](docs/installation.md).
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
Each document has a `country.document` key (`ni.cedula`, `gt.dpi`, ...) and three operations: validate, format and compact.
|
|
35
|
+
|
|
36
|
+
```pycon
|
|
37
|
+
>>> import stdnum_centroamerica as sca
|
|
38
|
+
|
|
39
|
+
>>> sca.is_valid("ni.cedula", "001-010190-0001N")
|
|
40
|
+
True
|
|
41
|
+
>>> sca.is_valid("ni.ruc", "J0310000000001") # legal entity
|
|
42
|
+
True
|
|
43
|
+
>>> sca.is_valid("ni.ruc", "0000300766011M") # a RUC cannot start with four zeros
|
|
44
|
+
False
|
|
45
|
+
|
|
46
|
+
>>> sca.make("ni.cedula").format("0010101900001n")
|
|
47
|
+
'001-010190-0001N'
|
|
48
|
+
>>> sca.make("ni.cedula").compact("001-010190-0001n")
|
|
49
|
+
'0010101900001N'
|
|
50
|
+
>>> sca.make("gt.dpi").format("1234567890101")
|
|
51
|
+
'1234 56789 0101'
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`validate()` also returns the reason for a failure, which is useful for error messages:
|
|
56
|
+
|
|
57
|
+
```pycon
|
|
58
|
+
>>> result = sca.validate("sv.dui", "12345678-5")
|
|
59
|
+
>>> result.is_valid
|
|
60
|
+
False
|
|
61
|
+
>>> result.error
|
|
62
|
+
<ErrorCode.CHECKSUM: 'checksum'>
|
|
63
|
+
>>> bool(sca.validate("sv.dui", "12345678-4"))
|
|
64
|
+
True
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
| `ErrorCode` | Value | Meaning |
|
|
69
|
+
|-------------|-------|---------|
|
|
70
|
+
| `FORMAT` | `"format"` | wrong length, characters or structure |
|
|
71
|
+
| `COMPONENT` | `"component"` | a component (date, department, municipality, prefix) is out of range |
|
|
72
|
+
| `CHECKSUM` | `"checksum"` | the check digit or letter does not match |
|
|
73
|
+
|
|
74
|
+
`ErrorCode` is a `StrEnum`, so it serializes to JSON as plain text.
|
|
75
|
+
|
|
76
|
+
### Options
|
|
77
|
+
|
|
78
|
+
The Nicaraguan cédula does not check its letter by default, because the CSE does not publish the formula (modulo 23, from unofficial sources). Options are passed as keyword arguments:
|
|
79
|
+
|
|
80
|
+
```pycon
|
|
81
|
+
>>> sca.is_valid("ni.cedula", "001-010190-0001A") # the correct letter is N
|
|
82
|
+
True
|
|
83
|
+
>>> sca.is_valid("ni.cedula", "001-010190-0001A", strict_letter=True)
|
|
84
|
+
False
|
|
85
|
+
>>> sca.is_valid("ni.cedula", "282-010190-0001A", strict_municipality=True) # 282 is not in the list
|
|
86
|
+
False
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The document classes can also be used directly:
|
|
91
|
+
|
|
92
|
+
```pycon
|
|
93
|
+
>>> from stdnum_centroamerica.ni import Cedula
|
|
94
|
+
>>> Cedula(strict_municipality=True).is_valid("001-010190-0001N")
|
|
95
|
+
True
|
|
96
|
+
>>> Cedula.check_letter("0010101900001")
|
|
97
|
+
'N'
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
A misspelled option raises `TypeError` instead of being silently ignored.
|
|
102
|
+
|
|
103
|
+
### Parsing components
|
|
104
|
+
|
|
105
|
+
`parse()` reads the components encoded in the number and returns an immutable object, or `None` if the number is not valid. It is available for `ni.cedula`, `hn.identidad`, `gt.dpi`, `pa.cedula` and the three MRZ formats.
|
|
106
|
+
|
|
107
|
+
```pycon
|
|
108
|
+
>>> cedula = sca.parse("ni.cedula", "001-010190-0001N")
|
|
109
|
+
>>> cedula.municipality_code, cedula.birth_day, cedula.birth_month, cedula.birth_year_two_digits
|
|
110
|
+
(1, 1, 1, 90)
|
|
111
|
+
>>> import datetime
|
|
112
|
+
>>> cedula.birth_year_candidates(datetime.date(2026, 11, 5))
|
|
113
|
+
(1990,)
|
|
114
|
+
|
|
115
|
+
>>> sca.parse("hn.identidad", "0906-1990-00001").municipality_code
|
|
116
|
+
6
|
|
117
|
+
>>> sca.parse("gt.dpi", "1234 56789 0101").department_code
|
|
118
|
+
1
|
|
119
|
+
>>> sca.parse("pa.cedula", "PE-5-687").kind
|
|
120
|
+
<CedulaKind.BORN_ABROAD: 'born_abroad'>
|
|
121
|
+
|
|
122
|
+
>>> mrz = "P<UTOERIKSSON<<ANNA<MARIA<<<<<<<<<<<<<<<<<<<\nL898902C36UTO7408122F1204159ZE184226B<<<<<10"
|
|
123
|
+
>>> sca.is_valid("mrz.td3", mrz)
|
|
124
|
+
True
|
|
125
|
+
>>> sca.parse("mrz.td3", mrz).surname
|
|
126
|
+
'ERIKSSON'
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The number does not encode the century, so `birth_year_candidates()` returns the possible years (it may be empty: 29/02/01 exists neither in 1901 nor in 2001). For the same reason it cannot be used to decide legal age: someone born in 2010 also gets the candidate 1910.
|
|
131
|
+
|
|
132
|
+
Components come from the structure of the number and are not checked against any registry. **Birth dates and the other components are personal data**: they are hidden from `repr()` so they do not end up in a log by accident, but treat them accordingly (see [docs/security.md](docs/security.md)).
|
|
133
|
+
|
|
134
|
+
### Test data
|
|
135
|
+
|
|
136
|
+
`generate()` returns a synthetic valid number, useful for fixtures and sample data. It is random and may match a real number by chance: do not use it as data about people.
|
|
137
|
+
|
|
138
|
+
```pycon
|
|
139
|
+
>>> sca.is_valid("gt.dpi", sca.generate("gt.dpi"))
|
|
140
|
+
True
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Catalog and custom documents
|
|
145
|
+
|
|
146
|
+
```pycon
|
|
147
|
+
>>> len(sca.keys())
|
|
148
|
+
18
|
|
149
|
+
>>> info = sca.describe()[0]
|
|
150
|
+
>>> info.key, info.country, info.name, info.level
|
|
151
|
+
('gt.nit', 'GT', 'NIT', <Level.COMPLETE: 'complete'>)
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
An unknown key raises `UnknownDocumentError` (with suggestions when it resembles an existing key). `sca.register(key, cls)` adds or replaces a document; the class must implement the `Document` protocol (`is_valid`, `validate`, `format`, `compact`). Call it at application startup.
|
|
156
|
+
|
|
157
|
+
## Supported documents
|
|
158
|
+
|
|
159
|
+
**complete** = validates format and check digit; **format** = validates length, characters and basic ranges.
|
|
160
|
+
|
|
161
|
+
| Country | Document | Key | Level |
|
|
162
|
+
|---------|----------|-----|-------|
|
|
163
|
+
| Guatemala | NIT | `gt.nit` | complete |
|
|
164
|
+
| Guatemala | DPI (CUI) | `gt.dpi` | complete (check digit and department) |
|
|
165
|
+
| El Salvador | DUI | `sv.dui` | complete |
|
|
166
|
+
| El Salvador | NIT | `sv.nit` | complete (also accepts the 9-digit DUI) |
|
|
167
|
+
| El Salvador | NRC | `sv.nrc` | format |
|
|
168
|
+
| Honduras | Número de identidad | `hn.identidad` | format (valid department and municipality) |
|
|
169
|
+
| Honduras | RTN | `hn.rtn` | format |
|
|
170
|
+
| Nicaragua | Cédula | `ni.cedula` | complete (check letter with `strict_letter`, off by default) |
|
|
171
|
+
| Nicaragua | RUC | `ni.ruc` | format |
|
|
172
|
+
| Costa Rica | Cédula física | `cr.cedula_fisica` | format |
|
|
173
|
+
| Costa Rica | Cédula jurídica | `cr.cedula_juridica` | format |
|
|
174
|
+
| Costa Rica | DIMEX | `cr.dimex` | format |
|
|
175
|
+
| Costa Rica | NITE | `cr.nite` | format |
|
|
176
|
+
| Panama | Cédula | `pa.cedula` | format |
|
|
177
|
+
| Panama | RUC | `pa.ruc` | format (the DV is not validated) |
|
|
178
|
+
| International | Passport MRZ (TD3) | `mrz.td3` | complete (ICAO 9303) |
|
|
179
|
+
| International | ID card MRZ (TD1) | `mrz.td1` | complete (ICAO 9303) |
|
|
180
|
+
| International | ID document MRZ (TD2) | `mrz.td2` | complete (ICAO 9303) |
|
|
181
|
+
|
|
182
|
+
Notes:
|
|
183
|
+
|
|
184
|
+
- **Panama:** volume and entry have variable length, so the number can only be read with its hyphens. Use `format()` for the canonical form (`8-926-1601`); `compact()` removes the hyphens.
|
|
185
|
+
- **Honduras:** `hn.identidad` accepts the special codes for naturalized citizens (`0890`) and dual nationality (`0880`).
|
|
186
|
+
- **MRZ:** accepted with or without line breaks. Dates stay as `YYMMDD`, because the century cannot be inferred. Country-specific passport numbers are not validated, only the MRZ.
|
|
187
|
+
- **Nicaragua:** the municipality codes and the check letter come from third parties (the CSE does not publish them), and code 888 has a single source. In Panama, provinces 10 to 13 and the PI and AV suffixes are not official either.
|
|
188
|
+
- The source of every rule, with the date it was consulted, is in [docs/sources.md](docs/sources.md).
|
|
189
|
+
|
|
190
|
+
## Framework integrations
|
|
191
|
+
|
|
192
|
+
The core works in any project with no configuration. The integrations are optional and report errors in each framework's own format. Their messages default to English and can be overridden (see each guide).
|
|
193
|
+
|
|
194
|
+
**FastAPI / Pydantic v2** ([guide](docs/fastapi.md)):
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
from typing import Annotated
|
|
198
|
+
from pydantic import BaseModel
|
|
199
|
+
from stdnum_centroamerica.contrib.pydantic import Document
|
|
200
|
+
|
|
201
|
+
Dpi = Annotated[str, Document("gt.dpi")] # validates and stores it compact
|
|
202
|
+
|
|
203
|
+
class Customer(BaseModel):
|
|
204
|
+
dpi: Dpi
|
|
205
|
+
cedula: Annotated[str, Document("ni.cedula", normalize="format", strict_letter=True)]
|
|
206
|
+
# When invalid, FastAPI responds with 422 and type="stdnum_checksum" (or format / component).
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Django and DRF** ([guide](docs/django.md)):
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
from stdnum_centroamerica.contrib.django import DocumentFormField, DocumentValidator
|
|
213
|
+
|
|
214
|
+
class Customer(models.Model):
|
|
215
|
+
dpi = models.CharField(max_length=13, validators=[DocumentValidator("gt.dpi")])
|
|
216
|
+
|
|
217
|
+
class CustomerForm(forms.Form):
|
|
218
|
+
dpi = DocumentFormField("gt.dpi") # cleaned_data["dpi"] is compact
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Flask** ([guide](docs/flask.md)):
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
from stdnum_centroamerica.contrib.wtforms import Document
|
|
225
|
+
|
|
226
|
+
class CustomerForm(FlaskForm):
|
|
227
|
+
dpi = StringField("DPI", validators=[DataRequired(), Document("gt.dpi")])
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Complete, tested examples in [examples/](examples/).
|
|
231
|
+
|
|
232
|
+
## Security
|
|
233
|
+
|
|
234
|
+
- No dependencies, no network access and no I/O: the package never stores or logs numbers.
|
|
235
|
+
- Only `str` inputs of up to 256 ASCII characters are accepted; anything else is a `FORMAT` error and no regular expression runs. This rejects digits from other scripts, Unicode hyphens and invisible spaces.
|
|
236
|
+
- Regular expressions run in linear time (no catastrophic backtracking), and the tests check this with adversarial inputs.
|
|
237
|
+
- Instances and data tables are immutable, so they can be shared between threads.
|
|
238
|
+
|
|
239
|
+
Details and recommendations for integrators in [docs/security.md](docs/security.md). To report a vulnerability, see [SECURITY.md](SECURITY.md).
|
|
240
|
+
|
|
241
|
+
## Development
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
uv sync
|
|
245
|
+
uv run pytest # tests (including Hypothesis property tests)
|
|
246
|
+
uv run ruff check . && uv run ruff format --check .
|
|
247
|
+
uv run mypy # strict mode
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for the branching model and how to add a document.
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
MIT
|