From 9d30359bdecfc882a1c7be482088899019c3c016 Mon Sep 17 00:00:00 2001 From: Yaswanth Kumar Togarapu Date: Tue, 17 Sep 2024 13:03:09 +0530 Subject: [PATCH 01/10] chore(gitignore): ignore temporary files and folders --- .gitignore | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .gitignore 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 From 0f2990c383bc1a0804b361a8c01a81aed52511f9 Mon Sep 17 00:00:00 2001 From: Yaswanth Kumar Togarapu Date: Tue, 17 Sep 2024 13:08:04 +0530 Subject: [PATCH 02/10] chore(setup): initialize project structure and basic configuration --- .python-version | 1 + README.md | 3 +++ pyproject.toml | 21 +++++++++++++++++++++ rapyuta_io_sdk_v2/__init__.py | 0 requirements-dev.lock | 12 ++++++++++++ requirements.lock | 12 ++++++++++++ 6 files changed, 49 insertions(+) create mode 100644 .python-version create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 rapyuta_io_sdk_v2/__init__.py create mode 100644 requirements-dev.lock create mode 100644 requirements.lock 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..410e637 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "rapyuta-io-sdk-v2" +version = "0.1.0" +description = "Version:2 for Rapyuta.io SDK" +dependencies = [] +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..e69de29 diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 0000000..505fd45 --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,12 @@ +# 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:. diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 0000000..505fd45 --- /dev/null +++ b/requirements.lock @@ -0,0 +1,12 @@ +# 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:. From 725a33bd02053c7f95da4062634b3c3fbfa6f6f6 Mon Sep 17 00:00:00 2001 From: Yaswanth Kumar Togarapu Date: Tue, 17 Sep 2024 14:35:31 +0530 Subject: [PATCH 03/10] feat: implement v2 clients for sync and async operations --- pyproject.toml | 4 ++- rapyuta_io_sdk_v2/async_client.py | 51 +++++++++++++++++++++++++++++++ rapyuta_io_sdk_v2/auth.py | 17 +++++++++++ rapyuta_io_sdk_v2/client.py | 38 +++++++++++++++++++++++ rapyuta_io_sdk_v2/exceptions.py | 5 +++ requirements-dev.lock | 17 +++++++++++ requirements.lock | 17 +++++++++++ 7 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 rapyuta_io_sdk_v2/async_client.py create mode 100644 rapyuta_io_sdk_v2/auth.py create mode 100644 rapyuta_io_sdk_v2/client.py create mode 100644 rapyuta_io_sdk_v2/exceptions.py diff --git a/pyproject.toml b/pyproject.toml index 410e637..2821a68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,9 @@ name = "rapyuta-io-sdk-v2" version = "0.1.0" description = "Version:2 for Rapyuta.io SDK" -dependencies = [] +dependencies = [ + "httpx>=0.27.2", +] readme = "README.md" requires-python = ">= 3.8" diff --git a/rapyuta_io_sdk_v2/async_client.py b/rapyuta_io_sdk_v2/async_client.py new file mode 100644 index 0000000..038cf5e --- /dev/null +++ b/rapyuta_io_sdk_v2/async_client.py @@ -0,0 +1,51 @@ +from rapyuta_io_sdk_v2.client import Client +from typing import Optional, override +import httpx + +class AsyncClient(Client): + def __init__(self, auth_token, project, organization: Optional[str] = None): + # super().__init__(auth_token=auth_token,project=project,organization=organization) + + self._host = "https://pr1056api.apps.okd4v2.okd4beta.rapyuta.io" + self._project = project + self._token = "Bearer "+auth_token + self._organization = organization + + async def async_init(self): + if not self._organization: + await self._set_organization(self._project) + + def _get_auth_header(self, with_project: bool = True) -> dict: + headers = dict(Authorization=self._token) + return headers + + @override + async def list_projects(self, organization_guid: str = None): + url = "{}/v2/projects/".format(self._host) + headers = self._get_auth_header(with_project=False) + params = {} + if organization_guid: + params.update({ + "organizations": organization_guid, + }) + async with httpx.AsyncClient() as client: + response = await client.get(url=url, headers=headers, params=params) + response.raise_for_status() + return response.json() + + @override + async def get_project(self, project_guid: str): + url = "{}/v2/projects/{}/".format(self._host, project_guid) + headers = self._get_auth_header() + async with httpx.AsyncClient() as client: + response = await client.get(url=url, headers=headers) + response.raise_for_status() + return response.json() + + async def _set_organization(self,project): + project_info= await self.get_project(project_guid=project) + self._organization = project_info['metadata']['organizationGUID'] + + + + diff --git a/rapyuta_io_sdk_v2/auth.py b/rapyuta_io_sdk_v2/auth.py new file mode 100644 index 0000000..29820b9 --- /dev/null +++ b/rapyuta_io_sdk_v2/auth.py @@ -0,0 +1,17 @@ +import httpx +from typing import Optional +from rapyuta_io_sdk_v2.exceptions import AuthenticationError + +def authenticate(email: str, password: str,environment: str) -> Optional[str]: + url = f"https://{environment}rip.apps.okd4v2.okd4beta.rapyuta.io/user/login" + try: + response = httpx.post(url,json={'email':email,'password':password}) + auth_token = response.json() + if auth_token and auth_token["success"]: + return auth_token["data"]["token"] + raise AuthenticationError() + except AuthenticationError as e: + raise + except Exception as e: + raise + diff --git a/rapyuta_io_sdk_v2/client.py b/rapyuta_io_sdk_v2/client.py new file mode 100644 index 0000000..2eeab06 --- /dev/null +++ b/rapyuta_io_sdk_v2/client.py @@ -0,0 +1,38 @@ +from typing import Optional +import httpx + +class Client(object): + def __init__(self,auth_token,project,organization:Optional[str] = None): + self._host = "https://pr1056api.apps.okd4v2.okd4beta.rapyuta.io" + self._project = project + self._token = "Bearer "+auth_token + if organization is not None: + self._organization = organization + return + self._organization = self.organization + + def _get_auth_header(self, with_project: bool = True) -> dict: + headers = dict(Authorization=self._token) + return headers + + def list_projects(self,organization_guid: str = None): + url = "{}/v2/projects/".format(self._host) + headers = self._get_auth_header(with_project=False) + params = {} + if organization_guid: + params.update({ + "organizations": organization_guid, + }) + response = httpx.get(url=url,headers=headers,params=params).json() + return response + + def get_project(self, project_guid: str): + url = "{}/v2/projects/{}/".format(self._host, project_guid) + headers = self._get_auth_header() + response = httpx.get(url=url,headers=headers).json() + return response + + @property + def organization(self) -> Optional[str]: + _organization = self.get_project(project_guid=self._project)['metadata']['organizationGUID'] + return _organization diff --git a/rapyuta_io_sdk_v2/exceptions.py b/rapyuta_io_sdk_v2/exceptions.py new file mode 100644 index 0000000..ccfcaa7 --- /dev/null +++ b/rapyuta_io_sdk_v2/exceptions.py @@ -0,0 +1,5 @@ +class AuthenticationError(Exception): + """Exception raised for errors in the authentication process.""" + def __init__(self, message="Authentication failed"): + self.message = message + super().__init__(self.message) diff --git a/requirements-dev.lock b/requirements-dev.lock index 505fd45..cee3f97 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -10,3 +10,20 @@ # universal: false -e file:. +anyio==4.4.0 + # via httpx +certifi==2024.8.30 + # via httpcore + # via httpx +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 +sniffio==1.3.1 + # via anyio + # via httpx diff --git a/requirements.lock b/requirements.lock index 505fd45..cee3f97 100644 --- a/requirements.lock +++ b/requirements.lock @@ -10,3 +10,20 @@ # universal: false -e file:. +anyio==4.4.0 + # via httpx +certifi==2024.8.30 + # via httpcore + # via httpx +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 +sniffio==1.3.1 + # via anyio + # via httpx From 6fc270690412bcf8d7eb361e968f61439727fb0b Mon Sep 17 00:00:00 2001 From: Yaswanth Kumar Togarapu Date: Tue, 17 Sep 2024 15:50:15 +0530 Subject: [PATCH 04/10] chore(workflow): add GitHub Actions workflows directory --- .github/CODEOWNERS | 1 + .github/workflows/conventional-commits.yml | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 .github/CODEOWNERS create mode 100644 .github/workflows/conventional-commits.yml 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 From 70a6a75b62d34ee4abd5c05eaf8c550762a97ca2 Mon Sep 17 00:00:00 2001 From: Yaswanth Kumar Togarapu Date: Fri, 20 Sep 2024 17:51:43 +0530 Subject: [PATCH 05/10] feat(clients): updates sync and async clients --- .idea/.gitignore | 3 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 ++ .idea/modules.xml | 8 ++ .idea/rapyuta-io-sdk-v2.iml | 10 ++ .idea/vcs.xml | 6 + pydantic_configs/__init__.py | 0 rapyuta_io_sdk_v2/__init__.py | 1 + rapyuta_io_sdk_v2/async_client.py | 100 ++++++++++++---- rapyuta_io_sdk_v2/auth.py | 17 --- rapyuta_io_sdk_v2/client.py | 108 ++++++++++++++---- rapyuta_io_sdk_v2/config.py | 102 +++++++++++++++++ rapyuta_io_sdk_v2/constants.py | 22 ++++ rapyuta_io_sdk_v2/exceptions.py | 54 +++++++++ rapyuta_io_sdk_v2/test.py | 8 ++ rapyuta_io_sdk_v2/utils.py | 29 +++++ 16 files changed, 422 insertions(+), 59 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/rapyuta-io-sdk-v2.iml create mode 100644 .idea/vcs.xml create mode 100644 pydantic_configs/__init__.py delete mode 100644 rapyuta_io_sdk_v2/auth.py create mode 100644 rapyuta_io_sdk_v2/config.py create mode 100644 rapyuta_io_sdk_v2/constants.py create mode 100644 rapyuta_io_sdk_v2/test.py create mode 100644 rapyuta_io_sdk_v2/utils.py 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/pydantic_configs/__init__.py b/pydantic_configs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rapyuta_io_sdk_v2/__init__.py b/rapyuta_io_sdk_v2/__init__.py index e69de29..c862fdc 100644 --- a/rapyuta_io_sdk_v2/__init__.py +++ b/rapyuta_io_sdk_v2/__init__.py @@ -0,0 +1 @@ +from rapyuta_io_sdk_v2.config import Configuration \ No newline at end of file diff --git a/rapyuta_io_sdk_v2/async_client.py b/rapyuta_io_sdk_v2/async_client.py index 038cf5e..d9354b9 100644 --- a/rapyuta_io_sdk_v2/async_client.py +++ b/rapyuta_io_sdk_v2/async_client.py @@ -1,28 +1,42 @@ +# -*- 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 +from typing import Optional, override, Any, AsyncGenerator, List, Dict import httpx -class AsyncClient(Client): - def __init__(self, auth_token, project, organization: Optional[str] = None): - # super().__init__(auth_token=auth_token,project=project,organization=organization) +# from rapyuta_io_sdk_v2.config import Configuration - self._host = "https://pr1056api.apps.okd4v2.okd4beta.rapyuta.io" - self._project = project - self._token = "Bearer "+auth_token - self._organization = organization - async def async_init(self): - if not self._organization: - await self._set_organization(self._project) +class AsyncClient(Client): + + def __init__(self,config: Any): + super().__init__(config) - def _get_auth_header(self, with_project: bool = True) -> dict: - headers = dict(Authorization=self._token) - return headers + @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._host) - headers = self._get_auth_header(with_project=False) + url = "{}/v2/projects/".format(self.v2api_host) + headers = self._get_headers(with_project=False) params = {} if organization_guid: params.update({ @@ -35,16 +49,58 @@ async def list_projects(self, organization_guid: str = None): @override async def get_project(self, project_guid: str): - url = "{}/v2/projects/{}/".format(self._host, project_guid) - headers = self._get_auth_header() - async with httpx.AsyncClient() as client: + url = "{}/v2/projects/{}/".format(self.v2api_host, project_guid) + headers = self._get_headers() + + async with self._get_client() as client: response = await client.get(url=url, headers=headers) response.raise_for_status() return response.json() - async def _set_organization(self,project): - project_info= await self.get_project(project_guid=project) - self._organization = project_info['metadata']['organizationGUID'] + @override + async def list_config_trees(self) -> List[str]: + url = "{}/v2/configtrees/".format(self.v2api_host) + try: + async with self._get_client() as client: + 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 [] + + 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) + try: + params: Dict[str, Any] = { + 'includeData': include_data, + 'contentTypes': filter_content_types, + 'keyPrefixes': filter_prefixes, + 'revision': rev_id, + } + + async with self._get_client() as client: + 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: {res.text}") from e + + raw_config_tree = res.json().get("keys", {}) + return raw_config_tree + + diff --git a/rapyuta_io_sdk_v2/auth.py b/rapyuta_io_sdk_v2/auth.py deleted file mode 100644 index 29820b9..0000000 --- a/rapyuta_io_sdk_v2/auth.py +++ /dev/null @@ -1,17 +0,0 @@ -import httpx -from typing import Optional -from rapyuta_io_sdk_v2.exceptions import AuthenticationError - -def authenticate(email: str, password: str,environment: str) -> Optional[str]: - url = f"https://{environment}rip.apps.okd4v2.okd4beta.rapyuta.io/user/login" - try: - response = httpx.post(url,json={'email':email,'password':password}) - auth_token = response.json() - if auth_token and auth_token["success"]: - return auth_token["data"]["token"] - raise AuthenticationError() - except AuthenticationError as e: - raise - except Exception as e: - raise - diff --git a/rapyuta_io_sdk_v2/client.py b/rapyuta_io_sdk_v2/client.py index 2eeab06..358e5b2 100644 --- a/rapyuta_io_sdk_v2/client.py +++ b/rapyuta_io_sdk_v2/client.py @@ -1,23 +1,56 @@ -from typing import Optional +# -*- 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, Any, Dict + import httpx +# from rapyuta_io_sdk_v2.config import Configuration +from rapyuta_io_sdk_v2.constants import GET_USER_PATH + class Client(object): - def __init__(self,auth_token,project,organization:Optional[str] = None): - self._host = "https://pr1056api.apps.okd4v2.okd4beta.rapyuta.io" - self._project = project - self._token = "Bearer "+auth_token - if organization is not None: - self._organization = organization - return - self._organization = self.organization - - def _get_auth_header(self, with_project: bool = True) -> dict: - headers = dict(Authorization=self._token) + def __init__(self,config: Any): + self.config = config + self.v2api_host = config.hosts.get("v2api_host") + + 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 + 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).json() + return response + except Exception as e: + raise + def list_projects(self,organization_guid: str = None): - url = "{}/v2/projects/".format(self._host) - headers = self._get_auth_header(with_project=False) + """ + + :param organization_guid: + :return: + """ + url = "{}/v2/projects/".format(self.v2api_host) + headers = self._get_headers(with_project=False) params = {} if organization_guid: params.update({ @@ -27,12 +60,47 @@ def list_projects(self,organization_guid: str = None): return response def get_project(self, project_guid: str): - url = "{}/v2/projects/{}/".format(self._host, project_guid) - headers = self._get_auth_header() + """ + + :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).json() return response - @property - def organization(self) -> Optional[str]: - _organization = self.get_project(project_guid=self._project)['metadata']['organizationGUID'] - return _organization + 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).json() + return response + + 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 + + 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) + return response + + def list_config_trees(self): + url = "{}/v2/configtrees/".format(self.v2api_host) + headers = self._get_headers() + response = httpx.get(url=url,headers=headers) + return response + diff --git a/rapyuta_io_sdk_v2/config.py b/rapyuta_io_sdk_v2/config.py new file mode 100644 index 0000000..a8efe9e --- /dev/null +++ b/rapyuta_io_sdk_v2/config.py @@ -0,0 +1,102 @@ +# -*- 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.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, UnauthorizedError +import httpx + +from rapyuta_io_sdk_v2.utils import validate_auth_token + + +class Configuration(object): + + def __init__(self, email: str, password: str, project_guid: str, organization_guid: str,auth_token: str=None, environment: 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._configure_environment(environment) + + def login(self): + _rip_host = self.hosts.get("rip_host") + + if self.auth_token is not None: + is_valid: bool = validate_auth_token(self) + if is_valid: + return + try: + 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'] + return + raise AuthenticationError() + except AuthenticationError as e: + raise + except Exception as e: + raise + + def logout(self): + self.email=None + self.auth_token=None + self.project_guid=None + self.organization_guid=None + self.environment=None + + def set_project(self, project): + self.project_guid = project + + def set_organization(self,organization_guid): + self.organization_guid = organization_guid + + def sync_client(self): + if self.auth_token is None: + raise LoggedOutError("You are not logged in. Run config.login() to login.") + + # validate the auth_token + return Client(self) + + def async_client(self): + if self.auth_token is None: + raise LoggedOutError("You are not logged in. Run config.login() to login.") + + # validate the auth_token + return AsyncClient(self) + + def _configure_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" + + catalog = 'https://{}catalog.{}'.format(name, subdomain) + core = 'https://{}apiserver.{}'.format(name, subdomain) + rip = 'https://{}rip.{}'.format(name, subdomain) + v2api = 'https://{}api.{}'.format(name, subdomain) + + self.hosts['environment'] = name + self.hosts['catalog_host'] = catalog + 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 index ccfcaa7..961d824 100644 --- a/rapyuta_io_sdk_v2/exceptions.py +++ b/rapyuta_io_sdk_v2/exceptions.py @@ -1,5 +1,59 @@ +# -*- 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 ValidationError(Exception): + + def __init__(self, message="Validation 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 UnAuthorizedError(Exception): + + def __init__(self,message="UnAuthorized to access"): + self.message = message + super().__init__(self.message) + +class InvalidAuthTokenException(Exception): + def __init__(self, msg=None): + Exception.__init__(self, msg) + +class BadRequestError(Exception): + def __init__(self, msg=None): + Exception.__init__(self, msg) + +class UnauthorizedError(Exception): + def __init__(self, msg=None): + Exception.__init__(self, msg) + + +class ForbiddenError(Exception): + def __init__(self, msg=None): + Exception.__init__(self, msg) + diff --git a/rapyuta_io_sdk_v2/test.py b/rapyuta_io_sdk_v2/test.py new file mode 100644 index 0000000..45285d8 --- /dev/null +++ b/rapyuta_io_sdk_v2/test.py @@ -0,0 +1,8 @@ +from rapyuta_io_sdk_v2 import Configuration + +config = Configuration( + "qa.rapyuta+e2e@gmail.com", + "Rapyuta01!","project-cp9k4kn2bars73bgs9s0", + "org-wvnwcmvfkbajavjetttcutga", + auth_token="t8k1JF4dWDm8UAK3dovI3hfmKz7raPzDrUzDB3wz",environment="pr1056") +config.login() \ 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..f6a3435 --- /dev/null +++ b/rapyuta_io_sdk_v2/utils.py @@ -0,0 +1,29 @@ +# -*- 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 typing import Any + +from rapyuta_io_sdk_v2.exceptions import InvalidAuthTokenException + + +def validate_auth_token(config: Any) -> bool: + try: + client = config.sync_client() + user = client.get_authenticated_user() + if 'error' in user: + raise InvalidAuthTokenException("Invalid token, please login again") + return True + except InvalidAuthTokenException as e: + raise \ No newline at end of file From 5fcda9fedd4abc6e0df77731a9dc3e530b6098a3 Mon Sep 17 00:00:00 2001 From: Yaswanth Kumar Togarapu Date: Mon, 23 Sep 2024 17:36:57 +0530 Subject: [PATCH 06/10] feat(v2SDK): updates v2 sdk clients --- rapyuta_io_sdk_v2/async_client.py | 35 +++++++++++++------ rapyuta_io_sdk_v2/client.py | 50 ++++++++++++++++++-------- rapyuta_io_sdk_v2/config.py | 43 +++++++++++------------ rapyuta_io_sdk_v2/exceptions.py | 33 ++++++------------ rapyuta_io_sdk_v2/test.py | 8 ----- rapyuta_io_sdk_v2/utils.py | 58 +++++++++++++++++++++++++------ 6 files changed, 138 insertions(+), 89 deletions(-) delete mode 100644 rapyuta_io_sdk_v2/test.py diff --git a/rapyuta_io_sdk_v2/async_client.py b/rapyuta_io_sdk_v2/async_client.py index d9354b9..62622fe 100644 --- a/rapyuta_io_sdk_v2/async_client.py +++ b/rapyuta_io_sdk_v2/async_client.py @@ -18,12 +18,9 @@ from typing import Optional, override, Any, AsyncGenerator, List, Dict import httpx -# from rapyuta_io_sdk_v2.config import Configuration - - class AsyncClient(Client): - def __init__(self,config: Any): + def __init__(self,config): super().__init__(config) @asynccontextmanager @@ -36,24 +33,22 @@ async def _get_client(self) -> AsyncGenerator[httpx.AsyncClient,None]: @override async def list_projects(self, organization_guid: str = None): url = "{}/v2/projects/".format(self.v2api_host) - headers = self._get_headers(with_project=False) params = {} if organization_guid: params.update({ "organizations": organization_guid, }) - async with httpx.AsyncClient() as client: - response = await client.get(url=url, headers=headers, params=params) + 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) - headers = self._get_headers() async with self._get_client() as client: - response = await client.get(url=url, headers=headers) + response = await client.get(url=url) response.raise_for_status() return response.json() @@ -73,6 +68,7 @@ async def list_config_trees(self) -> List[str]: else: return [] + @override async def get_config_tree( self, tree_name: str, rev_id: Optional[str] = None, @@ -95,9 +91,25 @@ async def get_config_tree( ) res.raise_for_status() except Exception as e: - raise ValueError(f"Failed to get config tree data: {res.text}") from e + raise ValueError(f"Failed to get config tree data: {res.text}") + + raw_config_tree = res.json() + return raw_config_tree + + @override + async def create_config_tree(self,tree_spec: dict): + url = "{}/v2/configtrees/".format(self.v2api_host) + try: + async with self._get_client() as client: + 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().get("keys", {}) + raw_config_tree = res.json() return raw_config_tree @@ -105,3 +117,4 @@ async def get_config_tree( + diff --git a/rapyuta_io_sdk_v2/client.py b/rapyuta_io_sdk_v2/client.py index 358e5b2..16d7d5c 100644 --- a/rapyuta_io_sdk_v2/client.py +++ b/rapyuta_io_sdk_v2/client.py @@ -14,15 +14,16 @@ # limitations under the License. from typing import Optional, List, Any, Dict -import httpx +import httpx, json, http -# from rapyuta_io_sdk_v2.config import Configuration +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: Any): self.config = config - self.v2api_host = config.hosts.get("v2api_host") + self.v2api_host = config.hosts.get("v2api_host",self.PROD_V2API_URL) def _get_headers(self, with_project: bool = True) -> dict: headers = { @@ -38,8 +39,9 @@ def get_authenticated_user(self) -> Optional[Dict]: _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).json() - return response + response = httpx.get(url=url, headers=headers) + handle_server_errors(response) + return response.json() except Exception as e: raise @@ -56,8 +58,9 @@ def list_projects(self,organization_guid: str = None): params.update({ "organizations": organization_guid, }) - response = httpx.get(url=url,headers=headers,params=params).json() - return response + response = httpx.get(url=url,headers=headers,params=params) + handle_server_errors(response) + return response.json() def get_project(self, project_guid: str): """ @@ -67,8 +70,9 @@ def get_project(self, project_guid: str): """ url = "{}/v2/projects/{}/".format(self.v2api_host, project_guid) headers = self._get_headers(with_project=False) - response = httpx.get(url=url,headers=headers).json() - return response + 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, @@ -82,25 +86,41 @@ def get_config_tree(self,tree_name: str, rev_id: Optional[str]=None, 'revision': rev_id, } headers = self._get_headers() - response = httpx.get(url=url,headers=headers,params=query).json() - return response + 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 + 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) - return response + 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) - return response + 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 index a8efe9e..175b76a 100644 --- a/rapyuta_io_sdk_v2/config.py +++ b/rapyuta_io_sdk_v2/config.py @@ -12,10 +12,12 @@ # 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, UnauthorizedError +from rapyuta_io_sdk_v2.exceptions import ValidationError, AuthenticationError, LoggedOutError import httpx from rapyuta_io_sdk_v2.utils import validate_auth_token @@ -23,63 +25,60 @@ class Configuration(object): - def __init__(self, email: str, password: str, project_guid: str, organization_guid: str,auth_token: str=None, environment: str=None): + 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._password = password self.auth_token = auth_token self.project_guid = project_guid self.organization_guid = organization_guid self.environment = environment self.hosts = {} - self._configure_environment(environment) + self.set_environment(environment) - def login(self): + def login(self) -> None: _rip_host = self.hosts.get("rip_host") - if self.auth_token is not None: - is_valid: bool = validate_auth_token(self) - if is_valid: - return 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() + response = httpx.post(url, json={'email': self.email, 'password': self._password}).json() if response['success']: self.auth_token = response['data']['token'] - return - raise AuthenticationError() + else: + raise AuthenticationError() except AuthenticationError as e: raise except Exception as e: raise - def logout(self): + def logout(self) -> None: self.email=None self.auth_token=None self.project_guid=None self.organization_guid=None self.environment=None - def set_project(self, project): + def set_project(self, project) -> None: self.project_guid = project - def set_organization(self,organization_guid): + def set_organization(self,organization_guid) -> None: self.organization_guid = organization_guid - def sync_client(self): + def sync_client(self) -> Optional[Client]: if self.auth_token is None: raise LoggedOutError("You are not logged in. Run config.login() to login.") - - # validate the auth_token return Client(self) - def async_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.") - - # validate the auth_token return AsyncClient(self) - def _configure_environment(self, name: str) -> None: + def set_environment(self, name: str) -> None: subdomain = PROD_ENVIRONMENT_SUBDOMAIN if name is not None: diff --git a/rapyuta_io_sdk_v2/exceptions.py b/rapyuta_io_sdk_v2/exceptions.py index 961d824..c47ca7a 100644 --- a/rapyuta_io_sdk_v2/exceptions.py +++ b/rapyuta_io_sdk_v2/exceptions.py @@ -21,39 +21,26 @@ def __init__(self, message="Authentication failed"): self.message = message super().__init__(self.message) - -class ValidationError(Exception): - - def __init__(self, message="Validation 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 UnAuthorizedError(Exception): - - def __init__(self,message="UnAuthorized to access"): +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 InvalidAuthTokenException(Exception): - def __init__(self, msg=None): - Exception.__init__(self, msg) - -class BadRequestError(Exception): - def __init__(self, msg=None): - Exception.__init__(self, msg) +class ValidationError(Exception): + def __init__(self, message=None): + self.message = message + super().__init__(self.message) -class UnauthorizedError(Exception): - def __init__(self, msg=None): - Exception.__init__(self, msg) -class ForbiddenError(Exception): - def __init__(self, msg=None): - Exception.__init__(self, msg) diff --git a/rapyuta_io_sdk_v2/test.py b/rapyuta_io_sdk_v2/test.py deleted file mode 100644 index 45285d8..0000000 --- a/rapyuta_io_sdk_v2/test.py +++ /dev/null @@ -1,8 +0,0 @@ -from rapyuta_io_sdk_v2 import Configuration - -config = Configuration( - "qa.rapyuta+e2e@gmail.com", - "Rapyuta01!","project-cp9k4kn2bars73bgs9s0", - "org-wvnwcmvfkbajavjetttcutga", - auth_token="t8k1JF4dWDm8UAK3dovI3hfmKz7raPzDrUzDB3wz",environment="pr1056") -config.login() \ No newline at end of file diff --git a/rapyuta_io_sdk_v2/utils.py b/rapyuta_io_sdk_v2/utils.py index f6a3435..7826874 100644 --- a/rapyuta_io_sdk_v2/utils.py +++ b/rapyuta_io_sdk_v2/utils.py @@ -13,17 +13,55 @@ # See the License for the specific language governing permissions and # limitations under the License. # from rapyuta_io_sdk_v2.config import Configuration -from typing import Any +from typing import Any,Dict +import http, httpx, json +from rapyuta_io_sdk_v2.exceptions import HttpNotFoundError, HttpAlreadyExistsError -from rapyuta_io_sdk_v2.exceptions import InvalidAuthTokenException - - -def validate_auth_token(config: Any) -> bool: +def validate_auth_token(config: Any) -> Dict: try: client = config.sync_client() user = client.get_authenticated_user() - if 'error' in user: - raise InvalidAuthTokenException("Invalid token, please login again") - return True - except InvalidAuthTokenException as e: - raise \ No newline at end of file + return user + except Exception as e: + raise + +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 From cd8254947c0f5082605a212f4f34d97402356611 Mon Sep 17 00:00:00 2001 From: Yaswanth Kumar Togarapu Date: Mon, 23 Sep 2024 17:40:15 +0530 Subject: [PATCH 07/10] feat(pydantic-settings): initializes pydantic settings for config-trees --- pydantic_configs/app_config_settings.py | 96 +++++++++++++++++++++++++ pydantic_configs/app_config_source.py | 63 ++++++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 pydantic_configs/app_config_settings.py create mode 100644 pydantic_configs/app_config_source.py diff --git a/pydantic_configs/app_config_settings.py b/pydantic_configs/app_config_settings.py new file mode 100644 index 0000000..ef1e1e0 --- /dev/null +++ b/pydantic_configs/app_config_settings.py @@ -0,0 +1,96 @@ +# -*- 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 + +from pydantic_settings import ( + BaseSettings, + InitSettingsSource, + PydanticBaseSettingsSource, + SettingsConfigDict, +) + +from pydantic_configs.app_config_source import RRConfigSettingsSource +from rapyuta_io_sdk_v2 import Configuration + + +class RIOAuthCredentials(BaseSettings): + rio_auth_token: str = "" + rio_organisation_guid: str = "" + rio_project_id: 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_organisation_guid: str = "" + rio_project_id: str = "" + + @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() + + rio_auth_token = init_settings.init_kwargs.get( + "rio_auth_token", rio_auth_creds.rio_auth_token + ) + rio_organisation_guid = init_settings.init_kwargs.get( + "rio_organisation_guid", rio_auth_creds.rio_organisation_guid + ) + rio_project_id = init_settings.init_kwargs.get( + "rio_project_id", rio_auth_creds.rio_project_id + ) + config_tree_name = init_settings.init_kwargs.get( + "config_tree_name", rio_config_tree.config_tree_name + ) + config_tree_revision = init_settings.init_kwargs.get( + "config_tree_version", rio_config_tree.config_tree_revision + ) + + _client_config = Configuration( + auth_token=rio_auth_token, + project_guid=rio_project_id, + organization_guid=rio_organisation_guid + ) + + rr_config_settings_source = RRConfigSettingsSource( + settings_cls, + client_config=_client_config, + config_tree_name=config_tree_name, + config_tree_revision=config_tree_revision, + ) + + return ( + init_settings, + env_settings, + dotenv_settings, + file_secret_settings, + rr_config_settings_source, + ) \ No newline at end of file diff --git a/pydantic_configs/app_config_source.py b/pydantic_configs/app_config_source.py new file mode 100644 index 0000000..9a2788c --- /dev/null +++ b/pydantic_configs/app_config_source.py @@ -0,0 +1,63 @@ +# -*- 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() + ) # set self._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, + ) + self._config_tree = res + + def get_field_value( + self, + field: FieldInfo, + field_name: str, + ) -> Tuple[Any, str, bool]: + return "dummy_val", "dummy_val", False + + def __call__(self): + return self._config_tree \ No newline at end of file From 52a5a05728d4da7216b12f1c5803211a0e29aeb4 Mon Sep 17 00:00:00 2001 From: Yaswanth Kumar Togarapu Date: Mon, 23 Sep 2024 17:41:56 +0530 Subject: [PATCH 08/10] chore(project-settings): updates project dependencies --- pyproject.toml | 2 ++ requirements-dev.lock | 29 +++++++++++++++++++++++++++++ requirements.lock | 29 +++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 2821a68..24e8c98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,8 @@ 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", ] readme = "README.md" requires-python = ">= 3.8" diff --git a/requirements-dev.lock b/requirements-dev.lock index cee3f97..d8b0624 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -10,11 +10,16 @@ # 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 @@ -24,6 +29,30 @@ httpx==0.27.2 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 +requests==2.32.3 + # via python-benedict 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 index cee3f97..d8b0624 100644 --- a/requirements.lock +++ b/requirements.lock @@ -10,11 +10,16 @@ # 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 @@ -24,6 +29,30 @@ httpx==0.27.2 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 +requests==2.32.3 + # via python-benedict 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 From 7bf568fba20d97f66b1a708cc18fb56f93a37e23 Mon Sep 17 00:00:00 2001 From: Yaswanth Kumar Togarapu Date: Tue, 24 Sep 2024 20:24:49 +0530 Subject: [PATCH 09/10] feat(pydantic-settings): Improves pydantic settings for configs --- pydantic_configs/app_config_settings.py | 30 +++++++++----- pydantic_configs/app_config_source.py | 16 +++++++- pyproject.toml | 1 + rapyuta_io_sdk_v2/async_client.py | 54 ++++++++++++------------- rapyuta_io_sdk_v2/client.py | 16 ++++++-- rapyuta_io_sdk_v2/config.py | 16 +++----- rapyuta_io_sdk_v2/utils.py | 32 ++++++++++++++- requirements-dev.lock | 2 + requirements.lock | 2 + 9 files changed, 114 insertions(+), 55 deletions(-) diff --git a/pydantic_configs/app_config_settings.py b/pydantic_configs/app_config_settings.py index ef1e1e0..4656d0d 100644 --- a/pydantic_configs/app_config_settings.py +++ b/pydantic_configs/app_config_settings.py @@ -12,7 +12,7 @@ # 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 +from typing import Optional, Tuple, Type,Any from pydantic_settings import ( BaseSettings, @@ -27,8 +27,9 @@ class RIOAuthCredentials(BaseSettings): rio_auth_token: str = "" - rio_organisation_guid: str = "" + rio_organization_id: str = "" rio_project_id: str = "" + rio_environment:str = "" class RIOConfigTree(BaseSettings): @@ -37,13 +38,13 @@ class RIOConfigTree(BaseSettings): class RRSettings(BaseSettings): - model_config = SettingsConfigDict(extra="allow") - + model_config = SettingsConfigDict(extra="allow",env_prefix="RIO_") rio_auth_token: str = "" config_tree_name: str config_tree_version: Optional[str] = None - rio_organisation_guid: str = "" + rio_organization_id: str = "" rio_project_id: str = "" + __config_source__ = None @classmethod def settings_customise_sources( @@ -61,12 +62,16 @@ def settings_customise_sources( rio_auth_token = init_settings.init_kwargs.get( "rio_auth_token", rio_auth_creds.rio_auth_token ) - rio_organisation_guid = init_settings.init_kwargs.get( - "rio_organisation_guid", rio_auth_creds.rio_organisation_guid + rio_organization_id = init_settings.init_kwargs.get( + "rio_organization_id", rio_auth_creds.rio_organization_id ) rio_project_id = init_settings.init_kwargs.get( "rio_project_id", rio_auth_creds.rio_project_id ) + + rio_environment = init_settings.init_kwargs.get( + "rio_environment", rio_auth_creds.rio_environment + ) config_tree_name = init_settings.init_kwargs.get( "config_tree_name", rio_config_tree.config_tree_name ) @@ -77,7 +82,8 @@ def settings_customise_sources( _client_config = Configuration( auth_token=rio_auth_token, project_guid=rio_project_id, - organization_guid=rio_organisation_guid + organization_guid=rio_organization_id, + environment=rio_environment ) rr_config_settings_source = RRConfigSettingsSource( @@ -86,6 +92,7 @@ def settings_customise_sources( config_tree_name=config_tree_name, config_tree_revision=config_tree_revision, ) + cls.__config_source__ = rr_config_settings_source return ( init_settings, @@ -93,4 +100,9 @@ def settings_customise_sources( dotenv_settings, file_secret_settings, rr_config_settings_source, - ) \ No newline at end of file + ) + + 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/pydantic_configs/app_config_source.py b/pydantic_configs/app_config_source.py index 9a2788c..9f715d5 100644 --- a/pydantic_configs/app_config_source.py +++ b/pydantic_configs/app_config_source.py @@ -49,6 +49,7 @@ 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 @@ -56,8 +57,19 @@ def get_field_value( self, field: FieldInfo, field_name: str, - ) -> Tuple[Any, str, bool]: - return "dummy_val", "dummy_val", False + ) -> 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/pyproject.toml b/pyproject.toml index 24e8c98..5ac6466 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ dependencies = [ "httpx>=0.27.2", "pydantic-settings>=2.5.2", "python-benedict>=0.33.2", + "pyyaml>=6.0.2", ] readme = "README.md" requires-python = ">= 3.8" diff --git a/rapyuta_io_sdk_v2/async_client.py b/rapyuta_io_sdk_v2/async_client.py index 62622fe..9796dd0 100644 --- a/rapyuta_io_sdk_v2/async_client.py +++ b/rapyuta_io_sdk_v2/async_client.py @@ -55,18 +55,18 @@ async def get_project(self, project_guid: str): @override async def list_config_trees(self) -> List[str]: url = "{}/v2/configtrees/".format(self.v2api_host) - try: - async with self._get_client() as client: + 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 + 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 [] + if tree_list := res.json().get("items"): + return [item["metadata"]["name"] for item in tree_list] + else: + return [] @override async def get_config_tree( @@ -76,41 +76,39 @@ async def get_config_tree( filter_prefixes: Optional[List[str]] = None ) : url = "{}/v2/configtrees/{}/".format(self.v2api_host, tree_name) - try: - params: Dict[str, Any] = { - 'includeData': include_data, - 'contentTypes': filter_content_types, - 'keyPrefixes': filter_prefixes, - 'revision': rev_id, - } - - async with self._get_client() as client: + 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: {res.text}") + except Exception as e: + raise ValueError(f"Failed to get config tree data") - raw_config_tree = res.json() - return raw_config_tree + 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) - try: - async with self._get_client() as client: + 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 + 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 index 16d7d5c..5b2dc3b 100644 --- a/rapyuta_io_sdk_v2/client.py +++ b/rapyuta_io_sdk_v2/client.py @@ -12,16 +12,17 @@ # 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, Any, Dict - -import httpx, json, http +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: Any): + def __init__(self,config): self.config = config self.v2api_host = config.hosts.get("v2api_host",self.PROD_V2API_URL) @@ -34,6 +35,13 @@ def _get_headers(self, with_project: bool = True) -> dict: } 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") diff --git a/rapyuta_io_sdk_v2/config.py b/rapyuta_io_sdk_v2/config.py index 175b76a..f1cc234 100644 --- a/rapyuta_io_sdk_v2/config.py +++ b/rapyuta_io_sdk_v2/config.py @@ -35,9 +35,11 @@ def __init__(self, project_guid: str, organization_guid: str, password: str=None self.hosts = {} self.set_environment(environment) - def login(self) -> None: - _rip_host = self.hosts.get("rip_host") + # 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) @@ -55,17 +57,11 @@ def login(self) -> None: except Exception as e: raise - def logout(self) -> None: - self.email=None - self.auth_token=None - self.project_guid=None - self.organization_guid=None - self.environment=None - 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]: @@ -89,13 +85,11 @@ def set_environment(self, name: str) -> None: else: name = "ga" - catalog = 'https://{}catalog.{}'.format(name, subdomain) core = 'https://{}apiserver.{}'.format(name, subdomain) rip = 'https://{}rip.{}'.format(name, subdomain) v2api = 'https://{}api.{}'.format(name, subdomain) self.hosts['environment'] = name - self.hosts['catalog_host'] = catalog self.hosts['core_api_host'] = core self.hosts['rip_host'] = rip self.hosts['v2api_host'] = v2api diff --git a/rapyuta_io_sdk_v2/utils.py b/rapyuta_io_sdk_v2/utils.py index 7826874..b9f1c68 100644 --- a/rapyuta_io_sdk_v2/utils.py +++ b/rapyuta_io_sdk_v2/utils.py @@ -13,9 +13,12 @@ # See the License for the specific language governing permissions and # limitations under the License. # from rapyuta_io_sdk_v2.config import Configuration -from typing import Any,Dict +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: @@ -25,6 +28,33 @@ def validate_auth_token(config: Any) -> Dict: 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 diff --git a/requirements-dev.lock b/requirements-dev.lock index d8b0624..2226e6b 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -44,6 +44,8 @@ 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 sniffio==1.3.1 diff --git a/requirements.lock b/requirements.lock index d8b0624..2226e6b 100644 --- a/requirements.lock +++ b/requirements.lock @@ -44,6 +44,8 @@ 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 sniffio==1.3.1 From 4158609190603f41608d55ebce0724cda9d6feb2 Mon Sep 17 00:00:00 2001 From: Yaswanth Kumar Togarapu Date: Wed, 25 Sep 2024 13:49:29 +0530 Subject: [PATCH 10/10] feat: improves clients and pydantic-settings package --- pydantic_configs/__init__.py | 0 pyproject.toml | 1 + rapyuta_io_sdk_v2/__init__.py | 4 +- rapyuta_io_sdk_v2/client.py | 2 +- .../pydantic_configs/__init__.py | 1 + .../pydantic_configs}/app_config_settings.py | 28 +++++++------- .../pydantic_configs}/app_config_source.py | 2 +- requirements-dev.lock | 2 + requirements.lock | 2 + setup.py | 38 +++++++++++++++++++ 10 files changed, 63 insertions(+), 17 deletions(-) delete mode 100644 pydantic_configs/__init__.py create mode 100644 rapyuta_io_sdk_v2/pydantic_configs/__init__.py rename {pydantic_configs => rapyuta_io_sdk_v2/pydantic_configs}/app_config_settings.py (78%) rename {pydantic_configs => rapyuta_io_sdk_v2/pydantic_configs}/app_config_source.py (98%) create mode 100644 setup.py diff --git a/pydantic_configs/__init__.py b/pydantic_configs/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pyproject.toml b/pyproject.toml index 5ac6466..2ce4006 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ dependencies = [ "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" diff --git a/rapyuta_io_sdk_v2/__init__.py b/rapyuta_io_sdk_v2/__init__.py index c862fdc..c12e8ca 100644 --- a/rapyuta_io_sdk_v2/__init__.py +++ b/rapyuta_io_sdk_v2/__init__.py @@ -1 +1,3 @@ -from rapyuta_io_sdk_v2.config import Configuration \ No newline at end of file +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/client.py b/rapyuta_io_sdk_v2/client.py index 5b2dc3b..8028ac5 100644 --- a/rapyuta_io_sdk_v2/client.py +++ b/rapyuta_io_sdk_v2/client.py @@ -17,7 +17,7 @@ from rapyuta_io_sdk_v2.utils import unflatten_keys import httpx, json -from utils import handle_server_errors +from .utils import handle_server_errors from rapyuta_io_sdk_v2.constants import GET_USER_PATH class Client(object): 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/pydantic_configs/app_config_settings.py b/rapyuta_io_sdk_v2/pydantic_configs/app_config_settings.py similarity index 78% rename from pydantic_configs/app_config_settings.py rename to rapyuta_io_sdk_v2/pydantic_configs/app_config_settings.py index 4656d0d..4cfeed2 100644 --- a/pydantic_configs/app_config_settings.py +++ b/rapyuta_io_sdk_v2/pydantic_configs/app_config_settings.py @@ -21,7 +21,7 @@ SettingsConfigDict, ) -from pydantic_configs.app_config_source import RRConfigSettingsSource +from rapyuta_io_sdk_v2.pydantic_configs.app_config_source import RRConfigSettingsSource from rapyuta_io_sdk_v2 import Configuration @@ -38,7 +38,7 @@ class RIOConfigTree(BaseSettings): class RRSettings(BaseSettings): - model_config = SettingsConfigDict(extra="allow",env_prefix="RIO_") + model_config = SettingsConfigDict(extra="allow") rio_auth_token: str = "" config_tree_name: str config_tree_version: Optional[str] = None @@ -59,38 +59,38 @@ def settings_customise_sources( rio_auth_creds = RIOAuthCredentials() rio_config_tree = RIOConfigTree() - rio_auth_token = init_settings.init_kwargs.get( + cls.rio_auth_token = init_settings.init_kwargs.get( "rio_auth_token", rio_auth_creds.rio_auth_token ) - rio_organization_id = init_settings.init_kwargs.get( + cls.rio_organization_id = init_settings.init_kwargs.get( "rio_organization_id", rio_auth_creds.rio_organization_id ) - rio_project_id = init_settings.init_kwargs.get( + cls.rio_project_id = init_settings.init_kwargs.get( "rio_project_id", rio_auth_creds.rio_project_id ) - rio_environment = init_settings.init_kwargs.get( + cls.rio_environment = init_settings.init_kwargs.get( "rio_environment", rio_auth_creds.rio_environment ) - config_tree_name = init_settings.init_kwargs.get( + cls.config_tree_name = init_settings.init_kwargs.get( "config_tree_name", rio_config_tree.config_tree_name ) - config_tree_revision = init_settings.init_kwargs.get( + cls.config_tree_revision = init_settings.init_kwargs.get( "config_tree_version", rio_config_tree.config_tree_revision ) _client_config = Configuration( - auth_token=rio_auth_token, - project_guid=rio_project_id, - organization_guid=rio_organization_id, - environment=rio_environment + 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=config_tree_name, - config_tree_revision=config_tree_revision, + config_tree_name=cls.config_tree_name, + config_tree_revision=cls.config_tree_revision, ) cls.__config_source__ = rr_config_settings_source diff --git a/pydantic_configs/app_config_source.py b/rapyuta_io_sdk_v2/pydantic_configs/app_config_source.py similarity index 98% rename from pydantic_configs/app_config_source.py rename to rapyuta_io_sdk_v2/pydantic_configs/app_config_source.py index 9f715d5..6f4eb9e 100644 --- a/pydantic_configs/app_config_source.py +++ b/rapyuta_io_sdk_v2/pydantic_configs/app_config_source.py @@ -39,7 +39,7 @@ def __init__( current_loop = asyncio.get_event_loop() current_loop.run_until_complete( self._retrieve_config_tree() - ) # set self._config_tree + ) super().__init__( settings_cls, diff --git a/requirements-dev.lock b/requirements-dev.lock index 2226e6b..330b673 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -48,6 +48,8 @@ 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 diff --git a/requirements.lock b/requirements.lock index 2226e6b..330b673 100644 --- a/requirements.lock +++ b/requirements.lock @@ -48,6 +48,8 @@ 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 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={}, +)