otdf-python 0.1.10__py3-none-any.whl → 0.3.5__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 (144) hide show
  1. otdf_python/__init__.py +25 -0
  2. otdf_python/__main__.py +12 -0
  3. otdf_python/address_normalizer.py +84 -0
  4. otdf_python/aesgcm.py +55 -0
  5. otdf_python/assertion_config.py +84 -0
  6. otdf_python/asym_crypto.py +198 -0
  7. otdf_python/auth_headers.py +33 -0
  8. otdf_python/autoconfigure_utils.py +113 -0
  9. otdf_python/cli.py +569 -0
  10. otdf_python/collection_store.py +41 -0
  11. otdf_python/collection_store_impl.py +22 -0
  12. otdf_python/config.py +69 -0
  13. otdf_python/connect_client.py +0 -0
  14. otdf_python/constants.py +1 -0
  15. otdf_python/crypto_utils.py +78 -0
  16. otdf_python/dpop.py +81 -0
  17. otdf_python/ecc_constants.py +176 -0
  18. otdf_python/ecc_mode.py +83 -0
  19. otdf_python/ecdh.py +317 -0
  20. otdf_python/eckeypair.py +75 -0
  21. otdf_python/header.py +181 -0
  22. otdf_python/invalid_zip_exception.py +8 -0
  23. otdf_python/kas_client.py +709 -0
  24. otdf_python/kas_connect_rpc_client.py +213 -0
  25. otdf_python/kas_info.py +25 -0
  26. otdf_python/kas_key_cache.py +52 -0
  27. otdf_python/key_type.py +31 -0
  28. otdf_python/key_type_constants.py +43 -0
  29. otdf_python/manifest.py +215 -0
  30. otdf_python/nanotdf.py +863 -0
  31. otdf_python/nanotdf_ecdsa_struct.py +132 -0
  32. otdf_python/nanotdf_type.py +43 -0
  33. otdf_python/policy_binding_serializer.py +39 -0
  34. otdf_python/policy_info.py +55 -0
  35. otdf_python/policy_object.py +22 -0
  36. otdf_python/policy_stub.py +2 -0
  37. otdf_python/resource_locator.py +172 -0
  38. otdf_python/sdk.py +436 -0
  39. otdf_python/sdk_builder.py +416 -0
  40. otdf_python/sdk_exceptions.py +16 -0
  41. otdf_python/symmetric_and_payload_config.py +30 -0
  42. otdf_python/tdf.py +480 -0
  43. otdf_python/tdf_reader.py +153 -0
  44. otdf_python/tdf_writer.py +23 -0
  45. otdf_python/token_source.py +34 -0
  46. otdf_python/version.py +57 -0
  47. otdf_python/zip_reader.py +47 -0
  48. otdf_python/zip_writer.py +70 -0
  49. otdf_python-0.3.5.dist-info/METADATA +153 -0
  50. otdf_python-0.3.5.dist-info/RECORD +137 -0
  51. {otdf_python-0.1.10.dist-info → otdf_python-0.3.5.dist-info}/WHEEL +1 -2
  52. {otdf_python-0.1.10.dist-info → otdf_python-0.3.5.dist-info/licenses}/LICENSE +1 -1
  53. otdf_python_proto/__init__.py +37 -0
  54. otdf_python_proto/authorization/__init__.py +1 -0
  55. otdf_python_proto/authorization/authorization_pb2.py +80 -0
  56. otdf_python_proto/authorization/authorization_pb2.pyi +161 -0
  57. otdf_python_proto/authorization/authorization_pb2_connect.py +191 -0
  58. otdf_python_proto/authorization/v2/authorization_pb2.py +105 -0
  59. otdf_python_proto/authorization/v2/authorization_pb2.pyi +134 -0
  60. otdf_python_proto/authorization/v2/authorization_pb2_connect.py +233 -0
  61. otdf_python_proto/common/__init__.py +1 -0
  62. otdf_python_proto/common/common_pb2.py +52 -0
  63. otdf_python_proto/common/common_pb2.pyi +61 -0
  64. otdf_python_proto/entity/__init__.py +1 -0
  65. otdf_python_proto/entity/entity_pb2.py +47 -0
  66. otdf_python_proto/entity/entity_pb2.pyi +50 -0
  67. otdf_python_proto/entityresolution/__init__.py +1 -0
  68. otdf_python_proto/entityresolution/entity_resolution_pb2.py +57 -0
  69. otdf_python_proto/entityresolution/entity_resolution_pb2.pyi +55 -0
  70. otdf_python_proto/entityresolution/entity_resolution_pb2_connect.py +149 -0
  71. otdf_python_proto/entityresolution/v2/entity_resolution_pb2.py +55 -0
  72. otdf_python_proto/entityresolution/v2/entity_resolution_pb2.pyi +55 -0
  73. otdf_python_proto/entityresolution/v2/entity_resolution_pb2_connect.py +149 -0
  74. otdf_python_proto/kas/__init__.py +9 -0
  75. otdf_python_proto/kas/kas_pb2.py +103 -0
  76. otdf_python_proto/kas/kas_pb2.pyi +170 -0
  77. otdf_python_proto/kas/kas_pb2_connect.py +192 -0
  78. otdf_python_proto/legacy_grpc/__init__.py +1 -0
  79. otdf_python_proto/legacy_grpc/authorization/authorization_pb2_grpc.py +163 -0
  80. otdf_python_proto/legacy_grpc/authorization/v2/authorization_pb2_grpc.py +206 -0
  81. otdf_python_proto/legacy_grpc/common/common_pb2_grpc.py +4 -0
  82. otdf_python_proto/legacy_grpc/entity/entity_pb2_grpc.py +4 -0
  83. otdf_python_proto/legacy_grpc/entityresolution/entity_resolution_pb2_grpc.py +122 -0
  84. otdf_python_proto/legacy_grpc/entityresolution/v2/entity_resolution_pb2_grpc.py +120 -0
  85. otdf_python_proto/legacy_grpc/kas/kas_pb2_grpc.py +172 -0
  86. otdf_python_proto/legacy_grpc/logger/audit/test_pb2_grpc.py +4 -0
  87. otdf_python_proto/legacy_grpc/policy/actions/actions_pb2_grpc.py +249 -0
  88. otdf_python_proto/legacy_grpc/policy/attributes/attributes_pb2_grpc.py +873 -0
  89. otdf_python_proto/legacy_grpc/policy/kasregistry/key_access_server_registry_pb2_grpc.py +602 -0
  90. otdf_python_proto/legacy_grpc/policy/keymanagement/key_management_pb2_grpc.py +251 -0
  91. otdf_python_proto/legacy_grpc/policy/namespaces/namespaces_pb2_grpc.py +427 -0
  92. otdf_python_proto/legacy_grpc/policy/objects_pb2_grpc.py +4 -0
  93. otdf_python_proto/legacy_grpc/policy/registeredresources/registered_resources_pb2_grpc.py +524 -0
  94. otdf_python_proto/legacy_grpc/policy/resourcemapping/resource_mapping_pb2_grpc.py +516 -0
  95. otdf_python_proto/legacy_grpc/policy/selectors_pb2_grpc.py +4 -0
  96. otdf_python_proto/legacy_grpc/policy/subjectmapping/subject_mapping_pb2_grpc.py +551 -0
  97. otdf_python_proto/legacy_grpc/policy/unsafe/unsafe_pb2_grpc.py +485 -0
  98. otdf_python_proto/legacy_grpc/wellknownconfiguration/wellknown_configuration_pb2_grpc.py +77 -0
  99. otdf_python_proto/logger/__init__.py +1 -0
  100. otdf_python_proto/logger/audit/test_pb2.py +43 -0
  101. otdf_python_proto/logger/audit/test_pb2.pyi +45 -0
  102. otdf_python_proto/policy/__init__.py +1 -0
  103. otdf_python_proto/policy/actions/actions_pb2.py +75 -0
  104. otdf_python_proto/policy/actions/actions_pb2.pyi +87 -0
  105. otdf_python_proto/policy/actions/actions_pb2_connect.py +275 -0
  106. otdf_python_proto/policy/attributes/attributes_pb2.py +234 -0
  107. otdf_python_proto/policy/attributes/attributes_pb2.pyi +328 -0
  108. otdf_python_proto/policy/attributes/attributes_pb2_connect.py +863 -0
  109. otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2.py +266 -0
  110. otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2.pyi +450 -0
  111. otdf_python_proto/policy/kasregistry/key_access_server_registry_pb2_connect.py +611 -0
  112. otdf_python_proto/policy/keymanagement/key_management_pb2.py +79 -0
  113. otdf_python_proto/policy/keymanagement/key_management_pb2.pyi +87 -0
  114. otdf_python_proto/policy/keymanagement/key_management_pb2_connect.py +275 -0
  115. otdf_python_proto/policy/namespaces/namespaces_pb2.py +117 -0
  116. otdf_python_proto/policy/namespaces/namespaces_pb2.pyi +147 -0
  117. otdf_python_proto/policy/namespaces/namespaces_pb2_connect.py +443 -0
  118. otdf_python_proto/policy/objects_pb2.py +150 -0
  119. otdf_python_proto/policy/objects_pb2.pyi +464 -0
  120. otdf_python_proto/policy/registeredresources/registered_resources_pb2.py +139 -0
  121. otdf_python_proto/policy/registeredresources/registered_resources_pb2.pyi +196 -0
  122. otdf_python_proto/policy/registeredresources/registered_resources_pb2_connect.py +527 -0
  123. otdf_python_proto/policy/resourcemapping/resource_mapping_pb2.py +139 -0
  124. otdf_python_proto/policy/resourcemapping/resource_mapping_pb2.pyi +194 -0
  125. otdf_python_proto/policy/resourcemapping/resource_mapping_pb2_connect.py +527 -0
  126. otdf_python_proto/policy/selectors_pb2.py +57 -0
  127. otdf_python_proto/policy/selectors_pb2.pyi +90 -0
  128. otdf_python_proto/policy/subjectmapping/subject_mapping_pb2.py +127 -0
  129. otdf_python_proto/policy/subjectmapping/subject_mapping_pb2.pyi +189 -0
  130. otdf_python_proto/policy/subjectmapping/subject_mapping_pb2_connect.py +569 -0
  131. otdf_python_proto/policy/unsafe/unsafe_pb2.py +113 -0
  132. otdf_python_proto/policy/unsafe/unsafe_pb2.pyi +145 -0
  133. otdf_python_proto/policy/unsafe/unsafe_pb2_connect.py +485 -0
  134. otdf_python_proto/wellknownconfiguration/__init__.py +1 -0
  135. otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2.py +51 -0
  136. otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2.pyi +32 -0
  137. otdf_python_proto/wellknownconfiguration/wellknown_configuration_pb2_connect.py +107 -0
  138. otdf_python/_gotdf_python.cpython-312-darwin.so +0 -0
  139. otdf_python/build.py +0 -190
  140. otdf_python/go.py +0 -1478
  141. otdf_python/gotdf_python.py +0 -383
  142. otdf_python-0.1.10.dist-info/METADATA +0 -149
  143. otdf_python-0.1.10.dist-info/RECORD +0 -10
  144. otdf_python-0.1.10.dist-info/top_level.txt +0 -1
