otdf-python 0.3.5__py3-none-any.whl → 0.4.1__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 (52) hide show
  1. otdf_python/__init__.py +1 -2
  2. otdf_python/__main__.py +1 -2
  3. otdf_python/address_normalizer.py +8 -10
  4. otdf_python/aesgcm.py +8 -0
  5. otdf_python/assertion_config.py +21 -0
  6. otdf_python/asym_crypto.py +18 -22
  7. otdf_python/auth_headers.py +7 -6
  8. otdf_python/autoconfigure_utils.py +22 -6
  9. otdf_python/cli.py +5 -5
  10. otdf_python/collection_store.py +13 -0
  11. otdf_python/collection_store_impl.py +5 -0
  12. otdf_python/config.py +13 -0
  13. otdf_python/connect_client.py +1 -0
  14. otdf_python/constants.py +2 -0
  15. otdf_python/crypto_utils.py +4 -0
  16. otdf_python/dpop.py +3 -5
  17. otdf_python/ecc_constants.py +12 -14
  18. otdf_python/ecc_mode.py +7 -2
  19. otdf_python/ecdh.py +24 -25
  20. otdf_python/eckeypair.py +5 -0
  21. otdf_python/header.py +5 -0
  22. otdf_python/invalid_zip_exception.py +6 -2
  23. otdf_python/kas_client.py +48 -55
  24. otdf_python/kas_connect_rpc_client.py +16 -19
  25. otdf_python/kas_info.py +4 -3
  26. otdf_python/kas_key_cache.py +10 -9
  27. otdf_python/key_type.py +4 -0
  28. otdf_python/key_type_constants.py +4 -11
  29. otdf_python/manifest.py +24 -0
  30. otdf_python/nanotdf.py +34 -24
  31. otdf_python/nanotdf_ecdsa_struct.py +5 -9
  32. otdf_python/nanotdf_type.py +12 -0
  33. otdf_python/policy_binding_serializer.py +6 -4
  34. otdf_python/policy_info.py +6 -0
  35. otdf_python/policy_object.py +8 -0
  36. otdf_python/policy_stub.py +2 -0
  37. otdf_python/resource_locator.py +22 -13
  38. otdf_python/sdk.py +49 -57
  39. otdf_python/sdk_builder.py +58 -41
  40. otdf_python/sdk_exceptions.py +11 -1
  41. otdf_python/symmetric_and_payload_config.py +6 -0
  42. otdf_python/tdf.py +47 -10
  43. otdf_python/tdf_reader.py +10 -13
  44. otdf_python/tdf_writer.py +5 -0
  45. otdf_python/token_source.py +4 -3
  46. otdf_python/version.py +5 -0
  47. otdf_python/zip_reader.py +10 -2
  48. otdf_python/zip_writer.py +11 -0
  49. {otdf_python-0.3.5.dist-info → otdf_python-0.4.1.dist-info}/METADATA +19 -2
  50. {otdf_python-0.3.5.dist-info → otdf_python-0.4.1.dist-info}/RECORD +52 -52
  51. {otdf_python-0.3.5.dist-info → otdf_python-0.4.1.dist-info}/WHEEL +1 -1
  52. {otdf_python-0.3.5.dist-info → otdf_python-0.4.1.dist-info}/licenses/LICENSE +0 -0
otdf_python/manifest.py CHANGED
@@ -1,3 +1,5 @@
1
+ """TDF manifest representation and serialization."""
2
+
1
3
  import json
2
4
  from dataclasses import asdict, dataclass, field
3
5
  from typing import Any
@@ -5,6 +7,8 @@ from typing import Any
5
7
 
6
8
  @dataclass
7
9
  class ManifestSegment:
10
+ """Encrypted segment information in TDF manifest."""
11
+
8
12
  hash: str
9
13
  segmentSize: int
10
14
  encryptedSegmentSize: int
