opencode-python-sdk 0.1.1__py3-none-any.whl → 0.1.3__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.
opencode/_rpc.py ADDED
@@ -0,0 +1,482 @@
1
+ """Typed calls to plugin RPCs.
2
+
3
+ A plugin registers an RPC with an id, methods and events. Declare the same shape once as a class, then call it
4
+ through ``client.rpc(Definition)``, the counterpart of ``client.rpc(definition)`` in the official TypeScript client::
5
+
6
+ from pydantic import BaseModel
7
+ from opencode import RpcDefinition, rpc_event, rpc_method
8
+
9
+
10
+ class AddInput(BaseModel):
11
+ amount: int
12
+
13
+
14
+ class Total(BaseModel):
15
+ total: int
16
+
17
+
18
+ class Counter(RpcDefinition, id="counter"):
19
+ add = rpc_method(AddInput, Total)
20
+ reset = rpc_method(None, Total)
21
+ changed = rpc_event(Total)
22
+
23
+
24
+ counter = client.rpc(Counter)
25
+ print(counter.call(Counter.add, AddInput(amount=2)).total)
26
+ with counter.subscribe(Counter.changed) as changes:
27
+ for change in changes:
28
+ print(change.data.total)
29
+
30
+ Inputs are validated and serialized, and outputs and event data validated, with pydantic, so any type pydantic
31
+ accepts works: models, dataclasses, ``TypedDict``, ``list[int]``. RPC failures raise :class:`RpcCallError`.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ from dataclasses import dataclass
37
+ from typing import TYPE_CHECKING, Any, ClassVar, Generic, TypeVar, cast, overload
38
+
39
+ import pydantic
40
+ from typing_extensions import Self
41
+
42
+ from ._base_client import type_adapter
43
+ from ._errors import RpcError, RpcInternalError
44
+ from ._exceptions import OpencodeError, validation_summary
45
+ from ._types import NOT_GIVEN, NotGiven
46
+ from .resources.rpc import AsyncRpcResource, RpcResource
47
+ from .types import RpcEvent
48
+
49
+ if TYPE_CHECKING:
50
+ from collections.abc import Mapping
51
+ from types import TracebackType
52
+
53
+ from ._client import AsyncOpencode, Opencode
54
+ from ._events import AsyncSubscription, Subscription
55
+ from ._types import Headers, Timeout
56
+ from .types import Location
57
+
58
+ __all__ = [
59
+ "AsyncRpcClient",
60
+ "AsyncRpcEvents",
61
+ "RpcCallError",
62
+ "RpcCallInternalError",
63
+ "RpcClient",
64
+ "RpcDefinition",
65
+ "RpcEventDefinition",
66
+ "RpcEventPayload",
67
+ "RpcEvents",
68
+ "RpcMethod",
69
+ "rpc_event",
70
+ "rpc_method",
71
+ ]
72
+
73
+ _D = TypeVar("_D", bound="RpcDefinition")
74
+ _I = TypeVar("_I")
75
+ _O = TypeVar("_O")
76
+ _T = TypeVar("_T")
77
+
78
+
79
+ class RpcCallError(OpencodeError):
80
+ """A plugin RPC method failed (the server answered with ``RpcError``).
81
+
82
+ Attributes:
83
+ rpc_id: The RPC that was called.
84
+ method: The method that was called.
85
+ type: The error type the plugin reported, for example ``"not_found"``.
86
+ message: The error message.
87
+ data: Error details, validated against the method's ``errors`` declaration when it names this type.
88
+ """
89
+
90
+ def __init__(self, rpc_id: str, method: str, type: str, message: str, data: object = None) -> None:
91
+ super().__init__(f"{rpc_id}.{method} failed with {type}: {message}")
92
+ self.rpc_id = rpc_id
93
+ self.method = method
94
+ self.type = type
95
+ self.message = message
96
+ self.data = data
97
+
98
+
99
+ class RpcCallInternalError(RpcCallError):
100
+ """The plugin RPC crashed or returned output that does not match its declaration.
101
+
102
+ ``type`` is ``"rpc.internal"`` or ``"rpc.invalid_output"``. Output this client cannot validate against the
103
+ method's declared output type is also reported as ``"rpc.invalid_output"``.
104
+ """
105
+
106
+
107
+ class RpcMethod(Generic[_I, _O]):
108
+ """One method of an :class:`RpcDefinition`. Create it with :func:`rpc_method`."""
109
+
110
+ name: str
111
+
112
+ def __init__(self, input: Any, output: Any, *, name: str | None, errors: Mapping[str, Any] | None) -> None:
113
+ self.input = input
114
+ self.output = output
115
+ self.errors = dict(errors or {})
116
+ if name is not None:
117
+ self.name = name
118
+
119
+ def __set_name__(self, owner: type, name: str) -> None:
120
+ if not hasattr(self, "name"):
121
+ self.name = name
122
+
123
+ def encode(self, value: _I) -> object:
124
+ """Validates ``value`` against the input type and returns its JSON form."""
125
+ if self.input is None:
126
+ return NOT_GIVEN if value is None else value
127
+ adapter = type_adapter(self.input)
128
+ return adapter.dump_python(adapter.validate_python(value), mode="json", by_alias=True)
129
+
130
+ def decode(self, rpc_id: str, value: object) -> _O:
131
+ """Validates a method's output."""
132
+ if self.output is None:
133
+ return cast("_O", None)
134
+ try:
135
+ return cast("_O", type_adapter(self.output).validate_python(value))
136
+ except pydantic.ValidationError as exc:
137
+ raise RpcCallInternalError(rpc_id, self.name, "rpc.invalid_output", validation_summary(exc), value) from exc
138
+
139
+ def error(self, rpc_id: str, exc: RpcError | RpcInternalError) -> RpcCallError:
140
+ """Maps a transport error to :class:`RpcCallError`, decoding declared error data."""
141
+ body = exc.error
142
+ data: object = body.data
143
+ if body.type in self.errors and data is not None:
144
+ try:
145
+ data = type_adapter(self.errors[body.type]).validate_python(data)
146
+ except pydantic.ValidationError:
147
+ data = body.data
148
+ cls = RpcCallInternalError if isinstance(exc, RpcInternalError) else RpcCallError
149
+ return cls(rpc_id, self.name, body.type, body.message, data)
150
+
151
+
152
+ class RpcEventDefinition(Generic[_T]):
153
+ """One event of an :class:`RpcDefinition`. Create it with :func:`rpc_event`."""
154
+
155
+ name: str
156
+
157
+ def __init__(self, data: Any, *, name: str | None) -> None:
158
+ self.data = data
159
+ if name is not None:
160
+ self.name = name
161
+
162
+ def __set_name__(self, owner: type, name: str) -> None:
163
+ if not hasattr(self, "name"):
164
+ self.name = name
165
+
166
+ def parse(self, event: RpcEvent) -> RpcEventPayload[_T]:
167
+ """Validates the data of a raw RPC event of this kind.
168
+
169
+ Raises:
170
+ pydantic.ValidationError: The data does not match the declared type.
171
+ """
172
+ data = cast("_T", type_adapter(self.data).validate_python(event.data))
173
+ return RpcEventPayload(name=self.name, data=data, event=event)
174
+
175
+
176
+ @overload
177
+ def rpc_method(
178
+ input: None, output: type[_O], *, name: str | None = None, errors: Mapping[str, Any] | None = None
179
+ ) -> RpcMethod[None, _O]: ...
180
+ @overload
181
+ def rpc_method(
182
+ input: type[_I], output: type[_O], *, name: str | None = None, errors: Mapping[str, Any] | None = None
183
+ ) -> RpcMethod[_I, _O]: ...
184
+ def rpc_method(
185
+ input: Any, output: Any, *, name: str | None = None, errors: Mapping[str, Any] | None = None
186
+ ) -> RpcMethod[Any, Any]:
187
+ """Declares an RPC method on an :class:`RpcDefinition`.
188
+
189
+ Args:
190
+ input: The input type, or ``None`` for a method without input.
191
+ output: The output type. Use ``type(None)`` for a method that returns nothing.
192
+ name: The method name on the server. Defaults to the attribute name.
193
+ errors: Data types of the errors the method declares, by error type. Matching error data is validated
194
+ into these types on :attr:`RpcCallError.data`.
195
+ """
196
+ return RpcMethod(input, output, name=name, errors=errors)
197
+
198
+
199
+ def rpc_event(data: type[_T], *, name: str | None = None) -> RpcEventDefinition[_T]:
200
+ """Declares an RPC event on an :class:`RpcDefinition`.
201
+
202
+ Args:
203
+ data: The type of the event's ``data``.
204
+ name: The event name on the server. Defaults to the attribute name.
205
+ """
206
+ return RpcEventDefinition(data, name=name)
207
+
208
+
209
+ class RpcDefinition:
210
+ """Base class for plugin RPC declarations. Pass the RPC id as a class keyword::
211
+
212
+ class Counter(RpcDefinition, id="counter"):
213
+ add = rpc_method(AddInput, Total)
214
+ changed = rpc_event(Total)
215
+
216
+ The class is only a declaration; bind it to a client with ``client.rpc(Counter)``.
217
+ """
218
+
219
+ id: ClassVar[str]
220
+ methods: ClassVar[dict[str, RpcMethod[Any, Any]]]
221
+ events: ClassVar[dict[str, RpcEventDefinition[Any]]]
222
+
223
+ def __init_subclass__(cls, *, id: str | None = None, **kwargs: Any) -> None:
224
+ super().__init_subclass__(**kwargs)
225
+ if id is not None:
226
+ cls.id = id
227
+ elif not hasattr(cls, "id"):
228
+ raise TypeError(f"{cls.__name__} needs an RPC id: class {cls.__name__}(RpcDefinition, id='...')")
229
+ methods: dict[str, RpcMethod[Any, Any]] = {}
230
+ events: dict[str, RpcEventDefinition[Any]] = {}
231
+ for base in reversed(cls.__mro__):
232
+ for value in vars(base).values():
233
+ if isinstance(value, RpcMethod):
234
+ methods[value.name] = value
235
+ elif isinstance(value, RpcEventDefinition):
236
+ events[value.name] = value
237
+ cls.methods = methods
238
+ cls.events = events
239
+
240
+ @classmethod
241
+ def event_type(cls, event: RpcEventDefinition[Any] | str) -> str:
242
+ """The wire ``type`` of an event of this RPC: ``rpc.<id>.<name>``."""
243
+ return f"rpc.{cls.id}.{event if isinstance(event, str) else event.name}"
244
+
245
+
246
+ @dataclass(frozen=True)
247
+ class RpcEventPayload(Generic[_T]):
248
+ """A plugin RPC event with validated data.
249
+
250
+ Attributes:
251
+ name: The event name, without the ``rpc.<id>.`` prefix.
252
+ data: The validated event data.
253
+ event: The raw event, with its ``id``, ``created`` time and ``location``.
254
+ """
255
+
256
+ name: str
257
+ data: _T
258
+ event: RpcEvent
259
+
260
+
261
+ def _check_method(definition: type[RpcDefinition], method: RpcMethod[Any, Any]) -> None:
262
+ if not any(known is method for known in definition.methods.values()):
263
+ raise ValueError(f"{getattr(method, 'name', method)!r} is not a method of {definition.__name__}")
264
+
265
+
266
+ def _check_event(definition: type[RpcDefinition], event: RpcEventDefinition[Any]) -> None:
267
+ if not any(known is event for known in definition.events.values()):
268
+ raise ValueError(f"{getattr(event, 'name', event)!r} is not an event of {definition.__name__}")
269
+
270
+
271
+ class RpcClient(Generic[_D]):
272
+ """A plugin RPC bound to a synchronous client. Get one with ``client.rpc(Definition)``."""
273
+
274
+ def __init__(self, client: Opencode, definition: type[_D]) -> None:
275
+ self._client = client
276
+ self.definition = definition
277
+
278
+ @overload
279
+ def call(
280
+ self,
281
+ method: RpcMethod[None, _O],
282
+ input: None = None,
283
+ *,
284
+ location: Location | NotGiven | None = NOT_GIVEN,
285
+ extra_headers: Headers | None = None,
286
+ timeout: Timeout | NotGiven = NOT_GIVEN,
287
+ ) -> _O: ...
288
+ @overload
289
+ def call(
290
+ self,
291
+ method: RpcMethod[_I, _O],
292
+ input: _I,
293
+ *,
294
+ location: Location | NotGiven | None = NOT_GIVEN,
295
+ extra_headers: Headers | None = None,
296
+ timeout: Timeout | NotGiven = NOT_GIVEN,
297
+ ) -> _O: ...
298
+ def call(
299
+ self,
300
+ method: RpcMethod[Any, Any],
301
+ input: Any = None,
302
+ *,
303
+ location: Location | NotGiven | None = NOT_GIVEN,
304
+ extra_headers: Headers | None = None,
305
+ timeout: Timeout | NotGiven = NOT_GIVEN,
306
+ ) -> Any:
307
+ """Calls a method of this RPC and returns its validated output.
308
+
309
+ Raises:
310
+ RpcCallError: The method failed.
311
+ RpcCallInternalError: The method crashed or returned invalid output.
312
+ """
313
+ _check_method(self.definition, method)
314
+ rpc_id = self.definition.id
315
+ try:
316
+ result = self._client.rpc.call(
317
+ rpc_id,
318
+ method.name,
319
+ input=method.encode(input),
320
+ location=location,
321
+ extra_headers=extra_headers,
322
+ timeout=timeout,
323
+ )
324
+ except (RpcError, RpcInternalError) as exc:
325
+ raise method.error(rpc_id, exc) from exc
326
+ return method.decode(rpc_id, result.output)
327
+
328
+ def events(self) -> Subscription[RpcEvent]:
329
+ """Subscribes to every event of this RPC, as raw events, on the client's shared event stream."""
330
+ prefix = f"rpc.{self.definition.id}."
331
+ return self._client.events.of_type(RpcEvent).where(lambda event: event.type.startswith(prefix))
332
+
333
+ def subscribe(self, event: RpcEventDefinition[_T]) -> RpcEvents[_T]:
334
+ """Subscribes to one event of this RPC, with validated data, on the client's shared event stream."""
335
+ _check_event(self.definition, event)
336
+ wire = self.definition.event_type(event)
337
+ return RpcEvents(self._client.events.of_type(RpcEvent).where(lambda raw: raw.type == wire), event)
338
+
339
+
340
+ class AsyncRpcClient(Generic[_D]):
341
+ """A plugin RPC bound to an asynchronous client. Get one with ``client.rpc(Definition)``."""
342
+
343
+ def __init__(self, client: AsyncOpencode, definition: type[_D]) -> None:
344
+ self._client = client
345
+ self.definition = definition
346
+
347
+ @overload
348
+ async def call(
349
+ self,
350
+ method: RpcMethod[None, _O],
351
+ input: None = None,
352
+ *,
353
+ location: Location | NotGiven | None = NOT_GIVEN,
354
+ extra_headers: Headers | None = None,
355
+ timeout: Timeout | NotGiven = NOT_GIVEN,
356
+ ) -> _O: ...
357
+ @overload
358
+ async def call(
359
+ self,
360
+ method: RpcMethod[_I, _O],
361
+ input: _I,
362
+ *,
363
+ location: Location | NotGiven | None = NOT_GIVEN,
364
+ extra_headers: Headers | None = None,
365
+ timeout: Timeout | NotGiven = NOT_GIVEN,
366
+ ) -> _O: ...
367
+ async def call(
368
+ self,
369
+ method: RpcMethod[Any, Any],
370
+ input: Any = None,
371
+ *,
372
+ location: Location | NotGiven | None = NOT_GIVEN,
373
+ extra_headers: Headers | None = None,
374
+ timeout: Timeout | NotGiven = NOT_GIVEN,
375
+ ) -> Any:
376
+ """Calls a method of this RPC and returns its validated output.
377
+
378
+ Raises:
379
+ RpcCallError: The method failed.
380
+ RpcCallInternalError: The method crashed or returned invalid output.
381
+ """
382
+ _check_method(self.definition, method)
383
+ rpc_id = self.definition.id
384
+ try:
385
+ result = await self._client.rpc.call(
386
+ rpc_id,
387
+ method.name,
388
+ input=method.encode(input),
389
+ location=location,
390
+ extra_headers=extra_headers,
391
+ timeout=timeout,
392
+ )
393
+ except (RpcError, RpcInternalError) as exc:
394
+ raise method.error(rpc_id, exc) from exc
395
+ return method.decode(rpc_id, result.output)
396
+
397
+ def events(self) -> AsyncSubscription[RpcEvent]:
398
+ """Subscribes to every event of this RPC, as raw events, on the client's shared event stream."""
399
+ prefix = f"rpc.{self.definition.id}."
400
+ return self._client.events.of_type(RpcEvent).where(lambda event: event.type.startswith(prefix))
401
+
402
+ def subscribe(self, event: RpcEventDefinition[_T]) -> AsyncRpcEvents[_T]:
403
+ """Subscribes to one event of this RPC, with validated data, on the client's shared event stream."""
404
+ _check_event(self.definition, event)
405
+ wire = self.definition.event_type(event)
406
+ return AsyncRpcEvents(self._client.events.of_type(RpcEvent).where(lambda raw: raw.type == wire), event)
407
+
408
+
409
+ class RpcEvents(Generic[_T]):
410
+ """Events of one kind from a plugin RPC. Iterate with ``for``; close it, or use ``with``, to leave."""
411
+
412
+ def __init__(self, source: Subscription[RpcEvent], event: RpcEventDefinition[_T]) -> None:
413
+ self._source = source
414
+ self._event = event
415
+
416
+ def __iter__(self) -> Self:
417
+ return self
418
+
419
+ def __next__(self) -> RpcEventPayload[_T]:
420
+ return self._event.parse(next(self._source))
421
+
422
+ def get(self, timeout: float | None = None) -> RpcEventPayload[_T]:
423
+ """Returns the next event, waiting up to ``timeout`` seconds. See :meth:`Subscription.get`."""
424
+ return self._event.parse(self._source.get(timeout))
425
+
426
+ def close(self) -> None:
427
+ """Leaves the shared event stream."""
428
+ self._source.close()
429
+
430
+ def __enter__(self) -> Self:
431
+ return self
432
+
433
+ def __exit__(
434
+ self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None
435
+ ) -> None:
436
+ self.close()
437
+
438
+
439
+ class AsyncRpcEvents(Generic[_T]):
440
+ """Events of one kind from a plugin RPC. Iterate with ``async for``; close it, or use ``async with``."""
441
+
442
+ def __init__(self, source: AsyncSubscription[RpcEvent], event: RpcEventDefinition[_T]) -> None:
443
+ self._source = source
444
+ self._event = event
445
+
446
+ def __aiter__(self) -> Self:
447
+ return self
448
+
449
+ async def __anext__(self) -> RpcEventPayload[_T]:
450
+ return self._event.parse(await self._source.__anext__())
451
+
452
+ async def get(self) -> RpcEventPayload[_T]:
453
+ """Returns the next event. See :meth:`AsyncSubscription.get`."""
454
+ return self._event.parse(await self._source.get())
455
+
456
+ async def close(self) -> None:
457
+ """Leaves the shared event stream."""
458
+ await self._source.close()
459
+
460
+ async def __aenter__(self) -> Self:
461
+ return self
462
+
463
+ async def __aexit__(
464
+ self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None
465
+ ) -> None:
466
+ await self.close()
467
+
468
+
469
+ class TypedRpcResource(RpcResource):
470
+ """``client.rpc``: the generated ``call`` plus ``client.rpc(Definition)`` for typed access."""
471
+
472
+ def __call__(self, definition: type[_D]) -> RpcClient[_D]:
473
+ """Binds a plugin RPC declaration to this client."""
474
+ return RpcClient(self._client, definition)
475
+
476
+
477
+ class AsyncTypedRpcResource(AsyncRpcResource):
478
+ """``client.rpc``: the generated ``call`` plus ``client.rpc(Definition)`` for typed access."""
479
+
480
+ def __call__(self, definition: type[_D]) -> AsyncRpcClient[_D]:
481
+ """Binds a plugin RPC declaration to this client."""
482
+ return AsyncRpcClient(self._client, definition)
opencode/_streaming.py CHANGED
@@ -2,8 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import contextlib
5
6
  import json
