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={},
+)