opencode-python-sdk 0.2.2__py3-none-any.whl → 0.2.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/__init__.py +9 -0
- opencode/_aiohttp.py +44 -0
- opencode/_base_client.py +237 -11
- opencode/_client.py +8 -2
- opencode/_logs.py +29 -0
- opencode/_pagination.py +14 -0
- opencode/_response.py +316 -0
- opencode/resources/agent.py +63 -0
- opencode/resources/command.py +59 -0
- opencode/resources/config.py +93 -20
- opencode/resources/credential.py +149 -56
- opencode/resources/debug.py +156 -20
- opencode/resources/event.py +59 -0
- opencode/resources/experimental.py +210 -36
- opencode/resources/file.py +71 -0
- opencode/resources/form.py +59 -0
- opencode/resources/generate.py +59 -0
- opencode/resources/integration.py +538 -128
- opencode/resources/location.py +87 -18
- opencode/resources/mcp.py +252 -82
- opencode/resources/message.py +59 -0
- opencode/resources/migration.py +126 -0
- opencode/resources/model.py +63 -0
- opencode/resources/permission.py +272 -38
- opencode/resources/plugin.py +95 -22
- opencode/resources/project.py +63 -0
- opencode/resources/provider.py +63 -0
- opencode/resources/pty.py +172 -20
- opencode/resources/reference.py +59 -0
- opencode/resources/rpc.py +59 -0
- opencode/resources/server.py +67 -0
- opencode/resources/session.py +1159 -405
- opencode/resources/shell.py +101 -20
- opencode/resources/skill.py +59 -0
- opencode/resources/vcs.py +172 -20
- opencode/resources/voice.py +59 -0
- opencode/resources/websearch.py +63 -0
- opencode/resources/worktree.py +123 -40
- {opencode_python_sdk-0.2.2.dist-info → opencode_python_sdk-0.2.3.dist-info}/METADATA +90 -2
- opencode_python_sdk-0.2.3.dist-info/RECORD +67 -0
- opencode_python_sdk-0.2.2.dist-info/RECORD +0 -64
- {opencode_python_sdk-0.2.2.dist-info → opencode_python_sdk-0.2.3.dist-info}/WHEEL +0 -0
- {opencode_python_sdk-0.2.2.dist-info → opencode_python_sdk-0.2.3.dist-info}/licenses/LICENSE +0 -0
opencode/__init__.py
CHANGED
|
@@ -17,6 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
from typing import TYPE_CHECKING, Any
|
|
18
18
|
|
|
19
19
|
from . import service
|
|
20
|
+
from ._aiohttp import DefaultAioHttpClient
|
|
20
21
|
from ._client import AsyncOpencode, Opencode
|
|
21
22
|
|
|
22
23
|
# <generated:error-imports>
|
|
@@ -75,9 +76,11 @@ from ._exceptions import (
|
|
|
75
76
|
ServiceUnavailableError,
|
|
76
77
|
UnprocessableEntityError,
|
|
77
78
|
)
|
|
79
|
+
from ._logs import setup_logging as _setup_logging
|
|
78
80
|
from ._models import BaseModel
|
|
79
81
|
from ._pagination import AsyncCursorPage, AsyncPaginator, SyncCursorPage
|
|
80
82
|
from ._pty import AsyncPtyConnection, PtyConnection, PtyConnectionError
|
|
83
|
+
from ._response import APIResponse, AsyncAPIResponse
|
|
81
84
|
from ._server import AsyncOpencodeServer, OpencodeServer, ServerStartError
|
|
82
85
|
from ._service import Endpoint, ServiceError
|
|
83
86
|
from ._streaming import AsyncStream, Stream
|
|
@@ -123,10 +126,12 @@ __all__ = [
|
|
|
123
126
|
"API_VERSION",
|
|
124
127
|
"NOT_GIVEN",
|
|
125
128
|
"APIConnectionError",
|
|
129
|
+
"APIResponse",
|
|
126
130
|
"APIError",
|
|
127
131
|
"APIResponseValidationError",
|
|
128
132
|
"APIStatusError",
|
|
129
133
|
"APITimeoutError",
|
|
134
|
+
"AsyncAPIResponse",
|
|
130
135
|
"AsyncCursorPage",
|
|
131
136
|
"AsyncOpencode",
|
|
132
137
|
"AsyncOpencodeServer",
|
|
@@ -138,6 +143,7 @@ __all__ = [
|
|
|
138
143
|
"BaseModel",
|
|
139
144
|
"ConflictError",
|
|
140
145
|
"ContentTooLargeError",
|
|
146
|
+
"DefaultAioHttpClient",
|
|
141
147
|
"Endpoint",
|
|
142
148
|
"InternalServerError",
|
|
143
149
|
"NotFoundError",
|
|
@@ -224,3 +230,6 @@ def __getattr__(name: str) -> Any:
|
|
|
224
230
|
|
|
225
231
|
return importlib.import_module(".types", __name__)
|
|
226
232
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
_setup_logging()
|
opencode/_aiohttp.py
ADDED
|
@@ -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)
|
opencode/_base_client.py
CHANGED
|
@@ -37,12 +37,13 @@ from ._exceptions import (
|
|
|
37
37
|
validation_summary,
|
|
38
38
|
)
|
|
39
39
|
from ._qs import encode_query, to_jsonable
|
|
40
|
+
from ._response import APIResponse, AsyncAPIResponse, raw_mode
|
|
40
41
|
from ._streaming import AsyncStream, Stream
|
|
41
42
|
from ._types import NOT_GIVEN, Body, Headers, NotGiven, Query, Timeout
|
|
42
43
|
from ._version import __version__
|
|
43
44
|
|
|
44
45
|
if TYPE_CHECKING:
|
|
45
|
-
from collections.abc import Mapping
|
|
46
|
+
from collections.abc import Callable, Coroutine, Mapping
|
|
46
47
|
from types import TracebackType
|
|
47
48
|
|
|
48
49
|
from ._pagination import AsyncPaginator, SyncCursorPage
|
|
@@ -354,13 +355,13 @@ class SyncAPIClient(_BaseClient):
|
|
|
354
355
|
) -> None:
|
|
355
356
|
self.close()
|
|
356
357
|
|
|
357
|
-
def _send(self, options: RequestOptions) -> httpx.Response:
|
|
358
|
+
def _send(self, options: RequestOptions, *, stream_body: bool = False) -> httpx.Response:
|
|
358
359
|
attempt = 0
|
|
359
360
|
while True:
|
|
360
361
|
request = self._build_request(options)
|
|
361
362
|
log.debug("Request %s %s (attempt %d)", request.method, request.url, attempt + 1)
|
|
362
363
|
try:
|
|
363
|
-
response = self._http.send(request, stream=options.stream)
|
|
364
|
+
response = self._http.send(request, stream=options.stream or stream_body)
|
|
364
365
|
except httpx.TransportError as exc:
|
|
365
366
|
if attempt < self.max_retries and self._should_retry_error(options, exc):
|
|
366
367
|
delay = self._retry_delay(attempt, None)
|
|
@@ -384,7 +385,7 @@ class SyncAPIClient(_BaseClient):
|
|
|
384
385
|
time.sleep(delay)
|
|
385
386
|
attempt += 1
|
|
386
387
|
continue
|
|
387
|
-
if options.stream:
|
|
388
|
+
if options.stream or stream_body:
|
|
388
389
|
response.read()
|
|
389
390
|
raise self._status_error(response, self._decode_error_body(response))
|
|
390
391
|
|
|
@@ -397,13 +398,23 @@ class SyncAPIClient(_BaseClient):
|
|
|
397
398
|
unwrap: bool = False,
|
|
398
399
|
) -> Any:
|
|
399
400
|
"""Sends a request and returns the decoded response."""
|
|
401
|
+
if raw_mode() is not None:
|
|
402
|
+
return self._raw(options, lambda response: self._parse(response, kind, response_type, unwrap))
|
|
400
403
|
response = self._send(options)
|
|
401
404
|
return self._parse(response, kind, response_type, unwrap)
|
|
402
405
|
|
|
406
|
+
def _raw(self, options: RequestOptions, parser: Callable[[httpx.Response], Any]) -> APIResponse[Any]:
|
|
407
|
+
response = self._send(options, stream_body=raw_mode() == "stream")
|
|
408
|
+
return APIResponse(response, parser, event_stream=options.stream)
|
|
409
|
+
|
|
403
410
|
def stream(self, options: RequestOptions, *, item_type: type[_T] | Any) -> Stream[Any]:
|
|
404
411
|
"""Opens a server-sent event stream."""
|
|
405
412
|
options.stream = True
|
|
406
|
-
|
|
413
|
+
if raw_mode() is not None:
|
|
414
|
+
return cast("Stream[Any]", self._raw(options, lambda response: self._open_stream(response, item_type)))
|
|
415
|
+
return self._open_stream(self._send(options), item_type)
|
|
416
|
+
|
|
417
|
+
def _open_stream(self, response: httpx.Response, item_type: Any) -> Stream[Any]:
|
|
407
418
|
content_type = response.headers.get("content-type", "")
|
|
408
419
|
if content_type.split(";", 1)[0].strip().lower() != "text/event-stream":
|
|
409
420
|
response.read()
|
|
@@ -413,10 +424,109 @@ class SyncAPIClient(_BaseClient):
|
|
|
413
424
|
)
|
|
414
425
|
return Stream(response, type_adapter(item_type))
|
|
415
426
|
|
|
427
|
+
def get(
|
|
428
|
+
self,
|
|
429
|
+
path: str,
|
|
430
|
+
*,
|
|
431
|
+
cast_to: Any = object,
|
|
432
|
+
query: Query | None = None,
|
|
433
|
+
headers: Headers | None = None,
|
|
434
|
+
timeout: Timeout | NotGiven = NOT_GIVEN,
|
|
435
|
+
) -> Any:
|
|
436
|
+
"""Sends ``GET path`` (relative to ``base_url``) with the client's auth, retries and error mapping.
|
|
437
|
+
|
|
438
|
+
For endpoints this client does not cover yet. The JSON response is validated against ``cast_to``.
|
|
439
|
+
"""
|
|
440
|
+
options = RequestOptions(method="GET", path=path, extra_query=query, extra_headers=headers, timeout=timeout)
|
|
441
|
+
return self.request(options, response_type=cast_to)
|
|
442
|
+
|
|
443
|
+
def post(
|
|
444
|
+
self,
|
|
445
|
+
path: str,
|
|
446
|
+
*,
|
|
447
|
+
cast_to: Any = object,
|
|
448
|
+
body: object | NotGiven = NOT_GIVEN,
|
|
449
|
+
query: Query | None = None,
|
|
450
|
+
headers: Headers | None = None,
|
|
451
|
+
timeout: Timeout | NotGiven = NOT_GIVEN,
|
|
452
|
+
) -> Any:
|
|
453
|
+
"""Sends ``POST path`` (relative to ``base_url``) with the client's auth, retries and error mapping.
|
|
454
|
+
|
|
455
|
+
For endpoints this client does not cover yet. The JSON response is validated against ``cast_to``.
|
|
456
|
+
"""
|
|
457
|
+
options = RequestOptions(
|
|
458
|
+
method="POST", path=path, body=body, extra_query=query, extra_headers=headers, timeout=timeout
|
|
459
|
+
)
|
|
460
|
+
return self.request(options, response_type=cast_to)
|
|
461
|
+
|
|
462
|
+
def put(
|
|
463
|
+
self,
|
|
464
|
+
path: str,
|
|
465
|
+
*,
|
|
466
|
+
cast_to: Any = object,
|
|
467
|
+
body: object | NotGiven = NOT_GIVEN,
|
|
468
|
+
query: Query | None = None,
|
|
469
|
+
headers: Headers | None = None,
|
|
470
|
+
timeout: Timeout | NotGiven = NOT_GIVEN,
|
|
471
|
+
) -> Any:
|
|
472
|
+
"""Sends ``PUT path`` (relative to ``base_url``) with the client's auth, retries and error mapping.
|
|
473
|
+
|
|
474
|
+
For endpoints this client does not cover yet. The JSON response is validated against ``cast_to``.
|
|
475
|
+
"""
|
|
476
|
+
options = RequestOptions(
|
|
477
|
+
method="PUT", path=path, body=body, extra_query=query, extra_headers=headers, timeout=timeout
|
|
478
|
+
)
|
|
479
|
+
return self.request(options, response_type=cast_to)
|
|
480
|
+
|
|
481
|
+
def patch(
|
|
482
|
+
self,
|
|
483
|
+
path: str,
|
|
484
|
+
*,
|
|
485
|
+
cast_to: Any = object,
|
|
486
|
+
body: object | NotGiven = NOT_GIVEN,
|
|
487
|
+
query: Query | None = None,
|
|
488
|
+
headers: Headers | None = None,
|
|
489
|
+
timeout: Timeout | NotGiven = NOT_GIVEN,
|
|
490
|
+
) -> Any:
|
|
491
|
+
"""Sends ``PATCH path`` (relative to ``base_url``) with the client's auth, retries and error mapping.
|
|
492
|
+
|
|
493
|
+
For endpoints this client does not cover yet. The JSON response is validated against ``cast_to``.
|
|
494
|
+
"""
|
|
495
|
+
options = RequestOptions(
|
|
496
|
+
method="PATCH", path=path, body=body, extra_query=query, extra_headers=headers, timeout=timeout
|
|
497
|
+
)
|
|
498
|
+
return self.request(options, response_type=cast_to)
|
|
499
|
+
|
|
500
|
+
def delete(
|
|
501
|
+
self,
|
|
502
|
+
path: str,
|
|
503
|
+
*,
|
|
504
|
+
cast_to: Any = object,
|
|
505
|
+
query: Query | None = None,
|
|
506
|
+
headers: Headers | None = None,
|
|
507
|
+
timeout: Timeout | NotGiven = NOT_GIVEN,
|
|
508
|
+
) -> Any:
|
|
509
|
+
"""Sends ``DELETE path`` (relative to ``base_url``) with the client's auth, retries and error mapping.
|
|
510
|
+
|
|
511
|
+
For endpoints this client does not cover yet. The JSON response is validated against ``cast_to``.
|
|
512
|
+
"""
|
|
513
|
+
options = RequestOptions(method="DELETE", path=path, extra_query=query, extra_headers=headers, timeout=timeout)
|
|
514
|
+
return self.request(options, response_type=cast_to)
|
|
515
|
+
|
|
416
516
|
def page(self, options: RequestOptions, *, response_type: Any) -> SyncCursorPage[Any]:
|
|
417
517
|
"""Fetches the first page of a cursor-paginated list."""
|
|
418
518
|
from ._pagination import SyncCursorPage
|
|
419
519
|
|
|
520
|
+
if raw_mode() is not None:
|
|
521
|
+
return cast(
|
|
522
|
+
"SyncCursorPage[Any]",
|
|
523
|
+
self._raw(
|
|
524
|
+
options,
|
|
525
|
+
lambda response: SyncCursorPage.build(
|
|
526
|
+
self, options, response_type, self._parse(response, "json", response_type, False)
|
|
527
|
+
),
|
|
528
|
+
),
|
|
529
|
+
)
|
|
420
530
|
return SyncCursorPage.fetch(self, options, response_type)
|
|
421
531
|
|
|
422
532
|
|
|
@@ -473,13 +583,13 @@ class AsyncAPIClient(_BaseClient):
|
|
|
473
583
|
) -> None:
|
|
474
584
|
await self.close()
|
|
475
585
|
|
|
476
|
-
async def _send(self, options: RequestOptions) -> httpx.Response:
|
|
586
|
+
async def _send(self, options: RequestOptions, *, stream_body: bool = False) -> httpx.Response:
|
|
477
587
|
attempt = 0
|
|
478
588
|
while True:
|
|
479
589
|
request = self._build_request(options)
|
|
480
590
|
log.debug("Request %s %s (attempt %d)", request.method, request.url, attempt + 1)
|
|
481
591
|
try:
|
|
482
|
-
response = await self._http.send(request, stream=options.stream)
|
|
592
|
+
response = await self._http.send(request, stream=options.stream or stream_body)
|
|
483
593
|
except httpx.TransportError as exc:
|
|
484
594
|
if attempt < self.max_retries and self._should_retry_error(options, exc):
|
|
485
595
|
delay = self._retry_delay(attempt, None)
|
|
@@ -503,7 +613,7 @@ class AsyncAPIClient(_BaseClient):
|
|
|
503
613
|
await anyio.sleep(delay)
|
|
504
614
|
attempt += 1
|
|
505
615
|
continue
|
|
506
|
-
if options.stream:
|
|
616
|
+
if options.stream or stream_body:
|
|
507
617
|
await response.aread()
|
|
508
618
|
raise self._status_error(response, self._decode_error_body(response))
|
|
509
619
|
|
|
@@ -516,13 +626,31 @@ class AsyncAPIClient(_BaseClient):
|
|
|
516
626
|
unwrap: bool = False,
|
|
517
627
|
) -> Any:
|
|
518
628
|
"""Sends a request and returns the decoded response."""
|
|
629
|
+
if raw_mode() is not None:
|
|
630
|
+
return await self._raw(options, lambda response: self._parse(response, kind, response_type, unwrap))
|
|
519
631
|
response = await self._send(options)
|
|
520
632
|
return self._parse(response, kind, response_type, unwrap)
|
|
521
633
|
|
|
634
|
+
async def _raw(self, options: RequestOptions, parser: Callable[[httpx.Response], Any]) -> AsyncAPIResponse[Any]:
|
|
635
|
+
response = await self._send(options, stream_body=raw_mode() == "stream")
|
|
636
|
+
return AsyncAPIResponse(response, parser, event_stream=options.stream)
|
|
637
|
+
|
|
522
638
|
async def stream(self, options: RequestOptions, *, item_type: type[_T] | Any) -> AsyncStream[Any]:
|
|
523
639
|
"""Opens a server-sent event stream."""
|
|
524
640
|
options.stream = True
|
|
525
|
-
|
|
641
|
+
if raw_mode() is not None:
|
|
642
|
+
raw = await self._raw(options, lambda response: self._open_stream(response, item_type))
|
|
643
|
+
return cast("AsyncStream[Any]", raw)
|
|
644
|
+
return await self._open_async_stream(await self._send(options), item_type)
|
|
645
|
+
|
|
646
|
+
@staticmethod
|
|
647
|
+
def _open_stream(response: httpx.Response, item_type: Any) -> AsyncStream[Any]:
|
|
648
|
+
content_type = response.headers.get("content-type", "")
|
|
649
|
+
if content_type.split(";", 1)[0].strip().lower() != "text/event-stream":
|
|
650
|
+
raise APIResponseValidationError(response, None, message=f"Expected an event stream, got {content_type!r}")
|
|
651
|
+
return AsyncStream(response, type_adapter(item_type))
|
|
652
|
+
|
|
653
|
+
async def _open_async_stream(self, response: httpx.Response, item_type: Any) -> AsyncStream[Any]:
|
|
526
654
|
content_type = response.headers.get("content-type", "")
|
|
527
655
|
if content_type.split(";", 1)[0].strip().lower() != "text/event-stream":
|
|
528
656
|
await response.aread()
|
|
@@ -532,8 +660,106 @@ class AsyncAPIClient(_BaseClient):
|
|
|
532
660
|
)
|
|
533
661
|
return AsyncStream(response, type_adapter(item_type))
|
|
534
662
|
|
|
663
|
+
async def get(
|
|
664
|
+
self,
|
|
665
|
+
path: str,
|
|
666
|
+
*,
|
|
667
|
+
cast_to: Any = object,
|
|
668
|
+
query: Query | None = None,
|
|
669
|
+
headers: Headers | None = None,
|
|
670
|
+
timeout: Timeout | NotGiven = NOT_GIVEN,
|
|
671
|
+
) -> Any:
|
|
672
|
+
"""Sends ``GET path`` (relative to ``base_url``) with the client's auth, retries and error mapping.
|
|
673
|
+
|
|
674
|
+
For endpoints this client does not cover yet. The JSON response is validated against ``cast_to``.
|
|
675
|
+
"""
|
|
676
|
+
options = RequestOptions(method="GET", path=path, extra_query=query, extra_headers=headers, timeout=timeout)
|
|
677
|
+
return await self.request(options, response_type=cast_to)
|
|
678
|
+
|
|
679
|
+
async def post(
|
|
680
|
+
self,
|
|
681
|
+
path: str,
|
|
682
|
+
*,
|
|
683
|
+
cast_to: Any = object,
|
|
684
|
+
body: object | NotGiven = NOT_GIVEN,
|
|
685
|
+
query: Query | None = None,
|
|
686
|
+
headers: Headers | None = None,
|
|
687
|
+
timeout: Timeout | NotGiven = NOT_GIVEN,
|
|
688
|
+
) -> Any:
|
|
689
|
+
"""Sends ``POST path`` (relative to ``base_url``) with the client's auth, retries and error mapping.
|
|
690
|
+
|
|
691
|
+
For endpoints this client does not cover yet. The JSON response is validated against ``cast_to``.
|
|
692
|
+
"""
|
|
693
|
+
options = RequestOptions(
|
|
694
|
+
method="POST", path=path, body=body, extra_query=query, extra_headers=headers, timeout=timeout
|
|
695
|
+
)
|
|
696
|
+
return await self.request(options, response_type=cast_to)
|
|
697
|
+
|
|
698
|
+
async def put(
|
|
699
|
+
self,
|
|
700
|
+
path: str,
|
|
701
|
+
*,
|
|
702
|
+
cast_to: Any = object,
|
|
703
|
+
body: object | NotGiven = NOT_GIVEN,
|
|
704
|
+
query: Query | None = None,
|
|
705
|
+
headers: Headers | None = None,
|
|
706
|
+
timeout: Timeout | NotGiven = NOT_GIVEN,
|
|
707
|
+
) -> Any:
|
|
708
|
+
"""Sends ``PUT path`` (relative to ``base_url``) with the client's auth, retries and error mapping.
|
|
709
|
+
|
|
710
|
+
For endpoints this client does not cover yet. The JSON response is validated against ``cast_to``.
|
|
711
|
+
"""
|
|
712
|
+
options = RequestOptions(
|
|
713
|
+
method="PUT", path=path, body=body, extra_query=query, extra_headers=headers, timeout=timeout
|
|
714
|
+
)
|
|
715
|
+
return await self.request(options, response_type=cast_to)
|
|
716
|
+
|
|
717
|
+
async def patch(
|
|
718
|
+
self,
|
|
719
|
+
path: str,
|
|
720
|
+
*,
|
|
721
|
+
cast_to: Any = object,
|
|
722
|
+
body: object | NotGiven = NOT_GIVEN,
|
|
723
|
+
query: Query | None = None,
|
|
724
|
+
headers: Headers | None = None,
|
|
725
|
+
timeout: Timeout | NotGiven = NOT_GIVEN,
|
|
726
|
+
) -> Any:
|
|
727
|
+
"""Sends ``PATCH path`` (relative to ``base_url``) with the client's auth, retries and error mapping.
|
|
728
|
+
|
|
729
|
+
For endpoints this client does not cover yet. The JSON response is validated against ``cast_to``.
|
|
730
|
+
"""
|
|
731
|
+
options = RequestOptions(
|
|
732
|
+
method="PATCH", path=path, body=body, extra_query=query, extra_headers=headers, timeout=timeout
|
|
733
|
+
)
|
|
734
|
+
return await self.request(options, response_type=cast_to)
|
|
735
|
+
|
|
736
|
+
async def delete(
|
|
737
|
+
self,
|
|
738
|
+
path: str,
|
|
739
|
+
*,
|
|
740
|
+
cast_to: Any = object,
|
|
741
|
+
query: Query | None = None,
|
|
742
|
+
headers: Headers | None = None,
|
|
743
|
+
timeout: Timeout | NotGiven = NOT_GIVEN,
|
|
744
|
+
) -> Any:
|
|
745
|
+
"""Sends ``DELETE path`` (relative to ``base_url``) with the client's auth, retries and error mapping.
|
|
746
|
+
|
|
747
|
+
For endpoints this client does not cover yet. The JSON response is validated against ``cast_to``.
|
|
748
|
+
"""
|
|
749
|
+
options = RequestOptions(method="DELETE", path=path, extra_query=query, extra_headers=headers, timeout=timeout)
|
|
750
|
+
return await self.request(options, response_type=cast_to)
|
|
751
|
+
|
|
535
752
|
def page(self, options: RequestOptions, *, response_type: Any) -> AsyncPaginator[Any]:
|
|
536
753
|
"""Returns an awaitable, async-iterable paginator over a cursor-paginated list."""
|
|
537
|
-
from ._pagination import AsyncPaginator
|
|
538
|
-
|
|
754
|
+
from ._pagination import AsyncCursorPage, AsyncPaginator
|
|
755
|
+
|
|
756
|
+
if raw_mode() is not None:
|
|
757
|
+
# Awaited by the raw-response wrapper: the raw form of a list call is the first page's response.
|
|
758
|
+
raw: Coroutine[Any, Any, AsyncAPIResponse[Any]] = self._raw(
|
|
759
|
+
options,
|
|
760
|
+
lambda response: AsyncCursorPage.build(
|
|
761
|
+
self, options, response_type, self._parse(response, "json", response_type, False)
|
|
762
|
+
),
|
|
763
|
+
)
|
|
764
|
+
return cast("AsyncPaginator[Any]", raw)
|
|
539
765
|
return AsyncPaginator(self, options, response_type)
|
opencode/_client.py
CHANGED
|
@@ -202,7 +202,8 @@ class Opencode(SyncAPIClient, SyncResources):
|
|
|
202
202
|
) -> Self:
|
|
203
203
|
"""Returns a client with some options changed. It shares this client's connection pool.
|
|
204
204
|
|
|
205
|
-
``default_headers`` and ``default_query`` are merged over the current values.
|
|
205
|
+
``default_headers`` and ``default_query`` are merged over the current values. Closing the copy leaves the
|
|
206
|
+
pool open; it closes with this client. Also available as ``copy``.
|
|
206
207
|
"""
|
|
207
208
|
client = type(self)(
|
|
208
209
|
base_url=base_url or self.base_url,
|
|
@@ -217,6 +218,8 @@ class Opencode(SyncAPIClient, SyncResources):
|
|
|
217
218
|
)
|
|
218
219
|
return client
|
|
219
220
|
|
|
221
|
+
copy = with_options
|
|
222
|
+
|
|
220
223
|
|
|
221
224
|
class AsyncOpencode(AsyncAPIClient, AsyncResources):
|
|
222
225
|
"""Asynchronous client for an OpenCode server. Works with asyncio and trio.
|
|
@@ -345,7 +348,8 @@ class AsyncOpencode(AsyncAPIClient, AsyncResources):
|
|
|
345
348
|
) -> Self:
|
|
346
349
|
"""Returns a client with some options changed. It shares this client's connection pool.
|
|
347
350
|
|
|
348
|
-
``default_headers`` and ``default_query`` are merged over the current values.
|
|
351
|
+
``default_headers`` and ``default_query`` are merged over the current values. Closing the copy leaves the
|
|
352
|
+
pool open; it closes with this client. Also available as ``copy``.
|
|
349
353
|
"""
|
|
350
354
|
client = type(self)(
|
|
351
355
|
base_url=base_url or self.base_url,
|
|
@@ -359,3 +363,5 @@ class AsyncOpencode(AsyncAPIClient, AsyncResources):
|
|
|
359
363
|
http_client=self._http,
|
|
360
364
|
)
|
|
361
365
|
return client
|
|
366
|
+
|
|
367
|
+
copy = with_options
|
opencode/_logs.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""``OPENCODE_LOG`` support: configure the ``opencode`` logger from the environment."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("opencode")
|
|
9
|
+
|
|
10
|
+
_LEVELS = {"debug": logging.DEBUG, "info": logging.INFO}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def setup_logging() -> None:
|
|
14
|
+
"""Attach a stderr handler to the ``opencode`` logger when ``OPENCODE_LOG`` is ``debug`` or ``info``.
|
|
15
|
+
|
|
16
|
+
Does nothing when the variable is unset or has any other value. The OpenCode CLI reads
|
|
17
|
+
``OPENCODE_LOG_LEVEL``, not ``OPENCODE_LOG``, so the two do not clash.
|
|
18
|
+
"""
|
|
19
|
+
level = _LEVELS.get(os.environ.get("OPENCODE_LOG", "").strip().lower())
|
|
20
|
+
if level is None:
|
|
21
|
+
return
|
|
22
|
+
if not any(getattr(h, "_opencode_env", False) for h in logger.handlers):
|
|
23
|
+
handler = logging.StreamHandler()
|
|
24
|
+
handler.setFormatter(
|
|
25
|
+
logging.Formatter("[%(asctime)s - %(name)s:%(lineno)d - %(levelname)s] %(message)s", "%Y-%m-%d %H:%M:%S")
|
|
26
|
+
)
|
|
27
|
+
handler._opencode_env = True # type: ignore[attr-defined]
|
|
28
|
+
logger.addHandler(handler)
|
|
29
|
+
logger.setLevel(level)
|
opencode/_pagination.py
CHANGED
|
@@ -79,6 +79,13 @@ class SyncCursorPage(_PageBase[_T]):
|
|
|
79
79
|
@classmethod
|
|
80
80
|
def fetch(cls, client: SyncAPIClient, options: RequestOptions, response_type: Any) -> SyncCursorPage[Any]:
|
|
81
81
|
payload = client.request(options, response_type=response_type)
|
|
82
|
+
return cls.build(client, options, response_type, payload)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def build(
|
|
86
|
+
cls, client: SyncAPIClient, options: RequestOptions, response_type: Any, payload: Any
|
|
87
|
+
) -> SyncCursorPage[Any]:
|
|
88
|
+
"""Builds a page from an already parsed response."""
|
|
82
89
|
data, next_cursor, previous_cursor = _page_parts(payload)
|
|
83
90
|
return cls(
|
|
84
91
|
client=client,
|
|
@@ -134,6 +141,13 @@ class AsyncCursorPage(_PageBase[_T]):
|
|
|
134
141
|
@classmethod
|
|
135
142
|
async def fetch(cls, client: AsyncAPIClient, options: RequestOptions, response_type: Any) -> AsyncCursorPage[Any]:
|
|
136
143
|
payload = await client.request(options, response_type=response_type)
|
|
144
|
+
return cls.build(client, options, response_type, payload)
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def build(
|
|
148
|
+
cls, client: AsyncAPIClient, options: RequestOptions, response_type: Any, payload: Any
|
|
149
|
+
) -> AsyncCursorPage[Any]:
|
|
150
|
+
"""Builds a page from an already parsed response."""
|
|
137
151
|
data, next_cursor, previous_cursor = _page_parts(payload)
|
|
138
152
|
return cls(
|
|
139
153
|
client=client,
|