@@ -12,12 +16,16 @@ class ManifestSegment:
12
16
 
13
17
  @dataclass
14
18
  class ManifestRootSignature:
19
+ """Root signature for manifest integrity."""
20
+
15
21
  alg: str
16
22
  sig: str
17
23
 
18
24
 
19
25
  @dataclass
20
26
  class ManifestIntegrityInformation:
27
+ """Manifest integrity information with signatures and hashes."""
28
+
21
29
  rootSignature: ManifestRootSignature
22
30
  segmentHashAlg: str
23
31
  segmentSizeDefault: int
@@ -27,12 +35,16 @@ class ManifestIntegrityInformation:
27
35
 
28
36
  @dataclass
29
37
  class ManifestPolicyBinding:
38
+ """Policy binding with algorithm and hash."""
39
+
30
40
  alg: str
31
41
  hash: str
32
42
 
33
43
 
34
44
  @dataclass
35
45
  class ManifestKeyAccess:
46
+ """Key access information in manifest."""
47
+
36
48
  type: str
37
49
  url: str
38
50
  protocol: str
@@ -47,6 +59,8 @@ class ManifestKeyAccess:
47
59
 
48
60
  @dataclass
49
61
  class ManifestMethod:
62
+ """Encryption method information in manifest."""
63
+
50
64
  algorithm: str
51
65
  iv: str
52
66
  isStreamable: bool | None = None
@@ -54,6 +68,8 @@ class ManifestMethod:
54
68
 
55
69
  @dataclass
56
70
  class ManifestEncryptionInformation:
71
+ """Encryption information in TDF manifest."""
72
+
57
73
  type: str
58
74
  policy: str
59
75
  keyAccess: list[ManifestKeyAccess]
@@ -63,6 +79,8 @@ class ManifestEncryptionInformation:
63
79
 
64
80
  @dataclass
65
81
  class ManifestPayload:
82
+ """Payload information in TDF manifest."""
83
+
66
84
  type: str
67
85
  url: str
68
86
  protocol: str
@@ -72,12 +90,16 @@ class ManifestPayload:
72
90
 
73
91
  @dataclass
74
92
  class ManifestBinding:
93
+ """Assertion binding information."""
94
+
75
95
  method: str
76
96
  signature: str
77
97
 
78
98
 
79
99
  @dataclass
80
100
  class ManifestAssertion:
101
+ """TDF assertion in manifest."""
102
+
81
103
  id: str
82
104
  type: str
83
105
  scope: str
@@ -88,6 +110,8 @@ class ManifestAssertion:
88
110
 
89
111
  @dataclass
90
112
  class Manifest:
113
+ """TDF manifest with encryption and payload information."""
114
+
91
115
  schemaVersion: str | None = None
92
116
  encryptionInformation: ManifestEncryptionInformation | None = None
93
117
  payload: ManifestPayload | None = None
otdf_python/nanotdf.py CHANGED
@@ -1,3 +1,5 @@
1
+ """NanoTDF reader and writer implementation."""
2
+
1
3
  import contextlib
2
4
  import hashlib
3
5
  import json
@@ -24,22 +26,32 @@ from .asym_crypto import AsymDecryption
24
26
 
25
27
 
26
28
  class NanoTDFException(SDKException):
29
+ """Base exception for NanoTDF operations."""
30
+
27
31
  pass
28
32
 
29
33
 
30
34
  class NanoTDFMaxSizeLimit(NanoTDFException):
35
+ """Exception for NanoTDF size limit exceeded."""
36
+
31
37
  pass
32
38
 
33
39
 
34
40
  class UnsupportedNanoTDFFeature(NanoTDFException):
41
+ """Exception for unsupported NanoTDF features."""
42
+
35
43
  pass
36
44
 
37
45
 
38
46
  class InvalidNanoTDFConfig(NanoTDFException):
47
+ """Exception for invalid NanoTDF configuration."""
48
+
39
49
  pass
