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
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
CS_TOKEN="secret_token"
CS_TOKEN=your-api-token-here
# CS_BASE_URL=https://codesphere.com/api
# CS_TEST_TEAM_ID=12345
# CS_TEST_DC_ID=2
163 changes: 50 additions & 113 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,116 +1,53 @@
# Copilot Instructions for Codesphere Python SDK
# Codesphere SDK - AI Instructions

## Project Context

This repository contains the **Codesphere Python SDK**, an official asynchronous Python client for the [Codesphere Public API](https://codesphere.com/api/swagger-ui/). It provides a high-level, type-safe interface for managing Codesphere resources including Teams, Workspaces, Domains, Environment Variables, and Metadata.

The SDK follows a resource-based architecture where API operations are defined declaratively and executed through a centralized handler system.
## Architecture & Core
- **Async-First:** All I/O operations MUST be `async/await`.
- **Base Classes:**
- Models: Use `core.base.CamelModel` (handles camelCase API conversion).
- Resources: Inherit from `ResourceBase` + `_APIOperationExecutor`.
- **HTTP:** Use `APIHttpClient` (wraps httpx). Raise `httpx.HTTPStatusError` for API errors.

## Project Structure

```
src/codesphere/
├── __init__.py # Public API exports
├── client.py # Main SDK entry point (CodesphereSDK)
├── config.py # Settings via pydantic-settings
├── exceptions.py # Custom exception classes
├── http_client.py # Async HTTP client wrapper (APIHttpClient)
├── utils.py # Utility functions
├── core/ # Core SDK infrastructure
│ ├── base.py # Base classes (ResourceBase, CamelModel, ResourceList)
│ ├── handler.py # API operation executor (_APIOperationExecutor, APIRequestHandler)
│ └── operations.py # APIOperation definition and AsyncCallable type
└── resources/ # API resource implementations
├── metadata/ # Datacenters, Plans, Images
├── team/ # Teams and nested Domains
│ └── domain/ # Domain management (schemas, operations, manager)
└── workspace/ # Workspaces and nested resources
├── envVars/ # Environment variables management
├── landscape/ # (Placeholder)
└── pipeline/ # (Placeholder)

tests/ # Test files mirroring src structure
examples/ # Usage examples organized by resource type
```

## Coding Guidelines

### General Principles

- **Async-First**: All API operations MUST be async. Use `async/await` syntax consistently.
- **Type Hints**: Always provide complete type annotations for function parameters, return values, and class attributes.
- **Pydantic Models**: Use Pydantic `BaseModel` for all data structures. Prefer `CamelModel` for API payloads to handle camelCase conversion.

### Naming Conventions

- **Variables/Functions**: Use `snake_case` (e.g., `workspace_id`, `list_datacenters`)
- **Classes**: Use `PascalCase` (e.g., `WorkspaceCreate`, `APIHttpClient`)
- **Constants/Operations**: Use `UPPER_SNAKE_CASE` with leading underscore for internal operations (e.g., `_LIST_BY_TEAM_OP`)
- **Private Attributes**: Prefix with underscore (e.g., `_http_client`, `_token`)

### Resource Pattern

When adding new API resources, follow this structure:

1. **schemas.py**: Define Pydantic models for Create, Base, Update, and the main resource class
2. **operations.py**: Define `APIOperation` instances for each endpoint
3. **resources.py**: Create the resource class extending `ResourceBase` with operation fields
4. **__init__.py**: Export public classes

```python
# Example operation definition
_GET_OP = APIOperation(
method="GET",
endpoint_template="/resources/{resource_id}",
response_model=ResourceModel,
)

# Example resource method
async def get(self, resource_id: int) -> ResourceModel:
return await self.get_op(data=resource_id)
```

### Model Guidelines

- Extend `CamelModel` for API request/response models (automatic camelCase aliasing)
- Extend `_APIOperationExecutor` for models that can perform operations on themselves
- Use `Field(default=..., exclude=True)` for operation callables
- Use `@cached_property` for lazy-loaded sub-managers (e.g., `workspace.env_vars`)

```python
class Workspace(WorkspaceBase, _APIOperationExecutor):
delete_op: AsyncCallable[None] = Field(default=_DELETE_OP, exclude=True)

async def delete(self) -> None:
await self.delete_op()
```

### Error Handling

- Raise `httpx.HTTPStatusError` for HTTP errors (handled by `APIHttpClient`)
- Raise `RuntimeError` for SDK misuse (e.g., accessing resources without context manager)
- Use custom exceptions from `exceptions.py` for SDK-specific errors

### Testing

- Use `pytest.mark.asyncio` for async tests
- Use `@dataclass` for test case definitions with parametrization
- Mock `httpx.AsyncClient` for HTTP request testing
- Test files should mirror the source structure in `tests/`

### Code Style

- Line length: 88 characters (Ruff/Black standard)
- Indentation: 4 spaces
- Quotes: Double quotes for strings
- Imports: Group stdlib, third-party, and local imports

### Development Commands

```bash
make install # Set up development environment
make lint # Run Ruff linter
make format # Format code with Ruff
make test # Run pytest
make commit # Guided commit with Commitizen
```
- `src/codesphere/core/`: Base classes & handlers.
- `src/codesphere/resources/`: Resource implementations (follow `schemas.py`, `operations.py`, `resources.py` pattern).
- `tests/integration/`: Real API tests (require `CS_TOKEN`).
- `tests/unit/`: Mocked logic tests.

## Resource Implementation Pattern
When adding resources, strictly follow this pattern:

1. **`schemas.py`**: Define Pydantic models (inherit `CamelModel`).
2. **`operations.py`**: Define `APIOperation` constants.
```python
_GET_OP = APIOperation(method="GET", endpoint_template="/res/{id}", response_model=ResModel)
```
3. **`resources.py`**: Implementation logic.
```python
class MyResource(ResourceBase, _APIOperationExecutor):
# Operation callable (exclude from model dump)
delete_op: AsyncCallable[None] = Field(default=_DELETE_OP, exclude=True)

async def delete(self) -> None:
await self.delete_op()
```

## Testing Rules
- **Unit Tests (`tests/`):** MUST mock all HTTP calls (`unittest.mock.AsyncMock`).
- **Integration Tests (`tests/integration/`):**
- Use `pytest.mark.integration`.
- Use provided fixtures: `sdk_client` (fresh client), `test_team_id`, `test_workspace`.
- **Cleanup:** Always delete created resources in a `try...finally` block.

## Key Fixtures & Env Vars
| Fixture | Scope | Description |
|---|---|---|
| `sdk_client` | func | Fresh `CodesphereSDK` instance. |
| `test_team_id` | session | ID from `CS_TEST_TEAM_ID` (or default). |
| `integration_token` | session | Token from `CS_TOKEN`. |

## Style & Naming
- **Classes:** PascalCase (`WorkspaceCreate`).
- **Vars:** snake_case.
- **Internal Ops:** UPPER_SNAKE (`_LIST_OP`).
- **Private:** Leading underscore (`_http_client`).
- **Typing:** Strict type hints required.
207 changes: 120 additions & 87 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Python tests
name: Python CI

on:
pull_request:
Expand All @@ -13,96 +13,129 @@ permissions:
pull-requests: write

jobs:
pytest:
security_check:
name: Security Check (Bandit)
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install uv package manager
uses: astral-sh/setup-uv@v6
with:
activate-environment: true

- name: Install dependencies
run: uv sync --extra dev
shell: bash

- name: Run Bandit security check
id: bandit_check
run: |
echo "Running Bandit security check..."
set +e
uv run bandit -r src/codesphere --format=custom --msg-template "{abspath}:{line}: {test_id}[{severity}]: {msg}" -o bandit-results.txt
BANDIT_EXIT_CODE=$?
set -e

echo "Bandit scan finished. Exit code: $BANDIT_EXIT_CODE"

# Zeige Ergebnisse im Log an
if [ -f bandit-results.txt ]; then
cat bandit-results.txt
fi

echo "BANDIT_EXIT_CODE=${BANDIT_EXIT_CODE}" >> $GITHUB_ENV
shell: bash

- name: Prepare Bandit comment body
id: prep_bandit_comment
if: github.event_name == 'pull_request'
run: |
echo "Preparing Bandit comment body..."
COMMENT_BODY_FILE="bandit-comment-body.md"
echo "COMMENT_BODY_FILE=${COMMENT_BODY_FILE}" >> $GITHUB_ENV

echo "### 🛡️ Bandit Security Scan Results" > $COMMENT_BODY_FILE
echo "" >> $COMMENT_BODY_FILE

# WICHTIG: Hier wurde der Pfad korrigiert (das 'backend/' Prefix entfernt)
if [ -s bandit-results.txt ]; then
echo "\`\`\`text" >> $COMMENT_BODY_FILE
cat bandit-results.txt >> $COMMENT_BODY_FILE
echo "\`\`\`" >> $COMMENT_BODY_FILE
else
echo "✅ No security issues found by Bandit." >> $COMMENT_BODY_FILE
fi
shell: bash

- name: Find Comment
uses: peter-evans/find-comment@v3
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: Bandit Security Scan Results

- name: Post Bandit results as PR comment
if: github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.fc.outputs.comment-id }}
body-file: ${{ env.COMMENT_BODY_FILE }}
edit-mode: replace

