openlinktoken 2.0.0__tar.gz → 2.1.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 (84) hide show
  1. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/PKG-INFO +3 -2
  2. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/pyproject.toml +3 -2
  3. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/requirements.txt +4 -1
  4. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/setup.py +1 -2
  5. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/__init__.py +1 -1
  6. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/__init__.py +4 -0
  7. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/attribute_expression.py +24 -1
  8. openlinktoken-2.1.0/src/main/openlinktoken/attributes/attribute_field.py +58 -0
  9. openlinktoken-2.1.0/src/main/openlinktoken/attributes/field_registry.py +137 -0
  10. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/metadata.py +1 -1
  11. openlinktoken-2.1.0/src/main/openlinktoken/tokens/definitions/field_ids.py +17 -0
  12. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/definitions/t1_token.py +5 -4
  13. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/definitions/t2_token.py +5 -4
  14. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/definitions/t3_token.py +5 -4
  15. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/definitions/t4_token.py +4 -3
  16. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/definitions/t5_token.py +4 -3
  17. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/token_generator.py +151 -22
  18. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken.egg-info/PKG-INFO +3 -2
  19. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken.egg-info/SOURCES.txt +3 -0
  20. openlinktoken-2.1.0/src/main/openlinktoken.egg-info/requires.txt +3 -0
  21. openlinktoken-2.0.0/src/main/openlinktoken.egg-info/requires.txt +0 -2
  22. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/MANIFEST.in +0 -0
  23. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/dev-requirements.txt +0 -0
  24. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/setup.cfg +0 -0
  25. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/attribute.py +0 -0
  26. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/attribute_loader.py +0 -0
  27. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/base_attribute.py +0 -0
  28. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/combined_attribute.py +0 -0
  29. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/general/__init__.py +0 -0
  30. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/general/date_attribute.py +0 -0
  31. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/general/decimal_attribute.py +0 -0
  32. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/general/integer_attribute.py +0 -0
  33. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/general/record_id_attribute.py +0 -0
  34. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/general/string_attribute.py +0 -0
  35. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/general/year_attribute.py +0 -0
  36. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/person/__init__.py +0 -0
  37. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/person/age_attribute.py +0 -0
  38. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/person/birth_date_attribute.py +0 -0
  39. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/person/birth_year_attribute.py +0 -0
  40. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/person/canadian_postal_code_attribute.py +0 -0
  41. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/person/first_name_attribute.py +0 -0
  42. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/person/last_name_attribute.py +0 -0
  43. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/person/postal_code_attribute.py +0 -0
  44. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/person/sex_attribute.py +0 -0
  45. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/person/social_security_number_attribute.py +0 -0
  46. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/person/us_postal_code_attribute.py +0 -0
  47. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/serializable_attribute.py +0 -0
  48. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/utilities/__init__.py +0 -0
  49. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/utilities/attribute_utilities.py +0 -0
  50. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/validation/__init__.py +0 -0
  51. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/validation/age_range_validator.py +0 -0
  52. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/validation/attribute_validator.py +0 -0
  53. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/validation/date_range_validator.py +0 -0
  54. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/validation/not_in_validator.py +0 -0
  55. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/validation/not_null_or_empty_validator.py +0 -0
  56. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/validation/not_starts_with_validator.py +0 -0
  57. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/validation/regex_validator.py +0 -0
  58. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/validation/serializable_attribute_validator.py +0 -0
  59. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/attributes/validation/year_range_validator.py +0 -0
  60. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/ec_key_utils.py +0 -0
  61. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/exchange_config.py +0 -0
  62. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/exchange_jwe.py +0 -0
  63. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/__init__.py +0 -0
  64. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/base_token_definition.py +0 -0
  65. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/token.py +0 -0
  66. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/token_definition.py +0 -0
  67. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/token_generation_exception.py +0 -0
  68. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/token_generator_result.py +0 -0
  69. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/token_registry.py +0 -0
  70. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/tokenizer/__init__.py +0 -0
  71. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/tokenizer/passthrough_tokenizer.py +0 -0
  72. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/tokenizer/sha256_tokenizer.py +0 -0
  73. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokens/tokenizer/tokenizer.py +0 -0
  74. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokentransformer/__init__.py +0 -0
  75. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokentransformer/decrypt_token_transformer.py +0 -0
  76. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokentransformer/encrypt_token_transformer.py +0 -0
  77. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokentransformer/encryption_constants.py +0 -0
  78. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokentransformer/hash_token_transformer.py +0 -0
  79. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokentransformer/jwe_match_token_formatter.py +0 -0
  80. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokentransformer/match_token_constants.py +0 -0
  81. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokentransformer/no_operation_token_transformer.py +0 -0
  82. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken/tokentransformer/token_transformer.py +0 -0
  83. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken.egg-info/dependency_links.txt +0 -0
  84. {openlinktoken-2.0.0 → openlinktoken-2.1.0}/src/main/openlinktoken.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openlinktoken
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Open Link Token Python core library for record linkage
5
5
  Home-page: https://github.com/TruvetaPublic/OpenLinkToken