40
50
 
41
51
 
42
52
  class NanoTDF:
53
+ """NanoTDF reader and writer for compact TDF format."""
54
+
43
55
  MAGIC_NUMBER_AND_VERSION = MAGIC_NUMBER_AND_VERSION
44
56
  K_MAX_TDF_SIZE = (16 * 1024 * 1024) - 3 - 32
45
57
  K_NANOTDF_GMAC_LENGTH = 8
@@ -48,6 +60,7 @@ class NanoTDF:
48
60
  K_EMPTY_IV = bytes([0x0] * 12)
49
61
 
50
62
  def __init__(self, services=None, collection_store: CollectionStore | None = None):
63
+ """Initialize NanoTDF reader/writer."""
51
64
  self.services = services
52
65
  self.collection_store = collection_store or NoOpCollectionStore()
53
66
 
@@ -59,7 +72,7 @@ class NanoTDF:
59
72
  return PolicyObject(uuid=policy_uuid, body=body)
60
73
 
61
74
  def _serialize_policy_object(self, obj):
62
- """Custom NanoTDF serializer to convert to compatible JSON format."""
75
+ """Serialize policy object to compatible JSON format."""
63
76
  from otdf_python.policy_object import AttributeObject, PolicyBody
64
77
 
65
78
  if isinstance(obj, PolicyBody):
@@ -82,8 +95,7 @@ class NanoTDF:
82
95
  return obj.__dict__
83
96
 
84
97
  def _prepare_payload(self, payload: bytes | BytesIO) -> bytes:
85
- """
86
- Convert BytesIO to bytes and validate payload size.
98
+ """Convert BytesIO to bytes and validate payload size.
87
99
 
88
100
  Args:
89
101
  payload: The payload data as bytes or BytesIO
@@ -93,6 +105,7 @@ class NanoTDF:
93
105
 
94
106
  Raises:
95
107
  NanoTDFMaxSizeLimit: If the payload exceeds the maximum size
108
+
96
109
  """
97
110
  if isinstance(payload, BytesIO):
98
111
  payload = payload.getvalue()
@@ -101,14 +114,14 @@ class NanoTDF:
101
114
  return payload
102
115
 
103
116
  def _prepare_policy_data(self, config: NanoTDFConfig) -> tuple[bytes, str]:
104
- """
105
- Prepare policy data from configuration.
117
+ """Prepare policy data from configuration.
106
118
 
107
119
  Args:
108
120
  config: NanoTDFConfig configuration
109
121
 
110
122
  Returns:
111
123
  tuple: (policy_body, policy_type)
124
+
112
125
  """
113
126
  attributes = config.attributes if config.attributes else []
114
127
  policy_object = self._create_policy_object(attributes)
@@ -150,8 +163,7 @@ class NanoTDF:
150
163
  config: NanoTDFConfig,
151
164
  ephemeral_public_key: bytes | None = None,
152
165
  ) -> bytes:
153
- """
154
- Create the NanoTDF header.
166
+ """Create the NanoTDF header.
155
167
 
156
168
  Args:
157
169
  policy_body: The policy body bytes
@@ -161,6 +173,7 @@ class NanoTDF:
161
173
 
162
174
  Returns:
163
175
  bytes: The header bytes
176
+
164
177
  """
165
178
  from otdf_python.header import Header # Local import to avoid circular import
166
179
 
@@ -228,8 +241,7 @@ class NanoTDF:
228
241
  return self.MAGIC_NUMBER_AND_VERSION + header_bytes
229
242
 
230
243
  def _is_ec_key(self, key_pem: str) -> bool:
231
- """
232
- Detect if a PEM key is an EC key (vs RSA).
244
+ """Detect if a PEM key is an EC key (vs RSA).
233
245
 
234
246
  Args:
235
247
  key_pem: PEM-formatted key string
@@ -239,6 +251,7 @@ class NanoTDF:
239
251
 
240
252
  Raises:
241
253
  SDKException: If key cannot be parsed
254
+
242
255
  """
