mirror of
https://github.com/d3vyce/fastapi-toolsets.git
synced 2026-03-01 17:00:48 +01:00
Merge pull request #1 from d3vyce/add_workflows
chore: add lint/test workflows
This commit is contained in:
100
.github/workflows/ci.yml
vendored
Normal file
100
.github/workflows/ci.yml
vendored
Normal 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
|
||||||
@@ -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.
|
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.
|
||||||
|
|
||||||
|
[](https://github.com/d3vyce/fastapi-toolsets/actions/workflows/ci.yml)
|
||||||
|
[](https://codecov.io/gh/d3vyce/fastapi-toolsets)
|
||||||
[](https://github.com/astral-sh/ty)
|
[](https://github.com/astral-sh/ty)
|
||||||
[](https://github.com/astral-sh/uv)
|
[](https://github.com/astral-sh/uv)
|
||||||
[](https://github.com/astral-sh/ruff)
|
[](https://github.com/astral-sh/ruff)
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ def _load_module_from_path(path: Path) -> object:
|
|||||||
path = path.resolve()
|
path = path.resolve()
|
||||||
|
|
||||||
# Add the parent directory to sys.path to support relative imports
|
# 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:
|
if parent_dir not in sys.path:
|
||||||
sys.path.insert(0, parent_dir)
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
|
|||||||
@@ -44,9 +44,7 @@ def _get_db_context(ctx: typer.Context):
|
|||||||
|
|
||||||
get_db_context = getattr(config, "get_db_context", None)
|
get_db_context = getattr(config, "get_db_context", None)
|
||||||
if get_db_context is None:
|
if get_db_context is None:
|
||||||
raise typer.BadParameter(
|
raise typer.BadParameter("Config module must have a 'get_db_context' function.")
|
||||||
"Config module must have a 'get_db_context' function."
|
|
||||||
)
|
|
||||||
|
|
||||||
return get_db_context
|
return get_db_context
|
||||||
|
|
||||||
@@ -56,7 +54,11 @@ def list_fixtures(
|
|||||||
ctx: typer.Context,
|
ctx: typer.Context,
|
||||||
context: Annotated[
|
context: Annotated[
|
||||||
str | None,
|
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,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""List all registered fixtures."""
|
"""List all registered fixtures."""
|
||||||
@@ -110,7 +112,9 @@ def show_graph(
|
|||||||
|
|
||||||
typer.echo("\nFixture Dependency Graph:\n")
|
typer.echo("\nFixture Dependency Graph:\n")
|
||||||
for fixture in fixtures:
|
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}")
|
typer.echo(f" {fixture.name}{deps}")
|
||||||
|
|
||||||
|
|
||||||
@@ -119,15 +123,21 @@ def load(
|
|||||||
ctx: typer.Context,
|
ctx: typer.Context,
|
||||||
contexts: Annotated[
|
contexts: Annotated[
|
||||||
list[str] | None,
|
list[str] | None,
|
||||||
typer.Argument(help="Contexts to load (base, production, development, testing)."),
|
typer.Argument(
|
||||||
|
help="Contexts to load (base, production, development, testing)."
|
||||||
|
),
|
||||||
] = None,
|
] = None,
|
||||||
strategy: Annotated[
|
strategy: Annotated[
|
||||||
str,
|
str,
|
||||||
typer.Option("--strategy", "-s", help="Load strategy: merge, insert, skip_existing."),
|
typer.Option(
|
||||||
|
"--strategy", "-s", help="Load strategy: merge, insert, skip_existing."
|
||||||
|
),
|
||||||
] = "merge",
|
] = "merge",
|
||||||
dry_run: Annotated[
|
dry_run: Annotated[
|
||||||
bool,
|
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,
|
] = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Load fixtures into the database."""
|
"""Load fixtures into the database."""
|
||||||
@@ -144,7 +154,9 @@ def load(
|
|||||||
try:
|
try:
|
||||||
load_strategy = LoadStrategy(strategy)
|
load_strategy = LoadStrategy(strategy)
|
||||||
except ValueError:
|
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)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
# Resolve what will be loaded
|
# Resolve what will be loaded
|
||||||
@@ -196,7 +208,9 @@ def show_fixture(
|
|||||||
|
|
||||||
typer.echo(f"\nFixture: {fixture.name}")
|
typer.echo(f"\nFixture: {fixture.name}")
|
||||||
typer.echo(f"Contexts: {', '.join(fixture.contexts)}")
|
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
|
# Show instances
|
||||||
instances = list(fixture.func())
|
instances = list(fixture.func())
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ from ..db import get_transaction
|
|||||||
from .fixtures import FixtureRegistry, LoadStrategy
|
from .fixtures import FixtureRegistry, LoadStrategy
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def register_fixtures(
|
def register_fixtures(
|
||||||
registry: FixtureRegistry,
|
registry: FixtureRegistry,
|
||||||
namespace: dict[str, Any],
|
namespace: dict[str, Any],
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import os
|
|||||||
import pytest
|
import pytest
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import ForeignKey, String
|
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 sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from fastapi_toolsets.crud import CrudFactory
|
from fastapi_toolsets.crud import CrudFactory
|
||||||
|
|
||||||
# PostgreSQL connection URL from environment or default for local development
|
# 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",
|
"TEST_DATABASE_URL",
|
||||||
"postgresql+asyncpg://postgres:postgres@localhost:5432/fastapi_toolsets_test",
|
"postgresql+asyncpg://postgres:postgres@localhost:5432/fastapi_toolsets_test",
|
||||||
)
|
)
|
||||||
@@ -149,7 +149,7 @@ async def engine():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
async def db_session(engine) -> AsyncSession:
|
async def db_session(engine):
|
||||||
"""Create a test database session with tables.
|
"""Create a test database session with tables.
|
||||||
|
|
||||||
Creates all tables before the test and drops them after.
|
Creates all tables before the test and drops them after.
|
||||||
|
|||||||
Reference in New Issue
Block a user