Compare commits

..
78 Commits
Author SHA1 Message Date
Thastertyn 456570d4f0 README 2025-04-22 14:03:05 +02:00
Thastertyn 7db93e9e8a Dashboard fix 2025-04-22 10:09:40 +02:00
Thastertyn 6903a8deb0 Final commit (most likely) 2025-04-22 00:16:08 +02:00
Thastertyn f2af9dc566 Added customer list, coupon list and started working on purchase list 2025-04-21 19:29:01 +02:00
Thastertyn 2fe5dbcb21 Product listing got slight tweaks 2025-04-20 22:04:57 +02:00
Thastertyn c60ec969d5 Products are nearly finished, also switched to mock api 2025-04-19 11:02:13 +02:00
Thastertyn 0986336aea Merge branch 'main' of ssh://gitea.thastertyn.xyz:3022/Thastertyn/swag-shop 2025-04-18 13:15:09 +02:00
Thastertyn cd9f68dfc9 Currency work and token validation 2025-04-18 13:14:46 +02:00
Thastertyn d2101247d1 Added currency list for further usage in an input 2025-03-18 22:46:16 +01:00
Thastertyn cbb799f879 Minor tweaks to design 2025-03-18 22:26:36 +01:00
Thastertyn 09ddb05efe Shop about page 2025-03-18 15:26:16 +01:00
Thastertyn 260bef98cd Minor tweaks to sign up on frontend and database structure 2025-03-17 19:27:09 +01:00
Thastertyn d0690f2f06 Major addition. User register, login and update is now functional on frontend, removed UUID5 for UUID4 for users and many more changes 2025-03-16 22:24:54 +01:00
Thastertyn d87c280109 Shop registration works now 2025-03-14 13:40:50 +01:00
Thastertyn a0d238c840 Switched to UV instead of poetry 2025-03-14 12:44:09 +01:00
Thastertyn 9c2b88b103 Renamed hooks on frontend and started working on shop registration 2025-03-13 15:41:44 +01:00
Thastertyn 3b42ea6914 Fixed dependency versions to finally stop conflicts 2025-03-13 07:45:19 +01:00
Thastertyn 39b99d5509 Minor tweaks to routes and frontend. Added generated routes from openapi.json 2025-03-12 20:43:17 +01:00
Thastertyn 75b2b1e142 [rewrite] Added openapi dependency for generation of function 2025-03-12 14:01:36 +01:00
Thastertyn 20ef2aa4e8 [rewrite] Implemented login and register 2025-03-12 14:01:13 +01:00
Thastertyn cd8fdb9c21 [rewrite] WIP login and register 2025-03-11 21:53:37 +01:00
Thastertyn f5547be799 [rewrite] Changes services to crud, modified routes abit 2025-03-11 15:21:30 +01:00
Thastertyn 543aadf521 [rewrite] Disabled restart for docker-compose 2025-03-11 08:29:38 +01:00
Thastertyn 272e765ca8 [rewrite] Minor fixes to models, session generator and example .env 2025-03-10 22:02:30 +01:00
Thastertyn 71e916586e [rewrite] Major changes to code - switched to postgres, added initial backend docker files 2025-03-10 18:02:57 +01:00
Thastertyn c7e20fc935 [rewrite] Added a security module from a fastapi example 2025-02-24 22:32:49 +01:00
Thastertyn 9e56eeca3f [rewrite] Work on user registration, slightly polished schemas structure 2025-02-24 19:02:39 +01:00
Thastertyn 5d12391635 [rewrite] Updated backend structure a bit and started working on registration 2025-02-23 21:14:23 +01:00
Thastertyn f0c378b20f [rewrite] Updated models a bit, preparing to make first functional routes 2025-02-23 15:56:39 +01:00
Thastertyn 414b84a2d4 [rewrite] Added tasks, more debug options to vscode and slightly updated frontends structure 2025-02-23 10:45:45 +01:00
Thastertyn c3502f9ede [rewrite] Updated main page 2025-02-22 23:00:44 +01:00
Thastertyn a9c85cefe1 [rewrite] Added first steps for authentication 2025-02-21 16:51:37 +01:00
Thastertyn 073f01d070 [rewrite] Updated frontend structure and played with custom routes 2025-02-21 14:51:17 +01:00
Thastertyn 43063dcf4e [rewrite] Added missing lib file, updated debug launch.json for frontend and updated .gitignore 2025-02-20 21:06:04 +01:00
Thastertyn 6e9334ba0e [rewrite] Added shadcn ui to frontend and slightly tweaked backend 2025-02-20 15:16:06 +01:00
Thastertyn 4c8817e853 [rewrite] Removed old code, updated .vscode, for real this time 2025-02-17 22:03:45 +01:00
Thastertyn 3b0336d9b4 [rewrite] Removed old code, updated .vscode 2025-02-17 21:59:41 +01:00
Thastertyn 3c005dbfa4 [rewrite] Added a config manager and some test code 2025-02-17 18:24:13 +01:00
Thastertyn 807d23da51 [rewrite] Added poetry, and models for new database 2025-01-20 06:50:07 +01:00
Thastertyn 3d00ae0aad [rewrite] Added initial route structure (WIP) 2024-12-02 21:15:25 +01:00
Thastertyn bb789a75e7 [rewrite] Updated gitignore and requirements.txt 2024-12-02 09:03:12 +01:00
Thastertyn 40aa0295b1 [rewrite] Initial project structure with dashboard frontend and api 2024-12-02 08:45:08 +01:00
Thastertyn c018d2fb52 Massive rewrite of user and product logic. Still WIP 2024-06-02 22:41:14 +02:00
Thastertyn be7b8064d6 Update .vscode for convenience 2024-06-02 16:47:18 +02:00
Thastertyn 80dd89212f Unify json naming 2024-05-06 00:52:03 +02:00
Thastertyn 3aaa14c986 Cleanup in User code 2024-05-05 23:51:14 +02:00
Thastertyn 628d210156 Register swagger 2024-05-05 23:50:50 +02:00
Thastertyn 4fb09e8fd7 Email rework and additional notifications 2024-05-04 10:18:01 +02:00
Thastertyn a22d8becbb cspell settings (minor) 2024-05-02 20:45:29 +02:00
Thastertyn 4f86361ee8 Additional swagger 2024-05-02 20:44:26 +02:00
Thastertyn aef6dd2761 Added password for redis 2024-03-28 21:22:23 +01:00
Thastertyn 7342516ce7 Fixed mail sending 2024-03-14 22:13:52 +01:00
Thastertyn c310586fca Added sample .env 2024-03-14 22:13:27 +01:00
Thastertyn f23003e28d Changed product route from /products/get to /products 2024-03-13 21:47:57 +01:00
Thastertyn 69ca94c77b Used correct HTTP error 2024-03-13 21:41:45 +01:00
Thastertyn b1fb6ee099 Swagger documentation progress 2024-03-13 21:40:55 +01:00
Thastertyn f259aa3993 Work on swag documentation 2024-03-13 13:43:34 +01:00
Thastertyn 0f7d69899a Added flasgger requirement 2024-03-13 13:43:16 +01:00
Thastertyn dfccda02b8 Started work on swagger 2024-03-12 20:12:19 +01:00
Thastertyn 248bf45044 Updated README 2024-03-12 20:11:26 +01:00
Thastertyn 3c35ac4075 Fix using nonexistent variable 2024-03-11 08:20:54 +01:00
Thastertyn 3d6f50399c Updated requirements - missing bcrypt and redis 2024-03-11 08:19:59 +01:00
Thastertyn 56bc0f2d69 Added README 2024-03-10 23:16:30 +01:00
Thastertyn 2a2783fd09 Added simple code documentation 2024-03-10 23:10:41 +01:00
Thastertyn afc9db32e4 Use try ... with statements everywhere 2024-03-10 22:58:04 +01:00
Thastertyn 031ed358f1 Use correct HTTP error codes 2024-03-10 22:48:25 +01:00
Thastertyn 105bbd3e96 Product creation 2024-03-10 22:47:42 +01:00
Thastertyn f3f1652890 Update DB to allow creating image-less products 2024-03-10 22:47:23 +01:00
Thastertyn 2eae36e8fd Initial work on mail sending 2024-03-10 21:41:48 +01:00
Thastertyn bc0dae4f97 Added mail as dependency 2024-03-10 21:22:00 +01:00
Thastertyn fb4f91a1d6 Update for product fetching 2024-03-10 21:19:34 +01:00
Thastertyn caa8b76c88 Mistake in error message 2024-03-10 17:11:53 +01:00
Thastertyn 2694701337 Added product fetching and pages 2024-03-10 17:11:26 +01:00
Thastertyn 78aa03b2fb More RESTful name 2024-03-10 17:11:14 +01:00
Thastertyn e6ed37ad5e Cart and purchase 2024-03-10 16:24:36 +01:00
Thastertyn ca8366d950 New triggers, increased decimal sizes, clean up 2024-03-10 16:24:29 +01:00
Thastertyn a6cb854dd8 Another word 2024-03-10 16:24:02 +01:00
Thastertyn fd43b3658e Grammar 2024-03-09 10:13:59 +01:00
328 changed files with 33952 additions and 842 deletions

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
# Port for the app to run on
# PORT=31714
# Secret key used for cryptographic operations. Must be a long, random string.
SECRET_KEY=
# Token expiration time in minutes. Default is 8 days (60 min * 24 hours * 8 days).
# ACCESS_TOKEN_EXPIRE_MINUTES=11520
# The environment in which the application is running.
# Options: local, staging, production
ENVIRONMENT=local
# The frontend host that interacts with the backend.
FRONTEND_HOST=
# CORS origins allowed to access the backend.
# Multiple values should be comma-separated, e.g., "http://localhost,http://example.com"
BACKEND_CORS_ORIGINS=
# MySQL database configuration
POSTGRES_SERVER=
POSTGRES_PORT=3306
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB=
# SMTP configuration for sending emails
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
# Use TLS for email security
SMTP_TLS=True
# Set to True if using SSL instead of TLS
SMTP_SSL=False
# Email sender information
EMAILS_FROM_EMAIL=
EMAILS_FROM_NAME=
# Expiration time for password reset tokens (in hours)
# EMAIL_RESET_TOKEN_EXPIRE_HOURS=48
# A test user email used for automated email testing
EMAIL_TEST_USER=test@example.com
# Superuser credentials for the first administrator
FIRST_SUPERUSER=
FIRST_SUPERUSER_PASSWORD=
DOCKER_IMAGE_BACKEND=backend
DOCKER_IMAGE_FRONTEND=frontend
+296 -1
View File
@@ -1,2 +1,297 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
**/__pycache__/
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# ---> Python
# 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
# 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/
!frontend/src/lib
frontend/openapi.json
+50 -13
View File
@@ -1,23 +1,60 @@
{
// Pro informace o možných atributech použijte technologii IntelliSense.
// Umístěním ukazatele myši zobrazíte popisy existujících atributů.
// Další informace najdete tady: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Flask Shop",
"name": "Backend",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/main.py",
"args": [],
"cwd": "${workspaceFolder}",
"env": {
"FLASK_APP": "main.py",
"FLASK_ENV": "development"
},
"cwd": "${workspaceFolder}/backend",
"module": "fastapi",
"args": ["dev", "${cwd}/backend/app/main.py"],
"console": "internalConsole",
"autoReload": {"enable": true},
"justMyCode": true
"serverReadyAction":{
"action": "openExternally",
"killOnServerStop": false,
"pattern": "Application startup complete.",
"uriFormat": "http://localhost:8000/docs"
}
},
{
"name": "Frontend: Dev",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/frontend",
"runtimeArgs": [
"run",
"dev"
],
"runtimeExecutable": "npm",
"skipFiles": [
"<node_internals>/**"
],
"console": "internalConsole"
}
,
{
"name": "Frontend: Prod",
"type": "node",
"request": "launch",
"cwd": "${workspaceFolder}/frontend",
"runtimeArgs": [
"run",
"preview"
],
"runtimeExecutable": "npm",
"skipFiles": [
"<node_internals>/**"
],
"console": "internalConsole"
}
],
"compounds": [
{
"name": "Full Stack Debug",
"configurations": [
"Backend",
"Frontend: Dev"
]
}
]
}
+12 -10
View File
@@ -1,12 +1,14 @@
{
"cSpell.words": [
"blocklist",
"displayname",
"dotenv",
"gensalt",
"hashpw",
"checkpw",
"jsonify",
"rtype"
]
// #region Backend settings
"files.exclude" : { "**/__pycache__/**": true }, // Hide __pycache__ directories
"mypy-type-checker.args" : ["--config-file='backend/mypy.ini'"], // Override mypy config
"python.defaultInterpreterPath" : "./backend/.venv/bin/python", // Use venv by default
"python.analysis.extraPaths" : ["./backend"], // Pylint - fix for import analysis
"pylint.cwd" : "${workspaceFolder}/backend",
// #endregion
// #region Frontend settings
"prettier.configPath": "./frontend/.prettierrc", // Prettier config override
"biome.lspBin": "${workspaceFolder}/frontend"
// #endregion
}
+14
View File
@@ -0,0 +1,14 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "build",
"path": "frontend",
"group": "build",
"problemMatcher": [],
"label": "npm: build - frontend",
"detail": "tsc -b && vite build"
}
]
}
+55
View File
@@ -0,0 +1,55 @@
# Swag Shop
An e-commerence multitenant shop app.
## Core idea
This app provides a dashboard for managing individual shop data like products and similar things. Then it's up to the tenant to craft their own frontend and make use of the API.
## Running
### Backend
1. Dependencies
Straight forward, you need to install dependencies via *[uv](https://docs.astral.sh/uv/)*
```
cd backend
uv sync
```
2. Fill out `.env`
You can use `.env.example` as a reference
3. Run it!
```
fastapi dev backend/app/main.py
```
### Frontend
1. Dependencies
Using npm (or your favorite package manager)
```
cd frontend
npm install
```
2. Fill out `.env`
As with backend, there is `.env.example`
3. Run it!
There is a dev package script
```
npm run dev
```
<br>
- For deployment a `docker-compose.yml` is provided
## Tech Stack
- **PostgreSQL**
- **Backend** - *FastAPI* + *SQLModel*
- **Frontend** - *Vite* + *Typescript* + *ShadcnUI* + *Tanstack Query*
## Future plans
- True API integration
- Audit log
- Stripe integration
-20
View File
@@ -1,20 +0,0 @@
from flask import Flask
from flask_jwt_extended import JWTManager
app = Flask(__name__)
jwt_manager = JWTManager(app)
def create_app():
from app.api import bp, bp_errors, bp_product, bp_user, bp_cart
app.register_blueprint(bp)
app.register_blueprint(bp_errors)
app.register_blueprint(bp_product)
app.register_blueprint(bp_user)
app.register_blueprint(bp_cart)
from app.config import FlaskTesting, FlaskProduction
app.config.from_object(FlaskTesting)
from . import jwt_utils
return app
-9
View File
@@ -1,9 +0,0 @@
from flask import Blueprint
bp_errors = Blueprint('errors', __name__)
bp = Blueprint('api', __name__)
bp_product = Blueprint('product', __name__, url_prefix="/product")
bp_user = Blueprint('user', __name__, url_prefix="/user")
bp_cart = Blueprint('cart', __name__, url_prefix="/cart")
from . import routes
-1
View File
@@ -1 +0,0 @@
from app.api.routes import main_routes, error_routes, product_routes, user_routes, cart_routes
-16
View File
@@ -1,16 +0,0 @@
from flask import jsonify, abort, request
from flask_jwt_extended import jwt_required, get_jwt_identity
from app.api import bp_cart
from app.services.cart_service import CartService
@bp_cart.route('add/<int:product_id>', methods=['PUT'])
@jwt_required()
def add_to_cart(product_id: int):
user_id = get_jwt_identity()
count = request.args.get('count', default=1, type=int)
result, status_code = CartService.add_to_cart(user_id, product_id, count)
return result, status_code
-29
View File
@@ -1,29 +0,0 @@
from app.api import bp_errors
@bp_errors.app_errorhandler(400)
def bad_request(e):
return {"Bad Request": "The request was incorrectly formatted, or contained invalid data"}, 400
@bp_errors.app_errorhandler(401)
def unauthorized(e):
return {"Unauthorized": "Failed to authorize the request"}, 401
@bp_errors.app_errorhandler(403)
def forbidden(e):
return {"Forbidden": "Forbidden from accessing this resource. Try logging in"}, 403
@bp_errors.app_errorhandler(404)
def not_found(e):
return {"Not Found": "The requested resource was not found"}, 404
@bp_errors.app_errorhandler(405)
def method_not_allowed(e):
return {"Method Not Allowed": "The method used is not allowed in current context"}, 405
@bp_errors.app_errorhandler(500)
def internal_error(e):
return {"Internal Server Error": "An error occured on he server"}, 500
@bp_errors.app_errorhandler(501)
def internal_error(e):
return {"Not Implemented": "This function has not been implemented yet. Check back soon!"}, 501
-7
View File
@@ -1,7 +0,0 @@
from flask import jsonify, abort
from app.api import bp
@bp.route('/')
def hello():
return jsonify({'message': 'Hello, Flask!'})
-20
View File
@@ -1,20 +0,0 @@
from flask import jsonify, abort, request
from app.api import bp_product
from app.services.product_service import ProductService
@bp_product.route('/<int:id>', methods=['GET'])
def get_product_info(id: int):
fields = ['name', 'price', 'image', 'image_name', 'seller']
fields_param = request.args.get('fields')
requested_fields = []
return abort(501)
@bp_product.route('/create', methods=['POST'])
def create_product_listing():
name = request.json.get('name')
-134
View File
@@ -1,134 +0,0 @@
from app.api import bp_user
from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt
from flask import request, abort
from app.services.user_service import UserService
@bp_user.route('/register', methods=['POST'])
def register():
username = request.json.get('username')
displayname = request.json.get('displayname')
email = request.json.get('email')
password = request.json.get('password')
if username is None or email is None or password is None or displayname is None:
return abort(400)
result, status_code = UserService.register(username, displayname, email, password)
return result, status_code
@bp_user.route('/login', methods=['POST'])
def login():
username = request.json.get('username')
password = request.json.get('password')
if username is None or password is None:
return abort(400)
result, status_code = UserService.login(username, password)
return result, status_code
@bp_user.route('/logout', methods=['DELETE'])
@jwt_required()
def logout():
jwt = get_jwt()
jti = jwt['jti']
exp = jwt['exp']
result, status_code = UserService.logout(jti, exp)
return result, status_code
@bp_user.route('/update/username', methods=['PUT'])
@jwt_required()
def update_username():
user_id = get_jwt_identity()
new_username = request.json.get('new_username')
if new_username is None:
return abort(400)
result, status_code = UserService.update_username(user_id, new_username)
jwt = get_jwt()
jti = jwt['jti']
exp = jwt['exp']
UserService.logout(jti, exp)
return result, status_code
@bp_user.route('/update/displayname', methods=['PUT'])
@jwt_required()
def update_displayname():
user_id = get_jwt_identity()
new_displayname = request.json.get('new_displayname')
if new_displayname is None:
return abort(400)
result, status_code = UserService.update_username(user_id, new_displayname)
jwt = get_jwt()
jti = jwt['jti']
exp = jwt['exp']
UserService.logout(jti, exp)
return result, status_code
@bp_user.route('/update/email', methods=['PUT'])
@jwt_required()
def update_email():
username = get_jwt_identity()
new_mail = request.json.get('new_email')
if new_mail is None:
return abort(400)
result, status_code = UserService.update_email(username, new_mail)
jwt = get_jwt()
jti = jwt['jti']
exp = jwt['exp']
UserService.logout(jti, exp)
return result, status_code
@bp_user.route('/update/password', methods=['PUT'])
@jwt_required()
def update_password():
username = get_jwt_identity()
new_password = request.json.get('new_password')
if new_password is None:
return abort(400)
result, status_code = UserService.update_password(username, new_password)
jwt = get_jwt()
jti = jwt['jti']
exp = jwt['exp']
UserService.logout(jti, exp)
return result, status_code
@bp_user.route('/delete', methods=['DELETE'])
@jwt_required()
def delete_user():
user_id = get_jwt_identity()
result, status_code = UserService.delete_user(user_id)
jwt = get_jwt()
jti = jwt['jti']
exp = jwt['exp']
UserService.logout(jti, exp)
return result, status_code
-22
View File
@@ -1,22 +0,0 @@
import os
class MySqlConfig:
MYSQL_USER = os.environ.get('MYSQL_USER')
MYSQL_DATABASE = os.environ.get('MYSQL_DATABASE')
MYSQL_HOST = os.environ.get('MYSQL_HOST')
MYSQL_PORT = os.environ.get('MYSQL_PORT')
MYSQL_PASSWORD = os.environ.get('MYSQL_PASSWORD')
class RedisConfig:
REDIS_HOST = os.environ.get('REDIS_HOST')
REDIS_PORT = os.environ.get('REDIS_PORT')
class FlaskProduction:
DEBUG = False
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY')
SERVER_NAME = os.environ.get('HOST') + ':' + os.environ.get('PORT')
class FlaskTesting:
DEBUG = True
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY')
SERVER_NAME = os.environ.get('HOST') + ':' + os.environ.get('PORT')
-22
View File
@@ -1,22 +0,0 @@
import mysql.connector
import redis
import os
from app.config import RedisConfig
from app.config import MySqlConfig
db_connection = mysql.connector.connect(
host=MySqlConfig.MYSQL_HOST,
user=MySqlConfig.MYSQL_USER,
password=MySqlConfig.MYSQL_PASSWORD,
database=MySqlConfig.MYSQL_DATABASE,
)
db_cursor = db_connection.cursor(dictionary=True)
jwt_redis_blocklist = redis.StrictRedis(
host=RedisConfig.REDIS_HOST,
port=RedisConfig.REDIS_PORT,
db=0,
decode_responses=True
)
-12
View File
@@ -1,12 +0,0 @@
from app.extensions import jwt_redis_blocklist
from . import jwt_manager
from app import app
@jwt_manager.token_in_blocklist_loader
def check_if_token_is_revoked(jwt_header, jwt_payload: dict) -> bool:
jti = jwt_payload["jti"]
token_in_redis = jwt_redis_blocklist.get(jti)
return token_in_redis is not None
-30
View File
@@ -1,30 +0,0 @@
from mysql.connector import Error
from typing import Tuple, Union
from app.extensions import db_cursor, db_connection
class CartService:
@staticmethod
def add_to_cart(user_id: str, product_id: int, count: int) -> Tuple[Union[dict, str], int]:
try:
with db_connection.cursor() as cursor:
db_connection.begin()
db_cursor.execute("select * from cart_item where cart_id = %s and product_id = %s", (user_id, product_id))
if db_cursor.rowcount != 0:
db_cursor.execute("update cart_item set count = count + %s where cart_id = %s and product_id = %s", (count, user_id, product_id))
else:
db_cursor.execute("insert into cart_item(cart_id, product_id, count) values (%s, %s, %s)", (user_id, product_id, count))
db_connection.commit()
return {"Success": "Successfully added to cart"}, 200
except Error as e:
return {"Failed": f"Failed to add item to cart. {e}"}
@staticmethod
def show_cart(user_id: str):
return None
-84
View File
@@ -1,84 +0,0 @@
import base64
from flask import abort
from mysql.connector import Error
from app.extensions import db_connection
class ProductService:
@staticmethod
def get_name(product_id: int):
db_cursor.execute(f"select name from product where product.id = {product_id}")
if db_cursor.rowcount != 1:
return abort(404)
result = db_cursor.fetchone()
return result['name']
@staticmethod
def get_manufacturer(product_id: int):
db_cursor.execute(f"select user.displayname as seller from product inner join user on product.seller_id = user.id where product.id = {product_id}")
if db_cursor.rowcount != 1:
return abort(404)
result = db_cursor.fetchone()
return result['seller']
@staticmethod
def get_price(product_id: int):
db_cursor.execute(f"select price_pc from product where product.id = {product_id}")
if db_cursor.rowcount != 1:
return abort(404)
result = db_cursor.fetchone()
return result['price_pc']
@staticmethod
def get_image(product_id: int):
db_cursor.execute(f"select image from product where product.id = {product_id}")
if db_cursor.rowcount != 1:
return abort(404)
result = db_cursor.fetchone()
return base64.b64encode(result['image']).decode('utf-8')
@staticmethod
def get_image_name(product_id: int):
db_cursor.execute(f"select image_name from product where product.id = {product_id}")
if db_cursor.rowcount != 1:
return abort(404)
result = db_cursor.fetchone()
return result['image_name']
@staticmethod
def get_all_info(product_id: int):
db_cursor.execute(f"select name, user.displayname as seller, price_pc, image_name, image from product inner join user on product.seller_id = user.id where product.id = {product_id}")
if db_cursor.rowcount != 1:
return abort(404)
result = db_cursor.fetchone()
return {
"name": result['name'],
"seller": result['seller'],
"price": result['price_pc'],
"image_name": result['image_name'],
"image": base64.b64encode(result['image']).decode('utf-8')
}
@staticmethod
def create_listing(name: str, seller_id: str, price: float, image_name: str, image):
try:
db_cursor.execute("insert into product(seller_id, name, price_pc, image, image_name) values (%s, %s, %s, %s, %s)", (seller_id, name, price, image, image_name))
db_connection.commit()
except Error as e:
return {"Failed": f"Failed to create product. {e}"}, 400
return {"Success", "Successfully created new product listing"}, 200
-261
View File
@@ -1,261 +0,0 @@
import bcrypt
import re
import jwt
import datetime
from typing import Tuple, Union
from mysql.connector import Error
from flask_jwt_extended import create_access_token
from app.extensions import db_cursor, db_connection
from app.extensions import jwt_redis_blocklist
class UserService:
"""
UserService class provides methods for user-related operations.
Methods:
- register(username: str, email: str, password: str) -> Tuple[Union[dict, str], int]
- login(username: str, password: str) -> Tuple[Union[dict, str], int]
- logout(jti, exp) -> Tuple[Union[dict, str], int]
- update_email(user_id: str, new_email: str) -> Tuple[Union[dict, str], int]
- update_username(user_id: str, new_username: str) -> Tuple[Union[dict, str], int]
- update_password(user_id: str, new_password: str) -> Tuple[Union[dict, str], int]
"""
@staticmethod
def register(username: str, displayname: str, email: str, password: str) -> Tuple[Union[dict, str], int]:
"""
Registers a new user with the provided username, email, and password.
:param username: User's username.
:type username: str
:param email: User's email address.
:type email: str
:param password: User's password.
:type password: str
:return: Tuple containing a dictionary and an HTTP status code.
:rtype: Tuple[Union[dict, str], int]
"""
if not UserService.__verify_username(username):
return {"Failed": "Failed to verify username. Try another username"}, 400
if not UserService.__verify_displayname(displayname):
return {"Failed": "Failed to verify display name. Try another name"}, 400
if not UserService.__verify_email(email):
return {"Failed": "Failed to verify email. Try another email"}, 400
if not UserService.__verify_password(password):
return {"Failed": "Failed to verify password. Try another (stronger) password"}, 400
hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
try:
db_cursor.execute("insert into user (username, displayname, email, password) values (%s, %s, %s, %s)", (username, displayname, email, hashed_password))
db_connection.commit()
except Error as e:
print(f"Error: {e}")
return {"Failed": "Failed to insert into database. Username or email are likely in use already"}, 500
return {"Success": "User created successfully"}, 200
@staticmethod
def login(username: str, password: str) -> Tuple[Union[dict, str], int]:
"""
Authenticates a user with the provided username and password.
:param username: User's username.
:type username: str
:param password: User's password.
:type password: str
:return: Tuple containing a dictionary with a token and an HTTP status code.
:rtype: Tuple[Union[dict, str], int]
"""
db_cursor.execute("select id, password from user where username = %s", (username,))
result = db_cursor.fetchone()
user_id = result['id']
password_hash = result['password']
if user_id is None:
return {"Failed": "Username not found"}, 400
if not bcrypt.checkpw(password.encode('utf-8'), password_hash.encode('utf-8')):
return {"Failed": "Incorrect password"}, 401
expire = datetime.timedelta(hours=1)
token = create_access_token(identity=user_id, expires_delta=expire)
return {"token": token}, 200
@staticmethod
def logout(jti, exp) -> Tuple[Union[dict, str], int]:
"""
Logs out a user by invalidating the provided JWT.
:param jti: JWT ID.
:type jti: str
:param exp: JWT expiration timestamp.
:type exp: int
:return: Tuple containing a dictionary and an HTTP status code.
:rtype: Tuple[Union[dict, str], int]
"""
UserService.__invalidate_token(jti, exp)
return {"Success": "Successfully logged out"}, 200
@staticmethod
def delete_user(user_id: str) -> Tuple[Union[dict, str], int]:
try:
db_cursor.execute("delete from user where id = %s", (user_id,))
db_connection.commit()
except Error as e:
return {"Failed": f"Failed to delete user. {e}"}, 500
return {"Success": "User successfully deleted"}, 200
@staticmethod
def update_email(user_id: str, new_email: str) -> Tuple[Union[dict, str], int]:
"""
Updates the email address for a user with the provided user ID.
:param user_id: User's ID.
:type user_id: str
:param new_email: New email address.
:type new_email: str
:return: Tuple containing a dictionary and an HTTP status code.
:rtype: Tuple[Union[dict, str], int]
"""
if not UserService.__verify_email(new_email):
return {"Failed": "Failed to verify email. Try another email"}, 400
try:
db_cursor.execute("update user set email = %s where id = %s", (new_email, user_id))
db_connection.commit()
except Error as e:
return {"Failed": f"Failed to update email. Email is likely in use already. Error: {e}"}, 500
return {"Success": "Email successfully updated"}, 200
@staticmethod
def update_username(user_id: str, new_username: str) -> Tuple[Union[dict, str], int]:
"""
Updates the username for a user with the provided user ID.
:param user_id: User's ID.
:type user_id: str
:param new_username: New username.
:type new_username: str
:return: Tuple containing a dictionary and an HTTP status code.
:rtype: Tuple[Union[dict, str], int]
"""
if not UserService.__verify_name(new_username):
return {"Failed": "Failed to verify username. Try another one"}, 400
try:
db_cursor.execute("update user set username = %s where id = %s", (new_username, user_id))
db_connection.commit()
except Error as e:
return {"Failed": f"Failed to update username. Username is likely in use already. Error: {e}"}, 500
return {"Success": "Username successfully updated"}, 200
@staticmethod
def update_password(user_id: str, new_password: str) -> Tuple[Union[dict, str], int]:
"""
Updates the password for a user with the provided user ID.
:param user_id: User's ID.
:type user_id: str
:param new_password: New password.
:type new_password: str
:return: Tuple containing a dictionary and an HTTP status code.
:rtype: Tuple[Union[dict, str], int]
"""
if not UserService.__verify_password(new_password):
return {"Failed": "Failed to verify password. Try another (stronger) one"}, 400
hashed_password = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt())
try:
db_cursor.execute("update user set password = %s where id = %s", (new_username, user_id))
db_connection.commit()
except Error as e:
return {"Failed": f"Failed to update password. Error: {e}"}, 500
return {"Success": "Password successfully updated"}, 200
@staticmethod
def __invalidate_token(jti: str, exp: int):
"""
Invalidates a JWT by adding its JTI to the Redis blocklist.
:param jti: JWT ID.
:type jti: str
:param exp: JWT expiration timestamp.
:type exp: int
"""
expiration = datetime.datetime.fromtimestamp(exp)
now = datetime.datetime.now()
delta = expiration - now
jwt_redis_blocklist.set(jti, "", ex=delta)
@staticmethod
def __verify_email(email: str) -> bool:
"""
Verifies a given email string against a regular expression.
:param email: Email string.
:type email: str
:return: Boolean indicating whether the email successfully passed the check.
:rtype: bool
"""
email_regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
return re.match(email_regex, email) and len(email) <= 64
@staticmethod
def __verify_displayname(displayname: str) -> bool:
"""
Verifies a given display name string against a regular expression.
:param displayname: Display name string.
:type displayname: str
:return: Boolean indicating whether the display name successfully passed the check.
:rtype: bool
"""
displayname_regex = r"^[a-zA-Z.-_]{1,64}$"
return re.match(displayname_regex, displayname)
@staticmethod
def __verify_username(username: str) -> bool:
"""
Verifies a given username string against a regular expression.
:param username: Username string.
:type username: str
:return: Boolean indicating whether the username successfully passed the check.
:rtype: bool
"""
username_regex = r"^[a-z]{1,64}$"
return re.match(username_regex, username)
@staticmethod
def __verify_password(password: str) -> bool:
"""
Verifies a given password string against a regular expression.
:param password: Password string.
:type password: str
:return: Boolean indicating whether the password successfully passed the check.
:rtype: bool
"""
password_regex = r"^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,64}$"
return re.match(password_regex, password)
+2
View File
@@ -0,0 +1,2 @@
[mypy-sqlalchemy.*]
follow_untyped_imports = True
+41
View File
@@ -0,0 +1,41 @@
FROM python:3.12
ENV PYTHONUNBUFFERED=1
WORKDIR /app/
# Install uv
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv
COPY --from=ghcr.io/astral-sh/uv:0.5.11 /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
# 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
ENV PYTHONPATH=/app
COPY ./pyproject.toml ./uv.lock /app/
COPY ./app /app/app
# Sync the project
# Ref: https://docs.astral.sh/uv/guides/integration/docker/#intermediate-layers
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync
CMD ["fastapi", "run", "--workers", "4", "app/main.py"]
View File
Whitespace-only changes.
+11
View File
@@ -0,0 +1,11 @@
from fastapi import APIRouter
from app.api.routes import cart_routes, login_routes, shop, user_routes, utils_routes, shop_routes
api_router = APIRouter()
api_router.include_router(user_routes.router)
api_router.include_router(utils_routes.router)
api_router.include_router(login_routes.router)
api_router.include_router(shop.shop_router)
api_router.include_router(shop_routes.router)
+50
View File
@@ -0,0 +1,50 @@
from typing import Annotated
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt.exceptions import InvalidTokenError
from pydantic import ValidationError
from sqlmodel import Session, select
from app.core import security
from app.core.config import settings
from app.database.manager import get_session
from app.database.models.user_model import User, UserRole
from app.schemas.user_schemas import TokenPayload
reusable_oauth2 = OAuth2PasswordBearer(
tokenUrl="/login/access-token"
)
SessionDep = Annotated[Session, Depends(get_session)]
TokenDep = Annotated[str, Depends(reusable_oauth2)]
LoginDep = Annotated[OAuth2PasswordRequestForm, Depends(OAuth2PasswordRequestForm)]
def get_current_user(session: SessionDep, token: TokenDep) -> User:
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
)
token_data = TokenPayload(**payload)
except (InvalidTokenError, ValidationError):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Could not validate credentials",
)
stmt = select(User).where(User.uuid == token_data.sub)
user = session.exec(stmt).one_or_none()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
CurrentUser = Annotated[User, Depends(get_current_user)]
def get_owner_user(current_user: CurrentUser) -> User:
if current_user.user_role != UserRole.OWNER:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="You must be an owner")
return current_user
CurrentOwnerUser = Annotated[User, Depends(get_owner_user)]
View File
Whitespace-only changes.
+38
View File
@@ -0,0 +1,38 @@
from fastapi import APIRouter, Query
router = APIRouter(
prefix="/cart",
tags=["Cart"]
)
@router.get("/")
async def show_cart():
raise NotImplementedError
@router.put("/add/{product_id}")
async def add_to_cart(
product_id: int,
count: int = Query(
1, ge=1, description="Count must be greater than or equal to 1")
):
raise NotImplementedError
@router.delete("/remove/{product_id}")
async def remove_from_cart(product_id: int):
raise NotImplementedError
@router.put("/update/{product_id}")
async def update_count_in_cart(
product_id: int,
count: int = Query(..., description="Count must be provided")
):
raise NotImplementedError
@router.get("/purchase")
async def purchase():
raise NotImplementedError
+44
View File
@@ -0,0 +1,44 @@
from datetime import timedelta
from fastapi import APIRouter, HTTPException
from app.api.dependencies import LoginDep, SessionDep
from app.core import security
from app.core.config import settings
from app.crud import user_crud
from app.schemas.user_schemas import Token
router = APIRouter(tags=["Dashboard", "Login"])
@router.post("/login/access-token", include_in_schema=settings.is_local_environment, response_model=Token)
def login_access_token(
session: SessionDep, form_data: LoginDep
) -> Token:
"""
OAuth2 compatible token login for the dashboard.
This endpoint generates an access token required for authenticating future
requests to the dashboard section of the application. The token is valid for
a predefined expiration period.
- **username**: User's email
- **password**: User's password
**Note:** This login is restricted to dashboard access only and cannot be
used for tenant accounts access to shops
"""
user = None
user = user_crud.authenticate(
session=session, email=form_data.username, password=form_data.password, shop_uuid=None
)
if not user:
raise HTTPException(status_code=400, detail="Incorrect email or password")
elif not user:
raise HTTPException(status_code=400, detail="Inactive user")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return Token(
access_token=security.create_access_token(
user.uuid, expires_delta=access_token_expires
)
)
+12
View File
@@ -0,0 +1,12 @@
from fastapi import APIRouter
from app.api.routes.shop import shop_login_routes, shop_user_routes
shop_router = APIRouter(
prefix="/shop/{shop_uuid}",
tags=["Shop"]
)
shop_router.include_router(shop_login_routes.router)
shop_router.include_router(shop_user_routes.router)
@@ -0,0 +1,37 @@
from datetime import timedelta
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException
from fastapi.security import OAuth2PasswordRequestForm
from app.api.dependencies import SessionDep
from app.core import security
from app.core.config import settings
from app.crud import user_crud
from app.schemas.user_schemas import Token
router = APIRouter(prefix="/login", tags=["Login"])
@router.post("/access-token")
def login_access_token(
session: SessionDep,
form_data: Annotated[OAuth2PasswordRequestForm, Depends()]
) -> Token:
"""
OAuth2 compatible token login, get an access token for future requests
"""
user = None
user = user_crud.authenticate(
session=session, email=form_data.username, password=form_data.password, shop_id=None
)
if not user:
raise HTTPException(status_code=400, detail="Incorrect email or password")
elif not user:
raise HTTPException(status_code=400, detail="Inactive user")
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
return Token(
access_token=security.create_access_token(
user.id, expires_delta=access_token_expires
)
)
@@ -0,0 +1,34 @@
import uuid
from typing import Annotated
from fastapi import APIRouter, Body, Path
from app.api.dependencies import SessionDep
from app.crud.user_crud import create_user
from app.database.models.user_model import UserRole
from app.schemas.user_schemas import UserRegister
router = APIRouter(
prefix="/user",
tags=["User"]
)
@router.delete("/delete", summary="Delete user")
async def delete_user(shop_uuid=Annotated[uuid.UUID, Path(title="UUID of the shop")]):
raise NotImplementedError("delete_user() needs to be implemented.")
@router.delete("/logout", summary="User logout")
async def logout():
raise NotImplementedError("logout() needs to be implemented.")
@router.post("/register", summary="Register new user")
async def register(session: SessionDep, user_data: UserRegister, shop_uuid=Annotated[uuid.UUID, Path(title="UUID of the shop")]):
create_user(session, user_data, shop_uuid, UserRole.CUSTOMER)
@router.put("/update", summary="Update user details")
async def update_user(data: dict = Body(...)):
raise NotImplementedError("update_user() needs to be implemented.")
+26
View File
@@ -0,0 +1,26 @@
from typing import Optional
from fastapi import APIRouter
from app.api.dependencies import CurrentOwnerUser, SessionDep
from app.crud.shop_crud import create_shop
from app.schemas.shop_schemas import ShopCreate, ShopPublic
router = APIRouter(prefix="/shop", tags=["Dashboard"])
@router.post("", status_code=201)
async def register_new_shop(
session: SessionDep,
current_user: CurrentOwnerUser,
shop_data: ShopCreate
) -> bool:
create_shop(session, shop_data, current_user)
return True
@router.get("")
async def get_owned_shop_info(
current_user: CurrentOwnerUser
) -> Optional[ShopPublic]:
return current_user.owned_shop
+60
View File
@@ -0,0 +1,60 @@
import logging
import re
from fastapi import APIRouter, HTTPException, status
from sqlalchemy.exc import IntegrityError
from app.api.dependencies import CurrentOwnerUser, SessionDep
from app.crud import user_crud
from app.database.models.user_model import UserRole
from app.schemas.user_schemas import UserPublic, UserRegister, UserUpdate
logger = logging.getLogger(__name__)
INTERNAL_SERVER_ERROR = HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal server error")
router = APIRouter(
prefix="/user",
tags=["User", "Dashboard"]
)
@router.get("", summary="Get information about currently logged in user")
async def get_user(current_user: CurrentOwnerUser) -> UserPublic:
return current_user
@router.post("", summary="Register new user", status_code=status.HTTP_201_CREATED, response_model=bool)
async def register(session: SessionDep, user_data: UserRegister):
try:
user_crud.create_user(session, user_data, None, UserRole.OWNER)
return True
except IntegrityError as e:
field_mapping = {"uuid": "email"} # If a UUID is duplicate, it means email is in use
constraint = e.orig.diag.constraint_name
column_name = re.sub(r"^user_(\w+)_key$", r"\1", constraint) if constraint else None
if column_name == "uuid":
column_name = "email"
detail = f"{field_mapping.get(column_name, column_name or 'Entry').capitalize()} already in use"
logger.warning("%s already exists in the database", column_name.capitalize)
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=detail)
except Exception as e:
if isinstance(e, HTTPException):
raise
logger.error(e)
raise INTERNAL_SERVER_ERROR
@router.delete("", summary="Delete user")
async def delete_user() -> bool:
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED, detail="delete_user() not implemented")
@router.put("", summary="Update user details")
async def update_user(session: SessionDep, data: UserUpdate, current_user: CurrentOwnerUser) -> bool:
user_crud.update_user(session, data, current_user)
return True
+32
View File
@@ -0,0 +1,32 @@
import logging
from fastapi import APIRouter
from sqlmodel import select
from app.api.dependencies import SessionDep
from app.core.config import settings
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/utils", tags=["Utils"])
@router.get("/health-check/")
async def health_check() -> bool:
"""
Ping the API whether it's alive or not
"""
return True
@router.get("/test-db/", include_in_schema=settings.is_local_environment)
async def test_db(session: SessionDep) -> bool:
"""
Ping database using select 1 to see if connection works
"""
try:
session.exec(select(1))
return True
except Exception as e:
logger.error(e)
return False
View File
Whitespace-only changes.
+112
View File
@@ -0,0 +1,112 @@
import secrets
import warnings
import logging
from typing import Annotated, Any, Literal
from pydantic import (
AnyUrl,
BeforeValidator,
EmailStr,
PostgresDsn,
computed_field,
model_validator,
)
from pydantic_core import MultiHostUrl
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing_extensions import Self
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):
model_config = SettingsConfigDict(
env_file="../.env",
env_ignore_empty=True,
extra="ignore",
)
PORT: int = 8000
SECRET_KEY: str = secrets.token_urlsafe(32)
VERBOSITY: Literal[*logging.getLevelNamesMapping().keys()] = "DEBUG"
# 60 minutes * 24 hours * 8 days = 8 days
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
FRONTEND_HOST: str
ENVIRONMENT: Literal["local", "staging", "production"] = "local"
BACKEND_CORS_ORIGINS: Annotated[
list[AnyUrl] | str, BeforeValidator(parse_cors)
] = []
@computed_field
@property
def all_cors_origins(self) -> list[str]:
return [str(origin).rstrip("/") for origin in self.BACKEND_CORS_ORIGINS] + [self.FRONTEND_HOST]
POSTGRES_SERVER: str
POSTGRES_PORT: int = 5432
POSTGRES_USER: str
POSTGRES_PASSWORD: str = ""
POSTGRES_DB: str = ""
@computed_field
@property
def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn:
return MultiHostUrl.build(
scheme='postgresql+psycopg2',
username=self.POSTGRES_USER,
password=self.POSTGRES_PASSWORD,
host=self.POSTGRES_SERVER,
port=self.POSTGRES_PORT,
path=self.POSTGRES_DB,
)
SMTP_TLS: bool = True
SMTP_SSL: bool = False
SMTP_PORT: int = 587
SMTP_HOST: str | None = None
SMTP_USER: str | None = None
SMTP_PASSWORD: str | None = None
EMAILS_FROM_EMAIL: EmailStr | None = None
EMAILS_FROM_NAME: EmailStr | None = None
EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
@computed_field
@property
def emails_enabled(self) -> bool:
return bool(self.SMTP_HOST and self.EMAILS_FROM_EMAIL)
@computed_field
@property
def is_local_environment(self) -> bool:
return self.ENVIRONMENT == "local"
EMAIL_TEST_USER: EmailStr = "test@example.com"
FIRST_SUPERUSER: EmailStr
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()
+13
View File
@@ -0,0 +1,13 @@
class SwagShopError(Exception):
"""
Generic error to be raised throughout the entire app.
All other custom errors extend this class
"""
class RepositoryError(SwagShopError):
"""Generic error occuring in a repository"""
class CrudError(SwagShopError):
"""Generic error occuring in a service"""
+28
View File
@@ -0,0 +1,28 @@
from datetime import datetime, timedelta, timezone
from typing import Any
import jwt
from passlib.context import CryptContext
from app.core.config import settings
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
ALGORITHM = "HS256"
def create_access_token(subject: str | Any, expires_delta: timedelta) -> str:
expire = datetime.now(timezone.utc) + expires_delta
to_encode = {"exp": expire, "sub": str(subject)}
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
+38
View File
@@ -0,0 +1,38 @@
import logging
from typing import Optional
from uuid import uuid4
from sqlmodel import Session, select
from app.database.models.shop_model import Shop, ShopStatus
from app.database.models.user_model import User
from app.schemas.shop_schemas import ShopCreate
logger = logging.getLogger(__name__)
def get_shop_by_id(session: Session, shop_id: int) -> Optional[Shop]:
logger.debug("Getting shop by shop_id - %d", id)
stmt = select(Shop).where(Shop.id == shop_id)
db_shop = session.exec(stmt).one_or_none()
return db_shop
def get_shop_by_uuid(session: Session, shop_uuid: str) -> Optional[Shop]:
logger.debug("Getting shop by UUID - %s" , shop_uuid)
stmt = select(Shop).where(Shop.uuid == shop_uuid)
db_shop = session.exec(stmt).one_or_none()
return db_shop
def create_shop(session: Session, shop_data: ShopCreate, creator: User) -> None:
shop_uuid = uuid4()
new_shop = Shop(
uuid=shop_uuid,
owner_id=creator.id,
name=shop_data.name,
description=shop_data.description,
status=ShopStatus.INACTIVE,
contact_email=creator.email,
contact_phone_number=creator.phone_number,
currency=shop_data.currency
)
session.add(new_shop)
session.commit()
+83
View File
@@ -0,0 +1,83 @@
import logging
from typing import Optional
from uuid import UUID, uuid4
from fastapi import HTTPException, status
from sqlmodel import Session, select, and_
from app.core.security import get_password_hash, verify_password
from app.crud.shop_crud import get_shop_by_uuid
from app.database.models.user_model import User, UserRole
from app.schemas.user_schemas import UserRegister, UserUpdate
from app.utils.models import generate_user_uuid5
logger = logging.getLogger(__name__)
def create_user(session: Session, user_register: UserRegister, shop_uuid: Optional[UUID], user_role: UserRole):
if shop_uuid:
logger.debug("Fetching shop by UUID")
shop_id = get_shop_by_uuid(session, shop_uuid).id
else:
logger.debug("No shop UUID provided -> Owner account is being created")
shop_id = None
logger.debug("Hashing password")
hashed_password = get_password_hash(user_register.password)
new_user = User(
uuid=uuid4(),
shop_id=shop_id,
email=user_register.email,
username=user_register.username,
phone_number=user_register.phone_number,
user_role=user_role,
password=hashed_password
)
logger.debug("Inserting new user")
session.add(new_user)
session.commit()
def update_user(session: Session, user_update: UserUpdate, current_user: User):
current_user.email = user_update.email
current_user.username = user_update.username
current_user.phone_number = user_update.phone_number
current_user.first_name = user_update.first_name
current_user.last_name = user_update.last_name
session.commit()
def get_user_by_uuid(session: Session, email: str, shop_uuid: Optional[UUID]) -> Optional[User]:
if shop_uuid:
shop_id = get_shop_by_uuid(session, shop_uuid).id
else:
shop_id = None
stmt = select(User).where(and_(
User.email == email,
User.shop_id == shop_id
))
logger.debug("Executing select query")
db_user = session.exec(stmt).one_or_none()
return db_user
def authenticate(session: Session, email: str, password: str, shop_uuid: Optional[int]) -> Optional[User]:
if shop_uuid:
shop_id = get_shop_by_uuid(session, shop_uuid).id
else:
shop_id = None
logger.debug("Fetching user from db by email - %s", email)
db_user = get_user_by_uuid(session, email, shop_id)
if db_user is None:
logger.warning("Didn't find User with email=%s for shop=%s", email, shop_uuid)
return None
if not verify_password(plain_password=password, hashed_password=db_user.password):
logger.warning("Found user with email=%s for shop=%s", email, shop_uuid)
return None
return db_user
View File
Whitespace-only changes.
+20
View File
@@ -0,0 +1,20 @@
class DatabaseError(Exception):
# Inspired by OSError which also uses errno's
# It's a better approach than using a class for each error
UNKNOWN_ERROR = -1
CONNECTION_ERROR = 1
EMPTY_CONFIG = 2
DUPLICATE_ENTRY = 3
def __init__(self, message: str, errno: int, **kwargs):
super().__init__(message)
self.message = message
self.errno = errno
for key, value in kwargs.items():
setattr(self, key, value)
__all__ = ["DatabaseError"]
+41
View File
@@ -0,0 +1,41 @@
import logging
from typing import Generator
from sqlalchemy.exc import DatabaseError as SqlAlchemyError
from sqlmodel import Session, create_engine, select, SQLModel
from app.core.config import settings
from app.database.exceptions import DatabaseError
import app.database.models # pylint: disable=unused-import
logger = logging.getLogger(__name__)
logger.info("Creating engine")
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
SQLModel.metadata.create_all(engine)
def test_connection():
logger.debug("Testing database connection")
try:
with Session(engine) as session:
session.exec(select(1))
logger.debug("Database connection successful")
except SqlAlchemyError as e:
logger.critical("Database connection failed: %s", e)
raise DatabaseError("Database connection failed", DatabaseError.CONNECTION_ERROR) from e
def cleanup() -> None:
logger.debug("Closing connection")
engine.dispose()
def get_session() -> Generator[Session, None, None]:
with Session(engine) as session:
yield session
test_connection()
+6
View File
@@ -0,0 +1,6 @@
from . import user_model, shop_model
__all__ = [
*user_model.__all__,
*shop_model.__all__
]
@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, Float, TIMESTAMP, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from .base_model import Base
class Cart(Base):
__tablename__ = "cart"
cart_id = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"), primary_key=True)
total = Column(Float, nullable=False)
last_updated = Column(TIMESTAMP, onupdate=func.now, nullable=True)
user = relationship("User", back_populates="cart", foreign_keys=[cart_id])
cart_entries = relationship("CartEntry", back_populates="cart")
@@ -0,0 +1,18 @@
from sqlalchemy import Column, Integer, TIMESTAMP, Float, ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from .base_model import Base
class Purchase(Base):
__tablename__ = "purchase"
id = Column(Integer, primary_key=True)
user_id = Column(Integer, ForeignKey("user.id"), nullable=False)
used_coupon_id = Column(Integer, ForeignKey("coupon.id"), nullable=True)
date_purchased = Column(TIMESTAMP, nullable=True)
total = Column(Float, nullable=True)
user = relationship("User", back_populates="purchases", foreign_keys=[user_id])
coupon = relationship("Coupon", back_populates="purchases", foreign_keys=[used_coupon_id])
purchase_entries = relationship("PurchaseEntry", back_populates="purchase")
Whitespace-only changes.
@@ -0,0 +1,15 @@
from sqlalchemy import Column, Integer, Float, TIMESTAMP, ForeignKey
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from .base_model import Base
class Wishlist(Base):
__tablename__ = "wishlist"
id = Column(Integer, ForeignKey("user.id", ondelete="CASCADE"), primary_key=True)
total = Column(Float, nullable=True)
last_updated = Column(TIMESTAMP, onupdate=func.now, nullable=True)
user = relationship("User", back_populates="wishlist", foreign_keys=[id])
wishlist_entries = relationship("WishlistEntry", back_populates="wishlist")
+81
View File
@@ -0,0 +1,81 @@
from datetime import datetime, time
from enum import Enum as PyEnum
from typing import Optional
from uuid import UUID
from pydantic import BaseModel, constr
from sqlalchemy import CheckConstraint, Column
from sqlalchemy.dialects.postgresql import JSONB
from sqlmodel import Enum, Field, Relationship, SQLModel
class ShopStatus(PyEnum):
ACTIVE = "active"
INACTIVE = "inactive"
SUSPENDED = "suspended"
class ShopLinkEntry(BaseModel):
name: constr(strip_whitespace=True, min_length=1)
url: constr(strip_whitespace=True, min_length=1)
class ShopLinks(BaseModel):
links: list[ShopLinkEntry] = []
class ShopBusinessHourEntry(BaseModel):
day: constr(strip_whitespace=True, min_length=1)
open_time: time
close_time: time
class ShopBusinessHours(BaseModel):
hours: list[ShopBusinessHourEntry] = []
class ShopAddress(SQLModel):
street: str = Field(default="", max_length=255, nullable=False)
city: str = Field(default="", max_length=100, nullable=False)
state: Optional[str] = Field(default=None, max_length=100, nullable=True)
postal_code: str = Field(default="", max_length=20, nullable=False)
country: str = Field(default="", max_length=100, nullable=False)
class Shop(SQLModel, table=True):
__tablename__ = 'shop'
id: Optional[int] = Field(default=None, primary_key=True)
uuid: UUID = Field(nullable=False, unique=True)
owner_id: int = Field(foreign_key='user.id', nullable=False)
name: str = Field(max_length=100, nullable=False, unique=True)
description: str = Field(max_length=500, nullable=False)
created_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
status: ShopStatus = Field(sa_column=Column(Enum(ShopStatus)))
logo: Optional[str] = Field(max_length=100)
contact_email: str = Field(max_length=128, nullable=False, unique=True)
contact_phone_number: str = Field(max_length=15, nullable=False)
address: ShopAddress = Field(
sa_column=Column(JSONB, nullable=False, default=lambda: ShopAddress().dict())
)
business_hours: ShopBusinessHours = Field(
sa_column=Column(JSONB, nullable=False, default=lambda: ShopBusinessHours().dict())
)
links: ShopLinks = Field(
sa_column=Column(JSONB, nullable=False, default=lambda: ShopLinks().dict())
)
currency: str = Field(max_length=3, nullable=False)
owner: "User" = Relationship(
back_populates='owned_shop',
sa_relationship_kwargs={"foreign_keys": "[Shop.owner_id]"}
)
registered_users: list["User"] = Relationship(
back_populates='registered_shop',
sa_relationship_kwargs={"foreign_keys": "[User.shop_id]"}
)
__all__ = ["Shop", "ShopBusinessHours", "ShopLinks", "ShopStatus"]
+72
View File
@@ -0,0 +1,72 @@
from datetime import datetime
from enum import Enum as PyEnum
from typing import Optional
from uuid import UUID
from sqlalchemy import Column
from sqlmodel import Enum, Field, Relationship, SQLModel, UniqueConstraint
class UserRole(PyEnum):
OWNER = "owner"
CUSTOMER = "customer"
EMPLOYEE = "employee"
MANAGER = "manager"
ADMIN = "admin"
class User(SQLModel, table=True):
__tablename__ = "user"
__table_args__ = (
UniqueConstraint("email", "shop_id", name="uix_email_shop_id"),
)
id: int = Field(primary_key=True)
uuid: UUID = Field(nullable=False, unique=True)
user_role: UserRole = Field(sa_column=Column(Enum(UserRole), nullable=False))
shop_id: Optional[int] = Field(foreign_key="shop.id")
status: UserRole = Field(sa_column=Column(Enum(UserRole)))
username: str = Field(max_length=64, nullable=False, unique=True)
email: str = Field(max_length=128, nullable=False, unique=True)
password: str = Field(max_length=60, nullable=False)
first_name: Optional[str] = Field(max_length=64)
last_name: Optional[str] = Field(max_length=64)
phone_number: str = Field(max_length=16, nullable=False)
profile_picture: Optional[str] = Field(max_length=100)
created_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
updated_at: datetime = Field(default_factory=datetime.utcnow, nullable=False)
last_login: Optional[datetime] = Field(default_factory=datetime.utcnow)
owned_shop: "Shop" = Relationship(
back_populates="owner",
sa_relationship_kwargs={"foreign_keys": "[Shop.owner_id]"}
)
registered_shop: Optional["Shop"] = Relationship(
back_populates="registered_users",
sa_relationship_kwargs={"foreign_keys": "[User.shop_id]"}
)
preferences: Optional["UserPreferences"] = Relationship(back_populates="user")
statistics: Optional["UserStatistics"] = Relationship(back_populates="user")
class UserPreferences(SQLModel, table=True):
__tablename__ = "user_preferences"
user_id: int = Field(foreign_key="user.id", primary_key=True)
user: Optional["User"] = Relationship(back_populates="preferences")
class UserStatistics(SQLModel, table=True):
__tablename__ = "user_statistics"
user_id: int = Field(foreign_key="user.id", primary_key=True)
total_spend: Optional[float] = Field(default=None)
user: Optional["User"] = Relationship(back_populates="statistics")
__all__ = ["User", "UserPreferences", "UserRole"]
Whitespace-only changes.
+37
View File
@@ -0,0 +1,37 @@
from fastapi import FastAPI
from fastapi.routing import APIRoute
from starlette.middleware.cors import CORSMiddleware
from app.api import api_router
from app.core.config import settings
from app.utils import logger
logger.setup_logger()
def custom_generate_unique_id(route: APIRoute) -> str:
return f"{route.tags[0]}-{route.name}"
app = FastAPI(
title="SWAG Shop",
version="0.0.1",
generate_unique_id_function=custom_generate_unique_id,
openapi_tags=[
{"name": "Dashboard", "description": "Operations endpoints related the admin dashboard. Hidden in production"},
{"name": "Shop", "description": "Shop endpoints which include also user, product and other entity management"},
{"name": "Utils"},
]
)
if settings.all_cors_origins:
app.add_middleware(
CORSMiddleware,
allow_origins=settings.all_cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(api_router)
View File
Whitespace-only changes.
+27
View File
@@ -0,0 +1,27 @@
from datetime import datetime
from pydantic import EmailStr, BaseModel, Field
from sqlmodel import SQLModel
from app.database.models.shop_model import ShopAddress, ShopStatus, ShopBusinessHours, ShopLinks
class ShopCreate(BaseModel):
name: str = Field(max_length=100, nullable=False, unique=True)
description: str = Field(max_length=500, nullable=False)
currency: str = Field(max_length=3, nullable=False)
contact_email: EmailStr = Field()
contact_phone_number: str = Field(min_length=2, max_length=16, pattern=r'^\+[1-9]\d{1,14}$')
address: ShopAddress
class ShopPublic(BaseModel):
name: str
description: str
currency: str
contact_email: EmailStr
contact_phone_number: str
address: ShopAddress
business_hours: ShopBusinessHours
links: ShopLinks
status: ShopStatus
+53
View File
@@ -0,0 +1,53 @@
import re
import uuid
from pydantic import BaseModel, EmailStr, Field, model_validator
class UserRegister(BaseModel):
username: str = Field(min_length=3, max_length=64)
email: EmailStr = Field()
phone_number: str = Field(min_length=2, max_length=16, pattern=r"^\+[1-9]\d{1,14}$")
password: str = Field(
min_length=8,
max_length=128,
examples=["Abc123#!"],
description="Password must conform to this regex: \n```\n^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$ %^&*-]).{8,128}$\n```",
)
@model_validator(mode="after")
def validate_using_regex(self):
self.__validate_password()
return self
def __validate_password(self):
password_regex = r"^(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\D*\d)(?=[^#?!@$ %^&*-]*[#?!@$ %^&*-]).{8,128}$"
if not re.match(password_regex, self.password):
raise ValueError("Password is too weak")
class UserUpdate(BaseModel):
email: EmailStr | None = Field()
phone_number: str | None = Field(min_length=2, max_length=16, pattern=r"^\+[1-9]\d{1,14}$")
username: str | None = Field(min_length=3, max_length=64)
first_name: str | None = Field(None, max_length=64)
last_name: str | None = Field(None, max_length=64)
class UserPublic(BaseModel):
uuid: uuid.UUID
username: str
email: str
first_name: str | None
last_name: str | None
phone_number: str
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
class TokenPayload(BaseModel):
sub: str | None = None
+13
View File
@@ -0,0 +1,13 @@
import sys
import logging
from app.core.config import settings
def setup_logger():
verbosity = settings.VERBOSITY
if verbosity == "DEBUG":
log_format = "[ %(name)s / %(levelname)s ] - %(filename)s:%(lineno)d - %(message)s"
else:
log_format = "[ %(levelname)s ] - %(message)s"
logging.basicConfig(level=verbosity, format=log_format, stream=sys.stdout)
+12
View File
@@ -0,0 +1,12 @@
import logging
from typing import Optional
from uuid import NAMESPACE_DNS, UUID, uuid5
logger = logging.getLogger(__name__)
def generate_user_uuid5(email: str, shop_id: Optional[int]) -> UUID:
unique_string = f"{email}-{shop_id}" if shop_id else email
generated_uuid = uuid5(NAMESPACE_DNS, unique_string)
logger.debug("Generated user UUID5 - %s", generated_uuid)
return generated_uuid
+19
View File
@@ -0,0 +1,19 @@
import logging
from functools import wraps
from fastapi import HTTPException, status
logger = logging.getLogger(__name__)
def raise_http_errors(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
if isinstance(e, HTTPException):
raise
logger.error(e)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Internal server error")
return wrapper
+41
View File
@@ -0,0 +1,41 @@
[project]
name = "swag-shop"
version = "0.1.0"
description = ""
authors = [{ name = "Thastertyn", email = "thastertyn@gmail.com" }]
readme = "../README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi[standard]>=0.115.8,<0.116.0",
"python-dotenv>=1.0.1,<2.0.0",
"sqlalchemy>=2.0.37,<3.0.0",
"passlib[bcrypt]>=1.7.4,<2.0.0",
"pyjwt>=2.10.1,<3.0.0",
"pydantic-settings>=2.8.1,<3.0.0",
"sqlmodel>=0.0.24,<0.1.0",
"psycopg2-binary>=2.9.10,<3.0.0",
]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG001", # unused arguments in functions
]
ignore = [
"E501", # line too long, handled by black
"B008", # do not perform function calls in argument defaults
"B904", # Allow raising exceptions without from e, for HTTPException
"UP007",
]
[tool.autopep8]
max_line_length = 200
ignore = "E501"
in_place = true
recursive = true
+777
View File
@@ -0,0 +1,777 @@
version = 1
revision = 1
requires-python = ">=3.12"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "anyio"
version = "4.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 },
]
[[package]]
name = "bcrypt"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719 },
{ url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001 },
{ url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451 },
{ url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792 },
{ url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752 },
{ url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762 },
{ url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384 },
{ url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329 },
{ url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241 },
{ url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617 },
{ url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751 },
{ url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965 },
{ url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316 },
{ url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752 },
{ url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019 },
{ url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174 },
{ url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870 },
{ url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601 },
{ url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660 },
{ url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083 },
{ url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237 },
{ url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737 },
{ url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741 },
{ url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472 },
{ url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606 },
{ url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867 },
{ url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589 },
{ url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794 },
{ url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969 },
{ url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158 },
{ url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285 },
{ url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583 },
{ url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896 },
{ url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492 },
{ url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213 },
{ url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162 },
{ url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856 },
{ url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726 },
{ url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664 },
{ url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128 },
{ url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598 },
{ url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799 },
]
[[package]]
name = "certifi"
version = "2025.1.31"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
]
[[package]]
name = "click"
version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "dnspython"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 },
]
[[package]]
name = "email-validator"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dnspython" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/48/ce/13508a1ec3f8bb981ae4ca79ea40384becc868bfae97fd1c942bb3a001b1/email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7", size = 48967 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d7/ee/bf0adb559ad3c786f12bcbc9296b3f5675f529199bef03e2df281fa1fadb/email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631", size = 33521 },
]
[[package]]
name = "fastapi"
version = "0.115.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b5/28/c5d26e5860df807241909a961a37d45e10533acef95fc368066c7dd186cd/fastapi-0.115.11.tar.gz", hash = "sha256:cc81f03f688678b92600a65a5e618b93592c65005db37157147204d8924bf94f", size = 294441 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/5d/4d8bbb94f0dbc22732350c06965e40740f4a92ca560e90bb566f4f73af41/fastapi-0.115.11-py3-none-any.whl", hash = "sha256:32e1541b7b74602e4ef4a0260ecaf3aadf9d4f19590bba3e1bf2ac4666aa2c64", size = 94926 },
]
[package.optional-dependencies]
standard = [
{ name = "email-validator" },
{ name = "fastapi-cli", extra = ["standard"] },
{ name = "httpx" },
{ name = "jinja2" },
{ name = "python-multipart" },
{ name = "uvicorn", extra = ["standard"] },
]
[[package]]
name = "fastapi-cli"
version = "0.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "rich-toolkit" },
{ name = "typer" },
{ name = "uvicorn", extra = ["standard"] },
]
sdist = { url = "https://files.pythonhosted.org/packages/fe/73/82a5831fbbf8ed75905bacf5b2d9d3dfd6f04d6968b29fe6f72a5ae9ceb1/fastapi_cli-0.0.7.tar.gz", hash = "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e", size = 16753 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/e6/5daefc851b514ce2287d8f5d358ae4341089185f78f3217a69d0ce3a390c/fastapi_cli-0.0.7-py3-none-any.whl", hash = "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4", size = 10705 },
]
[package.optional-dependencies]
standard = [
{ name = "uvicorn", extra = ["standard"] },
]
[[package]]
name = "greenlet"
version = "3.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260 },
{ url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064 },
{ url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420 },
{ url = "https://files.pythonhosted.org/packages/27/8f/2a93cd9b1e7107d5c7b3b7816eeadcac2ebcaf6d6513df9abaf0334777f6/greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441", size = 658035 },
{ url = "https://files.pythonhosted.org/packages/57/5c/7c6f50cb12be092e1dccb2599be5a942c3416dbcfb76efcf54b3f8be4d8d/greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36", size = 660105 },
{ url = "https://files.pythonhosted.org/packages/f1/66/033e58a50fd9ec9df00a8671c74f1f3a320564c6415a4ed82a1c651654ba/greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9", size = 613077 },
{ url = "https://files.pythonhosted.org/packages/19/c5/36384a06f748044d06bdd8776e231fadf92fc896bd12cb1c9f5a1bda9578/greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0", size = 1135975 },
{ url = "https://files.pythonhosted.org/packages/38/f9/c0a0eb61bdf808d23266ecf1d63309f0e1471f284300ce6dac0ae1231881/greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942", size = 1163955 },
{ url = "https://files.pythonhosted.org/packages/43/21/a5d9df1d21514883333fc86584c07c2b49ba7c602e670b174bd73cfc9c7f/greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01", size = 299655 },
{ url = "https://files.pythonhosted.org/packages/f3/57/0db4940cd7bb461365ca8d6fd53e68254c9dbbcc2b452e69d0d41f10a85e/greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1", size = 272990 },
{ url = "https://files.pythonhosted.org/packages/1c/ec/423d113c9f74e5e402e175b157203e9102feeb7088cee844d735b28ef963/greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff", size = 649175 },
{ url = "https://files.pythonhosted.org/packages/a9/46/ddbd2db9ff209186b7b7c621d1432e2f21714adc988703dbdd0e65155c77/greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a", size = 663425 },
{ url = "https://files.pythonhosted.org/packages/bc/f9/9c82d6b2b04aa37e38e74f0c429aece5eeb02bab6e3b98e7db89b23d94c6/greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e", size = 657736 },
{ url = "https://files.pythonhosted.org/packages/d9/42/b87bc2a81e3a62c3de2b0d550bf91a86939442b7ff85abb94eec3fc0e6aa/greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4", size = 660347 },
{ url = "https://files.pythonhosted.org/packages/37/fa/71599c3fd06336cdc3eac52e6871cfebab4d9d70674a9a9e7a482c318e99/greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e", size = 615583 },
{ url = "https://files.pythonhosted.org/packages/4e/96/e9ef85de031703ee7a4483489b40cf307f93c1824a02e903106f2ea315fe/greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1", size = 1133039 },
{ url = "https://files.pythonhosted.org/packages/87/76/b2b6362accd69f2d1889db61a18c94bc743e961e3cab344c2effaa4b4a25/greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c", size = 1160716 },
{ url = "https://files.pythonhosted.org/packages/1f/1b/54336d876186920e185066d8c3024ad55f21d7cc3683c856127ddb7b13ce/greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761", size = 299490 },
{ url = "https://files.pythonhosted.org/packages/5f/17/bea55bf36990e1638a2af5ba10c1640273ef20f627962cf97107f1e5d637/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011", size = 643731 },
{ url = "https://files.pythonhosted.org/packages/78/d2/aa3d2157f9ab742a08e0fd8f77d4699f37c22adfbfeb0c610a186b5f75e0/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13", size = 649304 },
{ url = "https://files.pythonhosted.org/packages/f1/8e/d0aeffe69e53ccff5a28fa86f07ad1d2d2d6537a9506229431a2a02e2f15/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475", size = 646537 },
{ url = "https://files.pythonhosted.org/packages/05/79/e15408220bbb989469c8871062c97c6c9136770657ba779711b90870d867/greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b", size = 642506 },
{ url = "https://files.pythonhosted.org/packages/18/87/470e01a940307796f1d25f8167b551a968540fbe0551c0ebb853cb527dd6/greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822", size = 602753 },
{ url = "https://files.pythonhosted.org/packages/e2/72/576815ba674eddc3c25028238f74d7b8068902b3968cbe456771b166455e/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01", size = 1122731 },
{ url = "https://files.pythonhosted.org/packages/ac/38/08cc303ddddc4b3d7c628c3039a61a3aae36c241ed01393d00c2fd663473/greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6", size = 1142112 },
]
[[package]]
name = "h11"
version = "0.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 },
]
[[package]]
name = "httpcore"
version = "1.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 },
]
[[package]]
name = "httptools"
version = "0.6.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a7/9a/ce5e1f7e131522e6d3426e8e7a490b3a01f39a6696602e1c4f33f9e94277/httptools-0.6.4.tar.gz", hash = "sha256:4e93eee4add6493b59a5c514da98c939b244fce4a0d8879cd3f466562f4b7d5c", size = 240639 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bb/0e/d0b71465c66b9185f90a091ab36389a7352985fe857e352801c39d6127c8/httptools-0.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:df017d6c780287d5c80601dafa31f17bddb170232d85c066604d8558683711a2", size = 200683 },
{ url = "https://files.pythonhosted.org/packages/e2/b8/412a9bb28d0a8988de3296e01efa0bd62068b33856cdda47fe1b5e890954/httptools-0.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:85071a1e8c2d051b507161f6c3e26155b5c790e4e28d7f236422dbacc2a9cc44", size = 104337 },
{ url = "https://files.pythonhosted.org/packages/9b/01/6fb20be3196ffdc8eeec4e653bc2a275eca7f36634c86302242c4fbb2760/httptools-0.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69422b7f458c5af875922cdb5bd586cc1f1033295aa9ff63ee196a87519ac8e1", size = 508796 },
{ url = "https://files.pythonhosted.org/packages/f7/d8/b644c44acc1368938317d76ac991c9bba1166311880bcc0ac297cb9d6bd7/httptools-0.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16e603a3bff50db08cd578d54f07032ca1631450ceb972c2f834c2b860c28ea2", size = 510837 },
{ url = "https://files.pythonhosted.org/packages/52/d8/254d16a31d543073a0e57f1c329ca7378d8924e7e292eda72d0064987486/httptools-0.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec4f178901fa1834d4a060320d2f3abc5c9e39766953d038f1458cb885f47e81", size = 485289 },
{ url = "https://files.pythonhosted.org/packages/5f/3c/4aee161b4b7a971660b8be71a92c24d6c64372c1ab3ae7f366b3680df20f/httptools-0.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb89ecf8b290f2e293325c646a211ff1c2493222798bb80a530c5e7502494f", size = 489779 },
{ url = "https://files.pythonhosted.org/packages/12/b7/5cae71a8868e555f3f67a50ee7f673ce36eac970f029c0c5e9d584352961/httptools-0.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:db78cb9ca56b59b016e64b6031eda5653be0589dba2b1b43453f6e8b405a0970", size = 88634 },
{ url = "https://files.pythonhosted.org/packages/94/a3/9fe9ad23fd35f7de6b91eeb60848986058bd8b5a5c1e256f5860a160cc3e/httptools-0.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ade273d7e767d5fae13fa637f4d53b6e961fb7fd93c7797562663f0171c26660", size = 197214 },
{ url = "https://files.pythonhosted.org/packages/ea/d9/82d5e68bab783b632023f2fa31db20bebb4e89dfc4d2293945fd68484ee4/httptools-0.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:856f4bc0478ae143bad54a4242fccb1f3f86a6e1be5548fecfd4102061b3a083", size = 102431 },
{ url = "https://files.pythonhosted.org/packages/96/c1/cb499655cbdbfb57b577734fde02f6fa0bbc3fe9fb4d87b742b512908dff/httptools-0.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:322d20ea9cdd1fa98bd6a74b77e2ec5b818abdc3d36695ab402a0de8ef2865a3", size = 473121 },
{ url = "https://files.pythonhosted.org/packages/af/71/ee32fd358f8a3bb199b03261f10921716990808a675d8160b5383487a317/httptools-0.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d87b29bd4486c0093fc64dea80231f7c7f7eb4dc70ae394d70a495ab8436071", size = 473805 },
{ url = "https://files.pythonhosted.org/packages/8a/0a/0d4df132bfca1507114198b766f1737d57580c9ad1cf93c1ff673e3387be/httptools-0.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:342dd6946aa6bda4b8f18c734576106b8a31f2fe31492881a9a160ec84ff4bd5", size = 448858 },
{ url = "https://files.pythonhosted.org/packages/1e/6a/787004fdef2cabea27bad1073bf6a33f2437b4dbd3b6fb4a9d71172b1c7c/httptools-0.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b36913ba52008249223042dca46e69967985fb4051951f94357ea681e1f5dc0", size = 452042 },
{ url = "https://files.pythonhosted.org/packages/4d/dc/7decab5c404d1d2cdc1bb330b1bf70e83d6af0396fd4fc76fc60c0d522bf/httptools-0.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:28908df1b9bb8187393d5b5db91435ccc9c8e891657f9cbb42a2541b44c82fc8", size = 87682 },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 },
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mdurl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 },
]
[[package]]
name = "markupsafe"
version = "3.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 },
{ url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 },
{ url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 },
{ url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 },
{ url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 },
{ url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 },
{ url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 },
{ url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 },
{ url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 },
{ url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 },
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 },
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 },
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 },
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 },
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 },
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 },
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 },
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 },
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 },
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 },
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 },
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 },
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 },
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 },
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 },
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 },
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 },
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 },
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 },
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 },
]
[[package]]
name = "mdurl"
version = "0.1.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 },
]
[[package]]
name = "passlib"
version = "1.7.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554 },
]
[package.optional-dependencies]
bcrypt = [
{ name = "bcrypt" },
]
[[package]]
name = "psycopg2-binary"
version = "2.9.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771 },
{ url = "https://files.pythonhosted.org/packages/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336 },
{ url = "https://files.pythonhosted.org/packages/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637 },
{ url = "https://files.pythonhosted.org/packages/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097 },
{ url = "https://files.pythonhosted.org/packages/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776 },
{ url = "https://files.pythonhosted.org/packages/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968 },
{ url = "https://files.pythonhosted.org/packages/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334 },
{ url = "https://files.pythonhosted.org/packages/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722 },
{ url = "https://files.pythonhosted.org/packages/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132 },
{ url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312 },
{ url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191 },
{ url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031 },
{ url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699 },
{ url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245 },
{ url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631 },
{ url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140 },
{ url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762 },
{ url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967 },
{ url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326 },
{ url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712 },
{ url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155 },
{ url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356 },
{ url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 },
]
[[package]]
name = "pydantic"
version = "2.10.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 },
]
[[package]]
name = "pydantic-core"
version = "2.27.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 },
{ url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 },
{ url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 },
{ url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 },
{ url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 },
{ url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 },
{ url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 },
{ url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 },
{ url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 },
{ url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 },
{ url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 },
{ url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 },
{ url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 },
{ url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 },
{ url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 },
{ url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 },
{ url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 },
{ url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 },
{ url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 },
{ url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 },
{ url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 },
{ url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 },
{ url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 },
{ url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 },
{ url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 },
{ url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 },
{ url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 },
{ url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 },
]
[[package]]
name = "pydantic-settings"
version = "2.8.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/88/82/c79424d7d8c29b994fb01d277da57b0a9b09cc03c3ff875f9bd8a86b2145/pydantic_settings-2.8.1.tar.gz", hash = "sha256:d5c663dfbe9db9d5e1c646b2e161da12f0d734d422ee56f567d0ea2cee4e8585", size = 83550 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0b/53/a64f03044927dc47aafe029c42a5b7aabc38dfb813475e0e1bf71c4a59d0/pydantic_settings-2.8.1-py3-none-any.whl", hash = "sha256:81942d5ac3d905f7f3ee1a70df5dfb62d5569c12f51a5a647defc1c3d9ee2e9c", size = 30839 },
]
[[package]]
name = "pygments"
version = "2.19.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 },
]
[[package]]
name = "pyjwt"
version = "2.10.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 },
]
[[package]]
name = "python-dotenv"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
]
[[package]]
name = "python-multipart"
version = "0.0.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 },
]
[[package]]
name = "pyyaml"
version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 },
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 },
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 },
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 },
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 },
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 },
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 },
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 },
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 },
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 },
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 },
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 },
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 },
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 },
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 },
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 },
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 },
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
]
[[package]]
name = "rich"
version = "13.9.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 },
]
[[package]]
name = "rich-toolkit"
version = "0.13.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5b/8a/71cfbf6bf6257ea785d1f030c22468f763eea1b3e5417620f2ba9abd6dca/rich_toolkit-0.13.2.tar.gz", hash = "sha256:fea92557530de7c28f121cbed572ad93d9e0ddc60c3ca643f1b831f2f56b95d3", size = 72288 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/1b/1c2f43af46456050b27810a7a013af8a7e12bc545a0cdc00eb0df55eb769/rich_toolkit-0.13.2-py3-none-any.whl", hash = "sha256:f3f6c583e5283298a2f7dbd3c65aca18b7f818ad96174113ab5bec0b0e35ed61", size = 13566 },
]
[[package]]
name = "shellingham"
version = "1.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "sqlalchemy"
version = "2.0.39"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/00/8e/e77fcaa67f8b9f504b4764570191e291524575ddbfe78a90fc656d671fdc/sqlalchemy-2.0.39.tar.gz", hash = "sha256:5d2d1fe548def3267b4c70a8568f108d1fed7cbbeccb9cc166e05af2abc25c22", size = 9644602 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/98/86/b2cb432aeb00a1eda7ed33ce86d943c2452dc1642f3ec51bfe9eaae9604b/sqlalchemy-2.0.39-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c457a38351fb6234781d054260c60e531047e4d07beca1889b558ff73dc2014b", size = 2107210 },
{ url = "https://files.pythonhosted.org/packages/bf/b0/b2479edb3419ca763ba1b587161c292d181351a33642985506a530f9162b/sqlalchemy-2.0.39-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:018ee97c558b499b58935c5a152aeabf6d36b3d55d91656abeb6d93d663c0c4c", size = 2097599 },
{ url = "https://files.pythonhosted.org/packages/58/5e/c5b792a4abcc71e68d44cb531c4845ac539d558975cc61db1afbc8a73c96/sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5493a8120d6fc185f60e7254fc056a6742f1db68c0f849cfc9ab46163c21df47", size = 3247012 },
{ url = "https://files.pythonhosted.org/packages/e0/a8/055fa8a7c5f85e6123b7e40ec2e9e87d63c566011d599b4a5ab75e033017/sqlalchemy-2.0.39-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2cf5b5ddb69142511d5559c427ff00ec8c0919a1e6c09486e9c32636ea2b9dd", size = 3257851 },
{ url = "https://files.pythonhosted.org/packages/f6/40/aec16681e91a22ddf03dbaeb3c659bce96107c5f47d2a7c665eb7f24a014/sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f03143f8f851dd8de6b0c10784363712058f38209e926723c80654c1b40327a", size = 3193155 },
{ url = "https://files.pythonhosted.org/packages/21/9d/cef697b137b9eb0b66ab8e9cf193a7c7c048da3b4bb667e5fcea4d90c7a2/sqlalchemy-2.0.39-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06205eb98cb3dd52133ca6818bf5542397f1dd1b69f7ea28aa84413897380b06", size = 3219770 },
{ url = "https://files.pythonhosted.org/packages/57/05/e109ca7dde837d8f2f1b235357e4e607f8af81ad8bc29c230fed8245687d/sqlalchemy-2.0.39-cp312-cp312-win32.whl", hash = "sha256:7f5243357e6da9a90c56282f64b50d29cba2ee1f745381174caacc50d501b109", size = 2077567 },
{ url = "https://files.pythonhosted.org/packages/97/c6/25ca068e38c29ed6be0fde2521888f19da923dbd58f5ff16af1b73ec9b58/sqlalchemy-2.0.39-cp312-cp312-win_amd64.whl", hash = "sha256:2ed107331d188a286611cea9022de0afc437dd2d3c168e368169f27aa0f61338", size = 2103136 },
{ url = "https://files.pythonhosted.org/packages/32/47/55778362642344324a900b6b2b1b26f7f02225b374eb93adc4a363a2d8ae/sqlalchemy-2.0.39-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe193d3ae297c423e0e567e240b4324d6b6c280a048e64c77a3ea6886cc2aa87", size = 2102484 },
{ url = "https://files.pythonhosted.org/packages/1b/e1/f5f26f67d095f408138f0fb2c37f827f3d458f2ae51881546045e7e55566/sqlalchemy-2.0.39-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:79f4f502125a41b1b3b34449e747a6abfd52a709d539ea7769101696bdca6716", size = 2092955 },
{ url = "https://files.pythonhosted.org/packages/c5/c2/0db0022fc729a54fc7aef90a3457bf20144a681baef82f7357832b44c566/sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a10ca7f8a1ea0fd5630f02feb055b0f5cdfcd07bb3715fc1b6f8cb72bf114e4", size = 3179367 },
{ url = "https://files.pythonhosted.org/packages/33/b7/f33743d87d0b4e7a1f12e1631a4b9a29a8d0d7c0ff9b8c896d0bf897fb60/sqlalchemy-2.0.39-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6b0a1c7ed54a5361aaebb910c1fa864bae34273662bb4ff788a527eafd6e14d", size = 3192705 },
{ url = "https://files.pythonhosted.org/packages/c9/74/6814f31719109c973ddccc87bdfc2c2a9bc013bec64a375599dc5269a310/sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52607d0ebea43cf214e2ee84a6a76bc774176f97c5a774ce33277514875a718e", size = 3125927 },
{ url = "https://files.pythonhosted.org/packages/e8/6b/18f476f4baaa9a0e2fbc6808d8f958a5268b637c8eccff497bf96908d528/sqlalchemy-2.0.39-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c08a972cbac2a14810463aec3a47ff218bb00c1a607e6689b531a7c589c50723", size = 3154055 },
{ url = "https://files.pythonhosted.org/packages/b4/60/76714cecb528da46bc53a0dd36d1ccef2f74ef25448b630a0a760ad07bdb/sqlalchemy-2.0.39-cp313-cp313-win32.whl", hash = "sha256:23c5aa33c01bd898f879db158537d7e7568b503b15aad60ea0c8da8109adf3e7", size = 2075315 },
{ url = "https://files.pythonhosted.org/packages/5b/7c/76828886d913700548bac5851eefa5b2c0251ebc37921fe476b93ce81b50/sqlalchemy-2.0.39-cp313-cp313-win_amd64.whl", hash = "sha256:4dabd775fd66cf17f31f8625fc0e4cfc5765f7982f94dc09b9e5868182cb71c0", size = 2099175 },
{ url = "https://files.pythonhosted.org/packages/7b/0f/d69904cb7d17e65c65713303a244ec91fd3c96677baf1d6331457fd47e16/sqlalchemy-2.0.39-py3-none-any.whl", hash = "sha256:a1c6b0a5e3e326a466d809b651c63f278b1256146a377a528b6938a279da334f", size = 1898621 },
]
[[package]]
name = "sqlmodel"
version = "0.0.24"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "sqlalchemy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/86/4b/c2ad0496f5bdc6073d9b4cef52be9c04f2b37a5773441cc6600b1857648b/sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423", size = 116780 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/91/484cd2d05569892b7fef7f5ceab3bc89fb0f8a8c0cde1030d383dbc5449c/sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193", size = 28622 },
]
[[package]]
name = "starlette"
version = "0.46.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/04/1b/52b27f2e13ceedc79a908e29eac426a63465a1a01248e5f24aa36a62aeb3/starlette-0.46.1.tar.gz", hash = "sha256:3c88d58ee4bd1bb807c0d1acb381838afc7752f9ddaec81bbe4383611d833230", size = 2580102 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/4b/528ccf7a982216885a1ff4908e886b8fb5f19862d1962f56a3fce2435a70/starlette-0.46.1-py3-none-any.whl", hash = "sha256:77c74ed9d2720138b25875133f3a2dae6d854af2ec37dceb56aef370c1d8a227", size = 71995 },
]
[[package]]
name = "swag-shop"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "fastapi", extra = ["standard"] },
{ name = "passlib", extra = ["bcrypt"] },
{ name = "psycopg2-binary" },
{ name = "pydantic-settings" },
{ name = "pyjwt" },
{ name = "python-dotenv" },
{ name = "sqlalchemy" },
{ name = "sqlmodel" },
]
[package.metadata]
requires-dist = [
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.8,<0.116.0" },
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4,<2.0.0" },
{ name = "psycopg2-binary", specifier = ">=2.9.10,<3.0.0" },
{ name = "pydantic-settings", specifier = ">=2.8.1,<3.0.0" },
{ name = "pyjwt", specifier = ">=2.10.1,<3.0.0" },
{ name = "python-dotenv", specifier = ">=1.0.1,<2.0.0" },
{ name = "sqlalchemy", specifier = ">=2.0.37,<3.0.0" },
{ name = "sqlmodel", specifier = ">=0.0.24,<0.1.0" },
]
[[package]]
name = "typer"
version = "0.15.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "rich" },
{ name = "shellingham" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8b/6f/3991f0f1c7fcb2df31aef28e0594d8d54b05393a0e4e34c65e475c2a5d41/typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5", size = 100711 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7f/fc/5b29fea8cee020515ca82cc68e3b8e1e34bb19a3535ad854cac9257b414c/typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc", size = 45061 },
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 },
]
[[package]]
name = "uvicorn"
version = "0.34.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 },
]
[package.optional-dependencies]
standard = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "httptools" },
{ name = "python-dotenv" },
{ name = "pyyaml" },
{ name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
{ name = "watchfiles" },
{ name = "websockets" },
]
[[package]]
name = "uvloop"
version = "0.21.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/af/c0/854216d09d33c543f12a44b393c402e89a920b1a0a7dc634c42de91b9cf6/uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3", size = 2492741 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/4c/03f93178830dc7ce8b4cdee1d36770d2f5ebb6f3d37d354e061eefc73545/uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c", size = 1471284 },
{ url = "https://files.pythonhosted.org/packages/43/3e/92c03f4d05e50f09251bd8b2b2b584a2a7f8fe600008bcc4523337abe676/uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2", size = 821349 },
{ url = "https://files.pythonhosted.org/packages/a6/ef/a02ec5da49909dbbfb1fd205a9a1ac4e88ea92dcae885e7c961847cd51e2/uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d", size = 4580089 },
{ url = "https://files.pythonhosted.org/packages/06/a7/b4e6a19925c900be9f98bec0a75e6e8f79bb53bdeb891916609ab3958967/uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc", size = 4693770 },
{ url = "https://files.pythonhosted.org/packages/ce/0c/f07435a18a4b94ce6bd0677d8319cd3de61f3a9eeb1e5f8ab4e8b5edfcb3/uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb", size = 4451321 },
{ url = "https://files.pythonhosted.org/packages/8f/eb/f7032be105877bcf924709c97b1bf3b90255b4ec251f9340cef912559f28/uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f", size = 4659022 },
{ url = "https://files.pythonhosted.org/packages/3f/8d/2cbef610ca21539f0f36e2b34da49302029e7c9f09acef0b1c3b5839412b/uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281", size = 1468123 },
{ url = "https://files.pythonhosted.org/packages/93/0d/b0038d5a469f94ed8f2b2fce2434a18396d8fbfb5da85a0a9781ebbdec14/uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af", size = 819325 },
{ url = "https://files.pythonhosted.org/packages/50/94/0a687f39e78c4c1e02e3272c6b2ccdb4e0085fda3b8352fecd0410ccf915/uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6", size = 4582806 },
{ url = "https://files.pythonhosted.org/packages/d2/19/f5b78616566ea68edd42aacaf645adbf71fbd83fc52281fba555dc27e3f1/uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816", size = 4701068 },
{ url = "https://files.pythonhosted.org/packages/47/57/66f061ee118f413cd22a656de622925097170b9380b30091b78ea0c6ea75/uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc", size = 4454428 },
{ url = "https://files.pythonhosted.org/packages/63/9a/0962b05b308494e3202d3f794a6e85abe471fe3cafdbcf95c2e8c713aabd/uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553", size = 4660018 },
]
[[package]]
name = "watchfiles"
version = "1.0.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/26/c705fc77d0a9ecdb9b66f1e2976d95b81df3cae518967431e7dbf9b5e219/watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205", size = 94625 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/1a/8f4d9a1461709756ace48c98f07772bc6d4519b1e48b5fa24a4061216256/watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2", size = 391345 },
{ url = "https://files.pythonhosted.org/packages/bc/d2/6750b7b3527b1cdaa33731438432e7238a6c6c40a9924049e4cebfa40805/watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9", size = 381515 },
{ url = "https://files.pythonhosted.org/packages/4e/17/80500e42363deef1e4b4818729ed939aaddc56f82f4e72b2508729dd3c6b/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712", size = 449767 },
{ url = "https://files.pythonhosted.org/packages/10/37/1427fa4cfa09adbe04b1e97bced19a29a3462cc64c78630787b613a23f18/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12", size = 455677 },
{ url = "https://files.pythonhosted.org/packages/c5/7a/39e9397f3a19cb549a7d380412fd9e507d4854eddc0700bfad10ef6d4dba/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844", size = 482219 },
{ url = "https://files.pythonhosted.org/packages/45/2d/7113931a77e2ea4436cad0c1690c09a40a7f31d366f79c6f0a5bc7a4f6d5/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733", size = 518830 },
{ url = "https://files.pythonhosted.org/packages/f9/1b/50733b1980fa81ef3c70388a546481ae5fa4c2080040100cd7bf3bf7b321/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af", size = 497997 },
{ url = "https://files.pythonhosted.org/packages/2b/b4/9396cc61b948ef18943e7c85ecfa64cf940c88977d882da57147f62b34b1/watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a", size = 452249 },
{ url = "https://files.pythonhosted.org/packages/fb/69/0c65a5a29e057ad0dc691c2fa6c23b2983c7dabaa190ba553b29ac84c3cc/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff", size = 614412 },
{ url = "https://files.pythonhosted.org/packages/7f/b9/319fcba6eba5fad34327d7ce16a6b163b39741016b1996f4a3c96b8dd0e1/watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e", size = 611982 },
{ url = "https://files.pythonhosted.org/packages/f1/47/143c92418e30cb9348a4387bfa149c8e0e404a7c5b0585d46d2f7031b4b9/watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94", size = 271822 },
{ url = "https://files.pythonhosted.org/packages/ea/94/b0165481bff99a64b29e46e07ac2e0df9f7a957ef13bec4ceab8515f44e3/watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c", size = 285441 },
{ url = "https://files.pythonhosted.org/packages/11/de/09fe56317d582742d7ca8c2ca7b52a85927ebb50678d9b0fa8194658f536/watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90", size = 277141 },
{ url = "https://files.pythonhosted.org/packages/08/98/f03efabec64b5b1fa58c0daab25c68ef815b0f320e54adcacd0d6847c339/watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9", size = 390954 },
{ url = "https://files.pythonhosted.org/packages/16/09/4dd49ba0a32a45813debe5fb3897955541351ee8142f586303b271a02b40/watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60", size = 381133 },
{ url = "https://files.pythonhosted.org/packages/76/59/5aa6fc93553cd8d8ee75c6247763d77c02631aed21551a97d94998bf1dae/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407", size = 449516 },
{ url = "https://files.pythonhosted.org/packages/4c/aa/df4b6fe14b6317290b91335b23c96b488d365d65549587434817e06895ea/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d", size = 454820 },
{ url = "https://files.pythonhosted.org/packages/5e/71/185f8672f1094ce48af33252c73e39b48be93b761273872d9312087245f6/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d", size = 481550 },
{ url = "https://files.pythonhosted.org/packages/85/d7/50ebba2c426ef1a5cb17f02158222911a2e005d401caf5d911bfca58f4c4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b", size = 518647 },
{ url = "https://files.pythonhosted.org/packages/f0/7a/4c009342e393c545d68987e8010b937f72f47937731225b2b29b7231428f/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590", size = 497547 },
{ url = "https://files.pythonhosted.org/packages/0f/7c/1cf50b35412d5c72d63b2bf9a4fffee2e1549a245924960dd087eb6a6de4/watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902", size = 452179 },
{ url = "https://files.pythonhosted.org/packages/d6/a9/3db1410e1c1413735a9a472380e4f431ad9a9e81711cda2aaf02b7f62693/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1", size = 614125 },
{ url = "https://files.pythonhosted.org/packages/f2/e1/0025d365cf6248c4d1ee4c3d2e3d373bdd3f6aff78ba4298f97b4fad2740/watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303", size = 611911 },
{ url = "https://files.pythonhosted.org/packages/55/55/035838277d8c98fc8c917ac9beeb0cd6c59d675dc2421df5f9fcf44a0070/watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80", size = 271152 },
{ url = "https://files.pythonhosted.org/packages/f0/e5/96b8e55271685ddbadc50ce8bc53aa2dff278fb7ac4c2e473df890def2dc/watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc", size = 285216 },
]
[[package]]
name = "websockets"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 },
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 },
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 },
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 },
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 },
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 },
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 },
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 },
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 },
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 },
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 },
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 },
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 },
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 },
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 },
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 },
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 },
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 },
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 },
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 },
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 },
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 },
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 },
]
+261
View File
@@ -0,0 +1,261 @@
"Abkhazian apsar[E]",
"₽",
"Afghan afghani",
"Euro",
"Albanian lek",
"Algerian dinar",
"Euro",
"Angolan kwanza",
"Eastern Caribbean dollar",
"Eastern Caribbean dollar",
"Argentine peso",
"Armenian dram",
"Aruban florin",
"Saint Helena pound",
"Australian dollar",
"Euro",
"Azerbaijani manat",
"Bahamian dollar",
"Bahraini dinar",
"Bangladeshi taka",
"Barbadian dollar",
"Belarusian ruble",
"Euro",
"Belize dollar",
"West African CFA franc",
"Bermudian dollar",
"Bhutanese ngultrum[F]",
"₹",
"Bolivian boliviano",
"United States dollar[G]",
"Bosnia and Herzegovina convertible mark",
"Botswana pula",
"Brazilian real",
"Sterling",
"United States dollar",
"Brunei dollar",
"$",
"Bulgarian lev",
"West African CFA franc",
"Burundian franc",
"Cambodian riel",
"Central African CFA franc",
"Canadian dollar",
"Cape Verdean escudo",
"Cayman Islands dollar",
"Central African CFA franc",
"Central African CFA franc",
"Chilean peso",
"Renminbi",
"Colombian peso",
"Comorian franc",
"Congolese franc",
"Central African CFA franc",
"Cook Islands dollar",
"$",
"Costa Rican colón",
"West African CFA franc",
"Euro",
"Cuban peso",
"Netherlands Antillean guilder",
"Euro",
"Czech koruna",
"Danish krone",
"Djiboutian franc",
"Eastern Caribbean dollar",
"Dominican peso",
"United States dollar",
"Egyptian pound",
"United States dollar",
"Central African CFA franc",
"Eritrean nakfa",
"Euro",
"Swazi lilangeni",
"R",
"Ethiopian birr",
"Falkland Islands pound",
"£",
"Danish krone",
"kr",
"Fijian dollar",
"Euro",
"Euro",
"CFP franc",
"Central African CFA franc",
"Gambian dalasi",
"Georgian lari",
"Euro",
"Ghanaian cedi",
"Gibraltar pound",
"£",
"Euro",
"Danish krone",
"Eastern Caribbean dollar",
"Guatemalan quetzal",
"Guernsey pound",
"£",
"Guinean franc",
"West African CFA franc",
"Guyanese dollar",
"Haitian gourde",
"Honduran lempira",
"Hong Kong dollar",
"Hungarian forint",
"Icelandic króna",
"Indian rupee",
"Indonesian rupiah",
"Iranian rial",
"Iraqi dinar",
"Euro",
"Manx pound",
"£",
"Israeli new shekel",
"Euro",
"Jamaican dollar",
"Japanese yen",
"Jersey pound",
"£",
"Jordanian dinar",
"Kazakhstani tenge",
"Kenyan shilling",
"Kiribati dollar[E]",
"$",
"North Korean won",
"South Korean won",
"Euro",
"Kuwaiti dinar",
"Kyrgyz som",
"Lao kip",
"Euro",
"Lebanese pound",
"Lesotho loti",
"R",
"Falkland Islands pound",
"£",
"Liberian dollar",
"$",
"Libyan dinar",
"Swiss franc",
"Euro",
"Euro",
"Macanese pataca",
"Malagasy ariary",
"Malawian kwacha",
"Malaysian ringgit",
"Maldivian rufiyaa",
"West African CFA franc",
"Euro",
"United States dollar",
"Mauritanian ouguiya",
"Mauritian rupee",
"Mexican peso",
"United States dollar",
"Moldovan leu",
"Euro",
"Mongolian tögrög",
"Euro",
"Eastern Caribbean dollar",
"Moroccan dirham",
"Mozambican metical",
"Burmese kyat",
"Namibian dollar",
"R",
"Australian dollar",
"Nepalese rupee",
"Euro",
"CFP franc",
"New Zealand dollar",
"Nicaraguan córdoba",
"West African CFA franc",
"Nigerian naira",
"New Zealand dollar",
"$",
"Macedonian denar",
"Turkish lira",
"Norwegian krone",
"Omani rial",
"Pakistani rupee",
"United States dollar",
"Israeli new shekel",
"LE",
"JD",
"Panamanian balboa",
"$",
"Papua New Guinean kina",
"Paraguayan guaraní",
"Peruvian sol",
"Philippine peso",
"New Zealand dollar",
"$",
"Polish złoty",
"Euro",
"Qatari riyal",
"Romanian leu",
"Russian ruble",
"Rwandan franc",
"United States dollar[G]",
"Moroccan dirham",
"Pta or Pts (pl.)",
"Saint Helena pound",
"£",
"Eastern Caribbean dollar",
"Eastern Caribbean dollar",
"Eastern Caribbean dollar",
"Samoan tālā",
"Euro",
"São Tomé and Príncipe dobra",
"Saudi riyal",
"West African CFA franc",
"Serbian dinar",
"Seychellois rupee",
"Sierra Leonean leone",
"Singapore dollar",
"$",
"United States dollar[G]",
"Netherlands Antillean guilder",
"Euro",
"Euro",
"Solomon Islands dollar",
"Somali shilling",
"Somaliland shilling",
"South African rand",
"Russian ruble",
"South Sudanese pound",
"Euro",
"Sri Lankan rupee",
"Sudanese pound",
"Surinamese dollar",
"Swedish krona",
"Swiss franc",
"Syrian pound",
"New Taiwan dollar",
"Tajikistani somoni",
"Tanzanian shilling",
"Thai baht",
"United States dollar",
"West African CFA franc",
"Tongan paʻanga[O]",
"Transnistrian ruble",
"Trinidad and Tobago dollar",
"Tunisian dinar",
"Turkish lira",
"Turkmenistani manat",
"United States dollar",
"Tuvaluan dollar",
"$",
"Ugandan shilling",
"Ukrainian hryvnia",
"United Arab Emirates dirham",
"Sterling",
"United States dollar",
"Uruguayan peso",
"Uzbekistani sum",
"Vanuatu vatu",
"Euro",
"Venezuelan sovereign bolívar",
"Bs.D",
"Vietnamese đồng",
"CFP franc",
"Yemeni rial",
"Zambian kwacha",
"Zimbabwe gold",
+156
View File
@@ -0,0 +1,156 @@
"Afghan afghani",
"Albanian lek",
"Algerian dinar",
"Angolan kwanza",
"Argentine peso",
"Armenian dram",
"Aruban florin",
"Australian dollar",
"Azerbaijani manat",
"Bahamian dollar",
"Bahraini dinar",
"Bangladeshi taka",
"Barbadian dollar",
"Belarusian ruble",
"Belize dollar",
"Bermudian dollar",
"Bhutanese ngultrum[F]",
"Bolivian boliviano",
"Bosnia and Herzegovina convertible mark",
"Botswana pula",
"Brazilian real",
"Brunei dollar",
"Bulgarian lev",
"Burmese kyat",
"Burundian franc",
"Cambodian riel",
"Canadian dollar",
"Cape Verdean escudo",
"Cayman Islands dollar",
"Central African CFA franc",
"CFP franc",
"Colombian peso",
"Comorian franc",
"Congolese franc",
"Costa Rican colón",
"Cuban peso",
"Czech koruna",
"Danish krone",
"Djiboutian franc",
"Dominican peso",
"Eastern Caribbean dollar",
"Egyptian pound",
"Eritrean nakfa",
"Ethiopian birr",
"Euro",
"Falkland Islands pound",
"Fijian dollar",
"Gambian dalasi",
"Georgian lari",
"Ghanaian cedi",
"Gibraltar pound",
"Guatemalan quetzal",
"Guinean franc",
"Guyanese dollar",
"Haitian gourde",
"Honduran lempira",
"Hong Kong dollar",
"Hungarian forint",
"Chilean peso",
"Icelandic króna",
"Indian rupee",
"Indonesian rupiah",
"Iranian rial",
"Iraqi dinar",
"Israeli new shekel",
"Jamaican dollar",
"Japanese yen",
"Jordanian dinar",
"Kazakhstani tenge",
"Kenyan shilling",
"Kuwaiti dinar",
"Kyrgyz som",
"Lao kip",
"Lebanese pound",
"Lesotho loti",
"Liberian dollar",
"Libyan dinar",
"Macanese pataca",
"Macedonian denar",
"Malagasy ariary",
"Malawian kwacha",
"Malaysian ringgit",
"Maldivian rufiyaa",
"Mauritanian ouguiya",
"Mauritian rupee",
"Mexican peso",
"Moldovan leu",
"Mongolian tögrög",
"Moroccan dirham",
"Mozambican metical",
"Namibian dollar",
"Nepalese rupee",
"Netherlands Antillean guilder",
"New Taiwan dollar",
"New Zealand dollar",
"Nicaraguan córdoba",
"Nigerian naira",
"North Korean won",
"Norwegian krone",
"Omani rial",
"Pakistani rupee",
"Panamanian balboa",
"Papua New Guinean kina",
"Paraguayan guaraní",
"Peruvian sol",
"Philippine peso",
"Polish złoty",
"Qatari riyal",
"Renminbi",
"Romanian leu",
"Russian ruble",
"Rwandan franc",
"Saint Helena pound",
"Samoan tālā",
"São Tomé and Príncipe dobra",
"Saudi riyal",
"Serbian dinar",
"Seychellois rupee",
"Sierra Leonean leone",
"Singapore dollar",
"Solomon Islands dollar",
"Somali shilling",
"South African rand",
"South Korean won",
"South Sudanese pound",
"Sri Lankan rupee",
"Sterling",
"Sudanese pound",
"Surinamese dollar",
"Swazi lilangeni",
"Swedish krona",
"Swiss franc",
"Syrian pound",
"Tajikistani somoni",
"Tanzanian shilling",
"Thai baht",
"Tongan paʻanga[O]",
"Trinidad and Tobago dollar",
"Tunisian dinar",
"Turkish lira",
"Turkmenistani manat",
"Tuvaluan dollar",
"Ugandan shilling",
"Ukrainian hryvnia",
"United Arab Emirates dirham",
"United States dollar",
"United States dollar[G]",
"Uruguayan peso",
"Uzbekistani sum",
"Vanuatu vatu",
"Venezuelan sovereign bolívar",
"Vietnamese đồng",
"West African CFA franc",
"Yemeni rial",
"Zambian kwacha",
"Zimbabwe gold",
+55
View File
@@ -0,0 +1,55 @@
version: "3.9"
services:
backend:
build:
context: ./backend
container_name: swagshop-backend
depends_on:
db:
condition: service_healthy
env_file:
- ./.env
networks:
- internal
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/utils/health-check/"]
interval: 10s
timeout: 5s
retries: 5
ports:
- "8000:8000"
frontend:
build:
context: ./frontend/
container_name: swagshop-frontend
env_file:
- ./frontend/.env
ports:
- "5173:5173"
db:
image: postgres:16
container_name: swagshop-postgres
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"]
interval: 10s
retries: 5
start_period: 30s
timeout: 10s
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: swagshop
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- internal
volumes:
pgdata:
networks:
internal:
driver: bridge
+2
View File
@@ -0,0 +1,2 @@
node_modules/
dist/
+2
View File
@@ -0,0 +1,2 @@
VITE_API_URL=http://localhost:8000
VITE_USE_MOCK_API=true
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+10
View File
@@ -0,0 +1,10 @@
{
"plugins": ["prettier-plugin-tailwindcss"],
"proseWrap": "always",
"requireConfig": false,
"useTabs": true,
"trailingComma": "none",
"bracketSpacing": true,
"jsxBracketSameLine": false,
"bracketSameLine": true
}
+14
View File
@@ -0,0 +1,14 @@
FROM node:20
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
RUN npm run build && npm install -g serve
EXPOSE 5173
CMD ["serve", "-s", "dist", "-l", "5173"]
+50
View File
@@ -0,0 +1,50 @@
# React + TypeScript + Vite
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,
},
})
```
+30
View File
@@ -0,0 +1,30 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": []
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": false,
"clientKind": "git",
"useIgnoreFile": false
},
"files": {
"ignoreUnknown": false,
"ignore": []
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "double"
}
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
+1
View File
@@ -0,0 +1 @@
"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD", "CAD", "CDF", "CLP", "CNY", "COP", "CRC", "CUP", "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GHS", "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HTG", "HUF", "CHF", "IDR", "ILS", "INR", "IQD", "IRR", "ISK", "JMD", "JOD", "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT", "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA", "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD", "RUB", "RWF", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLE", "SOS", "SRD", "SSP", "STN", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS", "VED", "VES", "VND", "VUV", "WST", "XAF", "XCD", "XOF", "XPF", "YER", "ZAR", "ZMW", "ZWG",
+28
View File
@@ -0,0 +1,28 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config(
{ ignores: ['dist'] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
)
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Swag Shop</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from "@hey-api/openapi-ts";
export default defineConfig({
input: "./openapi.json",
output: {
path: "./src/client"
},
plugins: [
"legacy/axios",
{
name: "@hey-api/sdk",
asClass: true,
operationId: true
}
]
});
+6992
View File
File diff suppressed because it is too large. Load diff
+95
View File
@@ -0,0 +1,95 @@
{
"name": "frontend",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"knip": "knip",
"generate-client": "openapi-ts",
"format:write": "biome format ./src/ --write",
"format:check": "biome check ./src/"
},
"dependencies": {
"@hookform/resolvers": "^3.9.1",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-alert-dialog": "^1.1.2",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-collapsible": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
"@radix-ui/react-icons": "^1.3.2",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.1",
"@radix-ui/react-scroll-area": "^1.2.1",
"@radix-ui/react-select": "^2.1.2",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.2.0",
"@radix-ui/react-switch": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.1",
"@radix-ui/react-toast": "^1.2.2",
"@radix-ui/react-tooltip": "^1.1.4",
"@radix-ui/react-visually-hidden": "^1.1.0",
"@tabler/icons-react": "^3.24.0",
"@tanstack/react-query": "^5.72.2",
"@tanstack/react-router": "^1.86.1",
"@tanstack/react-table": "^8.20.5",
"@vitejs/plugin-react": "^4.3.4",
"axios": "^1.7.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.0.4",
"date-fns": "^3.6.0",
"dexie": "^4.0.11",
"js-cookie": "^3.0.5",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.475.0",
"react": "^19.0.0",
"react-day-picker": "^9.6.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.0",
"react-phone-number-input": "^3.4.12",
"recharts": "^2.14.1",
"tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.1.0",
"zod": "^3.24.2",
"zustand": "^5.0.3"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@eslint/js": "^9.16.0",
"@faker-js/faker": "^9.3.0",
"@hey-api/client-axios": "^0.6.2",
"@hey-api/openapi-ts": "^0.64.11",
"@tanstack/eslint-plugin-query": "^5.62.1",
"@tanstack/react-query-devtools": "^5.62.3",
"@tanstack/router-devtools": "^1.86.1",
"@tanstack/router-plugin": "^1.86.0",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/js-cookie": "^3.0.6",
"@types/node": "^22.10.1",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.1",
"@vitejs/plugin-react-swc": "^3.7.2",
"autoprefixer": "^10.4.20",
"eslint": "^9.19.0",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.18",
"globals": "^15.14.0",
"knip": "^5.41.1",
"postcss": "^8.5.3",
"prettier": "^3.4.2",
"prettier-plugin-tailwindcss": "^0.6.9",
"tailwindcss": "^3.4.17",
"typescript": "~5.7.2",
"typescript-eslint": "^8.22.0",
"vite": "^6.1.0",
"vite-plugin-svgr": "^4.3.0"
}
}
+6157
View File
File diff suppressed because it is too large. Load diff
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+19
View File
@@ -0,0 +1,19 @@
import {
UserPublic,
UserRegister,
UserUpdate,
ShopLoginAccessTokenData,
} from "@/client";
import { Shop } from "./mock/models";
export interface AuthAPI {
getCurrentUser(): Promise<UserPublic | null>;
registerUser(data: UserRegister): Promise<void>;
loginUser(data: ShopLoginAccessTokenData): Promise<{ access_token: string }>;
updateUser(data: UserUpdate): Promise<void>;
}
export interface ShopAPI {
getShop(): Promise<Shop | null>;
updateShop(data: Partial<Shop>): Promise<void>;
}
+21
View File
@@ -0,0 +1,21 @@
import { RealAuthAPI } from "./real/auth-real-api";
import { MockAuthAPI } from "./mock/auth-mock-api";
import { AuthAPI, ShopAPI } from "./api-definition";
import { MockShopAPI } from "./mock/shop-mock-api";
import { MockProductAPI } from "./mock/products-mock-api";
import { MockUserAPI } from "./mock/user-mock-api";
import { MockCouponAPI } from "./mock/coupon-mock-api";
import { MockPurchaseAPI } from "./mock/purchase-mock-api";
export const authAPI: AuthAPI =
import.meta.env.VITE_USE_MOCK_API === "true" ? MockAuthAPI : RealAuthAPI;
export const shopAPI: ShopAPI = MockShopAPI;
export const productsAPI = MockProductAPI;
export const usersAPI = MockUserAPI;
export const couponAPI = MockCouponAPI;
export const purchaseAPI = MockPurchaseAPI;
+127
View File
@@ -0,0 +1,127 @@
import { AuthAPI } from "../api-definition";
import { v4 as uuidv4 } from "uuid";
import { UserRegister, UserUpdate, ShopLoginAccessTokenData } from "@/client";
import { extractSubFromJWT, generateFakeJWT } from "@/utils/jwt";
import { mockDB } from "./db";
import { MockUser, Shop } from "./models";
const db = mockDB;
export const MockAuthAPI: AuthAPI = {
async getCurrentUser() {
const token = localStorage.getItem("access_token");
if (!token) return null;
const userUUID = extractSubFromJWT(token);
if (!userUUID) return null;
const user = await db.users.where("uuid").equals(userUUID).first();
if (!user) return null;
const publicUser = {
...user,
first_name: user.first_name ?? null,
last_name: user.last_name ?? null,
profile_picture: user.profile_picture ?? null,
};
return publicUser;
},
async registerUser(data: UserRegister) {
const now = new Date().toISOString();
const userId = Date.now();
const userUUID = uuidv4();
const shopUUID = uuidv4();
const newUser: MockUser = {
id: userId,
uuid: userUUID,
user_role: "owner",
status: "owner",
shop_id: null,
username: data.username,
email: data.email,
password: data.password,
first_name: undefined,
last_name: undefined,
phone_number: data.phone_number,
profile_picture: undefined,
created_at: now,
updated_at: now,
last_login: now,
};
const newShop: Shop = {
id: userId,
uuid: shopUUID,
owner_id: userId,
name: `${data.username}'s Shop`,
description: "A newly created shop.",
created_at: now,
updated_at: now,
status: "inactive",
logo: null,
contact_email: data.email,
contact_phone_number: data.phone_number,
currency: "USD",
address: {
street: "",
city: "",
state: null,
postal_code: "",
country: "",
},
};
await mockDB.transaction(
"rw",
mockDB.users,
mockDB.shops,
mockDB.preferences,
mockDB.statistics,
async () => {
await mockDB.users.add(newUser);
await mockDB.preferences.add({ user_id: userId });
await mockDB.statistics.add({ user_id: userId, total_spend: 0 });
await mockDB.shops.add(newShop);
},
);
},
async loginUser(data: ShopLoginAccessTokenData) {
const user = await db.users
.where("email")
.equals(data.formData.username)
.first();
if (!user || user.password != data.formData.password) {
throw new Error("Invalid credentials");
}
await db.users.update(user.id, { last_login: new Date().toISOString() });
const token = generateFakeJWT(user.uuid);
localStorage.setItem("access_token", token);
return { access_token: token };
},
async updateUser(data: UserUpdate) {
const token = localStorage.getItem("access_token");
if (!token) return;
const userUUID = extractSubFromJWT(token);
if (!userUUID) return;
await db.users
.where("uuid")
.equals(userUUID)
.modify({
updated_at: new Date().toISOString(),
email: data.email ?? "",
phone_number: data.phone_number ?? "",
username: data.username ?? "",
first_name: data.first_name ?? undefined,
last_name: data.last_name ?? undefined,
});
},
};
+45
View File
@@ -0,0 +1,45 @@
import { mockDB } from "./db";
import { Coupon } from "./models";
export const MockCouponAPI = {
async getAllCoupons(): Promise<Coupon[]> {
return mockDB.coupons.toArray();
},
async getCouponById(id: number): Promise<Coupon | null> {
return (await mockDB.coupons.get(id)) ?? null;
},
async createCoupon(couponData: Omit<Coupon, "id">): Promise<Coupon> {
const id = Date.now();
const newCoupon: Coupon = {
...couponData,
id,
};
await mockDB.coupons.add(newCoupon);
return newCoupon;
},
async updateCoupon(
id: number,
data: Partial<Omit<Coupon, "id">>,
): Promise<Coupon | null> {
const existing = await mockDB.coupons.get(id);
if (!existing) return null;
const updated: Coupon = {
...existing,
...data,
};
await mockDB.coupons.put(updated);
return updated;
},
async deleteCoupon(id: number): Promise<boolean> {
await mockDB.coupons.delete(id);
return true;
},
};
@@ -0,0 +1,73 @@
import { MockPurchaseAPI } from "../purchase-mock-api";
import { mockDB } from "../db";
export async function createSamplePurchase() {
const users = await mockDB.users.where("user_role").equals("customer").toArray();
if (users.length === 0) throw new Error("No customers found");
const user = users[Math.floor(Math.random() * users.length)];
const products = await mockDB.products.toArray();
if (products.length === 0) throw new Error("No products found");
const entries = [];
for (let i = 0; i < Math.floor(Math.random() * 3) + 1; i++) {
const product = products[Math.floor(Math.random() * products.length)];
const quantity = Math.floor(Math.random() * 4) + 1;
entries.push({
purchase_id: 0,
product_id: product.id,
product_variant_id: 0,
quantity,
});
}
if (entries.length === 0) throw new Error("No suitable product variants found");
let total = 0;
for (const entry of entries) {
const product = await mockDB.products.get(entry.product_id);
if (product) {
total += product.price * entry.quantity;
}
}
const coupons = await mockDB.coupons.toArray();
const validCoupons = coupons.filter(
(c) => new Date(c.valid_due) > new Date()
);
const useCoupon = validCoupons.length > 0 && Math.random() < 0.5;
const chosenCoupon = useCoupon
? validCoupons[Math.floor(Math.random() * validCoupons.length)]
: null;
const discountedTotal = chosenCoupon
? Math.max(0, total - chosenCoupon.discount_amount)
: total;
// ✅ Generate a random date within the past year
const now = new Date();
const monthsAgo = Math.floor(Math.random() * 12); // 0–11 months ago
const date = new Date(now.getFullYear(), now.getMonth() - monthsAgo, 1);
const randomDay = Math.floor(Math.random() * 28) + 1;
date.setDate(randomDay);
const isoDate = date.toISOString();
const purchase = await MockPurchaseAPI.createPurchase(
{
user_id: user.id,
used_coupon_id: chosenCoupon?.id ?? null,
date_purchased: isoDate,
total: discountedTotal,
},
entries,
);
console.log(
`Created mock purchase${chosenCoupon ? " with coupon" : ""} on ${date.toDateString()}:`,
purchase,
);
}
+50
View File
@@ -0,0 +1,50 @@
import Dexie, { Table } from "dexie";
import {
Coupon,
MockUser,
MockUserPreferences,
MockUserStatistics,
Product,
ProductCategory,
ProductCategoryJunction,
ProductImage,
ProductVariant,
Purchase,
PurchaseEntry,
Shop,
} from "./models";
class MockDB extends Dexie {
users!: Table<MockUser, number>;
preferences!: Table<MockUserPreferences, number>;
statistics!: Table<MockUserStatistics, number>;
shops!: Table<Shop, number>;
products!: Table<Product, number>;
product_variants!: Table<ProductVariant, number>;
product_images!: Table<ProductImage, number>;
product_categories!: Table<ProductCategory, number>;
product_category_junctions!: Table<ProductCategoryJunction, [number, number]>;
coupons!: Table<Coupon, number>;
purchases!: Table<Purchase, number>;
purchase_entries!: Table<PurchaseEntry, number>;
constructor() {
super("MockDB");
this.version(1).stores({
users: "++id,username,email,uuid,user_role",
preferences: "user_id",
statistics: "user_id",
shops: "++id,uuid,name",
products: "++id,shop_id,name",
product_variants: "++id,product_id",
product_images: "++id,product_id",
product_categories: "++id,parent_category_id",
product_category_junctions: "[product_id+category_id]",
coupons: "++id,valid_due,name",
purchases: "++id,user_id,date_purchased",
purchase_entries: "++id,purchase_id,product_id,product_variant_id",
});
}
}
export const mockDB = new MockDB();
+140
View File
@@ -0,0 +1,140 @@
import { UserRegister } from "@/client";
export type UserRole = "owner" | "customer" | "employee" | "manager" | "admin";
export interface MockUser extends Omit<UserRegister, "password"> {
id: number;
uuid: string;
user_role: UserRole;
shop_id: number | null;
status: UserRole;
password: string;
created_at: string;
updated_at: string;
last_login?: string;
first_name?: string;
last_name?: string;
profile_picture?: string;
}
export interface MockUserPreferences {
user_id: number;
}
export interface MockUserStatistics {
user_id: number;
total_spend?: number;
}
export type ShopStatus = "active" | "inactive" | "suspended";
export interface ShopAddress {
street: string;
city: string;
state?: string | null;
postal_code: string;
country: string;
}
export interface Shop {
id: number;
uuid: string;
owner_id: number;
name: string;
description: string;
created_at: string;
updated_at: string;
status: ShopStatus;
logo?: string | null;
contact_email: string;
contact_phone_number: string;
address: ShopAddress;
currency: string;
}
export interface Product {
id: number;
shop_id: number;
name: string;
description: string;
price: number;
stock_quantity: number;
created_at: string;
updated_at: string;
}
export interface ProductCategoryJunction {
product_id: number;
category_id: number;
}
export interface ProductCategory {
id: number;
parent_category_id?: number;
name: string;
}
export interface ProductImage {
id: number;
product_id: number;
image_id: number;
image_url: string;
alt_text: string;
}
export interface ProductCreate {
name: string;
description: string;
price: number;
stock_quantity: number;
image_data?: string | undefined;
}
export interface ProductWithDetails extends Product {
images: ProductImage[];
categories: ProductCategory[];
}
export interface ProductVariant {
id: number;
product_id: number;
index: number;
name: string;
price: number;
comment?: string;
created_at: string;
updated_at: string;
}
export interface Coupon {
id: number;
name: string;
text: string;
valid_due: string;
discount_amount: number;
}
export interface Purchase {
id: number;
user_id: number;
used_coupon_id: number | null;
date_purchased: string;
total: number;
}
export interface PurchaseEntry {
id: number;
purchase_id: number;
product_id: number;
product_variant_id: number;
quantity: number;
}
export interface PurchaseWithDetails extends Purchase {
user: MockUser;
coupon?: Coupon | null;
entries: PurchaseEntryWithProduct[];
}
export interface PurchaseEntryWithProduct extends PurchaseEntry {
product: Product;
}
+158
View File
@@ -0,0 +1,158 @@
import { mockDB } from "./db";
import { ProductCategory, ProductCreate, ProductWithDetails } from "./models";
import { getCurrentUserDirect } from "./utils/currentUser";
export const MockProductAPI = {
async getProductsForShop(): Promise<ProductWithDetails[]> {
const user = await getCurrentUserDirect();
if (!user?.id) return [];
const products = await mockDB.products
.where("shop_id")
.equals(user.id)
.toArray();
const detailedProducts = await Promise.all(
products.map(async (product) => {
const images = await mockDB.product_images
.where("product_id")
.equals(product.id)
.toArray();
const variants = await mockDB.product_variants
.where("product_id")
.equals(product.id)
.toArray();
const categoryLinks = await mockDB.product_category_junctions
.where("product_id")
.equals(product.id)
.toArray();
const rawCategories = await Promise.all(
categoryLinks.map((link) =>
mockDB.product_categories.get(link.category_id),
),
);
const categories = rawCategories.filter(
(c): c is ProductCategory => c !== undefined,
);
return {
...product,
images,
variants,
categories,
};
}),
);
return detailedProducts;
},
async getProductById(productId: number): Promise<ProductWithDetails | null> {
const product = await mockDB.products.get(productId);
if (!product) return null;
const images = await mockDB.product_images
.where("product_id")
.equals(productId)
.toArray();
const categoryLinks = await mockDB.product_category_junctions
.where("product_id")
.equals(productId)
.toArray();
const rawCategories = await Promise.all(
categoryLinks.map((link) =>
mockDB.product_categories.get(link.category_id),
),
);
const categories = rawCategories.filter(
(c): c is ProductCategory => c !== undefined,
);
return {
...product,
images,
categories,
};
},
async createProduct(productData: ProductCreate) {
const user = await getCurrentUserDirect();
if (!user?.id) return null;
const now = new Date().toISOString();
const productId = Date.now();
await mockDB.products.add({
id: productId,
...productData,
shop_id: user.id,
created_at: now,
updated_at: now,
});
if ("image_data" in productData && productData.image_data) {
const image_id = Date.now();
await mockDB.product_images.add({
id: image_id,
product_id: productId,
image_id: image_id,
image_url: productData.image_data,
alt_text: `${productData.name} image`,
});
}
return mockDB.products.get(productId);
},
async updateProduct(productId: number, data: Partial<ProductCreate>) {
const product = await mockDB.products.get(productId);
if (!product) return null;
const updatedProduct = {
...product,
...data,
updated_at: new Date().toISOString(),
};
await mockDB.products.put(updatedProduct);
if (data.image_data) {
const existingImage = await mockDB.product_images
.where("product_id")
.equals(productId)
.first();
if (existingImage) {
await mockDB.product_images.put({
...existingImage,
image_url: data.image_data,
alt_text: `${data.name ?? product.name} image`,
});
} else {
const image_id = Date.now();
await mockDB.product_images.add({
id: image_id,
product_id: productId,
image_id: image_id,
image_url: data.image_data,
alt_text: `${data.name ?? product.name} image`,
});
}
}
return updatedProduct;
},
async deleteProduct(productId: number) {
await mockDB.product_images.where("product_id").equals(productId).delete();
await mockDB.product_variants
.where("product_id")
.equals(productId)
.delete();
await mockDB.product_category_junctions
.where("product_id")
.equals(productId)
.delete();
await mockDB.products.delete(productId);
return true;
},
};
@@ -0,0 +1,99 @@
import { mockDB } from "./db";
import {
Purchase,
PurchaseWithDetails,
PurchaseEntry,
PurchaseEntryWithProduct,
} from "./models";
export const MockPurchaseAPI = {
async getAllPurchases(): Promise<Purchase[]> {
return mockDB.purchases.toArray();
},
async getPurchaseById(purchaseId: number): Promise<Purchase | null> {
return (await mockDB.purchases.get(purchaseId)) ?? null;
},
async getPurchaseWithDetails(
purchaseId: number,
): Promise<PurchaseWithDetails | null> {
const purchase = await mockDB.purchases.get(purchaseId);
if (!purchase) return null;
const [user, coupon, entries] = await Promise.all([
mockDB.users.get(purchase.user_id),
purchase.used_coupon_id
? mockDB.coupons.get(purchase.used_coupon_id)
: null,
mockDB.purchase_entries.where("purchase_id").equals(purchaseId).toArray(),
]);
if (!user) return null;
const entriesWithProducts: PurchaseEntryWithProduct[] = await Promise.all(
entries.map(async (entry) => {
const product = await mockDB.products.get(entry.product_id);
if (!product) throw new Error("Product not found");
return { ...entry, product };
}),
);
return {
...purchase,
user,
coupon,
entries: entriesWithProducts,
};
},
async getPurchasesByUser(userId: number): Promise<Purchase[]> {
return mockDB.purchases.where("user_id").equals(userId).toArray();
},
async createPurchase(
purchase: Omit<Purchase, "id">,
entries: Omit<PurchaseEntry, "id">[] = [],
): Promise<Purchase> {
const id = Date.now();
const newPurchase: Purchase = {
...purchase,
id,
};
await mockDB.purchases.add(newPurchase);
if (entries.length > 0) {
await mockDB.purchase_entries.bulkAdd(
entries.map((entry) => ({
...entry,
purchase_id: id,
id: Date.now() + Math.random(),
})),
);
}
return newPurchase;
},
async updatePurchase(
purchaseId: number,
updates: Partial<Omit<Purchase, "id">>,
): Promise<Purchase | null> {
const existing = await mockDB.purchases.get(purchaseId);
if (!existing) return null;
const updated = { ...existing, ...updates };
await mockDB.purchases.put(updated);
return updated;
},
async deletePurchase(purchaseId: number): Promise<boolean> {
await mockDB.purchase_entries
.where("purchase_id")
.equals(purchaseId)
.delete();
await mockDB.purchases.delete(purchaseId);
return true;
},
};
+41
View File
@@ -0,0 +1,41 @@
import { ShopAPI } from "../api-definition";
import { mockDB } from "./db";
import { extractSubFromJWT } from "@/utils/jwt";
const db = mockDB;
export const MockShopAPI: ShopAPI = {
async getShop() {
const token = localStorage.getItem("access_token");
if (!token) return null;
const uuid = extractSubFromJWT(token);
if (!uuid) return null;
const user = await db.users.where("uuid").equals(uuid).first();
if (!user?.id) return null;
const dbShop = await db.shops.get(user.id);
return dbShop ?? null;
},
async updateShop(data) {
const token = localStorage.getItem("access_token");
console.log("Token ->", token);
if (!token) return;
const uuid = extractSubFromJWT(token);
console.log("UUID ->", uuid);
if (!uuid) return;
const user = await db.users.where("uuid").equals(uuid).first();
console.log("User ->", user);
if (!user?.id) return;
console.log("Saving data ->", data);
await db.shops.update(user.id, {
...data,
updated_at: new Date().toISOString(),
});
},
};
+60
View File
@@ -0,0 +1,60 @@
import { mockDB } from "./db";
import { MockUser } from "./models";
import { getCurrentUserDirect } from "./utils/currentUser";
export const MockUserAPI = {
async getAllUsers(): Promise<MockUser[]> {
return mockDB.users.toArray();
},
async getUserById(userId: number): Promise<MockUser | null> {
return (await mockDB.users.get(userId)) ?? null;
},
async createUser(
userData: Omit<MockUser, "id" | "created_at" | "updated_at">,
): Promise<MockUser> {
const now = new Date().toISOString();
const id = Date.now();
const newUser: MockUser = {
...userData,
id,
created_at: now,
updated_at: now,
};
await mockDB.users.add(newUser);
return newUser;
},
async updateUser(
userId: number,
data: Partial<Omit<MockUser, "id" | "created_at">>,
): Promise<MockUser | null> {
const existingUser = await mockDB.users.get(userId);
if (!existingUser) return null;
const updatedUser: MockUser = {
...existingUser,
...data,
updated_at: new Date().toISOString(),
};
await mockDB.users.put(updatedUser);
return updatedUser;
},
async deleteUser(userId: number): Promise<boolean> {
await mockDB.preferences.where("user_id").equals(userId).delete();
await mockDB.statistics.where("user_id").equals(userId).delete();
await mockDB.users.delete(userId);
return true;
},
async getCurrentUser(): Promise<MockUser | null> {
const user = await getCurrentUserDirect();
if (!user?.id) return null;
return (await mockDB.users.get(user.id)) ?? null;
},
};
@@ -0,0 +1,13 @@
import { mockDB } from "../db";
import { extractSubFromJWT } from "@/utils/jwt";
export async function getCurrentUserDirect() {
const token = localStorage.getItem("access_token");
if (!token) return null;
const uuid = extractSubFromJWT(token);
if (!uuid) return null;
const user = await mockDB.users.where("uuid").equals(uuid).first();
return user ?? null;
}
@@ -0,0 +1,7 @@
export const fileToBase64 = (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
+19
View File
@@ -0,0 +1,19 @@
import { DashboardService, LoginService, UserService } from "@/client";
import { AuthAPI } from "../api-definition";
export const RealAuthAPI: AuthAPI = {
async getCurrentUser() {
return DashboardService.userGetUser();
},
async registerUser(data) {
await DashboardService.userRegister({ requestBody: data });
},
async loginUser(data) {
return LoginService.dashboardLoginAccessToken({
formData: data.formData,
});
},
async updateUser(data) {
await UserService.userUpdateUser({ requestBody: data });
},
};
+12
View File
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg
viewBox="0 0 120 120"
xmlns="http://www.w3.org/2000/svg">
<rect width="120" height="120" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M33.2503 38.4816C33.2603 37.0472 34.4199 35.8864 35.8543 35.875H83.1463C84.5848 35.875 85.7503 37.0431 85.7503 38.4816V80.5184C85.7403 81.9528 84.5807 83.1136 83.1463 83.125H35.8543C34.4158 83.1236 33.2503 81.957 33.2503 80.5184V38.4816ZM80.5006 41.1251H38.5006V77.8751L62.8921 53.4783C63.9172 52.4536 65.5788 52.4536 66.6039 53.4783L80.5006 67.4013V41.1251ZM43.75 51.6249C43.75 54.5244 46.1005 56.8749 49 56.8749C51.8995 56.8749 54.25 54.5244 54.25 51.6249C54.25 48.7254 51.8995 46.3749 49 46.3749C46.1005 46.3749 43.75 48.7254 43.75 51.6249Z"
fill="currentColor"
/>
</svg>

After

Width:  |  Height:  |  Size: 763 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+15
View File
@@ -0,0 +1,15 @@
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF"/>
<stop offset="1" stop-color="#BD34FE"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83"/>
<stop offset="0.0833333" stop-color="#FFDD35"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+25
View File
@@ -0,0 +1,25 @@
import type { ApiRequestOptions } from "./ApiRequestOptions";
import type { ApiResult } from "./ApiResult";
export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: unknown;
public readonly request: ApiRequestOptions;
constructor(
request: ApiRequestOptions,
response: ApiResult,
message: string,
) {
super(message);
this.name = "ApiError";
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}
Loaded 100 of 328 files, more files were not shown because too many files have changed in this diff. Show more