Skip to content

Commit 72047a7

Browse files
committed
Rename methods to upload files
It was agreed to use `upload_from_path(path)` and `upload_from_bytes(content, name)`. Also update protocols which raised, it seems a valid error.
1 parent b64a0f2 commit 72047a7

6 files changed

Lines changed: 53 additions & 39 deletions

File tree

changelog/ihs193.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Added support for FileObject nodes with file upload and download capabilities. New methods `upload_from_path(path)` and `upload_from_bytes(content, name)` allow setting file content before saving, while `download_file(dest)` enables downloading files to memory or streaming to disk for large files.

infrahub_sdk/ctl/branch.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@ def generate_proposed_change_tables(proposed_changes: list[CoreProposedChange])
119119
proposed_change_table.add_row("Name", pc.name.value)
120120
proposed_change_table.add_row("State", str(pc.state.value))
121121
proposed_change_table.add_row("Is draft", "Yes" if pc.is_draft.value else "No")
122-
proposed_change_table.add_row("Created by", pc.created_by.peer.name.value) # type: ignore[union-attr]
123-
proposed_change_table.add_row("Created at", format_timestamp(str(pc.created_by.updated_at)))
122+
proposed_change_table.add_row("Created by", pc.created_by.peer.name.value) # type: ignore[attr-defined]
123+
proposed_change_table.add_row("Created at", format_timestamp(str(pc.created_by.updated_at))) # type: ignore[attr-defined]
124124
proposed_change_table.add_row("Approvals", str(len(pc.approved_by.peers)))
125125
proposed_change_table.add_row("Rejections", str(len(pc.rejected_by.peers)))
126126

infrahub_sdk/node/node.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,8 @@ def is_file_object(self) -> bool:
225225
"""Check if this node inherits from CoreFileObject and supports file uploads."""
226226
return self._file_object_support
227227

228-
def select_file_for_upload(self, path: Path) -> None:
229-
"""Select a file from disk to be uploaded when saving this FileObject node.
228+
def upload_from_path(self, path: Path) -> None:
229+
"""Set a file from disk to be uploaded when saving this FileObject node.
230230
231231
The file will be streamed during upload, avoiding loading the entire file into memory.
232232
@@ -237,7 +237,7 @@ def select_file_for_upload(self, path: Path) -> None:
237237
FeatureNotSupportedError: If this node doesn't inherit from CoreFileObject.
238238
239239
Example:
240-
node.select_file_for_upload(path=Path("/path/to/large_file.pdf"))
240+
node.upload_from_path(path=Path("/path/to/large_file.pdf"))
241241
"""
242242
if not self._file_object_support:
243243
raise FeatureNotSupportedError(
@@ -246,8 +246,8 @@ def select_file_for_upload(self, path: Path) -> None:
246246
self._file_content = path
247247
self._file_name = path.name
248248

249-
def select_content_for_upload(self, content: bytes | BinaryIO, name: str) -> None:
250-
"""Select content to be uploaded when saving this FileObject node.
249+
def upload_from_bytes(self, content: bytes | BinaryIO, name: str) -> None:
250+
"""Set content to be uploaded when saving this FileObject node.
251251
252252
The content can be provided as bytes or a file-like object.
253253
Using BinaryIO is recommended for large content to stream during upload.
@@ -261,11 +261,11 @@ def select_content_for_upload(self, content: bytes | BinaryIO, name: str) -> Non
261261
262262
Examples:
263263
# Using bytes (for small files)
264-
node.select_content_for_upload(content=b"file content", name="example.txt")
264+
node.upload_from_bytes(content=b"file content", name="example.txt")
265265
266266
# Using file-like object (for large files)
267267
with open("/path/to/file.bin", "rb") as f:
268-
node.select_content_for_upload(content=f, name="file.bin")
268+
node.upload_from_bytes(content=f, name="file.bin")
269269
"""
270270
if not self._file_object_support:
271271
raise FeatureNotSupportedError(
@@ -1159,7 +1159,7 @@ async def create(
11591159
) -> None:
11601160
if self._file_object_support and self._file_content is None:
11611161
raise ValueError(
1162-
f"Cannot create {self._schema.kind} without file content. Use select_file_for_upload() or select_content_for_upload() to provide "
1162+
f"Cannot create {self._schema.kind} without file content. Use upload_from_path() or upload_from_bytes() to provide "
11631163
"file content before saving."
11641164
)
11651165

@@ -2049,7 +2049,7 @@ def create(
20492049
) -> None:
20502050
if self._file_object_support and self._file_content is None:
20512051
raise ValueError(
2052-
f"Cannot create {self._schema.kind} without file content. Use select_file_for_upload() or select_content_for_upload() to provide "
2052+
f"Cannot create {self._schema.kind} without file content. Use upload_from_path() or upload_from_bytes() to provide "
20532053
"file content before saving."
20542054
)
20552055

infrahub_sdk/protocols.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
StringOptional,
3030
)
3131