- name: Fail if Bandit found issues
if: env.BANDIT_EXIT_CODE != '0'
run: exit ${{ env.BANDIT_EXIT_CODE }}

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

pytest:
name: Python Tests
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
env:
CS_TOKEN: 'dummy-token-for-ci'

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install uv package manager
uses: astral-sh/setup-uv@v6
with:
activate-environment: true

- name: Install dependencies using uv
run: |
uv sync --extra dev
shell: bash

- name: Run Bandit security check on backend code
id: bandit_check
run: |
echo "Running Bandit security check..."
set +e
bandit -r . -c pyproject.toml --format=custom --msg-template "{abspath}:{line}: {test_id}[{severity}]: {msg}" -o bandit-results.txt
cat bandit-results.txt
BANDIT_EXIT_CODE=$?
set -e
echo "Bandit scan finished. Exit code: $BANDIT_EXIT_CODE"
echo "BANDIT_EXIT_CODE=${BANDIT_EXIT_CODE}" >> $GITHUB_ENV
shell: bash

- name: Prepare Bandit comment body
id: prep_bandit_comment
if: github.event_name == 'pull_request'
run: |
echo "Preparing Bandit comment body..."
COMMENT_BODY_FILE="bandit-comment-body.md"
echo "COMMENT_BODY_FILE=${COMMENT_BODY_FILE}" >> $GITHUB_ENV

