Compare commits
74
Commits
e6ed37ad5e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
456570d4f0 | ||
|
|
7db93e9e8a | ||
|
|
6903a8deb0 | ||
|
|
f2af9dc566 | ||
|
|
2fe5dbcb21 | ||
|
|
c60ec969d5 | ||
|
|
0986336aea | ||
|
|
cd9f68dfc9 | ||
|
|
d2101247d1 | ||
|
|
cbb799f879 | ||
|
|
09ddb05efe | ||
|
|
260bef98cd | ||
|
|
d0690f2f06 | ||
|
|
d87c280109 | ||
|
|
a0d238c840 | ||
|
|
9c2b88b103 | ||
|
|
3b42ea6914 | ||
|
|
39b99d5509 | ||
|
|
75b2b1e142 | ||
|
|
20ef2aa4e8 | ||
|
|
cd8fdb9c21 | ||
|
|
f5547be799 | ||
|
|
543aadf521 | ||
|
|
272e765ca8 | ||
|
|
71e916586e | ||
|
|
c7e20fc935 | ||
|
|
9e56eeca3f | ||
|
|
5d12391635 | ||
|
|
f0c378b20f | ||
|
|
414b84a2d4 | ||
|
|
c3502f9ede | ||
|
|
a9c85cefe1 | ||
|
|
073f01d070 | ||
|
|
43063dcf4e | ||
|
|
6e9334ba0e | ||
|
|
4c8817e853 | ||
|
|
3b0336d9b4 | ||
|
|
3c005dbfa4 | ||
|
|
807d23da51 | ||
|
|
3d00ae0aad | ||
|
|
bb789a75e7 | ||
|
|
40aa0295b1 | ||
|
|
c018d2fb52 | ||
|
|
be7b8064d6 | ||
|
|
80dd89212f | ||
|
|
3aaa14c986 | ||
|
|
628d210156 | ||
|
|
4fb09e8fd7 | ||
|
|
a22d8becbb | ||
|
|
4f86361ee8 | ||
|
|
aef6dd2761 | ||
|
|
7342516ce7 | ||
|
|
c310586fca | ||
|
|
f23003e28d | ||
|
|
69ca94c77b | ||
|
|
b1fb6ee099 | ||
|
|
f259aa3993 | ||
|
|
0f7d69899a | ||
|
|
dfccda02b8 | ||
|
|
248bf45044 | ||
|
|
3c35ac4075 | ||
|
|
3d6f50399c | ||
|
|
56bc0f2d69 | ||
|
|
2a2783fd09 | ||
|
|
afc9db32e4 | ||
|
|
031ed358f1 | ||
|
|
105bbd3e96 | ||
|
|
f3f1652890 | ||
|
|
2eae36e8fd | ||
|
|
bc0dae4f97 | ||
|
|
fb4f91a1d6 | ||
|
|
caa8b76c88 | ||
|
|
2694701337 | ||
|
|
78aa03b2fb |
No files matched your search
@@ -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
@@ -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
|
.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
|
||||||
Vendored
+50
-13
@@ -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",
|
"version": "0.2.0",
|
||||||
"configurations": [
|
"configurations": [
|
||||||
{
|
{
|
||||||
"name": "Flask Shop",
|
"name": "Backend",
|
||||||
"type": "debugpy",
|
"type": "debugpy",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"program": "${workspaceFolder}/main.py",
|
"cwd": "${workspaceFolder}/backend",
|
||||||
"args": [],
|
"module": "fastapi",
|
||||||
"cwd": "${workspaceFolder}",
|
"args": ["dev", "${cwd}/backend/app/main.py"],
|
||||||
"env": {
|
|
||||||
"FLASK_APP": "main.py",
|
|
||||||
"FLASK_ENV": "development"
|
|
||||||
},
|
|
||||||
"console": "internalConsole",
|
"console": "internalConsole",
|
||||||
"autoReload": {"enable": true},
|
"serverReadyAction":{
|
||||||
"justMyCode": true
|
"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"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Vendored
+13
-12
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
// #region Backend settings
|
||||||
"blocklist",
|
"files.exclude" : { "**/__pycache__/**": true }, // Hide __pycache__ directories
|
||||||
"displayname",
|
"mypy-type-checker.args" : ["--config-file='backend/mypy.ini'"], // Override mypy config
|
||||||
"dotenv",
|
"python.defaultInterpreterPath" : "./backend/.venv/bin/python", // Use venv by default
|
||||||
"gensalt",
|
"python.analysis.extraPaths" : ["./backend"], // Pylint - fix for import analysis
|
||||||
"hashpw",
|
"pylint.cwd" : "${workspaceFolder}/backend",
|
||||||
"checkpw",
|
// #endregion
|
||||||
"jsonify",
|
|
||||||
"lastrowid",
|
// #region Frontend settings
|
||||||
"rtype"
|
"prettier.configPath": "./frontend/.prettierrc", // Prettier config override
|
||||||
]
|
"biome.lspBin": "${workspaceFolder}/frontend"
|
||||||
}
|
// #endregion
|
||||||
|
}
|
||||||
Vendored
+14
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
from app.api.routes import main_routes, error_routes, product_routes, user_routes, cart_routes
|
|
||||||
@@ -1,59 +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('', methods=['GET'])
|
|
||||||
@jwt_required()
|
|
||||||
def show_cart():
|
|
||||||
user_id = get_jwt_identity()
|
|
||||||
|
|
||||||
result, status_code = CartService.show_cart(user_id)
|
|
||||||
|
|
||||||
return result, status_code
|
|
||||||
|
|
||||||
@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)
|
|
||||||
|
|
||||||
if count < 1:
|
|
||||||
return abort(400)
|
|
||||||
|
|
||||||
result, status_code = CartService.add_to_cart(user_id, product_id, count)
|
|
||||||
|
|
||||||
return result, status_code
|
|
||||||
|
|
||||||
@bp_cart.route('/remove/<int:product_id>', methods=['DELETE'])
|
|
||||||
@jwt_required()
|
|
||||||
def remove_from_cart(product_id: int):
|
|
||||||
user_id = get_jwt_identity()
|
|
||||||
|
|
||||||
result, status_code = CartService.delete_from_cart(user_id, product_id)
|
|
||||||
|
|
||||||
return result, status_code
|
|
||||||
|
|
||||||
@bp_cart.route('/update/<int:product_id>', methods=['PUT'])
|
|
||||||
@jwt_required()
|
|
||||||
def update_count_in_cart(product_id: int):
|
|
||||||
user_id = get_jwt_identity()
|
|
||||||
count = request.args.get('count', type=int)
|
|
||||||
|
|
||||||
if not count:
|
|
||||||
return abort(400)
|
|
||||||
|
|
||||||
result, status_code = CartService.update_count(user_id, product_id, count)
|
|
||||||
|
|
||||||
return result, status_code
|
|
||||||
|
|
||||||
@bp_cart.route('/purchase', methods=['GET'])
|
|
||||||
@jwt_required()
|
|
||||||
def purchase():
|
|
||||||
user_id = get_jwt_identity()
|
|
||||||
|
|
||||||
result, status_code = CartService.purchase(user_id)
|
|
||||||
|
|
||||||
return result, status_code
|
|
||||||
@@ -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 occurred 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
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from flask import jsonify, abort
|
|
||||||
|
|
||||||
from app.api import bp
|
|
||||||
|
|
||||||
@bp.route('/')
|
|
||||||
def hello():
|
|
||||||
return jsonify({'message': 'Hello, Flask!'})
|
|
||||||
@@ -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')
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
@@ -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')
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -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
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
from mysql.connector import Error
|
|
||||||
from typing import Tuple, Union
|
|
||||||
|
|
||||||
from app.extensions import 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(dictionary=True) as cursor:
|
|
||||||
cursor.execute("select count from cart_item where cart_id = %s and product_id = %s", (user_id, product_id))
|
|
||||||
result = cursor.fetchone()
|
|
||||||
|
|
||||||
if cursor.rowcount != 0:
|
|
||||||
cursor.execute("update cart_item set count = count + %s where cart_id = %s and product_id = %s", (count, user_id, product_id))
|
|
||||||
else:
|
|
||||||
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. Reason: {e}"}, 400
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def update_count(user_id: str, product_id: int, count: int) -> Tuple[Union[dict, str], int]:
|
|
||||||
try:
|
|
||||||
if count <= 0:
|
|
||||||
return CartService.delete_from_cart(user_id, product_id)
|
|
||||||
|
|
||||||
with db_connection.cursor(dictionary=True) as cursor:
|
|
||||||
cursor.execute("update cart_item set count = %s where cart_id = %s and product_id = %s", (count, user_id, product_id))
|
|
||||||
db_connection.commit()
|
|
||||||
|
|
||||||
return {"Success": "Successfully added to cart"}, 200
|
|
||||||
|
|
||||||
except Error as e:
|
|
||||||
return {"Failed": f"Failed to update item count in cart. Reason: {e}"}, 400
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def delete_from_cart(user_id: str, product_id: int) -> Tuple[Union[dict, str], int]:
|
|
||||||
try:
|
|
||||||
with db_connection.cursor() as cursor:
|
|
||||||
cursor.execute("delete from cart_item where cart_id = %s and product_id = %s", (user_id, product_id))
|
|
||||||
db_connection.commit()
|
|
||||||
|
|
||||||
return {"Success": "Successfully removed item from cart"}, 200
|
|
||||||
except Error as e:
|
|
||||||
return {"Failed": f"Failed to remove item from cart. Reason: {e}"}, 400
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def show_cart(user_id: str) -> Tuple[Union[dict, str], int]:
|
|
||||||
try:
|
|
||||||
with db_connection.cursor(dictionary=True) as cursor:
|
|
||||||
cursor.execute("select product.name as product_name, count, price_subtotal, date_added from cart_item inner join product on cart_item.product_id = product.id where cart_item.cart_id = %s", (user_id,))
|
|
||||||
rows = cursor.fetchall()
|
|
||||||
|
|
||||||
results = []
|
|
||||||
|
|
||||||
for row in rows:
|
|
||||||
mid_result = {
|
|
||||||
"name": row['product_name'],
|
|
||||||
"count": row['count'],
|
|
||||||
"price_subtotal": row['price_subtotal'],
|
|
||||||
"date_added": row['date_added']
|
|
||||||
}
|
|
||||||
|
|
||||||
results.append(mid_result)
|
|
||||||
|
|
||||||
return results, 200
|
|
||||||
|
|
||||||
except Error as e:
|
|
||||||
return {"Failed": f"Failed to load cart. Reason: {e}"}, 400
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def purchase(user_id: str) -> Tuple[Union[dict, str], int]:
|
|
||||||
try:
|
|
||||||
with db_connection.cursor(dictionary=True) as cursor:
|
|
||||||
# get all cart items
|
|
||||||
cursor.execute("select id, product_id, count, price_subtotal from cart_item where cart_id = %s", (user_id,))
|
|
||||||
results = cursor.fetchall()
|
|
||||||
|
|
||||||
if len(results) < 1:
|
|
||||||
return {"Failed": "Failed to purchase. Cart is Empty"}, 400
|
|
||||||
|
|
||||||
# create a purchase
|
|
||||||
cursor.execute("insert into purchase(user_id) values (%s)", (user_id,))
|
|
||||||
|
|
||||||
last_id = cursor.lastrowid
|
|
||||||
|
|
||||||
parsed = []
|
|
||||||
ids = []
|
|
||||||
|
|
||||||
for row in results:
|
|
||||||
mid_row = (
|
|
||||||
last_id,
|
|
||||||
row['product_id'],
|
|
||||||
row['count'],
|
|
||||||
row['price_subtotal']
|
|
||||||
)
|
|
||||||
|
|
||||||
row_id = row['id']
|
|
||||||
|
|
||||||
parsed.append(mid_row)
|
|
||||||
ids.append(row_id)
|
|
||||||
|
|
||||||
insert_query = "INSERT INTO purchase_item (purchase_id, product_id, count, price_subtotal) VALUES (%s, %s, %s, %s)"
|
|
||||||
for row in parsed:
|
|
||||||
cursor.execute(insert_query, row)
|
|
||||||
|
|
||||||
delete_query = "delete from cart_item where id = %s"
|
|
||||||
for one_id in ids:
|
|
||||||
cursor.execute(delete_query, (one_id,))
|
|
||||||
|
|
||||||
db_connection.commit()
|
|
||||||
|
|
||||||
|
|
||||||
# clear cart
|
|
||||||
except Error as e:
|
|
||||||
return {"Failed": f"Failed to load cart. Reason: {e}"}, 400
|
|
||||||
|
|
||||||
return {"Success": "Successfully purchased"}, 200
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
@@ -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)
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[mypy-sqlalchemy.*]
|
||||||
|
follow_untyped_imports = True
|
||||||
@@ -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"]
|
||||||
Whitespace-only changes.
@@ -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)
|
||||||
@@ -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)]
|
||||||
Whitespace-only changes.
@@ -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
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -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.")
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Whitespace-only changes.
@@ -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()
|
||||||
@@ -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"""
|
||||||
@@ -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)
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
Whitespace-only changes.
@@ -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"]
|
||||||
@@ -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()
|
||||||
@@ -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")
|
||||||
@@ -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"]
|
||||||
@@ -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.
@@ -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)
|
||||||
Whitespace-only changes.
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Generated
+777
@@ -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 },
|
||||||
|
]
|
||||||
@@ -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",
|
||||||
@@ -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",
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
VITE_API_URL=http://localhost:8000
|
||||||
|
VITE_USE_MOCK_API=true
|
||||||
@@ -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?
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"],
|
||||||
|
"proseWrap": "always",
|
||||||
|
"requireConfig": false,
|
||||||
|
"useTabs": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"jsxBracketSameLine": false,
|
||||||
|
"bracketSameLine": true
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
@@ -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 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -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>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
Generated
+6992
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+6157
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
@@ -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 |
@@ -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>;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
@@ -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 });
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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
Reference in new issue
Block a user