contree-sdk 0.0.1.dev11__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_sdk/__init__.py +5 -0
- contree_sdk/_internals/README.md +17 -0
- contree_sdk/_internals/__init__.py +0 -0
- contree_sdk/_internals/client/__init__.py +0 -0
- contree_sdk/_internals/client/client.py +14 -0
- contree_sdk/_internals/client/v1/__init__.py +0 -0
- contree_sdk/_internals/client/v1/common.py +15 -0
- contree_sdk/_internals/client/v1/files.py +34 -0
- contree_sdk/_internals/client/v1/images.py +52 -0
- contree_sdk/_internals/client/v1/inspect.py +46 -0
- contree_sdk/_internals/client/v1/instances.py +15 -0
- contree_sdk/_internals/client/v1/operations.py +24 -0
- contree_sdk/_internals/lib/__init__.py +0 -0
- contree_sdk/_internals/lib/api_decorator.py +67 -0
- contree_sdk/_internals/lib/client_base.py +74 -0
- contree_sdk/_internals/lib/helpers.py +18 -0
- contree_sdk/_internals/lib/mixins.py +48 -0
- contree_sdk/_internals/lib/types.py +138 -0
- contree_sdk/_internals/models/__init__.py +0 -0
- contree_sdk/_internals/models/file.py +20 -0
- contree_sdk/_internals/models/image.py +27 -0
- contree_sdk/_internals/models/image_import.py +24 -0
- contree_sdk/_internals/models/instance.py +90 -0
- contree_sdk/_internals/models/operation.py +23 -0
- contree_sdk/_internals/utils/__init__.py +0 -0
- contree_sdk/_internals/utils/config.py +33 -0
- contree_sdk/_internals/utils/exception.py +65 -0
- contree_sdk/_internals/utils/other.py +18 -0
- contree_sdk/_internals/utils/typing.py +16 -0
- contree_sdk/_internals/utils/wrapper.py +123 -0
- contree_sdk/config.py +53 -0
- contree_sdk/sdk/__init__.py +0 -0
- contree_sdk/sdk/client/__init__.py +5 -0
- contree_sdk/sdk/client/_async.py +17 -0
- contree_sdk/sdk/client/_base.py +159 -0
- contree_sdk/sdk/client/_sync.py +17 -0
- contree_sdk/sdk/exceptions/__init__.py +48 -0
- contree_sdk/sdk/exceptions/api.py +51 -0
- contree_sdk/sdk/exceptions/base.py +16 -0
- contree_sdk/sdk/exceptions/image.py +44 -0
- contree_sdk/sdk/exceptions/operation.py +35 -0
- contree_sdk/sdk/exceptions/other.py +8 -0
- contree_sdk/sdk/managers/__init__.py +0 -0
- contree_sdk/sdk/managers/_base.py +6 -0
- contree_sdk/sdk/managers/files/__init__.py +5 -0
- contree_sdk/sdk/managers/files/_async.py +5 -0
- contree_sdk/sdk/managers/files/_base.py +25 -0
- contree_sdk/sdk/managers/files/_sync.py +10 -0
- contree_sdk/sdk/managers/images/__init__.py +5 -0
- contree_sdk/sdk/managers/images/_async.py +30 -0
- contree_sdk/sdk/managers/images/_base.py +197 -0
- contree_sdk/sdk/managers/images/_sync.py +30 -0
- contree_sdk/sdk/objects/__init__.py +0 -0
- contree_sdk/sdk/objects/image/__init__.py +5 -0
- contree_sdk/sdk/objects/image/_async.py +8 -0
- contree_sdk/sdk/objects/image/_base.py +4 -0
- contree_sdk/sdk/objects/image/_sync.py +8 -0
- contree_sdk/sdk/objects/image_fs/__init__.py +10 -0
- contree_sdk/sdk/objects/image_fs/_async.py +21 -0
- contree_sdk/sdk/objects/image_fs/_base.py +39 -0
- contree_sdk/sdk/objects/image_fs/_sync.py +22 -0
- contree_sdk/sdk/objects/image_like/__init__.py +0 -0
- contree_sdk/sdk/objects/image_like/_async.py +48 -0
- contree_sdk/sdk/objects/image_like/_base.py +358 -0
- contree_sdk/sdk/objects/image_like/_sync.py +119 -0
- contree_sdk/sdk/objects/image_like/result.py +56 -0
- contree_sdk/sdk/objects/image_like/state.py +14 -0
- contree_sdk/sdk/objects/run.py +27 -0
- contree_sdk/sdk/objects/session/__init__.py +5 -0
- contree_sdk/sdk/objects/session/_async.py +5 -0
- contree_sdk/sdk/objects/session/_base.py +16 -0
- contree_sdk/sdk/objects/session/_sync.py +5 -0
- contree_sdk/sdk/objects/subprocess/__init__.py +6 -0
- contree_sdk/sdk/objects/subprocess/_async.py +24 -0
- contree_sdk/sdk/objects/subprocess/_base.py +73 -0
- contree_sdk/sdk/objects/subprocess/_sync.py +29 -0
- contree_sdk/shell/__init__.py +0 -0
- contree_sdk/shell/__main__.py +0 -0
- contree_sdk/utils/__init__.py +0 -0
- contree_sdk/utils/codecs.py +57 -0
- contree_sdk/utils/io_wrap.py +76 -0
- contree_sdk/utils/models/__init__.py +0 -0
- contree_sdk/utils/models/file.py +22 -0
- contree_sdk/utils/models/image.py +6 -0
- contree_sdk/utils/models/operation.py +12 -0
- contree_sdk/utils/models/stream.py +16 -0
- contree_sdk-0.0.1.dev11.dist-info/METADATA +565 -0
- contree_sdk-0.0.1.dev11.dist-info/RECORD +90 -0
- contree_sdk-0.0.1.dev11.dist-info/WHEEL +4 -0
- contree_sdk-0.0.1.dev11.dist-info/licenses/LICENSE +177 -0
contree_sdk/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# _internals
|
|
2
|
+
|
|
3
|
+
Low-level HTTP client layer and other utils. Not part of the public API.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
- `client/` - HTTP client classes and API endpoints
|
|
8
|
+
- `lib/` - `@apied` decorator, request/response handling, async/sync mixins
|
|
9
|
+
- `models/` - Response dataclasses for API responses
|
|
10
|
+
- `utils/` - AsyncWrapper for sync client support
|
|
11
|
+
|
|
12
|
+
## Does NOT belong here
|
|
13
|
+
|
|
14
|
+
- Config (endpoint URLs) → `contree_sdk/config.py`
|
|
15
|
+
- Public types → `sdk/` or `utils/`
|
|
16
|
+
|
|
17
|
+
Everything here is subject to change without notice.
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
|
|
3
|
+
from contree_sdk._internals.client.v1.common import V1Mixin
|
|
4
|
+
from contree_sdk._internals.lib.client_base import ClientBase
|
|
5
|
+
from contree_sdk._internals.lib.mixins import AsyncClientMixin, SyncClientMixin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ContreeClientBase(ClientBase, V1Mixin, ABC): ...
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ContreeClient(AsyncClientMixin, ContreeClientBase): ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ContreeSyncClient(SyncClientMixin, ContreeClientBase): ...
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from contree_sdk._internals.client.v1.files import FilesMixin
|
|
2
|
+
from contree_sdk._internals.client.v1.images import ImagesMixin
|
|
3
|
+
from contree_sdk._internals.client.v1.inspect import InspectMixin
|
|
4
|
+
from contree_sdk._internals.client.v1.instances import InstancesMixin
|
|
5
|
+
from contree_sdk._internals.client.v1.operations import OperationsMixin
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class V1Mixin(
|
|
9
|
+
ImagesMixin,
|
|
10
|
+
FilesMixin,
|
|
11
|
+
InstancesMixin,
|
|
12
|
+
OperationsMixin,
|
|
13
|
+
InspectMixin,
|
|
14
|
+
):
|
|
15
|
+
pass
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import Annotated, overload
|
|
2
|
+
|
|
3
|
+
from contree_sdk._internals.lib.api_decorator import get, head, post
|
|
4
|
+
from contree_sdk._internals.lib.mixins import AsyncClientMixin, SyncClientMixin
|
|
5
|
+
from contree_sdk._internals.lib.types import FileContent, OctetFile
|
|
6
|
+
from contree_sdk.utils.models.file import UploadedFile
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class FilesMixin:
|
|
10
|
+
@overload
|
|
11
|
+
async def get_file_by_sha256(self: AsyncClientMixin, sha256: str) -> UploadedFile: ...
|
|
12
|
+
@overload
|
|
13
|
+
def get_file_by_sha256(self: SyncClientMixin, sha256: str) -> UploadedFile: ...
|
|
14
|
+
|
|
15
|
+
@get("/v1/files", json=True)
|
|
16
|
+
def get_file_by_sha256(self, sha256: str) -> UploadedFile: ...
|
|
17
|
+
|
|
18
|
+
@overload
|
|
19
|
+
async def check_file_exists(self: AsyncClientMixin, uuid: str) -> bool: ...
|
|
20
|
+
@overload
|
|
21
|
+
def check_file_exists(self: SyncClientMixin, uuid: str) -> bool: ...
|
|
22
|
+
|
|
23
|
+
@head("/v1/files")
|
|
24
|
+
def check_file_exists(self, uuid: str) -> bool: ...
|
|
25
|
+
|
|
26
|
+
@overload
|
|
27
|
+
async def upload_file(self: AsyncClientMixin, data: Annotated[FileContent, OctetFile]) -> UploadedFile: ...
|
|
28
|
+
@overload
|
|
29
|
+
def upload_file(self: SyncClientMixin, data: Annotated[FileContent, OctetFile]) -> UploadedFile: ...
|
|
30
|
+
|
|
31
|
+
@post("/v1/files", json=True)
|
|
32
|
+
def upload_file(self, data: Annotated[FileContent, OctetFile]) -> UploadedFile: ...
|
|
33
|
+
|
|
34
|
+
# todo add error templates to response
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Annotated, Literal, overload
|
|
2
|
+
|
|
3
|
+
from contree_sdk._internals.lib.api_decorator import get, post
|
|
4
|
+
from contree_sdk._internals.lib.mixins import AsyncClientMixin, SyncClientMixin
|
|
5
|
+
from contree_sdk._internals.lib.types import Body
|
|
6
|
+
from contree_sdk._internals.models.image import ContreeImageModel
|
|
7
|
+
from contree_sdk._internals.models.image_import import ImageImportRequest
|
|
8
|
+
from contree_sdk.utils.models.image import ImageKind
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ImagesMixin:
|
|
12
|
+
@overload
|
|
13
|
+
async def get_images(
|
|
14
|
+
self: AsyncClientMixin,
|
|
15
|
+
kind: ImageKind | None = None,
|
|
16
|
+
limit: int | None = None,
|
|
17
|
+
offset: int | None = None,
|
|
18
|
+
tagged: Literal[0, 1] | None = None,
|
|
19
|
+
since: str | None = None,
|
|
20
|
+
until: str | None = None,
|
|
21
|
+
) -> list[ContreeImageModel]: ...
|
|
22
|
+
@overload
|
|
23
|
+
def get_images(
|
|
24
|
+
self: SyncClientMixin,
|
|
25
|
+
kind: ImageKind | None = None,
|
|
26
|
+
limit: int | None = None,
|
|
27
|
+
offset: int | None = None,
|
|
28
|
+
tagged: Literal[0, 1] | None = None,
|
|
29
|
+
since: str | None = None,
|
|
30
|
+
until: str | None = None,
|
|
31
|
+
) -> list[ContreeImageModel]: ...
|
|
32
|
+
|
|
33
|
+
@get("/v1/images", json=["images"])
|
|
34
|
+
def get_images(
|
|
35
|
+
self,
|
|
36
|
+
kind: ImageKind | None = None,
|
|
37
|
+
limit: int | None = None,
|
|
38
|
+
offset: int | None = None,
|
|
39
|
+
tagged: Literal[0, 1] | None = None,
|
|
40
|
+
since: str | None = None,
|
|
41
|
+
until: str | None = None,
|
|
42
|
+
) -> list[ContreeImageModel]: ...
|
|
43
|
+
|
|
44
|
+
@overload
|
|
45
|
+
async def start_import_image(self: AsyncClientMixin, request: Annotated[ImageImportRequest, Body]) -> str: ...
|
|
46
|
+
@overload
|
|
47
|
+
def start_import_image(self: SyncClientMixin, request: Annotated[ImageImportRequest, Body]) -> str: ...
|
|
48
|
+
@post("/v1/images/import", json=["uuid"])
|
|
49
|
+
def start_import_image(self, request: Annotated[ImageImportRequest, Body]) -> str: ...
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# todo later use like this MainV1Client(ImagesMixin['/images'], OtherMixin, ContreeClientBase) and have all in one
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from pathlib import PurePosixPath
|
|
2
|
+
from typing import overload
|
|
3
|
+
from uuid import UUID
|
|
4
|
+
|
|
5
|
+
from contree_sdk._internals.lib.api_decorator import get
|
|
6
|
+
from contree_sdk._internals.lib.mixins import AsyncClientMixin, SyncClientMixin
|
|
7
|
+
from contree_sdk._internals.models.file import FileItemModel
|
|
8
|
+
from contree_sdk._internals.models.image import ContreeImageModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InspectMixin:
|
|
12
|
+
@overload
|
|
13
|
+
async def list_image_files(
|
|
14
|
+
self: AsyncClientMixin, image_uuid: str, path: str | PurePosixPath
|
|
15
|
+
) -> list[FileItemModel]: ...
|
|
16
|
+
@overload
|
|
17
|
+
def list_image_files(self: SyncClientMixin, image_uuid: str, path: str | PurePosixPath) -> list[FileItemModel]: ...
|
|
18
|
+
|
|
19
|
+
@get("/v1/inspect/{image_uuid}/list", json=["files"])
|
|
20
|
+
def list_image_files(self, image_uuid: str, path: str | PurePosixPath) -> list[FileItemModel]: ...
|
|
21
|
+
|
|
22
|
+
@overload
|
|
23
|
+
async def download_image_file(
|
|
24
|
+
self: AsyncClientMixin, image_uuid: str | UUID, path: str | PurePosixPath
|
|
25
|
+
) -> bytes: ...
|
|
26
|
+
@overload
|
|
27
|
+
def download_image_file(self: SyncClientMixin, image_uuid: str | UUID, path: str | PurePosixPath) -> bytes: ...
|
|
28
|
+
|
|
29
|
+
@get("/v1/inspect/{image_uuid}/download", json=False)
|
|
30
|
+
def download_image_file(self, image_uuid: str | UUID, path: str | PurePosixPath) -> bytes: ...
|
|
31
|
+
|
|
32
|
+
@overload
|
|
33
|
+
async def get_image_by_uuid(self: AsyncClientMixin, image_uuid: str) -> ContreeImageModel: ...
|
|
34
|
+
@overload
|
|
35
|
+
def get_image_by_uuid(self: SyncClientMixin, image_uuid: str) -> ContreeImageModel: ...
|
|
36
|
+
|
|
37
|
+
@get("/v1/inspect/{image_uuid}", json=True)
|
|
38
|
+
def get_image_by_uuid(self, image_uuid: str) -> ContreeImageModel: ...
|
|
39
|
+
|
|
40
|
+
@overload
|
|
41
|
+
async def get_image_by_tag(self: AsyncClientMixin, tag: str) -> ContreeImageModel: ...
|
|
42
|
+
@overload
|
|
43
|
+
def get_image_by_tag(self: SyncClientMixin, tag: str) -> ContreeImageModel: ...
|
|
44
|
+
|
|
45
|
+
@get("/v1/inspect", json=True)
|
|
46
|
+
def get_image_by_tag(self, tag: str) -> ContreeImageModel: ...
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from typing import Annotated, overload
|
|
2
|
+
|
|
3
|
+
from contree_sdk._internals.lib.api_decorator import post
|
|
4
|
+
from contree_sdk._internals.lib.mixins import AsyncClientMixin, SyncClientMixin
|
|
5
|
+
from contree_sdk._internals.lib.types import Body
|
|
6
|
+
from contree_sdk._internals.models.instance import InstanceSpawnRequest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InstancesMixin:
|
|
10
|
+
@overload
|
|
11
|
+
async def spawn_instance(self: AsyncClientMixin, request: Annotated[InstanceSpawnRequest, Body]) -> str: ...
|
|
12
|
+
@overload
|
|
13
|
+
def spawn_instance(self: SyncClientMixin, request: Annotated[InstanceSpawnRequest, Body]) -> str: ...
|
|
14
|
+
@post("/v1/instances", json=["uuid"])
|
|
15
|
+
def spawn_instance(self, request: Annotated[InstanceSpawnRequest, Body]) -> str: ...
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import overload
|
|
2
|
+
from uuid import UUID
|
|
3
|
+
|
|
4
|
+
from contree_sdk._internals.lib.api_decorator import delete, get
|
|
5
|
+
from contree_sdk._internals.lib.mixins import AsyncClientMixin, SyncClientMixin
|
|
6
|
+
from contree_sdk._internals.models.operation import OperationModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OperationsMixin:
|
|
10
|
+
@overload
|
|
11
|
+
async def get_operation_status(self: AsyncClientMixin, operation_id: str | UUID) -> OperationModel: ...
|
|
12
|
+
@overload
|
|
13
|
+
def get_operation_status(self: SyncClientMixin, operation_id: str | UUID) -> OperationModel: ...
|
|
14
|
+
|
|
15
|
+
@get("/v1/operations/{operation_id}", json=True)
|
|
16
|
+
def get_operation_status(self, operation_id: str | UUID) -> OperationModel: ...
|
|
17
|
+
|
|
18
|
+
@overload
|
|
19
|
+
async def cancel_operation(self: AsyncClientMixin, operation_id: str | UUID) -> None: ...
|
|
20
|
+
@overload
|
|
21
|
+
def cancel_operation(self: SyncClientMixin, operation_id: str | UUID) -> None: ...
|
|
22
|
+
|
|
23
|
+
@delete("/v1/operations/{operation_id}")
|
|
24
|
+
def cancel_operation(self, operation_id: str | UUID) -> None: ...
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Iterable
|
|
4
|
+
from functools import partial, wraps
|
|
5
|
+
from string import Formatter
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from contree_sdk._internals.lib.helpers import args_kwargs_to_kwargs
|
|
9
|
+
from contree_sdk._internals.lib.types import ApiEndpointInfo, ReturnType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from contree_sdk._internals.client.client import ContreeClientBase
|
|
14
|
+
|
|
15
|
+
_formatter = Formatter()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def apied(method: str, path: str, *, json: bool | Iterable[str] = False):
|
|
19
|
+
"""Define an API endpoint on a client method.
|
|
20
|
+
|
|
21
|
+
Builds ApiEndpointInfo from the function signature and annotations, then
|
|
22
|
+
replaces the method with a wrapper calling `_handle_api_call`.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
method: HTTP method name.
|
|
26
|
+
path: Endpoint path with `{param}` placeholders.
|
|
27
|
+
json:
|
|
28
|
+
False — no JSON handling,
|
|
29
|
+
True — whole response as JSON,
|
|
30
|
+
iterable — JSON path to extract.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A decorator that wraps a client method and routes the call through
|
|
34
|
+
`ContreeClientBase._handle_api_call`.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
match json:
|
|
38
|
+
case False:
|
|
39
|
+
json_path = None
|
|
40
|
+
case True:
|
|
41
|
+
json_path = []
|
|
42
|
+
case _:
|
|
43
|
+
json_path = list(json)
|
|
44
|
+
|
|
45
|
+
def decorator(func: Callable[..., ReturnType]):
|
|
46
|
+
endpoint_info = ApiEndpointInfo.from_func(
|
|
47
|
+
method=method,
|
|
48
|
+
path=path,
|
|
49
|
+
json_path=json_path,
|
|
50
|
+
func=func,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
@wraps(func)
|
|
54
|
+
def wrapper(self: ContreeClientBase, *args, **kwargs):
|
|
55
|
+
data = args_kwargs_to_kwargs(endpoint_info.all_params, args, kwargs)
|
|
56
|
+
return self._handle_api_call(endpoint_info=endpoint_info, data=data)
|
|
57
|
+
|
|
58
|
+
return wrapper
|
|
59
|
+
|
|
60
|
+
return decorator
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
get = partial(apied, "get")
|
|
64
|
+
post = partial(apied, "post")
|
|
65
|
+
put = partial(apied, "put")
|
|
66
|
+
head = partial(apied, "head")
|
|
67
|
+
delete = partial(apied, "delete")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import overload
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
from httpx import Request, Response
|
|
6
|
+
from httpx._config import DEFAULT_TIMEOUT_CONFIG, Timeout
|
|
7
|
+
|
|
8
|
+
from contree_sdk._internals.lib.helpers import convert_data_to_type
|
|
9
|
+
from contree_sdk._internals.lib.mixins import AsyncClientMixin, SyncClientMixin
|
|
10
|
+
from contree_sdk._internals.lib.types import EMPTY, ApiEndpointInfo, ReturnType
|
|
11
|
+
from contree_sdk._internals.utils.config import build_user_agent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ClientBase(ABC):
|
|
15
|
+
_client_class: type[httpx._client.BaseClient]
|
|
16
|
+
|
|
17
|
+
def __init__(self, token: str, base_url: str, transport_timeout: float = DEFAULT_TIMEOUT_CONFIG.connect) -> None:
|
|
18
|
+
headers = {
|
|
19
|
+
"Authorization": f"Bearer {token}",
|
|
20
|
+
"User-Agent": build_user_agent(),
|
|
21
|
+
}
|
|
22
|
+
self._client = self._client_class(
|
|
23
|
+
headers=headers, base_url=base_url, timeout=Timeout(timeout=transport_timeout)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def _build_request(self, endpoint_info: ApiEndpointInfo, data: dict) -> Request:
|
|
27
|
+
kwargs = endpoint_info.get_file_upload_kwargs(data)
|
|
28
|
+
return self._client.build_request(
|
|
29
|
+
method=endpoint_info.method.upper(),
|
|
30
|
+
url=endpoint_info.get_path_by_data(data),
|
|
31
|
+
params=endpoint_info.get_query_data_by_data(data),
|
|
32
|
+
json=endpoint_info.get_body_data_by_data(data),
|
|
33
|
+
**kwargs,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@overload
|
|
37
|
+
async def _send_request(self: AsyncClientMixin, request: Request) -> Response: ...
|
|
38
|
+
@overload
|
|
39
|
+
def _send_request(self: SyncClientMixin, request: Request) -> Response: ...
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def _send_request(self, request: Request) -> Response:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def _parse_response(
|
|
46
|
+
*,
|
|
47
|
+
response: Response,
|
|
48
|
+
endpoint_info: ApiEndpointInfo,
|
|
49
|
+
) -> ReturnType | dict | Response | str | bytes:
|
|
50
|
+
response.raise_for_status()
|
|
51
|
+
if endpoint_info.json_path is None:
|
|
52
|
+
if endpoint_info.return_type is str:
|
|
53
|
+
return response.text
|
|
54
|
+
if endpoint_info.return_type is bytes:
|
|
55
|
+
return response.content
|
|
56
|
+
return response
|
|
57
|
+
|
|
58
|
+
data = response.json()
|
|
59
|
+
for key in endpoint_info.json_path:
|
|
60
|
+
data = data[key]
|
|
61
|
+
if endpoint_info.return_type is not EMPTY:
|
|
62
|
+
return convert_data_to_type(data, endpoint_info.return_type)
|
|
63
|
+
return data
|
|
64
|
+
|
|
65
|
+
@overload
|
|
66
|
+
async def _handle_api_call(
|
|
67
|
+
self: AsyncClientMixin, endpoint_info: ApiEndpointInfo, data: dict
|
|
68
|
+
) -> ReturnType | dict | Response: ...
|
|
69
|
+
@overload
|
|
70
|
+
def _handle_api_call(
|
|
71
|
+
self: SyncClientMixin, endpoint_info: ApiEndpointInfo, data: dict
|
|
72
|
+
) -> ReturnType | dict | Response: ...
|
|
73
|
+
@abstractmethod
|
|
74
|
+
def _handle_api_call(self, endpoint_info: ApiEndpointInfo, data: dict) -> ReturnType | dict | Response: ...
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from cattrs.preconf.json import make_converter
|
|
2
|
+
|
|
3
|
+
from contree_sdk._internals.lib.types import ReturnType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
_converter = make_converter()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def convert_data_to_type(data: dict | int | str | list, return_type: type[ReturnType]) -> ReturnType:
|
|
10
|
+
return _converter.structure(data, return_type)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def args_kwargs_to_kwargs(all_params: list[str], args: tuple, kwargs: dict) -> dict:
|
|
14
|
+
kwargs = kwargs.copy()
|
|
15
|
+
|
|
16
|
+
for param, arg in zip(all_params, args, strict=False):
|
|
17
|
+
kwargs[param] = arg
|
|
18
|
+
return kwargs
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from httpx import AsyncClient, Client, Request, Response
|
|
6
|
+
|
|
7
|
+
from contree_sdk._internals.lib.types import ApiEndpointInfo, ReturnType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from contree_sdk._internals.lib.client_base import ClientBase
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SyncClientMixin:
|
|
15
|
+
_client_class: type[Client] = Client
|
|
16
|
+
_client: Client
|
|
17
|
+
|
|
18
|
+
def _send_request(self, request: Request) -> Response:
|
|
19
|
+
return self._client.send(request, follow_redirects=True)
|
|
20
|
+
|
|
21
|
+
def _handle_api_call(self: ClientBase, endpoint_info: ApiEndpointInfo, data: dict) -> ReturnType | dict | Response:
|
|
22
|
+
request = self._build_request(endpoint_info=endpoint_info, data=data)
|
|
23
|
+
resp = self._send_request(request)
|
|
24
|
+
return self._parse_response(response=resp, endpoint_info=endpoint_info)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AsyncClientMixin:
|
|
28
|
+
_client_class: type[AsyncClient] = AsyncClient
|
|
29
|
+
_client: AsyncClient
|
|
30
|
+
|
|
31
|
+
async def _send_request(self, request: Request) -> Response:
|
|
32
|
+
resp = await self._client.send(request, follow_redirects=True, stream=True)
|
|
33
|
+
try:
|
|
34
|
+
await resp.aread()
|
|
35
|
+
except BaseException as e:
|
|
36
|
+
e.response = resp
|
|
37
|
+
await resp.aclose()
|
|
38
|
+
raise
|
|
39
|
+
else:
|
|
40
|
+
return resp
|
|
41
|
+
|
|
42
|
+
async def _handle_api_call(
|
|
43
|
+
self: ClientBase, endpoint_info: ApiEndpointInfo, data: dict
|
|
44
|
+
) -> ReturnType | dict | Response:
|
|
45
|
+
request = self._build_request(endpoint_info=endpoint_info, data=data)
|
|
46
|
+
|
|
47
|
+
resp = await self._send_request(request)
|
|
48
|
+
return self._parse_response(response=resp, endpoint_info=endpoint_info)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from string import Formatter
|
|
7
|
+
from typing import IO, Annotated, Any, TypeVar, get_args, get_origin
|
|
8
|
+
|
|
9
|
+
import cattrs
|
|
10
|
+
from aiofiles.threadpool.binary import AsyncBufferedReader
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class _Empty:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
EMPTY = _Empty()
|
|
18
|
+
ReturnType = TypeVar("ReturnType")
|
|
19
|
+
_formatter = Formatter()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Body:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OctetFile:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(kw_only=True, frozen=True)
|
|
31
|
+
class ApiEndpointInfo:
|
|
32
|
+
# api info
|
|
33
|
+
method: str
|
|
34
|
+
path: str
|
|
35
|
+
|
|
36
|
+
# params info
|
|
37
|
+
all_params: list[str]
|
|
38
|
+
path_params: list[str]
|
|
39
|
+
query_params: list[str]
|
|
40
|
+
body_params: list[str]
|
|
41
|
+
file_params: list[str]
|
|
42
|
+
|
|
43
|
+
# how to parse
|
|
44
|
+
json_path: list | None
|
|
45
|
+
func: Callable[..., Any]
|
|
46
|
+
return_type: ReturnType | _Empty
|
|
47
|
+
|
|
48
|
+
def get_path_by_data(self, data: dict):
|
|
49
|
+
return self.path.format(**data)
|
|
50
|
+
|
|
51
|
+
def get_query_data_by_data(self, data: dict):
|
|
52
|
+
return {name: str(to_dict(data[name])) for name in self.query_params if name in data}
|
|
53
|
+
|
|
54
|
+
def get_body_data_by_data(self, data: dict):
|
|
55
|
+
if not self.body_params:
|
|
56
|
+
return None
|
|
57
|
+
res = {name: to_dict(data[name]) for name in self.body_params if name in data}
|
|
58
|
+
if len(self.body_params) == 1:
|
|
59
|
+
return res[self.body_params[0]]
|
|
60
|
+
return res
|
|
61
|
+
|
|
62
|
+
def get_file_upload_kwargs(self, data: dict) -> dict:
|
|
63
|
+
if not self.file_params:
|
|
64
|
+
return {}
|
|
65
|
+
if len(self.file_params) == 1:
|
|
66
|
+
return {
|
|
67
|
+
"headers": {
|
|
68
|
+
"Content-Type": "application/octet-stream",
|
|
69
|
+
},
|
|
70
|
+
"content": data[self.file_params[0]],
|
|
71
|
+
}
|
|
72
|
+
return {"files": {name: (None, data[name], "application/octet-stream") for name in self.file_params}}
|
|
73
|
+
|
|
74
|
+
@classmethod
|
|
75
|
+
def from_func(
|
|
76
|
+
cls,
|
|
77
|
+
*,
|
|
78
|
+
method: str,
|
|
79
|
+
path: str,
|
|
80
|
+
json_path: list[str] | None,
|
|
81
|
+
func: Callable[..., Any],
|
|
82
|
+
) -> ApiEndpointInfo:
|
|
83
|
+
parsed_path_params = {name for _, name, *_ in _formatter.parse(path) if name is not None}
|
|
84
|
+
|
|
85
|
+
all_params: list[str] = []
|
|
86
|
+
path_params: list[str] = []
|
|
87
|
+
query_params: list[str] = []
|
|
88
|
+
body_params: list[str] = []
|
|
89
|
+
file_params: list[str] = []
|
|
90
|
+
|
|
91
|
+
sig = inspect.signature(func)
|
|
92
|
+
for name, param in sig.parameters.items():
|
|
93
|
+
if name == "self":
|
|
94
|
+
continue
|
|
95
|
+
all_params.append(name)
|
|
96
|
+
|
|
97
|
+
is_body, is_file = cls._extract_flags(param)
|
|
98
|
+
|
|
99
|
+
if is_body:
|
|
100
|
+
body_params.append(name)
|
|
101
|
+
elif is_file:
|
|
102
|
+
file_params.append(name)
|
|
103
|
+
elif name in parsed_path_params:
|
|
104
|
+
path_params.append(name)
|
|
105
|
+
else:
|
|
106
|
+
query_params.append(name)
|
|
107
|
+
|
|
108
|
+
return cls(
|
|
109
|
+
method=method,
|
|
110
|
+
path=path,
|
|
111
|
+
all_params=all_params,
|
|
112
|
+
path_params=path_params,
|
|
113
|
+
query_params=query_params,
|
|
114
|
+
body_params=body_params,
|
|
115
|
+
file_params=file_params,
|
|
116
|
+
json_path=json_path,
|
|
117
|
+
func=func,
|
|
118
|
+
return_type=func.__annotations__.get("return", EMPTY),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _extract_flags(param: inspect.Parameter) -> tuple[bool, bool]:
|
|
123
|
+
ann = param.annotation
|
|
124
|
+
if get_origin(ann) is not Annotated:
|
|
125
|
+
return False, False
|
|
126
|
+
|
|
127
|
+
_, *meta = get_args(ann)
|
|
128
|
+
|
|
129
|
+
is_body = any(isinstance(m, type) and issubclass(m, Body) for m in meta)
|
|
130
|
+
is_file = any(isinstance(m, type) and issubclass(m, OctetFile) for m in meta)
|
|
131
|
+
return is_body, is_file
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def to_dict(data: Any) -> Any:
|
|
135
|
+
return cattrs.unstructure(data)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
FileContent = IO[bytes] | bytes | str | AsyncBufferedReader
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class FileItemModel:
|
|
6
|
+
size: int
|
|
7
|
+
path: str
|
|
8
|
+
uid: int
|
|
9
|
+
gid: int
|
|
10
|
+
mode: int
|
|
11
|
+
mtime: int
|
|
12
|
+
nlink: int
|
|
13
|
+
symlink_to: str
|
|
14
|
+
is_dir: bool
|
|
15
|
+
is_regular: bool
|
|
16
|
+
is_socket: bool
|
|
17
|
+
is_fifo: bool
|
|
18
|
+
is_symlink: bool
|
|
19
|
+
owner: str
|
|
20
|
+
group: str
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
ImageTag = str
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ImageSize:
|
|
10
|
+
logical: int
|
|
11
|
+
physical: int
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(kw_only=True)
|
|
15
|
+
class ContreeImageModel:
|
|
16
|
+
uuid: str
|
|
17
|
+
tag: str | None = None
|
|
18
|
+
created_at: datetime | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(kw_only=True)
|
|
22
|
+
class InspectImageResponse:
|
|
23
|
+
uuid: str
|
|
24
|
+
source: str | None = None
|
|
25
|
+
tag: str | None = None
|
|
26
|
+
created_at: int
|
|
27
|
+
size: ImageSize
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@dataclass
|
|
5
|
+
class RegistryCredentials:
|
|
6
|
+
username: str
|
|
7
|
+
password: str
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class PublicRegistryInfo:
|
|
12
|
+
url: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class PrivateRegistryInfo(PublicRegistryInfo):
|
|
17
|
+
credentials: RegistryCredentials
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ImageImportRequest:
|
|
22
|
+
registry: PublicRegistryInfo | PrivateRegistryInfo
|
|
23
|
+
tag: str | None = None
|
|
24
|
+
timeout: int = 300
|