base-api-utils 1.1.93__tar.gz → 1.1.95__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 (107) hide show
  1. {base_api_utils-1.1.93/src/base_api_utils.egg-info → base_api_utils-1.1.95}/PKG-INFO +1 -1
  2. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/pyproject.toml +1 -1
  3. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/serializers/v2/expands.py +16 -5
  4. base_api_utils-1.1.95/src/base_api_utils/utils/fingerprint_helpers.py +190 -0
  5. {base_api_utils-1.1.93 → base_api_utils-1.1.95/src/base_api_utils.egg-info}/PKG-INFO +1 -1
  6. base_api_utils-1.1.93/src/base_api_utils/utils/fingerprint_helpers.py +0 -99
  7. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/LICENSE +0 -0
  8. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/README.md +0 -0
  9. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/setup.cfg +0 -0
  10. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/__init__.py +0 -0
  11. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/commands/__init__.py +0 -0
  12. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/commands/failed_task_management_mixin.py +0 -0
  13. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/domain/__init__.py +0 -0
  14. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/domain/event_message.py +0 -0
  15. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/domain/event_message_factory.py +0 -0
  16. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/domain/events.py +0 -0
  17. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/dtos/__init__.py +0 -0
  18. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/dtos/authz_user.py +0 -0
  19. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/fields/__init__.py +0 -0
  20. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/fields/email_list_field.py +0 -0
  21. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/filters/__init__.py +0 -0
  22. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/filters/base_filter.py +0 -0
  23. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/filters/custom_ordering_filter.py +0 -0
  24. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/filters/filter_utils_mixin.py +0 -0
  25. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/handlers/__init__.py +0 -0
  26. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/handlers/base_event_handler.py +0 -0
  27. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/handlers/sponsor_event_handler.py +0 -0
  28. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/handlers/sponsorship_addon_event_handler.py +0 -0
  29. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/handlers/sponsorship_event_handler.py +0 -0
  30. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/handlers/summit_event_handler.py +0 -0
  31. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/ioc/__init__.py +0 -0
  32. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/ioc/container.py +0 -0
  33. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/models/__init__.py +0 -0
  34. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/models/item_image.py +0 -0
  35. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/security/__init__.py +0 -0
  36. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/security/abstract_access_token_service.py +0 -0
  37. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/security/abstract_oauth2_api_client.py +0 -0
  38. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/security/access_token_service.py +0 -0
  39. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/security/check_sponsor_user_permissions.py +0 -0
  40. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/security/group_required.py +0 -0
  41. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/security/oauth2_authentication.py +0 -0
  42. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/security/oauth2_client_factory.py +0 -0
  43. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/security/oauth2_scope_required.py +0 -0
  44. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/security/permission_event_handler.py +0 -0
  45. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/security/shared.py +0 -0
  46. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/security/user_permissions_service.py +0 -0
  47. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/serializers/__init__.py +0 -0
  48. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/serializers/base_model_serializer.py +0 -0
  49. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/serializers/email_list_char_field.py +0 -0
  50. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/serializers/nullable_slug_related_field.py +0 -0
  51. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/serializers/serializers_registry.py +0 -0
  52. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/serializers/task_result_serializer.py +0 -0
  53. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/serializers/timestamp_field.py +0 -0
  54. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/serializers/v2/__init__.py +0 -0
  55. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/serializers/v2/abstract_serializer.py +0 -0
  56. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/serializers/v2/base_model_serializer.py +0 -0
  57. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/serializers/v2/query_params.py +0 -0
  58. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/services/__init__.py +0 -0
  59. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/services/abstract_domain_events_service.py +0 -0
  60. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/services/abstract_event_dispatcher.py +0 -0
  61. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/services/abstract_images_service.py +0 -0
  62. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/services/abstract_sponsor_metadata_service.py +0 -0
  63. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/services/abstract_storage_service.py +0 -0
  64. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/services/abstract_summit_metadata_service.py +0 -0
  65. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/services/amqp_service.py +0 -0
  66. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/services/celery_failed.py +0 -0
  67. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/services/event_dispatcher.py +0 -0
  68. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/services/images_service.py +0 -0
  69. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/services/s3_service.py +0 -0
  70. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/services/strategies/__init__.py +0 -0
  71. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/services/strategies/abstract_task_runner_strategy.py +0 -0
  72. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/services/strategies/publish_domain_event_strategy.py +0 -0
  73. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/tests/__init__.py +0 -0
  74. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/tests/test_failed_celery_task_management.py +0 -0
  75. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/urls.py +0 -0
  76. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/__init__.py +0 -0
  77. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/async_helpers.py +0 -0
  78. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/circuit_breaker.py +0 -0
  79. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/config.py +0 -0
  80. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/custom_auto_schema.py +0 -0
  81. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/db_connection_helpers.py +0 -0
  82. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/debounce_helpers.py +0 -0
  83. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/enums.py +0 -0
  84. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/env_var_eval.py +0 -0
  85. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/exceptions.py +0 -0
  86. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/external_errors.py +0 -0
  87. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/file_lock.py +0 -0
  88. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/http_helpers.py +0 -0
  89. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/inject_job.py +0 -0
  90. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/model_helpers.py +0 -0
  91. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/oauth2_custom_schema_base.py +0 -0
  92. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/pagination.py +0 -0
  93. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/rate_utils.py +0 -0
  94. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/read_write_serializer_mixin.py +0 -0
  95. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/serializers.py +0 -0
  96. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/service_utils.py +0 -0
  97. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/string.py +0 -0
  98. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/utils/tests.py +0 -0
  99. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/views/__init__.py +0 -0
  100. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/views/base_view.py +0 -0
  101. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/views/parent_children_crud.py +0 -0
  102. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/views/root_entity_crud.py +0 -0
  103. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils/views/tasks_result_view.py +0 -0
  104. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils.egg-info/SOURCES.txt +0 -0
  105. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils.egg-info/dependency_links.txt +0 -0
  106. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils.egg-info/requires.txt +0 -0
  107. {base_api_utils-1.1.93 → base_api_utils-1.1.95}/src/base_api_utils.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: base-api-utils
