openlinktoken 2.0.0__tar.gz

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 (80) hide show
  1. openlinktoken-2.0.0/MANIFEST.in +15 -0
  2. openlinktoken-2.0.0/PKG-INFO +21 -0
  3. openlinktoken-2.0.0/dev-requirements.txt +3 -0
  4. openlinktoken-2.0.0/pyproject.toml +19 -0
  5. openlinktoken-2.0.0/requirements.txt +5 -0
  6. openlinktoken-2.0.0/setup.cfg +4 -0
  7. openlinktoken-2.0.0/setup.py +34 -0
  8. openlinktoken-2.0.0/src/main/openlinktoken/__init__.py +15 -0
  9. openlinktoken-2.0.0/src/main/openlinktoken/attributes/__init__.py +17 -0
  10. openlinktoken-2.0.0/src/main/openlinktoken/attributes/attribute.py +26 -0
  11. openlinktoken-2.0.0/src/main/openlinktoken/attributes/attribute_expression.py +232 -0
  12. openlinktoken-2.0.0/src/main/openlinktoken/attributes/attribute_loader.py +48 -0
  13. openlinktoken-2.0.0/src/main/openlinktoken/attributes/base_attribute.py +31 -0
  14. openlinktoken-2.0.0/src/main/openlinktoken/attributes/combined_attribute.py +69 -0
  15. openlinktoken-2.0.0/src/main/openlinktoken/attributes/general/__init__.py +17 -0
  16. openlinktoken-2.0.0/src/main/openlinktoken/attributes/general/date_attribute.py +117 -0
  17. openlinktoken-2.0.0/src/main/openlinktoken/attributes/general/decimal_attribute.py +86 -0
  18. openlinktoken-2.0.0/src/main/openlinktoken/attributes/general/integer_attribute.py +78 -0
  19. openlinktoken-2.0.0/src/main/openlinktoken/attributes/general/record_id_attribute.py +41 -0
  20. openlinktoken-2.0.0/src/main/openlinktoken/attributes/general/string_attribute.py +55 -0
  21. openlinktoken-2.0.0/src/main/openlinktoken/attributes/general/year_attribute.py +78 -0
  22. openlinktoken-2.0.0/src/main/openlinktoken/attributes/person/__init__.py +25 -0
  23. openlinktoken-2.0.0/src/main/openlinktoken/attributes/person/age_attribute.py +41 -0
  24. openlinktoken-2.0.0/src/main/openlinktoken/attributes/person/birth_date_attribute.py +40 -0
  25. openlinktoken-2.0.0/src/main/openlinktoken/attributes/person/birth_year_attribute.py +39 -0
  26. openlinktoken-2.0.0/src/main/openlinktoken/attributes/person/canadian_postal_code_attribute.py +165 -0
  27. openlinktoken-2.0.0/src/main/openlinktoken/attributes/person/first_name_attribute.py +112 -0
  28. openlinktoken-2.0.0/src/main/openlinktoken/attributes/person/last_name_attribute.py +146 -0
  29. openlinktoken-2.0.0/src/main/openlinktoken/attributes/person/postal_code_attribute.py +42 -0
  30. openlinktoken-2.0.0/src/main/openlinktoken/attributes/person/sex_attribute.py +48 -0
  31. openlinktoken-2.0.0/src/main/openlinktoken/attributes/person/social_security_number_attribute.py +119 -0
  32. openlinktoken-2.0.0/src/main/openlinktoken/attributes/person/us_postal_code_attribute.py +104 -0
  33. openlinktoken-2.0.0/src/main/openlinktoken/attributes/serializable_attribute.py +7 -0
  34. openlinktoken-2.0.0/src/main/openlinktoken/attributes/utilities/__init__.py +7 -0
  35. openlinktoken-2.0.0/src/main/openlinktoken/attributes/utilities/attribute_utilities.py +224 -0
  36. openlinktoken-2.0.0/src/main/openlinktoken/attributes/validation/__init__.py +23 -0
  37. openlinktoken-2.0.0/src/main/openlinktoken/attributes/validation/age_range_validator.py +41 -0
  38. openlinktoken-2.0.0/src/main/openlinktoken/attributes/validation/attribute_validator.py +22 -0
  39. openlinktoken-2.0.0/src/main/openlinktoken/attributes/validation/date_range_validator.py +111 -0
  40. openlinktoken-2.0.0/src/main/openlinktoken/attributes/validation/not_in_validator.py +48 -0
  41. openlinktoken-2.0.0/src/main/openlinktoken/attributes/validation/not_null_or_empty_validator.py +21 -0
  42. openlinktoken-2.0.0/src/main/openlinktoken/attributes/validation/not_starts_with_validator.py +48 -0
  43. openlinktoken-2.0.0/src/main/openlinktoken/attributes/validation/regex_validator.py +44 -0
  44. openlinktoken-2.0.0/src/main/openlinktoken/attributes/validation/serializable_attribute_validator.py +26 -0
  45. openlinktoken-2.0.0/src/main/openlinktoken/attributes/validation/year_range_validator.py +48 -0
  46. openlinktoken-2.0.0/src/main/openlinktoken/ec_key_utils.py +207 -0
  47. openlinktoken-2.0.0/src/main/openlinktoken/exchange_config.py +305 -0
  48. openlinktoken-2.0.0/src/main/openlinktoken/exchange_jwe.py +107 -0
  49. openlinktoken-2.0.0/src/main/openlinktoken/metadata.py +104 -0
  50. openlinktoken-2.0.0/src/main/openlinktoken/tokens/__init__.py +19 -0
  51. openlinktoken-2.0.0/src/main/openlinktoken/tokens/base_token_definition.py +48 -0
  52. openlinktoken-2.0.0/src/main/openlinktoken/tokens/definitions/t1_token.py +39 -0
  53. openlinktoken-2.0.0/src/main/openlinktoken/tokens/definitions/t2_token.py +39 -0
  54. openlinktoken-2.0.0/src/main/openlinktoken/tokens/definitions/t3_token.py +39 -0
  55. openlinktoken-2.0.0/src/main/openlinktoken/tokens/definitions/t4_token.py +37 -0
  56. openlinktoken-2.0.0/src/main/openlinktoken/tokens/definitions/t5_token.py +37 -0
  57. openlinktoken-2.0.0/src/main/openlinktoken/tokens/token.py +38 -0
  58. openlinktoken-2.0.0/src/main/openlinktoken/tokens/token_definition.py +46 -0
  59. openlinktoken-2.0.0/src/main/openlinktoken/tokens/token_generation_exception.py +16 -0
  60. openlinktoken-2.0.0/src/main/openlinktoken/tokens/token_generator.py +201 -0
  61. openlinktoken-2.0.0/src/main/openlinktoken/tokens/token_generator_result.py +33 -0
  62. openlinktoken-2.0.0/src/main/openlinktoken/tokens/token_registry.py +46 -0
  63. openlinktoken-2.0.0/src/main/openlinktoken/tokens/tokenizer/__init__.py +11 -0
  64. openlinktoken-2.0.0/src/main/openlinktoken/tokens/tokenizer/passthrough_tokenizer.py +66 -0
  65. openlinktoken-2.0.0/src/main/openlinktoken/tokens/tokenizer/sha256_tokenizer.py +72 -0
  66. openlinktoken-2.0.0/src/main/openlinktoken/tokens/tokenizer/tokenizer.py +31 -0
  67. openlinktoken-2.0.0/src/main/openlinktoken/tokentransformer/__init__.py +43 -0
  68. openlinktoken-2.0.0/src/main/openlinktoken/tokentransformer/decrypt_token_transformer.py +83 -0
  69. openlinktoken-2.0.0/src/main/openlinktoken/tokentransformer/encrypt_token_transformer.py +85 -0
  70. openlinktoken-2.0.0/src/main/openlinktoken/tokentransformer/encryption_constants.py +23 -0
  71. openlinktoken-2.0.0/src/main/openlinktoken/tokentransformer/hash_token_transformer.py +88 -0
  72. openlinktoken-2.0.0/src/main/openlinktoken/tokentransformer/jwe_match_token_formatter.py +128 -0
  73. openlinktoken-2.0.0/src/main/openlinktoken/tokentransformer/match_token_constants.py +57 -0
  74. openlinktoken-2.0.0/src/main/openlinktoken/tokentransformer/no_operation_token_transformer.py +24 -0
  75. openlinktoken-2.0.0/src/main/openlinktoken/tokentransformer/token_transformer.py +22 -0
  76. openlinktoken-2.0.0/src/main/openlinktoken.egg-info/PKG-INFO +21 -0
  77. openlinktoken-2.0.0/src/main/openlinktoken.egg-info/SOURCES.txt +80 -0
  78. openlinktoken-2.0.0/src/main/openlinktoken.egg-info/dependency_links.txt +1 -0
  79. openlinktoken-2.0.0/src/main/openlinktoken.egg-info/requires.txt +2 -0
  80. openlinktoken-2.0.0/src/main/openlinktoken.egg-info/top_level.txt +1 -0
