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.
Files changed (72) hide show
  1. openlinktoken/__init__.py +15 -0
  2. openlinktoken/attributes/__init__.py +17 -0
  3. openlinktoken/attributes/attribute.py +26 -0
  4. openlinktoken/attributes/attribute_expression.py +232 -0
  5. openlinktoken/attributes/attribute_loader.py +48 -0
  6. openlinktoken/attributes/base_attribute.py +31 -0
  7. openlinktoken/attributes/combined_attribute.py +69 -0
  8. openlinktoken/attributes/general/__init__.py +17 -0
  9. openlinktoken/attributes/general/date_attribute.py +117 -0
  10. openlinktoken/attributes/general/decimal_attribute.py +86 -0
  11. openlinktoken/attributes/general/integer_attribute.py +78 -0
  12. openlinktoken/attributes/general/record_id_attribute.py +41 -0
  13. openlinktoken/attributes/general/string_attribute.py +55 -0
  14. openlinktoken/attributes/general/year_attribute.py +78 -0
  15. openlinktoken/attributes/person/__init__.py +25 -0
  16. openlinktoken/attributes/person/age_attribute.py +41 -0
  17. openlinktoken/attributes/person/birth_date_attribute.py +40 -0
  18. openlinktoken/attributes/person/birth_year_attribute.py +39 -0
  19. openlinktoken/attributes/person/canadian_postal_code_attribute.py +165 -0
  20. openlinktoken/attributes/person/first_name_attribute.py +112 -0
  21. openlinktoken/attributes/person/last_name_attribute.py +146 -0
  22. openlinktoken/attributes/person/postal_code_attribute.py +42 -0
  23. openlinktoken/attributes/person/sex_attribute.py +48 -0
  24. openlinktoken/attributes/person/social_security_number_attribute.py +119 -0
  25. openlinktoken/attributes/person/us_postal_code_attribute.py +104 -0
  26. openlinktoken/attributes/serializable_attribute.py +7 -0
  27. openlinktoken/attributes/utilities/__init__.py +7 -0
  28. openlinktoken/attributes/utilities/attribute_utilities.py +224 -0
  29. openlinktoken/attributes/validation/__init__.py +23 -0
  30. openlinktoken/attributes/validation/age_range_validator.py +41 -0
  31. openlinktoken/attributes/validation/attribute_validator.py +22 -0
  32. openlinktoken/attributes/validation/date_range_validator.py +111 -0
  33. openlinktoken/attributes/validation/not_in_validator.py +48 -0
  34. openlinktoken/attributes/validation/not_null_or_empty_validator.py +21 -0
  35. openlinktoken/attributes/validation/not_starts_with_validator.py +48 -0
  36. openlinktoken/attributes/validation/regex_validator.py +44 -0
  37. openlinktoken/attributes/validation/serializable_attribute_validator.py +26 -0
  38. openlinktoken/attributes/validation/year_range_validator.py +48 -0
  39. openlinktoken/ec_key_utils.py +207 -0
  40. openlinktoken/exchange_config.py +305 -0
  41. openlinktoken/exchange_jwe.py +107 -0
  42. openlinktoken/metadata.py +104 -0
  43. openlinktoken/tokens/__init__.py +19 -0
  44. openlinktoken/tokens/base_token_definition.py +48 -0
  45. openlinktoken/tokens/definitions/t1_token.py +39 -0
  46. openlinktoken/tokens/definitions/t2_token.py +39 -0
  47. openlinktoken/tokens/definitions/t3_token.py +39 -0
  48. openlinktoken/tokens/definitions/t4_token.py +37 -0
  49. openlinktoken/tokens/definitions/t5_token.py +37 -0
  50. openlinktoken/tokens/token.py +38 -0
  51. openlinktoken/tokens/token_definition.py +46 -0
  52. openlinktoken/tokens/token_generation_exception.py +16 -0
  53. openlinktoken/tokens/token_generator.py +201 -0
  54. openlinktoken/tokens/token_generator_result.py +33 -0
  55. openlinktoken/tokens/token_registry.py +46 -0
  56. openlinktoken/tokens/tokenizer/__init__.py +11 -0
  57. openlinktoken/tokens/tokenizer/passthrough_tokenizer.py +66 -0
  58. openlinktoken/tokens/tokenizer/sha256_tokenizer.py +72 -0
  59. openlinktoken/tokens/tokenizer/tokenizer.py +31 -0
  60. openlinktoken/tokentransformer/__init__.py +43 -0
  61. openlinktoken/tokentransformer/decrypt_token_transformer.py +83 -0
  62. openlinktoken/tokentransformer/encrypt_token_transformer.py +85 -0
  63. openlinktoken/tokentransformer/encryption_constants.py +23 -0
  64. openlinktoken/tokentransformer/hash_token_transformer.py +88 -0
  65. openlinktoken/tokentransformer/jwe_match_token_formatter.py +128 -0
  66. openlinktoken/tokentransformer/match_token_constants.py +57 -0
  67. openlinktoken/tokentransformer/no_operation_token_transformer.py +24 -0
  68. openlinktoken/tokentransformer/token_transformer.py +22 -0
  69. openlinktoken-2.0.0.dist-info/METADATA +21 -0
  70. openlinktoken-2.0.0.dist-info/RECORD +72 -0
  71. openlinktoken-2.0.0.dist-info/WHEEL +5 -0
  72. openlinktoken-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,224 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ import re
