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.
Files changed (81) hide show
  1. catalogmx/__init__.py +56 -0
  2. catalogmx/catalogs/__init__.py +5 -0
  3. catalogmx/catalogs/banxico/__init__.py +24 -0
  4. catalogmx/catalogs/banxico/banks.py +136 -0
  5. catalogmx/catalogs/banxico/codigos_plaza.py +287 -0
  6. catalogmx/catalogs/banxico/instituciones_financieras.py +338 -0
  7. catalogmx/catalogs/banxico/monedas_divisas.py +386 -0
  8. catalogmx/catalogs/banxico/udis.py +279 -0
  9. catalogmx/catalogs/ift/__init__.py +15 -0
  10. catalogmx/catalogs/ift/codigos_lada.py +426 -0
  11. catalogmx/catalogs/ift/operadores_moviles.py +315 -0
  12. catalogmx/catalogs/inegi/__init__.py +21 -0
  13. catalogmx/catalogs/inegi/localidades.py +207 -0
  14. catalogmx/catalogs/inegi/municipios.py +73 -0
  15. catalogmx/catalogs/inegi/municipios_completo.py +236 -0
  16. catalogmx/catalogs/inegi/states.py +148 -0
  17. catalogmx/catalogs/mexico/__init__.py +17 -0
  18. catalogmx/catalogs/mexico/hoy_no_circula.py +215 -0
  19. catalogmx/catalogs/mexico/placas_formatos.py +184 -0
  20. catalogmx/catalogs/mexico/salarios_minimos.py +156 -0
  21. catalogmx/catalogs/mexico/uma.py +207 -0
  22. catalogmx/catalogs/sat/__init__.py +13 -0
  23. catalogmx/catalogs/sat/carta_porte/__init__.py +19 -0
  24. catalogmx/catalogs/sat/carta_porte/aeropuertos.py +76 -0
  25. catalogmx/catalogs/sat/carta_porte/carreteras.py +59 -0
  26. catalogmx/catalogs/sat/carta_porte/config_autotransporte.py +54 -0
  27. catalogmx/catalogs/sat/carta_porte/material_peligroso.py +66 -0
  28. catalogmx/catalogs/sat/carta_porte/puertos_maritimos.py +63 -0
  29. catalogmx/catalogs/sat/carta_porte/tipo_embalaje.py +48 -0
  30. catalogmx/catalogs/sat/carta_porte/tipo_permiso.py +54 -0
  31. catalogmx/catalogs/sat/cfdi_4/__init__.py +42 -0
  32. catalogmx/catalogs/sat/cfdi_4/clave_prod_serv.py +383 -0
  33. catalogmx/catalogs/sat/cfdi_4/clave_unidad.py +298 -0
  34. catalogmx/catalogs/sat/cfdi_4/exportacion.py +45 -0
  35. catalogmx/catalogs/sat/cfdi_4/forma_pago.py +45 -0
  36. catalogmx/catalogs/sat/cfdi_4/impuesto.py +57 -0
  37. catalogmx/catalogs/sat/cfdi_4/meses.py +34 -0
  38. catalogmx/catalogs/sat/cfdi_4/metodo_pago.py +45 -0
  39. catalogmx/catalogs/sat/cfdi_4/objeto_imp.py +45 -0
  40. catalogmx/catalogs/sat/cfdi_4/periodicidad.py +34 -0
  41. catalogmx/catalogs/sat/cfdi_4/regimen_fiscal.py +57 -0
  42. catalogmx/catalogs/sat/cfdi_4/tasa_o_cuota.py +42 -0
  43. catalogmx/catalogs/sat/cfdi_4/tipo_comprobante.py +45 -0
  44. catalogmx/catalogs/sat/cfdi_4/tipo_factor.py +34 -0
  45. catalogmx/catalogs/sat/cfdi_4/tipo_relacion.py +45 -0
  46. catalogmx/catalogs/sat/cfdi_4/uso_cfdi.py +45 -0
  47. catalogmx/catalogs/sat/comercio_exterior/__init__.py +39 -0
  48. catalogmx/catalogs/sat/comercio_exterior/claves_pedimento.py +77 -0
  49. catalogmx/catalogs/sat/comercio_exterior/estados.py +122 -0
  50. catalogmx/catalogs/sat/comercio_exterior/incoterms.py +226 -0
  51. catalogmx/catalogs/sat/comercio_exterior/monedas.py +107 -0
  52. catalogmx/catalogs/sat/comercio_exterior/motivos_traslado.py +54 -0
  53. catalogmx/catalogs/sat/comercio_exterior/paises.py +88 -0
  54. catalogmx/catalogs/sat/comercio_exterior/registro_ident_trib.py +76 -0
  55. catalogmx/catalogs/sat/comercio_exterior/unidades_aduana.py +54 -0
  56. catalogmx/catalogs/sat/comercio_exterior/validator.py +212 -0
  57. catalogmx/catalogs/sat/nomina/__init__.py +19 -0
  58. catalogmx/catalogs/sat/nomina/banco.py +50 -0
  59. catalogmx/catalogs/sat/nomina/periodicidad_pago.py +48 -0
  60. catalogmx/catalogs/sat/nomina/riesgo_puesto.py +56 -0
  61. catalogmx/catalogs/sat/nomina/tipo_contrato.py +47 -0
  62. catalogmx/catalogs/sat/nomina/tipo_jornada.py +42 -0
  63. catalogmx/catalogs/sat/nomina/tipo_nomina.py +52 -0
  64. catalogmx/catalogs/sat/nomina/tipo_regimen.py +47 -0
  65. catalogmx/catalogs/sepomex/__init__.py +5 -0
  66. catalogmx/catalogs/sepomex/codigos_postales.py +184 -0
  67. catalogmx/cli.py +185 -0
  68. catalogmx/helpers.py +324 -0
  69. catalogmx/utils/text.py +55 -0
  70. catalogmx/validators/__init__.py +0 -0
  71. catalogmx/validators/clabe.py +233 -0
  72. catalogmx/validators/curp.py +623 -0
  73. catalogmx/validators/nss.py +255 -0
  74. catalogmx/validators/rfc.py +1004 -0
  75. catalogmx-0.3.0.dist-info/METADATA +644 -0
  76. catalogmx-0.3.0.dist-info/RECORD +81 -0
  77. catalogmx-0.3.0.dist-info/WHEEL +5 -0
  78. catalogmx-0.3.0.dist-info/entry_points.txt +2 -0
  79. catalogmx-0.3.0.dist-info/licenses/AUTHORS.rst +5 -0
  80. catalogmx-0.3.0.dist-info/licenses/LICENSE +19 -0
  81. catalogmx-0.3.0.dist-info/top_level.txt +1 -0
