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
@@ -0,0 +1,386 @@
1
+ """
2
+ Catálogo de monedas y divisas internacionales (Banxico)
3
+
4
+ Este módulo proporciona acceso al catálogo de monedas y divisas
5
+ internacionales utilizadas en operaciones cambiarias en México,
6
+ basado en códigos ISO 4217.
7
+ """
8
+
9
+ import json
10
+ from pathlib import Path
11
+ from typing import TypedDict
12
+
13
+
14
+ class MonedaDivisa(TypedDict, total=False):
15
+ """Estructura de una moneda o divisa"""
16
+
17
+ codigo_iso: str
18
+ numero_iso: str
19
+ moneda: str
20
+ pais: str
21
+ simbolo: str
22
+ decimales: int
23
+ moneda_nacional: bool
24
+ tipo_cambio_banxico: bool
25
+ tipo_cambio_fix: bool # Optional
26
+ activa: bool
27
+ notas: str # Optional
28
+
29
+
30
+ class MonedasDivisas:
31
+ """
32
+ Catálogo de monedas y divisas internacionales.
33
+
34
+ Incluye todas las monedas con las que Banco de México publica
35
+ tipos de cambio, más otras monedas internacionales relevantes.
36
+
37
+ Características:
38
+ - Códigos ISO 4217 (código y número)
39
+ - Símbolos y decimales de cada moneda
40
+ - Indicación de tipo de cambio publicado por Banxico
41
+ - Indicación de tipo de cambio FIX
42
+ - Agrupación por regiones geográficas
43
+ - Formateo de montos en cada moneda
44
+
45
+ Ejemplo:
46
+ >>> from catalogmx.catalogs.banxico import MonedasDivisas
47
+ >>>
48
+ >>> # Obtener información del dólar
49
+ >>> usd = MonedasDivisas.get_por_codigo("USD")
50
+ >>> print(f"{usd['moneda']}: {usd['simbolo']}")
51
+ >>>
52
+ >>> # Obtener monedas con tipo de cambio Banxico
53
+ >>> con_tc = MonedasDivisas.get_con_tipo_cambio_banxico()
54
+ >>> print(f"Monedas con TC Banxico: {len(con_tc)}")
55
+ >>>
56
+ >>> # Formatear monto
57
+ >>> formatted = MonedasDivisas.formatear_monto(1234.56, "USD")
58
+ >>> print(formatted) # "US$ 1,234.56"
59
+ """
60
+
61
+ _data: list[MonedaDivisa] | None = None
62
+
63
+ @classmethod
64
+ def _load_data(cls) -> None:
65
+ """Carga lazy de datos desde JSON"""
66
+ if cls._data is not None:
67
+ return
68
+
69
+ # Path: catalogmx/packages/python/catalogmx/catalogs/banxico/monedas_divisas.py
70
+ # Target: catalogmx/packages/shared-data/banxico/monedas_divisas.json
71
+ data_path = (
72
+ Path(__file__).parent.parent.parent.parent.parent
73
+ / "shared-data"
74
+ / "banxico"
75
+ / "monedas_divisas.json"
76
+ )
77
+
78
+ with open(data_path, encoding="utf-8") as f:
79
+ json_data = json.load(f)
80
+ cls._data = json_data["monedas"]
81
+
82
+ @classmethod
83
+ def get_all(cls) -> list[MonedaDivisa]:
84
+ """
85
+ Obtiene todas las monedas.
86
+
87
+ Returns:
88
+ Lista completa de monedas y divisas
89
+
90
+ Ejemplo:
91
+ >>> monedas = MonedasDivisas.get_all()
92
+ >>> print(f"Total monedas: {len(monedas)}")
93
+ """
94
+ cls._load_data()
95
+ return cls._data.copy() # type: ignore
96
+
97
+ @classmethod
98
+ def get_por_codigo(cls, codigo_iso: str) -> MonedaDivisa | None:
99
+ """
100
+ Busca moneda por código ISO.
101
+
102
+ Args:
103
+ codigo_iso: Código ISO 4217 de la moneda (ej: "USD", "EUR", "MXN")
104
+
105
+ Returns:
106
+ Información de la moneda o None si no existe
107
+
108
+ Ejemplo:
109
+ >>> usd = MonedasDivisas.get_por_codigo("USD")
110
+ >>> print(usd['moneda']) # "Dólar Estadounidense"
111
+ """
112
+ cls._load_data()
113
+ codigo_upper = codigo_iso.upper()
114
+ for moneda in cls._data: # type: ignore
115
+ if moneda["codigo_iso"].upper() == codigo_upper:
116
+ return moneda
117
+ return None
118
+
119
+ @classmethod
120
+ def get_por_pais(cls, pais: str) -> list[MonedaDivisa]:
121
+ """
122
+ Busca monedas por país.
123
+
124
+ Args:
125
+ pais: Nombre del país (búsqueda parcial, case-insensitive)
126
+
127
+ Returns:
128
+ Lista de monedas del país
129
+
130
+ Ejemplo:
131
+ >>> mexico = MonedasDivisas.get_por_pais("México")
132
+ >>> for m in mexico:
133
+ ... print(f"{m['moneda']} ({m['codigo_iso']})")
134
+ """
135
+ cls._load_data()
136
+ pais_lower = pais.lower()
137
+ return [m for m in cls._data if pais_lower in m["pais"].lower()] # type: ignore
138
+
139
+ @classmethod
140
+ def get_con_tipo_cambio_banxico(cls) -> list[MonedaDivisa]:
141
+ """
142
+ Obtiene monedas con tipo de cambio publicado por Banxico.
143
+
144
+ Returns:
145
+ Lista de monedas con tipo de cambio Banxico
146
+
147
+ Ejemplo:
148
+ >>> con_tc = MonedasDivisas.get_con_tipo_cambio_banxico()
149
+ >>> for m in con_tc:
150
+ ... print(f"{m['codigo_iso']}: {m['moneda']}")
151
+ """
152
+ cls._load_data()
153
+ return [m for m in cls._data if m["tipo_cambio_banxico"]] # type: ignore
154
+
155
+ @classmethod
156
+ def get_con_tipo_cambio_fix(cls) -> list[MonedaDivisa]:
157
+ """
158
+ Obtiene monedas con tipo de cambio FIX.
159
+
160
+ Returns:
161
+ Lista de monedas con tipo de cambio FIX
162
+
163
+ Ejemplo:
164
+ >>> fix = MonedasDivisas.get_con_tipo_cambio_fix()
165
+ >>> for m in fix:
166
+ ... print(f"{m['codigo_iso']}: {m['moneda']}")
167
+ """
168
+ cls._load_data()
169
+ return [m for m in cls._data if m.get("tipo_cambio_fix", False)] # type: ignore
170
+
171
+ @classmethod
172
+ def get_por_region(cls, region: str) -> list[MonedaDivisa]:
173
+ """
174
+ Obtiene monedas de una región específica.
175
+
176
+ Regiones soportadas:
177
+ - America del Norte
178
+ - America Latina
179
+ - Europa
180
+ - Asia-Pacifico
181
+ - Africa
182
+
183
+ Args:
184
+ region: Nombre de la región
185
+
186
+ Returns:
187
+ Lista de monedas de esa región
188
+
189
+ Ejemplo:
190
+ >>> latam = MonedasDivisas.get_por_region("America Latina")
191
+ >>> for m in latam:
192
+ ... print(f"{m['codigo_iso']}: {m['pais']}")
193
+ """
194
+ cls._load_data()
195
+
196
+ regiones: dict[str, list[str]] = {
197
+ "America del Norte": ["USD", "CAD", "MXN"],
198
+ "America Latina": ["ARS", "BRL", "CLP", "COP", "PEN", "GTQ", "CRC", "UYU", "VES"],
199
+ "Europa": ["EUR", "GBP", "CHF", "SEK", "NOK", "DKK", "RUB"],
200
+ "Asia-Pacifico": ["JPY", "CNY", "AUD", "NZD", "SGD", "HKD", "INR", "KRW"],
201
+ "Africa": ["ZAR"],
202
+ }
203
+
204
+ codigos = regiones.get(region, [])
205
+ return [m for m in cls._data if m["codigo_iso"] in codigos] # type: ignore
206
+
207
+ @classmethod
208
+ def get_principales(cls) -> list[MonedaDivisa]:
209
+ """
210
+ Obtiene monedas principales para operaciones en México.
211
+
212
+ Returns:
213
+ Lista de monedas principales (MXN, USD, EUR, CAD, GBP, JPY, CHF)
214
+
215
+ Ejemplo:
216
+ >>> principales = MonedasDivisas.get_principales()
217
+ >>> for m in principales:
218
+ ... print(f"{m['codigo_iso']}: {m['moneda']}")
219
+ """
220
+ cls._load_data()
221
+ principales = ["MXN", "USD", "EUR", "CAD", "GBP", "JPY", "CHF"]
222
+ return [m for m in cls._data if m["codigo_iso"] in principales] # type: ignore
223
+
224
+ @classmethod
225
+ def get_latam(cls) -> list[MonedaDivisa]:
226
+ """
227
+ Obtiene monedas latinoamericanas.
228
+
229
+ Returns:
230
+ Lista de monedas de América Latina
231
+
232
+ Ejemplo:
233
+ >>> latam = MonedasDivisas.get_latam()
234
+ >>> for m in latam:
235
+ ... print(f"{m['codigo_iso']}: {m['pais']}")
236
+ """
237
+ cls._load_data()
238
+ latam = ["MXN", "ARS", "BRL", "CLP", "COP", "PEN", "GTQ", "CRC", "UYU", "VES"]
239
+ return [m for m in cls._data if m["codigo_iso"] in latam] # type: ignore
240
+
241
+ @classmethod
242
+ def validar_codigo_iso(cls, codigo: str) -> bool:
243
+ """
244
+ Valida código ISO de moneda.
245
+
246
+ Args:
247
+ codigo: Código ISO a validar
248
+
249
+ Returns:
250
+ True si el código existe, False en caso contrario
251
+
252
+ Ejemplo:
253
+ >>> MonedasDivisas.validar_codigo_iso("USD") # True
254
+ >>> MonedasDivisas.validar_codigo_iso("XXX") # False
255
+ """
256
+ cls._load_data()
257
+ codigo_upper = codigo.upper()
258
+ return any(m["codigo_iso"].upper() == codigo_upper for m in cls._data) # type: ignore
259
+
260
+ @classmethod
261
+ def get_formato_moneda(cls, codigo_iso: str) -> dict[str, str | int] | None:
262
+ """
263
+ Obtiene información de formato de moneda.
264
+
265
+ Args:
266
+ codigo_iso: Código ISO de la moneda
267
+
268
+ Returns:
269
+ Diccionario con símbolo, decimales y formato de ejemplo
270
+
271
+ Ejemplo:
272
+ >>> formato = MonedasDivisas.get_formato_moneda("USD")
273
+ >>> print(formato['formato_ejemplo']) # "US$ 1234.56"
274
+ """
275
+ moneda = cls.get_por_codigo(codigo_iso)
276
+ if not moneda:
277
+ return None
278
+
279
+ ejemplo_monto = 1234.56
280
+ if moneda["decimales"] == 0:
281
+ monto_formateado = str(round(ejemplo_monto))
282
+ else:
283
+ monto_formateado = f"{ejemplo_monto:.{moneda['decimales']}f}"
284
+
285
+ return {
286
+ "simbolo": moneda["simbolo"],
287
+ "decimales": moneda["decimales"],
288
+ "formato_ejemplo": f"{moneda['simbolo']} {monto_formateado}",
289
+ }
290
+
291
+ @classmethod
292
+ def formatear_monto(cls, monto: float, codigo_iso: str) -> str:
293
+ """
294
+ Formatea monto en una moneda específica.
295
+
296
+ Args:
297
+ monto: Monto a formatear
298
+ codigo_iso: Código ISO de la moneda
299
+
300
+ Returns:
301
+ Monto formateado con símbolo de moneda
302
+
303
+ Ejemplo:
304
+ >>> MonedasDivisas.formatear_monto(1234.56, "USD")
305
+ "US$ 1,234.56"
306
+ >>> MonedasDivisas.formatear_monto(1234.56, "JPY")
307
+ "¥ 1,235"
308
+ """
309
+ moneda = cls.get_por_codigo(codigo_iso)
310
+ if not moneda:
311
+ return str(monto)
312
+
313
+ if moneda["decimales"] == 0:
314
+ monto_formateado = f"{round(monto):,.0f}".replace(",", " ")
315
+ else:
316
+ monto_formateado = f"{monto:,.{moneda['decimales']}f}".replace(",", " ")
317
+
318
+ return f"{moneda['simbolo']} {monto_formateado}"
319
+
320
+ @classmethod
321
+ def get_mxn(cls) -> MonedaDivisa | None:
322
+ """Obtiene peso mexicano (MXN)."""
323
+ return cls.get_por_codigo("MXN")
324
+
325
+ @classmethod
326
+ def get_usd(cls) -> MonedaDivisa | None:
327
+ """Obtiene dólar estadounidense (USD)."""
328
+ return cls.get_por_codigo("USD")
329
+
330
+ @classmethod
331
+ def get_eur(cls) -> MonedaDivisa | None:
332
+ """Obtiene euro (EUR)."""
333
+ return cls.get_por_codigo("EUR")
334
+
335
+ @classmethod
336
+ def buscar_por_nombre(cls, nombre: str) -> list[MonedaDivisa]:
337
+ """
338
+ Busca monedas por nombre.
339
+
340
+ Args:
341
+ nombre: Texto a buscar en el nombre de la moneda
342
+
343
+ Returns:
344
+ Lista de monedas que coinciden
345
+
346
+ Ejemplo:
347
+ >>> dolares = MonedasDivisas.buscar_por_nombre("dólar")
348
+ >>> for m in dolares:
349
+ ... print(f"{m['codigo_iso']}: {m['moneda']}")
350
+ """
351
+ cls._load_data()
352
+ nombre_lower = nombre.lower()
353
+ return [m for m in cls._data if nombre_lower in m["moneda"].lower()] # type: ignore
354
+
355
+ @classmethod
356
+ def get_activas(cls) -> list[MonedaDivisa]:
357
+ """
358
+ Obtiene monedas activas.
359
+
360
+ Returns:
361
+ Lista de monedas activas
362
+
363
+ Ejemplo:
364
+ >>> activas = MonedasDivisas.get_activas()
365
+ >>> print(f"Monedas activas: {len(activas)}")
366
+ """
367
+ cls._load_data()
368
+ return [m for m in cls._data if m["activa"]] # type: ignore
369
+
370
+ @classmethod
371
+ def get_info_tipo_cambio_fix(cls) -> dict[str, str]:
372
+ """
373
+ Información del tipo de cambio FIX.
374
+
375
+ Returns:
376
+ Diccionario con información del tipo de cambio FIX
377
+
378
+ Ejemplo:
379
+ >>> info = MonedasDivisas.get_info_tipo_cambio_fix()
380
+ >>> print(info['horario'])
381
+ """
382
+ return {
383
+ "descripcion": "Tipo de cambio FIX determinado por Banco de México - Promedio ponderado de cotizaciones del mercado de cambios al mayoreo",
384
+ "horario": "12:00 hrs (mediodía) tiempo de la Ciudad de México",
385
+ "uso": "Referencia oficial para liquidación de obligaciones denominadas en dólares",
386
+ }
@@ -0,0 +1,279 @@
1
+ """
2
+ UDI (Unidades de Inversión) Catalog
3
+
4
+ This module provides access to UDI values from Banco de México.
5
+ UDIs are inflation-indexed investment units used in Mexico.
6
+ """
7
+
8
+ import json
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+
12
+
13
+ class UDICatalog:
14
+ """
15
+ Catalog of UDI (Unidades de Inversión) values
16
+
17
+ UDIs are inflation-indexed investment units maintained by Banco de México.
18
+ They are commonly used for mortgage loans and other long-term financial obligations.
19
+ """
20
+
21
+ _data: list[dict] | None = None
22
+ _by_fecha: dict[str, dict] | None = None
23
+ _mensual: dict[str, dict] | None = None
24
+ _anual: dict[int, dict] | None = None
25
+ _daily: list[dict] | None = None
26
+
27
+ @classmethod
28
+ def _load_data(cls) -> None:
29
+ """Load UDI data from JSON file"""
30
+ if cls._data is None:
31
+ # Path: catalogmx/packages/python/catalogmx/catalogs/banxico/udis.py
32
+ # Target: catalogmx/packages/shared-data/banxico/udis.json
33
+ current_file = Path(__file__)
34
+ shared_data_path = (
35
+ current_file.parent.parent.parent.parent.parent
36
+ / "shared-data"
37
+ / "banxico"
38
+ / "udis.json"
39
+ )
40
+
41
+ with open(shared_data_path, encoding="utf-8") as f:
42
+ cls._data = json.load(f)
43
+
44
+ if cls._by_fecha is not None:
45
+ return
46
+
47
+ cls._by_fecha = {}
48
+ cls._mensual = {}
49
+ cls._anual = {}
50
+ daily: list[dict] = []
51
+
52
+ for record in cls._data:
53
+ fecha = record.get("fecha")
54
+ if not fecha:
55
+ continue
56
+
57
+ existing = cls._by_fecha.get(fecha)
58
+ if existing is None or (
59
+ record.get("tipo") == "diario" and existing.get("tipo") != "diario"
60
+ ):
61
+ cls._by_fecha[fecha] = record
62
+
63
+ if record.get("tipo") == "diario":
64
+ daily.append(record)
65
+ elif record.get("tipo") == "promedio_mensual":
66
+ key = f"{record.get('año')}-{int(record.get('mes', 0)):02d}"
67
+ cls._mensual[key] = record
68
+ elif record.get("tipo") == "promedio_anual":
69
+ cls._anual[int(record.get("año"))] = record
70
+
71
+ daily.sort(key=lambda r: r["fecha"])
72
+ cls._daily = daily
73
+
74
+ @classmethod
75
+ def get_data(cls) -> list[dict]:
76
+ """
77
+ Get all UDI data
78
+
79
+ :return: List of all UDI records
80
+ """
81
+ cls._load_data()
82
+ return cls._data.copy()
83
+
84
+ @classmethod
85
+ def _get_by_fecha(cls, fecha: str) -> dict | None:
86
+ cls._load_data()
87
+ record = cls._by_fecha.get(fecha)
88
+ if record:
89
+ return record
90
+
91
+ try:
92
+ anio, mes, _dia = fecha.split("-")
93
+ mensual = cls._mensual.get(f"{int(anio)}-{int(mes):02d}") if cls._mensual else None
94
+ return mensual
95
+ except ValueError:
96
+ return None
97
+
98
+ @classmethod
99
+ def get_por_fecha(cls, fecha: str) -> dict | None:
100
+ """
101
+ Get UDI value for a specific date
102
+
103
+ :param fecha: Date string in YYYY-MM-DD format
104
+ :return: UDI record or None if not found
105
+ """
106
+ record = cls._get_by_fecha(fecha)
107
+ return record.copy() if record else None
108
+
109
+ @classmethod
110
+ def get_por_mes(cls, anio: int, mes: int) -> dict | None:
111
+ """
112
+ Get monthly average UDI value
113
+
114
+ :param anio: Year (e.g., 2024)
115
+ :param mes: Month (1-12)
116
+ :return: UDI record with monthly average or None if not found
117
+ """
118
+ cls._load_data()
119
+ key = f"{anio}-{mes:02d}"
120
+ record = cls._mensual.get(key) if cls._mensual else None
121
+ return record.copy() if record else None
122
+
123
+ @classmethod
124
+ def get_promedio_anual(cls, anio: int) -> dict | None:
125
+ """
126
+ Get annual average UDI value
127
+
128
+ :param anio: Year (e.g., 2024)
129
+ :return: UDI record with annual average or None if not found
130
+ """
131
+ cls._load_data()
132
+
133
+ record = cls._anual.get(anio) if cls._anual else None
134
+ return record.copy() if record else None
135
+
136
+ @classmethod
137
+ def get_por_anio(cls, anio: int) -> list[dict]:
138
+ """Return the daily UDI series for a given year."""
139
+ cls._load_data()
140
+
141
+ source = (
142
+ cls._daily
143
+ if cls._daily
144
+ else [r for r in cls._data if r.get("tipo") == "promedio_mensual"]
145
+ )
146
+ return [record.copy() for record in source if record.get("año") == anio]
147
+
148
+ @classmethod
149
+ def get_actual(cls) -> dict | None:
150
+ """
151
+ Get most recent UDI value
152
+
153
+ :return: Latest UDI record
154
+ """
155
+ cls._load_data()
156
+
157
+ if cls._daily:
158
+ return cls._daily[-1].copy()
159
+
160
+ if not cls._data:
161
+ return None
162
+
163
+ record = max(cls._data, key=lambda r: r.get("fecha", ""), default=None)
164
+ return record.copy() if record else None
165
+
166
+ @classmethod
167
+ def _get_valor_cercano(cls, fecha: str) -> dict | None:
168
+ cls._load_data()
169
+ objetivo = datetime.fromisoformat(fecha)
170
+
171
+ candidatos = (
172
+ cls._daily
173
+ if cls._daily
174
+ else [r for r in cls._data if r.get("tipo") == "promedio_mensual"]
175
+ )
176
+ if not candidatos:
177
+ return None
178
+
179
+ def _diff(record: dict) -> float:
180
+ return abs((datetime.fromisoformat(record["fecha"]) - objetivo).total_seconds())
181
+
182
+ return min(candidatos, key=_diff)
183
+
184
+ @classmethod
185
+ def pesos_a_udis(cls, pesos: float, fecha: str) -> float | None:
186
+ """
187
+ Convert Mexican pesos to UDIs
188
+
189
+ :param pesos: Amount in Mexican pesos
190
+ :param fecha: Date string in YYYY-MM-DD format
191
+ :return: Amount in UDIs or None if UDI value not found
192
+ """
193
+ record = cls.get_por_fecha(fecha)
194
+ if not record:
195
+ record = cls._get_valor_cercano(fecha)
196
+ if not record:
197
+ return None
198
+
199
+ valor_udi = record.get("valor")
200
+ if not valor_udi:
201
+ return None
202
+
203
+ return pesos / valor_udi
204
+
205
+ @classmethod
206
+ def udis_a_pesos(cls, udis: float, fecha: str) -> float | None:
207
+ """
208
+ Convert UDIs to Mexican pesos
209
+
210
+ :param udis: Amount in UDIs
211
+ :param fecha: Date string in YYYY-MM-DD format
212
+ :return: Amount in Mexican pesos or None if UDI value not found
213
+ """
214
+ record = cls.get_por_fecha(fecha)
215
+ if not record:
216
+ record = cls._get_valor_cercano(fecha)
217
+ if not record:
218
+ return None
219
+
220
+ valor_udi = record.get("valor")
221
+ if not valor_udi:
222
+ return None
223
+
224
+ return udis * valor_udi
225
+
226
+ @classmethod
227
+ def calcular_variacion(cls, fecha_inicio: str, fecha_fin: str) -> float | None:
228
+ """
229
+ Calculate percentage variation between two dates
230
+
231
+ :param fecha_inicio: Start date (YYYY-MM-DD)
232
+ :param fecha_fin: End date (YYYY-MM-DD)
233
+ :return: Percentage variation or None if values not found
234
+ """
235
+ record_inicio = cls.get_por_fecha(fecha_inicio) or cls._get_valor_cercano(fecha_inicio)
236
+ record_fin = cls.get_por_fecha(fecha_fin) or cls._get_valor_cercano(fecha_fin)
237
+
238
+ if not record_inicio or not record_fin:
239
+ return None
240
+
241
+ valor_inicio = record_inicio.get("valor")
242
+ valor_fin = record_fin.get("valor")
243
+
244
+ if not valor_inicio or not valor_fin:
245
+ return None
246
+
247
+ return ((valor_fin - valor_inicio) / valor_inicio) * 100
248
+
249
+
250
+ # Convenience functions
251
+ def get_udi_actual() -> dict | None:
252
+ """Get most recent UDI value"""
253
+ record = UDICatalog.get_actual()
254
+ return record.copy() if record else None
255
+
256
+
257
+ def get_udi_por_fecha(fecha: str) -> dict | None:
258
+ """Get UDI value for a specific date"""
259
+ return UDICatalog.get_por_fecha(fecha)
260
+
261
+
262
+ def pesos_a_udis(pesos: float, fecha: str) -> float | None:
263
+ """Convert pesos to UDIs"""
264
+ return UDICatalog.pesos_a_udis(pesos, fecha)
265
+
266
+
267
+ def udis_a_pesos(udis: float, fecha: str) -> float | None:
268
+ """Convert UDIs to pesos"""
269
+ return UDICatalog.udis_a_pesos(udis, fecha)
270
+
271
+
272
+ # Export commonly used functions and classes
273
+ __all__ = [
274
+ "UDICatalog",
275
+ "get_udi_actual",
276
+ "get_udi_por_fecha",
277
+ "pesos_a_udis",
278
+ "udis_a_pesos",
279
+ ]
@@ -0,0 +1,15 @@
1
+ """
2
+ catalogmx.catalogs.ift - Catálogos del IFT
3
+
4
+ Catálogos del Instituto Federal de Telecomunicaciones:
5
+ - CodigosLADACatalog: Plan de numeración telefónica (códigos LADA)
6
+ - OperadoresMovilesCatalog: Operadores de telefonía móvil
7
+ """
8
+
9
+ from .codigos_lada import CodigosLADACatalog
10
+ from .operadores_moviles import OperadoresMovilesCatalog
11
+
12
+ __all__ = [
13
+ "CodigosLADACatalog",
14
+ "OperadoresMovilesCatalog",
15
+ ]