Saltar a contenido

API — Rut

rutificador.rut.Rut

Builder inmutable de un RUT chileno completo.

Rut es el builder: acepta entrada de usuario en cualquier formato razonable (con o sin puntos, guiones alternativos, espacios internos) y normaliza antes de validar. Siempre produce un objeto válido o lanza una excepción tipada. Acepta además una base numérica sola (sin DV) y calcula el dígito verificador automáticamente.

Para clasificar una cadena sin lanzar excepciones, o para obtener el estado de un RUT parcialmente formado, usa :meth:Rut.parse en su lugar.

Diferencia clave respecto a :meth:parse: - Rut(s) normaliza la entrada (limpia espacios, guiones alternativos, ceros iniciales) y luego valida; es más tolerante en cuanto a formato. - Rut.parse(s, modo=ESTRICTO) clasifica la entrada bajo el modo indicado y devuelve un :class:ValidacionResultado con un estado ("valido", "posible", "invalido", "incompleto"). En modo ESTRICTO rechaza espacios internos; en modo FLEXIBLE los acepta con una advertencia.

Source code in rutificador/rut.py
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
class Rut:
    """Builder inmutable de un RUT chileno completo.

    ``Rut`` es el **builder**: acepta entrada de usuario en cualquier formato
    razonable (con o sin puntos, guiones alternativos, espacios internos) y
    normaliza antes de validar. Siempre produce un objeto válido o lanza una
    excepción tipada. Acepta además una base numérica sola (sin DV) y calcula
    el dígito verificador automáticamente.

    Para **clasificar** una cadena sin lanzar excepciones, o para obtener el
    estado de un RUT parcialmente formado, usa :meth:`Rut.parse` en su lugar.

    Diferencia clave respecto a :meth:`parse`:
    - ``Rut(s)`` normaliza la entrada (limpia espacios, guiones alternativos,
      ceros iniciales) y luego valida; es más tolerante en cuanto a formato.
    - ``Rut.parse(s, modo=ESTRICTO)`` *clasifica* la entrada bajo el modo
      indicado y devuelve un :class:`ValidacionResultado` con un ``estado``
      (``"valido"``, ``"posible"``, ``"invalido"``, ``"incompleto"``). En modo
      ``ESTRICTO`` rechaza espacios internos; en modo ``FLEXIBLE`` los acepta
      con una advertencia.
    """

    @staticmethod
    def _agregar_advertencia(
        advertencias: list[DetalleError], vistos: set[str], codigo: str
    ) -> None:
        if codigo in vistos:
            return
        advertencias.append(crear_detalle_error(codigo))
        vistos.add(codigo)

    @staticmethod
    def _validar_tipo_entrada(
        valor: str | int,
    ) -> tuple[str | None, str | None, list[DetalleError], list[DetalleError]]:
        errores: list[DetalleError] = []
        advertencias: list[DetalleError] = []
        if not isinstance(valor, (str, int)):
            errores.append(crear_detalle_error("ERROR_TIPO"))
            return None, None, errores, advertencias
        cadena_original = str(valor)
        cadena, _ = _limpiar_entrada(cadena_original)
        return cadena_original, cadena, errores, advertencias

    @staticmethod
    def _verificar_espacios(
        cadena_original: str,
        modo: RigorValidacion,
        errores: list[DetalleError],
        advertencias: list[DetalleError],
        vistos: set[str],
    ) -> bool:
        if not re.search(r"\s", cadena_original):
            return True
        if modo == RigorValidacion.ESTRICTO:
            errores.append(crear_detalle_error("CARACTERES_INVALIDOS"))
            return False
        Rut._agregar_advertencia(advertencias, vistos, "NORMALIZACION_ESPACIOS")
        return True

    @staticmethod
    def _verificar_guiones_alternativos(
        cadena_original: str,
        modo: RigorValidacion,
        errores: list[DetalleError],
        advertencias: list[DetalleError],
        vistos: set[str],
    ) -> bool:
        if any(simbolo in cadena_original for simbolo in ("_", "–", "—", "−")):
            if modo == RigorValidacion.ESTRICTO:
                errores.append(crear_detalle_error("CARACTERES_INVALIDOS"))
                return False
            Rut._agregar_advertencia(advertencias, vistos, "NORMALIZACION_GUION")
        return True

    @staticmethod
    def _validar_caracteres_base(
        cadena: str,
        errores: list[DetalleError],
    ) -> bool:
        if cadena == "":
            errores.append(crear_detalle_error("RUT_VACIO"))
            return False
        if re.search(r"[^0-9kK\.-]", cadena):
            errores.append(crear_detalle_error("CARACTERES_INVALIDOS"))
            return False
        if not re.search(r"\d", cadena):
            return False
        if cadena.count("-") > 1:
            errores.append(crear_detalle_error("FORMATO_GUION"))
            return False
        return True

    @staticmethod
    def _separar_base_dv(
        cadena: str,
        errores: list[DetalleError],
    ) -> tuple[str | None, str | None, bool]:
        if "-" not in cadena:
            return cadena, None, False
        base_raw, dv_raw = cadena.split("-", 1)
        if base_raw == "":
            errores.append(crear_detalle_error("FORMATO_GUION"))
            return None, None, False
        if dv_raw == "":
            return base_raw, None, True
        return base_raw, dv_raw, False

    @staticmethod
    def _normalizar_puntos(
        base_raw: str,
        advertencias: list[DetalleError],
        vistos: set[str],
    ) -> str | None:
        if "." not in base_raw:
            return base_raw
        if not RE_BASE_CON_PUNTOS.fullmatch(base_raw):
            return None
        Rut._agregar_advertencia(advertencias, vistos, "NORMALIZACION_PUNTOS")
        return base_raw.replace(".", "")

    @staticmethod
    def _normalizar_ceros(
        base_sin_puntos: str,
        advertencias: list[DetalleError],
        vistos: set[str],
    ) -> str:
        base_normalizada = base_sin_puntos.lstrip("0")
        if base_normalizada == "":
            base_normalizada = "0"
        if base_normalizada != base_sin_puntos:
            Rut._agregar_advertencia(advertencias, vistos, "CEROS_IZQUIERDA")
        return base_normalizada

    @staticmethod
    def _normalizar_dv(
        dv_raw: str | None,
        advertencias: list[DetalleError],
        vistos: set[str],
    ) -> tuple[str | None, bool]:
        if dv_raw is None:
            return None, True
        if len(dv_raw) != 1 or not re.fullmatch(r"[0-9kK]", dv_raw):
            return None, False
        dv_normalizado = dv_raw.lower()
        if dv_raw != dv_normalizado:
            Rut._agregar_advertencia(advertencias, vistos, "NORMALIZACION_DV")
        return dv_normalizado, True

    @staticmethod
    def _reconstruir_normalizado(
        base_normalizada: str,
        dv_normalizado: str | None,
        dv_faltante: bool,
    ) -> str:
        if dv_normalizado is None and dv_faltante:
            return f"{base_normalizada}-"
        if dv_normalizado is None:
            return base_normalizada
        return f"{base_normalizada}-{dv_normalizado}"

    @staticmethod
    def normalizar(
        valor: str | int,
        *,
        modo: RigorValidacion = RigorValidacion.ESTRICTO,
    ) -> tuple[str | None, list[DetalleError], list[DetalleError]]:
        """Normaliza un RUT sin validar su digito verificador."""

        cadena_original, cadena, errores, advertencias = Rut._validar_tipo_entrada(
            valor
        )
        if cadena is None:
            return None, errores, advertencias

        # Garantizado por _validar_tipo_entrada: si cadena no es None,
        # cadena_original tampoco lo es
        if cadena_original is None:
            return None, errores, advertencias

        vistos: set[str] = set()

        if not Rut._verificar_espacios(
            cadena_original, modo, errores, advertencias, vistos
        ):
            return None, errores, advertencias

        if not Rut._verificar_guiones_alternativos(
            cadena_original, modo, errores, advertencias, vistos
        ):
            return None, errores, advertencias

        if not Rut._validar_caracteres_base(cadena, errores):
            return None, errores, advertencias

        base_raw, dv_raw, dv_faltante = Rut._separar_base_dv(cadena, errores)
        if base_raw is None:
            return None, errores, advertencias

        base_sin_puntos = Rut._normalizar_puntos(base_raw, advertencias, vistos)
        if base_sin_puntos is None:
            errores.append(crear_detalle_error("FORMATO_PUNTOS"))
            return None, errores, advertencias

        base_normalizada = Rut._normalizar_ceros(base_sin_puntos, advertencias, vistos)

        dv_normalizado, dv_ok = Rut._normalizar_dv(dv_raw, advertencias, vistos)
        if not dv_ok:
            errores.append(crear_detalle_error("DV_INVALIDO"))
            return None, errores, advertencias

        normalizado = Rut._reconstruir_normalizado(
            base_normalizada, dv_normalizado, dv_faltante
        )
        return normalizado, errores, advertencias

    @staticmethod
    def parse(
        valor: str | int,
        *,
        modo: RigorValidacion = RigorValidacion.ESTRICTO,
        configuracion: ConfiguracionRut = CONFIGURACION_POR_DEFECTO,
    ) -> ValidacionResultado:
        """Clasifica un RUT sin lanzar excepciones (classifier — respeta el modo).

        Este método es el **classifier**: evalúa la entrada bajo el ``modo``
        de rigor indicado y devuelve un :class:`ValidacionResultado` con un
        ``estado`` explícito en lugar de lanzar excepciones. Es la alternativa
        segura al constructor cuando el RUT puede ser inválido o incompleto.

        A diferencia del constructor :meth:`Rut.__init__` (que normaliza
        silenciosamente la entrada), ``parse`` clasifica la cadena tal como
        llega bajo las reglas del modo:

        - ``ESTRICTO``: rechaza espacios internos, guiones alternativos no
          limpios y otros formatos no canónicos (el código de error es
          ``CARACTERES_INVALIDOS`` o ``NORMALIZACION_*`` según el caso).
        - ``FLEXIBLE``: acepta y normaliza esas variantes, registrando
          advertencias de tipo ``NORMALIZACION_*``.

        El ``estado`` devuelto puede ser:

        - ``"valido"`` — DV correcto y base dentro del rango.
        - ``"posible"`` — Base válida pero DV ausente (p.ej. ``"12345678"``).
        - ``"invalido"`` — Formato o DV incorrecto.
        - ``"incompleto"`` — Cadena vacía o sin dígitos.

        Para construir un objeto :class:`Rut` ya validado, usa el constructor
        directamente (normaliza la entrada y lanza si es inválido).
        """

        inicio = time.perf_counter()
        try:
            original = str(valor)
        except Exception:  # noqa: BLE001  # pragma: no cover - caso extremo
            original = "<valor-no-representable>"

        normalizado, errores, advertencias = Rut.normalizar(valor, modo=modo)
        base: str | None = None
        dv: str | None = None
        estado: EstadoValidacion = "invalido" if errores else "incompleto"

        if errores and all(error.codigo == "RUT_VACIO" for error in errores):
            estado = "incompleto"

        if not errores:
            if normalizado is None:
                estado = "incompleto"
            else:
                if "-" in normalizado:
                    base, dv_raw = normalizado.split("-", 1)
                    if dv_raw == "":
                        dv = None
                        estado = "posible"
                    else:
                        dv = dv_raw
                        estado = "posible"
                else:
                    base = normalizado
                    dv = None
                    estado = "posible"

                if base is None or base == "":
                    estado = "incompleto"
                elif len(base) < configuracion.min_digitos:
                    errores.append(crear_detalle_error("LONGITUD_MINIMA"))
                    estado = "invalido"
                elif len(base) > configuracion.max_digitos:
                    errores.append(crear_detalle_error("LONGITUD_MAXIMA"))
                    estado = "invalido"
                elif dv is None:
                    estado = "posible"
                else:
                    digito_calculado = calcular_digito_verificador(
                        base, configuracion=configuracion
                    )
                    if dv.lower() != digito_calculado.lower():
                        errores.append(crear_detalle_error("DV_DISCORDANTE"))
                        estado = "invalido"
                    else:
                        estado = "valido"

        duracion = time.perf_counter() - inicio
        return ValidacionResultado(
            original=original,
            normalizado=normalizado,
            base=base,
            dv=dv,
            estado=estado,
            errores=errores,
            advertencias=advertencias,
            validador_modo=modo.value,
            duracion=duracion,
        )

    @staticmethod
    def enmascarar(
        valor: str | int,
        *,
        mantener: int = 4,
        caracter: str = "*",
        modo: Literal["mascarada", "token"] = "mascarada",
        clave: str | bytes | None = None,
        separador_miles: bool = False,
        mayusculas: bool = False,
        separador_personalizado: str = ".",
    ) -> str:
        """Enmascara o tokeniza un RUT válido."""

        if not isinstance(mantener, int) or mantener < 0:
            raise ErrorValidacionRut(
                "mantener debe ser un entero mayor o igual a 0",
                codigo_error="ERROR_TIPO",
            )
        if not isinstance(caracter, str) or len(caracter) != 1:
            raise ErrorValidacionRut(
                "caracter debe ser un único carácter",
                codigo_error="ERROR_TIPO",
            )

        resultado = Rut.parse(valor)
        if (
            resultado.estado != "valido"
            or resultado.base is None
            or resultado.dv is None
        ):
            raise ErrorValidacionRut(
                crear_detalle_error("ESTADO_ENMASCARADO").mensaje,
                codigo_error="ESTADO_ENMASCARADO",
            )

        if modo == "token":
            if clave is None:
                raise ErrorValidacionRut(
                    crear_detalle_error("CLAVE_TOKEN_REQUERIDA").mensaje,
                    codigo_error="CLAVE_TOKEN_REQUERIDA",
                )
            clave_bytes = (
                clave if isinstance(clave, bytes) else str(clave).encode("utf-8")
            )
            if resultado.normalizado is None:
                raise ErrorValidacionRut(
                    crear_detalle_error("ESTADO_ENMASCARADO").mensaje,
                    codigo_error="ESTADO_ENMASCARADO",
                )
            mensaje = resultado.normalizado.encode("utf-8")
            digest = hmac.new(clave_bytes, mensaje, hashlib.sha256).digest()
            token = base64.b32encode(digest).decode("ascii").rstrip("=").lower()
            return f"tok_{token}"

        base = resultado.base
        if mantener >= len(base):
            base_visible = base
            mascara = ""
        elif mantener == 0:
            base_visible = ""
            mascara = caracter * len(base)
        else:
            base_visible = base[-mantener:]
            mascara = caracter * (len(base) - mantener)

        base_mascarada = f"{mascara}{base_visible}"
        if separador_miles:
            base_mascarada = Rut.agregar_separador_miles(
                base_mascarada, separador_personalizado
            )
        rut_mascarado = f"{base_mascarada}-{resultado.dv}"
        if mayusculas:
            rut_mascarado = rut_mascarado.upper()
        return rut_mascarado

    # Alias para compatibilidad
    mask = enmascarar

    def __init__(self, rut: str | int, validador: ValidadorRut | None = None) -> None:
        """Construye y valida un RUT (builder — normaliza la entrada).

        Este método es el **builder**: normaliza la entrada antes de validar
        (limpia espacios internos, guiones alternativos, ceros iniciales).
        Si se proporciona solo la base numérica (sin DV), calcula el dígito
        verificador automáticamente.

        Para clasificar sin lanzar excepciones, usa :meth:`Rut.parse`, que
        devuelve un ``estado`` y respeta el ``modo`` de rigor indicado.

        Args:
            rut: RUT como cadena (``"12.345.678-5"`` o ``"12345678-5"``) o
                entero (``12345678``). Se normalizan espacios, guiones
                alternativos (``–``, ``—``, ``_``) y separadores de miles.
                Si se omite el DV, se calcula.
            validador: Validador personalizado. Por defecto usa
                ``ValidadorRut()`` en modo ``ESTRICTO``.

        Raises:
            ErrorValidacionRut: Si el RUT no pasa la validación de tipo,
                formato o dígito verificador.
        """
        if not isinstance(rut, (str, int)):
            raise ErrorValidacionRut(
                f"El RUT debe ser cadena o entero, se recibió: {type(rut).__name__}",
                codigo_error="ERROR_TIPO",
            )
        # Normalizar entrada antes de validar
        self.cadena_rut, _ = _limpiar_entrada(str(rut).strip())
        if not self.cadena_rut:
            raise ErrorValidacionRut(
                "El RUT no puede estar vacío", codigo_error="RUT_VACIO"
            )
        self.validador = validador or ValidadorRut()
        self._analizar_y_validar()

    def _analizar_y_validar(self) -> None:
        if self.cadena_rut.isdigit():
            self.cadena_base = self.cadena_rut
            digito_ingresado = None
        else:
            match_result = self.validador.validar_formato(self.cadena_rut)
            self.cadena_base = match_result.group(1)
            digito_ingresado = match_result.group(3)

        self.base = RutBase(self.cadena_base, self.cadena_rut, self.validador)
        self.digito_verificador = calcular_digito_verificador(self.base.base)
        self.validador.validar_digito_verificador(
            digito_ingresado, self.digito_verificador
        )
        logger.debug("RUT validado correctamente")

    def __str__(self) -> str:  # pragma: no cover - trivial
        return f"{self.base}-{self.digito_verificador}"

    def __repr__(self) -> str:  # pragma: no cover - trivial
        return "Rut(base='********', dv='*')"

    def __eq__(self, other: object) -> bool:  # pragma: no cover - trivial
        if not isinstance(other, Rut):
            return NotImplemented
        return str(self) == str(other)

    def __hash__(self) -> int:  # pragma: no cover - trivial
        return hash(str(self))

    @contextmanager
    def _contexto_formateo(self) -> Iterator[None]:
        logger.debug("Inicio de formateo para el RUT")
        try:
            yield
        finally:
            logger.debug("Formateo completado para el RUT")

    def formatear(
        self,
        separador_miles: bool = False,
        mayusculas: bool = False,
        separador_personalizado: str = ".",
    ) -> str:
        """Formatea el RUT con las opciones especificadas.

        Args:
            separador_miles: Si ``True``, agrega separadores de miles
                (ej: ``12.345.678-5``).
            mayusculas: Si ``True``, la 'k' del DV se muestra en mayúscula.
            separador_personalizado: Carácter usado como separador de miles
                (por defecto ``"."``). Solo se aplica si ``separador_miles``
                es ``True``.

        Returns:
            El RUT formateado como cadena.

        Raises:
            ErrorValidacionRut: Si ``separador_personalizado`` no es un
                único carácter.
        """
        asegurar_booleano(separador_miles, "separador_miles")
        asegurar_booleano(mayusculas, "mayusculas")
        if (
            not isinstance(separador_personalizado, str)
            or len(separador_personalizado) != 1
        ):
            raise ErrorValidacionRut(
                "separador_personalizado debe ser un único carácter",
                codigo_error="ERROR_FORMATO",
            )
        with self._contexto_formateo():
            rut_str = str(self)
            if separador_miles:
                base_formateada = self.agregar_separador_miles(
                    str(self.base), separador_personalizado
                )
                rut_str = f"{base_formateada}-{self.digito_verificador}"
            if mayusculas:
                rut_str = rut_str.upper()
            logger.debug("RUT formateado correctamente")
            return rut_str

    @staticmethod
    def agregar_separador_miles(numero: str, separador: str = ".") -> str:
        """Expone un separador de miles público para reutilizar desde otros módulos."""
        return Rut._agregar_separador_miles(numero, separador)

    @staticmethod
    def _agregar_separador_miles(numero: str, separador: str = ".") -> str:
        if len(numero) <= 3:
            return numero
        invertido = numero[::-1]
        grupos = [
            invertido[slice(inicio, inicio + 3)]
            for inicio in range(0, len(invertido), 3)
        ]
        formateado = separador.join(grupos)
        return formateado[::-1]

    @staticmethod
    def sugerir(valor: str) -> list[str]:
        """Sugiere RUTs válidos cercanos a la entrada.

        Útil para capturar errores de digitación o 'Did you mean?' en interfaces.
        """
        return sugerir_ruts(valor)

    @staticmethod
    def mejorar(valor: str) -> str | None:
        """Intenta mejorar/corregir un RUT de forma segura.

        Solo una sugerencia inequívoca (distancia 1 y sin ambigüedad) será devuelta.
        """
        return mejorar_con_confianza(valor)

    @classmethod
    def limpiar_cache(cls) -> None:
        """Limpia la caché de la fábrica ``obtener_rut``."""
        obtener_rut.cache_clear()
        logger.info("Caché de RUT limpiada")

    @classmethod
    def estadisticas_cache(cls) -> "EstadisticasCache":
        """Retorna estadísticas de la caché de ``obtener_rut``.

        Returns:
            Diccionario con ``tamanio_cache`` y ``tamanio_max_cache``.
        """
        info = obtener_rut.cache_info()
        return {"tamanio_cache": info.currsize, "tamanio_max_cache": info.maxsize}

