opencode-python-sdk 0.1.2__py3-none-any.whl → 0.2.0__py3-none-any.whl

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.
Files changed (46) hide show
  1. opencode/__init__.py +62 -1
  2. opencode/_base_client.py +24 -3
  3. opencode/_client.py +90 -2
  4. opencode/_events.py +732 -0
  5. opencode/_rpc.py +489 -0
  6. opencode/_streaming.py +22 -1
  7. opencode/lib/__init__.py +5 -0
  8. opencode/lib/_ready.py +91 -0
  9. opencode/lib/_session.py +231 -0
  10. opencode/resources/agent.py +13 -11
  11. opencode/resources/command.py +9 -7
  12. opencode/resources/config.py +12 -10
  13. opencode/resources/credential.py +13 -11
  14. opencode/resources/debug.py +9 -7
  15. opencode/resources/event.py +9 -5
  16. opencode/resources/experimental.py +31 -29
  17. opencode/resources/file.py +15 -13
  18. opencode/resources/form.py +9 -7
  19. opencode/resources/generate.py +9 -7
  20. opencode/resources/integration.py +28 -26
  21. opencode/resources/location.py +9 -7
  22. opencode/resources/mcp.py +13 -11
  23. opencode/resources/message.py +9 -8
  24. opencode/resources/migration.py +10 -12
  25. opencode/resources/model.py +12 -10
  26. opencode/resources/permission.py +29 -27
  27. opencode/resources/plugin.py +12 -10
  28. opencode/resources/project.py +14 -12
  29. opencode/resources/provider.py +13 -11
  30. opencode/resources/pty.py +23 -21
  31. opencode/resources/reference.py +9 -7
  32. opencode/resources/rpc.py +9 -7
  33. opencode/resources/server.py +14 -12
  34. opencode/resources/session.py +92 -90
  35. opencode/resources/shell.py +19 -17
  36. opencode/resources/skill.py +9 -7
  37. opencode/resources/vcs.py +22 -20
  38. opencode/resources/voice.py +9 -7
  39. opencode/resources/websearch.py +13 -11
  40. opencode/resources/worktree.py +13 -11
  41. opencode/testing.py +508 -0
  42. {opencode_python_sdk-0.1.2.dist-info → opencode_python_sdk-0.2.0.dist-info}/METADATA +126 -1
  43. opencode_python_sdk-0.2.0.dist-info/RECORD +62 -0
  44. opencode_python_sdk-0.1.2.dist-info/RECORD +0 -56
  45. {opencode_python_sdk-0.1.2.dist-info → opencode_python_sdk-0.2.0.dist-info}/WHEEL +0 -0
  46. {opencode_python_sdk-0.1.2.dist-info → opencode_python_sdk-0.2.0.dist-info}/licenses/LICENSE +0 -0
opencode/__init__.py CHANGED
@@ -14,7 +14,8 @@ Quick start::
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- from . import types
17
+ from typing import TYPE_CHECKING, Any
18
+
18
19
  from ._client import AsyncOpencode, Opencode
19
20
 
20
21
  # <generated:error-imports>
@@ -81,6 +82,36 @@ from ._streaming import AsyncStream, Stream
81
82
  from ._types import NOT_GIVEN, NotGiven
82
83
  from ._version import __version__
83
84
 
85
+ # isort: split
86
+ # Hand-written helpers, kept apart from the generated error-import block above.
87
+ from ._events import (
88
+ SUBSCRIBER_CAPACITY,
89
+ AsyncEventHub,
90
+ AsyncSubscription,
91
+ EventHub,
92
+ EventOverflowError,
93
+ Reconnect,
94
+ Subscription,
95
+ session_id_of,
96
+ )
97
+ from ._rpc import (
98
+ AsyncRpcClient,
99
+ AsyncRpcEvents,
100
+ RpcCallError,
101
+ RpcCallInternalError,
102
+ RpcClient,
103
+ RpcDefinition,
104
+ RpcEventDefinition,
105
+ RpcEventPayload,
106
+ RpcEvents,
107
+ RpcMethod,
108
+ rpc_event,
109
+ rpc_method,
110
+ )
111
+
112
+ if TYPE_CHECKING:
113
+ from . import types
114
+
84
115
  # <generated:api-version>
85
116
  API_VERSION = "2.1.2"
86
117
  """The OpenCode server version this client was generated from."""
