openlinktoken 2.0.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.
- openlinktoken/__init__.py +15 -0
- openlinktoken/attributes/__init__.py +17 -0
- openlinktoken/attributes/attribute.py +26 -0
- openlinktoken/attributes/attribute_expression.py +232 -0
- openlinktoken/attributes/attribute_loader.py +48 -0
- openlinktoken/attributes/base_attribute.py +31 -0
- openlinktoken/attributes/combined_attribute.py +69 -0
- openlinktoken/attributes/general/__init__.py +17 -0
- openlinktoken/attributes/general/date_attribute.py +117 -0
- openlinktoken/attributes/general/decimal_attribute.py +86 -0
- openlinktoken/attributes/general/integer_attribute.py +78 -0
- openlinktoken/attributes/general/record_id_attribute.py +41 -0
- openlinktoken/attributes/general/string_attribute.py +55 -0
- openlinktoken/attributes/general/year_attribute.py +78 -0
- openlinktoken/attributes/person/__init__.py +25 -0
- openlinktoken/attributes/person/age_attribute.py +41 -0
- openlinktoken/attributes/person/birth_date_attribute.py +40 -0
- openlinktoken/attributes/person/birth_year_attribute.py +39 -0
- openlinktoken/attributes/person/canadian_postal_code_attribute.py +165 -0
- openlinktoken/attributes/person/first_name_attribute.py +112 -0
- openlinktoken/attributes/person/last_name_attribute.py +146 -0
- openlinktoken/attributes/person/postal_code_attribute.py +42 -0
- openlinktoken/attributes/person/sex_attribute.py +48 -0
- openlinktoken/attributes/person/social_security_number_attribute.py +119 -0
- openlinktoken/attributes/person/us_postal_code_attribute.py +104 -0
- openlinktoken/attributes/serializable_attribute.py +7 -0
- openlinktoken/attributes/utilities/__init__.py +7 -0
- openlinktoken/attributes/utilities/attribute_utilities.py +224 -0
- openlinktoken/attributes/validation/__init__.py +23 -0
- openlinktoken/attributes/validation/age_range_validator.py +41 -0
- openlinktoken/attributes/validation/attribute_validator.py +22 -0
- openlinktoken/attributes/validation/date_range_validator.py +111 -0
- openlinktoken/attributes/validation/not_in_validator.py +48 -0
- openlinktoken/attributes/validation/not_null_or_empty_validator.py +21 -0
- openlinktoken/attributes/validation/not_starts_with_validator.py +48 -0
- openlinktoken/attributes/validation/regex_validator.py +44 -0
- openlinktoken/attributes/validation/serializable_attribute_validator.py +26 -0
- openlinktoken/attributes/validation/year_range_validator.py +48 -0
- openlinktoken/ec_key_utils.py +207 -0
- openlinktoken/exchange_config.py +305 -0
- openlinktoken/exchange_jwe.py +107 -0
- openlinktoken/metadata.py +104 -0
- openlinktoken/tokens/__init__.py +19 -0
- openlinktoken/tokens/base_token_definition.py +48 -0
- openlinktoken/tokens/definitions/t1_token.py +39 -0
- openlinktoken/tokens/definitions/t2_token.py +39 -0
- openlinktoken/tokens/definitions/t3_token.py +39 -0
- openlinktoken/tokens/definitions/t4_token.py +37 -0
- openlinktoken/tokens/definitions/t5_token.py +37 -0
- openlinktoken/tokens/token.py +38 -0
- openlinktoken/tokens/token_definition.py +46 -0
- openlinktoken/tokens/token_generation_exception.py +16 -0
- openlinktoken/tokens/token_generator.py +201 -0
- openlinktoken/tokens/token_generator_result.py +33 -0
- openlinktoken/tokens/token_registry.py +46 -0
- openlinktoken/tokens/tokenizer/__init__.py +11 -0
- openlinktoken/tokens/tokenizer/passthrough_tokenizer.py +66 -0
- openlinktoken/tokens/tokenizer/sha256_tokenizer.py +72 -0
- openlinktoken/tokens/tokenizer/tokenizer.py +31 -0
- openlinktoken/tokentransformer/__init__.py +43 -0
- openlinktoken/tokentransformer/decrypt_token_transformer.py +83 -0
- openlinktoken/tokentransformer/encrypt_token_transformer.py +85 -0
- openlinktoken/tokentransformer/encryption_constants.py +23 -0
- openlinktoken/tokentransformer/hash_token_transformer.py +88 -0
- openlinktoken/tokentransformer/jwe_match_token_formatter.py +128 -0
- openlinktoken/tokentransformer/match_token_constants.py +57 -0
- openlinktoken/tokentransformer/no_operation_token_transformer.py +24 -0
- openlinktoken/tokentransformer/token_transformer.py +22 -0
- openlinktoken-2.0.0.dist-info/METADATA +21 -0
- openlinktoken-2.0.0.dist-info/RECORD +72 -0
- openlinktoken-2.0.0.dist-info/WHEEL +5 -0
- openlinktoken-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
|
|
5
|
+
from openlinktoken.attributes.base_attribute import BaseAttribute
|
|
6
|
+
from openlinktoken.attributes.validation import RegexValidator
|
|
7
|
+
from openlinktoken.attributes.validation.serializable_attribute_validator import SerializableAttributeValidator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IntegerAttribute(BaseAttribute):
|
|
11
|
+
"""Represents a generic integer attribute.
|
|
12
|
+
|
|
13
|
+
This class extends BaseAttribute and provides functionality for working with
|
|
14
|
+
integer fields. It recognizes "Integer" as a valid alias for this attribute type.
|
|
15
|
+
|
|
16
|
+
The attribute performs normalization on input values by trimming whitespace
|
|
17
|
+
and validates that the input is a valid integer.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
NAME = "Integer"
|
|
21
|
+
ALIASES = [NAME]
|
|
22
|
+
|
|
23
|
+
# Regular expression pattern for validating integer format with optional sign
|
|
24
|
+
VALIDATION_PATTERN = r"^\s*[+-]?\d+\s*$"
|
|
25
|
+
|
|
26
|
+
def __init__(self, additional_validators: Optional[List[SerializableAttributeValidator]] = None):
|
|
27
|
+
"""
|
|
28
|
+
Initialize the IntegerAttribute with optional additional validators.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
additional_validators: Optional list of additional validators to apply
|
|
32
|
+
"""
|
|
33
|
+
validation_rules: List[SerializableAttributeValidator] = [RegexValidator(self.VALIDATION_PATTERN)]
|
|
34
|
+
if additional_validators:
|
|
35
|
+
validation_rules.extend(additional_validators)
|
|
36
|
+
super().__init__(validation_rules)
|
|
37
|
+
|
|
38
|
+
def get_name(self) -> str:
|
|
39
|
+
"""Get the name of the attribute.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
str: The name "Integer"
|
|
43
|
+
"""
|
|
44
|
+
return self.NAME
|
|
45
|
+
|
|
46
|
+
def get_aliases(self) -> List[str]:
|
|
47
|
+
"""Get the aliases for the attribute.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
List[str]: A list containing the aliases for this attribute
|
|
51
|
+
"""
|
|
52
|
+
return self.ALIASES.copy()
|
|
53
|
+
|
|
54
|
+
def normalize(self, value: str) -> str:
|
|
55
|
+
"""Normalize the integer value by trimming whitespace and parsing.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
value: The integer string to normalize
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
str: The normalized integer value
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
ValueError: If the integer is not valid or empty
|
|
65
|
+
"""
|
|
66
|
+
if value is None:
|
|
67
|
+
raise ValueError("Integer value cannot be null")
|
|
68
|
+
if not value:
|
|
69
|
+
raise ValueError("Invalid integer format: empty or whitespace")
|
|
70
|
+
|
|
71
|
+
trimmed = value.strip()
|
|
72
|
+
|
|
73
|
+
# Validate it's a valid integer
|
|
74
|
+
try:
|
|
75
|
+
int_value = int(trimmed)
|
|
76
|
+
return str(int_value)
|
|
77
|
+
except ValueError:
|
|
78
|
+
raise ValueError(f"Invalid integer format: {value}")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from openlinktoken.attributes.general.string_attribute import StringAttribute
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RecordIdAttribute(StringAttribute):
|
|
9
|
+
"""Represents a record identifier attribute.
|
|
10
|
+
|
|
11
|
+
This class extends StringAttribute and provides functionality for working with
|
|
12
|
+
record ID fields. It recognizes "RecordId" as a valid alias for this
|
|
13
|
+
attribute type.
|
|
14
|
+
|
|
15
|
+
The attribute normalizes values by trimming whitespace (inherited from StringAttribute).
|
|
16
|
+
It validates that the value is not null or empty using the default BaseAttribute
|
|
17
|
+
validation rules.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
NAME = "RecordId"
|
|
21
|
+
ALIASES = [NAME, "Id"]
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
# Use validation rules from StringAttribute
|
|
25
|
+
super().__init__()
|
|
26
|
+
|
|
27
|
+
def get_name(self) -> str:
|
|
28
|
+
"""Get the name of the attribute.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
str: The name "RecordId"
|
|
32
|
+
"""
|
|
33
|
+
return self.NAME
|
|
34
|
+
|
|
35
|
+
def get_aliases(self) -> List[str]:
|
|
36
|
+
"""Get the aliases for the attribute.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
List[str]: A list containing the aliases for this attribute
|
|
40
|
+
"""
|
|
41
|
+
return self.ALIASES.copy()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from openlinktoken.attributes.base_attribute import BaseAttribute
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class StringAttribute(BaseAttribute):
|
|
9
|
+
"""Represents a generic string attribute.
|
|
10
|
+
|
|
11
|
+
This class extends BaseAttribute and provides functionality for working with
|
|
12
|
+
string fields. It normalizes values by trimming whitespace.
|
|
13
|
+
|
|
14
|
+
The attribute validates that the value is not null or empty using the
|
|
15
|
+
default BaseAttribute validation rules.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
NAME = "String"
|
|
19
|
+
ALIASES = [NAME, "Text"]
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
# Use default validation rules from BaseAttribute (not null or empty)
|
|
23
|
+
super().__init__()
|
|
24
|
+
|
|
25
|
+
def get_name(self) -> str:
|
|
26
|
+
"""Get the name of the attribute.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
str: The name "String"
|
|
30
|
+
"""
|
|
31
|
+
return self.NAME
|
|
32
|
+
|
|
33
|
+
def get_aliases(self) -> List[str]:
|
|
34
|
+
"""Get the aliases for the attribute.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List[str]: A list containing the aliases for this attribute
|
|
38
|
+
"""
|
|
39
|
+
return self.ALIASES.copy()
|
|
40
|
+
|
|
41
|
+
def normalize(self, value: str) -> str:
|
|
42
|
+
"""Normalize the string value by trimming whitespace.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
value: The string value to normalize
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
str: The trimmed string value
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
ValueError: If value is None
|
|
52
|
+
"""
|
|
53
|
+
if value is None:
|
|
54
|
+
raise ValueError("String value cannot be null")
|
|
55
|
+
return value.strip()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from openlinktoken.attributes.general.integer_attribute import IntegerAttribute
|
|
7
|
+
from openlinktoken.attributes.validation import RegexValidator
|
|
8
|
+
from openlinktoken.attributes.validation.serializable_attribute_validator import SerializableAttributeValidator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class YearAttribute(IntegerAttribute):
|
|
12
|
+
"""Represents a generic year attribute.
|
|
13
|
+
|
|
14
|
+
This class extends IntegerAttribute and provides functionality for working with
|
|
15
|
+
year fields. It recognizes "Year" as a valid alias for this attribute type.
|
|
16
|
+
|
|
17
|
+
The attribute performs normalization on input values by trimming whitespace
|
|
18
|
+
and validates that the year is a 4-digit year format.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
NAME = "Year"
|
|
22
|
+
ALIASES = [NAME]
|
|
23
|
+
|
|
24
|
+
# Regular expression pattern for validating 4-digit year format with optional whitespace
|
|
25
|
+
VALIDATION_PATTERN = re.compile(r"^\s*\d{4}\s*$")
|
|
26
|
+
# Pattern for extracting just the 4 digits
|
|
27
|
+
YEAR_PATTERN = re.compile(r"\d{4}")
|
|
28
|
+
|
|
29
|
+
def __init__(self, additional_validators: List[SerializableAttributeValidator] = None):
|
|
30
|
+
"""
|
|
31
|
+
Initialize the YearAttribute with optional additional validators.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
additional_validators: Optional list of additional validators to apply
|
|
35
|
+
"""
|
|
36
|
+
validation_rules = [RegexValidator(self.VALIDATION_PATTERN)]
|
|
37
|
+
if additional_validators:
|
|
38
|
+
validation_rules.extend(additional_validators)
|
|
39
|
+
super().__init__(validation_rules)
|
|
40
|
+
|
|
41
|
+
def get_name(self) -> str:
|
|
42
|
+
"""Get the name of the attribute.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
str: The name "Year"
|
|
46
|
+
"""
|
|
47
|
+
return self.NAME
|
|
48
|
+
|
|
49
|
+
def get_aliases(self) -> List[str]:
|
|
50
|
+
"""Get the aliases for the attribute.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
List[str]: A list containing the aliases for this attribute
|
|
54
|
+
"""
|
|
55
|
+
return self.ALIASES.copy()
|
|
56
|
+
|
|
57
|
+
def normalize(self, value: str) -> str:
|
|
58
|
+
"""Normalize the year value by trimming whitespace.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
value: The year string to normalize
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
str: The normalized year value as a string (leading zeros removed)
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
ValueError: If the year is not a valid 4-digit year or empty
|
|
68
|
+
"""
|
|
69
|
+
if not value:
|
|
70
|
+
raise ValueError(f"Invalid year format: {value}")
|
|
71
|
+
|
|
72
|
+
trimmed = value.strip()
|
|
73
|
+
|
|
74
|
+
# Validate it's exactly 4 digits using the pattern before delegating normalization
|
|
75
|
+
if not self.YEAR_PATTERN.match(trimmed):
|
|
76
|
+
raise ValueError(f"Invalid year format: {value}")
|
|
77
|
+
|
|
78
|
+
return super().normalize(trimmed)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Person attribute implementations."""
|
|
2
|
+
|
|
3
|
+
from .age_attribute import AgeAttribute
|
|
4
|
+
from .birth_date_attribute import BirthDateAttribute
|
|
5
|
+
from .birth_year_attribute import BirthYearAttribute
|
|
6
|
+
from .canadian_postal_code_attribute import CanadianPostalCodeAttribute
|
|
7
|
+
from .first_name_attribute import FirstNameAttribute
|
|
8
|
+
from .last_name_attribute import LastNameAttribute
|
|
9
|
+
from .postal_code_attribute import PostalCodeAttribute
|
|
10
|
+
from .sex_attribute import SexAttribute
|
|
11
|
+
from .social_security_number_attribute import SocialSecurityNumberAttribute
|
|
12
|
+
from .us_postal_code_attribute import USPostalCodeAttribute
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"AgeAttribute",
|
|
16
|
+
"BirthDateAttribute",
|
|
17
|
+
"BirthYearAttribute",
|
|
18
|
+
"CanadianPostalCodeAttribute",
|
|
19
|
+
"FirstNameAttribute",
|
|
20
|
+
"LastNameAttribute",
|
|
21
|
+
"PostalCodeAttribute",
|
|
22
|
+
"SexAttribute",
|
|
23
|
+
"SocialSecurityNumberAttribute",
|
|
24
|
+
"USPostalCodeAttribute",
|
|
25
|
+
]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from openlinktoken.attributes.general.integer_attribute import IntegerAttribute
|
|
6
|
+
from openlinktoken.attributes.validation.age_range_validator import AgeRangeValidator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AgeAttribute(IntegerAttribute):
|
|
10
|
+
"""Represents the age attribute.
|
|
11
|
+
|
|
12
|
+
This class extends IntegerAttribute and provides functionality for working with
|
|
13
|
+
age fields. It recognizes "Age" as a valid alias for this attribute type.
|
|
14
|
+
|
|
15
|
+
The attribute performs normalization on input values by trimming whitespace
|
|
16
|
+
and validates that the age is a valid integer between 0 and 120 (inherited from
|
|
17
|
+
IntegerAttribute for format validation).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
NAME = "Age"
|
|
21
|
+
ALIASES = [NAME]
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
"""Initialize the AgeAttribute with age range validation."""
|
|
25
|
+
super().__init__([AgeRangeValidator()])
|
|
26
|
+
|
|
27
|
+
def get_name(self) -> str:
|
|
28
|
+
"""Get the name of the attribute.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
str: The name "Age"
|
|
32
|
+
"""
|
|
33
|
+
return self.NAME
|
|
34
|
+
|
|
35
|
+
def get_aliases(self) -> List[str]:
|
|
36
|
+
"""Get the aliases for the attribute.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
List[str]: A list containing the aliases for this attribute
|
|
40
|
+
"""
|
|
41
|
+
return self.ALIASES.copy()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# src/openlinktoken/attributes/person/birth_date_attribute.py
|
|
2
|
+
from datetime import date
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from openlinktoken.attributes.general.date_attribute import DateAttribute
|
|
6
|
+
from openlinktoken.attributes.validation.date_range_validator import DateRangeValidator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BirthDateAttribute(DateAttribute):
|
|
10
|
+
"""Represents the birth date attribute.
|
|
11
|
+
|
|
12
|
+
This class extends DateAttribute and provides functionality for working with
|
|
13
|
+
birth date fields. It recognizes "BirthDate" as a valid alias for this
|
|
14
|
+
attribute type.
|
|
15
|
+
|
|
16
|
+
The attribute performs normalization on input values, converting them to a
|
|
17
|
+
standard format (yyyy-MM-dd).
|
|
18
|
+
|
|
19
|
+
Supported formats:
|
|
20
|
+
- yyyy-MM-dd
|
|
21
|
+
- yyyy/MM/dd
|
|
22
|
+
- MM/dd/yyyy
|
|
23
|
+
- MM-dd-yyyy
|
|
24
|
+
- dd.MM.yyyy
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
NAME = "BirthDate"
|
|
28
|
+
ALIASES = [NAME]
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
# Birth dates must be between 1910-01-01 and today
|
|
32
|
+
min_date = date(1910, 1, 1)
|
|
33
|
+
validator = DateRangeValidator(min_date=min_date, use_current_as_max=True)
|
|
34
|
+
super().__init__(additional_validators=[validator])
|
|
35
|
+
|
|
36
|
+
def get_name(self) -> str:
|
|
37
|
+
return self.NAME
|
|
38
|
+
|
|
39
|
+
def get_aliases(self) -> List[str]:
|
|
40
|
+
return self.ALIASES.copy()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from openlinktoken.attributes.general.year_attribute import YearAttribute
|
|
6
|
+
from openlinktoken.attributes.validation.year_range_validator import YearRangeValidator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BirthYearAttribute(YearAttribute):
|
|
10
|
+
"""Represents the birth year attribute.
|
|
11
|
+
|
|
12
|
+
This class extends YearAttribute and provides functionality for working with
|
|
13
|
+
birth year fields. It recognizes "BirthYear" as a valid alias for this attribute type.
|
|
14
|
+
|
|
15
|
+
The attribute performs normalization on input values by trimming whitespace
|
|
16
|
+
and validates that the birth year is a 4-digit year between 1910 and the current year.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
NAME = "BirthYear"
|
|
20
|
+
ALIASES = [NAME, "YearOfBirth"]
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
super().__init__(additional_validators=[YearRangeValidator()])
|
|
24
|
+
|
|
25
|
+
def get_name(self) -> str:
|
|
26
|
+
"""Get the name of the attribute.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
str: The name "BirthYear"
|
|
30
|
+
"""
|
|
31
|
+
return self.NAME
|
|
32
|
+
|
|
33
|
+
def get_aliases(self) -> List[str]:
|
|
34
|
+
"""Get the aliases for the attribute.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List[str]: A list containing the aliases for this attribute
|
|
38
|
+
"""
|
|
39
|
+
return self.ALIASES.copy()
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
from openlinktoken.attributes.base_attribute import BaseAttribute
|
|
7
|
+
from openlinktoken.attributes.utilities.attribute_utilities import AttributeUtilities
|
|
8
|
+
from openlinktoken.attributes.validation.not_starts_with_validator import NotStartsWithValidator
|
|
9
|
+
from openlinktoken.attributes.validation.regex_validator import RegexValidator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CanadianPostalCodeAttribute(BaseAttribute):
|
|
13
|
+
"""
|
|
14
|
+
Represents Canadian postal codes.
|
|
15
|
+
|
|
16
|
+
This class handles validation and normalization of Canadian postal codes,
|
|
17
|
+
supporting the A1A 1A1 format (letter-digit-letter space digit-letter-digit).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
NAME = "CanadianPostalCode"
|
|
21
|
+
ALIASES = [NAME, "CanadianZipCode"]
|
|
22
|
+
|
|
23
|
+
# Regular expression pattern for validating Canadian postal codes
|
|
24
|
+
# Supports 3-character (ZIP-3), partial (4-5 char), and full 6-character formats
|
|
25
|
+
CANADIAN_POSTAL_REGEX = r"^\s*[A-Za-z]\d[A-Za-z](\s?\d([A-Za-z]\d?)?)?\s*$"
|
|
26
|
+
|
|
27
|
+
# Full 6-character invalid postal codes
|
|
28
|
+
INVALID_ZIP_CODES = {
|
|
29
|
+
# 6-character Canadian postal code placeholders
|
|
30
|
+
"A1A 1A1",
|
|
31
|
+
"X0X 0X0",
|
|
32
|
+
"Y0Y 0Y0",
|
|
33
|
+
"Z0Z 0Z0",
|
|
34
|
+
"A0A 0A0",
|
|
35
|
+
"B1B 1B1",
|
|
36
|
+
"C2C 2C2",
|
|
37
|
+
"K1A 0A6", # Canadian government address
|
|
38
|
+
"H0H 0H0", # Santa Claus postal code
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# 3-character codes that are invalid when standalone (not when part of full codes)
|
|
42
|
+
INVALID_ZIP3_CODES = {
|
|
43
|
+
"K1A", # Canadian government
|
|
44
|
+
"M7A", # Government of Ontario
|
|
45
|
+
"H0H", # Santa Claus
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def __init__(self, min_length: int = 6):
|
|
49
|
+
"""
|
|
50
|
+
Initialize CanadianPostalCodeAttribute.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
min_length: Minimum length for postal codes (default: 6)
|
|
54
|
+
"""
|
|
55
|
+
validation_rules = []
|
|
56
|
+
super().__init__(validation_rules)
|
|
57
|
+
self.min_length = min_length
|
|
58
|
+
self.regex_validator = RegexValidator(self.CANADIAN_POSTAL_REGEX)
|
|
59
|
+
self.not_starts_with_validator = NotStartsWithValidator(self.INVALID_ZIP_CODES)
|
|
60
|
+
|
|
61
|
+
def validate(self, value: str) -> bool:
|
|
62
|
+
"""
|
|
63
|
+
Validate the Canadian postal code value.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
value: The postal code value to validate
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
True if the value is a valid Canadian postal code, False otherwise
|
|
70
|
+
"""
|
|
71
|
+
if value is None:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
# First, check the regex pattern on the ORIGINAL value
|
|
75
|
+
# This ensures the format is valid before normalization
|
|
76
|
+
if not self.regex_validator.eval(value):
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
# Normalize the value to ensure idempotency
|
|
80
|
+
# This converts to uppercase and formats consistently
|
|
81
|
+
normalized_value = self.normalize(value)
|
|
82
|
+
|
|
83
|
+
# Validate the NORMALIZED value against the full invalid codes
|
|
84
|
+
# This ensures "k1a 0a6" and "K1A 0A6" are treated consistently
|
|
85
|
+
for invalid_code in self.INVALID_ZIP_CODES:
|
|
86
|
+
if normalized_value.upper() == invalid_code.upper():
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
# Additionally check for codes starting with invalid ZIP-3 prefixes
|
|
90
|
+
# But ONLY for padded partial codes (normalized length is 7 and ends with padding)
|
|
91
|
+
# Full 6-character codes (without padding) are checked against the exact invalid list above
|
|
92
|
+
if len(normalized_value) == 7 and " " in normalized_value:
|
|
93
|
+
zip3 = normalized_value[:3].upper()
|
|
94
|
+
for invalid_zip3 in self.INVALID_ZIP3_CODES:
|
|
95
|
+
if zip3 == invalid_zip3.upper():
|
|
96
|
+
# Check if this is a padded partial code
|
|
97
|
+
# Padded codes end with "000" (3-char), "A0" (4-char), or "0" (5-char after last letter)
|
|
98
|
+
last_part = normalized_value[4:] # After "K1A "
|
|
99
|
+
if (
|
|
100
|
+
last_part == "000" # K1A → K1A 000
|
|
101
|
+
or re.match(r"\dA0$", last_part) # K1A1 → K1A 1A0
|
|
102
|
+
or re.match(r"\d[A-Z]0$", last_part)
|
|
103
|
+
): # K1A1A → K1A 1A0
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
def get_name(self) -> str:
|
|
109
|
+
return self.NAME
|
|
110
|
+
|
|
111
|
+
def get_aliases(self) -> List[str]:
|
|
112
|
+
return self.ALIASES.copy()
|
|
113
|
+
|
|
114
|
+
def normalize(self, value: str) -> str:
|
|
115
|
+
"""
|
|
116
|
+
Normalize a Canadian postal code to standard A1A 1A1 format.
|
|
117
|
+
|
|
118
|
+
For Canadian postal codes:
|
|
119
|
+
- Codes shorter than min_length are rejected (return original)
|
|
120
|
+
- 3-character format (e.g., "J1X") is padded with " 000" to create full format
|
|
121
|
+
(e.g., "J1X 000") if min_length <= 3
|
|
122
|
+
- 4-character format (e.g., "J1X1") is padded with "A0" to create full format
|
|
123
|
+
(e.g., "J1X 1A0") if min_length <= 4
|
|
124
|
+
- 5-character format (e.g., "J1X1A") is padded with "0" to create full format
|
|
125
|
+
(e.g., "J1X 1A0") if min_length <= 5
|
|
126
|
+
- 6-character format returns uppercase format with space (e.g., "k1a0a6" becomes "K1A 0A6")
|
|
127
|
+
If the input value is null or doesn't match Canadian postal pattern, the original
|
|
128
|
+
trimmed value is returned.
|
|
129
|
+
"""
|
|
130
|
+
if not value:
|
|
131
|
+
return value
|
|
132
|
+
|
|
133
|
+
trimmed = AttributeUtilities.remove_whitespace(value.strip())
|
|
134
|
+
|
|
135
|
+
# Check if it's a 3-character Canadian postal code (ZIP-3) - pad with " 000" if min_length allows
|
|
136
|
+
if re.match(r"^[A-Za-z]\d[A-Za-z]$", trimmed):
|
|
137
|
+
if self.min_length <= 3:
|
|
138
|
+
upper = trimmed.upper()
|
|
139
|
+
return f"{upper} 000"
|
|
140
|
+
# If min_length > 3, reject this by returning original
|
|
141
|
+
return value.strip()
|
|
142
|
+
|
|
143
|
+
# Check if it's a 4-character partial postal code (e.g., "A1A1") - pad with "A0" if min_length allows
|
|
144
|
+
if re.match(r"^[A-Za-z]\d[A-Za-z]\d$", trimmed):
|
|
145
|
+
if self.min_length <= 4:
|
|
146
|
+
upper = trimmed.upper()
|
|
147
|
+
return f"{upper[:3]} {upper[3]}A0"
|
|
148
|
+
# If min_length > 4, reject this by returning original
|
|
149
|
+
return value.strip()
|
|
150
|
+
|
|
151
|
+
# Check if it's a 5-character partial postal code (e.g., "A1A1A") - pad with "0" if min_length allows
|
|
152
|
+
if re.match(r"^[A-Za-z]\d[A-Za-z]\d[A-Za-z]$", trimmed):
|
|
153
|
+
if self.min_length <= 5:
|
|
154
|
+
upper = trimmed.upper()
|
|
155
|
+
return f"{upper[:3]} {upper[3:]}0"
|
|
156
|
+
# If min_length > 5, reject this by returning original
|
|
157
|
+
return value.strip()
|
|
158
|
+
|
|
159
|
+
# Check if it's a Canadian postal code (6 alphanumeric characters)
|
|
160
|
+
if re.match(r"[A-Za-z]\d[A-Za-z]\d[A-Za-z]\d", trimmed):
|
|
161
|
+
upper = trimmed.upper()
|
|
162
|
+
return f"{upper[:3]} {upper[3:6]}"
|
|
163
|
+
|
|
164
|
+
# For values that don't match Canadian postal patterns, return trimmed original
|
|
165
|
+
return value.strip()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import List
|
|
3
|
+
|
|
4
|
+
from openlinktoken.attributes.base_attribute import BaseAttribute
|
|
5
|
+
from openlinktoken.attributes.utilities.attribute_utilities import AttributeUtilities
|
|
6
|
+
from openlinktoken.attributes.validation.not_in_validator import NotInValidator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FirstNameAttribute(BaseAttribute):
|
|
10
|
+
"""Represents the first name of a person.
|
|
11
|
+
|
|
12
|
+
This class extends BaseAttribute and provides functionality for working with
|
|
13
|
+
first name fields. It recognizes "FirstName" and "GivenName" as valid aliases
|
|
14
|
+
for this attribute type.
|
|
15
|
+
|
|
16
|
+
The attribute performs no normalization on input values, returning them
|
|
17
|
+
unchanged.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
NAME = "FirstName"
|
|
21
|
+
ALIASES = [NAME, "GivenName"]
|
|
22
|
+
|
|
23
|
+
# Pattern to match and remove common titles
|
|
24
|
+
TITLE_PATTERN = re.compile(
|
|
25
|
+
r"(?i)^\s*(mr|mrs|ms|miss|dr|prof|capt|sir|col|gen|cmdr|lt|"
|
|
26
|
+
r"rabbi|father|brother|sister|hon|honorable|reverend|rev|doctor)\.?\s+",
|
|
27
|
+
re.IGNORECASE,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Pattern to match trailing periods and middle initials in names.
|
|
31
|
+
#
|
|
32
|
+
# This pattern matches:
|
|
33
|
+
# - A space, followed by a single non-space character (middle initial),
|
|
34
|
+
# optionally followed by a period, at the end of the string.
|
|
35
|
+
#
|
|
36
|
+
# Breakdown of the regex:
|
|
37
|
+
# \s A space
|
|
38
|
+
# [^\s] Any single non-space character (middle initial)
|
|
39
|
+
# \.? Optional period
|
|
40
|
+
# $ End of string
|
|
41
|
+
TRAILING_PERIOD_AND_INITIAL_PATTERN = re.compile(r"\s[^\s]\.?$")
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
placeholder_values = AttributeUtilities.COMMON_PLACEHOLDER_NAMES
|
|
45
|
+
validation_rules = [NotInValidator(placeholder_values)]
|
|
46
|
+
super().__init__(validation_rules)
|
|
47
|
+
|
|
48
|
+
def validate(self, value: str) -> bool:
|
|
49
|
+
"""
|
|
50
|
+
Validate the first name value.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
value: The first name value to validate
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
True if the value is a valid first name, False otherwise
|
|
57
|
+
"""
|
|
58
|
+
if value is None:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
# First, check placeholder values on the ORIGINAL value using built-in validators
|
|
62
|
+
# This ensures "N/A", "<masked>", etc. are properly rejected
|
|
63
|
+
if not super().validate(value):
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
# Normalize the value for validation
|
|
67
|
+
# This ensures that validate(normalize(x)) == validate(normalize(normalize(x)))
|
|
68
|
+
normalized_value = self.normalize(value)
|
|
69
|
+
|
|
70
|
+
# Check that normalized value is not empty
|
|
71
|
+
if normalized_value is None or not normalized_value.strip():
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
# Check that normalized value is not a placeholder
|
|
75
|
+
# This ensures idempotency: values like "TEST16" normalize to "TEST" which is a placeholder
|
|
76
|
+
if not super().validate(normalized_value):
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
def get_name(self) -> str:
|
|
82
|
+
return self.NAME
|
|
83
|
+
|
|
84
|
+
def get_aliases(self) -> List[str]:
|
|
85
|
+
return self.ALIASES.copy()
|
|
86
|
+
|
|
87
|
+
def normalize(self, value: str) -> str:
|
|
88
|
+
"""Returns the value unchanged after removing titles."""
|
|
89
|
+
if not value:
|
|
90
|
+
return value
|
|
91
|
+
|
|
92
|
+
normalized = AttributeUtilities.normalize_diacritics(value)
|
|
93
|
+
|
|
94
|
+
without_title = re.sub(self.TITLE_PATTERN, "", normalized).strip()
|
|
95
|
+
|
|
96
|
+
if without_title:
|
|
97
|
+
normalized = without_title
|
|
98
|
+
|
|
99
|
+
without_suffix = AttributeUtilities.remove_generational_suffix(normalized)
|
|
100
|
+
|
|
101
|
+
if without_suffix:
|
|
102
|
+
normalized = without_suffix
|
|
103
|
+
|
|
104
|
+
normalized = re.sub(self.TRAILING_PERIOD_AND_INITIAL_PATTERN, "", normalized).strip()
|
|
105
|
+
|
|
106
|
+
# Remove non-alphabetic characters
|
|
107
|
+
normalized = AttributeUtilities.NON_ALPHABETIC_PATTERN.sub("", normalized)
|
|
108
|
+
|
|
109
|
+
# Normalize whitespace
|
|
110
|
+
normalized = AttributeUtilities.WHITESPACE_PATTERN.sub(" ", normalized)
|
|
111
|
+
|
|
112
|
+
return normalized
|