mask class-attribute instance-attribute

mask = enmascarar

parse staticmethod

parse(valor: str | int, *, modo: RigorValidacion = RigorValidacion.ESTRICTO, configuracion: ConfiguracionRut = CONFIGURACION_POR_DEFECTO) -> ValidacionResultado

Clasifica un RUT sin lanzar excepciones (classifier — respeta el modo).

Este método es el classifier: evalúa la entrada bajo el modo de rigor indicado y devuelve un :class:ValidacionResultado con un estado explícito en lugar de lanzar excepciones. Es la alternativa segura al constructor cuando el RUT puede ser inválido o incompleto.

A diferencia del constructor :meth:Rut.__init__ (que normaliza silenciosamente la entrada), parse clasifica la cadena tal como llega bajo las reglas del modo:

  • ESTRICTO: rechaza espacios internos, guiones alternativos no limpios y otros formatos no canónicos (el código de error es CARACTERES_INVALIDOS o NORMALIZACION_* según el caso).
  • FLEXIBLE: acepta y normaliza esas variantes, registrando advertencias de tipo NORMALIZACION_*.

El estado devuelto puede ser:

  • "valido" — DV correcto y base dentro del rango.
  • "posible" — Base válida pero DV ausente (p.ej. "12345678").
  • "invalido" — Formato o DV incorrecto.
  • "incompleto" — Cadena vacía o sin dígitos.

