opencode-python-sdk 0.2.0__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/CHANGELOG.md +9 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/PKG-INFO +111 -2
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/README.md +108 -1
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/pyproject.toml +8 -2
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/__init__.py +14 -0
- opencode_python_sdk-0.2.3/src/opencode/_aiohttp.py +44 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_base_client.py +237 -11
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_client.py +115 -3
- opencode_python_sdk-0.2.3/src/opencode/_logs.py +29 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_pagination.py +14 -0
- opencode_python_sdk-0.2.3/src/opencode/_response.py +316 -0
- opencode_python_sdk-0.2.3/src/opencode/_service.py +1018 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/agent.py +63 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/command.py +59 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/config.py +93 -20
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/credential.py +149 -56
- opencode_python_sdk-0.2.3/src/opencode/resources/debug.py +314 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/event.py +59 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/experimental.py +210 -36
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/file.py +71 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/form.py +59 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/generate.py +59 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/integration.py +538 -128
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/location.py +87 -18
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/mcp.py +252 -82
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/message.py +59 -0
- opencode_python_sdk-0.2.3/src/opencode/resources/migration.py +244 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/model.py +63 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/permission.py +272 -38
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/plugin.py +95 -22
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/project.py +63 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/provider.py +63 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/pty.py +172 -20
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/reference.py +59 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/rpc.py +59 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/server.py +67 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/session.py +1159 -405
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/shell.py +101 -20
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/skill.py +59 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/vcs.py +172 -20
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/voice.py +59 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/websearch.py +63 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/worktree.py +123 -40
- opencode_python_sdk-0.2.3/src/opencode/service.py +50 -0
- opencode_python_sdk-0.2.3/tests/fake_service.py +77 -0
- opencode_python_sdk-0.2.3/tests/test_aiohttp.py +131 -0
- opencode_python_sdk-0.2.3/tests/test_logs.py +45 -0
- opencode_python_sdk-0.2.3/tests/test_response.py +155 -0
- opencode_python_sdk-0.2.3/tests/test_service.py +288 -0
- opencode_python_sdk-0.2.3/tests/test_service_live.py +67 -0
- opencode_python_sdk-0.2.3/tests/test_with_options.py +76 -0
- opencode_python_sdk-0.2.0/src/opencode/resources/debug.py +0 -178
- opencode_python_sdk-0.2.0/src/opencode/resources/migration.py +0 -118
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/.gitignore +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/LICENSE +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/api.md +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/.generated.json +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_constants.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_errors.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_events.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_exceptions.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_models.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_pty.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_qs.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_resource.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_rpc.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_server.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_streaming.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_types.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_unions.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/_version.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/lib/__init__.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/lib/_ready.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/lib/_session.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/py.typed +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/resources/__init__.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/testing.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/types/__init__.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/src/opencode/types/_generated.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/tests/__init__.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/tests/conftest.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/tests/test_async_client.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/tests/test_client.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/tests/test_docs_coverage.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/tests/test_encoding.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/tests/test_events.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/tests/test_helpers.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/tests/test_live.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/tests/test_models.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/tests/test_pagination.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/tests/test_pty.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/tests/test_rpc.py +0 -0
- {opencode_python_sdk-0.2.0 → opencode_python_sdk-0.2.3}/tests/test_streaming.py +0 -0
|
@@ -11,6 +11,15 @@ Releases are published automatically on every merge to `main`. Release notes for
|
|
|
11
11
|
- `client.rpc(Definition)`: typed plugin RPC calls and event subscriptions declared with `RpcDefinition`,
|
|
12
12
|
`rpc_method` and `rpc_event`; RPC failures raise `RpcCallError` / `RpcCallInternalError`.
|
|
13
13
|
- `Stream.on_activity` and `Stream.interrupt()`.
|
|
14
|
+
- `client.copy(...)`, an alias of `client.with_options(...)`; documented that copies share the connection pool
|
|
15
|
+
and do not close it.
|
|
16
|
+
- `OPENCODE_LOG=debug|info` sends the `opencode` logger to stderr at that level.
|
|
17
|
+
- `DefaultAioHttpClient`: an optional aiohttp transport for `AsyncOpencode`, installed with the `aiohttp` extra
|
|
18
|
+
(`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.
|
|
14
23
|
|
|
15
24
|
## 0.1.0
|
|
16
25
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: opencode-python-sdk
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Typed Python client for the OpenCode 2.x server API
|
|
5
5
|
Project-URL: Homepage, https://github.com/shrijayan/opencode-python-sdk
|
|
6
6
|
Project-URL: Documentation, https://shrijayan.github.io/opencode-python-sdk/
|
|
@@ -31,6 +31,8 @@ Requires-Dist: anyio<5,>=4.4
|
|
|
31
31
|
Requires-Dist: httpx<1,>=0.27
|
|
32
32
|
Requires-Dist: pydantic<3,>=2.12
|
|
33
33
|
Requires-Dist: typing-extensions>=4.12
|
|
34
|
+
Provides-Extra: aiohttp
|
|
35
|
+
Requires-Dist: httpx-aiohttp<0.3,>=0.1.9; extra == 'aiohttp'
|
|
34
36
|
Provides-Extra: pty
|
|
35
37
|
Requires-Dist: websockets<17,>=14; extra == 'pty'
|
|
36
38
|
Description-Content-Type: text/markdown
|
|
@@ -116,6 +118,27 @@ async def main() -> None:
|
|
|
116
118
|
asyncio.run(main())
|
|
117
119
|
```
|
|
118
120
|
|
|
121
|
+
### aiohttp transport
|
|
122
|
+
|
|
123
|
+
For high-concurrency asyncio code you can swap httpx's networking for [aiohttp](https://docs.aiohttp.org). Install
|
|
124
|
+
the extra and pass `DefaultAioHttpClient` as `http_client`:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install 'opencode-python-sdk[aiohttp]'
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from opencode import AsyncOpencode, DefaultAioHttpClient
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def main() -> None:
|
|
135
|
+
async with DefaultAioHttpClient() as http, AsyncOpencode("http://127.0.0.1:4096", http_client=http) as client:
|
|
136
|
+
info = await client.server.info()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Retries, timeouts and event streams work the same. aiohttp runs on asyncio only, not trio. The client does not close
|
|
140
|
+
an `http_client` you pass in, so close it yourself as above.
|
|
141
|
+
|
|
119
142
|
## Calling the API
|
|
120
143
|
|
|
121
144
|
Methods mirror the official TypeScript client: `client.<group>.<operation>()`, with names in `snake_case`. Path
|
|
@@ -350,6 +373,15 @@ client.session.get("ses_...", timeout=5.0) # per call
|
|
|
350
373
|
|
|
351
374
|
The default timeout is 10 minutes with a 10 second connect timeout.
|
|
352
375
|
|
|
376
|
+
`with_options` (alias `copy`) returns a client with some options changed: `timeout`, `max_retries`, `base_url`,
|
|
377
|
+
`password`, `directory`, `default_headers` and `default_query` (headers and query are merged over the current
|
|
378
|
+
ones). The copy shares the original's connection pool, and closing it leaves the pool open.
|
|
379
|
+
|
|
380
|
+
```python
|
|
381
|
+
client.with_options(max_retries=0).session.create() # no retries for this call
|
|
382
|
+
scoped = client.with_options(directory="/other/repo", default_headers={"X-Trace": "1"})
|
|
383
|
+
```
|
|
384
|
+
|
|
353
385
|
## Running an agent headless
|
|
354
386
|
|
|
355
387
|
`client.session.run` does the usual "prompt an agent and wait" round trip in one call: it creates a session (or
|
|
@@ -419,6 +451,27 @@ with server:
|
|
|
419
451
|
The server runs in `--stdio` mode, so it also exits when the Python process dies. `AsyncOpencodeServer` is the
|
|
420
452
|
async equivalent.
|
|
421
453
|
|
|
454
|
+
### The background service
|
|
455
|
+
|
|
456
|
+
OpenCode can also run one shared background server per user (`opencode serve --service`), which registers itself in
|
|
457
|
+
`$XDG_STATE_HOME/opencode/service.json`. `Opencode.from_service()` connects to it, starting it first if it is not
|
|
458
|
+
running:
|
|
459
|
+
|
|
460
|
+
```python
|
|
461
|
+
from opencode import Opencode, service
|
|
462
|
+
|
|
463
|
+
with Opencode.from_service(version="2.1.2") as client: # replaces a service running another version
|
|
464
|
+
print(client.server.info().version)
|
|
465
|
+
|
|
466
|
+
endpoint = service.discover() # an Endpoint, or None; never starts anything
|
|
467
|
+
service.stop(pty="handoff") # hand persistent terminals to the next service instead of closing them
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
`service.ensure()` returns the `Endpoint` (URL and credentials) without creating a client, and `service.headers()`
|
|
471
|
+
gives its `Authorization` header for other HTTP clients. Concurrent callers are safe: they race to start the service
|
|
472
|
+
and all get the one that won. `await AsyncOpencode.from_service()`, `service.async_discover()`,
|
|
473
|
+
`service.async_ensure()` and `service.async_stop()` are the async equivalents.
|
|
474
|
+
|
|
422
475
|
## Advanced
|
|
423
476
|
|
|
424
477
|
```python
|
|
@@ -436,7 +489,63 @@ client.session.create(title="t", extra_headers={"X-Trace": "1"}, extra_body={"fu
|
|
|
436
489
|
```
|
|
437
490
|
|
|
438
491
|
Logs go to the `opencode` logger. `logging.getLogger("opencode").setLevel(logging.DEBUG)` shows every request and
|
|
439
|
-
retry; credentials are never logged.
|
|
492
|
+
retry; credentials are never logged. Set `OPENCODE_LOG=debug` (or `info`) to have the SDK attach a stderr handler at
|
|
493
|
+
that level on import, without touching your own logging config.
|
|
494
|
+
|
|
495
|
+
### Accessing raw response data
|
|
496
|
+
|
|
497
|
+
Prefix any method with `.with_raw_response` to get the HTTP response (headers, status, request) instead of the
|
|
498
|
+
parsed value. `.parse()` returns what the plain call would have returned:
|
|
499
|
+
|
|
500
|
+
```python
|
|
501
|
+
response = client.session.with_raw_response.get("ses_...")
|
|
502
|
+
print(response.status_code, response.headers.get("content-type"))
|
|
503
|
+
session = response.parse() # SessionInfo
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
`.with_raw_response` reads the body before returning. `.with_streaming_response` does not: it is a context manager
|
|
507
|
+
that sends the request on entry and closes the connection on exit, so you can read large bodies incrementally with
|
|
508
|
+
`iter_bytes()`, `iter_text()` or `iter_lines()`, or still call `parse()`:
|
|
509
|
+
|
|
510
|
+
```python
|
|
511
|
+
with client.file.with_streaming_response.read("big.log") as response:
|
|
512
|
+
for line in response.iter_lines():
|
|
513
|
+
...
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
Both exist on every resource and sub-resource of both clients (`async with` and `await response.parse()` on
|
|
517
|
+
`AsyncOpencode`). `APIResponse` also has `.http_response`, `.request`, `.text`, `.json()` and `.read()`.
|
|
518
|
+
|
|
519
|
+
- **List methods** give the response of the first page; `parse()` returns a `SyncCursorPage` / `AsyncCursorPage`
|
|
520
|
+
that fetches the following pages as usual.
|
|
521
|
+
- **Event streams** are never read eagerly: `parse()` returns the `Stream` / `AsyncStream`, which reads events as
|
|
522
|
+
you iterate. Close the stream (or the response) when done.
|
|
523
|
+
- Error statuses still raise `APIStatusError` subclasses; the response is on the exception as `error.response`.
|
|
524
|
+
- Hand-written helpers such as `client.session.run()` and `client.pty.attach()` are not wrapped.
|
|
525
|
+
|
|
526
|
+
### Making custom/undocumented requests
|
|
527
|
+
|
|
528
|
+
Undocumented parameters go through `extra_headers`, `extra_query` and `extra_body` on every method (see above). For
|
|
529
|
+
an endpoint the client has no method for, `client.get`, `post`, `put`, `patch` and `delete` send a request relative
|
|
530
|
+
to `base_url` with the client's authentication, retries and typed errors, and return the decoded JSON:
|
|
531
|
+
|
|
532
|
+
```python
|
|
533
|
+
data = client.post("/api/new-endpoint", body={"key": "value"}, query={"directory": "/repo"})
|
|
534
|
+
|
|
535
|
+
# Validate into a type, e.g. a response wrapped in {"data": ...}:
|
|
536
|
+
from opencode.types import SessionInfo
|
|
537
|
+
|
|
538
|
+
payload = client.get("/api/session/ses_...", cast_to=dict[str, SessionInfo])
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### Determining the installed version
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
import opencode
|
|
545
|
+
|
|
546
|
+
print(opencode.__version__) # this package
|
|
547
|
+
print(opencode.API_VERSION) # the OpenCode server version it was generated from
|
|
548
|
+
```
|
|
440
549
|
|
|
441
550
|
## Versioning
|
|
442
551
|
|
|
@@ -79,6 +79,27 @@ async def main() -> None:
|
|
|
79
79
|
asyncio.run(main())
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
+
### aiohttp transport
|
|
83
|
+
|
|
84
|
+
For high-concurrency asyncio code you can swap httpx's networking for [aiohttp](https://docs.aiohttp.org). Install
|
|
85
|
+
the extra and pass `DefaultAioHttpClient` as `http_client`:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install 'opencode-python-sdk[aiohttp]'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from opencode import AsyncOpencode, DefaultAioHttpClient
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def main() -> None:
|
|
96
|
+
async with DefaultAioHttpClient() as http, AsyncOpencode("http://127.0.0.1:4096", http_client=http) as client:
|
|
97
|
+
info = await client.server.info()
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Retries, timeouts and event streams work the same. aiohttp runs on asyncio only, not trio. The client does not close
|
|
101
|
+
an `http_client` you pass in, so close it yourself as above.
|
|
102
|
+
|
|
82
103
|
## Calling the API
|
|
83
104
|
|
|
84
105
|
Methods mirror the official TypeScript client: `client.<group>.<operation>()`, with names in `snake_case`. Path
|
|
@@ -313,6 +334,15 @@ client.session.get("ses_...", timeout=5.0) # per call
|
|
|
313
334
|
|
|
314
335
|
The default timeout is 10 minutes with a 10 second connect timeout.
|
|
315
336
|
|
|
337
|
+
`with_options` (alias `copy`) returns a client with some options changed: `timeout`, `max_retries`, `base_url`,
|
|
338
|
+
`password`, `directory`, `default_headers` and `default_query` (headers and query are merged over the current
|
|
339
|
+
ones). The copy shares the original's connection pool, and closing it leaves the pool open.
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
client.with_options(max_retries=0).session.create() # no retries for this call
|
|
343
|
+
scoped = client.with_options(directory="/other/repo", default_headers={"X-Trace": "1"})
|
|
344
|
+
```
|
|
345
|
+
|
|
316
346
|
## Running an agent headless
|
|
317
347
|
|
|
318
348
|
`client.session.run` does the usual "prompt an agent and wait" round trip in one call: it creates a session (or
|
|
@@ -382,6 +412,27 @@ with server:
|
|
|
382
412
|
The server runs in `--stdio` mode, so it also exits when the Python process dies. `AsyncOpencodeServer` is the
|
|
383
413
|
async equivalent.
|
|
384
414
|
|
|
415
|
+
### The background service
|
|
416
|
+
|
|
417
|
+
OpenCode can also run one shared background server per user (`opencode serve --service`), which registers itself in
|
|
418
|
+
`$XDG_STATE_HOME/opencode/service.json`. `Opencode.from_service()` connects to it, starting it first if it is not
|
|
419
|
+
running:
|
|
420
|
+
|
|
421
|
+
```python
|
|
422
|
+
from opencode import Opencode, service
|
|
423
|
+
|
|
424
|
+
with Opencode.from_service(version="2.1.2") as client: # replaces a service running another version
|
|
425
|
+
print(client.server.info().version)
|
|
426
|
+
|
|
427
|
+
endpoint = service.discover() # an Endpoint, or None; never starts anything
|
|
428
|
+
service.stop(pty="handoff") # hand persistent terminals to the next service instead of closing them
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
`service.ensure()` returns the `Endpoint` (URL and credentials) without creating a client, and `service.headers()`
|
|
432
|
+
gives its `Authorization` header for other HTTP clients. Concurrent callers are safe: they race to start the service
|
|
433
|
+
and all get the one that won. `await AsyncOpencode.from_service()`, `service.async_discover()`,
|
|
434
|
+
`service.async_ensure()` and `service.async_stop()` are the async equivalents.
|
|
435
|
+
|
|
385
436
|
## Advanced
|
|
386
437
|
|
|
387
438
|
```python
|
|
@@ -399,7 +450,63 @@ client.session.create(title="t", extra_headers={"X-Trace": "1"}, extra_body={"fu
|
|
|
399
450
|
```
|
|
400
451
|
|
|
401
452
|
Logs go to the `opencode` logger. `logging.getLogger("opencode").setLevel(logging.DEBUG)` shows every request and
|
|
402
|
-
retry; credentials are never logged.
|
|
453
|
+
retry; credentials are never logged. Set `OPENCODE_LOG=debug` (or `info`) to have the SDK attach a stderr handler at
|
|
454
|
+
that level on import, without touching your own logging config.
|
|
455
|
+
|
|
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
|
+
```
|
|
403
510
|
|
|
404
511
|
## Versioning
|
|
405
512
|
|
|
@@ -43,21 +43,27 @@ Changelog = "https://github.com/shrijayan/opencode-python-sdk/blob/main/CHANGELO
|
|
|
43
43
|
pty = [
|
|
44
44
|
"websockets>=14,<17",
|
|
45
45
|
]
|
|
46
|
+
aiohttp = [
|
|
47
|
+
"httpx-aiohttp>=0.1.9,<0.3",
|
|
48
|
+
]
|
|
46
49
|
|
|
47
50
|
[dependency-groups]
|
|
48
51
|
dev = [
|
|
49
52
|
"mypy>=1.18",
|
|
50
53
|
"pyright>=1.1.408",
|
|
51
54
|
"pytest>=8.4",
|
|
55
|
+
"httpx-aiohttp>=0.1.9",
|
|
52
56
|
"pytest-cov>=7",
|
|
53
57
|
"respx>=0.22",
|
|
54
58
|
"ruff>=0.14",
|
|
55
59
|
"trio>=0.31",
|
|
56
60
|
"websockets>=14",
|
|
57
61
|
]
|
|
62
|
+
# Pillow (via [imaging]) only ships patched releases for Python 3.11+, so docs builds need 3.11+.
|
|
58
63
|
docs = [
|
|
59
|
-
"mkdocs-material[imaging]>=9.6",
|
|
60
|
-
"mkdocstrings[python]>=0.30",
|
|
64
|
+
"mkdocs-material[imaging]>=9.6; python_version >= '3.11'",
|
|
65
|
+
"mkdocstrings[python]>=0.30; python_version >= '3.11'",
|
|
66
|
+
"pillow>=13; python_version >= '3.11'",
|
|
61
67
|
]
|
|
62
68
|
|
|
63
69
|
[build-system]
|
|
@@ -16,6 +16,8 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
from typing import TYPE_CHECKING, Any
|
|
18
18
|
|
|
19
|
+
from . import service
|
|
20
|
+
from ._aiohttp import DefaultAioHttpClient
|
|
19
21
|
from ._client import AsyncOpencode, Opencode
|
|
20
22
|
|
|
21
23
|
# <generated:error-imports>
|
|
@@ -74,10 +76,13 @@ from ._exceptions import (
|
|
|
74
76
|
ServiceUnavailableError,
|
|
75
77
|
UnprocessableEntityError,
|
|
76
78
|
)
|
|
79
|
+
from ._logs import setup_logging as _setup_logging
|
|
77
80
|
from ._models import BaseModel
|
|
78
81
|
from ._pagination import AsyncCursorPage, AsyncPaginator, SyncCursorPage
|
|
79
82
|
from ._pty import AsyncPtyConnection, PtyConnection, PtyConnectionError
|
|
83
|
+
from ._response import APIResponse, AsyncAPIResponse
|
|
80
84
|
from ._server import AsyncOpencodeServer, OpencodeServer, ServerStartError
|
|
85
|
+
from ._service import Endpoint, ServiceError
|
|
81
86
|
from ._streaming import AsyncStream, Stream
|
|
82
87
|
from ._types import NOT_GIVEN, NotGiven
|
|
83
88
|
from ._version import __version__
|
|
@@ -121,10 +126,12 @@ __all__ = [
|
|
|
121
126
|
"API_VERSION",
|
|
122
127
|
"NOT_GIVEN",
|
|
123
128
|
"APIConnectionError",
|
|
129
|
+
"APIResponse",
|
|
124
130
|
"APIError",
|
|
125
131
|
"APIResponseValidationError",
|
|
126
132
|
"APIStatusError",
|
|
127
133
|
"APITimeoutError",
|
|
134
|
+
"AsyncAPIResponse",
|
|
128
135
|
"AsyncCursorPage",
|
|
129
136
|
"AsyncOpencode",
|
|
130
137
|
"AsyncOpencodeServer",
|
|
@@ -136,6 +143,8 @@ __all__ = [
|
|
|
136
143
|
"BaseModel",
|
|
137
144
|
"ConflictError",
|
|
138
145
|
"ContentTooLargeError",
|
|
146
|
+
"DefaultAioHttpClient",
|
|
147
|
+
"Endpoint",
|
|
139
148
|
"InternalServerError",
|
|
140
149
|
"NotFoundError",
|
|
141
150
|
"NotGiven",
|
|
@@ -148,11 +157,13 @@ __all__ = [
|
|
|
148
157
|
"PtyConnectionError",
|
|
149
158
|
"RateLimitError",
|
|
150
159
|
"ServerStartError",
|
|
160
|
+
"ServiceError",
|
|
151
161
|
"ServiceUnavailableError",
|
|
152
162
|
"Stream",
|
|
153
163
|
"SyncCursorPage",
|
|
154
164
|
"UnprocessableEntityError",
|
|
155
165
|
"__version__",
|
|
166
|
+
"service",
|
|
156
167
|
"types",
|
|
157
168
|
# Shared event stream and typed plugin RPC
|
|
158
169
|
"SUBSCRIBER_CAPACITY",
|
|
@@ -219,3 +230,6 @@ def __getattr__(name: str) -> Any:
|
|
|
219
230
|
|
|
220
231
|
return importlib.import_module(".types", __name__)
|
|
221
232
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
_setup_logging()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Optional aiohttp-backed HTTP client for `AsyncOpencode`.
|
|
2
|
+
|
|
3
|
+
Install the extra with ``pip install 'opencode-python-sdk[aiohttp]'`` and pass the client in::
|
|
4
|
+
|
|
5
|
+
async with AsyncOpencode(http_client=DefaultAioHttpClient()) as client:
|
|
6
|
+
...
|
|
7
|
+
|
|
8
|
+
The SDK still speaks httpx: ``httpx-aiohttp`` swaps the transport underneath, so retries, timeouts and event
|
|
9
|
+
streams behave the same. aiohttp only runs on asyncio, so this client does not work under trio.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from . import _constants as constants
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
_Base = httpx.AsyncClient
|
|
22
|
+
else:
|
|
23
|
+
try:
|
|
24
|
+
from httpx_aiohttp import HttpxAiohttpClient as _Base
|
|
25
|
+
except ImportError: # pragma: no cover - only without the extra
|
|
26
|
+
_Base = httpx.AsyncClient
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DefaultAioHttpClient(_Base):
|
|
30
|
+
"""aiohttp-backed `httpx.AsyncClient` with the SDK's default connection limits and redirect handling.
|
|
31
|
+
|
|
32
|
+
Pass it to `AsyncOpencode` as ``http_client``. Keyword arguments go to `httpx.AsyncClient`. The SDK does not
|
|
33
|
+
close a client it was given, so close it yourself (``async with`` it) when you are done. Creating one without
|
|
34
|
+
the ``aiohttp`` extra installed raises `RuntimeError`.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
38
|
+
if _Base is httpx.AsyncClient:
|
|
39
|
+
raise RuntimeError(
|
|
40
|
+
"DefaultAioHttpClient needs the aiohttp extra: pip install 'opencode-python-sdk[aiohttp]'"
|
|
41
|
+
)
|
|
42
|
+
kwargs.setdefault("limits", constants.DEFAULT_CONNECTION_LIMITS)
|
|
43
|
+
kwargs.setdefault("follow_redirects", True)
|
|
44
|
+
super().__init__(**kwargs)
|