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
@@ -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,9 @@
1
+ """
2
+ CNBV Catalogs - Sectores y entidades reguladas por la CNBV.
3
+
4
+ Comisión Nacional Bancaria y de Valores
5
+ """
6
+
7
+ from .sectores import ReguladorFinanciero, SectoresCNBV
8
+
9
+ __all__ = ["SectoresCNBV", "ReguladorFinanciero"]
@@ -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