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
catalogmx/helpers.py ADDED
@@ -0,0 +1,324 @@
1
+ #!/usr/bin/python
2
+ """
3
+ Modern, user-friendly API for RFC and CURP generation and validation.
4
+
5
+ This module provides simple functions for common use cases, making it easier
6
+ to work with Mexican identification codes without dealing with class constructors.
7
+ """
8
+
9
+ import datetime
10
+
11
+ from .validators.curp import CURPGenerator, CURPValidator
12
+ from .validators.rfc import RFCGeneratorFisicas, RFCGeneratorMorales, RFCValidator
13
+
14
+ # ============================================================================
15
+ # RFC Helper Functions
16
+ # ============================================================================
17
+
18
+
19
+ def generate_rfc_persona_fisica(
20
+ nombre: str,
21
+ apellido_paterno: str,
22
+ apellido_materno: str,
23
+ fecha_nacimiento: datetime.date | str,
24
+ **kwargs,
25
+ ) -> str:
26
+ """
27
+ Generate RFC for a natural person (Persona Física).
28
+
29
+ Args:
30
+ nombre: First name(s)
31
+ apellido_paterno: Father's surname
32
+ apellido_materno: Mother's surname
33
+ fecha_nacimiento: Birth date (datetime.date or 'YYYY-MM-DD' string)
34
+ **kwargs: Additional arguments passed to RFCGeneratorFisicas
35
+
36
+ Returns:
37
+ str: 13-character RFC code
38
+
39
+ Example:
40
+ >>> rfc = generate_rfc_persona_fisica(
41
+ ... nombre='Juan',
42
+ ... apellido_paterno='Pérez',
43
+ ... apellido_materno='García',
44
+ ... fecha_nacimiento='1990-05-15'
45
+ ... )
46
+ >>> print(rfc) # PEGJ900515...
47
+ """
48
+ # Convert string date to datetime.date if needed
49
+ if isinstance(fecha_nacimiento, str):
50
+ fecha_nacimiento = datetime.datetime.strptime(fecha_nacimiento, "%Y-%m-%d").date()
51
+
52
+ generator = RFCGeneratorFisicas(
53
+ paterno=apellido_paterno,
54
+ materno=apellido_materno,
55
+ nombre=nombre,
56
+ fecha=fecha_nacimiento,
57
+ **kwargs,
58
+ )
59
+ return generator.rfc
60
+
61
+
62
+ def generate_rfc_persona_moral(
63
+ razon_social: str, fecha_constitucion: datetime.date | str, **kwargs
64
+ ) -> str:
65
+ """
66
+ Generate RFC for a legal entity (Persona Moral/company).
67
+
68
+ Args:
69
+ razon_social: Company name
70
+ fecha_constitucion: Constitution date (datetime.date or 'YYYY-MM-DD' string)
71
+ **kwargs: Additional arguments passed to RFCGeneratorMorales
72
+
73
+ Returns:
74
+ str: 12-character RFC code
75
+
76
+ Example:
77
+ >>> rfc = generate_rfc_persona_moral(
78
+ ... razon_social='Grupo Bimbo S.A.B. de C.V.',
79
+ ... fecha_constitucion='1981-06-15'
80
+ ... )
81
+ >>> print(rfc) # GBI810615...
82
+ """
83
+ # Convert string date to datetime.date if needed
84
+ if isinstance(fecha_constitucion, str):
85
+ fecha_constitucion = datetime.datetime.strptime(fecha_constitucion, "%Y-%m-%d").date()
86
+
87
+ generator = RFCGeneratorMorales(razon_social=razon_social, fecha=fecha_constitucion, **kwargs)
88
+ return generator.rfc
89
+
90
+
91
+ def validate_rfc(rfc: str, check_checksum: bool = True) -> bool:
92
+ """
93
+ Validate an RFC code.
94
+
95
+ Args:
96
+ rfc: RFC code to validate
97
+ check_checksum: Whether to validate the checksum digit (default: True)
98
+
99
+ Returns:
100
+ bool: True if valid, False otherwise
101
+
102
+ Example:
103
+ >>> validate_rfc('PEGJ900515KL8')
104
+ True
105
+ >>> validate_rfc('INVALID')
106
+ False
107
+ """
108
+ try:
109
+ validator = RFCValidator(rfc)
110
+ if not validator.validate_general_regex():
111
+ return False
112
+ if check_checksum:
113
+ return validator.validate_checksum()
114
+ return True
115
+ except:
116
+ return False
117
+
118
+
119
+ def detect_rfc_type(rfc: str) -> str | None:
120
+ """
121
+ Detect the type of RFC (Persona Física, Persona Moral, or Genérico).
122
+
123
+ Args:
124
+ rfc: RFC code to analyze
125
+
126
+ Returns:
127
+ str: 'fisica', 'moral', 'generico', or None if invalid
128
+
129
+ Example:
130
+ >>> detect_rfc_type('PEGJ900515KL8')
131
+ 'fisica'
132
+ >>> detect_rfc_type('GBI810615945')
133
+ 'moral'
134
+ """
135
+ try:
136
+ validator = RFCValidator(rfc)
137
+ tipo = validator.detect_fisica_moral()
138
+ if tipo == "Persona Física":
139
+ return "fisica"
140
+ elif tipo == "Persona Moral":
141
+ return "moral"
142
+ elif tipo == "Genérico":
143
+ return "generico"
144
+ return None
145
+ except:
146
+ return None
147
+
148
+
149
+ # ============================================================================
150
+ # CURP Helper Functions
151
+ # ============================================================================
152
+
153
+
154
+ def generate_curp(
155
+ nombre: str,
156
+ apellido_paterno: str,
157
+ apellido_materno: str | None,
158
+ fecha_nacimiento: datetime.date | str,
159
+ sexo: str,
160
+ estado: str,
161
+ differentiator: str | None = None,
162
+ ) -> str:
163
+ """
164
+ Generate a CURP code.
165
+
166
+ Args:
167
+ nombre: First name(s)
168
+ apellido_paterno: Father's surname
169
+ apellido_materno: Mother's surname (can be empty string or None)
170
+ fecha_nacimiento: Birth date (datetime.date or 'YYYY-MM-DD' string)
171
+ sexo: Gender ('H' for male, 'M' for female)
172
+ estado: Birth state (name or 2-letter code)
173
+ differentiator: Optional custom differentiator (position 17)
174
+
175
+ Returns:
176
+ str: 18-character CURP code
177
+
178
+ Example:
179
+ >>> curp = generate_curp(
180
+ ... nombre='Juan',
181
+ ... apellido_paterno='Pérez',
182
+ ... apellido_materno='García',
183
+ ... fecha_nacimiento='1990-05-15',
184
+ ... sexo='H',
185
+ ... estado='Jalisco'
186
+ ... )
187
+ >>> print(curp) # PEGJ900515HJCRRN...
188
+ """
189
+ # Convert string date to datetime.date if needed
190
+ if isinstance(fecha_nacimiento, str):
191
+ fecha_nacimiento = datetime.datetime.strptime(fecha_nacimiento, "%Y-%m-%d").date()
192
+
193
+ # Handle empty apellido_materno
194
+ if not apellido_materno:
195
+ apellido_materno = ""
196
+
197
+ generator = CURPGenerator(
198
+ nombre=nombre,
199
+ paterno=apellido_paterno,
200
+ materno=apellido_materno,
201
+ fecha_nacimiento=fecha_nacimiento,
202
+ sexo=sexo,
203
+ estado=estado,
204
+ )
205
+
206
+ # If custom differentiator is provided, regenerate homoclave
207
+ if differentiator is not None:
208
+ # Generate base CURP (first 16 characters)
209
+ base = (
210
+ generator.generate_letters()
211
+ + generator.generate_date()
212
+ + generator.sexo
213
+ + generator.get_state_code(generator.estado)
214
+ + generator.generate_consonants()
215
+ )
216
+ # Add custom differentiator and calculate check digit
217
+ check_digit = CURPGenerator.calculate_check_digit(base + differentiator)
218
+ return base + differentiator + check_digit
219
+
220
+ return generator.curp
221
+
222
+
223
+ def validate_curp(curp: str, check_digit: bool = True) -> bool:
224
+ """
225
+ Validate a CURP code.
226
+
227
+ Args:
228
+ curp: CURP code to validate
229
+ check_digit: Whether to validate the check digit (default: True)
230
+
231
+ Returns:
232
+ bool: True if valid, False otherwise
233
+
234
+ Example:
235
+ >>> validate_curp('PEGJ900515HJCRRN05')
236
+ True
237
+ >>> validate_curp('INVALID')
238
+ False
239
+ """
240
+ try:
241
+ validator = CURPValidator(curp)
242
+ if not validator.validate():
243
+ return False
244
+ if check_digit:
245
+ return validator.validate_check_digit()
246
+ return True
247
+ except:
248
+ return False
249
+
250
+
251
+ def get_curp_info(curp: str) -> dict | None:
252
+ """
253
+ Extract information from a CURP code.
254
+
255
+ Args:
256
+ curp: CURP code to analyze
257
+
258
+ Returns:
259
+ dict: Extracted information or None if invalid
260
+
261
+ Example:
262
+ >>> info = get_curp_info('PEGJ900515HJCRRN05')
263
+ >>> print(info['fecha_nacimiento'])
264
+ '1990-05-15'
265
+ >>> print(info['sexo'])
266
+ 'Hombre'
267
+ """
268
+ try:
269
+ validator = CURPValidator(curp)
270
+ if not validator.validate():
271
+ return None
272
+
273
+ # Extract information
274
+ year = int(curp[4:6])
275
+ # Assume year 2000+ if < 50, otherwise 1900+
276
+ year = 2000 + year if year < 50 else 1900 + year
277
+ month = int(curp[6:8])
278
+ day = int(curp[8:10])
279
+
280
+ sexo_code = curp[10]
281
+ estado_code = curp[11:13]
282
+
283
+ return {
284
+ "fecha_nacimiento": f"{year:04d}-{month:02d}-{day:02d}",
285
+ "sexo": "Hombre" if sexo_code == "H" else "Mujer",
286
+ "sexo_code": sexo_code,
287
+ "estado_code": estado_code,
288
+ "differentiator": curp[16],
289
+ "check_digit": curp[17],
290
+ "check_digit_valid": validator.validate_check_digit(),
291
+ }
292
+ except:
293
+ return None
294
+
295
+
296
+ # ============================================================================
297
+ # Quick validation functions
298
+ # ============================================================================
299
+
300
+
301
+ def is_valid_rfc(rfc: str) -> bool:
302
+ """Quick RFC validation. Alias for validate_rfc()."""
303
+ return validate_rfc(rfc)
304
+
305
+
306
+ def is_valid_curp(curp: str) -> bool:
307
+ """Quick CURP validation. Alias for validate_curp()."""
308
+ return validate_curp(curp)
309
+
310
+
311
+ # ============================================================================
312
+ # Path Helper Functions
313
+ # ============================================================================
314
+ from pathlib import Path
315
+
316
+
317
+ def get_project_root() -> Path:
318
+ """Returns the project root folder by searching for a .git directory."""
319
+ current_path = Path(__file__).parent
320
+ while current_path.parent != current_path:
321
+ if (current_path / ".git").exists():
322
+ return current_path
323
+ current_path = current_path.parent
324
+ raise FileNotFoundError("Project root not found. Could not find .git directory.")
@@ -0,0 +1,55 @@
1
+ """
2
+ Text normalization utilities for catalogmx
3
+ ==========================================
4
+
5
+ Provides accent-insensitive text normalization for searching across catalogs.
6
+ """
7
+
8
+ try:
9
+ from unidecode import unidecode
10
+ except ImportError:
11
+ # Fallback if unidecode not available
12
+ def unidecode(text: str) -> str:
13
+ """Fallback implementation using unicodedata."""
14
+ import unicodedata
15
+
16
+ nfd = unicodedata.normalize("NFD", text)
17
+ return "".join(char for char in nfd if unicodedata.category(char) != "Mn")
18
+
19
+
20
+ def normalize_text(text: str) -> str:
21
+ """
22
+ Normalize text by removing accents and converting to uppercase.
23
+ Makes text searchable without worrying about accents or case.
24
+
25
+ Args:
26
+ text: Text to normalize
27
+
28
+ Returns:
29
+ Normalized text (uppercase, no accents)
30
+
31
+ Examples:
32
+ >>> normalize_text("México")
33
+ 'MEXICO'
34
+ >>> normalize_text("San José")
35
+ 'SAN JOSE'
36
+ >>> normalize_text("Michoacán de Ocampo")
37
+ 'MICHOACAN DE OCAMPO'
38
+ """
39
+ return unidecode(text).upper()
40
+
41
+
42
+ def normalize_for_search(text: str) -> str:
43
+ """
44
+ Alias for normalize_text for clarity in search contexts.
45
+
46
+ Args:
47
+ text: Text to normalize for searching
48
+
49
+ Returns:
50
+ Normalized text suitable for accent-insensitive search
51
+ """
52
+ return normalize_text(text)
53
+
54
+
55
+ __all__ = ["normalize_text", "normalize_for_search", "unidecode"]
File without changes
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CLABE (Clave Bancaria Estandarizada) Validator
4
+
5
+ CLABE is the standardized 18-digit bank account number used in Mexico for
6
+ interbank electronic transfers (SPEI).
7
+
8
+ Structure:
9
+ - 3 digits: Bank code
10
+ - 3 digits: Branch/Plaza code
11
+ - 11 digits: Account number
12
+ - 1 digit: Check digit (modulo 10 algorithm)
13
+
14
+ Example: 002010077777777771
15
+ 002: Banamex
16
+ 010: Branch code
17
+ 07777777777: Account number
18
+ 1: Check digit
19
+ """
20
+
21
+
22
+ class CLABEException(Exception):
23
+ pass
24
+
25
+
26
+ class CLABELengthError(CLABEException):
27
+ pass
28
+
29
+
30
+ class CLABEStructureError(CLABEException):
31
+ pass
32
+
33
+
34
+ class CLABECheckDigitError(CLABEException):
35
+ pass
36
+
37
+
38
+ class CLABEValidator:
39
+ """
40
+ Validates CLABE (Clave Bancaria Estandarizada) bank account numbers
41
+ """
42
+
43
+ LENGTH = 18
44
+
45
+ # Weights for check digit calculation (positions 0-16)
46
+ WEIGHTS = [3, 7, 1] * 6 # Pattern repeats: 3,7,1,3,7,1,...
47
+
48
+ def __init__(self, clabe: str | None) -> None:
49
+ """
50
+ :param clabe: The CLABE number to validate
51
+ """
52
+ self.clabe = ""
53
+ if bool(clabe) and isinstance(clabe, str):
54
+ self.clabe = clabe.strip()
55
+
56
+ def validate(self) -> bool:
57
+ """
58
+ Validates the CLABE structure and check digit
59
+ :return: True if valid, raises exception if invalid
60
+ """
61
+ value = self.clabe
62
+
63
+ # Check length
64
+ if len(value) != self.LENGTH:
65
+ raise CLABELengthError(f"CLABE length must be {self.LENGTH} digits, got {len(value)}")
66
+
67
+ # Check if all characters are digits
68
+ if not value.isdigit():
69
+ raise CLABEStructureError("CLABE must contain only digits")
70
+
71
+ # Validate check digit
72
+ if not self.verify_check_digit(value):
73
+ raise CLABECheckDigitError("Invalid CLABE check digit")
74
+
75
+ return True
76
+
77
+ def is_valid(self) -> bool:
78
+ """
79
+ Checks if CLABE is valid without raising exceptions
80
+ :return: True if valid, False otherwise
81
+ """
82
+ try:
83
+ return self.validate()
84
+ except CLABEException:
85
+ return False
86
+
87
+ @classmethod
88
+ def calculate_check_digit(cls, clabe_17: str) -> str:
89
+ """
90
+ Calculates the check digit for a 17-digit CLABE
91
+
92
+ Algorithm:
93
+ 1. Multiply each digit by its weight (3,7,1 pattern)
94
+ 2. Take modulo 10 of each result
95
+ 3. Sum all results
96
+ 4. Take modulo 10 of the sum
97
+ 5. Subtract from 10
98
+ 6. Take modulo 10 of the result
99
+
100
+ :param clabe_17: First 17 digits of CLABE
101
+ :return: Check digit (0-9)
102
+ """
103
+ if len(clabe_17) != 17:
104
+ raise CLABELengthError("Need exactly 17 digits to calculate check digit")
105
+
106
+ if not clabe_17.isdigit():
107
+ raise CLABEStructureError("CLABE must contain only digits")
108
+
109
+ # Calculate weighted sum
110
+ weighted_sum = 0
111
+ for i, digit in enumerate(clabe_17):
112
+ product = int(digit) * cls.WEIGHTS[i]
113
+ weighted_sum += product % 10
114
+
115
+ # Calculate check digit
116
+ check_digit = (10 - (weighted_sum % 10)) % 10
117
+
118
+ return str(check_digit)
119
+
120
+ @classmethod
121
+ def verify_check_digit(cls, clabe: str) -> bool:
122
+ """
123
+ Verifies the check digit of an 18-digit CLABE
124
+
125
+ :param clabe: Complete 18-digit CLABE
126
+ :return: True if check digit is valid, False otherwise
127
+ """
128
+ if len(clabe) != cls.LENGTH:
129
+ return False
130
+
131
+ calculated = cls.calculate_check_digit(clabe[:17])
132
+ return calculated == clabe[17]
133
+
134
+ def get_bank_code(self) -> str | None:
135
+ """
136
+ Extracts the bank code (first 3 digits)
137
+ :return: Bank code as string
138
+ """
139
+ if len(self.clabe) >= 3:
140
+ return self.clabe[:3]
141
+ return None
142
+
143
+ def get_branch_code(self) -> str | None:
144
+ """
145
+ Extracts the branch/plaza code (digits 4-6)
146
+ :return: Branch code as string
147
+ """
148
+ if len(self.clabe) >= 6:
149
+ return self.clabe[3:6]
150
+ return None
151
+
152
+ def get_account_number(self) -> str | None:
153
+ """
154
+ Extracts the account number (digits 7-17)
155
+ :return: Account number as string
156
+ """
157
+ if len(self.clabe) >= 17:
158
+ return self.clabe[6:17]
159
+ return None
160
+
161
+ def get_check_digit(self) -> str | None:
162
+ """
163
+ Extracts the check digit (digit 18)
164
+ :return: Check digit as string
165
+ """
166
+ if len(self.clabe) == self.LENGTH:
167
+ return self.clabe[17]
168
+ return None
169
+
170
+ def get_parts(self) -> dict[str, str] | None:
171
+ """
172
+ Returns all CLABE parts as a dictionary
173
+ :return: Dictionary with bank_code, branch_code, account_number, check_digit
174
+ """
175
+ if not self.is_valid():
176
+ return None
177
+
178
+ return {
179
+ "bank_code": self.get_bank_code(),
180
+ "branch_code": self.get_branch_code(),
181
+ "account_number": self.get_account_number(),
182
+ "check_digit": self.get_check_digit(),
183
+ "clabe": self.clabe,
184
+ }
185
+
186
+
187
+ def validate_clabe(clabe: str | None) -> bool:
188
+ """
189
+ Helper function to validate a CLABE
190
+
191
+ :param clabe: CLABE number as string
192
+ :return: True if valid, False otherwise
193
+ """
194
+ validator = CLABEValidator(clabe)
195
+ return validator.is_valid()
196
+
197
+
198
+ def generate_clabe(bank_code: str | int, branch_code: str | int, account_number: str | int) -> str:
199
+ """
200
+ Generates a complete CLABE with check digit
201
+
202
+ :param bank_code: 3-digit bank code
203
+ :param branch_code: 3-digit branch code
204
+ :param account_number: 11-digit account number
205
+ :return: Complete 18-digit CLABE
206
+ """
207
+ # Ensure all parts are strings and properly formatted
208
+ bank_code = str(bank_code).zfill(3)
209
+ branch_code = str(branch_code).zfill(3)
210
+ account_number = str(account_number).zfill(11)
211
+
212
+ if len(bank_code) != 3:
213
+ raise CLABEStructureError("Bank code must be 3 digits")
214
+ if len(branch_code) != 3:
215
+ raise CLABEStructureError("Branch code must be 3 digits")
216
+ if len(account_number) != 11:
217
+ raise CLABEStructureError("Account number must be 11 digits")
218
+
219
+ clabe_17 = bank_code + branch_code + account_number
220
+ check_digit = CLABEValidator.calculate_check_digit(clabe_17)
221
+
222
+ return clabe_17 + check_digit
223
+
224
+
225
+ def get_clabe_info(clabe: str | None) -> dict[str, str] | None:
226
+ """
227
+ Helper function to get information from a CLABE
228
+
229
+ :param clabe: CLABE number as string
230
+ :return: Dictionary with CLABE parts or None if invalid
231
+ """
232
+ validator = CLABEValidator(clabe)
233
+ return validator.get_parts()