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,47 @@
1
+ """Catálogo c_TipoRegimen"""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+
7
+ class TipoRegimenCatalog:
8
+ _data: list[dict] | None = None
9
+ _by_code: dict[str, dict] | None = None
10
+
11
+ @classmethod
12
+ def _load_data(cls) -> None:
13
+ if cls._data is None:
14
+ path = (
15
+ Path(__file__).parent.parent.parent.parent.parent.parent
16
+ / "shared-data"
17
+ / "sat"
18
+ / "nomina_1.2"
19
+ / "tipo_regimen.json"
20
+ )
21
+ with open(path, encoding="utf-8") as f:
22
+ data = json.load(f)
23
+ # Handle both list and dict formats
24
+ cls._data = data if isinstance(data, list) else data.get("regimenes", data)
25
+ cls._by_code = {item["code"]: item for item in cls._data}
26
+
27
+ @classmethod
28
+ def get_regimen(cls, code: str) -> dict | None:
29
+ """Obtiene tipo de régimen por código"""
30
+ cls._load_data()
31
+ return cls._by_code.get(code)
32
+
33
+ @classmethod
34
+ def is_valid(cls, code: str) -> bool:
35
+ """Verifica si un código de régimen es válido"""
36
+ return cls.get_regimen(code) is not None
37
+
38
+ @classmethod
39
+ def get_all(cls) -> list[dict]:
40
+ """Obtiene todos los tipos de régimen"""
41
+ cls._load_data()
42
+ return cls._data.copy()
43
+
44
+ @classmethod
45
+ def is_asimilado(cls, code: str) -> bool:
46
+ """Verifica si es régimen asimilado a salarios"""
47
+ return code in ["05", "06", "07", "08", "09", "10", "11", "12", "13"]
@@ -0,0 +1,5 @@
1
+ """Catálogos SEPOMEX"""
2
+
3
+ from .codigos_postales import CodigosPostales
4
+
5
+ __all__ = ["CodigosPostales"]
@@ -0,0 +1,184 @@
1
+ """Catálogo de Códigos Postales SEPOMEX"""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from catalogmx.utils.text import normalize_text
7
+
8
+
9
+ class CodigosPostales:
10
+ _data: list[dict] | None = None
11
+ _by_cp: dict[str, list[dict]] | None = None
12
+ _by_estado: dict[str, list[dict]] | None = None
13
+ _by_estado_normalized: dict[str, list[dict]] | None = None
14
+ _by_municipio_normalized: dict[str, list[dict]] | None = None
15
+
16
+ @classmethod
17
+ def _load_data(cls) -> None:
18
+ if cls._data is None:
19
+ # Path: catalogmx/packages/python/catalogmx/catalogs/sepomex/codigos_postales.py
20
+ # Target: catalogmx/packages/shared-data/sepomex/codigos_postales_completo.json
21
+ path = (
22
+ Path(__file__).parent.parent.parent.parent.parent
23
+ / "shared-data"
24
+ / "sepomex"
25
+ / "codigos_postales_completo.json"
26
+ )
27
+ with open(path, encoding="utf-8") as f:
28
+ cls._data = json.load(f)
29
+
30
+ # Index by CP (can have multiple settlements)
31
+ cls._by_cp = {}
32
+ for item in cls._data:
33
+ cp = item["cp"]
34
+ if cp not in cls._by_cp:
35
+ cls._by_cp[cp] = []
36
+ cls._by_cp[cp].append(item)
37
+
38
+ # Index by estado
39
+ cls._by_estado = {}
40
+ for item in cls._data:
41
+ estado = item["estado"]
42
+ if estado not in cls._by_estado:
43
+ cls._by_estado[estado] = []
44
+ cls._by_estado[estado].append(item)
45
+
46
+ # Index by estado normalized (accent-insensitive)
47
+ cls._by_estado_normalized = {}
48
+ for item in cls._data:
49
+ estado_norm = normalize_text(item["estado"])
50
+ if estado_norm not in cls._by_estado_normalized:
51
+ cls._by_estado_normalized[estado_norm] = []
52
+ cls._by_estado_normalized[estado_norm].append(item)
53
+
54
+ # Index by municipio normalized (accent-insensitive)
55
+ cls._by_municipio_normalized = {}
56
+ for item in cls._data:
57
+ municipio_norm = normalize_text(item["municipio"])
58
+ if municipio_norm not in cls._by_municipio_normalized:
59
+ cls._by_municipio_normalized[municipio_norm] = []
60
+ cls._by_municipio_normalized[municipio_norm].append(item)
61
+
62
+ @classmethod
63
+ def get_by_cp(cls, cp: str) -> list[dict]:
64
+ """Obtiene todos los asentamientos de un código postal"""
65
+ cls._load_data()
66
+ return cls._by_cp.get(cp, [])
67
+
68
+ @classmethod
69
+ def is_valid(cls, cp: str) -> bool:
70
+ """Verifica si un código postal existe"""
71
+ cls._load_data()
72
+ return cp in cls._by_cp
73
+
74
+ @classmethod
75
+ def get_by_estado(cls, estado: str) -> list[dict]:
76
+ """Obtiene todos los códigos postales de un estado (insensible a acentos)"""
77
+ cls._load_data()
78
+ estado_normalized = normalize_text(estado)
79
+ return cls._by_estado_normalized.get(estado_normalized, [])
80
+
81
+ @classmethod
82
+ def get_by_municipio(cls, municipio: str) -> list[dict]:
83
+ """Obtiene todos los códigos postales de un municipio (insensible a acentos)"""
84
+ cls._load_data()
85
+ municipio_normalized = normalize_text(municipio)
86
+ return cls._by_municipio_normalized.get(municipio_normalized, [])
87
+
88
+ @classmethod
89
+ def search_by_colonia(cls, colonia: str) -> list[dict]:
90
+ """Busca códigos postales por nombre de colonia (insensible a acentos)"""
91
+ cls._load_data()
92
+ colonia_normalized = normalize_text(colonia)
93
+ return [
94
+ item for item in cls._data if colonia_normalized in normalize_text(item["asentamiento"])
95
+ ]
96
+
97
+ @classmethod
98
+ def get_all(cls) -> list[dict]:
99
+ """Obtiene todos los códigos postales"""
100
+ cls._load_data()
101
+ return cls._data.copy()
102
+
103
+ @classmethod
104
+ def get_municipio(cls, cp: str) -> str | None:
105
+ """Obtiene el municipio de un código postal"""
106
+ settlements = cls.get_by_cp(cp)
107
+ return settlements[0]["municipio"] if settlements else None
108
+
109
+ @classmethod
110
+ def get_estado(cls, cp: str) -> str | None:
111
+ """Obtiene el estado de un código postal"""
112
+ settlements = cls.get_by_cp(cp)
113
+ return settlements[0]["estado"] if settlements else None
114
+
115
+
116
+ class CodigosPostalesSQLite:
117
+ _db_path = None
118
+ _connection = None
119
+
120
+ @classmethod
121
+ def _get_db_path(cls):
122
+ if cls._db_path is None:
123
+ cls._db_path = (
124
+ Path(__file__).parent.parent.parent.parent.parent
125
+ / "shared-data"
126
+ / "sqlite"
127
+ / "sepomex.db"
128
+ )
129
+ return cls._db_path
130
+
131
+ @classmethod
132
+ def _get_connection(cls):
133
+ import sqlite3
134
+
135
+ if cls._connection is None:
136
+ path = cls._get_db_path()
137
+ if not path.exists():
138
+ raise FileNotFoundError(
139
+ f"Database not found at {path}. Please run the migration script."
140
+ )
141
+ cls._connection = sqlite3.connect(path)
142
+ cls._connection.row_factory = sqlite3.Row
143
+ return cls._connection
144
+
145
+ @classmethod
146
+ def _query(cls, query: str, params: tuple = ()):
147
+ conn = cls._get_connection()
148
+ cursor = conn.cursor()
149
+ cursor.execute(query, params)
150
+ rows = cursor.fetchall()
151
+ return [dict(row) for row in rows]
152
+
153
+ @classmethod
154
+ def get_by_cp(cls, cp: str) -> list[dict]:
155
+ """Obtiene todos los asentamientos de un código postal desde SQLite"""
156
+ return cls._query("SELECT * FROM codigos_postales WHERE cp = ?", (cp,))
157
+
158
+ @classmethod
159
+ def is_valid(cls, cp: str) -> bool:
160
+ """Verifica si un código postal existe en SQLite"""
161
+ result = cls._query("SELECT 1 FROM codigos_postales WHERE cp = ? LIMIT 1", (cp,))
162
+ return len(result) > 0
163
+
164
+ @classmethod
165
+ def get_by_estado(cls, estado: str) -> list[dict]:
166
+ """Obtiene todos los códigos postales de un estado desde SQLite"""
167
+ return cls._query("SELECT * FROM codigos_postales WHERE estado = ?", (estado,))
168
+
169
+ @classmethod
170
+ def get_all(cls) -> list[dict]:
171
+ """Obtiene todos los códigos postales desde SQLite"""
172
+ return cls._query("SELECT * FROM codigos_postales")
173
+
174
+ @classmethod
175
+ def get_municipio(cls, cp: str) -> str | None:
176
+ """Obtiene el municipio de un código postal desde SQLite"""
177
+ settlements = cls.get_by_cp(cp)
178
+ return settlements[0]["municipio"] if settlements else None
179
+
180
+ @classmethod
181
+ def get_estado(cls, cp: str) -> str | None:
182
+ """Obtiene el estado de un código postal desde SQLite"""
183
+ settlements = cls.get_by_cp(cp)
184
+ return settlements[0]["estado"] if settlements else None
catalogmx/cli.py ADDED
@@ -0,0 +1,185 @@
1
+ """
2
+ Module that contains the command line app.
3
+
4
+ Why does this file exist, and why not put this in __main__?
5
+
6
+ You might be tempted to import things from __main__ later, but that will cause
7
+ problems: the code will get executed twice:
8
+
9
+ - When you run `python -mrfcmx` python will execute
10
+ ``__main__.py`` as a script. That means there won't be any
11
+ ``rfcmx.__main__`` in ``sys.modules``.
12
+ - When you import __main__ it will get executed again (as a module) because
13
+ there's no ``rfcmx.__main__`` in ``sys.modules``.
14
+
15
+ Also see (1) from http://click.pocoo.org/5/setuptools/#setuptools-integration
16
+ """
17
+
18
+ import datetime
19
+
20
+ import click
21
+
22
+ from catalogmx.validators.curp import CURPGenerator, CURPValidator
23
+ from catalogmx.validators.rfc import RFCGenerator, RFCValidator
24
+
25
+
26
+ @click.group()
27
+ @click.version_option(version="0.2.0")
28
+ def main():
29
+ """
30
+ Mexican RFC and CURP calculator and validator.
31
+
32
+ This tool helps you generate and validate:
33
+ - RFC (Registro Federal de Contribuyentes) for individuals and companies
34
+ - CURP (Clave Única de Registro de Población) for individuals
35
+ """
36
+ pass
37
+
38
+
39
+ @main.group()
40
+ def rfc():
41
+ """RFC (Registro Federal de Contribuyentes) commands"""
42
+ pass
43
+
44
+
45
+ @main.group()
46
+ def curp():
47
+ """CURP (Clave Única de Registro de Población) commands"""
48
+ pass
49
+
50
+
51
+ @rfc.command("validate")
52
+ @click.argument("rfc_code")
53
+ def rfc_validate(rfc_code):
54
+ """Validate an RFC code"""
55
+ validator = RFCValidator(rfc_code)
56
+
57
+ if validator.validate():
58
+ click.echo(click.style(f"✓ RFC {rfc_code} is valid", fg="green"))
59
+ tipo = validator.detect_fisica_moral()
60
+ click.echo(f" Type: {tipo}")
61
+
62
+ # Show validation details
63
+ validations = validator.validators()
64
+ click.echo("\n Validation details:")
65
+ for name, result in validations.items():
66
+ status = "✓" if result else "✗"
67
+ color = "green" if result else "red"
68
+ click.echo(f" {click.style(status, fg=color)} {name}")
69
+ else:
70
+ click.echo(click.style(f"✗ RFC {rfc_code} is invalid", fg="red"))
71
+
72
+
73
+ @rfc.command("generate-fisica")
74
+ @click.option("--nombre", "-n", required=True, help="First name(s)")
75
+ @click.option("--paterno", "-p", required=True, help="First surname (apellido paterno)")
76
+ @click.option("--materno", "-m", default="", help="Second surname (apellido materno)")
77
+ @click.option("--fecha", "-f", required=True, help="Birth date (YYYY-MM-DD)")
78
+ def rfc_generate_fisica(nombre, paterno, materno, fecha):
79
+ """Generate RFC for Persona Física (individual)"""
80
+ try:
81
+ # Parse date
82
+ fecha_obj = datetime.datetime.strptime(fecha, "%Y-%m-%d").date()
83
+
84
+ # Generate RFC
85
+ rfc_code = RFCGenerator.generate_fisica(
86
+ nombre=nombre, paterno=paterno, materno=materno, fecha=fecha_obj
87
+ )
88
+
89
+ click.echo(click.style(f"\nGenerated RFC: {rfc_code}", fg="green", bold=True))
90
+ click.echo(f"\nName: {nombre} {paterno} {materno}")
91
+ click.echo(f"Birth date: {fecha}")
92
+
93
+ except ValueError as e:
94
+ click.echo(click.style(f"Error: {str(e)}", fg="red"))
95
+ except Exception as e:
96
+ click.echo(click.style(f"Unexpected error: {str(e)}", fg="red"))
97
+
98
+
99
+ @rfc.command("generate-moral")
100
+ @click.option("--razon-social", "-r", required=True, help="Company name (razón social)")
101
+ @click.option("--fecha", "-f", required=True, help="Incorporation date (YYYY-MM-DD)")
102
+ def rfc_generate_moral(razon_social, fecha):
103
+ """Generate RFC for Persona Moral (company/legal entity)"""
104
+ try:
105
+ # Parse date
106
+ fecha_obj = datetime.datetime.strptime(fecha, "%Y-%m-%d").date()
107
+
108
+ # Generate RFC
109
+ rfc_code = RFCGenerator.generate_moral(razon_social=razon_social, fecha=fecha_obj)
110
+
111
+ click.echo(click.style(f"\nGenerated RFC: {rfc_code}", fg="green", bold=True))
112
+ click.echo(f"\nCompany: {razon_social}")
113
+ click.echo(f"Incorporation date: {fecha}")
114
+
115
+ except ValueError as e:
116
+ click.echo(click.style(f"Error: {str(e)}", fg="red"))
117
+ except Exception as e:
118
+ click.echo(click.style(f"Unexpected error: {str(e)}", fg="red"))
119
+
120
+
121
+ @curp.command("validate")
122
+ @click.argument("curp_code")
123
+ def curp_validate(curp_code):
124
+ """Validate a CURP code"""
125
+ validator = CURPValidator(curp_code)
126
+
127
+ if validator.is_valid():
128
+ click.echo(click.style(f"✓ CURP {curp_code} is valid", fg="green"))
129
+ else:
130
+ click.echo(click.style(f"✗ CURP {curp_code} is invalid", fg="red"))
131
+
132
+
133
+ @curp.command("generate")
134
+ @click.option("--nombre", "-n", required=True, help="First name(s)")
135
+ @click.option("--paterno", "-p", required=True, help="First surname (apellido paterno)")
136
+ @click.option("--materno", "-m", default="", help="Second surname (apellido materno)")
137
+ @click.option("--fecha", "-f", required=True, help="Birth date (YYYY-MM-DD)")
138
+ @click.option(
139
+ "--sexo",
140
+ "-s",
141
+ required=True,
142
+ type=click.Choice(["H", "M"], case_sensitive=False),
143
+ help="Gender: H (Hombre/Male) or M (Mujer/Female)",
144
+ )
145
+ @click.option("--estado", "-e", required=True, help="Birth state (e.g., Jalisco, CDMX, etc.)")
146
+ def curp_generate(nombre, paterno, materno, fecha, sexo, estado):
147
+ """Generate CURP for an individual"""
148
+ try:
149
+ # Parse date
150
+ fecha_obj = datetime.datetime.strptime(fecha, "%Y-%m-%d").date()
151
+
152
+ # Generate CURP
153
+ generator = CURPGenerator(
154
+ nombre=nombre,
155
+ paterno=paterno,
156
+ materno=materno,
157
+ fecha_nacimiento=fecha_obj,
158
+ sexo=sexo.upper(),
159
+ estado=estado,
160
+ )
161
+
162
+ curp_code = generator.curp
163
+
164
+ click.echo(click.style(f"\nGenerated CURP: {curp_code}", fg="green", bold=True))
165
+ click.echo(f"\nName: {nombre} {paterno} {materno}")
166
+ click.echo(f"Birth date: {fecha}")
167
+ click.echo(f"Gender: {sexo.upper()}")
168
+ click.echo(f"Birth state: {estado}")
169
+
170
+ # Show a note about homoclave
171
+ click.echo(
172
+ click.style(
173
+ '\nNote: The homoclave (last 2 characters) is a placeholder ("00").', fg="yellow"
174
+ )
175
+ )
176
+ click.echo(click.style("The official homoclave is assigned by RENAPO.", fg="yellow"))
177
+
178
+ except ValueError as e:
179
+ click.echo(click.style(f"Error: {str(e)}", fg="red"))
180
+ except Exception as e:
181
+ click.echo(click.style(f"Unexpected error: {str(e)}", fg="red"))
182
+
183
+
184
+ if __name__ == "__main__":
185
+ main()