Initial commit: Complete project-bootstrap tool

- Bootstrap script for creating monorepo projects
- FastAPI backend templates with uv, ruff, mypy, pytest
- React frontend templates with TypeScript, ESLint, Prettier
- Docker Compose setup with backend, frontend, and database
- 9 development and CI scripts
- Gitea Actions CI/CD workflows
- Comprehensive documentation (8 files)
- 45 template files for complete project structure
- Automated verification script (all tests pass)
- Based on coding-agent-rules standards
This commit is contained in:
2025-10-15 21:34:08 -04:00
commit 8dd4f0ca63
56 changed files with 3979 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
# Application
APP_NAME=backend
APP_VERSION=0.1.0
DEBUG=true
LOG_LEVEL=INFO
# API
API_HOST=0.0.0.0
API_PORT=8000
API_PREFIX=/api/v1
# CORS
CORS_ORIGINS=http://localhost:3000,http://localhost:8080
# Database (if needed)
# DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/dbname
# Security
# SECRET_KEY=your-secret-key-here
# ALGORITHM=HS256
# ACCESS_TOKEN_EXPIRE_MINUTES=30

View File

@@ -0,0 +1,36 @@
"""Application configuration."""
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Application settings."""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# Application
app_name: str = "backend"
app_version: str = "0.1.0"
debug: bool = False
log_level: str = "INFO"
# API
api_host: str = "0.0.0.0"
api_port: int = 8000
api_prefix: str = "/api/v1"
# CORS
cors_origins: list[str] = ["http://localhost:3000"]
@lru_cache
def get_settings() -> Settings:
"""Get cached settings instance."""
return Settings()

View File

@@ -0,0 +1,58 @@
"""Error handling utilities."""
from typing import Any
from fastapi import HTTPException, status
class AppException(Exception):
"""Base application exception."""
def __init__(self, message: str, details: dict[str, Any] | None = None) -> None:
"""Initialize exception."""
self.message = message
self.details = details or {}
super().__init__(self.message)
class NotFoundError(AppException):
"""Resource not found error."""
pass
class ValidationError(AppException):
"""Validation error."""
pass
class AuthenticationError(AppException):
"""Authentication error."""
pass
def raise_not_found(resource: str, identifier: str | int) -> None:
"""Raise HTTP 404 exception."""
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"{resource} with id '{identifier}' not found",
)
def raise_validation_error(message: str) -> None:
"""Raise HTTP 422 exception."""
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=message,
)
def raise_unauthorized(message: str = "Unauthorized") -> None:
"""Raise HTTP 401 exception."""
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=message,
headers={"WWW-Authenticate": "Bearer"},
)

View File

@@ -0,0 +1,35 @@
"""FastAPI application entry point."""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import get_settings
settings = get_settings()
app = FastAPI(
title=settings.app_name,
version=settings.app_version,
debug=settings.debug,
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def root() -> dict[str, str]:
"""Root endpoint."""
return {"message": "Welcome to the API", "version": settings.app_version}
@app.get("/health")
async def health() -> dict[str, str]:
"""Health check endpoint."""
return {"status": "healthy"}

View File

@@ -0,0 +1,82 @@
[project]
name = "backend"
version = "0.1.0"
description = "FastAPI backend service"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.104.0",
"uvicorn[standard]>=0.24.0",
"pydantic>=2.5.0",
"pydantic-settings>=2.1.0",
"httpx>=0.25.0",
"python-multipart>=0.0.6",
]
[project.optional-dependencies]
dev = [
"pytest>=7.4.0",
"pytest-cov>=4.1.0",
"pytest-asyncio>=0.21.0",
"ruff>=0.1.0",
"mypy>=1.7.0",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = []
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
[tool.mypy]
python_version = "3.11"
strict = true
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"--strict-markers",
"--cov=app",
"--cov-report=term-missing",
"--cov-report=html",
"--cov-fail-under=100",
]
asyncio_mode = "auto"
[tool.coverage.run]
source = ["app"]
omit = ["tests/*", "**/__init__.py"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]

View File

@@ -0,0 +1,12 @@
"""Test configuration and fixtures."""
import pytest
from fastapi.testclient import TestClient
from app.main import app
@pytest.fixture
def client() -> TestClient:
"""Create test client."""
return TestClient(app)

View File

@@ -0,0 +1,22 @@
"""Integration tests for health endpoints."""
from fastapi.testclient import TestClient
def test_health_endpoint(client: TestClient) -> None:
"""Test health check endpoint."""
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "healthy"}
def test_root_endpoint(client: TestClient) -> None:
"""Test root endpoint."""
response = client.get("/")
assert response.status_code == 200
data = response.json()
assert "message" in data
assert "version" in data
assert data["version"] == "0.1.0"

View File

@@ -0,0 +1,24 @@
"""Tests for core configuration."""
from app.core.config import Settings, get_settings
def test_settings_default_values() -> None:
"""Test that settings have correct default values."""
settings = Settings()
assert settings.app_name == "backend"
assert settings.app_version == "0.1.0"
assert settings.debug is False
assert settings.log_level == "INFO"
assert settings.api_host == "0.0.0.0"
assert settings.api_port == 8000
assert settings.api_prefix == "/api/v1"
def test_get_settings_returns_cached_instance() -> None:
"""Test that get_settings returns the same instance."""
settings1 = get_settings()
settings2 = get_settings()
assert settings1 is settings2