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
catalogmx/helpers.py CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/python
2
2
  """
3
- Modern, user-friendly API for RFC and CURP generation and validation.
3
+ Modern, user-friendly API for RFC, CURP, CLABE, and NSS validation and generation.
4
4
 
5
5
  This module provides simple functions for common use cases, making it easier
6
6
  to work with Mexican identification codes without dealing with class constructors.
@@ -8,7 +8,9 @@ to work with Mexican identification codes without dealing with class constructor
8
8
 
9
9
  import datetime
10
10
 
11
+ from .validators.clabe import CLABEValidator, generate_clabe, generate_clabe_random
11
12
  from .validators.curp import CURPGenerator, CURPValidator
13
+ from .validators.nss import NSSValidator
12
14
  from .validators.rfc import RFCGeneratorFisicas, RFCGeneratorMorales, RFCValidator
13
15
 
14
16
  # ============================================================================
@@ -21,7 +23,7 @@ def generate_rfc_persona_fisica(
21
23
  apellido_paterno: str,
22
24
  apellido_materno: str,
23
25
  fecha_nacimiento: datetime.date | str,
24
- **kwargs,
26
+ **kwargs: object,
25
27
  ) -> str:
26
28
  """
27
29
  Generate RFC for a natural person (Persona Física).
@@ -60,7 +62,7 @@ def generate_rfc_persona_fisica(
60
62
 
61
63
 
62
64
  def generate_rfc_persona_moral(
63
- razon_social: str, fecha_constitucion: datetime.date | str, **kwargs
65
+ razon_social: str, fecha_constitucion: datetime.date | str, **kwargs: object
64
66
  ) -> str:
65
67
  """
66
68
  Generate RFC for a legal entity (Persona Moral/company).
@@ -308,6 +310,150 @@ def is_valid_curp(curp: str) -> bool:
308
310
  return validate_curp(curp)
309
311
 
310
312
 
313
+ # ============================================================================
314
+ # CLABE Helper Functions
315
+ # ============================================================================
316
+
317
+
318
+ def validate_clabe(clabe: str) -> bool:
319
+ """
320
+ Validate a CLABE (Clave Bancaria Estandarizada) number.
321
+
322
+ Args:
323
+ clabe: CLABE number to validate (18 digits)
324
+
325
+ Returns:
326
+ bool: True if valid, False otherwise
327
+
328
+ Example:
329
+ >>> validate_clabe('002010077777777771')
330
+ True
331
+ >>> validate_clabe('INVALID')
332
+ False
333
+ """
334
+ try:
335
+ validator = CLABEValidator(clabe)
336
+ return validator.is_valid()
337
+ except Exception:
338
+ return False
339
+
340
+
341
+ def is_valid_clabe(clabe: str) -> bool:
342
+ """Quick CLABE validation. Alias for validate_clabe()."""
343
+ return validate_clabe(clabe)
344
+
345
+
346
+ def get_clabe_info(clabe: str) -> dict | None:
347
+ """
348
+ Extract information from a CLABE number.
349
+
350
+ Args:
351
+ clabe: CLABE number to analyze
352
+
353
+ Returns:
354
+ dict: Extracted information or None if invalid
355
+ - bank_code: 3-digit bank code
356
+ - branch_code: 3-digit branch/plaza code
357
+ - account_number: 11-digit account number
358
+ - check_digit: 1-digit check digit
359
+ - is_valid: True if CLABE is valid
360
+
361
+ Example:
362
+ >>> info = get_clabe_info('002010077777777771')
363
+ >>> print(info['bank_code'])
364
+ '002'
365
+ """
366
+ try:
367
+ if not clabe or len(clabe) != 18 or not clabe.isdigit():
368
+ return None
369
+
370
+ validator = CLABEValidator(clabe)
371
+ is_valid = validator.is_valid()
372
+
373
+ return {
374
+ "bank_code": clabe[:3],
375
+ "branch_code": clabe[3:6],
376
+ "account_number": clabe[6:17],
377
+ "check_digit": clabe[17],
378
+ "is_valid": is_valid,
379
+ }
380
+ except Exception:
381
+ return None
382
+
383
+
384
+ # ============================================================================
385
+ # NSS Helper Functions
386
+ # ============================================================================
387
+
388
+
389
+ def validate_nss(nss: str) -> bool:
390
+ """
391
+ Validate an NSS (Número de Seguridad Social) number.
392
+
393
+ Args:
394
+ nss: NSS number to validate (11 digits)
395
+
396
+ Returns:
397
+ bool: True if valid, False otherwise
398
+
399
+ Example:
400
+ >>> validate_nss('12345678903')
401
+ True
402
+ >>> validate_nss('INVALID')
403
+ False
404
+ """
405
+ try:
406
+ validator = NSSValidator(nss)
407
+ return validator.is_valid()
408
+ except Exception:
409
+ return False
410
+
411
+
412
+ def is_valid_nss(nss: str) -> bool:
413
+ """Quick NSS validation. Alias for validate_nss()."""
414
+ return validate_nss(nss)
415
+
416
+
417
+ def get_nss_info(nss: str) -> dict | None:
418
+ """
419
+ Extract information from an NSS number.
420
+
421
+ Args:
422
+ nss: NSS number to analyze
423
+
424
+ Returns:
425
+ dict: Extracted information or None if invalid
426
+ - subdelegation: 2-digit subdelegation code
427
+ - registration_year: 2-digit registration year
428
+ - birth_year: 2-digit birth year
429
+ - sequential: 4-digit sequential number
430
+ - check_digit: 1-digit check digit
431
+ - is_valid: True if NSS is valid
432
+
433
+ Example:
434
+ >>> info = get_nss_info('12345678903')
435
+ >>> print(info['subdelegation'])
436
+ '12'
437
+ """
438
+ try:
439
+ if not nss or len(nss) != 11 or not nss.isdigit():
440
+ return None
441
+
442
+ validator = NSSValidator(nss)
443
+ is_valid = validator.is_valid()
444
+
445
+ return {
446
+ "subdelegation": nss[:2],
447
+ "registration_year": nss[2:4],
448
+ "birth_year": nss[4:6],
449
+ "sequential": nss[6:10],
450
+ "check_digit": nss[10],
451
+ "is_valid": is_valid,
452
+ }
453
+ except Exception:
454
+ return None
455
+
456
+
311
457
  # ============================================================================
312
458
  # Path Helper Functions
313
459
  # ============================================================================
@@ -322,3 +468,31 @@ def get_project_root() -> Path:
322
468
  return current_path
323
469
  current_path = current_path.parent
324
470
  raise FileNotFoundError("Project root not found. Could not find .git directory.")
471
+
472
+
473
+ # Re-export CLABE generators for module-level access
474
+ __all__ = [
475
+ # RFC
476
+ "generate_rfc_persona_fisica",
477
+ "generate_rfc_persona_moral",
478
+ "validate_rfc",
479
+ "detect_rfc_type",
480
+ "is_valid_rfc",
481
+ # CURP
482
+ "generate_curp",
483
+ "validate_curp",
484
+ "get_curp_info",
485
+ "is_valid_curp",
486
+ # CLABE
487
+ "generate_clabe",
488
+ "generate_clabe_random",
489
+ "validate_clabe",
490
+ "get_clabe_info",
491
+ "is_valid_clabe",
492
+ # NSS
493
+ "validate_nss",
494
+ "get_nss_info",
495
+ "is_valid_nss",
496
+ # Utility
497
+ "get_project_root",
498
+ ]
@@ -0,0 +1,29 @@
1
+ """
2
+ catalogmx - Utility modules
3
+ """
4
+
5
+ from catalogmx.utils.clabe_utils import (
6
+ decode_clabe,
7
+ describe_clabe,
8
+ format_clabe,
9
+ generate_clabe_examples,
10
+ generate_clabe_for_bank,
11
+ generate_clabe_random,
12
+ get_common_banks,
13
+ get_plaza_suggestions,
14
+ )
15
+ from catalogmx.utils.text import normalize_text
16
+
17
+ __all__ = [
18
+ # Text utilities
19
+ "normalize_text",
20
+ # CLABE utilities
21
+ "decode_clabe",
22
+ "generate_clabe_random",
23
+ "generate_clabe_examples",
24
+ "generate_clabe_for_bank",
25
+ "get_common_banks",
26
+ "get_plaza_suggestions",
27
+ "format_clabe",
28
+ "describe_clabe",
29
+ ]
@@ -0,0 +1,417 @@
1
+ """
2
+ CLABE Utilities - Enhanced CLABE generation, decoding, and validation
3
+
4
+ This module provides comprehensive CLABE (Clave Bancaria Estandarizada) utilities
5
+ including generation with optional parameters, decoding with full bank/plaza info,
6
+ and example generation.
7
+
8
+ Examples:
9
+ >>> from catalogmx.utils.clabe_utils import generate_clabe_random, decode_clabe
10
+ >>>
11
+ >>> # Generate a random CLABE for BBVA in CDMX
12
+ >>> clabe = generate_clabe_random(bank_code="012", plaza_code="180")
13
+ >>> print(clabe)
14
+ 012180XXXXXXXXXXX
15
+ >>>
16
+ >>> # Decode a CLABE to get full info
17
+ >>> info = decode_clabe("002010077777777771")
18
+ >>> print(info["bank"]["name"])
19
+ BANAMEX
20
+ """
21
+
22
+ import random
23
+ from typing import TYPE_CHECKING, TypedDict
24
+
25
+ # Lazy imports to avoid circular dependencies
26
+ if TYPE_CHECKING:
27
+ pass
28
+
29
+
30
+ def _get_bank_catalog():
31
+ """Lazy import of BankCatalog to avoid circular imports."""
32
+ from catalogmx.catalogs.banxico.banks import BankCatalog
33
+
34
+ return BankCatalog
35
+
36
+
37
+ def _get_plaza_catalog():
38
+ """Lazy import of CodigosPlazaCatalog to avoid circular imports."""
39
+ from catalogmx.catalogs.banxico.codigos_plaza import CodigosPlazaCatalog
40
+
41
+ return CodigosPlazaCatalog
42
+
43
+
44
+ def _get_clabe_functions():
45
+ """Lazy import of CLABE functions to avoid circular imports."""
46
+ from catalogmx.validators.clabe import generate_clabe, validate_clabe
47
+
48
+ return generate_clabe, validate_clabe
49
+
50
+
51
+ class BankInfo(TypedDict, total=False):
52
+ """Bank information from catalog."""
53
+
54
+ code: str
55
+ name: str
56
+ full_name: str
57
+ rfc: str | None
58
+ spei: bool
59
+
60
+
61
+ class PlazaInfo(TypedDict, total=False):
62
+ """Plaza information from catalog."""
63
+
64
+ codigo: str
65
+ plaza: str
66
+ estado: str
67
+ cve_entidad: str
68
+
69
+
70
+ class DecodedCLABE(TypedDict, total=False):
71
+ """Full decoded CLABE information."""
72
+
73
+ clabe: str
74
+ is_valid: bool
75
+ bank_code: str
76
+ plaza_code: str
77
+ account_number: str
78
+ check_digit: str
79
+ bank: BankInfo | None
80
+ plaza: PlazaInfo | None
81
+ all_plazas: list[PlazaInfo] # Some plaza codes map to multiple locations
82
+
83
+
84
+ def decode_clabe(clabe: str) -> DecodedCLABE | None:
85
+ """
86
+ Decode a CLABE to extract all information including bank and plaza details.
87
+
88
+ Args:
89
+ clabe: 18-digit CLABE number
90
+
91
+ Returns:
92
+ Dictionary with full CLABE breakdown or None if format is invalid
93
+
94
+ Examples:
95
+ >>> info = decode_clabe("002010077777777771")
96
+ >>> info["is_valid"]
97
+ True
98
+ >>> info["bank"]["name"]
99
+ 'BANAMEX'
100
+ >>> info["bank"]["full_name"]
101
+ 'Banco Nacional de México, S.A.'
102
+
103
+ >>> # Invalid check digit
104
+ >>> info = decode_clabe("002010077777777770")
105
+ >>> info["is_valid"]
106
+ False
107
+
108
+ >>> # Get plaza info
109
+ >>> info = decode_clabe("012180000000000001")
110
+ >>> info["plaza"]["plaza"]
111
+ 'Ciudad de México'
112
+ """
113
+ clabe = clabe.strip() if clabe else ""
114
+
115
+ if len(clabe) != 18 or not clabe.isdigit():
116
+ return None
117
+
118
+ bank_code = clabe[:3]
119
+ plaza_code = clabe[3:6]
120
+ account_number = clabe[6:17]
121
+ check_digit = clabe[17]
122
+
123
+ # Lazy imports
124
+ BankCatalog = _get_bank_catalog()
125
+ CodigosPlazaCatalog = _get_plaza_catalog()
126
+ _, validate_clabe = _get_clabe_functions()
127
+
128
+ # Get bank info
129
+ bank_data = BankCatalog.get_bank_by_code(bank_code)
130
+ bank_info: BankInfo | None = None
131
+ if bank_data:
132
+ bank_info = {
133
+ "code": bank_data["code"],
134
+ "name": bank_data["name"],
135
+ "full_name": bank_data.get("full_name", ""),
136
+ "rfc": bank_data.get("rfc"),
137
+ "spei": bank_data.get("spei", False),
138
+ }
139
+
140
+ # Get plaza info (may have multiple matches)
141
+ all_plazas = CodigosPlazaCatalog.buscar_por_codigo(plaza_code)
142
+ plaza_info: PlazaInfo | None = None
143
+ if all_plazas:
144
+ plaza_info = all_plazas[0] # Return first match as primary
145
+
146
+ return {
147
+ "clabe": clabe,
148
+ "is_valid": validate_clabe(clabe),
149
+ "bank_code": bank_code,
150
+ "plaza_code": plaza_code,
151
+ "account_number": account_number,
152
+ "check_digit": check_digit,
153
+ "bank": bank_info,
154
+ "plaza": plaza_info,
155
+ "all_plazas": all_plazas,
156
+ }
157
+
158
+
159
+ def generate_clabe_random(
160
+ bank_code: str | None = None,
161
+ plaza_code: str | None = None,
162
+ account_number: str | None = None,
163
+ ) -> str:
164
+ """
165
+ Generate a valid CLABE with optional parameters.
166
+
167
+ Any parameter not provided will be randomly generated.
168
+
169
+ Args:
170
+ bank_code: 3-digit bank code (optional, e.g., "012" for BBVA)
171
+ plaza_code: 3-digit plaza code (optional, e.g., "180" for CDMX)
172
+ account_number: 11-digit account number (optional)
173
+
174
+ Returns:
175
+ Valid 18-digit CLABE
176
+
177
+ Examples:
178
+ >>> # Fully random CLABE
179
+ >>> clabe = generate_clabe_random()
180
+ >>> len(clabe)
181
+ 18
182
+
183
+ >>> # CLABE for BBVA (code 012)
184
+ >>> clabe = generate_clabe_random(bank_code="012")
185
+ >>> clabe[:3]
186
+ '012'
187
+
188
+ >>> # CLABE for any bank in CDMX (plaza 180)
189
+ >>> clabe = generate_clabe_random(plaza_code="180")
190
+ >>> clabe[3:6]
191
+ '180'
192
+
193
+ >>> # Fully specified
194
+ >>> clabe = generate_clabe_random("012", "180", "12345678901")
195
+ >>> clabe
196
+ '012180123456789010'
197
+ """
198
+ # Lazy imports
199
+ BankCatalog = _get_bank_catalog()
200
+ CodigosPlazaCatalog = _get_plaza_catalog()
201
+ generate_clabe, _ = _get_clabe_functions()
202
+
203
+ # Get or generate bank code
204
+ if bank_code is None:
205
+ all_banks = BankCatalog.get_spei_banks()
206
+ if all_banks:
207
+ bank_code = random.choice(all_banks)["code"]
208
+ else:
209
+ bank_code = str(random.randint(1, 999)).zfill(3)
210
+
211
+ # Get or generate plaza code
212
+ if plaza_code is None:
213
+ all_plazas = CodigosPlazaCatalog.get_all()
214
+ if all_plazas:
215
+ plaza_code = random.choice(all_plazas)["codigo"]
216
+ else:
217
+ plaza_code = str(random.randint(1, 999)).zfill(3)
218
+
219
+ # Get or generate account number
220
+ if account_number is None:
221
+ account_number = str(random.randint(0, 99999999999)).zfill(11)
222
+
223
+ return generate_clabe(bank_code, plaza_code, account_number)
224
+
225
+
226
+ def generate_clabe_examples(count: int = 5) -> list[dict]:
227
+ """
228
+ Generate example CLABEs with full decoded information.
229
+
230
+ Useful for testing, demos, and documentation.
231
+
232
+ Args:
233
+ count: Number of examples to generate (default 5)
234
+
235
+ Returns:
236
+ List of decoded CLABE dictionaries
237
+
238
+ Examples:
239
+ >>> examples = generate_clabe_examples(3)
240
+ >>> len(examples)
241
+ 3
242
+ >>> examples[0]["is_valid"]
243
+ True
244
+ >>> examples[0]["bank"]["name"] # Will vary
245
+ 'BBVA'
246
+ """
247
+ examples = []
248
+ for _ in range(count):
249
+ clabe = generate_clabe_random()
250
+ decoded = decode_clabe(clabe)
251
+ if decoded:
252
+ examples.append(decoded)
253
+ return examples
254
+
255
+
256
+ def generate_clabe_for_bank(bank_name: str, plaza_name: str | None = None) -> str | None:
257
+ """
258
+ Generate a CLABE for a specific bank (by name).
259
+
260
+ Args:
261
+ bank_name: Bank name (case-insensitive, e.g., "BBVA", "Banamex")
262
+ plaza_name: Optional plaza name (case-insensitive)
263
+
264
+ Returns:
265
+ Valid 18-digit CLABE or None if bank not found
266
+
267
+ Examples:
268
+ >>> clabe = generate_clabe_for_bank("BBVA")
269
+ >>> decode_clabe(clabe)["bank"]["name"]
270
+ 'BBVA'
271
+
272
+ >>> clabe = generate_clabe_for_bank("Santander", "Guadalajara")
273
+ >>> info = decode_clabe(clabe)
274
+ >>> info["bank"]["name"]
275
+ 'SANTANDER'
276
+ """
277
+ # Lazy imports
278
+ BankCatalog = _get_bank_catalog()
279
+ CodigosPlazaCatalog = _get_plaza_catalog()
280
+
281
+ bank = BankCatalog.get_bank_by_name(bank_name)
282
+ if not bank:
283
+ return None
284
+
285
+ bank_code = bank["code"]
286
+
287
+ plaza_code = None
288
+ if plaza_name:
289
+ plazas = CodigosPlazaCatalog.buscar_por_plaza(plaza_name)
290
+ if plazas:
291
+ plaza_code = plazas[0]["codigo"]
292
+
293
+ return generate_clabe_random(bank_code=bank_code, plaza_code=plaza_code)
294
+
295
+
296
+ def get_common_banks() -> list[dict]:
297
+ """
298
+ Get list of common banks used for CLABE generation.
299
+
300
+ Returns:
301
+ List of bank dictionaries with code, name, full_name
302
+
303
+ Examples:
304
+ >>> banks = get_common_banks()
305
+ >>> len(banks) > 10
306
+ True
307
+ >>> banks[0]["name"]
308
+ 'BANAMEX'
309
+ """
310
+ BankCatalog = _get_bank_catalog()
311
+ return BankCatalog.get_spei_banks()
312
+
313
+
314
+ def get_plaza_suggestions(query: str) -> list[dict]:
315
+ """
316
+ Search plazas by name for autocomplete/suggestions.
317
+
318
+ Args:
319
+ query: Search query (partial name)
320
+
321
+ Returns:
322
+ List of matching plazas
323
+
324
+ Examples:
325
+ >>> plazas = get_plaza_suggestions("Guada")
326
+ >>> any(p["plaza"] == "Guadalajara" for p in plazas)
327
+ True
328
+ """
329
+ CodigosPlazaCatalog = _get_plaza_catalog()
330
+ return CodigosPlazaCatalog.search(query)
331
+
332
+
333
+ def format_clabe(clabe: str, separator: str = "-") -> str:
334
+ """
335
+ Format a CLABE with separators for readability.
336
+
337
+ Args:
338
+ clabe: 18-digit CLABE
339
+ separator: Separator character (default "-")
340
+
341
+ Returns:
342
+ Formatted CLABE: XXX-XXX-XXXXXXXXXXX-X
343
+
344
+ Examples:
345
+ >>> format_clabe("002010077777777771")
346
+ '002-010-07777777777-1'
347
+
348
+ >>> format_clabe("002010077777777771", " ")
349
+ '002 010 07777777777 1'
350
+ """
351
+ if len(clabe) != 18:
352
+ return clabe
353
+ return f"{clabe[:3]}{separator}{clabe[3:6]}{separator}{clabe[6:17]}{separator}{clabe[17]}"
354
+
355
+
356
+ def describe_clabe(clabe: str) -> str:
357
+ """
358
+ Get a human-readable description of a CLABE.
359
+
360
+ Args:
361
+ clabe: 18-digit CLABE
362
+
363
+ Returns:
364
+ Human-readable description
365
+
366
+ Examples:
367
+ >>> print(describe_clabe("002010077777777771"))
368
+ CLABE: 002-010-07777777777-1
369
+ Estado: Válida ✓
370
+ Banco: BANAMEX (Banco Nacional de México, S.A.)
371
+ Plaza: Aguascalientes, Aguascalientes
372
+ Cuenta: 07777777777
373
+ """
374
+ info = decode_clabe(clabe)
375
+ if not info:
376
+ return f"CLABE inválida: {clabe}"
377
+
378
+ lines = [
379
+ f"CLABE: {format_clabe(clabe)}",
380
+ f"Estado: {'Válida ✓' if info['is_valid'] else 'Inválida ✗'}",
381
+ ]
382
+
383
+ if info["bank"]:
384
+ bank = info["bank"]
385
+ bank_line = f"Banco: {bank['name']}"
386
+ if bank.get("full_name"):
387
+ bank_line += f" ({bank['full_name']})"
388
+ lines.append(bank_line)
389
+ else:
390
+ lines.append(f"Banco: Código {info['bank_code']} (no encontrado)")
391
+
392
+ if info["plaza"]:
393
+ plaza = info["plaza"]
394
+ lines.append(f"Plaza: {plaza['plaza']}, {plaza['estado']}")
395
+ if len(info["all_plazas"]) > 1:
396
+ lines.append(f" (Nota: {len(info['all_plazas'])} ubicaciones con este código)")
397
+ else:
398
+ lines.append(f"Plaza: Código {info['plaza_code']} (no encontrado)")
399
+
400
+ lines.append(f"Cuenta: {info['account_number']}")
401
+
402
+ return "\n".join(lines)
403
+
404
+
405
+ __all__ = [
406
+ "decode_clabe",
407
+ "generate_clabe_random",
408
+ "generate_clabe_examples",
409
+ "generate_clabe_for_bank",
410
+ "get_common_banks",
411
+ "get_plaza_suggestions",
412
+ "format_clabe",
413
+ "describe_clabe",
414
+ "DecodedCLABE",
415
+ "BankInfo",
416
+ "PlazaInfo",
417
+ ]
catalogmx/utils/text.py CHANGED
@@ -5,8 +5,13 @@ Text normalization utilities for catalogmx
5
5
  Provides accent-insensitive text normalization for searching across catalogs.
6
6
  """
7
7
 
8
+ from collections.abc import Callable
9
+ from typing import cast
10
+
8
11
  try:
9
12
  from unidecode import unidecode
13
+
14
+ unidecode = cast(Callable[[str], str], unidecode)
10
15
  except ImportError:
11
16
  # Fallback if unidecode not available
12
17
  def unidecode(text: str) -> str:
@@ -36,7 +41,8 @@ def normalize_text(text: str) -> str:
36
41
  >>> normalize_text("Michoacán de Ocampo")
37
42
  'MICHOACAN DE OCAMPO'
38
43
  """
39
- return unidecode(text).upper()
44
+ result = unidecode(text)
45
+ return str(result).upper()
40
46
 
41
47
 
42
48
  def normalize_for_search(text: str) -> str: