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
@@ -0,0 +1,582 @@
1
+ """
2
+ Mexican Identity Generator
3
+
4
+ Generates complete Mexican identities with realistic data for testing.
5
+ Uses Faker for random data generation and catalogmx validators for
6
+ RFC, CURP, CLABE, and NSS generation.
7
+
8
+ Usage:
9
+ from catalogmx.generators import generate_identity
10
+
11
+ # Generate a random persona física
12
+ identity = generate_identity()
13
+
14
+ # Generate with specific parameters
15
+ identity = generate_identity(
16
+ sexo='M',
17
+ estado='Jalisco',
18
+ min_age=25,
19
+ max_age=45
20
+ )
21
+
22
+ print(identity['nombre_completo'])
23
+ print(identity['rfc'])
24
+ print(identity['curp'])
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import random
30
+ from dataclasses import dataclass, field
31
+ from datetime import date, timedelta
32
+ from typing import Literal
33
+
34
+ try:
35
+ from faker import Faker
36
+ except ImportError:
37
+ Faker = None # type: ignore
38
+
39
+ from catalogmx.helpers import (
40
+ generate_clabe,
41
+ generate_curp,
42
+ generate_rfc_persona_fisica,
43
+ generate_rfc_persona_moral,
44
+ )
45
+ from catalogmx.validators.nss import generate_nss
46
+
47
+
48
+ def _get_faker() -> Faker:
49
+ """Get a Faker instance with Mexican locale."""
50
+ if Faker is None:
51
+ raise ImportError(
52
+ "Faker is required for identity generation. " "Install it with: pip install faker"
53
+ )
54
+ return Faker("es_MX")
55
+
56
+
57
+ # Mexican states with CURP codes
58
+ ESTADOS_MEXICO = [
59
+ {"name": "AGUASCALIENTES", "code": "AS", "inegi": "01"},
60
+ {"name": "BAJA CALIFORNIA", "code": "BC", "inegi": "02"},
61
+ {"name": "BAJA CALIFORNIA SUR", "code": "BS", "inegi": "03"},
62
+ {"name": "CAMPECHE", "code": "CC", "inegi": "04"},
63
+ {"name": "CHIAPAS", "code": "CS", "inegi": "07"},
64
+ {"name": "CHIHUAHUA", "code": "CH", "inegi": "08"},
65
+ {"name": "CIUDAD DE MEXICO", "code": "DF", "inegi": "09"},
66
+ {"name": "COAHUILA", "code": "CL", "inegi": "05"},
67
+ {"name": "COLIMA", "code": "CM", "inegi": "06"},
68
+ {"name": "DURANGO", "code": "DG", "inegi": "10"},
69
+ {"name": "ESTADO DE MEXICO", "code": "MC", "inegi": "15"},
70
+ {"name": "GUANAJUATO", "code": "GT", "inegi": "11"},
71
+ {"name": "GUERRERO", "code": "GR", "inegi": "12"},
72
+ {"name": "HIDALGO", "code": "HG", "inegi": "13"},
73
+ {"name": "JALISCO", "code": "JC", "inegi": "14"},
74
+ {"name": "MICHOACAN", "code": "MN", "inegi": "16"},
75
+ {"name": "MORELOS", "code": "MS", "inegi": "17"},
76
+ {"name": "NAYARIT", "code": "NT", "inegi": "18"},
77
+ {"name": "NUEVO LEON", "code": "NL", "inegi": "19"},
78
+ {"name": "OAXACA", "code": "OC", "inegi": "20"},
79
+ {"name": "PUEBLA", "code": "PL", "inegi": "21"},
80
+ {"name": "QUERETARO", "code": "QT", "inegi": "22"},
81
+ {"name": "QUINTANA ROO", "code": "QR", "inegi": "23"},
82
+ {"name": "SAN LUIS POTOSI", "code": "SP", "inegi": "24"},
83
+ {"name": "SINALOA", "code": "SL", "inegi": "25"},
84
+ {"name": "SONORA", "code": "SR", "inegi": "26"},
85
+ {"name": "TABASCO", "code": "TC", "inegi": "27"},
86
+ {"name": "TAMAULIPAS", "code": "TS", "inegi": "28"},
87
+ {"name": "TLAXCALA", "code": "TL", "inegi": "29"},
88
+ {"name": "VERACRUZ", "code": "VZ", "inegi": "30"},
89
+ {"name": "YUCATAN", "code": "YN", "inegi": "31"},
90
+ {"name": "ZACATECAS", "code": "ZS", "inegi": "32"},
91
+ ]
92
+
93
+ # Common Mexican banks with CLABE codes
94
+ BANCOS_MEXICO = [
95
+ {"code": "002", "name": "BANAMEX"},
96
+ {"code": "012", "name": "BBVA MEXICO"},
97
+ {"code": "014", "name": "SANTANDER"},
98
+ {"code": "021", "name": "HSBC"},
99
+ {"code": "030", "name": "BAJIO"},
100
+ {"code": "036", "name": "INBURSA"},
101
+ {"code": "044", "name": "SCOTIABANK"},
102
+ {"code": "058", "name": "BANREGIO"},
103
+ {"code": "072", "name": "BANORTE"},
104
+ {"code": "127", "name": "AZTECA"},
105
+ {"code": "130", "name": "COMPARTAMOS"},
106
+ {"code": "137", "name": "BANCOPPEL"},
107
+ ]
108
+
109
+ # Regímenes fiscales for persona física
110
+ REGIMENES_PERSONA_FISICA = [
111
+ {"code": "605", "name": "Sueldos y Salarios e Ingresos Asimilados a Salarios"},
112
+ {"code": "606", "name": "Arrendamiento"},
113
+ {"code": "612", "name": "Personas Físicas con Actividades Empresariales y Profesionales"},
114
+ {"code": "621", "name": "Incorporación Fiscal"},
115
+ {
116
+ "code": "625",
117
+ "name": "Régimen de las Actividades Empresariales con ingresos a través de Plataformas Tecnológicas",
118
+ },
119
+ {"code": "626", "name": "Régimen Simplificado de Confianza"},
120
+ ]
121
+
122
+
123
+ @dataclass
124
+ class PersonaFisica:
125
+ """Represents a Mexican natural person (persona física)."""
126
+
127
+ nombre: str
128
+ apellido_paterno: str
129
+ apellido_materno: str
130
+ fecha_nacimiento: date
131
+ sexo: Literal["H", "M"]
132
+ estado_nacimiento: str
133
+ estado_nacimiento_code: str
134
+
135
+ # Calculated identifiers
136
+ rfc: str = ""
137
+ curp: str = ""
138
+ nss: str = ""
139
+
140
+ # Banking
141
+ banco: dict = field(default_factory=dict)
142
+ clabe: str = ""
143
+ cuenta: str = ""
144
+ tarjeta_debito: str = ""
145
+
146
+ # Address
147
+ calle: str = ""
148
+ numero_exterior: str = ""
149
+ numero_interior: str = ""
150
+ colonia: str = ""
151
+ codigo_postal: str = ""
152
+ municipio: str = ""
153
+ estado: str = ""
154
+
155
+ # Contact
156
+ telefono: str = ""
157
+ email: str = ""
158
+
159
+ # Fiscal
160
+ regimen_fiscal: dict = field(default_factory=dict)
161
+
162
+ @property
163
+ def nombre_completo(self) -> str:
164
+ """Full name."""
165
+ return f"{self.nombre} {self.apellido_paterno} {self.apellido_materno}".strip()
166
+
167
+ @property
168
+ def edad(self) -> int:
169
+ """Age in years."""
170
+ today = date.today()
171
+ return (
172
+ today.year
173
+ - self.fecha_nacimiento.year
174
+ - ((today.month, today.day) < (self.fecha_nacimiento.month, self.fecha_nacimiento.day))
175
+ )
176
+
177
+ def to_dict(self) -> dict:
178
+ """Convert to dictionary."""
179
+ return {
180
+ "tipo": "persona_fisica",
181
+ "nombre": self.nombre,
182
+ "apellido_paterno": self.apellido_paterno,
183
+ "apellido_materno": self.apellido_materno,
184
+ "nombre_completo": self.nombre_completo,
185
+ "fecha_nacimiento": self.fecha_nacimiento.isoformat(),
186
+ "edad": self.edad,
187
+ "sexo": self.sexo,
188
+ "sexo_descripcion": "Masculino" if self.sexo == "H" else "Femenino",
189
+ "estado_nacimiento": self.estado_nacimiento,
190
+ "estado_nacimiento_code": self.estado_nacimiento_code,
191
+ "rfc": self.rfc,
192
+ "curp": self.curp,
193
+ "nss": self.nss,
194
+ "banco": self.banco,
195
+ "clabe": self.clabe,
196
+ "cuenta": self.cuenta,
197
+ "tarjeta_debito": self.tarjeta_debito,
198
+ "direccion": {
199
+ "calle": self.calle,
200
+ "numero_exterior": self.numero_exterior,
201
+ "numero_interior": self.numero_interior,
202
+ "colonia": self.colonia,
203
+ "codigo_postal": self.codigo_postal,
204
+ "municipio": self.municipio,
205
+ "estado": self.estado,
206
+ },
207
+ "telefono": self.telefono,
208
+ "email": self.email,
209
+ "regimen_fiscal": self.regimen_fiscal,
210
+ }
211
+
212
+
213
+ @dataclass
214
+ class PersonaMoral:
215
+ """Represents a Mexican legal entity (persona moral)."""
216
+
217
+ razon_social: str
218
+ fecha_constitucion: date
219
+
220
+ # Calculated identifiers
221
+ rfc: str = ""
222
+
223
+ # Banking
224
+ banco: dict = field(default_factory=dict)
225
+ clabe: str = ""
226
+ cuenta: str = ""
227
+
228
+ # Address
229
+ calle: str = ""
230
+ numero_exterior: str = ""
231
+ numero_interior: str = ""
232
+ colonia: str = ""
233
+ codigo_postal: str = ""
234
+ municipio: str = ""
235
+ estado: str = ""
236
+
237
+ # Contact
238
+ telefono: str = ""
239
+ email: str = ""
240
+
241
+ # Fiscal
242
+ regimen_fiscal: dict = field(default_factory=dict)
243
+
244
+ def to_dict(self) -> dict:
245
+ """Convert to dictionary."""
246
+ return {
247
+ "tipo": "persona_moral",
248
+ "razon_social": self.razon_social,
249
+ "fecha_constitucion": self.fecha_constitucion.isoformat(),
250
+ "rfc": self.rfc,
251
+ "banco": self.banco,
252
+ "clabe": self.clabe,
253
+ "cuenta": self.cuenta,
254
+ "direccion": {
255
+ "calle": self.calle,
256
+ "numero_exterior": self.numero_exterior,
257
+ "numero_interior": self.numero_interior,
258
+ "colonia": self.colonia,
259
+ "codigo_postal": self.codigo_postal,
260
+ "municipio": self.municipio,
261
+ "estado": self.estado,
262
+ },
263
+ "telefono": self.telefono,
264
+ "email": self.email,
265
+ "regimen_fiscal": self.regimen_fiscal,
266
+ }
267
+
268
+
269
+ class IdentityGenerator:
270
+ """
271
+ Generator for Mexican identities.
272
+
273
+ Uses Faker for random data and catalogmx for RFC, CURP, CLABE, NSS generation.
274
+ """
275
+
276
+ def __init__(self, seed: int | None = None):
277
+ """
278
+ Initialize the generator.
279
+
280
+ Args:
281
+ seed: Optional random seed for reproducible results
282
+ """
283
+ self.faker = _get_faker()
284
+ if seed is not None:
285
+ self.faker.seed_instance(seed)
286
+ random.seed(seed)
287
+
288
+ def generate_persona_fisica(
289
+ self,
290
+ sexo: Literal["H", "M"] | None = None,
291
+ estado: str | None = None,
292
+ min_age: int = 18,
293
+ max_age: int = 65,
294
+ include_banking: bool = True,
295
+ include_address: bool = True,
296
+ include_contact: bool = True,
297
+ ) -> PersonaFisica:
298
+ """
299
+ Generate a random Mexican persona física.
300
+
301
+ Args:
302
+ sexo: Gender ('H' for male, 'M' for female), random if None
303
+ estado: Birth state name, random if None
304
+ min_age: Minimum age (default 18)
305
+ max_age: Maximum age (default 65)
306
+ include_banking: Include banking details
307
+ include_address: Include address
308
+ include_contact: Include contact info
309
+
310
+ Returns:
311
+ PersonaFisica with all generated data
312
+ """
313
+ # Random sex if not specified
314
+ if sexo is None:
315
+ sexo = random.choice(["H", "M"])
316
+
317
+ # Generate name based on sex
318
+ if sexo == "H":
319
+ nombre = self.faker.first_name_male()
320
+ else:
321
+ nombre = self.faker.first_name_female()
322
+
323
+ apellido_paterno = self.faker.last_name()
324
+ apellido_materno = self.faker.last_name()
325
+
326
+ # Random birth date within age range
327
+ today = date.today()
328
+ min_birth = today - timedelta(days=max_age * 365)
329
+ max_birth = today - timedelta(days=min_age * 365)
330
+ days_range = (max_birth - min_birth).days
331
+ fecha_nacimiento = min_birth + timedelta(days=random.randint(0, days_range))
332
+
333
+ # Random state if not specified
334
+ if estado:
335
+ estado_info = next(
336
+ (e for e in ESTADOS_MEXICO if e["name"].upper() == estado.upper()),
337
+ random.choice(ESTADOS_MEXICO),
338
+ )
339
+ else:
340
+ estado_info = random.choice(ESTADOS_MEXICO)
341
+
342
+ estado_nacimiento = estado_info["name"]
343
+ estado_nacimiento_code = estado_info["code"]
344
+
345
+ # Generate RFC
346
+ rfc = generate_rfc_persona_fisica(
347
+ nombre=nombre,
348
+ apellido_paterno=apellido_paterno,
349
+ apellido_materno=apellido_materno,
350
+ fecha_nacimiento=fecha_nacimiento,
351
+ )
352
+
353
+ # Generate CURP
354
+ curp = generate_curp(
355
+ nombre=nombre,
356
+ apellido_paterno=apellido_paterno,
357
+ apellido_materno=apellido_materno,
358
+ fecha_nacimiento=fecha_nacimiento,
359
+ sexo=sexo,
360
+ estado=estado_nacimiento,
361
+ )
362
+
363
+ # Generate NSS
364
+ subdelegation = random.randint(1, 97)
365
+ registration_year = random.randint(0, 99)
366
+ birth_year = fecha_nacimiento.year % 100
367
+ sequential = random.randint(0, 9999)
368
+ nss = generate_nss(subdelegation, registration_year, birth_year, sequential)
369
+
370
+ persona = PersonaFisica(
371
+ nombre=nombre,
372
+ apellido_paterno=apellido_paterno,
373
+ apellido_materno=apellido_materno,
374
+ fecha_nacimiento=fecha_nacimiento,
375
+ sexo=sexo,
376
+ estado_nacimiento=estado_nacimiento,
377
+ estado_nacimiento_code=estado_nacimiento_code,
378
+ rfc=rfc,
379
+ curp=curp,
380
+ nss=nss,
381
+ regimen_fiscal=random.choice(REGIMENES_PERSONA_FISICA),
382
+ )
383
+
384
+ if include_banking:
385
+ self._add_banking(persona)
386
+
387
+ if include_address:
388
+ self._add_address(persona)
389
+
390
+ if include_contact:
391
+ self._add_contact(persona, nombre, apellido_paterno)
392
+
393
+ return persona
394
+
395
+ def generate_persona_moral(
396
+ self,
397
+ include_banking: bool = True,
398
+ include_address: bool = True,
399
+ include_contact: bool = True,
400
+ ) -> PersonaMoral:
401
+ """
402
+ Generate a random Mexican persona moral (company).
403
+
404
+ Args:
405
+ include_banking: Include banking details
406
+ include_address: Include address
407
+ include_contact: Include contact info
408
+
409
+ Returns:
410
+ PersonaMoral with all generated data
411
+ """
412
+ # Generate company name
413
+ company_types = ["S.A. de C.V.", "S.A.P.I. de C.V.", "S. de R.L. de C.V.", "S.C."]
414
+ company_name = self.faker.company()
415
+ razon_social = f"{company_name} {random.choice(company_types)}"
416
+
417
+ # Random constitution date (1-50 years ago)
418
+ today = date.today()
419
+ min_date = today - timedelta(days=50 * 365)
420
+ max_date = today - timedelta(days=365)
421
+ days_range = (max_date - min_date).days
422
+ fecha_constitucion = min_date + timedelta(days=random.randint(0, days_range))
423
+
424
+ # Generate RFC
425
+ rfc = generate_rfc_persona_moral(
426
+ razon_social=razon_social,
427
+ fecha_constitucion=fecha_constitucion,
428
+ )
429
+
430
+ persona = PersonaMoral(
431
+ razon_social=razon_social,
432
+ fecha_constitucion=fecha_constitucion,
433
+ rfc=rfc,
434
+ regimen_fiscal={"code": "601", "name": "General de Ley Personas Morales"},
435
+ )
436
+
437
+ if include_banking:
438
+ self._add_banking(persona)
439
+
440
+ if include_address:
441
+ self._add_address(persona)
442
+
443
+ if include_contact:
444
+ # Use company name for email domain
445
+ domain = company_name.lower().replace(" ", "").replace(",", "")[:15]
446
+ persona.email = f"contacto@{domain}.com.mx"
447
+ persona.telefono = self._generate_phone()
448
+
449
+ return persona
450
+
451
+ def _add_banking(self, persona: PersonaFisica | PersonaMoral) -> None:
452
+ """Add banking information to persona."""
453
+ banco = random.choice(BANCOS_MEXICO)
454
+ persona.banco = banco
455
+
456
+ # Generate CLABE
457
+ branch = str(random.randint(1, 999)).zfill(3)
458
+ account = str(random.randint(0, 99999999999)).zfill(11)
459
+ persona.clabe = generate_clabe(banco["code"], branch, account)
460
+ persona.cuenta = account
461
+
462
+ # Generate debit card (Visa or Mastercard)
463
+ card_type = random.choice(["visa", "mastercard"])
464
+ if card_type == "visa":
465
+ prefix = "4"
466
+ persona.tarjeta_debito = prefix + "".join(str(random.randint(0, 9)) for _ in range(15))
467
+ else:
468
+ prefix = "5" + str(random.randint(1, 5))
469
+ persona.tarjeta_debito = prefix + "".join(str(random.randint(0, 9)) for _ in range(14))
470
+
471
+ # Add Luhn check digit
472
+ persona.tarjeta_debito = self._add_luhn_checksum(persona.tarjeta_debito[:-1])
473
+
474
+ def _add_luhn_checksum(self, card_number: str) -> str:
475
+ """Calculate and append Luhn checksum digit."""
476
+ digits = [int(d) for d in card_number]
477
+ odd_digits = digits[-1::-2]
478
+ even_digits = digits[-2::-2]
479
+
480
+ total = sum(odd_digits)
481
+ for d in even_digits:
482
+ total += sum(divmod(d * 2, 10))
483
+
484
+ check_digit = (10 - (total % 10)) % 10
485
+ return card_number + str(check_digit)
486
+
487
+ def _add_address(self, persona: PersonaFisica | PersonaMoral) -> None:
488
+ """Add address information to persona."""
489
+ persona.calle = self.faker.street_name()
490
+ persona.numero_exterior = str(random.randint(1, 9999))
491
+ persona.numero_interior = "" if random.random() > 0.3 else str(random.randint(1, 50))
492
+ persona.colonia = self.faker.city_suffix() + " " + self.faker.last_name()
493
+ persona.codigo_postal = str(random.randint(1000, 99999)).zfill(5)
494
+ persona.municipio = self.faker.city()
495
+ persona.estado = random.choice(ESTADOS_MEXICO)["name"].title()
496
+
497
+ def _add_contact(self, persona: PersonaFisica, nombre: str, apellido: str) -> None:
498
+ """Add contact information to persona física."""
499
+ persona.telefono = self._generate_phone()
500
+
501
+ # Generate email
502
+ email_styles = [
503
+ f"{nombre.lower()}_{apellido.lower()}",
504
+ f"{nombre.lower()}.{apellido.lower()}",
505
+ f"{nombre.lower()}{random.randint(1, 99)}",
506
+ f"{apellido.lower()}.{nombre.lower()}",
507
+ ]
508
+ email_domains = ["gmail.com", "hotmail.com", "outlook.com", "yahoo.com.mx", "proton.me"]
509
+
510
+ email_user = random.choice(email_styles)
511
+ # Normalize email (remove accents)
512
+ import unicodedata
513
+
514
+ email_user = unicodedata.normalize("NFD", email_user)
515
+ email_user = "".join(c for c in email_user if unicodedata.category(c) != "Mn")
516
+
517
+ persona.email = f"{email_user}@{random.choice(email_domains)}"
518
+
519
+ def _generate_phone(self) -> str:
520
+ """Generate a Mexican phone number (10 digits)."""
521
+ # Mexican area codes (LADA)
522
+ ladas = ["55", "33", "81", "222", "442", "477", "614", "656", "664", "744", "998"]
523
+ lada = random.choice(ladas)
524
+
525
+ remaining = 10 - len(lada)
526
+ number = "".join(str(random.randint(0, 9)) for _ in range(remaining))
527
+
528
+ return lada + number
529
+
530
+
531
+ def generate_identity(
532
+ tipo: Literal["fisica", "moral"] = "fisica",
533
+ **kwargs,
534
+ ) -> dict:
535
+ """
536
+ Generate a complete Mexican identity.
537
+
538
+ Args:
539
+ tipo: Type of identity ('fisica' or 'moral')
540
+ **kwargs: Additional arguments passed to generator
541
+ For persona física:
542
+ - sexo: 'H' or 'M'
543
+ - estado: Birth state name
544
+ - min_age: Minimum age (default 18)
545
+ - max_age: Maximum age (default 65)
546
+ For persona moral:
547
+ - No additional options
548
+
549
+ Returns:
550
+ Dictionary with complete identity data
551
+
552
+ Example:
553
+ >>> identity = generate_identity()
554
+ >>> print(identity['nombre_completo'])
555
+ 'Juan Pérez García'
556
+ >>> print(identity['rfc'])
557
+ 'PEGJ900515XXX'
558
+ """
559
+ generator = IdentityGenerator()
560
+
561
+ if tipo == "moral":
562
+ return generator.generate_persona_moral(**kwargs).to_dict()
563
+ else:
564
+ return generator.generate_persona_fisica(**kwargs).to_dict()
565
+
566
+
567
+ def generate_persona_fisica(**kwargs) -> dict:
568
+ """
569
+ Generate a Mexican persona física identity.
570
+
571
+ Shortcut for generate_identity(tipo='fisica', **kwargs)
572
+ """
573
+ return generate_identity(tipo="fisica", **kwargs)
574
+
575
+
576
+ def generate_persona_moral(**kwargs) -> dict:
577
+ """
578
+ Generate a Mexican persona moral identity.
579
+
580
+ Shortcut for generate_identity(tipo='moral', **kwargs)
581
+ """
582
+ return generate_identity(tipo="moral", **kwargs)