Skip to content

feat(sdk): regenerate controlplane client with externalId support (ENG-3141)#171

Merged
drappier-charles merged 6 commits into
mainfrom
cdrappier/devin/externalid-python-sdk
Jun 16, 2026
Merged

feat(sdk): regenerate controlplane client with externalId support (ENG-3141)#171
drappier-charles merged 6 commits into
mainfrom
cdrappier/devin/externalid-python-sdk

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Regenerates the controlplane API client from the OpenAPI spec on branch cdrappier/devin/externalid-all-resources-step2 (controlplane PR #4456).

Key additions from the new spec:

  • external_id field on Metadata model (max 64 chars, alphanumeric + dash)
  • GET /sandboxes/by-external-id/{externalId} endpoint → get_sandbox_by_external_id
  • external_id query parameter on list_sandboxes

Depends on: controlplane PR #4456 being merged first.

No manual edits — all files are auto-generated via openapi-python-client.

Link to Devin session: https://app.devin.ai/sessions/18b29621f60f4e459f119aba6516ba6b
Requested by: @drappier-charles


Note

Regenerates the controlplane API client adding externalId support (new field on Metadata, new GET /sandboxes/by-external-id/{externalId} endpoint, cursor pagination for list endpoints). Also fixes the runtime breakage from the previous review by unwrapping paginated responses with .data in sandbox, drive, and volume wrappers. Adds integration tests for the externalId feature.

Written by Mendral for commit 3249294.

…G-3141)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@mendral-app

mendral-app Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

✅ Linked to Linear issue ENG-3141 — status already In Progress, assigned to Charles Drappier.

Note

Posted by Linear Issue Enforcer · Tag @mendral-app with feedback.

mendral-app[bot]

This comment was marked as outdated.

…olume wrappers

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Good catch — fixed in f5add38. The list() methods in sandbox, drive, and volume wrappers now unwrap .data from the paginated list response types (SandboxList, DriveList, VolumeList).

@mendral-app

mendral-app Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

🧪 Testing Guide

What this PR addresses