@@ -0,0 +1,15 @@
1
+ # Include requirements files (must be in same directory as setup.py)
2
+ include requirements.txt
3
+ include dev-requirements.txt
4
+
5
+ # Include root README and LICENSE (relative to this file)
6
+ include ../../../README.md
7
+ include ../../../LICENSE
8
+
9
+ # Include all Python source files
10
+ recursive-include src *.py
11
+
12
+ # Exclude test files from distribution
13
+ recursive-exclude src/test *
14
+ recursive-exclude * __pycache__
15
+ recursive-exclude * *.py[co]
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.4
2
+ Name: openlinktoken
3
+ Version: 2.0.0
4
+ Summary: Open Link Token Python core library for record linkage
5
+ Home-page: https://github.com/TruvetaPublic/OpenLinkToken
6
+ Author: Open Link Token Contributors
7
+ Project-URL: Source, https://github.com/TruvetaPublic/OpenLinkToken
8
+ Project-URL: Documentation, https://github.com/TruvetaPublic/OpenLinkToken/blob/main/README.md
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: cryptography==48.0.1
12
+ Requires-Dist: jwcrypto==1.5.7
13
+ Dynamic: author
14
+ Dynamic: description
15
+ Dynamic: description-content-type
16
+ Dynamic: home-page
17
+ Dynamic: project-url
18
+ Dynamic: requires-python
19
+ Dynamic: summary
20
+
21
+ Open Link Token Python implementation for record linkage.
@@ -0,0 +1,3 @@
1
+ # Use consolidated dev requirements from parent directory
2
+ # Install with: uv pip install -r ../dev-requirements.txt
3
+ -r ../dev-requirements.txt
@@ -0,0 +1,19 @@
1
+ [project]
2
+ name = "openlinktoken"
3
+ version = "2.0.0"
4
+ requires-python = ">=3.10"
5
+ dynamic = [
6
+ "authors",
7
+ "description",
8
+ "optional-dependencies",
9
+ "readme",
10
+ "urls",
11
+ ]
12
+ dependencies = [
13
+ "cryptography==48.0.1",
14
+ "jwcrypto==1.5.7",
15
+ ]
16
+
17
+ [build-system]
18
+ requires = ["setuptools==82.0.1", "wheel"]
19
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,5 @@
1
+ # Cryptography - mandate secure version to address CVE-2026-44432
2
+ cryptography==48.0.1
3
+
4
+ # JOSE/JWE for match token format
5
+ jwcrypto==1.5.7
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env python3
2
+ """Setup script for Open Link Token Python package."""
3
+
4
+ import os
5
+
6
+ from setuptools import find_packages, setup
7
+
8
+ # Read the contents of the project README file.
9
+ this_directory = os.path.abspath(os.path.dirname(__file__))
10
+ root_readme = os.path.abspath(os.path.join(this_directory, "..", "..", "README.md"))
11
+ readme_path = root_readme if os.path.exists(root_readme) else os.path.join(this_directory, "README.md")
12
+ try:
13
+ with open(readme_path, encoding="utf-8") as f:
14
+ long_description = f.read()
15
+ except FileNotFoundError:
16
+ # Fallback to a short description if README is unavailable
17
+ long_description = "Open Link Token Python implementation for record linkage."
18
+
19
+ setup(
20
+ name="openlinktoken",
21
+ version="2.0.0",
22
+ author="Open Link Token Contributors",
23
+ description="Open Link Token Python core library for record linkage",
24
+ long_description=long_description,
25
+ long_description_content_type="text/markdown",
26
+ url="https://github.com/TruvetaPublic/OpenLinkToken",
27
+ project_urls={
28
+ "Source": "https://github.com/TruvetaPublic/OpenLinkToken",
29
+ "Documentation": "https://github.com/TruvetaPublic/OpenLinkToken/blob/main/README.md",
30
+ },
31
+ package_dir={"": "src/main"},
32
+ packages=find_packages(where="src/main"),
33
+ python_requires=">=3.10",
34
+ )
@@ -0,0 +1,15 @@
1
+ """
2
+ Open Link Token Python Package
3
+
4
+
5
+ This package provides utilities for tokenizing and processing person attributes
6
+ using various cryptographic transformations.
7
+ """
8
+
9
+ __version__ = "2.0.0"
10
+ __author__ = "Open Link Token Contributors"
11
+
12
+ __all__ = [
13
+ "__version__",
14
+ "__author__",
15
+ ]
@@ -0,0 +1,17 @@
1
+ """Base attribute types and loader utilities."""
2
+
3
+ from .attribute import Attribute
4
+ from .attribute_expression import AttributeExpression
5
+ from .attribute_loader import AttributeLoader
6
+ from .base_attribute import BaseAttribute
7
+ from .combined_attribute import CombinedAttribute
8
+ from .serializable_attribute import SerializableAttribute
9
+
10
+ __all__ = [
11
+ "Attribute",
12
+ "AttributeExpression",
13
+ "AttributeLoader",
14
+ "BaseAttribute",
15
+ "CombinedAttribute",
16
+ "SerializableAttribute",
17
+ ]
@@ -0,0 +1,26 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import List
3
+
4
+
5
+ class Attribute(ABC):
6
+ """Base interface for all attributes."""
7
+
8
+ @abstractmethod
9
+ def get_name(self) -> str:
10
+ """Get the name of the attribute."""
11
+ ... # pragma: no cover
12
+
13
+ @abstractmethod
14
+ def get_aliases(self) -> List[str]:
15
+ """Get the aliases for the attribute."""
16
+ ... # pragma: no cover
17
+
18
+ @abstractmethod
19
+ def normalize(self, value: str) -> str:
20
+ """Normalize the attribute value."""
21
+ ... # pragma: no cover
22
+
23
+ @abstractmethod
24
+ def validate(self, value: str) -> bool:
25
+ """Validate the attribute value."""
26
+ ... # pragma: no cover
@@ -0,0 +1,232 @@
1
+ # SPDX-License-Identifier: MIT
2
+ """
3
+ An attribute expression determines how the value of an attribute is normalized for consumption.
4
+
5
+ Below are the components used to compose the attribute expression:
6
+ - `T` - trim
7
+ - `U` - convert to upper case
8
+ - `S(start_index, end_index)` - substring of value
9
+ - `D` - treat as date
10
+ - `M(regex)` - match the regular expression
11
+ - `R(oldString, newString)` - replace old string with new string
12
+ - `|` - expression separator
13
+
14
+ Examples of attribute expressions:
15
+ - `T|S(0,3)|U` - trim the value, then take first 3 characters, and then convert to upper case.
16
+ - `T|D` - trim the value, treat the value as a date in the yyyy-MM-dd format.
17
+ - `T|M("\\d+")` - trim the value, then make sure that the value matches the regular expression.
18
+ """
19
+
20
+ import re
21
+ from datetime import datetime
22
+ from typing import TYPE_CHECKING, Optional, Type
23
+
24
+ if TYPE_CHECKING:
25
+ from openlinktoken.attributes.attribute import Attribute
26
+
27
+
28
+ class AttributeExpression:
29
+ """
30
+ An attribute expression determines how the value of an attribute is normalized for consumption.
31
+ """
32
+
33
+ EXPRESSION_PATTERN = re.compile(r"\s*(?P<expr>[^ (]+)(?:\((?P<args>[^\)]+)\))?")
34
+
35
+ def __init__(self, attribute_class: Type["Attribute"], expressions: Optional[str]):
36
+ """
37
+ Initialize the AttributeExpression.
38
+
39
+ Args:
40
+ attribute_class: The class of the attribute being processed.
41
+ expressions: The string of expressions to apply.
42
+ """
43
+ self.attribute_class = attribute_class
44
+ self.expressions = expressions
45
+
46
+ def get_effective_value(self, value: Optional[str]) -> str:
47
+ """
48
+ Get the effective value for an attribute after application of the attribute expression.
49
+
50
+ Args:
51
+ value: The attribute value.
52
+
53
+ Returns:
54
+ The effective value after applying the attribute expression.
55
+ """
56
+ if value is None or value.strip() == "":
57
+ return ""
58
+
59
+ if self.expressions is None or self.expressions.strip() == "":
60
+ return value
61
+
62
+ result = value
63
+ expression_parts = self.expressions.split("|")
64
+ for expression in expression_parts:
65
+ result = self._eval(result, expression)
66
+ return result
67
+
68
+ @staticmethod
69
+ def _eval_error(value: str, expression: str, inner_exception: Optional[Exception] = None) -> ValueError:
70
+ """
71
+ Create an error for failed expression evaluation.
72
+
73
+ Args:
74
+ value: The value being processed.
75
+ expression: The expression being applied.
76
+ inner_exception: The exception that occurred.
77
+
78
+ Returns:
79
+ The error with a detailed message.
80
+ """
81
+ message = f"Unable to evaluate expression [{expression}] over value [{value}]."
82
+ if inner_exception:
83
+ raise ValueError(message) from inner_exception
84
+ return ValueError(message)
85
+
86
+ @classmethod
87
+ def _eval(cls, value: str, expression: str) -> str:
88
+ """
89
+ Evaluate a single expression on the given value.
90
+
91
+ Args:
92
+ value: The value to process.
93
+ expression: The expression to apply.
94
+
95
+ Returns:
96
+ The processed value.
97
+ """
98
+ if value is None or expression is None:
99
+ raise cls._eval_error(value, expression)
100
+
101
+ matcher = cls.EXPRESSION_PATTERN.match(expression)
102
+ if not matcher:
103
+ raise cls._eval_error(value, expression)
104
+
105
+ expr = matcher.group("expr")
106
+ args = None
107
+ if matcher.group("args"):
108
+ args = matcher.group("args").strip().split(",")
109
+
110
+ expr_upper = expr.upper()
111
+
112
+ if expr_upper == "U":
113
+ return value.upper()
114
+ elif expr_upper == "T":
115
+ return value.strip()
116
+ elif expr_upper == "S":
117
+ if args is None:
118
+ raise cls._eval_error(value, expression)
119
+ return cls._substring(value, expression, args)
120
+ elif expr_upper == "R":
121
+ if args is None:
122
+ raise cls._eval_error(value, expression)
123
+ return cls._replace(value, expression, args)
124
+ elif expr_upper == "M":
125
+ if args is None:
126
+ raise cls._eval_error(value, expression)
127
+ return cls._match(value, expression, args)
128
+ elif expr_upper == "D":
129
+ return cls._date(value, expression)
130
+ else:
131
+ raise cls._eval_error(value, expression)
132
+
133
+ @classmethod
134
+ def _substring(cls, value: str, expression: str, args: list[str]) -> str:
135
+ """
136
+ Substring expression S(start, end).
137
+
138
+ Args:
139
+ value: The value to process.
140
+ expression: The expression being applied.
141
+ args: The arguments for the substring operation.
142
+
143
+ Returns:
144
+ The extracted substring.
145
+ """
146
+ if len(args) != 2:
147
+ raise cls._eval_error(value, expression)
148
+
149
+ try:
150
+ start = max(0, int(args[0]))
151
+ end = min(len(value), int(args[1]))
152
+ return value[start:end]
153
+ except (ValueError, IndexError) as ex:
154
+ raise cls._eval_error(value, expression, ex)
155
+
156
+ @classmethod
157
+ def _replace(cls, value: str, expression: str, args: list[str]) -> str:
158
+ """
159
+ Replace expression R(oldString, newString).
160
+
161
+ Args:
162
+ value: The value to process.
163
+ expression: The expression being applied.
164
+ args: The arguments for the replace operation.
165
+
166
+ Returns:
167
+ The value with replacements applied.
168
+ """
169
+ if len(args) != 2:
170
+ raise cls._eval_error(value, expression)
171
+
172
+ if len(args[0]) < 2 or len(args[1]) < 2:
173
+ raise cls._eval_error(value, expression, ValueError("Arguments must be quoted strings."))
174
+
175
+ try:
176
+ # Remove quotes from the arguments
177
+ old_val = args[0][1:-1] if len(args[0]) >= 2 else args[0]
178
+ new_val = args[1][1:-1] if len(args[1]) >= 2 else args[1]
179
+ return value.replace(old_val, new_val)
180
+ except Exception as ex:
181
+ raise cls._eval_error(value, expression, ex)
182
+
183
+ @classmethod
184
+ def _match(cls, value: str, expression: str, args: list[str]) -> str:
185
+ """
186
+ RegEx match M(regex).
187
+
188
+ Args:
189
+ value: The value to process.
190
+ expression: The expression being applied.
191
+ args: The arguments for the match operation.
192
+
193
+ Returns:
194
+ The concatenated matching parts.
195
+ """
196
+ if len(args) != 1:
197
+ raise cls._eval_error(value, expression)
198
+
199
+ try:
200
+ pattern = re.compile(args[0])
201
+ matches = pattern.findall(value)
202
+ return "".join(matches)
203
+ except re.error as ex:
204
+ raise cls._eval_error(value, expression, ex)
205
+
206
+ @classmethod
207
+ def _date(cls, value: str, expression: str) -> str:
208
+ """
209
+ Date expression D.
210
+
211
+ Supported date formats, and will be changed to "yyyy-MM-dd".
212
+ If the date is not in the supported formats, an exception will be thrown.
213
+
214
+ Args:
215
+ value: The value to process.
216
+ expression: The expression being applied.
217
+
218
+ Returns:
219
+ The formatted date string.
220
+ """
221
+ # Supported date formats
222
+ possible_formats = ["%Y-%m-%d"]
223
+
224
+ for fmt in possible_formats:
225
+ try:
226
+ date = datetime.strptime(value, fmt)
227
+ return date.strftime("%Y-%m-%d")
228
+ except ValueError:
229
+ continue
230
+
231
+ # If no format worked, raise an error
232
+ raise cls._eval_error(value, expression)
@@ -0,0 +1,48 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from typing import Set
4
+
5
+ from openlinktoken.attributes.attribute import Attribute
6
+ from openlinktoken.attributes.general.date_attribute import DateAttribute
7
+ from openlinktoken.attributes.general.decimal_attribute import DecimalAttribute
8
+ from openlinktoken.attributes.general.integer_attribute import IntegerAttribute
9
+ from openlinktoken.attributes.general.record_id_attribute import RecordIdAttribute
10
+ from openlinktoken.attributes.general.string_attribute import StringAttribute
11
+ from openlinktoken.attributes.general.year_attribute import YearAttribute
12
+ from openlinktoken.attributes.person.age_attribute import AgeAttribute
13
+ from openlinktoken.attributes.person.birth_date_attribute import BirthDateAttribute
14
+ from openlinktoken.attributes.person.birth_year_attribute import BirthYearAttribute
15
+ from openlinktoken.attributes.person.first_name_attribute import FirstNameAttribute
16
+ from openlinktoken.attributes.person.last_name_attribute import LastNameAttribute
17
+ from openlinktoken.attributes.person.postal_code_attribute import PostalCodeAttribute
18
+ from openlinktoken.attributes.person.sex_attribute import SexAttribute
19
+ from openlinktoken.attributes.person.social_security_number_attribute import SocialSecurityNumberAttribute
20
+
21
+
22
+ class AttributeLoader:
23
+ """
24
+ Loads all available attribute implementations.
25
+ """
26
+
27
+ def __init__(self):
28
+ raise RuntimeError("AttributeLoader should not be instantiated.")
29
+
30
+ @staticmethod
31
+ def load() -> Set[Attribute]:
32
+ """Load all attribute implementations."""
33
+ return {
34
+ RecordIdAttribute(),
35
+ StringAttribute(),
36
+ DateAttribute(),
37
+ DecimalAttribute(),
38
+ IntegerAttribute(),
39
+ YearAttribute(),
40
+ FirstNameAttribute(),
41
+ LastNameAttribute(),
42
+ BirthDateAttribute(),
43
+ AgeAttribute(),
44
+ BirthYearAttribute(),
45
+ SexAttribute(),
46
+ SocialSecurityNumberAttribute(),
47
+ PostalCodeAttribute(),
48
+ }
@@ -0,0 +1,31 @@
1
+ from typing import List
2
+
3
+ from openlinktoken.attributes.serializable_attribute import SerializableAttribute
4
+ from openlinktoken.attributes.validation.not_null_or_empty_validator import NotNullOrEmptyValidator
5
+ from openlinktoken.attributes.validation.serializable_attribute_validator import SerializableAttributeValidator
6
+
7
+
8
+ class BaseAttribute(SerializableAttribute):
9
+ """A base implementation of the SerializableAttribute interface.
10
+
11
+ This class provides a default implementation of the validate method
12
+ that validates the attribute value against a set of validation rules.
13
+
14
+ The default validation rules are:
15
+ - Not null or empty
16
+ """
17
+
18
+ def __init__(self, validation_rules: List[SerializableAttributeValidator] = None):
19
+ if validation_rules is None:
20
+ validation_rules = []
21
+
22
+ rule_list = [NotNullOrEmptyValidator()]
23
+ rule_list.extend(validation_rules)
24
+ self.validation_rules = rule_list
25
+
26
+ def validate(self, value: str) -> bool:
27
+ """Validates the attribute value against a set of validation rules."""
28
+ if value is None:
29
+ return False
30
+
31
+ return all(rule.eval(value) for rule in self.validation_rules)
@@ -0,0 +1,69 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import List
5
+
6
+ from openlinktoken.attributes.serializable_attribute import SerializableAttribute
7
+
8
+
9
+ class CombinedAttribute(SerializableAttribute, ABC):
10
+ """
11
+ Abstract base class for attributes that combine multiple attribute implementations.
12
+
13
+ This class allows for combining multiple attribute implementations where each
14
+ implementation handles a specific subset of validation and normalization logic.
15
+ The combined attribute will iterate through all implementations until it finds
16
+ one that reports the string as valid for validation, and uses the first
17
+ implementation that can normalize the value for normalization.
18
+
19
+ Subclasses must implement get_attribute_implementations() to provide
20
+ the list of attribute implementations to combine.
21
+ """
22
+
23
+ @abstractmethod
24
+ def get_attribute_implementations(self) -> List[SerializableAttribute]:
25
+ """
26
+ Get the list of attribute implementations to combine.
27
+
28
+ Returns:
29
+ List[SerializableAttribute]: The list of attribute implementations
30
+ """
31
+ pass
32
+
33
+ def validate(self, value: str) -> bool:
34
+ """
35
+ Validate the attribute value by checking if any of the combined
36
+ implementations consider the value valid.
37
+
38
+ Args:
39
+ value: The attribute value to validate
40
+
41
+ Returns:
42
+ True if any implementation validates the value successfully, False otherwise
43
+ """
44
+ if value is None:
45
+ return False
46
+
47
+ return any(impl.validate(value) for impl in self.get_attribute_implementations())
48
+
49
+ def normalize(self, value: str) -> str:
50
+ """
51
+ Normalize the attribute value using the first implementation that
52
+ successfully validates the value.
53
+
54
+ Args:
55
+ value: The attribute value to normalize
56
+
57
+ Returns:
58
+ The normalized value from the first validating implementation,
59
+ or the original trimmed value if no implementation validates it
60
+ """
61
+ if value is None:
62
+ return None
63
+
64
+ for impl in self.get_attribute_implementations():
65
+ if impl.validate(value):
66
+ return impl.normalize(value)
67
+
68
+ # If no implementation validates the value, return trimmed original
69
+ return value.strip()
@@ -0,0 +1,17 @@
1
+ """General-purpose attribute implementations."""
2
+
3
+ from .date_attribute import DateAttribute
4
+ from .decimal_attribute import DecimalAttribute
5
+ from .integer_attribute import IntegerAttribute
6
+ from .record_id_attribute import RecordIdAttribute
7
+ from .string_attribute import StringAttribute
8
+ from .year_attribute import YearAttribute
9
+
10
+ __all__ = [
11
+ "DateAttribute",
12
+ "DecimalAttribute",
13
+ "IntegerAttribute",
14
+ "RecordIdAttribute",
15
+ "StringAttribute",
16
+ "YearAttribute",
17
+ ]