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.
Files changed (90) hide show
  1. contree_sdk/__init__.py +5 -0
  2. contree_sdk/_internals/README.md +17 -0
  3. contree_sdk/_internals/__init__.py +0 -0
  4. contree_sdk/_internals/client/__init__.py +0 -0
  5. contree_sdk/_internals/client/client.py +14 -0
  6. contree_sdk/_internals/client/v1/__init__.py +0 -0
  7. contree_sdk/_internals/client/v1/common.py +15 -0
  8. contree_sdk/_internals/client/v1/files.py +34 -0
  9. contree_sdk/_internals/client/v1/images.py +52 -0
  10. contree_sdk/_internals/client/v1/inspect.py +46 -0
  11. contree_sdk/_internals/client/v1/instances.py +15 -0
  12. contree_sdk/_internals/client/v1/operations.py +24 -0
  13. contree_sdk/_internals/lib/__init__.py +0 -0
  14. contree_sdk/_internals/lib/api_decorator.py +67 -0
  15. contree_sdk/_internals/lib/client_base.py +74 -0
  16. contree_sdk/_internals/lib/helpers.py +18 -0
  17. contree_sdk/_internals/lib/mixins.py +48 -0
  18. contree_sdk/_internals/lib/types.py +138 -0
  19. contree_sdk/_internals/models/__init__.py +0 -0
  20. contree_sdk/_internals/models/file.py +20 -0
  21. contree_sdk/_internals/models/image.py +27 -0
  22. contree_sdk/_internals/models/image_import.py +24 -0
  23. contree_sdk/_internals/models/instance.py +90 -0
  24. contree_sdk/_internals/models/operation.py +23 -0
  25. contree_sdk/_internals/utils/__init__.py +0 -0
  26. contree_sdk/_internals/utils/config.py +33 -0
  27. contree_sdk/_internals/utils/exception.py +65 -0
  28. contree_sdk/_internals/utils/other.py +18 -0
  29. contree_sdk/_internals/utils/typing.py +16 -0
  30. contree_sdk/_internals/utils/wrapper.py +123 -0
  31. contree_sdk/config.py +53 -0
  32. contree_sdk/sdk/__init__.py +0 -0
  33. contree_sdk/sdk/client/__init__.py +5 -0
  34. contree_sdk/sdk/client/_async.py +17 -0
  35. contree_sdk/sdk/client/_base.py +159 -0
  36. contree_sdk/sdk/client/_sync.py +17 -0
  37. contree_sdk/sdk/exceptions/__init__.py +48 -0
  38. contree_sdk/sdk/exceptions/api.py +51 -0
  39. contree_sdk/sdk/exceptions/base.py +16 -0
  40. contree_sdk/sdk/exceptions/image.py +44 -0
  41. contree_sdk/sdk/exceptions/operation.py +35 -0
  42. contree_sdk/sdk/exceptions/other.py +8 -0
  43. contree_sdk/sdk/managers/__init__.py +0 -0
  44. contree_sdk/sdk/managers/_base.py +6 -0
  45. contree_sdk/sdk/managers/files/__init__.py +5 -0
  46. contree_sdk/sdk/managers/files/_async.py +5 -0
  47. contree_sdk/sdk/managers/files/_base.py +25 -0
  48. contree_sdk/sdk/managers/files/_sync.py +10 -0
  49. contree_sdk/sdk/managers/images/__init__.py +5 -0
  50. contree_sdk/sdk/managers/images/_async.py +30 -0
  51. contree_sdk/sdk/managers/images/_base.py +197 -0
  52. contree_sdk/sdk/managers/images/_sync.py +30 -0
  53. contree_sdk/sdk/objects/__init__.py +0 -0
  54. contree_sdk/sdk/objects/image/__init__.py +5 -0
  55. contree_sdk/sdk/objects/image/_async.py +8 -0
  56. contree_sdk/sdk/objects/image/_base.py +4 -0
  57. contree_sdk/sdk/objects/image/_sync.py +8 -0
  58. contree_sdk/sdk/objects/image_fs/__init__.py +10 -0
  59. contree_sdk/sdk/objects/image_fs/_async.py +21 -0
  60. contree_sdk/sdk/objects/image_fs/_base.py +39 -0
  61. contree_sdk/sdk/objects/image_fs/_sync.py +22 -0
  62. contree_sdk/sdk/objects/image_like/__init__.py +0 -0
  63. contree_sdk/sdk/objects/image_like/_async.py +48 -0
  64. contree_sdk/sdk/objects/image_like/_base.py +358 -0
  65. contree_sdk/sdk/objects/image_like/_sync.py +119 -0
  66. contree_sdk/sdk/objects/image_like/result.py +56 -0
  67. contree_sdk/sdk/objects/image_like/state.py +14 -0
  68. contree_sdk/sdk/objects/run.py +27 -0
  69. contree_sdk/sdk/objects/session/__init__.py +5 -0
  70. contree_sdk/sdk/objects/session/_async.py +5 -0
  71. contree_sdk/sdk/objects/session/_base.py +16 -0
  72. contree_sdk/sdk/objects/session/_sync.py +5 -0
  73. contree_sdk/sdk/objects/subprocess/__init__.py +6 -0
  74. contree_sdk/sdk/objects/subprocess/_async.py +24 -0
  75. contree_sdk/sdk/objects/subprocess/_base.py +73 -0
  76. contree_sdk/sdk/objects/subprocess/_sync.py +29 -0
  77. contree_sdk/shell/__init__.py +0 -0
  78. contree_sdk/shell/__main__.py +0 -0
  79. contree_sdk/utils/__init__.py +0 -0
  80. contree_sdk/utils/codecs.py +57 -0
  81. contree_sdk/utils/io_wrap.py +76 -0
  82. contree_sdk/utils/models/__init__.py +0 -0
  83. contree_sdk/utils/models/file.py +22 -0
  84. contree_sdk/utils/models/image.py +6 -0
  85. contree_sdk/utils/models/operation.py +12 -0
  86. contree_sdk/utils/models/stream.py +16 -0
  87. contree_sdk-0.0.1.dev11.dist-info/METADATA +565 -0
  88. contree_sdk-0.0.1.dev11.dist-info/RECORD +90 -0
  89. contree_sdk-0.0.1.dev11.dist-info/WHEEL +4 -0
  90. contree_sdk-0.0.1.dev11.dist-info/licenses/LICENSE +177 -0
@@ -0,0 +1,5 @@
1
+ from contree_sdk.sdk.client._async import Contree
2
+ from contree_sdk.sdk.client._sync import ContreeSync
3
+
4
+
5
+ __all__ = ["Contree", "ContreeSync"]
@@ -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