From f45816a8084139dcec37202975d0283c57742a8e Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Wed, 20 May 2026 10:53:53 -0700 Subject: [PATCH 1/4] feat(tables): add TablesAPI.upload for the new /v1/tables/upload/ endpoint Adds a `client.tables.upload(table_name, file, with_headers=True)` helper that wraps the new backend public endpoint. Accepts str | Path | bytes | BinaryIO | FileUpload for the file argument, defaults to the configured organization, and returns a typed TableUploadResponse. Regenerated openapi-python-client artifacts under _generated/ for the new operation + request/response schemas. Co-Authored-By: Claude Opus 4.7 (1M context) --- openapi/openapi.yml | 72 +++++++ src/roe/__init__.py | 2 + src/roe/_generated/api/tables/__init__.py | 1 + src/roe/_generated/api/tables/upload_table.py | 202 ++++++++++++++++++ src/roe/_generated/models/__init__.py | 4 + .../_generated/models/table_upload_request.py | 173 +++++++++++++++ .../models/table_upload_response.py | 99 +++++++++ src/roe/api/__init__.py | 3 +- src/roe/api/tables.py | 122 +++++++++++ src/roe/client.py | 11 + 10 files changed, 688 insertions(+), 1 deletion(-) create mode 100644 src/roe/_generated/api/tables/__init__.py create mode 100644 src/roe/_generated/api/tables/upload_table.py create mode 100644 src/roe/_generated/models/table_upload_request.py create mode 100644 src/roe/_generated/models/table_upload_response.py create mode 100644 src/roe/api/tables.py diff --git a/openapi/openapi.yml b/openapi/openapi.yml index d30f8a1..ea74378 100644 --- a/openapi/openapi.yml +++ b/openapi/openapi.yml @@ -1930,6 +1930,35 @@ paths: schema: $ref: '#/components/schemas/PolicyVersion' description: '' + /v1/tables/upload/: + post: + operationId: upload_table + description: Create a Roe table in the authenticated organization from an uploaded + CSV file. Organization API keys are scoped to one organization; if organization_id + is supplied, it must match that organization. + summary: Upload a CSV as a Roe table + tags: + - tables + - sdk + requestBody: + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/TableUploadRequest' + required: true + responses: + '201': + content: + application/json: + schema: + $ref: '#/components/schemas/TableUploadResponse' + description: '' + '400': + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + description: Bad request /v1/users/current_user/: get: operationId: users_current_user_retrieve @@ -2734,6 +2763,49 @@ components: - display_name - email - id + TableUploadRequest: + type: object + description: Serializer for public CSV table uploads. + properties: + table_name: + type: string + minLength: 1 + description: Name of the Roe table to create from the uploaded CSV + maxLength: 128 + file: + type: string + format: binary + description: CSV file to upload + with_headers: + type: boolean + default: true + description: Whether the first row of the CSV contains column headers + organization_id: + type: + - string + - 'null' + format: uuid + description: Optional organization ID. Organization API keys are already + scoped to one organization; if supplied, this must match that organization. + required: + - file + - table_name + TableUploadResponse: + type: object + description: Response payload for a public CSV table upload. + properties: + table_name: + type: string + description: Created Roe table name + organization_id: + type: string + format: uuid + description: Organization that owns the table + summary: + description: ClickHouse import summary for the uploaded file + required: + - organization_id + - table_name UpdatePolicy: type: object description: Serializer for updating policy metadata (name, description) diff --git a/src/roe/__init__.py b/src/roe/__init__.py index 0dea65b..73db699 100644 --- a/src/roe/__init__.py +++ b/src/roe/__init__.py @@ -35,6 +35,7 @@ ) from roe.models import FileUpload from roe.models.job import Job, JobBatch, JobStatus +from roe.api.tables import TablesAPI try: __version__ = version("roe-ai") @@ -51,6 +52,7 @@ "JobStatus", # File upload helper "FileUpload", + "TablesAPI", # Exceptions "RoeAPIException", "AuthenticationError", diff --git a/src/roe/_generated/api/tables/__init__.py b/src/roe/_generated/api/tables/__init__.py new file mode 100644 index 0000000..c9921b5 --- /dev/null +++ b/src/roe/_generated/api/tables/__init__.py @@ -0,0 +1 @@ +""" Contains endpoint functions for accessing the API """ diff --git a/src/roe/_generated/api/tables/upload_table.py b/src/roe/_generated/api/tables/upload_table.py new file mode 100644 index 0000000..f265427 --- /dev/null +++ b/src/roe/_generated/api/tables/upload_table.py @@ -0,0 +1,202 @@ +from http import HTTPStatus +from typing import Any, cast +from urllib.parse import quote + +import httpx + +from ...client import AuthenticatedClient, Client +from ...types import Response, UNSET +from ... import errors + +from ...models.error_response import ErrorResponse +from ...models.table_upload_request import TableUploadRequest +from ...models.table_upload_response import TableUploadResponse +from typing import cast + + + +def _get_kwargs( + *, + body: TableUploadRequest, + +) -> dict[str, Any]: + headers: dict[str, Any] = {} + + + + + + + _kwargs: dict[str, Any] = { + "method": "post", + "url": "/v1/tables/upload/", + } + + _kwargs["files"] = body.to_multipart() + + + + _kwargs["headers"] = headers + return _kwargs + + + +def _parse_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> ErrorResponse | TableUploadResponse | None: + if response.status_code == 201: + response_201 = TableUploadResponse.from_dict(response.json()) + + + + return response_201 + + if response.status_code == 400: + response_400 = ErrorResponse.from_dict(response.json()) + + + + return response_400 + + if client.raise_on_unexpected_status: + raise errors.UnexpectedStatus(response.status_code, response.content) + else: + return None + + +def _build_response(*, client: AuthenticatedClient | Client, response: httpx.Response) -> Response[ErrorResponse | TableUploadResponse]: + return Response( + status_code=HTTPStatus(response.status_code), + content=response.content, + headers=response.headers, + parsed=_parse_response(client=client, response=response), + ) + + +def sync_detailed( + *, + client: AuthenticatedClient | Client, + body: TableUploadRequest, + +) -> Response[ErrorResponse | TableUploadResponse]: + """ Upload a CSV as a Roe table + + Create a Roe table in the authenticated organization from an uploaded CSV file. Organization API + keys are scoped to one organization; if organization_id is supplied, it must match that + organization. + + Args: + body (TableUploadRequest): Serializer for public CSV table uploads. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | TableUploadResponse] + """ + + + kwargs = _get_kwargs( + body=body, + + ) + + response = client.get_httpx_client().request( + **kwargs, + ) + + return _build_response(client=client, response=response) + +def sync( + *, + client: AuthenticatedClient | Client, + body: TableUploadRequest, + +) -> ErrorResponse | TableUploadResponse | None: + """ Upload a CSV as a Roe table + + Create a Roe table in the authenticated organization from an uploaded CSV file. Organization API + keys are scoped to one organization; if organization_id is supplied, it must match that + organization. + + Args: + body (TableUploadRequest): Serializer for public CSV table uploads. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | TableUploadResponse + """ + + + return sync_detailed( + client=client, +body=body, + + ).parsed + +async def asyncio_detailed( + *, + client: AuthenticatedClient | Client, + body: TableUploadRequest, + +) -> Response[ErrorResponse | TableUploadResponse]: + """ Upload a CSV as a Roe table + + Create a Roe table in the authenticated organization from an uploaded CSV file. Organization API + keys are scoped to one organization; if organization_id is supplied, it must match that + organization. + + Args: + body (TableUploadRequest): Serializer for public CSV table uploads. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + Response[ErrorResponse | TableUploadResponse] + """ + + + kwargs = _get_kwargs( + body=body, + + ) + + response = await client.get_async_httpx_client().request( + **kwargs + ) + + return _build_response(client=client, response=response) + +async def asyncio( + *, + client: AuthenticatedClient | Client, + body: TableUploadRequest, + +) -> ErrorResponse | TableUploadResponse | None: + """ Upload a CSV as a Roe table + + Create a Roe table in the authenticated organization from an uploaded CSV file. Organization API + keys are scoped to one organization; if organization_id is supplied, it must match that + organization. + + Args: + body (TableUploadRequest): Serializer for public CSV table uploads. + + Raises: + errors.UnexpectedStatus: If the server returns an undocumented status code and Client.raise_on_unexpected_status is True. + httpx.TimeoutException: If the request takes longer than Client.timeout. + + Returns: + ErrorResponse | TableUploadResponse + """ + + + return (await asyncio_detailed( + client=client, +body=body, + + )).parsed diff --git a/src/roe/_generated/models/__init__.py b/src/roe/_generated/models/__init__.py index 7572c7c..3824c71 100644 --- a/src/roe/_generated/models/__init__.py +++ b/src/roe/_generated/models/__init__.py @@ -32,6 +32,8 @@ from .policy import Policy from .policy_version import PolicyVersion from .policy_version_created_by import PolicyVersionCreatedBy +from .table_upload_request import TableUploadRequest +from .table_upload_response import TableUploadResponse from .update_policy import UpdatePolicy from .update_policy_request import UpdatePolicyRequest from .user_info import UserInfo @@ -69,6 +71,8 @@ "Policy", "PolicyVersion", "PolicyVersionCreatedBy", + "TableUploadRequest", + "TableUploadResponse", "UpdatePolicy", "UpdatePolicyRequest", "UserInfo", diff --git a/src/roe/_generated/models/table_upload_request.py b/src/roe/_generated/models/table_upload_request.py new file mode 100644 index 0000000..7c715ee --- /dev/null +++ b/src/roe/_generated/models/table_upload_request.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field +import json +from .. import types + +from ..types import UNSET, Unset + +from ..types import File, FileTypes +from ..types import UNSET, Unset +from io import BytesIO +from typing import cast +from uuid import UUID + + + + + + +T = TypeVar("T", bound="TableUploadRequest") + + + +@_attrs_define +class TableUploadRequest: + """ Serializer for public CSV table uploads. + + Attributes: + table_name (str): Name of the Roe table to create from the uploaded CSV + file (File): CSV file to upload + with_headers (bool | Unset): Whether the first row of the CSV contains column headers Default: True. + organization_id (None | Unset | UUID): Optional organization ID. Organization API keys are already scoped to one + organization; if supplied, this must match that organization. + """ + + table_name: str + file: File + with_headers: bool | Unset = True + organization_id: None | Unset | UUID = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + table_name = self.table_name + + file = self.file.to_tuple() + + + with_headers = self.with_headers + + organization_id: None | str | Unset + if isinstance(self.organization_id, Unset): + organization_id = UNSET + elif isinstance(self.organization_id, UUID): + organization_id = str(self.organization_id) + else: + organization_id = self.organization_id + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "table_name": table_name, + "file": file, + }) + if with_headers is not UNSET: + field_dict["with_headers"] = with_headers + if organization_id is not UNSET: + field_dict["organization_id"] = organization_id + + return field_dict + + + def to_multipart(self) -> types.RequestFiles: + files: types.RequestFiles = [] + + files.append(("table_name", (None, str(self.table_name).encode(), "text/plain"))) + + + + files.append(("file", self.file.to_tuple())) + + + + if not isinstance(self.with_headers, Unset): + files.append(("with_headers", (None, str(self.with_headers).encode(), "text/plain"))) + + + + if not isinstance(self.organization_id, Unset): + if isinstance(self.organization_id, UUID): + + files.append(("organization_id", (None, str(self.organization_id), "text/plain"))) + else: + files.append(("organization_id", (None, str(self.organization_id).encode(), "text/plain"))) + + + + for prop_name, prop in self.additional_properties.items(): + files.append((prop_name, (None, str(prop).encode(), "text/plain"))) + + + + return files + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + table_name = d.pop("table_name") + + file = File( + payload = BytesIO(d.pop("file")) + ) + + + + + with_headers = d.pop("with_headers", UNSET) + + def _parse_organization_id(data: object) -> None | Unset | UUID: + if data is None: + return data + if isinstance(data, Unset): + return data + try: + if not isinstance(data, str): + raise TypeError() + organization_id_type_0 = UUID(data) + + + + return organization_id_type_0 + except (TypeError, ValueError, AttributeError, KeyError): + pass + return cast(None | Unset | UUID, data) + + organization_id = _parse_organization_id(d.pop("organization_id", UNSET)) + + + table_upload_request = cls( + table_name=table_name, + file=file, + with_headers=with_headers, + organization_id=organization_id, + ) + + + table_upload_request.additional_properties = d + return table_upload_request + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/roe/_generated/models/table_upload_response.py b/src/roe/_generated/models/table_upload_response.py new file mode 100644 index 0000000..ed098ec --- /dev/null +++ b/src/roe/_generated/models/table_upload_response.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, TypeVar, BinaryIO, TextIO, TYPE_CHECKING, Generator + +from attrs import define as _attrs_define +from attrs import field as _attrs_field + +from ..types import UNSET, Unset + +from ..types import UNSET, Unset +from uuid import UUID + + + + + + +T = TypeVar("T", bound="TableUploadResponse") + + + +@_attrs_define +class TableUploadResponse: + """ Response payload for a public CSV table upload. + + Attributes: + table_name (str): Created Roe table name + organization_id (UUID): Organization that owns the table + summary (Any | Unset): ClickHouse import summary for the uploaded file + """ + + table_name: str + organization_id: UUID + summary: Any | Unset = UNSET + additional_properties: dict[str, Any] = _attrs_field(init=False, factory=dict) + + + + + + def to_dict(self) -> dict[str, Any]: + table_name = self.table_name + + organization_id = str(self.organization_id) + + summary = self.summary + + + field_dict: dict[str, Any] = {} + field_dict.update(self.additional_properties) + field_dict.update({ + "table_name": table_name, + "organization_id": organization_id, + }) + if summary is not UNSET: + field_dict["summary"] = summary + + return field_dict + + + + @classmethod + def from_dict(cls: type[T], src_dict: Mapping[str, Any]) -> T: + d = dict(src_dict) + table_name = d.pop("table_name") + + organization_id = UUID(d.pop("organization_id")) + + + + + summary = d.pop("summary", UNSET) + + table_upload_response = cls( + table_name=table_name, + organization_id=organization_id, + summary=summary, + ) + + + table_upload_response.additional_properties = d + return table_upload_response + + @property + def additional_keys(self) -> list[str]: + return list(self.additional_properties.keys()) + + def __getitem__(self, key: str) -> Any: + return self.additional_properties[key] + + def __setitem__(self, key: str, value: Any) -> None: + self.additional_properties[key] = value + + def __delitem__(self, key: str) -> None: + del self.additional_properties[key] + + def __contains__(self, key: str) -> bool: + return key in self.additional_properties diff --git a/src/roe/api/__init__.py b/src/roe/api/__init__.py index 39f5d6e..cdfc4b7 100644 --- a/src/roe/api/__init__.py +++ b/src/roe/api/__init__.py @@ -2,5 +2,6 @@ from roe.api.agents import AgentsAPI from roe.api.policies import PoliciesAPI +from roe.api.tables import TablesAPI -__all__ = ["AgentsAPI", "PoliciesAPI"] +__all__ = ["AgentsAPI", "PoliciesAPI", "TablesAPI"] diff --git a/src/roe/api/tables.py b/src/roe/api/tables.py new file mode 100644 index 0000000..85ab816 --- /dev/null +++ b/src/roe/api/tables.py @@ -0,0 +1,122 @@ +"""Tables API — helpers for uploading Roe tables.""" + +from __future__ import annotations + +from io import BytesIO +import mimetypes +from pathlib import Path +from typing import BinaryIO +from uuid import UUID + +from roe._generated.api.tables import upload_table +from roe._generated.client import AuthenticatedClient +from roe._generated.models.table_upload_request import TableUploadRequest +from roe._generated.models.table_upload_response import TableUploadResponse +from roe._generated.types import File +from roe.config import RoeConfig +from roe.exceptions import translate_response +from roe.models import FileUpload + + +class TablesAPI: + """API for uploading CSV files into Roe tables.""" + + def __init__(self, config: RoeConfig, raw_client: AuthenticatedClient): + self.config = config + self._raw = raw_client + + def upload( + self, + *, + table_name: str, + file: str | Path | bytes | BinaryIO | FileUpload, + with_headers: bool = True, + organization_id: str | UUID | None = None, + filename: str | None = None, + mime_type: str | None = None, + ) -> TableUploadResponse: + """Upload a CSV file and create a Roe table. + + Args: + table_name: Name of the table to create. + file: CSV file path, bytes, binary file object, or ``FileUpload``. + with_headers: Whether the first CSV row contains column headers. + organization_id: Optional override; defaults to the client's configured org. + filename: Filename to use for bytes/file objects. + mime_type: MIME type override. Defaults to ``text/csv`` for ``.csv`` names. + """ + + upload_file, close_after = self._as_generated_file(file, filename, mime_type) + try: + body = TableUploadRequest( + table_name=table_name, + file=upload_file, + with_headers=with_headers, + organization_id=UUID(str(organization_id or self.config.organization_id)), + ) + resp = upload_table.sync_detailed(client=self._raw, body=body) + translate_response(resp) + return resp.parsed # type: ignore[return-value] + finally: + if close_after: + upload_file.payload.close() + + @staticmethod + def _as_generated_file( + file: str | Path | bytes | BinaryIO | FileUpload, + filename: str | None, + mime_type: str | None, + ) -> tuple[File, bool]: + if isinstance(file, FileUpload): + payload = file.open() + effective_filename = filename or file.effective_filename + effective_mime_type = mime_type or file.effective_mime_type + return ( + File( + payload=payload, + file_name=effective_filename, + mime_type=effective_mime_type, + ), + file.path is not None, + ) + + if isinstance(file, (str, Path)): + path = Path(file) + payload = path.open("rb") + effective_filename = filename or path.name + return ( + File( + payload=payload, + file_name=effective_filename, + mime_type=_mime_type(effective_filename, mime_type), + ), + True, + ) + + if isinstance(file, bytes): + effective_filename = filename or "upload.csv" + return ( + File( + payload=BytesIO(file), + file_name=effective_filename, + mime_type=_mime_type(effective_filename, mime_type), + ), + True, + ) + + effective_filename = filename or Path(getattr(file, "name", "upload.csv")).name + return ( + File( + payload=file, + file_name=effective_filename, + mime_type=_mime_type(effective_filename, mime_type), + ), + False, + ) + + +def _mime_type(filename: str, override: str | None) -> str: + if override: + return override + guessed, _ = mimetypes.guess_type(filename) + return guessed or "text/csv" diff --git a/src/roe/client.py b/src/roe/client.py index 66aaef5..6c02111 100644 --- a/src/roe/client.py +++ b/src/roe/client.py @@ -5,6 +5,7 @@ from roe._generated.client import AuthenticatedClient as RawClient from roe.api.agents import AgentsAPI from roe.api.policies import PoliciesAPI +from roe.api.tables import TablesAPI from roe.api.users import UsersAPI from roe.auth import RoeAuth from roe.config import RoeConfig @@ -93,6 +94,7 @@ def __init__( # Create API instances. All APIs delegate to the generated raw client. self._agents = AgentsAPI(self.config, self._raw) self._policies = PoliciesAPI(self.config, self._raw) + self._tables = TablesAPI(self.config, self._raw) self._users = UsersAPI(self.config, self._raw) @property @@ -158,6 +160,15 @@ def users(self) -> UsersAPI: """ return self._users + @property + def tables(self) -> TablesAPI: + """Access the tables API. + + Returns: + TablesAPI instance for uploading CSV files into Roe tables. + """ + return self._tables + @property def raw(self) -> RawClient: """Access the generated raw client.""" From f658386052e5d44b625ed974508b42745f5059b8 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Wed, 20 May 2026 11:14:19 -0700 Subject: [PATCH 2/4] fix(generated): skip organization_id form-field when value is None MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Greptile P1 on roe-ai/roe-python#40: `to_multipart()` serialized an explicit None to the literal bytes `b"None"`, which the backend rejects as an invalid UUID. The hand-written `TablesAPI.upload` wrapper never passes None at runtime (it coerces to the configured org id), but anyone calling the generated layer directly would hit it. Manual patch to the generated file — kept narrow so the next openapi-python-client regen produces a clean diff. The duplicate / unused imports Greptile also flagged in the same generated files (P2) are left alone for the same reason; they'll be cleaned up when codegen is re-run upstream. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/roe/_generated/models/table_upload_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/roe/_generated/models/table_upload_request.py b/src/roe/_generated/models/table_upload_request.py index 7c715ee..3b155ba 100644 --- a/src/roe/_generated/models/table_upload_request.py +++ b/src/roe/_generated/models/table_upload_request.py @@ -94,7 +94,7 @@ def to_multipart(self) -> types.RequestFiles: - if not isinstance(self.organization_id, Unset): + if not isinstance(self.organization_id, Unset) and self.organization_id is not None: if isinstance(self.organization_id, UUID): files.append(("organization_id", (None, str(self.organization_id), "text/plain"))) From 2b07dd79755a87008fb21811f2baf48e17396704 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Wed, 20 May 2026 11:18:03 -0700 Subject: [PATCH 3/4] fix(tables): pass UNSET (not None) when no organization_id is configured Greptile P1 on roe-ai/roe-python#40: TableUploadRequest's multipart serializer turns an explicit `organization_id=None` into the literal bytes `b"None"`, which the backend rejects as an invalid UUID. The first attempt patched the generated multipart serializer directly, but `check-codegen-drift` (correctly) blocks divergence between checked-in generated code and what openapi-python-client produces. Move the fix to the hand-written wrapper instead: coerce to UUID when a value is available, otherwise pass UNSET so the generated form-field serializer skips the field cleanly. Re-validated against the live dev stack with the canonical CSV. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/roe/api/tables.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/roe/api/tables.py b/src/roe/api/tables.py index 85ab816..885405e 100644 --- a/src/roe/api/tables.py +++ b/src/roe/api/tables.py @@ -12,7 +12,7 @@ from roe._generated.client import AuthenticatedClient from roe._generated.models.table_upload_request import TableUploadRequest from roe._generated.models.table_upload_response import TableUploadResponse -from roe._generated.types import File +from roe._generated.types import UNSET, File, Unset from roe.config import RoeConfig from roe.exceptions import translate_response from roe.models import FileUpload @@ -46,13 +46,21 @@ def upload( mime_type: MIME type override. Defaults to ``text/csv`` for ``.csv`` names. """ + # Coerce to UUID once; pass UNSET (not None) when no org id is + # available so the generated multipart serializer omits the form + # field cleanly. Sending a literal "None" would hit the backend + # UUID validator and return 400. + resolved_org: UUID | Unset + candidate = organization_id or self.config.organization_id + resolved_org = UUID(str(candidate)) if candidate else UNSET + upload_file, close_after = self._as_generated_file(file, filename, mime_type) try: body = TableUploadRequest( table_name=table_name, file=upload_file, with_headers=with_headers, - organization_id=UUID(str(organization_id or self.config.organization_id)), + organization_id=resolved_org, ) resp = upload_table.sync_detailed(client=self._raw, body=body) translate_response(resp) From 3764aed940292fe1a088c10a3774722f56f65a1e Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Wed, 20 May 2026 11:18:39 -0700 Subject: [PATCH 4/4] =?UTF-8?q?revert(generated):=20undo=20manual=20genera?= =?UTF-8?q?ted-file=20patch=20=E2=80=94=20fix=20moved=20to=20wrapper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The earlier `fix(generated)` commit patched openapi-python-client output to handle organization_id=None. That diverged from what the codegen would produce and tripped `check-codegen-drift`. The wrapper now passes UNSET instead of None (previous commit), so the generated layer no longer needs to special-case None — restore it to the canonical openapi-python-client output. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/roe/_generated/models/table_upload_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/roe/_generated/models/table_upload_request.py b/src/roe/_generated/models/table_upload_request.py index 3b155ba..7c715ee 100644 --- a/src/roe/_generated/models/table_upload_request.py +++ b/src/roe/_generated/models/table_upload_request.py @@ -94,7 +94,7 @@ def to_multipart(self) -> types.RequestFiles: - if not isinstance(self.organization_id, Unset) and self.organization_id is not None: + if not isinstance(self.organization_id, Unset): if isinstance(self.organization_id, UUID): files.append(("organization_id", (None, str(self.organization_id), "text/plain")))