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:
21
templates/backend/.env.example
Normal file
21
templates/backend/.env.example
Normal 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
|
||||
36
templates/backend/app/core/config.py
Normal file
36
templates/backend/app/core/config.py
Normal 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()
|
||||
58
templates/backend/app/core/errors.py
Normal file
58
templates/backend/app/core/errors.py
Normal 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"},
|
||||
)
|
||||
35
templates/backend/app/main.py
Normal file
35
templates/backend/app/main.py
Normal 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"}
|
||||
82
templates/backend/pyproject.toml
Normal file
82
templates/backend/pyproject.toml
Normal 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:",
|
||||
]
|
||||
12
templates/backend/tests/conftest.py
Normal file
12
templates/backend/tests/conftest.py
Normal 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)
|
||||
22
templates/backend/tests/integration/test_api/test_health.py
Normal file
22
templates/backend/tests/integration/test_api/test_health.py
Normal 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"
|
||||
24
templates/backend/tests/unit/test_core/test_config.py
Normal file
24
templates/backend/tests/unit/test_core/test_config.py
Normal 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
|
||||
Reference in New Issue
Block a user