From 14a2004550caa92d307cf4ec19449ea6c3062222 Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Tue, 19 Nov 2024 16:57:25 +0300 Subject: [PATCH 01/20] ADCM-6115: Implement Service object --- adcm_aio_client/core/objects/_base.py | 4 ++- adcm_aio_client/core/objects/_common.py | 7 +++++ adcm_aio_client/core/objects/cm.py | 40 ++++++++++++++++++------- adcm_aio_client/core/types.py | 6 ++++ 4 files changed, 45 insertions(+), 12 deletions(-) diff --git a/adcm_aio_client/core/objects/_base.py b/adcm_aio_client/core/objects/_base.py index 2806672..f255871 100644 --- a/adcm_aio_client/core/objects/_base.py +++ b/adcm_aio_client/core/objects/_base.py @@ -1,4 +1,5 @@ from collections import deque +from contextlib import suppress from functools import cached_property from typing import Any, Self @@ -48,7 +49,8 @@ def _construct_child[Child: "InteractiveChildObject"]( def _clear_cache(self: Self) -> None: for name in self._delete_on_refresh: # works for cached_property - delattr(self, name) + with suppress(AttributeError): + delattr(self, name) class RootInteractiveObject(InteractiveObject): diff --git a/adcm_aio_client/core/objects/_common.py b/adcm_aio_client/core/objects/_common.py index 32b839b..e1fcfa3 100644 --- a/adcm_aio_client/core/objects/_common.py +++ b/adcm_aio_client/core/objects/_common.py @@ -2,6 +2,7 @@ from typing import Self from adcm_aio_client.core.objects._base import AwareOfOwnPath, WithRequester +from adcm_aio_client.core.types import ADCMEntityStatus class Deletable(WithRequester, AwareOfOwnPath): @@ -9,6 +10,12 @@ async def delete(self: Self) -> None: await self._requester.delete(*self.get_own_path()) +class HasStatus(WithRequester, AwareOfOwnPath): + async def get_status(self: Self) -> ADCMEntityStatus: + response = await self._requester.get(*self.get_own_path()) + return ADCMEntityStatus(response.as_dict()["status"]) + + # todo whole section lacking implementation (and maybe code move is required) class WithConfig(WithRequester, AwareOfOwnPath): @cached_property diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 215d581..9657a13 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -1,14 +1,14 @@ from functools import cached_property -from typing import Literal, Self +from typing import Self from adcm_aio_client.core.objects._accessors import ( - NonPaginatedChildAccessor, PaginatedAccessor, PaginatedChildAccessor, ) from adcm_aio_client.core.objects._base import InteractiveChildObject, InteractiveObject from adcm_aio_client.core.objects._common import ( Deletable, + HasStatus, WithActionHostGroups, WithActions, WithConfig, @@ -27,7 +27,14 @@ class Host(Deletable, InteractiveObject): ... class Cluster( - Deletable, WithActions, WithUpgrades, WithConfig, WithActionHostGroups, WithConfigGroups, InteractiveObject + HasStatus, + Deletable, + WithActions, + WithUpgrades, + WithConfig, + WithActionHostGroups, + WithConfigGroups, + InteractiveObject, ): # data-based properties @@ -55,11 +62,6 @@ async def bundle(self: Self) -> Bundle: return self._construct(what=Bundle, from_data=response.as_dict()) # object-specific methods - - async def get_status(self: Self) -> Literal["up", "down"]: - response = await self._requester.get(*self.get_own_path()) - return response.as_dict()["status"] - async def set_ansible_forks(self: Self, value: int) -> Self: await self._requester.post( *self.get_own_path(), "ansible-config", data={"config": {"defaults": {"forks": value}}, "adcmMeta": {}} @@ -99,13 +101,29 @@ class HostsInClusterNode(PaginatedAccessor[Host, None]): class_type = Host -class Service(InteractiveChildObject[Cluster]): +class Service( + HasStatus, + Deletable, + WithActions, + WithConfig, + WithActionHostGroups, + WithConfigGroups, + InteractiveChildObject[Cluster], +): @property def id(self: Self) -> int: return int(self._data["id"]) + @property + def name(self: Self) -> str: + return self._data["name"] + + @property + def display_name(self: Self) -> str: + return self._data["displayName"] + def get_own_path(self: Self) -> Endpoint: - return (*self._parent.get_own_path(), "services", self.id) + return *self._parent.get_own_path(), "services", self.id @cached_property def components(self: Self) -> "ComponentsNode": @@ -125,5 +143,5 @@ def get_own_path(self: Self) -> Endpoint: return (*self._parent.get_own_path(), "components", self.id) -class ComponentsNode(NonPaginatedChildAccessor[Service, Component, None]): +class ComponentsNode(PaginatedChildAccessor[Service, Component, None]): class_type = Component diff --git a/adcm_aio_client/core/types.py b/adcm_aio_client/core/types.py index ee14b74..315aa6d 100644 --- a/adcm_aio_client/core/types.py +++ b/adcm_aio_client/core/types.py @@ -11,6 +11,7 @@ # limitations under the License. from dataclasses import asdict, dataclass +from enum import Enum from typing import Optional, Protocol, Self # Init / Authorization @@ -67,3 +68,8 @@ class WithRequester(Protocol): class AwareOfOwnPath(Protocol): def get_own_path(self: Self) -> Endpoint: ... + + +class ADCMEntityStatus(str, Enum): # TODO: stolen from ADCM-6118. unify after merge + UP = "up" + DOWN = "down" From 358a9acc5f61a4e6617a51fb6935ffd347512e8c Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Tue, 19 Nov 2024 17:13:50 +0300 Subject: [PATCH 02/20] ADCM-6115: add dummy `cluster` cached property --- adcm_aio_client/core/objects/cm.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 9657a13..8e296cb 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -122,6 +122,10 @@ def name(self: Self) -> str: def display_name(self: Self) -> str: return self._data["displayName"] + @cached_property + def cluster(self: Self) -> Cluster: + return self._parent # TODO: must be refreshable via `self.refresh()` + def get_own_path(self: Self) -> Endpoint: return *self._parent.get_own_path(), "services", self.id From fce03165c2790d5cf9025663f50523ab55432b1d Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Tue, 19 Nov 2024 17:52:10 +0300 Subject: [PATCH 03/20] ADCM-6115: review fixes --- adcm_aio_client/core/objects/_base.py | 2 +- adcm_aio_client/core/objects/_common.py | 2 +- adcm_aio_client/core/objects/cm.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/adcm_aio_client/core/objects/_base.py b/adcm_aio_client/core/objects/_base.py index f255871..f1ea070 100644 --- a/adcm_aio_client/core/objects/_base.py +++ b/adcm_aio_client/core/objects/_base.py @@ -48,7 +48,7 @@ def _construct_child[Child: "InteractiveChildObject"]( def _clear_cache(self: Self) -> None: for name in self._delete_on_refresh: - # works for cached_property + # Works for cached_property. Suppresses errors on deleting values not yet cached (absent in self.__dict__) with suppress(AttributeError): delattr(self, name) diff --git a/adcm_aio_client/core/objects/_common.py b/adcm_aio_client/core/objects/_common.py index e1fcfa3..f9a9ba3 100644 --- a/adcm_aio_client/core/objects/_common.py +++ b/adcm_aio_client/core/objects/_common.py @@ -10,7 +10,7 @@ async def delete(self: Self) -> None: await self._requester.delete(*self.get_own_path()) -class HasStatus(WithRequester, AwareOfOwnPath): +class WithStatus(WithRequester, AwareOfOwnPath): async def get_status(self: Self) -> ADCMEntityStatus: response = await self._requester.get(*self.get_own_path()) return ADCMEntityStatus(response.as_dict()["status"]) diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 8e296cb..f98044e 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -8,11 +8,11 @@ from adcm_aio_client.core.objects._base import InteractiveChildObject, InteractiveObject from adcm_aio_client.core.objects._common import ( Deletable, - HasStatus, WithActionHostGroups, WithActions, WithConfig, WithConfigGroups, + WithStatus, WithUpgrades, ) from adcm_aio_client.core.objects._imports import ClusterImports @@ -27,7 +27,7 @@ class Host(Deletable, InteractiveObject): ... class Cluster( - HasStatus, + WithStatus, Deletable, WithActions, WithUpgrades, @@ -102,7 +102,7 @@ class HostsInClusterNode(PaginatedAccessor[Host, None]): class Service( - HasStatus, + WithStatus, Deletable, WithActions, WithConfig, From 79dc14f847ec268ba596a2fa34ebdf8c0fac35f1 Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Wed, 20 Nov 2024 10:40:28 +0300 Subject: [PATCH 04/20] ADCM-6115: remove todo --- adcm_aio_client/core/objects/cm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index f98044e..680466d 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -124,7 +124,7 @@ def display_name(self: Self) -> str: @cached_property def cluster(self: Self) -> Cluster: - return self._parent # TODO: must be refreshable via `self.refresh()` + return self._parent def get_own_path(self: Self) -> Endpoint: return *self._parent.get_own_path(), "services", self.id From 20a71c05200f2ea6f4faa6afd3de2d3fc9c7ea8d Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Wed, 20 Nov 2024 14:08:12 +0300 Subject: [PATCH 05/20] ADCM-6117: Implement Component object --- adcm_aio_client/core/objects/_base.py | 4 ++- adcm_aio_client/core/objects/cm.py | 42 +++++++++++++++++++++++++-- poetry.lock | 18 +++++++++++- pyproject.toml | 1 + 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/adcm_aio_client/core/objects/_base.py b/adcm_aio_client/core/objects/_base.py index f1ea070..ac70984 100644 --- a/adcm_aio_client/core/objects/_base.py +++ b/adcm_aio_client/core/objects/_base.py @@ -3,6 +3,8 @@ from functools import cached_property from typing import Any, Self +from asyncstdlib.functools import CachedProperty + from adcm_aio_client.core.requesters import Requester from adcm_aio_client.core.types import AwareOfOwnPath, Endpoint, WithRequester @@ -18,7 +20,7 @@ def __init_subclass__(cls: type[Self]) -> None: for name in dir(cls): # None is for declared, but unset values attr = getattr(cls, name, None) - if isinstance(attr, cached_property): + if isinstance(attr, (cached_property, CachedProperty)): cls._delete_on_refresh.append(name) def __init__(self: Self, requester: Requester, data: dict[str, Any]) -> None: diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 680466d..e621412 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -1,6 +1,9 @@ from functools import cached_property from typing import Self +from asyncstdlib.functools import cached_property as async_cached_property + +from adcm_aio_client.core.errors import NotFoundError from adcm_aio_client.core.objects._accessors import ( PaginatedAccessor, PaginatedChildAccessor, @@ -138,13 +141,48 @@ class ServicesNode(PaginatedChildAccessor[Cluster, Service, None]): class_type = Service -class Component(InteractiveChildObject[Service]): +class Component( + WithStatus, WithActions, WithConfig, WithActionHostGroups, WithConfigGroups, InteractiveChildObject[Service] +): @property def id(self: Self) -> int: return int(self._data["id"]) + @property + def name(self: Self) -> int: + return int(self._data["name"]) + + @property + def display_name(self: Self) -> int: + return int(self._data["displayName"]) + + @async_cached_property + async def constraint(self: Self) -> list[int | str]: + response = (await self._requester.get(*self.cluster.get_own_path(), "mapping", "components")).as_list() + for component in response: + if component["id"] == self.id: + return component["constraints"] + + raise NotFoundError + + @cached_property + def service(self: Self) -> Service: + return self._parent + + @cached_property + def cluster(self: Self) -> Cluster: + return self._parent._parent + + @cached_property + def hosts(self: Self) -> HostsInClusterNode: + return HostsInClusterNode( + path=(*self.cluster.get_own_path(), "hosts"), + requester=self._requester, + # filter=Filter({"componentId": self.id}), # TODO: implement + ) + def get_own_path(self: Self) -> Endpoint: - return (*self._parent.get_own_path(), "components", self.id) + return *self._parent.get_own_path(), "components", self.id class ComponentsNode(PaginatedChildAccessor[Service, Component, None]): diff --git a/poetry.lock b/poetry.lock index 48ef0aa..a4a080f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -20,6 +20,22 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] trio = ["trio (>=0.26.1)"] +[[package]] +name = "asyncstdlib" +version = "3.13.0" +description = "The missing async toolbox" +optional = false +python-versions = "~=3.8" +files = [ + {file = "asyncstdlib-3.13.0-py3-none-any.whl", hash = "sha256:60e097c19e815f3c419a77426cf6c3653aebcb766544d631d5ce6128d0851ae8"}, + {file = "asyncstdlib-3.13.0.tar.gz", hash = "sha256:f2a6ffb44f118233bb99bef50861d6f64c432decbdcc4c2cb93b3fff40d1b533"}, +] + +[package.extras] +doc = ["sphinx", "sphinxcontrib-trio"] +test = ["black", "coverage", "flake8", "flake8-2020", "flake8-bugbear", "mypy", "pytest", "pytest-cov"] +typetest = ["mypy", "pyright", "typing-extensions"] + [[package]] name = "certifi" version = "2024.8.30" @@ -271,4 +287,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "20136fad059dd6f087334eea57612f368edbe485fcd2874666338000b4859d1b" +content-hash = "c14d12ccbcd8910aed151decce58a67d6f4798d9a9c6b7c086be12ca2c935587" diff --git a/pyproject.toml b/pyproject.toml index ce8b2c4..6cfadd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.12" httpx = "^0.27.2" +asyncstdlib = "^3.13.0" [tool.poetry.group.dev] optional = true From 3437117eec716fdb1b8fbf716f216ac16686bd1f Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Wed, 20 Nov 2024 14:19:47 +0300 Subject: [PATCH 06/20] ADCM-6115: after merge fixes --- adcm_aio_client/core/objects/cm.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 83c31c9..cc11ea9 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -5,7 +5,6 @@ PaginatedAccessor, PaginatedChildAccessor, ) -from adcm_aio_client.core.types import ADCMEntityStatus from adcm_aio_client.core.objects._base import InteractiveChildObject, InteractiveObject, RootInteractiveObject from adcm_aio_client.core.objects._common import ( Deletable, @@ -18,7 +17,7 @@ ) from adcm_aio_client.core.objects._imports import ClusterImports from adcm_aio_client.core.objects._mapping import ClusterMapping -from adcm_aio_client.core.types import Endpoint +from adcm_aio_client.core.types import ADCMEntityStatus, Endpoint class Bundle(Deletable, InteractiveObject): ... @@ -32,7 +31,7 @@ class Cluster( WithConfig, WithActionHostGroups, WithConfigGroups, - InteractiveObject, + RootInteractiveObject, ): PATH_PREFIX = "clusters" # data-based properties @@ -96,10 +95,6 @@ def get_own_path(self: Self) -> Endpoint: return ("clusters",) -class HostsInClusterNode(PaginatedAccessor[Host, None]): - class_type = Host - - class Service( WithStatus, Deletable, @@ -109,10 +104,6 @@ class Service( WithConfigGroups, InteractiveChildObject[Cluster], ): - @property - def id(self: Self) -> int: - return int(self._data["id"]) - @property def name(self: Self) -> str: return self._data["name"] From e5fd846ee3d75cde7d97a97eff946c044cf0199a Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Wed, 20 Nov 2024 14:22:01 +0300 Subject: [PATCH 07/20] ADCM-6115: after merge fixes --- adcm_aio_client/core/objects/cm.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index cc11ea9..9a8323f 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -104,6 +104,8 @@ class Service( WithConfigGroups, InteractiveChildObject[Cluster], ): + PATH_PREFIX = "services" + @property def name(self: Self) -> str: return self._data["name"] From 262be352b553037a186e0fb6ab933bfe41b82275 Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Wed, 20 Nov 2024 14:22:17 +0300 Subject: [PATCH 08/20] ADCM-6115: after merge fixes --- adcm_aio_client/core/objects/cm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 9a8323f..b0c4e25 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -104,7 +104,7 @@ class Service( WithConfigGroups, InteractiveChildObject[Cluster], ): - PATH_PREFIX = "services" + # PATH_PREFIX = "services" @property def name(self: Self) -> str: From 423adcc68bd1b57b734d588da9059172c32d32e2 Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Wed, 20 Nov 2024 14:24:58 +0300 Subject: [PATCH 09/20] ADCM-6115: after merge fixes --- adcm_aio_client/core/objects/cm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index b0c4e25..9a8323f 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -104,7 +104,7 @@ class Service( WithConfigGroups, InteractiveChildObject[Cluster], ): - # PATH_PREFIX = "services" + PATH_PREFIX = "services" @property def name(self: Self) -> str: From a9fc7bfe7978f8bd9a93cd31b1c46e6f1cc00333 Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Wed, 20 Nov 2024 14:26:12 +0300 Subject: [PATCH 10/20] ADCM-6115: remove todo --- adcm_aio_client/core/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adcm_aio_client/core/types.py b/adcm_aio_client/core/types.py index 315aa6d..487b0c5 100644 --- a/adcm_aio_client/core/types.py +++ b/adcm_aio_client/core/types.py @@ -70,6 +70,6 @@ class AwareOfOwnPath(Protocol): def get_own_path(self: Self) -> Endpoint: ... -class ADCMEntityStatus(str, Enum): # TODO: stolen from ADCM-6118. unify after merge +class ADCMEntityStatus(str, Enum): UP = "up" DOWN = "down" From 5c455fd59bbfa35aeff77e1589afa736a439e739 Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Wed, 20 Nov 2024 18:13:00 +0300 Subject: [PATCH 11/20] ADCM-6117: fix name, display_name of Component --- adcm_aio_client/core/objects/cm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 57847a1..32bc9c3 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -139,12 +139,12 @@ class Component( PATH_PREFIX = "components" @property - def name(self: Self) -> int: - return int(self._data["name"]) + def name(self: Self) -> str: + return self._data["name"] @property - def display_name(self: Self) -> int: - return int(self._data["displayName"]) + def display_name(self: Self) -> str: + return self._data["displayName"] @async_cached_property async def constraint(self: Self) -> list[int | str]: From e66f9c75ecfeff837703737179afb40a221bcb81 Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Thu, 21 Nov 2024 10:53:18 +0300 Subject: [PATCH 12/20] ADCM-6117: review fixes --- adcm_aio_client/core/objects/cm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 32bc9c3..0191a0f 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -122,7 +122,7 @@ def cluster(self: Self) -> Cluster: return self._parent def get_own_path(self: Self) -> Endpoint: - return *self._parent.get_own_path(), "services", self.id + return *self._parent.get_own_path(), self.PATH_PREFIX, self.id @cached_property def components(self: Self) -> "ComponentsNode": @@ -161,14 +161,14 @@ def service(self: Self) -> Service: @cached_property def cluster(self: Self) -> Cluster: - return self._parent._parent + return self.service.cluster @cached_property def hosts(self: Self) -> "HostsInClusterNode": - return HostsInClusterNode( + return HostsInClusterNode( # TODO: new ComponentHostsNode path=(*self.cluster.get_own_path(), "hosts"), requester=self._requester, - # filter=Filter({"componentId": self.id}), # TODO: implement + # filter=Filter({"componentId": self.id}), ) def get_own_path(self: Self) -> Endpoint: From 510048fc2f41fb6183089a07360cd9efcbc80cbb Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Thu, 21 Nov 2024 19:56:37 +0300 Subject: [PATCH 13/20] ADCM-6133: Implement node for getting hosts --- adcm_aio_client/core/client.py | 6 ++-- adcm_aio_client/core/objects/_accessors.py | 23 +++++++------ adcm_aio_client/core/objects/cm.py | 40 +++++++++++++++++----- adcm_aio_client/core/requesters.py | 8 ++--- adcm_aio_client/core/types.py | 4 +-- tests/unit/mocks/requesters.py | 4 +-- 6 files changed, 55 insertions(+), 30 deletions(-) diff --git a/adcm_aio_client/core/client.py b/adcm_aio_client/core/client.py index 832f951..3c11a26 100644 --- a/adcm_aio_client/core/client.py +++ b/adcm_aio_client/core/client.py @@ -24,15 +24,15 @@ def __init__(self: Self, requester: Requester) -> None: @cached_property def clusters(self: Self) -> ClustersNode: - return ClustersNode(path=(), requester=self._requester) + return ClustersNode(path=("clusters",), requester=self._requester) @cached_property def hosts(self: Self) -> HostsNode: - return HostsNode(path=(), requester=self._requester) + return HostsNode(path=("hosts",), requester=self._requester) @cached_property def hostproviders(self: Self) -> HostProvidersNode: - return HostProvidersNode(path=(), requester=self._requester) + return HostProvidersNode(path=("hostproviders",), requester=self._requester) async def build_client( diff --git a/adcm_aio_client/core/objects/_accessors.py b/adcm_aio_client/core/objects/_accessors.py index f9a0264..00f9e35 100644 --- a/adcm_aio_client/core/objects/_accessors.py +++ b/adcm_aio_client/core/objects/_accessors.py @@ -19,12 +19,13 @@ from adcm_aio_client.core.types import Endpoint, QueryParameters, Requester, RequesterResponse -class Accessor[ReturnObject: InteractiveObject, Filter](ABC): +class Accessor[ReturnObject: InteractiveObject, Filter: dict | None](ABC): class_type: type[ReturnObject] - def __init__(self: Self, path: Endpoint, requester: Requester) -> None: + def __init__(self: Self, path: Endpoint, requester: Requester, filters: Filter = None) -> None: self._path = path self._requester = requester + self._filters = filters or {} @abstractmethod async def iter(self: Self) -> AsyncGenerator[ReturnObject, None]: ... @@ -62,13 +63,13 @@ async def list(self: Self) -> list[ReturnObject]: return [self._create_object(obj) for obj in results] async def _request_endpoint(self: Self, query: QueryParameters) -> RequesterResponse: - return await self._requester.get(*self._path, query=query) + return await self._requester.get(*self._path, query={**query, **self._filters}) def _create_object(self: Self, data: dict[str, Any]) -> ReturnObject: return self.class_type(requester=self._requester, data=data) -class PaginatedAccessor[ReturnObject: InteractiveObject, Filter](Accessor[ReturnObject, Filter]): +class PaginatedAccessor[ReturnObject: InteractiveObject, Filter: dict | None](Accessor[ReturnObject, Filter]): async def iter(self: Self) -> AsyncGenerator[ReturnObject, None]: start, step = 0, 10 while True: @@ -87,18 +88,20 @@ def _extract_results_from_response(self: Self, response: RequesterResponse) -> l return response.as_dict()["results"] -class PaginatedChildAccessor[Parent, Child: InteractiveChildObject, Filter](PaginatedAccessor[Child, Filter]): - def __init__(self: Self, parent: Parent, path: Endpoint, requester: Requester) -> None: - super().__init__(path, requester) +class PaginatedChildAccessor[Parent, Child: InteractiveChildObject, Filter: dict | None]( + PaginatedAccessor[Child, Filter] +): + def __init__(self: Self, parent: Parent, path: Endpoint, requester: Requester, filters: Filter = None) -> None: + super().__init__(path, requester, filters) self._parent = parent def _create_object(self: Self, data: dict[str, Any]) -> Child: return self.class_type(parent=self._parent, requester=self._requester, data=data) -class NonPaginatedChildAccessor[Parent, Child: InteractiveChildObject, Filter](Accessor[Child, Filter]): - def __init__(self: Self, parent: Parent, path: Endpoint, requester: Requester) -> None: - super().__init__(path, requester) +class NonPaginatedChildAccessor[Parent, Child: InteractiveChildObject, Filter: dict | None](Accessor[Child, Filter]): + def __init__(self: Self, parent: Parent, path: Endpoint, requester: Requester, filters: Filter = None) -> None: + super().__init__(path, requester, filters) self._parent = parent async def iter(self: Self) -> AsyncGenerator[Child, None]: diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 0191a0f..71c50dd 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -1,5 +1,5 @@ from functools import cached_property -from typing import Self +from typing import Iterable, Self from asyncstdlib.functools import cached_property as async_cached_property @@ -164,11 +164,9 @@ def cluster(self: Self) -> Cluster: return self.service.cluster @cached_property - def hosts(self: Self) -> "HostsInClusterNode": - return HostsInClusterNode( # TODO: new ComponentHostsNode - path=(*self.cluster.get_own_path(), "hosts"), - requester=self._requester, - # filter=Filter({"componentId": self.id}), + def hosts(self: Self) -> "HostsNode": + return HostsNode( + path=(*self.cluster.get_own_path(), "hosts"), requester=self._requester, filters={"componentId": self.id} ) def get_own_path(self: Self) -> Endpoint: @@ -195,6 +193,10 @@ def description(self: Self) -> str: def display_name(self: Self) -> str: return str(self._data["prototype"]["displayName"]) + @cached_property + def hosts(self: Self) -> "HostsNode": + return HostsNode(path=("hosts",), requester=self._requester, filters={"hostproviderName": self.name}) + def get_own_path(self: Self) -> Endpoint: return self.PATH_PREFIX, self.id @@ -232,9 +234,29 @@ def get_own_path(self: Self) -> Endpoint: return self.PATH_PREFIX, self.id -class HostsNode(PaginatedAccessor[Host, None]): +class HostsNode(PaginatedAccessor[Host, dict | None]): class_type = Host -class HostsInClusterNode(PaginatedAccessor[Host, None]): - class_type = Host +class HostsInClusterNode(HostsNode): + async def add(self: Self, host: Host | Iterable[Host] | None = None, **filters: dict) -> None: + hosts = await self._get_hosts_from_arg_or_filter(host=host, **filters) + + await self._requester.post(*self._path, data=[{"hostId": host.id} for host in hosts]) + + async def remove(self: Self, host: Host | Iterable[Host] | None = None, **filters: dict) -> None: + for host_ in await self._get_hosts_from_arg_or_filter(host=host, **filters): + await self._requester.delete(*self._path, host_.id) + + async def _get_hosts_from_arg_or_filter( + self: Self, host: Host | Iterable[Host] | None = None, **filters: dict + ) -> Iterable[Host]: + if all((host, filters)): + raise ValueError("`host` and `filters` arguments are mutually exclusive.") + + if host: + hosts = [host] if isinstance(host, Host) else host + else: + hosts = await self.filter(**filters) + + return hosts diff --git a/adcm_aio_client/core/requesters.py b/adcm_aio_client/core/requesters.py index 98a68c9..2aec83f 100644 --- a/adcm_aio_client/core/requesters.py +++ b/adcm_aio_client/core/requesters.py @@ -154,11 +154,11 @@ async def login(self: Self, credentials: Credentials) -> Self: async def get(self: Self, *path: PathPart, query: QueryParameters | None = None) -> HTTPXRequesterResponse: return await self.request(*path, method=self.client.get, params=query or {}) - async def post(self: Self, *path: PathPart, data: dict) -> HTTPXRequesterResponse: - return await self.request(*path, method=self.client.post, data=data) + async def post(self: Self, *path: PathPart, data: Json) -> HTTPXRequesterResponse: + return await self.request(*path, method=self.client.post, json=data) - async def patch(self: Self, *path: PathPart, data: dict) -> HTTPXRequesterResponse: - return await self.request(*path, method=self.client.patch, data=data) + async def patch(self: Self, *path: PathPart, data: Json) -> HTTPXRequesterResponse: + return await self.request(*path, method=self.client.patch, json=data) async def delete(self: Self, *path: PathPart) -> HTTPXRequesterResponse: return await self.request(*path, method=self.client.delete) diff --git a/adcm_aio_client/core/types.py b/adcm_aio_client/core/types.py index 487b0c5..0762c9b 100644 --- a/adcm_aio_client/core/types.py +++ b/adcm_aio_client/core/types.py @@ -52,9 +52,9 @@ async def login(self: Self, credentials: Credentials) -> Self: ... async def get(self: Self, *path: PathPart, query: QueryParameters | None = None) -> RequesterResponse: ... - async def post(self: Self, *path: PathPart, data: dict) -> RequesterResponse: ... + async def post(self: Self, *path: PathPart, data: dict | list) -> RequesterResponse: ... - async def patch(self: Self, *path: PathPart, data: dict) -> RequesterResponse: ... + async def patch(self: Self, *path: PathPart, data: dict | list) -> RequesterResponse: ... async def delete(self: Self, *path: PathPart) -> RequesterResponse: ... diff --git a/tests/unit/mocks/requesters.py b/tests/unit/mocks/requesters.py index b00895d..5adadf6 100644 --- a/tests/unit/mocks/requesters.py +++ b/tests/unit/mocks/requesters.py @@ -39,11 +39,11 @@ async def get(self: Self, *path: PathPart, query: QueryParameters | None = None) _ = path, query return self._return_next_response() - async def post(self: Self, *path: PathPart, data: dict) -> RequesterResponse: + async def post(self: Self, *path: PathPart, data: dict | list) -> RequesterResponse: _ = path, data return self._return_next_response() - async def patch(self: Self, *path: PathPart, data: dict) -> RequesterResponse: + async def patch(self: Self, *path: PathPart, data: dict | list) -> RequesterResponse: _ = path, data return self._return_next_response() From 81c6d58a3dbef55ddd2d0cc26b7694e800a3cbfa Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Fri, 22 Nov 2024 10:40:52 +0300 Subject: [PATCH 14/20] ADCM-6133: fix some cached_properties --- adcm_aio_client/core/objects/cm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 71c50dd..bd1d1b6 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -52,7 +52,7 @@ def description(self: Self) -> str: # todo think how such properties will be invalidated when data is updated # during `refresh()` / `reread()` calls. # See cache invalidation or alternatives in documentation for `cached_property` - @cached_property + @async_cached_property async def bundle(self: Self) -> Bundle: prototype_id = self._data["prototype"]["id"] response = await self._requester.get("prototypes", prototype_id) @@ -220,13 +220,13 @@ async def get_status(self: Self) -> ADCMEntityStatus: response = await self._requester.get(*self.get_own_path()) return ADCMEntityStatus(response.as_dict()["status"]) - @cached_property + @async_cached_property async def cluster(self: Self) -> Cluster | None: if not self._data["cluster"]: return None return await Cluster.with_id(requester=self._requester, object_id=self._data["cluster"]["id"]) - @cached_property + @async_cached_property async def hostprovider(self: Self) -> HostProvider: return await HostProvider.with_id(requester=self._requester, object_id=self._data["hostprovider"]["id"]) From 56ba4a71a47f030a1566d3d3580986af8fd91025 Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Fri, 22 Nov 2024 14:59:49 +0300 Subject: [PATCH 15/20] Revert "ADCM-6133: fix some cached_properties" This reverts commit 81c6d58a3dbef55ddd2d0cc26b7694e800a3cbfa. --- adcm_aio_client/core/objects/cm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index bd1d1b6..71c50dd 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -52,7 +52,7 @@ def description(self: Self) -> str: # todo think how such properties will be invalidated when data is updated # during `refresh()` / `reread()` calls. # See cache invalidation or alternatives in documentation for `cached_property` - @async_cached_property + @cached_property async def bundle(self: Self) -> Bundle: prototype_id = self._data["prototype"]["id"] response = await self._requester.get("prototypes", prototype_id) @@ -220,13 +220,13 @@ async def get_status(self: Self) -> ADCMEntityStatus: response = await self._requester.get(*self.get_own_path()) return ADCMEntityStatus(response.as_dict()["status"]) - @async_cached_property + @cached_property async def cluster(self: Self) -> Cluster | None: if not self._data["cluster"]: return None return await Cluster.with_id(requester=self._requester, object_id=self._data["cluster"]["id"]) - @async_cached_property + @cached_property async def hostprovider(self: Self) -> HostProvider: return await HostProvider.with_id(requester=self._requester, object_id=self._data["hostprovider"]["id"]) From 67796a3e8ec4691ba6f0419b65bdd19e4af580c0 Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Fri, 22 Nov 2024 15:05:17 +0300 Subject: [PATCH 16/20] ADCM-6133: restore poetry.lock and pyproject.toml --- poetry.lock | 18 +----------------- pyproject.toml | 1 - 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/poetry.lock b/poetry.lock index a4a080f..48ef0aa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -20,22 +20,6 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] trio = ["trio (>=0.26.1)"] -[[package]] -name = "asyncstdlib" -version = "3.13.0" -description = "The missing async toolbox" -optional = false -python-versions = "~=3.8" -files = [ - {file = "asyncstdlib-3.13.0-py3-none-any.whl", hash = "sha256:60e097c19e815f3c419a77426cf6c3653aebcb766544d631d5ce6128d0851ae8"}, - {file = "asyncstdlib-3.13.0.tar.gz", hash = "sha256:f2a6ffb44f118233bb99bef50861d6f64c432decbdcc4c2cb93b3fff40d1b533"}, -] - -[package.extras] -doc = ["sphinx", "sphinxcontrib-trio"] -test = ["black", "coverage", "flake8", "flake8-2020", "flake8-bugbear", "mypy", "pytest", "pytest-cov"] -typetest = ["mypy", "pyright", "typing-extensions"] - [[package]] name = "certifi" version = "2024.8.30" @@ -287,4 +271,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "c14d12ccbcd8910aed151decce58a67d6f4798d9a9c6b7c086be12ca2c935587" +content-hash = "20136fad059dd6f087334eea57612f368edbe485fcd2874666338000b4859d1b" diff --git a/pyproject.toml b/pyproject.toml index 6cfadd6..ce8b2c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,6 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.12" httpx = "^0.27.2" -asyncstdlib = "^3.13.0" [tool.poetry.group.dev] optional = true From 6b03292f97755edf361b3e809e8bd700ef79b487 Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Fri, 22 Nov 2024 17:17:19 +0300 Subject: [PATCH 17/20] ADCM-6133: review fixes --- adcm_aio_client/core/errors.py | 4 ++++ adcm_aio_client/core/objects/cm.py | 22 +++++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/adcm_aio_client/core/errors.py b/adcm_aio_client/core/errors.py index 29c3691..775e6fb 100644 --- a/adcm_aio_client/core/errors.py +++ b/adcm_aio_client/core/errors.py @@ -83,3 +83,7 @@ class MultipleObjectsReturnedError(AccessorError): class ObjectDoesNotExistError(AccessorError): pass + + +class OperationError(AccessorError): + pass diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index a72a635..5df156d 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -1,7 +1,8 @@ from functools import cached_property from typing import Iterable, Self +import asyncio -from adcm_aio_client.core.errors import NotFoundError +from adcm_aio_client.core.errors import NotFoundError, OperationError, ResponseError from adcm_aio_client.core.objects._accessors import ( PaginatedAccessor, PaginatedChildAccessor, @@ -246,6 +247,9 @@ async def hostprovider(self: Self) -> HostProvider: def get_own_path(self: Self) -> Endpoint: return self.PATH_PREFIX, self.id + def __str__(self: Self) -> str: + return f"<{self.__class__.__name__} #{self.id} {self.name}>" + class HostsNode(PaginatedAccessor[Host, dict | None]): class_type = Host @@ -258,8 +262,20 @@ async def add(self: Self, host: Host | Iterable[Host] | None = None, **filters: await self._requester.post(*self._path, data=[{"hostId": host.id} for host in hosts]) async def remove(self: Self, host: Host | Iterable[Host] | None = None, **filters: dict) -> None: - for host_ in await self._get_hosts_from_arg_or_filter(host=host, **filters): - await self._requester.delete(*self._path, host_.id) + hosts = await self._get_hosts_from_arg_or_filter(host=host, **filters) + + results = await asyncio.gather( + *(self._requester.delete(*self._path, host_.id) for host_ in hosts), return_exceptions=True + ) + + errors = set() + for host_, result in zip(hosts, results): + if isinstance(result, ResponseError): + errors.add(str(host_)) + + if errors: + errors = ", ".join(errors) + raise OperationError(f"Some hosts can't be deleted from cluster: {errors}") async def _get_hosts_from_arg_or_filter( self: Self, host: Host | Iterable[Host] | None = None, **filters: dict From e6cf4b7b9130a7df38b9d33ddf54327ee057b5a6 Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Fri, 22 Nov 2024 17:54:07 +0300 Subject: [PATCH 18/20] ADCM-6133: review fixes --- adcm_aio_client/core/client.py | 6 ++-- adcm_aio_client/core/objects/_accessors.py | 31 ++++++++++++--------- adcm_aio_client/core/objects/cm.py | 32 +++++++++++++--------- 3 files changed, 40 insertions(+), 29 deletions(-) diff --git a/adcm_aio_client/core/client.py b/adcm_aio_client/core/client.py index b817d5d..3606e5a 100644 --- a/adcm_aio_client/core/client.py +++ b/adcm_aio_client/core/client.py @@ -13,7 +13,7 @@ from functools import cached_property from typing import Self -from adcm_aio_client.core.objects.cm import ADCM, ClustersNode, HostProvidersNode, HostsNode +from adcm_aio_client.core.objects.cm import ADCM, ClustersNode, HostProvidersNode, HostsAccessor from adcm_aio_client.core.requesters import Requester from adcm_aio_client.core.types import AuthToken, Cert, Credentials, Verify @@ -27,8 +27,8 @@ def clusters(self: Self) -> ClustersNode: return ClustersNode(path=("clusters",), requester=self._requester) @cached_property - def hosts(self: Self) -> HostsNode: - return HostsNode(path=("hosts",), requester=self._requester) + def hosts(self: Self) -> HostsAccessor: + return HostsAccessor(path=("hosts",), requester=self._requester) @cached_property def hostproviders(self: Self) -> HostProvidersNode: diff --git a/adcm_aio_client/core/objects/_accessors.py b/adcm_aio_client/core/objects/_accessors.py index 00f9e35..3f93cda 100644 --- a/adcm_aio_client/core/objects/_accessors.py +++ b/adcm_aio_client/core/objects/_accessors.py @@ -18,14 +18,17 @@ from adcm_aio_client.core.objects._base import InteractiveChildObject, InteractiveObject from adcm_aio_client.core.types import Endpoint, QueryParameters, Requester, RequesterResponse +# filter for narrowing response objects +type AccessorFilter = QueryParameters | None -class Accessor[ReturnObject: InteractiveObject, Filter: dict | None](ABC): + +class Accessor[ReturnObject: InteractiveObject, Filter](ABC): class_type: type[ReturnObject] - def __init__(self: Self, path: Endpoint, requester: Requester, filters: Filter = None) -> None: + def __init__(self: Self, path: Endpoint, requester: Requester, accessor_filter: AccessorFilter = None) -> None: self._path = path self._requester = requester - self._filters = filters or {} + self._accessor_filter = accessor_filter or {} @abstractmethod async def iter(self: Self) -> AsyncGenerator[ReturnObject, None]: ... @@ -63,13 +66,13 @@ async def list(self: Self) -> list[ReturnObject]: return [self._create_object(obj) for obj in results] async def _request_endpoint(self: Self, query: QueryParameters) -> RequesterResponse: - return await self._requester.get(*self._path, query={**query, **self._filters}) + return await self._requester.get(*self._path, query={**query, **self._accessor_filter}) def _create_object(self: Self, data: dict[str, Any]) -> ReturnObject: return self.class_type(requester=self._requester, data=data) -class PaginatedAccessor[ReturnObject: InteractiveObject, Filter: dict | None](Accessor[ReturnObject, Filter]): +class PaginatedAccessor[ReturnObject: InteractiveObject, Filter](Accessor[ReturnObject, Filter]): async def iter(self: Self) -> AsyncGenerator[ReturnObject, None]: start, step = 0, 10 while True: @@ -88,20 +91,22 @@ def _extract_results_from_response(self: Self, response: RequesterResponse) -> l return response.as_dict()["results"] -class PaginatedChildAccessor[Parent, Child: InteractiveChildObject, Filter: dict | None]( - PaginatedAccessor[Child, Filter] -): - def __init__(self: Self, parent: Parent, path: Endpoint, requester: Requester, filters: Filter = None) -> None: - super().__init__(path, requester, filters) +class PaginatedChildAccessor[Parent, Child: InteractiveChildObject, Filter](PaginatedAccessor[Child, Filter]): + def __init__( + self: Self, parent: Parent, path: Endpoint, requester: Requester, accessor_filter: AccessorFilter = None + ) -> None: + super().__init__(path, requester, accessor_filter) self._parent = parent def _create_object(self: Self, data: dict[str, Any]) -> Child: return self.class_type(parent=self._parent, requester=self._requester, data=data) -class NonPaginatedChildAccessor[Parent, Child: InteractiveChildObject, Filter: dict | None](Accessor[Child, Filter]): - def __init__(self: Self, parent: Parent, path: Endpoint, requester: Requester, filters: Filter = None) -> None: - super().__init__(path, requester, filters) +class NonPaginatedChildAccessor[Parent, Child: InteractiveChildObject, Filter](Accessor[Child, Filter]): + def __init__( + self: Self, parent: Parent, path: Endpoint, requester: Requester, accessor_filter: AccessorFilter = None + ) -> None: + super().__init__(path, requester, accessor_filter) self._parent = parent async def iter(self: Self) -> AsyncGenerator[Child, None]: diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 5df156d..456a7ea 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -21,6 +21,8 @@ from adcm_aio_client.core.objects._mapping import ClusterMapping from adcm_aio_client.core.types import ADCMEntityStatus, Endpoint +type Filter = object # TODO: implement + class ADCM(InteractiveObject, WithActions, WithConfig): @property @@ -178,9 +180,11 @@ def cluster(self: Self) -> Cluster: return self.service.cluster @cached_property - def hosts(self: Self) -> "HostsNode": - return HostsNode( - path=(*self.cluster.get_own_path(), "hosts"), requester=self._requester, filters={"componentId": self.id} + def hosts(self: Self) -> "HostsAccessor": + return HostsAccessor( + path=(*self.cluster.get_own_path(), "hosts"), + requester=self._requester, + accessor_filter={"componentId": self.id}, ) def get_own_path(self: Self) -> Endpoint: @@ -208,8 +212,10 @@ def display_name(self: Self) -> str: return str(self._data["prototype"]["displayName"]) @cached_property - def hosts(self: Self) -> "HostsNode": - return HostsNode(path=("hosts",), requester=self._requester, filters={"hostproviderName": self.name}) + def hosts(self: Self) -> "HostsAccessor": + return HostsAccessor( + path=("hosts",), requester=self._requester, accessor_filter={"hostproviderName": self.name} + ) def get_own_path(self: Self) -> Endpoint: return self.PATH_PREFIX, self.id @@ -251,18 +257,18 @@ def __str__(self: Self) -> str: return f"<{self.__class__.__name__} #{self.id} {self.name}>" -class HostsNode(PaginatedAccessor[Host, dict | None]): +class HostsAccessor(PaginatedAccessor[Host, dict | None]): class_type = Host -class HostsInClusterNode(HostsNode): - async def add(self: Self, host: Host | Iterable[Host] | None = None, **filters: dict) -> None: - hosts = await self._get_hosts_from_arg_or_filter(host=host, **filters) +class HostsInClusterNode(HostsAccessor): + async def add(self: Self, host: Host | Iterable[Host] | None = None, filters: Filter | None = None) -> None: + hosts = await self._get_hosts_from_arg_or_filter(host=host, filters=filters) await self._requester.post(*self._path, data=[{"hostId": host.id} for host in hosts]) - async def remove(self: Self, host: Host | Iterable[Host] | None = None, **filters: dict) -> None: - hosts = await self._get_hosts_from_arg_or_filter(host=host, **filters) + async def remove(self: Self, host: Host | Iterable[Host] | None = None, filters: Filter | None = None) -> None: + hosts = await self._get_hosts_from_arg_or_filter(host=host, filters=filters) results = await asyncio.gather( *(self._requester.delete(*self._path, host_.id) for host_ in hosts), return_exceptions=True @@ -278,7 +284,7 @@ async def remove(self: Self, host: Host | Iterable[Host] | None = None, **filter raise OperationError(f"Some hosts can't be deleted from cluster: {errors}") async def _get_hosts_from_arg_or_filter( - self: Self, host: Host | Iterable[Host] | None = None, **filters: dict + self: Self, host: Host | Iterable[Host] | None = None, filters: Filter | None = None ) -> Iterable[Host]: if all((host, filters)): raise ValueError("`host` and `filters` arguments are mutually exclusive.") @@ -286,6 +292,6 @@ async def _get_hosts_from_arg_or_filter( if host: hosts = [host] if isinstance(host, Host) else host else: - hosts = await self.filter(**filters) + hosts = await self.filter(filters) # type: ignore # TODO return hosts From 2f81263a619275294205bd934c6cf956c3e4d251 Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Mon, 25 Nov 2024 12:51:55 +0300 Subject: [PATCH 19/20] ADCM-6133: review fixes --- adcm_aio_client/core/requesters.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adcm_aio_client/core/requesters.py b/adcm_aio_client/core/requesters.py index 2aec83f..80247a2 100644 --- a/adcm_aio_client/core/requesters.py +++ b/adcm_aio_client/core/requesters.py @@ -154,10 +154,10 @@ async def login(self: Self, credentials: Credentials) -> Self: async def get(self: Self, *path: PathPart, query: QueryParameters | None = None) -> HTTPXRequesterResponse: return await self.request(*path, method=self.client.get, params=query or {}) - async def post(self: Self, *path: PathPart, data: Json) -> HTTPXRequesterResponse: + async def post(self: Self, *path: PathPart, data: dict | list) -> HTTPXRequesterResponse: return await self.request(*path, method=self.client.post, json=data) - async def patch(self: Self, *path: PathPart, data: Json) -> HTTPXRequesterResponse: + async def patch(self: Self, *path: PathPart, data: dict | list) -> HTTPXRequesterResponse: return await self.request(*path, method=self.client.patch, json=data) async def delete(self: Self, *path: PathPart) -> HTTPXRequesterResponse: From 6a531835ef5c8c450908696af450294e389481ac Mon Sep 17 00:00:00 2001 From: Skrynnik Daniil Date: Mon, 25 Nov 2024 14:02:24 +0300 Subject: [PATCH 20/20] ADCM-6143: Change `cached_property` to `async_cached_property` for existing objects --- adcm_aio_client/core/objects/_base.py | 4 +++- adcm_aio_client/core/objects/cm.py | 12 ++++++---- poetry.lock | 18 +++++++++++++- pyproject.toml | 1 + tests/integration/test_misc.py | 34 +++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 tests/integration/test_misc.py diff --git a/adcm_aio_client/core/objects/_base.py b/adcm_aio_client/core/objects/_base.py index 958db4b..c8e53d4 100644 --- a/adcm_aio_client/core/objects/_base.py +++ b/adcm_aio_client/core/objects/_base.py @@ -3,6 +3,8 @@ from functools import cached_property from typing import Any, Self +from asyncstdlib.functools import CachedProperty + from adcm_aio_client.core.requesters import Requester from adcm_aio_client.core.types import AwareOfOwnPath, Endpoint, WithRequester @@ -18,7 +20,7 @@ def __init_subclass__(cls: type[Self]) -> None: for name in dir(cls): # None is for declared, but unset values attr = getattr(cls, name, None) - if isinstance(attr, cached_property): # TODO: asyncstdlib.functools.CachedProperty + if isinstance(attr, (cached_property, CachedProperty)): cls._delete_on_refresh.append(name) def __init__(self: Self, requester: Requester, data: dict[str, Any]) -> None: diff --git a/adcm_aio_client/core/objects/cm.py b/adcm_aio_client/core/objects/cm.py index 456a7ea..6a88139 100644 --- a/adcm_aio_client/core/objects/cm.py +++ b/adcm_aio_client/core/objects/cm.py @@ -2,6 +2,8 @@ from typing import Iterable, Self import asyncio +from asyncstdlib.functools import cached_property as async_cached_property + from adcm_aio_client.core.errors import NotFoundError, OperationError, ResponseError from adcm_aio_client.core.objects._accessors import ( PaginatedAccessor, @@ -29,7 +31,7 @@ class ADCM(InteractiveObject, WithActions, WithConfig): def id(self: Self) -> int: return 1 - @cached_property + @async_cached_property async def version(self: Self) -> str: # TODO: override root_path for being without /api/v2 response = await self._requester.get("versions") @@ -68,7 +70,7 @@ def description(self: Self) -> str: # todo think how such properties will be invalidated when data is updated # during `refresh()` / `reread()` calls. # See cache invalidation or alternatives in documentation for `cached_property` - @cached_property # TODO: replace with asyncstdlib.functools.cached_property + @async_cached_property async def bundle(self: Self) -> Bundle: prototype_id = self._data["prototype"]["id"] response = await self._requester.get("prototypes", prototype_id) @@ -162,7 +164,7 @@ def name(self: Self) -> str: def display_name(self: Self) -> str: return self._data["displayName"] - @cached_property # TODO: replace with asyncstdlib.functools.cached_property + @async_cached_property async def constraint(self: Self) -> list[int | str]: response = (await self._requester.get(*self.cluster.get_own_path(), "mapping", "components")).as_list() for component in response: @@ -240,13 +242,13 @@ async def get_status(self: Self) -> ADCMEntityStatus: response = await self._requester.get(*self.get_own_path()) return ADCMEntityStatus(response.as_dict()["status"]) - @cached_property # TODO: replace with asyncstdlib.functools.cached_property + @async_cached_property async def cluster(self: Self) -> Cluster | None: if not self._data["cluster"]: return None return await Cluster.with_id(requester=self._requester, object_id=self._data["cluster"]["id"]) - @cached_property # TODO: replace with asyncstdlib.functools.cached_property + @async_cached_property async def hostprovider(self: Self) -> HostProvider: return await HostProvider.with_id(requester=self._requester, object_id=self._data["hostprovider"]["id"]) diff --git a/poetry.lock b/poetry.lock index 48ef0aa..a4a080f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -20,6 +20,22 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] trio = ["trio (>=0.26.1)"] +[[package]] +name = "asyncstdlib" +version = "3.13.0" +description = "The missing async toolbox" +optional = false +python-versions = "~=3.8" +files = [ + {file = "asyncstdlib-3.13.0-py3-none-any.whl", hash = "sha256:60e097c19e815f3c419a77426cf6c3653aebcb766544d631d5ce6128d0851ae8"}, + {file = "asyncstdlib-3.13.0.tar.gz", hash = "sha256:f2a6ffb44f118233bb99bef50861d6f64c432decbdcc4c2cb93b3fff40d1b533"}, +] + +[package.extras] +doc = ["sphinx", "sphinxcontrib-trio"] +test = ["black", "coverage", "flake8", "flake8-2020", "flake8-bugbear", "mypy", "pytest", "pytest-cov"] +typetest = ["mypy", "pyright", "typing-extensions"] + [[package]] name = "certifi" version = "2024.8.30" @@ -271,4 +287,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "20136fad059dd6f087334eea57612f368edbe485fcd2874666338000b4859d1b" +content-hash = "c14d12ccbcd8910aed151decce58a67d6f4798d9a9c6b7c086be12ca2c935587" diff --git a/pyproject.toml b/pyproject.toml index ce8b2c4..6cfadd6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.12" httpx = "^0.27.2" +asyncstdlib = "^3.13.0" [tool.poetry.group.dev] optional = true diff --git a/tests/integration/test_misc.py b/tests/integration/test_misc.py new file mode 100644 index 0000000..13772ae --- /dev/null +++ b/tests/integration/test_misc.py @@ -0,0 +1,34 @@ +from typing import Self + +from asyncstdlib.functools import cached_property as async_cached_property +import pytest + +pytestmark = [pytest.mark.asyncio] + + +class Dummy: + def __init__(self: Self) -> None: + self.counter = 0 + + @async_cached_property + async def func(self: Self) -> int: + self.counter += 1 + + return self.counter + + +async def test_async_cached_property() -> None: + obj = Dummy() + assert "func" not in obj.__dict__, "`func` key should not be cached yet" + + res = await obj.func + assert res == 1 + assert "func" in obj.__dict__, "`func` key should be cached" + + res = await obj.func + assert res == 1, "Cached value must be used" + + delattr(obj, "func") + res = await obj.func + assert res == 2, "Expected to execute func() again, increasing the counter" + assert "func" in obj.__dict__