3
- Version: 1.1.93
3
+ Version: 1.1.95
4
4
  Summary: Django Rest Framework micro services common utilities
5
5
  Author-email: Román Gutierrez <roman@tipit.net>, Sebastian Marcet <sebastian@tipit.net>
6
6
  Project-URL: Homepage, https://github.com/fntechgit/base-api-utils
@@ -7,7 +7,7 @@ where = ["src"]
7
7
 
8
8
  [project]
9
9
  name = "base-api-utils"
10
- version = "1.1.93"
10
+ version = "1.1.95"
11
11
  dependencies = [
12
12
  "boto3",
13
13
  "celery",
@@ -13,6 +13,7 @@ class ExpandMapping:
13
13
  original_attribute: str | None = None
14
14
  source: str | None = None
15
15
  verify_relation: bool = False
16
+ schema_override: type | None = None
16
17
 
17
18
  @property
18
19
  def source_kwargs(self) -> dict:
@@ -86,6 +87,17 @@ class One2ManyExpandSerializer(IExpandSerializer):
86
87
  fields[fk] = serializers.IntegerField(read_only=True)
87
88
 
88
89
 
90
+ class _IDListField(serializers.Field):
91
+ """Runtime: returns list of PKs. Schema: annotated externally via _spectacular_annotation."""
92
+ def to_representation(self, value):
93
+ if hasattr(value, 'all'):
94
+ return [item.pk for item in value.all()]
95
+ return [item.pk for item in value]
96
+
97
+ def to_internal_value(self, data):
98
+ return data
99
+
100
+
89
101
  class Many2OneExpandSerializer(IExpandSerializer):
90
102
  # to-many: keep same key; not expanded => list[int]; expanded => list[object]
91
103
  def apply(self, *, fields: dict, mapping: ExpandMapping, context: dict) -> None:
@@ -115,8 +127,7 @@ class Many2OneExpandSerializer(IExpandSerializer):
115
127
  **mapping.source_kwargs,
116
128
  )
117
129
  else:
118
- fields[attr] = serializers.PrimaryKeyRelatedField(
119
- many=True,
120
- read_only=True,
121
- **mapping.source_kwargs,
122
- )
130
+ schema_cls = mapping.schema_override or mapping.serializer
131
+ id_field = _IDListField(read_only=True, **mapping.source_kwargs)
132
+ id_field._spectacular_annotation = {"field": schema_cls(many=True, read_only=True)}
133
+ fields[attr] = id_field
@@ -0,0 +1,190 @@
1
+ """
2
+ 1 - Fingerprint utility functions for computing SHA-256 based identity hashes.
3
+
4
+ These functions enable change detection to determine when entities need to be
5
+ created or updated by computing deterministic fingerprints from their field values.
6
+
7
+ 2 - FingerPrintMixin to centralize models fingerprint generation.
8
+ """
9
+
10
+ import hashlib
11
+ from datetime import datetime
12
+ from typing import Any, Dict, Iterable, Optional
13
+
14
+
15
+ def compute_sha256(data: str) -> str:
16
+ """
17
+ Compute SHA-256 hash of the input string.
18
+
19
+ Args:
20
+ data: The string to hash
21
+
22
+ Returns:
23
+ 64-character lowercase hex string
24
+ """
25
+ return hashlib.sha256(data.encode('utf-8')).hexdigest()
26
+
27
+
28
+ def format_value(value: Any) -> Optional[str]:
29
+ """
30
+ Format a field value for inclusion in a fingerprint string.
31
+
32
+ Args:
33
+ value: The value to format
34
+
35
+ Returns:
36
+ Formatted string representation, or None if the value should be skipped
37
+ """
38
+ if value is None:
39
+ return None
40
+
41
+ if isinstance(value, bool):
42
+ return "true" if value else "false"
43
+
44
+ if isinstance(value, datetime):
45
+ return value.isoformat()
46
+
47
+ return str(value)
48
+
49
+
50
+ def build_fingerprint_string(fields: Dict[str, Any]) -> str:
51
+ """
52
+ Build a deterministic string from field name-value pairs.
53
+
54
+ Fields are sorted by name for determinism. Fields with None values are skipped.
55
+
56
+ Args:
57
+ fields: Dictionary of field names to values
58
+
59
+ Returns:
60
+ Concatenated string of "name=value" pairs separated by "|"
61
+ """
62
+ parts = []
63
+ for key in sorted(fields.keys()):
64
+ formatted = format_value(fields[key])
65
+ if formatted is not None:
66
+ parts.append(f"{key}={formatted}")
67
+ return "|".join(parts)
68
+
69
+
70
+ def format_id_list(ids: Iterable[int]) -> str:
71
+ """
72
+ Format a list of IDs as a comma-separated string.
73
+
74
+ IDs are sorted numerically for determinism.
75
+
76
+ Args:
77
+ ids: Iterable of integer IDs
78
+
79
+ Returns:
80
+ Comma-separated string of sorted IDs
81
+ """
82
+ return ",".join(str(id_) for id_ in sorted(ids))
83
+
84
+
85
+ def combine_fingerprints(fingerprints: Iterable[str]) -> str:
86
+ """
87
+ Combine multiple fingerprints into a single hash.
88
+
89
+ Fingerprints are sorted for determinism before being concatenated and hashed.
90
+
91
+ Args:
92
+ fingerprints: Iterable of fingerprint hex strings
93
+
94
+ Returns:
95
+ 64-character lowercase hex string, or empty string if no fingerprints
96
+ """
97
+ sorted_fps = sorted(fingerprints)
98
+ if not sorted_fps:
99
+ return ""
100
+ combined = "|".join(sorted_fps)
101
+ return compute_sha256(combined)
102
+
103
+
104
+ class FingerPrintMixin:
105
+ def _get_base_fingerprint_fields(self) -> dict:
106
+ return {}
107
+
108
+ def _get_specific_fingerprint_fields(self) -> dict:
109
+ return {}
110
+
111
+ def _get_m2m_fingerprint_fields(self) -> dict:
112
+ return {}
113
+
114
+ def _get_base_m2m_fields(self) -> dict:
115
+ return {}
116
+
117
+ def _get_children_accessors(self) -> list[dict]:
118
+ """
119
+ Returns a list of child descriptor dicts, each with:
120
+ - 'accessor': the related manager name as a string (e.g. 'meta_fields')
121
+ - 'order_by': field name used to sort the collection (e.g. 'id')
122
+
123
+ Subclasses should override this to declare their child relations.
124
+ Example:
125
+ return [{'accessor': 'meta_fields', 'order_by': 'id'}]
126
+ """
127
+ return []
128
+
129
+ def _resolve_children(self, accessor: str, order_by: str) -> list:
130
+ """
131
+ Resolves a child collection respecting three possible sources,
132
+ in priority order:
133
+
134
+ 1. In-memory prefetch cache (_prefetched_objects_cache) — avoids DB hits
135
+ when objects have been built or prefetched outside the ORM.
136
+ 2. Database query via the related manager — only when the instance
137
+ has already been persisted (pk is set).
138
+ 3. Empty list — for unsaved instances with no prefetched data.
139
+ """
140
+ cache = getattr(self, '_prefetched_objects_cache', {})
141
+
142
+ if accessor in cache:
143
+ # Sort in-memory using the order_by field (strip leading '-' for reverse)
144
+ reverse = order_by.startswith('-')
145
+ field = order_by.lstrip('-')
146
+ return sorted(cache[accessor], key=lambda x: getattr(x, field), reverse=reverse)
147
+
148
+ if self.pk:
149
+ return list(getattr(self, accessor).all().order_by(order_by))
150
+
151
+ return []
152
+
153
+ def _compute_own_fingerprint(self, field_overrides=None) -> str:
154
+ all_fields = {
155
+ **self._get_base_fingerprint_fields(),
156
+ **self._get_specific_fingerprint_fields(),
157
+ }
158
+
159
+ # Append M2M relation IDs as formatted strings
160
+ for key, id_list in self._get_m2m_fingerprint_fields().items():
161
+ if id_list:
162
+ all_fields[key] = format_id_list(id_list)
163
+
164
+ # Allow caller to substitute values without mutating the instance
165
+ if field_overrides:
166
+ all_fields.update(field_overrides)
167
+
168
+ return compute_sha256(build_fingerprint_string(all_fields))
169
+
170
+ def fingerprint(self, field_overrides=None) -> str:
171
+
172
+ # 1. Compute this instance's own hash
173
+ own_hash = self._compute_own_fingerprint(field_overrides)
174
+
175
+ # 2. Recursively collect hashes from each child collection
176
+ children_hashes = []
177
+ for descriptor in self._get_children_accessors():
178
+ children = self._resolve_children(
179
+ accessor=descriptor['accessor'],
180
+ order_by=descriptor['order_by'],
181
+ )
182
+ for child in children:
183
+ children_hashes.append(child.fingerprint())
184
+
185
+ # 3. Combine own hash with children hashes into a single fingerprint
186
+ if children_hashes:
187
+ combined = combine_fingerprints(children_hashes)
188
+ return compute_sha256(f"{own_hash}|{combined}")
189
+
190
+ return own_hash
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: base-api-utils
3
- Version: 1.1.93
3
+ Version: 1.1.95
4
4
  Summary: Django Rest Framework micro services common utilities