Para construir un objeto :class:Rut ya validado, usa el constructor directamente (normaliza la entrada y lanza si es inválido).

Source code in rutificador/rut.py
@staticmethod
def parse(
    valor: str | int,
    *,
    modo: RigorValidacion = RigorValidacion.ESTRICTO,
    configuracion: ConfiguracionRut = CONFIGURACION_POR_DEFECTO,
) -> ValidacionResultado:
    """Clasifica un RUT sin lanzar excepciones (classifier — respeta el modo).

    Este método es el **classifier**: evalúa la entrada bajo el ``modo``
    de rigor indicado y devuelve un :class:`ValidacionResultado` con un
    ``estado`` explícito en lugar de lanzar excepciones. Es la alternativa
    segura al constructor cuando el RUT puede ser inválido o incompleto.

    A diferencia del constructor :meth:`Rut.__init__` (que normaliza
    silenciosamente la entrada), ``parse`` clasifica la cadena tal como
    llega bajo las reglas del modo:

    - ``ESTRICTO``: rechaza espacios internos, guiones alternativos no
      limpios y otros formatos no canónicos (el código de error es
      ``CARACTERES_INVALIDOS`` o ``NORMALIZACION_*`` según el caso).
    - ``FLEXIBLE``: acepta y normaliza esas variantes, registrando
      advertencias de tipo ``NORMALIZACION_*``.

    El ``estado`` devuelto puede ser:

    - ``"valido"`` — DV correcto y base dentro del rango.
    - ``"posible"`` — Base válida pero DV ausente (p.ej. ``"12345678"``).
    - ``"invalido"`` — Formato o DV incorrecto.
    - ``"incompleto"`` — Cadena vacía o sin dígitos.

    Para construir un objeto :class:`Rut` ya validado, usa el constructor
    directamente (normaliza la entrada y lanza si es inválido).
    """

    inicio = time.perf_counter()
    try:
        original = str(valor)
    except Exception:  # noqa: BLE001  # pragma: no cover - caso extremo
        original = "<valor-no-representable>"

    normalizado, errores, advertencias = Rut.normalizar(valor, modo=modo)
    base: str | None = None
    dv: str | None = None
    estado: EstadoValidacion = "invalido" if errores else "incompleto"

    if errores and all(error.codigo == "RUT_VACIO" for error in errores):
        estado = "incompleto"

    if not errores:
        if normalizado is None:
            estado = "incompleto"
        else:
            if "-" in normalizado:
                base, dv_raw = normalizado.split("-", 1)
                if dv_raw == "":
                    dv = None
                    estado = "posible"
                else:
                    dv = dv_raw
                    estado = "posible"
            else:
                base = normalizado
                dv = None
                estado = "posible"

            if base is None or base == "":
                estado = "incompleto"
            elif len(base) < configuracion.min_digitos:
                errores.append(crear_detalle_error("LONGITUD_MINIMA"))
                estado = "invalido"
            elif len(base) > configuracion.max_digitos:
                errores.append(crear_detalle_error("LONGITUD_MAXIMA"))
                estado = "invalido"
            elif dv is None:
                estado = "posible"
            else:
                digito_calculado = calcular_digito_verificador(
                    base, configuracion=configuracion
                )
                if dv.lower() != digito_calculado.lower():
                    errores.append(crear_detalle_error("DV_DISCORDANTE"))
                    estado = "invalido"
                else:
                    estado = "valido"

    duracion = time.perf_counter() - inicio
    return ValidacionResultado(
        original=original,
        normalizado=normalizado,
        base=base,
        dv=dv,
        estado=estado,
        errores=errores,
        advertencias=advertencias,
        validador_modo=modo.value,
        duracion=duracion,
    )