@@ -0,0 +1,213 @@
1
+ """
2
+ KASConnectRPCClient: Handles Connect RPC communication with the Key Access Service (KAS).
3
+ This class encapsulates all interactions with otdf_python_proto.
4
+ """
5
+
6
+ import logging
7
+
8
+ import urllib3
9
+ from otdf_python_proto.kas import kas_pb2
10
+ from otdf_python_proto.kas.kas_pb2_connect import AccessServiceClient
11
+
12
+ from otdf_python.auth_headers import AuthHeaders
13
+
14
+ from .sdk_exceptions import SDKException
15
+
16
+
17
+ class KASConnectRPCClient:
18
+ """
19
+ Handles Connect RPC communication with KAS service using otdf_python_proto.
20
+ """
21
+
22
+ def __init__(self, use_plaintext=False, verify_ssl=True):
23
+ """
24
+ Initialize the Connect RPC client.
25
+
26
+ Args:
27
+ use_plaintext: Whether to use plaintext (HTTP) connections
28
+ verify_ssl: Whether to verify SSL certificates
29
+ """
30
+ self.use_plaintext = use_plaintext
31
+ self.verify_ssl = verify_ssl
32
+
33
+ def _create_http_client(self):
34
+ """
35
+ Create HTTP client with SSL verification configuration.
36
+
37
+ Returns:
38
+ urllib3.PoolManager configured for SSL verification settings
39
+ """
40
+ if self.verify_ssl:
41
+ logging.info("Using SSL verification enabled HTTP client")
42
+ return urllib3.PoolManager()
43
+ else:
44
+ logging.info("Using SSL verification disabled HTTP client")
45
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
46
+ return urllib3.PoolManager(cert_reqs="CERT_NONE")
47
+
48
+ def _prepare_connect_rpc_url(self, kas_url):
49
+ """
50
+ Prepare the base URL for Connect RPC client.
51
+
52
+ Args:
53
+ kas_url: The normalized KAS URL
54
+
55
+ Returns:
56
+ Base URL for Connect RPC client (without /kas suffix)
57
+ """
58
+ connect_rpc_base_url = kas_url
59
+ # Remove /kas suffix, if present
60
+ connect_rpc_base_url = connect_rpc_base_url.removesuffix("/kas")
61
+ return connect_rpc_base_url
62
+
63
+ def _prepare_auth_headers(self, access_token):
64
+ """
65
+ Prepare authentication headers if access token is available.
66
+
67
+ Args:
68
+ access_token: Bearer token for authentication
69
+
70
+ Returns:
71
+ Dictionary with authentication headers or None
72
+ """
73
+ if access_token:
74
+ auth_headers = AuthHeaders(
75
+ auth_header=f"Bearer {access_token}",
76
+ dpop_header="", # Empty for now, ready for future DPoP support
77
+ )
78
+ return auth_headers.to_dict()
79
+ return None
80
+
81
+ def get_public_key(self, normalized_kas_url, kas_info, access_token=None):
82
+ """
83
+ Get KAS public key using Connect RPC.
84
+
85
+ Args:
86
+ normalized_kas_url: The normalized KAS URL
87
+ kas_info: KAS information object with algorithm
88
+ access_token: Optional access token for authentication
89
+
90
+ Returns:
91
+ Updated kas_info with public_key and kid
92
+ """
93
+ logging.info(
94
+ f"KAS Connect RPC client settings for public key retrieval: "
95
+ f"verify_ssl={self.verify_ssl}, use_plaintext={self.use_plaintext}, "
96
+ f"kas_url={kas_info.url}"
97
+ )
98
+
99
+ http_client = self._create_http_client()
100
+
101
+ try:
102
+ connect_rpc_base_url = self._prepare_connect_rpc_url(normalized_kas_url)
103
+
104
+ logging.info(
105
+ f"Creating Connect RPC client for base URL: {connect_rpc_base_url}, "
106
+ f"for public key retrieval"
107
+ )
108
+
109
+ # Create Connect RPC client with configured HTTP client using Connect protocol
110
+ # Note: gRPC protocol is not supported with urllib3, use default Connect protocol
111
+ client = AccessServiceClient(connect_rpc_base_url, http_client=http_client)
112
+
113
+ # Create public key request
114
+ algorithm = getattr(kas_info, "algorithm", "") or ""
115
+ request = (
116
+ kas_pb2.PublicKeyRequest(algorithm=algorithm)
117
+ if algorithm
118
+ else kas_pb2.PublicKeyRequest()
119
+ )
120
+
121
+ # Prepare headers with authentication if available
122
+ extra_headers = self._prepare_auth_headers(access_token)
123
+
124
+ # Make the public key call with authentication headers
125
+ response = client.public_key(request, extra_headers=extra_headers)
126
+
127
+ # Update kas_info with response
128
+ kas_info.public_key = response.public_key
129
+ kas_info.kid = response.kid
130
+
131
+ return kas_info
132
+
133
+ except Exception as e:
134
+ import traceback
135
+
136
+ error_details = traceback.format_exc()
137
+ logging.error(
138
+ f"Connect RPC public key request failed: {type(e).__name__}: {e}"
139
+ )
140
+ logging.error(f"Full traceback: {error_details}")
141
+ raise SDKException(f"Connect RPC public key request failed: {e}")
142
+
143
+ def unwrap_key(
144
+ self, normalized_kas_url, key_access, signed_token, access_token=None
145
+ ):
146
+ """
147
+ Unwrap a key using Connect RPC.
148
+
149
+ Args:
150
+ normalized_kas_url: The normalized KAS URL
151
+ key_access: Key access information
152
+ signed_token: Signed JWT token for the request
153
+ access_token: Optional access token for authentication
154
+
155
+ Returns:
156
+ Unwrapped key bytes from the response
157
+ """
158
+ logging.info(
159
+ f"KAS Connect RPC client settings for unwrap: "
160
+ f"verify_ssl={self.verify_ssl}, use_plaintext={self.use_plaintext}, "
161
+ f"kas_url={key_access.url}"
162
+ )
163
+
164
+ http_client = self._create_http_client()
165
+
166
+ try:
167
+ kas_service_url = self._prepare_connect_rpc_url(normalized_kas_url)
168
+
169
+ logging.info(
170
+ f"Creating Connect RPC client for base URL: {kas_service_url}, for unwrap"
171
+ )
172
+
173
+ # Note: gRPC protocol is not supported with urllib3, use default Connect protocol
174
+ client = AccessServiceClient(kas_service_url, http_client=http_client)
175
+
176
+ # Create rewrap request
177
+ request = kas_pb2.RewrapRequest(
178
+ signed_request_token=signed_token,
179
+ )
180
+
181
+ # Debug: Log the signed token details
182
+ logging.info(f"Connect RPC signed token: {signed_token}")
183
+
184
+ # Prepare headers with authentication if available
185
+ extra_headers = self._prepare_auth_headers(access_token)
186
+
187
+ # Make the rewrap call with authentication headers
188
+ response = client.rewrap(request, extra_headers=extra_headers)
189
+
190
+ # Extract the entity wrapped key from v2 response structure
191
+ # The v2 response has responses[] array with results[] for each policy
192
+ if response.responses and len(response.responses) > 0:
193
+ policy_result = response.responses[0] # First policy
194
+ if policy_result.results and len(policy_result.results) > 0:
195
+ kao_result = policy_result.results[0] # First KAO result
196
+ if kao_result.kas_wrapped_key:
197
+ entity_wrapped_key = kao_result.kas_wrapped_key
198
+ else:
199
+ raise SDKException(f"KAO result error: {kao_result.error}")
200
+ else:
201
+ raise SDKException("No KAO results in policy response")
202
+ else:
203
+ # Fallback to legacy entity_wrapped_key field for backward compatibility
204
+ entity_wrapped_key = response.entity_wrapped_key
205
+ if not entity_wrapped_key:
206
+ raise SDKException("No entity_wrapped_key in Connect RPC response")
207
+
208
+ logging.info("Connect RPC rewrap succeeded")
209
+ return entity_wrapped_key
210
+
211
+ except Exception as e:
212
+ logging.error(f"Connect RPC rewrap failed: {e}")
213
+ raise SDKException(f"Connect RPC rewrap failed: {e}")
@@ -0,0 +1,25 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class KASInfo:
6
+ """
7
+ Configuration for Key Access Server (KAS) information.
8
+ This class stores details about a Key Access Server including its URL,
9
+ public key, key ID, default status, and cryptographic algorithm.
10
+ """
11
+
12
+ url: str
13
+ public_key: str | None = None
14
+ kid: str | None = None
15
+ default: bool | None = None
16
+ algorithm: str | None = None
17
+
18
+ def clone(self):
19
+ """Creates a copy of this KASInfo object."""
20
+ from copy import copy
21
+
22
+ return copy(self)
23
+
24
+ def __str__(self):
25
+ return f"KASInfo(url={self.url}, kid={self.kid}, default={self.default}, algorithm={self.algorithm})"
@@ -0,0 +1,52 @@
1
+ """
2
+ KASKeyCache: In-memory cache for KAS (Key Access Service) public keys and info.
3
+ """
4
+
5
+ import threading
6
+ from typing import Any
7
+
8
+
9
+ class KASKeyCache:
10
+ def __init__(self):
11
+ self._cache = {}
12
+ self._lock = threading.Lock()
13
+
14
+ def get(self, url: str, algorithm: str | None = None) -> Any | None:
15
+ """
16
+ Gets a KASInfo object from the cache based on URL and algorithm.
17
+
18
+ Args:
19
+ url: The URL of the KAS
20
+ algorithm: Optional algorithm identifier
21
+
22
+ Returns:
23
+ The cached KASInfo object, or None if not found
24
+ """
25
+ cache_key = self._make_key(url, algorithm)
26
+ with self._lock:
27
+ return self._cache.get(cache_key)
28
+
29
+ def store(self, kas_info) -> None:
30
+ """
31
+ Stores a KASInfo object in the cache.
32
+
33
+ Args:
34
+ kas_info: The KASInfo object to store
35
+ """
36
+ cache_key = self._make_key(kas_info.url, getattr(kas_info, "algorithm", None))
37
+ with self._lock:
38
+ self._cache[cache_key] = kas_info
39
+
40
+ def set(self, key, value):
41
+ """Store a key-value pair in the cache."""
42
+ with self._lock:
43
+ self._cache[key] = value
44
+
45
+ def clear(self):
46
+ """Clears the cache"""
47
+ with self._lock:
48
+ self._cache.clear()
49
+
50
+ def _make_key(self, url: str, algorithm: str | None = None) -> str:
51
+ """Creates a cache key from URL and algorithm"""
52
+ return f"{url}:{algorithm or ''}"
@@ -0,0 +1,31 @@
1
+ from enum import Enum
2
+
3
+
4
+ class KeyType(Enum):
5
+ RSA2048Key = "rsa:2048"
6
+ EC256Key = "ec:secp256r1"
7
+ EC384Key = "ec:secp384r1"
8
+ EC521Key = "ec:secp521r1"
9
+
10
+ def __str__(self):
11
+ return self.value
12
+
13
+ def get_curve_name(self):
14
+ if self == KeyType.EC256Key:
15
+ return "secp256r1"
16
+ elif self == KeyType.EC384Key:
17
+ return "secp384r1"
18
+ elif self == KeyType.EC521Key:
19
+ return "secp521r1"
20
+ else:
21
+ raise ValueError(f"Unsupported key type: {self}")
22
+
23
+ @staticmethod
24
+ def from_string(key_type):
25
+ for t in KeyType:
26
+ if t.value.lower() == key_type.lower():
27
+ return t
28
+ raise ValueError(f"No enum constant for key type: {key_type}")
29
+
30
+ def is_ec(self):
31
+ return self != KeyType.RSA2048Key
@@ -0,0 +1,43 @@
1
+ """
2
+ Constants for session key types used in the KAS client.
3
+ This matches the Java SDK's KeyType enum pattern.
4
+ """
5
+
6
+ from enum import Enum, auto
7
+
8
+
9
+ class KeyType(Enum):
10
+ """
11
+ Enum for key types used in the KAS client.
12
+ """
13
+
14
+ RSA2048 = auto()
15
+ EC_P256 = auto()
16
+ EC_P384 = auto()
17
+ EC_P521 = auto()
18
+
19
+ @property
20
+ def is_ec(self):
21
+ """
22
+ Returns True if this key type is an EC key, False otherwise.
23
+ """
24
+ return self in [KeyType.EC_P256, KeyType.EC_P384, KeyType.EC_P521]
25
+
26
+ @property
27
+ def curve_name(self):
28
+ """
29
+ Returns the curve name for EC keys.
30
+ """
31
+ if self == KeyType.EC_P256:
32
+ return "P-256"
33
+ elif self == KeyType.EC_P384:
34
+ return "P-384"
35
+ elif self == KeyType.EC_P521:
36
+ return "P-521"
37
+ else:
38
+ return None
39
+
40
+
41
+ # Constants for backward compatibility with string literals
42
+ RSA_KEY_TYPE = KeyType.RSA2048
43
+ EC_KEY_TYPE = KeyType.EC_P256 # Default EC curve
@@ -0,0 +1,215 @@
1
+ import json
2
+ from dataclasses import asdict, dataclass, field
3
+ from typing import Any
4
+
5
+
6
+ @dataclass
7
+ class ManifestSegment:
8
+ hash: str
9
+ segmentSize: int
10
+ encryptedSegmentSize: int
11
+
12
+
13
+ @dataclass
14
+ class ManifestRootSignature:
15
+ alg: str
16
+ sig: str
17
+
18
+
19
+ @dataclass
20
+ class ManifestIntegrityInformation:
21
+ rootSignature: ManifestRootSignature
22
+ segmentHashAlg: str
23
+ segmentSizeDefault: int
24
+ encryptedSegmentSizeDefault: int
25
+ segments: list[ManifestSegment]
26
+
27
+
28
+ @dataclass
29
+ class ManifestPolicyBinding:
30
+ alg: str
31
+ hash: str
32
+
33
+
34
+ @dataclass
35
+ class ManifestKeyAccess:
36
+ type: str
37
+ url: str
38
+ protocol: str
39
+ wrappedKey: str
40
+ policyBinding: Any = None
41
+ encryptedMetadata: str | None = None
42
+ kid: str | None = None
43
+ sid: str | None = None
44
+ schemaVersion: str | None = None
45
+ ephemeralPublicKey: str | None = None
46
+
47
+
48
+ @dataclass
49
+ class ManifestMethod:
50
+ algorithm: str
51
+ iv: str
52
+ isStreamable: bool | None = None
53
+
54
+
55
+ @dataclass
56
+ class ManifestEncryptionInformation:
57
+ type: str
58
+ policy: str
59
+ keyAccess: list[ManifestKeyAccess]
60
+ method: ManifestMethod
61
+ integrityInformation: ManifestIntegrityInformation
62
+
63
+
64
+ @dataclass
65
+ class ManifestPayload:
66
+ type: str
67
+ url: str
68
+ protocol: str
69
+ mimeType: str
70
+ isEncrypted: bool
71
+
72
+
73
+ @dataclass
74
+ class ManifestBinding:
75
+ method: str
76
+ signature: str
77
+
78
+
79
+ @dataclass
80
+ class ManifestAssertion:
81
+ id: str
82
+ type: str
83
+ scope: str
84
+ appliesTo_state: str
85
+ statement: Any
86
+ binding: ManifestBinding | None = None
87
+
88
+
89
+ @dataclass
90
+ class Manifest:
91
+ schemaVersion: str | None = None
92
+ encryptionInformation: ManifestEncryptionInformation | None = None
93
+ payload: ManifestPayload | None = None
94
+ assertions: list[ManifestAssertion] = field(default_factory=list)
95
+
96
+ def _remove_none_values_and_empty_lists(self, obj):
97
+ """Recursively remove None values and empty lists from dictionaries and lists."""
98
+ if isinstance(obj, dict):
99
+ cleaned = {}
100
+ for k, v in obj.items():
101
+ if v is not None:
102
+ # For 'assertions' field, exclude if it's an empty list
103
+ if k == "assertions" and isinstance(v, list) and len(v) == 0:
104
+ continue
105
+ cleaned[k] = self._remove_none_values_and_empty_lists(v)
106
+ return cleaned
107
+ elif isinstance(obj, list):
108
+ return [
109
+ self._remove_none_values_and_empty_lists(item)
110
+ for item in obj
111
+ if item is not None
112
+ ]
113
+ else:
114
+ return obj
115
+
116
+ def to_json(self) -> str:
117
+ # Create manifest dict with fields ordered to match otdfctl expectations
118
+ # Order: encryptionInformation, payload, schemaVersion, assertions
119
+ manifest_dict = {}
120
+
121
+ # Add fields in the order expected by otdfctl
122
+ if self.encryptionInformation is not None:
123
+ manifest_dict["encryptionInformation"] = asdict(self.encryptionInformation)
124
+
125
+ if self.payload is not None:
126
+ manifest_dict["payload"] = asdict(self.payload)
127
+
128
+ if self.schemaVersion is not None:
129
+ manifest_dict["schemaVersion"] = self.schemaVersion
130
+
131
+ if self.assertions and len(self.assertions) > 0:
132
+ manifest_dict["assertions"] = [
133
+ asdict(assertion) for assertion in self.assertions
134
+ ]
135
+
136
+ cleaned_dict = self._remove_none_values_and_empty_lists(manifest_dict)
137
+ return json.dumps(cleaned_dict, default=str)
138
+
139
+ @staticmethod
140
+ def from_json(data: str) -> "Manifest":
141
+ d = json.loads(data)
142
+
143
+ # Recursively instantiate nested dataclasses
144
+ def _payload(p):
145
+ return ManifestPayload(**p) if p else None
146
+
147
+ def _segment(s):
148
+ return ManifestSegment(**s)
149
+
150
+ def _root_sig(rs):
151
+ return ManifestRootSignature(**rs)
152
+
153
+ def _integrity(i):
154
+ # Handle both snake_case and camelCase fields
155
+ # TODO: This can probably be simplified to only camelCase
156
+ return ManifestIntegrityInformation(
157
+ rootSignature=_root_sig(
158
+ i.get("rootSignature", i.get("root_signature"))
159
+ ),
160
+ segmentHashAlg=i.get("segmentHashAlg", i.get("segment_hash_alg")),
161
+ segmentSizeDefault=i.get(
162
+ "segmentSizeDefault", i.get("segment_size_default")
163
+ ),
164
+ encryptedSegmentSizeDefault=i.get(
165
+ "encryptedSegmentSizeDefault",
166
+ i.get("encrypted_segment_size_default"),
167
+ ),
168
+ segments=[_segment(s) for s in i["segments"]],
169
+ )
170
+
171
+ def _method(m):
172
+ return ManifestMethod(**m)
173
+
174
+ def _key_access(k):
175
+ return ManifestKeyAccess(**k)
176
+
177
+ def _enc_info(e):
178
+ # Handle both snake_case and camelCase fields
179
+ # TODO: This can probably be simplified to only camelCase
180
+ return ManifestEncryptionInformation(
181
+ type=e.get("type", e.get("key_access_type", "split")),
182
+ policy=e["policy"],
183
+ keyAccess=[
184
+ _key_access(k)
185
+ for k in e.get("keyAccess", e.get("key_access_obj", []))
186
+ ],
187
+ method=_method(e["method"]),
188
+ integrityInformation=_integrity(
189
+ e.get("integrityInformation", e.get("integrity_information"))
190
+ ),
191
+ )
192
+
193
+ def _binding(b):
194
+ return ManifestBinding(**b) if b else None
195
+
196
+ def _assertion(a):
197
+ return ManifestAssertion(
198
+ id=a["id"],
199
+ type=a["type"],
200
+ scope=a["scope"],
201
+ appliesTo_state=a.get("appliesTo_state", a.get("applies_to_state")),
202
+ statement=a["statement"],
203
+ binding=_binding(a.get("binding")),
204
+ )
205
+
206
+ return Manifest(
207
+ schemaVersion=d.get("schemaVersion", d.get("tdf_version")),
208
+ encryptionInformation=_enc_info(
209
+ d.get("encryptionInformation", d.get("encryption_information"))
210
+ )
211
+ if d.get("encryptionInformation") or d.get("encryption_information")
212
+ else None,
213
+ payload=_payload(d["payload"]) if d.get("payload") else None,
214
+ assertions=[_assertion(a) for a in d.get("assertions", [])],
215
+ )