6
6
  Author: Open Link Token Contributors
@@ -8,8 +8,9 @@ Project-URL: Source, https://github.com/TruvetaPublic/OpenLinkToken
8
8
  Project-URL: Documentation, https://github.com/TruvetaPublic/OpenLinkToken/blob/main/README.md
9
9
  Requires-Python: >=3.10
10
10
  Description-Content-Type: text/markdown
11
- Requires-Dist: cryptography==48.0.1
11
+ Requires-Dist: cryptography==49.0.0
12
12
  Requires-Dist: jwcrypto==1.5.7
13
+ Requires-Dist: PyYAML==6.0.3
13
14
  Dynamic: author
14
15
  Dynamic: description
15
16
  Dynamic: description-content-type
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "openlinktoken"
3
- version = "2.0.0"
3
+ version = "2.1.0"
4
4
  requires-python = ">=3.10"
5
5
  dynamic = [
6
6
  "authors",
@@ -10,8 +10,9 @@ dynamic = [
10
10
  "urls",
11
11
  ]
12
12
  dependencies = [
13
- "cryptography==48.0.1",
13
+ "cryptography==49.0.0",
14
14
  "jwcrypto==1.5.7",
15
+ "PyYAML==6.0.3",
15
16
  ]
16
17
 
17
18
  [build-system]
@@ -1,5 +1,8 @@
1
1
  # Cryptography - mandate secure version to address CVE-2026-44432
2
- cryptography==48.0.1
2
+ cryptography==49.0.0
3
3
 
4
4
  # JOSE/JWE for match token format
5
5
  jwcrypto==1.5.7
6
+
7
+ # YAML parsing for tokenization config loader
8
+ PyYAML==6.0.3
@@ -15,10 +15,9 @@ try:
15
15
  except FileNotFoundError:
16
16
  # Fallback to a short description if README is unavailable
17
17
  long_description = "Open Link Token Python implementation for record linkage."
18
-
19
18
  setup(
20
19
  name="openlinktoken",
21
- version="2.0.0",
20
+ version="2.1.0",
22
21
  author="Open Link Token Contributors",
23
22
  description="Open Link Token Python core library for record linkage",
24
23
  long_description=long_description,
@@ -6,7 +6,7 @@ This package provides utilities for tokenizing and processing person attributes
6
6
  using various cryptographic transformations.
7
7
  """
8
8
 
9
- __version__ = "2.0.0"
9
+ __version__ = "2.1.0"
10
10
  __author__ = "Open Link Token Contributors"
11
11
 
12
12
  __all__ = [
@@ -2,16 +2,20 @@
2
2
 
3
3
  from .attribute import Attribute
4
4
  from .attribute_expression import AttributeExpression
5
+ from .attribute_field import AttributeField
5
6
  from .attribute_loader import AttributeLoader
6
7
  from .base_attribute import BaseAttribute
7
8
  from .combined_attribute import CombinedAttribute
9
+ from .field_registry import FieldRegistry
8
10
  from .serializable_attribute import SerializableAttribute
9
11
 
10
12
  __all__ = [
11
13
  "Attribute",
12
14
  "AttributeExpression",
15
+ "AttributeField",
13
16
  "AttributeLoader",
14
17
  "BaseAttribute",
15
18
  "CombinedAttribute",
19
+ "FieldRegistry",
16
20
  "SerializableAttribute",
17
21
  ]
@@ -32,16 +32,39 @@ class AttributeExpression:
32
32
 
33
33
  EXPRESSION_PATTERN = re.compile(r"\s*(?P<expr>[^ (]+)(?:\((?P<args>[^\)]+)\))?")
34
34
 
35
- def __init__(self, attribute_class: Type["Attribute"], expressions: Optional[str]):
35
+ def __init__(
36
+ self,
37
+ attribute_class: Type["Attribute"],
38
+ expressions: Optional[str],
39
+ field_id: Optional[str] = None,
40
+ ):
36
41
  """
37
42
  Initialize the AttributeExpression.
38
43
 
39
44
  Args:
40
45
  attribute_class: The class of the attribute being processed.
41
46
  expressions: The string of expressions to apply.
47
+ field_id: Optional field identifier for field-based lookup. When None,
48
+ the attribute class is used directly as the identity key (legacy behavior).
42
49
  """
43
50
  self.attribute_class = attribute_class
44
51
  self.expressions = expressions
52
+ self.field_id = field_id
53
+
54
+ @classmethod
55
+ def of(cls, field_id: str, attribute_class: Type["Attribute"], expressions: Optional[str]) -> "AttributeExpression":
56
+ """
57
+ Create an attribute expression using a field ID for identity.
58
+
59
+ Args:
60
+ field_id: Unique field identifier from the FieldRegistry.
61
+ attribute_class: The class of the attribute providing normalization and validation.
62
+ expressions: The string of expressions to apply.
63
+
64
+ Returns:
65
+ A new AttributeExpression with field_id set.
66
+ """
67
+ return cls(attribute_class, expressions, field_id=field_id)
45
68
 
46
69
  def get_effective_value(self, value: Optional[str]) -> str:
47
70
  """
@@ -0,0 +1,58 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from typing import TYPE_CHECKING, Type
4
+
5
+ if TYPE_CHECKING:
6
+ from openlinktoken.attributes.attribute import Attribute
7
+
8
+
9
+ class AttributeField:
10
+ """
11
+ Represents a named field slot in a person record.
12
+
13
+ An AttributeField separates two concerns that were previously conflated:
14
+ - Field identity: "which value from the person record?" (the field_id)
15
+ - Attribute behavior: "how to normalize/validate?" (the attribute_class)
16
+
17
+ This allows multiple fields to share the same attribute type (e.g., two fields
18
+ both using StringAttribute normalization) while remaining distinct keys in the
19
+ person attributes map.
20
+ """
21
+
22
+ def __init__(self, field_id: str, attribute_class: Type["Attribute"]):
23
+ """
24
+ Create an attribute field with the given identity and attribute type.
25
+
26
+ Args:
27
+ field_id: Unique field identifier (e.g., "LastName", "MotherLastName").
28
+ attribute_class: The attribute class providing normalization and validation behavior.
29
+ """
30
+ if field_id is None:
31
+ raise ValueError("field_id must not be None")
32
+ if attribute_class is None:
33
+ raise ValueError("attribute_class must not be None")
34
+ self._field_id = field_id
35
+ self._attribute_class = attribute_class
36
+
37
+ @property
38
+ def field_id(self) -> str:
39
+ """Get the unique field identifier."""
40
+ return self._field_id
41
+
42
+ @property
43
+ def attribute_class(self) -> Type["Attribute"]:
44
+ """Get the attribute class providing normalization and validation behavior."""
45
+ return self._attribute_class
46
+
47
+ def __eq__(self, other: object) -> bool:
48
+ if self is other:
49
+ return True
50
+ if not isinstance(other, AttributeField):
51
+ return False
52
+ return self._field_id == other._field_id
53
+
54
+ def __hash__(self) -> int:
55
+ return hash(self._field_id)
56
+
57
+ def __repr__(self) -> str:
58
+ return f"AttributeField(field_id='{self._field_id}', attribute_class={self._attribute_class.__name__})"
@@ -0,0 +1,137 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ from typing import Dict, Optional, Set, Type
4
+
5
+ from openlinktoken.attributes.attribute import Attribute
6
+ from openlinktoken.attributes.attribute_field import AttributeField
7
+ from openlinktoken.attributes.attribute_loader import AttributeLoader
8
+
9
+
10
+ class FieldRegistry:
11
+ """
12
+ Registry mapping field identifiers to their corresponding Attribute instances.
13
+
14
+ The FieldRegistry is the resolution layer that connects field identifiers
15
+ (string keys used in person-attribute maps) to the attribute instances that provide
16
+ normalization and validation behavior.
17
+
18
+ Built-in attributes are auto-registered using their canonical name as the field ID.
19
+ Config-driven fields can register additional mappings (e.g., "MotherLastName" → StringAttribute).
20
+ """
21
+
22
+ def __init__(self, fields: Dict[str, AttributeField], field_to_attribute: Dict[str, Attribute]):
23
+ """
24
+ Initialize a FieldRegistry with the given mappings.
25
+
26
+ Use FieldRegistry.create_default() or FieldRegistry.Builder for construction.
27
+
28
+ Args:
29
+ fields: Mapping of field ID to AttributeField.
30
+ field_to_attribute: Mapping of field ID to Attribute instance.
31
+ """
32
+ self._fields: Dict[str, AttributeField] = dict(fields)
33
+ self._field_to_attribute: Dict[str, Attribute] = dict(field_to_attribute)
34
+
35
+ @classmethod
36
+ def create_default(cls) -> "FieldRegistry":
37
+ """
38
+ Create a default registry populated with all built-in attributes.
39
+
40
+ Each attribute is registered using its canonical name (from get_name()) as the field ID.
41
+
42
+ Returns:
43
+ A new registry with built-in attribute registrations.
44
+ """
45
+ builder = cls.Builder()
46
+ for attribute in AttributeLoader.load():
47
+ builder.register(attribute.get_name(), type(attribute), attribute)
48
+ return builder.build()
49
+
50
+ def get_attribute(self, field_id: str) -> Optional[Attribute]:
51
+ """
52
+ Resolve the attribute instance for a given field ID.
53
+
54
+ Args:
55
+ field_id: The field identifier.
56
+
57
+ Returns:
58
+ The attribute if registered, None otherwise.
59
+ """
60
+ return self._field_to_attribute.get(field_id)
61
+
62
+ def get_field(self, field_id: str) -> Optional[AttributeField]:
63
+ """
64
+ Resolve the attribute field definition for a given field ID.
65
+
66
+ Args:
67
+ field_id: The field identifier.
68
+
69
+ Returns:
70
+ The AttributeField if registered, None otherwise.
71
+ """
72
+ return self._fields.get(field_id)
73
+
74
+ def get_field_ids(self) -> Set[str]:
75
+ """
76
+ Return all registered field IDs.
77
+
78
+ Returns:
79
+ A set of field identifiers.
80
+ """
81
+ return set(self._fields.keys())
82
+
83
+ def size(self) -> int:
84
+ """
85
+ Return the number of registered fields.
86
+
87
+ Returns:
88
+ The registry size.
89
+ """
90
+ return len(self._fields)
91
+
92
+ class Builder:
93
+ """Builder for constructing a FieldRegistry with custom registrations."""
94
+
95
+ def __init__(self):
96
+ self._fields: Dict[str, AttributeField] = {}
97
+ self._field_to_attribute: Dict[str, Attribute] = {}
98
+
99
+ @classmethod
100
+ def from_defaults(cls) -> "FieldRegistry.Builder":
101
+ """
102
+ Create a builder pre-populated with all built-in attribute registrations.
103
+
104
+ Returns:
105
+ A new builder with defaults loaded.
106
+ """
107
+ builder = cls()
108
+ for attribute in AttributeLoader.load():
109
+ builder.register(attribute.get_name(), type(attribute), attribute)
110
+ return builder
111
+
112
+ def register(
113
+ self, field_id: str, attribute_class: Type[Attribute], attribute: Attribute
114
+ ) -> "FieldRegistry.Builder":
115
+ """
116
+ Register a field ID with its attribute class and instance.
117
+
118
+ Args:
119
+ field_id: The unique field identifier.
120
+ attribute_class: The attribute class providing behavior.
121
+ attribute: The attribute instance for normalization/validation.
122
+
123
+ Returns:
124
+ This builder for chaining.
125
+ """
126
+ self._fields[field_id] = AttributeField(field_id, attribute_class)
127
+ self._field_to_attribute[field_id] = attribute
128
+ return self
129
+
130
+ def build(self) -> "FieldRegistry":
131
+ """
132
+ Build an immutable FieldRegistry from the current registrations.
133
+
134
+ Returns:
135
+ The constructed registry.
136
+ """
137
+ return FieldRegistry(self._fields, self._field_to_attribute)
@@ -22,7 +22,7 @@ class Metadata:
22
22
  METADATA_FILE_EXTENSION = ".metadata.json"
23
23
  SYSTEM_PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
24
24
 
25
- DEFAULT_VERSION = "2.0.0"
25
+ DEFAULT_VERSION = "2.1.0"
26
26
 
27
27
  # Output format values
28
28
  OUTPUT_FORMAT_JSON = "JSON"
@@ -0,0 +1,17 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+
4
+ class FieldIds:
5
+ """
6
+ Field identifiers referenced by the built-in T1-T5 token definitions.
7
+
8
+ Centralizing these values avoids duplicating the same field-id strings across
9
+ multiple token definition modules.
10
+ """
11
+
12
+ LAST_NAME = "LastName"
13
+ FIRST_NAME = "FirstName"
14
+ SEX = "Sex"
15
+ BIRTH_DATE = "BirthDate"
16
+ POSTAL_CODE = "PostalCode"
17
+ SOCIAL_SECURITY_NUMBER = "SocialSecurityNumber"
@@ -7,6 +7,7 @@ from openlinktoken.attributes.person.birth_date_attribute import BirthDateAttrib
7
7
  from openlinktoken.attributes.person.first_name_attribute import FirstNameAttribute
8
8
  from openlinktoken.attributes.person.last_name_attribute import LastNameAttribute
9
9
  from openlinktoken.attributes.person.sex_attribute import SexAttribute
10
+ from openlinktoken.tokens.definitions.field_ids import FieldIds
10
11
  from openlinktoken.tokens.token import Token
11
12
 
12
13
 
@@ -24,10 +25,10 @@ class T1Token(Token):
24
25
  def __init__(self):
25
26
  """Initialize the T1 token definition."""
26
27
  self._definition = [
27
- AttributeExpression(LastNameAttribute, "T|U"),
28
- AttributeExpression(FirstNameAttribute, "T|S(0,1)|U"),
29
- AttributeExpression(SexAttribute, "T|U"),
30
- AttributeExpression(BirthDateAttribute, "T|D"),
28
+ AttributeExpression(LastNameAttribute, "T|U", field_id=FieldIds.LAST_NAME),
29
+ AttributeExpression(FirstNameAttribute, "T|S(0,1)|U", field_id=FieldIds.FIRST_NAME),
30
+ AttributeExpression(SexAttribute, "T|U", field_id=FieldIds.SEX),
31
+ AttributeExpression(BirthDateAttribute, "T|D", field_id=FieldIds.BIRTH_DATE),
31
32
  ]
32
33
 
33
34
  def get_identifier(self) -> str:
@@ -7,6 +7,7 @@ from openlinktoken.attributes.person.birth_date_attribute import BirthDateAttrib
7
7
  from openlinktoken.attributes.person.first_name_attribute import FirstNameAttribute
8
8
  from openlinktoken.attributes.person.last_name_attribute import LastNameAttribute
9
9
  from openlinktoken.attributes.person.postal_code_attribute import PostalCodeAttribute
10
+ from openlinktoken.tokens.definitions.field_ids import FieldIds
10
11
  from openlinktoken.tokens.token import Token
11
12
 
12
13
 
@@ -24,10 +25,10 @@ class T2Token(Token):
24
25
  def __init__(self):
25
26
  """Initialize the T2 token definition."""
26
27
  self._definition = [
27
- AttributeExpression(LastNameAttribute, "T|U"),
28
- AttributeExpression(FirstNameAttribute, "T|U"),
29
- AttributeExpression(BirthDateAttribute, "T|D"),
30
- AttributeExpression(PostalCodeAttribute, "T|S(0,3)|U"),
28
+ AttributeExpression(LastNameAttribute, "T|U", field_id=FieldIds.LAST_NAME),
29
+ AttributeExpression(FirstNameAttribute, "T|U", field_id=FieldIds.FIRST_NAME),
30
+ AttributeExpression(BirthDateAttribute, "T|D", field_id=FieldIds.BIRTH_DATE),
31
+ AttributeExpression(PostalCodeAttribute, "T|S(0,3)|U", field_id=FieldIds.POSTAL_CODE),
31
32
  ]
32
33
 
33
34
  def get_identifier(self) -> str:
@@ -7,6 +7,7 @@ from openlinktoken.attributes.person.birth_date_attribute import BirthDateAttrib
7
7
  from openlinktoken.attributes.person.first_name_attribute import FirstNameAttribute
8
8
  from openlinktoken.attributes.person.last_name_attribute import LastNameAttribute
9
9
  from openlinktoken.attributes.person.sex_attribute import SexAttribute
10
+ from openlinktoken.tokens.definitions.field_ids import FieldIds
10
11
  from openlinktoken.tokens.token import Token
11
12
 
12
13
 
@@ -24,10 +25,10 @@ class T3Token(Token):
24
25
  def __init__(self):
25
26
  """Initialize the T3 token definition."""
26
27
  self._definition = [
27
- AttributeExpression(LastNameAttribute, "T|U"),
28
- AttributeExpression(FirstNameAttribute, "T|U"),
29
- AttributeExpression(SexAttribute, "T|U"),
30
- AttributeExpression(BirthDateAttribute, "T|D"),
28
+ AttributeExpression(LastNameAttribute, "T|U", field_id=FieldIds.LAST_NAME),
29
+ AttributeExpression(FirstNameAttribute, "T|U", field_id=FieldIds.FIRST_NAME),
30
+ AttributeExpression(SexAttribute, "T|U", field_id=FieldIds.SEX),
31
+ AttributeExpression(BirthDateAttribute, "T|D", field_id=FieldIds.BIRTH_DATE),
31
32
  ]
32
33
 
33
34
  def get_identifier(self) -> str:
@@ -6,6 +6,7 @@ from openlinktoken.attributes.attribute_expression import AttributeExpression
6
6
  from openlinktoken.attributes.person.birth_date_attribute import BirthDateAttribute
7
7
  from openlinktoken.attributes.person.sex_attribute import SexAttribute
8
8
  from openlinktoken.attributes.person.social_security_number_attribute import SocialSecurityNumberAttribute
9
+ from openlinktoken.tokens.definitions.field_ids import FieldIds
9
10
  from openlinktoken.tokens.token import Token
10
11
 
11
12
 
@@ -23,9 +24,9 @@ class T4Token(Token):
23
24
  def __init__(self):
24
25
  """Initialize the T4 token definition."""
25
26
  self._definition = [
26
- AttributeExpression(SocialSecurityNumberAttribute, "T|M(\\d+)"),
27
- AttributeExpression(SexAttribute, "T|U"),
28
- AttributeExpression(BirthDateAttribute, "T|D"),
27
+ AttributeExpression(SocialSecurityNumberAttribute, "T|M(\\d+)", field_id=FieldIds.SOCIAL_SECURITY_NUMBER),
28
+ AttributeExpression(SexAttribute, "T|U", field_id=FieldIds.SEX),
29
+ AttributeExpression(BirthDateAttribute, "T|D", field_id=FieldIds.BIRTH_DATE),
29
30
  ]
30
31
 
31
32
  def get_identifier(self) -> str:
@@ -6,6 +6,7 @@ from openlinktoken.attributes.attribute_expression import AttributeExpression
6
6
  from openlinktoken.attributes.person.first_name_attribute import FirstNameAttribute
7
7
  from openlinktoken.attributes.person.last_name_attribute import LastNameAttribute
8
8
  from openlinktoken.attributes.person.sex_attribute import SexAttribute
9
+ from openlinktoken.tokens.definitions.field_ids import FieldIds
9
10
  from openlinktoken.tokens.token import Token
10
11
 
11
12
 
@@ -23,9 +24,9 @@ class T5Token(Token):
23
24
  def __init__(self):
24
25
  """Initialize the T5 token definition."""
25
26
  self._definition = [
26
- AttributeExpression(LastNameAttribute, "T|U"),
27
- AttributeExpression(FirstNameAttribute, "T|S(0,3)|U"),
28
- AttributeExpression(SexAttribute, "T|U"),
27
+ AttributeExpression(LastNameAttribute, "T|U", field_id=FieldIds.LAST_NAME),
28
+ AttributeExpression(FirstNameAttribute, "T|S(0,3)|U", field_id=FieldIds.FIRST_NAME),
29
+ AttributeExpression(SexAttribute, "T|U", field_id=FieldIds.SEX),
29
30
  ]
30
31
 
31
32
  def get_identifier(self) -> str:
@@ -5,6 +5,7 @@ from typing import Dict, List, Optional, Set, Type
5
5
 
6
6
  from openlinktoken.attributes.attribute import Attribute
7
7
  from openlinktoken.attributes.attribute_loader import AttributeLoader
8
+ from openlinktoken.attributes.field_registry import FieldRegistry
8
9
  from openlinktoken.tokens.base_token_definition import BaseTokenDefinition
9
10
  from openlinktoken.tokens.token import Token
10
11
  from openlinktoken.tokens.token_generation_exception import TokenGenerationException
@@ -35,13 +36,20 @@ class TokenGenerator:
35
36
  """
36
37
  return cls(token_definition, SHA256Tokenizer(token_transformer_list))
37
38
 
38
- def __init__(self, token_definition: BaseTokenDefinition, tokenizer: Tokenizer):
39
+ def __init__(
40
+ self,
41
+ token_definition: BaseTokenDefinition,
42
+ tokenizer: Tokenizer,
43
+ field_registry: Optional[FieldRegistry] = None,
44
+ ):
39
45
  """
40
46
  Initialize the token generator with an explicit tokenizer.
41
47
 
42
48
  Args:
43
49
  token_definition: The token definition.
44
50
  tokenizer: Tokenizer implementation. Use PassthroughTokenizer for plain mode.
51
+ field_registry: Optional custom field registry for field-ID-based lookups.
52
+ When None, a default registry is created from built-in attributes.
45
53
  """
46
54
  self.token_definition = token_definition
47
55
  self.attribute_instance_map: Dict[Type[Attribute], Attribute] = {}
@@ -51,19 +59,20 @@ class TokenGenerator:
51
59
  self.attribute_instance_map[type(attribute)] = attribute
52
60
 
53
61
  self.tokenizer = tokenizer
62
+ self.field_registry = field_registry or FieldRegistry.create_default()
54
63
 
55
64
  def _get_token_signature(
56
65
  self, token_id: str, person_attributes: Dict[Type[Attribute], str], result: TokenGeneratorResult
57
66
  ) -> Optional[str]:
58
67
  """
59
- Get the token signature for a given token identifier.
68
+ Get the token signature using a class-keyed person attributes map.
60
69
 
61
- Populates the invalid_attributes list in the result object with the attributes
62
- that are invalid.
70
+ .. deprecated::
71
+ Use :meth:`_get_token_signature_via_field_id` with a field-ID-keyed map instead.
63
72
 
64
73
  Args:
65
74
  token_id: The token identifier.
66
- person_attributes: The person attributes map.
75
+ person_attributes: The person attributes map, keyed by attribute class.
67
76
  result: The token generator result.
68
77
 
69
78
  Returns:
@@ -107,12 +116,13 @@ class TokenGenerator:
107
116
 
108
117
  def get_all_token_signatures(self, person_attributes: Dict[Type[Attribute], str]) -> Dict[str, str]:
109
118
  """
110
- Get the token signatures for all token/rule identifiers.
119
+ Get the token signatures for all token/rule identifiers using a class-keyed map.
111
120
 
112
- This is mostly a debug/logging/test method.
121
+ .. deprecated::
122
+ Use :meth:`get_all_token_signatures_via_field_id` with a field-ID-keyed map instead.
113
123
 
114
124
  Args:
115
- person_attributes: The person attributes map.
125
+ person_attributes: The person attributes map, keyed by attribute class.
116
126
 
117
127
  Returns:
118
128
  A map of token/rule identifier to the token signature.
@@ -133,18 +143,10 @@ class TokenGenerator:
133
143
  self, token_id: str, person_attributes: Dict[Type[Attribute], str], result: TokenGeneratorResult
134
144
  ) -> Optional[str]:
135
145
  """
136
- Get token for a given token identifier.
137
-
138
- Args:
139
- token_id: The token identifier.
140
- person_attributes: The person attributes map.
141
- result: The token generator result.
146
+ Get token for a given token identifier using a class-keyed person attributes map.
142
147
 
143
- Returns:
144
- The token using the token definition for the given token identifier.
145
-
146
- Raises:
147
- TokenGenerationException: In case of failure to generate the token.
148
+ .. deprecated::
149
+ Use :meth:`get_all_tokens_via_field_id` with a field-ID-keyed map instead.
148
150
  """
149
151
  signature = self._get_token_signature(token_id, person_attributes, result)
150
152
  logger.debug(f"Token signature for token id {token_id}: {signature}")
@@ -161,10 +163,13 @@ class TokenGenerator:
161
163
 
162
164
  def get_all_tokens(self, person_attributes: Dict[Type[Attribute], str]) -> TokenGeneratorResult:
163
165
  """
164
- Get the tokens for all token/rule identifiers.
166
+ Get the tokens for all token/rule identifiers using a class-keyed person attributes map.
167
+
168
+ .. deprecated::
169
+ Use :meth:`get_all_tokens_via_field_id` with a field-ID-keyed ``Dict[str, str]`` map instead.
165
170
 
166
171
  Args:
167
- person_attributes: The person attributes map.
172
+ person_attributes: The person attributes map, keyed by attribute class.
168
173
 
169
174
  Returns:
170
175
  A TokenGeneratorResult object containing the tokens and invalid attributes.
@@ -185,8 +190,11 @@ class TokenGenerator:
185
190
  """
186
191
  Get invalid person attribute names.
187
192
 
193
+ .. deprecated::
194
+ Use field-ID-keyed person attributes with :meth:`get_all_tokens_via_field_id` instead.
195
+
188
196
  Args:
189
- person_attributes: The person attributes map.
197
+ person_attributes: The person attributes map, keyed by attribute class.
190
198
 
191
199
  Returns:
192
200
  A set of invalid person attribute names.
@@ -199,3 +207,124 @@ class TokenGenerator:
199
207
  response.add(attribute.get_name())
200
208
 
201
209
  return response
210
+
211
+ # ===== Primary API =====
212
+
213
+ def _get_token_signature_via_field_id(
214
+ self, token_id: str, person_attributes: Dict[str, str], result: TokenGeneratorResult
215
+ ) -> Optional[str]:
216
+ """
217
+ Get the token signature for a given token identifier.
218
+
219
+ Args:
220
+ token_id: The token identifier.
221
+ person_attributes: Person attributes keyed by field ID (e.g., "LastName" → "Smith").
222
+ result: The token generator result.
223
+
224
+ Returns:
225
+ The token signature, or None if required fields are missing or invalid.
226
+ """
227
+ definition = self.token_definition.get_token_definition(token_id)
228
+
229
+ if person_attributes is None:
230
+ raise ValueError("Person attributes cannot be null.")
231
+
232
+ values = []
233
+
234
+ for attribute_expression in definition:
235
+ resolved_field_id = self._resolve_field_id(attribute_expression)
236
+ if resolved_field_id is None or resolved_field_id not in person_attributes:
237
+ return None
238
+
239
+ attribute = self._resolve_attribute(attribute_expression, resolved_field_id)
240
+ if attribute is None:
241
+ return None
242
+
243
+ attribute_value = person_attributes[resolved_field_id]
244
+
245
+ if not attribute.validate(attribute_value):
246
+ result.invalid_attributes.add(attribute.get_name())
247
+ return None
248
+
249
+ attribute_value = attribute.normalize(attribute_value)
250
+
251
+ try:
252
+ attribute_value = attribute_expression.get_effective_value(attribute_value)
253
+ values.append(attribute_value)
254
+ except ValueError as e:
255
+ logger.error(str(e))
256
+ return None
257
+
258
+ filtered_values = [v for v in values if v is not None and v.strip() != ""]
259
+ return "|".join(filtered_values)
260
+
261
+ def get_all_tokens_via_field_id(self, person_attributes: Dict[str, str]) -> TokenGeneratorResult:
262
+ """
263
+ Get the tokens for all token/rule identifiers.
264
+
265
+ This is the preferred API. It natively supports multiple fields sharing the same
266
+ attribute type (e.g., "MotherLastName" and "FatherLastName" both backed by StringAttribute).
267
+
268
+ Args:
269
+ person_attributes: Person attributes keyed by field ID (e.g., "LastName" → "Smith").
270
+
271
+ Returns:
272
+ A TokenGeneratorResult object containing the tokens and invalid attributes.
273
+ """
274
+ result = TokenGeneratorResult()
275
+
276
+ for token_id in self.token_definition.get_token_identifiers():
277
+ try:
278
+ signature = self._get_token_signature_via_field_id(token_id, person_attributes, result)
279
+ logger.debug(f"Token signature for token id {token_id}: {signature}")
280
+ try:
281
+ token = self.tokenizer.tokenize(signature)
282
+ if Token.BLANK == token:
283
+ result.blank_tokens_by_rule.add(token_id)
284
+ if token is not None:
285
+ result.tokens[token_id] = token
286
+ except Exception as e:
287
+ logger.error(f"Error generating token for token id: {token_id}", exc_info=e)
288
+ except Exception as e:
289
+ logger.error(f"Error generating token for token id: {token_id}", exc_info=e)
290
+
291
+ return result
292
+
293
+ def get_all_token_signatures_via_field_id(self, person_attributes: Dict[str, str]) -> Dict[str, str]:
294
+ """
295
+ Get the token signatures for all token/rule identifiers. Mostly useful for debugging.
296
+
297
+ Args:
298
+ person_attributes: Person attributes keyed by field ID.
299
+
300
+ Returns:
301
+ A map of token/rule identifier to the token signature.
302
+ """
303
+ signatures = {}
304
+
305
+ for token_id in self.token_definition.get_token_identifiers():
306
+ try:
307
+ signature = self._get_token_signature_via_field_id(token_id, person_attributes, TokenGeneratorResult())
308
+ if signature is not None:
309
+ signatures[token_id] = signature
310
+ except Exception as e:
311
+ logger.error(f"Error generating token signature for token id: {token_id}", exc_info=e)
312
+
313
+ return signatures
314
+
315
+ def _resolve_field_id(self, expression) -> Optional[str]:
316
+ """Resolve the effective field ID from an AttributeExpression."""
317
+ if expression.field_id is not None:
318
+ return expression.field_id
319
+ # Legacy fallback: derive field ID from attribute class name
320
+ attribute = self.attribute_instance_map.get(expression.attribute_class)
321
+ return attribute.get_name() if attribute else None
322
+
323
+ def _resolve_attribute(self, expression, resolved_field_id: str) -> Optional[Attribute]:
324
+ """Resolve the attribute instance for an expression and field ID."""
325
+ # Try field registry first
326
+ from_registry = self.field_registry.get_attribute(resolved_field_id)
327
+ if from_registry is not None:
328
+ return from_registry
329
+ # Fallback to class-based lookup
330
+ return self.attribute_instance_map.get(expression.attribute_class)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openlinktoken
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Open Link Token Python core library for record linkage
5
5
  Home-page: https://github.com/TruvetaPublic/OpenLinkToken
6
6
  Author: Open Link Token Contributors
@@ -8,8 +8,9 @@ Project-URL: Source, https://github.com/TruvetaPublic/OpenLinkToken
8
8
  Project-URL: Documentation, https://github.com/TruvetaPublic/OpenLinkToken/blob/main/README.md
9
9
  Requires-Python: >=3.10
10
10
  Description-Content-Type: text/markdown
11
- Requires-Dist: cryptography==48.0.1
11
+ Requires-Dist: cryptography==49.0.0
12
12
  Requires-Dist: jwcrypto==1.5.7
13
+ Requires-Dist: PyYAML==6.0.3
13
14
  Dynamic: author
14
15
  Dynamic: description
15
16
  Dynamic: description-content-type
@@ -18,9 +18,11 @@ src/main/openlinktoken.egg-info/top_level.txt
18
18
  src/main/openlinktoken/attributes/__init__.py
19
19
  src/main/openlinktoken/attributes/attribute.py
20
20
  src/main/openlinktoken/attributes/attribute_expression.py
21
+ src/main/openlinktoken/attributes/attribute_field.py
21
22
  src/main/openlinktoken/attributes/attribute_loader.py
22
23
  src/main/openlinktoken/attributes/base_attribute.py
23
24
  src/main/openlinktoken/attributes/combined_attribute.py
25
+ src/main/openlinktoken/attributes/field_registry.py
24
26
  src/main/openlinktoken/attributes/serializable_attribute.py
25
27
  src/main/openlinktoken/attributes/general/__init__.py
26
28
  src/main/openlinktoken/attributes/general/date_attribute.py
@@ -60,6 +62,7 @@ src/main/openlinktoken/tokens/token_generation_exception.py
60
62
  src/main/openlinktoken/tokens/token_generator.py
61
63
  src/main/openlinktoken/tokens/token_generator_result.py
62
64
  src/main/openlinktoken/tokens/token_registry.py
65
+ src/main/openlinktoken/tokens/definitions/field_ids.py
63
66
  src/main/openlinktoken/tokens/definitions/t1_token.py
64
67
  src/main/openlinktoken/tokens/definitions/t2_token.py
65
68
  src/main/openlinktoken/tokens/definitions/t3_token.py
@@ -0,0 +1,3 @@
1
+ cryptography==49.0.0
2
+ jwcrypto==1.5.7
3
+ PyYAML==6.0.3
@@ -1,2 +0,0 @@
1
- cryptography==48.0.1
2
- jwcrypto==1.5.7
File without changes
File without changes