Skip to content

Commit 3e1699d

Browse files
committed
Merge branch 'refs/heads/claude/raw-response-8e1bbd' into feat/sdk-parity
# Conflicts: # CHANGELOG.md # docs/reference/clients.md
2 parents 1b898dc + c9ff787 commit 3e1699d

43 files changed

Lines changed: 5557 additions & 941 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Releases are published automatically on every merge to `main`. Release notes for
1616
- `OPENCODE_LOG=debug|info` sends the `opencode` logger to stderr at that level.
1717
- `DefaultAioHttpClient`: an optional aiohttp transport for `AsyncOpencode`, installed with the `aiohttp` extra
1818
(`pip install 'opencode-python-sdk[aiohttp]'`) and passed as `http_client=`.
19+
- `.with_raw_response` and `.with_streaming_response` on every resource (sync and async): return an
20+
`APIResponse` / `AsyncAPIResponse` with `headers`, `status_code`, `http_response`, `request`, `text`, `json()`,
21+
`iter_bytes()` / `iter_lines()` and `parse()` for the usual typed value.
22+
- `client.get` / `post` / `put` / `patch` / `delete` for endpoints the client has no method for.
1923

2024
## 0.1.0
2125

‎README.md‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,61 @@ Logs go to the `opencode` logger. `logging.getLogger("opencode").setLevel(loggin
453453
retry; credentials are never logged. Set `OPENCODE_LOG=debug` (or `info`) to have the SDK attach a stderr handler at
454454
that level on import, without touching your own logging config.
455455

456+
### Accessing raw response data
457+
458+
Prefix any method with `.with_raw_response` to get the HTTP response (headers, status, request) instead of the
459+
parsed value. `.parse()` returns what the plain call would have returned:
460+
461+
```python
462+
response = client.session.with_raw_response.get("ses_...")
463+
print(response.status_code, response.headers.get("content-type"))
464+
session = response.parse() # SessionInfo
465+
```
466+
467+
`.with_raw_response` reads the body before returning. `.with_streaming_response` does not: it is a context manager
468+
that sends the request on entry and closes the connection on exit, so you can read large bodies incrementally with
469+
`iter_bytes()`, `iter_text()` or `iter_lines()`, or still call `parse()`:
470+
471+
```python
472+
with client.file.with_streaming_response.read("big.log") as response:
473+
for line in response.iter_lines():
474+
...
475+
```
476+
477+
Both exist on every resource and sub-resource of both clients (`async with` and `await response.parse()` on
478+
`AsyncOpencode`). `APIResponse` also has `.http_response`, `.request`, `.text`, `.json()` and `.read()`.
479+
480+
- **List methods** give the response of the first page; `parse()` returns a `SyncCursorPage` / `AsyncCursorPage`
481+
that fetches the following pages as usual.
482+
- **Event streams** are never read eagerly: `parse()` returns the `Stream` / `AsyncStream`, which reads events as
483+
you iterate. Close the stream (or the response) when done.
484+
- Error statuses still raise `APIStatusError` subclasses; the response is on the exception as `error.response`.
485+
- Hand-written helpers such as `client.session.run()` and `client.pty.attach()` are not wrapped.
486+
487+
### Making custom/undocumented requests
488+
489+
Undocumented parameters go through `extra_headers`, `extra_query` and `extra_body` on every method (see above). For
490+
an endpoint the client has no method for, `client.get`, `post`, `put`, `patch` and `delete` send a request relative
491+
to `base_url` with the client's authentication, retries and typed errors, and return the decoded JSON:
492+
493+
```python
494+
data = client.post("/api/new-endpoint", body={"key": "value"}, query={"directory": "/repo"})
495+
496+
# Validate into a type, e.g. a response wrapped in {"data": ...}:
497+
from opencode.types import SessionInfo
498+
499+
payload = client.get("/api/session/ses_...", cast_to=dict[str, SessionInfo])
500+
```
501+
502+
### Determining the installed version
503+
504+
```python
505+
import opencode
506+
507+
print(opencode.__version__) # this package
508+
print(opencode.API_VERSION) # the OpenCode server version it was generated from
509+
```
510+
456511
## Versioning
457512

458513
| `opencode-python-sdk` (this package) | OpenCode server |

‎codegen/render.py‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,13 @@ def _render_method(operation: Operation, errors: dict[str, dict[str, Any]], *, i
359359
" )",
360360
]
361361
elif operation.kind == "void":
362-
lines += [f" {awaiting}self._client.request(", *request_options, ' kind="void",', " )"]
362+
# Returned so ``with_raw_response`` can hand back the response; a plain call returns ``None``.
363+
lines += [
364+
f' return cast("None", {awaiting}self._client.request(',
365+
*request_options,
366+
' kind="void",',
367+
" ))",
368+
]
363369
elif operation.kind == "binary":
364370
lines += [
365371
f' return cast("bytes", {awaiting}self._client.request(',
@@ -412,13 +418,67 @@ def _resource_classes(resource: Resource, errors: dict[str, dict[str, Any]], *,
412418
f' """Operations under ``client.{_attr_path(child)}``."""',
413419
f" return {prefix}{child.class_name}Resource(self._client)\n",
414420
]
421+
for kind, summary in _WRAPPERS:
422+
lines += [
423+
" @cached_property",
424+
f" def {kind}(self) -> {prefix}{resource.class_name}Resource{_WRAPPER_SUFFIX[kind]}:",
425+
f' """{summary}"""',
426+
f" return {prefix}{resource.class_name}Resource{_WRAPPER_SUFFIX[kind]}(self)\n",
427+
]
415428
for operation in resource.operations:
416429
lines += _render_method(operation, errors, is_async=is_async)
417430
lines.append("")
418431
blocks.append("\n".join(lines).rstrip() + "\n")
432+
for kind, _ in _WRAPPERS:
433+
blocks.append(_wrapper_class(resource, kind, is_async=is_async))
419434
return blocks
420435

421436

437+
_WRAPPERS = (
438+
(
439+
"with_raw_response",
440+
"Same methods, returning the raw HTTP response; call ``.parse()`` on it for the usual value.",
441+
),
442+
(
443+
"with_streaming_response",
444+
"Same methods, returning a context manager over the HTTP response without reading the body.",
445+
),
446+
)
447+
_WRAPPER_SUFFIX = {"with_raw_response": "WithRawResponse", "with_streaming_response": "WithStreamingResponse"}
448+
_WRAPPER_FUNCTION = {
449+
("with_raw_response", False): "to_raw_response_wrapper",
450+
("with_streaming_response", False): "to_streamed_response_wrapper",
451+
("with_raw_response", True): "async_to_raw_response_wrapper",
452+
("with_streaming_response", True): "async_to_streamed_response_wrapper",
453+
}
454+
455+
456+
def _wrapper_class(resource: Resource, kind: str, *, is_async: bool) -> str:
457+
prefix = "Async" if is_async else ""
458+
suffix = _WRAPPER_SUFFIX[kind]
459+
name = f"{prefix}{resource.class_name}Resource"
460+
wrap = _WRAPPER_FUNCTION[(kind, is_async)]
461+
lines = [
462+
f"class {name}{suffix}:",
463+
f' """``client.{_attr_path(resource)}.{kind}``."""\n',
464+
f" def __init__(self, resource: {name}) -> None:",
465+
" self._resource = resource",
466+
*[
467+
f" self.{operation.method_name} = {wrap}(resource.{operation.method_name})"
468+
for operation in resource.operations
469+
],
470+
"",
471+
]
472+
for child_name, child in resource.children.items():
473+
lines += [
474+
" @cached_property",
475+
f" def {child_name}(self) -> {prefix}{child.class_name}Resource{suffix}:",
476+
f' """``client.{_attr_path(child)}.{kind}``."""',
477+
f" return {prefix}{child.class_name}Resource{suffix}(self._resource.{child_name})\n",
478+
]
479+
return "\n".join(lines).rstrip() + "\n"
480+
481+
422482
def _attr_path(resource: Resource) -> str:
423483
return ".".join(safe_identifier(snake(part)) for part in resource.path)
424484

@@ -478,6 +538,12 @@ def render_resource_module(
478538
*[f"from ..{module} import {', '.join(sorted({sync, async_}))}" for module, sync, async_ in _mixins(resource)],
479539
*([f"from .._qs import {', '.join(runtime)}"] if runtime else []),
480540
"from .._resource import AsyncAPIResource, SyncAPIResource",
541+
"from .._response import (",
542+
" async_to_raw_response_wrapper,",
543+
" async_to_streamed_response_wrapper,",
544+
" to_raw_response_wrapper,",
545+
" to_streamed_response_wrapper,",
546+
")",
481547
*(["from .._streaming import AsyncStream, Stream"] if any(op.kind == "stream" for op in operations) else []),
482548
"from .._types import NOT_GIVEN, Body, Headers, NotGiven, Query, Timeout"
483549
+ (", OpenEnum" if "OpenEnum" in uses else ""),

‎docs/hooks/types_reference.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,11 @@ def resources_page() -> str:
121121
if path.name.startswith("_"):
122122
continue
123123
tree = ast.parse(path.read_text())
124-
classes = [n.name for n in tree.body if isinstance(n, ast.ClassDef) and not n.name.startswith("Async")]
124+
classes = [
125+
n.name
126+
for n in tree.body
127+
if isinstance(n, ast.ClassDef) and not n.name.startswith("Async") and n.name.endswith("Resource")
128+
]
125129
for cls in classes:
126130
lines += [f"::: opencode.resources.{path.stem}.{cls}", " options:", " show_bases: false", ""]
127131
return "\n".join(lines)

‎docs/reference/clients.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
::: opencode.Opencode
44
options:
5-
members: [with_options, copy, close, wait_until_ready, fake]
5+
members: [with_options, copy, close, wait_until_ready, fake, get, post, put, patch, delete]
66
show_bases: false
77

88
::: opencode.AsyncOpencode
99
options:
10-
members: [with_options, copy, close, wait_until_ready, fake]
10+
members: [with_options, copy, close, wait_until_ready, fake, get, post, put, patch, delete]
1111
show_bases: false
1212

1313
::: opencode.DefaultAioHttpClient

‎docs/reference/index.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Generated from the docstrings in `src/opencode`.
88
| [Server launcher](server.md) | `OpencodeServer`, `AsyncOpencodeServer` |
99
| [Background service](service.md) | `opencode.service`, `Endpoint`, `ServiceError` |
1010
| [Resources](resources.md) | Every `client.<group>` and its methods |
11-
| [Pagination and streams](pagination.md) | `SyncCursorPage`, `AsyncPaginator`, `Stream`, `AsyncStream` |
11+
| [Pagination and streams](pagination.md) | `SyncCursorPage`, `AsyncPaginator`, `Stream`, `AsyncStream`, `APIResponse` |
1212
| [Shared events and RPC](events.md) | `EventHub`, `Subscription`, `RpcDefinition`, `RpcClient` |
1313
| [Exceptions](exceptions.md) | Status errors and every declared server error |
1414
| [Types](types/index.md) | The models in `opencode.types`, grouped by domain |

‎docs/reference/pagination.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,12 @@
99
::: opencode.Stream
1010

1111
::: opencode.AsyncStream
12+
13+
## Raw responses
14+
15+
Returned by `with_raw_response` and `with_streaming_response` on every resource. See
16+
[Accessing raw response data](https://github.com/shrijayan/opencode-python-sdk#accessing-raw-response-data).
17+
18+
::: opencode.APIResponse
19+
20+
::: opencode.AsyncAPIResponse

‎src/opencode/__init__.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
from ._models import BaseModel
8181
from ._pagination import AsyncCursorPage, AsyncPaginator, SyncCursorPage
8282
from ._pty import AsyncPtyConnection, PtyConnection, PtyConnectionError
83+
from ._response import APIResponse, AsyncAPIResponse
8384
from ._server import AsyncOpencodeServer, OpencodeServer, ServerStartError
8485
from ._service import Endpoint, ServiceError
8586
from ._streaming import AsyncStream, Stream
@@ -125,10 +126,12 @@
125126
"API_VERSION",
126127
"NOT_GIVEN",
127128
"APIConnectionError",
129+
"APIResponse",
128130
"APIError",
129131
"APIResponseValidationError",
130132
"APIStatusError",
131133
"APITimeoutError",
134+
"AsyncAPIResponse",
132135
"AsyncCursorPage",
133136
"AsyncOpencode",
134137
"AsyncOpencodeServer",

0 commit comments

Comments
 (0)