6
7
  import re
8
+ import socket
7
9
  from dataclasses import dataclass, field
8
10
  from typing import TYPE_CHECKING, Any, Generic, TypeVar
9
11
 
@@ -14,7 +16,7 @@ from typing_extensions import Self
14
16
  from ._exceptions import APIConnectionError, APIResponseValidationError, validation_summary
15
17
 
16
18
  if TYPE_CHECKING:
17
- from collections.abc import AsyncGenerator, AsyncIterator, Generator, Iterator
19
+ from collections.abc import AsyncGenerator, AsyncIterator, Callable, Generator, Iterator
18
20
  from types import TracebackType
19
21
 
20
22
  __all__ = ["AsyncStream", "ServerSentEvent", "Stream"]
@@ -111,6 +113,12 @@ class _StreamBase(Generic[_T]):
111
113
  def __init__(self, response: httpx.Response, adapter: pydantic.TypeAdapter[_T]) -> None:
112
114
  self.response = response
113
115
  self._adapter = adapter
116
+ self.on_activity: Callable[[], object] | None = None
117
+ """Called for every chunk the stream receives, including keepalive comments that carry no event."""
118
+
119
+ def _activity(self) -> None:
120
+ if self.on_activity is not None:
121
+ self.on_activity()
114
122
 