243
256
  try:
244
257
  # Try to load as public key first
@@ -260,13 +273,12 @@ class NanoTDF:
260
273
  else:
261
274
  raise SDKException("Invalid PEM format - no BEGIN header found")
262
275
  except Exception as e:
263
- raise SDKException(f"Failed to detect key type: {e}")
276
+ raise SDKException(f"Failed to detect key type: {e}") from e
264
277
 
265
278
  def _derive_key_with_ecdh( # noqa: C901
266
279
  self, config: NanoTDFConfig
267
280
  ) -> tuple[bytes, bytes | None, bytes | None]:
268
- """
269
- Derive encryption key using ECDH if KAS public key is provided or can be fetched.
281
+ """Derive encryption key using ECDH if KAS public key is provided or can be fetched.
270
282
 
271
283
  This implements the NanoTDF spec's ECDH + HKDF key derivation:
272
284
  1. Generate ephemeral keypair
@@ -283,6 +295,7 @@ class NanoTDF:
283
295
  - derived_key: 32-byte AES-256 key for encrypting the payload
284
296
  - ephemeral_public_key_compressed: Compressed ephemeral public key to store in header (None for RSA)
285
297
  - kas_public_key: KAS public key PEM string (or None if not available)
298
+
286
299
  """
287
300
  import logging
288
301
 
@@ -384,8 +397,7 @@ class NanoTDF:
384
397
  return derived_key, ephemeral_public_key_compressed, kas_public_key
385
398
 
386
399
  def _encrypt_payload(self, payload: bytes, key: bytes) -> tuple[bytes, bytes]:
387
- """
388
- Encrypt the payload using AES-GCM.
400
+ """Encrypt the payload using AES-GCM.
389
401
 
390
402
  Args:
391
403
  payload: The payload to encrypt
@@ -393,6 +405,7 @@ class NanoTDF:
393
405
 
394
406
  Returns:
395
407
  tuple: (iv, ciphertext)
408
+
396
409
  """
397
410
  iv = secrets.token_bytes(self.K_NANOTDF_IV_SIZE)
398
411
  iv_padded = self.K_EMPTY_IV[: self.K_IV_PADDING] + iv
@@ -403,8 +416,7 @@ class NanoTDF:
403
416
  def create_nano_tdf(
404
417
  self, payload: bytes | BytesIO, output_stream: BinaryIO, config: NanoTDFConfig
405
418
  ) -> int:
406
- """
407
- Stream-based NanoTDF creation - writes encrypted payload to an output stream.
419
+ """Stream-based NanoTDF creation - writes encrypted payload to an output stream.
408
420
 
409
421
  For convenience method that returns bytes, use create_nanotdf() instead.
410
422
  Supports ECDH key derivation if KAS info with public key is provided in config.
@@ -422,8 +434,8 @@ class NanoTDF:
422
434
  UnsupportedNanoTDFFeature: If an unsupported feature is requested
423
435
  InvalidNanoTDFConfig: If the configuration is invalid
424
436
  SDKException: For other errors
425
- """
426
437
 
438
+ """
427
439
  # Process payload and validate size
428
440
  payload = self._prepare_payload(payload)
429
441
 
@@ -557,8 +569,7 @@ class NanoTDF:
557
569
  output_stream: BinaryIO,
558
570
  config: NanoTDFConfig,
559
571
  ) -> None:
560
- """
561
- Stream-based NanoTDF decryption - writes decrypted payload to an output stream.
572
+ """Stream-based NanoTDF decryption - writes decrypted payload to an output stream.
562
573
 
563
574
  For convenience method that returns bytes, use read_nanotdf() instead.
564
575
  Supports ECDH key derivation and KAS key unwrapping.