normalizar staticmethod

normalizar(valor: str | int, *, modo: RigorValidacion = RigorValidacion.ESTRICTO) -> tuple[str | None, list[DetalleError], list[DetalleError]]

Normaliza un RUT sin validar su digito verificador.

Source code in rutificador/rut.py
@staticmethod
def normalizar(
    valor: str | int,
    *,
    modo: RigorValidacion = RigorValidacion.ESTRICTO,
) -> tuple[str | None, list[DetalleError], list[DetalleError]]:
    """Normaliza un RUT sin validar su digito verificador."""

    cadena_original, cadena, errores, advertencias = Rut._validar_tipo_entrada(
        valor
    )
    if cadena is None:
        return None, errores, advertencias

    # Garantizado por _validar_tipo_entrada: si cadena no es None,
    # cadena_original tampoco lo es
    if cadena_original is None:
        return None, errores, advertencias

    vistos: set[str] = set()

    if not Rut._verificar_espacios(
        cadena_original, modo, errores, advertencias, vistos
    ):
        return None, errores, advertencias

    if not Rut._verificar_guiones_alternativos(
        cadena_original, modo, errores, advertencias, vistos
    ):
        return None, errores, advertencias

    if not Rut._validar_caracteres_base(cadena, errores):
        return None, errores, advertencias

    base_raw, dv_raw, dv_faltante = Rut._separar_base_dv(cadena, errores)
    if base_raw is None:
        return None, errores, advertencias

    base_sin_puntos = Rut._normalizar_puntos(base_raw, advertencias, vistos)
    if base_sin_puntos is None:
        errores.append(crear_detalle_error("FORMATO_PUNTOS"))
        return None, errores, advertencias

    base_normalizada = Rut._normalizar_ceros(base_sin_puntos, advertencias, vistos)

    dv_normalizado, dv_ok = Rut._normalizar_dv(dv_raw, advertencias, vistos)
    if not dv_ok:
        errores.append(crear_detalle_error("DV_INVALIDO"))
        return None, errores, advertencias

    normalizado = Rut._reconstruir_normalizado(
        base_normalizada, dv_normalizado, dv_faltante
    )
    return normalizado, errores, advertencias