115
123
  def _decode(self, event: ServerSentEvent) -> _T:
116
124
  try:
@@ -148,6 +156,7 @@ class Stream(_StreamBase[_T]):
148
156
  try:
149
157
  splitter = _LineSplitter()
150
158
  for chunk in self.response.iter_text():
159
+ self._activity()
151
160
  for line in splitter.feed(chunk):
152
161
  if (event := decoder.feed_line(line)) is not None:
153
162
  yield self._decode(event)
@@ -166,6 +175,17 @@ class Stream(_StreamBase[_T]):
166
175
  self._iterator.close()
167
176
  self.response.close()
168
177
 
178
+ def interrupt(self) -> None:
179
+ """Unblocks a read in progress on another thread, which then ends with :class:`APIConnectionError`.
180
+
181
+ ``close`` must run on the iterating thread. Call this from any other thread first to wake it.
182
+ """
183
+ network_stream: Any = self.response.extensions.get("network_stream")
184
+ sock: Any = network_stream.get_extra_info("socket") if network_stream is not None else None
185
+ if isinstance(sock, socket.socket):
186
+ with contextlib.suppress(OSError):
187
+ sock.shutdown(socket.SHUT_RDWR)
188
+
169
189
  def __enter__(self) -> Self:
170
190
  return self
