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
@@ -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 [m for m in cls._data if m.get("class", m.get("clase_riesgo", "")).startswith(hazard_class)]
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"
@@ -1,5 +1,6 @@
1
1
  """Catálogos SEPOMEX"""
2
2
 
3
3
  from .codigos_postales import CodigosPostales
4
+ from .codigos_postales_completo import CodigosPostalesCompleto
4
5
 
5
- __all__ = ["CodigosPostales"]
6
+ __all__ = ["CodigosPostales", "CodigosPostalesCompleto"]
@@ -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/codigos_postales_completo.json
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
- / "codigos_postales_completo.json"
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()