enmascarar staticmethod

enmascarar(valor: str | int, *, mantener: int = 4, caracter: str = '*', modo: Literal['mascarada', 'token'] = 'mascarada', clave: str | bytes | None = None, separador_miles: bool = False, mayusculas: bool = False, separador_personalizado: str = '.') -> str

Enmascara o tokeniza un RUT válido.

Source code in rutificador/rut.py
@staticmethod
def enmascarar(
    valor: str | int,
    *,
    mantener: int = 4,
    caracter: str = "*",
    modo: Literal["mascarada", "token"] = "mascarada",
    clave: str | bytes | None = None,
    separador_miles: bool = False,
    mayusculas: bool = False,
    separador_personalizado: str = ".",
) -> str:
    """Enmascara o tokeniza un RUT válido."""

    if not isinstance(mantener, int) or mantener < 0:
        raise ErrorValidacionRut(
            "mantener debe ser un entero mayor o igual a 0",
            codigo_error="ERROR_TIPO",
        )
    if not isinstance(caracter, str) or len(caracter) != 1:
        raise ErrorValidacionRut(
            "caracter debe ser un único carácter",
            codigo_error="ERROR_TIPO",
        )

    resultado = Rut.parse(valor)
    if (
        resultado.estado != "valido"
        or resultado.base is None
        or resultado.dv is None
    ):
        raise ErrorValidacionRut(
            crear_detalle_error("ESTADO_ENMASCARADO").mensaje,
            codigo_error="ESTADO_ENMASCARADO",
        )

    if modo == "token":
        if clave is None:
            raise ErrorValidacionRut(
                crear_detalle_error("CLAVE_TOKEN_REQUERIDA").mensaje,
                codigo_error="CLAVE_TOKEN_REQUERIDA",
            )
        clave_bytes = (
            clave if isinstance(clave, bytes) else str(clave).encode("utf-8")
        )
        if resultado.normalizado is None:
            raise ErrorValidacionRut(
                crear_detalle_error("ESTADO_ENMASCARADO").mensaje,
                codigo_error="ESTADO_ENMASCARADO",
            )
        mensaje = resultado.normalizado.encode("utf-8")
        digest = hmac.new(clave_bytes, mensaje, hashlib.sha256).digest()
        token = base64.b32encode(digest).decode("ascii").rstrip("=").lower()
        return f"tok_{token}"

    base = resultado.base
    if mantener >= len(base):
        base_visible = base
        mascara = ""
    elif mantener == 0:
        base_visible = ""
        mascara = caracter * len(base)
    else:
        base_visible = base[-mantener:]
        mascara = caracter * (len(base) - mantener)

    base_mascarada = f"{mascara}{base_visible}"
    if separador_miles:
        base_mascarada = Rut.agregar_separador_miles(
            base_mascarada, separador_personalizado
        )
    rut_mascarado = f"{base_mascarada}-{resultado.dv}"
    if mayusculas:
        rut_mascarado = rut_mascarado.upper()
    return rut_mascarado

