opencode-python-sdk 0.1.3__tar.gz → 0.2.2__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.1.3 → opencode_python_sdk-0.2.2}/PKG-INFO +71 -1
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/README.md +70 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/pyproject.toml +4 -2
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/__init__.py +19 -1
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_base_client.py +24 -3
- opencode_python_sdk-0.2.2/src/opencode/_client.py +361 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_events.py +5 -2
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_rpc.py +9 -2
- opencode_python_sdk-0.2.2/src/opencode/_service.py +1018 -0
- opencode_python_sdk-0.2.2/src/opencode/lib/__init__.py +5 -0
- opencode_python_sdk-0.2.2/src/opencode/lib/_ready.py +91 -0
- opencode_python_sdk-0.2.2/src/opencode/lib/_session.py +231 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/agent.py +13 -11
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/command.py +9 -7
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/config.py +12 -10
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/credential.py +13 -11
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/debug.py +9 -7
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/event.py +9 -5
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/experimental.py +31 -29
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/file.py +15 -13
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/form.py +9 -7
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/generate.py +9 -7
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/integration.py +28 -26
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/location.py +9 -7
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/mcp.py +13 -11
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/message.py +9 -8
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/migration.py +10 -12
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/model.py +12 -10
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/permission.py +29 -27
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/plugin.py +12 -10
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/project.py +14 -12
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/provider.py +13 -11
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/pty.py +23 -21
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/reference.py +9 -7
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/rpc.py +9 -7
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/server.py +14 -12
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/session.py +92 -90
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/shell.py +19 -17
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/skill.py +9 -7
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/vcs.py +22 -20
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/voice.py +9 -7
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/websearch.py +13 -11
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/worktree.py +13 -11
- opencode_python_sdk-0.2.2/src/opencode/service.py +50 -0
- opencode_python_sdk-0.2.2/src/opencode/testing.py +508 -0
- opencode_python_sdk-0.2.2/tests/fake_service.py +77 -0
- opencode_python_sdk-0.2.2/tests/test_helpers.py +252 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/tests/test_live.py +22 -1
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/tests/test_models.py +9 -0
- opencode_python_sdk-0.2.2/tests/test_service.py +288 -0
- opencode_python_sdk-0.2.2/tests/test_service_live.py +67 -0
- opencode_python_sdk-0.1.3/src/opencode/_client.py +0 -190
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/.gitignore +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/CHANGELOG.md +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/LICENSE +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/api.md +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/.generated.json +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_constants.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_errors.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_exceptions.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_models.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_pagination.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_pty.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_qs.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_resource.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_server.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_streaming.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_types.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_unions.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/_version.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/py.typed +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/resources/__init__.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/types/__init__.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/src/opencode/types/_generated.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/tests/__init__.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/tests/conftest.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/tests/test_async_client.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/tests/test_client.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/tests/test_docs_coverage.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/tests/test_encoding.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/tests/test_events.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/tests/test_pagination.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/tests/test_pty.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/tests/test_rpc.py +0 -0
- {opencode_python_sdk-0.1.3 → opencode_python_sdk-0.2.2}/tests/test_streaming.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: opencode-python-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.2
|
|
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/
|
|
@@ -350,6 +350,55 @@ client.session.get("ses_...", timeout=5.0) # per call
|
|
|
350
350
|
|
|
351
351
|
The default timeout is 10 minutes with a 10 second connect timeout.
|
|
352
352
|
|
|
353
|
+
## Running an agent headless
|
|
354
|
+
|
|
355
|
+
`client.session.run` does the usual "prompt an agent and wait" round trip in one call: it creates a session (or
|
|
356
|
+
reuses `session_id`), sends the prompt, blocks until the session is idle, and returns the new messages with the
|
|
357
|
+
final text and usage. `client.wait_until_ready` waits until agents and MCP servers have loaded, which happens
|
|
358
|
+
asynchronously after the server boots. Both have async versions on `AsyncOpencode`.
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
from opencode import Opencode
|
|
362
|
+
from opencode.types import ModelRef, PromptInputFileAttachment
|
|
363
|
+
|
|
364
|
+
with Opencode("http://127.0.0.1:4096", password="...") as client:
|
|
365
|
+
client.wait_until_ready(agents=["reviewer"], mcp_servers=["github"], timeout=60)
|
|
366
|
+
result = client.session.run(
|
|
367
|
+
"Review the attached diff",
|
|
368
|
+
agent="reviewer",
|
|
369
|
+
model=ModelRef(provider_id="anthropic", id="claude-sonnet-5-5"), # set it with the agent
|
|
370
|
+
files=[PromptInputFileAttachment(uri="data:text/plain;base64,...", name="diff.patch")],
|
|
371
|
+
timeout=600,
|
|
372
|
+
)
|
|
373
|
+
print(result.outcome, result.text)
|
|
374
|
+
print(result.cost, result.tokens.input, result.tokens.output) # this run; result.session has the totals
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
If the wait ends early (Ctrl-C, task cancellation, `timeout`, a connection error), `run` interrupts the session
|
|
378
|
+
on the server before re-raising, so the agent does not keep running unattended.
|
|
379
|
+
|
|
380
|
+
## Testing your code
|
|
381
|
+
|
|
382
|
+
`opencode.testing.FakeServer` is an in-process fake of the server: an `httpx` transport that keeps sessions,
|
|
383
|
+
messages and events in memory and answers with the same payloads as the real server. No process, no model.
|
|
384
|
+
|
|
385
|
+
```python
|
|
386
|
+
from opencode import Opencode
|
|
387
|
+
from opencode.testing import FakeServer
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def test_summarize() -> None:
|
|
391
|
+
fake = FakeServer(reply=lambda prompt: f"summary of {prompt!r}")
|
|
392
|
+
client = Opencode.fake(fake) # same as Opencode(transport=fake)
|
|
393
|
+
|
|
394
|
+
assert client.session.run("README").text == "summary of 'README'"
|
|
395
|
+
assert fake.prompts == ["README"]
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
`fake.fail(method, path_regex, status=..., exception=...)` injects errors, `fake.add_agent(..., after_polls=n)`
|
|
399
|
+
and `fake.set_mcp_status(...)` simulate slow boots, and `fake.requests` records every request. Routes the fake
|
|
400
|
+
does not implement answer `404`.
|
|
401
|
+
|
|
353
402
|
## Running a server
|
|
354
403
|
|
|
355
404
|
`OpencodeServer` runs `opencode serve` as a child process on a free local port with a random password:
|
|
@@ -370,6 +419,27 @@ with server:
|
|
|
370
419
|
The server runs in `--stdio` mode, so it also exits when the Python process dies. `AsyncOpencodeServer` is the
|
|
371
420
|
async equivalent.
|
|
372
421
|
|
|
422
|
+
### The background service
|
|
423
|
+
|
|
424
|
+
OpenCode can also run one shared background server per user (`opencode serve --service`), which registers itself in
|
|
425
|
+
`$XDG_STATE_HOME/opencode/service.json`. `Opencode.from_service()` connects to it, starting it first if it is not
|
|
426
|
+
running:
|
|
427
|
+
|
|
428
|
+
```python
|
|
429
|
+
from opencode import Opencode, service
|
|
430
|
+
|
|
431
|
+
with Opencode.from_service(version="2.1.2") as client: # replaces a service running another version
|
|
432
|
+
print(client.server.info().version)
|
|
433
|
+
|
|
434
|
+
endpoint = service.discover() # an Endpoint, or None; never starts anything
|
|
435
|
+
service.stop(pty="handoff") # hand persistent terminals to the next service instead of closing them
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
`service.ensure()` returns the `Endpoint` (URL and credentials) without creating a client, and `service.headers()`
|
|
439
|
+
gives its `Authorization` header for other HTTP clients. Concurrent callers are safe: they race to start the service
|
|
440
|
+
and all get the one that won. `await AsyncOpencode.from_service()`, `service.async_discover()`,
|
|
441
|
+
`service.async_ensure()` and `service.async_stop()` are the async equivalents.
|
|
442
|
+
|
|
373
443
|
## Advanced
|
|
374
444
|
|
|
375
445
|
```python
|
|
@@ -313,6 +313,55 @@ client.session.get("ses_...", timeout=5.0) # per call
|
|
|
313
313
|
|
|
314
314
|
The default timeout is 10 minutes with a 10 second connect timeout.
|
|
315
315
|
|
|
316
|
+
## Running an agent headless
|
|
317
|
+
|
|
318
|
+
`client.session.run` does the usual "prompt an agent and wait" round trip in one call: it creates a session (or
|
|
319
|
+
reuses `session_id`), sends the prompt, blocks until the session is idle, and returns the new messages with the
|
|
320
|
+
final text and usage. `client.wait_until_ready` waits until agents and MCP servers have loaded, which happens
|
|
321
|
+
asynchronously after the server boots. Both have async versions on `AsyncOpencode`.
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
from opencode import Opencode
|
|
325
|
+
from opencode.types import ModelRef, PromptInputFileAttachment
|
|
326
|
+
|
|
327
|
+
with Opencode("http://127.0.0.1:4096", password="...") as client:
|
|
328
|
+
client.wait_until_ready(agents=["reviewer"], mcp_servers=["github"], timeout=60)
|
|
329
|
+
result = client.session.run(
|
|
330
|
+
"Review the attached diff",
|
|
331
|
+
agent="reviewer",
|
|
332
|
+
model=ModelRef(provider_id="anthropic", id="claude-sonnet-5-5"), # set it with the agent
|
|
333
|
+
files=[PromptInputFileAttachment(uri="data:text/plain;base64,...", name="diff.patch")],
|
|
334
|
+
timeout=600,
|
|
335
|
+
)
|
|
336
|
+
print(result.outcome, result.text)
|
|
337
|
+
print(result.cost, result.tokens.input, result.tokens.output) # this run; result.session has the totals
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
If the wait ends early (Ctrl-C, task cancellation, `timeout`, a connection error), `run` interrupts the session
|
|
341
|
+
on the server before re-raising, so the agent does not keep running unattended.
|
|
342
|
+
|
|
343
|
+
## Testing your code
|
|
344
|
+
|
|
345
|
+
`opencode.testing.FakeServer` is an in-process fake of the server: an `httpx` transport that keeps sessions,
|
|
346
|
+
messages and events in memory and answers with the same payloads as the real server. No process, no model.
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
from opencode import Opencode
|
|
350
|
+
from opencode.testing import FakeServer
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def test_summarize() -> None:
|
|
354
|
+
fake = FakeServer(reply=lambda prompt: f"summary of {prompt!r}")
|
|
355
|
+
client = Opencode.fake(fake) # same as Opencode(transport=fake)
|
|
356
|
+
|
|
357
|
+
assert client.session.run("README").text == "summary of 'README'"
|
|
358
|
+
assert fake.prompts == ["README"]
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
`fake.fail(method, path_regex, status=..., exception=...)` injects errors, `fake.add_agent(..., after_polls=n)`
|
|
362
|
+
and `fake.set_mcp_status(...)` simulate slow boots, and `fake.requests` records every request. Routes the fake
|
|
363
|
+
does not implement answer `404`.
|
|
364
|
+
|
|
316
365
|
## Running a server
|
|
317
366
|
|
|
318
367
|
`OpencodeServer` runs `opencode serve` as a child process on a free local port with a random password:
|
|
@@ -333,6 +382,27 @@ with server:
|
|
|
333
382
|
The server runs in `--stdio` mode, so it also exits when the Python process dies. `AsyncOpencodeServer` is the
|
|
334
383
|
async equivalent.
|
|
335
384
|
|
|
385
|
+
### The background service
|
|
386
|
+
|
|
387
|
+
OpenCode can also run one shared background server per user (`opencode serve --service`), which registers itself in
|
|
388
|
+
`$XDG_STATE_HOME/opencode/service.json`. `Opencode.from_service()` connects to it, starting it first if it is not
|
|
389
|
+
running:
|
|
390
|
+
|
|
391
|
+
```python
|
|
392
|
+
from opencode import Opencode, service
|
|
393
|
+
|
|
394
|
+
with Opencode.from_service(version="2.1.2") as client: # replaces a service running another version
|
|
395
|
+
print(client.server.info().version)
|
|
396
|
+
|
|
397
|
+
endpoint = service.discover() # an Endpoint, or None; never starts anything
|
|
398
|
+
service.stop(pty="handoff") # hand persistent terminals to the next service instead of closing them
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
`service.ensure()` returns the `Endpoint` (URL and credentials) without creating a client, and `service.headers()`
|
|
402
|
+
gives its `Authorization` header for other HTTP clients. Concurrent callers are safe: they race to start the service
|
|
403
|
+
and all get the one that won. `await AsyncOpencode.from_service()`, `service.async_discover()`,
|
|
404
|
+
`service.async_ensure()` and `service.async_stop()` are the async equivalents.
|
|
405
|
+
|
|
336
406
|
## Advanced
|
|
337
407
|
|
|
338
408
|
```python
|
|
@@ -55,9 +55,11 @@ dev = [
|
|
|
55
55
|
"trio>=0.31",
|
|
56
56
|
"websockets>=14",
|
|
57
57
|
]
|
|
58
|
+
# Pillow (via [imaging]) only ships patched releases for Python 3.11+, so docs builds need 3.11+.
|
|
58
59
|
docs = [
|
|
59
|
-
"mkdocs-material[imaging]>=9.6",
|
|
60
|
-
"mkdocstrings[python]>=0.30",
|
|
60
|
+
"mkdocs-material[imaging]>=9.6; python_version >= '3.11'",
|
|
61
|
+
"mkdocstrings[python]>=0.30; python_version >= '3.11'",
|
|
62
|
+
"pillow>=13; python_version >= '3.11'",
|
|
61
63
|
]
|
|
62
64
|
|
|
63
65
|
[build-system]
|
|
@@ -14,7 +14,9 @@ Quick start::
|
|
|
14
14
|
|
|
15
15
|
from __future__ import annotations
|
|
16
16
|
|
|
17
|
-
from
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
from . import service
|
|
18
20
|
from ._client import AsyncOpencode, Opencode
|
|
19
21
|
|
|
20
22
|
# <generated:error-imports>
|
|
@@ -77,6 +79,7 @@ from ._models import BaseModel
|
|
|
77
79
|
from ._pagination import AsyncCursorPage, AsyncPaginator, SyncCursorPage
|
|
78
80
|
from ._pty import AsyncPtyConnection, PtyConnection, PtyConnectionError
|
|
79
81
|
from ._server import AsyncOpencodeServer, OpencodeServer, ServerStartError
|
|
82
|
+
from ._service import Endpoint, ServiceError
|
|
80
83
|
from ._streaming import AsyncStream, Stream
|
|
81
84
|
from ._types import NOT_GIVEN, NotGiven
|
|
82
85
|
from ._version import __version__
|
|
@@ -108,6 +111,9 @@ from ._rpc import (
|
|
|
108
111
|
rpc_method,
|
|
109
112
|
)
|
|
110
113
|
|
|
114
|
+
if TYPE_CHECKING:
|
|
115
|
+
from . import types
|
|
116
|
+
|
|
111
117
|
# <generated:api-version>
|
|
112
118
|
API_VERSION = "2.1.2"
|
|
113
119
|
"""The OpenCode server version this client was generated from."""
|
|
@@ -132,6 +138,7 @@ __all__ = [
|
|
|
132
138
|
"BaseModel",
|
|
133
139
|
"ConflictError",
|
|
134
140
|
"ContentTooLargeError",
|
|
141
|
+
"Endpoint",
|
|
135
142
|
"InternalServerError",
|
|
136
143
|
"NotFoundError",
|
|
137
144
|
"NotGiven",
|
|
@@ -144,11 +151,13 @@ __all__ = [
|
|
|
144
151
|
"PtyConnectionError",
|
|
145
152
|
"RateLimitError",
|
|
146
153
|
"ServerStartError",
|
|
154
|
+
"ServiceError",
|
|
147
155
|
"ServiceUnavailableError",
|
|
148
156
|
"Stream",
|
|
149
157
|
"SyncCursorPage",
|
|
150
158
|
"UnprocessableEntityError",
|
|
151
159
|
"__version__",
|
|
160
|
+
"service",
|
|
152
161
|
"types",
|
|
153
162
|
# Shared event stream and typed plugin RPC
|
|
154
163
|
"SUBSCRIBER_CAPACITY",
|
|
@@ -206,3 +215,12 @@ __all__ = [
|
|
|
206
215
|
"WorktreeError",
|
|
207
216
|
# </generated:error-exports>
|
|
208
217
|
]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def __getattr__(name: str) -> Any:
|
|
221
|
+
# `opencode.types` builds several hundred models, so it is imported on first use rather than with the package.
|
|
222
|
+
if name == "types":
|
|
223
|
+
import importlib
|
|
224
|
+
|
|
225
|
+
return importlib.import_module(".types", __name__)
|
|
226
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -7,6 +7,7 @@ transient failures, and turns responses into typed values or errors.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import base64
|
|
10
|
+
import builtins
|
|
10
11
|
import email.utils
|
|
11
12
|
import functools
|
|
12
13
|
import json
|
|
@@ -60,8 +61,18 @@ def _cached_adapter(tp: Any) -> pydantic.TypeAdapter[Any]:
|
|
|
60
61
|
return pydantic.TypeAdapter(tp)
|
|
61
62
|
|
|
62
63
|
|
|
64
|
+
@functools.cache
|
|
65
|
+
def _resolve_type(expr: str) -> Any:
|
|
66
|
+
# Generated resources name their response types as strings, so `import opencode` does not build every model.
|
|
67
|
+
from . import types
|
|
68
|
+
|
|
69
|
+
return eval(expr, {**vars(types), "builtins": builtins}) # expressions come from the generator
|
|
70
|
+
|
|
71
|
+
|
|
63
72
|
def type_adapter(tp: Any) -> pydantic.TypeAdapter[Any]:
|
|
64
|
-
"""Returns a cached ``TypeAdapter`` for a response type."""
|
|
73
|
+
"""Returns a cached ``TypeAdapter`` for a response type, or for a type expression over ``opencode.types``."""
|
|
74
|
+
if isinstance(tp, str):
|
|
75
|
+
tp = _resolve_type(tp)
|
|
65
76
|
try:
|
|
66
77
|
return _cached_adapter(tp)
|
|
67
78
|
except TypeError: # unhashable type expression
|
|
@@ -307,6 +318,7 @@ class SyncAPIClient(_BaseClient):
|
|
|
307
318
|
default_headers: Mapping[str, str] | None,
|
|
308
319
|
default_query: Mapping[str, object] | None,
|
|
309
320
|
http_client: httpx.Client | None,
|
|
321
|
+
transport: httpx.BaseTransport | None = None,
|
|
310
322
|
) -> None:
|
|
311
323
|
super().__init__(
|
|
312
324
|
base_url=base_url,
|
|
@@ -319,7 +331,11 @@ class SyncAPIClient(_BaseClient):
|
|
|
319
331
|
default_query=default_query,
|
|
320
332
|
)
|
|
321
333
|
self._owns_http = http_client is None
|
|
322
|
-
|
|
334
|
+
if http_client is not None and transport is not None:
|
|
335
|
+
raise ValueError("Pass either http_client or transport, not both")
|
|
336
|
+
self._http = http_client or httpx.Client(
|
|
337
|
+
limits=constants.DEFAULT_CONNECTION_LIMITS, follow_redirects=True, transport=transport
|
|
338
|
+
)
|
|
323
339
|
|
|
324
340
|
def close(self) -> None:
|
|
325
341
|
"""Closes the connection pool, unless it was passed in by the caller."""
|
|
@@ -421,6 +437,7 @@ class AsyncAPIClient(_BaseClient):
|
|
|
421
437
|
default_headers: Mapping[str, str] | None,
|
|
422
438
|
default_query: Mapping[str, object] | None,
|
|
423
439
|
http_client: httpx.AsyncClient | None,
|
|
440
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
424
441
|
) -> None:
|
|
425
442
|
super().__init__(
|
|
426
443
|
base_url=base_url,
|
|
@@ -433,7 +450,11 @@ class AsyncAPIClient(_BaseClient):
|
|
|
433
450
|
default_query=default_query,
|
|
434
451
|
)
|
|
435
452
|
self._owns_http = http_client is None
|
|
436
|
-
|
|
453
|
+
if http_client is not None and transport is not None:
|
|
454
|
+
raise ValueError("Pass either http_client or transport, not both")
|
|
455
|
+
self._http = http_client or httpx.AsyncClient(
|
|
456
|
+
limits=constants.DEFAULT_CONNECTION_LIMITS, follow_redirects=True, transport=transport
|
|
457
|
+
)
|
|
437
458
|
|
|
438
459
|
async def close(self) -> None:
|
|
439
460
|
"""Closes the connection pool, unless it was passed in by the caller."""
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import cached_property
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from typing_extensions import Self
|
|
7
|
+
|
|
8
|
+
from ._base_client import AsyncAPIClient, SyncAPIClient
|
|
9
|
+
from ._constants import DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, DEFAULT_USERNAME
|
|
10
|
+
from ._events import AsyncEventHub, EventHub
|
|
11
|
+
from ._rpc import AsyncTypedRpcResource, TypedRpcResource
|
|
12
|
+
from ._service import (
|
|
13
|
+
EnsureReason,
|
|
14
|
+
ServiceError,
|
|
15
|
+
Version,
|
|
16
|
+
async_discover,
|
|
17
|
+
async_ensure,
|
|
18
|
+
discover as discover_service,
|
|
19
|
+
ensure as ensure_service,
|
|
20
|
+
)
|
|
21
|
+
from ._types import NOT_GIVEN, NotGiven, Timeout
|
|
22
|
+
from .resources import AsyncResources, SyncResources
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
import os
|
|
26
|
+
from collections.abc import Callable, Iterable, Mapping, Sequence
|
|
27
|
+
|
|
28
|
+
import httpx
|
|
29
|
+
|
|
30
|
+
from .testing import FakeServer
|
|
31
|
+
|
|
32
|
+
__all__ = ["AsyncOpencode", "Opencode"]
|
|
33
|
+
|
|
34
|
+
FAKE_BASE_URL = "http://opencode.fake"
|
|
35
|
+
"""Base URL of clients made by ``Opencode.fake()``."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Opencode(SyncAPIClient, SyncResources):
|
|
39
|
+
"""Synchronous client for an OpenCode server.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
base_url: Server URL, for example ``http://127.0.0.1:4096``. Defaults to ``OPENCODE_BASE_URL``.
|
|
43
|
+
password: Server password. Defaults to ``OPENCODE_PASSWORD``, then ``OPENCODE_SERVER_PASSWORD``. Pass ``""``
|
|
44
|
+
to send no credentials.
|
|
45
|
+
username: Basic auth user name. The OpenCode server uses ``opencode``.
|
|
46
|
+
directory: Default project directory. Sent as the ``location`` of every request that accepts one, unless
|
|
47
|
+
the call passes its own ``location``.
|
|
48
|
+
timeout: Default timeout in seconds, or an ``httpx.Timeout``. Event streams only apply the connect timeout.
|
|
49
|
+
max_retries: Retries for connection failures and transient statuses. Requests that may have changed state
|
|
50
|
+
on the server are only retried when the server refused them before acting (429 and 503).
|
|
51
|
+
default_headers: Headers sent with every request.
|
|
52
|
+
default_query: Query parameters sent with every request.
|
|
53
|
+
http_client: A preconfigured ``httpx.Client`` (proxies, custom transports, event hooks). The caller keeps
|
|
54
|
+
ownership and must close it.
|
|
55
|
+
transport: An ``httpx`` transport for the client's own connection pool, such as
|
|
56
|
+
:class:`opencode.testing.FakeServer`. Cannot be combined with ``http_client``.
|
|
57
|
+
|
|
58
|
+
Example::
|
|
59
|
+
|
|
60
|
+
from opencode import Opencode
|
|
61
|
+
|
|
62
|
+
with Opencode("http://127.0.0.1:4096", password="...") as client:
|
|
63
|
+
for agent in client.agent.list():
|
|
64
|
+
print(agent.id)
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
base_url: str | httpx.URL | None = None,
|
|
70
|
+
*,
|
|
71
|
+
password: str | None = None,
|
|
72
|
+
username: str = DEFAULT_USERNAME,
|
|
73
|
+
directory: str | os.PathLike[str] | None = None,
|
|
74
|
+
timeout: Timeout = DEFAULT_TIMEOUT,
|
|
75
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
76
|
+
default_headers: Mapping[str, str] | None = None,
|
|
77
|
+
default_query: Mapping[str, object] | None = None,
|
|
78
|
+
http_client: httpx.Client | None = None,
|
|
79
|
+
transport: httpx.BaseTransport | None = None,
|
|
80
|
+
) -> None:
|
|
81
|
+
super().__init__(
|
|
82
|
+
base_url=base_url,
|
|
83
|
+
password=password,
|
|
84
|
+
username=username,
|
|
85
|
+
directory=directory,
|
|
86
|
+
timeout=timeout,
|
|
87
|
+
max_retries=max_retries,
|
|
88
|
+
default_headers=default_headers,
|
|
89
|
+
default_query=default_query,
|
|
90
|
+
http_client=http_client,
|
|
91
|
+
transport=transport,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_service(
|
|
96
|
+
cls,
|
|
97
|
+
*,
|
|
98
|
+
start: bool = True,
|
|
99
|
+
file: str | os.PathLike[str] | None = None,
|
|
100
|
+
version: Version | None = None,
|
|
101
|
+
command: Sequence[str] | None = None,
|
|
102
|
+
env: Mapping[str, str] | None = None,
|
|
103
|
+
on_start: Callable[[EnsureReason, str | None], None] | None = None,
|
|
104
|
+
directory: str | os.PathLike[str] | None = None,
|
|
105
|
+
timeout: Timeout = DEFAULT_TIMEOUT,
|
|
106
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
107
|
+
default_headers: Mapping[str, str] | None = None,
|
|
108
|
+
default_query: Mapping[str, object] | None = None,
|
|
109
|
+
) -> Self:
|
|
110
|
+
"""Connect to the local background service, starting ``opencode serve --service`` if needed.
|
|
111
|
+
|
|
112
|
+
Uses :func:`opencode.service.ensure`, or :func:`opencode.service.discover` when ``start`` is false, and
|
|
113
|
+
sends the service's password with every request.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
start: Start or replace the service when no matching one is running. When false, raise instead.
|
|
117
|
+
file: Registration file. Defaults to ``$XDG_STATE_HOME/opencode/service.json``.
|
|
118
|
+
version: Required exact version, or a predicate on the service version.
|
|
119
|
+
command: Service command and arguments, used when starting it.
|
|
120
|
+
env: Variables added to the environment of a started service.
|
|
121
|
+
on_start: Called once before a service process is started.
|
|
122
|
+
directory: Default project directory, as in the constructor.
|
|
123
|
+
timeout: Default request timeout, as in the constructor.
|
|
124
|
+
max_retries: Retries for failed requests, as in the constructor.
|
|
125
|
+
default_headers: Headers sent with every request.
|
|
126
|
+
default_query: Query parameters sent with every request.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ServiceError: No matching service is running and ``start`` is false, or it could not be started.
|
|
130
|
+
|
|
131
|
+
Example::
|
|
132
|
+
|
|
133
|
+
with Opencode.from_service() as client:
|
|
134
|
+
print(client.server.info().version)
|
|
135
|
+
"""
|
|
136
|
+
if start:
|
|
137
|
+
endpoint = ensure_service(file, version, command=command, env=env, on_start=on_start)
|
|
138
|
+
else:
|
|
139
|
+
found = discover_service(file, version)
|
|
140
|
+
if found is None:
|
|
141
|
+
raise ServiceError("No matching OpenCode background service is running")
|
|
142
|
+
endpoint = found
|
|
143
|
+
return cls(
|
|
144
|
+
endpoint.url,
|
|
145
|
+
# An empty password sends no credentials instead of falling back to OPENCODE_PASSWORD.
|
|
146
|
+
password="" if endpoint.auth is None else endpoint.auth.password,
|
|
147
|
+
username=DEFAULT_USERNAME if endpoint.auth is None else endpoint.auth.username,
|
|
148
|
+
directory=directory,
|
|
149
|
+
timeout=timeout,
|
|
150
|
+
max_retries=max_retries,
|
|
151
|
+
default_headers=default_headers,
|
|
152
|
+
default_query=default_query,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
@cached_property
|
|
156
|
+
def events(self) -> EventHub:
|
|
157
|
+
"""The shared event stream: one connection for every subscriber. See :class:`opencode.EventHub`."""
|
|
158
|
+
return EventHub(self)
|
|
159
|
+
|
|
160
|
+
@cached_property
|
|
161
|
+
def rpc(self) -> TypedRpcResource:
|
|
162
|
+
"""Plugin RPC calls: ``client.rpc.call(...)``, or ``client.rpc(Definition)`` for typed access."""
|
|
163
|
+
return TypedRpcResource(self)
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def fake(cls, server: FakeServer | None = None, **kwargs: Any) -> Self:
|
|
167
|
+
"""Returns a client wired to an in-process :class:`opencode.testing.FakeServer`, for tests.
|
|
168
|
+
|
|
169
|
+
Pass your own ``server`` to configure replies and inspect requests; other arguments go to the constructor.
|
|
170
|
+
"""
|
|
171
|
+
from .testing import FakeServer
|
|
172
|
+
|
|
173
|
+
kwargs.setdefault("password", "")
|
|
174
|
+
return cls(FAKE_BASE_URL, transport=server if server is not None else FakeServer(), **kwargs)
|
|
175
|
+
|
|
176
|
+
def wait_until_ready(
|
|
177
|
+
self, *, agents: Iterable[str] = (), mcp_servers: Iterable[str] = (), timeout: float = 60
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Blocks until the server answers, the named ``agents`` are listed and the named ``mcp_servers`` are
|
|
180
|
+
connected.
|
|
181
|
+
|
|
182
|
+
Agents load asynchronously after the server starts, so call this before creating sessions on a freshly
|
|
183
|
+
booted server. Connection errors and 503s while the server boots count as not ready yet.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
TimeoutError: Something was still missing after ``timeout`` seconds. The message names it.
|
|
187
|
+
"""
|
|
188
|
+
from .lib._ready import wait_until_ready
|
|
189
|
+
|
|
190
|
+
wait_until_ready(self, agents=agents, mcp_servers=mcp_servers, timeout=timeout)
|
|
191
|
+
|
|
192
|
+
def with_options(
|
|
193
|
+
self,
|
|
194
|
+
*,
|
|
195
|
+
base_url: str | httpx.URL | None = None,
|
|
196
|
+
password: str | NotGiven | None = NOT_GIVEN,
|
|
197
|
+
directory: str | os.PathLike[str] | NotGiven | None = NOT_GIVEN,
|
|
198
|
+
timeout: Timeout | NotGiven = NOT_GIVEN,
|
|
199
|
+
max_retries: int | None = None,
|
|
200
|
+
default_headers: Mapping[str, str] | None = None,
|
|
201
|
+
default_query: Mapping[str, object] | None = None,
|
|
202
|
+
) -> Self:
|
|
203
|
+
"""Returns a client with some options changed. It shares this client's connection pool.
|
|
204
|
+
|
|
205
|
+
``default_headers`` and ``default_query`` are merged over the current values.
|
|
206
|
+
"""
|
|
207
|
+
client = type(self)(
|
|
208
|
+
base_url=base_url or self.base_url,
|
|
209
|
+
password=self.password or "" if isinstance(password, NotGiven) else password,
|
|
210
|
+
username=self.username,
|
|
211
|
+
directory=self.directory if isinstance(directory, NotGiven) else directory,
|
|
212
|
+
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
|
|
213
|
+
max_retries=self.max_retries if max_retries is None else max_retries,
|
|
214
|
+
default_headers={**self._default_headers, **(default_headers or {})},
|
|
215
|
+
default_query={**self._default_query, **(default_query or {})},
|
|
216
|
+
http_client=self._http,
|
|
217
|
+
)
|
|
218
|
+
return client
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class AsyncOpencode(AsyncAPIClient, AsyncResources):
|
|
222
|
+
"""Asynchronous client for an OpenCode server. Works with asyncio and trio.
|
|
223
|
+
|
|
224
|
+
Takes the same arguments as :class:`Opencode`; ``http_client`` is an ``httpx.AsyncClient``.
|
|
225
|
+
|
|
226
|
+
Example::
|
|
227
|
+
|
|
228
|
+
from opencode import AsyncOpencode
|
|
229
|
+
|
|
230
|
+
async with AsyncOpencode("http://127.0.0.1:4096", password="...") as client:
|
|
231
|
+
for agent in await client.agent.list():
|
|
232
|
+
print(agent.id)
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
def __init__(
|
|
236
|
+
self,
|
|
237
|
+
base_url: str | httpx.URL | None = None,
|
|
238
|
+
*,
|
|
239
|
+
password: str | None = None,
|
|
240
|
+
username: str = DEFAULT_USERNAME,
|
|
241
|
+
directory: str | os.PathLike[str] | None = None,
|
|
242
|
+
timeout: Timeout = DEFAULT_TIMEOUT,
|
|
243
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
244
|
+
default_headers: Mapping[str, str] | None = None,
|
|
245
|
+
default_query: Mapping[str, object] | None = None,
|
|
246
|
+
http_client: httpx.AsyncClient | None = None,
|
|
247
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
248
|
+
) -> None:
|
|
249
|
+
super().__init__(
|
|
250
|
+
base_url=base_url,
|
|
251
|
+
password=password,
|
|
252
|
+
username=username,
|
|
253
|
+
directory=directory,
|
|
254
|
+
timeout=timeout,
|
|
255
|
+
max_retries=max_retries,
|
|
256
|
+
default_headers=default_headers,
|
|
257
|
+
default_query=default_query,
|
|
258
|
+
http_client=http_client,
|
|
259
|
+
transport=transport,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
@classmethod
|
|
263
|
+
async def from_service(
|
|
264
|
+
cls,
|
|
265
|
+
*,
|
|
266
|
+
start: bool = True,
|
|
267
|
+
file: str | os.PathLike[str] | None = None,
|
|
268
|
+
version: Version | None = None,
|
|
269
|
+
command: Sequence[str] | None = None,
|
|
270
|
+
env: Mapping[str, str] | None = None,
|
|
271
|
+
on_start: Callable[[EnsureReason, str | None], None] | None = None,
|
|
272
|
+
directory: str | os.PathLike[str] | None = None,
|
|
273
|
+
timeout: Timeout = DEFAULT_TIMEOUT,
|
|
274
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
275
|
+
default_headers: Mapping[str, str] | None = None,
|
|
276
|
+
default_query: Mapping[str, object] | None = None,
|
|
277
|
+
) -> Self:
|
|
278
|
+
"""Async version of :meth:`Opencode.from_service`. Await it: ``client = await AsyncOpencode.from_service()``."""
|
|
279
|
+
if start:
|
|
280
|
+
endpoint = await async_ensure(file, version, command=command, env=env, on_start=on_start)
|
|
281
|
+
else:
|
|
282
|
+
found = await async_discover(file, version)
|
|
283
|
+
if found is None:
|
|
284
|
+
raise ServiceError("No matching OpenCode background service is running")
|
|
285
|
+
endpoint = found
|
|
286
|
+
return cls(
|
|
287
|
+
endpoint.url,
|
|
288
|
+
# An empty password sends no credentials instead of falling back to OPENCODE_PASSWORD.
|
|
289
|
+
password="" if endpoint.auth is None else endpoint.auth.password,
|
|
290
|
+
username=DEFAULT_USERNAME if endpoint.auth is None else endpoint.auth.username,
|
|
291
|
+
directory=directory,
|
|
292
|
+
timeout=timeout,
|
|
293
|
+
max_retries=max_retries,
|
|
294
|
+
default_headers=default_headers,
|
|
295
|
+
default_query=default_query,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
@cached_property
|
|
299
|
+
def events(self) -> AsyncEventHub:
|
|
300
|
+
"""The shared event stream: one connection for every subscriber. See :class:`opencode.AsyncEventHub`."""
|
|
301
|
+
return AsyncEventHub(self)
|
|
302
|
+
|
|
303
|
+
@cached_property
|
|
304
|
+
def rpc(self) -> AsyncTypedRpcResource:
|
|
305
|
+
"""Plugin RPC calls: ``client.rpc.call(...)``, or ``client.rpc(Definition)`` for typed access."""
|
|
306
|
+
return AsyncTypedRpcResource(self)
|
|
307
|
+
|
|
308
|
+
@classmethod
|
|
309
|
+
def fake(cls, server: FakeServer | None = None, **kwargs: Any) -> Self:
|
|
310
|
+
"""Returns a client wired to an in-process :class:`opencode.testing.FakeServer`, for tests.
|
|
311
|
+
|
|
312
|
+
Pass your own ``server`` to configure replies and inspect requests; other arguments go to the constructor.
|
|
313
|
+
"""
|
|
314
|
+
from .testing import FakeServer
|
|
315
|
+
|
|
316
|
+
kwargs.setdefault("password", "")
|
|
317
|
+
return cls(FAKE_BASE_URL, transport=server if server is not None else FakeServer(), **kwargs)
|
|
318
|
+
|
|
319
|
+
async def wait_until_ready(
|
|
320
|
+
self, *, agents: Iterable[str] = (), mcp_servers: Iterable[str] = (), timeout: float = 60
|
|
321
|
+
) -> None:
|
|
322
|
+
"""Blocks until the server answers, the named ``agents`` are listed and the named ``mcp_servers`` are
|
|
323
|
+
connected.
|
|
324
|
+
|
|
325
|
+
Agents load asynchronously after the server starts, so call this before creating sessions on a freshly
|
|
326
|
+
booted server. Connection errors and 503s while the server boots count as not ready yet.
|
|
327
|
+
|
|
328
|
+
Raises:
|
|
329
|
+
TimeoutError: Something was still missing after ``timeout`` seconds. The message names it.
|
|
330
|
+
"""
|
|
331
|
+
from .lib._ready import async_wait_until_ready
|
|
332
|
+
|
|
333
|
+
await async_wait_until_ready(self, agents=agents, mcp_servers=mcp_servers, timeout=timeout)
|
|
334
|
+
|
|
335
|
+
def with_options(
|
|
336
|
+
self,
|
|
337
|
+
*,
|
|
338
|
+
base_url: str | httpx.URL | None = None,
|
|
339
|
+
password: str | NotGiven | None = NOT_GIVEN,
|
|
340
|
+
directory: str | os.PathLike[str] | NotGiven | None = NOT_GIVEN,
|
|
341
|
+
timeout: Timeout | NotGiven = NOT_GIVEN,
|
|
342
|
+
max_retries: int | None = None,
|
|
343
|
+
default_headers: Mapping[str, str] | None = None,
|
|
344
|
+
default_query: Mapping[str, object] | None = None,
|
|
345
|
+
) -> Self:
|
|
346
|
+
"""Returns a client with some options changed. It shares this client's connection pool.
|
|
347
|
+
|
|
348
|
+
``default_headers`` and ``default_query`` are merged over the current values.
|
|
349
|
+
"""
|
|
350
|
+
client = type(self)(
|
|
351
|
+
base_url=base_url or self.base_url,
|
|
352
|
+
password=self.password or "" if isinstance(password, NotGiven) else password,
|
|
353
|
+
username=self.username,
|
|
354
|
+
directory=self.directory if isinstance(directory, NotGiven) else directory,
|
|
355
|
+
timeout=self.timeout if isinstance(timeout, NotGiven) else timeout,
|
|
356
|
+
max_retries=self.max_retries if max_retries is None else max_retries,
|
|
357
|
+
default_headers={**self._default_headers, **(default_headers or {})},
|
|
358
|
+
default_query={**self._default_query, **(default_query or {})},
|
|
359
|
+
http_client=self._http,
|
|
360
|
+
)
|
|
361
|
+
return client
|