Showing preview only (463K chars total). Download the full file or copy to clipboard to get everything.
Repository: vintasoftware/nextjs-fastapi-template
Branch: main
Commit: 62b67456e8f0
Files: 144
Total size: 425.3 KB
Directory structure:
gitextract_r3jbnx09/
├── .github/
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ ├── ci.yml
│ ├── pre-commit.yml
│ └── release.yml
├── .gitignore
├── .pre-commit-config.docker.yaml
├── .pre-commit-config.yaml
├── CHANGELOG.md
├── LICENSE.txt
├── Makefile
├── README.md
├── docker-compose.yml
├── docs/
│ ├── additional-settings.md
│ ├── contributing.md
│ ├── deployment.md
│ ├── get-started.md
│ ├── stylesheets/
│ │ └── extra.css
│ ├── support.md
│ └── technology-selection.md
├── fastapi_backend/
│ ├── .gitignore
│ ├── Dockerfile
│ ├── alembic.ini
│ ├── alembic_migrations/
│ │ ├── README
│ │ ├── env.py
│ │ ├── script.py.mako
│ │ └── versions/
│ │ ├── 402d067a8b92_added_user_table.py
│ │ └── b389592974f8_add_item_model.py
│ ├── api/
│ │ └── index.py
│ ├── app/
│ │ ├── __init__.py
│ │ ├── config.py
│ │ ├── database.py
│ │ ├── email.py
│ │ ├── email_templates/
│ │ │ ├── __init__.py
│ │ │ └── password_reset.html
│ │ ├── main.py
│ │ ├── models.py
│ │ ├── routes/
│ │ │ ├── __init__.py
│ │ │ └── items.py
│ │ ├── schemas.py
│ │ ├── users.py
│ │ └── utils.py
│ ├── commands/
│ │ ├── __init__.py
│ │ └── generate_openapi_schema.py
│ ├── mypy.ini
│ ├── pyproject.toml
│ ├── pytest.ini
│ ├── requirements.txt
│ ├── start.sh
│ ├── tests/
│ │ ├── __init__.py
│ │ ├── commands/
│ │ │ ├── __init__.py
│ │ │ ├── files/
│ │ │ │ ├── openapi_test.json
│ │ │ │ └── openapi_test_output.json
│ │ │ └── test_generate_openapi_schema.py
│ │ ├── conftest.py
│ │ ├── main/
│ │ │ ├── __init__.py
│ │ │ └── test_main.py
│ │ ├── routes/
│ │ │ ├── __init__.py
│ │ │ └── test_items.py
│ │ ├── test_database.py
│ │ ├── test_email.py
│ │ └── utils/
│ │ ├── __init__.py
│ │ └── test_utils.py
│ ├── vercel.json
│ ├── vercel.prod.json
│ └── watcher.py
├── local-shared-data/
│ └── openapi.json
├── mkdocs.yml
├── nextjs-frontend/
│ ├── .gitignore
│ ├── .prettierignore
│ ├── Dockerfile
│ ├── __tests__/
│ │ ├── login.test.tsx
│ │ ├── loginPage.test.tsx
│ │ ├── passwordReset.test.tsx
│ │ ├── passwordResetConfirm.test.tsx
│ │ ├── passwordResetConfirmPage.test.tsx
│ │ ├── passwordResetPage.test.tsx
│ │ ├── register.test.ts
│ │ └── registerPage.test.tsx
│ ├── app/
│ │ ├── clientService.ts
│ │ ├── dashboard/
│ │ │ ├── add-item/
│ │ │ │ └── page.tsx
│ │ │ ├── deleteButton.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── login/
│ │ │ └── page.tsx
│ │ ├── openapi-client/
│ │ │ ├── client/
│ │ │ │ ├── client.gen.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── types.gen.ts
│ │ │ │ └── utils.gen.ts
│ │ │ ├── client.gen.ts
│ │ │ ├── core/
│ │ │ │ ├── auth.gen.ts
│ │ │ │ ├── bodySerializer.gen.ts
│ │ │ │ ├── params.gen.ts
│ │ │ │ ├── pathSerializer.gen.ts
│ │ │ │ ├── serverSentEvents.gen.ts
│ │ │ │ ├── types.gen.ts
│ │ │ │ └── utils.gen.ts
│ │ │ ├── index.ts
│ │ │ ├── sdk.gen.ts
│ │ │ └── types.gen.ts
│ │ ├── page.tsx
│ │ ├── password-recovery/
│ │ │ ├── confirm/
│ │ │ │ └── page.tsx
│ │ │ └── page.tsx
│ │ └── register/
│ │ └── page.tsx
│ ├── components/
│ │ ├── actions/
│ │ │ ├── items-action.ts
│ │ │ ├── login-action.ts
│ │ │ ├── logout-action.ts
│ │ │ ├── password-reset-action.ts
│ │ │ └── register-action.ts
│ │ ├── page-pagination.tsx
│ │ ├── page-size-selector.tsx
│ │ └── ui/
│ │ ├── FormError.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── select.tsx
│ │ ├── submitButton.tsx
│ │ ├── table.tsx
│ │ └── tabs.tsx
│ ├── components.json
│ ├── eslint.config.mjs
│ ├── jest.config.ts
│ ├── next.config.mjs
│ ├── openapi-ts.config.ts
│ ├── openapi.json
│ ├── package.json
│ ├── pnpm-workspace.yaml
│ ├── postcss.config.js
│ ├── proxy.ts
│ ├── start.sh
│ ├── tailwind.config.js
│ ├── tsconfig.json
│ ├── vercel.json
│ └── watcher.js
├── overrides/
│ └── main.html
├── prod-backend-deploy.yml
└── prod-frontend-deploy.yml
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
<!--- Provide a general summary of your changes in the Title above -->
## Description
<!--- Describe your changes in detail -->
## Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
## Screenshots (if appropriate):
## Steps to reproduce (if appropriate):
## Types of changes
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
## Checklist:
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
- [ ] My code follows the code style of this project.
- [ ] My change requires documentation updates.
- [ ] I have updated the documentation accordingly.
- [ ] My change requires dependencies updates.
- [ ] I have updated the dependencies accordingly.
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
push:
pull_request:
jobs:
build-fastapi:
name: FastAPI CI
runs-on: ubuntu-latest
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
TEST_DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
ACCESS_SECRET_KEY: ${{ secrets.ACCESS_SECRET_KEY }}
RESET_PASSWORD_SECRET_KEY: ${{ secrets.RESET_PASSWORD_SECRET_KEY }}
VERIFICATION_SECRET_KEY: ${{ secrets.VERIFICATION_SECRET_KEY }}
CORS_ORIGINS: ${{ secrets.CORS_ORIGINS }}
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: testdatabase
ports:
- 5433:5432
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Install the project
working-directory: ./fastapi_backend
run: uv sync --all-extras --dev
- name: Run tests
working-directory: ./fastapi_backend
run: uv run coverage run -m pytest
- name: Generate XML coverage report
working-directory: ./fastapi_backend
run: uv run coverage xml -o coverage.xml
- name: Coveralls GitHub Action
uses: coverallsapp/github-action@v2.3.4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
flag-name: python-coverage
parallel: true
path-to-lcov: fastapi_backend/coverage.xml
build-frontend:
name: Next.js CI
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
run_install: false
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Node dependencies
working-directory: ./nextjs-frontend
run: pnpm install
- name: Run tests
working-directory: ./nextjs-frontend
run: pnpm run coverage
- name: Coveralls GitHub Action
uses: coverallsapp/github-action@v2.3.4
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
flag-name: node-coverage
parallel: true
finish:
name: Coveralls
needs: [ build-fastapi, build-frontend ]
runs-on: ubuntu-latest
steps:
- name: Close parallel build
uses: coverallsapp/github-action@v2.3.4
with:
parallel-finished: true
carryforward: "python-coverage,node-coverage"
================================================
FILE: .github/workflows/pre-commit.yml
================================================
name: pre-commit
on:
push:
pull_request:
jobs:
pre-commit:
runs-on: ubuntu-latest
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
TEST_DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
ACCESS_SECRET_KEY: ${{ secrets.ACCESS_SECRET_KEY }}
RESET_PASSWORD_SECRET_KEY: ${{ secrets.RESET_PASSWORD_SECRET_KEY }}
VERIFICATION_SECRET_KEY: ${{ secrets.VERIFICATION_SECRET_KEY }}
OPENAPI_OUTPUT_FILE: ${{ secrets.OPENAPI_OUTPUT_FILE }}
CORS_ORIGINS: ${{ secrets.CORS_ORIGINS }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install uv
uses: astral-sh/setup-uv@v5
- name: Install the project
working-directory: ./fastapi_backend
run: uv sync --all-extras --dev
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 9
run_install: false
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install Node dependencies
working-directory: ./nextjs-frontend
run: pnpm install
- name: Run pre-commit
uses: pre-commit/action@v3.0.1
================================================
FILE: .github/workflows/release.yml
================================================
name: Release draft creation
on:
workflow_dispatch:
inputs:
version:
description: "Version of the release"
required: true
type: number
jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Extract changelog for version
run: |
VERSION=${{ github.event.inputs.version }}
# Extract changelog for version
CHANGELOG=$(awk -v version="$VERSION" 'BEGIN{RS="## "; FS="\n"} $0 ~ version {print "## "$0}' CHANGELOG.md)
# Remove the first line (version title)
CHANGELOG=$(echo "$CHANGELOG" | sed '1d')
# Verify if changelog was found
if [ -z "$CHANGELOG" ]; then
echo "Changelog for version $VERSION not found"
exit 1
fi
# Set output
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Create GitHub Release Draft
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.inputs.version }}
name: ${{ github.event.inputs.version }}
body: ${{ env.CHANGELOG }}
draft: true
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Summary
run: |
echo "## 🚀 Release Summary" >> $GITHUB_STEP_SUMMARY
echo "Release draft created for version ${{ github.event.inputs.version }}." >> $GITHUB_STEP_SUMMARY
echo "Visit the [Releases section](https://github.com/vintasoftware/nextjs-fastapi-template/releases) to review and publish the release." >> $GITHUB_STEP_SUMMARY
echo "Once the draft is published, another action will automatically be triggered to publish the packages." >> $GITHUB_STEP_SUMMARY
================================================
FILE: .gitignore
================================================
# 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
.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
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__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/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
================================================
FILE: .pre-commit-config.docker.yaml
================================================
fail_fast: true
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-added-large-files
args: ["--maxkb=500"]
exclude: >
(?x)^(
package-lock\.json
)$
- id: fix-byte-order-marker
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: debug-statements
- id: detect-private-key
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.8
hooks:
# Run the linter.
- id: ruff
args: [ --fix ]
# Run the formatter.
- id: ruff-format
- repo: local
hooks:
- id: generate-openapi-schema
name: generate OpenAPI schema
entry: sh -c 'docker compose run --rm --no-deps -T backend uv run python -m commands.generate_openapi_schema'
language: system
# Only run OpenAPI schema generation if schemas.py, main.py or package version have changed:
files: (main\.py$|schemas\.py$|pyproject\.toml)
pass_filenames: false
- id: generate-frontend-client
name: generate frontend client
entry: sh -c 'docker compose run --rm --no-deps -T frontend pnpm run generate-client'
language: system
# Only run frontend client generation if frontend files have changed:
files: ^local-shared-data/openapi\.json$
pass_filenames: false
================================================
FILE: .pre-commit-config.yaml
================================================
fail_fast: true
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
args: ["--maxkb=500"]
exclude: >
(?x)^(
package-lock\.json
)$
- id: fix-byte-order-marker
- id: check-case-conflict
- id: check-merge-conflict
- id: check-symlinks
- id: debug-statements
- id: detect-private-key
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.2
hooks:
# Run the linter.
- id: ruff
args: [--fix]
# Run the formatter.
- id: ruff-format
- repo: local
hooks:
- id: frontend-lint
name: run frontend lint
entry: sh -c 'cd nextjs-frontend && pnpm run lint'
language: system
types: [ file ]
files: ^nextjs-frontend/.*\.(js|jsx|ts|tsx)$
pass_filenames: true
- id: frontend-prettier
name: Run Prettier on frontend files
entry: sh -c 'cd nextjs-frontend && pnpm run prettier'
language: system
types: [ file ]
files: ^nextjs-frontend/.*\.(js|jsx|ts|tsx)$
pass_filenames: true
- id: frontend-tsc
name: run frontend tsc
entry: sh -c 'cd nextjs-frontend && pnpm run tsc'
language: system
types: [ file ]
files: ^nextjs-frontend/.*\.(ts|tsx)$
pass_filenames: false
- id: generate-openapi-schema
name: generate OpenAPI schema
entry: sh -c 'cd fastapi_backend && uv run python -m commands.generate_openapi_schema'
language: system
# Only run OpenAPI schema generation if schemas.py, main.py or package version have changed:
files: (main\.py$|schemas\.py$|pyproject\.toml)
pass_filenames: false
- id: generate-frontend-client
name: generate frontend client
entry: sh -c 'cd nextjs-frontend && pnpm run generate-client'
language: system
# Only run frontend client generation if frontend files have changed:
files: openapi\.json$
pass_filenames: false
================================================
FILE: CHANGELOG.md
================================================
# Changelog
This changelog references changes made both to the FastAPI backend, `fastapi_backend`, and the
frontend TypeScript client, `nextjs-frontend`.
!!! note
The backend and the frontend are versioned together, that is, they have the same version number.
When you update the backend, you should also update the frontend to the same version.
## 0.0.8 <small>December 17, 2025</small> {id="0.0.8"}
- Upgrade Next.js version to latest version
## 0.0.7 <small>October 24, 2025</small> {id="0.0.7"}
- Upgrade @hey-api/openapi-ts version to ^0.83.1
## 0.0.6 <small>September 1, 2025</small> {id="0.0.6"}
- Upgrade Next.js version to 15.5.0
## 0.0.5 <small>July 9, 2025</small> {id="0.0.5"}
- Items Pagination
## 0.0.4 <small>July 9, 2025</small> {id="0.0.4"}
- Fix ESlint missing for pre-commit
## 0.0.3 <small>April 23, 2025</small> {id="0.0.3"}
- Created docs
## 0.0.2 <small>March 12, 2025</small> {id="0.0.2"}
- Generate release draft using github actions
## 0.0.1 <small>March 12, 2025</small> {id="0.0.1"}
- Initial release
================================================
FILE: LICENSE.txt
================================================
MIT License
Copyright (c) 2017 Vinta Serviços e Soluções Tecnológicas Ltda
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: Makefile
================================================
# Makefile
# Variables
BACKEND_DIR=fastapi_backend
FRONTEND_DIR=nextjs-frontend
DOCKER_COMPOSE=docker compose
# Help
.PHONY: help
help:
@echo "Available commands:"
@awk '/^[a-zA-Z_-]+:/{split($$1, target, ":"); print " " target[1] "\t" substr($$0, index($$0,$$2))}' $(MAKEFILE_LIST)
# Backend commands
.PHONY: start-backend test-backend
start-backend: ## Start the backend server with FastAPI and hot reload
cd $(BACKEND_DIR) && ./start.sh
test-backend: ## Run backend tests using pytest
cd $(BACKEND_DIR) && uv run pytest
# Frontend commands
.PHONY: start-frontend test-frontend
start-frontend: ## Start the frontend server with pnpm and hot reload
cd $(FRONTEND_DIR) && ./start.sh
test-frontend: ## Run frontend tests using npm
cd $(FRONTEND_DIR) && pnpm run test
# Docker commands
.PHONY: docker-backend-shell docker-frontend-shell docker-build docker-build-backend \
docker-build-frontend docker-start-backend docker-start-frontend docker-up-test-db \
docker-migrate-db docker-db-schema docker-test-backend docker-test-frontend
docker-backend-shell: ## Access the backend container shell
$(DOCKER_COMPOSE) run --rm backend sh
docker-frontend-shell: ## Access the frontend container shell
$(DOCKER_COMPOSE) run --rm frontend sh
docker-build: ## Build all the services
$(DOCKER_COMPOSE) build --no-cache
docker-build-backend: ## Build the backend container with no cache
$(DOCKER_COMPOSE) build backend --no-cache
docker-build-frontend: ## Build the frontend container with no cache
$(DOCKER_COMPOSE) build frontend --no-cache
docker-start-backend: ## Start the backend container
$(DOCKER_COMPOSE) up backend
docker-start-frontend: ## Start the frontend container
$(DOCKER_COMPOSE) up frontend
docker-up-test-db: ## Start the test database container
$(DOCKER_COMPOSE) up db_test
docker-migrate-db: ## Run database migrations using Alembic
$(DOCKER_COMPOSE) run --rm backend alembic upgrade head
docker-db-schema: ## Generate a new migration schema. Usage: make docker-db-schema migration_name="add users"
$(DOCKER_COMPOSE) run --rm backend alembic revision --autogenerate -m "$(migration_name)"
docker-test-backend: ## Run tests for the backend
$(DOCKER_COMPOSE) run --rm backend pytest
docker-test-frontend: ## Run tests for the frontend
$(DOCKER_COMPOSE) run --rm frontend pnpm run test
docker-up-mailhog: ## Start mailhog server
$(DOCKER_COMPOSE) up mailhog
================================================
FILE: README.md
================================================
## Next.js FastAPI Template
<a href="https://www.vintasoftware.com/blog/next-js-fastapi-template"><img src="docs/images/banner.png" alt="Next.js FastAPI Template" width="auto"></a>
<p align="center">
<em>Next.js FastAPI Template: Python + Modern TypeScript stack with Zod validation.</em>
</p>
<p align="center">
<a href="https://github.com/vintasoftware/nextjs-fastapi-template/actions/workflows/ci.yml" target="_blank">
<img src="https://github.com/vintasoftware/nextjs-fastapi-template/actions/workflows/ci.yml/badge.svg" alt="CI">
</a>
<a href="https://coveralls.io/github/vintasoftware/nextjs-fastapi-template" target="_blank">
<img src="https://coveralls.io/repos/github/vintasoftware/nextjs-fastapi-template/badge.svg" alt="Coverage">
</a>
</p>
---
**Documentation**: <a href="https://vintasoftware.github.io/nextjs-fastapi-template/" target="_blank">https://vintasoftware.github.io/nextjs-fastapi-template/</a>
**Source Code**: <a href="https://github.com/vintasoftware/nextjs-fastapi-template/" target="_blank">https://github.com/vintasoftware/nextjs-fastapi-template/</a>
---
The Next.js FastAPI Template provides a solid foundation for scalable, high-performance web applications, following clean architecture and best practices. It simplifies development by integrating FastAPI, Pydantic, and Next.js with TypeScript and Zod, ensuring end-to-end type safety and schema validation between frontend and backend.
The FastAPI backend supports fully asynchronous operations, optimizing database queries, API routes, and test execution for better performance. Deployment is seamless, with both backend and frontend fully deployable to Vercel, enabling quick product releases with minimal configuration.
### Key features
✔ End-to-end type safety – Automatically generated typed clients from the OpenAPI schema ensure seamless API contracts between frontend and backend.
✔ Hot-reload updates – The client updates automatically when backend routes change, keeping FastAPI and Next.js in sync.
✔ Versatile foundation – Designed for MVPs and production-ready applications, with a pre-configured authentication system and API layer.
✔ Quick deployment – Deploys a full-stack application—including authentication flow and a dashboard—on Vercel in just a few steps.
✔ Production-ready authentication – Includes a pre-configured authentication system and dashboard interface, allowing you to immediately start development with user management features.
## Technology stack
This template features a carefully selected set of technologies to ensure efficiency, scalability, and ease of use:
- Zod + TypeScript – Type safety and schema validation across the stack.
- fastapi-users – Complete authentication system with:
- Secure password hashing
- JWT authentication
- Email-based password recovery
- shadcn/ui – Prebuilt React components with Tailwind CSS.
- OpenAPI-fetch – Fully typed client generation from the OpenAPI schema.
- UV – Simplified dependency management and packaging.
- Docker Compose – Consistent environments for development and production.
- Pre-commit hooks – Automated code linting, formatting, and validation before commits.
- Vercel Deployment – Serverless backend and scalable frontend, deployable with minimal configuration.
This is a partial list of the technologies included in the template. For a complete overview, visit our [Technology selection](https://vintasoftware.github.io/nextjs-fastapi-template/technology-selection/) page.
## Get Started
To use this template, visit our [Get Started](https://vintasoftware.github.io/nextjs-fastapi-template/get-started/) and follow the steps.
## Using the template? Let's talk!
We’re always curious to see how the community builds on top of it and where it’s being used. To collaborate:
- Join the conversation on [GitHub Discussions](https://github.com/vintasoftware/nextjs-fastapi-template/discussions)
- Report bugs or suggest improvements via [issues](https://github.com/vintasoftware/nextjs-fastapi-template/issues)
- Check the [Contributing](https://vintasoftware.github.io/nextjs-fastapi-template/contributing/) guide to get involved
This project is maintained by [Vinta Software](https://www.vinta.com.br/) and is actively used in production systems we build for clients. Talk to our expert consultants — get a free technical review: contact@vinta.com.br.
*Disclaimer: This project is not affiliated with Vercel.*
================================================
FILE: docker-compose.yml
================================================
services:
backend:
build:
context: fastapi_backend
environment:
- OPENAPI_OUTPUT_FILE=./shared-data/openapi.json
- DATABASE_URL=postgresql+asyncpg://postgres:password@db:5432/mydatabase
- TEST_DATABASE_URL=postgresql+asyncpg://postgres:password@db:5433/testdatabase
- MAIL_SERVER=mailhog
ports:
- "8000:8000"
networks:
- my_network
volumes:
- ./fastapi_backend:/app
- fastapi-venv:/app/.venv
- ./local-shared-data:/app/shared-data
depends_on:
- db
db:
image: postgres:17
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: mydatabase
ports:
- "5432:5432"
networks:
- my_network
volumes:
- postgres_data:/var/lib/postgresql/data
db_test:
image: postgres:17
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: testdatabase
ports:
- "5433:5432"
networks:
- my_network
restart: always
frontend:
build:
context: ./nextjs-frontend
user: node
ports:
- "3000:3000"
networks:
- my_network
environment:
NODE_ENV: development
API_BASE_URL: http://backend:8000
OPENAPI_OUTPUT_FILE: ./shared-data/openapi.json
volumes:
- ./nextjs-frontend:/app
- nextjs-node-modules:/app/node_modules
- ./local-shared-data:/app/shared-data
mailhog:
image: mailhog/mailhog
ports:
- "1025:1025" # SMTP server
- "8025:8025" # Web UI
networks:
- my_network
volumes:
postgres_data:
nextjs-node-modules:
fastapi-venv:
networks:
my_network:
driver: bridge
================================================
FILE: docs/additional-settings.md
================================================
### Production-Ready Authentication & Dashboard features
This template comes with a pre-configured authentication system and a simple dashboard interface, allowing you to start building your application with user management features immediately.
### Hot Reload on development
The project includes two hot reloads running the application, one for the backend and one for the frontend. These automatically restart local servers when they detect changes, ensuring that the application is always up to date without needing manual restarts.
- The **backend hot reload** monitors changes to the backend code.
- The **frontend hot reload** monitors changes to the frontend code and the `openapi.json` schema generated by the backend.
### Manual Execution of Hot Reload Commands
You can manually execute the same commands that the hot reloads call when they detect a change:
1. To export the `openapi.json` schema:
```bash
cd fastapi_backend && uv run python -m commands.generate_openapi_schema
```
or using Docker:
```bash
docker compose run --rm --no-deps -T backend uv run python -m commands.generate_openapi_schema
```
2. To generate the frontend client:
```bash
cd nextjs-frontend && npm run generate-client
```
or using Docker:
```bash
docker compose run --rm --no-deps -T frontend npm run generate-client
```
### Testing
To run the tests, you need to run the test database container:
```bash
make docker-up-test-db
```
Then run the tests locally:
```bash
make test-backend
make test-frontend
```
Or using Docker:
```bash
make docker-test-backend
make docker-test-frontend
```
### Pre-Commit Setup
To maintain code quality and consistency, the project includes two separate pre-commit configuration files:
- `.pre-commit-config.yaml` is used to run pre-commit checks locally.
- `.pre-commit-config.docker.yaml` is used to run pre-commit checks within Docker.
### Installing and Activating Pre-Commit Hooks
To activate pre-commit hooks, run the following commands for each configuration file:
- **For the local configuration file**:
```bash
pre-commit install -c .pre-commit-config.yaml
```
- **For the Docker configuration file**:
```bash
pre-commit install -c .pre-commit-config.docker.yaml
```
### Localhost Email Server Setup
To set up the email server locally, you need to start [MailHog](https://github.com/mailhog/MailHog) by running the following command:
```bash
make docker-up-mailhog
```
- **Email client**: Access the email at `http://localhost:8025`.
### Running Pre-Commit Checks
To manually run the pre-commit checks on all files, use:
```bash
pre-commit run --all-files -c .pre-commit-config.yaml
```
or
```bash
pre-commit run --all-files -c .pre-commit-config.docker.yaml
```
### Updating Pre-Commit Hooks
To update the hooks to their latest versions, run:
```bash
pre-commit autoupdate
```
### Alembic Database Migrations
If you need to create a new Database Migration:
```bash
make docker-db-schema migration_name="add users"
```
then apply the migration to the database:
```bash
make docker-migrate-db
```
### GitHub Actions
This project has a pre-configured GitHub Actions setup to enable CI/CD. The workflow configuration files are inside the .github/workflows directory. You can customize these workflows to suit your project's needs better.
### Secrets Configuration
For the workflows to function correctly, add the secret keys to your GitHub repository's settings. Navigate to Settings > Secrets and variables > Actions and add the following keys:
```
DATABASE_URL: The connection string for your primary database.
TEST_DATABASE_URL: The connection string for your test database.
ACCESS_SECRET_KEY: The secret key for access token generation.
RESET_PASSWORD_SECRET_KEY: The secret key for reset password functionality.
VERIFICATION_SECRET_KEY: The secret key for email or user verification.
```
## Makefile
This project includes a `Makefile` that provides a set of commands to simplify everyday tasks such as starting the backend and frontend servers, running tests, building Docker containers, and more.
### Available Commands
You can see all available commands and their descriptions by running the following command in your terminal:
```bash
make help
```
================================================
FILE: docs/contributing.md
================================================
# Contributing
We can always use your help to improve Next.js FastAPI Template! Please feel free to tackle existing [issues](https://github.com/vintasoftware/nextjs-fastapi-template/issues). If you have a new idea, please create a thread on [Discussions](https://github.com/vintasoftware/django-ai-assistant/discussions).
Please follow this guide to learn more about how to develop and test the project locally, before opening a pull request.
## Local Dev Setup
### Clone the repo
```bash
git clone git@github.com:vintasoftware/nextjs-fastapi-template.git
```
Check the [Get Started](get-started.md#setup) page to complete the setup.
## Install pre-commit hooks
Check the [Additional Settings - Install pre-commit hooks](additional-settings.md#pre-commit-setup) section to complete the setup.
It's critical to run the pre-commit hooks before pushing your code to follow the project's code style, and avoid linting errors.
## Updating the OpenAPI schema
It's critical to update the OpenAPI schema when you make changes to the FastAPI routes or related files:
Check the [Additional Settings - Manual execution of hot reload commands](additional-settings.md#manual-execution-of-hot-reload-commands) section to run the command.
## Tests
Check the [Additional Settings - Testing](additional-settings.md#testing) section to run the tests.
## Documentation
We use [mkdocs-material](https://squidfunk.github.io/mkdocs-material/) to generate the documentation from markdown files.
Check the files in the `docs` directory.
To run the documentation locally, you need to run:
```bash
uv run mkdocs serve
```
## Release
!!! info
The backend and the frontend are versioned together, that is, they should have the same version number.
To release and publish a new version, follow these steps:
1. Update the version in `fastapi_backend/pyproject.toml`, `nextjs-frontend/package.json`.
2. Update the changelog in `CHANGELOG.md`.
3. Open a PR with the changes.
4. Once the PR is merged, run the [Release GitHub Action](https://github.com/vintasoftware/nextjs-fastapi-template/actions/workflows/release.yml) to create a draft release.
5. Review the draft release, ensure the description has at least the associated changelog entry, and publish it.
================================================
FILE: docs/deployment.md
================================================
### Overview
Deploying to **Vercel** is supported, with dedicated buttons for the **Frontend** and **Backend** applications. Both require specific configurations during and after deployment to ensure proper functionality.
---
### Frontend Deployment
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvintasoftware%2Fnextjs-fastapi-template%2Ftree%2Fmain%2Fnextjs-frontend&env=API_BASE_URL&envDescription=The%20API_BASE_URL%20is%20the%20backend%20URL%20where%20the%20frontend%20sends%20requests.)
- Click the **Frontend** button above to start the deployment process.
- During deployment, you will be prompted to set the `API_BASE_URL`. Use a placeholder value (e.g., `https://`) for now, as this will be updated with the backend URL later.
- Complete the deployment process [here](#post-deployment-configuration).
### Backend Deployment
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvintasoftware%2Fnextjs-fastapi-template%2Ftree%2Fmain%2Ffastapi_backend&env=CORS_ORIGINS,ACCESS_SECRET_KEY,RESET_PASSWORD_SECRET_KEY,VERIFICATION_SECRET_KEY&stores=%5B%7B%22type%22%3A%22postgres%22%7D%5D)
- Click the **Backend** button above to begin deployment.
- First, set up the database. The connection is automatically configured, so follow the steps, and it should work by default.
- During the deployment process, you will be prompted to configure the following environment variables:
- **CORS_ORIGINS**
- Set this to `["*"]` initially to allow all origins. Later, you can update this with the frontend URL.
- **ACCESS_SECRET_KEY**, **RESET_PASSWORD_SECRET_KEY**, **VERIFICATION_SECRET_KEY**
- During deployment, you can temporarily set these secret keys as plain strings (e.g., `examplekey`). However, you should generate secure keys and update them after the deployment in the **Post-Deployment Configuration** section.
- Complete the deployment process [here](#post-deployment-configuration).
## CI (GitHub Actions) Setup for Production Deployment
We provide the **prod-backend-deploy.yml** and **prod-frontend-deploy.yml** files to enable continuous integration through Github Actions. To connect them to GitHub, simply move them to the .github/workflows/ directory.
You can do it with the following commands:
```bash
mv prod-backend-deploy.yml .github/workflows/prod-backend-deploy.yml
mv prod-frontend-deploy.yml .github/workflows/prod-frontend-deploy.yml
```
### Prerequisites
1. **Create a Vercel Token**:
- Generate your [Vercel Access Token](https://vercel.com/account/tokens).
- Save the token as `VERCEL_TOKEN` in your GitHub secrets.
2. **Install Vercel CLI**:
```bash
pnpm i -g vercel@latest
```
3. Authenticate your account.
```bash
vercel login
```
### Database Creation (Required)
1. **Choosing a Database**
- You can use your database hosted on a different service or opt for the [Neon](https://neon.tech/docs/introduction) database, which integrates seamlessly with Vercel.
2. **Setting Up a Neon Database via Vercel**
- In the **Projects dashboard** page on Vercel, navigate to the **Storage** section.
- Select the option to **Create a Database** to provision a Neon database.
3. **Configuring the Database URL**
- After creating the database, retrieve the **Database URL** provided by Neon.
- Include this URL in your **Environment Variables** under `DATABASE_URL`.
4. **Migrating the Database**
- The database migration will happen automatically during the GitHub action deployment, setting up the necessary tables and schema.
### Frontend Setup
1. Link the nextjs-frontend Project
2. Navigate to the nextjs-frontend directory and run:
```bash
cd nextjs-frontend
vercel link
```
3. Follow the prompts:
- Link to existing project? No
- Modify settings? No
4. Save Project IDs and Add GitHub Secrets:
- Open `nextjs-frontend/.vercel/project.json` and add the following to your GitHub repository secrets:
- `projectId` → `VERCEL_PROJECT_ID_FRONTEND`
- `orgId` → `VERCEL_ORG_ID`
### Backend Setup
1. Link the fastapi_backend Project
2. Navigate to the fastapi_backend directory and run:
```bash
cd fastapi_backend
vercel link --local-config=vercel.prod.json
```
- We use a specific configuration file to set the --local-config value.
3. Follow the prompts:
- Link to existing project? No
- Modify settings? No
4. Save Project IDs and Add GitHub Secrets:
- Open `fastapi_backend/.vercel/project.json` and add the following to your GitHub repository secrets:
- `projectId` → `VERCEL_PROJECT_ID_BACKEND`
- `orgId` → `VERCEL_ORG_ID` (Only in case you haven't added that before)
5. Update requirements.txt file:
```bash
cd fastapi_backend
uv export > requirements.txt
```
- Export a new requirements.txt file is required to vercel deploy when the uv.lock is modified.
### Notes
- Once everything is set up, run `git push`, and the deployment will automatically occur.
- Please ensure you complete the setup for both the frontend and backend separately.
- Refer to the [Vercel CLI Documentation](https://vercel.com/docs/cli) for more details.
- You can find the project_id into the vercel web project settings.
- You can find the organization_id into the vercel web organization settings.
## Post-Deployment Configuration
### Frontend
- Navigate to the **Settings** page of the deployed frontend project.
- Access the **Environment Variables** section.
- Update the `API_BASE_URL` variable with the backend URL once the backend deployment is complete.
### Backend
- Access the **Settings** page of the deployed backend project.
- Navigate to the **Environment Variables** section and update the following variables with secure values:
- **CORS_ORIGINS**
- Once the frontend is deployed, replace `["*"]` with the actual frontend URL.
- **ACCESS_SECRET_KEY**
- Generate a secure key for API access and set it here.
- **RESET_PASSWORD_SECRET_KEY**
- Generate a secure key for password reset functionality and set it.
- **VERIFICATION_SECRET_KEY**
- Generate a secure key for user verification and configure it.
- For detailed instructions on setting these secret keys, please look at the section on [Setting up Environment Variables](get-started.md#setting-up-environment-variables).
### Fluid serverless activation
[Fluid](https://vercel.com/docs/functions/fluid-compute) is Vercel's new concurrency model for serverless functions, allowing them to handle multiple
requests per execution instead of spinning up a new instance for each request. This improves performance,
reduces cold starts, and optimizes resource usage, making serverless workloads more efficient.
Follow this [guide](https://vercel.com/docs/functions/fluid-compute#how-to-enable-fluid-compute) to activate Fluid.
================================================
FILE: docs/get-started.md
================================================
To use this template for your own project:
1. Create a new repository using this template by following GitHub's [template repository guide](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template#creating-a-repository-from-a-template)
2. Clone your new repository and navigate to it: `cd your-project-name`
3. Make sure you have Python 3.12 installed
Once completed, proceed to the [Setup](#setup) section below.
## Setup
### Installing Required Tools
#### 1. uv
uv is used to manage Python dependencies in the backend. Install uv by following the [official installation guide](https://docs.astral.sh/uv/getting-started/installation/).
#### 2. Node.js, npm, and pnpm
To run the frontend, ensure Node.js and npm are installed. Follow the [Node.js installation guide](https://nodejs.org/en/download/).
After that, install pnpm by running:
```bash
npm install -g pnpm
```
#### 3. Docker
Docker is needed to run the project in a containerized environment. Follow the appropriate installation guide:
- [Install Docker for Mac](https://docs.docker.com/docker-for-mac/install/)
- [Install Docker for Windows](https://docs.docker.com/docker-for-windows/install/)
- [Get Docker CE for Linux](https://docs.docker.com/install/linux/docker-ce/)
#### 4. Docker Compose
Ensure `docker-compose` is installed. Refer to the [Docker Compose installation guide](https://docs.docker.com/compose/install/).
### Setting Up Environment Variables
**Backend (`fastapi_backend/.env`):**
Copy the `.env.example` files to `.env` and update the variables with your own values.
```bash
cd fastapi_backend && cp .env.example .env
```
You will only need to update the secret keys. You can use the following command to generate a new secret key:
```bash
python3 -c "import secrets; print(secrets.token_hex(32))"
```
- The DATABASE, MAIL, OPENAPI, CORS, and FRONTEND_URL settings are ready to use locally.
- The DATABASE and MAIL settings are already configured in Docker Compose if you're using Docker.
- The OPENAPI_URL setting is commented out. Uncommenting it will hide the /docs and openapi.json URLs, which is ideal for production.
You can check the .env.example file for more information about the variables.
**Frontend (`nextjs-frontend/.env.local`):**
Copy the `.env.example` files to `.env.local`. These values are unlikely to change, so you can leave them as they are.
```bash
cd nextjs-frontend && cp .env.example .env.local
```
### Running the Database
Use Docker to run the database to avoid local installation issues. Build and start the database container:
```bash
docker compose build db
docker compose up -d db
```
Run the following command to apply database migrations:
```bash
make docker-migrate-db
```
### Build the project (without Docker):
To set the project environment locally, use the following commands:
#### Backend
Navigate to the `fastapi_backend` directory and run:
```bash
uv sync
```
#### Frontend
Navigate to the `nextjs-frontend` directory and run:
```bash
pnpm install
```
### Build the project (with Docker):
Build the backend and frontend containers:
```bash
make docker-build
```
## Running the Application
**If you are not using Docker:**
Start the FastAPI server:
```bash
make start-backend
```
Start the Next.js development server:
```bash
make start-frontend
```
**If you are using Docker:**
Start the FastAPI server container:
```bash
make docker-start-backend
```
Start the Next.js development server container:
```bash
make docker-start-frontend
```
- **Backend**: Access the API at `http://localhost:8000`.
- **Frontend**: Access the web application at `http://localhost:3000`.
## Important Considerations
- **Environment Variables**: Ensure your `.env` files are up-to-date.
- **Database Setup**: It is recommended to use Docker to run the database, even when running the backend and frontend locally, to simplify configuration and avoid potential conflicts.
- **Consistency**: It is **not recommended** to switch between running the project locally and using Docker, as this may cause permission issues or unexpected problems. You can choose one method and stick with it.
================================================
FILE: docs/stylesheets/extra.css
================================================
:root > * {
--md-primary-fg-color: #004BC9;
}
================================================
FILE: docs/support.md
================================================
# Support
If you have any questions or need help, feel free to create a thread on [GitHub Discussions](https://github.com/vintasoftware/nextjs-fastapi-template/discussions).
In case you're facing a bug, please [check existing issues](https://github.com/vintasoftware/nextjs-fastapi-template/issues) and create a new one if needed.
## Commercial Support
[](https://www.vintasoftware.com/)
This is an open-source project maintained by [Vinta Software](https://www.vinta.com.br/). We are always looking for exciting work! If you need any commercial support, feel free to get in touch: contact@vinta.com.br
================================================
FILE: docs/technology-selection.md
================================================
This template streamlines building APIs with [FastAPI](https://fastapi.tiangolo.com/) and dynamic frontends with [Next.js](https://nextjs.org/). It integrates the backend and frontend using [@hey-api/openapi-ts](https://github.com/hey-ai/openapi-ts) to generate a type-safe client, with automated watchers to keep the OpenAPI schema and client updated, ensuring a smooth and synchronized development workflow.
- [Next.js](https://nextjs.org/): Fast, SEO-friendly frontend framework
- [FastAPI](https://fastapi.tiangolo.com/): High-performance Python backend
- [SQLAlchemy](https://www.sqlalchemy.org/): Powerful Python SQL toolkit and ORM
- [PostgreSQL](https://www.postgresql.org/): Advanced open-source relational database
- [Pydantic](https://docs.pydantic.dev/): Data validation and settings management using Python type annotations
- [Zod](https://zod.dev/) + [TypeScript](https://www.typescriptlang.org/): End-to-end type safety and schema validation
- [fastapi-users](https://fastapi-users.github.io/fastapi-users/): Complete authentication system with:
- Secure password hashing by default
- JWT (JSON Web Token) authentication
- Email-based password recovery
- [Shadcn/ui](https://ui.shadcn.com/): Beautiful and customizable React components
- [OpenAPI-fetch](https://github.com/Hey-AI/openapi-fetch): Fully typed client generation from OpenAPI schema
- [fastapi-mail](https://sabuhish.github.io/fastapi-mail/): Efficient email handling for FastAPI applications
- [uv](https://docs.astral.sh/uv/): An extremely fast Python package and project manager
- [Pytest](https://docs.pytest.org/): Powerful Python testing framework
- Code Quality Tools:
- [Ruff](https://github.com/astral-sh/ruff): Fast Python linter
- [ESLint](https://eslint.org/): JavaScript/TypeScript code quality
- Hot reload watchers:
- Backend: [Watchdog](https://github.com/gorakhargosh/watchdog) for monitoring file changes
- Frontend: [Chokidar](https://github.com/paulmillr/chokidar) for live updates
- [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/): Consistent environments for development and production
- [MailHog](https://github.com/mailhog/MailHog): Email server for development
- [Pre-commit hooks](https://pre-commit.com/): Enforce code quality with automated checks
- [OpenAPI JSON schema](https://swagger.io/specification/): Centralized API documentation and client generation
With this setup, you'll save time and maintain a seamless connection between your backend and frontend, boosting productivity and reliability.
================================================
FILE: fastapi_backend/.gitignore
================================================
.vercel
================================================
FILE: fastapi_backend/Dockerfile
================================================
FROM python:3.12-bookworm
# Set the working directory
WORKDIR /app
# The uv installer requires curl (and certificates) to download the release archive
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates
# Download the latest uv installer
ADD https://astral.sh/uv/install.sh /uv-installer.sh
# Run the uv installer then remove it
RUN sh /uv-installer.sh && rm /uv-installer.sh
# Ensure the installed binary is on the `PATH`
ENV PATH="/root/.local/bin/:$PATH"
# Copy dependency files first to leverage Docker caching
COPY pyproject.toml uv.lock ./
# Install dependencies using uv
RUN uv sync --frozen
ENV PATH="/app/.venv/bin:$PATH"
# Copy the rest of the application code
COPY . .
# Expose the application port
EXPOSE 8000
# Command to run the application
CMD ["./start.sh"]
================================================
FILE: fastapi_backend/alembic.ini
================================================
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# Use forward slashes (/) also on windows to provide an os agnostic path
script_location = alembic_migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to alembic/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
# version_path_separator = newline
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# We are including this url in the env.py file
# then we can use the .env file to set the url
# sqlalchemy.url = postgresql+asyncpg://postgres:password@localhost:5432/mydatabase
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
================================================
FILE: fastapi_backend/alembic_migrations/README
================================================
Generic single-database configuration with an async dbapi.
================================================
FILE: fastapi_backend/alembic_migrations/env.py
================================================
import asyncio
import os
from urllib.parse import urlparse
from logging.config import fileConfig
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from alembic import context
from app.models import Base
from dotenv import load_dotenv
load_dotenv()
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = Base.metadata
# target_metadata = None
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
# Retrieve the database URL from the environment
# set it during execution
database_url = os.getenv("DATABASE_URL")
if not database_url:
raise ValueError("DATABASE_URL environment variable is not set!")
parsed_db_url = urlparse(database_url)
async_db_connection_url = (
f"postgresql+asyncpg://{parsed_db_url.username}:{parsed_db_url.password}@"
f"{parsed_db_url.hostname}{':' + str(parsed_db_url.port) if parsed_db_url.port else ''}"
f"{parsed_db_url.path}"
)
config.set_main_option("sqlalchemy.url", async_db_connection_url)
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection: Connection) -> None:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations() -> None:
"""In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode."""
asyncio.run(run_async_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
================================================
FILE: fastapi_backend/alembic_migrations/script.py.mako
================================================
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import fastapi_users_db_sqlalchemy
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
================================================
FILE: fastapi_backend/alembic_migrations/versions/402d067a8b92_added_user_table.py
================================================
"""Added user table
Revision ID: 402d067a8b92
Revises:
Create Date: 2024-09-27 14:01:44.155160
"""
from typing import Sequence, Union
import fastapi_users_db_sqlalchemy
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = "402d067a8b92"
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"user",
sa.Column("id", fastapi_users_db_sqlalchemy.generics.GUID(), nullable=False),
sa.Column("email", sa.String(length=320), nullable=False),
sa.Column("hashed_password", sa.String(length=1024), nullable=False),
sa.Column("is_active", sa.Boolean(), nullable=False),
sa.Column("is_superuser", sa.Boolean(), nullable=False),
sa.Column("is_verified", sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(op.f("ix_user_email"), "user", ["email"], unique=True)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f("ix_user_email"), table_name="user")
op.drop_table("user")
# ### end Alembic commands ###
================================================
FILE: fastapi_backend/alembic_migrations/versions/b389592974f8_add_item_model.py
================================================
"""Add item model
Revision ID: b389592974f8
Revises: 402d067a8b92
Create Date: 2024-12-06 17:52:50.698249
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "b389592974f8"
down_revision: Union[str, None] = "402d067a8b92"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"items",
sa.Column("id", sa.UUID(), nullable=False),
sa.Column("name", sa.String(), nullable=False),
sa.Column("description", sa.String(), nullable=True),
sa.Column("quantity", sa.Integer(), nullable=True),
sa.Column("user_id", sa.UUID(), nullable=False),
sa.ForeignKeyConstraint(
["user_id"],
["user.id"],
),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("items")
# ### end Alembic commands ###
================================================
FILE: fastapi_backend/api/index.py
================================================
from app.main import app # noqa: F401
================================================
FILE: fastapi_backend/app/__init__.py
================================================
================================================
FILE: fastapi_backend/app/config.py
================================================
from typing import Set
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
# OpenAPI docs
OPENAPI_URL: str = "/openapi.json"
# Database
DATABASE_URL: str
TEST_DATABASE_URL: str | None = None
EXPIRE_ON_COMMIT: bool = False
# User
ACCESS_SECRET_KEY: str
RESET_PASSWORD_SECRET_KEY: str
VERIFICATION_SECRET_KEY: str
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_SECONDS: int = 3600
# Email
MAIL_USERNAME: str | None = None
MAIL_PASSWORD: str | None = None
MAIL_FROM: str | None = None
MAIL_SERVER: str | None = None
MAIL_PORT: int | None = None
MAIL_FROM_NAME: str = "FastAPI template"
MAIL_STARTTLS: bool = True
MAIL_SSL_TLS: bool = False
USE_CREDENTIALS: bool = True
VALIDATE_CERTS: bool = True
TEMPLATE_DIR: str = "email_templates"
# Frontend
FRONTEND_URL: str = "http://localhost:3000"
# CORS
CORS_ORIGINS: Set[str]
model_config = SettingsConfigDict(
env_file=".env", env_file_encoding="utf-8", extra="ignore"
)
settings = Settings()
================================================
FILE: fastapi_backend/app/database.py
================================================
from typing import AsyncGenerator
from urllib.parse import urlparse
from fastapi import Depends
from fastapi_users.db import SQLAlchemyUserDatabase
from sqlalchemy import NullPool
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from .config import settings
from .models import Base, User
parsed_db_url = urlparse(settings.DATABASE_URL)
async_db_connection_url = (
f"postgresql+asyncpg://{parsed_db_url.username}:{parsed_db_url.password}@"
f"{parsed_db_url.hostname}{':' + str(parsed_db_url.port) if parsed_db_url.port else ''}"
f"{parsed_db_url.path}"
)
# Disable connection pooling for serverless environments like Vercel
engine = create_async_engine(async_db_connection_url, poolclass=NullPool)
async_session_maker = async_sessionmaker(
engine, expire_on_commit=settings.EXPIRE_ON_COMMIT
)
async def create_db_and_tables():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
async with async_session_maker() as session:
yield session
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
yield SQLAlchemyUserDatabase(session, User)
================================================
FILE: fastapi_backend/app/email.py
================================================
from pathlib import Path
import urllib.parse
from fastapi_mail import FastMail, MessageSchema, ConnectionConfig, MessageType
from .config import settings
from .models import User
def get_email_config():
conf = ConnectionConfig(
MAIL_USERNAME=settings.MAIL_USERNAME,
MAIL_PASSWORD=settings.MAIL_PASSWORD,
MAIL_FROM=settings.MAIL_FROM,
MAIL_PORT=settings.MAIL_PORT,
MAIL_SERVER=settings.MAIL_SERVER,
MAIL_FROM_NAME=settings.MAIL_FROM_NAME,
MAIL_STARTTLS=settings.MAIL_STARTTLS,
MAIL_SSL_TLS=settings.MAIL_SSL_TLS,
USE_CREDENTIALS=settings.USE_CREDENTIALS,
VALIDATE_CERTS=settings.VALIDATE_CERTS,
TEMPLATE_FOLDER=Path(__file__).parent / settings.TEMPLATE_DIR,
)
return conf
async def send_reset_password_email(user: User, token: str):
conf = get_email_config()
email = user.email
base_url = f"{settings.FRONTEND_URL}/password-recovery/confirm?"
params = {"token": token}
encoded_params = urllib.parse.urlencode(params)
link = f"{base_url}{encoded_params}"
message = MessageSchema(
subject="Password recovery",
recipients=[email],
template_body={"username": email, "link": link},
subtype=MessageType.html,
)
fm = FastMail(conf)
await fm.send_message(message, template_name="password_reset.html")
================================================
FILE: fastapi_backend/app/email_templates/__init__.py
================================================
================================================
FILE: fastapi_backend/app/email_templates/password_reset.html
================================================
<html>
<body>
<p>Hello {{ username }},</p>
<p>We received a request to reset your password. You can reset your password by clicking the link below:</p>
<p><a href="{{ link }}">Reset password</a></p>
<p>If you did not request this, please ignore this email.</p>
<p>Best regards,<br>
YourCompany</p>
</body>
</html>
================================================
FILE: fastapi_backend/app/main.py
================================================
from fastapi import FastAPI
from fastapi_pagination import add_pagination
from .schemas import UserCreate, UserRead, UserUpdate
from .users import auth_backend, fastapi_users, AUTH_URL_PATH
from fastapi.middleware.cors import CORSMiddleware
from .utils import simple_generate_unique_route_id
from app.routes.items import router as items_router
from app.config import settings
app = FastAPI(
generate_unique_id_function=simple_generate_unique_route_id,
openapi_url=settings.OPENAPI_URL,
)
# Middleware for CORS configuration
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include authentication and user management routes
app.include_router(
fastapi_users.get_auth_router(auth_backend),
prefix=f"/{AUTH_URL_PATH}/jwt",
tags=["auth"],
)
app.include_router(
fastapi_users.get_register_router(UserRead, UserCreate),
prefix=f"/{AUTH_URL_PATH}",
tags=["auth"],
)
app.include_router(
fastapi_users.get_reset_password_router(),
prefix=f"/{AUTH_URL_PATH}",
tags=["auth"],
)
app.include_router(
fastapi_users.get_verify_router(UserRead),
prefix=f"/{AUTH_URL_PATH}",
tags=["auth"],
)
app.include_router(
fastapi_users.get_users_router(UserRead, UserUpdate),
prefix="/users",
tags=["users"],
)
# Include items routes
app.include_router(items_router, prefix="/items")
add_pagination(app)
================================================
FILE: fastapi_backend/app/models.py
================================================
from fastapi_users.db import SQLAlchemyBaseUserTableUUID
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import UUID
from uuid import uuid4
class Base(DeclarativeBase):
pass
class User(SQLAlchemyBaseUserTableUUID, Base):
items = relationship("Item", back_populates="user", cascade="all, delete-orphan")
class Item(Base):
__tablename__ = "items"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
name = Column(String, nullable=False)
description = Column(String, nullable=True)
quantity = Column(Integer, nullable=True)
user_id = Column(UUID(as_uuid=True), ForeignKey("user.id"), nullable=False)
user = relationship("User", back_populates="items")
================================================
FILE: fastapi_backend/app/routes/__init__.py
================================================
================================================
FILE: fastapi_backend/app/routes/items.py
================================================
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi_pagination import Page, Params
from fastapi_pagination.ext.sqlalchemy import apaginate
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from app.database import User, get_async_session
from app.models import Item
from app.schemas import ItemRead, ItemCreate
from app.users import current_active_user
router = APIRouter(tags=["item"])
def transform_items(items):
return [ItemRead.model_validate(item) for item in items]
@router.get("/", response_model=Page[ItemRead])
async def read_item(
db: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
page: int = Query(1, ge=1, description="Page number"),
size: int = Query(10, ge=1, le=100, description="Page size"),
):
params = Params(page=page, size=size)
query = select(Item).filter(Item.user_id == user.id)
return await apaginate(db, query, params, transformer=transform_items)
@router.post("/", response_model=ItemRead)
async def create_item(
item: ItemCreate,
db: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
):
db_item = Item(**item.model_dump(), user_id=user.id)
db.add(db_item)
await db.commit()
await db.refresh(db_item)
return db_item
@router.delete("/{item_id}")
async def delete_item(
item_id: UUID,
db: AsyncSession = Depends(get_async_session),
user: User = Depends(current_active_user),
):
result = await db.execute(
select(Item).filter(Item.id == item_id, Item.user_id == user.id)
)
item = result.scalars().first()
if not item:
raise HTTPException(status_code=404, detail="Item not found or not authorized")
await db.delete(item)
await db.commit()
return {"message": "Item successfully deleted"}
================================================
FILE: fastapi_backend/app/schemas.py
================================================
import uuid
from fastapi_users import schemas
from pydantic import BaseModel
from uuid import UUID
class UserRead(schemas.BaseUser[uuid.UUID]):
pass
class UserCreate(schemas.BaseUserCreate):
pass
class UserUpdate(schemas.BaseUserUpdate):
pass
class ItemBase(BaseModel):
name: str
description: str | None = None
quantity: int | None = None
class ItemCreate(ItemBase):
pass
class ItemRead(ItemBase):
id: UUID
user_id: UUID
model_config = {"from_attributes": True}
================================================
FILE: fastapi_backend/app/users.py
================================================
import uuid
import re
from typing import Optional
from fastapi import Depends, Request
from fastapi_users import (
BaseUserManager,
FastAPIUsers,
UUIDIDMixin,
InvalidPasswordException,
)
from fastapi_users.authentication import (
AuthenticationBackend,
BearerTransport,
JWTStrategy,
)
from fastapi_users.db import SQLAlchemyUserDatabase
from .config import settings
from .database import get_user_db
from .email import send_reset_password_email
from .models import User
from .schemas import UserCreate
AUTH_URL_PATH = "auth"
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
reset_password_token_secret = settings.RESET_PASSWORD_SECRET_KEY
verification_token_secret = settings.VERIFICATION_SECRET_KEY
async def on_after_register(self, user: User, request: Optional[Request] = None):
print(f"User {user.id} has registered.")
async def on_after_forgot_password(
self, user: User, token: str, request: Optional[Request] = None
):
await send_reset_password_email(user, token)
async def on_after_request_verify(
self, user: User, token: str, request: Optional[Request] = None
):
print(f"Verification requested for user {user.id}. Verification token: {token}")
async def validate_password(
self,
password: str,
user: UserCreate,
) -> None:
errors = []
if len(password) < 8:
errors.append("Password should be at least 8 characters.")
if user.email in password:
errors.append("Password should not contain e-mail.")
if not any(char.isupper() for char in password):
errors.append("Password should contain at least one uppercase letter.")
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
errors.append("Password should contain at least one special character.")
if errors:
raise InvalidPasswordException(reason=errors)
async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get_user_db)):
yield UserManager(user_db)
bearer_transport = BearerTransport(tokenUrl=f"{AUTH_URL_PATH}/jwt/login")
def get_jwt_strategy() -> JWTStrategy:
return JWTStrategy(
secret=settings.ACCESS_SECRET_KEY,
lifetime_seconds=settings.ACCESS_TOKEN_EXPIRE_SECONDS,
)
auth_backend = AuthenticationBackend(
name="jwt",
transport=bearer_transport,
get_strategy=get_jwt_strategy,
)
fastapi_users = FastAPIUsers[User, uuid.UUID](get_user_manager, [auth_backend])
current_active_user = fastapi_users.current_user(active=True)
================================================
FILE: fastapi_backend/app/utils.py
================================================
from fastapi.routing import APIRoute
def simple_generate_unique_route_id(route: APIRoute):
return f"{route.tags[0]}-{route.name}"
================================================
FILE: fastapi_backend/commands/__init__.py
================================================
================================================
FILE: fastapi_backend/commands/generate_openapi_schema.py
================================================
import json
from pathlib import Path
from app.main import app
import os
from dotenv import load_dotenv
load_dotenv()
OUTPUT_FILE = os.getenv("OPENAPI_OUTPUT_FILE")
def generate_openapi_schema(output_file):
schema = app.openapi()
output_path = Path(output_file)
updated_schema = remove_operation_id_tag(schema)
output_path.write_text(json.dumps(updated_schema, indent=2))
print(f"OpenAPI schema saved to {output_file}")
def remove_operation_id_tag(schema):
"""
Removes the tag prefix from the operation IDs in the OpenAPI schema.
This cleans up the OpenAPI operation IDs that are used by the frontend
client generator to create the names of the functions. The modified
schema is then returned.
"""
for path_data in schema["paths"].values():
for operation in path_data.values():
tag = operation["tags"][0]
operation_id = operation["operationId"]
to_remove = f"{tag}-"
new_operation_id = operation_id[len(to_remove) :]
operation["operationId"] = new_operation_id
return schema
if __name__ == "__main__":
generate_openapi_schema(OUTPUT_FILE)
================================================
FILE: fastapi_backend/mypy.ini
================================================
[mypy]
python_version = 3.12
files = src/**/*.py
follow_imports = skip
ignore_missing_imports = True
strict = True
================================================
FILE: fastapi_backend/pyproject.toml
================================================
[project]
name = "app"
version = "0.0.6"
description = ""
authors = [{ name = "Anderson Resende", email = "anderson@vinta.com.br" }]
requires-python = ">=3.12,<3.13"
readme = "README.md"
dependencies = [
"fastapi[standard]>=0.115.0,<0.116",
"asyncpg>=0.29.0,<0.30",
"fastapi-users[sqlalchemy]>=13.0.0,<14",
"pydantic-settings>=2.5.2,<3",
"fastapi-mail>=1.4.1,<2",
"fastapi-pagination==0.13.3"
]
[dependency-groups]
dev = [
"pre-commit>=3.4.0,<4",
"ruff>=0.1.0,<0.2",
"watchdog>=5.0.3,<6",
"python-dotenv>=1.0.1,<2",
"pytest>=8.3.3,<9",
"pytest-mock>=3.14.0,<4",
"mypy>=1.13.0,<2",
"coveralls>=4.0.1,<5",
"alembic>=1.14.0,<2",
"pytest-asyncio>=0.24.0,<0.25",
"mkdocs-material>=9.6.9",
"mkdocs-material[imaging]>=9.6.9",
]
[tool.uv]
package = false
[tool.hatch.build.targets.sdist]
include = ["commands"]
[tool.hatch.build.targets.wheel]
include = ["commands"]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
================================================
FILE: fastapi_backend/pytest.ini
================================================
[pytest]
asyncio_mode = auto
================================================
FILE: fastapi_backend/requirements.txt
================================================
# This file was autogenerated by uv via the following command:
# uv export
aiosmtplib==2.0.2 \
--hash=sha256:138599a3227605d29a9081b646415e9e793796ca05322a78f69179f0135016a3 \
--hash=sha256:1e631a7a3936d3e11c6a144fb8ffd94bb4a99b714f2cb433e825d88b698e37bc
alembic==1.14.0 \
--hash=sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25 \
--hash=sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b
annotated-types==0.7.0 \
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
anyio==4.8.0 \
--hash=sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a \
--hash=sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a
argon2-cffi==23.1.0 \
--hash=sha256:879c3e79a2729ce768ebb7d36d4609e3a78a4ca2ec3a9f12286ca057e3d0db08 \
--hash=sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea
argon2-cffi-bindings==21.2.0 \
--hash=sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c \
--hash=sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082 \
--hash=sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f \
--hash=sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d \
--hash=sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f \
--hash=sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae \
--hash=sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3 \
--hash=sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86 \
--hash=sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367 \
--hash=sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93 \
--hash=sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e
asyncpg==0.29.0 \
--hash=sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe \
--hash=sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175 \
--hash=sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106 \
--hash=sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178 \
--hash=sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb \
--hash=sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02 \
--hash=sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59 \
--hash=sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e \
--hash=sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364
babel==2.17.0 \
--hash=sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d \
--hash=sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2
backrefs==5.8 \
--hash=sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd \
--hash=sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b \
--hash=sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc \
--hash=sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486 \
--hash=sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d
bcrypt==4.1.2 \
--hash=sha256:02d9ef8915f72dd6daaef40e0baeef8a017ce624369f09754baf32bb32dba25f \
--hash=sha256:1c28973decf4e0e69cee78c68e30a523be441972c826703bb93099868a8ff5b5 \
--hash=sha256:2a298db2a8ab20056120b45e86c00a0a5eb50ec4075b6142db35f593b97cb3fb \
--hash=sha256:33313a1200a3ae90b75587ceac502b048b840fc69e7f7a0905b5f87fac7a1258 \
--hash=sha256:3566a88234e8de2ccae31968127b0ecccbb4cddb629da744165db72b58d88ca4 \
--hash=sha256:387e7e1af9a4dd636b9505a465032f2f5cb8e61ba1120e79a0e1cd0b512f3dfc \
--hash=sha256:44290ccc827d3a24604f2c8bcd00d0da349e336e6503656cb8192133e27335e2 \
--hash=sha256:57fa9442758da926ed33a91644649d3e340a71e2d0a5a8de064fb621fd5a3326 \
--hash=sha256:68e3c6642077b0c8092580c819c1684161262b2e30c4f45deb000c38947bf483 \
--hash=sha256:69057b9fc5093ea1ab00dd24ede891f3e5e65bee040395fb1e66ee196f9c9b4a \
--hash=sha256:6cad43d8c63f34b26aef462b6f5e44fdcf9860b723d2453b5d391258c4c8e966 \
--hash=sha256:71b8be82bc46cedd61a9f4ccb6c1a493211d031415a34adde3669ee1b0afbb63 \
--hash=sha256:732b3920a08eacf12f93e6b04ea276c489f1c8fb49344f564cca2adb663b3e4c \
--hash=sha256:9800ae5bd5077b13725e2e3934aa3c9c37e49d3ea3d06318010aa40f54c63551 \
--hash=sha256:ac621c093edb28200728a9cca214d7e838529e557027ef0581685909acd28b5e \
--hash=sha256:b8df79979c5bae07f1db22dcc49cc5bccf08a0380ca5c6f391cbb5790355c0b0 \
--hash=sha256:b90e216dc36864ae7132cb151ffe95155a37a14e0de3a8f64b49655dd959ff9c \
--hash=sha256:ba55e40de38a24e2d78d34c2d36d6e864f93e0d79d0b6ce915e4335aa81d01b1 \
--hash=sha256:be3ab1071662f6065899fe08428e45c16aa36e28bc42921c4901a191fda6ee42 \
--hash=sha256:ea505c97a5c465ab8c3ba75c0805a102ce526695cd6818c6de3b1a38f6f60da1 \
--hash=sha256:eb3bd3321517916696233b5e0c67fd7d6281f0ef48e66812db35fc963a422a1c \
--hash=sha256:f70d9c61f9c4ca7d57f3bfe88a5ccf62546ffbadf3681bb1e268d9d2e41c91a7 \
--hash=sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369
blinker==1.9.0 \
--hash=sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf \
--hash=sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc
cairocffi==1.7.1 \
--hash=sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b \
--hash=sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f
cairosvg==2.7.1 \
--hash=sha256:432531d72347291b9a9ebfb6777026b607563fd8719c46ee742db0aef7271ba0 \
--hash=sha256:8a5222d4e6c3f86f1f7046b63246877a63b49923a1cd202184c3a634ef546b3b
certifi==2024.12.14 \
--hash=sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56 \
--hash=sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db
cffi==1.17.1 \
--hash=sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36 \
--hash=sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824 \
--hash=sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3 \
--hash=sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8 \
--hash=sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903 \
--hash=sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c \
--hash=sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4 \
--hash=sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65 \
--hash=sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93 \
--hash=sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff \
--hash=sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5 \
--hash=sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99
cfgv==3.4.0 \
--hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
--hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
charset-normalizer==3.4.1 \
--hash=sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d \
--hash=sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa \
--hash=sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3 \
--hash=sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9 \
--hash=sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f \
--hash=sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545 \
--hash=sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b \
--hash=sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35 \
--hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d \
--hash=sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757 \
--hash=sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a \
--hash=sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85 \
--hash=sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7 \
--hash=sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1 \
--hash=sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616
click==8.1.8 \
--hash=sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2 \
--hash=sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a
colorama==0.4.6 \
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
coverage==7.6.10 \
--hash=sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50 \
--hash=sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853 \
--hash=sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359 \
--hash=sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0 \
--hash=sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23 \
--hash=sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852 \
--hash=sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078 \
--hash=sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0 \
--hash=sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247 \
--hash=sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022 \
--hash=sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b
coveralls==4.0.1 \
--hash=sha256:7a6b1fa9848332c7b2221afb20f3df90272ac0167060f41b5fe90429b30b1809 \
--hash=sha256:7b2a0a2bcef94f295e3cf28dcc55ca40b71c77d1c2446b538e85f0f7bc21aa69
cryptography==44.0.0 \
--hash=sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7 \
--hash=sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b \
--hash=sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc \
--hash=sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543 \
--hash=sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591 \
--hash=sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede \
--hash=sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb \
--hash=sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f \
--hash=sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123 \
--hash=sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c \
--hash=sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285 \
--hash=sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd \
--hash=sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092 \
--hash=sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289 \
--hash=sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02 \
--hash=sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64 \
--hash=sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053 \
--hash=sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417 \
--hash=sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e \
--hash=sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e \
--hash=sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7
cssselect2==0.8.0 \
--hash=sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e \
--hash=sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a
defusedxml==0.7.1 \
--hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
--hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
distlib==0.3.9 \
--hash=sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87 \
--hash=sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403
dnspython==2.7.0 \
--hash=sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86 \
--hash=sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1
docopt==0.6.2 \
--hash=sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491
email-validator==2.1.2 \
--hash=sha256:14c0f3d343c4beda37400421b39fa411bbe33a75df20825df73ad53e06a9f04c \
--hash=sha256:d89f6324e13b1e39889eab7f9ca2f91dc9aebb6fa50a6d8bd4329ab50f251115
fastapi==0.115.6 \
--hash=sha256:9ec46f7addc14ea472958a96aae5b5de65f39721a46aaf5705c480d9a8b76654 \
--hash=sha256:e9240b29e36fa8f4bb7290316988e90c381e5092e0cbe84e7818cc3713bcf305
fastapi-cli==0.0.7 \
--hash=sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e \
--hash=sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4
fastapi-mail==1.4.1 \
--hash=sha256:9095b713bd9d3abb02fe6d7abb637502aaf680b52e177d60f96273ef6bc8bb70 \
--hash=sha256:fa5ef23b2dea4d3ba4587f4bbb53f8f15274124998fb4e40629b3b636c76c398
fastapi-users==13.0.0 \
--hash=sha256:b397c815b7051c8fd4b560fbeee707acd28e00bd3e8f25c292ad158a1e47e884 \
--hash=sha256:e6246529e3080a5b50e5afeed1e996663b661f1dc791a1ac478925cb5bfc0fa0
fastapi-users-db-sqlalchemy==7.0.0 \
--hash=sha256:5fceac018e7cfa69efc70834dd3035b3de7988eb4274154a0dbe8b14f5aa001e \
--hash=sha256:6823eeedf8a92f819276a2b2210ef1dcfd71fe8b6e37f7b4da8d1c60e3dfd595
filelock==3.16.1 \
--hash=sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0 \
--hash=sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435
ghp-import==2.1.0 \
--hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \
--hash=sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343
greenlet==3.1.1 \
--hash=sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9 \
--hash=sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942 \
--hash=sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441 \
--hash=sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d \
--hash=sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467 \
--hash=sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01 \
--hash=sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36 \
--hash=sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0 \
--hash=sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa \
--hash=sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79
h11==0.14.0 \
--hash=sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d \
--hash=sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761
httpcore==1.0.7 \
--hash=sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c \
--hash=sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd
httptools==0.6.4 \
--hash=sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2 \
--hash=sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c \
--hash=sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1 \
--hash=sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44 \
--hash=sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970 \
--hash=sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2 \
--hash=sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81 \
--hash=sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f
httpx==0.28.1 \
--hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
identify==2.6.5 \
--hash=sha256:14181a47091eb75b337af4c23078c9d09225cd4c48929f521f3bf16b09d02566 \
--hash=sha256:c10b33f250e5bba374fae86fb57f3adcebf1161bce7cdf92031915fd480c13bc
idna==3.10 \
--hash=sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9 \
--hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
iniconfig==2.0.0 \
--hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
--hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
jinja2==3.1.5 \
--hash=sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb \
--hash=sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb
makefun==1.15.6 \
--hash=sha256:26bc63442a6182fb75efed8b51741dd2d1db2f176bec8c64e20a586256b8f149 \
--hash=sha256:e69b870f0bb60304765b1e3db576aaecf2f9b3e5105afe8cfeff8f2afe6ad067
mako==1.3.8 \
--hash=sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627 \
--hash=sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8
markdown==3.7 \
--hash=sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2 \
--hash=sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803
markdown-it-py==3.0.0 \
--hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \
--hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb
markupsafe==3.0.2 \
--hash=sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30 \
--hash=sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028 \
--hash=sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557 \
--hash=sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22 \
--hash=sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225 \
--hash=sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c \
--hash=sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87 \
--hash=sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf \
--hash=sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48 \
--hash=sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8 \
--hash=sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0
mdurl==0.1.2 \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
mergedeep==1.3.4 \
--hash=sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8 \
--hash=sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307
mkdocs==1.6.1 \
--hash=sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2 \
--hash=sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e
mkdocs-get-deps==0.2.0 \
--hash=sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c \
--hash=sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134
mkdocs-material==9.6.9 \
--hash=sha256:6e61b7fb623ce2aa4622056592b155a9eea56ff3487d0835075360be45a4c8d1 \
--hash=sha256:a4872139715a1f27b2aa3f3dc31a9794b7bbf36333c0ba4607cf04786c94f89c
mkdocs-material-extensions==1.3.1 \
--hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \
--hash=sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31
mypy==1.14.1 \
--hash=sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14 \
--hash=sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e \
--hash=sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6 \
--hash=sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11 \
--hash=sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b \
--hash=sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1 \
--hash=sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9 \
--hash=sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89
mypy-extensions==1.0.0 \
--hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \
--hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782
nodeenv==1.9.1 \
--hash=sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f \
--hash=sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9
packaging==24.2 \
--hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759 \
--hash=sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f
paginate==0.5.7 \
--hash=sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945 \
--hash=sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591
pathspec==0.12.1 \
--hash=sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08 \
--hash=sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712
pillow==10.4.0 \
--hash=sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06 \
--hash=sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a \
--hash=sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80 \
--hash=sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9 \
--hash=sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94 \
--hash=sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b \
--hash=sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42 \
--hash=sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597 \
--hash=sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a \
--hash=sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca \
--hash=sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9 \
--hash=sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef
platformdirs==4.3.6 \
--hash=sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907 \
--hash=sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb
pluggy==1.5.0 \
--hash=sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1 \
--hash=sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669
pre-commit==3.8.0 \
--hash=sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af \
--hash=sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f
pwdlib==0.2.0 \
--hash=sha256:b1bdafc064310eb6d3d07144a210267063ab4f45ac73a97be948e6589f74e861 \
--hash=sha256:be53812012ab66795a57ac9393a59716ae7c2b60841ed453eb1262017fdec144
pycparser==2.22 \
--hash=sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6 \
--hash=sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc
pydantic==2.10.4 \
--hash=sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d \
--hash=sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06
pydantic-core==2.27.2 \
--hash=sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6 \
--hash=sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7 \
--hash=sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc \
--hash=sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4 \
--hash=sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4 \
--hash=sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b \
--hash=sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934 \
--hash=sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2 \
--hash=sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef \
--hash=sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c \
--hash=sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0 \
--hash=sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57 \
--hash=sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9 \
--hash=sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3 \
--hash=sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39
pydantic-settings==2.7.1 \
--hash=sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93 \
--hash=sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd
pygments==2.19.0 \
--hash=sha256:4755e6e64d22161d5b61432c0600c923c5927214e7c956e31c23923c89251a9b \
--hash=sha256:afc4146269910d4bdfabcd27c24923137a74d562a23a320a41a55ad303e19783
pyjwt==2.8.0 \
--hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \
--hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320
pymdown-extensions==10.14.3 \
--hash=sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9 \
--hash=sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b
pytest==8.3.4 \
--hash=sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6 \
--hash=sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761
pytest-asyncio==0.24.0 \
--hash=sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b \
--hash=sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276
pytest-mock==3.14.0 \
--hash=sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f \
--hash=sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0
python-dateutil==2.9.0.post0 \
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
python-dotenv==1.0.1 \
--hash=sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca \
--hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a
python-multipart==0.0.9 \
--hash=sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026 \
--hash=sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215
pyyaml==6.0.2 \
--hash=sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48 \
--hash=sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5 \
--hash=sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8 \
--hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476 \
--hash=sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b \
--hash=sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425 \
--hash=sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab \
--hash=sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725 \
--hash=sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e \
--hash=sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4
pyyaml-env-tag==0.1 \
--hash=sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb \
--hash=sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069
requests==2.32.3 \
--hash=sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760 \
--hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
rich==13.9.4 \
--hash=sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098 \
--hash=sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90
rich-toolkit==0.12.0 \
--hash=sha256:a2da4416384410ae871e890db7edf8623e1f5e983341dbbc8cc03603ce24f0ab \
--hash=sha256:facb0b40418010309f77abd44e2583b4936656f6ee5c8625da807564806a6c40
ruff==0.1.15 \
--hash=sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447 \
--hash=sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f \
--hash=sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587 \
--hash=sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df \
--hash=sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852 \
--hash=sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f \
--hash=sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5 \
--hash=sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e \
--hash=sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807 \
--hash=sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360 \
--hash=sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2 \
--hash=sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1 \
--hash=sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec \
--hash=sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5 \
--hash=sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8 \
--hash=sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e \
--hash=sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b
shellingham==1.5.4 \
--hash=sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686 \
--hash=sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de
six==1.17.0 \
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
sniffio==1.3.1 \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
sqlalchemy==2.0.36 \
--hash=sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588 \
--hash=sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855 \
--hash=sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e \
--hash=sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5 \
--hash=sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686 \
--hash=sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a \
--hash=sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5 \
--hash=sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4 \
--hash=sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e \
--hash=sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53
starlette==0.41.3 \
--hash=sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835 \
--hash=sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7
tinycss2==1.4.0 \
--hash=sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7 \
--hash=sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289
typer==0.15.1 \
--hash=sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847 \
--hash=sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a
typing-extensions==4.12.2 \
--hash=sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d \
--hash=sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8
urllib3==2.3.0 \
--hash=sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df \
--hash=sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d
uvicorn==0.34.0 \
--hash=sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4 \
--hash=sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9
uvloop==0.21.0 ; platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32' \
--hash=sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f \
--hash=sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c \
--hash=sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3 \
--hash=sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb \
--hash=sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc \
--hash=sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d \
--hash=sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2
virtualenv==20.28.1 \
--hash=sha256:412773c85d4dab0409b83ec36f7a6499e72eaf08c80e81e9576bca61831c71cb \
--hash=sha256:5d34ab240fdb5d21549b76f9e8ff3af28252f5499fb6d6f031adac4e5a8c5329
watchdog==5.0.3 \
--hash=sha256:0f9332243355643d567697c3e3fa07330a1d1abf981611654a1f2bf2175612b7 \
--hash=sha256:108f42a7f0345042a854d4d0ad0834b741d421330d5f575b81cb27b883500176 \
--hash=sha256:1e9679245e3ea6498494b3028b90c7b25dbb2abe65c7d07423ecfc2d6218ff7c \
--hash=sha256:26dd201857d702bdf9d78c273cafcab5871dd29343748524695cecffa44a8d97 \
--hash=sha256:294b7a598974b8e2c6123d19ef15de9abcd282b0fbbdbc4d23dfa812959a9e05 \
--hash=sha256:349c9488e1d85d0a58e8cb14222d2c51cbc801ce11ac3936ab4c3af986536926 \
--hash=sha256:49f4d36cb315c25ea0d946e018c01bb028048023b9e103d3d3943f58e109dd45 \
--hash=sha256:53a3f10b62c2d569e260f96e8d966463dec1a50fa4f1b22aec69e3f91025060e \
--hash=sha256:78864cc8f23dbee55be34cc1494632a7ba30263951b5b2e8fc8286b95845f82c \
--hash=sha256:9413384f26b5d050b6978e6fcd0c1e7f0539be7a4f1a885061473c5deaa57221 \
--hash=sha256:94d11b07c64f63f49876e0ab8042ae034674c8653bfcdaa8c4b32e71cfff87e8 \
--hash=sha256:c66f80ee5b602a9c7ab66e3c9f36026590a0902db3aea414d59a2f55188c1f49 \
--hash=sha256:dd021efa85970bd4824acacbb922066159d0f9e546389a4743d56919b6758b91 \
--hash=sha256:f00b4cf737f568be9665563347a910f8bdc76f88c2970121c86243c8cfdf90e9
watchfiles==1.0.3 \
--hash=sha256:0d1ec043f02ca04bf21b1b32cab155ce90c651aaf5540db8eb8ad7f7e645cba8 \
--hash=sha256:1df924ba82ae9e77340101c28d56cbaff2c991bd6fe8444a545d24075abb0a87 \
--hash=sha256:48681c86f2cb08348631fed788a116c89c787fdf1e6381c5febafd782f6c3b44 \
--hash=sha256:49bc1bc26abf4f32e132652f4b3bfeec77d8f8f62f57652703ef127e85a3e38d \
--hash=sha256:632a52dcaee44792d0965c17bdfe5dc0edad5b86d6a29e53d6ad4bf92dc0ff49 \
--hash=sha256:65ab1fb635476f6170b07e8e21db0424de94877e4b76b7feabfe11f9a5fc12b5 \
--hash=sha256:6a5bc3ca468bb58a2ef50441f953e1f77b9a61bd1b8c347c8223403dc9b4ac9a \
--hash=sha256:80bf4b459d94a0387617a1b499f314aa04d8a64b7a0747d15d425b8c8b151da0 \
--hash=sha256:93436ed550e429da007fbafb723e0769f25bae178fbb287a94cb4ccdf42d3af3 \
--hash=sha256:9e080cf917b35b20c889225a13f290f2716748362f6071b859b60b8847a6aa43 \
--hash=sha256:c18f3502ad0737813c7dad70e3e1cc966cc147fbaeef47a09463bbffe70b0a00 \
--hash=sha256:ca94c85911601b097d53caeeec30201736ad69a93f30d15672b967558df02885 \
--hash=sha256:f3ff7da165c99a5412fe5dd2304dd2dbaaaa5da718aad942dcb3a178eaa70c56 \
--hash=sha256:f58d3bfafecf3d81c15d99fc0ecf4319e80ac712c77cf0ce2661c8cf8bf84066
webencodings==0.5.1 \
--hash=sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78 \
--hash=sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923
websockets==14.1 \
--hash=sha256:1d045cbe1358d76b24d5e20e7b1878efe578d9897a25c24e6006eef788c0fdf0 \
--hash=sha256:398b10c77d471c0aab20a845e7a60076b6390bfdaac7a6d2edb0d2c59d75e8d8 \
--hash=sha256:4b6caec8576e760f2c7dd878ba817653144d5f369200b6ddf9771d64385b84d4 \
--hash=sha256:4d4fc827a20abe6d544a119896f6b78ee13fe81cbfef416f3f2ddf09a03f0e2e \
--hash=sha256:6a6c9bcf7cdc0fd41cc7b7944447982e8acfd9f0d560ea6d6845428ed0562058 \
--hash=sha256:87e31011b5c14a33b29f17eb48932e63e1dcd3fa31d72209848652310d3d1f0d \
--hash=sha256:90f4c7a069c733d95c308380aae314f2cb45bd8a904fb03eb36d1a4983a4993f \
--hash=sha256:9777564c0a72a1d457f0848977a1cbe15cfa75fa2f67ce267441e465717dcf1a \
--hash=sha256:a3dfff83ca578cada2d19e665e9c8368e1598d4e787422a460ec70e531dbdd58 \
--hash=sha256:a655bde548ca98f55b43711b0ceefd2a88a71af6350b0c168aa77562104f3f45 \
--hash=sha256:bc6ccf7d54c02ae47a48ddf9414c54d48af9c01076a2e1023e3b486b6e72c707 \
--hash=sha256:eb6d38971c800ff02e4a6afd791bbe3b923a9a57ca9aeab7314c21c84bf9ff05 \
--hash=sha256:ed907449fe5e021933e46a3e65d651f641975a768d0649fee59f10c2985529ed
fastapi-pagination==0.13.3
================================================
FILE: fastapi_backend/start.sh
================================================
#!/bin/bash
if [ -f /.dockerenv ]; then
echo "Running in Docker"
fastapi dev app/main.py --host 0.0.0.0 --port 8000 --reload &
python watcher.py
else
echo "Running locally with uv"
uv run fastapi dev app/main.py --host 0.0.0.0 --port 8000 --reload &
uv run python watcher.py
fi
wait
================================================
FILE: fastapi_backend/tests/__init__.py
================================================
================================================
FILE: fastapi_backend/tests/commands/__init__.py
================================================
================================================
FILE: fastapi_backend/tests/commands/files/openapi_test.json
================================================
{
"openapi": "3.1.0",
"info": {
"title": "FastAPI",
"version": "0.1.0"
},
"paths": {
"/auth/jwt/login": {
"post": {
"tags": [
"auth"
],
"summary": "Auth:Jwt.Login",
"operationId": "auth-auth:jwt.login_post",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/login_post"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BearerResponse"
},
"example": {
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiOTIyMWZmYzktNjQwZi00MzcyLTg2ZDMtY2U2NDJjYmE1NjAzIiwiYXVkIjoiZmFzdGFwaS11c2VyczphdXRoIiwiZXhwIjoxNTcxNTA0MTkzfQ.M10bjOe45I5Ncu_uXvOmVV8QxnL-nZfcH96U90JaocI",
"token_type": "bearer"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
},
"examples": {
"LOGIN_BAD_CREDENTIALS": {
"summary": "Bad credentials or the user is inactive.",
"value": {
"detail": "LOGIN_BAD_CREDENTIALS"
}
},
"LOGIN_USER_NOT_VERIFIED": {
"summary": "The user is not verified.",
"value": {
"detail": "LOGIN_USER_NOT_VERIFIED"
}
}
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/auth/jwt/logout": {
"post": {
"tags": [
"auth"
],
"summary": "Auth:Jwt.Logout",
"operationId": "auth-auth:jwt.logout_post",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"401": {
"description": "Missing token or inactive user."
}
},
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
},
"/auth/register": {
"post": {
"tags": [
"auth"
],
"summary": "Register:Register",
"operationId": "auth-register:register_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserCreate"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRead"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
},
"examples": {
"REGISTER_USER_ALREADY_EXISTS": {
"summary": "A user with this email already exists.",
"value": {
"detail": "REGISTER_USER_ALREADY_EXISTS"
}
},
"REGISTER_INVALID_PASSWORD": {
"summary": "Password validation failed.",
"value": {
"detail": {
"code": "REGISTER_INVALID_PASSWORD",
"reason": "Password should beat least 3 characters"
}
}
}
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/auth/forgot-password": {
"post": {
"tags": [
"auth"
],
"summary": "Reset:Forgot Password",
"operationId": "auth-reset:forgot_password_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Body_auth-reset_forgot_password_post"
}
}
},
"required": true
},
"responses": {
"202": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/auth/reset-password": {
"post": {
"tags": [
"auth"
],
"summary": "Reset:Reset Password",
"operationId": "auth-reset:reset_password_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Body_auth-reset_reset_password_post"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
},
"examples": {
"RESET_PASSWORD_BAD_TOKEN": {
"summary": "Bad or expired token.",
"value": {
"detail": "RESET_PASSWORD_BAD_TOKEN"
}
},
"RESET_PASSWORD_INVALID_PASSWORD": {
"summary": "Password validation failed.",
"value": {
"detail": {
"code": "RESET_PASSWORD_INVALID_PASSWORD",
"reason": "Password should be at least 3 characters"
}
}
}
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/auth/request-verify-token": {
"post": {
"tags": [
"auth"
],
"summary": "Verify:Request-Token",
"operationId": "auth-verify:request-token_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Body_auth-verify_request-token_post"
}
}
},
"required": true
},
"responses": {
"202": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/auth/verify": {
"post": {
"tags": [
"auth"
],
"summary": "Verify:Verify",
"operationId": "auth-verify:verify_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Body_auth-verify_verify_post"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRead"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
},
"examples": {
"VERIFY_USER_BAD_TOKEN": {
"summary": "Bad token, not existing user ornot the e-mail currently set for the user.",
"value": {
"detail": "VERIFY_USER_BAD_TOKEN"
}
},
"VERIFY_USER_ALREADY_VERIFIED": {
"summary": "The user is already verified.",
"value": {
"detail": "VERIFY_USER_ALREADY_VERIFIED"
}
}
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/users/me": {
"get": {
"tags": [
"users"
],
"summary": "Users:Current User",
"operationId": "users-users:current_user_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRead"
}
}
}
},
"401": {
"description": "Missing token or inactive user."
}
},
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
"patch": {
"tags": [
"users"
],
"summary": "Users:Patch Current User",
"operationId": "users-users:patch_current_user_patch",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserUpdate"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRead"
}
}
}
},
"401": {
"description": "Missing token or inactive user."
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
},
"examples": {
"UPDATE_USER_EMAIL_ALREADY_EXISTS": {
"summary": "A user with this email already exists.",
"value": {
"detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS"
}
},
"UPDATE_USER_INVALID_PASSWORD": {
"summary": "Password validation failed.",
"value": {
"detail": {
"code": "UPDATE_USER_INVALID_PASSWORD",
"reason": "Password should beat least 3 characters"
}
}
}
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
},
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
},
"/users/{id}": {
"get": {
"tags": [
"users"
],
"summary": "Users:User",
"operationId": "users-users:user_get",
"security": [
{
"OAuth2PasswordBearer": []
}
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Id"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRead"
}
}
}
},
"401": {
"description": "Missing token or inactive user."
},
"403": {
"description": "Not a superuser."
},
"404": {
"description": "The user does not exist."
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"patch": {
"tags": [
"users"
],
"summary": "Users:Patch User",
"operationId": "users-users:patch_user_patch",
"security": [
{
"OAuth2PasswordBearer": []
}
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Id"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserUpdate"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRead"
}
}
}
},
"401": {
"description": "Missing token or inactive user."
},
"403": {
"description": "Not a superuser."
},
"404": {
"description": "The user does not exist."
},
"400": {
"content": {
"application/json": {
"examples": {
"UPDATE_USER_EMAIL_ALREADY_EXISTS": {
"summary": "A user with this email already exists.",
"value": {
"detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS"
}
},
"UPDATE_USER_INVALID_PASSWORD": {
"summary": "Password validation failed.",
"value": {
"detail": {
"code": "UPDATE_USER_INVALID_PASSWORD",
"reason": "Password should beat least 3 characters"
}
}
}
},
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Bad Request"
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"delete": {
"tags": [
"users"
],
"summary": "Users:Delete User",
"operationId": "users-users:delete_user_delete",
"security": [
{
"OAuth2PasswordBearer": []
}
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Id"
}
}
],
"responses": {
"204": {
"description": "Successful Response"
},
"401": {
"description": "Missing token or inactive user."
},
"403": {
"description": "Not a superuser."
},
"404": {
"description": "The user does not exist."
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/authenticated-route": {
"get": {
"tags": [
"custom-auth"
],
"summary": "Authenticated Route",
"operationId": "custom-auth-authenticated_route_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
},
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
}
},
"components": {
"schemas": {
"BearerResponse": {
"properties": {
"access_token": {
"type": "string",
"title": "Access Token"
},
"token_type": {
"type": "string",
"title": "Token Type"
}
},
"type": "object",
"required": [
"access_token",
"token_type"
],
"title": "BearerResponse"
},
"Body_auth-reset_forgot_password_post": {
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "Email"
}
},
"type": "object",
"required": [
"email"
],
"title": "Body_auth-reset:forgot_password_post"
},
"Body_auth-reset_reset_password_post": {
"properties": {
"token": {
"type": "string",
"title": "Token"
},
"password": {
"type": "string",
"title": "Password"
}
},
"type": "object",
"required": [
"token",
"password"
],
"title": "Body_auth-reset:reset_password_post"
},
"Body_auth-verify_request-token_post": {
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "Email"
}
},
"type": "object",
"required": [
"email"
],
"title": "Body_auth-verify:request-token_post"
},
"Body_auth-verify_verify_post": {
"properties": {
"token": {
"type": "string",
"title": "Token"
}
},
"type": "object",
"required": [
"token"
],
"title": "Body_auth-verify:verify_post"
},
"ErrorModel": {
"properties": {
"detail": {
"anyOf": [
{
"type": "string"
},
{
"additionalProperties": {
"type": "string"
},
"type": "object"
}
],
"title": "Detail"
}
},
"type": "object",
"required": [
"detail"
],
"title": "ErrorModel"
},
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
},
"UserCreate": {
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "Email"
},
"password": {
"type": "string",
"title": "Password"
},
"is_active": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Is Active",
"default": true
},
"is_superuser": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Is Superuser",
"default": false
},
"is_verified": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Is Verified",
"default": false
}
},
"type": "object",
"required": [
"email",
"password"
],
"title": "UserCreate"
},
"UserRead": {
"properties": {
"id": {
"type": "string",
"format": "uuid",
"title": "Id"
},
"email": {
"type": "string",
"format": "email",
"title": "Email"
},
"is_active": {
"type": "boolean",
"title": "Is Active",
"default": true
},
"is_superuser": {
"type": "boolean",
"title": "Is Superuser",
"default": false
},
"is_verified": {
"type": "boolean",
"title": "Is Verified",
"default": false
}
},
"type": "object",
"required": [
"id",
"email"
],
"title": "UserRead"
},
"UserUpdate": {
"properties": {
"password": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Password"
},
"email": {
"anyOf": [
{
"type": "string",
"format": "email"
},
{
"type": "null"
}
],
"title": "Email"
},
"is_active": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Is Active"
},
"is_superuser": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Is Superuser"
},
"is_verified": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Is Verified"
}
},
"type": "object",
"title": "UserUpdate"
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"login_post": {
"properties": {
"grant_type": {
"anyOf": [
{
"type": "string",
"pattern": "password"
},
{
"type": "null"
}
],
"title": "Grant Type"
},
"username": {
"type": "string",
"title": "Username"
},
"password": {
"type": "string",
"title": "Password"
},
"scope": {
"type": "string",
"title": "Scope",
"default": ""
},
"client_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Client Id"
},
"client_secret": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Client Secret"
}
},
"type": "object",
"required": [
"username",
"password"
],
"title": "Body_auth-auth:jwt.login_post"
}
},
"securitySchemes": {
"OAuth2PasswordBearer": {
"type": "oauth2",
"flows": {
"password": {
"scopes": {},
"tokenUrl": "auth/jwt/login"
}
}
}
}
}
}
================================================
FILE: fastapi_backend/tests/commands/files/openapi_test_output.json
================================================
{
"openapi": "3.1.0",
"info": {
"title": "FastAPI",
"version": "0.1.0"
},
"paths": {
"/auth/jwt/login": {
"post": {
"tags": [
"auth"
],
"summary": "Auth:Jwt.Login",
"operationId": "auth:jwt.login_post",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/login_post"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BearerResponse"
},
"example": {
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiOTIyMWZmYzktNjQwZi00MzcyLTg2ZDMtY2U2NDJjYmE1NjAzIiwiYXVkIjoiZmFzdGFwaS11c2VyczphdXRoIiwiZXhwIjoxNTcxNTA0MTkzfQ.M10bjOe45I5Ncu_uXvOmVV8QxnL-nZfcH96U90JaocI",
"token_type": "bearer"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
},
"examples": {
"LOGIN_BAD_CREDENTIALS": {
"summary": "Bad credentials or the user is inactive.",
"value": {
"detail": "LOGIN_BAD_CREDENTIALS"
}
},
"LOGIN_USER_NOT_VERIFIED": {
"summary": "The user is not verified.",
"value": {
"detail": "LOGIN_USER_NOT_VERIFIED"
}
}
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/auth/jwt/logout": {
"post": {
"tags": [
"auth"
],
"summary": "Auth:Jwt.Logout",
"operationId": "auth:jwt.logout_post",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"401": {
"description": "Missing token or inactive user."
}
},
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
},
"/auth/register": {
"post": {
"tags": [
"auth"
],
"summary": "Register:Register",
"operationId": "register:register_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserCreate"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRead"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
},
"examples": {
"REGISTER_USER_ALREADY_EXISTS": {
"summary": "A user with this email already exists.",
"value": {
"detail": "REGISTER_USER_ALREADY_EXISTS"
}
},
"REGISTER_INVALID_PASSWORD": {
"summary": "Password validation failed.",
"value": {
"detail": {
"code": "REGISTER_INVALID_PASSWORD",
"reason": "Password should beat least 3 characters"
}
}
}
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/auth/forgot-password": {
"post": {
"tags": [
"auth"
],
"summary": "Reset:Forgot Password",
"operationId": "reset:forgot_password_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Body_auth-reset_forgot_password_post"
}
}
},
"required": true
},
"responses": {
"202": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/auth/reset-password": {
"post": {
"tags": [
"auth"
],
"summary": "Reset:Reset Password",
"operationId": "reset:reset_password_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Body_auth-reset_reset_password_post"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
},
"examples": {
"RESET_PASSWORD_BAD_TOKEN": {
"summary": "Bad or expired token.",
"value": {
"detail": "RESET_PASSWORD_BAD_TOKEN"
}
},
"RESET_PASSWORD_INVALID_PASSWORD": {
"summary": "Password validation failed.",
"value": {
"detail": {
"code": "RESET_PASSWORD_INVALID_PASSWORD",
"reason": "Password should be at least 3 characters"
}
}
}
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/auth/request-verify-token": {
"post": {
"tags": [
"auth"
],
"summary": "Verify:Request-Token",
"operationId": "verify:request-token_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Body_auth-verify_request-token_post"
}
}
},
"required": true
},
"responses": {
"202": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/auth/verify": {
"post": {
"tags": [
"auth"
],
"summary": "Verify:Verify",
"operationId": "verify:verify_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Body_auth-verify_verify_post"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRead"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
},
"examples": {
"VERIFY_USER_BAD_TOKEN": {
"summary": "Bad token, not existing user ornot the e-mail currently set for the user.",
"value": {
"detail": "VERIFY_USER_BAD_TOKEN"
}
},
"VERIFY_USER_ALREADY_VERIFIED": {
"summary": "The user is already verified.",
"value": {
"detail": "VERIFY_USER_ALREADY_VERIFIED"
}
}
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/users/me": {
"get": {
"tags": [
"users"
],
"summary": "Users:Current User",
"operationId": "users:current_user_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRead"
}
}
}
},
"401": {
"description": "Missing token or inactive user."
}
},
"security": [
{
"OAuth2PasswordBearer": []
}
]
},
"patch": {
"tags": [
"users"
],
"summary": "Users:Patch Current User",
"operationId": "users:patch_current_user_patch",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserUpdate"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRead"
}
}
}
},
"401": {
"description": "Missing token or inactive user."
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
},
"examples": {
"UPDATE_USER_EMAIL_ALREADY_EXISTS": {
"summary": "A user with this email already exists.",
"value": {
"detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS"
}
},
"UPDATE_USER_INVALID_PASSWORD": {
"summary": "Password validation failed.",
"value": {
"detail": {
"code": "UPDATE_USER_INVALID_PASSWORD",
"reason": "Password should beat least 3 characters"
}
}
}
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
},
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
},
"/users/{id}": {
"get": {
"tags": [
"users"
],
"summary": "Users:User",
"operationId": "users:user_get",
"security": [
{
"OAuth2PasswordBearer": []
}
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Id"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRead"
}
}
}
},
"401": {
"description": "Missing token or inactive user."
},
"403": {
"description": "Not a superuser."
},
"404": {
"description": "The user does not exist."
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"patch": {
"tags": [
"users"
],
"summary": "Users:Patch User",
"operationId": "users:patch_user_patch",
"security": [
{
"OAuth2PasswordBearer": []
}
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Id"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserUpdate"
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRead"
}
}
}
},
"401": {
"description": "Missing token or inactive user."
},
"403": {
"description": "Not a superuser."
},
"404": {
"description": "The user does not exist."
},
"400": {
"content": {
"application/json": {
"examples": {
"UPDATE_USER_EMAIL_ALREADY_EXISTS": {
"summary": "A user with this email already exists.",
"value": {
"detail": "UPDATE_USER_EMAIL_ALREADY_EXISTS"
}
},
"UPDATE_USER_INVALID_PASSWORD": {
"summary": "Password validation failed.",
"value": {
"detail": {
"code": "UPDATE_USER_INVALID_PASSWORD",
"reason": "Password should beat least 3 characters"
}
}
}
},
"schema": {
"$ref": "#/components/schemas/ErrorModel"
}
}
},
"description": "Bad Request"
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
},
"delete": {
"tags": [
"users"
],
"summary": "Users:Delete User",
"operationId": "users:delete_user_delete",
"security": [
{
"OAuth2PasswordBearer": []
}
],
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Id"
}
}
],
"responses": {
"204": {
"description": "Successful Response"
},
"401": {
"description": "Missing token or inactive user."
},
"403": {
"description": "Not a superuser."
},
"404": {
"description": "The user does not exist."
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/authenticated-route": {
"get": {
"tags": [
"custom-auth"
],
"summary": "Authenticated Route",
"operationId": "authenticated_route_get",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
}
},
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
}
},
"components": {
"schemas": {
"BearerResponse": {
"properties": {
"access_token": {
"type": "string",
"title": "Access Token"
},
"token_type": {
"type": "string",
"title": "Token Type"
}
},
"type": "object",
"required": [
"access_token",
"token_type"
],
"title": "BearerResponse"
},
"Body_auth-reset_forgot_password_post": {
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "Email"
}
},
"type": "object",
"required": [
"email"
],
"title": "Body_auth-reset:forgot_password_post"
},
"Body_auth-reset_reset_password_post": {
"properties": {
"token": {
"type": "string",
"title": "Token"
},
"password": {
"type": "string",
"title": "Password"
}
},
"type": "object",
"required": [
"token",
"password"
],
"title": "Body_auth-reset:reset_password_post"
},
"Body_auth-verify_request-token_post": {
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "Email"
}
},
"type": "object",
"required": [
"email"
],
"title": "Body_auth-verify:request-token_post"
},
"Body_auth-verify_verify_post": {
"properties": {
"token": {
"type": "string",
"title": "Token"
}
},
"type": "object",
"required": [
"token"
],
"title": "Body_auth-verify:verify_post"
},
"ErrorModel": {
"properties": {
"detail": {
"anyOf": [
{
"type": "string"
},
{
"additionalProperties": {
"type": "string"
},
"type": "object"
}
],
"title": "Detail"
}
},
"type": "object",
"required": [
"detail"
],
"title": "ErrorModel"
},
"HTTPValidationError": {
"properties": {
"detail": {
"items": {
"$ref": "#/components/schemas/ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
},
"UserCreate": {
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "Email"
},
"password": {
"type": "string",
"title": "Password"
},
"is_active": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Is Active",
"default": true
},
"is_superuser": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Is Superuser",
"default": false
},
"is_verified": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Is Verified",
"default": false
}
},
"type": "object",
"required": [
"email",
"password"
],
"title": "UserCreate"
},
"UserRead": {
"properties": {
"id": {
"type": "string",
"format": "uuid",
"title": "Id"
},
"email": {
"type": "string",
"format": "email",
"title": "Email"
},
"is_active": {
"type": "boolean",
"title": "Is Active",
"default": true
},
"is_superuser": {
"type": "boolean",
"title": "Is Superuser",
"default": false
},
"is_verified": {
"type": "boolean",
"title": "Is Verified",
"default": false
}
},
"type": "object",
"required": [
"id",
"email"
],
"title": "UserRead"
},
"UserUpdate": {
"properties": {
"password": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Password"
},
"email": {
"anyOf": [
{
"type": "string",
"format": "email"
},
{
"type": "null"
}
],
"title": "Email"
},
"is_active": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Is Active"
},
"is_superuser": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Is Superuser"
},
"is_verified": {
"anyOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"title": "Is Verified"
}
},
"type": "object",
"title": "UserUpdate"
},
"ValidationError": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"login_post": {
"properties": {
"grant_type": {
"anyOf": [
{
"type": "string",
"pattern": "password"
},
{
"type": "null"
}
],
"title": "Grant Type"
},
"username": {
"type": "string",
"title": "Username"
},
"password": {
"type": "string",
"title": "Password"
},
"scope": {
"type": "string",
"title": "Scope",
"default": ""
},
"client_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Client Id"
},
"client_secret": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Client Secret"
}
},
"type": "object",
"required": [
"username",
"password"
],
"title": "Body_auth-auth:jwt.login_post"
}
},
"securitySchemes": {
"OAuth2PasswordBearer": {
"type": "oauth2",
"flows": {
"password": {
"scopes": {},
"tokenUrl": "auth/jwt/login"
}
}
}
}
}
}
================================================
FILE: fastapi_backend/tests/commands/test_generate_openapi_schema.py
================================================
import json
import os
import pytest
from pathlib import Path
from commands.generate_openapi_schema import (
generate_openapi_schema,
remove_operation_id_tag,
)
def load_json_file(filename):
test_dir = os.path.dirname(__file__)
file_path = os.path.join(test_dir, "files", filename)
with open(file_path, "r") as f:
return json.load(f)
@pytest.fixture
def sample_openapi_schema():
return load_json_file("openapi_test.json")
@pytest.fixture
def expected_output_schema():
return load_json_file("openapi_test_output.json")
def test_remove_operation_id_tag(sample_openapi_schema, expected_output_schema):
cleaned_schema = remove_operation_id_tag(sample_openapi_schema)
assert cleaned_schema == expected_output_schema
@pytest.fixture
def mock_app(mocker):
app = mocker.patch("commands.generate_openapi_schema.app")
app.openapi.return_value = {
"openapi": "3.1.0",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {},
}
return app
def test_generate_openapi_schema(mocker, mock_app):
mock_remove_operation_id_tag = mocker.patch(
"commands.generate_openapi_schema.remove_operation_id_tag"
)
mock_remove_operation_id_tag.return_value = {"mocked_schema": True}
output_file = "openapi_test.json"
expected_output = json.dumps({"mocked_schema": True}, indent=2)
generate_openapi_schema(output_file)
mock_app.openapi.assert_called_once()
mock_remove_operation_id_tag.assert_called_once_with(mock_app.openapi.return_value)
output_path = Path(output_file)
assert output_path.is_file()
with open(output_file, "r") as f:
content = f.read()
assert content == expected_output
output_path.unlink()
================================================
FILE: fastapi_backend/tests/conftest.py
================================================
from httpx import AsyncClient, ASGITransport
import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from fastapi_users.db import SQLAlchemyUserDatabase
from fastapi_users.password import PasswordHelper
import uuid
from app.config import settings
from app.models import User, Base
from app.database import get_user_db, get_async_session
from app.main import app
from app.users import get_jwt_strategy
@pytest_asyncio.fixture(scope="function")
async def engine():
"""Create a fresh test database engine for each test function."""
engine = create_async_engine(settings.TEST_DATABASE_URL, echo=True)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await engine.dispose()
@pytest_asyncio.fixture(scope="function")
async def db_session(engine):
"""Create a fresh database session for each test."""
async_session_maker = async_sessionmaker(
engine, class_=AsyncSession, expire_on_commit=False
)
async with async_session_maker() as session:
yield session
await session.rollback()
await session.close()
@pytest_asyncio.fixture(scope="function")
async def test_client(db_session):
"""Fixture to create a test client that uses the test database session."""
# FastAPI-Users database override (wraps session with user operation helpers)
async def override_get_user_db():
session = SQLAlchemyUserDatabase(db_session, User)
try:
yield session
finally:
await db_session.close()
# General database override (raw session access)
async def override_get_async_session():
try:
yield db_session
finally:
await db_session.close()
# Set up test database overrides
app.dependency_overrides[get_user_db] = override_get_user_db
app.dependency_overrides[get_async_session] = override_get_async_session
async with AsyncClient(
transport=ASGITransport(app=app), base_url="http://localhost:8000"
) as client:
yield client
@pytest_asyncio.fixture(scope="function")
async def authenticated_user(test_client, db_session):
"""Fixture to create and authenticate a test user directly in the database."""
# Create user data
user_data = {
"id": uuid.uuid4(),
"email": "test@example.com",
"hashed_password": PasswordHelper().hash("TestPassword123#"),
"is_active": True,
"is_superuser": False,
"is_verified": True,
}
# Create user directly in database
user = User(**user_data)
db_session.add(user)
await db_session.commit()
await db_session.refresh(user)
# Generate token using the strategy directly
strategy = get_jwt_strategy()
access_token = await strategy.write_token(user)
# Return both the headers and the user data
return {
"headers": {"Authorization": f"Bearer {access_token}"},
"user": user,
"user_data": {"email": user_data["email"], "password": "TestPassword123#"},
}
================================================
FILE: fastapi_backend/tests/main/__init__.py
================================================
================================================
FILE: fastapi_backend/tests/main/test_main.py
================================================
import pytest
from fastapi import status
from fastapi_users.router import ErrorCode
from sqlalchemy import select
from app.models import User
class TestPasswordValidation:
@pytest.mark.parametrize(
"email, password, expected_status, expected_detail",
[
(
"test@example.com",
"short",
status.HTTP_400_BAD_REQUEST,
{
"detail": {
"code": ErrorCode.REGISTER_INVALID_PASSWORD.value,
"reason": ["Password should be at least 8 characters."],
}
},
),
(
"test@example.com",
"test@example.com",
status.HTTP_400_BAD_REQUEST,
{
"detail": {
"code": ErrorCode.REGISTER_INVALID_PASSWORD.value,
"reason": ["Password should not contain e-mail."],
}
},
),
(
"test@example.com",
"lowercasepassword",
status.HTTP_400_BAD_REQUEST,
{
"detail": {
"code": ErrorCode.REGISTER_INVALID_PASSWORD.value,
"reason": [
"Password should contain at least one uppercase letter."
],
}
},
),
(
"test@example.com",
"Nosppecialchar1",
status.HTTP_400_BAD_REQUEST,
{
"detail": {
"code": ErrorCode.REGISTER_INVALID_PASSWORD.value,
"reason": [
"Password should contain at least one special character."
],
}
},
),
(
"test@example.com",
"shorttest",
status.HTTP_400_BAD_REQUEST,
{
"detail": {
"code": ErrorCode.REGISTER_INVALID_PASSWORD.value,
"reason": [
"Password should be at least 8 characters.",
"Password should contain at least one uppercase letter.",
"Password should contain at least one special character.",
],
}
},
),
],
)
@pytest.mark.asyncio(loop_scope="function")
async def test_password_validation(
self, test_client, email, password, expected_status, expected_detail
):
"""Test user registration with password validation."""
json = {"email": email, "password": password}
response = await test_client.post("/auth/register", json=json)
assert response.status_code == expected_status
@pytest.mark.asyncio(loop_scope="function")
async def test_register_user_with_valid_password(self, test_client, db_session):
"""Test user registration with success"""
json = {
"email": "user@1.com",
"password": "Sppecialchar1#",
}
response = await test_client.post("/auth/register", json=json)
row = await db_session.execute(select(User))
user = row.scalars().first()
assert response.status_code == status.HTTP_201_CREATED
assert user is not None
assert user.email == "user@1.com"
================================================
FILE: fastapi_backend/tests/routes/__init__.py
================================================
================================================
FILE: fastapi_backend/tests/routes/test_items.py
================================================
import pytest
from fastapi import status
from sqlalchemy import select, insert
from app.models import Item
class TestItems:
@pytest.mark.asyncio(loop_scope="function")
async def test_create_item(self, test_client, db_session, authenticated_user):
"""Test creating an item."""
item_data = {"name": "Test Item", "description": "Test Description"}
create_response = await test_client.post(
"/items/", json=item_data, headers=authenticated_user["headers"]
)
assert create_response.status_code == status.HTTP_200_OK
created_item = create_response.json()
assert created_item["name"] == item_data["name"]
assert created_item["description"] == item_data["description"]
# Check if the item is in the database
item = await db_session.execute(
select(Item).where(Item.id == created_item["id"])
)
item = item.scalar()
assert item is not None
assert item.name == item_data["name"]
assert item.description == item_data["description"]
@pytest.mark.asyncio(loop_scope="function")
async def test_read_items(self, test_client, db_session, authenticated_user):
"""Test reading items."""
# Create multiple items
items_data = [
{
"name": "First Item",
"description": "First Description",
"user_id": authenticated_user["user"].id,
},
{
"name": "Second Item",
"description": "Second Description",
"user_id": authenticated_user["user"].id,
},
]
# create items in the database
for item_data in items_data:
await db_session.execute(insert(Item).values(**item_data))
await db_session.commit() # Add commit to ensure items are saved
# Read items - test pagination response
read_response = await test_client.get(
"/items/", headers=authenticated_user["headers"]
)
assert read_response.status_code == status.HTTP_200_OK
response_data = read_response.json()
# Check pagination structure
assert "items" in response_data
assert "total" in response_data
assert "page" in response_data
assert "size" in response_data
items = response_data["items"]
# Filter items created in this test (to avoid interference from other tests)
test_items = [
item for item in items if item["name"] in ["First Item", "Second Item"]
]
assert len(test_items) == 2
assert any(item["name"] == "First Item" for item in test_items)
assert any(item["name"] == "Second Item" for item in test_items)
@pytest.mark.asyncio(loop_scope="function")
async def test_delete_item(self, test_client, db_session, authenticated_user):
"""Test deleting an item."""
# Create an item directly in the database
item_data = {
"name": "Item to Delete",
"description": "Will be deleted",
"user_id": authenticated_user["user"].id,
}
await db_session.execute(insert(Item).values(**item_data))
# Get the created item from database
db_item = (
await db_session.execute(select(Item).where(Item.name == item_data["name"]))
).scalar()
# Delete the item
delete_response = await test_client.delete(
f"/items/{db_item.id}", headers=authenticated_user["headers"]
)
assert delete_response.status_code == status.HTTP_200_OK
# Verify item is deleted from database
db_check = (
await db_session.execute(select(Item).where(Item.id == db_item.id))
).scalar()
assert db_check is None
@pytest.mark.asyncio(loop_scope="function")
async def test_delete_nonexistent_item(self, test_client, authenticated_user):
"""Test deleting an item that doesn't exist."""
# Try to delete non-existent item
delete_response = await test_client.delete(
"/items/00000000-0000-0000-0000-000000000000",
headers=authenticated_user["headers"],
)
assert delete_response.status_code == status.HTTP_404_NOT_FOUND
@pytest.mark.asyncio(loop_scope="function")
async def test_unauthorized_read_items(self, test_client):
"""Test reading items without authentication."""
response = await test_client.get("/items/")
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.asyncio(loop_scope="function")
async def test_unauthorized_create_item(self, test_client):
"""Test creating item without authentication."""
item_data = {"name": "Unauthorized Item", "description": "Should fail"}
response = await test_client.post("/items/", json=item_data)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
@pytest.mark.asyncio(loop_scope="function")
async def test_unauthorized_delete_item(self, test_client):
"""Test deleting item without authentication."""
response = await test_client.delete(
"/items/00000000-0000-0000-0000-000000000000"
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
================================================
FILE: fastapi_backend/tests/test_database.py
================================================
import pytest
from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine
from fastapi_users.db import SQLAlchemyUserDatabase
from app.database import (
async_session_maker,
create_db_and_tables,
get_async_session,
get_user_db,
)
from app.models import Base, User
@pytest.fixture
async def mock_engine(mocker):
# Mock the engine
mock_engine = mocker.AsyncMock(spec=AsyncEngine)
# Create a mock connection
mock_conn = mocker.AsyncMock()
mock_conn.run_sync = mocker.AsyncMock()
# Set up the context manager properly
mock_context = mocker.AsyncMock()
mock_context.__aenter__.return_value = mock_conn
mock_engine.begin.return_value = mock_context
return mock_engine
@pytest.fixture
async def mock_session(mocker):
# Create a mock session
mock_session = mocker.AsyncMock(spec=AsyncSession)
# Mock the session context manager
mock_session.__aenter__.return_value = mock_session
mock_session.__aexit__.return_value = None
# Mock the session maker
mock_session_maker = mocker.patch("app.database.async_session_maker")
mock_session_maker.return_value = mock_session
return mock_session
@pytest.mark.asyncio
async def test_create_db_and_tables(mock_engine, mocker):
# Replace the real engine with our mock
mocker.patch("app.database.engine", mock_engine)
await create_db_and_tables()
# Verify that begin was called
mock_engine.begin.assert_called_once()
# Verify that create_all was called
mock_conn = mock_engine.begin.return_value.__aenter__.return_value
mock_conn.run_sync.assert_called_once_with(Base.metadata.create_all)
@pytest.mark.asyncio
async def test_get_async_session(mock_session):
# Test the session generator
session_generator = get_async_session()
session = await session_generator.__anext__()
# Verify we got the mock session
assert session == mock_session
# Verify the session was created with the expected context
mock_session.__aenter__.assert_called_once()
@pytest.mark.asyncio
async def test_get_user_db(mock_session):
# Test the user db generator
user_db_generator = get_user_db(mock_session)
user_db = await user_db_generator.__anext__()
# Verify we got a SQLAlchemyUserDatabase instance
assert isinstance(user_db, SQLAlchemyUserDatabase)
assert user_db.session == mock_session
# Verify the model class is correct
assert user_db.user_table == User
def test_engine_creation(mocker):
# Mock settings
mock_settings = mocker.patch("app.database.settings")
mock_settings.DATABASE_URL = "sqlite+aiosqlite:///./test.db"
mock_settings.EXPIRE_ON_COMMIT = False
# Import engine to trigger creation with mocked settings
from app.database import engine, async_session_maker
# Verify engine is created
assert isinstance(engine, AsyncEngine)
# Verify session maker is configured
assert async_session_maker.kw["expire_on_commit"] is False
@pytest.mark.asyncio
async def test_session_maker_configuration():
# Create a test session
async with async_session_maker() as session:
assert isinstance(session, AsyncSession)
================================================
FILE: fastapi_backend/tests/test_email.py
================================================
import pytest
from pathlib import Path
from fastapi_mail import ConnectionConfig, MessageSchema
from app.email import get_email_config, send_reset_password_email
from app.models import User
@pytest.fixture
def mock_settings(mocker):
mock = mocker.patch("app.email.settings")
# Set up mock settings with test values
mock.MAIL_USERNAME = "test_user"
mock.MAIL_PASSWORD = "test_pass"
mock.MAIL_FROM = "test@example.com"
mock.MAIL_PORT = 587
mock.MAIL_SERVER = "smtp.test.com"
mock.MAIL_FROM_NAME = "Test Sender"
mock.MAIL_STARTTLS = True
mock.MAIL_SSL_TLS = False
mock.USE_CREDENTIALS = True
mock.VALIDATE_CERTS = True
mock.TEMPLATE_DIR = "email_templates"
mock.FRONTEND_URL = "http://test-frontend.com"
return mock
@pytest.fixture
def mock_user():
return User(
email="user@example.com",
)
def test_get_email_config(mock_settings):
config = get_email_config()
assert isinstance(config, ConnectionConfig)
assert config.MAIL_USERNAME == "test_user"
assert config.MAIL_PASSWORD == "test_pass"
assert config.MAIL_FROM == "test@example.com"
assert config.MAIL_PORT == 587
assert config.MAIL_SERVER == "smtp.test.com"
assert config.MAIL_FROM_NAME == "Test Sender"
assert config.MAIL_STARTTLS
assert not config.MAIL_SSL_TLS
assert config.USE_CREDENTIALS
assert config.VALIDATE_CERTS
assert isinstance(config.TEMPLATE_FOLDER, Path)
@pytest.mark.asyncio
async def test_send_reset_password_email(mock_settings, mock_user, mocker):
# Mock FastMail
mock_fastmail = mocker.patch("app.email.FastMail")
mock_fastmail_instance = mock_fastmail.return_value
mock_fastmail_instance.send_message = mocker.AsyncMock()
# Test data
test_token = "test-token-123"
# Call the function
await send_reset_password_email(mock_user, test_token)
# Verify FastMail was instantiated with correct config
mock_fastmail.assert_called_once()
config_arg = mock_fastmail.call_args[0][0]
assert isinstance(config_arg, ConnectionConfig)
# Verify send_message was called
mock_fastmail_instance.send_message.assert_called_once()
# Verify the message schema
message_arg = mock_fastmail_instance.send_message.call_args[0][0]
assert isinstance(message_arg, MessageSchema)
assert message_arg.subject == "Password recovery"
assert message_arg.recipients == [mock_user.email]
# Verify template body contains correct data
expected_link = (
f"http://test-frontend.com/password-recovery/confirm?token={test_token}"
)
assert message_arg.template_body == {
"username": mock_user.email,
"link": expected_link,
}
# Verify template name
template_name = mock_fastmail_instance.send_message.call_args[1]["template_name"]
assert template_name == "password_reset.html"
================================================
FILE: fastapi_backend/tests/utils/__init__.py
================================================
================================================
FILE: fastapi_backend/tests/utils/test_utils.py
================================================
from fastapi.routing import APIRoute
from app.utils import simple_generate_unique_route_id
def test_simple_generate_unique_route_id(mocker):
mock_route = mocker.Mock(spec=APIRoute)
mock_route.tags = ["auth"]
mock_route.name = "authenticate_user"
unique_id = simple_generate_unique_route_id(mock_route)
assert unique_id == "auth-authenticate_user"
================================================
FILE: fastapi_backend/vercel.json
================================================
{
"buildCommand": "python3 -m venv venv && . venv/bin/activate && pip install -r requirements.txt && alembic upgrade head && deactivate && rm -rf venv",
"outputDirectory": "api",
"git": {
"deploymentEnabled": {
"main": false
}
},
"routes": [
{
"src": "/(.*)",
"dest": "api/index.py"
}
]
}
================================================
FILE: fastapi_backend/vercel.prod.json
================================================
{
"routes": [
{
"src": "/(.*)",
"dest": "api/index.py"
}
]
}
================================================
FILE: fastapi_backend/watcher.py
================================================
import time
import re
import subprocess
import os
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from threading import Timer
# Updated regex to include main.py, schemas.py, and all .py files in app/routes
WATCHER_REGEX_PATTERN = re.compile(r"(main\.py|schemas\.py|routes/.*\.py)$")
APP_PATH = "app"
class MyHandler(FileSystemEventHandler):
def __init__(self):
super().__init__()
self.debounce_timer = None
self.last_modified = 0
def on_modified(self, event):
if not event.is_directory and WATCHER_REGEX_PATTERN.search(
os.path.relpath(event.src_path, APP_PATH)
):
current_time = time.time()
if current_time - self.last_modified > 1:
self.last_modified = current_time
if self.debounce_timer:
self.debounce_timer.cancel()
self.debounce_timer = Timer(1.0, self.execute_command, [event.src_path])
self.debounce_timer.start()
def execute_command(self, file_path):
print(f"File {file_path} has been modified and saved.")
self.run_mypy_checks()
self.run_openapi_schema_generation()
def run_mypy_checks(self):
"""Run mypy type checks and print output."""
print("Running mypy type checks...")
result = subprocess.run(
["uv", "run", "mypy", "app"],
capture_output=True,
text=True,
check=False,
)
print(result.stdout, result.stderr, sep="\n")
print(
"Type errors detected! We recommend checking the mypy output for "
"more information on the issues."
if result.returncode
else "No type errors detected."
)
def run_openapi_schema_generation(self):
"""Run the OpenAPI schema generation command."""
print("Proceeding with OpenAPI schema generation...")
try:
subprocess.run(
[
"uv",
"run",
"python",
"-m",
"commands.generate_openapi_schema",
],
check=True,
)
print("OpenAPI schema generation completed successfully.")
except subprocess.CalledProcessError as e:
print(f"An error occurred while generating OpenAPI schema: {e}")
if __name__ == "__main__":
observer = Observer()
observer.schedule(MyHandler(), APP_PATH, recursive=True)
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
================================================
FILE: local-shared-data/openapi.json
================================================
{
"openapi": "3.1.0",
"info": {
"title": "FastAPI",
"version": "0.1.0"
},
"paths": {
"/auth/jwt/login": {
"post": {
"tags": [
"auth"
],
"summary": "Auth:Jwt.Login",
"operationId": "auth:jwt.login",
"requestBody": {
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"$ref": "#/components/schemas/login"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BearerResponse"
},
"example": {
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiOTIyMWZmYzktNjQwZi00MzcyLTg2ZDMtY2U2NDJjYmE1NjAzIiwiYXVkIjoiZmFzdGFwaS11c2VyczphdXRoIiwiZXhwIjoxNTcxNTA0MTkzfQ.M10bjOe45I5Ncu_uXvOmVV8QxnL-nZfcH96U90JaocI",
"token_type": "bearer"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
},
"examples": {
"LOGIN_BAD_CREDENTIALS": {
"summary": "Bad credentials or the user is inactive.",
"value": {
"detail": "LOGIN_BAD_CREDENTIALS"
}
},
"LOGIN_USER_NOT_VERIFIED": {
"summary": "The user is not verified.",
"value": {
"detail": "LOGIN_USER_NOT_VERIFIED"
}
}
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/auth/jwt/logout": {
"post": {
"tags": [
"auth"
],
"summary": "Auth:Jwt.Logout",
"operationId": "auth:jwt.logout",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {}
}
}
},
"401": {
"description": "Missing token or inactive user."
}
},
"security": [
{
"OAuth2PasswordBearer": []
}
]
}
},
"/auth/register": {
"post": {
"tags": [
"auth"
],
"summary": "Register:Register",
"operationId": "register:register",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserCreate"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UserRead"
}
}
}
},
"400": {
"description": "Bad Request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorModel"
},
"examples": {
"REGISTER_USER_ALREADY_EXISTS": {
"summary": "A user with this email already exists.",
"value": {
"detail": "REGISTER_USER_ALREADY_EXISTS"
}
},
"REGISTER_INVALID_PASSWORD": {
"summary": "Password validation failed.",
"value": {
"detail": {
"code": "REGISTER_INVALID_PASSWORD",
gitextract_r3jbnx09/ ├── .github/ │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── ci.yml │ ├── pre-commit.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.docker.yaml ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── LICENSE.txt ├── Makefile ├── README.md ├── docker-compose.yml ├── docs/ │ ├── additional-settings.md │ ├── contributing.md │ ├── deployment.md │ ├── get-started.md │ ├── stylesheets/ │ │ └── extra.css │ ├── support.md │ └── technology-selection.md ├── fastapi_backend/ │ ├── .gitignore │ ├── Dockerfile │ ├── alembic.ini │ ├── alembic_migrations/ │ │ ├── README │ │ ├── env.py │ │ ├── script.py.mako │ │ └── versions/ │ │ ├── 402d067a8b92_added_user_table.py │ │ └── b389592974f8_add_item_model.py │ ├── api/ │ │ └── index.py │ ├── app/ │ │ ├── __init__.py │ │ ├── config.py │ │ ├── database.py │ │ ├── email.py │ │ ├── email_templates/ │ │ │ ├── __init__.py │ │ │ └── password_reset.html │ │ ├── main.py │ │ ├── models.py │ │ ├── routes/ │ │ │ ├── __init__.py │ │ │ └── items.py │ │ ├── schemas.py │ │ ├── users.py │ │ └── utils.py │ ├── commands/ │ │ ├── __init__.py │ │ └── generate_openapi_schema.py │ ├── mypy.ini │ ├── pyproject.toml │ ├── pytest.ini │ ├── requirements.txt │ ├── start.sh │ ├── tests/ │ │ ├── __init__.py │ │ ├── commands/ │ │ │ ├── __init__.py │ │ │ ├── files/ │ │ │ │ ├── openapi_test.json │ │ │ │ └── openapi_test_output.json │ │ │ └── test_generate_openapi_schema.py │ │ ├── conftest.py │ │ ├── main/ │ │ │ ├── __init__.py │ │ │ └── test_main.py │ │ ├── routes/ │ │ │ ├── __init__.py │ │ │ └── test_items.py │ │ ├── test_database.py │ │ ├── test_email.py │ │ └── utils/ │ │ ├── __init__.py │ │ └── test_utils.py │ ├── vercel.json │ ├── vercel.prod.json │ └── watcher.py ├── local-shared-data/ │ └── openapi.json ├── mkdocs.yml ├── nextjs-frontend/ │ ├── .gitignore │ ├── .prettierignore │ ├── Dockerfile │ ├── __tests__/ │ │ ├── login.test.tsx │ │ ├── loginPage.test.tsx │ │ ├── passwordReset.test.tsx │ │ ├── passwordResetConfirm.test.tsx │ │ ├── passwordResetConfirmPage.test.tsx │ │ ├── passwordResetPage.test.tsx │ │ ├── register.test.ts │ │ └── registerPage.test.tsx │ ├── app/ │ │ ├── clientService.ts │ │ ├── dashboard/ │ │ │ ├── add-item/ │ │ │ │ └── page.tsx │ │ │ ├── deleteButton.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── login/ │ │ │ └── page.tsx │ │ ├── openapi-client/ │ │ │ ├── client/ │ │ │ │ ├── client.gen.ts │ │ │ │ ├── index.ts │ │ │ │ ├── types.gen.ts │ │ │ │ └── utils.gen.ts │ │ │ ├── client.gen.ts │ │ │ ├── core/ │ │ │ │ ├── auth.gen.ts │ │ │ │ ├── bodySerializer.gen.ts │ │ │ │ ├── params.gen.ts │ │ │ │ ├── pathSerializer.gen.ts │ │ │ │ ├── serverSentEvents.gen.ts │ │ │ │ ├── types.gen.ts │ │ │ │ └── utils.gen.ts │ │ │ ├── index.ts │ │ │ ├── sdk.gen.ts │ │ │ └── types.gen.ts │ │ ├── page.tsx │ │ ├── password-recovery/ │ │ │ ├── confirm/ │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ └── register/ │ │ └── page.tsx │ ├── components/ │ │ ├── actions/ │ │ │ ├── items-action.ts │ │ │ ├── login-action.ts │ │ │ ├── logout-action.ts │ │ │ ├── password-reset-action.ts │ │ │ └── register-action.ts │ │ ├── page-pagination.tsx │ │ ├── page-size-selector.tsx │ │ └── ui/ │ │ ├── FormError.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── select.tsx │ │ ├── submitButton.tsx │ │ ├── table.tsx │ │ └── tabs.tsx │ ├── components.json │ ├── eslint.config.mjs │ ├── jest.config.ts │ ├── next.config.mjs │ ├── openapi-ts.config.ts │ ├── openapi.json │ ├── package.json │ ├── pnpm-workspace.yaml │ ├── postcss.config.js │ ├── proxy.ts │ ├── start.sh │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── vercel.json │ └── watcher.js ├── overrides/ │ └── main.html ├── prod-backend-deploy.yml └── prod-frontend-deploy.yml
SYMBOL INDEX (245 symbols across 55 files)
FILE: fastapi_backend/alembic_migrations/env.py
function run_migrations_offline (line 54) | def run_migrations_offline() -> None:
function do_run_migrations (line 78) | def do_run_migrations(connection: Connection) -> None:
function run_async_migrations (line 85) | async def run_async_migrations() -> None:
function run_migrations_online (line 103) | def run_migrations_online() -> None:
FILE: fastapi_backend/alembic_migrations/versions/402d067a8b92_added_user_table.py
function upgrade (line 23) | def upgrade() -> None:
function downgrade (line 39) | def downgrade() -> None:
FILE: fastapi_backend/alembic_migrations/versions/b389592974f8_add_item_model.py
function upgrade (line 22) | def upgrade() -> None:
function downgrade (line 40) | def downgrade() -> None:
FILE: fastapi_backend/app/config.py
class Settings (line 6) | class Settings(BaseSettings):
FILE: fastapi_backend/app/database.py
function create_db_and_tables (line 29) | async def create_db_and_tables():
function get_async_session (line 34) | async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
function get_user_db (line 39) | async def get_user_db(session: AsyncSession = Depends(get_async_session)):
FILE: fastapi_backend/app/email.py
function get_email_config (line 9) | def get_email_config():
function send_reset_password_email (line 26) | async def send_reset_password_email(user: User, token: str):
FILE: fastapi_backend/app/models.py
class Base (line 9) | class Base(DeclarativeBase):
class User (line 13) | class User(SQLAlchemyBaseUserTableUUID, Base):
class Item (line 17) | class Item(Base):
FILE: fastapi_backend/app/routes/items.py
function transform_items (line 17) | def transform_items(items):
function read_item (line 22) | async def read_item(
function create_item (line 34) | async def create_item(
function delete_item (line 47) | async def delete_item(
FILE: fastapi_backend/app/schemas.py
class UserRead (line 8) | class UserRead(schemas.BaseUser[uuid.UUID]):
class UserCreate (line 12) | class UserCreate(schemas.BaseUserCreate):
class UserUpdate (line 16) | class UserUpdate(schemas.BaseUserUpdate):
class ItemBase (line 20) | class ItemBase(BaseModel):
class ItemCreate (line 26) | class ItemCreate(ItemBase):
class ItemRead (line 30) | class ItemRead(ItemBase):
FILE: fastapi_backend/app/users.py
class UserManager (line 30) | class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
method on_after_register (line 34) | async def on_after_register(self, user: User, request: Optional[Reques...
method on_after_forgot_password (line 37) | async def on_after_forgot_password(
method on_after_request_verify (line 42) | async def on_after_request_verify(
method validate_password (line 47) | async def validate_password(
function get_user_manager (line 67) | async def get_user_manager(user_db: SQLAlchemyUserDatabase = Depends(get...
function get_jwt_strategy (line 74) | def get_jwt_strategy() -> JWTStrategy:
FILE: fastapi_backend/app/utils.py
function simple_generate_unique_route_id (line 4) | def simple_generate_unique_route_id(route: APIRoute):
FILE: fastapi_backend/commands/generate_openapi_schema.py
function generate_openapi_schema (line 13) | def generate_openapi_schema(output_file):
function remove_operation_id_tag (line 23) | def remove_operation_id_tag(schema):
FILE: fastapi_backend/tests/commands/test_generate_openapi_schema.py
function load_json_file (line 12) | def load_json_file(filename):
function sample_openapi_schema (line 20) | def sample_openapi_schema():
function expected_output_schema (line 25) | def expected_output_schema():
function test_remove_operation_id_tag (line 29) | def test_remove_operation_id_tag(sample_openapi_schema, expected_output_...
function mock_app (line 35) | def mock_app(mocker):
function test_generate_openapi_schema (line 45) | def test_generate_openapi_schema(mocker, mock_app):
FILE: fastapi_backend/tests/conftest.py
function engine (line 17) | async def engine():
function db_session (line 33) | async def db_session(engine):
function test_client (line 46) | async def test_client(db_session):
function authenticated_user (line 75) | async def authenticated_user(test_client, db_session):
FILE: fastapi_backend/tests/main/test_main.py
class TestPasswordValidation (line 8) | class TestPasswordValidation:
method test_password_validation (line 78) | async def test_password_validation(
method test_register_user_with_valid_password (line 88) | async def test_register_user_with_valid_password(self, test_client, db...
FILE: fastapi_backend/tests/routes/test_items.py
class TestItems (line 7) | class TestItems:
method test_create_item (line 9) | async def test_create_item(self, test_client, db_session, authenticate...
method test_read_items (line 32) | async def test_read_items(self, test_client, db_session, authenticated...
method test_delete_item (line 78) | async def test_delete_item(self, test_client, db_session, authenticate...
method test_delete_nonexistent_item (line 106) | async def test_delete_nonexistent_item(self, test_client, authenticate...
method test_unauthorized_read_items (line 116) | async def test_unauthorized_read_items(self, test_client):
method test_unauthorized_create_item (line 122) | async def test_unauthorized_create_item(self, test_client):
method test_unauthorized_delete_item (line 129) | async def test_unauthorized_delete_item(self, test_client):
FILE: fastapi_backend/tests/test_database.py
function mock_engine (line 15) | async def mock_engine(mocker):
function mock_session (line 32) | async def mock_session(mocker):
function test_create_db_and_tables (line 48) | async def test_create_db_and_tables(mock_engine, mocker):
function test_get_async_session (line 63) | async def test_get_async_session(mock_session):
function test_get_user_db (line 76) | async def test_get_user_db(mock_session):
function test_engine_creation (line 88) | def test_engine_creation(mocker):
function test_session_maker_configuration (line 105) | async def test_session_maker_configuration():
FILE: fastapi_backend/tests/test_email.py
function mock_settings (line 9) | def mock_settings(mocker):
function mock_user (line 28) | def mock_user():
function test_get_email_config (line 34) | def test_get_email_config(mock_settings):
function test_send_reset_password_email (line 52) | async def test_send_reset_password_email(mock_settings, mock_user, mocker):
FILE: fastapi_backend/tests/utils/test_utils.py
function test_simple_generate_unique_route_id (line 5) | def test_simple_generate_unique_route_id(mocker):
FILE: fastapi_backend/watcher.py
class MyHandler (line 14) | class MyHandler(FileSystemEventHandler):
method __init__ (line 15) | def __init__(self):
method on_modified (line 20) | def on_modified(self, event):
method execute_command (line 32) | def execute_command(self, file_path):
method run_mypy_checks (line 37) | def run_mypy_checks(self):
method run_openapi_schema_generation (line 54) | def run_openapi_schema_generation(self):
FILE: nextjs-frontend/app/dashboard/add-item/page.tsx
function CreateItemPage (line 11) | function CreateItemPage() {
FILE: nextjs-frontend/app/dashboard/deleteButton.tsx
type DeleteButtonProps (line 6) | interface DeleteButtonProps {
function DeleteButton (line 10) | function DeleteButton({ itemId }: DeleteButtonProps) {
FILE: nextjs-frontend/app/dashboard/layout.tsx
function DashboardLayout (line 21) | function DashboardLayout({
FILE: nextjs-frontend/app/dashboard/page.tsx
type DashboardPageProps (line 23) | interface DashboardPageProps {
function DashboardPage (line 30) | async function DashboardPage({
FILE: nextjs-frontend/app/layout.tsx
function RootLayout (line 21) | function RootLayout({
FILE: nextjs-frontend/app/login/page.tsx
function Page (line 19) | function Page() {
FILE: nextjs-frontend/app/openapi-client/client.gen.ts
type CreateClientConfig (line 19) | type CreateClientConfig<T extends DefaultClientOptions = ClientOptions> =
FILE: nextjs-frontend/app/openapi-client/client/types.gen.ts
type Config (line 22) | interface Config<T extends ClientOptions = ClientOptions>
type RequestOptions (line 62) | interface RequestOptions<
type ClientOptions (line 92) | interface ClientOptions {
type RequestResult (line 97) | type RequestResult<
type MethodFn (line 121) | type MethodFn = <
type SseFn (line 129) | type SseFn = <
type RequestFn (line 137) | type RequestFn = <
type BuildUrlFn (line 146) | type BuildUrlFn = <
type Client (line 157) | type Client = CoreClient<
type CreateClientConfig (line 175) | type CreateClientConfig<T extends ClientOptions = ClientOptions> = (
type TDataShape (line 179) | interface TDataShape {
type OmitKeys (line 187) | type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
type Options (line 189) | type Options<
type OptionsLegacyParser (line 199) | type OptionsLegacyParser<
FILE: nextjs-frontend/app/openapi-client/core/auth.gen.ts
type AuthToken (line 3) | type AuthToken = string | undefined;
type Auth (line 5) | interface Auth {
FILE: nextjs-frontend/app/openapi-client/core/bodySerializer.gen.ts
type QuerySerializer (line 9) | type QuerySerializer = (query: Record<string, unknown>) => string;
type BodySerializer (line 11) | type BodySerializer = (body: any) => any;
type QuerySerializerOptions (line 13) | interface QuerySerializerOptions {
FILE: nextjs-frontend/app/openapi-client/core/params.gen.ts
type Slot (line 3) | type Slot = "body" | "headers" | "path" | "query";
type Field (line 5) | type Field =
type Fields (line 27) | interface Fields {
type FieldsConfig (line 32) | type FieldsConfig = ReadonlyArray<Field | Fields>;
type KeyMap (line 42) | type KeyMap = Map<
type Params (line 71) | interface Params {
FILE: nextjs-frontend/app/openapi-client/core/pathSerializer.gen.ts
type SerializeOptions (line 3) | interface SerializeOptions<T>
type SerializePrimitiveOptions (line 7) | interface SerializePrimitiveOptions {
type SerializerOptions (line 12) | interface SerializerOptions<T> {
type ArrayStyle (line 20) | type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited";
type ArraySeparatorStyle (line 21) | type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
type MatrixStyle (line 22) | type MatrixStyle = "label" | "matrix" | "simple";
type ObjectStyle (line 23) | type ObjectStyle = "form" | "deepObject";
type ObjectSeparatorStyle (line 24) | type ObjectSeparatorStyle = ObjectStyle | MatrixStyle;
type SerializePrimitiveParam (line 26) | interface SerializePrimitiveParam extends SerializePrimitiveOptions {
FILE: nextjs-frontend/app/openapi-client/core/serverSentEvents.gen.ts
type ServerSentEventsOptions (line 5) | type ServerSentEventsOptions<TData = unknown> = Omit<
type StreamEvent (line 70) | interface StreamEvent<TData = unknown> {
type ServerSentEventsResult (line 77) | type ServerSentEventsResult<
FILE: nextjs-frontend/app/openapi-client/core/types.gen.ts
type HttpMethod (line 10) | type HttpMethod =
type Client (line 21) | type Client<
type Config (line 41) | interface Config {
type IsExactlyNeverOrNeverUndefined (line 106) | type IsExactlyNeverOrNeverUndefined<T> = [T] extends [never]
type OmitNever (line 114) | type OmitNever<T extends Record<string, unknown>> = {
FILE: nextjs-frontend/app/openapi-client/core/utils.gen.ts
type PathSerializer (line 11) | interface PathSerializer {
constant PATH_PARAM_RE (line 16) | const PATH_PARAM_RE = /\{[^{}]+\}/g;
function getValidRequestBody (line 116) | function getValidRequestBody(options: {
FILE: nextjs-frontend/app/openapi-client/sdk.gen.ts
type Options (line 58) | type Options<
FILE: nextjs-frontend/app/openapi-client/types.gen.ts
type BearerResponse (line 6) | type BearerResponse = {
type BodyAuthResetForgotPassword (line 20) | type BodyAuthResetForgotPassword = {
type BodyAuthResetResetPassword (line 30) | type BodyAuthResetResetPassword = {
type BodyAuthVerifyRequestToken (line 44) | type BodyAuthVerifyRequestToken = {
type BodyAuthVerifyVerify (line 54) | type BodyAuthVerifyVerify = {
type ErrorModel (line 64) | type ErrorModel = {
type HttpValidationError (line 78) | type HttpValidationError = {
type ItemCreate (line 88) | type ItemCreate = {
type ItemRead (line 106) | type ItemRead = {
type PageItemRead (line 132) | type PageItemRead = {
type UserCreate (line 158) | type UserCreate = {
type UserRead (line 184) | type UserRead = {
type UserUpdate (line 210) | type UserUpdate = {
type ValidationError (line 236) | type ValidationError = {
type Login (line 254) | type Login = {
type AuthJwtLoginData (line 281) | type AuthJwtLoginData = {
type AuthJwtLoginErrors (line 288) | type AuthJwtLoginErrors = {
type AuthJwtLoginError (line 299) | type AuthJwtLoginError = AuthJwtLoginErrors[keyof AuthJwtLoginErrors];
type AuthJwtLoginResponses (line 301) | type AuthJwtLoginResponses = {
type AuthJwtLoginResponse (line 308) | type AuthJwtLoginResponse =
type AuthJwtLogoutData (line 311) | type AuthJwtLogoutData = {
type AuthJwtLogoutErrors (line 318) | type AuthJwtLogoutErrors = {
type AuthJwtLogoutResponses (line 325) | type AuthJwtLogoutResponses = {
type RegisterRegisterData (line 332) | type RegisterRegisterData = {
type RegisterRegisterErrors (line 339) | type RegisterRegisterErrors = {
type RegisterRegisterError (line 350) | type RegisterRegisterError =
type RegisterRegisterResponses (line 353) | type RegisterRegisterResponses = {
type RegisterRegisterResponse (line 360) | type RegisterRegisterResponse =
type ResetForgotPasswordData (line 363) | type ResetForgotPasswordData = {
type ResetForgotPasswordErrors (line 370) | type ResetForgotPasswordErrors = {
type ResetForgotPasswordError (line 377) | type ResetForgotPasswordError =
type ResetForgotPasswordResponses (line 380) | type ResetForgotPasswordResponses = {
type ResetResetPasswordData (line 387) | type ResetResetPasswordData = {
type ResetResetPasswordErrors (line 394) | type ResetResetPasswordErrors = {
type ResetResetPasswordError (line 405) | type ResetResetPasswordError =
type ResetResetPasswordResponses (line 408) | type ResetResetPasswordResponses = {
type VerifyRequestTokenData (line 415) | type VerifyRequestTokenData = {
type VerifyRequestTokenErrors (line 422) | type VerifyRequestTokenErrors = {
type VerifyRequestTokenError (line 429) | type VerifyRequestTokenError =
type VerifyRequestTokenResponses (line 432) | type VerifyRequestTokenResponses = {
type VerifyVerifyData (line 439) | type VerifyVerifyData = {
type VerifyVerifyErrors (line 446) | type VerifyVerifyErrors = {
type VerifyVerifyError (line 457) | type VerifyVerifyError = VerifyVerifyErrors[keyof VerifyVerifyErrors];
type VerifyVerifyResponses (line 459) | type VerifyVerifyResponses = {
type VerifyVerifyResponse (line 466) | type VerifyVerifyResponse =
type UsersCurrentUserData (line 469) | type UsersCurrentUserData = {
type UsersCurrentUserErrors (line 476) | type UsersCurrentUserErrors = {
type UsersCurrentUserResponses (line 483) | type UsersCurrentUserResponses = {
type UsersCurrentUserResponse (line 490) | type UsersCurrentUserResponse =
type UsersPatchCurrentUserData (line 493) | type UsersPatchCurrentUserData = {
type UsersPatchCurrentUserErrors (line 500) | type UsersPatchCurrentUserErrors = {
type UsersPatchCurrentUserError (line 515) | type UsersPatchCurrentUserError =
type UsersPatchCurrentUserResponses (line 518) | type UsersPatchCurrentUserResponses = {
type UsersPatchCurrentUserResponse (line 525) | type UsersPatchCurrentUserResponse =
type UsersDeleteUserData (line 528) | type UsersDeleteUserData = {
type UsersDeleteUserErrors (line 540) | type UsersDeleteUserErrors = {
type UsersDeleteUserError (line 559) | type UsersDeleteUserError =
type UsersDeleteUserResponses (line 562) | type UsersDeleteUserResponses = {
type UsersDeleteUserResponse (line 569) | type UsersDeleteUserResponse =
type UsersUserData (line 572) | type UsersUserData = {
type UsersUserErrors (line 584) | type UsersUserErrors = {
type UsersUserError (line 603) | type UsersUserError = UsersUserErrors[keyof UsersUserErrors];
type UsersUserResponses (line 605) | type UsersUserResponses = {
type UsersUserResponse (line 612) | type UsersUserResponse = UsersUserResponses[keyof UsersUserResponses];
type UsersPatchUserData (line 614) | type UsersPatchUserData = {
type UsersPatchUserErrors (line 626) | type UsersPatchUserErrors = {
type UsersPatchUserError (line 649) | type UsersPatchUserError =
type UsersPatchUserResponses (line 652) | type UsersPatchUserResponses = {
type UsersPatchUserResponse (line 659) | type UsersPatchUserResponse =
type ReadItemData (line 662) | type ReadItemData = {
type ReadItemErrors (line 680) | type ReadItemErrors = {
type ReadItemError (line 687) | type ReadItemError = ReadItemErrors[keyof ReadItemErrors];
type ReadItemResponses (line 689) | type ReadItemResponses = {
type ReadItemResponse (line 696) | type ReadItemResponse = ReadItemResponses[keyof ReadItemResponses];
type CreateItemData (line 698) | type CreateItemData = {
type CreateItemErrors (line 705) | type CreateItemErrors = {
type CreateItemError (line 712) | type CreateItemError = CreateItemErrors[keyof CreateItemErrors];
type CreateItemResponses (line 714) | type CreateItemResponses = {
type CreateItemResponse (line 721) | type CreateItemResponse = CreateItemResponses[keyof CreateItemResponses];
type DeleteItemData (line 723) | type DeleteItemData = {
type DeleteItemErrors (line 735) | type DeleteItemErrors = {
type DeleteItemError (line 742) | type DeleteItemError = DeleteItemErrors[keyof DeleteItemErrors];
type DeleteItemResponses (line 744) | type DeleteItemResponses = {
type ClientOptions (line 751) | type ClientOptions = {
FILE: nextjs-frontend/app/page.tsx
function Home (line 6) | function Home() {
FILE: nextjs-frontend/app/password-recovery/confirm/page.tsx
function ResetPasswordForm (line 19) | function ResetPasswordForm() {
function Page (line 68) | function Page() {
FILE: nextjs-frontend/app/password-recovery/page.tsx
function Page (line 19) | function Page() {
FILE: nextjs-frontend/app/register/page.tsx
function Page (line 19) | function Page() {
FILE: nextjs-frontend/components/actions/items-action.ts
function fetchItems (line 9) | async function fetchItems(page: number = 1, size: number = 10) {
function removeItem (line 34) | async function removeItem(id: string) {
function addItem (line 57) | async function addItem(prevState: {}, formData: FormData) {
FILE: nextjs-frontend/components/actions/login-action.ts
function login (line 10) | async function login(prevState: unknown, formData: FormData) {
FILE: nextjs-frontend/components/actions/logout-action.ts
function logout (line 7) | async function logout() {
FILE: nextjs-frontend/components/actions/password-reset-action.ts
function passwordReset (line 8) | async function passwordReset(prevState: unknown, formData: FormData) {
function passwordResetConfirm (line 29) | async function passwordResetConfirm(
FILE: nextjs-frontend/components/actions/register-action.ts
function register (line 10) | async function register(prevState: unknown, formData: FormData) {
FILE: nextjs-frontend/components/page-pagination.tsx
type PagePaginationProps (line 10) | interface PagePaginationProps {
function PagePagination (line 18) | function PagePagination({
FILE: nextjs-frontend/components/page-size-selector.tsx
type PageSizeSelectorProps (line 12) | interface PageSizeSelectorProps {
function PageSizeSelector (line 16) | function PageSizeSelector({ currentSize }: PageSizeSelectorProps) {
FILE: nextjs-frontend/components/ui/FormError.tsx
type ErrorState (line 1) | interface ErrorState {
type FormErrorProps (line 9) | interface FormErrorProps {
function FormError (line 14) | function FormError({ state, className = "" }: FormErrorProps) {
type FieldErrorProps (line 23) | interface FieldErrorProps {
function FieldError (line 29) | function FieldError({ state, field, className = "" }: FieldErrorProps) {
FILE: nextjs-frontend/components/ui/badge.tsx
type BadgeProps (line 26) | interface BadgeProps
function Badge (line 30) | function Badge({ className, variant, ...props }: BadgeProps) {
FILE: nextjs-frontend/components/ui/button.tsx
type ButtonProps (line 37) | interface ButtonProps
FILE: nextjs-frontend/components/ui/form.tsx
type FormFieldContextValue (line 20) | type FormFieldContextValue<
type FormItemContextValue (line 67) | type FormItemContextValue = {
FILE: nextjs-frontend/components/ui/input.tsx
type InputProps (line 5) | interface InputProps
FILE: nextjs-frontend/components/ui/submitButton.tsx
function SubmitButton (line 4) | function SubmitButton({ text }: { text: string }) {
FILE: nextjs-frontend/proxy.ts
function proxy (line 5) | async function proxy(request: NextRequest) {
Condensed preview — 144 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (473K chars).
[
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 1072,
"preview": "<!--- Provide a general summary of your changes in the Title above -->\n\n## Description\n<!--- Describe your changes in de"
},
{
"path": ".github/workflows/ci.yml",
"chars": 2690,
"preview": "name: CI\n\non:\n push:\n pull_request:\n\njobs:\n build-fastapi:\n name: FastAPI CI\n\n runs-on: ubuntu-latest\n\n env:"
},
{
"path": ".github/workflows/pre-commit.yml",
"chars": 1310,
"preview": "name: pre-commit\n\non:\n push:\n pull_request:\n\njobs:\n pre-commit:\n\n runs-on: ubuntu-latest\n\n env:\n DATABASE_"
},
{
"path": ".github/workflows/release.yml",
"chars": 1935,
"preview": "name: Release draft creation\n\non:\n workflow_dispatch:\n inputs:\n version:\n description: \"Version of the r"
},
{
"path": ".gitignore",
"chars": 2789,
"preview": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packagi"
},
{
"path": ".pre-commit-config.docker.yaml",
"chars": 1444,
"preview": "fail_fast: true\n\nrepos:\n - repo: https://github.com/pre-commit/pre-commit-hooks\n rev: v4.5.0\n hooks:\n - id: "
},
{
"path": ".pre-commit-config.yaml",
"chars": 2133,
"preview": "fail_fast: true\n\nrepos:\n - repo: https://github.com/pre-commit/pre-commit-hooks\n rev: v5.0.0\n hooks:\n - id: "
},
{
"path": "CHANGELOG.md",
"chars": 1055,
"preview": "# Changelog\n\nThis changelog references changes made both to the FastAPI backend, `fastapi_backend`, and the\nfrontend Typ"
},
{
"path": "LICENSE.txt",
"chars": 1100,
"preview": "MIT License\n\nCopyright (c) 2017 Vinta Serviços e Soluções Tecnológicas Ltda\n\nPermission is hereby granted, free of charg"
},
{
"path": "Makefile",
"chars": 2423,
"preview": "# Makefile\n\n# Variables\nBACKEND_DIR=fastapi_backend\nFRONTEND_DIR=nextjs-frontend\nDOCKER_COMPOSE=docker compose\n\n# Help\n."
},
{
"path": "README.md",
"chars": 4432,
"preview": "## Next.js FastAPI Template\n\n<a href=\"https://www.vintasoftware.com/blog/next-js-fastapi-template\"><img src=\"docs/images"
},
{
"path": "docker-compose.yml",
"chars": 1698,
"preview": "services:\n backend:\n build:\n context: fastapi_backend\n environment:\n - OPENAPI_OUTPUT_FILE=./shared-dat"
},
{
"path": "docs/additional-settings.md",
"chars": 4322,
"preview": "### Production-Ready Authentication & Dashboard features\nThis template comes with a pre-configured authentication system"
},
{
"path": "docs/contributing.md",
"chars": 2257,
"preview": "# Contributing\n\nWe can always use your help to improve Next.js FastAPI Template! Please feel free to tackle existing [is"
},
{
"path": "docs/deployment.md",
"chars": 7059,
"preview": "### Overview\n\n Deploying to **Vercel** is supported, with dedicated buttons for the **Frontend** and **Backend** applica"
},
{
"path": "docs/get-started.md",
"chars": 4289,
"preview": "To use this template for your own project:\n\n1. Create a new repository using this template by following GitHub's [templa"
},
{
"path": "docs/stylesheets/extra.css",
"chars": 53,
"preview": ":root > * {\n --md-primary-fg-color: #004BC9;\n}\n\n\n\n\n"
},
{
"path": "docs/support.md",
"chars": 655,
"preview": "# Support\n\nIf you have any questions or need help, feel free to create a thread on [GitHub Discussions](https://github.c"
},
{
"path": "docs/technology-selection.md",
"chars": 2600,
"preview": "This template streamlines building APIs with [FastAPI](https://fastapi.tiangolo.com/) and dynamic frontends with [Next.j"
},
{
"path": "fastapi_backend/.gitignore",
"chars": 8,
"preview": ".vercel\n"
},
{
"path": "fastapi_backend/Dockerfile",
"chars": 818,
"preview": "FROM python:3.12-bookworm\n\n# Set the working directory\nWORKDIR /app\n\n# The uv installer requires curl (and certificates)"
},
{
"path": "fastapi_backend/alembic.ini",
"chars": 3736,
"preview": "# A generic, single database configuration.\n\n[alembic]\n# path to migration scripts.\n# Use forward slashes (/) also on wi"
},
{
"path": "fastapi_backend/alembic_migrations/README",
"chars": 58,
"preview": "Generic single-database configuration with an async dbapi."
},
{
"path": "fastapi_backend/alembic_migrations/env.py",
"chars": 3019,
"preview": "import asyncio\nimport os\nfrom urllib.parse import urlparse\n\nfrom logging.config import fileConfig\n\nfrom sqlalchemy impor"
},
{
"path": "fastapi_backend/alembic_migrations/script.py.mako",
"chars": 670,
"preview": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision | comma,n}\nCreate Date: ${create_date}\n\n\"\"\"\nfrom typ"
},
{
"path": "fastapi_backend/alembic_migrations/versions/402d067a8b92_added_user_table.py",
"chars": 1357,
"preview": "\"\"\"Added user table\n\nRevision ID: 402d067a8b92\nRevises:\nCreate Date: 2024-09-27 14:01:44.155160\n\n\"\"\"\n\nfrom typing import"
},
{
"path": "fastapi_backend/alembic_migrations/versions/b389592974f8_add_item_model.py",
"chars": 1169,
"preview": "\"\"\"Add item model\n\nRevision ID: b389592974f8\nRevises: 402d067a8b92\nCreate Date: 2024-12-06 17:52:50.698249\n\n\"\"\"\n\nfrom ty"
},
{
"path": "fastapi_backend/api/index.py",
"chars": 39,
"preview": "from app.main import app # noqa: F401\n"
},
{
"path": "fastapi_backend/app/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "fastapi_backend/app/config.py",
"chars": 1113,
"preview": "from typing import Set\n\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass Settings(BaseSettings):\n "
},
{
"path": "fastapi_backend/app/database.py",
"chars": 1252,
"preview": "from typing import AsyncGenerator\nfrom urllib.parse import urlparse\n\nfrom fastapi import Depends\nfrom fastapi_users.db i"
},
{
"path": "fastapi_backend/app/email.py",
"chars": 1370,
"preview": "from pathlib import Path\nimport urllib.parse\n\nfrom fastapi_mail import FastMail, MessageSchema, ConnectionConfig, Messag"
},
{
"path": "fastapi_backend/app/email_templates/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "fastapi_backend/app/email_templates/password_reset.html",
"chars": 345,
"preview": "<html>\n <body>\n <p>Hello {{ username }},</p>\n\n <p>We received a request to reset your password. You can reset you"
},
{
"path": "fastapi_backend/app/main.py",
"chars": 1463,
"preview": "from fastapi import FastAPI\nfrom fastapi_pagination import add_pagination\nfrom .schemas import UserCreate, UserRead, Use"
},
{
"path": "fastapi_backend/app/models.py",
"chars": 836,
"preview": "from fastapi_users.db import SQLAlchemyBaseUserTableUUID\nfrom sqlalchemy.orm import DeclarativeBase\nfrom sqlalchemy impo"
},
{
"path": "fastapi_backend/app/routes/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "fastapi_backend/app/routes/items.py",
"chars": 1898,
"preview": "from uuid import UUID\n\nfrom fastapi import APIRouter, Depends, HTTPException, Query\nfrom fastapi_pagination import Page,"
},
{
"path": "fastapi_backend/app/schemas.py",
"chars": 516,
"preview": "import uuid\n\nfrom fastapi_users import schemas\nfrom pydantic import BaseModel\nfrom uuid import UUID\n\n\nclass UserRead(sch"
},
{
"path": "fastapi_backend/app/users.py",
"chars": 2617,
"preview": "import uuid\nimport re\n\nfrom typing import Optional\n\nfrom fastapi import Depends, Request\nfrom fastapi_users import (\n "
},
{
"path": "fastapi_backend/app/utils.py",
"chars": 136,
"preview": "from fastapi.routing import APIRoute\n\n\ndef simple_generate_unique_route_id(route: APIRoute):\n return f\"{route.tags[0]"
},
{
"path": "fastapi_backend/commands/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "fastapi_backend/commands/generate_openapi_schema.py",
"chars": 1174,
"preview": "import json\nfrom pathlib import Path\nfrom app.main import app\nimport os\n\nfrom dotenv import load_dotenv\n\nload_dotenv()\n\n"
},
{
"path": "fastapi_backend/mypy.ini",
"chars": 114,
"preview": "[mypy]\npython_version = 3.12\nfiles = src/**/*.py\nfollow_imports = skip\nignore_missing_imports = True\nstrict = True"
},
{
"path": "fastapi_backend/pyproject.toml",
"chars": 1010,
"preview": "[project]\nname = \"app\"\nversion = \"0.0.6\"\ndescription = \"\"\nauthors = [{ name = \"Anderson Resende\", email = \"anderson@vint"
},
{
"path": "fastapi_backend/pytest.ini",
"chars": 28,
"preview": "[pytest]\nasyncio_mode = auto"
},
{
"path": "fastapi_backend/requirements.txt",
"chars": 37275,
"preview": "# This file was autogenerated by uv via the following command:\n# uv export\naiosmtplib==2.0.2 \\\n --hash=sha256:1385"
},
{
"path": "fastapi_backend/start.sh",
"chars": 309,
"preview": "#!/bin/bash\n\nif [ -f /.dockerenv ]; then\n echo \"Running in Docker\"\n fastapi dev app/main.py --host 0.0.0.0 --port "
},
{
"path": "fastapi_backend/tests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "fastapi_backend/tests/commands/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "fastapi_backend/tests/commands/files/openapi_test.json",
"chars": 27683,
"preview": "{\n \"openapi\": \"3.1.0\",\n \"info\": {\n \"title\": \"FastAPI\",\n \"version\": \"0.1.0\"\n },\n \"paths\": {\n \"/auth/jwt/logi"
},
{
"path": "fastapi_backend/tests/commands/files/openapi_test_output.json",
"chars": 27606,
"preview": "{\n \"openapi\": \"3.1.0\",\n \"info\": {\n \"title\": \"FastAPI\",\n \"version\": \"0.1.0\"\n },\n \"paths\": {\n \"/auth/jwt/logi"
},
{
"path": "fastapi_backend/tests/commands/test_generate_openapi_schema.py",
"chars": 1758,
"preview": "import json\nimport os\nimport pytest\nfrom pathlib import Path\n\nfrom commands.generate_openapi_schema import (\n generat"
},
{
"path": "fastapi_backend/tests/conftest.py",
"chars": 3206,
"preview": "from httpx import AsyncClient, ASGITransport\nimport pytest_asyncio\nfrom sqlalchemy.ext.asyncio import AsyncSession, asyn"
},
{
"path": "fastapi_backend/tests/main/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "fastapi_backend/tests/main/test_main.py",
"chars": 3601,
"preview": "import pytest\nfrom fastapi import status\nfrom fastapi_users.router import ErrorCode\nfrom sqlalchemy import select\nfrom a"
},
{
"path": "fastapi_backend/tests/routes/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "fastapi_backend/tests/routes/test_items.py",
"chars": 5340,
"preview": "import pytest\nfrom fastapi import status\nfrom sqlalchemy import select, insert\nfrom app.models import Item\n\n\nclass TestI"
},
{
"path": "fastapi_backend/tests/test_database.py",
"chars": 3186,
"preview": "import pytest\nfrom sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine\nfrom fastapi_users.db import SQLAlchemyUserDa"
},
{
"path": "fastapi_backend/tests/test_email.py",
"chars": 2880,
"preview": "import pytest\nfrom pathlib import Path\nfrom fastapi_mail import ConnectionConfig, MessageSchema\nfrom app.email import ge"
},
{
"path": "fastapi_backend/tests/utils/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "fastapi_backend/tests/utils/test_utils.py",
"chars": 372,
"preview": "from fastapi.routing import APIRoute\nfrom app.utils import simple_generate_unique_route_id\n\n\ndef test_simple_generate_un"
},
{
"path": "fastapi_backend/vercel.json",
"chars": 334,
"preview": "{\n \"buildCommand\": \"python3 -m venv venv && . venv/bin/activate && pip install -r requirements.txt && alembic upgrade h"
},
{
"path": "fastapi_backend/vercel.prod.json",
"chars": 84,
"preview": "{\n \"routes\": [\n {\n \"src\": \"/(.*)\",\n \"dest\": \"api/index.py\"\n }\n ]\n}"
},
{
"path": "fastapi_backend/watcher.py",
"chars": 2712,
"preview": "import time\nimport re\nimport subprocess\nimport os\nfrom watchdog.observers import Observer\nfrom watchdog.events import Fi"
},
{
"path": "local-shared-data/openapi.json",
"chars": 33920,
"preview": "{\n \"openapi\": \"3.1.0\",\n \"info\": {\n \"title\": \"FastAPI\",\n \"version\": \"0.1.0\"\n },\n \"paths\": {\n \"/auth/jwt/logi"
},
{
"path": "mkdocs.yml",
"chars": 2699,
"preview": "site_name: Next.js FastAPI Template\nsite_description: Kickstart scalable apps with our Next.js FastAPI Template. Include"
},
{
"path": "nextjs-frontend/.gitignore",
"chars": 402,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "nextjs-frontend/.prettierignore",
"chars": 12,
"preview": "openapi.json"
},
{
"path": "nextjs-frontend/Dockerfile",
"chars": 424,
"preview": "FROM node:20-bookworm\n\n# Set the working directory\nWORKDIR /app\n\n# Install pnpm globally\nRUN npm install -g pnpm\n\n# Copy"
},
{
"path": "nextjs-frontend/__tests__/login.test.tsx",
"chars": 2921,
"preview": "import { login } from \"@/components/actions/login-action\";\nimport { authJwtLogin } from \"@/app/clientService\";\nimport { "
},
{
"path": "nextjs-frontend/__tests__/loginPage.test.tsx",
"chars": 3034,
"preview": "import { render, screen, fireEvent, waitFor } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\n\nimport"
},
{
"path": "nextjs-frontend/__tests__/passwordReset.test.tsx",
"chars": 2091,
"preview": "import { passwordReset } from \"@/components/actions/password-reset-action\";\nimport { resetForgotPassword } from \"@/app/c"
},
{
"path": "nextjs-frontend/__tests__/passwordResetConfirm.test.tsx",
"chars": 3003,
"preview": "import { passwordResetConfirm } from \"@/components/actions/password-reset-action\";\nimport { resetResetPassword } from \"@"
},
{
"path": "nextjs-frontend/__tests__/passwordResetConfirmPage.test.tsx",
"chars": 3990,
"preview": "import { render, screen, fireEvent, waitFor } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\n\nimport"
},
{
"path": "nextjs-frontend/__tests__/passwordResetPage.test.tsx",
"chars": 2282,
"preview": "import { render, screen, fireEvent, waitFor } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\n\nimport"
},
{
"path": "nextjs-frontend/__tests__/register.test.ts",
"chars": 2695,
"preview": "import { register } from \"@/components/actions/register-action\";\nimport { redirect } from \"next/navigation\";\nimport { re"
},
{
"path": "nextjs-frontend/__tests__/registerPage.test.tsx",
"chars": 4680,
"preview": "import { render, screen, fireEvent, waitFor } from \"@testing-library/react\";\nimport \"@testing-library/jest-dom\";\n\nimport"
},
{
"path": "nextjs-frontend/app/clientService.ts",
"chars": 64,
"preview": "export * from \"./openapi-client\";\n\nimport \"@/lib/clientConfig\";\n"
},
{
"path": "nextjs-frontend/app/dashboard/add-item/page.tsx",
"chars": 3299,
"preview": "\"use client\";\n\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { ad"
},
{
"path": "nextjs-frontend/app/dashboard/deleteButton.tsx",
"chars": 490,
"preview": "\"use client\";\n\nimport { removeItem } from \"@/components/actions/items-action\";\nimport { DropdownMenuItem } from \"@/compo"
},
{
"path": "nextjs-frontend/app/dashboard/layout.tsx",
"chars": 3829,
"preview": "import Link from \"next/link\";\nimport { Home, Users2, List } from \"lucide-react\";\nimport Image from \"next/image\";\n\nimport"
},
{
"path": "nextjs-frontend/app/dashboard/page.tsx",
"chars": 3734,
"preview": "import {\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableRow,\n TableHeader,\n} from \"@/components/ui/table\";\nimpo"
},
{
"path": "nextjs-frontend/app/globals.css",
"chars": 1657,
"preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n@layer base {\n :root {\n --background: 0 0% 100%;\n --fo"
},
{
"path": "nextjs-frontend/app/layout.tsx",
"chars": 715,
"preview": "import type { Metadata } from \"next\";\nimport localFont from \"next/font/local\";\nimport \"./globals.css\";\n\nconst geistSans "
},
{
"path": "nextjs-frontend/app/login/page.tsx",
"chars": 3178,
"preview": "\"use client\";\n\nimport Link from \"next/link\";\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitl"
},
{
"path": "nextjs-frontend/app/openapi-client/client/client.gen.ts",
"chars": 4477,
"preview": "// This file is auto-generated by @hey-api/openapi-ts\n\nimport type { AxiosError, AxiosInstance, RawAxiosRequestHeaders }"
},
{
"path": "nextjs-frontend/app/openapi-client/client/index.ts",
"chars": 625,
"preview": "// This file is auto-generated by @hey-api/openapi-ts\n\nexport type { Auth } from \"../core/auth.gen\";\nexport type { Query"
},
{
"path": "nextjs-frontend/app/openapi-client/client/types.gen.ts",
"chars": 5629,
"preview": "// This file is auto-generated by @hey-api/openapi-ts\n\nimport type {\n AxiosError,\n AxiosInstance,\n AxiosRequestHeader"
},
{
"path": "nextjs-frontend/app/openapi-client/client/utils.gen.ts",
"chars": 5682,
"preview": "// This file is auto-generated by @hey-api/openapi-ts\n\nimport { getAuthToken } from \"../core/auth.gen\";\nimport type { Qu"
},
{
"path": "nextjs-frontend/app/openapi-client/client.gen.ts",
"chars": 840,
"preview": "// This file is auto-generated by @hey-api/openapi-ts\n\nimport type { ClientOptions } from \"./types.gen\";\nimport {\n type"
},
{
"path": "nextjs-frontend/app/openapi-client/core/auth.gen.ts",
"chars": 864,
"preview": "// This file is auto-generated by @hey-api/openapi-ts\n\nexport type AuthToken = string | undefined;\n\nexport interface Aut"
},
{
"path": "nextjs-frontend/app/openapi-client/core/bodySerializer.gen.ts",
"chars": 2271,
"preview": "// This file is auto-generated by @hey-api/openapi-ts\n\nimport type {\n ArrayStyle,\n ObjectStyle,\n SerializerOptions,\n}"
},
{
"path": "nextjs-frontend/app/openapi-client/core/params.gen.ts",
"chars": 3405,
"preview": "// This file is auto-generated by @hey-api/openapi-ts\n\ntype Slot = \"body\" | \"headers\" | \"path\" | \"query\";\n\nexport type F"
},
{
"path": "nextjs-frontend/app/openapi-client/core/pathSerializer.gen.ts",
"chars": 4259,
"preview": "// This file is auto-generated by @hey-api/openapi-ts\n\ninterface SerializeOptions<T>\n extends SerializePrimitiveOptions"
},
{
"path": "nextjs-frontend/app/openapi-client/core/serverSentEvents.gen.ts",
"chars": 7331,
"preview": "// This file is auto-generated by @hey-api/openapi-ts\n\nimport type { Config } from \"./types.gen\";\n\nexport type ServerSen"
},
{
"path": "nextjs-frontend/app/openapi-client/core/types.gen.ts",
"chars": 3359,
"preview": "// This file is auto-generated by @hey-api/openapi-ts\n\nimport type { Auth, AuthToken } from \"./auth.gen\";\nimport type {\n"
},
{
"path": "nextjs-frontend/app/openapi-client/core/utils.gen.ts",
"chars": 3414,
"preview": "// This file is auto-generated by @hey-api/openapi-ts\n\nimport type { BodySerializer, QuerySerializer } from \"./bodySeria"
},
{
"path": "nextjs-frontend/app/openapi-client/index.ts",
"chars": 111,
"preview": "// This file is auto-generated by @hey-api/openapi-ts\n\nexport * from \"./types.gen\";\nexport * from \"./sdk.gen\";\n"
},
{
"path": "nextjs-frontend/app/openapi-client/sdk.gen.ts",
"chars": 8916,
"preview": "// This file is auto-generated by @hey-api/openapi-ts\n\nimport {\n type Options as ClientOptions,\n type Client,\n type T"
},
{
"path": "nextjs-frontend/app/openapi-client/types.gen.ts",
"chars": 11464,
"preview": "// This file is auto-generated by @hey-api/openapi-ts\n\n/**\n * BearerResponse\n */\nexport type BearerResponse = {\n /**\n "
},
{
"path": "nextjs-frontend/app/page.tsx",
"chars": 1672,
"preview": "import { Button } from \"@/components/ui/button\";\nimport Link from \"next/link\";\nimport { FaGithub } from \"react-icons/fa\""
},
{
"path": "nextjs-frontend/app/password-recovery/confirm/page.tsx",
"chars": 2283,
"preview": "\"use client\";\n\nimport { useActionState } from \"react\";\nimport { notFound, useSearchParams } from \"next/navigation\";\nimpo"
},
{
"path": "nextjs-frontend/app/password-recovery/page.tsx",
"chars": 2449,
"preview": "\"use client\";\n\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/car"
},
{
"path": "nextjs-frontend/app/register/page.tsx",
"chars": 2867,
"preview": "\"use client\";\n\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/car"
},
{
"path": "nextjs-frontend/components/actions/items-action.ts",
"chars": 2046,
"preview": "\"use server\";\n\nimport { cookies } from \"next/headers\";\nimport { readItem, deleteItem, createItem } from \"@/app/clientSer"
},
{
"path": "nextjs-frontend/components/actions/login-action.ts",
"chars": 1125,
"preview": "\"use server\";\n\nimport { cookies } from \"next/headers\";\n\nimport { authJwtLogin } from \"@/app/clientService\";\nimport { red"
},
{
"path": "nextjs-frontend/components/actions/logout-action.ts",
"chars": 574,
"preview": "\"use server\";\n\nimport { cookies } from \"next/headers\";\nimport { authJwtLogout } from \"@/app/clientService\";\nimport { red"
},
{
"path": "nextjs-frontend/components/actions/password-reset-action.ts",
"chars": 1743,
"preview": "\"use server\";\n\nimport { resetForgotPassword, resetResetPassword } from \"@/app/clientService\";\nimport { redirect } from \""
},
{
"path": "nextjs-frontend/components/actions/register-action.ts",
"chars": 1026,
"preview": "\"use server\";\n\nimport { redirect } from \"next/navigation\";\n\nimport { registerRegister } from \"@/app/clientService\";\n\nimp"
},
{
"path": "nextjs-frontend/components/page-pagination.tsx",
"chars": 2670,
"preview": "import { Button } from \"@/components/ui/button\";\nimport Link from \"next/link\";\nimport {\n ChevronLeftIcon,\n ChevronRigh"
},
{
"path": "nextjs-frontend/components/page-size-selector.tsx",
"chars": 1056,
"preview": "\"use client\";\n\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui"
},
{
"path": "nextjs-frontend/components/ui/FormError.tsx",
"chars": 1102,
"preview": "interface ErrorState {\n errors?: {\n [key: string]: string | string[];\n };\n server_validation_error?: string;\n ser"
},
{
"path": "nextjs-frontend/components/ui/avatar.tsx",
"chars": 1432,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\";\n\nimport { cn }"
},
{
"path": "nextjs-frontend/components/ui/badge.tsx",
"chars": 1147,
"preview": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\n\nimport { cn } from \""
},
{
"path": "nextjs-frontend/components/ui/breadcrumb.tsx",
"chars": 2760,
"preview": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cn } from \"@/lib/utils\";\nimport { "
},
{
"path": "nextjs-frontend/components/ui/button.tsx",
"chars": 1849,
"preview": "import * as React from \"react\";\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"cla"
},
{
"path": "nextjs-frontend/components/ui/card.tsx",
"chars": 1876,
"preview": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Card = React.forwardRef<\n HTMLDivElement,\n R"
},
{
"path": "nextjs-frontend/components/ui/dropdown-menu.tsx",
"chars": 7502,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\";\ni"
},
{
"path": "nextjs-frontend/components/ui/form.tsx",
"chars": 4166,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { Slot } "
},
{
"path": "nextjs-frontend/components/ui/input.tsx",
"chars": 830,
"preview": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport interface InputProps\n extends React.InputHTM"
},
{
"path": "nextjs-frontend/components/ui/label.tsx",
"chars": 734,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as LabelPrimitive from \"@radix-ui/react-label\";\nimport { cva, ty"
},
{
"path": "nextjs-frontend/components/ui/select.tsx",
"chars": 5817,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as SelectPrimitive from \"@radix-ui/react-select\";\nimport { cn } "
},
{
"path": "nextjs-frontend/components/ui/submitButton.tsx",
"chars": 327,
"preview": "import { useFormStatus } from \"react-dom\";\nimport { Button } from \"@/components/ui/button\";\n\nexport function SubmitButto"
},
{
"path": "nextjs-frontend/components/ui/table.tsx",
"chars": 2882,
"preview": "import * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst Table = React.forwardRef<\n HTMLTableElement,\n"
},
{
"path": "nextjs-frontend/components/ui/tabs.tsx",
"chars": 1906,
"preview": "\"use client\";\n\nimport * as React from \"react\";\nimport * as TabsPrimitive from \"@radix-ui/react-tabs\";\n\nimport { cn } fro"
},
{
"path": "nextjs-frontend/components.json",
"chars": 418,
"preview": "{\n \"$schema\": \"https://ui.shadcn.com/schema.json\",\n \"style\": \"new-york\",\n \"rsc\": true,\n \"tsx\": true,\n \"tailwind\": {"
},
{
"path": "nextjs-frontend/eslint.config.mjs",
"chars": 909,
"preview": "import js from \"@eslint/js\";\nimport nextPlugin from \"@next/eslint-plugin-next\";\nimport tseslint from \"typescript-eslint\""
},
{
"path": "nextjs-frontend/jest.config.ts",
"chars": 6765,
"preview": "/**\n * For a detailed explanation regarding each configuration property, visit:\n * https://jestjs.io/docs/configuration\n"
},
{
"path": "nextjs-frontend/next.config.mjs",
"chars": 590,
"preview": "import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';\n\n/** @type {import('next').NextConfig} */\nconst"
},
{
"path": "nextjs-frontend/openapi-ts.config.ts",
"chars": 370,
"preview": "import { defineConfig } from \"@hey-api/openapi-ts\";\nimport { config } from \"dotenv\";\n\nconfig({ path: \".env.local\" });\n\nc"
},
{
"path": "nextjs-frontend/openapi.json",
"chars": 33920,
"preview": "{\n \"openapi\": \"3.1.0\",\n \"info\": {\n \"title\": \"FastAPI\",\n \"version\": \"0.1.0\"\n },\n \"paths\": {\n \"/auth/jwt/logi"
},
{
"path": "nextjs-frontend/package.json",
"chars": 2460,
"preview": "{\n \"name\": \"nextjs-frontend\",\n \"version\": \"0.0.8\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev --webpack\",\n"
},
{
"path": "nextjs-frontend/pnpm-workspace.yaml",
"chars": 51,
"preview": "packages:\n - \".\"\nonlyBuiltDependencies:\n - sharp\n"
},
{
"path": "nextjs-frontend/postcss.config.js",
"chars": 83,
"preview": "module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n};\n"
},
{
"path": "nextjs-frontend/proxy.ts",
"chars": 668,
"preview": "import { NextResponse } from \"next/server\";\nimport type { NextRequest } from \"next/server\";\nimport { usersCurrentUser } "
},
{
"path": "nextjs-frontend/start.sh",
"chars": 50,
"preview": "#!/bin/bash\n\npnpm run dev &\n\nnode watcher.js\n\nwait"
},
{
"path": "nextjs-frontend/tailwind.config.js",
"chars": 1918,
"preview": "/** @type {import('tailwindcss').Config} */\n\n/* eslint-disable @typescript-eslint/no-require-imports */\nmodule.exports ="
},
{
"path": "nextjs-frontend/tsconfig.json",
"chars": 650,
"preview": "{\n \"compilerOptions\": {\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n \"skipLibCheck\": true,\n "
},
{
"path": "nextjs-frontend/vercel.json",
"chars": 72,
"preview": "{\n \"git\": {\n \"deploymentEnabled\": {\n \"main\": false\n }\n }\n}\n"
},
{
"path": "nextjs-frontend/watcher.js",
"chars": 711,
"preview": "/* eslint-disable @typescript-eslint/no-require-imports */\nconst chokidar = require(\"chokidar\");\nconst { exec } = requir"
},
{
"path": "overrides/main.html",
"chars": 1115,
"preview": "{% extends \"base.html\" %}\n\n{% block extrahead %}\n {% set title = config.site_name %}\n {% if page and page.meta and pag"
},
{
"path": "prod-backend-deploy.yml",
"chars": 1638,
"preview": "name: Vercel Production Backend Deployment\nenv:\n VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}\n VERCEL_PROJECT_ID: ${{ s"
},
{
"path": "prod-frontend-deploy.yml",
"chars": 1093,
"preview": "name: Vercel Production Frontend Deployment\nenv:\n VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}\n VERCEL_PROJECT_ID: ${{ "
}
]
About this extraction
This page contains the full source code of the vintasoftware/nextjs-fastapi-template GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 144 files (425.3 KB), approximately 114.7k tokens, and a symbol index with 245 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.