Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
types: [opened, synchronize, reopened, ready_for_review, draft]
paths:
- 'src/codesphere/**'
- 'scripts/**'
- '.github/workflows/ci.yml'
- 'tests/**'
- 'pyproject.toml'
Expand Down Expand Up @@ -39,6 +40,15 @@ jobs:
run: uv run ty check
shell: bash

- name: Verify generated sync client is up to date
run: |
uv run python scripts/gen_sync.py
git diff --exit-code --stat src/codesphere/_sync || {
echo "::error::src/codesphere/_sync is stale. Run 'make sync-gen' and commit the result."
exit 1
}
shell: bash

- name: Minimize uv cache
run: uv cache prune --ci

Expand Down
45 changes: 45 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Docs

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Build docs
run: |
uv sync --group docs
uv run mkdocs build --strict

- name: Upload pages artifact
uses: actions/upload-pages-artifact@v3
with:
path: site

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ __marimo__
.coverage
coverage.xml
test-results/
# MkDocs build output
site/
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ repos:
language: system
types: [python]
pass_filenames: false
- id: sync-gen
name: regenerate sync client
entry: uv run python scripts/gen_sync.py
language: system
files: ^(src/codesphere/_async/|src/codesphere/_sync/|scripts/gen_sync\.py|pyproject\.toml)
pass_filenames: false
43 changes: 39 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
## Unreleased

### Added

- Synchronous client: `from codesphere.sync import CodesphereSDK` mirrors
the async API with identical signatures (generated from the async
source via unasyncd; parity is enforced in CI). Request/response
models are shared between both flavors via the new `codesphere.models`
package.
- Per-call timeout: every API method now accepts a keyword-only
`timeout=` (float or `httpx.Timeout`) that overrides the client-wide
timeout for that call only (applied per retry attempt).
- Hosted documentation site (mkdocs-material + mkdocstrings) with
quickstart, guides, full API reference, and `llms.txt`; deployed to
GitHub Pages on pushes to `main`.

### Changed

- Retries on transient failures are now enabled by default
(`max_retries=2`). Idempotent methods (`GET`, `HEAD`, `PUT`, `DELETE`)
are retried on `429`/`502`/`503`/`504` and connect/timeout errors.
Set `RetryConfig(max_retries=0)` to restore the previous opt-in
behavior. `POST` is still never retried unless explicitly added to
`retry_methods`.
- Debug/error logs no longer include request or response bodies (they can
contain secrets such as env-var values); only structural information is
logged.
- Package classifiers: `Development Status :: 5 - Production/Stable` and
`Typing :: Typed`.

### Removed

- The deprecated module path `codesphere.resources.workspace.envVars`
(use `codesphere.resources.workspace.env_vars`).

## v1.0.0 (2026-02-21)