formatear

formatear(separador_miles: bool = False, mayusculas: bool = False, separador_personalizado: str = '.') -> str

Formatea el RUT con las opciones especificadas.

Parameters:

Name Type Description Default
separador_miles bool

Si True, agrega separadores de miles (ej: 12.345.678-5).

False
mayusculas bool

Si True, la 'k' del DV se muestra en mayúscula.

False
separador_personalizado str

Carácter usado como separador de miles (por defecto "."). Solo se aplica si separador_miles es True.

'.'

Returns:

Type Description
str

El RUT formateado como cadena.

Raises:

Type Description
ErrorValidacionRut

Si separador_personalizado no es un único carácter.

Source code in rutificador/rut.py
def formatear(
    self,
    separador_miles: bool = False,
    mayusculas: bool = False,
    separador_personalizado: str = ".",
) -> str:
    """Formatea el RUT con las opciones especificadas.

    Args:
        separador_miles: Si ``True``, agrega separadores de miles
            (ej: ``12.345.678-5``).
        mayusculas: Si ``True``, la 'k' del DV se muestra en mayúscula.
        separador_personalizado: Carácter usado como separador de miles
            (por defecto ``"."``). Solo se aplica si ``separador_miles``
            es ``True``.

    Returns:
        El RUT formateado como cadena.

    Raises:
        ErrorValidacionRut: Si ``separador_personalizado`` no es un
            único carácter.
    """
    asegurar_booleano(separador_miles, "separador_miles")
    asegurar_booleano(mayusculas, "mayusculas")
    if (
        not isinstance(separador_personalizado, str)
        or len(separador_personalizado) != 1
    ):
        raise ErrorValidacionRut(
            "separador_personalizado debe ser un único carácter",
            codigo_error="ERROR_FORMATO",
        )
    with self._contexto_formateo():
        rut_str = str(self)
        if separador_miles:
            base_formateada = self.agregar_separador_miles(
                str(self.base), separador_personalizado
            )
            rut_str = f"{base_formateada}-{self.digito_verificador}"
        if mayusculas:
            rut_str = rut_str.upper()
        logger.debug("RUT formateado correctamente")
        return rut_str

