From e762d82c173dd2e02b5a59b55e2136a7e9d4e43e Mon Sep 17 00:00:00 2001 From: JD <> Date: Tue, 22 Jul 2025 13:51:54 +0200 Subject: [PATCH 1/2] feat(src/codesphere/resources/workspace/env-vars): support env-vars endpoints of public api --- .../workspaces/env-vars/delete_envvars.py | 25 ++++++ examples/workspaces/env-vars/list_envvars.py | 20 +++++ examples/workspaces/env-vars/set_envvars.py | 27 ++++++ src/codesphere/client.py | 5 +- .../resources/workspace/env-vars/model.py | 90 +++++++++++++++++++ .../resources/workspace/env-vars/modely.py | 0 .../resources/workspace/env-vars/resources.py | 39 ++++++++ src/codesphere/resources/workspace/models.py | 48 +++++++++- 8 files changed, 251 insertions(+), 3 deletions(-) create mode 100644 src/codesphere/resources/workspace/env-vars/model.py delete mode 100644 src/codesphere/resources/workspace/env-vars/modely.py diff --git a/examples/workspaces/env-vars/delete_envvars.py b/examples/workspaces/env-vars/delete_envvars.py index e69de29..875370b 100644 --- a/examples/workspaces/env-vars/delete_envvars.py +++ b/examples/workspaces/env-vars/delete_envvars.py @@ -0,0 +1,25 @@ +import asyncio +import pprint +from codesphere import CodesphereSDK + + +async def main(): + """Fetches a team and lists all workspaces within it.""" + async with CodesphereSDK() as sdk: + teams = await sdk.teams.list() + workspaces = await sdk.workspaces.list_by_team(team_id=teams[0].id) + + workspace = workspaces[0] + + envs = await workspace.get_env_vars() + print("Current Environment Variables:") + pprint.pprint(envs[0].name) + + await workspace.delete_env_vars([envs[0].name]) # you can pass a list of strings to delete multiple env vars + + print("Environment Variables after deletion:") + updated_envs = await workspace.get_env_vars() + pprint.pprint(updated_envs) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/workspaces/env-vars/list_envvars.py b/examples/workspaces/env-vars/list_envvars.py index e69de29..e0719f3 100644 --- a/examples/workspaces/env-vars/list_envvars.py +++ b/examples/workspaces/env-vars/list_envvars.py @@ -0,0 +1,20 @@ +import asyncio +import pprint +from codesphere import CodesphereSDK + + +async def main(): + """Fetches a team and lists all workspaces within it.""" + async with CodesphereSDK() as sdk: + teams = await sdk.teams.list() + workspaces = await sdk.workspaces.list_by_team(team_id=teams[0].id) + + workspace = workspaces[0] + + envs = await workspace.get_env_vars() + print("Current Environment Variables:") + pprint.pprint(envs) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/examples/workspaces/env-vars/set_envvars.py b/examples/workspaces/env-vars/set_envvars.py index e69de29..2ff8f7f 100644 --- a/examples/workspaces/env-vars/set_envvars.py +++ b/examples/workspaces/env-vars/set_envvars.py @@ -0,0 +1,27 @@ +import asyncio +import pprint +from codesphere import CodesphereSDK + + +async def main(): + """Fetches a team and lists all workspaces within it.""" + async with CodesphereSDK() as sdk: + teams = await sdk.teams.list() + workspaces = await sdk.workspaces.list_by_team(team_id=teams[0].id) + + workspace = workspaces[0] + + envs = await workspace.get_env_vars() + print("Current Environment Variables:") + pprint.pprint(envs) + + envs[0].value = "new_value" # Modify an environment variable + await workspace.set_env_vars(envs) # Update the environment variables + + print("Updated Environment Variables:") + updated_envs = await workspace.get_env_vars() + pprint.pprint(updated_envs) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/codesphere/client.py b/src/codesphere/client.py index df8139e..003f151 100644 --- a/src/codesphere/client.py +++ b/src/codesphere/client.py @@ -23,8 +23,11 @@ def __init__(self, base_url: str = "https://codesphere.com/api"): setattr(self, method, partial(self.request, method.upper())) async def __aenter__(self): + timeout_config = httpx.Timeout(10.0, read=30.0) self.client = httpx.AsyncClient( - base_url=self._base_url, headers={"Authorization": f"Bearer {self._token}"} + base_url=self._base_url, + headers={"Authorization": f"Bearer {self._token}"}, + timeout=timeout_config, ) return self diff --git a/src/codesphere/resources/workspace/env-vars/model.py b/src/codesphere/resources/workspace/env-vars/model.py new file mode 100644 index 0000000..105506b --- /dev/null +++ b/src/codesphere/resources/workspace/env-vars/model.py @@ -0,0 +1,90 @@ +from __future__ import annotations +from pydantic import BaseModel, PrivateAttr +from typing import Optional, List, TYPE_CHECKING + +if TYPE_CHECKING: + from ...client import APIHttpClient + + +class EnvVarPair(BaseModel): + name: str + value: str + + +class WorkspaceCreate(BaseModel): + teamId: int + name: str + planId: int + baseImage: Optional[str] = None + isPrivateRepo: bool = True + replicas: int = 1 + gitUrl: Optional[str] = None + initialBranch: Optional[str] = None + cloneDepth: Optional[int] = None + sourceWorkspaceId: Optional[int] = None + welcomeMessage: Optional[str] = None + vpnConfig: Optional[str] = None + restricted: Optional[bool] = None + env: Optional[List[EnvVarPair]] = None + + +# Defines the request body for PATCH /workspaces/{workspaceId} +class WorkspaceUpdate(BaseModel): + planId: Optional[int] = None + baseImage: Optional[str] = None + name: Optional[str] = None + replicas: Optional[int] = None + vpnConfig: Optional[str] = None + restricted: Optional[bool] = None + + +# Defines the response from GET /workspaces/{workspaceId}/status +class WorkspaceStatus(BaseModel): + isRunning: bool + + +# This is the main model for a workspace, returned by GET, POST, and LIST +class Workspace(BaseModel): + _http_client: Optional[APIHttpClient] = PrivateAttr(default=None) + + id: int + teamId: int + name: str + planId: int + isPrivateRepo: bool + replicas: int + baseImage: Optional[str] = None + dataCenterId: int + userId: int + gitUrl: Optional[str] = None + initialBranch: Optional[str] = None + sourceWorkspaceId: Optional[int] = None + welcomeMessage: Optional[str] = None + vpnConfig: Optional[str] = None + restricted: bool + + async def update(self, data: WorkspaceUpdate) -> None: + """Updates this workspace with new data.""" + if not self._http_client: + raise RuntimeError("Cannot make API calls on a detached model.") + + await self._http_client.patch( + f"/workspaces/{self.id}", json=data.model_dump(exclude_unset=True) + ) + # Optionally, update the local object's state + for key, value in data.model_dump(exclude_unset=True).items(): + setattr(self, key, value) + + async def delete(self) -> None: + """Deletes this workspace.""" + if not self._http_client: + raise RuntimeError("Cannot make API calls on a detached model.") + await self._http_client.delete(f"/workspaces/{self.id}") + + async def get_status(self) -> WorkspaceStatus: + """Gets the running status of this workspace.""" + if not self._http_client: + raise RuntimeError("Cannot make API calls on a detached model.") + + response = await self._http_client.get(f"/workspaces/{self.id}/status") + return WorkspaceStatus.model_validate(response.json()) diff --git a/src/codesphere/resources/workspace/env-vars/modely.py b/src/codesphere/resources/workspace/env-vars/modely.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/codesphere/resources/workspace/env-vars/resources.py b/src/codesphere/resources/workspace/env-vars/resources.py index e69de29..58f640a 100644 --- a/src/codesphere/resources/workspace/env-vars/resources.py +++ b/src/codesphere/resources/workspace/env-vars/resources.py @@ -0,0 +1,39 @@ +from typing import List +from ..base import ResourceBase, APIOperation +from .models import Workspace, WorkspaceCreate, WorkspaceUpdate + + +class WorkspacesResource(ResourceBase): + """Manages all API operations for the Workspace resource.""" + + list_by_team = APIOperation( + method="GET", + endpoint_template="/workspaces/team/{team_id}", + response_model=List[Workspace], + ) + + get = APIOperation( + method="GET", + endpoint_template="/workspaces/{workspace_id}", + response_model=Workspace, + ) + + create = APIOperation( + method="POST", + endpoint_template="/workspaces", + input_model=WorkspaceCreate, + response_model=Workspace, + ) + + update = APIOperation( + method="PATCH", + endpoint_template="/workspaces/{workspace_id}", + input_model=WorkspaceUpdate, + response_model=None, + ) + + delete = APIOperation( + method="DELETE", + endpoint_template="/workspaces/{workspace_id}", + response_model=None, + ) diff --git a/src/codesphere/resources/workspace/models.py b/src/codesphere/resources/workspace/models.py index 105506b..93e75fd 100644 --- a/src/codesphere/resources/workspace/models.py +++ b/src/codesphere/resources/workspace/models.py @@ -1,6 +1,6 @@ from __future__ import annotations -from pydantic import BaseModel, PrivateAttr -from typing import Optional, List, TYPE_CHECKING +from pydantic import BaseModel, PrivateAttr, parse_obj_as +from typing import Optional, List, TYPE_CHECKING, Union, Dict if TYPE_CHECKING: from ...client import APIHttpClient @@ -88,3 +88,47 @@ async def get_status(self) -> WorkspaceStatus: response = await self._http_client.get(f"/workspaces/{self.id}/status") return WorkspaceStatus.model_validate(response.json()) + + async def get_env_vars(self) -> list[EnvVarPair]: + """Fetches all environment variables for this workspace.""" + if not self._http_client: + raise RuntimeError("Cannot make API calls on a detached model.") + + response = await self._http_client.get(f"/workspaces/{self.id}/env-vars") + return parse_obj_as(list[EnvVarPair], response.json()) + + async def set_env_vars( + self, env_vars: Union[List[EnvVarPair], List[Dict[str, str]]] + ) -> None: + """ + Sets or updates environment variables for this workspace. + This operation replaces all existing variables with the provided list. + Accepts either a list of EnvVarPair models or a list of dictionaries. + """ + if not self._http_client: + raise RuntimeError("Cannot make API calls on a detached model.") + + json_payload = [] + if env_vars and isinstance(env_vars[0], EnvVarPair): + json_payload = [var.model_dump() for var in env_vars] + else: + json_payload = env_vars + + await self._http_client.put( + f"/workspaces/{self.id}/env-vars", json=json_payload + ) + + async def delete_env_vars( + self, var_names: Union[List[str], List[EnvVarPair]] + ) -> None: + """Deletes specific environment variables from this workspace.""" + if not self._http_client: + raise RuntimeError("Cannot make API calls on a detached model.") + + payload = [] + if var_names and isinstance(var_names[0], EnvVarPair): + payload = [var.name for var in var_names] + else: + payload = var_names + + await self._http_client.delete(f"/workspaces/{self.id}/env-vars", json=payload) From eb07d64c6cf6d222f52b93ef848d68bf7254a37f Mon Sep 17 00:00:00 2001 From: JD <> Date: Tue, 22 Jul 2025 13:51:54 +0200 Subject: [PATCH 2/2] =?UTF-8?q?bump:=20version=200.3.0=20=E2=86=92=200.4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 6 ++++++ pyproject.toml | 2 +- uv.lock | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acb80ed..010b616 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v0.4.0 (2025-07-22) + +### Feat + +- **src/codesphere/resources/workspace/env-vars**: support env-vars endpoints of public api + ## v0.3.0 (2025-07-22) ### Feat diff --git a/pyproject.toml b/pyproject.toml index 9928baa..e695884 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "codesphere" -version = "0.3.0" +version = "0.4.0" description = "Use Codesphere within python scripts." readme = "README.md" license = { file="LICENSE" } diff --git a/uv.lock b/uv.lock index cee40ec..35a3883 100644 --- a/uv.lock +++ b/uv.lock @@ -182,7 +182,7 @@ wheels = [ [[package]] name = "codesphere" -version = "0.3.0" +version = "0.4.0" source = { editable = "." } dependencies = [ { name = "aiohttp" },