From aad4448689d47301003930d66916d9fae258289c Mon Sep 17 00:00:00 2001 From: jdengoh Date: Mon, 25 Aug 2025 22:38:55 +0800 Subject: [PATCH 1/2] feat: added initial git repo template --- .dockerignore | 197 ++++++++++++ .githooks/commit-msg | 27 ++ .githooks/pre-push | 24 ++ .github/ISSUE_TEMPLATE/bug_report.md | 40 +++ .github/ISSUE_TEMPLATE/feature_request.md | 19 ++ .github/PULL_REQUEST_TEMPLATE.md | 25 ++ .github/dependabot.yml | 28 ++ .github/workflows/lint-markdown.yml | 53 ++++ .gitignore | 6 +- .lycheeignore | 1 + .markdownlint.jsonc | 32 ++ .pre-commit-config.yaml | 73 +++++ .python-version | 1 + Dockerfile | 35 +++ commitlint.config.js | 10 + docker-compose.dev.yaml | 25 ++ docker-compose.infra.yaml | 30 ++ docker-compose.yaml | 30 ++ package.json | 6 + pyproject.toml | 16 + scripts/setup_hooks.sh | 45 +++ scripts/start.sh | 41 +++ scripts/start_docker.ps1 | 12 + scripts/start_docker.sh | 8 + scripts/start_docker_prod.sh | 9 + uv.lock | 347 ++++++++++++++++++++++ 26 files changed, 1138 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 .githooks/commit-msg create mode 100644 .githooks/pre-push create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/lint-markdown.yml create mode 100644 .lycheeignore create mode 100644 .markdownlint.jsonc create mode 100644 .pre-commit-config.yaml create mode 100644 .python-version create mode 100644 Dockerfile create mode 100644 commitlint.config.js create mode 100644 docker-compose.dev.yaml create mode 100644 docker-compose.infra.yaml create mode 100644 docker-compose.yaml create mode 100644 package.json create mode 100644 pyproject.toml create mode 100755 scripts/setup_hooks.sh create mode 100755 scripts/start.sh create mode 100755 scripts/start_docker.ps1 create mode 100755 scripts/start_docker.sh create mode 100755 scripts/start_docker_prod.sh create mode 100644 uv.lock diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f86cfac --- /dev/null +++ b/.dockerignore @@ -0,0 +1,197 @@ +# Created by .ignore support plugin (hsz.mobi) +# Check out: https://github.com/long2ice/fastapi-cache/blob/main/.dockerignore + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +notebook/ +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +.idea + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Docker-related files +Dockerfile +docker-compose*.yml +.dockerignore + +# Git-related files +.git/ +.github/ +.gitignore + +# DVC specific +.dvc/ +.dvcignore + +# Azure deployment state +# .azure/ +# infra/ +# azure.yaml + +# Project artifacts and documentation +docs/ +Makefile +media/ +postman/ +README.md + +# Obsolete requirements file +requirements.txt + +# Local data that gets mounted (don't bake it into the image) +data/ + + +# IDE and editor files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store + +# Package manager files that might contain dev dependencies +node_modules/ +.npm diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100644 index 0000000..450449e --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,27 @@ +#!/usr/bin/env sh + +# Example commit-msg hook using commitlint +# 1) Ensure commitlint is installed (npm i -D @commitlint/config-conventional @commitlint/cli). +# 2) Make sure this file is executable (chmod +x .githooks/commit-msg). +# 3) Configure Git to use .githooks as the hook path (see next step). + +# "$1" is the path to the file containing the commit message. +npx commitlint --edit "$1" +exit_code=$? + +if [ $exit_code -ne 0 ]; then + echo "❌ Error: Commit message does not follow Conventional Commits." + echo "Please use one of the following patterns:" + echo " - feat: ..." + echo " - fix: ..." + echo " - docs: ..." + echo " - styles: ..." + echo " - refactor: ..." + echo " - test: ..." + echo " - chore: ..." + echo " - chore(release): vX.Y.Z" + exit 1 +fi + +echo "βœ… Commit message follows Conventional Commits!" +exit 0 diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100644 index 0000000..5089f99 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# Retrieve the current branch name +BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) + +# Define the regex pattern for valid branch names +VALID_BRANCH_REGEX="^(main|staging|release\/[0-9]{4}-[0-9]{2}-[0-9]{2}|feature\/.+|fix\/.+|bugfix\/.+|hotfix\/.+)$" + +# Check if the branch name matches the valid pattern +if [[ ! $BRANCH_NAME =~ $VALID_BRANCH_REGEX ]]; then + echo "❌ Error: Branch name '$BRANCH_NAME' does not follow the naming convention." + echo "Please use one of the following patterns:" + echo " - main" + echo " - staging" + echo " - release/yyyy-mm-dd" + echo " - feature/xxx" + echo " - fix/xxx" + echo " - bugfix/xxx" + echo " - hotfix/xxx" + exit 1 +fi + +echo "βœ… Branch name is valid. Proceeding with push!" +exit 0 diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..cec36ab --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,40 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "" +labels: "bug" +assignees: "" +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] + +**Smartphone (please complete the following information):** + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..9b90523 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,19 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "" +labels: "enhancement" +assignees: "" +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..b163a41 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,25 @@ +# Pull Request + +## Description + +Please describe the changes made in this PR, and ensure the title summarises this PR. If it is related to an issue, reference it as well ("Fixes #123"). + +## Type of Change + +- [ ] Feature (a new feature implemented) +- [ ] Refactor (code refactorisation) +- [ ] Chore (config, no production code changes) +- [ ] Bug Fix (fixes a bug/issue) +- [ ] Documentation Update + +## Checklist + +Please ensure the following have been completed: + +- [ ] The PR has a meaningful title. +- [ ] I have summarized the changes of my PR. +- [ ] The PR is ready to be merged and **NOT WORK IN PROGRESS**. + +## Additional Notes + +You may include any additional notes or screenshot if applicable. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8e2b1fc --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,28 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + target-branch: "staging" + groups: + github-actions: + patterns: + - "*" + + # Maintain dependencies for pip + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + target-branch: "staging" + groups: + python-requirements: + patterns: + - "*" diff --git a/.github/workflows/lint-markdown.yml b/.github/workflows/lint-markdown.yml new file mode 100644 index 0000000..d06263b --- /dev/null +++ b/.github/workflows/lint-markdown.yml @@ -0,0 +1,53 @@ +name: Lint Markdown + +on: + pull_request: + branches: + - main + - staging + +jobs: + detect-changes: + name: Detect Changed Markdown + runs-on: ubuntu-latest + outputs: + should_skip: ${{ steps.detect.outputs.should_skip }} + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + # Ensure we have the actual PR commit + ref: ${{ github.event.pull_request.head.sha }} + + - name: Detect .md Changes + id: detect + run: | + git fetch --no-tags --prune --depth=1 origin ${{ github.base_ref }} + CHANGED_FILES="$(git diff --name-only --diff-filter=AM HEAD origin/${{ github.base_ref }})" + + echo "Changed files:" + echo "$CHANGED_FILES" + + if echo "$CHANGED_FILES" | grep -E '\.md$'; then + echo "should_skip=false" >> "$GITHUB_OUTPUT" + else + echo "should_skip=true" >> "$GITHUB_OUTPUT" + fi + + lint-markdown: + name: Check for Markdown Linting Errors + runs-on: ubuntu-latest + needs: detect-changes + if: ${{ needs.detect-changes.outputs.should_skip == 'false' }} + steps: + - name: Checkout Repo + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Run Markdownlint + uses: DavidAnson/markdownlint-cli2-action@v20 + with: + config: .markdownlint.jsonc + globs: | + **/*.md + !data/ diff --git a/.gitignore b/.gitignore index b7faf40..9001161 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +node_modules/ + # Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] @@ -182,9 +184,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/.lycheeignore b/.lycheeignore new file mode 100644 index 0000000..37062c1 --- /dev/null +++ b/.lycheeignore @@ -0,0 +1 @@ +# Add links to exclude during checking of broken links (regex is supported) diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc new file mode 100644 index 0000000..c7c128e --- /dev/null +++ b/.markdownlint.jsonc @@ -0,0 +1,32 @@ +// https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.jsonc +{ + // Default state for all rules + "default": true, + + // MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md013.md + "MD013": false, + + // MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md025.md + "MD025": false, + + // MD028/no-blanks-blockquote : Blank line inside blockquote : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md028.md + "MD028": false, + + // MD029/ol-prefix : Ordered list item prefix : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md029.md + "MD029": false, + + //MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md033.md + "MD033": { "allowed_elements": ["a", "br", "details", "summary"] }, + + // MD036/no-emphasis-as-heading : Emphasis used instead of a heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md036.md + "MD036": false, + + // MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md041.md + "MD041": false, + + //MD051/link-fragments : Link fragments should be valid : https://github.com/DavidAnson/markdownlint/blob/v0.34.0/doc/md051.md + "MD051": false, + + // MD059/descriptive-link-text : Link text should be descriptive : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md059.md + "MD059": false, +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6d699f6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,73 @@ +# # See https://pre-commit.com for more information +# # See https://pre-commit.com/hooks.html for more hooks +exclude: "^app/backend/tests/unit/prepdocslib/snapshots/" +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-toml + - id: check-yaml + - id: check-added-large-files + args: ["--maxkb=100000"] # Set the maximum file size to 2048 KB (2 MB) + - id: end-of-file-fixer + - id: requirements-txt-fixer + - id: trailing-whitespace + - id: detect-private-key + - id: mixed-line-ending + + # Removes unnecessary # noqa comments. + - repo: https://github.com/asottile/yesqa + rev: v1.5.0 + hooks: + - id: yesqa + + # Check for blanket (overly broad) noqa comments and encourage type annotations. + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: python-check-blanket-noqa + - id: python-use-type-annotations + + - repo: https://github.com/psf/black + rev: 25.1.0 + hooks: + - id: black + args: [--safe] + + - repo: https://github.com/pycqa/isort + rev: 6.0.1 + hooks: + - id: isort + args: [--profile=black] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.10 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + + - repo: https://github.com/nbQA-dev/nbQA + rev: 1.9.1 + hooks: + - id: nbqa-black + additional_dependencies: [black==25.1.0] + - id: nbqa-isort + additional_dependencies: [isort==6.0.1] + args: [--profile=black] + - id: nbqa-ruff + additional_dependencies: [ruff==0.11.10] + + - repo: https://github.com/DavidAnson/markdownlint-cli2 + rev: v0.18.1 + hooks: + - id: markdownlint-cli2 + args: [--fix] + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v4.0.0-alpha.8 + hooks: + - id: prettier + args: [--write] + types_or: [css, json, javascript, ts, tsx, html] + additional_dependencies: + - prettier@3.5.3 diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b62c17e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM python:3.13-slim + + +# Set environment variables consistently +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + UV_PROJECT_ENVIRONMENT="/usr/local/" \ + # Ensure scripts know where the app root is + PYTHONPATH="/app" + + +# Install uv package manager +# Install in /usr/local/bin as it is a "local" software +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/ + + +WORKDIR /app + +# Copy and install dependencies first for caching efficiency +COPY pyproject.toml uv.lock* ./ +RUN uv sync --no-dev --frozen --no-cache + +COPY . /app + +RUN chmod +x scripts/* || true + +# Create and switch to a non-root user +RUN useradd -ms /bin/bash appuser +USER appuser + +# Make port 8000 available (adjust if your API uses a different port) +EXPOSE 8000 + +# Default command (no reload in production) +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..583cfc4 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,10 @@ +module.exports = { + extends: ["@commitlint/config-conventional"], + rules: { + "type-enum": [ + 2, + "always", + ["feat", "fix", "docs", "style", "refactor", "test", "chore"], + ], + }, +}; diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml new file mode 100644 index 0000000..bada3c4 --- /dev/null +++ b/docker-compose.dev.yaml @@ -0,0 +1,25 @@ +services: + # FastAPI application (depends on postgres) + fastapi-template: + environment: + - ENV_TYPE=development + - RUNNING_IN_DOCKER=true + + # Dev database connection + - POSTGRES_SERVER=postgres + - POSTGRES_PORT=5432 + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=Password123 + - POSTGRES_DB=fastapi-template-db + volumes: + # Mount app/ directory for live code updates + - ./app:/app/app + depends_on: + postgres: + condition: service_healthy + command: > + uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + restart: "no" + healthcheck: + disable: true + \ No newline at end of file diff --git a/docker-compose.infra.yaml b/docker-compose.infra.yaml new file mode 100644 index 0000000..6acc9f6 --- /dev/null +++ b/docker-compose.infra.yaml @@ -0,0 +1,30 @@ +services: + # PostgreSQL for development (dependency first) + postgres: + image: postgres:alpine + container_name: fastapi-template-db + environment: + POSTGRES_DB: fastapi-template-db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: Password123 + ports: + - "5432:5432" + volumes: + - fastapi-template-db:/var/lib/postgresql/data + healthcheck: + # Run a healthcheck, which our app will depend on before starting + test: ["CMD-SHELL", "pg_isready -U postgres -d fastapi-template-db"] + interval: 5s + timeout: 5s + retries: 5 + start_period: 10s + networks: + - fastapi-template-infra-network + +volumes: + fastapi-template-db: + +networks: + fastapi-template-infra-network: + name: fastapi-template-infra-network + driver: bridge \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..82e0983 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,30 @@ +services: + fastapi-template: + container_name: fastapi-template + build: + context: . + dockerfile: Dockerfile + ports: + - "8000:8000" + env_file: + - .env + environment: + - ENV_TYPE=production + - RUNNING_IN_DOCKER=true + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + restart: unless-stopped + networks: + - fastapi-template-network + +networks: + fastapi-template-network: + name: fastapi-template-network + driver: bridge + fastapi-template-infra-network: + name: fastapi-template-infra-network + driver: bridge \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..1440d13 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "devDependencies": { + "@commitlint/cli": "^19.8.0", + "@commitlint/config-conventional": "^19.8.0" + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c344bf3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[project] +name = "fastapi-template" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "asyncpg>=0.30.0", + "fastapi>=0.116.1", + "pydantic>=2.11.7", + "pydantic-settings>=2.10.1", + "pyyaml>=6.0.2", + "rich>=14.1.0", + "sqlalchemy>=2.0.42", + "uvicorn>=0.35.0", +] diff --git a/scripts/setup_hooks.sh b/scripts/setup_hooks.sh new file mode 100755 index 0000000..c0a70f3 --- /dev/null +++ b/scripts/setup_hooks.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash + +echo "πŸ› οΈ Setting up Git hooks..." + +# Reset hooks path to default +git config --unset core.hooksPath 2>/dev/null || true + +# Install npm dependencies +if [ -f "package.json" ]; then + echo "πŸ“₯ Installing dependencies..." + npm install +else + echo "πŸ“₯ Installing and saving dependencies..." + npm install --save-dev @commitlint/config-conventional @commitlint/cli +fi + +echo "βœ… Dependencies installed successfully." + +SETUP_SUCCESS=true + +# Setup pre-commit if available +if command -v pre-commit &> /dev/null; then + echo "πŸ”§ Setting up pre-commit..." + pre-commit install +else + echo "❌ pre-commit not found - skipping pre-commit setup" + echo "Check UV environment and ensure pre-commit is installed." + SETUP_SUCCESS=false +fi + +# Copy custom hooks +for hook in commit-msg pre-push; do + if [ -f ".githooks/$hook" ]; then + echo "Installing $hook hook..." + cp ".githooks/$hook" ".git/hooks/$hook" + chmod +x ".git/hooks/$hook" + echo "βœ… $hook copied successfully." + fi +done + +if [ "$SETUP_SUCCESS" = true ]; then + echo "βœ… Git hooks setup completed!" +else + echo "❌ Git hooks setup failed." +fi diff --git a/scripts/start.sh b/scripts/start.sh new file mode 100755 index 0000000..3737567 --- /dev/null +++ b/scripts/start.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + +# Set strict error handling +set -euo pipefail + +# cd into the parent directory of the script, +cd "${0%/*}" || exit 1 + +cd ../ + +echo "βš™οΈ Starting infrastructure..." + +# Check if infrastructure is already running +if docker compose -f docker-compose.infra.yaml ps | grep -q "Up"; then + echo " Infrastructure already running, skipping startup..." +else + docker compose -f docker-compose.infra.yaml up -d +fi + + +echo "πŸš€ Starting backend..." + +port=8000 +host=127.0.0.1 +uvicorn "app.main:app" --host "$host" --port "$port" --reload +out=$? + +# Cleanup function +cleanup() { + echo "" + echo "πŸ›‘ Shutting down..." + docker compose -f docker-compose.infra.yaml down +} + +trap cleanup EXIT + + +if [ $out -ne 0 ]; then + echo "❌ Failed to start backend" + exit $out +fi diff --git a/scripts/start_docker.ps1 b/scripts/start_docker.ps1 new file mode 100755 index 0000000..b41b561 --- /dev/null +++ b/scripts/start_docker.ps1 @@ -0,0 +1,12 @@ +# Set strict error handling +$ErrorActionPreference = "Stop" + +Set-StrictMode -Version Latest + +if ($PSVersionTable.PSVersion -ge [Version]"7.2") { + $PSNativeCommandUseErrorActionPreference = $true +} + +Write-Host "πŸš€ Starting backend (Docker)..." + +docker compose -f docker-compose.yaml up --build diff --git a/scripts/start_docker.sh b/scripts/start_docker.sh new file mode 100755 index 0000000..4ecc7ec --- /dev/null +++ b/scripts/start_docker.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +# Set strict error handling +set -euo pipefail + +echo "πŸš€ Starting backend (Docker)..." + +docker compose -f docker-compose.yaml -f docker-compose.infra.yaml -f docker-compose.dev.yaml up --build diff --git a/scripts/start_docker_prod.sh b/scripts/start_docker_prod.sh new file mode 100755 index 0000000..839cb0c --- /dev/null +++ b/scripts/start_docker_prod.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# Set strict error handling +set -euo pipefail + +echo "πŸš€ Starting backend (Docker)..." + +# Start the prod Docker environment +docker compose -f docker-compose.yaml up --build \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..5cdd551 --- /dev/null +++ b/uv.lock @@ -0,0 +1,347 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/b4/636b3b65173d3ce9a38ef5f0522789614e590dab6a8d505340a4efe4c567/anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6", size = 213252, upload-time = "2025-08-04T08:54:26.451Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/12/e5e0282d673bb9746bacfb6e2dba8719989d3660cdb2ea79aee9a9651afb/anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1", size = 107213, upload-time = "2025-08-04T08:54:24.882Z" }, +] + +[[package]] +name = "asyncpg" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746, upload-time = "2024-10-20T00:30:41.127Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373, upload-time = "2024-10-20T00:29:55.165Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745, upload-time = "2024-10-20T00:29:57.14Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103, upload-time = "2024-10-20T00:29:58.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471, upload-time = "2024-10-20T00:30:00.354Z" }, + { url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253, upload-time = "2024-10-20T00:30:02.794Z" }, + { url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720, upload-time = "2024-10-20T00:30:04.501Z" }, + { url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404, upload-time = "2024-10-20T00:30:06.537Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623, upload-time = "2024-10-20T00:30:09.024Z" }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.116.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485, upload-time = "2025-07-11T16:22:32.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" }, +] + +[[package]] +name = "fastapi-template" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "asyncpg" }, + { name = "fastapi" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "sqlalchemy" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "asyncpg", specifier = ">=0.30.0" }, + { name = "fastapi", specifier = ">=0.116.1" }, + { name = "pydantic", specifier = ">=2.11.7" }, + { name = "pydantic-settings", specifier = ">=2.10.1" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "rich", specifier = ">=14.1.0" }, + { name = "sqlalchemy", specifier = ">=2.0.42" }, + { name = "uvicorn", specifier = ">=0.35.0" }, +] + +[[package]] +name = "greenlet" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/b8/704d753a5a45507a7aab61f18db9509302ed3d0a27ac7e0359ec2905b1a6/greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d", size = 188260, upload-time = "2025-08-07T13:24:33.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, + { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0b/bc13f787394920b23073ca3b6c4a7a21396301ed75a655bcb47196b50e6e/greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc", size = 655191, upload-time = "2025-08-07T13:45:29.752Z" }, + { url = "https://files.pythonhosted.org/packages/f2/d6/6adde57d1345a8d0f14d31e4ab9c23cfe8e2cd39c3baf7674b4b0338d266/greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a", size = 649516, upload-time = "2025-08-07T13:53:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3b/3a3328a788d4a473889a2d403199932be55b1b0060f4ddd96ee7cdfcad10/greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504", size = 652169, upload-time = "2025-08-07T13:18:32.861Z" }, + { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, + { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, + { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, + { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, + { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, + { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, + { url = "https://files.pythonhosted.org/packages/c0/aa/687d6b12ffb505a4447567d1f3abea23bd20e73a5bed63871178e0831b7a/greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5", size = 699218, upload-time = "2025-08-07T13:45:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.42" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/03/a0af991e3a43174d6b83fca4fb399745abceddd1171bdabae48ce877ff47/sqlalchemy-2.0.42.tar.gz", hash = "sha256:160bedd8a5c28765bd5be4dec2d881e109e33b34922e50a3b881a7681773ac5f", size = 9749972, upload-time = "2025-07-29T12:48:09.323Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/7e/25d8c28b86730c9fb0e09156f601d7a96d1c634043bf8ba36513eb78887b/sqlalchemy-2.0.42-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:941804f55c7d507334da38133268e3f6e5b0340d584ba0f277dd884197f4ae8c", size = 2127905, upload-time = "2025-07-29T13:29:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/e5/a1/9d8c93434d1d983880d976400fcb7895a79576bd94dca61c3b7b90b1ed0d/sqlalchemy-2.0.42-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d3d06a968a760ce2aa6a5889fefcbdd53ca935735e0768e1db046ec08cbf01", size = 2115726, upload-time = "2025-07-29T13:29:23.496Z" }, + { url = "https://files.pythonhosted.org/packages/a2/cc/d33646fcc24c87cc4e30a03556b611a4e7bcfa69a4c935bffb923e3c89f4/sqlalchemy-2.0.42-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cf10396a8a700a0f38ccd220d940be529c8f64435c5d5b29375acab9267a6c9", size = 3246007, upload-time = "2025-07-29T13:26:44.166Z" }, + { url = "https://files.pythonhosted.org/packages/67/08/4e6c533d4c7f5e7c4cbb6fe8a2c4e813202a40f05700d4009a44ec6e236d/sqlalchemy-2.0.42-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cae6c2b05326d7c2c7c0519f323f90e0fb9e8afa783c6a05bb9ee92a90d0f04", size = 3250919, upload-time = "2025-07-29T13:22:33.74Z" }, + { url = "https://files.pythonhosted.org/packages/5c/82/f680e9a636d217aece1b9a8030d18ad2b59b5e216e0c94e03ad86b344af3/sqlalchemy-2.0.42-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f50f7b20677b23cfb35b6afcd8372b2feb348a38e3033f6447ee0704540be894", size = 3180546, upload-time = "2025-07-29T13:26:45.648Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a2/8c8f6325f153894afa3775584c429cc936353fb1db26eddb60a549d0ff4b/sqlalchemy-2.0.42-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d88a1c0d66d24e229e3938e1ef16ebdbd2bf4ced93af6eff55225f7465cf350", size = 3216683, upload-time = "2025-07-29T13:22:34.977Z" }, + { url = "https://files.pythonhosted.org/packages/39/44/3a451d7fa4482a8ffdf364e803ddc2cfcafc1c4635fb366f169ecc2c3b11/sqlalchemy-2.0.42-cp313-cp313-win32.whl", hash = "sha256:45c842c94c9ad546c72225a0c0d1ae8ef3f7c212484be3d429715a062970e87f", size = 2093990, upload-time = "2025-07-29T13:16:13.036Z" }, + { url = "https://files.pythonhosted.org/packages/4b/9e/9bce34f67aea0251c8ac104f7bdb2229d58fb2e86a4ad8807999c4bee34b/sqlalchemy-2.0.42-cp313-cp313-win_amd64.whl", hash = "sha256:eb9905f7f1e49fd57a7ed6269bc567fcbbdac9feadff20ad6bd7707266a91577", size = 2120473, upload-time = "2025-07-29T13:16:14.502Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/ba2546ab09a6adebc521bf3974440dc1d8c06ed342cceb30ed62a8858835/sqlalchemy-2.0.42-py3-none-any.whl", hash = "sha256:defcdff7e661f0043daa381832af65d616e060ddb54d3fe4476f51df7eaa1835", size = 1922072, upload-time = "2025-07-29T13:09:17.061Z" }, +] + +[[package]] +name = "starlette" +version = "0.47.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, +] From 6ef963b22339ca12f40abe82cfec8aa4fd2551ef Mon Sep 17 00:00:00 2001 From: jdengoh Date: Mon, 25 Aug 2025 22:41:58 +0800 Subject: [PATCH 2/2] feat: base fastapi template --- app/README.md | 1 + app/api/__init__.py | 0 app/app.py | 50 +++++++++++++++++ app/config/logging.yaml | 26 +++++++++ app/core/__init__.py | 0 app/core/config.py | 101 +++++++++++++++++++++++++++++++++++ app/core/dependencies.py | 6 +++ app/core/environment.py | 39 ++++++++++++++ app/core/logging.py | 13 +++++ app/database/__init__.py | 0 app/database/db.py | 39 ++++++++++++++ app/database/enums.py | 15 ++++++ app/database/models.py | 54 +++++++++++++++++++ app/main.py | 11 ++++ app/schemas/users.py | 12 +++++ app/services/auth_service.py | 9 ++++ 16 files changed, 376 insertions(+) create mode 100644 app/README.md create mode 100644 app/api/__init__.py create mode 100644 app/app.py create mode 100644 app/config/logging.yaml create mode 100644 app/core/__init__.py create mode 100644 app/core/config.py create mode 100644 app/core/dependencies.py create mode 100644 app/core/environment.py create mode 100644 app/core/logging.py create mode 100644 app/database/__init__.py create mode 100644 app/database/db.py create mode 100644 app/database/enums.py create mode 100644 app/database/models.py create mode 100644 app/main.py create mode 100644 app/schemas/users.py create mode 100644 app/services/auth_service.py diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..781894a --- /dev/null +++ b/app/README.md @@ -0,0 +1 @@ +# Backend diff --git a/app/api/__init__.py b/app/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/app.py b/app/app.py new file mode 100644 index 0000000..8508022 --- /dev/null +++ b/app/app.py @@ -0,0 +1,50 @@ +import logging + +from fastapi import APIRouter, FastAPI +from fastapi.concurrency import asynccontextmanager +from fastapi.params import Depends + +from app.api import ( + login, +) +from app.core.config import Settings, get_settings + +logger = logging.getLogger(__name__) +router = APIRouter() + + +# @router.get("/health", summary="Health Check") +# async def health_check(): +# """Health check endpoint to verify if the application is running.""" +# return {"status": "ok", "message": "Application is running"} + + +@router.get("/config", summary="Configuration") +async def get_config(s: Settings = Depends(get_settings)): + """Get application configuration.""" + return s.__dict__ + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Application lifespan event handler.""" + logger.info("Starting application...") + + # TODO: Any client initialization can be done here + # s = get_settings() + + logger.info("Application started") + yield + + logger.info("Application stopped") + + +def create_app() -> FastAPI: + app = FastAPI(title="Fastapi-Template", lifespan=lifespan) + + # Setup routers + routers = [router, login.router] + for r in routers: + app.include_router(r) + + return app diff --git a/app/config/logging.yaml b/app/config/logging.yaml new file mode 100644 index 0000000..aa5d002 --- /dev/null +++ b/app/config/logging.yaml @@ -0,0 +1,26 @@ +version: 1 +disable_existing_loggers: false + +formatters: + simple: + format: "%(name)s:\t%(message)s" + +handlers: + console: + (): rich.logging.RichHandler + level: INFO + formatter: simple + markup: true + rich_tracebacks: true + +loggers: + # Assuming 'app' is the main app package + app: { level: INFO } + scripts: { level: INFO } + # Configure uvicorn's loggers if running with uvicorn directly + uvicorn.error: { level: WARNING } + uvicorn.access: { level: WARNING } + +root: + level: INFO + handlers: [console] diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/config.py b/app/core/config.py new file mode 100644 index 0000000..5204f7e --- /dev/null +++ b/app/core/config.py @@ -0,0 +1,101 @@ +from functools import lru_cache +from pathlib import Path +from typing import Annotated, Any, Literal + +from pydantic import AnyUrl, BeforeValidator, PostgresDsn, computed_field +from pydantic_core import MultiHostUrl +from pydantic_settings import BaseSettings, SettingsConfigDict + + +def parse_cors(v: Any) -> list[str] | str: + if isinstance(v, str) and not v.startswith("["): + return [i.strip() for i in v.split(",")] + elif isinstance(v, list | str): + return v + raise ValueError(v) + + +def get_env_file_path() -> str | None: + """ + Returns the file path to the .env file. + + Returns: + str | None: The file path to the .env file or None if not found. + """ + + possible_paths = [ + ".env", + # "../.env", + # "../../.env", + # "./.env", + ] + + for path in possible_paths: + if Path(path).exists(): + abs_path = Path(path).resolve() + return str(abs_path) + + return None + + +class Settings(BaseSettings): + + model_config = SettingsConfigDict( + env_file=get_env_file_path(), + env_ignore_empty=True, + extra="ignore", + ) + + PROJECT_NAME: str | None + + # Auth Configuration + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 + + # Frontend Configuration + FRONTEND_HOST: str = "http://localhost:3000" + ENVIRONMENT: Literal["development", "production"] = "development" + + BACKEND_CORS_ORIGINS: Annotated[list[AnyUrl] | str, BeforeValidator(parse_cors)] = ( + [] + ) + + @computed_field # type: ignore[prop-decorator] + @property + def all_cors_origins(self) -> list[str]: + """ + Dynamically calculates CORS origins by combining backend CORS origins with the frontend host. + + Example Input: + - BACKEND_CORS_ORIGINS = ["http://localhost:8000/", "https://api.myapp.com"] + - FRONTEND_HOST = "http://localhost:3000" + + Result: ["http://localhost:8000", "https://api.myapp.com", "http://localhost:3000"] + """ + return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [ + self.FRONTEND_HOST + ] + + # DB Config + # - Currently, this is configured to use Supabase's PostgreSQL database. + POSTGRES_SERVER: str + POSTGRES_PORT: int = 5432 + POSTGRES_USER: str + POSTGRES_PASSWORD: str = "" + POSTGRES_DB: str = "" + + @computed_field # type: ignore[prop-decorator] + @property + def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn: + return MultiHostUrl.build( + scheme="postgresql+asyncpg", + username=self.POSTGRES_USER, + password=self.POSTGRES_PASSWORD, + host=self.POSTGRES_SERVER, + port=self.POSTGRES_PORT, + path=self.POSTGRES_DB, + ) + + +@lru_cache # builds once, the first time it’s asked for +def get_settings() -> Settings: + return Settings() diff --git a/app/core/dependencies.py b/app/core/dependencies.py new file mode 100644 index 0000000..642d974 --- /dev/null +++ b/app/core/dependencies.py @@ -0,0 +1,6 @@ +from fastapi import Depends +from sqlalchemy.ext.asyncio import AsyncSession + +# from app.database.db import get_db + +# Define dependencies here \ No newline at end of file diff --git a/app/core/environment.py b/app/core/environment.py new file mode 100644 index 0000000..062e4e4 --- /dev/null +++ b/app/core/environment.py @@ -0,0 +1,39 @@ +import logging +import os + +from dotenv import load_dotenv + +logger = logging.getLogger("scripts") + + +def load_app_env(): + """Load environment variables from .env file.""" + # Check if running in Docker and skip loading .env if true + logger.info("Loading environment variables...") + if os.getenv("RUNNING_IN_DOCKER") == "true": + logger.info("Skipping `load_app_env`, Docker environment detected.") + return + + # Load .env file from the specified paths + try: + paths = [".env"] # Add more paths if needed + for path in paths: + if os.path.exists(path): + load_dotenv(path, override=True) + logger.info(f"βœ… Loading .env from: {os.path.abspath(path)}") + return + logger.warning("❌ No .env file found") + load_dotenv(override=True) + logger.info("Environment variables loaded successfully.") + + except Exception as e: + logger.error(f"Failed to load environment variables: {e}") + raise + + +# TODO: Function is not working, needs to be awaited. +# def _running_in_docker() -> bool: +# """ +# Returns True when we are inside a Docker container. +# """ +# return Path("/.dockerenv").exists() # present in every Linux container diff --git a/app/core/logging.py b/app/core/logging.py new file mode 100644 index 0000000..923632a --- /dev/null +++ b/app/core/logging.py @@ -0,0 +1,13 @@ +import logging.config +from pathlib import Path + +import yaml + + +def setup_logging(): + """Load logging configuration from YAML file.""" + config_path = Path(__file__).resolve().parents[1] / "config" / "logging.yaml" + with open(config_path) as f: + config = yaml.safe_load(f) + + logging.config.dictConfig(config) diff --git a/app/database/__init__.py b/app/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/database/db.py b/app/database/db.py new file mode 100644 index 0000000..05077f5 --- /dev/null +++ b/app/database/db.py @@ -0,0 +1,39 @@ +import logging +from typing import AsyncGenerator + +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine + +from app.core.config import get_settings + +logger = logging.getLogger(__name__) + +settings = get_settings() + +# TODO: Set dynamic echo based on environment + +engine = create_async_engine( + str(settings.SQLALCHEMY_DATABASE_URI), + echo=False, # Set to True for debugging, False in production +) + +AsyncSessionLocal = async_sessionmaker( + engine, + class_=AsyncSession, + autocommit=False, + autoflush=False, + expire_on_commit=False, +) + +logger.info(f"Database engine created with URL: {engine.url}") + + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + """FastAPI dependency for async database sessions""" + async with AsyncSessionLocal() as session: + try: + async with session.begin(): + yield session + except Exception as e: + logger.error(f"Database session error: {e}") + await session.rollback() + raise diff --git a/app/database/enums.py b/app/database/enums.py new file mode 100644 index 0000000..c1ac753 --- /dev/null +++ b/app/database/enums.py @@ -0,0 +1,15 @@ +""" +Database enumerations for the application. +""" + +from enum import Enum + + +class UserType(Enum): + """User types enum""" + + ADMIN = "admin" + USER = "user" + + + diff --git a/app/database/models.py b/app/database/models.py new file mode 100644 index 0000000..2d3bb32 --- /dev/null +++ b/app/database/models.py @@ -0,0 +1,54 @@ +import uuid +from datetime import datetime +from typing import Optional + +from sqlalchemy import ( + DECIMAL, + Boolean, + DateTime, +) +from sqlalchemy import Enum as SQLEnum +from sqlalchemy import ( + ForeignKey, + Integer, + String, + Text, + func, +) +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship + +from app.database.enums import UserType + + +# Base class for all models +class Base(DeclarativeBase): + pass + + +class User(Base): + __tablename__ = "users" + + id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 + ) + email: Mapped[str] = mapped_column( + String(255), unique=True, index=True, nullable=False + ) + first_name: Mapped[str] = mapped_column(String(100), nullable=False) + last_name: Mapped[str] = mapped_column(String(100), nullable=False) + password: Mapped[str] = mapped_column( + String(255), nullable=False + ) # Will store hashed password + user_type: Mapped[UserType] = mapped_column( + SQLEnum(UserType), nullable=False, default=UserType.USER + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), nullable=False + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False, + ) diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..3e3bce4 --- /dev/null +++ b/app/main.py @@ -0,0 +1,11 @@ +from app.app import create_app +from app.core.environment import load_app_env +from app.core.logging import setup_logging + +# Set up logging configuration +setup_logging() + +# #TODO: Improve load_dotenv to cater for docker instance vs uvicorn instance +load_app_env() + +app = create_app() diff --git a/app/schemas/users.py b/app/schemas/users.py new file mode 100644 index 0000000..b61a06a --- /dev/null +++ b/app/schemas/users.py @@ -0,0 +1,12 @@ +import uuid + +from pydantic import BaseModel, Field + +class UserBase(BaseModel): + """Base schema for user data""" + #TODO: to modify + id: uuid.UUID = Field(..., description="User unique identifier") + email: str = Field(..., max_length=255, description="User email address") + first_name: str = Field(..., max_length=100, description="User's first name") + last_name: str = Field(..., max_length=100, description="User's last name") + user_type: str = Field(..., description="Type of user (e.g., admin, user)") \ No newline at end of file diff --git a/app/services/auth_service.py b/app/services/auth_service.py new file mode 100644 index 0000000..3a9f9c0 --- /dev/null +++ b/app/services/auth_service.py @@ -0,0 +1,9 @@ +import logging + + +logger = logging.getLogger(__name__) + + +class AuthService: + def __init__(self, db: AsyncSession): + self.db = db \ No newline at end of file