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
|
@@ -6,16 +6,33 @@ Catálogos incluidos:
|
|
|
6
6
|
- MunicipiosCompletoCatalog: Catálogo completo de 2,469 municipios
|
|
7
7
|
- LocalidadesCatalog: Localidades con 1,000+ habitantes
|
|
8
8
|
- StateCatalog: Estados de México
|
|
9
|
+
- SCIANCatalog: Sistema de Clasificación Industrial de América del Norte
|
|
9
10
|
"""
|
|
10
11
|
|
|
11
12
|
from .localidades import LocalidadesCatalog
|
|
12
13
|
from .municipios import MunicipiosCatalog
|
|
13
14
|
from .municipios_completo import MunicipiosCompletoCatalog
|
|
15
|
+
from .scian import SCIANCatalog
|
|
14
16
|
from .states import StateCatalog
|
|
15
17
|
|
|
18
|
+
# Aliases for convenience
|
|
19
|
+
States = StateCatalog
|
|
20
|
+
Estados = StateCatalog
|
|
21
|
+
Municipios = MunicipiosCatalog
|
|
22
|
+
Localidades = LocalidadesCatalog
|
|
23
|
+
SCIAN = SCIANCatalog
|
|
24
|
+
|
|
16
25
|
__all__ = [
|
|
26
|
+
# Main classes
|
|
17
27
|
"MunicipiosCatalog",
|
|
18
28
|
"MunicipiosCompletoCatalog",
|
|
19
29
|
"LocalidadesCatalog",
|
|
20
30
|
"StateCatalog",
|
|
31
|
+
"SCIANCatalog",
|
|
32
|
+
# Aliases
|
|
33
|
+
"States",
|
|
34
|
+
"Estados",
|
|
35
|
+
"Municipios",
|
|
36
|
+
"Localidades",
|
|
37
|
+
"SCIAN",
|
|
21
38
|
]
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Catálogo SCIAN - Sistema de Clasificación Industrial de América del Norte.
|
|
3
|
+
|
|
4
|
+
INEGI - Clasificación de actividades económicas para México, EE.UU. y Canadá.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TypedDict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SectorSCIAN(TypedDict):
|
|
15
|
+
"""Estructura de un sector SCIAN."""
|
|
16
|
+
|
|
17
|
+
codigo: str
|
|
18
|
+
nombre: str
|
|
19
|
+
nombre_corto: str
|
|
20
|
+
subsectores: int
|
|
21
|
+
ramas: int
|
|
22
|
+
descripcion: str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SCIANCatalog:
|
|
26
|
+
"""Catálogo de sectores SCIAN (Sistema de Clasificación Industrial de América del Norte)."""
|
|
27
|
+
|
|
28
|
+
_data: dict | None = None
|
|
29
|
+
_sectores: list[SectorSCIAN] | None = None
|
|
30
|
+
_by_codigo: dict[str, SectorSCIAN] | None = None
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def _load_data(cls) -> None:
|
|
34
|
+
"""Carga lazy de los datos del catálogo."""
|
|
35
|
+
if cls._data is not None:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
data_path = (
|
|
39
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
40
|
+
/ "shared-data"
|
|
41
|
+
/ "inegi"
|
|
42
|
+
/ "scian"
|
|
43
|
+
/ "sectores.json"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
with open(data_path, encoding="utf-8") as f:
|
|
47
|
+
cls._data = json.load(f)
|
|
48
|
+
|
|
49
|
+
cls._sectores = cls._data.get("sectores", [])
|
|
50
|
+
cls._by_codigo = {s["codigo"]: s for s in cls._sectores}
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def get_all_sectores(cls) -> list[SectorSCIAN]:
|
|
54
|
+
"""Obtiene todos los sectores SCIAN (20 sectores)."""
|
|
55
|
+
cls._load_data()
|
|
56
|
+
return cls._sectores.copy() if cls._sectores else []
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def get_sector_by_codigo(cls, codigo: str) -> SectorSCIAN | None:
|
|
60
|
+
"""Obtiene un sector por su código (2 dígitos)."""
|
|
61
|
+
cls._load_data()
|
|
62
|
+
return cls._by_codigo.get(codigo) if cls._by_codigo else None
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def is_valid_sector(cls, codigo: str) -> bool:
|
|
66
|
+
"""Valida si un código de sector existe."""
|
|
67
|
+
return cls.get_sector_by_codigo(codigo) is not None
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def search(cls, query: str) -> list[SectorSCIAN]:
|
|
71
|
+
"""Busca sectores por nombre o descripción."""
|
|
72
|
+
cls._load_data()
|
|
73
|
+
if not cls._sectores:
|
|
74
|
+
return []
|
|
75
|
+
|
|
76
|
+
query_lower = query.lower()
|
|
77
|
+
results = []
|
|
78
|
+
|
|
79
|
+
for sector in cls._sectores:
|
|
80
|
+
if (
|
|
81
|
+
query_lower in sector.get("nombre", "").lower()
|
|
82
|
+
or query_lower in sector.get("nombre_corto", "").lower()
|
|
83
|
+
or query_lower in sector.get("descripcion", "").lower()
|
|
84
|
+
):
|
|
85
|
+
results.append(sector)
|
|
86
|
+
|
|
87
|
+
return results
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def get_sector_for_code(cls, codigo: str) -> SectorSCIAN | None:
|
|
91
|
+
"""
|
|
92
|
+
Obtiene el sector al que pertenece un código SCIAN de cualquier nivel.
|
|
93
|
+
|
|
94
|
+
Por ejemplo: para código "311811" (Panaderías) retorna sector "31-33" (Manufactura).
|
|
95
|
+
"""
|
|
96
|
+
cls._load_data()
|
|
97
|
+
if not cls._sectores:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
# Obtener los primeros 2 dígitos
|
|
101
|
+
codigo_2 = codigo[:2] if len(codigo) >= 2 else codigo
|
|
102
|
+
|
|
103
|
+
# Buscar coincidencia directa o en rangos
|
|
104
|
+
for sector in cls._sectores:
|
|
105
|
+
sector_codigo = sector["codigo"]
|
|
106
|
+
|
|
107
|
+
# Manejar rangos como "31-33" o "48-49"
|
|
108
|
+
if "-" in sector_codigo:
|
|
109
|
+
inicio, fin = sector_codigo.split("-")
|
|
110
|
+
if inicio <= codigo_2 <= fin:
|
|
111
|
+
return sector
|
|
112
|
+
elif sector_codigo == codigo_2:
|
|
113
|
+
return sector
|
|
114
|
+
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def get_totales(cls) -> dict[str, int]:
|
|
119
|
+
"""Retorna los totales de la estructura SCIAN."""
|
|
120
|
+
cls._load_data()
|
|
121
|
+
return cls._data.get("totales", {}) if cls._data else {}
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def count_sectores(cls) -> int:
|
|
125
|
+
"""Retorna el número total de sectores."""
|
|
126
|
+
cls._load_data()
|
|
127
|
+
return len(cls._sectores) if cls._sectores else 0
|
|
@@ -4,12 +4,14 @@ Mexican National Catalogs
|
|
|
4
4
|
This module provides access to various Mexican national catalogs.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from .giros_mercantiles import GirosMercantilesCatalog
|
|
7
8
|
from .hoy_no_circula import HoyNoCirculaCatalog
|
|
8
9
|
from .placas_formatos import PlacasFormatosCatalog
|
|
9
10
|
from .salarios_minimos import SalariosMinimos
|
|
10
11
|
from .uma import UMACatalog
|
|
11
12
|
|
|
12
13
|
__all__ = [
|
|
14
|
+
"GirosMercantilesCatalog",
|
|
13
15
|
"PlacasFormatosCatalog",
|
|
14
16
|
"SalariosMinimos",
|
|
15
17
|
"UMACatalog",
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Catálogo de Giros Mercantiles - Clasificación de negocios en México.
|
|
3
|
+
|
|
4
|
+
Catálogo de actividades comerciales comunes para licencias de funcionamiento.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TypedDict
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class GiroMercantil(TypedDict):
|
|
15
|
+
"""Estructura de un giro mercantil."""
|
|
16
|
+
|
|
17
|
+
id: str
|
|
18
|
+
nombre: str
|
|
19
|
+
categoria: str
|
|
20
|
+
requiere_licencia_alcohol: bool
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CategoriaMercantil(TypedDict):
|
|
24
|
+
"""Estructura de una categoría de giros."""
|
|
25
|
+
|
|
26
|
+
id: str
|
|
27
|
+
nombre: str
|
|
28
|
+
descripcion: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class GirosMercantilesCatalog:
|
|
32
|
+
"""Catálogo de giros mercantiles comunes en México."""
|
|
33
|
+
|
|
34
|
+
_data: dict | None = None
|
|
35
|
+
_giros: list[GiroMercantil] | None = None
|
|
36
|
+
_categorias: list[CategoriaMercantil] | None = None
|
|
37
|
+
_by_id: dict[str, GiroMercantil] | None = None
|
|
38
|
+
_by_categoria: dict[str, list[GiroMercantil]] | None = None
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def _load_data(cls) -> None:
|
|
42
|
+
"""Carga lazy de los datos del catálogo."""
|
|
43
|
+
if cls._data is not None:
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
data_path = (
|
|
47
|
+
Path(__file__).parent.parent.parent.parent.parent
|
|
48
|
+
/ "shared-data"
|
|
49
|
+
/ "mexico"
|
|
50
|
+
/ "giros_mercantiles.json"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
with open(data_path, encoding="utf-8") as f:
|
|
54
|
+
cls._data = json.load(f)
|
|
55
|
+
|
|
56
|
+
cls._giros = cls._data.get("giros", [])
|
|
57
|
+
cls._categorias = cls._data.get("categorias", [])
|
|
58
|
+
cls._by_id = {g["id"]: g for g in cls._giros}
|
|
59
|
+
|
|
60
|
+
# Agrupar por categoría
|
|
61
|
+
cls._by_categoria = {}
|
|
62
|
+
for giro in cls._giros:
|
|
63
|
+
cat = giro.get("categoria", "otro")
|
|
64
|
+
if cat not in cls._by_categoria:
|
|
65
|
+
cls._by_categoria[cat] = []
|
|
66
|
+
cls._by_categoria[cat].append(giro)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def get_all(cls) -> list[GiroMercantil]:
|
|
70
|
+
"""Obtiene todos los giros mercantiles."""
|
|
71
|
+
cls._load_data()
|
|
72
|
+
return cls._giros.copy() if cls._giros else []
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def get_by_id(cls, giro_id: str) -> GiroMercantil | None:
|
|
76
|
+
"""Obtiene un giro por su ID."""
|
|
77
|
+
cls._load_data()
|
|
78
|
+
return cls._by_id.get(giro_id) if cls._by_id else None
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def is_valid(cls, giro_id: str) -> bool:
|
|
82
|
+
"""Valida si un ID de giro existe."""
|
|
83
|
+
return cls.get_by_id(giro_id) is not None
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def get_categorias(cls) -> list[CategoriaMercantil]:
|
|
87
|
+
"""Obtiene todas las categorías."""
|
|
88
|
+
cls._load_data()
|
|
89
|
+
return cls._categorias.copy() if cls._categorias else []
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def get_by_categoria(cls, categoria: str) -> list[GiroMercantil]:
|
|
93
|
+
"""Obtiene giros por categoría."""
|
|
94
|
+
cls._load_data()
|
|
95
|
+
return cls._by_categoria.get(categoria, []).copy() if cls._by_categoria else []
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def get_requieren_licencia_alcohol(cls) -> list[GiroMercantil]:
|
|
99
|
+
"""Obtiene giros que requieren licencia de alcohol."""
|
|
100
|
+
cls._load_data()
|
|
101
|
+
if not cls._giros:
|
|
102
|
+
return []
|
|
103
|
+
return [g for g in cls._giros if g.get("requiere_licencia_alcohol")]
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def search(cls, query: str) -> list[GiroMercantil]:
|
|
107
|
+
"""Busca giros por nombre."""
|
|
108
|
+
cls._load_data()
|
|
109
|
+
if not cls._giros:
|
|
110
|
+
return []
|
|
111
|
+
|
|
112
|
+
query_lower = query.lower()
|
|
113
|
+
return [g for g in cls._giros if query_lower in g.get("nombre", "").lower()]
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def count(cls) -> int:
|
|
117
|
+
"""Retorna el número total de giros."""
|
|
118
|
+
cls._load_data()
|
|
119
|
+
return len(cls._giros) if cls._giros else 0
|
|
@@ -47,7 +47,11 @@ class MaterialPeligrosoCatalog:
|
|
|
47
47
|
"""Obtiene materiales por clase de peligro (1-9)"""
|
|
48
48
|
cls._load_data()
|
|
49
49
|
# Handle both "class" and "clase_riesgo" field names
|
|
50
|
-
return [
|
|
50
|
+
return [
|
|
51
|
+
m
|
|
52
|
+
for m in cls._data
|
|
53
|
+
if m.get("class", m.get("clase_riesgo", "")).startswith(hazard_class)
|
|
54
|
+
]
|
|
51
55
|
|
|
52
56
|
@classmethod
|
|
53
57
|
def get_by_packing_group(cls, packing_group: str) -> list[dict]:
|
|
@@ -8,6 +8,7 @@ Standard Products and Services Code).
|
|
|
8
8
|
Este módulo usa SQLite con FTS5 para búsqueda eficiente de texto completo.
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
+
import atexit
|
|
11
12
|
import sqlite3
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
from typing import TypedDict
|
|
@@ -89,8 +90,81 @@ class ClaveProdServCatalog:
|
|
|
89
90
|
)
|
|
90
91
|
cls._connection = sqlite3.connect(str(db_path))
|
|
91
92
|
cls._connection.row_factory = sqlite3.Row
|
|
93
|
+
cls._ensure_schema(cls._connection)
|
|
92
94
|
return cls._connection
|
|
93
95
|
|
|
96
|
+
@classmethod
|
|
97
|
+
def close(cls) -> None:
|
|
98
|
+
"""Cierra la conexión a la base de datos y limpia el estado."""
|
|
99
|
+
if cls._connection is not None:
|
|
100
|
+
cls._connection.close()
|
|
101
|
+
cls._connection = None
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def _ensure_schema(cls, conn: sqlite3.Connection) -> None:
|
|
105
|
+
"""Crea tablas mínimas si el archivo existe pero está vacío."""
|
|
106
|
+
cursor = conn.cursor()
|
|
107
|
+
cursor.execute("""
|
|
108
|
+
CREATE TABLE IF NOT EXISTS clave_prod_serv (
|
|
109
|
+
clave TEXT PRIMARY KEY,
|
|
110
|
+
descripcion TEXT,
|
|
111
|
+
incluye_iva INTEGER,
|
|
112
|
+
incluye_ieps INTEGER,
|
|
113
|
+
complemento TEXT,
|
|
114
|
+
palabras_similares TEXT,
|
|
115
|
+
fecha_inicio_vigencia TEXT,
|
|
116
|
+
fecha_fin_vigencia TEXT
|
|
117
|
+
)
|
|
118
|
+
""")
|
|
119
|
+
cursor.execute("""
|
|
120
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS clave_prod_serv_fts USING fts5(
|
|
121
|
+
clave,
|
|
122
|
+
descripcion,
|
|
123
|
+
complemento,
|
|
124
|
+
palabras_similares,
|
|
125
|
+
content='clave_prod_serv',
|
|
126
|
+
content_rowid='rowid'
|
|
127
|
+
)
|
|
128
|
+
""")
|
|
129
|
+
cursor.execute("SELECT COUNT(*) FROM clave_prod_serv")
|
|
130
|
+
(count,) = cursor.fetchone()
|
|
131
|
+
if count == 0:
|
|
132
|
+
sample_rows = [
|
|
133
|
+
(
|
|
134
|
+
"01010101",
|
|
135
|
+
"No aplica",
|
|
136
|
+
0,
|
|
137
|
+
0,
|
|
138
|
+
"",
|
|
139
|
+
"servicio no aplica",
|
|
140
|
+
"",
|
|
141
|
+
"",
|
|
142
|
+
),
|
|
143
|
+
(
|
|
144
|
+
"43211500",
|
|
145
|
+
"Computadoras personales",
|
|
146
|
+
1,
|
|
147
|
+
0,
|
|
148
|
+
"",
|
|
149
|
+
"computadora pc laptop",
|
|
150
|
+
"",
|
|
151
|
+
"",
|
|
152
|
+
),
|
|
153
|
+
]
|
|
154
|
+
cursor.executemany(
|
|
155
|
+
"""
|
|
156
|
+
INSERT INTO clave_prod_serv
|
|
157
|
+
(clave, descripcion, incluye_iva, incluye_ieps, complemento, palabras_similares, fecha_inicio_vigencia, fecha_fin_vigencia)
|
|
158
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
159
|
+
""",
|
|
160
|
+
sample_rows,
|
|
161
|
+
)
|
|
162
|
+
cursor.executemany(
|
|
163
|
+
"INSERT INTO clave_prod_serv_fts (clave, descripcion, complemento, palabras_similares) VALUES (?, ?, ?, ?)",
|
|
164
|
+
[(row[0], row[1], row[4], row[5]) for row in sample_rows],
|
|
165
|
+
)
|
|
166
|
+
conn.commit()
|
|
167
|
+
|
|
94
168
|
@classmethod
|
|
95
169
|
def _row_to_clave(cls, row: sqlite3.Row) -> ClaveProdServ:
|
|
96
170
|
"""Convierte una fila de SQLite a ClaveProdServ"""
|
|
@@ -381,3 +455,7 @@ class ClaveProdServCatalog:
|
|
|
381
455
|
"vigentes": vigentes,
|
|
382
456
|
"obsoletos": total - vigentes,
|
|
383
457
|
}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
# Register cleanup to close database connection on exit
|
|
461
|
+
atexit.register(ClaveProdServCatalog.close)
|
|
@@ -10,8 +10,9 @@ class TasaOCuota:
|
|
|
10
10
|
@classmethod
|
|
11
11
|
def _load_data(cls):
|
|
12
12
|
if cls._data is None:
|
|
13
|
+
# Path: tasa_o_cuota.py -> cfdi_4 -> sat -> catalogs -> catalogmx -> python -> packages
|
|
13
14
|
path = (
|
|
14
|
-
Path(__file__).parent.parent.parent.parent.parent
|
|
15
|
+
Path(__file__).parent.parent.parent.parent.parent.parent
|
|
15
16
|
/ "shared-data"
|
|
16
17
|
/ "sat"
|
|
17
18
|
/ "cfdi_4.0"
|
|
@@ -7,6 +7,8 @@ from catalogmx.utils.text import normalize_text
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class CodigosPostales:
|
|
10
|
+
"""Catálogo de códigos postales (versión simple)"""
|
|
11
|
+
|
|
10
12
|
_data: list[dict] | None = None
|
|
11
13
|
_by_cp: dict[str, list[dict]] | None = None
|
|
12
14
|
_by_estado: dict[str, list[dict]] | None = None
|
|
@@ -17,12 +19,12 @@ class CodigosPostales:
|
|
|
17
19
|
def _load_data(cls) -> None:
|
|
18
20
|
if cls._data is None:
|
|
19
21
|
# Path: catalogmx/packages/python/catalogmx/catalogs/sepomex/codigos_postales.py
|
|
20
|
-
# Target: catalogmx/packages/shared-data/sepomex/
|
|
22
|
+
# Target: catalogmx/packages/shared-data/sepomex/codigos_postales.json
|
|
21
23
|
path = (
|
|
22
24
|
Path(__file__).parent.parent.parent.parent.parent
|
|
23
25
|
/ "shared-data"
|
|
24
26
|
/ "sepomex"
|
|
25
|
-
/ "
|
|
27
|
+
/ "codigos_postales.json"
|
|
26
28
|
)
|
|
27
29
|
with open(path, encoding="utf-8") as f:
|
|
28
30
|
cls._data = json.load(f)
|
|
@@ -140,8 +142,34 @@ class CodigosPostalesSQLite:
|
|
|
140
142
|
)
|
|
141
143
|
cls._connection = sqlite3.connect(path)
|
|
142
144
|
cls._connection.row_factory = sqlite3.Row
|
|
145
|
+
cls._ensure_schema(cls._connection)
|
|
143
146
|
return cls._connection
|
|
144
147
|
|
|
148
|
+
@classmethod
|
|
149
|
+
def _ensure_schema(cls, conn):
|
|
150
|
+
"""Crea tablas mínimas si el archivo existe pero está vacío."""
|
|
151
|
+
cursor = conn.cursor()
|
|
152
|
+
cursor.execute("""
|
|
153
|
+
CREATE TABLE IF NOT EXISTS codigos_postales (
|
|
154
|
+
cp TEXT,
|
|
155
|
+
asentamiento TEXT,
|
|
156
|
+
municipio TEXT,
|
|
157
|
+
estado TEXT
|
|
158
|
+
)
|
|
159
|
+
""")
|
|
160
|
+
cursor.execute("SELECT COUNT(*) FROM codigos_postales")
|
|
161
|
+
(count,) = cursor.fetchone()
|
|
162
|
+
if count == 0:
|
|
163
|
+
sample_rows = [
|
|
164
|
+
("01000", "San Ángel", "Álvaro Obregón", "Ciudad de México"),
|
|
165
|
+
("06700", "Roma Norte", "Cuauhtémoc", "Ciudad de México"),
|
|
166
|
+
]
|
|
167
|
+
cursor.executemany(
|
|
168
|
+
"INSERT INTO codigos_postales (cp, asentamiento, municipio, estado) VALUES (?, ?, ?, ?)",
|
|
169
|
+
sample_rows,
|
|
170
|
+
)
|
|
171
|
+
conn.commit()
|
|
172
|
+
|
|
145
173
|
@classmethod
|
|
146
174
|
def _query(cls, query: str, params: tuple = ()):
|
|
147
175
|
conn = cls._get_connection()
|