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,236 @@
1
+ """
2
+ INEGI - Catálogo Completo de Municipios Mexicanos
3
+
4
+ Catálogo completo de todos los 2,469 municipios de México
5
+ (2,462 municipios + 7 alcaldías CDMX)
6
+ """
7
+
8
+ import json
9
+ from pathlib import Path
10
+
11
+
12
+ class MunicipiosCompletoCatalog:
13
+ """
14
+ Catálogo completo de municipios mexicanos.
15
+
16
+ Incluye todos los 2,469 municipios (2,462 municipios + 7 alcaldías CDMX)
17
+ con información demográfica completa.
18
+
19
+ WARNING: Este catálogo carga todos los municipios en memoria (~940KB).
20
+ Para consultas simples, considere usar MunicipiosCatalog en su lugar.
21
+
22
+ Características:
23
+ - 2,469 municipios totales
24
+ - Población total, masculina, femenina
25
+ - Viviendas habitadas
26
+ - Nombre de cabecera municipal
27
+ - Códigos INEGI completos
28
+
29
+ Ejemplo:
30
+ >>> from catalogmx.catalogs.inegi import MunicipiosCompletoCatalog
31
+ >>>
32
+ >>> # Obtener todos los municipios
33
+ >>> municipios = MunicipiosCompletoCatalog.get_all()
34
+ >>> print(f"Total municipios: {len(municipios)}")
35
+ >>>
36
+ >>> # Buscar municipio por código
37
+ >>> guadalajara = MunicipiosCompletoCatalog.get_municipio("14039")
38
+ >>> print(f"{guadalajara['nom_municipio']}: {guadalajara['poblacion_total']:,} habitantes")
39
+ >>>
40
+ >>> # Obtener municipios por estado
41
+ >>> jalisco = MunicipiosCompletoCatalog.get_by_entidad("14")
42
+ >>> print(f"Jalisco tiene {len(jalisco)} municipios")
43
+ """
44
+
45
+ _data: list[dict] | None = None
46
+
47
+ @classmethod
48
+ def _load_data(cls) -> None:
49
+ """Carga lazy de datos desde JSON"""
50
+ if cls._data is not None:
51
+ return
52
+
53
+ # Path: catalogmx/packages/python/catalogmx/catalogs/inegi/municipios_completo.py
54
+ # Target: catalogmx/packages/shared-data/inegi/municipios_completo.json
55
+ data_path = (
56
+ Path(__file__).parent.parent.parent.parent.parent
57
+ / "shared-data"
58
+ / "inegi"
59
+ / "municipios_completo.json"
60
+ )
61
+
62
+ with open(data_path, encoding="utf-8") as f:
63
+ cls._data = json.load(f)
64
+
65
+ @classmethod
66
+ def get_all(cls) -> list[dict]:
67
+ """
68
+ Obtiene todos los municipios (2,469 total).
69
+
70
+ WARNING: Retorna todos los municipios en memoria.
71
+
72
+ Returns:
73
+ Lista completa de municipios
74
+
75
+ Ejemplo:
76
+ >>> municipios = MunicipiosCompletoCatalog.get_all()
77
+ >>> print(f"Total: {len(municipios)}") # 2469
78
+ """
79
+ cls._load_data()
80
+ return cls._data.copy() # type: ignore
81
+
82
+ @classmethod
83
+ def get_municipio(cls, cve_completa: str) -> dict | None:
84
+ """
85
+ Obtiene municipio por código completo (cve_completa).
86
+
87
+ Args:
88
+ cve_completa: Código completo de 5 dígitos (ej: "14039" para Guadalajara)
89
+
90
+ Returns:
91
+ Información del municipio o None si no existe
92
+
93
+ Ejemplo:
94
+ >>> mun = MunicipiosCompletoCatalog.get_municipio("14039")
95
+ >>> print(mun['nom_municipio']) # "Guadalajara"
96
+ """
97
+ cls._load_data()
98
+ for mun in cls._data: # type: ignore
99
+ if mun["cve_completa"] == cve_completa:
100
+ return mun
101
+ return None
102
+
103
+ @classmethod
104
+ def get_by_entidad(cls, cve_entidad: str) -> list[dict]:
105
+ """
106
+ Obtiene municipios por estado (cve_entidad).
107
+
108
+ Args:
109
+ cve_entidad: Código de entidad de 2 dígitos (ej: "14" para Jalisco)
110
+
111
+ Returns:
112
+ Lista de municipios del estado
113
+
114
+ Ejemplo:
115
+ >>> jalisco = MunicipiosCompletoCatalog.get_by_entidad("14")
116
+ >>> print(f"Jalisco: {len(jalisco)} municipios")
117
+ """
118
+ cls._load_data()
119
+ return [mun for mun in cls._data if mun["cve_entidad"] == cve_entidad] # type: ignore
120
+
121
+ @classmethod
122
+ def search_by_name(cls, name: str) -> list[dict]:
123
+ """
124
+ Busca municipios por nombre (case-insensitive).
125
+
126
+ Args:
127
+ name: Nombre o parte del nombre a buscar
128
+
129
+ Returns:
130
+ Lista de municipios que coinciden
131
+
132
+ Ejemplo:
133
+ >>> san = MunicipiosCompletoCatalog.search_by_name("San")
134
+ >>> for mun in san[:5]:
135
+ ... print(f"{mun['nom_municipio']}, {mun['nom_entidad']}")
136
+ """
137
+ cls._load_data()
138
+ search_term = name.upper()
139
+ return [
140
+ mun for mun in cls._data if search_term in mun["nom_municipio"].upper() # type: ignore
141
+ ]
142
+
143
+ @classmethod
144
+ def get_by_state_name(cls, state_name: str) -> list[dict]:
145
+ """
146
+ Obtiene municipios por nombre de estado.
147
+
148
+ Args:
149
+ state_name: Nombre del estado (ej: "Jalisco", "CDMX")
150
+
151
+ Returns:
152
+ Lista de municipios del estado
153
+
154
+ Ejemplo:
155
+ >>> jalisco = MunicipiosCompletoCatalog.get_by_state_name("Jalisco")
156
+ >>> for mun in jalisco[:5]:
157
+ ... print(mun['nom_municipio'])
158
+ """
159
+ cls._load_data()
160
+ search_term = state_name.upper()
161
+ return [
162
+ mun for mun in cls._data if mun["nom_entidad"].upper() == search_term # type: ignore
163
+ ]
164
+
165
+ @classmethod
166
+ def get_count_by_entidad(cls, cve_entidad: str) -> int:
167
+ """
168
+ Obtiene el número de municipios en un estado.
169
+
170
+ Args:
171
+ cve_entidad: Código de entidad de 2 dígitos
172
+
173
+ Returns:
174
+ Número de municipios en el estado
175
+
176
+ Ejemplo:
177
+ >>> count = MunicipiosCompletoCatalog.get_count_by_entidad("14")
178
+ >>> print(f"Jalisco tiene {count} municipios")
179
+ """
180
+ return len(cls.get_by_entidad(cve_entidad))
181
+
182
+ @classmethod
183
+ def is_valid(cls, cve_completa: str) -> bool:
184
+ """
185
+ Valida código de municipio.
186
+
187
+ Args:
188
+ cve_completa: Código completo de 5 dígitos
189
+
190
+ Returns:
191
+ True si el código existe, False en caso contrario
192
+
193
+ Ejemplo:
194
+ >>> MunicipiosCompletoCatalog.is_valid("14039") # True
195
+ >>> MunicipiosCompletoCatalog.is_valid("99999") # False
196
+ """
197
+ return cls.get_municipio(cve_completa) is not None
198
+
199
+ @classmethod
200
+ def get_total_count(cls) -> int:
201
+ """
202
+ Obtiene el total de municipios.
203
+
204
+ Returns:
205
+ Número total de municipios (2,469)
206
+
207
+ Ejemplo:
208
+ >>> total = MunicipiosCompletoCatalog.get_total_count()
209
+ >>> print(f"Total municipios: {total}") # 2469
210
+ """
211
+ cls._load_data()
212
+ return len(cls._data) # type: ignore
213
+
214
+ @classmethod
215
+ def get_estadisticas(cls) -> dict[str, int]:
216
+ """
217
+ Obtiene estadísticas del catálogo.
218
+
219
+ Returns:
220
+ Diccionario con estadísticas de municipios
221
+
222
+ Ejemplo:
223
+ >>> stats = MunicipiosCompletoCatalog.get_estadisticas()
224
+ >>> print(f"Total municipios: {stats['total_municipios']}")
225
+ >>> print(f"Total estados: {stats['total_estados']}")
226
+ """
227
+ cls._load_data()
228
+
229
+ estados = {mun["cve_entidad"] for mun in cls._data} # type: ignore
230
+ poblacion_total = sum(mun["poblacion_total"] for mun in cls._data) # type: ignore
231
+
232
+ return {
233
+ "total_municipios": len(cls._data), # type: ignore
234
+ "total_estados": len(estados),
235
+ "poblacion_total": poblacion_total,
236
+ }
@@ -0,0 +1,148 @@
1
+ """
2
+ Mexican States Catalog from INEGI
3
+
4
+ This module provides access to the official catalog of Mexican states
5
+ (entidades federativas) with their CURP codes, INEGI codes, and abbreviations.
6
+ """
7
+
8
+ import json
9
+ from pathlib import Path
10
+
11
+ from catalogmx.utils.text import normalize_text
12
+
13
+
14
+ class StateCatalog:
15
+ """
16
+ Catalog of Mexican states
17
+ """
18
+
19
+ _data: list[dict] | None = None
20
+ _state_by_code: dict[str, dict] | None = None
21
+ _state_by_name: dict[str, dict] | None = None
22
+ _state_by_name_normalized: dict[str, dict] | None = None
23
+ _state_by_inegi: dict[str, dict] | None = None
24
+
25
+ @classmethod
26
+ def _load_data(cls) -> None:
27
+ """Load state data from JSON file"""
28
+ if cls._data is None:
29
+ # Get path to shared data
30
+ current_file = Path(__file__)
31
+ shared_data_path = (
32
+ current_file.parent.parent.parent.parent.parent
33
+ / "shared-data"
34
+ / "inegi"
35
+ / "states.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._state_by_code = {state["code"]: state for state in cls._data}
43
+ cls._state_by_name = {state["name"].upper(): state for state in cls._data}
44
+ cls._state_by_name_normalized = {
45
+ normalize_text(state["name"]): state for state in cls._data
46
+ }
47
+ cls._state_by_inegi = {state["clave_inegi"]: state for state in cls._data}
48
+
49
+ # Add aliases to name lookup
50
+ for state in cls._data:
51
+ if "aliases" in state:
52
+ for alias in state["aliases"]:
53
+ cls._state_by_name[alias.upper()] = state
54
+ cls._state_by_name_normalized[normalize_text(alias)] = state
55
+
56
+ @classmethod
57
+ def get_all_states(cls) -> list[dict]:
58
+ """
59
+ Get all states in the catalog
60
+
61
+ :return: List of state dictionaries
62
+ """
63
+ cls._load_data()
64
+ return cls._data.copy()
65
+
66
+ @classmethod
67
+ def get_state_by_code(cls, code: str) -> dict | None:
68
+ """
69
+ Get state information by CURP code
70
+
71
+ :param code: 2-letter CURP state code (e.g., 'AS' for Aguascalientes)
72
+ :return: State dictionary or None if not found
73
+ """
74
+ cls._load_data()
75
+ return cls._state_by_code.get(code.upper())
76
+
77
+ @classmethod
78
+ def get_state_by_name(cls, name: str) -> dict | None:
79
+ """
80
+ Get state information by name (case and accent insensitive)
81
+
82
+ :param name: State name (case and accent insensitive)
83
+ :return: State dictionary or None if not found
84
+ """
85
+ cls._load_data()
86
+ return cls._state_by_name_normalized.get(normalize_text(name))
87
+
88
+ @classmethod
89
+ def get_state_by_inegi_code(cls, code: str) -> dict | None:
90
+ """
91
+ Get state information by INEGI code
92
+
93
+ :param code: 2-digit INEGI code (e.g., '01' for Aguascalientes)
94
+ :return: State dictionary or None if not found
95
+ """
96
+ cls._load_data()
97
+ code = str(code).zfill(2)
98
+ return cls._state_by_inegi.get(code)
99
+
100
+ @classmethod
101
+ def validate_state_code(cls, code: str) -> bool:
102
+ """
103
+ Validate if a state CURP code exists
104
+
105
+ :param code: 2-letter CURP state code
106
+ :return: True if exists, False otherwise
107
+ """
108
+ return cls.get_state_by_code(code) is not None
109
+
110
+ @classmethod
111
+ def get_state_codes(cls) -> dict[str, str]:
112
+ """
113
+ Get dictionary of state names to CURP codes
114
+
115
+ :return: Dictionary mapping state names to codes
116
+ """
117
+ cls._load_data()
118
+ return {state["name"]: state["code"] for state in cls._data}
119
+
120
+ @classmethod
121
+ def get_inegi_codes(cls) -> dict[str, str]:
122
+ """
123
+ Get dictionary of state names to INEGI codes
124
+
125
+ :return: Dictionary mapping state names to INEGI codes
126
+ """
127
+ cls._load_data()
128
+ return {state["name"]: state["clave_inegi"] for state in cls._data}
129
+
130
+
131
+ # Convenience functions
132
+ def get_states_dict() -> dict[str, dict]:
133
+ """Get dictionary of all states indexed by CURP code"""
134
+ StateCatalog._load_data()
135
+ return StateCatalog._state_by_code.copy()
136
+
137
+
138
+ def get_state_names() -> list[str]:
139
+ """Get list of all state names"""
140
+ return [state["name"] for state in StateCatalog.get_all_states()]
141
+
142
+
143
+ # Export commonly used functions
144
+ __all__ = [
145
+ "StateCatalog",
146
+ "get_states_dict",
147
+ "get_state_names",
148
+ ]
@@ -0,0 +1,17 @@
1
+ """
2
+ Mexican National Catalogs
3
+
4
+ This module provides access to various Mexican national catalogs.
5
+ """
6
+
7
+ from .hoy_no_circula import HoyNoCirculaCatalog
8
+ from .placas_formatos import PlacasFormatosCatalog
9
+ from .salarios_minimos import SalariosMinimos
10
+ from .uma import UMACatalog
11
+
12
+ __all__ = [
13
+ "PlacasFormatosCatalog",
14
+ "SalariosMinimos",
15
+ "UMACatalog",
16
+ "HoyNoCirculaCatalog",
17
+ ]
@@ -0,0 +1,215 @@
1
+ """
2
+ Hoy No Circula CDMX Catalog
3
+
4
+ This module provides access to the Hoy No Circula traffic restriction program
5
+ for Mexico City (CDMX) and Estado de México.
6
+ """
7
+
8
+ import json
9
+ from pathlib import Path
10
+
11
+
12
+ class HoyNoCirculaCatalog:
13
+ """
14
+ Catalog for Hoy No Circula traffic restriction program
15
+
16
+ The Hoy No Circula program restricts vehicle circulation in Mexico City
17
+ and Estado de México based on the last digit of the license plate number
18
+ and the vehicle's verification hologram.
19
+ """
20
+
21
+ _data: dict | None = None
22
+
23
+ @classmethod
24
+ def _load_data(cls) -> None:
25
+ """Load Hoy No Circula data from JSON file"""
26
+ if cls._data is None:
27
+ # Path: catalogmx/packages/python/catalogmx/catalogs/mexico/hoy_no_circula.py
28
+ # Target: catalogmx/packages/shared-data/mexico/hoy_no_circula_cdmx.json
29
+ current_file = Path(__file__)
30
+ shared_data_path = (
31
+ current_file.parent.parent.parent.parent.parent
32
+ / "shared-data"
33
+ / "mexico"
34
+ / "hoy_no_circula_cdmx.json"
35
+ )
36
+
37
+ with open(shared_data_path, encoding="utf-8") as f:
38
+ cls._data = json.load(f)
39
+
40
+ @classmethod
41
+ def get_data(cls) -> dict:
42
+ """
43
+ Get all Hoy No Circula data
44
+
45
+ :return: Complete Hoy No Circula data dictionary
46
+ """
47
+ cls._load_data()
48
+ return cls._data.copy()
49
+
50
+ @classmethod
51
+ def get_restricciones(cls) -> list[dict]:
52
+ """
53
+ Get all restrictions by day of week
54
+
55
+ :return: List of restriction dictionaries
56
+ """
57
+ cls._load_data()
58
+ return cls._data.get("restricciones_por_dia", []).copy()
59
+
60
+ @classmethod
61
+ def get_restriccion_por_dia(cls, dia: str) -> dict | None:
62
+ """
63
+ Get restriction for a specific day of week
64
+
65
+ :param dia: Day of week (e.g., 'lunes', 'martes')
66
+ :return: Restriction dictionary or None if not found
67
+ """
68
+ cls._load_data()
69
+ restricciones = cls._data.get("restricciones_por_dia", [])
70
+
71
+ for restriccion in restricciones:
72
+ if restriccion.get("dia", "").lower() == dia.lower():
73
+ return restriccion.copy()
74
+
75
+ return None
76
+
77
+ @classmethod
78
+ def get_exenciones(cls) -> list[dict]:
79
+ """
80
+ Get all exemptions by hologram type
81
+
82
+ :return: List of exemption dictionaries
83
+ """
84
+ cls._load_data()
85
+ return cls._data.get("exenciones_por_holograma", []).copy()
86
+
87
+ @classmethod
88
+ def get_exencion_por_holograma(cls, holograma: str) -> dict | None:
89
+ """
90
+ Get exemption information for a specific hologram
91
+
92
+ :param holograma: Hologram type (e.g., '00', '0', '1', '2')
93
+ :return: Exemption dictionary or None if not found
94
+ """
95
+ cls._load_data()
96
+ exenciones = cls._data.get("exenciones_por_holograma", [])
97
+
98
+ for exencion in exenciones:
99
+ if exencion.get("holograma") == holograma:
100
+ return exencion.copy()
101
+
102
+ return None
103
+
104
+ @classmethod
105
+ def puede_circular(cls, terminacion: str, dia: str, holograma: str = "2") -> bool:
106
+ """
107
+ Check if a vehicle can circulate on a given day
108
+
109
+ :param terminacion: Last digit of license plate (0-9)
110
+ :param dia: Day of week ('lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado')
111
+ :param holograma: Verification hologram ('00', '0', '1', '2')
112
+ :return: True if can circulate, False otherwise
113
+ """
114
+ cls._load_data()
115
+
116
+ # Check exemption by hologram first
117
+ exencion = cls.get_exencion_por_holograma(holograma)
118
+ if exencion:
119
+ # Hologram 00 and 0 can circulate all days (except Saturdays for 0)
120
+ if holograma == "00":
121
+ return True
122
+ elif holograma == "0":
123
+ return dia.lower() != "sábado"
124
+
125
+ # Check restriction for the day
126
+ restriccion = cls.get_restriccion_por_dia(dia)
127
+ if not restriccion:
128
+ # No restriction for this day (e.g., Sunday)
129
+ return True
130
+
131
+ # Check if the termination is restricted
132
+ terminaciones_restringidas = restriccion.get("terminacion_placa", [])
133
+ return str(terminacion) not in [str(t) for t in terminaciones_restringidas]
134
+
135
+ @classmethod
136
+ def get_dia_restriccion(cls, terminacion: str) -> str | None:
137
+ """
138
+ Get the day of week when a vehicle is restricted (for hologram 2)
139
+
140
+ :param terminacion: Last digit of license plate (0-9)
141
+ :return: Day of week when restricted or None if not restricted
142
+ """
143
+ cls._load_data()
144
+
145
+ restricciones = cls._data.get("restricciones_por_dia", [])
146
+ for restriccion in restricciones:
147
+ terminaciones = restriccion.get("terminacion_placa", [])
148
+ if str(terminacion) in [str(t) for t in terminaciones]:
149
+ return restriccion.get("dia")
150
+
151
+ return None
152
+
153
+ @classmethod
154
+ def get_engomado(cls, terminacion: str) -> str | None:
155
+ """
156
+ Get the engomado (sticker color) for a license plate termination
157
+
158
+ :param terminacion: Last digit of license plate (0-9)
159
+ :return: Engomado color or None if not found
160
+ """
161
+ cls._load_data()
162
+
163
+ restricciones = cls._data.get("restricciones_por_dia", [])
164
+ for restriccion in restricciones:
165
+ terminaciones = restriccion.get("terminacion_placa", [])
166
+ if str(terminacion) in [str(t) for t in terminaciones]:
167
+ engomados = restriccion.get("engomado", [])
168
+ return engomados[0] if engomados else None
169
+
170
+ return None
171
+
172
+ @classmethod
173
+ def get_contingencias(cls) -> dict:
174
+ """
175
+ Get contingency program information
176
+
177
+ :return: Dictionary with environmental contingency rules
178
+ """
179
+ cls._load_data()
180
+ return cls._data.get("contingencias_ambientales", {}).copy()
181
+
182
+ @classmethod
183
+ def get_sabatinos(cls) -> dict:
184
+ """
185
+ Get Saturday restriction information
186
+
187
+ :return: Dictionary with Saturday restriction rules
188
+ """
189
+ cls._load_data()
190
+ return cls._data.get("sabatinos", {}).copy()
191
+
192
+
193
+ # Convenience functions
194
+ def puede_circular(terminacion: str, dia: str, holograma: str = "2") -> bool:
195
+ """Check if a vehicle can circulate on a given day"""
196
+ return HoyNoCirculaCatalog.puede_circular(terminacion, dia, holograma)
197
+
198
+
199
+ def get_dia_restriccion(terminacion: str) -> str | None:
200
+ """Get the day when a vehicle is restricted"""
201
+ return HoyNoCirculaCatalog.get_dia_restriccion(terminacion)
202
+
203
+
204
+ def get_engomado(terminacion: str) -> str | None:
205
+ """Get the engomado color for a license plate termination"""
206
+ return HoyNoCirculaCatalog.get_engomado(terminacion)
207
+
208
+
209
+ # Export commonly used functions and classes
210
+ __all__ = [
211
+ "HoyNoCirculaCatalog",
212
+ "puede_circular",
213
+ "get_dia_restriccion",
214
+ "get_engomado",
215
+ ]