Thank you for your interest in contributing to Syfthub! This document provides guidelines and instructions for contributing.
- Fork the repository and clone your fork:
git clone https://github.com/YOUR_USERNAME/syfthub.git
cd syfthub- Install uv if you haven't already:
curl -LsSf https://astral.sh/uv/install.sh | sh- Install the development dependencies:
uv sync --dev- Install pre-commit hooks:
uv run pre-commit installWe use Ruff for both linting and formatting. The configuration is in pyproject.toml.
Before committing:
- Format your code:
uv run ruff format src/ tests/ - Check linting:
uv run ruff check src/ tests/ - Run type checking:
uv run mypy src/
All 4xx/5xx API responses use a consistent shape:
{
"detail": {
"code": "MACHINE_READABLE_CODE",
"message": "Human-readable description",
"<optional_extra_field>": "..."
}
}The outer "detail" key is Starlette's standard envelope. The inner object is always an ErrorResponse with at minimum code and message.
Raise domain exceptions from services and domain logic. Never import HTTPException from FastAPI in the service or domain layers:
# ✅ CORRECT — raise from service
from syfthub.domain.exceptions import NotFoundError, PermissionDeniedError, ConflictError
raise NotFoundError("User") # → 404
raise PermissionDeniedError("Admin role required") # → 403
raise ConflictError("user", "username") # → 409The DomainException handler in observability/handlers.py auto-maps these to HTTP responses via DOMAIN_EXCEPTION_STATUS_MAP.
Raise HTTPException directly only at the transport boundary (endpoints / auth dependencies) for errors that are not domain-level:
# ✅ CORRECT — raise at endpoint boundary with structured detail
raise HTTPException(
status_code=400,
detail={"code": "MISSING_AUDIENCE", "message": "The 'aud' query parameter is required."},
)# ❌ WRONG — plain string detail
raise HTTPException(status_code=400, detail="Missing audience")
# ❌ WRONG — raising HTTPException from a service
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="User not found")- Add the class to
components/backend/src/syfthub/domain/exceptions.py— follow existing patterns. - Add the exception → HTTP status mapping to
DOMAIN_EXCEPTION_STATUS_MAPinobservability/handlers.py. - Export the new exception from
domain/__init__.pyif callers need it. - Add unit tests to
tests/test_domain/test_exceptions.py.
auth/router.py:register_user is the reference for how to handle domain exceptions at the router boundary when you need to catch specific exceptions rather than letting them bubble up.
- No bare
except Exception: pass— always log before swallowing. - No raw exception strings in responses — log server-side, return generic messages to clients.
- No plain strings in
detail— always use{"code": "...", "message": "..."}.
All code changes should include tests. We use pytest for testing.
- Run all tests:
uv run pytest - Run with coverage:
uv run pytest --cov - Run specific test file:
uv run pytest tests/test_main.py
Please use clear and descriptive commit messages:
- Use present tense ("Add feature" not "Added feature")
- Use imperative mood ("Move cursor to..." not "Moves cursor to...")
- Limit first line to 72 characters
- Reference issues and pull requests when relevant
- Create a new branch for your feature or bugfix:
git checkout -b feature/your-feature-name-
Make your changes and ensure all tests pass
-
Run the full test suite:
uv run pre-commit run --all-files
uv run pytest
uv run mypy src/-
Commit your changes with a descriptive message
-
Push to your fork and create a pull request
-
Ensure all CI checks pass
-
Request review from maintainers
All submissions require review. We use GitHub pull requests for this purpose.
Feel free to submit issues and enhancement requests.
Feel free to open an issue with your question or reach out to the maintainers.
Thank you for contributing!