### Features
Expand All @@ -7,10 +42,10 @@
- feat(git): add git resource (#45)
- feat(landscape): add landscape resource (#41)
- feat(model-export): provide model methods to export data (#40)
- feat(excetions): add base exceptions (#39)
- feat(exceptions): add base exceptions (#39)
- feat(agent): add copilot instructions (#35)
- feat(domain): add domains resource (#33)
- feat(workspace): impleöment base operations for workspaces (#32)
- feat(workspace): implement base operations for workspaces (#32)

### Refactors

Expand All @@ -25,7 +60,7 @@
- refac(commit-hook): remove commitizen (#38)
- test: fix ci
- test(suite): set up initial testsuite (#36)
- chore(test): bla bla (#28)
- chore(test): test maintenance (#28)
- chore(types): clean types annotations (#10)

## v0.4.0 (2025-07-22)
Expand All @@ -44,7 +79,7 @@

### Fix

- **dsd**: sdfsf
- internal CI fixes

## v0.2.2 (2025-07-21)

Expand Down
50 changes: 50 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,56 @@ versions.

---

## Gating an Endpoint Behind a Feature Flag

Platform instances enable features via flags (`GET /ide-service/flags`,
three categories: `internal`, `preview`, `features`). If an SDK feature only
works when a flag is enabled, declare it on the operation — **always with an
explicit category**:

```python
from ...core.operations import APIOperation
from ...feature_flags import FlagCategory, FlagRequirement

_LIST_VPN_CONFIGS_OP = APIOperation(
method="GET",
endpoint_template="/vpn/configs",
response_model=ResourceList[VpnConfig],
required_flag=FlagRequirement("vpn", FlagCategory.INTERNAL),
)
```

Nothing else is needed: `execute_operation` checks the flag **before any
request** and raises `FeatureNotAvailableError` (flag missing on this
instance) or `FeatureNotEnabledError` (available but off). The flags
snapshot is fetched once per client and cached; `await sdk.flags.refresh()`
re-fetches. Instances without the flags endpoint fail closed.

For code paths that don't go through an `APIOperation` (SSE streams,
shell-command features), check explicitly:

```python
await self._require_flag(FlagRequirement("workspace-ssh", FlagCategory.PREVIEW))
```

For async generators, perform this check in a plain `async def` factory
*before* returning the generator, so the call fails fast instead of at the
first iteration.

**Testing a gated endpoint:** register a respx route for
`/ide-service/flags` alongside the endpoint route, and assert the endpoint
route was **not** called when the flag is disabled:

```python
mock_api.get("/ide-service/flags").respond(200, json={"preview": {"available": ["x"], "enabled": []}})
route = mock_api.get("/gated").respond(200, json={})
with pytest.raises(FeatureNotEnabledError):
await resource.gated_call()
assert route.called is False
```

---

## Testing Guidelines

We maintain two types of tests: **unit tests** and **integration tests**. When contributing, please ensure appropriate test coverage for your changes.
Expand Down
18 changes: 16 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help install lint format typecheck test test-integration test-unit bump release pypi tree version changelog
.PHONY: help install lint format typecheck test test-integration test-unit bump release pypi tree version changelog docs docs-build sync-gen sync-check

.DEFAULT_GOAL := help

Expand Down Expand Up @@ -61,7 +61,7 @@ bump: ## Bumps the version. Usage: make bump VERSION=0.5.0
exit 1; \
fi
@echo ">>> Bumping version from $(CURRENT_VERSION) to $(VERSION)..."
@sed -i '' 's/^version = ".*"/version = "$(VERSION)"/' pyproject.toml
@sed -i.bak 's/^version = ".*"/version = "$(VERSION)"/' pyproject.toml && rm -f pyproject.toml.bak
@echo ">>> Updating lockfile..."
uv lock
@echo "\033[0;32mVersion updated to $(VERSION) in pyproject.toml\033[0m"
Expand Down Expand Up @@ -144,5 +144,19 @@ pypi: ## Builds and publishes to PyPI (usually called by CI)
uv publish
@echo "\n\033[0;32mPyPI release complete!\033[0m"

sync-gen: ## Regenerates the sync client (src/codesphere/_sync) from the async source
uv run python scripts/gen_sync.py

sync-check: ## Verifies the generated sync client is up to date with the async source
uv run python scripts/gen_sync.py
@git diff --exit-code --stat src/codesphere/_sync \
|| (echo "\033[0;31mERROR: src/codesphere/_sync is stale. Run 'make sync-gen' and commit the result.\033[0m"; exit 1)

docs: ## Serves the documentation locally with live reload
uv run --group docs mkdocs serve

docs-build: ## Builds the documentation (strict: fails on broken references)
uv run --group docs mkdocs build --strict

tree: ## Shows filetree in terminal without uninteresting files
tree -I "*.pyc|*.lock"
53 changes: 45 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,28 +60,47 @@ any environment variable).

### Retries

Retries are off by default. Opt in with a `RetryConfig`:
Transient failures are retried automatically (2 retries by default).
Requests using an idempotent method (`GET`, `HEAD`, `PUT`, `DELETE`) are
retried on `429`, `502`, `503`, and `504` responses as well as on
connect/timeout errors. The delay honors the server's `Retry-After` header
(seconds or HTTP-date) when present, otherwise exponential backoff with
jitter (`backoff_factor * 2**attempt`).

Tune or disable the behavior with a `RetryConfig`:

```python
from codesphere import CodesphereSDK, RetryConfig

sdk = CodesphereSDK(
token="your-api-token",
retry=RetryConfig(max_retries=3),
retry=RetryConfig(max_retries=0), # disable retries entirely
)
```

Requests using an idempotent method (`GET`, `HEAD`, `PUT`, `DELETE`) are then
retried on `429`, `502`, `503`, and `504` responses as well as on
connect/timeout errors. The delay honors the server's `Retry-After` header
(seconds or HTTP-date) when present, otherwise exponential backoff with
jitter (`backoff_factor * 2**attempt`). `POST` is never retried unless you
add it to `retry_methods` explicitly:
`POST` is never retried unless you add it to `retry_methods` explicitly
(only do this if your endpoints tolerate duplicate submissions):

```python
RetryConfig(max_retries=3, retry_methods=frozenset({"GET", "POST"}))
```

### Feature flags

Platform instances enable different features. Inspect them via `sdk.flags`:

```python
snapshot = await sdk.flags.get() # fetched once, then cached
if await sdk.flags.is_enabled("workspace-ssh"):
...

await sdk.flags.refresh() # re-fetch after platform changes
```

SDK features that depend on a platform flag raise `FeatureNotEnabledError`
(or `FeatureNotAvailableError` if the instance doesn't have the feature at
all) *before* sending any request when the flag is off.

## Quick Start

```python
Expand All @@ -97,6 +116,24 @@ async def main():
asyncio.run(main())
```

### Synchronous client

The same API is available without `async`/`await` — ideal for scripts and
tools that don't run an event loop:

```python
from codesphere.sync import CodesphereSDK

with CodesphereSDK() as sdk:
teams = sdk.teams.list()
for team in teams:
print(f"{team.name} (ID: {team.id})")
```

Both flavors share the same request/response models, configuration, and
error types; only the entities with API methods (`Workspace`, `Team`, …)
exist per flavor.

## Usage

### Teams
Expand Down
3 changes: 3 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Changelog

--8<-- "CHANGELOG.md"
61 changes: 61 additions & 0 deletions docs/guides/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Configuration

Every option can be passed directly to the constructor:

```python
from codesphere import CodesphereSDK

sdk = CodesphereSDK(token="your-api-token")
# or, e.g. against a different deployment:
sdk = CodesphereSDK(
token="your-api-token",
base_url="https://my-instance.example.com/api",
)
```

Alternatively, configure via environment variables (or a `.env` file in your
project root). Explicit constructor arguments always win over the environment:

| Variable | Description | Default |
|----------|-------------|---------|
| `CS_TOKEN` | API token (required) | – |
| `CS_BASE_URL` | API base URL | `https://codesphere.com/api` |
| `CS_CLIENT_TIMEOUT_CONNECT` | Connect timeout in seconds | `10.0` |
| `CS_CLIENT_TIMEOUT_READ` | Read timeout in seconds | `30.0` |

If no token is provided at all, `CodesphereSDK()` raises an
`AuthenticationError` at construction (importing the package never requires
any environment variable).

## Client lifecycle

The client owns an HTTP connection pool. Use it as an async context manager
(recommended) or open/close it explicitly:

```python
async with CodesphereSDK() as sdk:
...

# or
sdk = CodesphereSDK()
await sdk.open()
try:
...
finally:
await sdk.close()
```

## Logging

The SDK logs to the `codesphere` logger (a `NullHandler` is installed by
default). Enable debug output to see requests, responses, and retries:

```python
import logging

logging.getLogger("codesphere").setLevel(logging.DEBUG)
logging.basicConfig()
```

Request and response bodies are never logged — payloads such as env-var
values can contain secrets.
Loading
Loading