diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..8923b9a --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @rapyuta-robotics/io-cli-owner @rapyuta-robotics/io-first-reviewer \ No newline at end of file diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml new file mode 100644 index 0000000..500dcd5 --- /dev/null +++ b/.github/workflows/conventional-commits.yml @@ -0,0 +1,20 @@ +name: 💬 Check Commit Hygiene + +on: + pull_request: + branches: + - main + - devel + +jobs: + verify: + name: Conventional Commits + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + name: Checkout code + + - uses: rapyuta-robotics/action-conventional-commits@v1.1.1 + name: Check if commit messages are compliant + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2546d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# python generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# venv +.venv \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8651ba2 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..d939460 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/rapyuta-io-sdk-v2.iml b/.idea/rapyuta-io-sdk-v2.iml new file mode 100644 index 0000000..4a48cb6 --- /dev/null +++ b/.idea/rapyuta-io-sdk-v2.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..d9506ce --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12.5 diff --git a/README.md b/README.md new file mode 100644 index 0000000..7b9e565 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# rapyuta-io-sdk-v2 + +Describe your project here. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2ce4006 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "rapyuta-io-sdk-v2" +version = "0.1.0" +description = "Version:2 for Rapyuta.io SDK" +dependencies = [ + "httpx>=0.27.2", + "pydantic-settings>=2.5.2", + "python-benedict>=0.33.2", + "pyyaml>=6.0.2", + "setuptools>=75.1.0", +] +readme = "README.md" +requires-python = ">= 3.8" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.rye] +managed = true +dev-dependencies = [] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.build.targets.wheel] +packages = ["rapyuta_io_sdk_v2"] diff --git a/rapyuta_io_sdk_v2/__init__.py b/rapyuta_io_sdk_v2/__init__.py new file mode 100644 index 0000000..c12e8ca --- /dev/null +++ b/rapyuta_io_sdk_v2/__init__.py @@ -0,0 +1,3 @@ +from rapyuta_io_sdk_v2.config import Configuration +from rapyuta_io_sdk_v2.pydantic_configs import RRSettings +__version__ = "1.17.0" \ No newline at end of file diff --git a/rapyuta_io_sdk_v2/async_client.py b/rapyuta_io_sdk_v2/async_client.py new file mode 100644 index 0000000..9796dd0 --- /dev/null +++ b/rapyuta_io_sdk_v2/async_client.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from contextlib import asynccontextmanager + +from rapyuta_io_sdk_v2.client import Client +from typing import Optional, override, Any, AsyncGenerator, List, Dict +import httpx + +class AsyncClient(Client): + + def __init__(self,config): + super().__init__(config) + + @asynccontextmanager + async def _get_client(self) -> AsyncGenerator[httpx.AsyncClient,None]: + async with httpx.AsyncClient( + headers=self._get_headers(), + ) as async_client: + yield async_client + + @override + async def list_projects(self, organization_guid: str = None): + url = "{}/v2/projects/".format(self.v2api_host) + params = {} + if organization_guid: + params.update({ + "organizations": organization_guid, + }) + async with self._get_client() as client: + response = await client.get(url=url, params=params) + response.raise_for_status() + return response.json() + + @override + async def get_project(self, project_guid: str): + url = "{}/v2/projects/{}/".format(self.v2api_host, project_guid) + + async with self._get_client() as client: + response = await client.get(url=url) + response.raise_for_status() + return response.json() + + @override + async def list_config_trees(self) -> List[str]: + url = "{}/v2/configtrees/".format(self.v2api_host) + async with self._get_client() as client: + try: + res = await client.get(url=url) + res.raise_for_status() + + except Exception as e: + raise ValueError(f"Failed to list config trees: {res.text}") from e + + if tree_list := res.json().get("items"): + return [item["metadata"]["name"] for item in tree_list] + else: + return [] + + @override + async def get_config_tree( + self, + tree_name: str, rev_id: Optional[str] = None, + include_data: bool = False, filter_content_types: Optional[List[str]] = None, + filter_prefixes: Optional[List[str]] = None + ) : + url = "{}/v2/configtrees/{}/".format(self.v2api_host, tree_name) + params: Dict[str, Any] = { + 'includeData': include_data, + 'contentTypes': filter_content_types, + 'keyPrefixes': filter_prefixes, + 'revision': rev_id, + } + async with self._get_client() as client: + try: + res = await client.get( + url=url, + params=params + ) + res.raise_for_status() + except Exception as e: + raise ValueError(f"Failed to get config tree data") + + raw_config_tree = res.json().get("keys", {}) + return self._preprocess_config_tree_data(raw_config_tree) + + @override + async def create_config_tree(self,tree_spec: dict): + url = "{}/v2/configtrees/".format(self.v2api_host) + async with self._get_client() as client: + try: + res = await client.post( + url=url, + json=tree_spec + ) + res.raise_for_status() + except Exception as e: + raise ValueError(f"Failed to create config tree: {res.text}") + raw_config_tree = res.json() + return raw_config_tree + + + + + + + diff --git a/rapyuta_io_sdk_v2/client.py b/rapyuta_io_sdk_v2/client.py new file mode 100644 index 0000000..8028ac5 --- /dev/null +++ b/rapyuta_io_sdk_v2/client.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Optional, List, Dict, Any +from benedict import benedict +from rapyuta_io_sdk_v2.utils import unflatten_keys +import httpx, json + +from .utils import handle_server_errors +from rapyuta_io_sdk_v2.constants import GET_USER_PATH + +class Client(object): + PROD_V2API_URL = "https://api.rapyuta.io" + def __init__(self,config): + self.config = config + self.v2api_host = config.hosts.get("v2api_host",self.PROD_V2API_URL) + + def _get_headers(self, with_project: bool = True) -> dict: + headers = { + "Authorization" : 'Bearer '+self.config.auth_token, + "Content-Type": "application/json", + "project": self.config.project_guid, + "organizationguid": self.config.organization_guid, + } + return headers + + @staticmethod + def _preprocess_config_tree_data(raw_config_tree: Optional[Dict[str, Any]]): + if raw_config_tree: + return unflatten_keys(raw_config_tree) + else: + return benedict() + + def get_authenticated_user(self) -> Optional[Dict]: + try: + _core_api_host = self.config.hosts.get("core_api_host") + url = "{}{}".format(_core_api_host, GET_USER_PATH) + headers = self._get_headers() + response = httpx.get(url=url, headers=headers) + handle_server_errors(response) + return response.json() + except Exception as e: + raise + + def list_projects(self,organization_guid: str = None): + """ + + :param organization_guid: + :return: + """ + url = "{}/v2/projects/".format(self.v2api_host) + headers = self._get_headers(with_project=False) + params = {} + if organization_guid: + params.update({ + "organizations": organization_guid, + }) + response = httpx.get(url=url,headers=headers,params=params) + handle_server_errors(response) + return response.json() + + def get_project(self, project_guid: str): + """ + + :param project_guid: + :return: + """ + url = "{}/v2/projects/{}/".format(self.v2api_host, project_guid) + headers = self._get_headers(with_project=False) + response = httpx.get(url=url,headers=headers) + handle_server_errors(response) + return response.json() + + def get_config_tree(self,tree_name: str, rev_id: Optional[str]=None, + include_data: bool = False, filter_content_types: Optional[List[str]] = None, + filter_prefixes: Optional[List[str]] = None): + + url = "{}/v2/configtrees/{}/".format(self.v2api_host, tree_name) + query = { + 'includeData': include_data, + 'contentTypes': filter_content_types, + 'keyPrefixes': filter_prefixes, + 'revision': rev_id, + } + headers = self._get_headers() + response = httpx.get(url=url,headers=headers,params=query) + handle_server_errors(response) + return response.json() + + def create_config_tree(self,tree_spec: dict): + url = "{}/v2/configtrees/".format(self.v2api_host) + headers = self._get_headers() + response = httpx.post(url=url,headers=headers,json=tree_spec) + handle_server_errors(response) + return response.json() + + def delete_config_tree(self,tree_name:str): + url = "{}/v2/configtrees/{}/".format(self.v2api_host, tree_name) + headers = self._get_headers() + response = httpx.delete(url=url,headers=headers) + handle_server_errors(response) + return response.json() + + def list_config_trees(self): + url = "{}/v2/configtrees/".format(self.v2api_host) + headers = self._get_headers() + response = httpx.get(url=url,headers=headers) + handle_server_errors(response) + return response.json() + + def set_revision_config_tree(self, tree_name: str, spec: dict) -> None: + url = "{}/v2/configtrees/{}/".format(self.v2api_host, tree_name) + headers = self._get_headers() + response = httpx.put(url=url,headers=headers,json=spec) + handle_server_errors(response) + + data = json.loads(response.text) + print(data) + if not data.ok: + err_msg = data.get('error') + raise Exception("configtree: {}".format(err_msg)) + + diff --git a/rapyuta_io_sdk_v2/config.py b/rapyuta_io_sdk_v2/config.py new file mode 100644 index 0000000..f1cc234 --- /dev/null +++ b/rapyuta_io_sdk_v2/config.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Optional + +from rapyuta_io_sdk_v2.async_client import AsyncClient +from rapyuta_io_sdk_v2.client import Client +from rapyuta_io_sdk_v2.constants import LOGIN_ROUTE_PATH, NAMED_ENVIRONMENTS, STAGING_ENVIRONMENT_SUBDOMAIN, PROD_ENVIRONMENT_SUBDOMAIN +from rapyuta_io_sdk_v2.exceptions import ValidationError, AuthenticationError, LoggedOutError +import httpx + +from rapyuta_io_sdk_v2.utils import validate_auth_token + + +class Configuration(object): + + def __init__(self, project_guid: str, organization_guid: str, password: str=None,auth_token: str=None, environment: str=None,email: str=None): + self.email = email + self._password = password + self.auth_token = auth_token + self.project_guid = project_guid + self.organization_guid = organization_guid + self.environment = environment + self.hosts = {} + self.set_environment(environment) + + # login + self._login() + + def _login(self) -> None: + _rip_host = self.hosts.get("rip_host") + try: + if self.auth_token is not None: + user = validate_auth_token(self) + self.email=user["emailID"] + return + + url = '{}{}'.format(_rip_host,LOGIN_ROUTE_PATH) + response = httpx.post(url, json={'email': self.email, 'password': self._password}).json() + if response['success']: + self.auth_token = response['data']['token'] + else: + raise AuthenticationError() + except AuthenticationError as e: + raise + except Exception as e: + raise + + def set_project(self, project) -> None: + self.project_guid = project + + def set_organization(self,organization_guid) -> None: + self.project_guid=None + self.organization_guid = organization_guid + + def sync_client(self) -> Optional[Client]: + if self.auth_token is None: + raise LoggedOutError("You are not logged in. Run config.login() to login.") + return Client(self) + + def async_client(self) -> Optional[AsyncClient]: + if self.auth_token is None: + raise LoggedOutError("You are not logged in. Run config.login() to login.") + return AsyncClient(self) + + def set_environment(self, name: str) -> None: + + subdomain = PROD_ENVIRONMENT_SUBDOMAIN + if name is not None: + is_valid_env = name in NAMED_ENVIRONMENTS or name.startswith('pr') + if not is_valid_env: + raise ValidationError("Invalid environment") + subdomain = STAGING_ENVIRONMENT_SUBDOMAIN + else: + name = "ga" + + core = 'https://{}apiserver.{}'.format(name, subdomain) + rip = 'https://{}rip.{}'.format(name, subdomain) + v2api = 'https://{}api.{}'.format(name, subdomain) + + self.hosts['environment'] = name + self.hosts['core_api_host'] = core + self.hosts['rip_host'] = rip + self.hosts['v2api_host'] = v2api diff --git a/rapyuta_io_sdk_v2/constants.py b/rapyuta_io_sdk_v2/constants.py new file mode 100644 index 0000000..f558bfb --- /dev/null +++ b/rapyuta_io_sdk_v2/constants.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +LOGIN_ROUTE_PATH = '/user/login' +GET_USER_PATH = '/api/user/me/get' + + +STAGING_ENVIRONMENT_SUBDOMAIN = "apps.okd4v2.okd4beta.rapyuta.io" +PROD_ENVIRONMENT_SUBDOMAIN = "apps.okd4v2.prod.rapyuta.io" +NAMED_ENVIRONMENTS = ["v11", "v12", "v13", "v14", "v15", "qa", "dev"] \ No newline at end of file diff --git a/rapyuta_io_sdk_v2/exceptions.py b/rapyuta_io_sdk_v2/exceptions.py new file mode 100644 index 0000000..c47ca7a --- /dev/null +++ b/rapyuta_io_sdk_v2/exceptions.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class AuthenticationError(Exception): + """Exception raised for errors in the authentication process.""" + + def __init__(self, message="Authentication failed"): + self.message = message + super().__init__(self.message) + +class LoggedOutError(Exception): + + def __init__(self,message="Not Authenticated"): + self.message = message + super().__init__(self.message) + +class HttpNotFoundError(Exception): + def __init__(self, message='resource not found'): + self.message = message + super().__init__(self.message) +class HttpAlreadyExistsError(Exception): + def __init__(self, message='resource already exists'): + self.message = message + super().__init__(self.message) + +class ValidationError(Exception): + def __init__(self, message=None): + self.message = message + super().__init__(self.message) + + + + diff --git a/rapyuta_io_sdk_v2/pydantic_configs/__init__.py b/rapyuta_io_sdk_v2/pydantic_configs/__init__.py new file mode 100644 index 0000000..8d776f6 --- /dev/null +++ b/rapyuta_io_sdk_v2/pydantic_configs/__init__.py @@ -0,0 +1 @@ +from rapyuta_io_sdk_v2.pydantic_configs.app_config_settings import RRSettings \ No newline at end of file diff --git a/rapyuta_io_sdk_v2/pydantic_configs/app_config_settings.py b/rapyuta_io_sdk_v2/pydantic_configs/app_config_settings.py new file mode 100644 index 0000000..4cfeed2 --- /dev/null +++ b/rapyuta_io_sdk_v2/pydantic_configs/app_config_settings.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Optional, Tuple, Type,Any + +from pydantic_settings import ( + BaseSettings, + InitSettingsSource, + PydanticBaseSettingsSource, + SettingsConfigDict, +) + +from rapyuta_io_sdk_v2.pydantic_configs.app_config_source import RRConfigSettingsSource +from rapyuta_io_sdk_v2 import Configuration + + +class RIOAuthCredentials(BaseSettings): + rio_auth_token: str = "" + rio_organization_id: str = "" + rio_project_id: str = "" + rio_environment:str = "" + + +class RIOConfigTree(BaseSettings): + config_tree_name: str = "" + config_tree_revision: Optional[str] = None + + +class RRSettings(BaseSettings): + model_config = SettingsConfigDict(extra="allow") + rio_auth_token: str = "" + config_tree_name: str + config_tree_version: Optional[str] = None + rio_organization_id: str = "" + rio_project_id: str = "" + __config_source__ = None + + @classmethod + def settings_customise_sources( + cls, + settings_cls: Type[BaseSettings], + init_settings: InitSettingsSource, # overriding the default init settings + env_settings: PydanticBaseSettingsSource, + dotenv_settings: PydanticBaseSettingsSource, + file_secret_settings: PydanticBaseSettingsSource, + ) -> Tuple[PydanticBaseSettingsSource, ...]: + + rio_auth_creds = RIOAuthCredentials() + rio_config_tree = RIOConfigTree() + + cls.rio_auth_token = init_settings.init_kwargs.get( + "rio_auth_token", rio_auth_creds.rio_auth_token + ) + cls.rio_organization_id = init_settings.init_kwargs.get( + "rio_organization_id", rio_auth_creds.rio_organization_id + ) + cls.rio_project_id = init_settings.init_kwargs.get( + "rio_project_id", rio_auth_creds.rio_project_id + ) + + cls.rio_environment = init_settings.init_kwargs.get( + "rio_environment", rio_auth_creds.rio_environment + ) + cls.config_tree_name = init_settings.init_kwargs.get( + "config_tree_name", rio_config_tree.config_tree_name + ) + cls.config_tree_revision = init_settings.init_kwargs.get( + "config_tree_version", rio_config_tree.config_tree_revision + ) + + _client_config = Configuration( + auth_token=cls.rio_auth_token, + project_guid=cls.rio_project_id, + organization_guid=cls.rio_organization_id, + environment=cls.rio_environment + ) + + rr_config_settings_source = RRConfigSettingsSource( + settings_cls, + client_config=_client_config, + config_tree_name=cls.config_tree_name, + config_tree_revision=cls.config_tree_revision, + ) + cls.__config_source__ = rr_config_settings_source + + return ( + init_settings, + env_settings, + dotenv_settings, + file_secret_settings, + rr_config_settings_source, + ) + + def get(self, key: str) -> Optional[Any]: + if hasattr(self.__config_source__,'get_value'): + return self.__config_source__.get_value(key) + return None \ No newline at end of file diff --git a/rapyuta_io_sdk_v2/pydantic_configs/app_config_source.py b/rapyuta_io_sdk_v2/pydantic_configs/app_config_source.py new file mode 100644 index 0000000..6f4eb9e --- /dev/null +++ b/rapyuta_io_sdk_v2/pydantic_configs/app_config_source.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import asyncio +from typing import Any, Optional, Tuple, Type +from pydantic.fields import FieldInfo +from pydantic_settings import ( + BaseSettings, + PydanticBaseSettingsSource, +) + +from rapyuta_io_sdk_v2.config import Configuration + + +class RRConfigSettingsSource(PydanticBaseSettingsSource): + def __init__( + self, + settings_cls: Type[BaseSettings], + client_config: Configuration, + config_tree_name: str, + config_tree_revision: Optional[str] = None, + ): + self._config_tree = None + self.config_tree_aysnc_client = client_config.async_client() + self.config_tree_name = config_tree_name + self.config_tree_revision = config_tree_revision + + current_loop = asyncio.get_event_loop() + current_loop.run_until_complete( + self._retrieve_config_tree() + ) + + super().__init__( + settings_cls, + ) + + async def _retrieve_config_tree(self) -> None: + res = await self.config_tree_aysnc_client.get_config_tree( + tree_name=self.config_tree_name, + rev_id=self.config_tree_revision, + include_data=True + ) + self._config_tree = res + + def get_field_value( + self, + field: FieldInfo, + field_name: str, + ) -> Tuple[Any, str]: + return "str","str" + + def get_value(self, key: str) -> Optional[Any]: + keys = key.split('.') + value = self._config_tree + + try: + for k in keys: + value = value[k] + return value + except (KeyError, TypeError): + return None + + def __call__(self): + return self._config_tree \ No newline at end of file diff --git a/rapyuta_io_sdk_v2/utils.py b/rapyuta_io_sdk_v2/utils.py new file mode 100644 index 0000000..b9f1c68 --- /dev/null +++ b/rapyuta_io_sdk_v2/utils.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Copyright 2024 Rapyuta Robotics +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# from rapyuta_io_sdk_v2.config import Configuration +from base64 import b64decode +from typing import Any,Dict,Optional +import http, httpx, json +from rapyuta_io_sdk_v2.exceptions import HttpNotFoundError, HttpAlreadyExistsError +from benedict import benedict +import yaml + +def validate_auth_token(config: Any) -> Dict: + try: + client = config.sync_client() + user = client.get_authenticated_user() + return user + except Exception as e: + raise + +def combine_metadata(keys: dict) -> dict: + result = {} + + for key, val in keys.items(): + data = val.get("data", None) + if data is not None: + data = b64decode(data).decode("utf-8") + data = yaml.safe_load(data) + metadata = val.get("metadata", None) + + if metadata: + result[key] = { + "value": data, + "metadata": metadata, + } + else: + result[key] = data + + return result + +def unflatten_keys(keys: Optional[dict]) -> benedict: + if keys is None: + return benedict() + + data = combine_metadata(keys) + return benedict(data).unflatten(separator="/") + +def handle_server_errors(response: httpx.Response): + status_code = response.status_code + + if status_code < 400: + return + + err = '' + try: + err = response.json().get('error') + except json.JSONDecodeError: + err = response.text + + # 404 Not Found + if status_code == http.HTTPStatus.NOT_FOUND: + raise HttpNotFoundError(err) + # 409 Conflict + if status_code == http.HTTPStatus.CONFLICT: + raise HttpAlreadyExistsError() + # 500 Internal Server Error + if status_code == http.HTTPStatus.INTERNAL_SERVER_ERROR: + raise Exception('internal server error') + # 501 Not Implemented + if status_code == http.HTTPStatus.NOT_IMPLEMENTED: + raise Exception('not implemented') + # 502 Bad Gateway + if status_code == http.HTTPStatus.BAD_GATEWAY: + raise Exception('bad gateway') + # 503 Service Unavailable + if status_code == http.HTTPStatus.SERVICE_UNAVAILABLE: + raise Exception('service unavailable') + # 504 Gateway Timeout + if status_code == http.HTTPStatus.GATEWAY_TIMEOUT: + raise Exception('gateway timeout') + # 401 UnAuthorize Access + if status_code == http.HTTPStatus.UNAUTHORIZED: + raise Exception('unauthorized permission access') + + # Anything else that is not known + if status_code > 504: + raise Exception('unknown server error') \ No newline at end of file diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 0000000..330b673 --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,62 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: false +# with-sources: false +# generate-hashes: false +# universal: false + +-e file:. +annotated-types==0.7.0 + # via pydantic +anyio==4.4.0 + # via httpx +certifi==2024.8.30 + # via httpcore + # via httpx + # via requests +charset-normalizer==3.3.2 + # via requests +h11==0.14.0 + # via httpcore +httpcore==1.0.5 + # via httpx +httpx==0.27.2 + # via rapyuta-io-sdk-v2 +idna==3.10 + # via anyio + # via httpx + # via requests +pydantic==2.9.2 + # via pydantic-settings +pydantic-core==2.23.4 + # via pydantic +pydantic-settings==2.5.2 + # via rapyuta-io-sdk-v2 +python-benedict==0.33.2 + # via rapyuta-io-sdk-v2 +python-dotenv==1.0.1 + # via pydantic-settings +python-fsutil==0.14.1 + # via python-benedict +python-slugify==8.0.4 + # via python-benedict +pyyaml==6.0.2 + # via rapyuta-io-sdk-v2 +requests==2.32.3 + # via python-benedict +setuptools==75.1.0 + # via rapyuta-io-sdk-v2 +sniffio==1.3.1 + # via anyio + # via httpx +text-unidecode==1.3 + # via python-slugify +typing-extensions==4.12.2 + # via pydantic + # via pydantic-core +urllib3==2.2.3 + # via requests diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 0000000..330b673 --- /dev/null +++ b/requirements.lock @@ -0,0 +1,62 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: false +# with-sources: false +# generate-hashes: false +# universal: false + +-e file:. +annotated-types==0.7.0 + # via pydantic +anyio==4.4.0 + # via httpx +certifi==2024.8.30 + # via httpcore + # via httpx + # via requests +charset-normalizer==3.3.2 + # via requests +h11==0.14.0 + # via httpcore +httpcore==1.0.5 + # via httpx +httpx==0.27.2 + # via rapyuta-io-sdk-v2 +idna==3.10 + # via anyio + # via httpx + # via requests +pydantic==2.9.2 + # via pydantic-settings +pydantic-core==2.23.4 + # via pydantic +pydantic-settings==2.5.2 + # via rapyuta-io-sdk-v2 +python-benedict==0.33.2 + # via rapyuta-io-sdk-v2 +python-dotenv==1.0.1 + # via pydantic-settings +python-fsutil==0.14.1 + # via python-benedict +python-slugify==8.0.4 + # via python-benedict +pyyaml==6.0.2 + # via rapyuta-io-sdk-v2 +requests==2.32.3 + # via python-benedict +setuptools==75.1.0 + # via rapyuta-io-sdk-v2 +sniffio==1.3.1 + # via anyio + # via httpx +text-unidecode==1.3 + # via python-slugify +typing-extensions==4.12.2 + # via pydantic + # via pydantic-core +urllib3==2.2.3 + # via requests diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..75c007e --- /dev/null +++ b/setup.py @@ -0,0 +1,38 @@ +from setuptools import setup, find_packages + +import re +version = re.search( + '^__version__\s*=\s*"(.*)"', open("rapyuta_io_sdk_v2/__init__.py").read(), re.M +).group(1) + +with open("README.md", encoding="utf-8") as f: + long_desc = f.read() + +setup( + name="rapyuta_io_sdk_v2", + version=version, + description="Rapyuta.io Python SDK V2", + long_description=long_desc, + long_description_content_type="text/markdown", + author="Rapyuta Robotics", + author_email="opensource@rapyuta-robotics.com", + packages=find_packages(include=["rapyuta_io_sdk_v2*"]), + python_requires=">=3.8", + license="Apache 2.0", + classifiers=[ + "Programming Language :: Python", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + ], + install_requires=[ + "httpx>=0.27.2", + "pydantic-settings>=2.5.2", + "python-benedict>=0.33.2", + "pyyaml>=6.0.2", + "setuptools>=75.1.0", + ], + extras_require={}, +)