32-
# pylint: disable=too-many-ancestors
3332

3433
# ---------------------------------------------
3534
# ASYNC
@@ -108,6 +107,14 @@ class CoreCredential(CoreNode):
108107
description: StringOptional
109108

110109

110+
class CoreFileObject(CoreNode):
111+
file_name: String
112+
checksum: String
113+
file_size: Integer
114+
file_type: String
115+
storage_id: String
116+
117+
111118
class CoreGenericAccount(CoreNode):
112119
name: String
113120
password: HashedPassword
@@ -227,6 +234,7 @@ class CoreValidator(CoreNode):
227234
class CoreWebhook(CoreNode):
228235
name: String
229236
event_type: Enum
237+
active: Boolean
230238
branch_scope: Dropdown
231239
node_kind: StringOptional
232240
description: StringOptional
@@ -499,7 +507,6 @@ class CoreProposedChange(CoreTaskTarget):
499507
approved_by: RelationshipManager
500508
rejected_by: RelationshipManager
501509
reviewers: RelationshipManager
502-
created_by: RelatedNode
503510
comments: RelationshipManager
504511
threads: RelationshipManager
505512
validations: RelationshipManager
@@ -665,6 +672,14 @@ class CoreCredentialSync(CoreNodeSync):
665672
description: StringOptional
666673

667674

675+
class CoreFileObjectSync(CoreNodeSync):
676+
file_name: String
677+
checksum: String
678+
file_size: Integer
679+
file_type: String
680+
storage_id: String
681+
682+
668683
class CoreGenericAccountSync(CoreNodeSync):
669684
name: String
670685
password: HashedPassword
@@ -784,6 +799,7 @@ class CoreValidatorSync(CoreNodeSync):
784799
class CoreWebhookSync(CoreNodeSync):
785800
name: String
786801
event_type: Enum
802+
active: Boolean
787803
branch_scope: Dropdown
788804
node_kind: StringOptional
789805
description: StringOptional
@@ -1056,7 +1072,6 @@ class CoreProposedChangeSync(CoreTaskTargetSync):
10561072
approved_by: RelationshipManagerSync
10571073
rejected_by: RelationshipManagerSync
10581074
reviewers: RelationshipManagerSync
1059-
created_by: RelatedNodeSync
10601075
comments: RelationshipManagerSync
10611076
threads: RelationshipManagerSync
10621077
validations: RelationshipManagerSync