@@ -571,6 +582,7 @@ class NanoTDF:
571
582
  Raises:
572
583
  InvalidNanoTDFConfig: If the NanoTDF format is invalid or config is missing required info
573
584
  SDKException: For other errors
585
+
574
586
  """
575
587
  # Convert to bytes if BytesIO
576
588
  if isinstance(nano_tdf_data, BytesIO):
@@ -582,7 +594,7 @@ class NanoTDF:
582
594
  header_len = Header.peek_length(nano_tdf_data)
583
595
  header_obj = Header.from_bytes(nano_tdf_data[:header_len])
584
596
  except Exception as e:
585
- raise InvalidNanoTDFConfig(f"Failed to parse NanoTDF header: {e}")
597
+ raise InvalidNanoTDFConfig(f"Failed to parse NanoTDF header: {e}") from e
586
598
 
587
599
  # Read payload section per NanoTDF spec:
588
600
  # [3 bytes: length] [3 bytes: IV] [variable: ciphertext] [tag]
@@ -768,8 +780,7 @@ class NanoTDF:
768
780
  return key, config
769
781
 
770
782
  def create_nanotdf(self, data: bytes, config: dict | NanoTDFConfig) -> bytes:
771
- """
772
- Convenience method - creates a NanoTDF and returns the encrypted bytes.
783
+ """Create a NanoTDF and return the encrypted bytes.
773
784
 
774
785
  For stream-based version, use create_nano_tdf() instead.
775
786
  """
@@ -846,8 +857,7 @@ class NanoTDF:
846
857
  def read_nanotdf(
847
858
  self, nanotdf_bytes: bytes, config: dict | NanoTDFConfig | None = None
848
859
  ) -> bytes:
849
- """
850
- Convenience method - decrypts a NanoTDF and returns the plaintext bytes.
860
+ """Decrypt a NanoTDF and return the plaintext bytes.
851
861
 
852
862
  For stream-based version, use read_nano_tdf() instead.
853
863
  """
@@ -1,6 +1,4 @@
1
- """
2
- NanoTDF ECDSA Signature Structure.
3
- """
1
+ """NanoTDF ECDSA Signature Structure."""
4
2
 
5
3
  from dataclasses import dataclass, field
6
4
 
@@ -13,8 +11,7 @@ class IncorrectNanoTDFECDSASignatureSize(Exception):
13
11
 
14
12
  @dataclass
15
13
  class NanoTDFECDSAStruct:
16
- """
17
- Class to handle ECDSA signature structure for NanoTDF.
14
+ """Class to handle ECDSA signature structure for NanoTDF.
18
15
 
19
16
  This structure represents an ECDSA signature as required by the NanoTDF format.
20
17
  It consists of r and s values along with their lengths.
@@ -29,8 +26,7 @@ class NanoTDFECDSAStruct:
29
26
  def from_bytes(
30
27
  cls, ecdsa_signature_value: bytes, key_size: int
31
28
  ) -> "NanoTDFECDSAStruct":
32
- """
33
- Create a NanoTDFECDSAStruct from a byte array.
29
+ """Create a NanoTDFECDSAStruct from a byte array.
34
30
 
35
31
  Args:
36
32
  ecdsa_signature_value: The signature value as bytes
@@ -41,6 +37,7 @@ class NanoTDFECDSAStruct:
41
37
 
42
38
  Raises:
43
39
  IncorrectNanoTDFECDSASignatureSize: If the signature buffer size is invalid
40
+
44
41
  """
45
42
  if len(ecdsa_signature_value) != (2 * key_size) + 2:
