Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pydantic-settings): Initializes the pydantic settings for rio config-trees #2

Draft
wants to merge 10 commits into
base: devel
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @rapyuta-robotics/io-cli-owner @rapyuta-robotics/io-first-reviewer
20 changes: 20 additions & 0 deletions .github/workflows/conventional-commits.yml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
name: Check if commit messages are compliant
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# python generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: add .idea to this file.

# venv
.venv
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/inspectionProfiles/profiles_settings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions .idea/rapyuta-io-sdk-v2.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.12.5
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# rapyuta-io-sdk-v2

Describe your project here.
27 changes: 27 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[project]
name = "rapyuta-io-sdk-v2"
version = "0.1.0"
description = "Version:2 for Rapyuta.io SDK"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description = "Version:2 for Rapyuta.io SDK"
description = "A Python SDK for rapyuta.io v2 APIs"

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"]
3 changes: 3 additions & 0 deletions rapyuta_io_sdk_v2/__init__.py
Original file line number Diff line number Diff line change
@@ -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"
118 changes: 118 additions & 0 deletions rapyuta_io_sdk_v2/async_client.py
Original file line number Diff line number Diff line change
@@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

todo: Run a Reformat on the entire code in Pycharm. That should fix all the formatting.

Suggested change
def __init__(self,config):
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







134 changes: 134 additions & 0 deletions rapyuta_io_sdk_v2/client.py
Original file line number Diff line number Diff line change
@@ -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))


Loading
Loading