postali-api 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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .mypy_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Postali <hi@postali.app>
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,195 @@
1
+ Metadata-Version: 2.5
2
+ Name: postali-api
3
+ Version: 0.1.0
4
+ Summary: Cliente oficial de la API gratuita de códigos postales de Postali (México, Colombia, España). Sin dependencias.
5
+ Project-URL: Homepage, https://postali.app
6
+ Project-URL: Documentation, https://postali.app/api/docs
7
+ Project-URL: Repository, https://github.com/gomflo/postali-py
8
+ Project-URL: Issues, https://github.com/gomflo/postali-py/issues
9
+ Author-email: Postali <hi@postali.app>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api,codigo postal,colombia,colonias,cp,códigos postales,geonames,mexico,postal code,sepomex,spain,zip code
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Natural Language :: Spanish
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Internet :: WWW/HTTP
26
+ Classifier: Topic :: Software Development :: Libraries
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.9
29
+ Description-Content-Type: text/markdown
30
+
31
+ # postali
32
+
33
+ Cliente oficial para Python de la **API gratuita de códigos postales de [Postali](https://postali.app)**: México, Colombia y España.
34
+
35
+ - Sin API key, sin registro, sin cuota mensual.
36
+ - Cero dependencias: sólo la biblioteca estándar (`urllib`). Python 3.9+.
37
+ - Respuestas tipadas como `dataclasses` inmutables (`py.typed` incluido).
38
+ - Restaura el cero inicial que se pierde en Excel, un CSV o `int()` (`6700` → `"06700"`).
39
+
40
+ **Documentación de la API:** https://postali.app/api/docs · **Sitio:** https://postali.app
41
+
42
+ ## Instalación
43
+
44
+ ```bash
45
+ pip install postali-api
46
+ ```
47
+
48
+ El paquete se instala como `postali-api` y se importa como `postali`.
49
+
50
+ ## Uso rápido
51
+
52
+ ```python
53
+ from postali import Postali
54
+
55
+ postali = Postali("mx")
56
+ print(postali.cp("06700").asentamientos[0].nombre) # Roma Norte
57
+ ```
58
+
59
+ ## Ejemplos
60
+
61
+ ### Autocompletar la colonia en un formulario de dirección
62
+
63
+ Un endpoint de tu backend (aquí con Flask) que el formulario llama cuando el usuario termina de escribir el CP:
64
+
65
+ ```python
66
+ from flask import Flask, jsonify
67
+ from postali import Postali, PostaliError
68
+
69
+ app = Flask(__name__)
70
+ postali = Postali("mx")
71
+
72
+ @app.get("/direccion/<cp>")
73
+ def direccion(cp: str):
74
+ try:
75
+ r = postali.cp(cp) # "6700" también funciona
76
+ except PostaliError as e:
77
+ if e.code in ("not_found", "invalid_cp"):
78
+ return jsonify(error="CP no encontrado"), 404
79
+ raise
80
+ return jsonify(
81
+ estado=r.estado,
82
+ municipio=r.municipio,
83
+ colonias=[a.nombre for a in r.asentamientos],
84
+ )
85
+ ```
86
+
87
+ ¿Buscas por nombre de colonia en lugar de CP?
88
+
89
+ ```python
90
+ for hit in postali.search("roma norte", limit=5).results:
91
+ print(hit.cp, hit.nombre, hit.municipio, hit.estado)
92
+ ```
93
+
94
+ ### Validar un CP
95
+
96
+ ```python
97
+ if not postali.validate("06700"): # ValidateResult es truthy si el CP existe
98
+ raise ValueError("Ese código postal no existe.")
99
+ ```
100
+
101
+ `validate` lanza `PostaliError` con `code == "invalid_cp"` si el texto ni siquiera tiene formato de CP (p. ej. `"abc"`), sin hacer la petición.
102
+
103
+ ### Limpiar una columna de CPs (CSV / pandas)
104
+
105
+ ```python
106
+ r = postali.bulk(["06700", "44100", 6700, "abc"])
107
+ for item in r.results: # mismo orden que la entrada
108
+ print(item.cp, item.valid, item.municipio)
109
+ # 06700 True Cuauhtémoc · 44100 True Guadalajara · 06700 True Cuauhtémoc · abc False None
110
+ ```
111
+
112
+ `bulk` normaliza cada código, marca los que no tienen formato válido como `valid=False` y parte la lista en lotes de 100.
113
+
114
+ ### Colombia y España
115
+
116
+ ```python
117
+ Postali("co").cp("050001").municipio # 'Medellín'
118
+ Postali("es").cp(8001).municipio # "08001" → 'Barcelona'
119
+ ```
120
+
121
+ ## API
122
+
123
+ `Postali(country="mx", *, base_url="https://postali.app", timeout=10.0, user_agent=...)`
124
+
125
+ | Método | Endpoint | Devuelve |
126
+ |---|---|---|
127
+ | `cp(codigo)` | `GET /api/v1/{country}/cp/{codigo}` | `CpResult` |
128
+ | `validate(codigo)` | `GET /api/v1/{country}/validate/{codigo}` | `ValidateResult` |
129
+ | `search(q, limit=None)` | `GET /api/v1/{country}/search?q=` | `SearchResult` |
130
+ | `estados()` | `GET /api/v1/{country}/estados` | `EstadosResult` |
131
+ | `estado(slug)` | `GET /api/v1/{country}/estado/{slug}` | `Estado` |
132
+ | `municipios(estado_slug)` | `GET /api/v1/{country}/estado/{slug}/municipios` | `MunicipiosResult` |
133
+ | `municipio(estado_slug, municipio_slug)` | `GET /api/v1/{country}/municipio/{estado}/{municipio}` | `MunicipioResult` |
134
+ | `bulk(codigos)` | `POST /api/v1/{country}/bulk` | `BulkResult` |
135
+
136
+ "Estado" es el nivel 1 de cada país: estado en México, departamento en Colombia, provincia en España. Los campos de cada resultado se llaman igual que en el JSON de la API.
137
+
138
+ ### Errores
139
+
140
+ Todo error de la API o de red se lanza como `PostaliError`, con `code`, `status`, `message` y `docs_url`:
141
+
142
+ ```python
143
+ from postali import PostaliError
144
+
145
+ try:
146
+ postali.cp("00000")
147
+ except PostaliError as e:
148
+ print(e.code, e.status, e.message) # not_found 404 No se encontró el recurso solicitado.
149
+ ```
150
+
151
+ | `code` | Cuándo |
152
+ |---|---|
153
+ | `invalid_cp` | El CP no tiene el formato del país |
154
+ | `invalid_query` | Falta `q`, está vacía o hay un parámetro inválido |
155
+ | `not_found` | El CP, estado o municipio no existe |
156
+ | `rate_limited` | Demasiadas peticiones desde tu IP |
157
+ | `internal_error` | Error del servidor (5xx) |
158
+ | `timeout` | Se superó el `timeout` |
159
+ | `network_error` | No se pudo conectar |
160
+ | `http_error` | Otra respuesta no exitosa |
161
+
162
+ ### Normalización de CPs
163
+
164
+ ```python
165
+ from postali import normalize_cp
166
+
167
+ normalize_cp("6700") # '06700'
168
+ normalize_cp(" 76 148 ") # '76148'
169
+ normalize_cp("50001", "co") # '050001'
170
+ normalize_cp("670") # None (incompleto)
171
+ ```
172
+
173
+ Se añade como máximo un cero, y nunca delante de otro cero: ningún CP de México, Colombia o España empieza por `00`.
174
+
175
+ ## Datos y atribución
176
+
177
+ Datos: Sepomex vía [Postali](https://postali.app) / GeoNames ([CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)) para CO y ES.
178
+
179
+ ## English
180
+
181
+ `postali` is the official zero-dependency Python client (stdlib `urllib`, Python 3.9+) for the free [Postali](https://postali.app) postal-code API covering Mexico, Colombia and Spain. No API key required.
182
+
183
+ ```python
184
+ from postali import Postali
185
+ r = Postali("mx").cp("06700") # "mx" | "co" | "es"
186
+ print(r.estado, r.municipio, [a.nombre for a in r.asentamientos])
187
+ ```
188
+
189
+ Responses are typed frozen dataclasses; errors raise `PostaliError` with `code` (`invalid_cp`, `not_found`, `rate_limited`, `timeout`…), `status` and `docs_url`. Postal codes that lost their leading zero (`6700`) are restored automatically. API reference: https://postali.app/api/docs
190
+
191
+ Data: Sepomex via Postali / GeoNames (CC BY 4.0) for CO and ES.
192
+
193
+ ## Licencia
194
+
195
+ MIT © Postali
@@ -0,0 +1,165 @@
1
+ # postali
2
+
3
+ Cliente oficial para Python de la **API gratuita de códigos postales de [Postali](https://postali.app)**: México, Colombia y España.
4
+
5
+ - Sin API key, sin registro, sin cuota mensual.
6
+ - Cero dependencias: sólo la biblioteca estándar (`urllib`). Python 3.9+.
7
+ - Respuestas tipadas como `dataclasses` inmutables (`py.typed` incluido).
8
+ - Restaura el cero inicial que se pierde en Excel, un CSV o `int()` (`6700` → `"06700"`).
9
+
10
+ **Documentación de la API:** https://postali.app/api/docs · **Sitio:** https://postali.app
11
+
12
+ ## Instalación
13
+
14
+ ```bash
15
+ pip install postali-api
16
+ ```
17
+
18
+ El paquete se instala como `postali-api` y se importa como `postali`.
19
+
20
+ ## Uso rápido
21
+
22
+ ```python
23
+ from postali import Postali
24
+
25
+ postali = Postali("mx")
26
+ print(postali.cp("06700").asentamientos[0].nombre) # Roma Norte
27
+ ```
28
+
29
+ ## Ejemplos
30
+
31
+ ### Autocompletar la colonia en un formulario de dirección
32
+
33
+ Un endpoint de tu backend (aquí con Flask) que el formulario llama cuando el usuario termina de escribir el CP:
34
+
35
+ ```python
36
+ from flask import Flask, jsonify
37
+ from postali import Postali, PostaliError
38
+
39
+ app = Flask(__name__)
40
+ postali = Postali("mx")
41
+
42
+ @app.get("/direccion/<cp>")
43
+ def direccion(cp: str):
44
+ try:
45
+ r = postali.cp(cp) # "6700" también funciona
46
+ except PostaliError as e:
47
+ if e.code in ("not_found", "invalid_cp"):
48
+ return jsonify(error="CP no encontrado"), 404
49
+ raise
50
+ return jsonify(
51
+ estado=r.estado,
52
+ municipio=r.municipio,
53
+ colonias=[a.nombre for a in r.asentamientos],
54
+ )
55
+ ```
56
+
57
+ ¿Buscas por nombre de colonia en lugar de CP?
58
+
59
+ ```python
60
+ for hit in postali.search("roma norte", limit=5).results:
61
+ print(hit.cp, hit.nombre, hit.municipio, hit.estado)
62
+ ```
63
+
64
+ ### Validar un CP
65
+
66
+ ```python
67
+ if not postali.validate("06700"): # ValidateResult es truthy si el CP existe
68
+ raise ValueError("Ese código postal no existe.")
69
+ ```
70
+
71
+ `validate` lanza `PostaliError` con `code == "invalid_cp"` si el texto ni siquiera tiene formato de CP (p. ej. `"abc"`), sin hacer la petición.
72
+
73
+ ### Limpiar una columna de CPs (CSV / pandas)
74
+
75
+ ```python
76
+ r = postali.bulk(["06700", "44100", 6700, "abc"])
77
+ for item in r.results: # mismo orden que la entrada
78
+ print(item.cp, item.valid, item.municipio)
79
+ # 06700 True Cuauhtémoc · 44100 True Guadalajara · 06700 True Cuauhtémoc · abc False None
80
+ ```
81
+
82
+ `bulk` normaliza cada código, marca los que no tienen formato válido como `valid=False` y parte la lista en lotes de 100.
83
+
84
+ ### Colombia y España
85
+
86
+ ```python
87
+ Postali("co").cp("050001").municipio # 'Medellín'
88
+ Postali("es").cp(8001).municipio # "08001" → 'Barcelona'
89
+ ```
90
+
91
+ ## API
92
+
93
+ `Postali(country="mx", *, base_url="https://postali.app", timeout=10.0, user_agent=...)`
94
+
95
+ | Método | Endpoint | Devuelve |
96
+ |---|---|---|
97
+ | `cp(codigo)` | `GET /api/v1/{country}/cp/{codigo}` | `CpResult` |
98
+ | `validate(codigo)` | `GET /api/v1/{country}/validate/{codigo}` | `ValidateResult` |
99
+ | `search(q, limit=None)` | `GET /api/v1/{country}/search?q=` | `SearchResult` |
100
+ | `estados()` | `GET /api/v1/{country}/estados` | `EstadosResult` |
101
+ | `estado(slug)` | `GET /api/v1/{country}/estado/{slug}` | `Estado` |
102
+ | `municipios(estado_slug)` | `GET /api/v1/{country}/estado/{slug}/municipios` | `MunicipiosResult` |
103
+ | `municipio(estado_slug, municipio_slug)` | `GET /api/v1/{country}/municipio/{estado}/{municipio}` | `MunicipioResult` |
104
+ | `bulk(codigos)` | `POST /api/v1/{country}/bulk` | `BulkResult` |
105
+
106
+ "Estado" es el nivel 1 de cada país: estado en México, departamento en Colombia, provincia en España. Los campos de cada resultado se llaman igual que en el JSON de la API.
107
+
108
+ ### Errores
109
+
110
+ Todo error de la API o de red se lanza como `PostaliError`, con `code`, `status`, `message` y `docs_url`:
111
+
112
+ ```python
113
+ from postali import PostaliError
114
+
115
+ try:
116
+ postali.cp("00000")
117
+ except PostaliError as e:
118
+ print(e.code, e.status, e.message) # not_found 404 No se encontró el recurso solicitado.
119
+ ```
120
+
121
+ | `code` | Cuándo |
122
+ |---|---|
123
+ | `invalid_cp` | El CP no tiene el formato del país |
124
+ | `invalid_query` | Falta `q`, está vacía o hay un parámetro inválido |
125
+ | `not_found` | El CP, estado o municipio no existe |
126
+ | `rate_limited` | Demasiadas peticiones desde tu IP |
127
+ | `internal_error` | Error del servidor (5xx) |
128
+ | `timeout` | Se superó el `timeout` |
129
+ | `network_error` | No se pudo conectar |
130
+ | `http_error` | Otra respuesta no exitosa |
131
+
132
+ ### Normalización de CPs
133
+
134
+ ```python
135
+ from postali import normalize_cp
136
+
137
+ normalize_cp("6700") # '06700'
138
+ normalize_cp(" 76 148 ") # '76148'
139
+ normalize_cp("50001", "co") # '050001'
140
+ normalize_cp("670") # None (incompleto)
141
+ ```
142
+
143
+ Se añade como máximo un cero, y nunca delante de otro cero: ningún CP de México, Colombia o España empieza por `00`.
144
+
145
+ ## Datos y atribución
146
+
147
+ Datos: Sepomex vía [Postali](https://postali.app) / GeoNames ([CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)) para CO y ES.
148
+
149
+ ## English
150
+
151
+ `postali` is the official zero-dependency Python client (stdlib `urllib`, Python 3.9+) for the free [Postali](https://postali.app) postal-code API covering Mexico, Colombia and Spain. No API key required.
152
+
153
+ ```python
154
+ from postali import Postali
155
+ r = Postali("mx").cp("06700") # "mx" | "co" | "es"
156
+ print(r.estado, r.municipio, [a.nombre for a in r.asentamientos])
157
+ ```
158
+
159
+ Responses are typed frozen dataclasses; errors raise `PostaliError` with `code` (`invalid_cp`, `not_found`, `rate_limited`, `timeout`…), `status` and `docs_url`. Postal codes that lost their leading zero (`6700`) are restored automatically. API reference: https://postali.app/api/docs
160
+
161
+ Data: Sepomex via Postali / GeoNames (CC BY 4.0) for CO and ES.
162
+
163
+ ## Licencia
164
+
165
+ MIT © Postali
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "postali-api"
7
+ dynamic = ["version"]
8
+ description = "Cliente oficial de la API gratuita de códigos postales de Postali (México, Colombia, España). Sin dependencias."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Postali", email = "hi@postali.app" }]
14
+ keywords = [
15
+ "codigo postal", "códigos postales", "cp", "sepomex", "colonias",
16
+ "postal code", "zip code", "mexico", "colombia", "spain", "geonames", "api",
17
+ ]
18
+ classifiers = [
19
+ "Development Status :: 4 - Beta",
20
+ "Intended Audience :: Developers",
21
+ "Natural Language :: Spanish",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3",
24
+ "Programming Language :: Python :: 3 :: Only",
25
+ "Programming Language :: Python :: 3.9",
26
+ "Programming Language :: Python :: 3.10",
27
+ "Programming Language :: Python :: 3.11",
28
+ "Programming Language :: Python :: 3.12",
29
+ "Programming Language :: Python :: 3.13",
30
+ "Programming Language :: Python :: 3.14",
31
+ "Topic :: Internet :: WWW/HTTP",
32
+ "Topic :: Software Development :: Libraries",
33
+ "Typing :: Typed",
34
+ ]
35
+ dependencies = []
36
+
37
+ [project.urls]
38
+ Homepage = "https://postali.app"
39
+ Documentation = "https://postali.app/api/docs"
40
+ Repository = "https://github.com/gomflo/postali-py"
41
+ Issues = "https://github.com/gomflo/postali-py/issues"
42
+
43
+ [tool.hatch.version]
44
+ path = "src/postali/_version.py"
45
+
46
+ [tool.hatch.build.targets.wheel]
47
+ packages = ["src/postali"]
48
+
49
+ [tool.hatch.build.targets.sdist]
50
+ include = ["src/postali", "tests", "README.md", "LICENSE", "pyproject.toml"]
51
+
52
+ [tool.pytest.ini_options]
53
+ testpaths = ["tests"]
54
+ markers = ["live: pruebas contra la API real (POSTALI_LIVE=1)"]
@@ -0,0 +1,65 @@
1
+ """Cliente oficial de la API gratuita de códigos postales de Postali.
2
+
3
+ México, Colombia y España. Sin API key, sin dependencias.
4
+
5
+ >>> from postali import Postali
6
+ >>> Postali("mx").cp("06700").asentamientos[0].nombre
7
+ 'Roma Norte'
8
+
9
+ Documentación de la API: https://postali.app/api/docs
10
+ """
11
+
12
+ from ._client import (
13
+ BULK_MAX,
14
+ DEFAULT_BASE_URL,
15
+ DEFAULT_TIMEOUT,
16
+ DEFAULT_USER_AGENT,
17
+ SEARCH_MAX_LENGTH,
18
+ Postali,
19
+ )
20
+ from ._errors import DOCS_URL, PostaliError
21
+ from ._models import (
22
+ Asentamiento,
23
+ BulkItem,
24
+ BulkResult,
25
+ Colonia,
26
+ CpResult,
27
+ Estado,
28
+ EstadosResult,
29
+ MunicipioItem,
30
+ MunicipioResult,
31
+ MunicipiosResult,
32
+ SearchHit,
33
+ SearchResult,
34
+ ValidateResult,
35
+ )
36
+ from ._normalize import COUNTRIES, CP_LENGTH, normalize_cp
37
+ from ._version import __version__
38
+
39
+ __all__ = [
40
+ "Postali",
41
+ "PostaliError",
42
+ "normalize_cp",
43
+ "COUNTRIES",
44
+ "CP_LENGTH",
45
+ "BULK_MAX",
46
+ "SEARCH_MAX_LENGTH",
47
+ "DEFAULT_BASE_URL",
48
+ "DEFAULT_TIMEOUT",
49
+ "DEFAULT_USER_AGENT",
50
+ "DOCS_URL",
51
+ "Asentamiento",
52
+ "BulkItem",
53
+ "BulkResult",
54
+ "Colonia",
55
+ "CpResult",
56
+ "Estado",
57
+ "EstadosResult",
58
+ "MunicipioItem",
59
+ "MunicipioResult",
60
+ "MunicipiosResult",
61
+ "SearchHit",
62
+ "SearchResult",
63
+ "ValidateResult",
64
+ "__version__",
65
+ ]