contree-client 0.1.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.
- contree_client/__init__.py +174 -0
- contree_client/aiohttp.py +153 -0
- contree_client/asyncio.py +48 -0
- contree_client/base.py +2653 -0
- contree_client/exceptions.py +97 -0
- contree_client/http.py +334 -0
- contree_client/httpx.py +230 -0
- contree_client/models.py +2004 -0
- contree_client/operations.py +478 -0
- contree_client/profiles.py +131 -0
- contree_client/py.typed +0 -0
- contree_client/requests.py +140 -0
- contree_client/runtime.py +619 -0
- contree_client/spec_info.py +2785 -0
- contree_client/sync.py +55 -0
- contree_client/testing.py +275 -0
- contree_client/types.py +37 -0
- contree_client/urllib3.py +139 -0
- contree_client-0.1.0.dist-info/METADATA +389 -0
- contree_client-0.1.0.dist-info/RECORD +22 -0
- contree_client-0.1.0.dist-info/WHEEL +4 -0
- contree_client-0.1.0.dist-info/licenses/LICENSE +177 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Contree API client, generated from the OpenAPI specification.
|
|
2
|
+
|
|
3
|
+
Pick a backend module and import its ``ContreeClient``::
|
|
4
|
+
|
|
5
|
+
from contree_client.requests import ContreeClient
|
|
6
|
+
|
|
7
|
+
Synchronous backends export ``ContreeClient``, asynchronous ones
|
|
8
|
+
export ``ContreeAsyncClient``: ``contree_client.http`` (stdlib
|
|
9
|
+
http.client, no extra dependencies), ``contree_client.urllib3`` and
|
|
10
|
+
``contree_client.requests`` (sync), ``contree_client.httpx`` (both)
|
|
11
|
+
and ``contree_client.aiohttp`` (async). All of them share the
|
|
12
|
+
interface of :class:`contree_client.types.ContreeSyncClient` or
|
|
13
|
+
:class:`contree_client.types.ContreeAsyncClient` - annotate against
|
|
14
|
+
those base classes to stay backend-agnostic.
|
|
15
|
+
|
|
16
|
+
When any installed backend will do, let the package pick one::
|
|
17
|
+
|
|
18
|
+
from contree_client.sync import ContreeClient
|
|
19
|
+
from contree_client.asyncio import ContreeAsyncClient
|
|
20
|
+
|
|
21
|
+
Generated by codegen from the OpenAPI spec - do not edit.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from .exceptions import (
|
|
25
|
+
BadRequestError,
|
|
26
|
+
ConflictError,
|
|
27
|
+
ContreeAPIError,
|
|
28
|
+
ContreeError,
|
|
29
|
+
ForbiddenError,
|
|
30
|
+
GoneError,
|
|
31
|
+
NotFoundError,
|
|
32
|
+
ServerError,
|
|
33
|
+
SSEStreamError,
|
|
34
|
+
TooEarlyError,
|
|
35
|
+
UnauthorizedError,
|
|
36
|
+
UnprocessableEntityError,
|
|
37
|
+
)
|
|
38
|
+
from .models import (
|
|
39
|
+
ACTIVE_STATUSES,
|
|
40
|
+
TERMINAL_STATUSES,
|
|
41
|
+
ContreeModel,
|
|
42
|
+
DirectoryList,
|
|
43
|
+
Error,
|
|
44
|
+
EventData,
|
|
45
|
+
EventDataCompletion,
|
|
46
|
+
EventDataExit,
|
|
47
|
+
EventDataInit,
|
|
48
|
+
EventDataNetwork,
|
|
49
|
+
EventDataNetworkConfigured,
|
|
50
|
+
EventDataShutdown,
|
|
51
|
+
EventDataSizeCap,
|
|
52
|
+
EventDataSpawn,
|
|
53
|
+
EventDataStream,
|
|
54
|
+
EventDataTruncated,
|
|
55
|
+
EventResources,
|
|
56
|
+
File,
|
|
57
|
+
FileItem,
|
|
58
|
+
FileResponse,
|
|
59
|
+
FilesListResponse,
|
|
60
|
+
FileSpec,
|
|
61
|
+
Image,
|
|
62
|
+
ImageImportMetadata,
|
|
63
|
+
ImageImportMetadataRegistry,
|
|
64
|
+
ImageImportMetadataRegistryCredentials,
|
|
65
|
+
ImageImportRegistry,
|
|
66
|
+
ImageImportRegistryCredentials,
|
|
67
|
+
ImageImportRequest,
|
|
68
|
+
ImageListResponse,
|
|
69
|
+
InstanceResourcesLimits,
|
|
70
|
+
InstanceResult,
|
|
71
|
+
InstanceResultResources,
|
|
72
|
+
InstanceResultState,
|
|
73
|
+
InstanceSpawnRequest,
|
|
74
|
+
InstanceSpawnResponse,
|
|
75
|
+
NetworkInterface,
|
|
76
|
+
OperationEvent,
|
|
77
|
+
OperationEventType,
|
|
78
|
+
OperationInstanceMetadata,
|
|
79
|
+
OperationResponse,
|
|
80
|
+
OperationResult,
|
|
81
|
+
OperationStatus,
|
|
82
|
+
OperationSummary,
|
|
83
|
+
StreamRepr,
|
|
84
|
+
WhoAmIResponse,
|
|
85
|
+
decode_chunk,
|
|
86
|
+
decode_stream,
|
|
87
|
+
parse_datetime,
|
|
88
|
+
)
|
|
89
|
+
from .profiles import (
|
|
90
|
+
Profile,
|
|
91
|
+
ProfileError,
|
|
92
|
+
load_profiles,
|
|
93
|
+
resolve_profile,
|
|
94
|
+
)
|
|
95
|
+
from .runtime import (
|
|
96
|
+
RequestSpec,
|
|
97
|
+
ResponseData,
|
|
98
|
+
RetryPolicy,
|
|
99
|
+
)
|
|
100
|
+
from .spec_info import (
|
|
101
|
+
DEFAULT_BASE_URL,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
__all__ = [
|
|
105
|
+
"ACTIVE_STATUSES",
|
|
106
|
+
"DEFAULT_BASE_URL",
|
|
107
|
+
"TERMINAL_STATUSES",
|
|
108
|
+
"BadRequestError",
|
|
109
|
+
"ConflictError",
|
|
110
|
+
"ContreeAPIError",
|
|
111
|
+
"ContreeError",
|
|
112
|
+
"ContreeModel",
|
|
113
|
+
"DirectoryList",
|
|
114
|
+
"Error",
|
|
115
|
+
"EventData",
|
|
116
|
+
"EventDataCompletion",
|
|
117
|
+
"EventDataExit",
|
|
118
|
+
"EventDataInit",
|
|
119
|
+
"EventDataNetwork",
|
|
120
|
+
"EventDataNetworkConfigured",
|
|
121
|
+
"EventDataShutdown",
|
|
122
|
+
"EventDataSizeCap",
|
|
123
|
+
"EventDataSpawn",
|
|
124
|
+
"EventDataStream",
|
|
125
|
+
"EventDataTruncated",
|
|
126
|
+
"EventResources",
|
|
127
|
+
"File",
|
|
128
|
+
"FileItem",
|
|
129
|
+
"FileResponse",
|
|
130
|
+
"FileSpec",
|
|
131
|
+
"FilesListResponse",
|
|
132
|
+
"ForbiddenError",
|
|
133
|
+
"GoneError",
|
|
134
|
+
"Image",
|
|
135
|
+
"ImageImportMetadata",
|
|
136
|
+
"ImageImportMetadataRegistry",
|
|
137
|
+
"ImageImportMetadataRegistryCredentials",
|
|
138
|
+
"ImageImportRegistry",
|
|
139
|
+
"ImageImportRegistryCredentials",
|
|
140
|
+
"ImageImportRequest",
|
|
141
|
+
"ImageListResponse",
|
|
142
|
+
"InstanceResourcesLimits",
|
|
143
|
+
"InstanceResult",
|
|
144
|
+
"InstanceResultResources",
|
|
145
|
+
"InstanceResultState",
|
|
146
|
+
"InstanceSpawnRequest",
|
|
147
|
+
"InstanceSpawnResponse",
|
|
148
|
+
"NetworkInterface",
|
|
149
|
+
"NotFoundError",
|
|
150
|
+
"OperationEvent",
|
|
151
|
+
"OperationEventType",
|
|
152
|
+
"OperationInstanceMetadata",
|
|
153
|
+
"OperationResponse",
|
|
154
|
+
"OperationResult",
|
|
155
|
+
"OperationStatus",
|
|
156
|
+
"OperationSummary",
|
|
157
|
+
"Profile",
|
|
158
|
+
"ProfileError",
|
|
159
|
+
"RequestSpec",
|
|
160
|
+
"ResponseData",
|
|
161
|
+
"RetryPolicy",
|
|
162
|
+
"SSEStreamError",
|
|
163
|
+
"ServerError",
|
|
164
|
+
"StreamRepr",
|
|
165
|
+
"TooEarlyError",
|
|
166
|
+
"UnauthorizedError",
|
|
167
|
+
"UnprocessableEntityError",
|
|
168
|
+
"WhoAmIResponse",
|
|
169
|
+
"decode_chunk",
|
|
170
|
+
"decode_stream",
|
|
171
|
+
"load_profiles",
|
|
172
|
+
"parse_datetime",
|
|
173
|
+
"resolve_profile",
|
|
174
|
+
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Contree API client backed by aiohttp."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import gzip
|
|
7
|
+
import ssl
|
|
8
|
+
from collections.abc import AsyncGenerator
|
|
9
|
+
from contextlib import suppress
|
|
10
|
+
|
|
11
|
+
import aiohttp
|
|
12
|
+
|
|
13
|
+
from . import base
|
|
14
|
+
from .runtime import (
|
|
15
|
+
CHUNK_SIZE,
|
|
16
|
+
RequestSpec,
|
|
17
|
+
ResponseData,
|
|
18
|
+
RetryPolicy,
|
|
19
|
+
error_for_response,
|
|
20
|
+
library_version,
|
|
21
|
+
)
|
|
22
|
+
from .spec_info import DEFAULT_BASE_URL
|
|
23
|
+
from .types import logger
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ContreeAsyncClient(base.ContreeAsyncClient):
|
|
27
|
+
"""Asynchronous Contree API client on top of `aiohttp.ClientSession`.
|
|
28
|
+
|
|
29
|
+
The session is created lazily on the first request so the client
|
|
30
|
+
may be constructed outside of a running event loop.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
log = logger.getChild("aiohttp")
|
|
34
|
+
UA_TRANSPORT_LIBRARY = library_version(aiohttp)
|
|
35
|
+
retryable_errors = (
|
|
36
|
+
aiohttp.ClientConnectionError,
|
|
37
|
+
aiohttp.ServerConnectionError,
|
|
38
|
+
)
|
|
39
|
+
nonretryable_errors = (TimeoutError, asyncio.TimeoutError)
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
token: str,
|
|
44
|
+
*,
|
|
45
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
46
|
+
project: str | None = None,
|
|
47
|
+
timeout: float | None = 300.0,
|
|
48
|
+
retry: RetryPolicy | None = None,
|
|
49
|
+
identity: str | None = None,
|
|
50
|
+
ssl_context: ssl.SSLContext | None = None,
|
|
51
|
+
# adapter-specific, prefixed by adapter name
|
|
52
|
+
aiohttp_session: aiohttp.ClientSession | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
super().__init__(
|
|
55
|
+
token,
|
|
56
|
+
base_url=base_url,
|
|
57
|
+
project=project,
|
|
58
|
+
timeout=timeout,
|
|
59
|
+
retry=retry,
|
|
60
|
+
identity=identity,
|
|
61
|
+
)
|
|
62
|
+
if aiohttp_session is not None and ssl_context is not None:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
"ssl_context cannot be combined with aiohttp_session;"
|
|
65
|
+
" configure TLS on the session connector itself"
|
|
66
|
+
)
|
|
67
|
+
self.__owns_session = aiohttp_session is None
|
|
68
|
+
self._session = aiohttp_session
|
|
69
|
+
self._ssl_context = ssl_context
|
|
70
|
+
|
|
71
|
+
def _get_session(self) -> aiohttp.ClientSession:
|
|
72
|
+
if self._session is None:
|
|
73
|
+
connector = (
|
|
74
|
+
aiohttp.TCPConnector(ssl=self._ssl_context)
|
|
75
|
+
if self._ssl_context is not None
|
|
76
|
+
else None
|
|
77
|
+
)
|
|
78
|
+
self._session = aiohttp.ClientSession(connector=connector)
|
|
79
|
+
return self._session
|
|
80
|
+
|
|
81
|
+
async def open(self) -> None:
|
|
82
|
+
"""Create the session eagerly (`async with client:` path)."""
|
|
83
|
+
self._get_session()
|
|
84
|
+
|
|
85
|
+
async def request(self, spec: RequestSpec) -> ResponseData:
|
|
86
|
+
url = self.build_url(spec)
|
|
87
|
+
async with self._get_session().request(
|
|
88
|
+
spec.method,
|
|
89
|
+
url,
|
|
90
|
+
data=spec.body,
|
|
91
|
+
headers=self.build_headers(spec),
|
|
92
|
+
allow_redirects=False,
|
|
93
|
+
timeout=aiohttp.ClientTimeout(total=self.timeout),
|
|
94
|
+
) as response:
|
|
95
|
+
body = await response.read()
|
|
96
|
+
return ResponseData(
|
|
97
|
+
status=response.status,
|
|
98
|
+
headers={k.lower(): v for k, v in response.headers.items()},
|
|
99
|
+
body=body,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
async def stream(
|
|
103
|
+
self,
|
|
104
|
+
spec: RequestSpec,
|
|
105
|
+
auto_decompress: bool = True,
|
|
106
|
+
) -> AsyncGenerator[bytes, None]:
|
|
107
|
+
url = self.build_url(spec)
|
|
108
|
+
async with self._get_session().request(
|
|
109
|
+
spec.method,
|
|
110
|
+
url,
|
|
111
|
+
data=spec.body,
|
|
112
|
+
headers=self.build_headers(spec),
|
|
113
|
+
allow_redirects=False,
|
|
114
|
+
auto_decompress=auto_decompress,
|
|
115
|
+
timeout=aiohttp.ClientTimeout(
|
|
116
|
+
total=None,
|
|
117
|
+
sock_connect=self.timeout,
|
|
118
|
+
# only SSE may idle (bounded by spec.read_timeout
|
|
119
|
+
# when a deadline is set); downloads must time out
|
|
120
|
+
sock_read=(
|
|
121
|
+
spec.read_timeout
|
|
122
|
+
if spec.accept == "text/event-stream"
|
|
123
|
+
else self.timeout
|
|
124
|
+
),
|
|
125
|
+
),
|
|
126
|
+
) as response:
|
|
127
|
+
self.log.debug("%s %s -> %d (stream)", spec.method, url, response.status)
|
|
128
|
+
if not 200 <= response.status < 300:
|
|
129
|
+
body = await response.read()
|
|
130
|
+
# auto_decompress=False applies to the payload only:
|
|
131
|
+
# the error body must still be decoded, or the parsed
|
|
132
|
+
# server message is lost
|
|
133
|
+
encoding = (response.headers.get("Content-Encoding") or "").lower()
|
|
134
|
+
if not auto_decompress and encoding == "gzip":
|
|
135
|
+
with suppress(gzip.BadGzipFile, OSError):
|
|
136
|
+
body = gzip.decompress(body)
|
|
137
|
+
raise error_for_response(
|
|
138
|
+
ResponseData(
|
|
139
|
+
status=response.status,
|
|
140
|
+
headers={k.lower(): v for k, v in response.headers.items()},
|
|
141
|
+
body=body,
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
async for chunk in response.content.iter_chunked(CHUNK_SIZE):
|
|
145
|
+
yield chunk
|
|
146
|
+
|
|
147
|
+
async def close(self) -> None:
|
|
148
|
+
if (
|
|
149
|
+
self.__owns_session
|
|
150
|
+
and self._session is not None
|
|
151
|
+
and not self._session.closed
|
|
152
|
+
):
|
|
153
|
+
await self._session.close()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Autodetected asynchronous client.
|
|
2
|
+
|
|
3
|
+
Tries the installed backends in priority order and exposes the first
|
|
4
|
+
importable one as ``ContreeAsyncClient``::
|
|
5
|
+
|
|
6
|
+
from contree_client.asyncio import ContreeAsyncClient
|
|
7
|
+
|
|
8
|
+
Raises an ImportError with an installation suggestion when neither
|
|
9
|
+
aiohttp nor httpx is available.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from importlib import import_module
|
|
15
|
+
|
|
16
|
+
from . import base
|
|
17
|
+
from .types import logger
|
|
18
|
+
|
|
19
|
+
log = logger.getChild("asyncio")
|
|
20
|
+
|
|
21
|
+
# Ordered by ecosystem popularity - PyPI download totals per ClickPy
|
|
22
|
+
# (https://clickpy.clickhouse.com): aiohttp ~10B, httpx ~7.6B.
|
|
23
|
+
BACKEND_PRIORITY = ("aiohttp", "httpx")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def detect_backend() -> tuple[str, type[base.ContreeAsyncClient]]:
|
|
27
|
+
for name in BACKEND_PRIORITY:
|
|
28
|
+
try:
|
|
29
|
+
module = import_module(f"contree_client.{name}")
|
|
30
|
+
except ModuleNotFoundError as exc:
|
|
31
|
+
# only the backend's own missing dependency (or a wholly
|
|
32
|
+
# absent backend module) means "not installed"; anything
|
|
33
|
+
# else is a real error to surface
|
|
34
|
+
if exc.name not in (name, f"contree_client.{name}"):
|
|
35
|
+
raise
|
|
36
|
+
continue
|
|
37
|
+
client_class: type[base.ContreeAsyncClient] = module.ContreeAsyncClient
|
|
38
|
+
log.debug("autodetected backend: %s", name)
|
|
39
|
+
return name, client_class
|
|
40
|
+
raise ImportError(
|
|
41
|
+
"no asynchronous contree-client backend could be imported;"
|
|
42
|
+
" install one of:\n"
|
|
43
|
+
" pip install contree-client[aiohttp]\n"
|
|
44
|
+
" pip install contree-client[httpx]"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
BACKEND, ContreeAsyncClient = detect_backend()
|