catalogmx/__init__.py ADDED
@@ -0,0 +1,56 @@
1
+ __version__ = "0.3.0"
2
+
3
+ # RFC imports
4
+ # Modern helper functions (recommended API)
5
+ from .helpers import (
6
+ detect_rfc_type,
7
+ # CURP helpers
8
+ generate_curp,
9
+ # RFC helpers
10
+ generate_rfc_persona_fisica,
11
+ generate_rfc_persona_moral,
12
+ get_curp_info,
13
+ is_valid_curp,
14
+ is_valid_rfc,
15
+ validate_curp,
16
+ validate_rfc,
17
+ )
18
+
19
+ # CURP imports
20
+ from .validators.curp import (
21
+ CURPException,
22
+ CURPGenerator,
23
+ CURPLengthError,
24
+ CURPStructureError,
25
+ CURPValidator,
26
+ )
27
+ from .validators.rfc import (
28
+ RFCGenerator,
29
+ RFCGeneratorFisicas,
30
+ RFCGeneratorMorales,
31
+ RFCValidator,
32
+ )
33
+
34
+ __all__ = [
35
+ # RFC Classes (legacy/advanced usage)
36
+ "RFCValidator",
37
+ "RFCGenerator",
38
+ "RFCGeneratorFisicas",
39
+ "RFCGeneratorMorales",
40
+ # CURP Classes (legacy/advanced usage)
41
+ "CURPValidator",
42
+ "CURPGenerator",
43
+ "CURPException",
44
+ "CURPLengthError",
45
+ "CURPStructureError",
46
+ # Modern helper functions (recommended)
47
+ "generate_rfc_persona_fisica",
48
+ "generate_rfc_persona_moral",
49
+ "validate_rfc",
50
+ "detect_rfc_type",
51
+ "is_valid_rfc",
52
+ "generate_curp",
53
+ "validate_curp",
54
+ "get_curp_info",
55
+ "is_valid_curp",
56
+ ]
@@ -0,0 +1,5 @@
1
+ """
2
+ catalogmx.catalogs - Catálogos oficiales mexicanos
3
+ """
4
+
5
+ __all__ = ["sat", "banxico", "inegi", "sepomex", "ift"]
@@ -0,0 +1,24 @@
1
+ """
2
+ catalogmx.catalogs.banxico - Catálogos de Banxico
3
+
4
+ Catálogos incluidos:
5
+ - BankCatalog: Bancos autorizados por Banxico
6
+ - UDICatalog: UDIs (Unidades de Inversión)
7
+ - InstitucionesFinancieras: Tipos de instituciones del sistema financiero
8
+ - MonedasDivisas: Monedas y divisas internacionales
9
+ - CodigosPlazaCatalog: Códigos de plaza para CLABE
10
+ """
11
+
12
+ from .banks import BankCatalog
13
+ from .codigos_plaza import CodigosPlazaCatalog
14
+ from .instituciones_financieras import InstitucionesFinancieras
15
+ from .monedas_divisas import MonedasDivisas
16
+ from .udis import UDICatalog
17
+
18
+ __all__ = [
19
+ "BankCatalog",
20
+ "UDICatalog",
21
+ "InstitucionesFinancieras",
22
+ "MonedasDivisas",
23
+ "CodigosPlazaCatalog",
24
+ ]
@@ -0,0 +1,136 @@
1
+ """
2
+ Bank Catalog from Banxico
3
+
4
+ This module provides access to the official catalog of Mexican banks
5
+ participating in the SPEI (Sistema de Pagos Electrónicos Interbancarios).
6
+ """
7
+
8
+ import json
9
+ from pathlib import Path
10
+
11
+ from catalogmx.utils.text import normalize_text
12
+
13
+
14
+ class BankCatalog:
15
+ """
16
+ Catalog of Mexican banks
17
+ """
18
+
19
+ _data: list[dict] | None = None
20
+ _bank_by_code: dict[str, dict] | None = None
21
+ _bank_by_name: dict[str, dict] | None = None
22
+ _bank_by_name_normalized: dict[str, dict] | None = None
23
+
24
+ @classmethod
25
+ def _load_data(cls) -> None:
26
+ """Load bank data from JSON file"""
27
+ if cls._data is None:
28
+ # Path: catalogmx/packages/python/catalogmx/catalogs/banxico/banks.py
29
+ # Target: catalogmx/packages/shared-data/banxico/banks.json
30
+ current_file = Path(__file__)
31
+ shared_data_path = (
32
+ current_file.parent.parent.parent.parent.parent
33
+ / "shared-data"
34
+ / "banxico"
35
+ / "banks.json"
36
+ )
37
+
38
+ with open(shared_data_path, encoding="utf-8") as f:
39
+ cls._data = json.load(f)
40
+
41
+ # Create lookup dictionaries
42
+ cls._bank_by_code = {bank["code"]: bank for bank in cls._data}
43
+ cls._bank_by_name = {bank["name"].upper(): bank for bank in cls._data}
44
+ # Accent-insensitive lookup
45
+ cls._bank_by_name_normalized = {
46
+ normalize_text(bank["name"]): bank for bank in cls._data
47
+ }
48
+
49
+ @classmethod
50
+ def get_all_banks(cls) -> list[dict]:
51
+ """
52
+ Get all banks in the catalog
53
+
54
+ :return: List of bank dictionaries
55
+ """
56
+ cls._load_data()
57
+ return cls._data.copy()
58
+
59
+ @classmethod
60
+ def get_bank_by_code(cls, code: str) -> dict | None:
61
+ """
62
+ Get bank information by code
63
+
64
+ :param code: 3-digit bank code (e.g., '002' for Banamex)
65
+ :return: Bank dictionary or None if not found
66
+ """
67
+ cls._load_data()
68
+ code = str(code).zfill(3)
69
+ return cls._bank_by_code.get(code)
70
+
71
+ @classmethod
72
+ def get_bank_by_name(cls, name: str) -> dict | None:
73
+ """
74
+ Get bank information by name (accent-insensitive)
75
+
76
+ :param name: Bank name (case and accent insensitive, e.g., 'BANAMEX' or 'Banamex')
77
+ :return: Bank dictionary or None if not found
78
+
79
+ Examples:
80
+ >>> # Both searches work the same
81
+ >>> bank = BankCatalog.get_bank_by_name("Banamex")
82
+ >>> bank = BankCatalog.get_bank_by_name("Banámex") # same result
83
+ """
84
+ cls._load_data()
85
+ return cls._bank_by_name_normalized.get(normalize_text(name))
86
+
87
+ @classmethod
88
+ def is_spei_participant(cls, code: str) -> bool:
89
+ """
90
+ Check if a bank participates in SPEI
91
+
92
+ :param code: 3-digit bank code
93
+ :return: True if bank participates in SPEI, False otherwise
94
+ """
95
+ bank = cls.get_bank_by_code(code)
96
+ return bank.get("spei", False) if bank else False
97
+
98
+ @classmethod
99
+ def get_spei_banks(cls) -> list[dict]:
100
+ """
101
+ Get all banks that participate in SPEI
102
+
103
+ :return: List of SPEI participant banks
104
+ """
105
+ cls._load_data()
106
+ return [bank for bank in cls._data if bank.get("spei", False)]
107
+
108
+ @classmethod
109
+ def validate_bank_code(cls, code: str) -> bool:
110
+ """
111
+ Validate if a bank code exists
112
+
113
+ :param code: 3-digit bank code
114
+ :return: True if exists, False otherwise
115
+ """
116
+ return cls.get_bank_by_code(code) is not None
117
+
118
+
119
+ # Convenience dictionaries for direct access
120
+ def get_banks_dict() -> dict[str, dict]:
121
+ """Get dictionary of all banks indexed by code"""
122
+ BankCatalog._load_data()
123
+ return BankCatalog._bank_by_code.copy()
124
+
125
+
126
+ def get_spei_banks() -> list[dict]:
127
+ """Get list of all SPEI participant banks"""
128
+ return BankCatalog.get_spei_banks()
129
+
130
+
131
+ # Export commonly used functions
132
+ __all__ = [
133
+ "BankCatalog",
134
+ "get_banks_dict",
135
+ "get_spei_banks",
136
+ ]
@@ -0,0 +1,287 @@
1
+ """
2
+ Catálogo de Códigos de Plaza CLABE
3
+ =====================================
4
+
5
+ Códigos de plaza para el sistema CLABE (Clave Bancaria Estandarizada).
6
+ Los códigos de plaza son identificadores de 3 dígitos que indican la ubicación
7
+ geográfica de las sucursales bancarias en México.
8
+
9
+ Fuente: Banco de México (BANXICO) / Sistema de Pagos Electrónicos Interbancarios (SPEI)
10
+ """
11
+
12
+ import json
13
+ import os
14
+ from typing import TypedDict
15
+
16
+ try:
17
+ from unidecode import unidecode
18
+ except ImportError:
19
+ # Fallback if unidecode not available
20
+ def unidecode(text):
21
+ return text
22
+
23
+
24
+ class CodigoPlaza(TypedDict):
25
+ """Estructura de un código de plaza CLABE."""
26
+
27
+ codigo: str # Código de 3 dígitos
28
+ plaza: str # Nombre de la plaza/ciudad
29
+ estado: str # Estado
30
+ cve_entidad: str # Código INEGI del estado
31
+
32
+
33
+ class CodigosPlazaCatalog:
34
+ """Catálogo de códigos de plaza para CLABE."""
35
+
36
+ _data: list[CodigoPlaza] | None = None
37
+ _by_codigo: dict[str, list[CodigoPlaza]] | None = None
38
+ _by_estado: dict[str, list[CodigoPlaza]] | None = None
39
+ _by_plaza: dict[str, list[CodigoPlaza]] | None = None
40
+ _by_plaza_normalized: dict[str, list[CodigoPlaza]] | None = None
41
+
42
+ @classmethod
43
+ def _normalize(cls, text: str) -> str:
44
+ """Normaliza texto removiendo acentos y convirtiendo a mayúsculas."""
45
+ return unidecode(text).upper()
46
+
47
+ @classmethod
48
+ def _load(cls) -> None:
49
+ """Carga los datos del catálogo."""
50
+ if cls._data is not None:
51
+ return
52
+
53
+ data_path = os.path.join(
54
+ os.path.dirname(__file__), "../../../../shared-data/banxico/codigos_plaza.json"
55
+ )
56
+
57
+ with open(data_path, encoding="utf-8") as f:
58
+ catalog = json.load(f)
59
+ cls._data = catalog["plazas"]
60
+
61
+ # Build indices
62
+ cls._by_codigo = {}
63
+ cls._by_estado = {}
64
+ cls._by_plaza = {}
65
+ cls._by_plaza_normalized = {}
66
+
67
+ for plaza in cls._data:
68
+ # By codigo (puede haber múltiples plazas con el mismo código)
69
+ if plaza["codigo"] not in cls._by_codigo:
70
+ cls._by_codigo[plaza["codigo"]] = []
71
+ cls._by_codigo[plaza["codigo"]].append(plaza)
72
+
73
+ # By estado
74
+ if plaza["estado"] not in cls._by_estado:
75
+ cls._by_estado[plaza["estado"]] = []
76
+ cls._by_estado[plaza["estado"]].append(plaza)
77
+
78
+ # By plaza name (exact match)
79
+ plaza_key = plaza["plaza"].upper()
80
+ if plaza_key not in cls._by_plaza:
81
+ cls._by_plaza[plaza_key] = []
82
+ cls._by_plaza[plaza_key].append(plaza)
83
+
84
+ # By plaza name (normalized, accent-insensitive)
85
+ plaza_normalized = cls._normalize(plaza["plaza"])
86
+ if plaza_normalized not in cls._by_plaza_normalized:
87
+ cls._by_plaza_normalized[plaza_normalized] = []
88
+ cls._by_plaza_normalized[plaza_normalized].append(plaza)
89
+
90
+ @classmethod
91
+ def get_all(cls) -> list[CodigoPlaza]:
92
+ """
93
+ Obtiene todos los códigos de plaza.
94
+
95
+ Returns:
96
+ Lista con todos los códigos de plaza
97
+ """
98
+ cls._load()
99
+ return cls._data.copy()
100
+
101
+ @classmethod
102
+ def buscar_por_codigo(cls, codigo: str) -> list[CodigoPlaza]:
103
+ """
104
+ Busca plazas por código.
105
+
106
+ Args:
107
+ codigo: Código de plaza (3 dígitos)
108
+
109
+ Returns:
110
+ Lista de plazas con ese código (puede haber múltiples)
111
+
112
+ Examples:
113
+ >>> plazas = CodigosPlazaCatalog.buscar_por_codigo("320")
114
+ >>> for p in plazas:
115
+ ... print(f"{p['plaza']}, {p['estado']}")
116
+ Guadalajara, Jalisco
117
+ Tonala, Jalisco
118
+ ...
119
+ """
120
+ cls._load()
121
+ codigo_padded = codigo.zfill(3)
122
+ return cls._by_codigo.get(codigo_padded, [])
123
+
124
+ @classmethod
125
+ def buscar_por_plaza(cls, nombre_plaza: str) -> list[CodigoPlaza]:
126
+ """
127
+ Busca códigos por nombre de plaza (insensible a acentos).
128
+
129
+ Args:
130
+ nombre_plaza: Nombre de la plaza/ciudad
131
+
132
+ Returns:
133
+ Lista de códigos para esa plaza
134
+
135
+ Examples:
136
+ >>> # Tonalá aparece en dos estados diferentes
137
+ >>> plazas = CodigosPlazaCatalog.buscar_por_plaza("Tonala")
138
+ >>> for p in plazas:
139
+ ... print(f"Código {p['codigo']}: {p['plaza']}, {p['estado']}")
140
+ Código 135: Tonala, Chiapas
141
+ Código 320: Tonala, Jalisco
142
+
143
+ >>> # Búsqueda insensible a acentos
144
+ >>> tuxpam = CodigosPlazaCatalog.buscar_por_plaza("Tuxpam") # sin acento
145
+ >>> print(len(tuxpam)) # Encuentra "Túxpam" con acento
146
+ 2
147
+ """
148
+ cls._load()
149
+ plaza_normalized = cls._normalize(nombre_plaza)
150
+ return cls._by_plaza_normalized.get(plaza_normalized, [])
151
+
152
+ @classmethod
153
+ def get_por_estado(cls, estado: str) -> list[CodigoPlaza]:
154
+ """
155
+ Obtiene todas las plazas de un estado.
156
+
157
+ Args:
158
+ estado: Nombre del estado
159
+
160
+ Returns:
161
+ Lista de plazas en ese estado
162
+
163
+ Examples:
164
+ >>> plazas = CodigosPlazaCatalog.get_por_estado("Jalisco")
165
+ >>> print(f"Jalisco tiene {len(plazas)} plazas")
166
+ """
167
+ cls._load()
168
+ return cls._by_estado.get(estado, [])
169
+
170
+ @classmethod
171
+ def get_por_cve_entidad(cls, cve_entidad: str) -> list[CodigoPlaza]:
172
+ """
173
+ Obtiene todas las plazas por código INEGI de entidad.
174
+
175
+ Args:
176
+ cve_entidad: Código INEGI del estado (2 dígitos)
177
+
178
+ Returns:
179
+ Lista de plazas en esa entidad
180
+
181
+ Examples:
182
+ >>> # Jalisco tiene cve_entidad '14'
183
+ >>> plazas = CodigosPlazaCatalog.get_por_cve_entidad("14")
184
+ >>> print(f"Entidad 14 tiene {len(plazas)} plazas")
185
+ """
186
+ cls._load()
187
+ return [p for p in cls._data if p["cve_entidad"] == cve_entidad]
188
+
189
+ @classmethod
190
+ def validar_codigo_clabe(cls, codigo_plaza: str) -> dict:
191
+ """
192
+ Valida un código de plaza dentro de una CLABE.
193
+
194
+ Args:
195
+ codigo_plaza: Código de plaza (3 dígitos)
196
+
197
+ Returns:
198
+ Diccionario con información de validación
199
+
200
+ Examples:
201
+ >>> result = CodigosPlazaCatalog.validar_codigo_clabe("180")
202
+ >>> print(result['valido'])
203
+ True
204
+ >>> print(result['plazas'][0]['plaza'])
205
+ Ciudad de México
206
+ """
207
+ cls._load()
208
+ codigo_padded = codigo_plaza.zfill(3)
209
+ plazas = cls.buscar_por_codigo(codigo_padded)
210
+
211
+ return {
212
+ "valido": len(plazas) > 0,
213
+ "codigo": codigo_padded,
214
+ "plazas": plazas,
215
+ "num_plazas": len(plazas),
216
+ }
217
+
218
+ @classmethod
219
+ def get_plazas_duplicadas(cls) -> dict[str, list[CodigoPlaza]]:
220
+ """
221
+ Obtiene plazas con nombres duplicados en diferentes estados.
222
+
223
+ Returns:
224
+ Diccionario con nombres de plaza y sus instancias
225
+
226
+ Examples:
227
+ >>> duplicadas = CodigosPlazaCatalog.get_plazas_duplicadas()
228
+ >>> for nombre, plazas in duplicadas.items():
229
+ ... print(f"{nombre}: {len(plazas)} ubicaciones")
230
+ Tonala: 2 ubicaciones (Chiapas, Jalisco)
231
+ Túxpam: 2 ubicaciones (Jalisco, Nayarit)
232
+ """
233
+ cls._load()
234
+ duplicadas = {}
235
+ for nombre, plazas in cls._by_plaza.items():
236
+ if len(plazas) > 1:
237
+ duplicadas[nombre] = plazas
238
+ return duplicadas
239
+
240
+ @classmethod
241
+ def search(cls, query: str) -> list[CodigoPlaza]:
242
+ """
243
+ Busca plazas por nombre parcial (insensible a acentos y mayúsculas).
244
+
245
+ Args:
246
+ query: Texto a buscar
247
+
248
+ Returns:
249
+ Lista de plazas que coinciden
250
+
251
+ Examples:
252
+ >>> # Buscar todas las plazas con "San" en el nombre
253
+ >>> plazas = CodigosPlazaCatalog.search("San")
254
+ >>> for p in plazas[:5]:
255
+ ... print(f"{p['codigo']}: {p['plaza']}, {p['estado']}")
256
+
257
+ >>> # Búsqueda insensible a acentos
258
+ >>> plazas = CodigosPlazaCatalog.search("Tuxpam") # sin acento
259
+ >>> print(len(plazas)) # Encuentra "Túxpam" con acento
260
+ 3
261
+ """
262
+ cls._load()
263
+ query_normalized = cls._normalize(query)
264
+ return [p for p in cls._data if query_normalized in cls._normalize(p["plaza"])]
265
+
266
+ @classmethod
267
+ def get_estadisticas(cls) -> dict:
268
+ """
269
+ Obtiene estadísticas del catálogo.
270
+
271
+ Returns:
272
+ Diccionario con estadísticas
273
+ """
274
+ cls._load()
275
+
276
+ estados = {p["estado"] for p in cls._data}
277
+ codigos_unicos = len(cls._by_codigo)
278
+
279
+ return {
280
+ "total_plazas": len(cls._data),
281
+ "codigos_unicos": codigos_unicos,
282
+ "estados_cubiertos": len(estados),
283
+ "plazas_duplicadas": len(cls.get_plazas_duplicadas()),
284
+ }
285
+
286
+
287
+ __all__ = ["CodigosPlazaCatalog", "CodigoPlaza"]