4
+ import unicodedata
5
+ from typing import Dict, Set
6
+
7
+
8
+ class AttributeUtilities:
9
+ """
10
+ This class includes functions such as normalizing accents,
11
+ standardizing formats, and other attribute-related transformations.
12
+ """
13
+
14
+ # Pattern that matches any character that is not an alphabetic character (a-z or A-Z).
15
+ # Used for removing or identifying non-alphabetic characters in strings.
16
+ NON_ALPHABETIC_PATTERN = re.compile(r"[^a-zA-Z]")
17
+
18
+ # Pattern that matches one or more whitespace characters.
19
+ # This includes spaces, tabs, line breaks, and other Unicode whitespace.
20
+ #
21
+ # Examples:
22
+ # " " -> single space
23
+ # "\t" -> tab
24
+ # "\n" -> newline
25
+ # "\r\n" -> carriage return + newline
26
+ # " " -> multiple spaces
27
+ WHITESPACE_PATTERN = re.compile(r"\s+")
28
+
29
+ # Characters from Latin-1 Supplement and Latin Extended blocks that do not
30
+ # decompose to ASCII-compatible forms with NFD alone.
31
+ LATIN_EXTENDED_TRANSLITERATION_MAP: Dict[str, str] = {
32
+ "Æ": "AE",
33
+ "æ": "ae",
34
+ "Ð": "D",
35
+ "ð": "d",
36
+ "Ø": "O",
37
+ "ø": "o",
38
+ "Þ": "TH",
39
+ "þ": "th",
40
+ "ß": "ss",
41
+ "ẞ": "SS",
42
+ "Đ": "D",
43
+ "đ": "d",
44
+ "Ħ": "H",
45
+ "ħ": "h",
46
+ "ı": "i",
47
+ "IJ": "IJ",
48
+ "ij": "ij",
49
+ "Ŀ": "L",
50
+ "ŀ": "l",
51
+ "Ł": "L",
52
+ "ł": "l",
53
+ "ʼn": "n",
54
+ "Ŋ": "NG",
55
+ "ŋ": "ng",
56
+ "Œ": "OE",
57
+ "œ": "oe",
58
+ "Ŧ": "T",
59
+ "ŧ": "t",
60
+ "ſ": "s",
61
+ "Ƒ": "F",
62
+ "ƒ": "f",
63
+ "DŽ": "DZ",
64
+ "Dž": "Dz",
65
+ "dž": "dz",
66
+ "LJ": "LJ",
67
+ "Lj": "Lj",
68
+ "lj": "lj",
69
+ "NJ": "NJ",
70
+ "Nj": "Nj",
71
+ "nj": "nj",
72
+ "DZ": "DZ",
73
+ "Dz": "Dz",
74
+ "dz": "dz",
75
+ "Ǽ": "AE",
76
+ "ǽ": "ae",
77
+ "Ǿ": "O",
78
+ "ǿ": "o",
79
+ }
80
+
81
+ # Pattern that matches generational suffixes at the end of a string.
82
+ # Matches case-insensitive suffixes after a whitespace character.
83
+ #
84
+ # Matches the following types of generational suffixes:
85
+ # - Jr, Jr., Junior
86
+ # - Sr, Sr., Senior
87
+ # - Roman numerals (I, II, III, IV, V, VI, VII, VIII, IX, X)
88
+ # - Ordinal numbers (1st, 2nd, 3rd, 4th, etc.)
89
+ #
90
+ # Examples:
91
+ # "John Smith Jr" -> matches " Jr"
92
+ # "Jane Doe Sr." -> matches " Sr."
93
+ # "Robert Johnson III" -> matches " III"
94
+ # "Thomas Wilson 2nd" -> matches " 2nd"
95
+ GENERATIONAL_SUFFIX_PATTERN = re.compile(
96
+ r"(?i)\s+(jr\.?|junior|sr\.?|senior|I{1,3}|IV|V|VI{0,3}|IX|X|\d+(st|nd|rd|th))$"
97
+ )
98
+
99
+ # A set of common placeholder names used to identify non-identifying or
100
+ # placeholder text in data fields.
101
+ #
102
+ # This set includes various standard terms and phrases that are commonly used
103
+ # as placeholders in data records when actual values are unknown, not applicable,
104
+ # unavailable, or intentionally masked.
105
+ # These placeholders might be found in production data but don't represent
106
+ # actual identifying information.
107
+ #
108
+ # These values should be treated as non-identifying data and may need special
109
+ # handling during data processing, anonymization, or when analyzing data quality.
110
+ COMMON_PLACEHOLDER_NAMES: Set[str] = {
111
+ "Unknown", # Placeholder for unknown last names
112
+ "N/A", # Not applicable
113
+ "None", # No last name provided
114
+ "Test", # Commonly used in testing scenarios
115
+ "Sample", # Sample data placeholder
116
+ "Donor", # Placeholder for donor records
117
+ "Patient", # Placeholder for patient records
118
+ "Automation Test", # Placeholder for automation tests
119
+ "Automationtest", # Another variation of automation test
120
+ "patient not found", # Placeholder for cases where patient data is not found
121
+ "patientnotfound", # Another variation of patient not found
122
+ "<masked>", # Placeholder for masked data
123
+ "Anonymous", # Placeholder for anonymous records
124
+ "zzztrash", # Placeholder for test or trash data
125
+ "Missing", # Placeholder for missing data
126
+ "Unavailable", # Placeholder for unavailable data
127
+ "Not Available", # Placeholder for data not available
128
+ "NotAvailable", # Placeholder for data not available (no spaces)
129
+ }
130
+
131
+ def __init__(self):
132
+ """Prevent instantiation of utility class."""
133
+ raise RuntimeError("This is a utility class and cannot be instantiated")
134
+
135
+ @staticmethod
136
+ def normalize_diacritics(value: str) -> str:
137
+ """
138
+ Removes diacritic marks from the given string.
139
+
140
+ This method performs the following steps:
141
+ 1. Trims the input string
142
+ 2. Normalizes the string using NFD form, which separates characters from
143
+ their diacritical marks
144
+ 3. Removes all diacritical marks using Unicode category filtering
145
+
146
+ Args:
147
+ value: The string from which to remove diacritical marks
148
+
149
+ Returns:
150
+ A new string with all diacritical marks removed
151
+
152
+ Raises:
153
+ AttributeError: If value is None
154
+ """
155
+ if value is None:
156
+ raise AttributeError("Value cannot be None")
157
+
158
+ trimmed_value = value.strip()
159
+ transliterated_value = "".join(
160
+ AttributeUtilities.LATIN_EXTENDED_TRANSLITERATION_MAP.get(character, character)
161
+ for character in trimmed_value
162
+ )
163
+
164
+ # Normalize to NFD (decomposed form) and filter out combining characters
165
+ normalized = unicodedata.normalize("NFD", transliterated_value)
166
+ return "".join(character for character in normalized if not unicodedata.category(character).startswith("M"))
167
+
168
+ @staticmethod
169
+ def remove_whitespace(value: str) -> str:
170
+ """
171
+ Remove all whitespace characters from the string.
172
+
173
+ Args:
174
+ value: The string from which to remove whitespace
175
+
176
+ Returns:
177
+ String with all whitespace removed
178
+ """
179
+ if value is None:
180
+ return None
181
+
182
+ return AttributeUtilities.WHITESPACE_PATTERN.sub("", value)
183
+
184
+ @staticmethod
185
+ def remove_non_alphabetic_characters(value: str) -> str:
186
+ """
187
+ Remove all non-alphabetic characters from the string.
188
+
189
+ Args:
190
+ value: The string from which to remove non-alphabetic characters
191
+
192
+ Returns:
193
+ String with only alphabetic characters (a-z, A-Z)
194
+ """
195
+ if value is None:
196
+ return None
197
+
198
+ return AttributeUtilities.NON_ALPHABETIC_PATTERN.sub("", value)
199
+
200
+ @staticmethod
201
+ def remove_generational_suffix(value: str) -> str:
202
+ """
203
+ Remove generational suffixes (Jr, Sr, III, etc.) from the string.
204
+
205
+ Args:
206
+ value: The string from which to remove generational suffixes
207
+
208
+ Returns:
209
+ String with generational suffixes removed
210
+ """
211
+ if value is None:
212
+ return None
213
+
214
+ return AttributeUtilities.GENERATIONAL_SUFFIX_PATTERN.sub("", value).strip()
215
+
216
+ @staticmethod
217
+ def get_common_placeholder_names() -> Set[str]:
218
+ """
219
+ Get a copy of the common placeholder names set.
220
+
221
+ Returns:
222
+ A copy of the set containing common placeholder names
223
+ """
224
+ return AttributeUtilities.COMMON_PLACEHOLDER_NAMES.copy()
@@ -0,0 +1,23 @@
1
+ """Validation utilities for attributes."""
2
+
3
+ from .age_range_validator import AgeRangeValidator
4
+ from .attribute_validator import AttributeValidator
5
+ from .date_range_validator import DateRangeValidator
6
+ from .not_in_validator import NotInValidator
7
+ from .not_null_or_empty_validator import NotNullOrEmptyValidator
8
+ from .not_starts_with_validator import NotStartsWithValidator
9
+ from .regex_validator import RegexValidator
10
+ from .serializable_attribute_validator import SerializableAttributeValidator
11
+ from .year_range_validator import YearRangeValidator
12
+
13
+ __all__ = [
14
+ "AgeRangeValidator",
15
+ "AttributeValidator",
16
+ "DateRangeValidator",
17
+ "NotInValidator",
18
+ "NotNullOrEmptyValidator",
19
+ "NotStartsWithValidator",
20
+ "RegexValidator",
21
+ "SerializableAttributeValidator",
22
+ "YearRangeValidator",
23
+ ]
@@ -0,0 +1,41 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from openlinktoken.attributes.validation.serializable_attribute_validator import SerializableAttributeValidator
4
+
5
+
6
+ class AgeRangeValidator(SerializableAttributeValidator):
7
+ """
8
+ A validator that asserts that an age value is within an acceptable range (0-120).
9
+
10
+ This validator checks that ages are:
11
+ - Not less than 0
12
+ - Not greater than 120
13
+
14
+ If the age is outside this range, the validation fails.
15
+ """
16
+
17
+ MIN_AGE = 0
18
+ MAX_AGE = 120
19
+
20
+ def __init__(self):
21
+ """Initialize the validator with default age range (0-120)."""
22
+ pass
23
+
24
+ def eval(self, value: str) -> bool:
25
+ """
26
+ Validate that the age value is within acceptable range.
27
+
28
+ Args:
29
+ value: The age string to validate
30
+
31
+ Returns:
32
+ True if the age is within acceptable range (0-120), False otherwise
33
+ """
34
+ if not value or not value.strip():
35
+ return False
36
+
37
+ try:
38
+ age = int(value.strip())
39
+ return self.MIN_AGE <= age <= self.MAX_AGE
40
+ except ValueError:
41
+ return False
@@ -0,0 +1,22 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class AttributeValidator(ABC):
7
+ """
8
+ A generic interface for attribute validation.
9
+ """
10
+
11
+ @abstractmethod
12
+ def eval(self, value: str) -> bool:
13
+ """
14
+ Validate an attribute value.
15
+
16
+ Args:
17
+ value: The attribute value
18
+
19
+ Returns:
20
+ True if the attribute is valid, False otherwise
21
+ """
22
+ pass
@@ -0,0 +1,111 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from datetime import date, datetime
4
+ from typing import Optional
5
+
6
+ from openlinktoken.attributes.validation.serializable_attribute_validator import SerializableAttributeValidator
7
+
8
+
9
+ class DateRangeValidator(SerializableAttributeValidator):
10
+ """
11
+ A generic validator that asserts that a date value is within an acceptable range.
12
+
13
+ This validator checks that dates are:
14
+ - Not before the specified minimum date (if provided)
15
+ - Not after the specified maximum date (if provided)
16
+ - If use_current_as_max is True, uses today's date as the maximum
17
+
18
+ If the date is outside the specified range, the validation fails.
19
+ """
20
+
21
+ # Supported date formats for parsing
22
+ POSSIBLE_INPUT_FORMATS = [
23
+ "%Y-%m-%d",
24
+ "%Y/%m/%d",
25
+ "%m/%d/%Y",
26
+ "%m-%d-%Y",
27
+ "%d.%m.%Y",
28
+ # ISO 8601 timestamp formats
29
+ "%Y-%m-%dT%H:%M:%S.%fZ",
30
+ "%Y-%m-%dT%H:%M:%SZ",
31
+ "%Y-%m-%dT%H:%M:%S.%f%z",
32
+ "%Y-%m-%dT%H:%M:%S%z",
33
+ ]
34
+
35
+ def __init__(
36
+ self, min_date: Optional[date] = None, max_date: Optional[date] = None, use_current_as_max: bool = False
37
+ ):
38
+ """
39
+ Initialize the validator with a date range.
40
+
41
+ Args:
42
+ min_date: The minimum allowed date (inclusive), or None for no minimum
43
+ max_date: The maximum allowed date (inclusive), or None for no maximum
44
+ use_current_as_max: If True, uses today's date as the maximum date
45
+ """
46
+ self._min_date = min_date
47
+ self._use_current_as_max = use_current_as_max
48
+ self._max_date = max_date
49
+
50
+ @property
51
+ def min_date(self) -> Optional[date]:
52
+ """Get the minimum allowed date."""
53
+ return self._min_date
54
+
55
+ @property
56
+ def max_date(self) -> Optional[date]:
57
+ """Get the maximum allowed date."""
58
+ if self._use_current_as_max:
59
+ return date.today()
60
+ return self._max_date
61
+
62
+ @property
63
+ def use_current_as_max(self) -> bool:
64
+ """Check if using current date as maximum."""
65
+ return self._use_current_as_max
66
+
67
+ def eval(self, value: str) -> bool:
68
+ """
69
+ Validate that the date value is within acceptable range.
70
+
71
+ Args:
72
+ value: The date string to validate
73
+
74
+ Returns:
75
+ True if the date is within acceptable range, False otherwise
76
+ """
77
+ if not value or not value.strip():
78
+ return False
79
+
80
+ parsed_date = self._parse_date(value.strip())
81
+ if parsed_date is None:
82
+ return False
83
+
84
+ # Check minimum date if specified
85
+ if self._min_date is not None and parsed_date < self._min_date:
86
+ return False
87
+
88
+ # Check maximum date
89
+ max_date_to_check = self.max_date
90
+ if max_date_to_check is not None and parsed_date > max_date_to_check:
91
+ return False
92
+
93
+ return True
94
+
95
+ def _parse_date(self, value: str) -> Optional[date]:
96
+ """
97
+ Parse the date string using various supported formats.
98
+
99
+ Args:
100
+ value: The date string to parse
101
+
102
+ Returns:
103
+ Parsed date object, or None if parsing fails
104
+ """
105
+ for date_format in self.POSSIBLE_INPUT_FORMATS:
106
+ try:
107
+ parsed_datetime = datetime.strptime(value, date_format)
108
+ return parsed_datetime.date()
109
+ except ValueError:
110
+ continue
111
+ return None
@@ -0,0 +1,48 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from typing import Set
4
+
5
+ from openlinktoken.attributes.validation.serializable_attribute_validator import SerializableAttributeValidator
6
+
7
+
8
+ class NotInValidator(SerializableAttributeValidator):
9
+ """
10
+ A Validator that asserts that the attribute value is NOT IN the list of invalid values.
11
+ """
12
+
13
+ def __init__(self, invalid_values: Set[str]):
14
+ """
15
+ Initialize the validator with a set of invalid values.
16
+
17
+ Args:
18
+ invalid_values: Set of values that should be considered invalid
19
+
20
+ Raises:
21
+ ValueError: If invalid_values is None
22
+ """
23
+ if invalid_values is None:
24
+ raise ValueError("Invalid values set cannot be None")
25
+
26
+ # Convert to lowercase for case-insensitive comparison
27
+ self._invalid_values = {value.lower() for value in invalid_values}
28
+
29
+ @property
30
+ def invalid_values(self) -> Set[str]:
31
+ """Get the set of invalid values (in lowercase)."""
32
+ return self._invalid_values.copy()
33
+
34
+ def eval(self, value: str) -> bool:
35
+ """
36
+ Validate that the attribute value is not in the list of invalid values
37
+ (case-insensitive comparison).
38
+
39
+ Args:
40
+ value: The attribute value to validate
41
+
42
+ Returns:
43
+ True if the value is not in the invalid values list, False otherwise
44
+ """
45
+ if value is None:
46
+ return False
47
+
48
+ return value.lower() not in self._invalid_values
@@ -0,0 +1,21 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from openlinktoken.attributes.validation.serializable_attribute_validator import SerializableAttributeValidator
4
+
5
+
6
+ class NotNullOrEmptyValidator(SerializableAttributeValidator):
7
+ """
8
+ A Validator that asserts the value is NOT None and not blank.
9
+ """
10
+
11
+ def eval(self, value: str) -> bool:
12
+ """
13
+ Validate that the attribute value is not None or blank.
14
+
15
+ Args:
16
+ value: The attribute value to validate
17
+
18
+ Returns:
19
+ True if value is not None and not blank, False otherwise
20
+ """
21
+ return value is not None and value.strip() != ""
@@ -0,0 +1,48 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from typing import Set
4
+
5
+ from openlinktoken.attributes.validation.serializable_attribute_validator import SerializableAttributeValidator
6
+
7
+
8
+ class NotStartsWithValidator(SerializableAttributeValidator):
9
+ """
10
+ A Validator that asserts that the attribute value does NOT START WITH
11
+ any of the invalid prefixes.
12
+ """
13
+
14
+ def __init__(self, invalid_prefixes: Set[str]):
15
+ """
16
+ Initialize the validator with a set of invalid prefixes.
17
+
18
+ Args:
19
+ invalid_prefixes: Set of prefixes that values should not start with
20
+
21
+ Raises:
22
+ ValueError: If invalid_prefixes is None
23
+ """
24
+ if invalid_prefixes is None:
25
+ raise ValueError("Invalid prefixes set cannot be None")
26
+
27
+ self._invalid_prefixes = invalid_prefixes.copy()
28
+
29
+ @property
30
+ def invalid_prefixes(self) -> Set[str]:
31
+ """Get the set of invalid prefixes."""
32
+ return self._invalid_prefixes.copy()
33
+
34
+ def eval(self, value: str) -> bool:
35
+ """
36
+ Validate that the attribute value does not start with any of the invalid prefixes.
37
+
38
+ Args:
39
+ value: The attribute value to validate
40
+
41
+ Returns:
42
+ True if the value doesn't start with any invalid prefix, False otherwise
43
+ """
44
+ if value is None:
45
+ return False
46
+
47
+ trimmed_value = value.strip()
48
+ return not any(trimmed_value.startswith(prefix) for prefix in self._invalid_prefixes)
@@ -0,0 +1,44 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ import re
4
+ from typing import Pattern
5
+
6
+ from openlinktoken.attributes.validation.serializable_attribute_validator import SerializableAttributeValidator
7
+
8
+
9
+ class RegexValidator(SerializableAttributeValidator):
10
+ """
11
+ A Validator that is designed for validating with regex expressions.
12
+ """
13
+
14
+ def __init__(self, pattern: str):
15
+ """
16
+ Initialize the regex validator with a pattern.
17
+
18
+ Args:
19
+ pattern: The regex pattern string to compile and use for validation
20
+
21
+ Raises:
22
+ re.error: If the pattern is invalid
23
+ """
24
+ if not pattern:
25
+ raise ValueError("Pattern cannot be None or empty")
26
+
27
+ self._compiled_pattern: Pattern[str] = re.compile(pattern)
28
+
29
+ @property
30
+ def compiled_pattern(self) -> Pattern[str]:
31
+ """Get the compiled regex pattern."""
32
+ return self._compiled_pattern
33
+
34
+ def eval(self, value: str) -> bool:
35
+ """
36
+ Validate that the value matches the regex pattern.
37
+
38
+ Args:
39
+ value: The attribute value to validate
40
+
41
+ Returns:
42
+ True if the value matches the pattern, False otherwise
43
+ """
44
+ return value is not None and bool(self._compiled_pattern.match(value))
@@ -0,0 +1,26 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from abc import ABC
4
+
5
+ from openlinktoken.attributes.validation.attribute_validator import AttributeValidator
6
+
7
+
8
+ class SerializableAttributeValidator(AttributeValidator, ABC):
9
+ """
10
+ An extension of the AttributeValidator interface that is also serializable.
11
+
12
+ This interface should be implemented by validator classes that need to be
13
+ serialized, such as when they are part of serializable attributes or need to
14
+ be transmitted over a network.
15
+
16
+ Implementing classes must ensure that all their fields are serializable or
17
+ handled appropriately if they cannot be serialized.
18
+
19
+ Note: In Python, most objects are serializable by default with pickle,
20
+ but this class serves as a marker for validators that are explicitly
21
+ designed to be serializable.
22
+ """
23
+
24
+ # This is a marker interface that combines AttributeValidator and serializable capability
25
+ # No additional methods are required
26
+ pass
@@ -0,0 +1,48 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from datetime import date
4
+
5
+ from openlinktoken.attributes.validation.serializable_attribute_validator import SerializableAttributeValidator
6
+
7
+
8
+ class YearRangeValidator(SerializableAttributeValidator):
9
+ """
10
+ A validator that asserts that a year value is within an acceptable range.
11
+
12
+ This validator checks that years are:
13
+ - Not before 1910
14
+ - Not after the current year
15
+
16
+ If the year is outside this range, the validation fails.
17
+ """
18
+
19
+ MIN_YEAR = 1910
20
+
21
+ def __init__(self):
22
+ """Initialize the validator with default year range (1910-current year)."""
23
+ pass
24
+
25
+ @property
26
+ def max_year(self) -> int:
27
+ """Get the maximum allowed year (current year)."""
28
+ return date.today().year
29
+
30
+ def eval(self, value: str) -> bool:
31
+ """
32
+ Validate that the year value is within acceptable range.
33
+
34
+ Args:
35
+ value: The year string to validate
36
+
37
+ Returns:
38
+ True if the year is within acceptable range (1910-current year),
39
+ False otherwise
40
+ """
41
+ if not value or not value.strip():
42
+ return False
43
+
44
+ try:
45
+ year = int(value.strip())
46
+ return self.MIN_YEAR <= year <= self.max_year
47
+ except ValueError:
48
+ return False