catalogmx 0.3.0__py3-none-any.whl
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.
- catalogmx/__init__.py +56 -0
- catalogmx/catalogs/__init__.py +5 -0
- catalogmx/catalogs/banxico/__init__.py +24 -0
- catalogmx/catalogs/banxico/banks.py +136 -0
- catalogmx/catalogs/banxico/codigos_plaza.py +287 -0
- catalogmx/catalogs/banxico/instituciones_financieras.py +338 -0
- catalogmx/catalogs/banxico/monedas_divisas.py +386 -0
- catalogmx/catalogs/banxico/udis.py +279 -0
- catalogmx/catalogs/ift/__init__.py +15 -0
- catalogmx/catalogs/ift/codigos_lada.py +426 -0
- catalogmx/catalogs/ift/operadores_moviles.py +315 -0
- catalogmx/catalogs/inegi/__init__.py +21 -0
- catalogmx/catalogs/inegi/localidades.py +207 -0
- catalogmx/catalogs/inegi/municipios.py +73 -0
- catalogmx/catalogs/inegi/municipios_completo.py +236 -0
- catalogmx/catalogs/inegi/states.py +148 -0
- catalogmx/catalogs/mexico/__init__.py +17 -0
- catalogmx/catalogs/mexico/hoy_no_circula.py +215 -0
- catalogmx/catalogs/mexico/placas_formatos.py +184 -0
- catalogmx/catalogs/mexico/salarios_minimos.py +156 -0
- catalogmx/catalogs/mexico/uma.py +207 -0
- catalogmx/catalogs/sat/__init__.py +13 -0
- catalogmx/catalogs/sat/carta_porte/__init__.py +19 -0
- catalogmx/catalogs/sat/carta_porte/aeropuertos.py +76 -0
- catalogmx/catalogs/sat/carta_porte/carreteras.py +59 -0
- catalogmx/catalogs/sat/carta_porte/config_autotransporte.py +54 -0
- catalogmx/catalogs/sat/carta_porte/material_peligroso.py +66 -0
- catalogmx/catalogs/sat/carta_porte/puertos_maritimos.py +63 -0
- catalogmx/catalogs/sat/carta_porte/tipo_embalaje.py +48 -0
- catalogmx/catalogs/sat/carta_porte/tipo_permiso.py +54 -0
- catalogmx/catalogs/sat/cfdi_4/__init__.py +42 -0
- catalogmx/catalogs/sat/cfdi_4/clave_prod_serv.py +383 -0
- catalogmx/catalogs/sat/cfdi_4/clave_unidad.py +298 -0
- catalogmx/catalogs/sat/cfdi_4/exportacion.py +45 -0
- catalogmx/catalogs/sat/cfdi_4/forma_pago.py +45 -0
- catalogmx/catalogs/sat/cfdi_4/impuesto.py +57 -0
- catalogmx/catalogs/sat/cfdi_4/meses.py +34 -0
- catalogmx/catalogs/sat/cfdi_4/metodo_pago.py +45 -0
- catalogmx/catalogs/sat/cfdi_4/objeto_imp.py +45 -0
- catalogmx/catalogs/sat/cfdi_4/periodicidad.py +34 -0
- catalogmx/catalogs/sat/cfdi_4/regimen_fiscal.py +57 -0
- catalogmx/catalogs/sat/cfdi_4/tasa_o_cuota.py +42 -0
- catalogmx/catalogs/sat/cfdi_4/tipo_comprobante.py +45 -0
- catalogmx/catalogs/sat/cfdi_4/tipo_factor.py +34 -0
- catalogmx/catalogs/sat/cfdi_4/tipo_relacion.py +45 -0
- catalogmx/catalogs/sat/cfdi_4/uso_cfdi.py +45 -0
- catalogmx/catalogs/sat/comercio_exterior/__init__.py +39 -0
- catalogmx/catalogs/sat/comercio_exterior/claves_pedimento.py +77 -0
- catalogmx/catalogs/sat/comercio_exterior/estados.py +122 -0
- catalogmx/catalogs/sat/comercio_exterior/incoterms.py +226 -0
- catalogmx/catalogs/sat/comercio_exterior/monedas.py +107 -0
- catalogmx/catalogs/sat/comercio_exterior/motivos_traslado.py +54 -0
- catalogmx/catalogs/sat/comercio_exterior/paises.py +88 -0
- catalogmx/catalogs/sat/comercio_exterior/registro_ident_trib.py +76 -0
- catalogmx/catalogs/sat/comercio_exterior/unidades_aduana.py +54 -0
- catalogmx/catalogs/sat/comercio_exterior/validator.py +212 -0
- catalogmx/catalogs/sat/nomina/__init__.py +19 -0
- catalogmx/catalogs/sat/nomina/banco.py +50 -0
- catalogmx/catalogs/sat/nomina/periodicidad_pago.py +48 -0
- catalogmx/catalogs/sat/nomina/riesgo_puesto.py +56 -0
- catalogmx/catalogs/sat/nomina/tipo_contrato.py +47 -0
- catalogmx/catalogs/sat/nomina/tipo_jornada.py +42 -0
- catalogmx/catalogs/sat/nomina/tipo_nomina.py +52 -0
- catalogmx/catalogs/sat/nomina/tipo_regimen.py +47 -0
- catalogmx/catalogs/sepomex/__init__.py +5 -0
- catalogmx/catalogs/sepomex/codigos_postales.py +184 -0
- catalogmx/cli.py +185 -0
- catalogmx/helpers.py +324 -0
- catalogmx/utils/text.py +55 -0
- catalogmx/validators/__init__.py +0 -0
- catalogmx/validators/clabe.py +233 -0
- catalogmx/validators/curp.py +623 -0
- catalogmx/validators/nss.py +255 -0
- catalogmx/validators/rfc.py +1004 -0
- catalogmx-0.3.0.dist-info/METADATA +644 -0
- catalogmx-0.3.0.dist-info/RECORD +81 -0
- catalogmx-0.3.0.dist-info/WHEEL +5 -0
- catalogmx-0.3.0.dist-info/entry_points.txt +2 -0
- catalogmx-0.3.0.dist-info/licenses/AUTHORS.rst +5 -0
- catalogmx-0.3.0.dist-info/licenses/LICENSE +19 -0
- catalogmx-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Catálogo de operadores de telefonía móvil en México (IFT)
|
|
3
|
+
|
|
4
|
+
Este módulo proporciona acceso al catálogo de operadores móviles
|
|
5
|
+
registrados ante el Instituto Federal de Telecomunicaciones (IFT).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import TypedDict
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OperadorMovil(TypedDict):
|
|
14
|
+
"""Estructura de un operador móvil"""
|
|
15
|
+
|
|
16
|
+
nombre_comercial: str
|
|
17
|
+
razon_social: str
|
|
18
|
+
tipo: str # OMR (Operador Móvil con Red) | OMV (Operador Móvil Virtual)
|
|
19
|
+
grupo_empresarial: str
|
|
20
|
+
tecnologias: list[str] # 2G, 3G, 4G, 5G
|
|
21
|
+
cobertura: str # nacional | regional
|
|
22
|
+
servicios: list[str] # prepago, postpago, datos
|
|
23
|
+
market_share_aprox: float
|
|
24
|
+
fecha_inicio_operaciones: str
|
|
25
|
+
activo: bool
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OperadoresMovilesCatalog:
|
|
29
|
+
"""
|
|
30
|
+
Catálogo de operadores de telefonía móvil en México.
|
|
31
|
+
|
|
32
|
+
Incluye operadores móviles con red propia (OMR) y operadores móviles
|
|
33
|
+
virtuales (OMV).
|
|
34
|
+
|
|
35
|
+
Características:
|
|
36
|
+
- Operadores activos e históricos
|
|
37
|
+
- Información de tecnologías (2G, 3G, 4G, 5G)
|
|
38
|
+
- Market share aproximado
|
|
39
|
+
- Clasificación por tipo (OMR/OMV)
|
|
40
|
+
- Cobertura (nacional/regional)
|
|
41
|
+
|
|
42
|
+
Ejemplo:
|
|
43
|
+
>>> from catalogmx.catalogs.ift import OperadoresMovilesCatalog
|
|
44
|
+
>>>
|
|
45
|
+
>>> # Obtener operadores activos
|
|
46
|
+
>>> activos = OperadoresMovilesCatalog.get_activos()
|
|
47
|
+
>>> for op in activos:
|
|
48
|
+
... print(f"{op['nombre_comercial']}: {op['market_share_aprox']}%")
|
|
49
|
+
>>>
|
|
50
|
+
>>> # Buscar por nombre
|
|
51
|
+
>>> telcel = OperadoresMovilesCatalog.buscar_por_nombre("Telcel")
|
|
52
|
+
>>> print(telcel['razon_social'])
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
_data: list[OperadorMovil] | None = None
|
|
56
|
+
_by_nombre: dict[str, OperadorMovil] | None = None
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def _load_data(cls) -> None:
|
|
60
|
+
"""Carga lazy de datos desde JSON"""
|
|
61
|
+
if cls._data is not None:
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
# Path: catalogmx/packages/python/catalogmx/catalogs/ift/operadores_moviles.py
|
|
65
|
+
# Target: catalogmx/packages/shared-data/ift/operadores_moviles.json
|
|
66
|
+
data_path = (
|
|
67
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
68
|
+
/ "shared-data"
|
|
69
|
+
/ "ift"
|
|
70
|
+
/ "operadores_moviles.json"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
with open(data_path, encoding="utf-8") as f:
|
|
74
|
+
json_data = json.load(f)
|
|
75
|
+
cls._data = json_data["operadores"]
|
|
76
|
+
|
|
77
|
+
# Índice por nombre comercial
|
|
78
|
+
cls._by_nombre = {item["nombre_comercial"].lower(): item for item in cls._data}
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def get_all(cls) -> list[OperadorMovil]:
|
|
82
|
+
"""
|
|
83
|
+
Obtiene todos los operadores móviles.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Lista completa de operadores
|
|
87
|
+
|
|
88
|
+
Ejemplo:
|
|
89
|
+
>>> operadores = OperadoresMovilesCatalog.get_all()
|
|
90
|
+
>>> print(f"Total operadores: {len(operadores)}")
|
|
91
|
+
"""
|
|
92
|
+
cls._load_data()
|
|
93
|
+
return cls._data.copy() # type: ignore
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def get_activos(cls) -> list[OperadorMovil]:
|
|
97
|
+
"""
|
|
98
|
+
Obtiene solo operadores activos.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Lista de operadores actualmente operando
|
|
102
|
+
|
|
103
|
+
Ejemplo:
|
|
104
|
+
>>> activos = OperadoresMovilesCatalog.get_activos()
|
|
105
|
+
>>> for op in activos:
|
|
106
|
+
... print(f"{op['nombre_comercial']} ({op['tipo']})")
|
|
107
|
+
"""
|
|
108
|
+
cls._load_data()
|
|
109
|
+
return [op for op in cls._data if op["activo"]] # type: ignore
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def get_inactivos(cls) -> list[OperadorMovil]:
|
|
113
|
+
"""
|
|
114
|
+
Obtiene operadores que dejaron de operar.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Lista de operadores inactivos
|
|
118
|
+
|
|
119
|
+
Ejemplo:
|
|
120
|
+
>>> inactivos = OperadoresMovilesCatalog.get_inactivos()
|
|
121
|
+
>>> print(f"Operadores históricos: {len(inactivos)}")
|
|
122
|
+
"""
|
|
123
|
+
cls._load_data()
|
|
124
|
+
return [op for op in cls._data if not op["activo"]] # type: ignore
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def buscar_por_nombre(cls, nombre: str) -> OperadorMovil | None:
|
|
128
|
+
"""
|
|
129
|
+
Busca un operador por nombre comercial.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
nombre: Nombre comercial del operador (ej: "Telcel", "AT&T")
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Información del operador o None si no existe
|
|
136
|
+
|
|
137
|
+
Ejemplo:
|
|
138
|
+
>>> telcel = OperadoresMovilesCatalog.buscar_por_nombre("Telcel")
|
|
139
|
+
>>> if telcel:
|
|
140
|
+
... print(f"Razón social: {telcel['razon_social']}")
|
|
141
|
+
... print(f"Market share: {telcel['market_share_aprox']}%")
|
|
142
|
+
"""
|
|
143
|
+
cls._load_data()
|
|
144
|
+
nombre_lower = nombre.lower()
|
|
145
|
+
|
|
146
|
+
# Búsqueda exacta
|
|
147
|
+
exact = cls._by_nombre.get(nombre_lower) # type: ignore
|
|
148
|
+
if exact:
|
|
149
|
+
return exact
|
|
150
|
+
|
|
151
|
+
# Búsqueda parcial
|
|
152
|
+
for op in cls._data: # type: ignore
|
|
153
|
+
if nombre_lower in op["nombre_comercial"].lower():
|
|
154
|
+
return op
|
|
155
|
+
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
@classmethod
|
|
159
|
+
def get_por_tipo(cls, tipo: str) -> list[OperadorMovil]:
|
|
160
|
+
"""
|
|
161
|
+
Obtiene operadores por tipo.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
tipo: "OMR" (con red propia) o "OMV" (virtual)
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Lista de operadores del tipo especificado
|
|
168
|
+
|
|
169
|
+
Ejemplo:
|
|
170
|
+
>>> omr = OperadoresMovilesCatalog.get_por_tipo("OMR")
|
|
171
|
+
>>> print(f"Operadores con red propia: {len(omr)}")
|
|
172
|
+
>>>
|
|
173
|
+
>>> omv = OperadoresMovilesCatalog.get_por_tipo("OMV")
|
|
174
|
+
>>> print(f"Operadores virtuales: {len(omv)}")
|
|
175
|
+
"""
|
|
176
|
+
cls._load_data()
|
|
177
|
+
tipo_upper = tipo.upper()
|
|
178
|
+
return [op for op in cls._data if op["tipo"] == tipo_upper] # type: ignore
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def get_con_tecnologia(cls, tecnologia: str) -> list[OperadorMovil]:
|
|
182
|
+
"""
|
|
183
|
+
Obtiene operadores que soportan una tecnología específica.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
tecnologia: "2G", "3G", "4G", "5G"
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Lista de operadores con la tecnología
|
|
190
|
+
|
|
191
|
+
Ejemplo:
|
|
192
|
+
>>> con_5g = OperadoresMovilesCatalog.get_con_tecnologia("5G")
|
|
193
|
+
>>> for op in con_5g:
|
|
194
|
+
... print(f"{op['nombre_comercial']} tiene 5G")
|
|
195
|
+
"""
|
|
196
|
+
cls._load_data()
|
|
197
|
+
tecnologia_upper = tecnologia.upper()
|
|
198
|
+
return [op for op in cls._data if tecnologia_upper in op["tecnologias"]] # type: ignore
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def get_por_cobertura(cls, cobertura: str) -> list[OperadorMovil]:
|
|
202
|
+
"""
|
|
203
|
+
Obtiene operadores por tipo de cobertura.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
cobertura: "nacional" o "regional"
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Lista de operadores con ese tipo de cobertura
|
|
210
|
+
|
|
211
|
+
Ejemplo:
|
|
212
|
+
>>> nacionales = OperadoresMovilesCatalog.get_por_cobertura("nacional")
|
|
213
|
+
>>> print(f"Operadores con cobertura nacional: {len(nacionales)}")
|
|
214
|
+
"""
|
|
215
|
+
cls._load_data()
|
|
216
|
+
cobertura_lower = cobertura.lower()
|
|
217
|
+
return [op for op in cls._data if op["cobertura"] == cobertura_lower] # type: ignore
|
|
218
|
+
|
|
219
|
+
@classmethod
|
|
220
|
+
def get_por_grupo(cls, grupo: str) -> list[OperadorMovil]:
|
|
221
|
+
"""
|
|
222
|
+
Obtiene operadores de un grupo empresarial.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
grupo: Nombre del grupo empresarial
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
Lista de operadores del grupo
|
|
229
|
+
|
|
230
|
+
Ejemplo:
|
|
231
|
+
>>> america_movil = OperadoresMovilesCatalog.get_por_grupo("América Móvil")
|
|
232
|
+
>>> for op in america_movil:
|
|
233
|
+
... print(op['nombre_comercial'])
|
|
234
|
+
"""
|
|
235
|
+
cls._load_data()
|
|
236
|
+
grupo_lower = grupo.lower()
|
|
237
|
+
return [
|
|
238
|
+
op
|
|
239
|
+
for op in cls._data # type: ignore
|
|
240
|
+
if "grupo_empresarial" in op and grupo_lower in op["grupo_empresarial"].lower()
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
@classmethod
|
|
244
|
+
def get_con_servicio(cls, servicio: str) -> list[OperadorMovil]:
|
|
245
|
+
"""
|
|
246
|
+
Obtiene operadores que ofrecen un servicio específico.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
servicio: "prepago", "postpago", "datos"
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Lista de operadores con el servicio
|
|
253
|
+
|
|
254
|
+
Ejemplo:
|
|
255
|
+
>>> prepago = OperadoresMovilesCatalog.get_con_servicio("prepago")
|
|
256
|
+
>>> print(f"Operadores con prepago: {len(prepago)}")
|
|
257
|
+
"""
|
|
258
|
+
cls._load_data()
|
|
259
|
+
servicio_lower = servicio.lower()
|
|
260
|
+
return [op for op in cls._data if servicio_lower in op["servicios"]] # type: ignore
|
|
261
|
+
|
|
262
|
+
@classmethod
|
|
263
|
+
def get_top_por_market_share(cls, limit: int = 5) -> list[OperadorMovil]:
|
|
264
|
+
"""
|
|
265
|
+
Obtiene los operadores con mayor market share.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
limit: Número de operadores a retornar (default: 5)
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Lista de top operadores ordenados por market share
|
|
272
|
+
|
|
273
|
+
Ejemplo:
|
|
274
|
+
>>> top3 = OperadoresMovilesCatalog.get_top_por_market_share(3)
|
|
275
|
+
>>> for i, op in enumerate(top3, 1):
|
|
276
|
+
... print(f"{i}. {op['nombre_comercial']}: {op['market_share_aprox']}%")
|
|
277
|
+
"""
|
|
278
|
+
cls._load_data()
|
|
279
|
+
sorted_ops = sorted(
|
|
280
|
+
[op for op in cls._data if op["activo"]], # type: ignore
|
|
281
|
+
key=lambda x: x["market_share_aprox"],
|
|
282
|
+
reverse=True,
|
|
283
|
+
)
|
|
284
|
+
return sorted_ops[:limit]
|
|
285
|
+
|
|
286
|
+
@classmethod
|
|
287
|
+
def get_estadisticas(cls) -> dict[str, int | float | list]:
|
|
288
|
+
"""
|
|
289
|
+
Obtiene estadísticas del catálogo.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
Diccionario con estadísticas
|
|
293
|
+
|
|
294
|
+
Ejemplo:
|
|
295
|
+
>>> stats = OperadoresMovilesCatalog.get_estadisticas()
|
|
296
|
+
>>> print(f"Total operadores: {stats['total_operadores']}")
|
|
297
|
+
>>> print(f"Activos: {stats['operadores_activos']}")
|
|
298
|
+
>>> print(f"Con 5G: {stats['operadores_con_5g']}")
|
|
299
|
+
"""
|
|
300
|
+
cls._load_data()
|
|
301
|
+
|
|
302
|
+
activos = cls.get_activos()
|
|
303
|
+
con_5g = cls.get_con_tecnologia("5G")
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
"total_operadores": len(cls._data), # type: ignore
|
|
307
|
+
"operadores_activos": len(activos),
|
|
308
|
+
"operadores_inactivos": len(cls.get_inactivos()),
|
|
309
|
+
"omr_count": len(cls.get_por_tipo("OMR")),
|
|
310
|
+
"omv_count": len(cls.get_por_tipo("OMV")),
|
|
311
|
+
"operadores_con_5g": len(con_5g),
|
|
312
|
+
"cobertura_nacional": len(cls.get_por_cobertura("nacional")),
|
|
313
|
+
"market_share_total": sum(op["market_share_aprox"] for op in activos),
|
|
314
|
+
"tecnologias_disponibles": ["2G", "3G", "4G", "5G"],
|
|
315
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Catálogos INEGI
|
|
3
|
+
|
|
4
|
+
Catálogos incluidos:
|
|
5
|
+
- MunicipiosCatalog: Municipios de México
|
|
6
|
+
- MunicipiosCompletoCatalog: Catálogo completo de 2,469 municipios
|
|
7
|
+
- LocalidadesCatalog: Localidades con 1,000+ habitantes
|
|
8
|
+
- StateCatalog: Estados de México
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .localidades import LocalidadesCatalog
|
|
12
|
+
from .municipios import MunicipiosCatalog
|
|
13
|
+
from .municipios_completo import MunicipiosCompletoCatalog
|
|
14
|
+
from .states import StateCatalog
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"MunicipiosCatalog",
|
|
18
|
+
"MunicipiosCompletoCatalog",
|
|
19
|
+
"LocalidadesCatalog",
|
|
20
|
+
"StateCatalog",
|
|
21
|
+
]
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Catálogo de Localidades INEGI (filtrado por población)"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from catalogmx.utils.text import normalize_text
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LocalidadesCatalog:
|
|
10
|
+
"""
|
|
11
|
+
Catálogo de localidades de México con 1,000+ habitantes.
|
|
12
|
+
|
|
13
|
+
Incluye:
|
|
14
|
+
- 10,635 localidades con población >= 1,000 habitantes
|
|
15
|
+
- Coordenadas GPS (latitud, longitud)
|
|
16
|
+
- Población y viviendas habitadas
|
|
17
|
+
- Clasificación urbano/rural
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
_data: list[dict] | None = None
|
|
21
|
+
_by_cvegeo: dict[str, dict] | None = None
|
|
22
|
+
_by_municipio: dict[str, list[dict]] | None = None
|
|
23
|
+
_by_entidad: dict[str, list[dict]] | None = None
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def _load_data(cls) -> None:
|
|
27
|
+
if cls._data is None:
|
|
28
|
+
# Path: catalogmx/packages/python/catalogmx/catalogs/inegi/localidades.py
|
|
29
|
+
# Target: catalogmx/packages/shared-data/inegi/localidades.json
|
|
30
|
+
path = (
|
|
31
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
32
|
+
/ "shared-data"
|
|
33
|
+
/ "inegi"
|
|
34
|
+
/ "localidades.json"
|
|
35
|
+
)
|
|
36
|
+
with open(path, encoding="utf-8") as f:
|
|
37
|
+
cls._data = json.load(f)
|
|
38
|
+
|
|
39
|
+
# Crear índices
|
|
40
|
+
cls._by_cvegeo = {item["cvegeo"]: item for item in cls._data}
|
|
41
|
+
|
|
42
|
+
# Índice por municipio
|
|
43
|
+
cls._by_municipio = {}
|
|
44
|
+
for item in cls._data:
|
|
45
|
+
cve_mun = item["cve_municipio"]
|
|
46
|
+
if cve_mun not in cls._by_municipio:
|
|
47
|
+
cls._by_municipio[cve_mun] = []
|
|
48
|
+
cls._by_municipio[cve_mun].append(item)
|
|
49
|
+
|
|
50
|
+
# Índice por entidad
|
|
51
|
+
cls._by_entidad = {}
|
|
52
|
+
for item in cls._data:
|
|
53
|
+
cve_ent = item["cve_entidad"]
|
|
54
|
+
if cve_ent not in cls._by_entidad:
|
|
55
|
+
cls._by_entidad[cve_ent] = []
|
|
56
|
+
cls._by_entidad[cve_ent].append(item)
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def get_localidad(cls, cvegeo: str) -> dict | None:
|
|
60
|
+
"""
|
|
61
|
+
Obtiene una localidad por su clave geoestadística (CVEGEO).
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
cvegeo: Clave geoestadística (ej: "010010001")
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Diccionario con datos de la localidad o None si no existe
|
|
68
|
+
"""
|
|
69
|
+
cls._load_data()
|
|
70
|
+
return cls._by_cvegeo.get(cvegeo)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def is_valid(cls, cvegeo: str) -> bool:
|
|
74
|
+
"""Verifica si una clave geoestadística existe"""
|
|
75
|
+
return cls.get_localidad(cvegeo) is not None
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def get_by_municipio(cls, cve_municipio: str) -> list[dict]:
|
|
79
|
+
"""
|
|
80
|
+
Obtiene todas las localidades de un municipio.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
cve_municipio: Código del municipio (ej: "001")
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Lista de localidades del municipio
|
|
87
|
+
"""
|
|
88
|
+
cls._load_data()
|
|
89
|
+
return cls._by_municipio.get(cve_municipio, []).copy()
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def get_by_entidad(cls, cve_entidad: str) -> list[dict]:
|
|
93
|
+
"""
|
|
94
|
+
Obtiene todas las localidades de un estado.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
cve_entidad: Código del estado (ej: "01")
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Lista de localidades del estado
|
|
101
|
+
"""
|
|
102
|
+
cls._load_data()
|
|
103
|
+
cve_ent = cve_entidad.zfill(2)
|
|
104
|
+
return cls._by_entidad.get(cve_ent, []).copy()
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def get_all(cls) -> list[dict]:
|
|
108
|
+
"""Obtiene todas las localidades"""
|
|
109
|
+
cls._load_data()
|
|
110
|
+
return cls._data.copy()
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def get_urbanas(cls) -> list[dict]:
|
|
114
|
+
"""Obtiene solo localidades urbanas"""
|
|
115
|
+
cls._load_data()
|
|
116
|
+
return [loc for loc in cls._data if loc["ambito"] == "U"]
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def get_rurales(cls) -> list[dict]:
|
|
120
|
+
"""Obtiene solo localidades rurales"""
|
|
121
|
+
cls._load_data()
|
|
122
|
+
return [loc for loc in cls._data if loc["ambito"] == "R"]
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def search_by_name(cls, nombre: str) -> list[dict]:
|
|
126
|
+
"""
|
|
127
|
+
Busca localidades por nombre (búsqueda parcial, insensible a acentos).
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
nombre: Nombre o parte del nombre a buscar
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Lista de localidades que coinciden
|
|
134
|
+
|
|
135
|
+
Ejemplo:
|
|
136
|
+
>>> # Búsqueda con o sin acentos funciona igual
|
|
137
|
+
>>> locs = LocalidadesCatalog.search_by_name("san jose")
|
|
138
|
+
>>> locs = LocalidadesCatalog.search_by_name("san josé") # mismo resultado
|
|
139
|
+
"""
|
|
140
|
+
cls._load_data()
|
|
141
|
+
nombre_normalized = normalize_text(nombre)
|
|
142
|
+
return [
|
|
143
|
+
loc for loc in cls._data if nombre_normalized in normalize_text(loc["nom_localidad"])
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def get_by_coordinates(cls, lat: float, lon: float, radio_km: float = 10) -> list[dict]:
|
|
148
|
+
"""
|
|
149
|
+
Busca localidades cercanas a unas coordenadas.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
lat: Latitud
|
|
153
|
+
lon: Longitud
|
|
154
|
+
radio_km: Radio de búsqueda en kilómetros (default: 10)
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Lista de localidades dentro del radio, ordenadas por distancia
|
|
158
|
+
"""
|
|
159
|
+
from math import atan2, cos, radians, sin, sqrt
|
|
160
|
+
|
|
161
|
+
cls._load_data()
|
|
162
|
+
|
|
163
|
+
def distancia_haversine(lat1, lon1, lat2, lon2):
|
|
164
|
+
"""Calcula distancia en km entre dos puntos GPS"""
|
|
165
|
+
R = 6371 # Radio de la Tierra en km
|
|
166
|
+
|
|
167
|
+
dlat = radians(lat2 - lat1)
|
|
168
|
+
dlon = radians(lon2 - lon1)
|
|
169
|
+
|
|
170
|
+
a = sin(dlat / 2) ** 2 + cos(radians(lat1)) * cos(radians(lat2)) * sin(dlon / 2) ** 2
|
|
171
|
+
c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
|
172
|
+
|
|
173
|
+
return R * c
|
|
174
|
+
|
|
175
|
+
resultados = []
|
|
176
|
+
for loc in cls._data:
|
|
177
|
+
if loc["latitud"] is None or loc["longitud"] is None:
|
|
178
|
+
continue
|
|
179
|
+
|
|
180
|
+
distancia = distancia_haversine(lat, lon, loc["latitud"], loc["longitud"])
|
|
181
|
+
if distancia <= radio_km:
|
|
182
|
+
loc_con_distancia = loc.copy()
|
|
183
|
+
loc_con_distancia["distancia_km"] = round(distancia, 2)
|
|
184
|
+
resultados.append(loc_con_distancia)
|
|
185
|
+
|
|
186
|
+
# Ordenar por distancia
|
|
187
|
+
resultados.sort(key=lambda x: x["distancia_km"])
|
|
188
|
+
return resultados
|
|
189
|
+
|
|
190
|
+
@classmethod
|
|
191
|
+
def get_by_population_range(cls, min_pob: int, max_pob: int | None = None) -> list[dict]:
|
|
192
|
+
"""
|
|
193
|
+
Obtiene localidades en un rango de población.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
min_pob: Población mínima
|
|
197
|
+
max_pob: Población máxima (None para sin límite)
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Lista de localidades en el rango
|
|
201
|
+
"""
|
|
202
|
+
cls._load_data()
|
|
203
|
+
|
|
204
|
+
if max_pob is None:
|
|
205
|
+
return [loc for loc in cls._data if loc["poblacion_total"] >= min_pob]
|
|
206
|
+
else:
|
|
207
|
+
return [loc for loc in cls._data if min_pob <= loc["poblacion_total"] <= max_pob]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Catálogo de Municipios INEGI"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from catalogmx.utils.text import normalize_text
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MunicipiosCatalog:
|
|
10
|
+
_data: list[dict] | None = None
|
|
11
|
+
_by_cve_completa: dict[str, dict] | None = None
|
|
12
|
+
_by_entidad: dict[str, list[dict]] | None = None
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def _load_data(cls) -> None:
|
|
16
|
+
if cls._data is None:
|
|
17
|
+
# Path: catalogmx/packages/python/catalogmx/catalogs/inegi/municipios.py
|
|
18
|
+
# Target: catalogmx/packages/shared-data/inegi/municipios_completo.json
|
|
19
|
+
path = (
|
|
20
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
21
|
+
/ "shared-data"
|
|
22
|
+
/ "inegi"
|
|
23
|
+
/ "municipios_completo.json"
|
|
24
|
+
)
|
|
25
|
+
with open(path, encoding="utf-8") as f:
|
|
26
|
+
cls._data = json.load(f)
|
|
27
|
+
|
|
28
|
+
cls._by_cve_completa = {item["cve_completa"]: item for item in cls._data}
|
|
29
|
+
|
|
30
|
+
# Index by entidad
|
|
31
|
+
cls._by_entidad = {}
|
|
32
|
+
for item in cls._data:
|
|
33
|
+
entidad = item["cve_entidad"]
|
|
34
|
+
if entidad not in cls._by_entidad:
|
|
35
|
+
cls._by_entidad[entidad] = []
|
|
36
|
+
cls._by_entidad[entidad].append(item)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def get_municipio(cls, cve_completa: str) -> dict | None:
|
|
40
|
+
"""Obtiene municipio por clave completa (5 dígitos)"""
|
|
41
|
+
cls._load_data()
|
|
42
|
+
return cls._by_cve_completa.get(cve_completa)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def get_by_entidad(cls, cve_entidad: str) -> list[dict]:
|
|
46
|
+
"""Obtiene todos los municipios de una entidad"""
|
|
47
|
+
cls._load_data()
|
|
48
|
+
return cls._by_entidad.get(cve_entidad, [])
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def is_valid(cls, cve_completa: str) -> bool:
|
|
52
|
+
"""Verifica si una clave de municipio es válida"""
|
|
53
|
+
return cls.get_municipio(cve_completa) is not None
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def get_all(cls) -> list[dict]:
|
|
57
|
+
"""Obtiene todos los municipios"""
|
|
58
|
+
cls._load_data()
|
|
59
|
+
return cls._data.copy()
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def search_by_name(cls, nombre: str) -> list[dict]:
|
|
63
|
+
"""
|
|
64
|
+
Busca municipios por nombre (búsqueda parcial, insensible a acentos).
|
|
65
|
+
|
|
66
|
+
Ejemplo:
|
|
67
|
+
>>> # Ambas búsquedas funcionan igual
|
|
68
|
+
>>> munis = MunicipiosCatalog.search_by_name("leon")
|
|
69
|
+
>>> munis = MunicipiosCatalog.search_by_name("león") # mismo resultado
|
|
70
|
+
"""
|
|
71
|
+
cls._load_data()
|
|
72
|
+
nombre_normalized = normalize_text(nombre)
|
|
73
|
+
return [m for m in cls._data if nombre_normalized in normalize_text(m["nom_municipio"])]
|