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
catalogmx/helpers.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/python
|
|
2
2
|
"""
|
|
3
|
-
Modern, user-friendly API for RFC and
|
|
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
|
-
|
|
44
|
+
result = unidecode(text)
|
|
45
|
+
return str(result).upper()
|
|
40
46
|
|
|
41
47
|
|
|
42
48
|
def normalize_for_search(text: str) -> str:
|