otdf-python 0.1.10__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +85 -0
  7. otdf_python/asym_decryption.py +53 -0
  8. otdf_python/asym_encryption.py +75 -0
  9. otdf_python/auth_headers.py +21 -0
  10. otdf_python/autoconfigure_utils.py +113 -0
  11. otdf_python/cli.py +570 -0
  12. otdf_python/collection_store.py +41 -0
  13. otdf_python/collection_store_impl.py +22 -0
  14. otdf_python/config.py +69 -0
  15. otdf_python/connect_client.py +0 -0
  16. otdf_python/constants.py +1 -0
  17. otdf_python/crypto_utils.py +78 -0
  18. otdf_python/dpop.py +81 -0
  19. otdf_python/ecc_mode.py +32 -0
  20. otdf_python/eckeypair.py +75 -0
  21. otdf_python/header.py +143 -0
  22. otdf_python/invalid_zip_exception.py +8 -0
  23. otdf_python/kas_client.py +603 -0
  24. otdf_python/kas_connect_rpc_client.py +207 -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 +553 -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 +78 -0
  35. otdf_python/policy_object.py +22 -0
  36. otdf_python/policy_stub.py +2 -0
  37. otdf_python/resource_locator.py +44 -0
  38. otdf_python/sdk.py +528 -0
  39. otdf_python/sdk_builder.py +448 -0
  40. otdf_python/sdk_exceptions.py +16 -0
  41. otdf_python/symmetric_and_payload_config.py +30 -0
  42. otdf_python/tdf.py +479 -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.0.dist-info/METADATA +231 -0
  50. otdf_python-0.3.0.dist-info/RECORD +137 -0
  51. {otdf_python-0.1.10.dist-info → otdf_python-0.3.0.dist-info}/WHEEL +1 -2
  52. {otdf_python-0.1.10.dist-info → otdf_python-0.3.0.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,207 @@
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 .sdk_exceptions import SDKException
13
+
14
+
15
+ class KASConnectRPCClient:
16
+ """
17
+ Handles Connect RPC communication with KAS service using otdf_python_proto.
18
+ """
19
+
20
+ def __init__(self, use_plaintext=False, verify_ssl=True):
21
+ """
22
+ Initialize the Connect RPC client.
23
+
24
+ Args:
25
+ use_plaintext: Whether to use plaintext (HTTP) connections
26
+ verify_ssl: Whether to verify SSL certificates
27
+ """
28
+ self.use_plaintext = use_plaintext
29
+ self.verify_ssl = verify_ssl
30
+
31
+ def _create_http_client(self):
32
+ """
33
+ Create HTTP client with SSL verification configuration.
34
+
35
+ Returns:
36
+ urllib3.PoolManager configured for SSL verification settings
37
+ """
38
+ if self.verify_ssl:
39
+ logging.info("Using SSL verification enabled HTTP client")
40
+ return urllib3.PoolManager()
41
+ else:
42
+ logging.info("Using SSL verification disabled HTTP client")
43
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
44
+ return urllib3.PoolManager(cert_reqs="CERT_NONE")
45
+
46
+ def _prepare_connect_rpc_url(self, kas_url):
47
+ """
48
+ Prepare the base URL for Connect RPC client.
49
+
50
+ Args:
51
+ kas_url: The normalized KAS URL
52
+
53
+ Returns:
54
+ Base URL for Connect RPC client (without /kas suffix)
55
+ """
56
+ connect_rpc_base_url = kas_url
57
+ # Remove /kas suffix, if present
58
+ connect_rpc_base_url = connect_rpc_base_url.removesuffix("/kas")
59
+ return connect_rpc_base_url
60
+
61
+ def _prepare_auth_headers(self, access_token):
62
+ """
63
+ Prepare authentication headers if access token is available.
64
+
65
+ Args:
66
+ access_token: Bearer token for authentication
67
+
68
+ Returns:
69
+ Dictionary with authentication headers or None
70
+ """
71
+ if access_token:
72
+ return {"Authorization": f"Bearer {access_token}"}
73
+ return None
74
+
75
+ def get_public_key(self, normalized_kas_url, kas_info, access_token=None):
76
+ """
77
+ Get KAS public key using Connect RPC.
78
+
79
+ Args:
80
+ normalized_kas_url: The normalized KAS URL
81
+ kas_info: KAS information object with algorithm
82
+ access_token: Optional access token for authentication
83
+
84
+ Returns:
85
+ Updated kas_info with public_key and kid
86
+ """
87
+ logging.info(
88
+ f"KAS Connect RPC client settings for public key retrieval: "
89
+ f"verify_ssl={self.verify_ssl}, use_plaintext={self.use_plaintext}, "
90
+ f"kas_url={kas_info.url}"
91
+ )
92
+
93
+ http_client = self._create_http_client()
94
+
95
+ try:
96
+ connect_rpc_base_url = self._prepare_connect_rpc_url(normalized_kas_url)
97
+
98
+ logging.info(
99
+ f"Creating Connect RPC client for base URL: {connect_rpc_base_url}, "
100
+ f"for public key retrieval"
101
+ )
102
+
103
+ # Create Connect RPC client with configured HTTP client using Connect protocol
104
+ # Note: gRPC protocol is not supported with urllib3, use default Connect protocol
105
+ client = AccessServiceClient(connect_rpc_base_url, http_client=http_client)
106
+
107
+ # Create public key request
108
+ algorithm = getattr(kas_info, "algorithm", "") or ""
109
+ request = (
110
+ kas_pb2.PublicKeyRequest(algorithm=algorithm)
111
+ if algorithm
112
+ else kas_pb2.PublicKeyRequest()
113
+ )
114
+
115
+ # Prepare headers with authentication if available
116
+ extra_headers = self._prepare_auth_headers(access_token)
117
+
118
+ # Make the public key call with authentication headers
119
+ response = client.public_key(request, extra_headers=extra_headers)
120
+
121
+ # Update kas_info with response
122
+ kas_info.public_key = response.public_key
123
+ kas_info.kid = response.kid
124
+
125
+ return kas_info
126
+
127
+ except Exception as e:
128
+ import traceback
129
+
130
+ error_details = traceback.format_exc()
131
+ logging.error(
132
+ f"Connect RPC public key request failed: {type(e).__name__}: {e}"
133
+ )
134
+ logging.error(f"Full traceback: {error_details}")
135
+ raise SDKException(f"Connect RPC public key request failed: {e}")
136
+
137
+ def unwrap_key(
138
+ self, normalized_kas_url, key_access, signed_token, access_token=None
139
+ ):
140
+ """
141
+ Unwrap a key using Connect RPC.
142
+
143
+ Args:
144
+ normalized_kas_url: The normalized KAS URL
145
+ key_access: Key access information
146
+ signed_token: Signed JWT token for the request
147
+ access_token: Optional access token for authentication
148
+
149
+ Returns:
150
+ Unwrapped key bytes from the response
151
+ """
152
+ logging.info(
153
+ f"KAS Connect RPC client settings for unwrap: "
154
+ f"verify_ssl={self.verify_ssl}, use_plaintext={self.use_plaintext}, "
155
+ f"kas_url={key_access.url}"
156
+ )
157
+
158
+ http_client = self._create_http_client()
159
+
160
+ try:
161
+ kas_service_url = self._prepare_connect_rpc_url(normalized_kas_url)
162
+
163
+ logging.info(
164
+ f"Creating Connect RPC client for base URL: {kas_service_url}, for unwrap"
165
+ )
166
+
167
+ # Note: gRPC protocol is not supported with urllib3, use default Connect protocol
168
+ client = AccessServiceClient(kas_service_url, http_client=http_client)
169
+
170
+ # Create rewrap request
171
+ request = kas_pb2.RewrapRequest(
172
+ signed_request_token=signed_token,
173
+ )
174
+
175
+ # Debug: Log the signed token details
176
+ logging.info(f"Connect RPC signed token: {signed_token}")
177
+
178
+ # Prepare headers with authentication if available
179
+ extra_headers = self._prepare_auth_headers(access_token)
180
+
181
+ # Make the rewrap call with authentication headers
182
+ response = client.rewrap(request, extra_headers=extra_headers)
183
+
184
+ # Extract the entity wrapped key from v2 response structure
185
+ # The v2 response has responses[] array with results[] for each policy
186
+ if response.responses and len(response.responses) > 0:
187
+ policy_result = response.responses[0] # First policy
188
+ if policy_result.results and len(policy_result.results) > 0:
189
+ kao_result = policy_result.results[0] # First KAO result
190
+ if kao_result.kas_wrapped_key:
191
+ entity_wrapped_key = kao_result.kas_wrapped_key
192
+ else:
193
+ raise SDKException(f"KAO result error: {kao_result.error}")
194
+ else:
195
+ raise SDKException("No KAO results in policy response")
196
+ else:
197
+ # Fallback to legacy entity_wrapped_key field for backward compatibility
198
+ entity_wrapped_key = response.entity_wrapped_key
199
+ if not entity_wrapped_key:
200
+ raise SDKException("No entity_wrapped_key in Connect RPC response")
201
+
202
+ logging.info("Connect RPC rewrap succeeded")
203
+ return entity_wrapped_key
204
+
205
+ except Exception as e:
206
+ logging.error(f"Connect RPC rewrap failed: {e}")
207
+ 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
+ )