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,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TIIE (Tasa de Interés Interbancaria de Equilibrio) Catalog - SQLite Backend
|
|
3
|
+
|
|
4
|
+
This module provides access to TIIE rates from Banco de México using a SQLite
|
|
5
|
+
database that can be automatically updated without requiring library releases.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sqlite3
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from catalogmx.data.updater import get_database_path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TIIECatalog:
|
|
15
|
+
"""
|
|
16
|
+
Catalog of TIIE (Tasa de Interés Interbancaria de Equilibrio) values
|
|
17
|
+
|
|
18
|
+
TIIE is the equilibrium interbank interest rate used as a reference
|
|
19
|
+
for variable rate loans in Mexico.
|
|
20
|
+
|
|
21
|
+
This catalog uses an auto-updating SQLite database backend.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
_db_path: Path | None = None
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def _get_db_path(cls) -> Path:
|
|
28
|
+
"""Get path to database with auto-update"""
|
|
29
|
+
if cls._db_path is None:
|
|
30
|
+
cls._db_path = get_database_path(auto_update=True, max_age_hours=24)
|
|
31
|
+
return cls._db_path
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def _row_to_dict(cls, row: sqlite3.Row) -> dict:
|
|
35
|
+
"""Convert SQLite row to dictionary"""
|
|
36
|
+
return {
|
|
37
|
+
"fecha": row["fecha"],
|
|
38
|
+
"plazo": row["plazo"],
|
|
39
|
+
"tasa": row["tasa"],
|
|
40
|
+
"año": row["anio"],
|
|
41
|
+
"mes": row["mes"],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def get_data(cls, plazo: int = 28) -> list[dict]:
|
|
46
|
+
"""
|
|
47
|
+
Get all TIIE data for a specific term
|
|
48
|
+
|
|
49
|
+
:param plazo: Term in days (28, 91, or 182)
|
|
50
|
+
:return: List of all TIIE records
|
|
51
|
+
"""
|
|
52
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
53
|
+
db.row_factory = sqlite3.Row
|
|
54
|
+
cursor = db.execute(
|
|
55
|
+
"""
|
|
56
|
+
SELECT fecha, plazo, tasa, anio, mes
|
|
57
|
+
FROM tiie
|
|
58
|
+
WHERE plazo = ?
|
|
59
|
+
ORDER BY fecha
|
|
60
|
+
""",
|
|
61
|
+
(plazo,),
|
|
62
|
+
)
|
|
63
|
+
results = [cls._row_to_dict(row) for row in cursor.fetchall()]
|
|
64
|
+
db.close()
|
|
65
|
+
return results
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def get_por_fecha(cls, fecha: str, plazo: int = 28) -> dict | None:
|
|
69
|
+
"""
|
|
70
|
+
Get TIIE rate for a specific date and term
|
|
71
|
+
|
|
72
|
+
:param fecha: Date string in YYYY-MM-DD format
|
|
73
|
+
:param plazo: Term in days (28, 91, or 182)
|
|
74
|
+
:return: TIIE record or None if not found
|
|
75
|
+
"""
|
|
76
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
77
|
+
db.row_factory = sqlite3.Row
|
|
78
|
+
|
|
79
|
+
cursor = db.execute(
|
|
80
|
+
"""
|
|
81
|
+
SELECT fecha, plazo, tasa, anio, mes
|
|
82
|
+
FROM tiie
|
|
83
|
+
WHERE fecha = ? AND plazo = ?
|
|
84
|
+
LIMIT 1
|
|
85
|
+
""",
|
|
86
|
+
(fecha, plazo),
|
|
87
|
+
)
|
|
88
|
+
row = cursor.fetchone()
|
|
89
|
+
db.close()
|
|
90
|
+
|
|
91
|
+
if row:
|
|
92
|
+
return cls._row_to_dict(row)
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def get_actual(cls, plazo: int = 28) -> dict | None:
|
|
97
|
+
"""
|
|
98
|
+
Get most recent TIIE rate
|
|
99
|
+
|
|
100
|
+
:param plazo: Term in days (28, 91, or 182)
|
|
101
|
+
:return: Latest TIIE record
|
|
102
|
+
"""
|
|
103
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
104
|
+
db.row_factory = sqlite3.Row
|
|
105
|
+
|
|
106
|
+
cursor = db.execute(
|
|
107
|
+
"""
|
|
108
|
+
SELECT fecha, plazo, tasa, anio, mes
|
|
109
|
+
FROM tiie
|
|
110
|
+
WHERE plazo = ?
|
|
111
|
+
ORDER BY fecha DESC
|
|
112
|
+
LIMIT 1
|
|
113
|
+
""",
|
|
114
|
+
(plazo,),
|
|
115
|
+
)
|
|
116
|
+
row = cursor.fetchone()
|
|
117
|
+
db.close()
|
|
118
|
+
|
|
119
|
+
if row:
|
|
120
|
+
return cls._row_to_dict(row)
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def get_valor_actual(cls, plazo: int = 28) -> float | None:
|
|
125
|
+
"""
|
|
126
|
+
Get current TIIE rate value
|
|
127
|
+
|
|
128
|
+
:param plazo: Term in days (28, 91, or 182)
|
|
129
|
+
:return: Current TIIE rate or None
|
|
130
|
+
"""
|
|
131
|
+
record = cls.get_actual(plazo)
|
|
132
|
+
return record.get("tasa") if record else None
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def get_por_anio(cls, anio: int, plazo: int = 28) -> list[dict]:
|
|
136
|
+
"""
|
|
137
|
+
Get all TIIE rates for a specific year and term
|
|
138
|
+
|
|
139
|
+
:param anio: Year (e.g., 2024)
|
|
140
|
+
:param plazo: Term in days (28, 91, or 182)
|
|
141
|
+
:return: List of TIIE records for the year
|
|
142
|
+
"""
|
|
143
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
144
|
+
db.row_factory = sqlite3.Row
|
|
145
|
+
|
|
146
|
+
cursor = db.execute(
|
|
147
|
+
"""
|
|
148
|
+
SELECT fecha, plazo, tasa, anio, mes
|
|
149
|
+
FROM tiie
|
|
150
|
+
WHERE anio = ? AND plazo = ?
|
|
151
|
+
ORDER BY fecha
|
|
152
|
+
""",
|
|
153
|
+
(anio, plazo),
|
|
154
|
+
)
|
|
155
|
+
results = [cls._row_to_dict(row) for row in cursor.fetchall()]
|
|
156
|
+
db.close()
|
|
157
|
+
return results
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def calcular_variacion(cls, fecha_inicio: str, fecha_fin: str, plazo: int = 28) -> float | None:
|
|
161
|
+
"""
|
|
162
|
+
Calculate variation between two dates
|
|
163
|
+
|
|
164
|
+
:param fecha_inicio: Start date (YYYY-MM-DD)
|
|
165
|
+
:param fecha_fin: End date (YYYY-MM-DD)
|
|
166
|
+
:param plazo: Term in days (28, 91, or 182)
|
|
167
|
+
:return: Variation in percentage points or None
|
|
168
|
+
"""
|
|
169
|
+
record_inicio = cls.get_por_fecha(fecha_inicio, plazo)
|
|
170
|
+
record_fin = cls.get_por_fecha(fecha_fin, plazo)
|
|
171
|
+
|
|
172
|
+
if not record_inicio or not record_fin:
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
tasa_inicio = record_inicio.get("tasa")
|
|
176
|
+
tasa_fin = record_fin.get("tasa")
|
|
177
|
+
|
|
178
|
+
if tasa_inicio is None or tasa_fin is None:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
# Return difference in percentage points (not percentage change)
|
|
182
|
+
return tasa_fin - tasa_inicio
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def get_promedio_anual(cls, anio: int, plazo: int = 28) -> float | None:
|
|
186
|
+
"""
|
|
187
|
+
Calculate annual average TIIE rate
|
|
188
|
+
|
|
189
|
+
:param anio: Year (e.g., 2024)
|
|
190
|
+
:param plazo: Term in days (28, 91, or 182)
|
|
191
|
+
:return: Annual average rate or None if no data
|
|
192
|
+
"""
|
|
193
|
+
records = cls.get_por_anio(anio, plazo)
|
|
194
|
+
if not records:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
tasas = [r.get("tasa") for r in records if r.get("tasa") is not None]
|
|
198
|
+
return sum(tasas) / len(tasas) if tasas else None
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def get_tasa_actual(cls, plazo: int = 28) -> float | None:
|
|
202
|
+
"""
|
|
203
|
+
Get current TIIE rate value (alias for get_valor_actual)
|
|
204
|
+
|
|
205
|
+
:param plazo: Term in days (28, 91, or 182)
|
|
206
|
+
:return: Current TIIE rate or None
|
|
207
|
+
"""
|
|
208
|
+
return cls.get_valor_actual(plazo)
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def calcular_interes(
|
|
212
|
+
cls, monto: float, fecha_inicio: str, fecha_fin: str, plazo: int = 28
|
|
213
|
+
) -> float | None:
|
|
214
|
+
"""
|
|
215
|
+
Calculate interest using TIIE rate
|
|
216
|
+
|
|
217
|
+
:param monto: Principal amount
|
|
218
|
+
:param fecha_inicio: Start date (YYYY-MM-DD)
|
|
219
|
+
:param fecha_fin: End date (YYYY-MM-DD)
|
|
220
|
+
:param plazo: Term in days (28, 91, or 182)
|
|
221
|
+
:return: Interest amount or None if data not available
|
|
222
|
+
"""
|
|
223
|
+
from datetime import datetime
|
|
224
|
+
|
|
225
|
+
record_inicio = cls.get_por_fecha(fecha_inicio, plazo)
|
|
226
|
+
record_fin = cls.get_por_fecha(fecha_fin, plazo)
|
|
227
|
+
|
|
228
|
+
if not record_inicio and not record_fin:
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
# Use available rate (start or end, or average if both)
|
|
232
|
+
tasa = None
|
|
233
|
+
if record_inicio and record_fin:
|
|
234
|
+
tasa = (record_inicio.get("tasa", 0) + record_fin.get("tasa", 0)) / 2
|
|
235
|
+
elif record_inicio:
|
|
236
|
+
tasa = record_inicio.get("tasa")
|
|
237
|
+
elif record_fin:
|
|
238
|
+
tasa = record_fin.get("tasa")
|
|
239
|
+
|
|
240
|
+
if tasa is None:
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
# Calculate days between dates
|
|
244
|
+
date_inicio = datetime.strptime(fecha_inicio, "%Y-%m-%d")
|
|
245
|
+
date_fin = datetime.strptime(fecha_fin, "%Y-%m-%d")
|
|
246
|
+
dias = (date_fin - date_inicio).days
|
|
247
|
+
|
|
248
|
+
if dias <= 0:
|
|
249
|
+
return 0.0
|
|
250
|
+
|
|
251
|
+
# Calculate simple interest: I = P * r * t / 36500
|
|
252
|
+
# (tasa is annual percentage, divide by 100 to get decimal, then by 365 for daily rate)
|
|
253
|
+
interes = monto * (tasa / 100) * (dias / 365)
|
|
254
|
+
return round(interes, 2)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# Convenience functions for TIIE 28 (most commonly used)
|
|
258
|
+
def get_tiie_actual(plazo: int = 28) -> dict | None:
|
|
259
|
+
"""Get most recent TIIE rate"""
|
|
260
|
+
return TIIECatalog.get_actual(plazo)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def get_tiie_por_fecha(fecha: str, plazo: int = 28) -> dict | None:
|
|
264
|
+
"""Get TIIE rate for a specific date"""
|
|
265
|
+
return TIIECatalog.get_por_fecha(fecha, plazo)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def get_tiie_valor_actual(plazo: int = 28) -> float | None:
|
|
269
|
+
"""Get current TIIE rate value"""
|
|
270
|
+
return TIIECatalog.get_valor_actual(plazo)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# Export commonly used functions and classes
|
|
274
|
+
__all__ = [
|
|
275
|
+
"TIIECatalog",
|
|
276
|
+
"get_tiie_actual",
|
|
277
|
+
"get_tiie_por_fecha",
|
|
278
|
+
"get_tiie_valor_actual",
|
|
279
|
+
]
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tipo de Cambio USD/MXN Catalog - SQLite Backend
|
|
3
|
+
|
|
4
|
+
This module provides access to USD/MXN exchange rate values using a SQLite database
|
|
5
|
+
that can be automatically updated without requiring library releases.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sqlite3
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from catalogmx.data.updater import get_database_path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TipoCambioUSDCatalog:
|
|
15
|
+
"""
|
|
16
|
+
Catalog of USD/MXN exchange rate values
|
|
17
|
+
|
|
18
|
+
The FIX exchange rate is the official reference rate published daily by
|
|
19
|
+
Banco de México. This catalog uses an auto-updating SQLite database backend.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
_db_path: Path | None = None
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def _get_db_path(cls) -> Path:
|
|
26
|
+
"""Get path to database with auto-update"""
|
|
27
|
+
if cls._db_path is None:
|
|
28
|
+
cls._db_path = get_database_path(auto_update=True, max_age_hours=24)
|
|
29
|
+
return cls._db_path
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def _row_to_dict(cls, row: sqlite3.Row) -> dict:
|
|
33
|
+
"""Convert SQLite row to dictionary"""
|
|
34
|
+
return {
|
|
35
|
+
"fecha": row["fecha"],
|
|
36
|
+
"tipo_cambio": row["tipo_cambio"],
|
|
37
|
+
"año": row["anio"],
|
|
38
|
+
"mes": row["mes"],
|
|
39
|
+
"fuente": row["fuente"],
|
|
40
|
+
"moneda_origen": row["moneda_origen"],
|
|
41
|
+
"moneda_destino": row["moneda_destino"],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def get_data(cls) -> list[dict]:
|
|
46
|
+
"""
|
|
47
|
+
Get all exchange rate data
|
|
48
|
+
|
|
49
|
+
:return: List of all exchange rate records
|
|
50
|
+
"""
|
|
51
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
52
|
+
db.row_factory = sqlite3.Row
|
|
53
|
+
cursor = db.execute("""
|
|
54
|
+
SELECT fecha, tipo_cambio, anio, mes, fuente, moneda_origen, moneda_destino
|
|
55
|
+
FROM tipo_cambio
|
|
56
|
+
WHERE fuente = 'FIX'
|
|
57
|
+
ORDER BY fecha
|
|
58
|
+
""")
|
|
59
|
+
results = [cls._row_to_dict(row) for row in cursor.fetchall()]
|
|
60
|
+
db.close()
|
|
61
|
+
return results
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def get_por_fecha(cls, fecha: str) -> dict | None:
|
|
65
|
+
"""
|
|
66
|
+
Get exchange rate for a specific date
|
|
67
|
+
|
|
68
|
+
:param fecha: Date string in YYYY-MM-DD format
|
|
69
|
+
:return: Exchange rate record or None if not found
|
|
70
|
+
"""
|
|
71
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
72
|
+
db.row_factory = sqlite3.Row
|
|
73
|
+
|
|
74
|
+
cursor = db.execute(
|
|
75
|
+
"""
|
|
76
|
+
SELECT fecha, tipo_cambio, anio, mes, fuente, moneda_origen, moneda_destino
|
|
77
|
+
FROM tipo_cambio
|
|
78
|
+
WHERE fecha = ? AND fuente = 'FIX'
|
|
79
|
+
LIMIT 1
|
|
80
|
+
""",
|
|
81
|
+
(fecha,),
|
|
82
|
+
)
|
|
83
|
+
row = cursor.fetchone()
|
|
84
|
+
db.close()
|
|
85
|
+
|
|
86
|
+
if row:
|
|
87
|
+
return cls._row_to_dict(row)
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def get_por_anio(cls, anio: int) -> list[dict]:
|
|
92
|
+
"""
|
|
93
|
+
Get all exchange rates for a specific year
|
|
94
|
+
|
|
95
|
+
:param anio: Year (e.g., 2024)
|
|
96
|
+
:return: List of exchange rate records for the year
|
|
97
|
+
"""
|
|
98
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
99
|
+
db.row_factory = sqlite3.Row
|
|
100
|
+
|
|
101
|
+
cursor = db.execute(
|
|
102
|
+
"""
|
|
103
|
+
SELECT fecha, tipo_cambio, anio, mes, fuente, moneda_origen, moneda_destino
|
|
104
|
+
FROM tipo_cambio
|
|
105
|
+
WHERE anio = ? AND fuente = 'FIX'
|
|
106
|
+
ORDER BY fecha
|
|
107
|
+
""",
|
|
108
|
+
(anio,),
|
|
109
|
+
)
|
|
110
|
+
results = [cls._row_to_dict(row) for row in cursor.fetchall()]
|
|
111
|
+
db.close()
|
|
112
|
+
return results
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def get_actual(cls) -> dict | None:
|
|
116
|
+
"""
|
|
117
|
+
Get most recent exchange rate
|
|
118
|
+
|
|
119
|
+
:return: Latest exchange rate record
|
|
120
|
+
"""
|
|
121
|
+
db = sqlite3.connect(cls._get_db_path())
|
|
122
|
+
db.row_factory = sqlite3.Row
|
|
123
|
+
|
|
124
|
+
cursor = db.execute("""
|
|
125
|
+
SELECT fecha, tipo_cambio, anio, mes, fuente, moneda_origen, moneda_destino
|
|
126
|
+
FROM tipo_cambio
|
|
127
|
+
WHERE fuente = 'FIX'
|
|
128
|
+
ORDER BY fecha DESC
|
|
129
|
+
LIMIT 1
|
|
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_valor_actual(cls) -> float | None:
|
|
140
|
+
"""
|
|
141
|
+
Get current exchange rate value
|
|
142
|
+
|
|
143
|
+
:return: Current USD/MXN rate or None
|
|
144
|
+
"""
|
|
145
|
+
record = cls.get_actual()
|
|
146
|
+
return record.get("tipo_cambio") if record else None
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def usd_a_mxn(cls, usd: float, fecha: str | None = None) -> float | None:
|
|
150
|
+
"""
|
|
151
|
+
Convert USD to MXN
|
|
152
|
+
|
|
153
|
+
:param usd: Amount in USD
|
|
154
|
+
:param fecha: Date string in YYYY-MM-DD format, or None for latest rate
|
|
155
|
+
:return: Amount in MXN or None if rate not found
|
|
156
|
+
"""
|
|
157
|
+
if fecha:
|
|
158
|
+
record = cls.get_por_fecha(fecha)
|
|
159
|
+
else:
|
|
160
|
+
record = cls.get_actual()
|
|
161
|
+
|
|
162
|
+
if not record:
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
rate = record.get("tipo_cambio")
|
|
166
|
+
return usd * rate if rate else None
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def mxn_a_usd(cls, mxn: float, fecha: str | None = None) -> float | None:
|
|
170
|
+
"""
|
|
171
|
+
Convert MXN to USD
|
|
172
|
+
|
|
173
|
+
:param mxn: Amount in MXN
|
|
174
|
+
:param fecha: Date string in YYYY-MM-DD format, or None for latest rate
|
|
175
|
+
:return: Amount in USD or None if rate not found
|
|
176
|
+
"""
|
|
177
|
+
if fecha:
|
|
178
|
+
record = cls.get_por_fecha(fecha)
|
|
179
|
+
else:
|
|
180
|
+
record = cls.get_actual()
|
|
181
|
+
|
|
182
|
+
if not record:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
rate = record.get("tipo_cambio")
|
|
186
|
+
return mxn / rate if rate else None
|
|
187
|
+
|
|
188
|
+
@classmethod
|
|
189
|
+
def calcular_variacion(cls, fecha_inicio: str, fecha_fin: str) -> float | None:
|
|
190
|
+
"""
|
|
191
|
+
Calculate percentage variation between two dates
|
|
192
|
+
|
|
193
|
+
:param fecha_inicio: Start date (YYYY-MM-DD)
|
|
194
|
+
:param fecha_fin: End date (YYYY-MM-DD)
|
|
195
|
+
:return: Percentage variation or None if values not found
|
|
196
|
+
"""
|
|
197
|
+
record_inicio = cls.get_por_fecha(fecha_inicio)
|
|
198
|
+
record_fin = cls.get_por_fecha(fecha_fin)
|
|
199
|
+
|
|
200
|
+
if not record_inicio or not record_fin:
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
rate_inicio = record_inicio.get("tipo_cambio")
|
|
204
|
+
rate_fin = record_fin.get("tipo_cambio")
|
|
205
|
+
|
|
206
|
+
if not rate_inicio or not rate_fin:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
return ((rate_fin - rate_inicio) / rate_inicio) * 100
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def get_promedio_anual(cls, anio: int) -> float | None:
|
|
213
|
+
"""
|
|
214
|
+
Calculate annual average exchange rate
|
|
215
|
+
|
|
216
|
+
:param anio: Year (e.g., 2024)
|
|
217
|
+
:return: Annual average rate or None if no data
|
|
218
|
+
"""
|
|
219
|
+
records = cls.get_por_anio(anio)
|
|
220
|
+
if not records:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
rates = [r.get("tipo_cambio") for r in records if r.get("tipo_cambio")]
|
|
224
|
+
return sum(rates) / len(rates) if rates else None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# Convenience functions
|
|
228
|
+
def get_tipo_cambio_actual() -> dict | None:
|
|
229
|
+
"""Get most recent exchange rate"""
|
|
230
|
+
return TipoCambioUSDCatalog.get_actual()
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_tipo_cambio_por_fecha(fecha: str) -> dict | None:
|
|
234
|
+
"""Get exchange rate for a specific date"""
|
|
235
|
+
return TipoCambioUSDCatalog.get_por_fecha(fecha)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def usd_a_mxn(usd: float, fecha: str | None = None) -> float | None:
|
|
239
|
+
"""Convert USD to MXN"""
|
|
240
|
+
return TipoCambioUSDCatalog.usd_a_mxn(usd, fecha)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def mxn_a_usd(mxn: float, fecha: str | None = None) -> float | None:
|
|
244
|
+
"""Convert MXN to USD"""
|
|
245
|
+
return TipoCambioUSDCatalog.mxn_a_usd(mxn, fecha)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# Export commonly used functions and classes
|
|
249
|
+
__all__ = [
|
|
250
|
+
"TipoCambioUSDCatalog",
|
|
251
|
+
"get_tipo_cambio_actual",
|
|
252
|
+
"get_tipo_cambio_por_fecha",
|
|
253
|
+
"usd_a_mxn",
|
|
254
|
+
"mxn_a_usd",
|
|
255
|
+
]
|