echo "### 🛡️ Bandit Security Scan Results" > $COMMENT_BODY_FILE
echo "" >> $COMMENT_BODY_FILE
echo "" >> $COMMENT_BODY_FILE
echo "" >> $COMMENT_BODY_FILE

if [ -s backend/bandit-results.txt ]; then
echo "\`\`\`text" >> $COMMENT_BODY_FILE
cat backend/bandit-results.txt >> $COMMENT_BODY_FILE
echo "\`\`\`" >> $COMMENT_BODY_FILE
else
echo "✅ No security issues found by Bandit." >> $COMMENT_BODY_FILE
fi
shell: bash

- name: Find Comment
uses: peter-evans/find-comment@v3
id: fc
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: Bandit Security Scan Results

- name: Post Bandit results as PR comment
if: github.event_name == 'pull_request' && always()
uses: peter-evans/create-or-update-comment@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.fc.outputs.comment-id }}
body-file: ${{ env.COMMENT_BODY_FILE }}
edit-mode: replace

- name: Run tests with pytest using uv
run: |
pytest --junitxml=junit/test-results.xml --cov-report=xml --cov-report=html --cov=. | tee pytest-coverage.txt
shell: bash

- name: Pytest coverage comment
if: github.event_name == 'pull_request' && always()
uses: MishaKav/pytest-coverage-comment@main
with:
unique-id-for-comment: coverage-report
pytest-xml-coverage-path: coverage.xml
pytest-coverage-path: pytest-coverage.txt
junitxml-path: junit/test-results.xml
title: Pytest Coverage Report
junitxml-title: Test Execution Summary

- name: Minimize uv cache
run: uv cache prune --ci
- name: Checkout repository
uses: actions/checkout@v4

- name: Install uv package manager
uses: astral-sh/setup-uv@v6
with:
activate-environment: true

- name: Install dependencies
run: uv sync --extra dev
shell: bash

- name: Run tests with pytest
run: |
uv run pytest --junitxml=junit/test-results.xml --cov-report=xml --cov-report=html --cov=. --ignore=tests/integration | tee pytest-coverage.txt
shell: bash

- name: Pytest coverage comment
if: github.event_name == 'pull_request' && always()
uses: MishaKav/pytest-coverage-comment@main
with:
unique-id-for-comment: coverage-report
pytest-xml-coverage-path: coverage.xml
pytest-coverage-path: pytest-coverage.txt
junitxml-path: junit/test-results.xml
title: Pytest Coverage Report
junitxml-title: Test Execution Summary

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