sugerir staticmethod

sugerir(valor: str) -> list[str]

Sugiere RUTs válidos cercanos a la entrada.

Útil para capturar errores de digitación o 'Did you mean?' en interfaces.

Source code in rutificador/rut.py
@staticmethod
def sugerir(valor: str) -> list[str]:
    """Sugiere RUTs válidos cercanos a la entrada.

    Útil para capturar errores de digitación o 'Did you mean?' en interfaces.
    """
    return sugerir_ruts(valor)

mejorar staticmethod

mejorar(valor: str) -> str | None

Intenta mejorar/corregir un RUT de forma segura.

Solo una sugerencia inequívoca (distancia 1 y sin ambigüedad) será devuelta.

Source code in rutificador/rut.py
@staticmethod
def mejorar(valor: str) -> str | None:
    """Intenta mejorar/corregir un RUT de forma segura.

    Solo una sugerencia inequívoca (distancia 1 y sin ambigüedad) será devuelta.
    """
    return mejorar_con_confianza(valor)

agregar_separador_miles staticmethod

agregar_separador_miles(numero: str, separador: str = '.') -> str

Expone un separador de miles público para reutilizar desde otros módulos.

Source code in rutificador/rut.py
@staticmethod
def agregar_separador_miles(numero: str, separador: str = ".") -> str:
    """Expone un separador de miles público para reutilizar desde otros módulos."""
    return Rut._agregar_separador_miles(numero, separador)

