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.
@@ -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()