From 0b9144f44a6bc7757576b924d60acb3748486813 Mon Sep 17 00:00:00 2001 From: guptadev21 Date: Thu, 24 Oct 2024 14:38:08 +0530 Subject: [PATCH] feat: add some functions --- .github/workflows/lint-check.yml | 13 ----- .github/workflows/pull-request.yml | 14 ++++++ .github/workflows/pypi.yml | 23 +++++++++ .github/workflows/python-compatibility.yml | 12 +++-- .github/workflows/release.yml | 24 +++++++++ .gitignore | 1 - pyproject.toml | 1 + rapyuta_io_sdk_v2/async_client.py | 43 +++++++++++++++- rapyuta_io_sdk_v2/client.py | 42 +++++++++++++++- rapyuta_io_sdk_v2/config.py | 58 +++++++++++----------- rapyuta_io_sdk_v2/constants.py | 2 +- rapyuta_io_sdk_v2/utils.py | 39 ++++++++++++--- tests/test_get_token.py | 6 ++- uv.lock | 11 ++++ 14 files changed, 229 insertions(+), 60 deletions(-) delete mode 100644 .github/workflows/lint-check.yml create mode 100644 .github/workflows/pull-request.yml create mode 100644 .github/workflows/pypi.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/lint-check.yml b/.github/workflows/lint-check.yml deleted file mode 100644 index 6e634f8..0000000 --- a/.github/workflows/lint-check.yml +++ /dev/null @@ -1,13 +0,0 @@ -name: "๐Ÿงน Ruff" - -on: [pull_request] - -jobs: - lint: - name: Lint Check - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v4 - - uses: chartboost/ruff-action@v1 - with: - args: "check" \ No newline at end of file diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 0000000..c62435f --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,14 @@ +name: โœ… Quality Checks +on: [ push ] + +jobs: + code-quality-checks: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Run checks + uses: astral-sh/ruff-action@v1 + with: + args: "check" \ No newline at end of file diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml new file mode 100644 index 0000000..e40d4fc --- /dev/null +++ b/.github/workflows/pypi.yml @@ -0,0 +1,23 @@ +name: ๐Ÿ“ฆ๏ธ Upload to PyPi +on: + release: + types: + - published + +jobs: + upload: + runs-on: ubuntu-latest + environment: release + permissions: + id-token: write + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v3 + + - name: Publish to pypi + run: | + uv build + uv publish --trusted-publishing always \ No newline at end of file diff --git a/.github/workflows/python-compatibility.yml b/.github/workflows/python-compatibility.yml index dc2db3e..fdda428 100644 --- a/.github/workflows/python-compatibility.yml +++ b/.github/workflows/python-compatibility.yml @@ -1,12 +1,12 @@ name: ๐Ÿ Python Compatibility Check -on: [ push ] +on: [push] jobs: python-compatibility: runs-on: ubuntu-latest strategy: matrix: - python-version: [ '3.8.10', '3.9', '3.10', '3.11', '3.12', '3.13' ] + python-version: ['3.8.10', '3.9', '3.10', '3.11', '3.12', '3.13'] steps: - name: Checkout Code uses: actions/checkout@v4 @@ -17,5 +17,9 @@ jobs: - name: Set up Python ${{ matrix.python-version }} run: uv python install ${{ matrix.python-version }} - - name: PyTest - run: uv pytest tests/test_get_token.py \ No newline at end of file + - name: Install the project + run: uv sync --all-extras --dev + + - name: Run tests + # For example, using `pytest` + run: uv run pytest tests/test_get_token.py \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4a7f258 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,24 @@ +name: ๐ŸŽ‰ Release +on: + push: + branches: + - main + +jobs: + release: + runs-on: ubuntu-20.04 + steps: + - name: Checkout Code + uses: actions/checkout@v4.2.2 + with: + token: ${{ secrets.GH_TOKEN }} + + - name: Run semantic-release + run: | + npm install --save-dev semantic-release@19.0.2 + npm install @semantic-release/git -D + npm install @semantic-release/changelog -D + npm install @semantic-release/exec -D + npx semantic-release + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4813f1f..cea5741 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ wheels/ .venv*/ .ruff_cache .idea -.github/workflows/release.yml .vscode main_test.py test_config.json \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a77a1a8..daef684 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ description = "Python SDK for rapyuta.io v2 APIs" dependencies = [ "httpx>=0.27.2", "mock>=5.1.0", + "munch>=4.0.0", "pytest-mock>=3.14.0", "pytest>=8.3.3", "tenacity>=9.0.0", diff --git a/rapyuta_io_sdk_v2/async_client.py b/rapyuta_io_sdk_v2/async_client.py index 6f16988..3b51f38 100644 --- a/rapyuta_io_sdk_v2/async_client.py +++ b/rapyuta_io_sdk_v2/async_client.py @@ -19,7 +19,7 @@ from rapyuta_io_sdk_v2.client import Client from rapyuta_io_sdk_v2.config import Configuration -from rapyuta_io_sdk_v2.utils import handle_server_errors +from rapyuta_io_sdk_v2.utils import handle_server_errors, projects_list_munch class AsyncClient(Client): @@ -84,3 +84,44 @@ async def refresh_token(self, token: str) -> str: data = response.json()["data"] return data["Token"] + + async def list_projects(self, organization_guid: str): + if organization_guid is None: + raise ValueError("organization_guid is required") + v2api_host = self.config.hosts.get("v2api_host") + self.config.organization_guid = organization_guid + headers = self._get_headers(with_project=False) + + async with httpx.AsyncClient() as asyncClient: + response = await asyncClient.get( + url="{}/v2/projects/".format(v2api_host), headers=headers, timeout=10 + ) + handle_server_errors(response) + return projects_list_munch(response) + + async def get_project(self, organization_guid: str, project_guid: str): + """Get a project by its GUID + + Args: + organization_guid (str): Organization GUID + project_guid (str): Project GUID + + Raises: + ValueError: If organization_guid or project_guid is None + + Returns: + _type_: Project details in json + """ + if organization_guid is None or project_guid is None: + raise ValueError("organization_guid and project_guid are required") + v2api_host = self.config.hosts.get("v2api_host") + self.config.organization_guid = organization_guid + headers = self._get_headers(with_project=False) + async with httpx.AsyncClient() as asyncClient: + response = await asyncClient.get( + url="{}/v2/projects/{}/".format(v2api_host, project_guid), + headers=headers, + timeout=10, + ) + handle_server_errors(response) + return response.json() diff --git a/rapyuta_io_sdk_v2/client.py b/rapyuta_io_sdk_v2/client.py index 5b63997..fdf92cd 100644 --- a/rapyuta_io_sdk_v2/client.py +++ b/rapyuta_io_sdk_v2/client.py @@ -18,7 +18,7 @@ from rapyuta_io_sdk_v2.config import Configuration from rapyuta_io_sdk_v2.constants import GET_USER_API_PATH -from rapyuta_io_sdk_v2.utils import handle_server_errors +from rapyuta_io_sdk_v2.utils import handle_server_errors, projects_list_munch class Client(object): @@ -115,14 +115,52 @@ def set_project(self, project_guid: str): def set_organization(self, organization_guid: str): self.config.organization_guid = organization_guid + # Projects def list_projects(self, organization_guid: str): + """List all projects in the organization + + Args: + organization_guid (str): The organization GUID + + Raises: + ValueError: If organization_guid is None + + Returns: + _type_: List of projects (Munch object) + """ if organization_guid is None: raise ValueError("organization_guid is required") v2api_host = self.config.hosts.get("v2api_host") - self.config.organization_guid = organization_guid + self.set_organization(organization_guid) headers = self._get_headers(with_project=False) response = httpx.get( url="{}/v2/projects/".format(v2api_host), headers=headers, timeout=10 ) handle_server_errors(response) + return projects_list_munch(response) + + def get_project(self, organization_guid: str, project_guid: str): + """Get a project by its GUID + + Args: + organization_guid (str): Organization GUID + project_guid (str): Project GUID + + Raises: + ValueError: If organization_guid or project_guid is None + + Returns: + _type_: Project details in json + """ + if organization_guid is None or project_guid is None: + raise ValueError("organization_guid and project_guid are required") + v2api_host = self.config.hosts.get("v2api_host") + self.set_organization(organization_guid) + headers = self._get_headers(with_project=False) + response = httpx.get( + url="{}/v2/projects/{}/".format(v2api_host, project_guid), + headers=headers, + timeout=10, + ) + handle_server_errors(response) return response.json() diff --git a/rapyuta_io_sdk_v2/config.py b/rapyuta_io_sdk_v2/config.py index 0625050..6441086 100644 --- a/rapyuta_io_sdk_v2/config.py +++ b/rapyuta_io_sdk_v2/config.py @@ -14,58 +14,60 @@ # limitations under the License. import json from dataclasses import dataclass +import os from rapyuta_io_sdk_v2.constants import ( NAMED_ENVIRONMENTS, PROD_ENVIRONMENT_SUBDOMAIN, STAGING_ENVIRONMENT_SUBDOMAIN, ) +from rapyuta_io_sdk_v2.utils import get_default_app_dir from rapyuta_io_sdk_v2.exceptions import ValidationError @dataclass class Configuration(object): - email: str - _password: str - auth_token: str - project_guid: str - organization_guid: str + email: str = None + _password: str = None + auth_token: str = None + project_guid: str = None + organization_guid: str = None environment: str = "ga" # Default environment is prod - def __init__( - self, - project_guid: str = None, - organization_guid: str = None, - 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 + def __post_init__(self): self.hosts = {} - self.set_environment(environment) + self.set_environment(self.environment) @staticmethod def from_file(file_path: str) -> "Configuration": + """Create a configuration object from a file. + + Args: + file_path (str): Path to the file. + + Returns: + Configuration: Configuration object. + """ + if file_path is None: + app_name = "rio_cli" + default_dir = get_default_app_dir(app_name) + file_path = os.path.join(default_dir, "config.json") + with open(file_path, "r") as file: data = json.load(file) return Configuration( email=data.get("email"), - password=data.get("password"), + _password=data.get("password"), project_guid=data.get("project_guid"), organization_guid=data.get("organization_guid"), environment=data.get("environment"), + auth_token=data.get("auth_token"), ) - def set_project(self, project) -> None: - self.project_guid = project + def set_project(self, project_guid: str) -> None: + self.project_guid = project_guid - def set_organization(self, organization_guid) -> None: + def set_organization(self, organization_guid: str) -> None: self.organization_guid = organization_guid def set_environment(self, name: str = None) -> None: @@ -81,12 +83,10 @@ def set_environment(self, name: str = None) -> None: if self.environment is not None: name = self.environment if name is not None: - is_valid_env = name in NAMED_ENVIRONMENTS or name.startswith("pr") - if not is_valid_env: + if not (name in NAMED_ENVIRONMENTS or name.startswith("pr")): raise ValidationError("Invalid environment") subdomain = STAGING_ENVIRONMENT_SUBDOMAIN - else: - name = "ga" + name = name or "ga" rip = "https://{}rip.{}".format(name, subdomain) v2api = "https://{}api.{}".format(name, subdomain) diff --git a/rapyuta_io_sdk_v2/constants.py b/rapyuta_io_sdk_v2/constants.py index da46aa5..9579491 100644 --- a/rapyuta_io_sdk_v2/constants.py +++ b/rapyuta_io_sdk_v2/constants.py @@ -18,4 +18,4 @@ 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"] +NAMED_ENVIRONMENTS = ["ga", "qa", "dev"] diff --git a/rapyuta_io_sdk_v2/utils.py b/rapyuta_io_sdk_v2/utils.py index 9fe9e36..45fd42e 100644 --- a/rapyuta_io_sdk_v2/utils.py +++ b/rapyuta_io_sdk_v2/utils.py @@ -15,20 +15,24 @@ # from rapyuta_io_sdk_v2.config import Configuration import http import json -from typing import Any, Dict +import os +import sys import httpx +from munch import Munch from rapyuta_io_sdk_v2.exceptions import HttpAlreadyExistsError, HttpNotFoundError -def validate_auth_token(config: Any) -> Dict: - try: - client = config.sync_client() - user = client.get_authenticated_user() - return user - except Exception: - raise +def projects_list_munch(response: httpx.Response) -> Munch: + data = response.json() + projects = [] + for item in data.get("items", []): + project = item.get("metadata", {}).get("name") + project_guid = item.get("metadata", {}).get("projectGUID") + if project and project_guid: + projects.append({"project": project, "project_guid": project_guid}) + return Munch({"projects": projects}) def handle_server_errors(response: httpx.Response): @@ -74,3 +78,22 @@ def handle_server_errors(response: httpx.Response): # Anything else that is not known if status_code > 504: raise Exception("unknown server error") + + +def get_default_app_dir(app_name: str) -> str: + """Get the default application directory based on OS.""" + # On Windows + if os.name == "nt": + appdata = os.environ.get("APPDATA") or os.environ.get("LOCALAPPDATA") + if appdata: + return os.path.join(appdata, app_name) + + # On macOS + if sys.platform == "darwin": + return os.path.join( + os.path.expanduser("~"), "Library", "Application Support", app_name + ) + + # On Linux and other Unix-like systems + xdg_config_home = os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) + return os.path.join(xdg_config_home, app_name) diff --git a/tests/test_get_token.py b/tests/test_get_token.py index 95c1675..21c429e 100644 --- a/tests/test_get_token.py +++ b/tests/test_get_token.py @@ -1,4 +1,5 @@ from rapyuta_io_sdk_v2.client import Client +from rapyuta_io_sdk_v2.config import Configuration # Test case for a successful token retrieval @@ -19,7 +20,10 @@ def test_get_token_success(mocker): mock_post.return_value = mock_response # Call the function under test - test_client = Client() + test_config = Configuration( + email=email, _password=password, environment="pr_mock_test" + ) + test_client = Client(config=test_config) token = test_client.get_token(email, password, "pr_mock_test") # config_instance.hosts = {"rip_host": "http://mocked-rip-host.com"} diff --git a/uv.lock b/uv.lock index 645bd6a..7196b19 100644 --- a/uv.lock +++ b/uv.lock @@ -108,6 +108,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6b/20/471f41173930550f279ccb65596a5ac19b9ac974a8d93679bcd3e0c31498/mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744", size = 30938 }, ] +[[package]] +name = "munch" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/2b/45098135b5f9f13221820d90f9e0516e11a2a0f55012c13b081d202b782a/munch-4.0.0.tar.gz", hash = "sha256:542cb151461263216a4e37c3fd9afc425feeaf38aaa3025cd2a981fadb422235", size = 19089 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/b3/7c69b37f03260a061883bec0e7b05be7117c1b1c85f5212c72c8c2bc3c8c/munch-4.0.0-py2.py3-none-any.whl", hash = "sha256:71033c45db9fb677a0b7eb517a4ce70ae09258490e419b0e7f00d1e386ecb1b4", size = 9950 }, +] + [[package]] name = "packaging" version = "24.1" @@ -162,6 +171,7 @@ source = { editable = "." } dependencies = [ { name = "httpx" }, { name = "mock" }, + { name = "munch" }, { name = "pytest" }, { name = "pytest-mock" }, { name = "tenacity" }, @@ -171,6 +181,7 @@ dependencies = [ requires-dist = [ { name = "httpx", specifier = ">=0.27.2" }, { name = "mock", specifier = ">=5.1.0" }, + { name = "munch", specifier = ">=4.0.0" }, { name = "pytest", specifier = ">=8.3.3" }, { name = "pytest-mock", specifier = ">=3.14.0" }, { name = "tenacity", specifier = ">=9.0.0" },