estadisticas_cache classmethod

estadisticas_cache() -> EstadisticasCache

Retorna estadísticas de la caché de obtener_rut.

Returns:

Type Description
EstadisticasCache

Diccionario con tamanio_cache y tamanio_max_cache.

Source code in rutificador/rut.py
@classmethod
def estadisticas_cache(cls) -> "EstadisticasCache":
    """Retorna estadísticas de la caché de ``obtener_rut``.

    Returns:
        Diccionario con ``tamanio_cache`` y ``tamanio_max_cache``.
    """
    info = obtener_rut.cache_info()
    return {"tamanio_cache": info.currsize, "tamanio_max_cache": info.maxsize}

rutificador.rut.ValidacionResultado dataclass

Resultado estructurado de una validación incremental.

Source code in rutificador/rut.py
@dataclass(frozen=True)
class ValidacionResultado:
    """Resultado estructurado de una validación incremental."""

    original: str
    normalizado: str | None
    base: str | None
    dv: str | None
    estado: EstadoValidacion
    errores: list[DetalleError]
    advertencias: list[DetalleError]
    validador_modo: str
    duracion: float

rutificador.rut.RutBase dataclass

Representación inmutable de la base de un RUT.

Source code in rutificador/rut.py
@dataclass(frozen=True)
class RutBase:
    """Representación inmutable de la base de un RUT."""

    base: str
    rut_original: str
    validador: ValidadorRut = field(
        default_factory=ValidadorRut, repr=False, compare=False
    )

    def __post_init__(self) -> None:
        if not isinstance(self.rut_original, str):
            raise ErrorValidacionRut(
                "El RUT original debe ser una cadena", codigo_error="ERROR_TIPO"
            )
        base_validada = self.validador.validar_base(self.base, self.rut_original)
        object.__setattr__(self, "base", base_validada)

    def __str__(self) -> str:  # pragma: no cover - trivial
        return self.base