chore: add lint/test workflows

This commit is contained in:
2026-01-25 16:21:10 +01:00
parent 762ed35341
commit 8a46a12d15
6 changed files with 132 additions and 15 deletions

100
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,100 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint (Ruff)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Set up Python
run: uv python install 3.13
- name: Install dependencies
run: uv sync --extra dev
- name: Run Ruff linter
run: uv run ruff check .
- name: Run Ruff formatter check
run: uv run ruff format --check .
typecheck:
name: Type Check (ty)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Set up Python
run: uv python install 3.13
- name: Install dependencies
run: uv sync --extra dev
- name: Run ty
run: uv run ty check
test:
name: Test (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13"]
services:
postgres:
image: postgres:18
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
- name: Install dependencies
run: uv sync --extra dev
- name: Run tests with coverage
env:
DATABASE_URL: postgresql+asyncpg://postgres:postgres@localhost:5432/test_db
run: |
uv run pytest --cov --cov-report=xml --cov-report=term-missing
- name: Upload coverage to Codecov
if: matrix.python-version == '3.13'
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
fail_ci_if_error: false

View File

@@ -2,6 +2,8 @@
FastAPI Toolsets provides production-ready utilities for FastAPI applications built with async SQLAlchemy and PostgreSQL. It includes generic CRUD operations, a fixture system with dependency resolution, a Django-like CLI, standardized API responses, and structured exception handling with automatic OpenAPI documentation.
[![CI](https://github.com/d3vyce/fastapi-toolsets/actions/workflows/ci.yml/badge.svg)](https://github.com/d3vyce/fastapi-toolsets/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/d3vyce/fastapi-toolsets/graph/badge.svg)](https://codecov.io/gh/d3vyce/fastapi-toolsets)
[![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)

View File

@@ -76,7 +76,9 @@ def _load_module_from_path(path: Path) -> object:
path = path.resolve()
# Add the parent directory to sys.path to support relative imports
parent_dir = str(path.parent.parent) # Go up two levels (e.g., from app/cli_config.py to project root)
parent_dir = str(
path.parent.parent
) # Go up two levels (e.g., from app/cli_config.py to project root)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)

View File

@@ -44,9 +44,7 @@ def _get_db_context(ctx: typer.Context):
get_db_context = getattr(config, "get_db_context", None)
if get_db_context is None:
raise typer.BadParameter(
"Config module must have a 'get_db_context' function."
)
raise typer.BadParameter("Config module must have a 'get_db_context' function.")
return get_db_context
@@ -56,7 +54,11 @@ def list_fixtures(
ctx: typer.Context,
context: Annotated[
str | None,
typer.Option("--context", "-c", help="Filter by context (base, production, development, testing)."),
typer.Option(
"--context",
"-c",
help="Filter by context (base, production, development, testing).",
),
] = None,
) -> None:
"""List all registered fixtures."""
@@ -110,7 +112,9 @@ def show_graph(
typer.echo("\nFixture Dependency Graph:\n")
for fixture in fixtures:
deps = f" -> [{', '.join(fixture.depends_on)}]" if fixture.depends_on else ""
deps = (
f" -> [{', '.join(fixture.depends_on)}]" if fixture.depends_on else ""
)
typer.echo(f" {fixture.name}{deps}")
@@ -119,15 +123,21 @@ def load(
ctx: typer.Context,
contexts: Annotated[
list[str] | None,
typer.Argument(help="Contexts to load (base, production, development, testing)."),
typer.Argument(
help="Contexts to load (base, production, development, testing)."
),
] = None,
strategy: Annotated[
str,
typer.Option("--strategy", "-s", help="Load strategy: merge, insert, skip_existing."),
typer.Option(
"--strategy", "-s", help="Load strategy: merge, insert, skip_existing."
),
] = "merge",
dry_run: Annotated[
bool,
typer.Option("--dry-run", "-n", help="Show what would be loaded without loading."),
typer.Option(
"--dry-run", "-n", help="Show what would be loaded without loading."
),
] = False,
) -> None:
"""Load fixtures into the database."""
@@ -144,7 +154,9 @@ def load(
try:
load_strategy = LoadStrategy(strategy)
except ValueError:
typer.echo(f"Invalid strategy: {strategy}. Use: merge, insert, skip_existing", err=True)
typer.echo(
f"Invalid strategy: {strategy}. Use: merge, insert, skip_existing", err=True
)
raise typer.Exit(1)
# Resolve what will be loaded
@@ -196,7 +208,9 @@ def show_fixture(
typer.echo(f"\nFixture: {fixture.name}")
typer.echo(f"Contexts: {', '.join(fixture.contexts)}")
typer.echo(f"Dependencies: {', '.join(fixture.depends_on) if fixture.depends_on else 'None'}")
typer.echo(
f"Dependencies: {', '.join(fixture.depends_on) if fixture.depends_on else 'None'}"
)
# Show instances
instances = list(fixture.func())

View File

@@ -62,7 +62,6 @@ from ..db import get_transaction
from .fixtures import FixtureRegistry, LoadStrategy
def register_fixtures(
registry: FixtureRegistry,
namespace: dict[str, Any],

View File

@@ -5,13 +5,13 @@ import os
import pytest
from pydantic import BaseModel
from sqlalchemy import ForeignKey, String
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from fastapi_toolsets.crud import CrudFactory
# PostgreSQL connection URL from environment or default for local development
DATABASE_URL = os.getenv(
DATABASE_URL = os.getenv("DATABASE_URL") or os.getenv(
"TEST_DATABASE_URL",
"postgresql+asyncpg://postgres:postgres@localhost:5432/fastapi_toolsets_test",
)
@@ -149,7 +149,7 @@ async def engine():
@pytest.fixture(scope="function")
async def db_session(engine) -> AsyncSession:
async def db_session(engine):
"""Create a test database session with tables.
Creates all tables before the test and drops them after.