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.
- catalogmx/__init__.py +133 -19
- catalogmx/calculators/__init__.py +113 -0
- catalogmx/calculators/costo_trabajador.py +213 -0
- catalogmx/calculators/impuestos.py +920 -0
- catalogmx/calculators/imss.py +370 -0
- catalogmx/calculators/isr.py +290 -0
- catalogmx/calculators/resico.py +154 -0
- catalogmx/catalogs/banxico/__init__.py +29 -3
- catalogmx/catalogs/banxico/cetes_sqlite.py +279 -0
- catalogmx/catalogs/banxico/inflacion_sqlite.py +302 -0
- catalogmx/catalogs/banxico/salarios_minimos_sqlite.py +295 -0
- catalogmx/catalogs/banxico/tiie_sqlite.py +279 -0
- catalogmx/catalogs/banxico/tipo_cambio_usd_sqlite.py +255 -0
- catalogmx/catalogs/banxico/udis_sqlite.py +332 -0
- catalogmx/catalogs/cnbv/__init__.py +9 -0
- catalogmx/catalogs/cnbv/sectores.py +173 -0
- catalogmx/catalogs/conapo/__init__.py +15 -0
- catalogmx/catalogs/conapo/sistema_urbano_nacional.py +50 -0
- catalogmx/catalogs/conapo/zonas_metropolitanas.py +230 -0
- catalogmx/catalogs/ift/__init__.py +1 -1
- catalogmx/catalogs/ift/codigos_lada.py +517 -313
- catalogmx/catalogs/inegi/__init__.py +17 -0
- catalogmx/catalogs/inegi/scian.py +127 -0
- catalogmx/catalogs/mexico/__init__.py +2 -0
- catalogmx/catalogs/mexico/giros_mercantiles.py +119 -0
- catalogmx/catalogs/sat/carta_porte/material_peligroso.py +5 -1
- catalogmx/catalogs/sat/cfdi_4/clave_prod_serv.py +78 -0
- catalogmx/catalogs/sat/cfdi_4/tasa_o_cuota.py +2 -1
- catalogmx/catalogs/sepomex/__init__.py +2 -1
- catalogmx/catalogs/sepomex/codigos_postales.py +30 -2
- catalogmx/catalogs/sepomex/codigos_postales_completo.py +261 -0
- catalogmx/cli.py +12 -9
- catalogmx/data/__init__.py +10 -0
- catalogmx/data/mexico_dynamic.sqlite3 +0 -0
- catalogmx/data/updater.py +362 -0
- catalogmx/generators/__init__.py +20 -0
- catalogmx/generators/identity.py +582 -0
- catalogmx/helpers.py +177 -3
- catalogmx/utils/__init__.py +29 -0
- catalogmx/utils/clabe_utils.py +417 -0
- catalogmx/utils/text.py +7 -1
- catalogmx/validators/clabe.py +52 -2
- catalogmx/validators/nss.py +32 -27
- catalogmx/validators/rfc.py +185 -52
- catalogmx-0.4.0.dist-info/METADATA +905 -0
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/RECORD +51 -25
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/WHEEL +1 -1
- catalogmx/catalogs/banxico/udis.py +0 -279
- catalogmx-0.3.0.dist-info/METADATA +0 -644
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/entry_points.txt +0 -0
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/licenses/AUTHORS.rst +0 -0
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {catalogmx-0.3.0.dist-info → catalogmx-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UDI (Unidades de Inversión) Catalog - SQLite Backend
|
|
3
|
+
|
|
4
|
+
This module provides access to UDI values from Banco de México using a SQLite database
|
|
5
|
+
that can be automatically updated without requiring library releases.
|
|
6
|
+
|
|
7
|
+
UDIs are inflation-indexed investment units used in Mexico.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sqlite3
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from catalogmx.data.updater import get_database_path
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UDICatalog:
|
|
17
|
+
"""
|
|
18
|
+
Catalog of UDI (Unidades de Inversión) values
|
|
19
|
+
|
|
20
|
+
UDIs are inflation-indexed investment units maintained by Banco de México.
|
|
21
|
+
They are commonly used for mortgage loans and other long-term financial obligations.
|
|
22
|
+
|
|
23
|
+
This catalog uses an auto-updating SQLite database backend.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
_db_path: Path | None = None
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def _get_db_path(cls) -> Path:
|
|
30
|
+
"""Get path to database with auto-update"""
|
|
31
|
+
if cls._db_path is None:
|
|
32
|
+
cls._db_path = get_database_path(auto_update=True, max_age_hours=24)
|
|
33
|
+
return cls._db_path
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def _row_to_dict(cls, row: sqlite3.Row) -> dict:
|
|
37
|
+
"""Convert SQLite row to dictionary"""
|
|
38
|
+
return {
|
|
39
|
+
"fecha": row["fecha"],
|
|
40
|
+
"valor": row["valor"],
|
|
41
|
+
"año": row["anio"],
|
|
42
|
+
"mes": row["mes"],
|
|
43
|
+
"tipo": row["tipo"],
|
|
44
|
+
"moneda": row["moneda"] if "moneda" in row.keys() else "MXN",
|
|
45
|
+
"notas": row["notas"] if "notas" in row.keys() else None,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def get_data(cls) -> list[dict]:
|
|
50
|
+
"""
|
|
51
|
+
Get all UDI data
|
|
52
|
+
|
|
53
|
+
:return: List of all UDI records
|
|
54
|
+
"""
|
|
55
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
56
|
+
db.row_factory = sqlite3.Row
|
|
57
|
+
cursor = db.execute(
|
|
58
|
+
"SELECT fecha, valor, anio, mes, tipo, moneda, notas FROM udis ORDER BY fecha"
|
|
59
|
+
)
|
|
60
|
+
results = [cls._row_to_dict(row) for row in cursor.fetchall()]
|
|
61
|
+
db.close()
|
|
62
|
+
return results
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def get_por_fecha(cls, fecha: str) -> dict | None:
|
|
66
|
+
"""
|
|
67
|
+
Get UDI value for a specific date
|
|
68
|
+
|
|
69
|
+
:param fecha: Date string in YYYY-MM-DD format
|
|
70
|
+
:return: UDI record or None if not found
|
|
71
|
+
"""
|
|
72
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
73
|
+
db.row_factory = sqlite3.Row
|
|
74
|
+
|
|
75
|
+
# Try exact match first
|
|
76
|
+
cursor = db.execute(
|
|
77
|
+
"""
|
|
78
|
+
SELECT fecha, valor, anio, mes, tipo, moneda, notas
|
|
79
|
+
FROM udis
|
|
80
|
+
WHERE fecha = ? AND tipo IN ('diario', 'oficial_banxico')
|
|
81
|
+
LIMIT 1
|
|
82
|
+
""",
|
|
83
|
+
(fecha,),
|
|
84
|
+
)
|
|
85
|
+
row = cursor.fetchone()
|
|
86
|
+
|
|
87
|
+
# If not found, try monthly average
|
|
88
|
+
if not row:
|
|
89
|
+
try:
|
|
90
|
+
anio, mes, _dia = fecha.split("-")
|
|
91
|
+
cursor = db.execute(
|
|
92
|
+
"""
|
|
93
|
+
SELECT fecha, valor, anio, mes, tipo, moneda, notas
|
|
94
|
+
FROM udis
|
|
95
|
+
WHERE anio = ? AND mes = ? AND tipo = 'promedio_mensual'
|
|
96
|
+
LIMIT 1
|
|
97
|
+
""",
|
|
98
|
+
(int(anio), int(mes)),
|
|
99
|
+
)
|
|
100
|
+
row = cursor.fetchone()
|
|
101
|
+
except ValueError:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
db.close()
|
|
105
|
+
|
|
106
|
+
if row:
|
|
107
|
+
return cls._row_to_dict(row)
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def get_por_mes(cls, anio: int, mes: int) -> dict | None:
|
|
112
|
+
"""
|
|
113
|
+
Get monthly average UDI value
|
|
114
|
+
|
|
115
|
+
:param anio: Year (e.g., 2024)
|
|
116
|
+
:param mes: Month (1-12)
|
|
117
|
+
:return: UDI record with monthly average or None if not found
|
|
118
|
+
"""
|
|
119
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
120
|
+
db.row_factory = sqlite3.Row
|
|
121
|
+
|
|
122
|
+
cursor = db.execute(
|
|
123
|
+
"""
|
|
124
|
+
SELECT fecha, valor, anio, mes, tipo, moneda, notas
|
|
125
|
+
FROM udis
|
|
126
|
+
WHERE anio = ? AND mes = ? AND tipo = 'promedio_mensual'
|
|
127
|
+
LIMIT 1
|
|
128
|
+
""",
|
|
129
|
+
(anio, mes),
|
|
130
|
+
)
|
|
131
|
+
row = cursor.fetchone()
|
|
132
|
+
db.close()
|
|
133
|
+
|
|
134
|
+
if row:
|
|
135
|
+
return cls._row_to_dict(row)
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def get_promedio_anual(cls, anio: int) -> dict | None:
|
|
140
|
+
"""
|
|
141
|
+
Get annual average UDI value
|
|
142
|
+
|
|
143
|
+
:param anio: Year (e.g., 2024)
|
|
144
|
+
:return: UDI record with annual average or None if not found
|
|
145
|
+
"""
|
|
146
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
147
|
+
db.row_factory = sqlite3.Row
|
|
148
|
+
|
|
149
|
+
cursor = db.execute(
|
|
150
|
+
"""
|
|
151
|
+
SELECT fecha, valor, anio, mes, tipo, moneda, notas
|
|
152
|
+
FROM udis
|
|
153
|
+
WHERE anio = ? AND tipo = 'promedio_anual'
|
|
154
|
+
LIMIT 1
|
|
155
|
+
""",
|
|
156
|
+
(anio,),
|
|
157
|
+
)
|
|
158
|
+
row = cursor.fetchone()
|
|
159
|
+
db.close()
|
|
160
|
+
|
|
161
|
+
if row:
|
|
162
|
+
return cls._row_to_dict(row)
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def get_por_anio(cls, anio: int) -> list[dict]:
|
|
167
|
+
"""
|
|
168
|
+
Return the daily UDI series for a given year
|
|
169
|
+
|
|
170
|
+
:param anio: Year (e.g., 2024)
|
|
171
|
+
:return: List of UDI records for the year
|
|
172
|
+
"""
|
|
173
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
174
|
+
db.row_factory = sqlite3.Row
|
|
175
|
+
|
|
176
|
+
cursor = db.execute(
|
|
177
|
+
"""
|
|
178
|
+
SELECT fecha, valor, anio, mes, tipo, moneda, notas
|
|
179
|
+
FROM udis
|
|
180
|
+
WHERE anio = ? AND tipo IN ('diario', 'oficial_banxico')
|
|
181
|
+
ORDER BY fecha
|
|
182
|
+
""",
|
|
183
|
+
(anio,),
|
|
184
|
+
)
|
|
185
|
+
results = [cls._row_to_dict(row) for row in cursor.fetchall()]
|
|
186
|
+
db.close()
|
|
187
|
+
return results
|
|
188
|
+
|
|
189
|
+
@classmethod
|
|
190
|
+
def get_actual(cls) -> dict | None:
|
|
191
|
+
"""
|
|
192
|
+
Get most recent UDI value
|
|
193
|
+
|
|
194
|
+
:return: Latest UDI record
|
|
195
|
+
"""
|
|
196
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
197
|
+
db.row_factory = sqlite3.Row
|
|
198
|
+
|
|
199
|
+
cursor = db.execute("""
|
|
200
|
+
SELECT fecha, valor, anio, mes, tipo, moneda, notas
|
|
201
|
+
FROM udis
|
|
202
|
+
WHERE tipo IN ('diario', 'oficial_banxico')
|
|
203
|
+
ORDER BY fecha DESC
|
|
204
|
+
LIMIT 1
|
|
205
|
+
""")
|
|
206
|
+
row = cursor.fetchone()
|
|
207
|
+
db.close()
|
|
208
|
+
|
|
209
|
+
if row:
|
|
210
|
+
return cls._row_to_dict(row)
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def _get_valor_cercano(cls, fecha: str) -> dict | None:
|
|
215
|
+
"""Get UDI value closest to given date"""
|
|
216
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
217
|
+
db.row_factory = sqlite3.Row
|
|
218
|
+
|
|
219
|
+
# Get closest date (before or after)
|
|
220
|
+
cursor = db.execute(
|
|
221
|
+
"""
|
|
222
|
+
SELECT fecha, valor, anio, mes, tipo, moneda, notas,
|
|
223
|
+
ABS(julianday(fecha) - julianday(?)) as diff
|
|
224
|
+
FROM udis
|
|
225
|
+
WHERE tipo IN ('diario', 'oficial_banxico')
|
|
226
|
+
ORDER BY diff
|
|
227
|
+
LIMIT 1
|
|
228
|
+
""",
|
|
229
|
+
(fecha,),
|
|
230
|
+
)
|
|
231
|
+
row = cursor.fetchone()
|
|
232
|
+
db.close()
|
|
233
|
+
|
|
234
|
+
if row:
|
|
235
|
+
return cls._row_to_dict(row)
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
@classmethod
|
|
239
|
+
def pesos_a_udis(cls, pesos: float, fecha: str) -> float | None:
|
|
240
|
+
"""
|
|
241
|
+
Convert Mexican pesos to UDIs
|
|
242
|
+
|
|
243
|
+
:param pesos: Amount in Mexican pesos
|
|
244
|
+
:param fecha: Date string in YYYY-MM-DD format
|
|
245
|
+
:return: Amount in UDIs or None if UDI value not found
|
|
246
|
+
"""
|
|
247
|
+
record = cls.get_por_fecha(fecha)
|
|
248
|
+
if not record:
|
|
249
|
+
record = cls._get_valor_cercano(fecha)
|
|
250
|
+
if not record:
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
valor_udi = record.get("valor")
|
|
254
|
+
if not valor_udi:
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
return pesos / valor_udi
|
|
258
|
+
|
|
259
|
+
@classmethod
|
|
260
|
+
def udis_a_pesos(cls, udis: float, fecha: str) -> float | None:
|
|
261
|
+
"""
|
|
262
|
+
Convert UDIs to Mexican pesos
|
|
263
|
+
|
|
264
|
+
:param udis: Amount in UDIs
|
|
265
|
+
:param fecha: Date string in YYYY-MM-DD format
|
|
266
|
+
:return: Amount in Mexican pesos or None if UDI value not found
|
|
267
|
+
"""
|
|
268
|
+
record = cls.get_por_fecha(fecha)
|
|
269
|
+
if not record:
|
|
270
|
+
record = cls._get_valor_cercano(fecha)
|
|
271
|
+
if not record:
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
valor_udi = record.get("valor")
|
|
275
|
+
if not valor_udi:
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
return udis * valor_udi
|
|
279
|
+
|
|
280
|
+
@classmethod
|
|
281
|
+
def calcular_variacion(cls, fecha_inicio: str, fecha_fin: str) -> float | None:
|
|
282
|
+
"""
|
|
283
|
+
Calculate percentage variation between two dates
|
|
284
|
+
|
|
285
|
+
:param fecha_inicio: Start date (YYYY-MM-DD)
|
|
286
|
+
:param fecha_fin: End date (YYYY-MM-DD)
|
|
287
|
+
:return: Percentage variation or None if values not found
|
|
288
|
+
"""
|
|
289
|
+
record_inicio = cls.get_por_fecha(fecha_inicio) or cls._get_valor_cercano(fecha_inicio)
|
|
290
|
+
record_fin = cls.get_por_fecha(fecha_fin) or cls._get_valor_cercano(fecha_fin)
|
|
291
|
+
|
|
292
|
+
if not record_inicio or not record_fin:
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
valor_inicio = record_inicio.get("valor")
|
|
296
|
+
valor_fin = record_fin.get("valor")
|
|
297
|
+
|
|
298
|
+
if not valor_inicio or not valor_fin:
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
return ((valor_fin - valor_inicio) / valor_inicio) * 100
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# Convenience functions
|
|
305
|
+
def get_udi_actual() -> dict | None:
|
|
306
|
+
"""Get most recent UDI value"""
|
|
307
|
+
return UDICatalog.get_actual()
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def get_udi_por_fecha(fecha: str) -> dict | None:
|
|
311
|
+
"""Get UDI value for a specific date"""
|
|
312
|
+
return UDICatalog.get_por_fecha(fecha)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def pesos_a_udis(pesos: float, fecha: str) -> float | None:
|
|
316
|
+
"""Convert pesos to UDIs"""
|
|
317
|
+
return UDICatalog.pesos_a_udis(pesos, fecha)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def udis_a_pesos(udis: float, fecha: str) -> float | None:
|
|
321
|
+
"""Convert UDIs to pesos"""
|
|
322
|
+
return UDICatalog.udis_a_pesos(udis, fecha)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# Export commonly used functions and classes
|
|
326
|
+
__all__ = [
|
|
327
|
+
"UDICatalog",
|
|
328
|
+
"get_udi_actual",
|
|
329
|
+
"get_udi_por_fecha",
|
|
330
|
+
"pesos_a_udis",
|
|
331
|
+
"udis_a_pesos",
|
|
332
|
+
]
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Catálogo de Sectores CNBV - Entidades reguladas por la Comisión Nacional Bancaria y de Valores.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TypedDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SectorCNBV(TypedDict, total=False):
|
|
13
|
+
"""Estructura de un sector CNBV."""
|
|
14
|
+
|
|
15
|
+
id: str
|
|
16
|
+
nombre: str
|
|
17
|
+
nombre_corto: str
|
|
18
|
+
descripcion: str
|
|
19
|
+
ley_aplicable: str
|
|
20
|
+
regulador_principal: str
|
|
21
|
+
supervisores_adicionales: list[str]
|
|
22
|
+
ejemplos: list[str]
|
|
23
|
+
subtipos: list[dict]
|
|
24
|
+
caracteristicas: str
|
|
25
|
+
nota: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Regulador(TypedDict):
|
|
29
|
+
"""Estructura de un regulador financiero."""
|
|
30
|
+
|
|
31
|
+
id: str
|
|
32
|
+
nombre: str
|
|
33
|
+
siglas: str
|
|
34
|
+
funcion: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SectoresCNBV:
|
|
38
|
+
"""Catálogo de sectores financieros regulados por la CNBV."""
|
|
39
|
+
|
|
40
|
+
_data: dict | None = None
|
|
41
|
+
_sectores: list[SectorCNBV] | None = None
|
|
42
|
+
_by_id: dict[str, SectorCNBV] | None = None
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def _load_data(cls) -> None:
|
|
46
|
+
"""Carga lazy de los datos del catálogo."""
|
|
47
|
+
if cls._data is not None:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
data_path = (
|
|
51
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
52
|
+
/ "shared-data"
|
|
53
|
+
/ "cnbv"
|
|
54
|
+
/ "sectores.json"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
with open(data_path, encoding="utf-8") as f:
|
|
58
|
+
cls._data = json.load(f)
|
|
59
|
+
|
|
60
|
+
cls._sectores = cls._data.get("sectores", [])
|
|
61
|
+
cls._by_id = {s["id"]: s for s in cls._sectores}
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def get_all(cls) -> list[SectorCNBV]:
|
|
65
|
+
"""Obtiene todos los sectores CNBV."""
|
|
66
|
+
cls._load_data()
|
|
67
|
+
return cls._sectores.copy() if cls._sectores else []
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def get_by_id(cls, sector_id: str) -> SectorCNBV | None:
|
|
71
|
+
"""Obtiene un sector por su ID."""
|
|
72
|
+
cls._load_data()
|
|
73
|
+
return cls._by_id.get(sector_id) if cls._by_id else None
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def is_valid(cls, sector_id: str) -> bool:
|
|
77
|
+
"""Valida si un ID de sector existe."""
|
|
78
|
+
return cls.get_by_id(sector_id) is not None
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def search(cls, query: str) -> list[SectorCNBV]:
|
|
82
|
+
"""Busca sectores por nombre o descripción."""
|
|
83
|
+
cls._load_data()
|
|
84
|
+
if not cls._sectores:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
query_lower = query.lower()
|
|
88
|
+
results = []
|
|
89
|
+
|
|
90
|
+
for sector in cls._sectores:
|
|
91
|
+
if (
|
|
92
|
+
query_lower in sector.get("nombre", "").lower()
|
|
93
|
+
or query_lower in sector.get("nombre_corto", "").lower()
|
|
94
|
+
or query_lower in sector.get("descripcion", "").lower()
|
|
95
|
+
):
|
|
96
|
+
results.append(sector)
|
|
97
|
+
|
|
98
|
+
return results
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def get_by_regulador(cls, regulador: str) -> list[SectorCNBV]:
|
|
102
|
+
"""Obtiene sectores por regulador principal."""
|
|
103
|
+
cls._load_data()
|
|
104
|
+
if not cls._sectores:
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
regulador_upper = regulador.upper()
|
|
108
|
+
return [
|
|
109
|
+
s for s in cls._sectores if s.get("regulador_principal", "").upper() == regulador_upper
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def count(cls) -> int:
|
|
114
|
+
"""Retorna el número total de sectores."""
|
|
115
|
+
cls._load_data()
|
|
116
|
+
return len(cls._sectores) if cls._sectores else 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ReguladorFinanciero:
|
|
120
|
+
"""Catálogo de reguladores del sistema financiero mexicano."""
|
|
121
|
+
|
|
122
|
+
_reguladores: list[Regulador] | None = None
|
|
123
|
+
_by_id: dict[str, Regulador] | None = None
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def _load_data(cls) -> None:
|
|
127
|
+
"""Carga lazy de los datos del catálogo."""
|
|
128
|
+
if cls._reguladores is not None:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
data_path = (
|
|
132
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
133
|
+
/ "shared-data"
|
|
134
|
+
/ "cnbv"
|
|
135
|
+
/ "sectores.json"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
with open(data_path, encoding="utf-8") as f:
|
|
139
|
+
data = json.load(f)
|
|
140
|
+
|
|
141
|
+
cls._reguladores = data.get("reguladores", [])
|
|
142
|
+
cls._by_id = {r["id"]: r for r in cls._reguladores}
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def get_all(cls) -> list[Regulador]:
|
|
146
|
+
"""Obtiene todos los reguladores."""
|
|
147
|
+
cls._load_data()
|
|
148
|
+
return cls._reguladores.copy() if cls._reguladores else []
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def get_by_id(cls, regulador_id: str) -> Regulador | None:
|
|
152
|
+
"""Obtiene un regulador por su ID."""
|
|
153
|
+
cls._load_data()
|
|
154
|
+
return cls._by_id.get(regulador_id) if cls._by_id else None
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def get_by_siglas(cls, siglas: str) -> Regulador | None:
|
|
158
|
+
"""Obtiene un regulador por sus siglas."""
|
|
159
|
+
cls._load_data()
|
|
160
|
+
if not cls._reguladores:
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
siglas_upper = siglas.upper()
|
|
164
|
+
for reg in cls._reguladores:
|
|
165
|
+
if reg.get("siglas", "").upper() == siglas_upper:
|
|
166
|
+
return reg
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
@classmethod
|
|
170
|
+
def count(cls) -> int:
|
|
171
|
+
"""Retorna el número total de reguladores."""
|
|
172
|
+
cls._load_data()
|
|
173
|
+
return len(cls._reguladores) if cls._reguladores else 0
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
catalogmx.catalogs.conapo - Catálogos de CONAPO
|
|
3
|
+
|
|
4
|
+
Catálogos del Consejo Nacional de Población:
|
|
5
|
+
- ZonasMetropolitanasCatalog: Metrópolis de México 2020 (SEDATU/INEGI/CONAPO)
|
|
6
|
+
- SistemaUrbanoNacionalCatalog: Sistema Urbano Nacional 2020
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .sistema_urbano_nacional import SistemaUrbanoNacionalCatalog
|
|
10
|
+
from .zonas_metropolitanas import ZonasMetropolitanasCatalog
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"ZonasMetropolitanasCatalog",
|
|
14
|
+
"SistemaUrbanoNacionalCatalog",
|
|
15
|
+
]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Sistema Urbano Nacional 2020
|
|
3
|
+
|
|
4
|
+
Fuente: CONAPO
|
|
5
|
+
https://www.datos.gob.mx/dataset/sistema_urbano_nacional
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import csv
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SistemaUrbanoNacionalCatalog:
|
|
13
|
+
"""Catálogo del Sistema Urbano Nacional 2020"""
|
|
14
|
+
|
|
15
|
+
_data: list[dict] | None = None
|
|
16
|
+
_by_clave: dict[str, dict] | None = None
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def _load_data(cls) -> None:
|
|
20
|
+
"""Carga los datos del catálogo"""
|
|
21
|
+
if cls._data is not None:
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
data_path = Path(__file__).resolve().parents[4] / "shared-data" / "conapo" / "sun_2020.csv"
|
|
25
|
+
|
|
26
|
+
cls._data = []
|
|
27
|
+
cls._by_clave = {}
|
|
28
|
+
|
|
29
|
+
with open(data_path, encoding="utf-8") as f:
|
|
30
|
+
reader = csv.DictReader(f)
|
|
31
|
+
for row in reader:
|
|
32
|
+
record = {
|
|
33
|
+
"cve_ciudad": row["cve_cd"],
|
|
34
|
+
"nombre": row["nom_cd"],
|
|
35
|
+
"poblacion_2020": int(row["pob_2020"]),
|
|
36
|
+
}
|
|
37
|
+
cls._data.append(record)
|
|
38
|
+
cls._by_clave[row["cve_cd"]] = record
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def get_all(cls) -> list[dict]:
|
|
42
|
+
"""Obtiene todas las ciudades del SUN"""
|
|
43
|
+
cls._load_data()
|
|
44
|
+
return cls._data.copy() if cls._data else []
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def get_por_clave(cls, cve_ciudad: str) -> dict | None:
|
|
48
|
+
"""Obtiene ciudad por clave"""
|
|
49
|
+
cls._load_data()
|
|
50
|
+
return cls._by_clave.get(cve_ciudad) if cls._by_clave else None
|