Development Guide¶
This guide covers how to contribute to Ospobox development.
Development Setup¶
# Clone the repository
git clone https://github.com/your-org/ospobox.git
cd ospobox
# Install all dependencies including dev tools
make dev
# Copy environment file
cp .env.example .env
# Run migrations
make migrate
# Start development server
make run
Project Structure¶
ospobox/
├── src/ospobox/
│ ├── app.py # Litestar app factory
│ ├── config.py # Configuration (pydantic-settings)
│ ├── domain/
│ │ ├── models/ # SQLAlchemy models (Nouns)
│ │ └── logic/ # Pure functions (Verbs)
│ ├── persistence/
│ │ ├── repositories/ # Advanced Alchemy repositories
│ │ └── services/ # Advanced Alchemy services
│ ├── api/
│ │ ├── controllers/ # REST API controllers
│ │ └── dtos/ # Data transfer objects
│ ├── web/
│ │ ├── controllers/ # Web page controllers
│ │ └── templates/ # Jinja2 templates
│ ├── extensions/ # Chassis plugin surface (see below)
│ │ └── builtin/ # In-tree evidence sources, scorers, rules
│ ├── services/ # Application services (alerts, health,
│ │ # evidence, webhooks, organizations)
│ ├── platforms/ # Forge adapters (GitHub, GitLab, Codeberg,
│ │ # SourceHut)
│ ├── auth/ # Authentication (JWT, OAuth)
│ ├── workers/ # Background tasks (SAQ)
│ └── lib/ # Shared utilities
├── tests/
│ ├── a_unit/ # Unit tests (~70%)
│ ├── b_integration/ # Integration tests (~20%)
│ └── c_e2e/ # End-to-end tests (~10%)
├── alembic/ # Database migrations
├── docs/ # Documentation (Zensical)
└── notes/ # Project planning documents
Two plugin surfaces¶
platforms/ holds the forge adapters. extensions/ is the broader
chassis surface: evidence sources, scorers and alert-rule evaluators,
all loadable from a third-party package's entry points. Anything
domain-shaped almost certainly belongs there. See
Extension points.
The schema comes from migrations¶
The application does not create tables at startup. Run make migrate
before starting it, and after pulling changes. tests/test_migrations.py
fails if the models and the migrations drift apart, so a new model needs
a migration in the same change.
Make Commands¶
| Command | Description |
|---|---|
make dev |
Install all dependencies |
make run |
Run development server |
make test |
Run test suite |
make lint |
Run linter (ruff) |
make format |
Format code (ruff) |
make typecheck |
Run type checker (ty) |
make check |
Run all checks |
make migrate |
Run database migrations |
make migrate-new MSG="description" |
Create new migration |
make migrate-down |
Rollback last migration |
Code Patterns¶
Nouns and Verbs¶
Ospobox follows a "Nouns and Verbs" pattern:
Models (Nouns) - Hold state, shallow methods:
# domain/models/organizations.py
class MonitoredOrganization(OspoboxBase):
__tablename__ = "monitored_organizations"
name: Mapped[str]
platform: Mapped[str]
account_id: Mapped[UUID] = mapped_column(ForeignKey("accounts.id"))
@property
def is_synced(self) -> bool:
return self.sync_status == SyncStatus.COMPLETED
Logic (Verbs) - Pure functions, no I/O:
# domain/logic/health.py
def calculate_health_score(
commits_30d: int,
prs_30d: int,
issues_30d: int,
contributors_30d: int,
) -> HealthScore:
"""Calculate repository health score from metrics."""
# Pure computation, no database calls
...
Controllers¶
Controllers handle HTTP concerns only:
# api/controllers/organizations.py
class OrganizationsController(Controller):
path = "/api/organizations"
@get("/")
async def list_organizations(
self,
session: AsyncSession,
request: Request,
) -> list[OrganizationDTO]:
# Get user from request
account_id = request.user.account_id
# Query database
orgs = await get_organizations_for_account(session, account_id)
# Return DTOs
return [OrganizationDTO.from_model(org) for org in orgs]
Testing¶
Use pytest with async support:
# tests/a_unit/domain/test_health.py
import pytest
from ospobox.domain.logic.health import calculate_health_score
class TestHealthScore:
def test_healthy_repository(self) -> None:
score = calculate_health_score(
commits_30d=50,
prs_30d=10,
issues_30d=5,
contributors_30d=8,
)
assert score.grade == "A"
assert score.value >= 80
Integration tests with database:
# tests/b_integration/test_api_organizations.py
import pytest
from httpx import AsyncClient
@pytest.mark.anyio
async def test_create_organization(client: AsyncClient, session: AsyncSession):
response = await client.post(
"/api/organizations/",
json={"platform": "github", "name": "test-org"},
)
assert response.status_code == 201
Database Testing¶
Use flush() instead of commit() for test isolation:
async def test_create_user(db_session: AsyncSession):
user = User(email="test@example.com", name="Test")
db_session.add(user)
await db_session.flush() # Not commit!
# Refresh to get server-generated values
await db_session.refresh(user)
assert user.id is not None
assert user.created_at is not None
Running Tests¶
# Run all tests
make test
# Run specific test file
pytest tests/a_unit/domain/test_health.py
# Run with coverage
pytest --cov=ospobox tests/
# Run only unit tests
pytest tests/a_unit/
# Run with verbose output
pytest -v tests/
Code Quality¶
Linting¶
Type Checking¶
Pre-commit Hooks¶
Install pre-commit hooks to catch issues before commit:
Database Migrations¶
Creating Migrations¶
After modifying models:
Review the generated migration in alembic/versions/, then apply:
Rolling Back¶
API Documentation¶
The API documentation is auto-generated from the code:
- Swagger UI: http://localhost:8000/docs
- OpenAPI JSON: http://localhost:8000/schema
Documentation¶
Documentation is built with Zensical:
# Install Zensical
pip install zensical
# Serve documentation locally
zensical serve
# Build static documentation
zensical build
Debugging¶
Debug Mode¶
Enable debug mode for detailed error pages:
SQL Logging¶
Enable SQL query logging:
Development Login¶
In debug mode, use the backdoor login:
- Email:
admin@admin - Password:
admin
Contributing¶
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Make your changes
- Run tests:
make check - Commit with a descriptive message
- Push and create a pull request
Commit Messages¶
Follow conventional commits: