From eaeb266eb6905df5e41051b2f4c88a0f5d9eb2e2 Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Mon, 1 Nov 2021 18:50:22 +0200 Subject: [PATCH 1/2] Raise dedicated NotSupportedError for unsupported REST API calls --- neuro-cli/src/neuro_cli/main.py | 4 ++++ neuro-sdk/src/neuro_sdk/__init__.py | 2 ++ neuro-sdk/src/neuro_sdk/admin.py | 31 +++++++++++++++++------------ neuro-sdk/src/neuro_sdk/errors.py | 4 ++++ neuro-sdk/tests/conftest.py | 7 +++++-- neuro-sdk/tests/test_admin.py | 18 ++++++++++++++++- 6 files changed, 50 insertions(+), 16 deletions(-) diff --git a/neuro-cli/src/neuro_cli/main.py b/neuro-cli/src/neuro_cli/main.py index fecb8601b..f662d7164 100644 --- a/neuro-cli/src/neuro_cli/main.py +++ b/neuro-cli/src/neuro_cli/main.py @@ -602,6 +602,10 @@ def main(args: Optional[List[str]] = None) -> None: log.exception(f"Docker API error: {error.message}") sys.exit(EX_PROTOCOL) + except neuro_sdk.NotSupportedError as error: + log.exception(f"{_err_to_str(error)}") + sys.exit(EX_SOFTWARE) + except NotImplementedError as error: log.exception(f"{_err_to_str(error)}") sys.exit(EX_SOFTWARE) diff --git a/neuro-sdk/src/neuro_sdk/__init__.py b/neuro-sdk/src/neuro_sdk/__init__.py index 0217e2e99..b45dcd962 100644 --- a/neuro-sdk/src/neuro_sdk/__init__.py +++ b/neuro-sdk/src/neuro_sdk/__init__.py @@ -51,6 +51,7 @@ ConfigError, IllegalArgumentError, NDJSONError, + NotSupportedError, ResourceNotFound, ServerNotAvailable, ) @@ -138,6 +139,7 @@ "Jobs", "LocalImage", "NDJSONError", + "NotSupportedError", "PASS_CONFIG_ENV_NAME", "Parser", "Permission", diff --git a/neuro-sdk/src/neuro_sdk/admin.py b/neuro-sdk/src/neuro_sdk/admin.py index 1052ca4ab..ea6668d11 100644 --- a/neuro-sdk/src/neuro_sdk/admin.py +++ b/neuro-sdk/src/neuro_sdk/admin.py @@ -5,9 +5,11 @@ from typing import Any, Dict, List, Mapping, Optional from dateutil.parser import isoparse +from yarl import URL from .config import Config from .core import _Core +from .errors import NotSupportedError from .server_cfg import Preset from .utils import NoPublicConstructor @@ -102,6 +104,14 @@ def __init__(self, core: _Core, config: Config) -> None: self._core = core self._config = config + @property + def _admin_url(self) -> URL: + url = self._config.admin_url + if not url: + raise NotSupportedError("admin API is not supported by server") + else: + return url + async def list_cloud_providers(self) -> Dict[str, Dict[str, Any]]: url = self._config.api_url / "cloud_providers" auth = await self._config._api_auth() @@ -122,7 +132,7 @@ async def list_clusters(self) -> Dict[str, _Cluster]: return ret async def add_cluster(self, name: str, config: Dict[str, Any]) -> None: - url = self._config.admin_url / "clusters" + url = self._admin_url / "clusters" auth = await self._config._api_auth() payload = {"name": name} async with self._core.request("POST", url, auth=auth, json=payload) as resp: @@ -147,7 +157,7 @@ async def list_cluster_users( self, cluster_name: Optional[str] = None ) -> List[_ClusterUser]: cluster_name = cluster_name or self._config.cluster_name - url = self._config.admin_url / "clusters" / cluster_name / "users" + url = self._admin_url / "clusters" / cluster_name / "users" auth = await self._config._api_auth() async with self._core.request( "GET", url, auth=auth, params={"with_user_info": "true"} @@ -162,7 +172,7 @@ async def get_cluster_user( ) -> _ClusterUser: cluster_name = cluster_name or self._config.cluster_name user_name = user_name or self._config.username - url = self._config.admin_url / "clusters" / cluster_name / "users" / user_name + url = self._admin_url / "clusters" / cluster_name / "users" / user_name auth = await self._config._api_auth() async with self._core.request( "GET", url, auth=auth, params={"with_user_info": "true"} @@ -173,7 +183,7 @@ async def get_cluster_user( async def add_cluster_user( self, cluster_name: str, user_name: str, role: str ) -> _ClusterUser: - url = self._config.admin_url / "clusters" / cluster_name / "users" + url = self._admin_url / "clusters" / cluster_name / "users" payload = {"user_name": user_name, "role": role} auth = await self._config._api_auth() @@ -184,7 +194,7 @@ async def add_cluster_user( return _cluster_user_from_api(payload) async def remove_cluster_user(self, cluster_name: str, user_name: str) -> None: - url = self._config.admin_url / "clusters" / cluster_name / "users" / user_name + url = self._admin_url / "clusters" / cluster_name / "users" / user_name auth = await self._config._api_auth() async with self._core.request("DELETE", url, auth=auth): @@ -198,12 +208,7 @@ async def set_user_quota( total_running_jobs: Optional[int], ) -> _ClusterUser: url = ( - self._config.admin_url - / "clusters" - / cluster_name - / "users" - / user_name - / "quota" + self._admin_url / "clusters" / cluster_name / "users" / user_name / "quota" ) payload = { "quota": { @@ -227,7 +232,7 @@ async def set_user_credits( credits: Optional[Decimal], ) -> _ClusterUser: url = ( - self._config.admin_url + self._admin_url / "clusters" / cluster_name / "users" @@ -253,7 +258,7 @@ async def add_user_credits( additional_credits: Decimal, ) -> _ClusterUser: url = ( - self._config.admin_url + self._admin_url / "clusters" / cluster_name / "users" diff --git a/neuro-sdk/src/neuro_sdk/errors.py b/neuro-sdk/src/neuro_sdk/errors.py index 6447c2492..42a895492 100644 --- a/neuro-sdk/src/neuro_sdk/errors.py +++ b/neuro-sdk/src/neuro_sdk/errors.py @@ -36,3 +36,7 @@ class ConfigLoadException(Exception): class NDJSONError(ValueError): pass + + +class NotSupportedError(NotImplementedError): + pass diff --git a/neuro-sdk/tests/conftest.py b/neuro-sdk/tests/conftest.py index 6eb8e240c..be8ce363f 100644 --- a/neuro-sdk/tests/conftest.py +++ b/neuro-sdk/tests/conftest.py @@ -100,7 +100,8 @@ def go( registry_url: str = "https://registry-dev.neu.ro", trace_id: str = "bd7a977555f6b982", clusters: Optional[Dict[str, Cluster]] = None, - token_url: Optional[URL] = None + token_url: Optional[URL] = None, + admin_url: Optional[URL] = None, ) -> Client: url = URL(url_str) if clusters is None: @@ -162,11 +163,13 @@ def go( real_auth_config = replace(auth_config, token_url=token_url) else: real_auth_config = auth_config + if admin_url is None: + admin_url = URL(url) / ".." / ".." / "apis" / "admin" / "v1" config = _ConfigData( auth_config=real_auth_config, auth_token=_AuthToken.create_non_expiring(token), url=URL(url), - admin_url=URL(url) / ".." / ".." / "apis" / "admin" / "v1", + admin_url=admin_url, version=__version__, cluster_name=next(iter(clusters)), clusters=clusters, diff --git a/neuro-sdk/tests/test_admin.py b/neuro-sdk/tests/test_admin.py index a957dd15a..c14c0baff 100644 --- a/neuro-sdk/tests/test_admin.py +++ b/neuro-sdk/tests/test_admin.py @@ -2,11 +2,13 @@ from decimal import Decimal from typing import Callable +import pytest from aiohttp import web from aiohttp.web import HTTPCreated, HTTPNoContent from aiohttp.web_exceptions import HTTPOk +from yarl import URL -from neuro_sdk import Client +from neuro_sdk import Client, NotSupportedError from neuro_sdk.admin import ( _Balance, _CloudProvider, @@ -536,6 +538,20 @@ async def handle_get_cluster_user(request: web.Request) -> web.StreamResponse: assert requested_users == ["test"] +async def test_not_supported_admin_api( + aiohttp_server: _TestServerFactory, make_client: _MakeClient +) -> None: + app = web.Application() + + srv = await aiohttp_server(app) + + async with make_client(srv.make_url("/api/v1"), admin_url=URL()) as client: + with pytest.raises( + NotSupportedError, match="admin API is not supported by server" + ): + await client._admin.get_cluster_user("default", "test") + + async def test_add_cluster_user( aiohttp_server: _TestServerFactory, make_client: _MakeClient ) -> None: From b554974155b7409b95f97f7727425fcd9e9c2b2f Mon Sep 17 00:00:00 2001 From: Andrew Svetlov Date: Mon, 1 Nov 2021 18:51:31 +0200 Subject: [PATCH 2/2] Add CHANGELOG --- CHANGELOG.D/2407.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 CHANGELOG.D/2407.feature diff --git a/CHANGELOG.D/2407.feature b/CHANGELOG.D/2407.feature new file mode 100644 index 000000000..f3ff6668e --- /dev/null +++ b/CHANGELOG.D/2407.feature @@ -0,0 +1 @@ +Raise dedicated `NotSupportedError` for unsupported REST API calls