171
191
 
@@ -200,6 +220,7 @@ class AsyncStream(_StreamBase[_T]):
200
220
  try:
201
221
  splitter = _LineSplitter()
202
222
  async for chunk in self.response.aiter_text():
223
+ self._activity()
203
224
  for line in splitter.feed(chunk):
204
225
  if (event := decoder.feed_line(line)) is not None:
205
226
  yield self._decode(event)
@@ -11,6 +11,7 @@ from functools import cached_property
11
11
  from typing import cast
12
12
 
13
13
  from .._base_client import RequestOptions
14
+ from .._pty import AsyncPersistentPtyAttachMixin, PersistentPtyAttachMixin
14
15
  from .._qs import encode_path_segment
15
16
  from .._resource import AsyncAPIResource, SyncAPIResource
16
17
  from .._types import NOT_GIVEN, Body, Headers, NotGiven, Query, Timeout
@@ -30,7 +31,7 @@ from ..types import (
30
31
  __all__ = ["ExperimentalResource", "AsyncExperimentalResource"]
31
32
 
32
33
 
33
- class ExperimentalPersistentPtyResource(SyncAPIResource):
34
+ class ExperimentalPersistentPtyResource(PersistentPtyAttachMixin):
34
35
  """Operations under ``client.experimental.persistent_pty`` (OpenCode ``experimental.persistentPty``)."""
35
36
 
36
37
  def read(
@@ -396,7 +397,7 @@ class ExperimentalResource(SyncAPIResource):
396
397
  return ExperimentalPersistentPtyResource(self._client)
397
398
 
398
399
 
399
- class AsyncExperimentalPersistentPtyResource(AsyncAPIResource):
400
+ class AsyncExperimentalPersistentPtyResource(AsyncPersistentPtyAttachMixin):
400
401
  """Operations under ``client.experimental.persistent_pty`` (OpenCode ``experimental.persistentPty``)."""
401
402
 
402
403
  async def read(
opencode/resources/pty.py CHANGED
@@ -10,6 +10,7 @@ from functools import cached_property
10
10
  from typing import cast
11
11
 
12
12
  from .._base_client import RequestOptions
13
+ from .._pty import AsyncPtyAttachMixin, PtyAttachMixin
13
14
  from .._qs import encode_path_segment
14
15
  from .._resource import AsyncAPIResource, SyncAPIResource
15
16
  from .._types import NOT_GIVEN, Body, Headers, NotGiven, Query, Timeout
@@ -72,7 +73,7 @@ class PtyConnectResource(SyncAPIResource):
72
73
  )
73
74
 
74
75
 
75
- class PtyResource(SyncAPIResource):
76
+ class PtyResource(PtyAttachMixin):
76
77
  """Operations under ``client.pty`` (OpenCode ``pty``)."""
77
78
 
78
79
  @cached_property
@@ -318,7 +319,7 @@ class AsyncPtyConnectResource(AsyncAPIResource):
318
319
  )
319
320
 
320
321
 
321
- class AsyncPtyResource(AsyncAPIResource):
322
+ class AsyncPtyResource(AsyncPtyAttachMixin):
322
323
  """Operations under ``client.pty`` (OpenCode ``pty``)."""
323
324
 
324
325
  @cached_property