catalogmx 0.3.0__py3-none-any.whl → 0.4.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.
Files changed (53) hide show
  1. catalogmx/__init__.py +133 -19
  2. catalogmx/calculators/__init__.py +113 -0
  3. catalogmx/calculators/costo_trabajador.py +213 -0
  4. catalogmx/calculators/impuestos.py +920 -0
  5. catalogmx/calculators/imss.py +370 -0
  6. catalogmx/calculators/isr.py +290 -0
  7. catalogmx/calculators/resico.py +154 -0
  8. catalogmx/catalogs/banxico/__init__.py +29 -3
  9. catalogmx/catalogs/banxico/cetes_sqlite.py +279 -0
  10. catalogmx/catalogs/banxico/inflacion_sqlite.py +302 -0
  11. catalogmx/catalogs/banxico/salarios_minimos_sqlite.py +295 -0
  12. catalogmx/catalogs/banxico/tiie_sqlite.py +279 -0
  13. catalogmx/catalogs/banxico/tipo_cambio_usd_sqlite.py +255 -0
  14. catalogmx/catalogs/banxico/udis_sqlite.py +332 -0
  15. catalogmx/catalogs/cnbv/__init__.py +9 -0
  16. catalogmx/catalogs/cnbv/sectores.py +173 -0
  17. catalogmx/catalogs/conapo/__init__.py +15 -0
  18. catalogmx/catalogs/conapo/sistema_urbano_nacional.py +50 -0
  19. catalogmx/catalogs/conapo/zonas_metropolitanas.py +230 -0
  20. catalogmx/catalogs/ift/__init__.py +1 -1
  21. catalogmx/catalogs/ift/codigos_lada.py +517 -313
  22. catalogmx/catalogs/inegi/__init__.py +17 -0
  23. catalogmx/catalogs/inegi/scian.py +127 -0
  24. catalogmx/catalogs/mexico/__init__.py +2 -0
  25. catalogmx/catalogs/mexico/giros_mercantiles.py +119 -0
  26. catalogmx/catalogs/sat/carta_porte/material_peligroso.py +5 -1
  27. catalogmx/catalogs/sat/cfdi_4/clave_prod_serv.py +78 -0
  28. catalogmx/catalogs/sat/cfdi_4/tasa_o_cuota.py +2 -1
  29. catalogmx/catalogs/sepomex/__init__.py +2 -1
  30. catalogmx/catalogs/sepomex/codigos_postales.py +30 -2
  31. catalogmx/catalogs/sepomex/codigos_postales_completo.py +261 -0
  32. catalogmx/cli.py +12 -9
  33. catalogmx/data/__init__.py +10 -0
  34. catalogmx/data/mexico_dynamic.sqlite3 +0 -0
  35. catalogmx/data/updater.py +362 -0
  36. catalogmx/generators/__init__.py +20 -0
  37. catalogmx/generators/identity.py +582 -0
  38. catalogmx/helpers.py +177 -3
  39. catalogmx/utils/__init__.py +29 -0
  40. catalogmx/utils/clabe_utils.py +417 -0
  41. catalogmx/utils/text.py +7 -1
  42. catalogmx/validators/clabe.py +52 -2
  43. catalogmx/validators/nss.py +32 -27
  44. catalogmx/validators/rfc.py +185 -52
  45. catalogmx-0.4.0.dist-info/METADATA +905 -0
  46. {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/RECORD +51 -25
  47. {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/WHEEL +1 -1
  48. catalogmx/catalogs/banxico/udis.py +0 -279
  49. catalogmx-0.3.0.dist-info/METADATA +0 -644
  50. {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/entry_points.txt +0 -0
  51. {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/licenses/AUTHORS.rst +0 -0
  52. {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/licenses/LICENSE +0 -0
  53. {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/top_level.txt +0 -0
@@ -1,426 +1,630 @@
1
1
  """
2
- Catálogo de códigos LADA (plan de numeración telefónica) en México (IFT)
2
+ Catálogo de Códigos LADA de México
3
3
 
4
- Este módulo proporciona acceso al catálogo completo de códigos LADA
5
- del Instituto Federal de Telecomunicaciones (IFT).
4
+ Basado en el Plan de Numeración Nacional del IFT (Instituto Federal de Telecomunicaciones).
5
+ Incluye mapeo geográfico a códigos INEGI y zonas metropolitanas.
6
6
  """
7
7
 
8
8
  import json
9
+ import random
9
10
  from pathlib import Path
10
- from typing import TypedDict
11
11
 
12
- from catalogmx.utils.text import normalize_text
13
12
 
13
+ class CodigosLADACatalog:
14
+ """Catálogo de códigos LADA con mapeo geográfico"""
15
+
16
+ _data: list[dict] | None = None
17
+ _by_lada: dict[str, dict] | None = None
18
+ _by_municipio: dict[tuple[str, str], list[dict]] | None = None
19
+ _by_entidad: dict[str, list[dict]] | None = None
20
+
21
+ # Zonas metropolitanas principales (LADA compartida entre municipios de distintos estados)
22
+ ZONAS_METROPOLITANAS: dict[str, dict] = {
23
+ "55": { # CDMX - Valle de México
24
+ "nombre": "Zona Metropolitana del Valle de México",
25
+ "municipios": [
26
+ # CDMX (todos los municipios/alcaldías)
27
+ ("09", None), # Toda la entidad 09
28
+ # Estado de México - municipios de Zona 5 IFT (60 municipios)
29
+ ("15", "002"), # Acolman
30
+ ("15", "009"), # Amecameca
31
+ ("15", "010"), # Apaxco
32
+ ("15", "011"), # Atenco
33
+ ("15", "013"), # Atizapán de Zaragoza
34
+ ("15", "015"), # Atlautla
35
+ ("15", "016"), # Axapusco
36
+ ("15", "017"), # Ayapango
37
+ ("15", "020"), # Coacalco de Berriozábal
38
+ ("15", "022"), # Cocotitlán
39
+ ("15", "023"), # Coyotepec
40
+ ("15", "024"), # Cuautitlán
41
+ ("15", "025"), # Chalco
42
+ ("15", "026"), # Chapa de Mota
43
+ ("15", "028"), # Chiautla
44
+ ("15", "029"), # Chicoloapan
45
+ ("15", "030"), # Chiconcuac
46
+ ("15", "031"), # Chimalhuacán
47
+ ("15", "033"), # Ecatepec de Morelos
48
+ ("15", "034"), # Ecatzingo
49
+ ("15", "035"), # Huehuetoca
50
+ ("15", "036"), # Hueypoxtla
51
+ ("15", "037"), # Huixquilucan
52
+ ("15", "038"), # Isidro Fabela
53
+ ("15", "039"), # Ixtapaluca
54
+ ("15", "044"), # Jaltenco
55
+ ("15", "046"), # Jilotzingo
56
+ ("15", "050"), # Juchitepec
57
+ ("15", "053"), # Melchor Ocampo
58
+ ("15", "057"), # Naucalpan de Juárez
59
+ ("15", "058"), # Nezahualcóyotl
60
+ ("15", "059"), # Nextlalpan
61
+ ("15", "060"), # Nicolás Romero
62
+ ("15", "061"), # Nopaltepec
63
+ ("15", "065"), # Otumba
64
+ ("15", "068"), # Ozumba
65
+ ("15", "069"), # Papalotla
66
+ ("15", "070"), # La Paz
67
+ ("15", "075"), # San Martín de las Pirámides
68
+ ("15", "081"), # Tecámac
69
+ ("15", "083"), # Temamatla
70
+ ("15", "084"), # Temascalapa
71
+ ("15", "089"), # Tenango del Aire
72
+ ("15", "091"), # Teoloyucan
73
+ ("15", "092"), # Teotihuacán
74
+ ("15", "093"), # Tepetlaoxtoc
75
+ ("15", "094"), # Tepetlixpa
76
+ ("15", "095"), # Tepotzotlán
77
+ ("15", "096"), # Tequixquiac
78
+ ("15", "099"), # Texcoco
79
+ ("15", "100"), # Tezoyuca
80
+ ("15", "103"), # Tlalmanalco
81
+ ("15", "104"), # Tlalnepantla de Baz
82
+ ("15", "108"), # Tultepec
83
+ ("15", "109"), # Tultitlán
84
+ ("15", "112"), # Villa del Carbón
85
+ ("15", "120"), # Zumpango
86
+ ("15", "121"), # Cuautitlán Izcalli
87
+ ("15", "122"), # Valle de Chalco Solidaridad
88
+ ("15", "125"), # Tonanitla
89
+ # Hidalgo
90
+ ("13", "069"), # Tizayuca
91
+ ],
92
+ },
93
+ "33": { # Guadalajara
94
+ "nombre": "Zona Metropolitana de Guadalajara",
95
+ "municipios": [
96
+ ("14", "039"), # Guadalajara
97
+ ("14", "070"), # El Salto
98
+ ("14", "097"), # Tlajomulco de Zúñiga
99
+ ("14", "098"), # Tlaquepaque
100
+ ("14", "101"), # Tonalá
101
+ ("14", "120"), # Zapopan
102
+ ("14", "124"), # Juanacatlán
103
+ ("14", "044"), # Ixtlahuacán de los Membrillos
104
+ ],
105
+ },
106
+ "81": { # Monterrey
107
+ "nombre": "Zona Metropolitana de Monterrey",
108
+ "municipios": [
109
+ ("19", "006"), # Apodaca
110
+ ("19", "009"), # Cadereyta Jiménez
111
+ ("19", "010"), # El Carmen
112
+ ("19", "018"), # García
113
+ ("19", "019"), # San Pedro Garza García
114
+ ("19", "021"), # General Escobedo
115
+ ("19", "026"), # Guadalupe
116
+ ("19", "031"), # Juárez
117
+ ("19", "039"), # Monterrey
118
+ ("19", "045"), # Salinas Victoria
119
+ ("19", "046"), # San Nicolás de los Garza
120
+ ("19", "048"), # Santa Catarina
121
+ ("19", "049"), # Santiago
122
+ ],
123
+ },
124
+ "222": { # Puebla-Tlaxcala (también 220, 221)
125
+ "nombre": "Zona Metropolitana de Puebla-Tlaxcala",
126
+ "municipios": [
127
+ # Puebla
128
+ ("21", "114"), # Puebla
129
+ ("21", "015"), # Amozoc
130
+ ("21", "034"), # Coronango
131
+ ("21", "041"), # Cuautlancingo
132
+ ("21", "074"), # Huejotzingo
133
+ ("21", "090"), # Juan C. Bonilla
134
+ ("21", "106"), # Ocoyucan
135
+ ("21", "119"), # San Andrés Cholula
136
+ ("21", "132"), # San Martín Texmelucan
137
+ ("21", "140"), # San Pedro Cholula
138
+ # Tlaxcala
139
+ ("29", "022"), # Mazatecochco de José María Morelos
140
+ ("29", "024"), # Papalotla de Xicohténcatl
141
+ ("29", "028"), # San Pablo del Monte
142
+ ("29", "032"), # Tenancingo
143
+ ("29", "033"), # Teolocholco
144
+ ("29", "034"), # Tepeyanco
145
+ ("29", "038"), # Xicohtzinco
146
+ ("29", "044"), # Zacatelco
147
+ ],
148
+ },
149
+ "664": { # Tijuana-Tecate-Rosarito (también 661, 663, 665)
150
+ "nombre": "Zona Metropolitana de Tijuana",
151
+ "municipios": [
152
+ # Baja California
153
+ ("02", "004"), # Tijuana
154
+ ("02", "003"), # Tecate
155
+ ("02", "005"), # Playas de Rosarito
156
+ ],
157
+ },
158
+ "656": { # Ciudad Juárez (también 657)
159
+ "nombre": "Zona Metropolitana de Ciudad Juárez",
160
+ "municipios": [
161
+ # Chihuahua
162
+ ("08", "037"), # Juárez
163
+ ("08", "031"), # Guadalupe (Chihuahua)
164
+ ("08", "051"), # Praxedis G. Guerrero
165
+ ],
166
+ },
167
+ "999": { # Mérida (también 990)
168
+ "nombre": "Zona Metropolitana de Mérida",
169
+ "municipios": [
170
+ # Yucatán
171
+ ("31", "050"), # Mérida
172
+ ("31", "041"), # Kanasín
173
+ ("31", "100"), # Ucú
174
+ ("31", "101"), # Umán
175
+ ("31", "013"), # Conkal
176
+ ("31", "059"), # Progreso
177
+ ],
178
+ },
179
+ "998": { # Cancún-Riviera Maya
180
+ "nombre": "Zona Metropolitana de Cancún",
181
+ "municipios": [
182
+ # Quintana Roo
183
+ ("23", "005"), # Benito Juárez (Cancún)
184
+ ("23", "008"), # Solidaridad (Playa del Carmen)
185
+ ("23", "009"), # Tulum
186
+ ("23", "004"), # Isla Mujeres
187
+ ("23", "011"), # Puerto Morelos
188
+ ],
189
+ },
190
+ }
191
+
192
+ # Prefijos asociados a cada zona metropolitana
193
+ # Solo CDMX (55), GDL (33) y MTY (81) son de 2 dígitos
194
+ # Las expansiones son específicas por ciudad, NO por patrón de zona
195
+ LADAS_METROPOLITANAS: dict[str, list[str]] = {
196
+ "55": ["55", "56"], # CDMX: 55 + 56x (expansión móvil)
197
+ "33": ["33"], # Guadalajara
198
+ "81": ["81"], # Monterrey
199
+ "222": ["220", "221", "222"], # Puebla-Tlaxcala (expansiones)
200
+ "664": ["663", "664"], # Tijuana (663 overlay 2018)
201
+ "656": ["656", "657"], # Ciudad Juárez (657 overlay)
202
+ "999": ["990", "999"], # Mérida (990 expansión)
203
+ "998": ["998"], # Cancún
204
+ }
205
+
206
+ # Lookup de zona metropolitana: (cve_entidad, cve_municipio) -> LADA
207
+ _zm_lookup: dict[tuple[str, str | None], str] | None = None
14
208
 
15
- class CodigoLADA(TypedDict):
16
- """Estructura de un código LADA"""
209
+ @classmethod
210
+ def _load_data(cls) -> None:
211
+ """Carga los datos del catálogo"""
212
+ if cls._data is not None:
213
+ return
17
214
 
18
- lada: str
19
- ciudad: str
20
- estado: str
21
- tipo: str # metropolitana | fronteriza | turistica | normal
22
- region: str
215
+ data_path = (
216
+ Path(__file__).resolve().parents[4] / "shared-data" / "ift" / "codigos_lada.json"
217
+ )
23
218
 
219
+ with open(data_path, encoding="utf-8") as f:
220
+ raw = json.load(f)
24
221
 
25
- class ValidacionNumero(TypedDict):
26
- """Resultado de validación de número telefónico"""
222
+ cls._data = raw.get("codigos", raw) if isinstance(raw, dict) else raw
27
223
 
28
- valid: bool
29
- lada: str | None
30
- numero_local: str | None
31
- ciudad: str | None
32
- estado: str | None
33
- error: str | None
224
+ if not cls._data:
225
+ cls._data = []
34
226
 
227
+ # Build indexes
228
+ cls._by_lada = {c["lada"]: c for c in cls._data}
229
+ cls._by_municipio = {}
230
+ cls._by_entidad = {}
35
231
 
36
- class InfoNumero(TypedDict):
37
- """Información detallada de un número telefónico"""
232
+ for c in cls._data:
233
+ ent = c.get("cve_entidad")
234
+ mun = c.get("cve_municipio")
38
235
 
39
- lada: str
40
- ciudad: str
41
- estado: str
42
- tipo: str
43
- region: str
236
+ if ent and mun:
237
+ key = (ent, mun)
238
+ if key not in cls._by_municipio:
239
+ cls._by_municipio[key] = []
240
+ cls._by_municipio[key].append(c)
44
241
 
242
+ if ent:
243
+ if ent not in cls._by_entidad:
244
+ cls._by_entidad[ent] = []
245
+ cls._by_entidad[ent].append(c)
45
246
 
46
- class CodigosLADACatalog:
47
- """
48
- Catálogo de códigos LADA de México.
49
-
50
- Proporciona métodos para búsqueda, validación y formato de números telefónicos.
51
-
52
- Características:
53
- - 231+ códigos LADA (expandible a 397 según plan IFT)
54
- - Cobertura nacional (32 estados)
55
- - Clasificación por tipo (metropolitana, fronteriza, turística, normal)
56
- - Validación de números telefónicos de 10 dígitos
57
- - Formateo automático de números
58
-
59
- Ejemplo:
60
- >>> from catalogmx.catalogs.ift import CodigosLADACatalog
61
- >>>
62
- >>> # Buscar por LADA
63
- >>> codigo = CodigosLADACatalog.buscar_por_lada("33")
64
- >>> print(codigo['ciudad']) # "Guadalajara"
65
- >>>
66
- >>> # Validar número telefónico
67
- >>> info = CodigosLADACatalog.validar_numero("3312345678")
68
- >>> print(info['valid']) # True
69
- >>> print(info['ciudad']) # "Guadalajara"
70
- """
71
-
72
- _data: list[CodigoLADA] | None = None
73
- _by_lada: dict[str, CodigoLADA] | None = None
74
- _by_estado: dict[str, list[CodigoLADA]] | None = None
75
- _by_tipo: dict[str, list[CodigoLADA]] | None = None
76
- _by_region: dict[str, list[CodigoLADA]] | None = None
247
+ # Build ZM lookup
248
+ cls._zm_lookup = {}
249
+ for lada, zm in cls.ZONAS_METROPOLITANAS.items():
250
+ for cve_ent, cve_mun in zm["municipios"]:
251
+ cls._zm_lookup[(cve_ent, cve_mun)] = lada
77
252
 
78
253
  @classmethod
79
- def _load_data(cls) -> None:
80
- """Carga lazy de datos desde JSON"""
81
- if cls._data is not None:
82
- return
83
-
84
- # Path: catalogmx/packages/python/catalogmx/catalogs/ift/codigos_lada.py
85
- # Target: catalogmx/packages/shared-data/ift/codigos_lada.json
86
- data_path = (
87
- Path(__file__).parent.parent.parent.parent.parent
88
- / "shared-data"
89
- / "ift"
90
- / "codigos_lada.json"
91
- )
254
+ def get_all(cls) -> list[dict]:
255
+ """Obtiene todos los códigos LADA"""
256
+ cls._load_data()
257
+ return cls._data.copy() if cls._data else []
92
258
 
93
- with open(data_path, encoding="utf-8") as f:
94
- json_data = json.load(f)
95
- cls._data = json_data["codigos"]
96
-
97
- # Crear índices para búsquedas rápidas
98
- cls._by_lada = {item["lada"]: item for item in cls._data}
99
-
100
- # Índice por estado
101
- cls._by_estado = {}
102
- for item in cls._data:
103
- estado = item["estado"].lower()
104
- if estado not in cls._by_estado:
105
- cls._by_estado[estado] = []
106
- cls._by_estado[estado].append(item)
107
-
108
- # Índice por tipo
109
- cls._by_tipo = {}
110
- for item in cls._data:
111
- tipo = item["tipo"]
112
- if tipo not in cls._by_tipo:
113
- cls._by_tipo[tipo] = []
114
- cls._by_tipo[tipo].append(item)
115
-
116
- # Índice por región
117
- cls._by_region = {}
118
- for item in cls._data:
119
- region = item["region"].lower()
120
- if region not in cls._by_region:
121
- cls._by_region[region] = []
122
- cls._by_region[region].append(item)
259
+ @classmethod
260
+ def buscar_por_lada(cls, lada: str) -> dict | None:
261
+ """Busca información por código LADA"""
262
+ cls._load_data()
263
+ return cls._by_lada.get(lada) if cls._by_lada else None
123
264
 
124
265
  @classmethod
125
- def get_all(cls) -> list[CodigoLADA]:
126
- """
127
- Obtiene todos los códigos LADA.
266
+ def buscar_por_ciudad(cls, ciudad: str) -> list[dict]:
267
+ """Busca códigos LADA por nombre de ciudad"""
268
+ cls._load_data()
269
+ if not cls._data:
270
+ return []
271
+ ciudad_upper = ciudad.upper()
272
+ return [c for c in cls._data if ciudad_upper in c.get("ciudad", "").upper()]
128
273
 
129
- Returns:
130
- Lista completa de códigos LADA
131
- """
274
+ @classmethod
275
+ def get_por_estado(cls, estado: str) -> list[dict]:
276
+ """Obtiene códigos LADA por estado"""
132
277
  cls._load_data()
133
- return cls._data.copy() # type: ignore
278
+ if not cls._data:
279
+ return []
280
+ estado_upper = estado.upper()
281
+ return [c for c in cls._data if estado_upper in c.get("estado", "").upper()]
134
282
 
135
283
  @classmethod
136
- def buscar_por_lada(cls, lada: str) -> CodigoLADA | None:
137
- """
138
- Busca un código LADA específico.
284
+ def get_por_cve_entidad(cls, cve_entidad: str) -> list[dict]:
285
+ """Obtiene códigos LADA por clave de entidad INEGI"""
286
+ cls._load_data()
287
+ return cls._by_entidad.get(cve_entidad, []).copy() if cls._by_entidad else []
139
288
 
140
- Args:
141
- lada: Código LADA a buscar (ej: "33", "55", "664")
289
+ @classmethod
290
+ def get_por_municipio(cls, cve_entidad: str, cve_municipio: str) -> list[dict]:
291
+ """Obtiene códigos LADA por municipio INEGI"""
292
+ cls._load_data()
293
+ key = (cve_entidad, cve_municipio)
294
+ return cls._by_municipio.get(key, []).copy() if cls._by_municipio else []
142
295
 
143
- Returns:
144
- Información del código LADA o None si no existe
296
+ @classmethod
297
+ def get_por_tipo(cls, tipo: str) -> list[dict]:
298
+ """Obtiene códigos LADA por tipo (metropolitana, fronteriza, turistica, normal)"""
299
+ cls._load_data()
300
+ if not cls._data:
301
+ return []
302
+ return [c for c in cls._data if c.get("tipo") == tipo]
145
303
 
146
- Ejemplo:
147
- >>> codigo = CodigosLADACatalog.buscar_por_lada("33")
148
- >>> print(codigo['ciudad']) # "Guadalajara"
149
- >>> print(codigo['estado']) # "Jalisco"
150
- """
304
+ @classmethod
305
+ def get_por_region(cls, region: str) -> list[dict]:
306
+ """Obtiene códigos LADA por región"""
151
307
  cls._load_data()
152
- return cls._by_lada.get(lada) # type: ignore
308
+ if not cls._data:
309
+ return []
310
+ return [c for c in cls._data if c.get("region") == region]
153
311
 
154
312
  @classmethod
155
- def buscar_por_ciudad(cls, ciudad: str) -> list[CodigoLADA]:
313
+ def get_prefijos_por_municipio(cls, cve_entidad: str, cve_municipio: str) -> list[str]:
156
314
  """
157
- Busca códigos LADA por nombre de ciudad (búsqueda parcial, insensible a acentos).
315
+ Obtiene todos los prefijos telefónicos probables para un municipio.
316
+
317
+ Retorna todos los prefijos que podrían corresponder a ese municipio,
318
+ incluyendo expansiones metropolitanas. Los teléfonos siempre se
319
+ completan a 10 dígitos.
158
320
 
159
321
  Args:
160
- ciudad: Nombre o parte del nombre de la ciudad
322
+ cve_entidad: Clave de entidad INEGI (01-32)
323
+ cve_municipio: Clave de municipio INEGI (001-570)
161
324
 
162
325
  Returns:
163
- Lista de códigos LADA que coinciden
164
-
165
- Ejemplo:
166
- >>> # Búsqueda con o sin acentos funciona igual
167
- >>> codigos = CodigosLADACatalog.buscar_por_ciudad("san jose")
168
- >>> codigos = CodigosLADACatalog.buscar_por_ciudad("san josé") # mismo resultado
169
- >>> for codigo in codigos:
170
- ... print(f"{codigo['lada']} - {codigo['ciudad']}")
326
+ Lista de prefijos (ej: ["55", "561", "562", ...])
171
327
  """
172
328
  cls._load_data()
173
- ciudad_normalized = normalize_text(ciudad)
174
- return [
175
- item
176
- for item in cls._data # type: ignore
177
- if ciudad_normalized in normalize_text(item["ciudad"])
178
- ]
329
+ prefijos: list[str] = []
330
+
331
+ # 1. Verificar si pertenece a una zona metropolitana con LADAs asociadas
332
+ if cls._zm_lookup:
333
+ # Buscar por municipio específico
334
+ lada = cls._zm_lookup.get((cve_entidad, cve_municipio))
335
+ if lada and lada in cls.LADAS_METROPOLITANAS:
336
+ prefijos.extend(cls.LADAS_METROPOLITANAS[lada])
337
+
338
+ # Buscar si toda la entidad está en una ZM (ej: CDMX)
339
+ if not prefijos:
340
+ lada = cls._zm_lookup.get((cve_entidad, None))
341
+ if lada and lada in cls.LADAS_METROPOLITANAS:
342
+ prefijos.extend(cls.LADAS_METROPOLITANAS[lada])
343
+
344
+ # 2. Agregar LADAs directamente mapeadas al municipio
345
+ if cls._by_municipio:
346
+ for c in cls._by_municipio.get((cve_entidad, cve_municipio), []):
347
+ if c["lada"] not in prefijos:
348
+ prefijos.append(c["lada"])
349
+
350
+ # 3. Si no hay nada, buscar por entidad (estado)
351
+ if not prefijos and cls._by_entidad:
352
+ for c in cls._by_entidad.get(cve_entidad, []):
353
+ if c["lada"] not in prefijos:
354
+ prefijos.append(c["lada"])
355
+ break # Solo el principal del estado
356
+
357
+ return prefijos
179
358
 
180
359
  @classmethod
181
- def get_por_estado(cls, estado: str) -> list[CodigoLADA]:
360
+ def get_lada_for_location(
361
+ cls, cve_entidad: str, cve_municipio: str | None = None
362
+ ) -> dict | None:
182
363
  """
183
- Obtiene todos los códigos LADA de un estado.
364
+ Obtiene el código LADA más apropiado para una ubicación geográfica.
365
+
366
+ Prioridad:
367
+ 1. Zona metropolitana (si el municipio pertenece a una)
368
+ 2. Municipio específico
369
+ 3. Estado (ciudad principal)
184
370
 
185
371
  Args:
186
- estado: Nombre del estado (ej: "Jalisco", "CDMX")
372
+ cve_entidad: Clave de entidad INEGI (01-32)
373
+ cve_municipio: Clave de municipio INEGI (opcional)
187
374
 
188
375
  Returns:
189
- Lista de códigos LADA del estado
190
-
191
- Ejemplo:
192
- >>> codigos = CodigosLADACatalog.get_por_estado("Jalisco")
193
- >>> print(f"Jalisco tiene {len(codigos)} códigos LADA")
376
+ Dict con información del LADA o None
194
377
  """
195
378
  cls._load_data()
196
- estado_lower = estado.lower()
197
- return cls._by_estado.get(estado_lower, []).copy() # type: ignore
379
+
380
+ # Priority 1: Check zona metropolitana
381
+ if cls._zm_lookup:
382
+ # Check specific municipality
383
+ if cve_municipio:
384
+ lada = cls._zm_lookup.get((cve_entidad, cve_municipio))
385
+ if lada and cls._by_lada:
386
+ return cls._by_lada.get(lada)
387
+ # Check if entire state is in ZM
388
+ lada = cls._zm_lookup.get((cve_entidad, None))
389
+ if lada and cls._by_lada:
390
+ return cls._by_lada.get(lada)
391
+
392
+ # Priority 2: Specific municipality
393
+ if cve_municipio and cls._by_municipio:
394
+ candidates = cls._by_municipio.get((cve_entidad, cve_municipio), [])
395
+ if candidates:
396
+ # Prefer metropolitana type
397
+ for c in candidates:
398
+ if c.get("tipo") == "metropolitana":
399
+ return c
400
+ return candidates[0]
401
+
402
+ # Priority 3: State level - prefer metropolitana
403
+ if cls._by_entidad:
404
+ candidates = cls._by_entidad.get(cve_entidad, [])
405
+ if candidates:
406
+ # Sort by type priority
407
+ tipo_priority = {
408
+ "metropolitana": 0,
409
+ "turistica": 1,
410
+ "fronteriza": 2,
411
+ "normal": 3,
412
+ }
413
+ candidates.sort(key=lambda x: tipo_priority.get(x.get("tipo", ""), 99))
414
+ return candidates[0]
415
+
416
+ return None
198
417
 
199
418
  @classmethod
200
- def get_por_tipo(cls, tipo: str) -> list[CodigoLADA]:
419
+ def get_municipios_por_lada(cls, lada: str) -> list[tuple[str, str | None]]:
201
420
  """
202
- Obtiene códigos LADA por tipo.
421
+ Obtiene todos los municipios INEGI asociados a una LADA.
422
+
423
+ Para LADAs metropolitanas, retorna todos los municipios de la zona.
424
+ Para LADAs regulares, retorna el municipio principal del catálogo.
203
425
 
204
426
  Args:
205
- tipo: Tipo de código ("metropolitana", "fronteriza", "turistica", "normal")
427
+ lada: Código LADA (ej: "55", "222", "561")
206
428
 
207
429
  Returns:
208
- Lista de códigos del tipo especificado
209
-
210
- Ejemplo:
211
- >>> metropolitanas = CodigosLADACatalog.get_por_tipo("metropolitana")
212
- >>> for codigo in metropolitanas:
213
- ... print(f"{codigo['lada']} - {codigo['ciudad']}")
430
+ Lista de tuplas (cve_entidad, cve_municipio) con códigos INEGI
214
431
  """
215
432
  cls._load_data()
216
- return cls._by_tipo.get(tipo, []).copy() # type: ignore
217
433
 
218
- @classmethod
219
- def get_por_region(cls, region: str) -> list[CodigoLADA]:
220
- """
221
- Obtiene códigos LADA por región geográfica.
434
+ # Check if this LADA belongs to a metropolitan area
435
+ for main_lada, associated_ladas in cls.LADAS_METROPOLITANAS.items():
436
+ if lada in associated_ladas:
437
+ # Return all municipalities from this metropolitan zone
438
+ zm = cls.ZONAS_METROPOLITANAS.get(main_lada)
439
+ if zm:
440
+ return list(zm["municipios"])
222
441
 
223
- Args:
224
- region: Región ("noroeste", "norte", "noreste", "occidente",
225
- "centro", "golfo", "sur", "sureste")
442
+ # Check direct metropolitan zones
443
+ if lada in cls.ZONAS_METROPOLITANAS:
444
+ return list(cls.ZONAS_METROPOLITANAS[lada]["municipios"])
226
445
 
227
- Returns:
228
- Lista de códigos de la región
446
+ # Regular LADA - return municipality from catalog
447
+ info = cls.buscar_por_lada(lada)
448
+ if info and info.get("cve_entidad") and info.get("cve_municipio"):
449
+ return [(info["cve_entidad"], info["cve_municipio"])]
229
450
 
230
- Ejemplo:
231
- >>> codigos_norte = CodigosLADACatalog.get_por_region("norte")
232
- >>> print(f"Región norte: {len(codigos_norte)} códigos")
233
- """
234
- cls._load_data()
235
- region_lower = region.lower()
236
- return cls._by_region.get(region_lower, []).copy() # type: ignore
451
+ return []
237
452
 
238
453
  @classmethod
239
- def get_metropolitanas(cls) -> list[CodigoLADA]:
454
+ def get_zona_metropolitana(cls, lada: str) -> dict | None:
240
455
  """
241
- Obtiene códigos LADA de zonas metropolitanas.
456
+ Obtiene información de la zona metropolitana para una LADA.
457
+
458
+ Args:
459
+ lada: Código LADA (ej: "55", "222", "561")
242
460
 
243
461
  Returns:
244
- Lista de códigos metropolitanos
462
+ Dict con nombre, municipios y LADAs asociadas, o None
245
463
  """
246
- return cls.get_por_tipo("metropolitana")
464
+ # Check if this LADA belongs to a metropolitan area
465
+ for main_lada, associated_ladas in cls.LADAS_METROPOLITANAS.items():
466
+ if lada in associated_ladas:
467
+ zm = cls.ZONAS_METROPOLITANAS.get(main_lada)
468
+ if zm:
469
+ return {
470
+ "lada_principal": main_lada,
471
+ "nombre": zm["nombre"],
472
+ "municipios": zm["municipios"],
473
+ "ladas_asociadas": associated_ladas,
474
+ }
475
+
476
+ # Check direct metropolitan zones
477
+ if lada in cls.ZONAS_METROPOLITANAS:
478
+ zm = cls.ZONAS_METROPOLITANAS[lada]
479
+ return {
480
+ "lada_principal": lada,
481
+ "nombre": zm["nombre"],
482
+ "municipios": zm["municipios"],
483
+ "ladas_asociadas": cls.LADAS_METROPOLITANAS.get(lada, [lada]),
484
+ }
485
+
486
+ return None
247
487
 
248
488
  @classmethod
249
- def get_fronterizas(cls) -> list[CodigoLADA]:
489
+ def generar_telefono(
490
+ cls,
491
+ cve_entidad: str,
492
+ cve_municipio: str | None = None,
493
+ _tipo: str = "fijo", # Reserved for future mobile vs fixed differentiation
494
+ ) -> str | None:
250
495
  """
251
- Obtiene códigos LADA de ciudades fronterizas.
496
+ Genera un número de teléfono válido para una ubicación.
497
+
498
+ Args:
499
+ cve_entidad: Clave de entidad INEGI
500
+ cve_municipio: Clave de municipio INEGI (opcional)
501
+ tipo: 'fijo' o 'movil'
252
502
 
253
503
  Returns:
254
- Lista de códigos fronterizos
504
+ Número de teléfono de 10 dígitos o None
255
505
  """
256
- return cls.get_por_tipo("fronteriza")
506
+ lada_info = cls.get_lada_for_location(cve_entidad, cve_municipio)
507
+ if not lada_info:
508
+ return None
257
509
 
258
- @classmethod
259
- def get_turisticas(cls) -> list[CodigoLADA]:
260
- """
261
- Obtiene códigos LADA de destinos turísticos.
510
+ lada = lada_info["lada"]
262
511
 
263
- Returns:
264
- Lista de códigos turísticos
265
- """
266
- return cls.get_por_tipo("turistica")
512
+ # Generate remaining digits
513
+ # LADA can be 2 or 3 digits, total number is always 10
514
+ remaining = 10 - len(lada)
515
+
516
+ # Generate random digits (avoid starting with 0)
517
+ first_digit = random.randint(1, 9)
518
+ other_digits = "".join(str(random.randint(0, 9)) for _ in range(remaining - 1))
519
+
520
+ return f"{lada}{first_digit}{other_digits}"
267
521
 
268
522
  @classmethod
269
- def validar_numero(cls, numero: str) -> ValidacionNumero:
523
+ def validar_numero(cls, numero: str) -> dict:
270
524
  """
271
- Valida y analiza un número telefónico mexicano.
272
-
273
- Desde agosto 2019, México usa un plan de marcación cerrado de 10 dígitos.
274
- Este método valida el formato y extrae información geográfica.
525
+ Valida un número de teléfono mexicano.
275
526
 
276
527
  Args:
277
- numero: Número telefónico (puede contener espacios o guiones)
528
+ numero: Número de teléfono (10 dígitos)
278
529
 
279
530
  Returns:
280
- Diccionario con validación e información del número
281
-
282
- Ejemplo:
283
- >>> info = CodigosLADACatalog.validar_numero("33 1234 5678")
284
- >>> if info['valid']:
285
- ... print(f"LADA: {info['lada']}")
286
- ... print(f"Ciudad: {info['ciudad']}")
287
- ... print(f"Número local: {info['numero_local']}")
531
+ Dict con información de validación
288
532
  """
289
533
  cls._load_data()
290
534
 
291
- # Limpiar número (eliminar espacios y guiones)
292
- numero_limpio = numero.replace(" ", "").replace("-", "")
535
+ # Clean number
536
+ numero_clean = "".join(c for c in numero if c.isdigit())
293
537
 
294
- # Validar que sean 10 dígitos
295
- if not numero_limpio.isdigit() or len(numero_limpio) != 10:
296
- return {
297
- "valid": False,
298
- "lada": None,
299
- "numero_local": None,
300
- "ciudad": None,
301
- "estado": None,
302
- "error": "El número debe tener exactamente 10 dígitos",
303
- }
304
-
305
- # Intentar extraer LADA (primeros 2 o 3 dígitos)
306
- # Primero intentar con 3 dígitos
307
- lada = numero_limpio[:3]
308
- codigo = cls._by_lada.get(lada) # type: ignore
309
-
310
- # Si no se encuentra, intentar con 2 dígitos
311
- if not codigo:
312
- lada = numero_limpio[:2]
313
- codigo = cls._by_lada.get(lada) # type: ignore
314
-
315
- if codigo:
316
- numero_local = numero_limpio[len(lada) :]
317
- return {
318
- "valid": True,
319
- "lada": codigo["lada"],
320
- "numero_local": numero_local,
321
- "ciudad": codigo["ciudad"],
322
- "estado": codigo["estado"],
323
- "error": None,
324
- }
325
-
326
- return {
327
- "valid": False,
328
- "lada": lada,
329
- "numero_local": None,
538
+ result = {
539
+ "numero": numero_clean,
540
+ "valido": False,
541
+ "lada": None,
330
542
  "ciudad": None,
331
543
  "estado": None,
332
- "error": f"Código LADA {lada} no encontrado en el catálogo",
544
+ "tipo": None,
545
+ "error": None,
333
546
  }
334
547
 
548
+ if len(numero_clean) != 10:
549
+ result["error"] = "Debe tener 10 dígitos"
550
+ return result
551
+
552
+ # Try 2-digit LADA first (55, 33, 81)
553
+ lada_2 = numero_clean[:2]
554
+ if lada_2 in ("55", "33", "81") and cls._by_lada:
555
+ info = cls._by_lada.get(lada_2)
556
+ if info:
557
+ result["valido"] = True
558
+ result["lada"] = lada_2
559
+ result["ciudad"] = info.get("ciudad")
560
+ result["estado"] = info.get("estado")
561
+ result["tipo"] = info.get("tipo")
562
+ return result
563
+
564
+ # Try 3-digit LADA
565
+ lada_3 = numero_clean[:3]
566
+ if cls._by_lada:
567
+ info = cls._by_lada.get(lada_3)
568
+ if info:
569
+ result["valido"] = True
570
+ result["lada"] = lada_3
571
+ result["ciudad"] = info.get("ciudad")
572
+ result["estado"] = info.get("estado")
573
+ result["tipo"] = info.get("tipo")
574
+ return result
575
+
576
+ result["error"] = "LADA no reconocida"
577
+ return result
578
+
335
579
  @classmethod
336
580
  def formatear_numero(cls, numero: str) -> str:
337
581
  """
338
- Formatea un número telefónico al formato estándar.
582
+ Formatea un número de teléfono mexicano.
339
583
 
340
584
  Args:
341
- numero: Número telefónico sin formato
585
+ numero: Número de teléfono (10 dígitos)
342
586
 
343
587
  Returns:
344
- Número formateado (LADA XXXX XXXX)
345
-
346
- Ejemplo:
347
- >>> formateado = CodigosLADACatalog.formatear_numero("3312345678")
348
- >>> print(formateado) # "33 1234 5678"
588
+ Número formateado (ej: "55 1234 5678" o "333 123 4567")
349
589
  """
350
- validacion = cls.validar_numero(numero)
590
+ numero_clean = "".join(c for c in numero if c.isdigit())
351
591
 
352
- if not validacion["valid"] or not validacion["lada"] or not validacion["numero_local"]:
592
+ if len(numero_clean) != 10:
353
593
  return numero
354
594
 
355
- lada = validacion["lada"]
356
- local = validacion["numero_local"]
595
+ # Check if 2-digit LADA
596
+ if numero_clean[:2] in ("55", "33", "81"):
597
+ return f"{numero_clean[:2]} {numero_clean[2:6]} {numero_clean[6:]}"
357
598
 
358
- # Formato: LADA XXXX XXXX
359
- if len(local) == 7:
360
- return f"{lada} {local[:3]} {local[3:]}"
361
- elif len(local) == 8:
362
- return f"{lada} {local[:4]} {local[4:]}"
363
- else:
364
- return f"{lada} {local}"
599
+ # 3-digit LADA
600
+ return f"{numero_clean[:3]} {numero_clean[3:6]} {numero_clean[6:]}"
365
601
 
366
602
  @classmethod
367
- def get_info_numero(cls, numero: str) -> InfoNumero | None:
368
- """
369
- Obtiene información completa de un número telefónico.
370
-
371
- Args:
372
- numero: Número telefónico
373
-
374
- Returns:
375
- Información completa del número o None si es inválido
376
-
377
- Ejemplo:
378
- >>> info = CodigosLADACatalog.get_info_numero("3312345678")
379
- >>> if info:
380
- ... print(f"Ciudad: {info['ciudad']}")
381
- ... print(f"Estado: {info['estado']}")
382
- ... print(f"Tipo: {info['tipo']}")
383
- ... print(f"Región: {info['region']}")
384
- """
385
- validacion = cls.validar_numero(numero)
386
-
387
- if not validacion["valid"] or not validacion["lada"]:
388
- return None
603
+ def get_estadisticas(cls) -> dict:
604
+ """Obtiene estadísticas del catálogo"""
605
+ cls._load_data()
389
606
 
390
- codigo = cls.buscar_por_lada(validacion["lada"])
607
+ if not cls._data:
608
+ return {}
391
609
 
392
- if not codigo:
393
- return None
610
+ tipos = {}
611
+ regiones = {}
612
+ estados = {}
394
613
 
395
- return {
396
- "lada": codigo["lada"],
397
- "ciudad": codigo["ciudad"],
398
- "estado": codigo["estado"],
399
- "tipo": codigo["tipo"],
400
- "region": codigo["region"],
401
- }
614
+ for c in cls._data:
615
+ t = c.get("tipo", "unknown")
616
+ tipos[t] = tipos.get(t, 0) + 1
402
617
 
403
- @classmethod
404
- def get_estadisticas(cls) -> dict[str, int | dict]:
405
- """
406
- Obtiene estadísticas del catálogo.
407
-
408
- Returns:
409
- Diccionario con estadísticas del catálogo
618
+ r = c.get("region", "unknown")
619
+ regiones[r] = regiones.get(r, 0) + 1
410
620
 
411
- Ejemplo:
412
- >>> stats = CodigosLADACatalog.get_estadisticas()
413
- >>> print(f"Total códigos: {stats['total_codigos']}")
414
- >>> print(f"Estados: {stats['estados_cubiertos']}")
415
- """
416
- cls._load_data()
621
+ e = c.get("estado", "unknown")
622
+ estados[e] = estados.get(e, 0) + 1
417
623
 
418
624
  return {
419
- "total_codigos": len(cls._data), # type: ignore
420
- "codigos_metropolitanos": len(cls.get_metropolitanas()),
421
- "codigos_fronterizos": len(cls.get_fronterizas()),
422
- "codigos_turisticos": len(cls.get_turisticas()),
423
- "estados_cubiertos": len(cls._by_estado), # type: ignore
424
- "regiones": list(cls._by_region.keys()), # type: ignore
425
- "tipos": list(cls._by_tipo.keys()), # type: ignore
625
+ "total_codigos": len(cls._data),
626
+ "por_tipo": tipos,
627
+ "por_region": regiones,
628
+ "estados_cubiertos": len(estados),
629
+ "zonas_metropolitanas": len(cls.ZONAS_METROPOLITANAS),
426
630
  }