This PR regenerates the controlplane Python SDK client from an updated OpenAPI spec (from controlplane PR #4456). Key changes include:

  • external_id field added to the Metadata model (max 64 chars, alphanumeric + dash)
  • New endpoint: GET /sandboxes/by-external-id/{externalId} to retrieve the most recent non-terminated sandbox by external ID
  • New query parameter: external_id on list_sandboxes
  • Cursor-based pagination added to all list endpoints (agents, sandboxes, drives, functions, jobs, models, policies, volumes)
  • New list response wrappers (AgentList, SandboxList, etc.) with data and meta (pagination) fields
  • Several other new endpoints: get_policy_usages, get_egress_gateway_usage, list_job_execution_tasks

Steps to exercise the new behavior

Since this is auto-generated client code, focus on:

  1. Run integration tests (requires a running controlplane with the matching spec):
    pytest tests/integration/core/sandbox/test_external_id.py -v
  2. Verify the external_id field works on sandbox creation:
    • Create a sandbox with metadata.external_id set to a valid value (e.g., "my-ext-id-123")
    • Call GET /sandboxes/by-external-id/my-ext-id-123 and confirm the sandbox is returned
    • Call list_sandboxes with external_id=my-ext-id-123 query param and confirm filtering works
  3. Verify pagination on list endpoints:
    • Call any list endpoint (e.g., list_agents) with limit=1
    • Confirm the response includes meta with a cursor for the next page
    • Use the cursor to fetch the next page
  4. Verify backward compatibility of SDK wrappers:
    • Confirm that high-level wrapper classes (Sandbox, Drive, Volume) correctly unwrap .data from paginated responses so existing users of those wrappers are not broken

What to verify (expected behavior)

  • All existing tests pass (pytest) — no regressions from the regenerated client
  • New integration tests in tests/integration/core/sandbox/test_external_id.py pass when run against a controlplane with externalId support
  • Metadata model accepts external_id as an optional string field
  • get_sandbox_by_external_id endpoint returns the correct sandbox or appropriate error for unknown IDs
  • List endpoints return *List wrapper objects with .data (list of resources) and .meta (pagination info)
  • SDK wrapper classes (e.g., src/blaxel/core/sandbox.py, drive.py, volume.py) properly handle the new paginated response format by unwrapping .data
  • No breaking changes for consumers using the high-level SDK wrappers

Note: This PR depends on controlplane PR #4456 being merged first. Integration testing requires a controlplane instance running that branch or newer.

Note

Posted by PR Testing Guide · Tag @mendral-app with feedback.

drappier-charles and others added 2 commits June 16, 2026 23:06
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
mendral-app[bot]

This comment was marked as outdated.

drappier-charles and others added 2 commits June 16, 2026 23:18
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@drappier-charles drappier-charles marked this pull request as ready for review June 16, 2026 23:28
@drappier-charles drappier-charles merged commit 03d7d6f into main Jun 16, 2026
17 of 21 checks passed
@drappier-charles drappier-charles deleted the cdrappier/devin/externalid-python-sdk branch June 16, 2026 23:28

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

Open in Devin Review

Comment on lines +259 to +260
volumes = response.data if hasattr(response, "data") else response
return [cls(volume) for volume in volumes or []]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔴 VolumeInstance.list() wraps LiteVolume objects in VolumeInstance, causing AttributeError on property access

The list_volumes API now returns VolumeList whose .data field contains LiteVolume objects (src/blaxel/core/client/models/volume_list.py:27), but both VolumeInstance.__init__ and SyncVolumeInstance.__init__ expect full Volume objects (src/blaxel/core/volume/volume.py:143). LiteVolume uses LiteVolumeSpec (only has region and size) instead of VolumeSpec (has region, size, template, infrastructure_id), and LiteVolumeMetadata (only name, display_name, created_at, updated_at) instead of Metadata (has labels, workspace, url, etc.).

Accessing VolumeInstance.template on items from .list() will crash with AttributeError because LiteVolumeSpec has no template attribute (src/blaxel/core/volume/volume.py:176). Similarly, accessing .metadata.labels (used in test cleanup at tests/integration/core/conftest.py:47) will crash because LiteVolumeMetadata has no labels.

Prompt for agents
The root problem is that VolumeList.data contains LiteVolume objects but VolumeInstance and SyncVolumeInstance expect full Volume objects. LiteVolume is missing key attributes that VolumeInstance exposes (template, labels, etc.).

Possible approaches:
1. In VolumeInstance.list() and SyncVolumeInstance.list(), do NOT unwrap response.data directly. Instead, for each LiteVolume, call VolumeInstance.get(name) to fetch the full Volume. This is expensive but correct.
2. Change VolumeInstance to accept either Volume or LiteVolume, and make the .template property gracefully handle the missing attribute (e.g., return None or UNSET when the underlying object is a LiteVolume).
3. Change the generated VolumeList model to use Volume instead of LiteVolume (requires changing the OpenAPI spec and regenerating).

Option 2 is probably the best balance: update VolumeInstance.__init__ to accept Union[Volume, LiteVolume], and guard property accessors with hasattr checks or try/except. The same fix is needed in SyncVolumeInstance at line 414.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 413 to +415
response = list_volumes_sync(client=client)
return [cls(volume) for volume in response or []]
volumes = response.data if hasattr(response, "data") else response
return [cls(volume) for volume in volumes or []]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🔴 SyncVolumeInstance.list() has the same LiteVolume type mismatch

Same issue as the async VolumeInstance.list()SyncVolumeInstance.list() also wraps LiteVolume objects (from VolumeList.data) into SyncVolumeInstance which expects Volume. Accessing .template or .metadata.labels on returned instances will crash with AttributeError.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 63 to 65
if response.status_code == 200:
response_200 = []
_response_200 = response.json()
for response_200_item_data in _response_200:
response_200_item = Sandbox.from_dict(response_200_item_data)

response_200.append(response_200_item)
response_200 = SandboxList.from_dict(response.json())

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🚩 Backward compatibility concern: SandboxList.from_dict crashes on bare arrays from older API versions

The generated _parse_response at src/blaxel/core/client/api/compute/list_sandboxes.py:64 now calls SandboxList.from_dict(response.json()) for all 200 responses. The docstring states older API versions return a bare array, but SandboxList.from_dict (src/blaxel/core/client/models/sandbox_list.py:60-88) expects a dict. If response.json() returns a list [...], the call d = src_dict.copy() succeeds (lists have .copy()), but then d.pop("data", UNSET) will raise TypeError because list.pop() takes an integer index, not a string key. This same pattern affects all list endpoints (agents, drives, functions, jobs, models, policies, volumes). The wrapper code's hasattr(response, "data") fallback won't help because the crash happens inside the generated parser before the wrapper is reached. This means the SDK is incompatible with older API servers that return bare arrays.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

Devin is archived and cannot be woken up. Please unarchive Devin if you want to continue using it.

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.

1 participant