46
43
  raise IncorrectNanoTDFECDSASignatureSize(
@@ -72,8 +69,7 @@ class NanoTDFECDSAStruct:
72
69
  return struct_obj
73
70
 
74
71
  def as_bytes(self) -> bytes:
75
- """
76
- Convert the signature structure to bytes.
72
+ """Convert the signature structure to bytes.
77
73
  Raises ValueError if r_value or s_value is None.
78
74
  """
79
75
  if self.r_value is None or self.s_value is None:
@@ -1,7 +1,11 @@
1
+ """NanoTDF type enumeration."""
2
+
1
3
  from enum import Enum
2
4
 
3
5
 
4
6
  class ECCurve(Enum):
7
+ """Elliptic curve enumeration for NanoTDF."""
8
+
5
9
  SECP256R1 = "secp256r1"
6
10
  SECP384R1 = "secp384r1"
7
11
  SECP521R1 = "secp384r1"
@@ -12,11 +16,15 @@ class ECCurve(Enum):
12
16
 
13
17
 
14
18
  class Protocol(Enum):
19
+ """Protocol enumeration for KAS communication."""
20
+
15
21
  HTTP = "HTTP"
16
22
  HTTPS = "HTTPS"
17
23
 
18
24
 
19
25
  class IdentifierType(Enum):
26
+ """Identifier type enumeration for NanoTDF."""
27
+
20
28
  NONE = 0
21
29
  TWO_BYTES = 2
22
30
  EIGHT_BYTES = 8
@@ -27,6 +35,8 @@ class IdentifierType(Enum):
27
35
 
28
36
 
29
37
  class PolicyType(Enum):
38
+ """Policy type enumeration for NanoTDF."""
39
+
30
40
  REMOTE_POLICY = 0
31
41
  EMBEDDED_POLICY_PLAIN_TEXT = 1
32
42
  EMBEDDED_POLICY_ENCRYPTED = 2
@@ -34,6 +44,8 @@ class PolicyType(Enum):
34
44
 
35
45
 
36
46
  class Cipher(Enum):
47
+ """Cipher enumeration for NanoTDF encryption."""
48
+
37
49
  AES_256_GCM_64_TAG = 0
38
50
  AES_256_GCM_96_TAG = 1
39
51
  AES_256_GCM_104_TAG = 2
@@ -1,21 +1,23 @@
1
+ """Policy binding serialization for HMAC calculation."""
2
+
1
3
  from typing import Any
2
4
 
3
5
 
4
6
  class PolicyBinding:
5
- """
6
- Represents a policy binding in the TDF manifest.
7
+ """Represents a policy binding in the TDF manifest.
8
+
7
9
  This is a placeholder implementation as the complete details of
8
10
  the PolicyBinding class aren't provided in the code snippets.
9
11
  """
10
12
 
11
13
  def __init__(self, **kwargs):
14
+ """Initialize policy binding from kwargs."""
12
15
  for key, value in kwargs.items():
13
16
  setattr(self, key, value)
14
17
 
15
18
 
16
19
  class PolicyBindingSerializer:
17
- """
18
- Handles serialization and deserialization of policy bindings.
20
+ """Handles serialization and deserialization of policy bindings.
19
21
  This class provides static methods to convert between JSON representations
20
22
  and PolicyBinding objects.
21
23
  """
@@ -1,9 +1,15 @@
1
+ """Policy information handling for NanoTDF."""
2
+
3
+
1
4
  class PolicyInfo:
5
+ """Policy information."""
6
+
2
7
  def __init__(
3
8
  self,
4
9
  policy_type: int = 0,
5
10
  body: bytes | None = None,
6
11
  ):
12
+ """Initialize policy information."""
7
13
  self.policy_type = policy_type
8
14
  self.body = body
9
15
 
@@ -1,8 +1,12 @@
1
+ """Policy object dataclasses for OpenTDF."""
2
+
1
3
  from dataclasses import dataclass
2
4
 
3
5
 
4
6
  @dataclass
5
7
  class AttributeObject:
8
+ """An attribute object."""
9
+
6
10
  attribute: str
7
11
  display_name: str | None = None
8
12
  is_default: bool = False
@@ -12,11 +16,15 @@ class AttributeObject:
12
16
 
13
17
  @dataclass
14
18
  class PolicyBody:
19
+ """A policy body."""
20
+
15
21
  data_attributes: list[AttributeObject]
16
22
  dissem: list[str]
17
23
 
18
24
 
19
25
  @dataclass
20
26
  class PolicyObject:
27
+ """A policy object."""
28
+
21
29
  uuid: str
22
30
  body: PolicyBody
@@ -1,2 +1,4 @@
1
+ """Policy UUID constants for OpenTDF."""
2
+
1
3
  # TODO: Replace this with a proper Policy UUID values
2
4
  NULL_POLICY_UUID: str = "00000000-0000-0000-0000-000000000000"
@@ -1,15 +1,19 @@
1
+ """NanoTDF resource locator handling."""
2
+
3
+
1
4
  class ResourceLocator:
2
- """
3
- NanoTDF Resource Locator per the spec:
4
- https://github.com/opentdf/spec/blob/main/schema/nanotdf/README.md
5
+ """Represent NanoTDF Resource Locator per specification.
6
+
7
+ See https://github.com/opentdf/spec/blob/main/schema/nanotdf/README.md
5
8
 
6
9
  Format:
7
- - Byte 0: Protocol Enum (bits 0-3) + Identifier Length (bits 4-7)
8
- - Protocol: 0x0=HTTP, 0x1=HTTPS, 0xF=Shared Resource Directory
9
- - Identifier: 0x0=None, 0x1=2 bytes, 0x2=8 bytes, 0x3=32 bytes
10
- - Byte 1: Body Length (1-255 bytes)
11
- - Bytes 2-N: Body (URL path)
12
- - Bytes N+1-M: Identifier (optional, 0/2/8/32 bytes)
10
+ - Byte 0: Protocol Enum (bits 0-3) + Identifier Length (bits 4-7)
11
+ - Protocol: 0x0=HTTP, 0x1=HTTPS, 0xF=Shared Resource Directory
12
+ - Identifier: 0x0=None, 0x1=2 bytes, 0x2=8 bytes, 0x3=32 bytes
13
+ - Byte 1: Body Length (1-255 bytes)
14
+ - Bytes 2-N: Body (URL path)
15
+ - Bytes N+1-M: Identifier (optional, 0/2/8/32 bytes)
16
+
13
17
  """
14
18
 
15
19
  # Protocol enum values
@@ -24,6 +28,13 @@ class ResourceLocator:
24
28
  IDENTIFIER_32_BYTES = 0x3
25
29
 
26
30
  def __init__(self, resource_url: str | None = None, identifier: str | None = None):
31
+ """Initialize resource locator.
32
+
33
+ Args:
34
+ resource_url: URL of the resource
35
+ identifier: Optional identifier for the resource
36
+
37
+ """
27
38
  self.resource_url = resource_url or ""
28
39
  self.identifier = identifier or ""
29
40
 
@@ -71,8 +82,7 @@ class ResourceLocator:
71
82
  raise ValueError(f"Identifier too long: {id_len} bytes (max 32)")
72
83
 
73
84
  def to_bytes(self):
74
- """
75
- Convert to NanoTDF Resource Locator format per spec.
85
+ """Convert to NanoTDF Resource Locator format per spec.
76
86
 
77
87
  Format:
78
88
  - Byte 0: Protocol Enum (bits 0-3) + Identifier Length (bits 4-7)
@@ -106,8 +116,7 @@ class ResourceLocator:
106
116
 
107
117
  @staticmethod
108
118
  def from_bytes_with_size(buffer: bytes): # noqa: C901
109
- """
110
- Parse NanoTDF Resource Locator from bytes per spec.
119
+ """Parse NanoTDF Resource Locator from bytes per spec.
111
120
 
112
121
  Format:
113
122
  - Byte 0: Protocol Enum (bits 0-3) + Identifier Length (bits 4-7)