rut-validator 0.1.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.
@@ -0,0 +1,26 @@
1
+ """
2
+ rut-validator: Validation of Chilean RUTs for Pydantic and FastAPI
3
+ """
4
+
5
+ from .core.validator import RutValidator
6
+ from .core.patterns import RutFormat
7
+ from .core.orm.pydantic.schema import RutStr
8
+ from .errors import (
9
+ RutInvalidFormatError,
10
+ RutInvalidValueError,
11
+ RutModuleElevenValidationError,
12
+ RutValidationError,
13
+ )
14
+ from .types.rut import Rut as ValidatedRut
15
+
16
+ __version__ = "0.1.0"
17
+ __all__ = [
18
+ "RutValidator", # For pure validation
19
+ "ValidatedRut", # Validation result (alias for Rut)
20
+ "RutStr", # For use with Pydantic
21
+ "RutFormat", # Format enumeration
22
+ "RutInvalidFormatError", # Custom exception
23
+ "RutInvalidValueError", # Custom exception
24
+ "RutModuleElevenValidationError", # Custom exception
25
+ "RutValidationError", # Base exception
26
+ ]
@@ -0,0 +1,3 @@
1
+ """Constants for RUT validation."""
2
+
3
+ RUT_MODULE_ELEVEN_FACTORS = [2, 3, 4, 5, 6, 7]
@@ -0,0 +1,14 @@
1
+ """Core validation logic - no external dependencies"""
2
+
3
+ from .formatter import RutFormatter
4
+ from .parser import RutParser
5
+ from .patterns import RutPatterns, RutFormat
6
+ from .validator import RutValidator
7
+
8
+ __all__ = [
9
+ "RutFormatter",
10
+ "RutParser",
11
+ "RutPatterns",
12
+ "RutFormat",
13
+ "RutValidator",
14
+ ]
@@ -0,0 +1,32 @@
1
+ from rut_validator.core.patterns import RutPatterns
2
+
3
+
4
+ class RutFormatter:
5
+ """Formatter for RUT strings with different output formats."""
6
+
7
+ @staticmethod
8
+ def to_original_format(rut: str) -> str:
9
+ """
10
+ Converts a RUT string to its original format with dots and dashes (e.g. "12345678-9" -> "12.345.678-9").
11
+
12
+ Args:
13
+ rut (str): The RUT string to format (e.g. "123456789")
14
+
15
+ Returns:
16
+ str: The RUT string in original format (e.g. "12.345.678-9")
17
+ """
18
+ return RutPatterns.formatted(rut)
19
+
20
+ @staticmethod
21
+ def to_normalize_format(rut: str) -> str:
22
+ """
23
+ Cleans the RUT string by removing dots and dashes, leaving only the digits and check digit.
24
+ This is used internally to normalize the RUT before validation.
25
+
26
+ Args:
27
+ rut (str): The RUT string to clean (e.g. "12.345.678-9")
28
+
29
+ Returns:
30
+ str: The cleaned RUT string (e.g. "123456789")
31
+ """
32
+ return RutPatterns.normalized(rut)
File without changes
@@ -0,0 +1,86 @@
1
+ from django.db.models import CharField
2
+ from django.core.exceptions import ValidationError
3
+
4
+ from typing import Any
5
+
6
+ from ...validator import RutValidator
7
+ from ....errors import (
8
+ RutInvalidValueError,
9
+ RutInvalidFormatError,
10
+ RutModuleElevenValidationError,
11
+ )
12
+
13
+
14
+ class RUTField(CharField):
15
+ description = "A field to store Chilean RUT (Rol Único Tributario) numbers."
16
+
17
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
18
+ kwargs.setdefault(
19
+ "max_length", 12
20
+ ) # RUT can be up to 12 characters including formatting
21
+ super().__init__(*args, **kwargs)
22
+
23
+ # After initializing the field, we can set up any additional attributes or validators if needed.
24
+ # For example, we could add a validator that uses the RutValidator from the core module
25
+
26
+ def validator(value: Any) -> None:
27
+ """
28
+ Custom validator that uses the RutValidator to validate the RUT value.
29
+
30
+ Args:
31
+ value (Any): The value to validate as a RUT.
32
+
33
+ Raises:
34
+ ValidationError: If the value is not a valid RUT.
35
+ """
36
+
37
+ try:
38
+ if value is None:
39
+ raise ValueError("RUT value cannot be None.")
40
+
41
+ RutValidator.validate(str(value))
42
+
43
+ except (
44
+ RutInvalidValueError,
45
+ RutInvalidFormatError,
46
+ RutModuleElevenValidationError,
47
+ ) as e:
48
+ raise ValidationError(str(f"Invalid RUT value: {e}"))
49
+
50
+ self.validators.append(validator)
51
+
52
+ def to_python(self, value: object | None) -> str | None:
53
+ """
54
+ Convert the input value to a Python string and validate it as a RUT.
55
+
56
+ Args:
57
+ value (object | None): The value to convert and validate.
58
+
59
+ Returns:
60
+ str | None: The validated RUT string or None if the input was None.
61
+ """
62
+
63
+ self.ensure_type(value)
64
+
65
+ if value is None:
66
+ return None
67
+
68
+ try:
69
+ return RutValidator.validate(str(value)).normalized
70
+ except Exception as e:
71
+ raise ValidationError(str(e))
72
+
73
+ def get_prep_value(self, value: object | None) -> str | None:
74
+ return self.to_python(value)
75
+
76
+ def ensure_type(self, value: object | None) -> str | None:
77
+
78
+ # If the value is None, we can return it directly (or we could choose to return an empty string if we prefer).
79
+ if value is None:
80
+ return value
81
+
82
+ # If the value is already a string, we can return it directly.
83
+ if not isinstance(value, str):
84
+ raise ValidationError("RUT value must be a string.")
85
+
86
+ return str(value)
File without changes
@@ -0,0 +1,80 @@
1
+ """RutStr - Pydantic-integrated RUT validator (style: EmailStr)"""
2
+
3
+ from typing import Any, Annotated
4
+ from typing import TYPE_CHECKING
5
+
6
+ from pydantic import GetCoreSchemaHandler, GetJsonSchemaHandler
7
+ from pydantic.json_schema import JsonSchemaValue
8
+
9
+ from pydantic_core.core_schema import str_schema, no_info_after_validator_function
10
+ from pydantic_core.core_schema import CoreSchema
11
+
12
+ from ...validator import RutValidator
13
+
14
+
15
+ if TYPE_CHECKING:
16
+ # For type checkers, we can just treat RutStr as a regular string.
17
+ RutStr = Annotated[str, ...]
18
+
19
+ else:
20
+
21
+ class RutStr(str):
22
+ @classmethod
23
+ def __get_pydantic_core_schema__(
24
+ cls,
25
+ _source_type: type[Any],
26
+ _handler: GetCoreSchemaHandler,
27
+ ) -> CoreSchema:
28
+ """
29
+ Defines the core schema for Pydantic validation.
30
+ This method is called by Pydantic to get the schema that will be used for validation
31
+ and parsing of the field.
32
+ """
33
+
34
+ return no_info_after_validator_function(
35
+ cls._validate,
36
+ str_schema(),
37
+ )
38
+
39
+ @classmethod
40
+ def __get_pydantic_json_schema__(
41
+ cls,
42
+ schema: CoreSchema,
43
+ handler: GetJsonSchemaHandler,
44
+ ) -> JsonSchemaValue:
45
+ """Defines JSON schema for OpenAPI/Swagger"""
46
+ field_schema = handler(schema)
47
+ field_schema.update(
48
+ {
49
+ "type": "string",
50
+ "pattern": r"^\d{1,8}-[0-9kK]$|^\d{8}[0-9kK]$",
51
+ "example": "12345678-9",
52
+ "description": "Valid Chilean RUT (e.g: 12345678-9 or 123456789)",
53
+ }
54
+ )
55
+
56
+ return field_schema
57
+
58
+ def __new__(cls, value: str) -> "RutStr":
59
+ validated = cls._validate(value)
60
+ return str.__new__(cls, validated)
61
+
62
+ @classmethod
63
+ def _validate(cls, input_value: str, /) -> str:
64
+ """
65
+ Validates the input value as a RUT and returns the normalized form.
66
+ If the input is not a valid RUT, it raises a ValueError with an appropriate message.
67
+
68
+ Args:
69
+ input_value (str): The input value to validate as a RUT.
70
+
71
+ Returns:
72
+ str: The normalized RUT.
73
+
74
+ Raises:
75
+ ValueError: If the input is not a valid RUT.
76
+ """
77
+ try:
78
+ return RutValidator.validate(input_value).normalized
79
+ except Exception as e:
80
+ raise ValueError(str(e))
File without changes
@@ -0,0 +1,54 @@
1
+ from sqlalchemy.types import String, TypeDecorator
2
+ from sqlalchemy import Dialect
3
+
4
+
5
+ from rut_validator.core.validator import RutValidator
6
+
7
+
8
+ class RutSQLAlchemy(TypeDecorator):
9
+ """
10
+ Custom SQLAlchemy type for RUT validation and normalization. This type will ensure that any value being stored in the database is a valid RUT and is normalized to a consistent format.
11
+ When retrieving values from the database, it returns the stored normalized string.
12
+ """
13
+
14
+ impl = String # Save as VARCHAR/String into the database
15
+ cache_ok = True
16
+
17
+ def process_bind_param(self, value: str | None, dialect: Dialect) -> str | None:
18
+ """
19
+ the `process_bind_param` method is called when a value is being bound to a parameter in a SQL statement. This is where we can validate and normalize the RUT value before it gets sent to the database.
20
+ If the value is None, we can return it directly (or we could choose to return an empty string if we prefer). If the value is already a RutStr instance, we can return its normalized form directly.
21
+ If it's a regular string, we validate and normalize it using the RutStr._validate method before returning it.
22
+
23
+ Args:
24
+ value (Any): The value to be bound to the SQL parameter. This can be a string, a RutStr instance, or None.
25
+ dialect (Dialect): The SQLAlchemy dialect in use, which can be useful if we need to handle different database backends differently (though in this case we likely won't need it).
26
+
27
+ Returns:
28
+ str | None: The validated and normalized RUT string to be stored in the database, or None if the input was None.
29
+ """
30
+ if value is None:
31
+ return None
32
+
33
+ try:
34
+ validated_rut = RutValidator.validate(str(value))
35
+ return validated_rut.normalized
36
+ except Exception as e:
37
+ raise ValueError(str(e))
38
+
39
+ def process_result_value(self, value: str | None, dialect: Dialect) -> str | None:
40
+ """
41
+ The `process_result_value` method is called when a value is being retrieved from the database.
42
+ This is where we can convert the stored string back into a RutStr object for easier manipulation in Python.
43
+
44
+ Args:
45
+ value (Any): The value retrieved from the database. This can be a string or None.
46
+ dialect (Dialect): The SQLAlchemy dialect in use.
47
+
48
+ Returns:
49
+ str | None: The stored normalized RUT string, or None if the input was None.
50
+ """
51
+ if value is None:
52
+ return None
53
+
54
+ return value
@@ -0,0 +1,59 @@
1
+ from typing import Tuple, Optional
2
+
3
+ from rut_validator.core.patterns import RutPatterns
4
+ from rut_validator.types.enums import RutFormat
5
+
6
+ from rut_validator.errors import (
7
+ RutInvalidFormatError,
8
+ RutInvalidValueError,
9
+ )
10
+
11
+
12
+ class RutParser:
13
+ """Parser for Chilean RUT strings with format detection."""
14
+
15
+ @classmethod
16
+ def parse(cls, rut: str) -> Tuple[str, str, Optional[RutFormat]]:
17
+ """
18
+ Parses a RUT string and returns body, check_digit, and detected format.
19
+
20
+ Args:
21
+ rut: The RUT string to parse.
22
+
23
+ Returns:
24
+ Tuple of (body, check_digit, format).
25
+
26
+ Raises:
27
+ RutInvalidValueError: If RUT is empty or None.
28
+ RutInvalidFormatError: If RUT format is invalid.
29
+ """
30
+ if rut.strip() == "":
31
+ raise RutInvalidValueError(
32
+ "No se puede parsear un RUT vacío, por favor ingrese un valor"
33
+ )
34
+
35
+ body, check_digit, format_detected = cls.destructure(rut)
36
+ return body, check_digit, format_detected
37
+
38
+ @classmethod
39
+ def destructure(cls, rut: str) -> Tuple[str, str, Optional[RutFormat]]:
40
+ """
41
+ Destructures a RUT string into its body and check digit components,
42
+ while also detecting the input format.
43
+
44
+ Args:
45
+ rut: The RUT string to parse.
46
+
47
+ Returns:
48
+ Tuple of (body, check_digit, format).
49
+ """
50
+ format_detected = RutPatterns.detect_format(rut)
51
+
52
+ if format_detected is None:
53
+ raise RutInvalidFormatError(
54
+ "Formato no válido, se esperaba algo como '12345678-9', '123456789' o '12.345.678-9'"
55
+ )
56
+
57
+ normalized = RutPatterns.normalize(rut)
58
+ body, digit_check = normalized[:-1], normalized[-1]
59
+ return body, digit_check, format_detected
@@ -0,0 +1,123 @@
1
+ """RUT pattern definitions and format detection."""
2
+
3
+ from re import compile
4
+ from typing import Optional
5
+
6
+ from rut_validator.types.enums import RutFormat
7
+
8
+
9
+ class RutPatterns:
10
+ """Collection of regex patterns for RUT validation and format detection."""
11
+
12
+ # Individual patterns for format detection
13
+ FORMATTED_PATTERN = compile(r"^\d{1,2}(?:\.\d{3}){2}-[\dkK]$")
14
+ HYPHENATED_PATTERN = compile(r"^\d{7,8}-[\dkK]$")
15
+ NORMALIZED_PATTERN = compile(r"^\d{7,8}[\dkK]$")
16
+
17
+ # Combined pattern for general validation
18
+ VALIDATION_PATTERN = compile(
19
+ r"^(?:\d{1,2}(?:\.\d{3}){2}-[\dkK]|\d{7,8}-[\dkK]|\d{7,8}[\dkK])$"
20
+ )
21
+
22
+ # Maximum supported length for a formatted RUT string.
23
+ MAX_RUT_LENGTH = 15
24
+
25
+ # Cleaning pattern (removes dots, hyphens, keeps digits and K/k)
26
+ CLEANING_PATTERN = compile(r"[^0-9kK]")
27
+
28
+ @classmethod
29
+ def detect_format(cls, rut: str) -> Optional[RutFormat]:
30
+ """
31
+ Detect the format of a RUT string.
32
+
33
+ Args:
34
+ rut: The RUT string to analyze.
35
+
36
+ Returns:
37
+ RutFormat if the format is recognized, None otherwise.
38
+ """
39
+ if cls.FORMATTED_PATTERN.match(rut):
40
+ return RutFormat.FORMATTED
41
+
42
+ elif cls.HYPHENATED_PATTERN.match(rut):
43
+ return RutFormat.HYPHENATED
44
+
45
+ elif cls.NORMALIZED_PATTERN.match(rut):
46
+ return RutFormat.NORMALIZED
47
+
48
+ return None
49
+
50
+ @classmethod
51
+ def is_valid_format(cls, rut: str) -> bool:
52
+ """
53
+ Check if a RUT string has a valid format (any supported format).
54
+
55
+ Args:
56
+ rut: The RUT string to validate.
57
+
58
+ Returns:
59
+ True if the format is valid, False otherwise.
60
+ """
61
+ if len(rut) > cls.MAX_RUT_LENGTH:
62
+ return False
63
+
64
+ return cls.VALIDATION_PATTERN.match(rut) is not None
65
+
66
+ @classmethod
67
+ def normalize(cls, rut: str) -> str:
68
+ """
69
+ Normalize a RUT string by removing formatting characters.
70
+
71
+ Args:
72
+ rut: The RUT string to normalize.
73
+
74
+ Returns:
75
+ The normalized RUT string (digits + check digit, uppercased).
76
+ """
77
+ return cls.CLEANING_PATTERN.sub("", rut).upper()
78
+
79
+ @classmethod
80
+ def formatted(cls, rut: str) -> str:
81
+ """
82
+ Format a RUT string with dots and hyphen.
83
+
84
+ Args:
85
+ rut: The RUT string to format (any supported RUT format).
86
+
87
+ Returns:
88
+ The formatted RUT string (e.g., "12.345.678-5").
89
+ """
90
+ normalized_rut = cls.normalize(rut)
91
+ body = normalized_rut[:-1]
92
+ check_digit = normalized_rut[-1]
93
+ body_formatted = f"{int(body):,}".replace(",", ".")
94
+ return f"{body_formatted}-{check_digit}"
95
+
96
+ @classmethod
97
+ def hyphenated(cls, rut: str) -> str:
98
+ """
99
+ Format a RUT string with hyphen only (hyphenated format).
100
+
101
+ Args:
102
+ rut: The RUT string to format (any supported RUT format).
103
+
104
+ Returns:
105
+ The hyphenated RUT string (e.g., "12345678-5").
106
+ """
107
+ normalized_rut = cls.normalize(rut)
108
+ body = normalized_rut[:-1]
109
+ check_digit = normalized_rut[-1]
110
+ return f"{body}-{check_digit}"
111
+
112
+ @classmethod
113
+ def normalized(cls, rut: str) -> str:
114
+ """
115
+ Return the normalized RUT string without formatting.
116
+
117
+ Args:
118
+ rut: The RUT string to normalize (any supported RUT format).
119
+
120
+ Returns:
121
+ The normalized RUT string (e.g., "123456785").
122
+ """
123
+ return cls.normalize(rut)
@@ -0,0 +1,165 @@
1
+ """Pure RUT validator - no dependencies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ from typing import TYPE_CHECKING
8
+
9
+ from rut_validator.core.parser import RutParser
10
+ from rut_validator.types.enums import ValidationResult
11
+
12
+ from rut_validator.errors import (
13
+ RutInvalidFormatError,
14
+ RutInvalidValueError,
15
+ RutModuleElevenValidationError,
16
+ )
17
+
18
+ from rut_validator.constants import RUT_MODULE_ELEVEN_FACTORS
19
+
20
+ if TYPE_CHECKING:
21
+ from rut_validator.types.rut import Rut
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class RutValidator:
27
+ """Validator for Chilean RUTs with pure Python logic."""
28
+
29
+ __slots__ = []
30
+
31
+ @classmethod
32
+ def get_validation_result(cls, rut: str) -> ValidationResult:
33
+ """
34
+ Get the validation result for a RUT string.
35
+
36
+ Args:
37
+ rut (str): The RUT string to validate.
38
+
39
+ Returns:
40
+ ValidationResult: The validation result.
41
+ """
42
+ logger.debug(f"Getting validation result for RUT: {rut}")
43
+
44
+ try:
45
+ body, check_digit, _ = RutParser.parse(rut)
46
+
47
+ except RutInvalidValueError:
48
+ logger.debug("RUT invalid due to empty or missing value")
49
+ return ValidationResult.INVALID_VALUE
50
+
51
+ except RutInvalidFormatError:
52
+ logger.debug("RUT invalid due to incorrect format")
53
+ return ValidationResult.INVALID_FORMAT
54
+
55
+ if cls.is_valid_check_digit(body, check_digit):
56
+ logger.debug("RUT check digit is valid")
57
+ return ValidationResult.VALID
58
+
59
+ logger.debug("RUT check digit is invalid")
60
+ return ValidationResult.INVALID_CHECK_DIGIT
61
+
62
+ @classmethod
63
+ def is_valid(cls, rut: str) -> bool:
64
+ """
65
+ Check if a RUT string is valid without raising exceptions.
66
+
67
+ Args:
68
+ rut (str): The RUT string to check.
69
+
70
+ Returns:
71
+ bool: True if valid, False otherwise.
72
+ """
73
+ validation_result = cls.get_validation_result(rut)
74
+ is_valid = validation_result == ValidationResult.VALID
75
+
76
+ logger.debug(f"RUT {rut} validity: {is_valid} ({validation_result})")
77
+
78
+ return is_valid
79
+
80
+ @classmethod
81
+ def validate(cls, rut: str) -> Rut:
82
+ """
83
+ Validate a RUT string and return a Rut object.
84
+
85
+ Args:
86
+ rut (str): The RUT string to validate.
87
+
88
+ Returns:
89
+ Rut: A validated Rut object.
90
+
91
+ Raises:
92
+ RutInvalidValueError: If the RUT is empty or missing.
93
+ RutInvalidFormatError: If the RUT format is invalid.
94
+ RutModuleElevenValidationError: If the check digit does not match.
95
+ """
96
+ logger.debug(f"Validating RUT: {rut}")
97
+
98
+ try:
99
+ body, check_digit, format_detected = RutParser.parse(rut)
100
+
101
+ except RutInvalidValueError:
102
+ raise RutInvalidValueError(
103
+ "No se puede parsear un RUT vacío, por favor ingrese un valor"
104
+ )
105
+
106
+ except RutInvalidFormatError:
107
+ raise RutInvalidFormatError(
108
+ "Formato no válido, se esperaba algo como '12345678-9', "
109
+ "'123456789' o '12.345.678-9'"
110
+ )
111
+
112
+ if not cls.is_valid_check_digit(body, check_digit):
113
+ check_digit_expected = cls.module_eleven(body)
114
+
115
+ logger.warning(
116
+ f"Validation failed for RUT {rut}: expected {check_digit_expected}, got {check_digit}"
117
+ )
118
+
119
+ raise RutModuleElevenValidationError(
120
+ f"El dígito verificador no coincide, se esperaba '{check_digit_expected}' en vez de '{check_digit}'"
121
+ )
122
+
123
+ from rut_validator.types.rut import Rut
124
+
125
+ logger.info(f"Validation successful for RUT: {rut}")
126
+ return Rut(rut, format_detected, skip_validation=True)
127
+
128
+ @classmethod
129
+ def module_eleven(cls, body: str) -> str:
130
+ """
131
+ Calculates the check digit using the modulo 11 algorithm.
132
+
133
+ Returns:
134
+ str: The calculated check digit (0-9 or 'K')
135
+ """
136
+ reversed_digits = map(int, reversed(body))
137
+ total = sum(
138
+ d * RUT_MODULE_ELEVEN_FACTORS[i % len(RUT_MODULE_ELEVEN_FACTORS)]
139
+ for i, d in enumerate(reversed_digits)
140
+ )
141
+ remainder = total % 11
142
+ result = 11 - remainder
143
+
144
+ if result == 11:
145
+ return "0"
146
+
147
+ if result == 10:
148
+ return "K"
149
+
150
+ return str(result)
151
+
152
+ @classmethod
153
+ def is_valid_check_digit(cls, body: str, check_digit: str) -> bool:
154
+ """
155
+ Validates that the provided check digit matches the calculated one.
156
+
157
+ Args:
158
+ body (str): The numeric part of the RUT.
159
+ check_digit (str): The check digit to validate.
160
+
161
+ Returns:
162
+ bool: True if the check digit is valid, False otherwise.
163
+ """
164
+ expected_check_digit = cls.module_eleven(body)
165
+ return check_digit.upper() == expected_check_digit
@@ -0,0 +1,25 @@
1
+ """Custom exceptions for RUT validation."""
2
+
3
+
4
+ class RutValidationError(ValueError):
5
+ """Base exception for all RUT validation errors."""
6
+
7
+ pass
8
+
9
+
10
+ class RutInvalidValueError(RutValidationError):
11
+ """Raised when the RUT value is empty or invalid."""
12
+
13
+ pass
14
+
15
+
16
+ class RutInvalidFormatError(RutValidationError):
17
+ """Raised when the RUT format is invalid."""
18
+
19
+ pass
20
+
21
+
22
+ class RutModuleElevenValidationError(RutValidationError):
23
+ """Raised when the RUT fails the modulo 11 validation."""
24
+
25
+ pass
rut_validator/py.typed ADDED
File without changes
@@ -0,0 +1,5 @@
1
+ """RUT data types"""
2
+
3
+ from .enums import ValidationResult, RutFormat
4
+
5
+ __all__ = ["RutFormat", "ValidationResult"]
@@ -0,0 +1,18 @@
1
+ from enum import Enum
2
+
3
+
4
+ class RutFormat(Enum):
5
+ """Enumeration of supported RUT formats."""
6
+
7
+ FORMATTED = "formatted" # 12.345.678-9
8
+ HYPHENATED = "hyphenated" # 12345678-9
9
+ NORMALIZED = "normalized" # 123456789
10
+
11
+
12
+ class ValidationResult(Enum):
13
+ """Result states for RUT validation."""
14
+
15
+ VALID = "valid"
16
+ INVALID_VALUE = "invalid_value"
17
+ INVALID_FORMAT = "invalid_format"
18
+ INVALID_CHECK_DIGIT = "invalid_check_digit"
@@ -0,0 +1,136 @@
1
+ """Validated RUT value object."""
2
+
3
+ from typing import Optional
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from rut_validator.core.parser import RutParser
8
+ from rut_validator.core.patterns import RutPatterns, RutFormat
9
+ from rut_validator.errors import RutModuleElevenValidationError
10
+
11
+
12
+ @dataclass(init=True)
13
+ class Rut:
14
+ """
15
+ A class representing a validated RUT (Rol Único Tributario) value object.
16
+
17
+ This class encapsulates a validated RUT value and provides properties to access its components,
18
+ such as the body and check digit, as well as formatted and normalized representations.
19
+
20
+ The Rut class ensures that the RUT value is valid upon instantiation,
21
+ and it provides methods to retrieve the normalized and formatted versions of the RUT.
22
+
23
+ The normalized version is the raw RUT value without any formatting,
24
+ while the formatted version includes thousands separators and a hyphen before the check digit.
25
+
26
+ Attributes:
27
+ value (str): The original RUT input string as provided by the caller.
28
+ format (Optional[RutFormat]): The detected format of the input.
29
+
30
+ Methods:
31
+ normalized: Returns the normalized RUT value.
32
+ formatted: Returns the formatted RUT value.
33
+ body: Returns the body of the RUT (the numeric part).
34
+ check_digit: Returns the check digit of the RUT.
35
+ is_formatted: Returns True if input was formatted format.
36
+ is_hyphenated: Returns True if input was hyphenated format.
37
+ is_normalized: Returns True if input was normalized format.
38
+ """
39
+
40
+ __slots__ = ("value", "format")
41
+
42
+ def __init__(
43
+ self,
44
+ value: str,
45
+ format_detected: Optional[RutFormat] = None,
46
+ *,
47
+ skip_validation: bool = False,
48
+ ):
49
+ body, check_digit, detected_format = RutParser.destructure(value)
50
+ self.value = value
51
+ self.format = (
52
+ format_detected if format_detected is not None else detected_format
53
+ )
54
+
55
+ if not skip_validation:
56
+ from rut_validator.core.validator import RutValidator
57
+
58
+ if not RutValidator.is_valid_check_digit(body, check_digit):
59
+ raise RutModuleElevenValidationError(
60
+ f"El dígito verificador no coincide, se esperaba '{RutValidator.module_eleven(body)}' en vez de '{check_digit}'"
61
+ )
62
+
63
+ @property
64
+ def normalized(self) -> str:
65
+ """
66
+ Returns the normalized RUT value.
67
+ """
68
+ return RutPatterns.normalize(self.value)
69
+
70
+ @property
71
+ def formatted(self) -> str:
72
+ """
73
+ Returns the formatted RUT value.
74
+ """
75
+ return RutPatterns.formatted(self.normalized)
76
+
77
+ @property
78
+ def hyphenated(self) -> str:
79
+ """
80
+ Returns the hyphenated RUT value.
81
+ """
82
+ return RutPatterns.hyphenated(self.normalized)
83
+
84
+ @property
85
+ def body(self) -> int:
86
+ """
87
+ Returns the body of the RUT (the numeric part).
88
+ """
89
+ return int(self.normalized[:-1])
90
+
91
+ @property
92
+ def check_digit(self) -> str:
93
+ """
94
+ Returns the check digit of the RUT.
95
+ """
96
+ return self.normalized[-1]
97
+
98
+ @property
99
+ def is_formatted(self) -> bool:
100
+ """
101
+ Returns True if input was formatted format.
102
+ """
103
+ return self.format == RutFormat.FORMATTED
104
+
105
+ @property
106
+ def is_hyphenated(self) -> bool:
107
+ """
108
+ Returns True if input was hyphenated format.
109
+ """
110
+ return self.format == RutFormat.HYPHENATED
111
+
112
+ @property
113
+ def is_normalized(self) -> bool:
114
+ """
115
+ Returns True if input was normalized format.
116
+ """
117
+ return self.format == RutFormat.NORMALIZED
118
+
119
+ def __str__(self) -> str:
120
+ return self.formatted
121
+
122
+ def __repr__(self) -> str:
123
+ return f"Rut(value='{self.value}', format={self.format})"
124
+
125
+ def equals(self, other: object) -> bool:
126
+ """Return True when two Rut objects represent the same normalized RUT."""
127
+ return self == other
128
+
129
+ def __eq__(self, other: object) -> bool:
130
+ if not isinstance(other, Rut):
131
+ return NotImplemented
132
+
133
+ return self.normalized == other.normalized
134
+
135
+ def __hash__(self) -> int:
136
+ return hash(self.normalized)
File without changes
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.4
2
+ Name: rut-validator
3
+ Version: 0.1.0
4
+ Summary: Chilean RUT validation library with Pydantic, Django, and SQLAlchemy support
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Keywords: rut,chile,validation,pydantic,django,sqlalchemy,fastapi
8
+ Author: Eli-ezer Reuven Ramirez Ruiz
9
+ Author-email: ramirez.ruiz.eliezer.reuven@gmail.com
10
+ Requires-Python: >=3.9,<4.0
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
+ Classifier: Topic :: Utilities
23
+ Provides-Extra: dev
24
+ Provides-Extra: docs
25
+ Provides-Extra: fastapi
26
+ Provides-Extra: pydantic
27
+ Requires-Dist: black ; extra == "dev"
28
+ Requires-Dist: click (>=8.0.0,<9.0.0)
29
+ Requires-Dist: django (>=3.2,<5.0)
30
+ Requires-Dist: fastapi (>=0.111.0,<1.0.0) ; extra == "fastapi"
31
+ Requires-Dist: flake8 ; extra == "dev"
32
+ Requires-Dist: isort ; extra == "dev"
33
+ Requires-Dist: mypy ; extra == "dev"
34
+ Requires-Dist: myst-parser ; extra == "docs"
35
+ Requires-Dist: pre-commit ; extra == "dev"
36
+ Requires-Dist: pydantic (>=2.12.5,<3.0.0) ; extra == "pydantic"
37
+ Requires-Dist: pytest (>=7.0.0) ; extra == "dev"
38
+ Requires-Dist: pytest-cov ; extra == "dev"
39
+ Requires-Dist: ruff ; extra == "dev"
40
+ Requires-Dist: sphinx ; extra == "docs"
41
+ Requires-Dist: sphinx-rtd-theme ; extra == "docs"
42
+ Requires-Dist: sqlalchemy (>=2.0.49,<3.0.0)
43
+ Project-URL: Changelog, https://github.com/yourusername/rut-validator/blob/main/CHANGELOG.md
44
+ Project-URL: Documentation, https://rut-validator.readthedocs.io/
45
+ Project-URL: Homepage, https://github.com/yourusername/rut-validator
46
+ Project-URL: Issues, https://github.com/yourusername/rut-validator/issues
47
+ Project-URL: Repository, https://github.com/yourusername/rut-validator
48
+ Description-Content-Type: text/markdown
49
+
50
+ # rut-validator
51
+
52
+ [![PyPI version](https://badge.fury.io/py/rut-validator.svg)](https://pypi.org/project/rut-validator/)
53
+ [![Python versions](https://img.shields.io/pypi/pyversions/rut-validator.svg)](https://pypi.org/project/rut-validator/)
54
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
55
+ [![CI](https://github.com/yourusername/rut-validator/workflows/CI/badge.svg)](https://github.com/yourusername/rut-validator/actions)
56
+ [![codecov](https://codecov.io/gh/yourusername/rut-validator/branch/main/graph/badge.svg)](https://codecov.io/gh/yourusername/rut-validator)
57
+ [![Documentation Status](https://readthedocs.org/projects/rut-validator/badge/?version=latest)](https://rut-validator.readthedocs.io/en/latest/?badge=latest)
58
+
59
+ Librería para validar RUT chileno con enfoque Pydantic-first y soporte completo para frameworks web.
60
+
61
+ ## ✨ Características
62
+
63
+ - ✅ **Validación pura de RUT chileno** usando algoritmo módulo 11
64
+ - ✅ **Detección automática de formato**: dotted (`12.345.678-9`), hyphenated (`12345678-9`), numeric (`123456789`)
65
+ - ✅ **Integración completa con Pydantic** (`RutStr`)
66
+ - ✅ **Campo Django** (`RUTField`) listo para usar
67
+ - ✅ **Tipo SQLAlchemy** (`RutSQLAlchemy`) para bases de datos
68
+ - ✅ **Compatible con FastAPI** y otros frameworks web
69
+ - ✅ **Type hints completos** para mejor desarrollo
70
+ - ✅ **Sin dependencias externas** para funcionalidad core
71
+ - ✅ **Tests exhaustivos** con alta cobertura
72
+
73
+ ## 🚀 Instalación
74
+
75
+ ```bash
76
+ pip install rut-validator
77
+
78
+ # Con soporte Pydantic
79
+ pip install rut-validator[pydantic]
80
+
81
+ # Con soporte FastAPI
82
+ pip install rut-validator[fastapi]
83
+
84
+ # Para desarrollo
85
+ pip install rut-validator[dev]
86
+ ```
87
+
88
+ ## 📖 Uso Básico
89
+
90
+ ### Validación Simple
91
+
92
+ ```python
93
+ from rut_validator import RutValidator
94
+
95
+ # Validar RUT con cualquier formato
96
+ rut = RutValidator.validate("20.884.437-7")
97
+ print(f"RUT válido: {rut.formatted}") # "20.884.437-7"
98
+ print(f"Número: {rut.number}") # 20884437
99
+ print(f"Dígito: {rut.digit}") # "7"
100
+ print(f"Formato: {rut.format}") # RutFormat.DOTTED
101
+ ```
102
+
103
+ ### Detección de Formato
104
+
105
+ ```python
106
+ from rut_validator import RutValidator
107
+
108
+ # La librería detecta automáticamente el formato de entrada
109
+ formats = [
110
+ "20.884.437-7", # Formato dotted
111
+ "20884437-7", # Formato hyphenated
112
+ "208844377", # Formato numeric
113
+ ]
114
+
115
+ for rut_str in formats:
116
+ rut = RutValidator.validate(rut_str)
117
+ print(f"'{rut_str}' -> Formato: {rut.format}, Es dotted: {rut.is_dotted}")
118
+ ```
119
+
120
+ ### Con Pydantic
121
+
122
+ ```python
123
+ from pydantic import BaseModel
124
+ from rut_validator import RutStr
125
+
126
+ class User(BaseModel):
127
+ name: str
128
+ rut: RutStr # Validación automática
129
+
130
+ # Uso
131
+ user = User(name="Juan Pérez", rut="12.345.678-5")
132
+ print(user.rut) # "12.345.678-5"
133
+ ```
134
+
135
+ ### Con Django
136
+
137
+ ```python
138
+ from django.db import models
139
+ from rut_validator.core.orm.django.schema import RUTField
140
+
141
+ class Person(models.Model):
142
+ name = models.CharField(max_length=100)
143
+ rut = RUTField(unique=True) # Validación automática en DB
144
+ ```
145
+
146
+ ### Con SQLAlchemy
147
+
148
+ ```python
149
+ from sqlalchemy import Column, Integer, String
150
+ from sqlalchemy.ext.declarative import declarative_base
151
+ from rut_validator.core.orm.sqlalchemy.schema import RutSQLAlchemy
152
+
153
+ Base = declarative_base()
154
+
155
+ class Person(Base):
156
+ __tablename__ = 'persons'
157
+
158
+ id = Column(Integer, primary_key=True)
159
+ name = Column(String)
160
+ rut = Column(RutSQLAlchemy) # Validación automática
161
+ ```
162
+
163
+ ## 📚 Guía Completa
164
+
165
+ Lee la documentación completa en [docs/GUIA_RUT_VALIDATOR.md](docs/GUIA_RUT_VALIDATOR.md)
166
+
167
+ ## 🧪 Ejemplos
168
+
169
+ - [Validación pura](examples/01_pure_validation.py)
170
+ - [Uso con Pydantic](examples/02_pydantic_usage.py)
171
+ - [Uso con FastAPI](examples/03_fastapi_usage.py)
172
+
173
+ ## 🔧 Desarrollo
174
+
175
+ ### Configuración del entorno
176
+
177
+ ```bash
178
+ # Clonar repositorio
179
+ git clone https://github.com/yourusername/rut-validator.git
180
+ cd rut-validator
181
+
182
+ # Instalar dependencias de desarrollo
183
+ poetry install --with dev
184
+
185
+ # Ejecutar tests
186
+ poetry run pytest
187
+
188
+ # Ejecutar linting
189
+ poetry run black src/ tests/
190
+ poetry run isort src/ tests/
191
+ poetry run flake8 src/ tests/
192
+ poetry run mypy src/rut_validator/
193
+ ```
194
+
195
+ ### Pre-commit hooks
196
+
197
+ ```bash
198
+ poetry run pre-commit install
199
+ ```
200
+
201
+ ## 🤝 Contribuir
202
+
203
+ 1. Fork el proyecto
204
+ 2. Crea una rama para tu feature (`git checkout -b feature/AmazingFeature`)
205
+ 3. Commit tus cambios (`git commit -m 'Add some AmazingFeature'`)
206
+ 4. Push a la rama (`git push origin feature/AmazingFeature`)
207
+ 5. Abre un Pull Request
208
+
209
+ ## 📄 Licencia
210
+
211
+ Este proyecto está bajo la Licencia MIT - ver el archivo [LICENSE](LICENSE) para más detalles.
212
+
213
+ ## 🙏 Agradecimientos
214
+
215
+ - Algoritmo de validación basado en el estándar chileno del Servicio de Impuestos Internos
216
+ - Inspirado en bibliotecas similares pero con enfoque moderno y tipado fuerte
217
+
218
+ ## 📞 Soporte
219
+
220
+ - 🐛 [Reportar bugs](https://github.com/yourusername/rut-validator/issues)
221
+ - 💡 [Sugerir features](https://github.com/yourusername/rut-validator/issues)
222
+ - 📖 [Documentación](https://rut-validator.readthedocs.io/)
223
+
@@ -0,0 +1,24 @@
1
+ rut_validator/__init__.py,sha256=UaZ-sj1ctf6KeVso3DtqXXudiHDDtoK5BhYeQ6VI_O8,793
2
+ rut_validator/constants.py,sha256=5VXbvM2vHhRGwz_E6bvNuexZTugsKh82DCdrVbB4R5o,84
3
+ rut_validator/core/__init__.py,sha256=S8IGTUcn-COEUEyOiBvQsBt2AzFxBdKwPcVYKyOcB5k,311
4
+ rut_validator/core/formatter.py,sha256=gdwohjrJVYGUYuS-yftnkjj08fKLMZYrUSsExQbcQdE,1022
5
+ rut_validator/core/orm/django/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ rut_validator/core/orm/django/schema.py,sha256=FaKdER1GW7EVB582zp4kpiRiAjJ5Oiz-4JsFh0ns-hc,2703
7
+ rut_validator/core/orm/pydantic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ rut_validator/core/orm/pydantic/schema.py,sha256=jG4kCbF2bgqbnZFai-xp_BSR-dyNuwJrY4YPbHCzKEw,2589
9
+ rut_validator/core/orm/sqlalchemy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ rut_validator/core/orm/sqlalchemy/schema.py,sha256=YBiO1wBAJUe1MIM0_KUoVEFDFj-APrboH7j8EYbVWBE,2576
11
+ rut_validator/core/parser.py,sha256=HVyn9aI_SaHbAaSJKw30crE0ot-WJX7o2v4dvoSUwg8,1822
12
+ rut_validator/core/patterns.py,sha256=JcDhtQ1FlBLW8sWADRUnG-Gc8YFUeMwRzIsMXkZ5fhs,3538
13
+ rut_validator/core/validator.py,sha256=-jeA86GbLlglfllYSbXh1_A6cCXdNf_NQOcDT7h6uW8,4915
14
+ rut_validator/errors.py,sha256=X_iM1yqssh_-2QAgC0IOud5bGavA_1J6LEFeM5flFss,509
15
+ rut_validator/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
+ rut_validator/types/__init__.py,sha256=2t2lpWbiSKLE4P6g8_FCyNYAXBtAw5VMRJNX3QxGOnI,114
17
+ rut_validator/types/enums.py,sha256=72wIRUQN-xcKT1Ix0tOpIafKvW1HE9xr1HYZTiO5xLc,446
18
+ rut_validator/types/rut.py,sha256=7LAYFF3XAbxaVa9XpsnLevJDQXZBEuyCw7hA7dbXO6c,4241
19
+ rut_validator/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
+ rut_validator-0.1.0.dist-info/METADATA,sha256=u7Ovic-hleqo5J7dMHnxfrhgwyGClR8ZfzE2EjXOZp8,7210
21
+ rut_validator-0.1.0.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
22
+ rut_validator-0.1.0.dist-info/entry_points.txt,sha256=UBDN0ZY58VIptPbuUnPIpnJubUOKFcbvXofDgjdUMs0,55
23
+ rut_validator-0.1.0.dist-info/licenses/LICENSE,sha256=RWPNeNp8UvOe5ReVomHctEU8umwticGydzEwaj4Ihag,1084
24
+ rut_validator-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.4.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ rut-validator=rut_validator.cli:cli
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eli-ezer Reuven Ramirez Ruiz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.