5
5
  Author-email: Román Gutierrez <roman@tipit.net>, Sebastian Marcet <sebastian@tipit.net>
6
6
  Project-URL: Homepage, https://github.com/fntechgit/base-api-utils
@@ -1,99 +0,0 @@
1
- """
2
- Fingerprint utility functions for computing SHA-256 based identity hashes.
3
-
4
- These functions enable change detection to determine when entities need to be
5
- created or updated by computing deterministic fingerprints from their field values.
6
- """
7
-
8
- import hashlib
9
- from datetime import datetime
10
- from typing import Any, Dict, Iterable, Optional
11
-
12
-
13
- def compute_sha256(data: str) -> str:
14
- """
15
- Compute SHA-256 hash of the input string.
16
-
17
- Args:
18
- data: The string to hash
19
-
20
- Returns:
21
- 64-character lowercase hex string
22
- """
23
- return hashlib.sha256(data.encode('utf-8')).hexdigest()
24
-
25
-
26
- def format_value(value: Any) -> Optional[str]:
27
- """
28
- Format a field value for inclusion in a fingerprint string.
29
-
30
- Args:
31
- value: The value to format
32
-
33
- Returns:
34
- Formatted string representation, or None if the value should be skipped
35
- """
36
- if value is None:
37
- return None
38
-
39
- if isinstance(value, bool):
40
- return "true" if value else "false"
41
-
42
- if isinstance(value, datetime):
43
- return value.isoformat()
44
-
45
- return str(value)
46
-
47
-
48
- def build_fingerprint_string(fields: Dict[str, Any]) -> str:
49
- """
50
- Build a deterministic string from field name-value pairs.
51
-
52
- Fields are sorted by name for determinism. Fields with None values are skipped.
53
-
54
- Args:
55
- fields: Dictionary of field names to values
56
-
57
- Returns:
58
- Concatenated string of "name=value" pairs separated by "|"
59
- """
60
- parts = []
61
- for key in sorted(fields.keys()):
62
- formatted = format_value(fields[key])
63
- if formatted is not None:
64
- parts.append(f"{key}={formatted}")
65
- return "|".join(parts)
66
-
67
-
68
- def format_id_list(ids: Iterable[int]) -> str:
69
- """
70
- Format a list of IDs as a comma-separated string.
71
-
72
- IDs are sorted numerically for determinism.
73
-
74
- Args:
75
- ids: Iterable of integer IDs
76
-
77
- Returns:
78
- Comma-separated string of sorted IDs
79
- """
80
- return ",".join(str(id_) for id_ in sorted(ids))
81
-
82
-
83
- def combine_fingerprints(fingerprints: Iterable[str]) -> str:
84
- """
85
- Combine multiple fingerprints into a single hash.
86
-
87
- Fingerprints are sorted for determinism before being concatenated and hashed.
88
-
89
- Args:
90
- fingerprints: Iterable of fingerprint hex strings
91
-
92
- Returns:
93
- 64-character lowercase hex string, or empty string if no fingerprints
94
- """
95
- sorted_fps = sorted(fingerprints)
96
- if not sorted_fps:
97
- return ""
98
- combined = "|".join(sorted_fps)
99
- return compute_sha256(combined)
File without changes