Skip to content

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

# Check for issues
make lint

# Auto-fix issues
make format

Type Checking

make typecheck

Pre-commit Hooks

Install pre-commit hooks to catch issues before commit:

pre-commit install

Database Migrations

Creating Migrations

After modifying models:

make migrate-new MSG="add user preferences"

Review the generated migration in alembic/versions/, then apply:

make migrate

Rolling Back

make migrate-down  # Roll back one migration

API Documentation

The API documentation is auto-generated from the code:

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:

DEBUG=true make run

SQL Logging

Enable SQL query logging:

DATABASE_ECHO=true make run

Development Login

In debug mode, use the backdoor login:

  • Email: admin@admin
  • Password: admin

Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Make your changes
  4. Run tests: make check
  5. Commit with a descriptive message
  6. Push and create a pull request

Commit Messages

Follow conventional commits:

feat: add user preferences page
fix: correct health score calculation
docs: update API documentation
refactor: extract service layer
test: add tests for alert generation