@@ -123,6 +154,27 @@ __all__ = [
123
154
  "UnprocessableEntityError",
124
155
  "__version__",
125
156
  "types",
157
+ # Shared event stream and typed plugin RPC
158
+ "SUBSCRIBER_CAPACITY",
159
+ "AsyncEventHub",
160
+ "AsyncRpcClient",
161
+ "AsyncRpcEvents",
162
+ "AsyncSubscription",
163
+ "EventHub",
164
+ "EventOverflowError",
165
+ "Reconnect",
166
+ "RpcCallError",
167
+ "RpcCallInternalError",
168
+ "RpcClient",
169
+ "RpcDefinition",
170
+ "RpcEventDefinition",
171
+ "RpcEventPayload",
172
+ "RpcEvents",
173
+ "RpcMethod",
174
+ "Subscription",
175
+ "rpc_event",
176
+ "rpc_method",
177
+ "session_id_of",
126
178
  # <generated:error-exports>
127
179
  "AgentNotFoundError",
128
180
  "CommandExecutionError",
@@ -158,3 +210,12 @@ __all__ = [
158
210
  "WorktreeError",
159
211
  # </generated:error-exports>
160
212
  ]
213
+
214
+
215
+ def __getattr__(name: str) -> Any:
216
+ # `opencode.types` builds several hundred models, so it is imported on first use rather than with the package.
217
+ if name == "types":
218
+ import importlib
219
+
220
+ return importlib.import_module(".types", __name__)
221
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
opencode/_base_client.py CHANGED
@@ -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
- self._http = http_client or httpx.Client(limits=constants.DEFAULT_CONNECTION_LIMITS, follow_redirects=True)
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
- self._http = http_client or httpx.AsyncClient(limits=constants.DEFAULT_CONNECTION_LIMITS, follow_redirects=True)
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."""
opencode/_client.py CHANGED
@@ -1,22 +1,30 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from functools import cached_property
4
+ from typing import TYPE_CHECKING, Any
4
5
 
5
6
  from typing_extensions import Self
6
7
 
7
8
  from ._base_client import AsyncAPIClient, SyncAPIClient
8
9
  from ._constants import DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, DEFAULT_USERNAME
10
+ from ._events import AsyncEventHub, EventHub
11
+ from ._rpc import AsyncTypedRpcResource, TypedRpcResource
9
12
  from ._types import NOT_GIVEN, NotGiven, Timeout
10
13
  from .resources import AsyncResources, SyncResources
11
14
 
12
15
  if TYPE_CHECKING:
13
16
  import os
14
- from collections.abc import Mapping
17
+ from collections.abc import Iterable, Mapping
15
18
 
16
19
  import httpx
17
20
 
21
+ from .testing import FakeServer
22
+
18
23
  __all__ = ["AsyncOpencode", "Opencode"]
19
24
 
25
+ FAKE_BASE_URL = "http://opencode.fake"
26
+ """Base URL of clients made by ``Opencode.fake()``."""
27
+
20
28
 
21
29
  class Opencode(SyncAPIClient, SyncResources):
22
30
  """Synchronous client for an OpenCode server.
@@ -35,6 +43,8 @@ class Opencode(SyncAPIClient, SyncResources):
35
43
  default_query: Query parameters sent with every request.
36
44
  http_client: A preconfigured ``httpx.Client`` (proxies, custom transports, event hooks). The caller keeps
37
45
  ownership and must close it.
46
+ transport: An ``httpx`` transport for the client's own connection pool, such as
47
+ :class:`opencode.testing.FakeServer`. Cannot be combined with ``http_client``.
38
48
 
39
49
  Example::
40
50
 
@@ -57,6 +67,7 @@ class Opencode(SyncAPIClient, SyncResources):
57
67
  default_headers: Mapping[str, str] | None = None,
58
68
  default_query: Mapping[str, object] | None = None,
59
69
  http_client: httpx.Client | None = None,
70
+ transport: httpx.BaseTransport | None = None,
60
71
  ) -> None:
61
72
  super().__init__(
62
73
  base_url=base_url,
@@ -68,8 +79,46 @@ class Opencode(SyncAPIClient, SyncResources):
68
79
  default_headers=default_headers,
69
80
  default_query=default_query,
70
81
  http_client=http_client,
82
+ transport=transport,
71
83
  )
72
84
 
85
+ @cached_property
86
+ def events(self) -> EventHub:
87
+ """The shared event stream: one connection for every subscriber. See :class:`opencode.EventHub`."""
88
+ return EventHub(self)
89
+
90
+ @cached_property
91
+ def rpc(self) -> TypedRpcResource:
92
+ """Plugin RPC calls: ``client.rpc.call(...)``, or ``client.rpc(Definition)`` for typed access."""
93
+ return TypedRpcResource(self)
94
+
95
+ @classmethod
96
+ def fake(cls, server: FakeServer | None = None, **kwargs: Any) -> Self:
97
+ """Returns a client wired to an in-process :class:`opencode.testing.FakeServer`, for tests.
98
+
99
+ Pass your own ``server`` to configure replies and inspect requests; other arguments go to the constructor.
100
+ """
101
+ from .testing import FakeServer
102
+
103
+ kwargs.setdefault("password", "")
104
+ return cls(FAKE_BASE_URL, transport=server if server is not None else FakeServer(), **kwargs)
105
+
106
+ def wait_until_ready(
107
+ self, *, agents: Iterable[str] = (), mcp_servers: Iterable[str] = (), timeout: float = 60
108
+ ) -> None:
109
+ """Blocks until the server answers, the named ``agents`` are listed and the named ``mcp_servers`` are
110
+ connected.
111
+
112
+ Agents load asynchronously after the server starts, so call this before creating sessions on a freshly
113
+ booted server. Connection errors and 503s while the server boots count as not ready yet.
114
+
115
+ Raises:
116
+ TimeoutError: Something was still missing after ``timeout`` seconds. The message names it.
117
+ """
118
+ from .lib._ready import wait_until_ready
119
+
120
+ wait_until_ready(self, agents=agents, mcp_servers=mcp_servers, timeout=timeout)
121
+
73
122
  def with_options(
74
123
  self,
75
124
  *,
@@ -125,6 +174,7 @@ class AsyncOpencode(AsyncAPIClient, AsyncResources):
125
174
  default_headers: Mapping[str, str] | None = None,
126
175
  default_query: Mapping[str, object] | None = None,
127
176
  http_client: httpx.AsyncClient | None = None,
177
+ transport: httpx.AsyncBaseTransport | None = None,
128
178
  ) -> None:
129
179
  super().__init__(
130
180
  base_url=base_url,
@@ -136,8 +186,46 @@ class AsyncOpencode(AsyncAPIClient, AsyncResources):
136
186
  default_headers=default_headers,
137
187
  default_query=default_query,
138
188
  http_client=http_client,
189
+ transport=transport,
139
190
  )
140
191
 
192
+ @cached_property
193
+ def events(self) -> AsyncEventHub:
194
+ """The shared event stream: one connection for every subscriber. See :class:`opencode.AsyncEventHub`."""
195
+ return AsyncEventHub(self)
196
+
197
+ @cached_property
198
+ def rpc(self) -> AsyncTypedRpcResource:
199
+ """Plugin RPC calls: ``client.rpc.call(...)``, or ``client.rpc(Definition)`` for typed access."""
200
+ return AsyncTypedRpcResource(self)
201
+
202
+ @classmethod
203
+ def fake(cls, server: FakeServer | None = None, **kwargs: Any) -> Self:
204
+ """Returns a client wired to an in-process :class:`opencode.testing.FakeServer`, for tests.
205
+
206
+ Pass your own ``server`` to configure replies and inspect requests; other arguments go to the constructor.
207
+ """
208
+ from .testing import FakeServer
209
+
210
+ kwargs.setdefault("password", "")
211
+ return cls(FAKE_BASE_URL, transport=server if server is not None else FakeServer(), **kwargs)
212
+
213
+ async def wait_until_ready(
214
+ self, *, agents: Iterable[str] = (), mcp_servers: Iterable[str] = (), timeout: float = 60
215
+ ) -> None:
216
+ """Blocks until the server answers, the named ``agents`` are listed and the named ``mcp_servers`` are
217
+ connected.
218
+
219
+ Agents load asynchronously after the server starts, so call this before creating sessions on a freshly
220
+ booted server. Connection errors and 503s while the server boots count as not ready yet.
221
+
222
+ Raises:
223
+ TimeoutError: Something was still missing after ``timeout`` seconds. The message names it.
224
+ """
225
+ from .lib._ready import async_wait_until_ready
226
+
227
+ await async_wait_until_ready(self, agents=agents, mcp_servers=mcp_servers, timeout=timeout)
228
+
141
229
  def with_options(
142
230
  self,
143
231
  *,