Skip to content

IHS-193 Add support for file upload/download for CoreFileObject#792

Open
gmazoyer wants to merge 19 commits intoinfrahub-developfrom
gma-20260202-ihs193
Open

IHS-193 Add support for file upload/download for CoreFileObject#792
gmazoyer wants to merge 19 commits intoinfrahub-developfrom
gma-20260202-ihs193

Conversation

@gmazoyer
Copy link
Contributor

@gmazoyer gmazoyer commented Feb 2, 2026

This change adds support to create, update and retrieve nodes which schemas implement CoreFileObject.

The proposed user exposed API follows these key points:

  • node.upload_from_path(path) to select a file from disk for upload (streams during upload)
  • node.upload_from_bytes(content, name) to set content for upload (supports bytes or BinaryIO)
  • node.download_file(dest) to download a file to memory or stream to disk

Being able to stream a file to disk or from a disk is important in order to support large files and to avoid them being loaded completely into memory (which would be problematic for +1GB files in general).

The choice of using upload_from_path and upload_from_bytes is to prevent a collision with a potential attribute or relationship called file in the schema. That is also the reason why the file GraphQL parameter is outside the data one instead of inside it.

Here we introduce a couple of components to try to make our code SOLID (it's not much for now, but it's honest work):

  • FileHandler / FileHandlerSync dedicated classes for file I/O operations
  • MultipartBuilder GraphQL Multipart Request Spec payload building

It sure won't make our code SOLID but it won't add to the burden for now.

So given the user who loaded a schema, using our SDK will look like:

Upload a file when creating a node

from pathlib import Path
from infrahub_sdk import InfrahubClientSync

client = InfrahubClientSync()

contract = client.create(
    kind="NetworkCircuitContract", contract_start="2026-01-01", contract_end="2026-12-31"
)

# Option 1: Select file from disk (will be streamed during upload)
contract.upload_from_path(path=Path("/tmp/contract.pdf"))

# Option 2: Upload from memory (for small files or dynamically generated content)
contract.upload_from_bytes(content=b"file content", name="contract.pdf")

# Save as usual
contract.save()

Download a file from a node

from pathlib import Path

contract = client.get(kind="NetworkCircuitContract", id="abc123")

# Download to memory (suitable for small files)
content = contract.download_file()

# Or stream directly to disk (suitable for large files)
bytes_written = contract.download_file(dest=Path("/tmp/downloaded.pdf"))

Summary by CodeRabbit

  • New Features

    • Add file upload support via GraphQL multipart mutations and node-level attach/clear file APIs.
    • Add file download support for file-like nodes (return in-memory or stream to disk for large files).
    • Add efficient streaming for large server payloads and configurable proxy/network handling.
  • Tests

    • Add comprehensive unit tests covering multipart uploads, file preparation, downloads/streaming, and node file workflows.

@coderabbitai
Copy link

coderabbitai bot commented Feb 2, 2026

Warning

Rate limit exceeded

@gmazoyer has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 2 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

Adds file upload/download support across async and sync SDK paths. Introduces MultipartBuilder and multipart payload helpers, FileHandler/FileHandlerSync and PreparedFile, ProxyConfig/ProxyConfigSync types, multipart POST/request and streaming GET helpers, and client methods execute_graphql_with_file. Node classes gain file state, upload_from_path/upload_from_bytes/clear_file, and download_file with validation. GraphQL variable types and constants updated to support Upload, and unit tests were added.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: adding file upload/download support for CoreFileObject nodes, which aligns with the comprehensive changeset that introduces file handling capabilities throughout the SDK.
Docstring Coverage ✅ Passed Docstring coverage is 82.09% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Feb 2, 2026

Deploying infrahub-sdk-python with  Cloudflare Pages  Cloudflare Pages

Latest commit: 72047a7
Status: ✅  Deploy successful!
Preview URL: https://fec4665a.infrahub-sdk-python.pages.dev
Branch Preview URL: https://gma-20260202-ihs193.infrahub-sdk-python.pages.dev

View logs

@codecov
Copy link

codecov bot commented Feb 2, 2026

Codecov Report

❌ Patch coverage is 83.46253% with 64 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
infrahub_sdk/client.py 75.00% 22 Missing and 8 partials ⚠️
infrahub_sdk/file_handler.py 81.69% 20 Missing and 6 partials ⚠️
infrahub_sdk/node/node.py 90.36% 4 Missing and 4 partials ⚠️
@@                 Coverage Diff                  @@
##           infrahub-develop     #792      +/-   ##
====================================================
+ Coverage             80.36%   80.44%   +0.08%     
====================================================
  Files                   115      117       +2     
  Lines                  9865    10225     +360     
  Branches               1504     1547      +43     
