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.
Files changed (31) hide show
  1. stdnum_centroamerica-0.1.0/LICENSE +21 -0
  2. stdnum_centroamerica-0.1.0/PKG-INFO +290 -0
  3. stdnum_centroamerica-0.1.0/README.md +254 -0
  4. stdnum_centroamerica-0.1.0/pyproject.toml +185 -0
  5. stdnum_centroamerica-0.1.0/pyproject.toml.orig +173 -0
  6. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/__init__.py +67 -0
  7. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_base.py +166 -0
  8. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_data/__init__.py +1 -0
  9. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_data/honduras.py +43 -0
  10. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_data/nicaragua.py +41 -0
  11. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_dates.py +16 -0
  12. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_random.py +28 -0
  13. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_registry.py +184 -0
  14. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/_types.py +111 -0
  15. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/contrib/__init__.py +9 -0
  16. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/contrib/_common.py +41 -0
  17. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/contrib/django.py +129 -0
  18. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/contrib/pydantic.py +91 -0
  19. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/contrib/wtforms.py +76 -0
  20. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/cr/__init__.py +64 -0
  21. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/exceptions.py +20 -0
  22. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/gt/__init__.py +103 -0
  23. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/hn/__init__.py +102 -0
  24. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/mrz/__init__.py +408 -0
  25. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/ni/__init__.py +204 -0
  26. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/pa/__init__.py +176 -0
  27. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/py.typed +0 -0
  28. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/sv/__init__.py +7 -0
  29. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/sv/dui.py +37 -0
  30. stdnum_centroamerica-0.1.0/src/stdnum_centroamerica/sv/nit.py +64 -0
  31. 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
+ [![ci](https://github.com/momotombodevs/stdnum-centroamerica-py/actions/workflows/ci.yml/badge.svg)](https://github.com/momotombodevs/stdnum-centroamerica-py/actions/workflows/ci.yml)
40
+ [![PyPI](https://img.shields.io/pypi/v/stdnum-centroamerica)](https://pypi.org/project/stdnum-centroamerica/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/stdnum-centroamerica)](pyproject.toml)
42
+ [![License](https://img.shields.io/pypi/l/stdnum-centroamerica)](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
+ [![ci](https://github.com/momotombodevs/stdnum-centroamerica-py/actions/workflows/ci.yml/badge.svg)](https://github.com/momotombodevs/stdnum-centroamerica-py/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/stdnum-centroamerica)](https://pypi.org/project/stdnum-centroamerica/)
5
+ [![Python](https://img.shields.io/pypi/pyversions/stdnum-centroamerica)](pyproject.toml)
6
+ [![License](https://img.shields.io/pypi/l/stdnum-centroamerica)](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