From 7350d76cf3fd92c1fe38a5f12822d8799e3cd0ac Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Wed, 17 Jun 2026 14:35:14 -0700 Subject: [PATCH 1/6] feat: add complete public SDK coverage --- README.md | 28 +- SDK_EXAMPLES.md | 266 +++++-------------- openapi/wrappers.yml | 409 ++++++++++++++++++++++++++++- scripts/generate_wrappers.py | 98 ++++++- src/roe/api/_generated_registry.py | 4 + src/roe/api/connections.py | 200 ++++++++++++++ src/roe/api/connectors.py | 44 ++++ src/roe/api/tables.py | 94 ++++++- 8 files changed, 904 insertions(+), 239 deletions(-) create mode 100644 src/roe/api/connections.py create mode 100644 src/roe/api/connectors.py diff --git a/README.md b/README.md index 1e05a1c..275bb0d 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ A Python SDK for the [Roe](https://www.roe-ai.com/) API. > use cases. -> **v1.0.0** — The SDK delegates to OpenAPI-generated types and transports -> (`roe._generated`); ergonomic wrappers on `client.agents` and -> `client.policies` remain. Noteworthy API and behavioral changes compared -> to earlier releases are listed in **[CHANGELOG.md](CHANGELOG.md)**. +> **v1.0.0** - The SDK delegates transport and type details to the generated +> OpenAPI client behind stable public methods such as `client.agents` and +> `client.policies`. Noteworthy API and behavioral changes compared to earlier +> releases are listed in **[CHANGELOG.md](CHANGELOG.md)**. ## Installation @@ -110,26 +110,6 @@ result carries `result["status"] == JobStatus.FAILURE` and `result["error_message"]`. Transport / HTTP errors hit the typed hierarchy above. -## Raw API Access - -When the ergonomic wrappers don't expose an endpoint you need, the generated -client is available as `client.raw` and the operation modules live under -`roe._generated.api..`. Submodule names follow the -upstream OpenAPI tags + `operationId`s and may shift across releases, so the -portable form uses `client.raw.get_httpx_client()` to send a request through -the same auth-configured `httpx.Client`: - -```python -from roe import RoeClient - -client = RoeClient(api_key="your-api-key", organization_id="your-org-uuid") -response = client.raw.get_httpx_client().get("/v1/users/current_user/") -print(response.status_code) -``` - -For typed request/response models, call the generated operation module -directly — see `roe/_generated/api/` for the current surface. - ## SDK Operation Groups diff --git a/SDK_EXAMPLES.md b/SDK_EXAMPLES.md index 02799d8..4d8656f 100644 --- a/SDK_EXAMPLES.md +++ b/SDK_EXAMPLES.md @@ -281,22 +281,12 @@ from roe import RoeClient client = RoeClient() -agent_id = "agent_id" # required path - -response = client.raw.get_httpx_client().request( - "PUT", - f"/v1/agents/{agent_id}/", - params={ - "organization_id": "00000000-0000-0000-0000-000000000000", # optional - }, - json={ - "name": "name", # optional - "disable_cache": True, # optional - "cache_failed_jobs": True, # optional - }, +result = client.agents.replace( + agent_id="agent_id", # required + name="name", # optional + disable_cache=True, # optional + cache_failed_jobs=True, # optional ) -response.raise_for_status() -result = response.json() ``` #### `agents_duplicate_create` @@ -430,22 +420,12 @@ from roe import RoeClient client = RoeClient() -agent_id = "agent_id" # required path -agent_version_id = "agent_version_id" # required path - -response = client.raw.get_httpx_client().request( - "PUT", - f"/v1/agents/{agent_id}/versions/{agent_version_id}/", - params={ - "organization_id": "00000000-0000-0000-0000-000000000000", # optional - }, - json={ - "version_name": "version_name", # optional - "description": "description", # optional - }, +result = client.agents.versions.replace( + agent_id="agent_id", # required + version_id="version_id", # required + version_name="version_name", # optional + description="description", # optional ) -response.raise_for_status() -result = response.json() ``` ### Connections @@ -459,19 +439,12 @@ from roe import RoeClient client = RoeClient() -response = client.raw.get_httpx_client().request( - "GET", - "/v1/connections/", - params={ - "connector_type": "connector_type", # optional - "organization_id": "00000000-0000-0000-0000-000000000000", # optional - "page": 1, # optional - "page_size": 1, # optional - "search": "search", # optional - }, +result = client.connections.list( + connector_type="connector_type", # optional + search="search", # optional + page=1, # optional + page_size=1, # optional ) -response.raise_for_status() -result = response.json() ``` #### `connections_create` @@ -483,23 +456,13 @@ from roe import RoeClient client = RoeClient() -response = client.raw.get_httpx_client().request( - "POST", - "/v1/connections/", - params={ - "organization_id": "00000000-0000-0000-0000-000000000000", # optional - }, - json={ - "connector_type": "connector_type", # required - "name": "name", # required - "description": "description", # optional - "config": {}, # required - "auth_config": {}, # optional - "organization_id": "00000000-0000-0000-0000-000000000000", # optional - }, +result = client.connections.create( + connector_type="connector_type", # required + name="name", # required + config={}, # required + description="description", # optional + auth_config={}, # optional ) -response.raise_for_status() -result = response.json() ``` #### `connections_test_credentials_create` @@ -511,17 +474,11 @@ from roe import RoeClient client = RoeClient() -response = client.raw.get_httpx_client().request( - "POST", - "/v1/connections/test-credentials/", - json={ - "connector_type": "connector_type", # required - "config": {}, # required - "auth_config": {}, # optional - }, +result = client.connections.test_credentials( + connector_type="connector_type", # required + config={}, # required + auth_config={}, # optional ) -response.raise_for_status() -result = response.json() ``` #### `connections_destroy` @@ -533,17 +490,9 @@ from roe import RoeClient client = RoeClient() -id = "id" # required path - -response = client.raw.get_httpx_client().request( - "DELETE", - f"/v1/connections/{id}/", - params={ - "organization_id": "00000000-0000-0000-0000-000000000000", # optional - }, +result = client.connections.delete( + connection_id="connection_id", # required ) -response.raise_for_status() -result = response.json() ``` #### `connections_retrieve` @@ -555,17 +504,9 @@ from roe import RoeClient client = RoeClient() -id = "id" # required path - -response = client.raw.get_httpx_client().request( - "GET", - f"/v1/connections/{id}/", - params={ - "organization_id": "00000000-0000-0000-0000-000000000000", # optional - }, +result = client.connections.retrieve( + connection_id="connection_id", # required ) -response.raise_for_status() -result = response.json() ``` #### `connections_partial_update` @@ -577,23 +518,13 @@ from roe import RoeClient client = RoeClient() -id = "id" # required path - -response = client.raw.get_httpx_client().request( - "PATCH", - f"/v1/connections/{id}/", - params={ - "organization_id": "00000000-0000-0000-0000-000000000000", # optional - }, - json={ - "name": "name", # optional - "description": "description", # optional - "config": {}, # optional - "auth_config": {}, # optional - }, +result = client.connections.update( + connection_id="connection_id", # required + name="name", # optional + description="description", # optional + config={}, # optional + auth_config={}, # optional ) -response.raise_for_status() -result = response.json() ``` #### `connections_update` @@ -605,23 +536,13 @@ from roe import RoeClient client = RoeClient() -id = "id" # required path - -response = client.raw.get_httpx_client().request( - "PUT", - f"/v1/connections/{id}/", - params={ - "organization_id": "00000000-0000-0000-0000-000000000000", # optional - }, - json={ - "name": "name", # optional - "description": "description", # optional - "config": {}, # optional - "auth_config": {}, # optional - }, +result = client.connections.replace( + connection_id="connection_id", # required + name="name", # optional + description="description", # optional + config={}, # optional + auth_config={}, # optional ) -response.raise_for_status() -result = response.json() ``` #### `connections_test_create` @@ -633,17 +554,9 @@ from roe import RoeClient client = RoeClient() -id = "id" # required path - -response = client.raw.get_httpx_client().request( - "POST", - f"/v1/connections/{id}/test/", - params={ - "organization_id": "00000000-0000-0000-0000-000000000000", # optional - }, +result = client.connections.test( + connection_id="connection_id", # required ) -response.raise_for_status() -result = response.json() ``` ### Connectors @@ -657,12 +570,7 @@ from roe import RoeClient client = RoeClient() -response = client.raw.get_httpx_client().request( - "GET", - "/v1/connectors/", -) -response.raise_for_status() -result = response.json() +result = client.connectors.list() ``` #### `connectors_retrieve_by_type` @@ -674,14 +582,9 @@ from roe import RoeClient client = RoeClient() -connector_type = "connector_type" # required path - -response = client.raw.get_httpx_client().request( - "GET", - f"/v1/connectors/{connector_type}/", +result = client.connectors.retrieve( + connector_type="connector_type", # required ) -response.raise_for_status() -result = response.json() ``` ### Discovery @@ -799,21 +702,11 @@ from roe import RoeClient client = RoeClient() -id = "id" # required path - -response = client.raw.get_httpx_client().request( - "PUT", - f"/v1/policies/{id}/", - params={ - "organization_id": "00000000-0000-0000-0000-000000000000", # optional - }, - json={ - "name": "name", # required - "description": "description", # optional - }, +result = client.policies.replace( + policy_id="policy_id", # required + name="name", # required + description="description", # optional ) -response.raise_for_status() -result = response.json() ``` #### `policies_versions_list` @@ -875,12 +768,7 @@ from roe import RoeClient client = RoeClient() -response = client.raw.get_httpx_client().request( - "GET", - "/v1/tables/", -) -response.raise_for_status() -result = response.json() +result = client.tables.list() ``` #### `tables_query_create` @@ -892,16 +780,10 @@ from roe import RoeClient client = RoeClient() -response = client.raw.get_httpx_client().request( - "POST", - "/v1/tables/query/", - json={ - "sql": "sql", # required - "limit": 1, # optional - }, +result = client.tables.query( + sql="sql", # required + limit=1, # optional ) -response.raise_for_status() -result = response.json() ``` #### `tables_query_result_retrieve` @@ -913,14 +795,9 @@ from roe import RoeClient client = RoeClient() -table_query_id = "table_query_id" # required path - -response = client.raw.get_httpx_client().request( - "GET", - f"/v1/tables/query/{table_query_id}/result/", +result = client.tables.query_result( + table_query_id="table_query_id", # required ) -response.raise_for_status() -result = response.json() ``` #### `tables_destroy` @@ -932,14 +809,9 @@ from roe import RoeClient client = RoeClient() -table_name = "table_name" # required path - -response = client.raw.get_httpx_client().request( - "DELETE", - f"/v1/tables/{table_name}/", +result = client.tables.delete( + table_name="table_name", # required ) -response.raise_for_status() -result = response.json() ``` #### `tables_describe_retrieve` @@ -951,14 +823,9 @@ from roe import RoeClient client = RoeClient() -table_name = "table_name" # required path - -response = client.raw.get_httpx_client().request( - "GET", - f"/v1/tables/{table_name}/describe/", +result = client.tables.describe( + table_name="table_name", # required ) -response.raise_for_status() -result = response.json() ``` #### `tables_preview_retrieve` @@ -970,17 +837,10 @@ from roe import RoeClient client = RoeClient() -table_name = "table_name" # required path - -response = client.raw.get_httpx_client().request( - "GET", - f"/v1/tables/{table_name}/preview/", - params={ - "limit": 1, # optional - }, +result = client.tables.preview( + table_name="table_name", # required + limit=1, # optional ) -response.raise_for_status() -result = response.json() ``` #### `upload_table` diff --git a/openapi/wrappers.yml b/openapi/wrappers.yml index 15464d0..e74e459 100644 --- a/openapi/wrappers.yml +++ b/openapi/wrappers.yml @@ -28,8 +28,16 @@ apis: pass_unset_when_none: true tables: class_name: TablesAPI - docstring: API for uploading CSV files into Roe tables. + docstring: API for managing Roe tables. operations: + - kind: body + method_name: list + docstring: List Roe tables. + method: GET + path: /v1/tables/ + endpoint_module: roe._generated.api.tables.tables_list + return_type: TableListResponse + return_import: roe._generated.models.table_list_response.TableListResponse - kind: table_upload method_name: upload docstring: Upload a CSV file and create a Roe table. @@ -39,6 +47,83 @@ apis: empty_response_message: table upload returned an empty response body_type: TableUploadRequest body_import: roe._generated.models.table_upload_request.TableUploadRequest + - kind: body + method_name: query + docstring: Run a read-only query against Roe tables. + method: POST + path: /v1/tables/query/ + endpoint_module: roe._generated.api.tables.tables_query_create + return_type: TableQuerySubmitResponse + return_import: roe._generated.models.table_query_submit_response.TableQuerySubmitResponse + body_type: TableQueryRequest + body_import: roe._generated.models.table_query_request.TableQueryRequest + parameters: + - name: sql + location: body + wire_name: sql + annotation: str + - name: limit + location: body + wire_name: limit + annotation: int | None + default: null + pass_unset_when_none: true + - kind: body + method_name: query_result + docstring: Get the result for a submitted table query. + method: GET + path: /v1/tables/query/{table_query_id}/result/ + endpoint_module: roe._generated.api.tables.tables_query_result_retrieve + return_type: TableQueryResultResponse + return_import: roe._generated.models.table_query_result_response.TableQueryResultResponse + parameters: + - name: table_query_id + location: path + wire_name: table_query_id + annotation: str + - kind: body + method_name: describe + docstring: Describe a Roe table. + method: GET + path: /v1/tables/{table_name}/describe/ + endpoint_module: roe._generated.api.tables.tables_describe_retrieve + return_type: TableDescribeResponse + return_import: roe._generated.models.table_describe_response.TableDescribeResponse + parameters: + - name: table_name + location: path + wire_name: table_name + annotation: str + - kind: body + method_name: preview + docstring: Preview rows from a Roe table. + method: GET + path: /v1/tables/{table_name}/preview/ + endpoint_module: roe._generated.api.tables.tables_preview_retrieve + return_type: TablePreviewResponse + return_import: roe._generated.models.table_preview_response.TablePreviewResponse + parameters: + - name: table_name + location: path + wire_name: table_name + annotation: str + - name: limit + location: query + wire_name: limit + annotation: int | None + default: null + pass_unset_when_none: true + - kind: body + method_name: delete + docstring: Delete a Roe table. + method: DELETE + path: /v1/tables/{table_name}/ + endpoint_module: roe._generated.api.tables.tables_destroy + parameters: + - name: table_name + location: path + wire_name: table_name + annotation: str agents: class_name: AgentsAPI docstring: API for managing and running agents. @@ -143,6 +228,36 @@ apis: annotation: bool | None default: null pass_unset_when_none: true + - kind: body + method_name: replace + docstring: Replace an agent via PUT. + method: PUT + path: /v1/agents/{agent_id}/ + endpoint_module: roe._generated.api.agents.agents_update + return_type: BaseAgent + return_import: roe._generated.models.base_agent.BaseAgent + body_type: BaseAgentUpdateRequest + body_import: roe._generated.models.base_agent_update_request.BaseAgentUpdateRequest + parameters: + - name: agent_id + location: path + annotation: str + coerce: uuid + - name: name + location: body + annotation: str | None + default: null + pass_unset_when_none: true + - name: disable_cache + location: body + annotation: bool | None + default: null + pass_unset_when_none: true + - name: cache_failed_jobs + location: body + annotation: bool | None + default: null + pass_unset_when_none: true - kind: body method_name: delete docstring: '' @@ -317,6 +432,35 @@ apis: annotation: str | None default: null pass_unset_when_none: true + - kind: body + method_name: replace + docstring: Replace an agent version via PUT. + method: PUT + path: /v1/agents/{agent_id}/versions/{agent_version_id}/ + endpoint_module: roe._generated.api.agents.agents_versions_update + return_type: MessageResponse + return_import: roe._generated.models.message_response.MessageResponse + body_type: AgentVersionUpdateRequest + body_import: roe._generated.models.agent_version_update_request.AgentVersionUpdateRequest + parameters: + - name: agent_id + location: path + annotation: str + coerce: uuid + - name: version_id + location: path + annotation: str + coerce: uuid + - name: version_name + location: body + annotation: str | None + default: null + pass_unset_when_none: true + - name: description + location: body + annotation: str | None + default: null + pass_unset_when_none: true - kind: body method_name: delete docstring: '' @@ -413,6 +557,246 @@ apis: docstring: Download a binary reference produced by an agent job. method: GET path: /v1/agents/jobs/{agent_job_id}/references/{resource_id}/ + connections: + class_name: ConnectionsAPI + docstring: API for managing external data connections. + operations: + - kind: body + method_name: list + docstring: List connections. + method: GET + path: /v1/connections/ + endpoint_module: roe._generated.api.connections.connections_list + return_type: PaginatedConnectionListList + return_import: roe._generated.models.paginated_connection_list_list.PaginatedConnectionListList + inject_organization_id: true + parameters: + - name: connector_type + location: query + wire_name: connector_type + annotation: str | None + default: null + pass_unset_when_none: true + - name: search + location: query + wire_name: search + annotation: str | None + default: null + pass_unset_when_none: true + - name: page + location: query + wire_name: page + annotation: int | None + default: null + pass_unset_when_none: true + - name: page_size + location: query + wire_name: page_size + annotation: int | None + default: null + pass_unset_when_none: true + - kind: body + method_name: create + docstring: Create a connection. + method: POST + path: /v1/connections/ + endpoint_module: roe._generated.api.connections.connections_create + return_type: Connection + return_import: roe._generated.models.connection.Connection + body_format: dict + inject_organization_id: true + parameters: + - name: connector_type + location: body + wire_name: connector_type + annotation: str + - name: name + location: body + wire_name: name + annotation: str + - name: config + location: body + wire_name: config + annotation: dict[str, Any] + - name: description + location: body + wire_name: description + annotation: str | None + default: null + pass_unset_when_none: true + - name: auth_config + location: body + wire_name: auth_config + annotation: dict[str, Any] | None + default: null + pass_unset_when_none: true + - kind: body + method_name: test_credentials + docstring: Test connection credentials without saving a connection. + method: POST + path: /v1/connections/test-credentials/ + endpoint_module: roe._generated.api.connections.connections_test_credentials_create + return_type: TestConnection + return_import: roe._generated.models.test_connection.TestConnection + body_format: dict + parameters: + - name: connector_type + location: body + wire_name: connector_type + annotation: str + - name: config + location: body + wire_name: config + annotation: dict[str, Any] + - name: auth_config + location: body + wire_name: auth_config + annotation: dict[str, Any] | None + default: null + pass_unset_when_none: true + - kind: body + method_name: retrieve + docstring: Retrieve a connection. + method: GET + path: /v1/connections/{id}/ + endpoint_module: roe._generated.api.connections.connections_retrieve + return_type: Connection + return_import: roe._generated.models.connection.Connection + inject_organization_id: true + parameters: + - name: connection_id + location: path + wire_name: id + annotation: str + - kind: body + method_name: update + docstring: Update mutable connection fields. + method: PATCH + path: /v1/connections/{id}/ + endpoint_module: roe._generated.api.connections.connections_partial_update + return_type: Connection + return_import: roe._generated.models.connection.Connection + body_format: dict + inject_organization_id: true + parameters: + - name: connection_id + location: path + wire_name: id + annotation: str + - name: name + location: body + wire_name: name + annotation: str | None + default: null + pass_unset_when_none: true + - name: description + location: body + wire_name: description + annotation: str | None + default: null + pass_unset_when_none: true + - name: config + location: body + wire_name: config + annotation: dict[str, Any] | None + default: null + pass_unset_when_none: true + - name: auth_config + location: body + wire_name: auth_config + annotation: dict[str, Any] | None + default: null + pass_unset_when_none: true + - kind: body + method_name: replace + docstring: Replace a connection. + method: PUT + path: /v1/connections/{id}/ + endpoint_module: roe._generated.api.connections.connections_update + return_type: Connection + return_import: roe._generated.models.connection.Connection + body_format: dict + inject_organization_id: true + parameters: + - name: connection_id + location: path + wire_name: id + annotation: str + - name: name + location: body + wire_name: name + annotation: str | None + default: null + pass_unset_when_none: true + - name: description + location: body + wire_name: description + annotation: str | None + default: null + pass_unset_when_none: true + - name: config + location: body + wire_name: config + annotation: dict[str, Any] | None + default: null + pass_unset_when_none: true + - name: auth_config + location: body + wire_name: auth_config + annotation: dict[str, Any] | None + default: null + pass_unset_when_none: true + - kind: body + method_name: delete + docstring: Delete a connection. + method: DELETE + path: /v1/connections/{id}/ + endpoint_module: roe._generated.api.connections.connections_destroy + inject_organization_id: true + parameters: + - name: connection_id + location: path + wire_name: id + annotation: str + - kind: body + method_name: test + docstring: Test a saved connection. + method: POST + path: /v1/connections/{id}/test/ + endpoint_module: roe._generated.api.connections.connections_test_create + return_type: TestConnection + return_import: roe._generated.models.test_connection.TestConnection + inject_organization_id: true + parameters: + - name: connection_id + location: path + wire_name: id + annotation: str + connectors: + class_name: ConnectorsAPI + docstring: API for discovering available connector types. + operations: + - kind: body + method_name: list + docstring: List available connector types. + method: GET + path: /v1/connectors/ + endpoint_module: roe._generated.api.connectors.connectors_retrieve + return_type: ConnectorListResponse + return_import: roe._generated.models.connector_list_response.ConnectorListResponse + - kind: body + method_name: retrieve + docstring: Retrieve metadata for a connector type. + method: GET + path: /v1/connectors/{connector_type}/ + endpoint_module: roe._generated.api.connectors.connectors_retrieve_by_type + return_type: ConnectorMetadata + return_import: roe._generated.models.connector_metadata.ConnectorMetadata + parameters: + - name: connector_type + location: path + wire_name: connector_type + annotation: str policies: class_name: PoliciesAPI docstring: API for managing policies used by agentic workflows. @@ -499,6 +883,29 @@ apis: annotation: str | None default: null pass_unset_when_none: true + - kind: body + method_name: replace + docstring: Replace a policy via PUT. + method: PUT + path: /v1/policies/{id}/ + endpoint_module: roe._generated.api.policies.policies_update + return_type: UpdatePolicy + return_import: roe._generated.models.update_policy.UpdatePolicy + body_type: UpdatePolicyRequest + body_import: roe._generated.models.update_policy_request.UpdatePolicyRequest + parameters: + - name: policy_id + location: path + annotation: str + coerce: uuid + - name: name + location: body + annotation: str + - name: description + location: body + annotation: str | None + default: null + pass_unset_when_none: true - kind: body method_name: delete docstring: Delete a policy and all its versions. diff --git a/scripts/generate_wrappers.py b/scripts/generate_wrappers.py index c5fc020..172c8f5 100644 --- a/scripts/generate_wrappers.py +++ b/scripts/generate_wrappers.py @@ -321,6 +321,28 @@ def _signature_param(param: dict[str, Any]) -> str: return f"{param['name']}: {param['annotation']}" +def _dict_body_lines(operation: dict[str, Any], body_params: list[dict[str, Any]]) -> list[str]: + if operation.get("body_format") != "dict": + return [] + lines = [" body: dict[str, Any] = {}\n"] + if operation.get("inject_organization_id"): + lines.append(' body["organization_id"] = str(self._org_id)\n') + for param in body_params: + name = param["name"] + wire_name = param.get("wire_name", name) + value = _field_expr(param) + if param.get("pass_unset_when_none"): + lines.extend( + [ + f" if {name} is not None:\n", + f" body[{wire_name!r}] = {value}\n", + ] + ) + else: + lines.append(f" body[{wire_name!r}] = {value}\n") + return lines + + def _body_method(operation: dict[str, Any]) -> str: method_name = operation["method_name"] return_type = operation.get("return_type") @@ -349,6 +371,7 @@ def _body_method(operation: dict[str, Any]) -> str: f' """{docstring}"""\n', ] body_type = operation.get("body_type") + has_json_body = bool(body_type or operation.get("body_format") == "dict") if body_type: lines.append(f" body = {body_type}(\n") if operation.get("inject_organization_id"): @@ -356,11 +379,13 @@ def _body_method(operation: dict[str, Any]) -> str: for param in body_params: lines.append(f" {param['name']}={_field_expr(param)},\n") lines.append(" )\n") + else: + lines.extend(_dict_body_lines(operation, body_params)) endpoint_name = _module_import_parts(operation["endpoint_module"])[1] call_args = [" self._raw,\n", f" {endpoint_name},\n"] call_args.extend(f" {_field_expr(param)},\n" for param in path_params) - if body_type: + if has_json_body: call_args.append(" body=body,\n") for param in query_params: call_args.append(f" {param['name']}={_field_expr(param)},\n") @@ -370,12 +395,12 @@ def _body_method(operation: dict[str, Any]) -> str: if return_type or operation.get("refetch_with_retrieve"): lines.append( " resp = request_json(\n" - if body_type + if has_json_body else " response = request_raw(\n" ) else: lines.append( - " request_json(\n" if body_type else " request_raw(\n" + " request_json(\n" if has_json_body else " request_raw(\n" ) lines.extend(call_args) lines.append(" )\n") @@ -391,7 +416,7 @@ def _body_method(operation: dict[str, Any]) -> str: ] ) elif return_type: - if body_type: + if has_json_body: lines.append(" return resp.parsed # type: ignore[return-value]\n") elif operation.get("return_shape") == "list": label = operation.get("list_error_label", return_type) @@ -417,8 +442,13 @@ def _render_api_module(api_name: str, spec: dict[str, Any]) -> str: endpoint_imports: dict[str, list[str]] = defaultdict(list) model_imports: dict[str, list[str]] = defaultdict(list) + needs_any = False + needs_org_id = False + needs_request_json = False + needs_request_raw = False needs_unset = False needs_roe_api_exception = False + needs_translate_response = False needs_table_upload_helpers = False methods: list[str] = [] @@ -426,12 +456,14 @@ def _render_api_module(api_name: str, spec: dict[str, Any]) -> str: package, endpoint_name = _module_import_parts(operation["endpoint_module"]) endpoint_imports[package].append(endpoint_name) - return_module, return_class = _class_import_parts(operation["return_import"]) - model_imports[return_module].append(return_class) + if operation.get("return_import"): + return_module, return_class = _class_import_parts(operation["return_import"]) + model_imports[return_module].append(return_class) kind = operation.get("kind", "simple") if kind == "simple": needs_roe_api_exception = True + needs_translate_response = True if any( param.get("pass_unset_when_none") for param in operation.get("parameters") or [] @@ -440,21 +472,53 @@ def _render_api_module(api_name: str, spec: dict[str, Any]) -> str: methods.append(_simple_method(operation)) elif kind == "table_upload": needs_roe_api_exception = True + needs_translate_response = True needs_table_upload_helpers = True needs_unset = True body_module, body_class = _class_import_parts(operation["body_import"]) model_imports[body_module].append(body_class) methods.append(_table_upload_method(operation)) + elif kind == "body": + parameters = operation.get("parameters") or [] + has_json_body = bool( + operation.get("body_type") or operation.get("body_format") == "dict" + ) + needs_any = ( + needs_any + or "Any" in operation.get("return_type", "") + or any("Any" in param.get("annotation", "") for param in parameters) + ) + needs_request_json = needs_request_json or has_json_body + needs_request_raw = needs_request_raw or not has_json_body + needs_org_id = needs_org_id or bool(operation.get("inject_organization_id")) + needs_roe_api_exception = needs_roe_api_exception or bool( + operation.get("refetch_with_retrieve") + or operation.get("return_shape") == "list" + ) + needs_unset = needs_unset or any( + param.get("pass_unset_when_none") for param in parameters + ) + if operation.get("body_import"): + body_module, body_class = _class_import_parts(operation["body_import"]) + model_imports[body_module].append(body_class) + methods.append(_body_method(operation)) else: raise ValueError(f"Unsupported wrapper kind {kind!r} in {api_name}") lines = [HEADER] + typing_imports = [] + if needs_any: + typing_imports.append("Any") if needs_table_upload_helpers: lines.append("from io import BytesIO\n") lines.append("import mimetypes\n") lines.append("from pathlib import Path\n") - lines.append("from typing import BinaryIO\n") + typing_imports.append("BinaryIO") + if typing_imports: + lines.append(f"from typing import {', '.join(sorted(set(typing_imports)))}\n") + if needs_table_upload_helpers or needs_org_id: lines.append("from uuid import UUID\n") + if needs_table_upload_helpers or typing_imports or needs_org_id: lines.append("\n") for package, names in sorted(endpoint_imports.items()): @@ -475,12 +539,23 @@ def _render_api_module(api_name: str, spec: dict[str, Any]) -> str: elif needs_unset: lines.append("from roe._generated.types import UNSET\n") lines.append("from roe.config import RoeConfig\n") - if needs_roe_api_exception: + if needs_roe_api_exception and needs_translate_response: lines.append("from roe.exceptions import RoeAPIException, translate_response\n") - else: + elif needs_roe_api_exception: + lines.append("from roe.exceptions import RoeAPIException\n") + elif needs_translate_response: lines.append("from roe.exceptions import translate_response\n") if needs_table_upload_helpers: lines.append("from roe.models import FileUpload\n") + request_helpers = [] + if needs_request_json: + request_helpers.append("request_json") + if needs_request_raw: + request_helpers.append("request_raw") + if request_helpers: + lines.append( + f"from roe.utils.generated_request import {', '.join(request_helpers)}\n" + ) lines.append("\n\n") lines.append(f"class {class_name}:\n") @@ -492,6 +567,11 @@ def _render_api_module(api_name: str, spec: dict[str, Any]) -> str: lines.append(" self.config = config\n") lines.append(" self._raw = raw_client\n") lines.append("\n") + if needs_org_id: + lines.append(" @property\n") + lines.append(" def _org_id(self) -> UUID:\n") + lines.append(" return UUID(str(self.config.organization_id))\n") + lines.append("\n") lines.append("\n".join(methods)) if needs_table_upload_helpers: diff --git a/src/roe/api/_generated_registry.py b/src/roe/api/_generated_registry.py index 5739805..d8de5ef 100644 --- a/src/roe/api/_generated_registry.py +++ b/src/roe/api/_generated_registry.py @@ -5,11 +5,15 @@ from __future__ import annotations +from roe.api.connections import ConnectionsAPI +from roe.api.connectors import ConnectorsAPI from roe.api.discovery import DiscoveryAPI from roe.api.tables import TablesAPI GENERATED_API_CLASSES = { + "connections": ConnectionsAPI, + "connectors": ConnectorsAPI, "discovery": DiscoveryAPI, "tables": TablesAPI, } diff --git a/src/roe/api/connections.py b/src/roe/api/connections.py new file mode 100644 index 0000000..fa379d9 --- /dev/null +++ b/src/roe/api/connections.py @@ -0,0 +1,200 @@ +"""Auto-generated friendly API facades for the Roe SDK.""" + +# Generated by scripts/generate-sdk from openapi/wrappers.yml. +# Do not edit by hand. + +from __future__ import annotations + +from typing import Any +from uuid import UUID + +from roe._generated.api.connections import ( + connections_create, + connections_destroy, + connections_list, + connections_partial_update, + connections_retrieve, + connections_test_create, + connections_test_credentials_create, + connections_update, +) +from roe._generated.client import AuthenticatedClient +from roe._generated.models.connection import Connection +from roe._generated.models.paginated_connection_list_list import ( + PaginatedConnectionListList, +) +from roe._generated.models.test_connection import TestConnection +from roe._generated.types import UNSET +from roe.config import RoeConfig +from roe.utils.generated_request import request_json, request_raw + + +class ConnectionsAPI: + """API for managing external data connections.""" + + def __init__(self, config: RoeConfig, raw_client: AuthenticatedClient): + self.config = config + self._raw = raw_client + + @property + def _org_id(self) -> UUID: + return UUID(str(self.config.organization_id)) + + def list( + self, + connector_type: str | None = None, + search: str | None = None, + page: int | None = None, + page_size: int | None = None, + ) -> PaginatedConnectionListList: + """List connections.""" + response = request_raw( + self._raw, + connections_list, + connector_type=connector_type if connector_type is not None else UNSET, + search=search if search is not None else UNSET, + page=page if page is not None else UNSET, + page_size=page_size if page_size is not None else UNSET, + organization_id=self._org_id, + ) + return PaginatedConnectionListList.from_dict(response.json()) + + def create( + self, + connector_type: str, + name: str, + config: dict[str, Any], + description: str | None = None, + auth_config: dict[str, Any] | None = None, + ) -> Connection: + """Create a connection.""" + body: dict[str, Any] = {} + body["organization_id"] = str(self._org_id) + body["connector_type"] = connector_type + body["name"] = name + body["config"] = config + if description is not None: + body["description"] = description if description is not None else UNSET + if auth_config is not None: + body["auth_config"] = auth_config if auth_config is not None else UNSET + resp = request_json( + self._raw, + connections_create, + body=body, + organization_id=self._org_id, + ) + return resp.parsed # type: ignore[return-value] + + def test_credentials( + self, + connector_type: str, + config: dict[str, Any], + auth_config: dict[str, Any] | None = None, + ) -> TestConnection: + """Test connection credentials without saving a connection.""" + body: dict[str, Any] = {} + body["connector_type"] = connector_type + body["config"] = config + if auth_config is not None: + body["auth_config"] = auth_config if auth_config is not None else UNSET + resp = request_json( + self._raw, + connections_test_credentials_create, + body=body, + ) + return resp.parsed # type: ignore[return-value] + + def retrieve( + self, + connection_id: str, + ) -> Connection: + """Retrieve a connection.""" + response = request_raw( + self._raw, + connections_retrieve, + connection_id, + organization_id=self._org_id, + ) + return Connection.from_dict(response.json()) + + def update( + self, + connection_id: str, + name: str | None = None, + description: str | None = None, + config: dict[str, Any] | None = None, + auth_config: dict[str, Any] | None = None, + ) -> Connection: + """Update mutable connection fields.""" + body: dict[str, Any] = {} + body["organization_id"] = str(self._org_id) + if name is not None: + body["name"] = name if name is not None else UNSET + if description is not None: + body["description"] = description if description is not None else UNSET + if config is not None: + body["config"] = config if config is not None else UNSET + if auth_config is not None: + body["auth_config"] = auth_config if auth_config is not None else UNSET + resp = request_json( + self._raw, + connections_partial_update, + connection_id, + body=body, + organization_id=self._org_id, + ) + return resp.parsed # type: ignore[return-value] + + def replace( + self, + connection_id: str, + name: str | None = None, + description: str | None = None, + config: dict[str, Any] | None = None, + auth_config: dict[str, Any] | None = None, + ) -> Connection: + """Replace a connection.""" + body: dict[str, Any] = {} + body["organization_id"] = str(self._org_id) + if name is not None: + body["name"] = name if name is not None else UNSET + if description is not None: + body["description"] = description if description is not None else UNSET + if config is not None: + body["config"] = config if config is not None else UNSET + if auth_config is not None: + body["auth_config"] = auth_config if auth_config is not None else UNSET + resp = request_json( + self._raw, + connections_update, + connection_id, + body=body, + organization_id=self._org_id, + ) + return resp.parsed # type: ignore[return-value] + + def delete( + self, + connection_id: str, + ) -> None: + """Delete a connection.""" + request_raw( + self._raw, + connections_destroy, + connection_id, + organization_id=self._org_id, + ) + return None + + def test( + self, + connection_id: str, + ) -> TestConnection: + """Test a saved connection.""" + response = request_raw( + self._raw, + connections_test_create, + connection_id, + organization_id=self._org_id, + ) + return TestConnection.from_dict(response.json()) diff --git a/src/roe/api/connectors.py b/src/roe/api/connectors.py new file mode 100644 index 0000000..10aa731 --- /dev/null +++ b/src/roe/api/connectors.py @@ -0,0 +1,44 @@ +"""Auto-generated friendly API facades for the Roe SDK.""" + +# Generated by scripts/generate-sdk from openapi/wrappers.yml. +# Do not edit by hand. + +from __future__ import annotations + +from roe._generated.api.connectors import ( + connectors_retrieve, + connectors_retrieve_by_type, +) +from roe._generated.client import AuthenticatedClient +from roe._generated.models.connector_list_response import ConnectorListResponse +from roe._generated.models.connector_metadata import ConnectorMetadata +from roe.config import RoeConfig +from roe.utils.generated_request import request_raw + + +class ConnectorsAPI: + """API for discovering available connector types.""" + + def __init__(self, config: RoeConfig, raw_client: AuthenticatedClient): + self.config = config + self._raw = raw_client + + def list(self) -> ConnectorListResponse: + """List available connector types.""" + response = request_raw( + self._raw, + connectors_retrieve, + ) + return ConnectorListResponse.from_dict(response.json()) + + def retrieve( + self, + connector_type: str, + ) -> ConnectorMetadata: + """Retrieve metadata for a connector type.""" + response = request_raw( + self._raw, + connectors_retrieve_by_type, + connector_type, + ) + return ConnectorMetadata.from_dict(response.json()) diff --git a/src/roe/api/tables.py b/src/roe/api/tables.py index cbcbe14..ba72e72 100644 --- a/src/roe/api/tables.py +++ b/src/roe/api/tables.py @@ -11,23 +11,46 @@ from typing import BinaryIO from uuid import UUID -from roe._generated.api.tables import upload_table +from roe._generated.api.tables import ( + tables_describe_retrieve, + tables_destroy, + tables_list, + tables_preview_retrieve, + tables_query_create, + tables_query_result_retrieve, + upload_table, +) from roe._generated.client import AuthenticatedClient +from roe._generated.models.table_describe_response import TableDescribeResponse +from roe._generated.models.table_list_response import TableListResponse +from roe._generated.models.table_preview_response import TablePreviewResponse +from roe._generated.models.table_query_request import TableQueryRequest +from roe._generated.models.table_query_result_response import TableQueryResultResponse +from roe._generated.models.table_query_submit_response import TableQuerySubmitResponse from roe._generated.models.table_upload_request import TableUploadRequest from roe._generated.models.table_upload_response import TableUploadResponse from roe._generated.types import File, UNSET, Unset from roe.config import RoeConfig from roe.exceptions import RoeAPIException, translate_response from roe.models import FileUpload +from roe.utils.generated_request import request_json, request_raw class TablesAPI: - """API for uploading CSV files into Roe tables.""" + """API for managing Roe tables.""" def __init__(self, config: RoeConfig, raw_client: AuthenticatedClient): self.config = config self._raw = raw_client + def list(self) -> TableListResponse: + """List Roe tables.""" + response = request_raw( + self._raw, + tables_list, + ) + return TableListResponse.from_dict(response.json()) + def upload( self, *, @@ -122,6 +145,73 @@ def _as_generated_file( False, ) + def query( + self, + sql: str, + limit: int | None = None, + ) -> TableQuerySubmitResponse: + """Run a read-only query against Roe tables.""" + body = TableQueryRequest( + sql=sql, + limit=limit if limit is not None else UNSET, + ) + resp = request_json( + self._raw, + tables_query_create, + body=body, + ) + return resp.parsed # type: ignore[return-value] + + def query_result( + self, + table_query_id: str, + ) -> TableQueryResultResponse: + """Get the result for a submitted table query.""" + response = request_raw( + self._raw, + tables_query_result_retrieve, + table_query_id, + ) + return TableQueryResultResponse.from_dict(response.json()) + + def describe( + self, + table_name: str, + ) -> TableDescribeResponse: + """Describe a Roe table.""" + response = request_raw( + self._raw, + tables_describe_retrieve, + table_name, + ) + return TableDescribeResponse.from_dict(response.json()) + + def preview( + self, + table_name: str, + limit: int | None = None, + ) -> TablePreviewResponse: + """Preview rows from a Roe table.""" + response = request_raw( + self._raw, + tables_preview_retrieve, + table_name, + limit=limit if limit is not None else UNSET, + ) + return TablePreviewResponse.from_dict(response.json()) + + def delete( + self, + table_name: str, + ) -> None: + """Delete a Roe table.""" + request_raw( + self._raw, + tables_destroy, + table_name, + ) + return None + def _mime_type(filename: str, override: str | None) -> str: if override: From d7e9ac34422353dd51658e888ed4fa717631c633 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Wed, 17 Jun 2026 15:45:02 -0700 Subject: [PATCH 2/6] fix: support dict generated request bodies --- openapi/wrappers.yml | 3 ++ scripts/generate_wrappers.py | 8 ++++-- src/roe/utils/generated_request.py | 13 ++++++++- tests/unit/test_generated_request.py | 43 ++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 tests/unit/test_generated_request.py diff --git a/openapi/wrappers.yml b/openapi/wrappers.yml index e74e459..e62659b 100644 --- a/openapi/wrappers.yml +++ b/openapi/wrappers.yml @@ -238,6 +238,7 @@ apis: return_import: roe._generated.models.base_agent.BaseAgent body_type: BaseAgentUpdateRequest body_import: roe._generated.models.base_agent_update_request.BaseAgentUpdateRequest + inject_organization_id: true parameters: - name: agent_id location: path @@ -442,6 +443,7 @@ apis: return_import: roe._generated.models.message_response.MessageResponse body_type: AgentVersionUpdateRequest body_import: roe._generated.models.agent_version_update_request.AgentVersionUpdateRequest + inject_organization_id: true parameters: - name: agent_id location: path @@ -893,6 +895,7 @@ apis: return_import: roe._generated.models.update_policy.UpdatePolicy body_type: UpdatePolicyRequest body_import: roe._generated.models.update_policy_request.UpdatePolicyRequest + inject_organization_id: true parameters: - name: policy_id location: path diff --git a/scripts/generate_wrappers.py b/scripts/generate_wrappers.py index 172c8f5..e69b0f7 100644 --- a/scripts/generate_wrappers.py +++ b/scripts/generate_wrappers.py @@ -321,7 +321,9 @@ def _signature_param(param: dict[str, Any]) -> str: return f"{param['name']}: {param['annotation']}" -def _dict_body_lines(operation: dict[str, Any], body_params: list[dict[str, Any]]) -> list[str]: +def _dict_body_lines( + operation: dict[str, Any], body_params: list[dict[str, Any]] +) -> list[str]: if operation.get("body_format") != "dict": return [] lines = [" body: dict[str, Any] = {}\n"] @@ -457,7 +459,9 @@ def _render_api_module(api_name: str, spec: dict[str, Any]) -> str: endpoint_imports[package].append(endpoint_name) if operation.get("return_import"): - return_module, return_class = _class_import_parts(operation["return_import"]) + return_module, return_class = _class_import_parts( + operation["return_import"] + ) model_imports[return_module].append(return_class) kind = operation.get("kind", "simple") diff --git a/src/roe/utils/generated_request.py b/src/roe/utils/generated_request.py index 38974a5..dde5848 100644 --- a/src/roe/utils/generated_request.py +++ b/src/roe/utils/generated_request.py @@ -7,6 +7,14 @@ from roe.exceptions import translate_response +class _DictBody: + def __init__(self, value: dict[str, Any]): + self.value = value + + def to_dict(self) -> dict[str, Any]: + return self.value + + def request_raw( raw: AuthenticatedClient, ep_module: Any, @@ -16,7 +24,10 @@ def request_raw( ) -> Any: """Call a generated endpoint while forcing JSON body serialization.""" if not isinstance(body, Unset): - request_kwargs = ep_module._get_kwargs(*path_args, body=body, **kwargs) + generated_body = _DictBody(body) if isinstance(body, dict) else body + request_kwargs = ep_module._get_kwargs( + *path_args, body=generated_body, **kwargs + ) request_kwargs.pop("data", None) request_kwargs.pop("files", None) request_kwargs["json"] = body.to_dict() if hasattr(body, "to_dict") else body diff --git a/tests/unit/test_generated_request.py b/tests/unit/test_generated_request.py new file mode 100644 index 0000000..3a6a51c --- /dev/null +++ b/tests/unit/test_generated_request.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import MagicMock + +import httpx + +from roe.utils.generated_request import request_json + + +class _DictBodyEndpoint: + @staticmethod + def _get_kwargs(*, body, organization_id): + return { + "method": "post", + "url": "/v1/connections/", + "params": {"organization_id": str(organization_id)}, + "json": body.to_dict(), + "headers": {"Content-Type": "application/json"}, + } + + @staticmethod + def _build_response(*, client, response): + return SimpleNamespace(parsed=response.json()) + + +def test_request_json_accepts_plain_dict_body_before_generated_serialization(): + request = MagicMock(return_value=httpx.Response(200, json={"ok": True})) + raw_client = MagicMock() + raw_client.get_httpx_client.return_value = SimpleNamespace(request=request) + + result = request_json( + raw_client, + _DictBodyEndpoint, + body={"connector_type": "salesforce", "config": {"domain": "example"}}, + organization_id="00000000-0000-0000-0000-000000000000", + ) + + assert result.parsed == {"ok": True} + assert request.call_args.kwargs["json"] == { + "connector_type": "salesforce", + "config": {"domain": "example"}, + } From 3e77f4b02fd1b98a5854448dc328aa26788624e3 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Wed, 17 Jun 2026 16:00:32 -0700 Subject: [PATCH 3/6] refactor: remove unused generated facade partials --- src/roe/api/agents.py | 51 +++++++++++++++++++ src/roe/api/policies.py | 24 +++++++++ tests/unit/test_agents_wrapper_transport.py | 47 +++++++++++++++++ tests/unit/test_policies_wrapper_transport.py | 46 +++++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 tests/unit/test_policies_wrapper_transport.py diff --git a/src/roe/api/agents.py b/src/roe/api/agents.py index f2449c0..922cd14 100644 --- a/src/roe/api/agents.py +++ b/src/roe/api/agents.py @@ -36,6 +36,7 @@ agents_run_async_create, agents_run_async_many, agents_run_version, + agents_update, agents_run_versions_async_create, agents_versions_create, agents_versions_current_retrieve, @@ -43,6 +44,7 @@ agents_versions_list, agents_versions_partial_update, agents_versions_retrieve, + agents_versions_update, ) from roe._generated.client import AuthenticatedClient from roe._generated.models.agent_datum import AgentDatum @@ -67,8 +69,11 @@ ) from roe._generated.models.agent_version import AgentVersion from roe._generated.models.agent_version_create_request import AgentVersionCreateRequest +from roe._generated.models.agent_version_update_request import AgentVersionUpdateRequest from roe._generated.models.base_agent import BaseAgent from roe._generated.models.base_agent_create_request import BaseAgentCreateRequest +from roe._generated.models.base_agent_update_request import BaseAgentUpdateRequest +from roe._generated.models.message_response import MessageResponse from roe._generated.models.paginated_base_agent_list import PaginatedBaseAgentList from roe._generated.models.patched_base_agent_update_request import ( PatchedBaseAgentUpdateRequest, @@ -200,6 +205,28 @@ def update( organization_id=self._org_id, ) + def replace( + self, + agent_id: str, + version_id: str, + version_name: str | None = None, + description: str | None = None, + ) -> MessageResponse: + """Replace an agent version via PUT.""" + body = AgentVersionUpdateRequest( + version_name=version_name if version_name is not None else UNSET, + description=description if description is not None else UNSET, + ) + resp = request_json( + self._raw, + agents_versions_update, + UUID(str(agent_id)), + UUID(str(version_id)), + body=body, + organization_id=self._org_id, + ) + return resp.parsed # type: ignore[return-value] + def delete(self, agent_id: str, version_id: str) -> None: request_raw( self._raw, @@ -442,6 +469,30 @@ def update( ) return resp.parsed # type: ignore[return-value] + def replace( + self, + agent_id: str, + name: str | None = None, + disable_cache: bool | None = None, + cache_failed_jobs: bool | None = None, + ) -> BaseAgent: + """Replace an agent via PUT.""" + body = BaseAgentUpdateRequest( + name=name if name is not None else UNSET, + disable_cache=disable_cache if disable_cache is not None else UNSET, + cache_failed_jobs=cache_failed_jobs + if cache_failed_jobs is not None + else UNSET, + ) + resp = request_json( + self._raw, + agents_update, + UUID(str(agent_id)), + body=body, + organization_id=self._org_id, + ) + return resp.parsed # type: ignore[return-value] + def delete(self, agent_id: str) -> None: request_raw( self._raw, diff --git a/src/roe/api/policies.py b/src/roe/api/policies.py index 94f50fc..f02c177 100644 --- a/src/roe/api/policies.py +++ b/src/roe/api/policies.py @@ -16,6 +16,7 @@ policies_list, policies_partial_update, policies_retrieve, + policies_update, policies_versions_create, policies_versions_list, policies_versions_retrieve, @@ -35,6 +36,8 @@ ) from roe._generated.models.policy import Policy from roe._generated.models.policy_version import PolicyVersion +from roe._generated.models.update_policy import UpdatePolicy +from roe._generated.models.update_policy_request import UpdatePolicyRequest from roe._generated.types import UNSET from roe.config import RoeConfig from roe.utils.generated_request import request_json, request_raw @@ -208,6 +211,27 @@ def update( ) return resp.parsed + def replace( + self, + policy_id: str, + name: str, + description: str | None = None, + ) -> UpdatePolicy: + """Replace a policy via PUT.""" + org_id = UUID(str(self.config.organization_id)) + body = UpdatePolicyRequest( + name=name, + description=description if description is not None else UNSET, + ) + resp = request_json( + self._raw, + policies_update, + UUID(str(policy_id)), + body=body, + organization_id=org_id, + ) + return resp.parsed # type: ignore[return-value] + def delete(self, policy_id: str) -> None: """Delete a policy and all its versions.""" request_raw( diff --git a/tests/unit/test_agents_wrapper_transport.py b/tests/unit/test_agents_wrapper_transport.py index 180fe63..63420ae 100644 --- a/tests/unit/test_agents_wrapper_transport.py +++ b/tests/unit/test_agents_wrapper_transport.py @@ -4,6 +4,7 @@ from types import SimpleNamespace from unittest.mock import MagicMock +from uuid import UUID import httpx import pytest @@ -17,6 +18,23 @@ JOB_ID = "00000000-0000-0000-0000-000000000333" +def _base_agent_json() -> dict[str, object]: + return { + "id": AGENT_ID, + "created_at": "2025-01-01T00:00:00Z", + "name": "Agent", + "disable_cache": False, + "cache_failed_jobs": False, + "organization_id": ORG_ID, + "engine_class_id": "engine", + "current_version_id": VERSION_ID, + "job_count": None, + "most_recent_job": None, + "engine_name": "Engine", + "tags": [], + } + + def _api(response: httpx.Response) -> tuple[AgentsAPI, MagicMock]: request = MagicMock(return_value=response) raw_client = MagicMock() @@ -56,6 +74,35 @@ def test_run_passes_idempotency_key_through_dynamic_wrapper(): assert job.id == JOB_ID +def test_agent_replace_uses_put_with_org_query_and_model_body(): + api, request = _api(httpx.Response(200, json=_base_agent_json())) + + result = api.replace(AGENT_ID, name="Renamed", disable_cache=True) + + kwargs = request.call_args.kwargs + assert kwargs["method"] == "put" + assert kwargs["params"] == {"organization_id": ORG_ID} + assert kwargs["json"] == {"name": "Renamed", "disable_cache": True} + assert result.id == UUID(AGENT_ID) + + +def test_agent_version_replace_uses_put_with_org_query_and_model_body(): + api, request = _api(httpx.Response(200, json={"message": "ok"})) + + result = api.versions.replace( + AGENT_ID, + VERSION_ID, + version_name="v2", + description="desc", + ) + + kwargs = request.call_args.kwargs + assert kwargs["method"] == "put" + assert kwargs["params"] == {"organization_id": ORG_ID} + assert kwargs["json"] == {"version_name": "v2", "description": "desc"} + assert result.message == "ok" + + def test_retrieve_status_parses_single_status_shape_without_id(): # GET /v1/agents/jobs/{job_id}/status/ returns AgentJobSingleStatus # ({status, timestamp, error_message?}) with no "id" field — parsing it diff --git a/tests/unit/test_policies_wrapper_transport.py b/tests/unit/test_policies_wrapper_transport.py new file mode 100644 index 0000000..4aa46f2 --- /dev/null +++ b/tests/unit/test_policies_wrapper_transport.py @@ -0,0 +1,46 @@ +"""Regression tests for public policy wrapper transport behavior.""" + +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import MagicMock +from uuid import UUID + +import httpx + +from roe.api.policies import PoliciesAPI + +ORG_ID = "00000000-0000-0000-0000-000000000123" +POLICY_ID = "00000000-0000-0000-0000-000000000444" + + +def _api(response: httpx.Response) -> tuple[PoliciesAPI, MagicMock]: + request = MagicMock(return_value=response) + raw_client = MagicMock() + raw_client.get_httpx_client.return_value = SimpleNamespace(request=request) + config = SimpleNamespace(organization_id=ORG_ID) + return PoliciesAPI(config, raw_client), request + + +def _update_policy_json() -> dict[str, object]: + return { + "id": POLICY_ID, + "name": "Policy", + "organization_id": ORG_ID, + "current_version_id": None, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z", + "description": "desc", + } + + +def test_policy_replace_uses_put_with_org_query_and_model_body(): + api, request = _api(httpx.Response(200, json=_update_policy_json())) + + result = api.replace(POLICY_ID, name="Policy", description="desc") + + kwargs = request.call_args.kwargs + assert kwargs["method"] == "put" + assert kwargs["params"] == {"organization_id": ORG_ID} + assert kwargs["json"] == {"name": "Policy", "description": "desc"} + assert result.id == UUID(POLICY_ID) From a454d040b6d603439a58dc83505fd6891507752c Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Wed, 17 Jun 2026 17:38:49 -0700 Subject: [PATCH 4/6] fix: simplify optional generated body fields --- scripts/generate_wrappers.py | 4 ++-- src/roe/api/connections.py | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/scripts/generate_wrappers.py b/scripts/generate_wrappers.py index e69b0f7..ac7c950 100644 --- a/scripts/generate_wrappers.py +++ b/scripts/generate_wrappers.py @@ -332,15 +332,15 @@ def _dict_body_lines( for param in body_params: name = param["name"] wire_name = param.get("wire_name", name) - value = _field_expr(param) if param.get("pass_unset_when_none"): lines.extend( [ f" if {name} is not None:\n", - f" body[{wire_name!r}] = {value}\n", + f" body[{wire_name!r}] = {name}\n", ] ) else: + value = _field_expr(param) lines.append(f" body[{wire_name!r}] = {value}\n") return lines diff --git a/src/roe/api/connections.py b/src/roe/api/connections.py index fa379d9..11cb533 100644 --- a/src/roe/api/connections.py +++ b/src/roe/api/connections.py @@ -74,9 +74,9 @@ def create( body["name"] = name body["config"] = config if description is not None: - body["description"] = description if description is not None else UNSET + body["description"] = description if auth_config is not None: - body["auth_config"] = auth_config if auth_config is not None else UNSET + body["auth_config"] = auth_config resp = request_json( self._raw, connections_create, @@ -96,7 +96,7 @@ def test_credentials( body["connector_type"] = connector_type body["config"] = config if auth_config is not None: - body["auth_config"] = auth_config if auth_config is not None else UNSET + body["auth_config"] = auth_config resp = request_json( self._raw, connections_test_credentials_create, @@ -129,13 +129,13 @@ def update( body: dict[str, Any] = {} body["organization_id"] = str(self._org_id) if name is not None: - body["name"] = name if name is not None else UNSET + body["name"] = name if description is not None: - body["description"] = description if description is not None else UNSET + body["description"] = description if config is not None: - body["config"] = config if config is not None else UNSET + body["config"] = config if auth_config is not None: - body["auth_config"] = auth_config if auth_config is not None else UNSET + body["auth_config"] = auth_config resp = request_json( self._raw, connections_partial_update, @@ -157,13 +157,13 @@ def replace( body: dict[str, Any] = {} body["organization_id"] = str(self._org_id) if name is not None: - body["name"] = name if name is not None else UNSET + body["name"] = name if description is not None: - body["description"] = description if description is not None else UNSET + body["description"] = description if config is not None: - body["config"] = config if config is not None else UNSET + body["config"] = config if auth_config is not None: - body["auth_config"] = auth_config if auth_config is not None else UNSET + body["auth_config"] = auth_config resp = request_json( self._raw, connections_update, From 084142b17e43aa36c45701a0cd3a13512352f1f0 Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Wed, 17 Jun 2026 20:26:02 -0700 Subject: [PATCH 5/6] fix: keep connection org id out of update bodies --- scripts/generate_wrappers.py | 2 +- src/roe/api/connections.py | 2 - .../test_connections_wrapper_transport.py | 92 +++++++++++++++++++ 3 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 tests/unit/test_connections_wrapper_transport.py diff --git a/scripts/generate_wrappers.py b/scripts/generate_wrappers.py index ac7c950..0df3960 100644 --- a/scripts/generate_wrappers.py +++ b/scripts/generate_wrappers.py @@ -327,7 +327,7 @@ def _dict_body_lines( if operation.get("body_format") != "dict": return [] lines = [" body: dict[str, Any] = {}\n"] - if operation.get("inject_organization_id"): + if operation.get("inject_organization_id") and operation.get("method") == "POST": lines.append(' body["organization_id"] = str(self._org_id)\n') for param in body_params: name = param["name"] diff --git a/src/roe/api/connections.py b/src/roe/api/connections.py index 11cb533..33d5618 100644 --- a/src/roe/api/connections.py +++ b/src/roe/api/connections.py @@ -127,7 +127,6 @@ def update( ) -> Connection: """Update mutable connection fields.""" body: dict[str, Any] = {} - body["organization_id"] = str(self._org_id) if name is not None: body["name"] = name if description is not None: @@ -155,7 +154,6 @@ def replace( ) -> Connection: """Replace a connection.""" body: dict[str, Any] = {} - body["organization_id"] = str(self._org_id) if name is not None: body["name"] = name if description is not None: diff --git a/tests/unit/test_connections_wrapper_transport.py b/tests/unit/test_connections_wrapper_transport.py new file mode 100644 index 0000000..7ece977 --- /dev/null +++ b/tests/unit/test_connections_wrapper_transport.py @@ -0,0 +1,92 @@ +"""Regression tests for public connection wrapper transport behavior.""" + +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import MagicMock +from uuid import UUID + +import httpx + +from roe.api.connections import ConnectionsAPI + +ORG_ID = "00000000-0000-0000-0000-000000000123" +CONNECTION_ID = "00000000-0000-0000-0000-000000000555" + + +def _connection_json() -> dict[str, object]: + return { + "id": CONNECTION_ID, + "user": None, + "organization": ORG_ID, + "connector_type": "salesforce", + "connector_display_name": "Salesforce", + "name": "CRM", + "auth_config": {}, + "created_at": "2025-01-01T00:00:00Z", + "updated_at": "2025-01-01T00:00:00Z", + "description": "desc", + "config": {"region": "us"}, + "status": "active", + } + + +def _api(response: httpx.Response) -> tuple[ConnectionsAPI, MagicMock]: + request = MagicMock(return_value=response) + raw_client = MagicMock() + raw_client.get_httpx_client.return_value = SimpleNamespace(request=request) + config = SimpleNamespace(organization_id=ORG_ID) + return ConnectionsAPI(config, raw_client), request + + +def test_connection_create_keeps_org_id_in_body_and_query(): + api, request = _api(httpx.Response(201, json=_connection_json())) + + result = api.create( + connector_type="salesforce", + name="CRM", + config={"region": "us"}, + ) + + kwargs = request.call_args.kwargs + assert kwargs["method"] == "post" + assert kwargs["params"] == {"organization_id": ORG_ID} + assert kwargs["json"] == { + "organization_id": ORG_ID, + "connector_type": "salesforce", + "name": "CRM", + "config": {"region": "us"}, + } + assert result.id == UUID(CONNECTION_ID) + + +def test_connection_update_keeps_org_id_out_of_patch_body(): + api, request = _api(httpx.Response(200, json=_connection_json())) + + result = api.update( + CONNECTION_ID, + name="CRM", + config={"region": "us"}, + ) + + kwargs = request.call_args.kwargs + assert kwargs["method"] == "patch" + assert kwargs["params"] == {"organization_id": ORG_ID} + assert kwargs["json"] == {"name": "CRM", "config": {"region": "us"}} + assert result.id == UUID(CONNECTION_ID) + + +def test_connection_replace_keeps_org_id_out_of_put_body(): + api, request = _api(httpx.Response(200, json=_connection_json())) + + result = api.replace( + CONNECTION_ID, + name="CRM", + config={"region": "us"}, + ) + + kwargs = request.call_args.kwargs + assert kwargs["method"] == "put" + assert kwargs["params"] == {"organization_id": ORG_ID} + assert kwargs["json"] == {"name": "CRM", "config": {"region": "us"}} + assert result.id == UUID(CONNECTION_ID) From 32e9279948bb2e322016eb21cab5be801f515f8b Mon Sep 17 00:00:00 2001 From: Jaden Fix Date: Wed, 17 Jun 2026 23:37:23 -0700 Subject: [PATCH 6/6] docs: fix Python SDK client examples --- README.md | 10 ++++++++-- examples/run_agent_with_timeout.py | 6 +++--- src/roe/client.py | 7 ++++--- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 275bb0d..49a48bf 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,8 @@ httpx layer. `Retry-After` is preserved exactly as sent, so it may be numeric seconds or an HTTP-date: ```python +try: + client.agents.retrieve("00000000-0000-0000-0000-000000000000") except RoeAPIException as exc: if exc.status_code == 429 and exc.headers: retry_after = exc.headers.get("retry-after") @@ -589,9 +591,13 @@ job = client.agents.run_version( url="https://example.com", ) -# Also works directly on agent and version models +# Reuse a retrieved agent id when building the run request agent = client.agents.retrieve("agent-uuid") -job = agent.run(metadata={"source": "sdk"}, url="https://example.com") +job = client.agents.run( + agent_id=str(agent.id), + metadata={"source": "sdk"}, + url="https://example.com", +) ``` ## Agent Management diff --git a/examples/run_agent_with_timeout.py b/examples/run_agent_with_timeout.py index 04baaf3..102625a 100755 --- a/examples/run_agent_with_timeout.py +++ b/examples/run_agent_with_timeout.py @@ -12,7 +12,7 @@ import os import time -from roe import RoeClient +from roe import JobStatus, RoeClient # Configuration - set these environment variables AGENT_ID = os.getenv("AGENT_ID", "your-agent-uuid-here") @@ -209,11 +209,11 @@ def example_checking_status_manually(): print(f" [{elapsed:.1f}s] Status: {status.status}") - if status.status in (2, 6): # SUCCESS or CACHED + if status.status in (JobStatus.SUCCESS, JobStatus.CACHED): job.retrieve_result() print("✓ Job completed successfully!") break - elif status.status in (3, 4): # FAILURE or CANCELLED + elif status.status in (JobStatus.FAILURE, JobStatus.CANCELLED): print("✗ Job failed or was cancelled") break diff --git a/src/roe/client.py b/src/roe/client.py index af3942d..daab0bd 100644 --- a/src/roe/client.py +++ b/src/roe/client.py @@ -120,9 +120,10 @@ def agents(self) -> AgentsAPI: prompt="Analyze this document" ) - # Get agent details and run - agent = client.agents.get("agent-uuid") - result = agent.run( + # Retrieve agent details and run by id + agent = client.agents.retrieve("agent-uuid") + result = client.agents.run( + agent_id=str(agent.id), document="path/to/file.pdf", prompt="Analyze this document" )