diff --git a/Makefile b/Makefile index 96279c3..da61f33 100644 --- a/Makefile +++ b/Makefile @@ -65,4 +65,7 @@ pypi: ## publishes to PyPI uv build @echo "\n>>> Publishing to PyPI..." uv publish - @echo "\n\033[0;32mPyPI release complete! The GitHub Action will now create the GitHub Release.\033[0m" \ No newline at end of file + @echo "\n\033[0;32mPyPI release complete! The GitHub Action will now create the GitHub Release.\033[0m" + +tree: ## shows filetree in terminal without uninteresting files + tree -I "*.pyc|*.lock" \ No newline at end of file diff --git a/examples/metadata/get_datacenters.py b/examples/metadata/get_datacenters.py index fddbebb..a4b699b 100644 --- a/examples/metadata/get_datacenters.py +++ b/examples/metadata/get_datacenters.py @@ -1,11 +1,14 @@ import asyncio +import logging from codesphere import CodesphereSDK +logging.basicConfig(level=logging.INFO) + async def main(): """Fetches datacenters.""" async with CodesphereSDK() as sdk: - datacenters = await sdk.metadata.datacenters() + datacenters = await sdk.metadata.list_datacenters() for datacenter in datacenters: print(datacenter.model_dump_json(indent=2)) diff --git a/examples/metadata/get_workspace_base_image.py b/examples/metadata/get_workspace_base_image.py index 6bbd24b..1eeca58 100644 --- a/examples/metadata/get_workspace_base_image.py +++ b/examples/metadata/get_workspace_base_image.py @@ -1,6 +1,9 @@ import asyncio +import logging from codesphere import CodesphereSDK +logging.basicConfig(level=logging.INFO) + async def main(): """Fetches base images.""" diff --git a/examples/metadata/get_workspace_plans.py b/examples/metadata/get_workspace_plans.py index eb25538..81393ed 100644 --- a/examples/metadata/get_workspace_plans.py +++ b/examples/metadata/get_workspace_plans.py @@ -1,6 +1,9 @@ import asyncio +import logging from codesphere import CodesphereSDK +logging.basicConfig(level=logging.INFO) + async def main(): """Fetches workspace plans.""" diff --git a/examples/teams/create_team.py b/examples/teams/create_team.py index f7010b2..aa2908f 100644 --- a/examples/teams/create_team.py +++ b/examples/teams/create_team.py @@ -1,13 +1,15 @@ import asyncio +import logging from codesphere import CodesphereSDK, TeamCreate +logging.basicConfig(level=logging.INFO) + async def main(): try: async with CodesphereSDK() as sdk: newTeam = TeamCreate(name="test", dc=2) created_team = await sdk.teams.create(data=newTeam) - print("\n--- Details for the created team ---") print(created_team.model_dump_json(indent=2)) except Exception as e: print(f"An error occurred: {e}") diff --git a/examples/teams/delete_team.py b/examples/teams/delete_team.py index acf9e22..29362f4 100644 --- a/examples/teams/delete_team.py +++ b/examples/teams/delete_team.py @@ -1,12 +1,14 @@ import asyncio +import logging from codesphere import CodesphereSDK +logging.basicConfig(level=logging.INFO) + async def main(): try: async with CodesphereSDK() as sdk: team_to_delete = await sdk.teams.get(team_id="") - print("\n--- Details of the team to be deleted ---") print(team_to_delete.model_dump_json(indent=2)) await team_to_delete.delete() print(f"Team with ID {team_to_delete.id} was successfully deleted.") diff --git a/examples/teams/get_team.py b/examples/teams/get_team.py index ffd78f6..38be3a8 100644 --- a/examples/teams/get_team.py +++ b/examples/teams/get_team.py @@ -1,16 +1,15 @@ import asyncio - +import logging from codesphere import CodesphereSDK +logging.basicConfig(level=logging.INFO) + async def main(): try: async with CodesphereSDK() as sdk: - teams = await sdk.teams.list() - print(teams[0].model_dump_json(indent=2)) - first_team = await sdk.teams.get(team_id=teams[0].id) - print("\n--- Details for the first team ---") - print(first_team.model_dump_json(indent=2)) + team = await sdk.teams.get(team_id="") + print(team.model_dump_json(indent=2)) except Exception as e: print(f"An error occurred: {e}") diff --git a/examples/teams/list_teams.py b/examples/teams/list_teams.py index 30d1a22..6f11b6e 100644 --- a/examples/teams/list_teams.py +++ b/examples/teams/list_teams.py @@ -1,7 +1,9 @@ import asyncio - +import logging from codesphere import CodesphereSDK +logging.basicConfig(level=logging.INFO) + async def main(): try: diff --git a/src/codesphere/__init__.py b/src/codesphere/__init__.py index bb3866f..fd3f493 100644 --- a/src/codesphere/__init__.py +++ b/src/codesphere/__init__.py @@ -1,34 +1,39 @@ -from .client import APIHttpClient -from .resources.team.resources import TeamsResource, TeamCreate -from .resources.workspace.resources import WorkspacesResource -from .resources.metadata.resources import MetadataResource -from .resources.workspace.models import ( - Workspace, - WorkspaceCreate, - WorkspaceUpdate, - WorkspaceStatus, -) +""" +Codesphere SDK - Python client for the Codesphere API. + +This package provides a high-level asynchronous client for interacting +with the `Codesphere Public API `_. +Main Entrypoint: + `from codesphere import CodesphereSDK` -class CodesphereSDK: - def __init__(self, token: str = None): - self._http_client = APIHttpClient() - self.teams: TeamsResource | None = None - self.workspaces: WorkspacesResource | None = None +Basic Usage: + >>> import asyncio + >>> from codesphere import CodesphereSDK + >>> + >>> async def main(): + >>> async with CodesphereSDK() as sdk: + >>> teams = await sdk.teams.list() + >>> print(teams) + >>> + >>> asyncio.run(main()) +""" - async def __aenter__(self): - """Wird beim Eintritt in den 'async with'-Block aufgerufen.""" - await self._http_client.__aenter__() +import logging +from .client import CodesphereSDK - self.teams = TeamsResource(self._http_client) - self.workspaces = WorkspacesResource(self._http_client) - self.metadata = MetadataResource(self._http_client) - return self +from .cs_types.exceptions.exceptions import CodesphereError, AuthenticationError - async def __aexit__(self, exc_type, exc_val, exc_tb): - """Wird beim Verlassen des 'async with'-Blocks aufgerufen.""" - await self._http_client.__aexit__(exc_type, exc_val, exc_tb) +from .resources.team import Team, TeamCreate, TeamBase +from .resources.workspace import ( + Workspace, + WorkspaceCreate, + WorkspaceUpdate, + WorkspaceStatus, +) +from .resources.metadata import Datacenter, Characteristic, WsPlan, Image +logging.getLogger("codesphere").addHandler(logging.NullHandler()) __all__ = [ "CodesphereSDK", @@ -36,9 +41,13 @@ async def __aexit__(self, exc_type, exc_val, exc_tb): "AuthenticationError", "Team", "TeamCreate", - "TeamInList", + "TeamBase", "Workspace", "WorkspaceCreate", "WorkspaceUpdate", "WorkspaceStatus", + "Datacenter", + "Characteristic", + "WsPlan", + "Image", ] diff --git a/src/codesphere/client.py b/src/codesphere/client.py index 5ad727b..4d74240 100644 --- a/src/codesphere/client.py +++ b/src/codesphere/client.py @@ -1,50 +1,75 @@ -import httpx -from pydantic import BaseModel -from typing import Optional, Any -from functools import partial +""" +Codesphere SDK Client -from .config import settings +This module provides the main client class, CodesphereSDK. +""" +from .cs_types.rest.http_client import APIHttpClient +from .resources.metadata.resources import MetadataResource +from .resources.team.resources import TeamsResource +from .resources.workspace.resources import WorkspacesResource -class APIHttpClient: - def __init__(self, base_url: str = "https://codesphere.com/api"): - self._token = settings.token.get_secret_value() - self._base_url = base_url or str(settings.base_url) - self.client: Optional[httpx.AsyncClient] = None - # Dynamically create get, post, put, patch, delete methods - for method in ["get", "post", "put", "patch", "delete"]: - setattr(self, method, partial(self.request, method.upper())) +class CodesphereSDK: + """The main entrypoint for interacting with the `Codesphere Public API `_. - async def __aenter__(self): - timeout_config = httpx.Timeout( - settings.client_timeout_connect, read=settings.client_timeout_read - ) - self.client = httpx.AsyncClient( - base_url=self._base_url, - headers={"Authorization": f"Bearer {self._token}"}, - timeout=timeout_config, - ) - return self + This class manages the HTTP client, its lifecycle, + and provides access to the various API resources. + + Primary usage is via an asynchronous context manager: + + Usage: + >>> import asyncio + >>> from codesphere import CodesphereSDK + >>> + >>> async def main(): + >>> async with CodesphereSDK() as sdk: + >>> teams = await sdk.teams.list() + >>> print(teams) + >>> + >>> asyncio.run(main()) + + Attributes: + teams (TeamsResource): Access to Team API operations. + workspaces (WorkspacesResource): Access to Workspace API operations. + metadata (MetadataResource): Access to Metadata API operations. + """ + + teams: TeamsResource + """Access to the Team API. (e.g., `sdk.teams.list()`)""" - async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any): - if self.client: - await self.client.aclose() + workspaces: WorkspacesResource + """Access to the Workspace API. (e.g., `sdk.workspaces.list()`)""" - async def request( - self, method: str, endpoint: str, **kwargs: Any - ) -> httpx.Response: - if not self.client: - raise RuntimeError( - "APIHttpClient must be used within an 'async with' statement." - ) + metadata: MetadataResource + """Access to the Metadata API. (e.g., `sdk.metadata.list_plans()`)""" - # If a 'json' payload is a Pydantic model, automatically convert it. - if "json" in kwargs and isinstance(kwargs["json"], BaseModel): - kwargs["json"] = kwargs["json"].model_dump(exclude_none=True) + def __init__(self): + self._http_client = APIHttpClient() + self.teams = TeamsResource(self._http_client) + self.workspaces = WorkspacesResource(self._http_client) + self.metadata = MetadataResource(self._http_client) - print(f"{method} {endpoint} {kwargs}") + async def open(self): + """Manually opens the underlying HTTP client session. + + Required for manual lifecycle control when not using `async with`. + + Usage: + >>> sdk = CodesphereSDK() + >>> await sdk.open() + >>> # ... API calls ... + >>> await sdk.close() + """ + await self._http_client.open() + + async def close(self): + """Manually closes the underlying HTTP client session.""" + await self._http_client.close() + + async def __aenter__(self): + await self.open() + return self - response = await self.client.request(method, endpoint, **kwargs) - response.raise_for_status() - return response + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self._http_client.close(exc_type, exc_val, exc_tb) diff --git a/src/codesphere/config.py b/src/codesphere/config.py index 55117d7..5e5559e 100644 --- a/src/codesphere/config.py +++ b/src/codesphere/config.py @@ -5,8 +5,6 @@ class Settings(BaseSettings): """ API Client Settings - - TODO: add List of available env vars """ model_config = SettingsConfigDict( diff --git a/src/codesphere/cs_types/__init__.py b/src/codesphere/cs_types/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/codesphere/resources/exceptions/exceptions.py b/src/codesphere/cs_types/exceptions/exceptions.py similarity index 100% rename from src/codesphere/resources/exceptions/exceptions.py rename to src/codesphere/cs_types/exceptions/exceptions.py diff --git a/src/codesphere/cs_types/rest/handler.py b/src/codesphere/cs_types/rest/handler.py new file mode 100644 index 0000000..16e2288 --- /dev/null +++ b/src/codesphere/cs_types/rest/handler.py @@ -0,0 +1,113 @@ +from functools import partial +import logging +from typing import Any, List, Optional, Type, get_args, get_origin + +import httpx +from pydantic import BaseModel, PrivateAttr, ValidationError + +from .http_client import APIHttpClient +from .operations import APIOperation + +log = logging.getLogger(__name__) + + +class _APIOperationExecutor: + _http_client: Optional[APIHttpClient] = PrivateAttr(default=None) + + def __getattribute__(self, name: str) -> Any: + attr = super().__getattribute__(name) + + if isinstance(attr, APIOperation): + return partial(self._execute_operation, operation=attr) + return attr + + async def _execute_operation(self, operation: APIOperation, **kwargs: Any) -> Any: + handler = APIRequestHandler(executor=self, operation=operation, kwargs=kwargs) + return await handler.execute() + + +class APIRequestHandler: + def __init__( + self, executor: _APIOperationExecutor, operation: APIOperation, kwargs: dict + ): + self.executor = executor + self.operation = operation + self.kwargs = kwargs + self.http_client = executor._http_client + + async def execute(self) -> Any: + endpoint, request_kwargs = self._prepare_request_args() + + response = await self._make_request( + self.operation.method, endpoint, **request_kwargs + ) + + return await self._parse_and_validate_response( + response, self.operation.response_model, endpoint + ) + + def _prepare_request_args(self) -> tuple[str, dict]: + format_args = {} + if isinstance(self.executor, BaseModel): + format_args.update(self.executor.model_dump()) + format_args.update(self.kwargs) + endpoint = self.operation.endpoint_template.format(**format_args) + + payload = None + if json_data_obj := self.kwargs.get("data"): + if isinstance(json_data_obj, BaseModel): + payload = json_data_obj.model_dump(exclude_none=True) + + request_kwargs = {"params": self.kwargs.get("params"), "json": payload} + return endpoint, {k: v for k, v in request_kwargs.items() if v is not None} + + async def _make_request( + self, method: str, endpoint: str, **kwargs: Any + ) -> httpx.Response: + if not self.http_client: + raise RuntimeError("HTTP Client is not initialized.") + return await self.http_client.request( + method=method, endpoint=endpoint, **kwargs + ) + + def _inject_client_into_model(self, model_instance: BaseModel) -> BaseModel: + if hasattr(model_instance, "_http_client"): + model_instance._http_client = self.http_client + return model_instance + + async def _parse_and_validate_response( + self, + response: httpx.Response, + response_model: Type[BaseModel] | Type[List[BaseModel]] | None, + endpoint_for_logging: str, + ) -> Any: + if response_model is None: + return None + + try: + json_response = response.json() + log.debug(f"Validating JSON response for endpoint: {endpoint_for_logging}") + except httpx.ResponseNotRead: + log.warning(f"No JSON response body for {endpoint_for_logging}") + return None + + try: + origin = get_origin(response_model) + if origin is list or origin is List: + item_model = get_args(response_model)[0] + instances = [item_model.model_validate(item) for item in json_response] + for instance in instances: + self._inject_client_into_model(instance) + log.debug("Successfully validated response into list of models.") + return instances + else: + instance = response_model.model_validate(json_response) + self._inject_client_into_model(instance) + log.debug("Successfully validated response into single model.") + return instance + except ValidationError as e: + log.error( + f"Pydantic validation failed for {endpoint_for_logging}. Error: {e}" + ) + log.error(f"Failing JSON data: {json_response}") + raise e diff --git a/src/codesphere/cs_types/rest/http_client.py b/src/codesphere/cs_types/rest/http_client.py new file mode 100644 index 0000000..ea9ee67 --- /dev/null +++ b/src/codesphere/cs_types/rest/http_client.py @@ -0,0 +1,85 @@ +from functools import partial +import logging +import httpx +from pydantic import BaseModel +from typing import Optional, Any +from ...config import settings + +log = logging.getLogger(__name__) + + +class APIHttpClient: + def __init__(self, base_url: str = "https://codesphere.com/api"): + self._token = settings.token.get_secret_value() + self._base_url = base_url or str(settings.base_url) + self._client: Optional[httpx.AsyncClient] = None + + self._timeout_config = httpx.Timeout( + settings.client_timeout_connect, read=settings.client_timeout_read + ) + self._client_config = { + "base_url": self._base_url, + "headers": {"Authorization": f"Bearer {self._token}"}, + "timeout": self._timeout_config, + } + + for method in ["get", "post", "put", "patch", "delete"]: + setattr(self, method, partial(self.request, method.upper())) + + def _get_client(self) -> httpx.AsyncClient: + if not self._client: + raise RuntimeError( + "Client is not open. Please use 'async with sdk:' " + "or call 'await sdk.open()' before making requests." + ) + return self._client + + async def open(self): + if not self._client: + self._client = httpx.AsyncClient(**self._client_config) + await self._client.__aenter__() + + async def close(self, exc_type=None, exc_val=None, exc_tb=None): + if self._client: + await self._client.__aexit__(exc_type, exc_val, exc_tb) + self._client = None + + async def __aenter__(self): + await self.open() + return self + + async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any): + await self.close(exc_type, exc_val, exc_tb) + + async def request( + self, method: str, endpoint: str, **kwargs: Any + ) -> httpx.Response: + client = self._get_client() + + if "json" in kwargs and isinstance(kwargs["json"], BaseModel): + kwargs["json"] = kwargs["json"].model_dump(exclude_none=True) + + log.debug(f"Request: {method} {endpoint}") + log.debug(f"Request kwargs: {kwargs}") + + try: + response = await client.request(method, endpoint, **kwargs) + log.debug( + f"Response: {response.status_code} {response.reason_phrase} for {method} {endpoint}" + ) + + response.raise_for_status() + return response + + except httpx.HTTPStatusError as e: + log.error( + f"HTTP Error {e.response.status_code} for {e.request.method} {e.request.url}" + ) + try: + log.error(f"Error Response Body: {e.response.json()}") + except Exception: + log.error(f"Error Response Body (non-json): {e.response.text}") + raise e + except Exception as e: + log.error(f"An unexpected error occurred: {e}") + raise e diff --git a/src/codesphere/cs_types/rest/operations.py b/src/codesphere/cs_types/rest/operations.py new file mode 100644 index 0000000..2f6b137 --- /dev/null +++ b/src/codesphere/cs_types/rest/operations.py @@ -0,0 +1,22 @@ +from typing import Callable, Awaitable, List, Optional, Type, TypeAlias, TypeVar + +from pydantic import BaseModel + + +_T = TypeVar("_T") + +AsyncCallable: TypeAlias = Callable[[], Awaitable[_T]] + + +class APIOperation: + def __init__( + self, + method: str, + endpoint_template: str, + response_model: Type[BaseModel] | Type[List[BaseModel]], + input_model: Optional[Type[BaseModel]] = None, + ): + self.method = method + self.endpoint_template = endpoint_template + self.response_model = response_model + self.input_model = input_model diff --git a/src/codesphere/resources/base.py b/src/codesphere/resources/base.py index a0b4376..28eff64 100644 --- a/src/codesphere/resources/base.py +++ b/src/codesphere/resources/base.py @@ -1,74 +1,7 @@ -from functools import partial -from typing import Any, Type, List, get_origin, get_args, Optional -from pydantic import BaseModel -from ..client import APIHttpClient +from ..cs_types.rest.http_client import APIHttpClient +from ..cs_types.rest.handler import _APIOperationExecutor -class APIOperation: - """Describes a single, reusable API operation.""" - - def __init__( - self, - method: str, - endpoint_template: str, - response_model: Type[BaseModel] | Type[List[BaseModel]], - input_model: Optional[Type[BaseModel]] = None, - ): - self.method = method - self.endpoint_template = endpoint_template - self.response_model = response_model - self.input_model = input_model - - -class ResourceBase: - """The base class for all resources, containing the logic to execute APIOperations.""" - +class ResourceBase(_APIOperationExecutor): def __init__(self, http_client: APIHttpClient): self._http_client = http_client - - def __getattribute__(self, name: str) -> Any: - """Intercepts attribute access to execute APIOperations dynamically.""" - attr = super().__getattribute__(name) - if isinstance(attr, APIOperation): - return partial(self._execute_operation, operation=attr) - return attr - - async def _execute_operation(self, operation: APIOperation, **kwargs: Any) -> Any: - """Executes an APIOperation, fills placeholders, and parses the response.""" - format_args = {**self.__dict__, **kwargs} - endpoint = operation.endpoint_template.format(**format_args) - - params = kwargs.get("params") - json_data_obj = kwargs.get("data") - payload = None - - if json_data_obj and isinstance(json_data_obj, BaseModel): - payload = json_data_obj.model_dump(exclude_none=True) - elif operation.input_model: - input_data = operation.input_model(**kwargs) - payload = input_data.model_dump(exclude_none=True) - - response = await self._http_client.request( - method=operation.method, endpoint=endpoint, json=payload, params=params - ) - - if operation.response_model is None: - return None - - json_response = response.json() - - # print("--- RAW API RESPONSE ---") - # pprint.pprint(json_response) - # print("------------------------") - - origin = get_origin(operation.response_model) - if origin is list or origin is List: - item_model = get_args(operation.response_model)[0] - instances = [item_model.model_validate(item) for item in json_response] - for instance in instances: - instance._http_client = self._http_client - return instances - else: - instance = operation.response_model.model_validate(json_response) - instance._http_client = self._http_client - return instance diff --git a/src/codesphere/resources/domain/__init__.py b/src/codesphere/resources/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/codesphere/resources/metadata/__init__.py b/src/codesphere/resources/metadata/__init__.py new file mode 100644 index 0000000..b0c28cf --- /dev/null +++ b/src/codesphere/resources/metadata/__init__.py @@ -0,0 +1,6 @@ +"""Metadata Resource & Models""" + +from .models import Datacenter, Characteristic, WsPlan, Image +from .resources import MetadataResource + +__all__ = ["Datacenter", "Characteristic", "WsPlan", "Image", "MetadataResource"] diff --git a/src/codesphere/resources/metadata/models.py b/src/codesphere/resources/metadata/models.py index c101caf..1791e4f 100644 --- a/src/codesphere/resources/metadata/models.py +++ b/src/codesphere/resources/metadata/models.py @@ -1,14 +1,10 @@ from __future__ import annotations from pydantic import BaseModel import datetime -from typing import TYPE_CHECKING -if TYPE_CHECKING: - pass - -class Datacenters(BaseModel): - """Defines the request body for creating a team.""" +class Datacenter(BaseModel): + """Represents a physical data center location.""" id: int name: str @@ -16,8 +12,8 @@ class Datacenters(BaseModel): countryCode: str -class Characteristics(BaseModel): - """Defines the hardware and service characteristics of a workspace plan.""" +class Characteristic(BaseModel): + """Defines the resource specifications for a WsPlan.""" id: int CPU: float @@ -28,19 +24,21 @@ class Characteristics(BaseModel): onDemand: bool -class WsPlans(BaseModel): - """Contains all fields that appear in a workspace-plans response.""" +class WsPlan(BaseModel): + """ + Represents a purchasable workspace plan. + """ id: int priceUsd: int title: str deprecated: bool - characteristics: Characteristics + characteristics: Characteristic maxReplicas: int -class Images(BaseModel): - """Represents a team as it appears in the list response.""" +class Image(BaseModel): + """Represents a runnable workspace base image.""" id: str name: str diff --git a/src/codesphere/resources/metadata/resources.py b/src/codesphere/resources/metadata/resources.py index 7911e2e..4cb1168 100644 --- a/src/codesphere/resources/metadata/resources.py +++ b/src/codesphere/resources/metadata/resources.py @@ -1,31 +1,37 @@ -from typing import Awaitable, Callable, List -from ..base import ResourceBase, APIOperation -from .models import Datacenters, WsPlans, Images +""" +Defines the resource class for the Metadata API endpoints. +""" +from typing import List +from ...cs_types.rest.operations import APIOperation, AsyncCallable +from ..base import ResourceBase +from .models import Datacenter, WsPlan, Image -class MetadataResource(ResourceBase): - """Contains all API operations for team ressources.""" - datacenters: Callable[[], Awaitable[List[Datacenters]]] - datacenters = APIOperation( +class MetadataResource(ResourceBase): + list_datacenters: AsyncCallable[List[Datacenter]] + """Fetches a list of all available data centers.""" + list_datacenters = APIOperation( method="GET", endpoint_template="/metadata/datacenters", input_model=None, - response_model=List[Datacenters], + response_model=List[Datacenter], ) - plans: Callable[[], Awaitable[List[WsPlans]]] - plans = APIOperation( + list_plans: AsyncCallable[List[WsPlan]] + """Fetches a list of all available workspace plans.""" + list_plans = APIOperation( method="GET", endpoint_template="/metadata/workspace-plans", input_model=None, - response_model=List[WsPlans], + response_model=List[WsPlan], ) - images: Callable[[], Awaitable[List[Images]]] - images = APIOperation( + list_images: AsyncCallable[List[Image]] + """Fetches a list of all available workspace base images.""" + list_images = APIOperation( method="GET", endpoint_template="/metadata/workspace-base-images", input_model=None, - response_model=List[Images], + response_model=List[Image], ) diff --git a/src/codesphere/resources/team/__init__.py b/src/codesphere/resources/team/__init__.py new file mode 100644 index 0000000..85e8cf6 --- /dev/null +++ b/src/codesphere/resources/team/__init__.py @@ -0,0 +1,4 @@ +from .models import Team, TeamCreate, TeamBase +from .resources import TeamsResource + +__all__ = ["Team", "TeamCreate", "TeamBase", "TeamsResource"] diff --git a/src/codesphere/resources/team/models.py b/src/codesphere/resources/team/models.py index 41f710f..2a0104c 100644 --- a/src/codesphere/resources/team/models.py +++ b/src/codesphere/resources/team/models.py @@ -1,21 +1,26 @@ +""" +Pydantic models for the Team resource. + +Includes the 'active' Team model (with API methods) +and data-only models for creation payloads. +""" + from __future__ import annotations -from pydantic import BaseModel, PrivateAttr -from typing import Optional, TYPE_CHECKING +from pydantic import BaseModel, Field +from typing import Optional -if TYPE_CHECKING: - from ...client import APIHttpClient +from ...cs_types.rest.handler import _APIOperationExecutor +from ...cs_types.rest.operations import APIOperation, AsyncCallable class TeamCreate(BaseModel): - """Defines the request body for creating a team.""" + """Data payload required for creating a new team.""" name: str dc: int class TeamBase(BaseModel): - """Contains all fields that appear in almost every team response.""" - id: int name: str description: Optional[str] = None @@ -26,16 +31,31 @@ class TeamBase(BaseModel): role: Optional[int] = None -class Team(TeamBase): - """ - Represents a complete, single team object (detail view). - This is the "active" model with methods. +class Team(TeamBase, _APIOperationExecutor): """ + Represents a complete, 'active' team object returned from the API. - _http_client: Optional[APIHttpClient] = PrivateAttr(default=None) + This model includes methods to interact with the resource directly. - async def delete(self) -> None: - """Deletes this team via the API.""" - if not self._http_client: - raise RuntimeError("Cannot make API calls on a detached model.") - await self._http_client.delete(f"/teams/{self.id}") + Usage: + >>> # Get a team instance + >>> team = await sdk.teams.get(team_id=123) + >>> # Call methods on it + >>> await team.delete() + """ + + delete: AsyncCallable[None] + """ + Deletes this team via the API. + + .. warning:: + This is a destructive operation and cannot be undone. + """ + delete = Field( + default=APIOperation( + method="DELETE", + endpoint_template="/teams/{id}", + response_model=None, + ), + exclude=True, + ) diff --git a/src/codesphere/resources/team/resources.py b/src/codesphere/resources/team/resources.py index a40a7e8..c2ba118 100644 --- a/src/codesphere/resources/team/resources.py +++ b/src/codesphere/resources/team/resources.py @@ -1,5 +1,11 @@ -from typing import Awaitable, Callable, List, Protocol -from ..base import ResourceBase, APIOperation +""" +Defines the resource class for the Team API endpoints. +""" + +from typing import List, Protocol + +from ...cs_types.rest.operations import APIOperation, AsyncCallable +from ..base import ResourceBase from .models import Team, TeamCreate @@ -12,11 +18,23 @@ async def __call__(self, *, data: TeamCreate) -> Team: ... class TeamsResource(ResourceBase): - """Contains all API operations for team ressources.""" + """ + Provides access to the Team API operations. - list: Callable[[], Awaitable[List[Team]]] + Usage: + >>> # Access via the main SDK client + >>> async with CodesphereSDK() as sdk: + >>> new_team_data = TeamCreate(name="My Team", dc=1) + >>> new_team = await sdk.teams.create(data=new_team_data) + >>> team = await sdk.teams.get(team_id=new_team.id) """ - Fetches all teams. + + list: AsyncCallable[List[Team]] + """ + Fetches a list of all teams the user belongs to. + + Returns: + List[Team]: A list of Team objects. """ list = APIOperation( method="GET", @@ -47,7 +65,8 @@ class TeamsResource(ResourceBase): Creates a new team. Args: - data (TeamCreate): The data payload for the new team. + data (TeamCreate): A :class:`~.models.TeamCreate` object + containing the new team's information. Returns: Team: The newly created Team object. diff --git a/src/codesphere/resources/workspace/__init__.py b/src/codesphere/resources/workspace/__init__.py new file mode 100644 index 0000000..af4e319 --- /dev/null +++ b/src/codesphere/resources/workspace/__init__.py @@ -0,0 +1,10 @@ +from .models import Workspace, WorkspaceCreate, WorkspaceUpdate, WorkspaceStatus +from .resources import WorkspacesResource + +__all__ = [ + "Workspace", + "WorkspaceCreate", + "WorkspaceUpdate", + "WorkspaceStatus", + "WorkspacesResource", +] diff --git a/src/codesphere/resources/workspace/resources.py b/src/codesphere/resources/workspace/resources.py index f31fdef..55015a8 100644 --- a/src/codesphere/resources/workspace/resources.py +++ b/src/codesphere/resources/workspace/resources.py @@ -1,5 +1,7 @@ from typing import List, Protocol -from ..base import ResourceBase, APIOperation + +from ...cs_types.rest.operations import APIOperation +from ..base import ResourceBase from .models import Workspace, WorkspaceCreate