tests/unit/sdk/test_file_object.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ async def test_node_create_with_file_uses_multipart(
8989

9090
node.contract_start.value = "2024-01-01T00:00:00Z" # type: ignore[union-attr]
9191
node.contract_end.value = "2024-12-31T23:59:59Z" # type: ignore[union-attr]
92-
node.select_content_for_upload(content=FILE_CONTENT, name=FILE_NAME)
92+
node.upload_from_bytes(content=FILE_CONTENT, name=FILE_NAME)
9393

9494
if isinstance(node, InfrahubNode):
9595
await node.save()
@@ -121,7 +121,7 @@ async def test_node_update_with_file_uses_multipart(
121121
node._existing = True
122122
node.contract_start.value = "2024-01-01T00:00:00Z" # type: ignore[union-attr]
123123
node.contract_end.value = "2024-12-31T23:59:59Z" # type: ignore[union-attr]
124-
node.select_content_for_upload(content=FILE_CONTENT, name=FILE_NAME)
124+
node.upload_from_bytes(content=FILE_CONTENT, name=FILE_NAME)
125125

126126
if isinstance(node, InfrahubNode):
127127
await node.save()
@@ -173,7 +173,7 @@ async def test_node_save_clears_file_after_upload(
173173
node.contract_start.value = "2024-01-01T00:00:00Z" # type: ignore[union-attr]
174174
node.contract_end.value = "2024-12-31T23:59:59Z" # type: ignore[union-attr]
175175

176-
node.select_content_for_upload(content=FILE_CONTENT, name=FILE_NAME)
176+
node.upload_from_bytes(content=FILE_CONTENT, name=FILE_NAME)
177177
assert node._file_content is not None
178178
assert node._file_name is not None
179179

tests/unit/sdk/test_node.py

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3303,27 +3303,25 @@ async def test_node_is_file_object_false(
33033303

33043304

33053305
@pytest.mark.parametrize("client_type", client_types)
3306-
async def test_node_select_content_for_upload_with_bytes(
3306+
async def test_node_upload_from_bytes_with_bytes(
33073307
client_type: str, clients: BothClients, file_object_schema: NodeSchemaAPI
33083308
) -> None:
3309-
"""Test that select_content_for_upload works with bytes on FileObject nodes."""
3309+
"""Test that upload_from_bytes works with bytes on FileObject nodes."""
33103310
if client_type == "standard":
33113311
node = InfrahubNode(client=clients.standard, schema=file_object_schema, branch="main")
33123312
else:
33133313
node = InfrahubNodeSync(client=clients.sync, schema=file_object_schema, branch="main")
33143314

33153315
file_content = b"PDF content here"
3316-
node.select_content_for_upload(content=file_content, name="contract.pdf")
3316+
node.upload_from_bytes(content=file_content, name="contract.pdf")
33173317

33183318
assert node._file_content == file_content
33193319
assert node._file_name == "contract.pdf"
33203320

33213321

33223322
@pytest.mark.parametrize("client_type", client_types)
3323-
async def test_node_select_file_for_upload(
3324-
client_type: str, clients: BothClients, file_object_schema: NodeSchemaAPI
3325-
) -> None:
3326-
"""Test that select_file_for_upload works with a Path object."""
3323+
async def test_node_upload_from_path(client_type: str, clients: BothClients, file_object_schema: NodeSchemaAPI) -> None:
3324+
"""Test that upload_from_path works with a Path object."""
33273325
if client_type == "standard":
33283326
node = InfrahubNode(client=clients.standard, schema=file_object_schema, branch="main")
33293327
else:
@@ -3335,16 +3333,16 @@ async def test_node_select_file_for_upload(
33353333
tmp.flush()
33363334
tmp_path = Path(tmp.name)
33373335

3338-
node.select_file_for_upload(path=tmp_path)
3336+
node.upload_from_path(path=tmp_path)
33393337
assert node._file_content == tmp_path
33403338
assert node._file_name == tmp_path.name
33413339

33423340

33433341
@pytest.mark.parametrize("client_type", client_types)
3344-
async def test_node_select_content_for_upload_with_binary_io(
3342+
async def test_node_upload_from_bytes_with_binary_io(
33453343
client_type: str, clients: BothClients, file_object_schema: NodeSchemaAPI
33463344
) -> None:
3347-
"""Test that select_content_for_upload works with a BinaryIO object."""
3345+
"""Test that upload_from_bytes works with a BinaryIO object."""
33483346
if client_type == "standard":
33493347
node = InfrahubNode(client=clients.standard, schema=file_object_schema, branch="main")
33503348
else:
@@ -3353,38 +3351,38 @@ async def test_node_select_content_for_upload_with_binary_io(
33533351
file_content = b"Content from BinaryIO"
33543352
file_obj = BytesIO(file_content)
33553353

3356-
node.select_content_for_upload(content=file_obj, name="uploaded.pdf")
3354+
node.upload_from_bytes(content=file_obj, name="uploaded.pdf")
33573355

33583356
assert node._file_content == file_obj
33593357
assert node._file_name == "uploaded.pdf"
33603358

33613359

33623360
@pytest.mark.parametrize("client_type", client_types)
3363-
async def test_node_select_content_for_upload_on_non_file_object_raises(
3361+
async def test_node_upload_from_bytes_on_non_file_object_raises(
33643362
client_type: str, clients: BothClients, non_file_object_schema: NodeSchemaAPI
33653363
) -> None:
3366-
"""Test that select_content_for_upload raises FeatureNotSupportedError on non-FileObject nodes."""
3364+
"""Test that upload_from_bytes raises FeatureNotSupportedError on non-FileObject nodes."""
33673365
if client_type == "standard":
33683366
node = InfrahubNode(client=clients.standard, schema=non_file_object_schema, branch="main")
33693367
else:
33703368
node = InfrahubNodeSync(client=clients.sync, schema=non_file_object_schema, branch="main")
33713369

33723370
with pytest.raises(FeatureNotSupportedError, match=r"File upload is not supported"):
3373-
node.select_content_for_upload(content=b"some content", name="file.txt")
3371+
node.upload_from_bytes(content=b"some content", name="file.txt")
33743372

33753373

33763374
@pytest.mark.parametrize("client_type", client_types)
3377-
async def test_node_select_file_for_upload_on_non_file_object_raises(
3375+
async def test_node_upload_from_path_on_non_file_object_raises(
33783376
client_type: str, clients: BothClients, non_file_object_schema: NodeSchemaAPI
33793377
) -> None:
3380-
"""Test that select_file_for_upload raises FeatureNotSupportedError on non-FileObject nodes."""
3378+
"""Test that upload_from_path raises FeatureNotSupportedError on non-FileObject nodes."""
33813379
if client_type == "standard":
33823380
node = InfrahubNode(client=clients.standard, schema=non_file_object_schema, branch="main")
33833381
else:
33843382
node = InfrahubNodeSync(client=clients.sync, schema=non_file_object_schema, branch="main")
33853383

33863384
with pytest.raises(FeatureNotSupportedError, match=r"File upload is not supported"):
3387-
node.select_file_for_upload(path=Path("/some/file.txt"))
3385+
node.upload_from_path(path=Path("/some/file.txt"))
33883386

33893387

33903388
@pytest.mark.parametrize("client_type", client_types)
@@ -3398,7 +3396,7 @@ async def test_node_clear_file(client_type: str, clients: BothClients, file_obje
33983396
file_content = b"Test content"
33993397
file_name = "file.txt"
34003398

3401-
node.select_content_for_upload(content=file_content, name=file_name)
3399+
node.upload_from_bytes(content=file_content, name=file_name)
34023400
assert node._file_content == file_content
34033401
assert node._file_name == file_name
34043402

@@ -3419,7 +3417,7 @@ async def test_node_get_file_for_upload_bytes(
34193417

34203418
file_content = b"Test content"
34213419
file_name = "test.txt"
3422-
node.select_content_for_upload(content=file_content, name=file_name)
3420+
node.upload_from_bytes(content=file_content, name=file_name)
34233421

34243422
if isinstance(node, InfrahubNode):
34253423
prepared = await node._get_file_for_upload()
@@ -3448,7 +3446,7 @@ async def test_node_get_file_for_upload_path(
34483446
tmp.flush()
34493447
tmp_path = Path(tmp.name)
34503448

3451-
node.select_file_for_upload(path=tmp_path)
3449+
node.upload_from_path(path=tmp_path)
34523450

34533451
if isinstance(node, InfrahubNode):
34543452
prepared = await node._get_file_for_upload()
@@ -3475,7 +3473,7 @@ async def test_node_get_file_for_upload_binary_io(
34753473
file_content = b"Content from BinaryIO"
34763474
file_name = "test.bin"
34773475
file_obj_input = BytesIO(file_content)
3478-
node.select_content_for_upload(content=file_obj_input, name=file_name)
3476+
node.upload_from_bytes(content=file_obj_input, name=file_name)
34793477

34803478
if isinstance(node, InfrahubNode):
34813479
prepared = await node._get_file_for_upload()
@@ -3517,7 +3515,7 @@ async def test_node_generate_input_data_with_file(
35173515
else:
35183516
node = InfrahubNodeSync(client=clients.sync, schema=file_object_schema, branch="main")
35193517

3520-
node.select_content_for_upload(content=b"test content", name="test.txt")
3518+
node.upload_from_bytes(content=b"test content", name="test.txt")
35213519

35223520
input_data = node._generate_input_data()
35233521

0 commit comments

Comments
 (0)