diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d53ede3 --- /dev/null +++ b/.env.example @@ -0,0 +1,62 @@ +# Application +PROJECT_NAME=FastAPI Starter +API_VERSION=v1 +DEBUG=false +ENVIRONMENT=development # development, staging, production + +# Server +HOST=0.0.0.0 +PORT=8000 +RELOAD=false + +# Database +DATABASE_HOST=localhost +DATABASE_PORT=5432 +DATABASE_USER=postgres +DATABASE_PASSWORD=your_password_here +DATABASE_NAME=app_db +DATABASE_ECHO=false # SQL query logging +DATABASE_URL=postgresql+asyncpg://{DATABASE_USER}:{DATABASE_PASSWORD}@{DATABASE_HOST}:{DATABASE_PORT}/{DATABASE_NAME} +# Database Pool +DATABASE_POOL_SIZE=20 +DATABASE_MAX_OVERFLOW=10 +DATABASE_POOL_TIMEOUT=30 +DATABASE_POOL_RECYCLE=3600 + +# Security +SECRET_KEY=your-secret-key-min-32-chars-long-change-in-production +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=30 +REFRESH_TOKEN_EXPIRE_DAYS=7 + +# CORS +BACKEND_CORS_ORIGINS=["http://localhost:3000","http://localhost:8000","http://localhost:8080"] + +# Rate Limiting +RATE_LIMIT_PER_MINUTE=60 + +# Logging +LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR, CRITICAL +LOG_FORMAT=json # json or text + +# Redis (Optional) +REDIS_HOST=localhost +REDIS_PORT=6379 +REDIS_DB=0 +REDIS_PASSWORD= + +# Sentry (Optional) +SENTRY_DSN= + +# Email (Optional) +SMTP_HOST= +SMTP_PORT=587 +SMTP_USER= +SMTP_PASSWORD= +SMTP_FROM=noreply@example.com + +# AWS (Optional) +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_REGION=us-east-1 +S3_BUCKET= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1907c4a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,142 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +jobs: + lint: + name: Lint & Format Check + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + cache: "pip" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff mypy + + - name: Run Ruff linter + run: ruff check . + + - name: Run Ruff formatter check + run: ruff format --check . + + - name: Run MyPy type checker + run: mypy app/ --ignore-missing-imports + continue-on-error: true + + test: + name: Test + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.13"] + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + + - name: Run tests + run: pytest -v --tb=short + + - name: Upload coverage + if: matrix.python-version == '3.13' + uses: codecov/codecov-action@v4 + with: + files: ./coverage.xml + fail_ci_if_error: false + + security: + name: Security Scan + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + cache: "pip" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install safety bandit + + - name: Run safety check + run: safety check --json || true + continue-on-error: true + + - name: Run bandit security scan + run: bandit -r app/ -f json || true + continue-on-error: true + + build: + name: Build & Validate + runs-on: ubuntu-latest + needs: [lint, test] + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + cache: "pip" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + + - name: Validate package build + run: | + pip install build + python -m build + + - name: Test import + run: python -c "from app.main import create_app; print('Import successful')" + + docker: + name: Docker Build + runs-on: ubuntu-latest + needs: [lint, test] + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: false + tags: fastapi-starter:test + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 0000000..c7e5219 --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,61 @@ +name: Commit Message Validation + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + +jobs: + commitlint: + name: Validate Commit Messages + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install commitlint + run: | + npm install -g @commitlint/{cli,config-conventional} + + - name: Create commitlint config + run: | + cat > commitlint.config.js << EOF + module.exports = { + extends: ['@commitlint/config-conventional'], + rules: { + 'type-enum': [ + 2, + 'always', + [ + 'feat', + 'fix', + 'docs', + 'style', + 'refactor', + 'perf', + 'test', + 'build', + 'ci', + 'chore', + 'revert' + ] + ], + 'subject-case': [0], + 'subject-max-length': [2, 'always', 100] + } + }; + EOF + + - name: Validate PR commits + run: | + npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose + + - name: Validate PR title + run: | + echo "${{ github.event.pull_request.title }}" | npx commitlint --verbose diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 0000000..d450c49 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,52 @@ +name: Code Coverage + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + coverage: + name: Generate Code Coverage + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + cache: "pip" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" + pip install pytest-cov + + - name: Run tests with coverage + run: | + pytest --cov=app --cov-report=xml --cov-report=html --cov-report=term + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + + - name: Upload coverage artifacts + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: htmlcov/ + + - name: Coverage comment + uses: py-cov-action/python-coverage-comment-action@v3 + with: + GITHUB_TOKEN: ${{ github.token }} + MINIMUM_GREEN: 80 + MINIMUM_ORANGE: 60 diff --git a/.github/workflows/dependencies.yml b/.github/workflows/dependencies.yml new file mode 100644 index 0000000..44d2e96 --- /dev/null +++ b/.github/workflows/dependencies.yml @@ -0,0 +1,47 @@ +name: Dependency Update + +on: + schedule: + # Run weekly on Monday at 9:00 AM UTC + - cron: "0 9 * * 1" + workflow_dispatch: + +jobs: + dependency-update: + name: Check for Dependency Updates + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install pip-tools + run: | + python -m pip install --upgrade pip + pip install pip-tools pip-audit + + - name: Check for outdated packages + run: pip list --outdated + + - name: Security audit + run: pip-audit || true + continue-on-error: true + + - name: Create issue for updates + if: success() + uses: actions/github-script@v7 + with: + script: | + const title = 'Weekly Dependency Update Check'; + const body = 'Automated dependency check completed. Please review outdated packages.'; + github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: title, + body: body, + labels: ['dependencies', 'automated'] + }); diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..79efd54 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1 @@ +# add your production CI workflow here diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..ab8d3b4 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,32 @@ +name: Pre-commit Checks + +on: + pull_request: + branches: [main, develop] + +jobs: + pre-commit: + name: Run Pre-commit Hooks + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install pre-commit + run: | + python -m pip install --upgrade pip + pip install pre-commit + + - name: Cache pre-commit environments + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + + - name: Run pre-commit + run: pre-commit run --all-files --show-diff-on-failure diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..afaa0e3 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,130 @@ +name: Auto Release Notes + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + tag: + description: 'Tag to create release for' + required: true + default: 'v0.1.0' + +jobs: + release: + name: Create Release with Auto-generated Notes + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get previous tag + id: previoustag + run: | + PREV_TAG=$(git tag --sort=-version:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sed -n '2p') + if [ -z "$PREV_TAG" ]; then + PREV_TAG=$(git rev-list --max-parents=0 HEAD) + fi + echo "tag=$PREV_TAG" >> $GITHUB_OUTPUT + + - name: Generate changelog + id: changelog + run: | + CURRENT_TAG="${{ github.ref_name }}" + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + CURRENT_TAG="${{ github.event.inputs.tag }}" + fi + PREV_TAG="${{ steps.previoustag.outputs.tag }}" + + echo "## πŸŽ‰ What's Changed" > CHANGELOG.md + echo "" >> CHANGELOG.md + + # Features + echo "### ✨ Features" >> CHANGELOG.md + git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --grep="^feat" --grep="^feature" -i >> CHANGELOG.md || echo "- No new features" >> CHANGELOG.md + echo "" >> CHANGELOG.md + echo "" >> CHANGELOG.md + + # Bug Fixes + echo "### πŸ› Bug Fixes" >> CHANGELOG.md + git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --grep="^fix" -i >> CHANGELOG.md || echo "- No bug fixes" >> CHANGELOG.md + echo "" >> CHANGELOG.md + echo "" >> CHANGELOG.md + + # Documentation + echo "### πŸ“ Documentation" >> CHANGELOG.md + git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --grep="^docs" -i >> CHANGELOG.md || echo "- No documentation changes" >> CHANGELOG.md + echo "" >> CHANGELOG.md + echo "" >> CHANGELOG.md + + # Performance + echo "### ⚑ Performance" >> CHANGELOG.md + git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --grep="^perf" -i >> CHANGELOG.md || echo "- No performance improvements" >> CHANGELOG.md + echo "" >> CHANGELOG.md + echo "" >> CHANGELOG.md + + # Refactoring + echo "### ♻️ Refactoring" >> CHANGELOG.md + git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --grep="^refactor" -i >> CHANGELOG.md || echo "- No refactoring changes" >> CHANGELOG.md + echo "" >> CHANGELOG.md + echo "" >> CHANGELOG.md + + # CI/CD + echo "### πŸš€ CI/CD" >> CHANGELOG.md + git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --grep="^ci" --grep="^build" -i >> CHANGELOG.md || echo "- No CI/CD changes" >> CHANGELOG.md + echo "" >> CHANGELOG.md + echo "" >> CHANGELOG.md + + # Breaking Changes + echo "### πŸ’₯ Breaking Changes" >> CHANGELOG.md + git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --grep="BREAKING CHANGE" -i >> CHANGELOG.md || echo "- No breaking changes" >> CHANGELOG.md + echo "" >> CHANGELOG.md + echo "" >> CHANGELOG.md + + # Contributors + echo "### πŸ‘₯ Contributors" >> CHANGELOG.md + git log ${PREV_TAG}..HEAD --pretty=format:"- @%an" | sort -u >> CHANGELOG.md + echo "" >> CHANGELOG.md + echo "" >> CHANGELOG.md + + # Full Changelog + echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${CURRENT_TAG}" >> CHANGELOG.md + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + tag_name: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} + name: Release ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }} + body_path: CHANGELOG.md + draft: false + prerelease: false + generate_release_notes: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Update CHANGELOG file + run: | + if [ -f "CHANGELOG.md" ]; then + cat CHANGELOG.md > temp_changelog.md + echo "" >> temp_changelog.md + echo "---" >> temp_changelog.md + echo "" >> temp_changelog.md + if [ -f "CHANGELOG.md.bak" ]; then + cat CHANGELOG.md.bak >> temp_changelog.md + fi + mv temp_changelog.md CHANGELOG.md + fi + + - name: Commit updated CHANGELOG + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + git add CHANGELOG.md || true + git commit -m "docs: update CHANGELOG for ${{ github.ref_name }}" || true + git push origin HEAD:main || true diff --git a/.gitignore b/.gitignore index b7faf40..09be0ef 100644 --- a/.gitignore +++ b/.gitignore @@ -182,9 +182,9 @@ cython_debug/ .abstra/ # Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, +# and can be added to the global gitignore or merged into this file. However, if you prefer, # you could uncomment the following to ignore the entire vscode folder # .vscode/ diff --git a/.gitmessage b/.gitmessage new file mode 100644 index 0000000..d5aadf1 --- /dev/null +++ b/.gitmessage @@ -0,0 +1,66 @@ +# Commit Message Template +# +# Format: (): +# +# +# +#