====================================================
+ Hits                   7928     8226     +298     
- Misses                 1415     1463      +48     
- Partials                522      536      +14     
Flag Coverage Δ
integration-tests 40.22% <9.04%> (-1.20%) ⬇️
python-3.10 51.86% <59.94%> (+0.47%) ⬆️
python-3.11 51.86% <59.94%> (+0.49%) ⬆️
python-3.12 51.86% <59.94%> (+0.47%) ⬆️
python-3.13 51.86% <59.94%> (+0.47%) ⬆️
python-3.14 53.57% <63.04%> (+0.53%) ⬆️
python-filler-3.12 24.00% <22.48%> (-0.07%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
infrahub_sdk/ctl/branch.py 81.13% <100.00%> (ø)
infrahub_sdk/graphql/__init__.py 100.00% <100.00%> (ø)
infrahub_sdk/graphql/constants.py 100.00% <100.00%> (ø)
infrahub_sdk/graphql/multipart.py 100.00% <100.00%> (ø)
infrahub_sdk/graphql/renderers.py 92.94% <100.00%> (+0.08%) ⬆️
infrahub_sdk/node/constants.py 100.00% <100.00%> (ø)
infrahub_sdk/protocols.py 100.00% <100.00%> (ø)
infrahub_sdk/node/node.py 85.37% <90.36%> (+0.02%) ⬆️
infrahub_sdk/file_handler.py 81.69% <81.69%> (ø)
infrahub_sdk/client.py 72.62% <75.00%> (+0.62%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gmazoyer gmazoyer marked this pull request as ready for review February 4, 2026 12:35
@gmazoyer gmazoyer requested a review from a team as a code owner February 4, 2026 12:35
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@infrahub_sdk/file_handler.py`:
- Around line 98-118: The handler handle_error_response should guard against
non-JSON responses by wrapping each exc.response.json() call in a try/except
(catching json.JSONDecodeError or ValueError) and falling back to
exc.response.text; for 401/403, if json parsing fails treat the body text as a
single message (or empty) when building the AuthenticationError message, and for
404 use the fallback text as the detail when constructing NodeNotFoundError;
preserve the original HTTPStatusError as the cause (raise ... from exc).

In `@infrahub_sdk/node/node.py`:
- Around line 1160-1163: The error raised when self._file_object_support is true
and self._file_content is None references the wrong helper method name; update
the ValueError text in the block that checks self._file_object_support /
self._file_content (near usage of self._schema.kind) to instruct callers to use
the correct methods select_file_for_upload() or select_content_for_upload()
instead of set_file().
- Around line 2049-2052: The error message inside the check for
self._file_object_support and self._file_content should reference the actual
synchronous setter method name instead of the non-existent set_file(); update
the message to instruct callers to use the real sync setter (e.g.,
set_file_content()) — modify the ValueError string that currently mentions
set_file() so it references set_file_content() (or the actual sync method used
in this class) and keep the rest of the message intact; target the block that
checks _file_object_support and _file_content and update the message formatting
around _schema.kind accordingly.
🧹 Nitpick comments (1)
infrahub_sdk/client.py (1)

997-1035: Avoid mutating caller-provided variables in file-upload helpers.

Both async and sync paths mutate the incoming dict. Prefer a new dict to avoid side effects.

Suggested refactor
-        variables = variables or {}
-        variables["file"] = None
+        variables = {**(variables or {}), "file": None}
@@
-        variables = variables or {}
-        variables["file"] = None
+        variables = {**(variables or {}), "file": None}

Also applies to: 2055-2093

@wvandeun
Copy link
Contributor

wvandeun commented Feb 5, 2026

I would prefer the upload_from_path and upload_from_content names. Maybe even upload_from_bytes for the latter?

@gmazoyer gmazoyer force-pushed the gma-20260202-ihs193 branch 2 times, most recently from e9e0f9e to 1b304a6 Compare February 5, 2026 16:28
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@infrahub_sdk/protocols.py`:
- Around line 110-115: The CoreFileObject class (and the other affected
generated blocks referenced at lines 234-237, 675-680, 799-802) were edited
manually but this file is generated; revert any hand edits to these definitions
and re-run the generation task or update the source schema so the generator
emits the correct definitions (e.g., restore CoreFileObject and the other
generated classes/fields to match the generator output), do not make direct
changes in infrahub_sdk/protocols.py, and commit the regenerated file instead.
🧹 Nitpick comments (1)
infrahub_sdk/node/node.py (1)

224-288: Reuse _validate_file_object_support to avoid duplicated checks.
upload_from_path and upload_from_bytes repeat the same FeatureNotSupportedError logic; reusing the helper keeps error messaging consistent and centralized.

♻️ Suggested refactor
-        if not self._file_object_support:
-            raise FeatureNotSupportedError(
-                f"File upload is not supported for {self._schema.kind}. Only nodes inheriting from CoreFileObject support file uploads."
-            )
+        self._validate_file_object_support(
+            message=(
+                f"File upload is not supported for {self._schema.kind}. "
+                "Only nodes inheriting from CoreFileObject support file uploads."
+            )
+        )
@@
-        if not self._file_object_support:
-            raise FeatureNotSupportedError(
-                f"File upload is not supported for {self._schema.kind}. Only nodes inheriting from CoreFileObject support file uploads."
-            )
+        self._validate_file_object_support(
+            message=(
+                f"File upload is not supported for {self._schema.kind}. "
+                "Only nodes inheriting from CoreFileObject support file uploads."
+            )
+        )

Also applies to: 509-512

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.
@gmazoyer gmazoyer force-pushed the gma-20260202-ihs193 branch from 1b304a6 to 72047a7 Compare February 5, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants