diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..17dc750 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,52 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# Top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 +indent_style = space +indent_size = 4 + +# Python files +[*.py] +indent_size = 4 +max_line_length = 88 +quote_type = "double" + +# YAML files +[*.{yaml,yml}] +indent_style = space +indent_size = 2 + +# JSON files +[*.json] +indent_style = space +indent_size = 2 + +# TOML files +[*.toml] +indent_style = space +indent_size = 2 + +# Markdown files +[*.md] +trim_trailing_whitespace = false + +# Makefiles +[Makefile] +indent_style = tab + +# Dockerfiles +[Dockerfile] +indent_style = space +indent_size = 4 + +# Shell scripts +[*.sh] +indent_style = space +indent_size = 2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 202dfd4..d7714a3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,20 @@ repos: exclude: ^app/alembic/ - id: check-added-large-files exclude: ^app/alembic/ + - id: check-toml + exclude: ^app/alembic/ + - id: check-yaml + exclude: ^app/alembic/ + types_or: [yaml, yml] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.1 + hooks: + - id: ruff + args: [--fix, --show-fixes] + exclude: ^app/alembic/ + - id: ruff-format + exclude: ^app/alembic/ - repo: https://github.com/myint/autoflake rev: v2.3.1 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..67b8b28 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,27 @@ +{ + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.ruff": "explicit", + "source.organizeImports.ruff": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.rulers": [88], + "python.analysis.typeCheckingMode": "basic", + "python.linting.enabled": true, + "python.linting.ruffEnabled": true, + "python.linting.pylintEnabled": false, + "python.formatting.provider": "none", + "python.analysis.autoImportCompletions": true, + "python.analysis.autoSearchPaths": true, + "python.analysis.diagnosticMode": "workspace", + "python.analysis.inlayHints.variableTypes": true, + "python.analysis.inlayHints.functionReturnTypes": true, + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.ruff": "explicit", + "source.organizeImports.ruff": "explicit" + } + } +} diff --git a/README.md b/README.md index 10b57dc..881096b 100644 --- a/README.md +++ b/README.md @@ -17,109 +17,136 @@ This is a template project for a FastAPI application with a PostgreSQL database, - Password reset functionality - **Environment-based configuration**: Different settings for development, staging, and production -## Prerequisites +## Getting Started -- **Local Development**: - - Python 3.11 or higher - - PostgreSQL installed locally or accessible - - pip or another Python package manager +### Prerequisites -- **Docker Deployment**: - - Docker and Docker Compose installed - - A code editor (e.g., VS Code) - - A terminal or command prompt +- **Python 3.11+** and pip/uv +- **PostgreSQL** (local or remote) +- **Git** for version control +- **Docker & Docker Compose** (for containerized development) +- **Terminal/Command Prompt** (PowerShell recommended for Windows) -## Basic Configuration +### Quick Start -1. **Environment Variables**: - This project uses a `.env` file for local development configuration. If it doesn't exist, run this command to create: +**Clone the repository**: + ```bash + git clone https://github.com/Texagon-Dev/fastapi-postgres-template your-project-name + cd your-project-name + git remote set-url origin + ``` - ```python - cp .env.example .env - ``` +### Development Setup - Ensure you set the following environment variables in your `.env` file: +### Option 1: Automated Setup (Recommended) +We provide a setup script to help you get started quickly. Choose the appropriate command for your operating system: -```python -# Core settings -FRONTEND_URL='http://localhost:3000' -SECRET_KEY='your_32_char_strong_secret_key_here' -DEBUG=True -ENVIRONMENT='development' # Options: development, staging, production - -# Database settings -POSTGRES_USER="your_username" -POSTGRES_PASSWORD="your_password" -POSTGRES_DB_NAME="fastapi_db" - -# Authentication -ALGORITHM='HS256' -ACCESS_TOKEN_EXPIRE_MINUTES=30 # Short-lived access tokens for security -REFRESH_TOKEN_EXPIRE_DAYS=7 # Longer-lived refresh tokens +#### Mac/Linux: +```bash +# Make the script executable +chmod +x scripts/dev_setup.py + +# Run the setup script +./scripts/dev_setup.py ``` -## Local Installation and Setup +#### Windows (Command Prompt): +```cmd +# Run the setup script directly with Python +python scripts/dev_setup.py +``` -#### (Recommended uv) +#### Windows (PowerShell): +```powershell +# Run the setup script directly with Python +python .\scripts\dev_setup.py +``` -1. **Install dependencies directly with uv**: +### Option 2: Manual Setup +**The setup script will**: +- Create a `.env` file from the example +- Set up git hooks +- Install pre-commit hooks + + 2. **Set up environment**: + - Copy the example environment file: + ```bash + # Linux/macOS + cp .example.env .env + + # Windows (Command Prompt) + copy .example.env .env + + # Windows (PowerShell) + Copy-Item -Path .example.env -Destination .env + ``` + - Update the `.env` file with your configuration (see Configuration section below) + + ### Configuration + + - Edit the `.env` file with your settings: + + ```env + # Core settings + FRONTEND_URL='http://localhost:3000' + SECRET_KEY='your_32_char_strong_secret_key_here' + DEBUG=True + ENVIRONMENT='development' # Options: development, staging, production + + # Database settings + POSTGRES_USER="your_username" + POSTGRES_PASSWORD="your_password" + POSTGRES_DB_NAME="fastapi_db" + + # Authentication + ALGORITHM='HS256' + ACCESS_TOKEN_EXPIRE_MINUTES=30 # Short-lived access tokens for security + REFRESH_TOKEN_EXPIRE_DAYS=7 # Longer-lived refresh tokens + ``` + 3. **Install dependencies** (using uv - recommended): ```bash - # uv will make .venv automatically + # Install dependencies and create virtual environment uv sync + + # Activate virtual environment + # Linux/macOS: + source .venv/bin/activate + # Windows (Command Prompt): + .venv\Scripts\activate + # Windows (PowerShell): + .\.venv\Scripts\Activate.ps1 ``` -2. **Install new dependencies with uv**: + 4. **Set up the database**: ```bash - uv add dependency_name - ``` -3. **Run server**: - - ```bash - uv run uvicorn app.main:app --reload - ``` - OR if you have make available - ``` - make start - ``` - -#### (Not recommended) - -1. **Create a virtual environment**: - - ```bash - # Using standard venv - python -m venv venv - source venv/bin/activate # On Windows: venv\Scripts\activate - - # OR using uv (faster) - uv venv - source .venv/bin/activate # On Windows: .venv\Scripts\activate + # Run migrations + uv run alembic upgrade head + + # Or using make (if available) + make alembic-upgrade ``` -2. **Install dependencies**: - + 5. **Set up pre-commit hooks**: ```bash - # Using pip - pip install -r requirements.txt - - # OR using uv (faster) - uv pip install -r requirements.txt + # Linux/macOS/Windows (Git Bash) + cp misc/pre-commit .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit + + # Windows (Command Prompt) + copy /Y misc\pre-commit .git\hooks\pre-commit ``` + 6. **Start the development server**: + ```bash + # Using uvicorn directly + uv run uvicorn app.main:app --reload + + # Or using make (if available) + make start + ``` + The API will be available at `http://localhost:8000` and interactive docs at `http://localhost:8000/docs` -#### Set up the database: - - Create your PostgreSQL database and run migrations: - - ```bash - # Make sure you've set the correct DATABASE_URL in your .env file - alembic upgrade head - ``` - OR if you have make available - ``` - make alembic-upgrade - ``` ### Database Migrations Guide diff --git a/misc/pre-commit b/misc/pre-commit new file mode 100755 index 0000000..86b1fbd --- /dev/null +++ b/misc/pre-commit @@ -0,0 +1,38 @@ +#!/bin/bash + +# Get list of staged Python files +files=$(git diff --cached --name-only --diff-filter=ACM | grep "\\.py$") + +echo "Modified Python files: $files" + +# If no Python files are modified, exit successfully +if [ -z "$files" ]; then + echo "No Python files modified. Skipping pre-commit hooks." + exit 0 +fi + +# Run ruff on the modified files with --output-format=full to capture warnings +echo "šŸ” Running ruff checks..." +# Capture both stdout and stderr to catch warnings +ruff_output=$(uv run ruff check $files --output-format=full 2>&1) +ruff_exit_code=$? + +# Check if there are any warnings or errors +if [[ $ruff_output == *"warning:"* ]] || [ $ruff_exit_code -ne 0 ]; then + echo "āŒ Ruff found issues that need to be fixed:" + echo "$ruff_output" + echo "\nPlease fix the above issues before committing." + exit 1 +fi + +# Capture the exit code of the pre-commit command +pre_commit_exit_code=$? + +# If pre-commit found issues, exit with its exit code +if [ $pre_commit_exit_code -ne 0 ]; then + echo "āŒ Pre-commit hooks found issues that need to be fixed" + exit $pre_commit_exit_code +fi + +echo "āœ… All checks passed" +exit 0 \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6322c40..4f3c46c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,10 @@ name = "FastAPI-Postgres-Template" version = "0.1.0" description = "Add your description here" readme = "README.md" -requires-python = ">=3.11" +authors = [{ name = "HBilal Khan", email = "hbilal@texagon.io" }, { name = "M Talha", email = "mtalha@texagon.io" }] +license = { text = "Proprietary" } +urls = { Homepage = "https://example.com/saas-backend", Repository = "https://github.com/Texagon-Dev/fastapi-hexagonal-ddd-postgres-template", Issues = "https://github.com/Texagon-Dev/fastapi-hexagonal-ddd-postgres-template/issues" } +requires-python = ">=3.12" dependencies = [ "aiocache[redis]>=0.12.3", "alembic==1.16.1", @@ -57,6 +60,103 @@ dev = [ "ruff>=0.12.9", ] +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["app"] + +# ------------------------- +# Ruff (lint + format) +# ------------------------- [tool.ruff] -line-length = 120 -select = ["E", "F", "W", "C", "B"] +line-length = 120 # Matches Black's default +target-version = "py311" +# Exclude non-source directories and common build artifacts +exclude = [ + ".git", + ".venv", + "__pycache__", + "**/__pycache__", + "build", + "dist", + ".mypy_cache", + ".pytest_cache", + ".ruff_cache", + "migrations", + "alembic", + "**/migrations", + "**/alembic", + "**/tests", + "**/test_*.py", + "**/*_test.py", + "**/conftest.py" +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort (import sorting) + "B", # bugbear + "UP", # pyupgrade + "SIM", # simplify +] +ignore = [ + "E501", # line too long (formatter handles) + "E203", # whitespace before ':' + "B008", # FastAPI Depends used in defaults + "B904", # raise in except without from +] +# From A → enable sweeping autofix and smarter dummy variables +fixable = ["ALL"] +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] +"alembic/env.py" = ["E402", "F401"] +"app/main.py" = ["E402"] +"app/services/prompting/suggestion_prompt.py" = ["F541"] + +# ------------------------- +# MyPy +# ------------------------- +[tool.mypy] +python_version = "3.11" +# Enable strict mode +strict = true +# Keep some additional strictness flags for clarity +disallow_any_generics = true +disallow_subclassing_any = true +disallow_untyped_decorators = true +warn_return_any = true +warn_unreachable = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_no_return = true +no_implicit_optional = true +check_untyped_defs = true +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "app.migrations.*" +ignore_errors = true + +[[tool.mypy.overrides]] +module = "alembic.*" +ignore_errors = true + +# ------------------------- +# Bandit (security) +# ------------------------- +[tool.bandit] +exclude_dirs = ["tests", "migrations", "alembic"] +skips = ["B101", "B601"] + diff --git a/scripts/dev_setup.py b/scripts/dev_setup.py new file mode 100644 index 0000000..6d65ec5 --- /dev/null +++ b/scripts/dev_setup.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +import os +import shutil +import sys +import subprocess +from pathlib import Path + +def run_command(cmd: str, cwd: str = None) -> bool: + """Run a shell command and return True if successful.""" + print(f"\n$ {cmd}") + try: + subprocess.run(cmd, shell=True, check=True, cwd=cwd) + return True + except subprocess.CalledProcessError as e: + print(f"āŒ Command failed with exit code {e.returncode}") + return False + +def setup_env() -> bool: + """Set up the environment file.""" + example_env = Path(".example.env") + env_file = Path(".env") + + if not example_env.exists(): + print("āŒ .example.env not found. Please create it first.") + return False + + if not env_file.exists(): + print("šŸ”„ Creating .env from .example.env...") + shutil.copy(example_env, env_file) + print("āœ… Created .env file") + else: + print("ā„¹ļø .env file already exists, skipping creation") + return True + +def setup_git_hooks() -> bool: + """Set up git hooks.""" + hooks_dir = Path(".git/hooks") + pre_commit_src = Path("misc/pre-commit") + + if not pre_commit_src.exists(): + print(f"āŒ Pre-commit hook not found at {pre_commit_src}") + return False + + if not hooks_dir.exists(): + print("ā„¹ļø Initializing git repository...") + if not run_command("git init"): + return False + + print("šŸ”§ Setting up git hooks...") + pre_commit_dest = hooks_dir / "pre-commit" + + # Copy the pre-commit hook + shutil.copy(pre_commit_src, pre_commit_dest) + + # Make it executable (works on Unix-like systems) + try: + pre_commit_dest.chmod(0o755) + except Exception as e: + print(f"āš ļø Could not set executable permissions: {e}") + print("āš ļø On Windows, you may need to run this as Administrator") + + # Install pre-commit hooks + if not run_command("pre-commit install"): + print("āš ļø Failed to install pre-commit hooks") + return False + + print("āœ… Git hooks set up successfully") + return True + +def main(): + print("\nšŸš€ Starting project setup...") + + # Create scripts directory if it doesn't exist + scripts_dir = Path("scripts") + scripts_dir.mkdir(exist_ok=True) + + if not setup_env(): + sys.exit(1) + + if not setup_git_hooks(): + print("āš ļø Git hooks setup had issues, but continuing...") + + print("\nšŸŽ‰ Setup completed successfully!") + print("Next steps:") + print("1. Review and update the .env file with your configuration") + print("2. Run 'uv sync' to install dependencies") + print("3. Run 'uv run alembic upgrade head' to set up the database") + print("4. Run 'uv run uvicorn app.main:app --reload' to start the development server") + +if __name__ == "__main__": + main() \ No newline at end of file