Modify backend
This commit is contained in:
parent
b6c15809f9
commit
50ba9b340b
85 changed files with 4848 additions and 50 deletions
1
.python-version
Normal file
1
.python-version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
3.13
|
||||||
30
backend/.env
Normal file
30
backend/.env
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# Domain
|
||||||
|
# This would be set to the production domain with an env var on deployment
|
||||||
|
DOMAIN=localhost
|
||||||
|
|
||||||
|
# FRONTEND_HOST=http://localhost:5173
|
||||||
|
|
||||||
|
# Environment: local, staging, production
|
||||||
|
ENVIRONMENT=local
|
||||||
|
|
||||||
|
PROJECT_NAME="FastAPI Supabase Template"
|
||||||
|
|
||||||
|
# Backend
|
||||||
|
BACKEND_CORS_ORIGINS="http://localhost"
|
||||||
|
SECRET_KEY=local_dev
|
||||||
|
FIRST_SUPERUSER=admin@example.com
|
||||||
|
FIRST_SUPERUSER_PASSWORD=admin12345
|
||||||
|
|
||||||
|
# run `supabase status`
|
||||||
|
# API URL
|
||||||
|
SUPABASE_URL=http://127.0.0.1:54321
|
||||||
|
# service_role key
|
||||||
|
SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
|
||||||
|
|
||||||
|
# Postgres
|
||||||
|
# DB URL: postgresql://postgres:postgres@localhost:54322/postgres
|
||||||
|
POSTGRES_SERVER=localhost
|
||||||
|
POSTGRES_PORT=54322
|
||||||
|
POSTGRES_DB=postgres
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=postgres
|
||||||
1
backend/.github/CODEOWNERS
vendored
Normal file
1
backend/.github/CODEOWNERS
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
* @Atticuszz
|
||||||
41
backend/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
41
backend/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
backend/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
backend/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
16
backend/.github/dependabot.yml
vendored
Normal file
16
backend/.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# GitHub Actions
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
commit-message:
|
||||||
|
prefix: ⬆
|
||||||
|
# Python
|
||||||
|
- package-ecosystem: "pip"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
commit-message:
|
||||||
|
prefix: ⬆
|
||||||
74
backend/.github/workflows/main.yml
vendored
Normal file
74
backend/.github/workflows/main.yml
vendored
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
name: Test And Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Run Tests / OS ${{ matrix.os }} / Python ${{ matrix.python-version }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ ubuntu-latest ]
|
||||||
|
python-version: ["3.11", "3.12", "3.13"]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v5
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
- name: Install brew
|
||||||
|
run: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||||
|
|
||||||
|
- name: Install Supabase
|
||||||
|
run: |
|
||||||
|
echo >> /home/runner/.bashrc
|
||||||
|
echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> /home/runner/.bashrc
|
||||||
|
eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
|
||||||
|
HOMEBREW_NO_INSTALL_CLEANUP=1
|
||||||
|
brew install supabase/tap/supabase
|
||||||
|
bash scripts/update-env.sh
|
||||||
|
uv python install ${{ matrix.python-version }}
|
||||||
|
cd backend
|
||||||
|
uv sync --python ${{ matrix.python-version }} --all-extras --dev
|
||||||
|
uv run bash scripts/pre-start.sh
|
||||||
|
uv run bash scripts/tests-start.sh
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Bump Version and Release
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v5
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: uv sync --dev
|
||||||
|
|
||||||
|
- name: Generate a changelog
|
||||||
|
env:
|
||||||
|
ATTICUS_PAT: ${{ secrets.ATTICUS_PAT }}
|
||||||
|
run: uv run git-cliff -vv --latest --strip header --github-token "$ATTICUS_PAT" -o CHANGES.md
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
body_path: CHANGES.md
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Reference
|
||||||
|
# 1. https://docs.astral.sh/uv/guides/integration/github/#syncing-and-running
|
||||||
|
# 2. https://github.com/Kludex/python-template/blob/main/.github/workflows/main.yml
|
||||||
|
# 3. https://github.com/softprops/action-gh-release/tree/master/
|
||||||
167
backend/.gitignore
vendored
Normal file
167
backend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.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/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# UV
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# 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/
|
||||||
|
|
||||||
|
# VSCode
|
||||||
|
.vscode/PythonImportHelper-v2-Completion.json
|
||||||
|
.vscode/settings.json
|
||||||
60
backend/.pre-commit-config.yaml
Normal file
60
backend/.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
# https://pre-commit.com/
|
||||||
|
# `pre-commit install` to set up the git hook scripts
|
||||||
|
# `pre-commit autoupdate` to update repos
|
||||||
|
# `pre-commit run --all-files` run hooks for all file
|
||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v5.0.0
|
||||||
|
hooks:
|
||||||
|
- id: check-toml
|
||||||
|
- id: check-yaml
|
||||||
|
- id: sort-simple-yaml
|
||||||
|
- id: check-json
|
||||||
|
- id: pretty-format-json
|
||||||
|
args: [--autofix, --no-sort-keys ]
|
||||||
|
- id: check-added-large-files
|
||||||
|
args: [--maxkb=51200]
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: detect-private-key
|
||||||
|
|
||||||
|
- repo: https://github.com/codespell-project/codespell
|
||||||
|
rev: v2.4.1
|
||||||
|
hooks:
|
||||||
|
- id: codespell
|
||||||
|
args: [--write-changes]
|
||||||
|
|
||||||
|
files: \.(py|sh|json|yml|yaml|md)$
|
||||||
|
|
||||||
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
rev: v0.9.9
|
||||||
|
|
||||||
|
hooks:
|
||||||
|
- id: ruff
|
||||||
|
args: [--fix]
|
||||||
|
- id: ruff-format
|
||||||
|
|
||||||
|
- repo: https://github.com/hadolint/hadolint
|
||||||
|
rev: v2.13.1-beta
|
||||||
|
hooks:
|
||||||
|
- id: hadolint
|
||||||
|
name: Lint Dockerfiles
|
||||||
|
description: Runs hadolint to lint Dockerfiles
|
||||||
|
entry: hadolint
|
||||||
|
language: system
|
||||||
|
types: ["dockerfile"]
|
||||||
|
|
||||||
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
||||||
|
# uv version.
|
||||||
|
rev: 0.6.3
|
||||||
|
|
||||||
|
hooks:
|
||||||
|
# Update the uv lockfile
|
||||||
|
- id: uv-lock
|
||||||
|
|
||||||
|
ci:
|
||||||
|
autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks
|
||||||
|
# Settings for the https://pre-commit.ci/ continuous integration service
|
||||||
|
autofix_prs: true
|
||||||
|
autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate
|
||||||
|
autoupdate_schedule: monthly
|
||||||
1
backend/.python-version
Normal file
1
backend/.python-version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
3.13
|
||||||
138
backend/CHANGELOG.md
Normal file
138
backend/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## unreleased
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Lint error
|
||||||
|
|
||||||
|
### 🚜 Refactor
|
||||||
|
|
||||||
|
- Move src/app to top level of dir as app/
|
||||||
|
|
||||||
|
### 📚 Documentation
|
||||||
|
|
||||||
|
- Add docs dir for mkdocs
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Vscode import
|
||||||
|
- Remove semantic-release and add mkdocs build
|
||||||
|
- *(git)* Pre-commit upgrade to ruff
|
||||||
|
- Add scripts for test and lint etc.
|
||||||
|
|
||||||
|
## 0.4.1 - 2024-09-18
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Remove supabase-py-async with supabase-py
|
||||||
|
- Merge pull request #139 from Atticuszz/work-with-supabase-py in #139
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Update README.md
|
||||||
|
- Remove ci
|
||||||
|
|
||||||
|
## 0.4.0 - 2024-08-30
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Use uv to manage venvs
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Remove ci
|
||||||
|
- Ci docker push
|
||||||
|
- Ci
|
||||||
|
|
||||||
|
## 0.3.2 - 2024-07-30
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Update deps
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Update README.md
|
||||||
|
|
||||||
|
## 0.3.1 - 2024-01-15
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Bump version to 0.3.1
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Delete .idea directory
|
||||||
|
- Update README.md
|
||||||
|
|
||||||
|
## 0.3.0 - 2024-01-13
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Add Dockerfile
|
||||||
|
- Add Dockerfile and image push ci
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Update README.md
|
||||||
|
- RUN pre-commit-hooks
|
||||||
|
- Add Dockerfile
|
||||||
|
|
||||||
|
## 0.2.1 - 2024-01-13
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Update README.md
|
||||||
|
- Add latest_changes.yml in #22
|
||||||
|
- Update latest_changes.yml
|
||||||
|
- Update ci.yml
|
||||||
|
|
||||||
|
## 0.2.0 - 2024-01-13
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Release in #21
|
||||||
|
|
||||||
|
## 0.1.0 - 2024-01-13
|
||||||
|
|
||||||
|
### 🚀 Features
|
||||||
|
|
||||||
|
- Update ci and README.md
|
||||||
|
- Release in #20
|
||||||
|
|
||||||
|
### 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- Add latest_changes.yml
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Update ci
|
||||||
|
|
||||||
|
## 0.0.2 - 2024-01-13
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Remove latest-changes: ci
|
||||||
|
- Set pro-commit-hooks autofix_prs -> True
|
||||||
|
- Update ci
|
||||||
|
- Update README.md
|
||||||
|
|
||||||
|
### Bugs
|
||||||
|
|
||||||
|
- Failed to auth as dep on new user by access token
|
||||||
|
|
||||||
|
### Upgrade
|
||||||
|
|
||||||
|
- Release 0.1.0
|
||||||
|
|
||||||
|
## 0.0.1 - 2024-01-11
|
||||||
|
|
||||||
|
### ⚙️ Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Update ci,add changelog as pushed ,Publish to GitHub Releases as test passed and merged from PR
|
||||||
|
- Update ci
|
||||||
|
|
||||||
|
<!-- generated by git-cliff -->
|
||||||
21
backend/LICENSE
Normal file
21
backend/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Atticus Zeller
|
||||||
|
|
||||||
|
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.
|
||||||
71
backend/README.md
Normal file
71
backend/README.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# FastAPI Supbase Template
|
||||||
|
|
||||||
|
## Environment
|
||||||
|
|
||||||
|
### Python
|
||||||
|
|
||||||
|
> [uv](https://github.com/astral-sh/uv) is an extremely fast Python package and project manager, written in Rust.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
uv sync --all-groups --dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### [Supabase](https://supabase.com/docs/guides/local-development/cli/getting-started?queryGroups=platform&platform=linux&queryGroups=access-method&access-method=postgres)
|
||||||
|
|
||||||
|
install supabase-cli
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# brew in linux https://brew.sh/
|
||||||
|
brew install supabase/tap/supabase
|
||||||
|
```
|
||||||
|
|
||||||
|
launch supabase docker containers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# under repo root
|
||||||
|
supabase start
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
>```bash
|
||||||
|
># Update `.env`
|
||||||
|
>bash scripts/update-env.sh
|
||||||
|
>```
|
||||||
|
> modify the `.env` from the output of `supabase start` or run `supabase status` manually.
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
# test connection of db and migration
|
||||||
|
scripts/pre-start.sh
|
||||||
|
# unit test
|
||||||
|
scripts/test.sh
|
||||||
|
# test connection of db and test code
|
||||||
|
scripts/tests-start.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
> [!note]
|
||||||
|
> `atticux/fastapi_supabase_template` is your image tag name, remember replace it with yours
|
||||||
|
|
||||||
|
build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
docker build -t atticux/fastapi_supabase_template .
|
||||||
|
```
|
||||||
|
|
||||||
|
test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/update-env.sh
|
||||||
|
supabase start
|
||||||
|
cd backend
|
||||||
|
docker run --network host \
|
||||||
|
--env-file ../.env \
|
||||||
|
-it atticux/fastapi_supabase_template:latest \
|
||||||
|
bash -c "sh scripts/pre-start.sh && sh scripts/tests-start.sh"
|
||||||
|
```
|
||||||
184
backend/backend/.dockerignore
Normal file
184
backend/backend/.dockerignore
Normal file
|
|
@ -0,0 +1,184 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
**/compose.yml
|
||||||
|
**/docker-compose.yml
|
||||||
|
**/*Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
**/.gitignore
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.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/
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode
|
||||||
|
.devcontainer
|
||||||
|
|
||||||
|
# Custom
|
||||||
|
.pre-commit-config.yaml
|
||||||
|
assets
|
||||||
|
docs
|
||||||
|
CHANGELOG.md
|
||||||
|
mkdocs.yml
|
||||||
54
backend/backend/Dockerfile
Normal file
54
backend/backend/Dockerfile
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Ref: https://github.com/fastapi/full-stack-fastapi-template/blob/master/backend/Dockerfile
|
||||||
|
FROM python:3.12-slim-bookworm
|
||||||
|
|
||||||
|
# Print logs immediately
|
||||||
|
# Ref: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONUNBUFFERED
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Install system dependencies including PostgreSQL client library
|
||||||
|
# hadolint ignore=DL3008
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends\
|
||||||
|
libpq-dev \
|
||||||
|
gcc \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Change the working directory to the `app` directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install uv
|
||||||
|
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:0.5.18 /uv /uvx /bin/
|
||||||
|
|
||||||
|
# Place executables in the environment at the front of the path
|
||||||
|
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#using-the-environment
|
||||||
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
|
|
||||||
|
# Compile bytecode to speed up the startup time
|
||||||
|
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#compiling-bytecode
|
||||||
|
ENV UV_COMPILE_BYTECODE=1
|
||||||
|
|
||||||
|
# uv Cache
|
||||||
|
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#caching
|
||||||
|
ENV UV_LINK_MODE=copy
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
|
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||||
|
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||||
|
uv sync --frozen --no-install-project
|
||||||
|
|
||||||
|
# Copy the project into the image
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Sync the project
|
||||||
|
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
|
||||||
|
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||||
|
uv sync
|
||||||
|
|
||||||
|
# Set the default command
|
||||||
|
# Ref: https://fastapi.tiangolo.com/deployment/docker/
|
||||||
|
CMD ["fastapi", "run", "app/main.py", "--port", "80"]
|
||||||
|
|
||||||
|
# If running behind a proxy like Nginx or Traefik add --proxy-headers
|
||||||
|
# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"]
|
||||||
115
backend/backend/alembic.ini
Normal file
115
backend/backend/alembic.ini
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
# A generic, single database configuration.
|
||||||
|
|
||||||
|
[alembic]
|
||||||
|
# path to migration scripts
|
||||||
|
# Use forward slashes (/) also on windows to provide an os agnostic path
|
||||||
|
script_location = app/alembic
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
||||||
|
# for all available tokens
|
||||||
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
[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 = WARNING
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARNING
|
||||||
|
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
|
||||||
0
backend/backend/app/__init__.py
Normal file
0
backend/backend/app/__init__.py
Normal file
1
backend/backend/app/alembic/README
Normal file
1
backend/backend/app/alembic/README
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Generic single-database configuration.
|
||||||
91
backend/backend/app/alembic/env.py
Normal file
91
backend/backend/app/alembic/env.py
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
from logging.config import fileConfig
|
||||||
|
|
||||||
|
from alembic import context
|
||||||
|
from sqlalchemy import engine_from_config, pool
|
||||||
|
from sqlmodel import SQLModel
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.models import * # noqa: F403
|
||||||
|
|
||||||
|
# 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 = SQLModel.metadata
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
|
||||||
|
def get_url() -> str:
|
||||||
|
url = str(settings.SQLALCHEMY_DATABASE_URI)
|
||||||
|
return url.replace("postgresql+asyncpg://", "postgresql://")
|
||||||
|
|
||||||
|
|
||||||
|
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 = get_url()
|
||||||
|
context.configure(
|
||||||
|
url=url,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
compare_type=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
def run_migrations_online() -> None:
|
||||||
|
"""Run migrations in 'online' mode.
|
||||||
|
|
||||||
|
In this scenario we need to create an Engine
|
||||||
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
"""
|
||||||
|
configuration = config.get_section(config.config_ini_section)
|
||||||
|
if configuration is None:
|
||||||
|
raise FileNotFoundError("alembic config is None!")
|
||||||
|
configuration["sqlalchemy.url"] = get_url()
|
||||||
|
connectable = engine_from_config(
|
||||||
|
configuration, prefix="sqlalchemy.", poolclass=pool.NullPool
|
||||||
|
)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
compare_type=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
||||||
26
backend/backend/app/alembic/script.py.mako
Normal file
26
backend/backend/app/alembic/script.py.mako
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
"""${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
|
||||||
|
${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"}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
"""initial commit
|
||||||
|
|
||||||
|
Revision ID: 2c0516590c18
|
||||||
|
Revises:
|
||||||
|
Create Date: 2024-11-11 13:59:36.474238
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlmodel
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '2c0516590c18'
|
||||||
|
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('users',
|
||||||
|
# sa.Column('id', sa.Uuid(), nullable=False),
|
||||||
|
# sa.Column('email', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False),
|
||||||
|
# sa.PrimaryKeyConstraint('id'),
|
||||||
|
# schema='auth'
|
||||||
|
# )
|
||||||
|
op.create_table('item',
|
||||||
|
sa.Column('title', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=False),
|
||||||
|
sa.Column('description', sqlmodel.sql.sqltypes.AutoString(length=255), nullable=True),
|
||||||
|
sa.Column('id', sa.Uuid(), nullable=False),
|
||||||
|
sa.Column('owner_id', sa.Uuid(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['owner_id'], ['auth.users.id'], ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_table('item')
|
||||||
|
# op.drop_table('users', schema='auth')
|
||||||
|
# ### end Alembic commands ###
|
||||||
0
backend/backend/app/api/__init__.py
Normal file
0
backend/backend/app/api/__init__.py
Normal file
13
backend/backend/app/api/deps.py
Normal file
13
backend/backend/app/api/deps.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
from app.core.auth import get_current_user
|
||||||
|
from app.core.db import get_db
|
||||||
|
from app.schemas.auth import UserIn
|
||||||
|
|
||||||
|
CurrentUser = Annotated[UserIn, Depends(get_current_user)]
|
||||||
|
|
||||||
|
|
||||||
|
SessionDep = Annotated[Session, Depends(get_db)]
|
||||||
7
backend/backend/app/api/main.py
Normal file
7
backend/backend/app/api/main.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app.api.routes import items, utils
|
||||||
|
|
||||||
|
api_router = APIRouter()
|
||||||
|
api_router.include_router(items.router)
|
||||||
|
api_router.include_router(utils.router)
|
||||||
0
backend/backend/app/api/routes/__init__.py
Normal file
0
backend/backend/app/api/routes/__init__.py
Normal file
38
backend/backend/app/api/routes/items.py
Normal file
38
backend/backend/app/api/routes/items.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app.api.deps import CurrentUser, SessionDep
|
||||||
|
from app.crud import item
|
||||||
|
from app.models.item import Item, ItemCreate, ItemUpdate
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/items", tags=["items"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/create-item")
|
||||||
|
async def create_item(
|
||||||
|
item_in: ItemCreate, user: CurrentUser, session: SessionDep
|
||||||
|
) -> Item:
|
||||||
|
return item.create(session, owner_id=UUID(user.id), obj_in=item_in)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/get-item/{id}")
|
||||||
|
async def read_item_by_id(id: str, session: SessionDep) -> Item | None:
|
||||||
|
return item.get(session, id=UUID(id))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/get-items")
|
||||||
|
async def read_items(
|
||||||
|
session: SessionDep, skip: int = 0, limit: int = 100
|
||||||
|
) -> list[Item]:
|
||||||
|
return list(item.get_multi(session, skip=skip, limit=limit))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/update-item/{id}")
|
||||||
|
async def update_item(id: str, item_in: ItemUpdate, session: SessionDep) -> Item | None:
|
||||||
|
return item.update(session, id=UUID(id), obj_in=item_in)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/delete/{id}")
|
||||||
|
async def delete_item(id: str, session: SessionDep) -> Item | None:
|
||||||
|
return item.remove(session, id=UUID(id))
|
||||||
8
backend/backend/app/api/routes/utils.py
Normal file
8
backend/backend/app/api/routes/utils.py
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/utils", tags=["utils"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health-check/")
|
||||||
|
async def health_check() -> bool:
|
||||||
|
return True
|
||||||
0
backend/backend/app/core/__init__.py
Normal file
0
backend/backend/app/core/__init__.py
Normal file
43
backend/backend/app/core/auth.py
Normal file
43
backend/backend/app/core/auth.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import logging
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import Depends, HTTPException
|
||||||
|
from fastapi.security import OAuth2PasswordBearer
|
||||||
|
from supabase import AsyncClientOptions
|
||||||
|
from supabase._async.client import AsyncClient, create_client
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.schemas.auth import UserIn
|
||||||
|
|
||||||
|
|
||||||
|
async def get_super_client() -> AsyncClient:
|
||||||
|
"""for validation access_token init at life span event"""
|
||||||
|
super_client = await create_client(
|
||||||
|
settings.SUPABASE_URL,
|
||||||
|
settings.SUPABASE_KEY,
|
||||||
|
options=AsyncClientOptions(
|
||||||
|
postgrest_client_timeout=10, storage_client_timeout=10
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if not super_client:
|
||||||
|
raise HTTPException(status_code=500, detail="Super client not initialized")
|
||||||
|
return super_client
|
||||||
|
|
||||||
|
|
||||||
|
SuperClient = Annotated[AsyncClient, Depends(get_super_client)]
|
||||||
|
|
||||||
|
|
||||||
|
# auto get token from header
|
||||||
|
reusable_oauth2 = OAuth2PasswordBearer(
|
||||||
|
tokenUrl=f"{settings.API_V1_STR}/login/access-token"
|
||||||
|
)
|
||||||
|
TokenDep = Annotated[str, Depends(reusable_oauth2)]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_user(token: TokenDep, super_client: SuperClient) -> UserIn:
|
||||||
|
"""get current user from token and validate same time"""
|
||||||
|
user_rsp = await super_client.auth.get_user(jwt=token)
|
||||||
|
if not user_rsp:
|
||||||
|
logging.error("User not found")
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
return UserIn(**user_rsp.user.model_dump(), access_token=token)
|
||||||
102
backend/backend/app/core/config.py
Normal file
102
backend/backend/app/core/config.py
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
import secrets
|
||||||
|
import warnings
|
||||||
|
from typing import Annotated, Any, Literal, Self
|
||||||
|
|
||||||
|
from pydantic import (
|
||||||
|
AnyUrl,
|
||||||
|
BeforeValidator,
|
||||||
|
PostgresDsn,
|
||||||
|
computed_field,
|
||||||
|
model_validator,
|
||||||
|
)
|
||||||
|
from pydantic_core import MultiHostUrl
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
def parse_cors(v: Any) -> list[str] | str:
|
||||||
|
if isinstance(v, str) and not v.startswith("["):
|
||||||
|
return [i.strip() for i in v.split(",")]
|
||||||
|
elif isinstance(v, list | str):
|
||||||
|
return v
|
||||||
|
raise ValueError(v)
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
"""auto load config from .env and validate settings"""
|
||||||
|
|
||||||
|
# https://docs.pydantic.dev/latest/concepts/pydantic_settings/#dotenv-env-support
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
# Use top level .env file (one level above ./backend/)
|
||||||
|
env_file="../.env",
|
||||||
|
env_ignore_empty=True,
|
||||||
|
extra="ignore",
|
||||||
|
)
|
||||||
|
API_V1_STR: str = "/api/v1"
|
||||||
|
SECRET_KEY: str = secrets.token_urlsafe(32)
|
||||||
|
ENVIRONMENT: Literal["local", "staging", "production"] = "local"
|
||||||
|
|
||||||
|
# FRONTEND_HOST: str = "http://localhost:5173"
|
||||||
|
BACKEND_CORS_ORIGINS: Annotated[
|
||||||
|
list[AnyUrl] | str, BeforeValidator(parse_cors)
|
||||||
|
] = []
|
||||||
|
|
||||||
|
@computed_field # type: ignore[prop-decorator]
|
||||||
|
@property
|
||||||
|
def all_cors_origins(self) -> list[str]:
|
||||||
|
return (
|
||||||
|
[str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS]
|
||||||
|
+ [
|
||||||
|
# self.FRONTEND_HOST
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
PROJECT_NAME: str
|
||||||
|
|
||||||
|
## DB
|
||||||
|
SUPABASE_URL: str
|
||||||
|
# NOTE: super user key is service_role key instead of the anon key
|
||||||
|
SUPABASE_KEY: str
|
||||||
|
|
||||||
|
POSTGRES_SERVER: str
|
||||||
|
POSTGRES_PORT: int = 5432
|
||||||
|
POSTGRES_USER: str
|
||||||
|
POSTGRES_PASSWORD: str = ""
|
||||||
|
POSTGRES_DB: str = ""
|
||||||
|
|
||||||
|
@computed_field # type: ignore[prop-decorator]
|
||||||
|
@property
|
||||||
|
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
|
||||||
|
return MultiHostUrl.build( # type:ignore
|
||||||
|
scheme="postgresql+psycopg",
|
||||||
|
username=self.POSTGRES_USER,
|
||||||
|
password=self.POSTGRES_PASSWORD,
|
||||||
|
host=self.POSTGRES_SERVER,
|
||||||
|
port=self.POSTGRES_PORT,
|
||||||
|
path=self.POSTGRES_DB,
|
||||||
|
)
|
||||||
|
|
||||||
|
FIRST_SUPERUSER: str
|
||||||
|
FIRST_SUPERUSER_PASSWORD: str
|
||||||
|
|
||||||
|
def _check_default_secret(self, var_name: str, value: str | None) -> None:
|
||||||
|
if value == "changethis":
|
||||||
|
message = (
|
||||||
|
f'The value of {var_name} is "changethis", '
|
||||||
|
"for security, please change it, at least for deployments."
|
||||||
|
)
|
||||||
|
if self.ENVIRONMENT == "local":
|
||||||
|
warnings.warn(message, stacklevel=1)
|
||||||
|
else:
|
||||||
|
raise ValueError(message)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def _enforce_non_default_secrets(self) -> Self:
|
||||||
|
self._check_default_secret("SECRET_KEY", self.SECRET_KEY)
|
||||||
|
self._check_default_secret("POSTGRES_PASSWORD", self.POSTGRES_PASSWORD)
|
||||||
|
self._check_default_secret(
|
||||||
|
"FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD
|
||||||
|
)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings() # type: ignore[call-arg] # load args from env
|
||||||
41
backend/backend/app/core/db.py
Normal file
41
backend/backend/app/core/db.py
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
from collections.abc import Generator
|
||||||
|
|
||||||
|
from sqlmodel import Session, create_engine, select
|
||||||
|
from supabase import create_client
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.models import User
|
||||||
|
|
||||||
|
# make sure all SQLModel models are imported (app.models) before initializing DB
|
||||||
|
# otherwise, SQLModel might fail to initialize relationships properly
|
||||||
|
# for more details: https://github.com/fastapi/full-stack-fastapi-template/issues/28
|
||||||
|
|
||||||
|
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
|
||||||
|
|
||||||
|
|
||||||
|
def get_db() -> Generator[Session, None]:
|
||||||
|
with Session(engine) as session:
|
||||||
|
yield session
|
||||||
|
|
||||||
|
|
||||||
|
def init_db(session: Session) -> None:
|
||||||
|
# Tables should be created with Alembic migrations
|
||||||
|
# But if you don't want to use migrations, create
|
||||||
|
# the tables un-commenting the next lines
|
||||||
|
# from sqlmodel import SQLModel
|
||||||
|
# # This works because the models are already imported and registered from app.models
|
||||||
|
# SQLModel.metadata.create_all(engine)
|
||||||
|
|
||||||
|
result = session.exec(select(User).where(User.email == settings.FIRST_SUPERUSER))
|
||||||
|
user = result.first()
|
||||||
|
if not user:
|
||||||
|
super_client = create_client(settings.SUPABASE_URL, settings.SUPABASE_KEY)
|
||||||
|
response = super_client.auth.sign_up(
|
||||||
|
{
|
||||||
|
"email": settings.FIRST_SUPERUSER,
|
||||||
|
"password": settings.FIRST_SUPERUSER_PASSWORD,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert response.user.email == settings.FIRST_SUPERUSER
|
||||||
|
assert response.user.id is not None
|
||||||
|
assert response.session.access_token is not None
|
||||||
9
backend/backend/app/crud/__init__.py
Normal file
9
backend/backend/app/crud/__init__.py
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
from .crud_item import item
|
||||||
|
|
||||||
|
# For a new basic set of CRUD operations you could just do
|
||||||
|
__all__ = ["item"]
|
||||||
|
# from .base import CRUDBase
|
||||||
|
# from app.models.item import Item
|
||||||
|
# from app.schemas.item import ItemCreate, ItemUpdate
|
||||||
|
|
||||||
|
# item = CRUDBase[Item, ItemCreate, ItemUpdate](Item)
|
||||||
69
backend/backend/app/crud/base.py
Normal file
69
backend/backend/app/crud/base.py
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import uuid
|
||||||
|
from collections.abc import Sequence
|
||||||
|
from typing import Generic, TypeVar
|
||||||
|
|
||||||
|
from sqlmodel import Session, SQLModel, select
|
||||||
|
|
||||||
|
from app.models.base import InDBBase
|
||||||
|
|
||||||
|
ModelType = TypeVar("ModelType", bound=InDBBase)
|
||||||
|
CreateSchemaType = TypeVar("CreateSchemaType", bound=SQLModel)
|
||||||
|
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=SQLModel)
|
||||||
|
|
||||||
|
|
||||||
|
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
||||||
|
def __init__(self, model: type[ModelType]):
|
||||||
|
"""
|
||||||
|
CRUD object with default methods to Create, Read, Update, Delete (CRUD).
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
* `model`: A SQLModel model class
|
||||||
|
"""
|
||||||
|
self.model = model
|
||||||
|
|
||||||
|
def get(self, session: Session, *, id: uuid.UUID) -> ModelType | None:
|
||||||
|
"""Get a single record by id"""
|
||||||
|
statement = select(self.model).where(self.model.id == id)
|
||||||
|
result = session.exec(statement)
|
||||||
|
return result.one_or_none()
|
||||||
|
|
||||||
|
def get_multi(
|
||||||
|
self, session: Session, *, skip: int = 0, limit: int = 100
|
||||||
|
) -> Sequence[ModelType]:
|
||||||
|
"""Get multiple records with pagination"""
|
||||||
|
statement = select(self.model).offset(skip).limit(limit)
|
||||||
|
result = session.exec(statement)
|
||||||
|
return result.all()
|
||||||
|
|
||||||
|
def create(
|
||||||
|
self, session: Session, *, owner_id: uuid.UUID, obj_in: CreateSchemaType
|
||||||
|
) -> ModelType:
|
||||||
|
"""Create new record"""
|
||||||
|
db_obj = self.model(**dict(owner_id=owner_id, **obj_in.model_dump()))
|
||||||
|
session.add(db_obj)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self, session: Session, *, id: uuid.UUID, obj_in: UpdateSchemaType
|
||||||
|
) -> ModelType | None:
|
||||||
|
"""Update existing record"""
|
||||||
|
db_obj = self.get(session, id=id)
|
||||||
|
if db_obj:
|
||||||
|
update_data = obj_in.model_dump(exclude_unset=True)
|
||||||
|
db_obj.sqlmodel_update(update_data)
|
||||||
|
|
||||||
|
session.add(db_obj)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def remove(self, session: Session, *, id: uuid.UUID) -> ModelType | None:
|
||||||
|
"""Remove a record"""
|
||||||
|
obj = self.get(session, id=id)
|
||||||
|
if obj:
|
||||||
|
session.delete(obj)
|
||||||
|
session.commit()
|
||||||
|
return obj
|
||||||
21
backend/backend/app/crud/crud_item.py
Normal file
21
backend/backend/app/crud/crud_item.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
from app.crud.base import CRUDBase
|
||||||
|
from app.models.item import Item, ItemCreate, ItemUpdate
|
||||||
|
|
||||||
|
|
||||||
|
class CRUDItem(CRUDBase[Item, ItemCreate, ItemUpdate]):
|
||||||
|
def create(
|
||||||
|
self, session: Session, *, owner_id: uuid.UUID, obj_in: ItemCreate
|
||||||
|
) -> Item:
|
||||||
|
return super().create(session, owner_id=owner_id, obj_in=obj_in)
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self, session: Session, *, id: uuid.UUID, obj_in: ItemUpdate
|
||||||
|
) -> Item | None:
|
||||||
|
return super().update(session, id=id, obj_in=obj_in)
|
||||||
|
|
||||||
|
|
||||||
|
item = CRUDItem(Item)
|
||||||
74
backend/backend/app/main.py
Normal file
74
backend/backend/app/main.py
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import logging
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.concurrency import asynccontextmanager
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from uvicorn.config import LOGGING_CONFIG
|
||||||
|
|
||||||
|
from app.api.main import api_router
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.utils import custom_generate_unique_id
|
||||||
|
|
||||||
|
logger = logging.getLogger("uvicorn")
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: # noqa ARG001
|
||||||
|
"""life span events"""
|
||||||
|
try:
|
||||||
|
logger.info("lifespan start")
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
logger.info("lifespan exit")
|
||||||
|
|
||||||
|
|
||||||
|
# init FastAPI with lifespan
|
||||||
|
app = FastAPI(
|
||||||
|
lifespan=lifespan,
|
||||||
|
title=settings.PROJECT_NAME,
|
||||||
|
openapi_url=f"{settings.API_V1_STR}/openapi.json",
|
||||||
|
generate_unique_id_function=custom_generate_unique_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Set all CORS enabled origins
|
||||||
|
if settings.all_cors_origins:
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=settings.all_cors_origins,
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Include the routers
|
||||||
|
app.include_router(api_router, prefix=settings.API_V1_STR)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", tags=["root"])
|
||||||
|
async def read_root() -> dict[str, str]:
|
||||||
|
return {"Hello": "World"}
|
||||||
|
|
||||||
|
|
||||||
|
# Logger
|
||||||
|
def timestamp_log_config(uvicorn_log_config: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""https://github.com/fastapi/fastapi/discussions/7457#discussioncomment-5565969"""
|
||||||
|
datefmt = "%d-%m-%Y %H:%M:%S"
|
||||||
|
formatters = uvicorn_log_config["formatters"]
|
||||||
|
formatters["default"]["fmt"] = "%(levelprefix)s [%(asctime)s] %(message)s"
|
||||||
|
formatters["access"]["fmt"] = (
|
||||||
|
'%(levelprefix)s [%(asctime)s] %(client_addr)s - "%(request_line)s" %(status_code)s'
|
||||||
|
)
|
||||||
|
formatters["access"]["datefmt"] = datefmt
|
||||||
|
formatters["default"]["datefmt"] = datefmt
|
||||||
|
return uvicorn_log_config
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(
|
||||||
|
app, host="0.0.0.0", port=8000, log_config=timestamp_log_config(LOGGING_CONFIG)
|
||||||
|
)
|
||||||
4
backend/backend/app/models/__init__.py
Normal file
4
backend/backend/app/models/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
from .item import Item
|
||||||
|
from .user import User
|
||||||
|
|
||||||
|
__all__ = ["User", "Item"]
|
||||||
10
backend/backend/app/models/base.py
Normal file
10
backend/backend/app/models/base.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class InDBBase(SQLModel):
|
||||||
|
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||||
|
owner_id: uuid.UUID = Field(
|
||||||
|
foreign_key="auth.users.id", nullable=False, ondelete="CASCADE"
|
||||||
|
)
|
||||||
37
backend/backend/app/models/item.py
Normal file
37
backend/backend/app/models/item.py
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
from app.models.base import InDBBase
|
||||||
|
|
||||||
|
|
||||||
|
# Shared properties
|
||||||
|
class ItemBase(SQLModel):
|
||||||
|
title: str = Field(min_length=1, max_length=255)
|
||||||
|
description: str | None = Field(default=None, max_length=255)
|
||||||
|
|
||||||
|
|
||||||
|
# Properties to receive on item creation
|
||||||
|
class ItemCreate(ItemBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Properties to receive on item update
|
||||||
|
class ItemUpdate(ItemBase):
|
||||||
|
title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
# Database model, database table inferred from class name
|
||||||
|
class Item(InDBBase, ItemBase, table=True):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Properties to return via API, id is always required
|
||||||
|
class ItemPublic(ItemBase):
|
||||||
|
id: uuid.UUID
|
||||||
|
owner_id: uuid.UUID
|
||||||
|
|
||||||
|
|
||||||
|
class ItemsPublic(SQLModel):
|
||||||
|
data: list[ItemPublic]
|
||||||
|
count: int
|
||||||
13
backend/backend/app/models/user.py
Normal file
13
backend/backend/app/models/user.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from pydantic import EmailStr
|
||||||
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
|
|
||||||
|
class User(SQLModel, table=True):
|
||||||
|
"""NOTE: do not migrate with alembic with it"""
|
||||||
|
|
||||||
|
__tablename__ = "users"
|
||||||
|
__table_args__ = {"schema": "auth", "keep_existing": True}
|
||||||
|
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
||||||
|
email: EmailStr = Field(max_length=255)
|
||||||
3
backend/backend/app/schemas/__init__.py
Normal file
3
backend/backend/app/schemas/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .auth import Token
|
||||||
|
|
||||||
|
__all__ = ["Token"]
|
||||||
43
backend/backend/app/schemas/auth.py
Normal file
43
backend/backend/app/schemas/auth.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
from gotrue import User, UserAttributes # type: ignore
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
# Shared properties
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str | None = None
|
||||||
|
refresh_token: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# request
|
||||||
|
class UserIn(Token, User): # type: ignore
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Properties to receive via API on creation
|
||||||
|
# in
|
||||||
|
class UserCreate(BaseModel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Properties to receive via API on update
|
||||||
|
# in
|
||||||
|
class UserUpdate(UserAttributes): # type: ignore
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# response
|
||||||
|
|
||||||
|
|
||||||
|
class UserInDBBase(BaseModel):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Properties to return to client via api
|
||||||
|
# out
|
||||||
|
class UserOut(Token):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Properties properties stored in DB
|
||||||
|
class UserInDB(User): # type: ignore
|
||||||
|
pass
|
||||||
0
backend/backend/app/services/__init__.py
Normal file
0
backend/backend/app/services/__init__.py
Normal file
5
backend/backend/app/utils/__init__.py
Normal file
5
backend/backend/app/utils/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from fastapi.routing import APIRoute
|
||||||
|
|
||||||
|
|
||||||
|
def custom_generate_unique_id(route: APIRoute) -> str:
|
||||||
|
return f"{route.tags[0]}-{route.name}"
|
||||||
24
backend/backend/app/utils/init_data.py
Normal file
24
backend/backend/app/utils/init_data.py
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from sqlalchemy import Engine
|
||||||
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
from app.core.db import engine, init_db
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def init(db_engine: Engine) -> None:
|
||||||
|
with Session(db_engine) as session:
|
||||||
|
init_db(session)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
logger.info("Creating initial data")
|
||||||
|
init(engine)
|
||||||
|
logger.info("Initial data created")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
39
backend/backend/app/utils/test_pre_start.py
Normal file
39
backend/backend/app/utils/test_pre_start.py
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from sqlalchemy import Engine
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from tenacity import after_log, before_log, retry, stop_after_attempt, wait_fixed
|
||||||
|
|
||||||
|
from app.core.db import engine
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
max_tries = 60 * 5 # 5 minutes
|
||||||
|
wait_seconds = 1
|
||||||
|
|
||||||
|
|
||||||
|
@retry(
|
||||||
|
stop=stop_after_attempt(max_tries),
|
||||||
|
wait=wait_fixed(wait_seconds),
|
||||||
|
before=before_log(logger, logging.INFO),
|
||||||
|
after=after_log(logger, logging.WARN),
|
||||||
|
)
|
||||||
|
def init(db_engine: Engine) -> None:
|
||||||
|
try:
|
||||||
|
with Session(db_engine) as session:
|
||||||
|
# Try to create session to check if DB is awake
|
||||||
|
session.exec(select(1))
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
logger.info("Initializing service")
|
||||||
|
init(engine)
|
||||||
|
logger.info("Service finished initializing")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
117
backend/backend/pyproject.toml
Normal file
117
backend/backend/pyproject.toml
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
[project]
|
||||||
|
name = "app"
|
||||||
|
version = "0.4.1"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"uvicorn>=0.30.6",
|
||||||
|
"pydantic[email]>=2.8.2",
|
||||||
|
"pydantic-settings>=2.4.0",
|
||||||
|
"python-multipart>=0.0.9",
|
||||||
|
"supabase>=2.7.4",
|
||||||
|
"fastapi[standard]>=0.112.2",
|
||||||
|
"sqlmodel>=0.0.22",
|
||||||
|
"alembic>=1.14.0",
|
||||||
|
"tenacity>=9.0.0",
|
||||||
|
"psycopg2-binary>=2.9.10",
|
||||||
|
"psycopg>=3.2.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"coverage>=7.6.1",
|
||||||
|
"faker>=28.0.0",
|
||||||
|
"mypy>=1.13.0",
|
||||||
|
"pre-commit>=3.8.0",
|
||||||
|
"pytest-sugar>=1.0.0",
|
||||||
|
"pytest>=8.3.2",
|
||||||
|
"httpx>=0.28.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
## Test
|
||||||
|
[tool.mypy]
|
||||||
|
strict = true
|
||||||
|
exclude = ["venv", ".venv", "alembic"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
# Set additional command line options for pytest
|
||||||
|
# Ref: https://docs.pytest.org/en/stable/reference/reference.html#command-line-flags
|
||||||
|
addopts = "-rXs --strict-config --strict-markers --tb=short"
|
||||||
|
xfail_strict = true # Treat tests that are marked as xfail but pass as test failures
|
||||||
|
# filterwarnings = ["error"] # Treat all warnings as errors
|
||||||
|
pythonpath = "app"
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
branch = true
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
skip_covered = true
|
||||||
|
show_missing = true
|
||||||
|
precision = 2
|
||||||
|
exclude_lines = [
|
||||||
|
'def __repr__',
|
||||||
|
'pragma= no cover',
|
||||||
|
'raise NotImplementedError',
|
||||||
|
'if TYPE_CHECKING=',
|
||||||
|
'if typing.TYPE_CHECKING=',
|
||||||
|
'@overload',
|
||||||
|
'@typing.overload',
|
||||||
|
'\(Protocol\)=$',
|
||||||
|
'typing.assert_never',
|
||||||
|
'assert_never',
|
||||||
|
'if __name__ == "__main__":',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
## Linter and formatter
|
||||||
|
[tool.ruff]
|
||||||
|
# cover and extend the default config in https=//docs.astral.sh/ruff/configuration/
|
||||||
|
extend-exclude = ["alembic"]
|
||||||
|
target-version = "py310"
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"E", # pycodestyle errors
|
||||||
|
"W", # pycodestyle warnings
|
||||||
|
"F", # pyflakes
|
||||||
|
"I", # isort
|
||||||
|
"B", # flake8-bugbear
|
||||||
|
"C4", # flake8-comprehensions
|
||||||
|
"UP", # pyupgrade
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
"E501", # line too long, handled by black
|
||||||
|
"B008", # do not perform function calls in argument defaults
|
||||||
|
"W191", # indentation contains tabs
|
||||||
|
"B904", # Allow raising exceptions without from e, for HTTPException
|
||||||
|
"COM819", # Trailing comma prohibited
|
||||||
|
"D100", # Missing docstring in public module(file)
|
||||||
|
"D104", # Missing docstring in public package
|
||||||
|
"D203", # 1 blank line required before class docstring
|
||||||
|
"E201", # Whitespace after '('
|
||||||
|
"E202", # Whitespace before ')'
|
||||||
|
"E203", # Whitespace before '='
|
||||||
|
"E221", # Multiple spaces before operator
|
||||||
|
"E241", # Multiple spaces after ','
|
||||||
|
"E251", # Unexpected spaces around keyword / parameter equals
|
||||||
|
"W291", # Trailing whitespace
|
||||||
|
"W293", # Blank line contains whitespace
|
||||||
|
]
|
||||||
|
|
||||||
|
isort = { combine-as-imports = true, split-on-trailing-comma = false }
|
||||||
|
|
||||||
|
# Avoid trying to fix flake8-bugbear (`B`) violations.
|
||||||
|
unfixable = ["B"]
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
docstring-code-format = true
|
||||||
|
skip-magic-trailing-comma = true
|
||||||
|
|
||||||
|
# Reference
|
||||||
|
# 1. https=//github.com/Kludex/python-template/blob/main/template/%7B%7B%20project_slug%20%7D%7D/pyproject.toml.jinja
|
||||||
|
# 2. https=//github.com/fastapi/full-stack-fastapi-template/blob/master/backend/pyproject.toml
|
||||||
|
# 3. https=//github.com/pydantic/logfire
|
||||||
|
# 4. https=//coverage.readthedocs.io/en/latest/index.html
|
||||||
5
backend/backend/scripts/format.sh
Executable file
5
backend/backend/scripts/format.sh
Executable file
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -x
|
||||||
|
|
||||||
|
ruff check app scripts tests --fix
|
||||||
|
ruff format app scripts tests
|
||||||
8
backend/backend/scripts/lint.sh
Executable file
8
backend/backend/scripts/lint.sh
Executable file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
set -x
|
||||||
|
|
||||||
|
mypy app # type check
|
||||||
|
ruff check app tests # linter
|
||||||
|
ruff format app --check # formatter
|
||||||
18
backend/backend/scripts/pre-start.sh
Executable file
18
backend/backend/scripts/pre-start.sh
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
set -x
|
||||||
|
|
||||||
|
# Let the DB start
|
||||||
|
python -m app.utils.test_pre_start
|
||||||
|
|
||||||
|
# before migrations
|
||||||
|
# alembic init
|
||||||
|
# mkdir -p app/alembic/versions
|
||||||
|
# alembic revision --autogenerate -m "initial commit"
|
||||||
|
|
||||||
|
# Run migrations
|
||||||
|
alembic upgrade head
|
||||||
|
|
||||||
|
# Create initial data in DB
|
||||||
|
python -m app.utils.init_data
|
||||||
8
backend/backend/scripts/test.sh
Executable file
8
backend/backend/scripts/test.sh
Executable file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
set -x
|
||||||
|
|
||||||
|
coverage run --source=app -m pytest
|
||||||
|
coverage report --show-missing
|
||||||
|
coverage html --title "${@-coverage}"
|
||||||
7
backend/backend/scripts/tests-start.sh
Executable file
7
backend/backend/scripts/tests-start.sh
Executable file
|
|
@ -0,0 +1,7 @@
|
||||||
|
#! /usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
set -x
|
||||||
|
|
||||||
|
python -m app.utils.test_pre_start
|
||||||
|
|
||||||
|
bash scripts/test.sh "$@"
|
||||||
0
backend/backend/tests/__init__.py
Normal file
0
backend/backend/tests/__init__.py
Normal file
0
backend/backend/tests/api/__init__.py
Normal file
0
backend/backend/tests/api/__init__.py
Normal file
0
backend/backend/tests/api/api_v1/__init__.py
Normal file
0
backend/backend/tests/api/api_v1/__init__.py
Normal file
113
backend/backend/tests/api/api_v1/test_items.py
Normal file
113
backend/backend/tests/api/api_v1/test_items.py
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
from faker import Faker
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.models.item import Item, ItemCreate, ItemUpdate
|
||||||
|
from app.schemas.auth import Token
|
||||||
|
from tests.utils import get_auth_header
|
||||||
|
|
||||||
|
fake = Faker()
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_item(client: TestClient, token: Token) -> None:
|
||||||
|
"""Test create item endpoint"""
|
||||||
|
# Prepare test data
|
||||||
|
title = fake.sentence(nb_words=3)
|
||||||
|
description = fake.text(max_nb_chars=200)
|
||||||
|
item_in = ItemCreate(title=title, description=description)
|
||||||
|
|
||||||
|
# Make request
|
||||||
|
response = client.post(
|
||||||
|
f"{settings.API_V1_STR}/items/create-item",
|
||||||
|
headers=get_auth_header(token.access_token),
|
||||||
|
json=item_in.model_dump(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert response
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["title"] == title
|
||||||
|
assert data["description"] == description
|
||||||
|
assert "id" in data
|
||||||
|
assert "owner_id" in data
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_item(client: TestClient, token: Token, test_item: Item) -> None:
|
||||||
|
"""Test get item by id endpoint"""
|
||||||
|
# Make request
|
||||||
|
response = client.get(
|
||||||
|
f"{settings.API_V1_STR}/items/get-item/{test_item.id}",
|
||||||
|
headers=get_auth_header(token.access_token),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert response
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] == str(test_item.id)
|
||||||
|
assert data["title"] == test_item.title
|
||||||
|
assert data["description"] == test_item.description
|
||||||
|
assert data["owner_id"] == str(test_item.owner_id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_items(client: TestClient, token: Token, test_item: Item) -> None:
|
||||||
|
"""Test get items list endpoint"""
|
||||||
|
# Make request
|
||||||
|
response = client.get(
|
||||||
|
f"{settings.API_V1_STR}/items/get-items",
|
||||||
|
headers=get_auth_header(token.access_token),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert response
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert isinstance(data, list)
|
||||||
|
assert len(data) > 0
|
||||||
|
|
||||||
|
# Verify the test_item is in the response
|
||||||
|
item_ids = [item["id"] for item in data]
|
||||||
|
assert str(test_item.id) in item_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_item(client: TestClient, token: Token, test_item: Item) -> None:
|
||||||
|
"""Test update item endpoint"""
|
||||||
|
# Prepare update data
|
||||||
|
new_title = fake.sentence(nb_words=3)
|
||||||
|
new_description = fake.text(max_nb_chars=200)
|
||||||
|
item_update = ItemUpdate(title=new_title, description=new_description)
|
||||||
|
|
||||||
|
# Make request
|
||||||
|
response = client.put(
|
||||||
|
f"{settings.API_V1_STR}/items/update-item/{test_item.id}",
|
||||||
|
headers=get_auth_header(token.access_token),
|
||||||
|
json=item_update.model_dump(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert response
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] == str(test_item.id)
|
||||||
|
assert data["title"] == new_title
|
||||||
|
assert data["description"] == new_description
|
||||||
|
assert data["owner_id"] == str(test_item.owner_id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_item(client: TestClient, token: Token, test_item) -> None:
|
||||||
|
"""Test delete item endpoint"""
|
||||||
|
# Make delete request
|
||||||
|
response = client.delete(
|
||||||
|
f"{settings.API_V1_STR}/items/delete/{test_item.id}",
|
||||||
|
headers=get_auth_header(token.access_token),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert delete response
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] == str(test_item.id)
|
||||||
|
|
||||||
|
# Verify item is deleted by trying to get it
|
||||||
|
get_response = client.get(
|
||||||
|
f"{settings.API_V1_STR}/items/get-item/{test_item.id}",
|
||||||
|
headers=get_auth_header(token.access_token),
|
||||||
|
)
|
||||||
|
assert get_response.status_code == 200
|
||||||
|
assert get_response.json() is None
|
||||||
13
backend/backend/tests/api/api_v1/test_utils.py
Normal file
13
backend/backend/tests/api/api_v1/test_utils.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_item(client: TestClient) -> None:
|
||||||
|
"""Test get item by id endpoint"""
|
||||||
|
# Make request
|
||||||
|
response = client.get(f"{settings.API_V1_STR}/utils/health-check/")
|
||||||
|
|
||||||
|
# Assert response
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.content
|
||||||
75
backend/backend/tests/conftest.py
Normal file
75
backend/backend/tests/conftest.py
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import uuid
|
||||||
|
from collections.abc import Generator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from faker import Faker
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from gotrue import User
|
||||||
|
from sqlmodel import Session, delete
|
||||||
|
from supabase import Client, create_client
|
||||||
|
|
||||||
|
from app import crud
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.core.db import engine, init_db
|
||||||
|
from app.main import app
|
||||||
|
from app.models.item import Item, ItemCreate
|
||||||
|
from app.schemas.auth import Token
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def db() -> Generator[Session, None]:
|
||||||
|
with Session(engine) as session:
|
||||||
|
init_db(session)
|
||||||
|
yield session
|
||||||
|
statement = delete(Item)
|
||||||
|
session.exec(statement) # type: ignore
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def client() -> Generator[TestClient, None, None]:
|
||||||
|
with TestClient(app) as c:
|
||||||
|
yield c
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session", autouse=True)
|
||||||
|
def global_cleanup() -> Generator[None, None]:
|
||||||
|
yield
|
||||||
|
# Clean up all users
|
||||||
|
super_client = create_client(settings.SUPABASE_URL, settings.SUPABASE_KEY)
|
||||||
|
users = super_client.auth.admin.list_users()
|
||||||
|
for user in users:
|
||||||
|
super_client.auth.admin.delete_user(user.id)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def super_client() -> Generator[Client, None]:
|
||||||
|
super_client = create_client(settings.SUPABASE_URL, settings.SUPABASE_KEY)
|
||||||
|
yield super_client
|
||||||
|
|
||||||
|
|
||||||
|
fake = Faker()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def test_user(super_client: Client) -> Generator[User, None]:
|
||||||
|
response = super_client.auth.sign_up(
|
||||||
|
{"email": fake.email(), "password": "testpassword123"}
|
||||||
|
)
|
||||||
|
yield response.user
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def test_item(db: Session, test_user: User) -> Generator[Item, None]:
|
||||||
|
item_in = ItemCreate(
|
||||||
|
title=fake.sentence(nb_words=3), description=fake.text(max_nb_chars=200)
|
||||||
|
)
|
||||||
|
yield crud.item.create(db, owner_id=uuid.UUID(test_user.id), obj_in=item_in)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def token(super_client: Client) -> Generator[Token, None]:
|
||||||
|
response = super_client.auth.sign_up(
|
||||||
|
{"email": fake.email(), "password": "testpassword123"}
|
||||||
|
)
|
||||||
|
yield Token(access_token=response.session.access_token)
|
||||||
0
backend/backend/tests/crud/__init__.py
Normal file
0
backend/backend/tests/crud/__init__.py
Normal file
86
backend/backend/tests/crud/test_item.py
Normal file
86
backend/backend/tests/crud/test_item.py
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from faker import Faker
|
||||||
|
from gotrue import User
|
||||||
|
from sqlmodel import Session
|
||||||
|
|
||||||
|
from app import crud
|
||||||
|
from app.models.item import Item, ItemCreate, ItemUpdate
|
||||||
|
|
||||||
|
fake = Faker()
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_item(db: Session, test_user: User) -> None:
|
||||||
|
"""Test creating a new item"""
|
||||||
|
title = fake.sentence(nb_words=3)
|
||||||
|
description = fake.text(max_nb_chars=200)
|
||||||
|
item_in = ItemCreate(title=title, description=description)
|
||||||
|
|
||||||
|
item = crud.item.create(db, owner_id=uuid.UUID(test_user.id), obj_in=item_in)
|
||||||
|
|
||||||
|
assert item.id is not None
|
||||||
|
assert item.title == title
|
||||||
|
assert item.description == description
|
||||||
|
assert item.owner_id == uuid.UUID(test_user.id)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_item(db: Session, test_item: Item) -> None:
|
||||||
|
"""Test retrieving a single item"""
|
||||||
|
stored_item = crud.item.get(db, id=test_item.id)
|
||||||
|
|
||||||
|
assert stored_item is not None
|
||||||
|
assert stored_item.id == test_item.id
|
||||||
|
assert stored_item.title == test_item.title
|
||||||
|
assert stored_item.description == test_item.description
|
||||||
|
assert stored_item.owner_id == test_item.owner_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_multi_items(db: Session, test_user: User) -> None:
|
||||||
|
"""Test retrieving multiple items"""
|
||||||
|
# Create multiple items
|
||||||
|
items = []
|
||||||
|
for _ in range(5):
|
||||||
|
item_in = ItemCreate(
|
||||||
|
title=fake.sentence(nb_words=3), description=fake.text(max_nb_chars=200)
|
||||||
|
)
|
||||||
|
item = crud.item.create(db, owner_id=uuid.UUID(test_user.id), obj_in=item_in)
|
||||||
|
items.append(item)
|
||||||
|
# Retrieve multiple items
|
||||||
|
stored_items = crud.item.get_multi(db)
|
||||||
|
# Verify all items are in the list
|
||||||
|
|
||||||
|
assert all(item in stored_items for item in items)
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_item(db: Session, test_item: Item) -> None:
|
||||||
|
"""Test updating an item"""
|
||||||
|
new_title = fake.sentence(nb_words=3)
|
||||||
|
new_description = fake.text(max_nb_chars=200)
|
||||||
|
update_data = ItemUpdate(title=new_title, description=new_description)
|
||||||
|
|
||||||
|
updated_item = crud.item.update(db, id=test_item.id, obj_in=update_data)
|
||||||
|
|
||||||
|
assert updated_item is not None
|
||||||
|
assert updated_item.id == test_item.id
|
||||||
|
assert updated_item.title == new_title
|
||||||
|
assert updated_item.description == new_description
|
||||||
|
assert updated_item.owner_id == test_item.owner_id
|
||||||
|
|
||||||
|
# update with empty data
|
||||||
|
updated_item = crud.item.update(db, id=uuid.uuid4(), obj_in=update_data)
|
||||||
|
assert updated_item is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_item(db: Session, test_item: Item) -> None:
|
||||||
|
"""Test deleting an item"""
|
||||||
|
deleted_item = crud.item.remove(db, id=test_item.id)
|
||||||
|
assert deleted_item is not None
|
||||||
|
assert deleted_item.id == test_item.id
|
||||||
|
|
||||||
|
# Verify item is deleted
|
||||||
|
item = crud.item.get(db, id=test_item.id)
|
||||||
|
assert item is None
|
||||||
|
|
||||||
|
# delete empty items
|
||||||
|
deleted_item = crud.item.remove(db, id=test_item.id)
|
||||||
|
assert deleted_item is None
|
||||||
23
backend/backend/tests/test_main.py
Normal file
23
backend/backend/tests/test_main.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from app.utils.init_data import main as init_db
|
||||||
|
from app.utils.test_pre_start import main as pre_start
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_db() -> None:
|
||||||
|
init_db()
|
||||||
|
|
||||||
|
|
||||||
|
def test_pre_start() -> None:
|
||||||
|
pre_start()
|
||||||
|
|
||||||
|
|
||||||
|
def test_root(client: TestClient) -> None:
|
||||||
|
response = client.get("/")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"Hello": "World"}
|
||||||
7
backend/backend/tests/utils.py
Normal file
7
backend/backend/tests/utils.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_header(access_token: str | None) -> dict[str, str]:
|
||||||
|
if not access_token:
|
||||||
|
raise HTTPException(status_code=401, detail="No access token")
|
||||||
|
return {"Authorization": f"Bearer {access_token}"}
|
||||||
1902
backend/backend/uv.lock
Normal file
1902
backend/backend/uv.lock
Normal file
File diff suppressed because it is too large
Load diff
121
backend/pyproject.toml
Normal file
121
backend/pyproject.toml
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
[project]
|
||||||
|
name = "fastapi_supabase_template"
|
||||||
|
version = "0.4.1"
|
||||||
|
description = ""
|
||||||
|
authors = [{ name = "Atticus.J.Zeller", email = "hello@atticux.me" }]
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
|
## VCS
|
||||||
|
[tool.git-cliff.remote.github]
|
||||||
|
owner = "atticuszeller"
|
||||||
|
repo = "fastapi_supabase_template"
|
||||||
|
|
||||||
|
[tool.git-cliff.changelog]
|
||||||
|
# template for the changelog header
|
||||||
|
header = """
|
||||||
|
# Changelog\n
|
||||||
|
All notable changes to this project will be documented in this file.\n
|
||||||
|
"""
|
||||||
|
# template for the changelog body
|
||||||
|
# https://keats.github.io/tera/docs/#introduction
|
||||||
|
body = """
|
||||||
|
{% if version %}\
|
||||||
|
## {{ version | trim_start_matches(pat="v") }} - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||||
|
{% else %}\
|
||||||
|
## unreleased
|
||||||
|
{% endif %}\
|
||||||
|
{% for group, commits in commits | group_by(attribute="group") %}
|
||||||
|
### {{ group | striptags | trim | upper_first }}
|
||||||
|
{% for commit in commits| unique(attribute="message") %}
|
||||||
|
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
||||||
|
{% if commit.breaking %}[**breaking**] {% endif %}\
|
||||||
|
{{ commit.message | upper_first }}\
|
||||||
|
{% if commit.remote.pr_number %} in #{{ commit.remote.pr_number }}{%- endif %}\
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}\n
|
||||||
|
"""
|
||||||
|
# template for the changelog footer
|
||||||
|
footer = """
|
||||||
|
<!-- generated by git-cliff -->
|
||||||
|
"""
|
||||||
|
# remove the leading and trailings
|
||||||
|
trim = true
|
||||||
|
# postprocessors
|
||||||
|
# postprocessors = [
|
||||||
|
# { pattern = '<REPO>', replace = "https://github.com/atticuszz/python-uv" }, # replace repository URL
|
||||||
|
# ]
|
||||||
|
# render body even when there are no releases to process
|
||||||
|
render_always = true
|
||||||
|
# output file path
|
||||||
|
output = "CHANGELOG.md"
|
||||||
|
|
||||||
|
[tool.git-cliff.git]
|
||||||
|
# parse the commits based on https://www.conventionalcommits.org
|
||||||
|
conventional_commits = true
|
||||||
|
# filter out the commits that are not conventional
|
||||||
|
filter_unconventional = true
|
||||||
|
# process each line of a commit as an individual commit
|
||||||
|
split_commits = false
|
||||||
|
# regex for preprocessing the commit messages
|
||||||
|
commit_preprocessors = [
|
||||||
|
# If the spelling is incorrect, it will be automatically fixed.
|
||||||
|
{ pattern = '.*', replace_command = 'typos --write-changes -' },
|
||||||
|
]
|
||||||
|
# regex for parsing and grouping commits
|
||||||
|
commit_parsers = [
|
||||||
|
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
||||||
|
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
||||||
|
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
|
||||||
|
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
||||||
|
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
||||||
|
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
||||||
|
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
||||||
|
{ message = "^chore\\(release\\)", skip = true },
|
||||||
|
{ message = "^chore\\(deps.*\\)", skip = true },
|
||||||
|
{ message = "^chore\\(pr\\)", skip = true },
|
||||||
|
{ message = "^chore\\(pull\\)", skip = true },
|
||||||
|
{ message = "^chore|^ci", group = "<!-- 7 -->⚙️ Miscellaneous Tasks" },
|
||||||
|
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
|
||||||
|
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
|
||||||
|
]
|
||||||
|
# filter out the commits that are not matched by commit parsers
|
||||||
|
filter_commits = false
|
||||||
|
# sort the tags topologically
|
||||||
|
topo_order = false
|
||||||
|
# sort the commits inside sections by oldest/newest order
|
||||||
|
sort_commits = "oldest"
|
||||||
|
|
||||||
|
|
||||||
|
[tool.bumpversion]
|
||||||
|
current_version = "0.4.1"
|
||||||
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
||||||
|
serialize = ["{major}.{minor}.{patch}"]
|
||||||
|
search = "{current_version}"
|
||||||
|
replace = "{new_version}"
|
||||||
|
regex = false
|
||||||
|
ignore_missing_version = false
|
||||||
|
ignore_missing_files = false
|
||||||
|
tag = true
|
||||||
|
sign_tags = false
|
||||||
|
tag_name = "v{new_version}"
|
||||||
|
tag_message = "chore(release): {current_version} → {new_version}"
|
||||||
|
allow_dirty = true # git-cliff first then bump patch
|
||||||
|
commit = true
|
||||||
|
message = "chore(release): {current_version} → {new_version}"
|
||||||
|
commit_args = ""
|
||||||
|
setup_hooks = []
|
||||||
|
pre_commit_hooks = []
|
||||||
|
post_commit_hooks = []
|
||||||
|
|
||||||
|
[[tool.bumpversion.files]]
|
||||||
|
filename = "pyproject.toml"
|
||||||
|
search = 'version = "{current_version}"'
|
||||||
|
replace = 'version = "{new_version}"'
|
||||||
|
|
||||||
|
[[tool.bumpversion.files]]
|
||||||
|
filename = "CHANGELOG.md"
|
||||||
|
search = "unreleased"
|
||||||
|
replace = "{new_version} - {now:%Y-%m-%d}"
|
||||||
|
|
||||||
|
|
||||||
|
# https://callowayproject.github.io/bump-my-version/reference/search-and-replace-config/
|
||||||
8
backend/scripts/bump.sh
Executable file
8
backend/scripts/bump.sh
Executable file
|
|
@ -0,0 +1,8 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# update CHANGELOG.md use GITHUB_REPO ENV as github token
|
||||||
|
uvx git-cliff -o -v --github-repo "atticuszeller/fastapi_supabase_template"
|
||||||
|
# bump version and commit with tags
|
||||||
|
uvx bump-my-version bump patch
|
||||||
|
# push remote
|
||||||
|
git push origin main --tags
|
||||||
19
backend/scripts/update-env.sh
Normal file
19
backend/scripts/update-env.sh
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
temp_file=$(mktemp)
|
||||||
|
supabase start | tee "$temp_file"
|
||||||
|
|
||||||
|
SUPABASE_URL=$(grep "API URL:" "$temp_file" | awk '{print $3}')
|
||||||
|
SUPABASE_KEY=$(grep "service_role key:" "$temp_file" | awk '{print $3}')
|
||||||
|
POSTGRES_PORT=$(grep "DB URL:" "$temp_file" | sed 's/.*:54\([0-9]*\).*/\1/g')
|
||||||
|
|
||||||
|
sed -i.bak \
|
||||||
|
-e "s#SUPABASE_URL=.*#SUPABASE_URL=$SUPABASE_URL#" \
|
||||||
|
-e "s#SUPABASE_KEY=.*#SUPABASE_KEY=$SUPABASE_KEY#" \
|
||||||
|
-e "s#POSTGRES_PORT=.*#POSTGRES_PORT=54$POSTGRES_PORT#" \
|
||||||
|
.env
|
||||||
|
|
||||||
|
rm "$temp_file"
|
||||||
|
rm -f .env.bak
|
||||||
|
|
||||||
|
echo "done"
|
||||||
4
backend/supabase/.gitignore
vendored
Normal file
4
backend/supabase/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Supabase
|
||||||
|
.branches
|
||||||
|
.temp
|
||||||
|
.env
|
||||||
256
backend/supabase/config.toml
Normal file
256
backend/supabase/config.toml
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||||
|
# working directory name when running `supabase init`.
|
||||||
|
project_id = "test"
|
||||||
|
|
||||||
|
[api]
|
||||||
|
enabled = true
|
||||||
|
# Port to use for the API URL.
|
||||||
|
port = 54321
|
||||||
|
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||||
|
# endpoints. `public` is always included.
|
||||||
|
schemas = ["public", "graphql_public"]
|
||||||
|
# Extra schemas to add to the search_path of every request. `public` is always included.
|
||||||
|
extra_search_path = ["public", "extensions"]
|
||||||
|
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
|
||||||
|
# for accidental or malicious requests.
|
||||||
|
max_rows = 1000
|
||||||
|
|
||||||
|
[api.tls]
|
||||||
|
enabled = false
|
||||||
|
|
||||||
|
[db]
|
||||||
|
# Port to use for the local database URL.
|
||||||
|
port = 54322
|
||||||
|
# Port used by db diff command to initialize the shadow database.
|
||||||
|
shadow_port = 54320
|
||||||
|
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
||||||
|
# server_version;` on the remote database to check.
|
||||||
|
major_version = 15
|
||||||
|
|
||||||
|
[db.pooler]
|
||||||
|
enabled = false
|
||||||
|
# Port to use for the local connection pooler.
|
||||||
|
port = 54329
|
||||||
|
# Specifies when a server connection can be reused by other clients.
|
||||||
|
# Configure one of the supported pooler modes: `transaction`, `session`.
|
||||||
|
pool_mode = "transaction"
|
||||||
|
# How many server connections to allow per user/database pair.
|
||||||
|
default_pool_size = 20
|
||||||
|
# Maximum number of client connections allowed.
|
||||||
|
max_client_conn = 100
|
||||||
|
|
||||||
|
[db.seed]
|
||||||
|
# If enabled, seeds the database after migrations during a db reset.
|
||||||
|
enabled = true
|
||||||
|
# Specifies an ordered list of seed files to load during db reset.
|
||||||
|
# Supports glob patterns relative to supabase directory. For example:
|
||||||
|
# sql_paths = ['./seeds/*.sql', '../project-src/seeds/*-load-testing.sql']
|
||||||
|
sql_paths = ['./seed.sql']
|
||||||
|
|
||||||
|
[realtime]
|
||||||
|
enabled = true
|
||||||
|
# Bind realtime via either IPv4 or IPv6. (default: IPv4)
|
||||||
|
# ip_version = "IPv6"
|
||||||
|
# The maximum length in bytes of HTTP request headers. (default: 4096)
|
||||||
|
# max_header_length = 4096
|
||||||
|
|
||||||
|
[studio]
|
||||||
|
enabled = true
|
||||||
|
# Port to use for Supabase Studio.
|
||||||
|
port = 54323
|
||||||
|
# External URL of the API server that frontend connects to.
|
||||||
|
api_url = "http://127.0.0.1"
|
||||||
|
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
|
||||||
|
openai_api_key = "env(OPENAI_API_KEY)"
|
||||||
|
|
||||||
|
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
|
||||||
|
# are monitored, and you can view the emails that would have been sent from the web interface.
|
||||||
|
[inbucket]
|
||||||
|
enabled = true
|
||||||
|
# Port to use for the email testing server web interface.
|
||||||
|
port = 54324
|
||||||
|
# Uncomment to expose additional ports for testing user applications that send emails.
|
||||||
|
# smtp_port = 54325
|
||||||
|
# pop3_port = 54326
|
||||||
|
|
||||||
|
[storage]
|
||||||
|
enabled = true
|
||||||
|
# The maximum file size allowed (e.g. "5MB", "500KB").
|
||||||
|
file_size_limit = "50MiB"
|
||||||
|
|
||||||
|
[storage.image_transformation]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
# Uncomment to configure local storage buckets
|
||||||
|
# [storage.buckets.images]
|
||||||
|
# public = false
|
||||||
|
# file_size_limit = "50MiB"
|
||||||
|
# allowed_mime_types = ["image/png", "image/jpeg"]
|
||||||
|
# objects_path = "./images"
|
||||||
|
|
||||||
|
[auth]
|
||||||
|
enabled = true
|
||||||
|
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||||
|
# in emails.
|
||||||
|
site_url = "http://127.0.0.1:3000"
|
||||||
|
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||||
|
additional_redirect_urls = ["https://127.0.0.1:3000"]
|
||||||
|
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||||
|
jwt_expiry = 3600
|
||||||
|
# If disabled, the refresh token will never expire.
|
||||||
|
enable_refresh_token_rotation = true
|
||||||
|
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
|
||||||
|
# Requires enable_refresh_token_rotation = true.
|
||||||
|
refresh_token_reuse_interval = 10
|
||||||
|
# Allow/disallow new user signups to your project.
|
||||||
|
enable_signup = true
|
||||||
|
# Allow/disallow anonymous sign-ins to your project.
|
||||||
|
enable_anonymous_sign_ins = false
|
||||||
|
# Allow/disallow testing manual linking of accounts
|
||||||
|
enable_manual_linking = false
|
||||||
|
|
||||||
|
[auth.email]
|
||||||
|
# Allow/disallow new user signups via email to your project.
|
||||||
|
enable_signup = true
|
||||||
|
# If enabled, a user will be required to confirm any email change on both the old, and new email
|
||||||
|
# addresses. If disabled, only the new email is required to confirm.
|
||||||
|
double_confirm_changes = true
|
||||||
|
# If enabled, users need to confirm their email address before signing in.
|
||||||
|
enable_confirmations = false
|
||||||
|
# If enabled, users will need to reauthenticate or have logged in recently to change their password.
|
||||||
|
secure_password_change = false
|
||||||
|
# Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
|
||||||
|
max_frequency = "1s"
|
||||||
|
# Number of characters used in the email OTP.
|
||||||
|
otp_length = 6
|
||||||
|
# Number of seconds before the email OTP expires (defaults to 1 hour).
|
||||||
|
otp_expiry = 3600
|
||||||
|
|
||||||
|
# Use a production-ready SMTP server
|
||||||
|
# [auth.email.smtp]
|
||||||
|
# host = "smtp.sendgrid.net"
|
||||||
|
# port = 587
|
||||||
|
# user = "apikey"
|
||||||
|
# pass = "env(SENDGRID_API_KEY)"
|
||||||
|
# admin_email = "admin@email.com"
|
||||||
|
# sender_name = "Admin"
|
||||||
|
|
||||||
|
# Uncomment to customize email template
|
||||||
|
# [auth.email.template.invite]
|
||||||
|
# subject = "You have been invited"
|
||||||
|
# content_path = "./supabase/templates/invite.html"
|
||||||
|
|
||||||
|
[auth.sms]
|
||||||
|
# Allow/disallow new user signups via SMS to your project.
|
||||||
|
enable_signup = true
|
||||||
|
# If enabled, users need to confirm their phone number before signing in.
|
||||||
|
enable_confirmations = false
|
||||||
|
# Template for sending OTP to users
|
||||||
|
template = "Your code is {{ .Code }} ."
|
||||||
|
# Controls the minimum amount of time that must pass before sending another sms otp.
|
||||||
|
max_frequency = "5s"
|
||||||
|
|
||||||
|
# Use pre-defined map of phone number to OTP for testing.
|
||||||
|
# [auth.sms.test_otp]
|
||||||
|
# 4152127777 = "123456"
|
||||||
|
|
||||||
|
# Configure logged in session timeouts.
|
||||||
|
# [auth.sessions]
|
||||||
|
# Force log out after the specified duration.
|
||||||
|
# timebox = "24h"
|
||||||
|
# Force log out if the user has been inactive longer than the specified duration.
|
||||||
|
# inactivity_timeout = "8h"
|
||||||
|
|
||||||
|
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
|
||||||
|
# [auth.hook.custom_access_token]
|
||||||
|
# enabled = true
|
||||||
|
# uri = "pg-functions://<database>/<schema>/<hook_name>"
|
||||||
|
|
||||||
|
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
|
||||||
|
[auth.sms.twilio]
|
||||||
|
enabled = false
|
||||||
|
account_sid = ""
|
||||||
|
message_service_sid = ""
|
||||||
|
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
|
||||||
|
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
|
||||||
|
|
||||||
|
[auth.mfa]
|
||||||
|
# Control how many MFA factors can be enrolled at once per user.
|
||||||
|
max_enrolled_factors = 10
|
||||||
|
|
||||||
|
# Control use of MFA via App Authenticator (TOTP)
|
||||||
|
[auth.mfa.totp]
|
||||||
|
enroll_enabled = true
|
||||||
|
verify_enabled = true
|
||||||
|
|
||||||
|
# Configure Multi-factor-authentication via Phone Messaging
|
||||||
|
# [auth.mfa.phone]
|
||||||
|
# enroll_enabled = true
|
||||||
|
# verify_enabled = true
|
||||||
|
# otp_length = 6
|
||||||
|
# template = "Your code is {{ .Code }} ."
|
||||||
|
# max_frequency = "10s"
|
||||||
|
|
||||||
|
# Configure Multi-factor-authentication via WebAuthn
|
||||||
|
# [auth.mfa.web_authn]
|
||||||
|
# enroll_enabled = true
|
||||||
|
# verify_enabled = true
|
||||||
|
|
||||||
|
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
|
||||||
|
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
|
||||||
|
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
|
||||||
|
[auth.external.apple]
|
||||||
|
enabled = false
|
||||||
|
client_id = ""
|
||||||
|
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
|
||||||
|
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
|
||||||
|
# Overrides the default auth redirectUrl.
|
||||||
|
redirect_uri = ""
|
||||||
|
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
|
||||||
|
# or any other third-party OIDC providers.
|
||||||
|
url = ""
|
||||||
|
# If enabled, the nonce check will be skipped. Required for local sign in with Google auth.
|
||||||
|
skip_nonce_check = false
|
||||||
|
|
||||||
|
# Use Firebase Auth as a third-party provider alongside Supabase Auth.
|
||||||
|
[auth.third_party.firebase]
|
||||||
|
enabled = false
|
||||||
|
# project_id = "my-firebase-project"
|
||||||
|
|
||||||
|
# Use Auth0 as a third-party provider alongside Supabase Auth.
|
||||||
|
[auth.third_party.auth0]
|
||||||
|
enabled = false
|
||||||
|
# tenant = "my-auth0-tenant"
|
||||||
|
# tenant_region = "us"
|
||||||
|
|
||||||
|
# Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth.
|
||||||
|
[auth.third_party.aws_cognito]
|
||||||
|
enabled = false
|
||||||
|
# user_pool_id = "my-user-pool-id"
|
||||||
|
# user_pool_region = "us-east-1"
|
||||||
|
|
||||||
|
[edge_runtime]
|
||||||
|
enabled = true
|
||||||
|
# Configure one of the supported request policies: `oneshot`, `per_worker`.
|
||||||
|
# Use `oneshot` for hot reload, or `per_worker` for load testing.
|
||||||
|
policy = "oneshot"
|
||||||
|
inspector_port = 8083
|
||||||
|
|
||||||
|
[analytics]
|
||||||
|
enabled = true
|
||||||
|
port = 54327
|
||||||
|
# Configure one of the supported backends: `postgres`, `bigquery`.
|
||||||
|
backend = "postgres"
|
||||||
|
|
||||||
|
# Experimental features may be deprecated any time
|
||||||
|
[experimental]
|
||||||
|
# Configures Postgres storage engine to use OrioleDB (S3)
|
||||||
|
orioledb_version = ""
|
||||||
|
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
|
||||||
|
s3_host = "env(S3_HOST)"
|
||||||
|
# Configures S3 bucket region, eg. us-east-1
|
||||||
|
s3_region = "env(S3_REGION)"
|
||||||
|
# Configures AWS_ACCESS_KEY_ID for S3 bucket
|
||||||
|
s3_access_key = "env(S3_ACCESS_KEY)"
|
||||||
|
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
|
||||||
|
s3_secret_key = "env(S3_SECRET_KEY)"
|
||||||
8
backend/uv.lock
Normal file
8
backend/uv.lock
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
version = 1
|
||||||
|
revision = 1
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastapi-supabase-template"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = { virtual = "." }
|
||||||
1
scripts/.python-version
Normal file
1
scripts/.python-version
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
3.13
|
||||||
0
scripts/README.md
Normal file
0
scripts/README.md
Normal file
12
scripts/main.py
Normal file
12
scripts/main.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
API_KEY = "26b632b940cbf3186ac62ce7ffc00de8-e298dd8e-96f862e4"
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
print("ho")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
10
scripts/pyproject.toml
Normal file
10
scripts/pyproject.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[project]
|
||||||
|
name = "scripts"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
dependencies = [
|
||||||
|
"pandas>=2.2.3",
|
||||||
|
"ruff>=0.9.9",
|
||||||
|
]
|
||||||
266
scripts/uv.lock
Normal file
266
scripts/uv.lock
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
version = 1
|
||||||
|
revision = 1
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.12'",
|
||||||
|
"python_full_version == '3.11.*'",
|
||||||
|
"python_full_version == '3.10.*'",
|
||||||
|
"python_full_version < '3.10'",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version < '3.10'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a9/75/10dd1f8116a8b796cb2c737b674e02d02e80454bda953fa7e65d8c12b016/numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78", size = 18902015 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/91/3495b3237510f79f5d81f2508f9f13fea78ebfdf07538fc7444badda173d/numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece", size = 21165245 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/33/26178c7d437a87082d11019292dce6d3fe6f0e9026b7b2309cbf3e489b1d/numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04", size = 13738540 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/31/cc46e13bf07644efc7a4bf68df2df5fb2a1a88d0cd0da9ddc84dc0033e51/numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66", size = 5300623 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/16/7bfcebf27bb4f9d7ec67332ffebee4d1bf085c84246552d52dbb548600e7/numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b", size = 6901774 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/a3/561c531c0e8bf082c5bef509d00d56f82e0ea7e1e3e3a7fc8fa78742a6e5/numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd", size = 13907081 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/66/f7177ab331876200ac7563a580140643d1179c8b4b6a6b0fc9838de2a9b8/numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318", size = 19523451 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/7f/0b209498009ad6453e4efc2c65bcdf0ae08a182b2b7877d7ab38a92dc542/numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8", size = 19927572 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/df/2619393b1e1b565cd2d4c4403bdd979621e2c4dea1f8532754b2598ed63b/numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326", size = 14400722 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/ad/77e921b9f256d5da36424ffb711ae79ca3f451ff8489eeca544d0701d74a/numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97", size = 6472170 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/05/3442317535028bc29cf0c0dd4c191a4481e8376e9f0db6bcf29703cadae6/numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131", size = 15905558 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/cf/034500fb83041aa0286e0fb16e7c76e5c8b67c0711bb6e9e9737a717d5fe/numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448", size = 21169137 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/d9/32de45561811a4b87fbdee23b5797394e3d1504b4a7cf40c10199848893e/numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195", size = 13703552 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/ca/2f384720020c7b244d22508cb7ab23d95f179fcfff33c31a6eeba8d6c512/numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57", size = 5298957 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/78/a3e4f9fb6aa4e6fdca0c5428e8ba039408514388cf62d89651aade838269/numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a", size = 6905573 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/72/cfc3a1beb2caf4efc9d0b38a15fe34025230da27e1c08cc2eb9bfb1c7231/numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669", size = 13914330 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/a8/c17acf65a931ce551fee11b72e8de63bf7e8a6f0e21add4c937c83563538/numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951", size = 19534895 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/86/8767f3d54f6ae0165749f84648da9dcc8cd78ab65d415494962c86fac80f/numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9", size = 19937253 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/87/f76450e6e1c14e5bb1eae6836478b1028e096fd02e85c1c37674606ab752/numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15", size = 14414074 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/ca/0f0f328e1e59f73754f06e1adfb909de43726d4f24c6a3f8805f34f2b0fa/numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4", size = 6470640 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/57/3a3f14d3a759dcf9bf6e9eda905794726b758819df4663f217d658a58695/numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc", size = 15910230 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/40/2e117be60ec50d98fa08c2f8c48e09b3edea93cfcabd5a9ff6925d54b1c2/numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b", size = 20895803 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/92/1b8b8dee833f53cef3e0a3f69b2374467789e0bb7399689582314df02651/numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e", size = 13471835 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/19/e2793bde475f1edaea6945be141aef6c8b4c669b90c90a300a8954d08f0a/numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c", size = 5038499 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/ff/ddf6dac2ff0dd50a7327bcdba45cb0264d0e96bb44d33324853f781a8f3c/numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c", size = 6633497 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/21/67f36eac8e2d2cd652a2e69595a54128297cdcb1ff3931cfc87838874bd4/numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692", size = 13621158 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/68/e9f1126d757653496dbc096cb429014347a36b228f5a991dae2c6b6cfd40/numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a", size = 19236173 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/e9/1f5333281e4ebf483ba1c888b1d61ba7e78d7e910fdd8e6499667041cc35/numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c", size = 19634174 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/af/a469674070c8d8408384e3012e064299f7a2de540738a8e414dcfd639996/numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded", size = 14099701 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/3d/08ea9f239d0e0e939b6ca52ad403c84a2bce1bde301a8eb4888c1c1543f1/numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5", size = 6174313 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/b5/4ac39baebf1fdb2e72585c8352c56d063b6126be9fc95bd2bb5ef5770c20/numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a", size = 15606179 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/c1/41c8f6df3162b0c6ffd4437d729115704bd43363de0090c7f913cfbc2d89/numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c", size = 21169942 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/bc/fd298f308dcd232b56a4031fd6ddf11c43f9917fbc937e53762f7b5a3bb1/numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd", size = 13711512 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/ff/06d1aa3eeb1c614eda245c1ba4fb88c483bee6520d361641331872ac4b82/numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b", size = 5306976 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/98/121996dcfb10a6087a05e54453e28e58694a7db62c5a5a29cee14c6e047b/numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729", size = 6906494 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/31/9dffc70da6b9bbf7968f6551967fc21156207366272c2a40b4ed6008dc9b/numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1", size = 13912596 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/14/78635daab4b07c0930c919d451b8bf8c164774e6a3413aed04a6d95758ce/numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd", size = 19526099 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/4c/0eeca4614003077f68bfe7aac8b7496f04221865b3a5e7cb230c9d055afd/numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d", size = 19932823 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/46/ea25b98b13dccaebddf1a803f8c748680d972e00507cd9bc6dcdb5aa2ac1/numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d", size = 14404424 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/a6/177dd88d95ecf07e722d21008b1b40e681a929eb9e329684d449c36586b2/numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa", size = 6476809 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/2b/7fc9f4e7ae5b507c1a3a21f0f15ed03e794c1242ea8a242ac158beb56034/numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73", size = 15911314 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/3b/df5a870ac6a3be3a86856ce195ef42eec7ae50d2a202be1f5a4b3b340e14/numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8", size = 21025288 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/97/51af92f18d6f6f2d9ad8b482a99fb74e142d71372da5d834b3a2747a446e/numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4", size = 6762793 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/46/de1fbd0c1b5ccaa7f9a005b66761533e2f6a3e560096682683a223631fe9/numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c", size = 19334885 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/dc/d330a6faefd92b446ec0f0dfea4c3207bb1fef3c4771d19cf4543efd2c78/numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385", size = 15828784 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.2.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.12'",
|
||||||
|
"python_full_version == '3.11.*'",
|
||||||
|
"python_full_version == '3.10.*'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fb/90/8956572f5c4ae52201fdec7ba2044b2c882832dcec7d5d0922c9e9acf2de/numpy-2.2.3.tar.gz", hash = "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020", size = 20262700 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/e1/1816d5d527fa870b260a1c2c5904d060caad7515637bd54f495a5ce13ccd/numpy-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cbc6472e01952d3d1b2772b720428f8b90e2deea8344e854df22b0618e9cce71", size = 21232911 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/46/9f25dc19b359f10c0e52b6bac25d3181eb1f4b4d04c9846a32cf5ea52762/numpy-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cdfe0c22692a30cd830c0755746473ae66c4a8f2e7bd508b35fb3b6a0813d787", size = 14371955 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/d7/de941296e6b09a5c81d3664ad912f1496a0ecdd2f403318e5e35604ff70f/numpy-2.2.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e37242f5324ffd9f7ba5acf96d774f9276aa62a966c0bad8dae692deebec7716", size = 5410476 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/ce/55f685995110f8a268fdca0f198c9a84fa87b39512830965cc1087af6391/numpy-2.2.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:95172a21038c9b423e68be78fd0be6e1b97674cde269b76fe269a5dfa6fadf0b", size = 6945730 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/84/abdb9f6e22576d89c259401c3234d4755b322539491bbcffadc8bcb120d3/numpy-2.2.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5b47c440210c5d1d67e1cf434124e0b5c395eee1f5806fdd89b553ed1acd0a3", size = 14350752 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/88/3870cfa9bef4dffb3a326507f430e6007eeac258ebeef6b76fc542aef66d/numpy-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0391ea3622f5c51a2e29708877d56e3d276827ac5447d7f45e9bc4ade8923c52", size = 16399386 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/10/3f629682dd0b457525c131945329c4e81e2dadeb11256e6ce4c9a1a6fb41/numpy-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f6b3dfc7661f8842babd8ea07e9897fe3d9b69a1d7e5fbb743e4160f9387833b", size = 15561826 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/18/fd35673ba9751eba449d4ce5d24d94e3b612cdbfba79348da71488c0b7ac/numpy-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1ad78ce7f18ce4e7df1b2ea4019b5817a2f6a8a16e34ff2775f646adce0a5027", size = 18188593 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/4c/c0f897b580ea59484b4cc96a441fea50333b26675a60a1421bc912268b5f/numpy-2.2.3-cp310-cp310-win32.whl", hash = "sha256:5ebeb7ef54a7be11044c33a17b2624abe4307a75893c001a4800857956b41094", size = 6590421 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/5b/aaabbfc7060c5c8f0124c5deb5e114a3b413a548bbc64e372c5b5db36165/numpy-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:596140185c7fa113563c67c2e894eabe0daea18cf8e33851738c19f70ce86aeb", size = 12925667 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/86/453aa3949eab6ff54e2405f9cb0c01f756f031c3dc2a6d60a1d40cba5488/numpy-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8", size = 21237256 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/c3/93ecceadf3e155d6a9e4464dd2392d8d80cf436084c714dc8535121c83e8/numpy-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b", size = 14408049 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/29/076999b69bd9264b8df5e56f2be18da2de6b2a2d0e10737e5307592e01de/numpy-2.2.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a", size = 5408655 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/a7/b14f0a73eb0fe77cb9bd5b44534c183b23d4229c099e339c522724b02678/numpy-2.2.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636", size = 6949996 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/2f/8063da0616bb0f414b66dccead503bd96e33e43685c820e78a61a214c098/numpy-2.2.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d", size = 14355789 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/d7/3cd47b00b8ea95ab358c376cf5602ad21871410950bc754cf3284771f8b6/numpy-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb", size = 16411356 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/c0/a2379e202acbb70b85b41483a422c1e697ff7eee74db642ca478de4ba89f/numpy-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2", size = 15576770 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/63/a13ee650f27b7999e5b9e1964ae942af50bb25606d088df4229283eda779/numpy-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b", size = 18200483 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4c/87/e71f89935e09e8161ac9c590c82f66d2321eb163893a94af749dfa8a3cf8/numpy-2.2.3-cp311-cp311-win32.whl", hash = "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5", size = 6588415 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/c6/cd4298729826af9979c5f9ab02fcaa344b82621e7c49322cd2d210483d3f/numpy-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f", size = 12929604 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/ec/43628dcf98466e087812142eec6d1c1a6c6bdfdad30a0aa07b872dc01f6f/numpy-2.2.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d", size = 20929458 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/c0/2f4225073e99a5c12350954949ed19b5d4a738f541d33e6f7439e33e98e4/numpy-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95", size = 14115299 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/fa/d2c5575d9c734a7376cc1592fae50257ec95d061b27ee3dbdb0b3b551eb2/numpy-2.2.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea", size = 5145723 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/dc/023dad5b268a7895e58e791f28dc1c60eb7b6c06fcbc2af8538ad069d5f3/numpy-2.2.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532", size = 6678797 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/19/bcd641ccf19ac25abb6fb1dcd7744840c11f9d62519d7057b6ab2096eb60/numpy-2.2.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e", size = 14067362 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/04/78d2e7402fb479d893953fb78fa7045f7deb635ec095b6b4f0260223091a/numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe", size = 16116679 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/a1/e90f7aa66512be3150cb9d27f3d9995db330ad1b2046474a13b7040dfd92/numpy-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021", size = 15264272 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/b6/50bd027cca494de4fa1fc7bf1662983d0ba5f256fa0ece2c376b5eb9b3f0/numpy-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8", size = 17880549 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/30/f7bf4acb5f8db10a96f73896bdeed7a63373137b131ca18bd3dab889db3b/numpy-2.2.3-cp312-cp312-win32.whl", hash = "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe", size = 6293394 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/6e/55580a538116d16ae7c9aa17d4edd56e83f42126cb1dfe7a684da7925d2c/numpy-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d", size = 12626357 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/8b/88b98ed534d6a03ba8cddb316950fe80842885709b58501233c29dfa24a9/numpy-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba", size = 20916001 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/b4/def6ec32c725cc5fbd8bdf8af80f616acf075fe752d8a23e895da8c67b70/numpy-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50", size = 14130721 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/60/70af0acc86495b25b672d403e12cb25448d79a2b9658f4fc45e845c397a8/numpy-2.2.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1", size = 5130999 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/69/d96c006fb73c9a47bcb3611417cf178049aae159afae47c48bd66df9c536/numpy-2.2.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5", size = 6665299 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/3f/d8a877b6e48103733ac224ffa26b30887dc9944ff95dffdfa6c4ce3d7df3/numpy-2.2.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2", size = 14064096 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/43/619c2c7a0665aafc80efca465ddb1f260287266bdbdce517396f2f145d49/numpy-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1", size = 16114758 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/79/ee4fe4f60967ccd3897aa71ae14cdee9e3c097e3256975cc9575d393cb42/numpy-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304", size = 15259880 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/c8/8b55cf05db6d85b7a7d414b3d1bd5a740706df00bfa0824a08bf041e52ee/numpy-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d", size = 17876721 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/d6/b4c2f0564b7dcc413117b0ffbb818d837e4b29996b9234e38b2025ed24e7/numpy-2.2.3-cp313-cp313-win32.whl", hash = "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693", size = 6290195 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/e7/7d55a86719d0de7a6a597949f3febefb1009435b79ba510ff32f05a8c1d7/numpy-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b", size = 12619013 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/1f/0b863d5528b9048fd486a56e0b97c18bf705e88736c8cea7239012119a54/numpy-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890", size = 20944621 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/99/b478c384f7a0a2e0736177aafc97dc9152fc036a3fdb13f5a3ab225f1494/numpy-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c", size = 14142502 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/61/2d9a694a0f9cd0a839501d362de2a18de75e3004576a3008e56bdd60fcdb/numpy-2.2.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94", size = 5176293 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/35/51e94011b23e753fa33f891f601e5c1c9a3d515448659b06df9d40c0aa6e/numpy-2.2.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0", size = 6691874 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/cf/06e37619aad98a9d03bd8d65b8e3041c3a639be0f5f6b0a0e2da544538d4/numpy-2.2.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610", size = 14036826 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/93/5d7d19955abd4d6099ef4a8ee006f9ce258166c38af259f9e5558a172e3e/numpy-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76", size = 16096567 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/53/d1c599acf7732d81f46a93621dab6aa8daad914b502a7a115b3f17288ab2/numpy-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a", size = 15242514 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/43/c0f5411c7b3ea90adf341d05ace762dad8cb9819ef26093e27b15dd121ac/numpy-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf", size = 17872920 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/57/6dbdd45ab277aff62021cafa1e15f9644a52f5b5fc840bc7591b4079fb58/numpy-2.2.3-cp313-cp313t-win32.whl", hash = "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef", size = 6346584 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/9b/484f7d04b537d0a1202a5ba81c6f53f1846ae6c63c2127f8df869ed31342/numpy-2.2.3-cp313-cp313t-win_amd64.whl", hash = "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082", size = 12706784 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0a/b5/a7839f5478be8f859cb880f13d90fcfe4b0ec7a9ebaff2bcc30d96760596/numpy-2.2.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3c2ec8a0f51d60f1e9c0c5ab116b7fc104b165ada3f6c58abf881cb2eb16044d", size = 21064244 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/e8/5da32ffcaa7a72f7ecd82f90c062140a061eb823cb88e90279424e515cf4/numpy-2.2.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:ed2cf9ed4e8ebc3b754d398cba12f24359f018b416c380f577bbae112ca52fc9", size = 6809418 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/a9/68aa7076c7656a7308a0f73d0a2ced8c03f282c9fd98fa7ce21c12634087/numpy-2.2.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39261798d208c3095ae4f7bc8eaeb3481ea8c6e03dc48028057d3cbdbdb8937e", size = 16215461 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/7f/d322a4125405920401450118dbdc52e0384026bd669939484670ce8b2ab9/numpy-2.2.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:783145835458e60fa97afac25d511d00a1eca94d4a8f3ace9fe2043003c678e4", size = 12839607 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pandas"
|
||||||
|
version = "2.2.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy", version = "2.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||||
|
{ name = "numpy", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "pytz" },
|
||||||
|
{ name = "tzdata" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/70/c853aec59839bceed032d52010ff5f1b8d87dc3114b762e4ba2727661a3b/pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5", size = 12580827 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/f2/c4527768739ffa4469b2b4fff05aa3768a478aed89a2f271a79a40eee984/pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348", size = 11303897 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/12/86c1747ea27989d7a4064f806ce2bae2c6d575b950be087837bdfcabacc9/pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed", size = 66480908 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/50/7db2cd5e6373ae796f0ddad3675268c8d59fb6076e66f0c339d61cea886b/pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57", size = 13064210 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/61/a89015a6d5536cb0d6c3ba02cebed51a95538cf83472975275e28ebf7d0c/pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42", size = 16754292 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/0d/4cc7b69ce37fac07645a94e1d4b0880b15999494372c1523508511b09e40/pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f", size = 14416379 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/9e/6ebb433de864a6cd45716af52a4d7a8c3c9aaf3a98368e61db9e69e69a9c/pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645", size = 11598471 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a8/44/d9502bf0ed197ba9bf1103c9867d5904ddcaf869e52329787fc54ed70cc8/pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039", size = 12602222 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/11/9eac327a38834f162b8250aab32a6781339c69afe7574368fffe46387edf/pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd", size = 11321274 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/fb/c4beeb084718598ba19aa9f5abbc8aed8b42f90930da861fcb1acdb54c3a/pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698", size = 15579836 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/5f/4dba1d39bb9c38d574a9a22548c540177f78ea47b32f99c0ff2ec499fac5/pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc", size = 13058505 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/57/708135b90391995361636634df1f1130d03ba456e95bcf576fada459115a/pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3", size = 16744420 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/4a/03ed6b7ee323cf30404265c284cee9c65c56a212e0a08d9ee06984ba2240/pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32", size = 14440457 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/8c/87ddf1fcb55d11f9f847e3c69bb1c6f8e46e2f40ab1a2d2abadb2401b007/pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5", size = 11617166 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/a3/fb2734118db0af37ea7433f57f722c0a56687e14b14690edff0cdb4b7e58/pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9", size = 12529893 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/0c/ad295fd74bfac85358fd579e271cded3ac969de81f62dd0142c426b9da91/pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4", size = 11363475 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/2a/4bba3f03f7d07207481fed47f5b35f556c7441acddc368ec43d6643c5777/pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3", size = 15188645 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/38/f8/d8fddee9ed0d0c0f4a2132c1dfcf0e3e53265055da8df952a53e7eaf178c/pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319", size = 12739445 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/e8/45a05d9c39d2cea61ab175dbe6a2de1d05b679e8de2011da4ee190d7e748/pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8", size = 16359235 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/99/617d07a6a5e429ff90c90da64d428516605a1ec7d7bea494235e1c3882de/pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a", size = 14056756 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/8c/8848a4c9b8fdf5a534fe2077af948bf53cd713d77ffbcd7bd15710348fd7/pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39", size = 12595535 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/b9/5cead4f63b6d31bdefeb21a679bc5a7f4aaf262ca7e07e2bc1c341b68470/pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30", size = 11319822 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/af/89e35619fb573366fa68dc26dad6ad2c08c17b8004aad6d98f1a31ce4bb3/pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c", size = 15625439 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/dd/bed19c2974296661493d7acc4407b1d2db4e2a482197df100f8f965b6225/pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c", size = 13068928 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/a3/18508e10a31ea108d746c848b5a05c0711e0278fa0d6f1c52a8ec52b80a5/pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea", size = 16783266 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/a5/3429bd13d82bebc78f4d78c3945efedef63a7cd0c15c17b2eeb838d1121f/pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761", size = 14450871 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/49/5c30646e96c684570925b772eac4eb0a8cb0ca590fa978f56c5d3ae73ea1/pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e", size = 11618011 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytz"
|
||||||
|
version = "2025.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.9.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6f/c3/418441a8170e8d53d05c0b9dad69760dbc7b8a12c10dbe6db1e1205d2377/ruff-0.9.9.tar.gz", hash = "sha256:0062ed13f22173e85f8f7056f9a24016e692efeea8704d1a5e8011b8aa850933", size = 3717448 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/c3/2c4afa9ba467555d074b146d9aed0633a56ccdb900839fb008295d037b89/ruff-0.9.9-py3-none-linux_armv6l.whl", hash = "sha256:628abb5ea10345e53dff55b167595a159d3e174d6720bf19761f5e467e68d367", size = 10027252 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/d1/439e58487cf9eac26378332e25e7d5ade4b800ce1eec7dc2cfc9b0d7ca96/ruff-0.9.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cd1428e834b35d7493354723543b28cc11dc14d1ce19b685f6e68e07c05ec7", size = 10840721 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/44/fead822c38281ba0122f1b76b460488a175a9bd48b130650a6fb6dbcbcf9/ruff-0.9.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5ee162652869120ad260670706f3cd36cd3f32b0c651f02b6da142652c54941d", size = 10161439 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/ae/d404a2ab8e61ddf6342e09cc6b7f7846cce6b243e45c2007dbe0ca928a5d/ruff-0.9.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa0f6b75082c9be1ec5a1db78c6d4b02e2375c3068438241dc19c7c306cc61a", size = 10336264 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/4e/7c268aa7d84cd709fb6f046b8972313142cffb40dfff1d2515c5e6288d54/ruff-0.9.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:584cc66e89fb5f80f84b05133dd677a17cdd86901d6479712c96597a3f28e7fe", size = 9908774 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/26/c618a878367ef1b76270fd027ca93692657d3f6122b84ba48911ef5f2edc/ruff-0.9.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf3369325761a35aba75cd5c55ba1b5eb17d772f12ab168fbfac54be85cf18c", size = 11428127 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/9a/c5588a93d9bfed29f565baf193fe802fa676a0c837938137ea6cf0576d8c/ruff-0.9.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3403a53a32a90ce929aa2f758542aca9234befa133e29f4933dcef28a24317be", size = 12133187 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/ff/e7980a7704a60905ed7e156a8d73f604c846d9bd87deda9cabfa6cba073a/ruff-0.9.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18454e7fa4e4d72cffe28a37cf6a73cb2594f81ec9f4eca31a0aaa9ccdfb1590", size = 11602937 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/78/3690444ad9e3cab5c11abe56554c35f005b51d1d118b429765249095269f/ruff-0.9.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fadfe2c88724c9617339f62319ed40dcdadadf2888d5afb88bf3adee7b35bfb", size = 13771698 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/bf/e477c2faf86abe3988e0b5fd22a7f3520e820b2ee335131aca2e16120038/ruff-0.9.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6df104d08c442a1aabcfd254279b8cc1e2cbf41a605aa3e26610ba1ec4acf0b0", size = 11249026 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/82/cdaffd59e5a8cb5b14c408c73d7a555a577cf6645faaf83e52fe99521715/ruff-0.9.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d7c62939daf5b2a15af48abbd23bea1efdd38c312d6e7c4cedf5a24e03207e17", size = 10220432 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/a4/2507d0026225efa5d4412b6e294dfe54725a78652a5c7e29e6bd0fc492f3/ruff-0.9.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9494ba82a37a4b81b6a798076e4a3251c13243fc37967e998efe4cce58c8a8d1", size = 9874602 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/be/f3aab1813846b476c4bcffe052d232244979c3cd99d751c17afb530ca8e4/ruff-0.9.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4efd7a96ed6d36ef011ae798bf794c5501a514be369296c672dab7921087fa57", size = 10851212 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/45/8e5fd559bea0d2f57c4e12bf197a2fade2fac465aa518284f157dfbca92b/ruff-0.9.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ab90a7944c5a1296f3ecb08d1cbf8c2da34c7e68114b1271a431a3ad30cb660e", size = 11327490 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/55/e6c90f13880aeef327746052907e7e930681f26a164fe130ddac28b08269/ruff-0.9.9-py3-none-win32.whl", hash = "sha256:6b4c376d929c25ecd6d87e182a230fa4377b8e5125a4ff52d506ee8c087153c1", size = 10227912 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/b2/da925693cb82a1208aa34966c0f36cb222baca94e729dd22a587bc22d0f3/ruff-0.9.9-py3-none-win_amd64.whl", hash = "sha256:837982ea24091d4c1700ddb2f63b7070e5baec508e43b01de013dc7eff974ff1", size = 11355632 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/d8/de873d1c1b020d668d8ec9855d390764cb90cf8f6486c0983da52be8b7b7/ruff-0.9.9-py3-none-win_arm64.whl", hash = "sha256:3ac78f127517209fe6d96ab00f3ba97cafe38718b23b1db3e96d8b2d39e37ddf", size = 10435860 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scripts"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pandas" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "pandas", specifier = ">=2.2.3" },
|
||||||
|
{ name = "ruff", specifier = ">=0.9.9" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2025.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/43/0f/fa4723f22942480be4ca9527bbde8d43f6c3f2fe8412f00e7f5f6746bc8b/tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694", size = 194950 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/dd/84f10e23edd882c6f968c21c2434fe67bd4a528967067515feca9e611e5e/tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639", size = 346762 },
|
||||||
|
]
|
||||||
51
ui/README.md
51
ui/README.md
|
|
@ -1,50 +1 @@
|
||||||
# React + TypeScript + Vite
|
# XTablo Source Code
|
||||||
|
|
||||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
|
||||||
|
|
||||||
Currently, two official plugins are available:
|
|
||||||
|
|
||||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
|
||||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
|
||||||
|
|
||||||
## Expanding the ESLint configuration
|
|
||||||
|
|
||||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
|
||||||
|
|
||||||
- Configure the top-level `parserOptions` property like this:
|
|
||||||
|
|
||||||
```js
|
|
||||||
export default tseslint.config({
|
|
||||||
languageOptions: {
|
|
||||||
// other options...
|
|
||||||
parserOptions: {
|
|
||||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
||||||
tsconfigRootDir: import.meta.dirname,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
|
||||||
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
|
||||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// eslint.config.js
|
|
||||||
import react from 'eslint-plugin-react'
|
|
||||||
|
|
||||||
export default tseslint.config({
|
|
||||||
// Set the react version
|
|
||||||
settings: { react: { version: '18.3' } },
|
|
||||||
plugins: {
|
|
||||||
// Add the react plugin
|
|
||||||
react,
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
// other rules...
|
|
||||||
// Enable its recommended rules
|
|
||||||
...react.configs.recommended.rules,
|
|
||||||
...react.configs['jsx-runtime'].rules,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
Loading…
Reference in a new issue