Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba5e90c516 | ||
|
|
5cdcd342dc | ||
|
|
9fd44c0ad9 | ||
|
|
c4a6a88d05 | ||
|
|
da8284ae64 | ||
|
|
5ca1e2af15 | ||
|
|
d65c4117ff | ||
|
|
5e3dfda882 | ||
|
|
6a36b468e5 | ||
|
|
230dd5201b | ||
|
|
e0ebf45ccb | ||
|
|
41486fd35a | ||
|
|
b65c68204e | ||
|
|
198506f062 | ||
|
|
862a1eeddc | ||
|
|
97e4b07c47 | ||
|
|
443d40d7b6 | ||
|
|
727491fbb8 | ||
|
|
209049734e | ||
|
|
98294ce91f | ||
|
|
c913909775 | ||
|
|
098461cd58 | ||
|
|
f7e8b6b947 |
@@ -21,6 +21,9 @@ Thumbs.db
|
|||||||
*.env
|
*.env
|
||||||
#db
|
#db
|
||||||
*.db
|
*.db
|
||||||
|
DB/
|
||||||
|
#logs
|
||||||
|
logs/
|
||||||
|
|
||||||
#Примеры документов
|
#Примеры документов
|
||||||
input/
|
input/
|
||||||
|
|||||||
+14
-2
@@ -1,4 +1,16 @@
|
|||||||
SECRET_KEY = "change_me"
|
SECRET_KEY = "change_me"
|
||||||
ALGORITHM = "HS256"
|
ALGORITHM = "HS256"
|
||||||
ACCESS_TOKEN_EXPIRE_MINUTES = 15
|
ACCESS_TOKEN_EXPIRE_MINUTES = 15 #int
|
||||||
REFRESH_TOKEN_EXPIRE_DAYS= 45
|
REFRESH_TOKEN_EXPIRE_DAYS= 45 #int
|
||||||
|
|
||||||
|
DB_USER="change_me"
|
||||||
|
DB_PASSWORD="change_me"
|
||||||
|
DB_POSTGRESS="change_me"
|
||||||
|
DB_HOST="change_me"
|
||||||
|
DB_PORT="change_me"
|
||||||
|
|
||||||
|
REDIS_PASSWORD="change_me"
|
||||||
|
REDIS_PORT=change_me #int
|
||||||
|
REDIS_HOST="change_me"
|
||||||
|
|
||||||
|
PROD_MODE=bool
|
||||||
+63
-1
@@ -1,4 +1,4 @@
|
|||||||
name: excel-project
|
name: disexcel
|
||||||
|
|
||||||
services:
|
services:
|
||||||
backend-dev:
|
backend-dev:
|
||||||
@@ -11,6 +11,8 @@ services:
|
|||||||
target: dev
|
target: dev
|
||||||
init: true #Manage processes and reap zombies
|
init: true #Manage processes and reap zombies
|
||||||
ipc: private #Inter-Process Communication (IPC) namespace for high-performance applications
|
ipc: private #Inter-Process Communication (IPC) namespace for high-performance applications
|
||||||
|
environment:
|
||||||
|
- DB_HOST=psql #rewrite DB_HOST var to communicate inside the docker network
|
||||||
volumes:
|
volumes:
|
||||||
- type: bind
|
- type: bind
|
||||||
source: ../src
|
source: ../src
|
||||||
@@ -27,8 +29,16 @@ services:
|
|||||||
- type: bind
|
- type: bind
|
||||||
source: ../uploads
|
source: ../uploads
|
||||||
target: /home/excel-project/uploads
|
target: /home/excel-project/uploads
|
||||||
|
- type: bind
|
||||||
|
source: ../logs
|
||||||
|
target: /home/excel-project/logs
|
||||||
networks:
|
networks:
|
||||||
- backend
|
- backend
|
||||||
|
depends_on:
|
||||||
|
psql:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "80:8000"
|
- "80:8000"
|
||||||
entrypoint: ["./entrypoint.sh", "--dev"]
|
entrypoint: ["./entrypoint.sh", "--dev"]
|
||||||
@@ -43,6 +53,8 @@ services:
|
|||||||
target: prod
|
target: prod
|
||||||
init: true #Manage processes and reap zombies
|
init: true #Manage processes and reap zombies
|
||||||
ipc: private #Inter-Process Communication (IPC) namespace for high-performance applications
|
ipc: private #Inter-Process Communication (IPC) namespace for high-performance applications
|
||||||
|
environment:
|
||||||
|
- DB_HOST=psql
|
||||||
volumes:
|
volumes:
|
||||||
- type: bind
|
- type: bind
|
||||||
source: ../configs
|
source: ../configs
|
||||||
@@ -53,12 +65,62 @@ services:
|
|||||||
- type: bind
|
- type: bind
|
||||||
source: ../uploads
|
source: ../uploads
|
||||||
target: /home/excel-project/uploads
|
target: /home/excel-project/uploads
|
||||||
|
- type: bind
|
||||||
|
source: ../logs
|
||||||
|
target: /home/excel-project/logs
|
||||||
networks:
|
networks:
|
||||||
- backend
|
- backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
psql:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "80:8000"
|
- "80:8000"
|
||||||
|
|
||||||
|
psql:
|
||||||
|
profiles: ["prod", "dev", "db"]
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: psql
|
||||||
|
init: true
|
||||||
|
ipc: private
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${DB_USER}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
POSTGRES_DB: ${DB_POSTGRESS}
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ../DB
|
||||||
|
target: /var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_POSTGRESS}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
ports:
|
||||||
|
- "${DB_PORT}:5432"
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:latest
|
||||||
|
profiles: ["prod", "dev", "redis"]
|
||||||
|
container_name: redis
|
||||||
|
init: true
|
||||||
|
ipc: private
|
||||||
|
ports:
|
||||||
|
- '${REDIS_PORT}:6379'
|
||||||
|
command: ["redis-server", "--requirepass", "${REDIS_PASSWORD}", "--appendonly", "yes"]
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "redis-cli -a ${REDIS_PASSWORD} ping | grep PONG"]
|
||||||
|
interval: 1s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
backend:
|
backend:
|
||||||
name: "${BACKEND_NETWORK:-backend_network}"
|
name: "${BACKEND_NETWORK:-backend_network}"
|
||||||
+35
-11
@@ -1,12 +1,13 @@
|
|||||||
# --- Stage 1: Python Backend dev ---
|
# --- Stage 1: Python Backend dev ---
|
||||||
|
|
||||||
FROM python:3.14-slim AS dev
|
FROM python:3.14-slim AS dev
|
||||||
|
|
||||||
LABEL org.opencontainers.image.title="the-great-excel-project-dev"
|
LABEL org.opencontainers.image.title="The-DisExcel-project-dev"
|
||||||
LABEL org.opencontainers.image.source="https://git.homyk.space/MH.Dmitrii/The_Great_Excel_Project"
|
LABEL org.opencontainers.image.source="https://git.homyk.space/MH.Dmitrii/The_DisExcel_project"
|
||||||
|
|
||||||
WORKDIR /home/excel-project
|
WORKDIR /home/excel-project
|
||||||
|
|
||||||
COPY pyproject.toml poetry.lock docker/entrypoint.sh ./
|
COPY pyproject.toml poetry.lock docker/entrypoint.sh alembic.ini ./
|
||||||
|
|
||||||
RUN chmod +x ./entrypoint.sh
|
RUN chmod +x ./entrypoint.sh
|
||||||
|
|
||||||
@@ -14,28 +15,51 @@ RUN pip install --no-cache-dir --break-system-packages poetry \
|
|||||||
&& poetry config virtualenvs.create false \
|
&& poetry config virtualenvs.create false \
|
||||||
&& poetry install --no-root --no-interaction
|
&& poetry install --no-root --no-interaction
|
||||||
|
|
||||||
|
|
||||||
|
# --- Stage 1: Python Backend builder ---
|
||||||
|
|
||||||
|
FROM python:3.14-slim AS builder
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="The-DisExcel-project-dev"
|
||||||
|
LABEL org.opencontainers.image.source="https://git.homyk.space/MH.Dmitrii/The_DisExcel_project"
|
||||||
|
|
||||||
|
WORKDIR /home/excel-project
|
||||||
|
|
||||||
|
ENV VIRTUAL_ENV=/opt/venv
|
||||||
|
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|
||||||
|
RUN python -m venv "$VIRTUAL_ENV"
|
||||||
|
|
||||||
|
COPY pyproject.toml poetry.lock ./
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir poetry \
|
||||||
|
&& poetry config virtualenvs.create false \
|
||||||
|
&& poetry install --no-root --no-interaction --only main \
|
||||||
|
&& pip uninstall -y poetry poetry-core poetry-plugin-export
|
||||||
|
|
||||||
# --- Stage 2: Python Backend prod ---
|
# --- Stage 2: Python Backend prod ---
|
||||||
|
|
||||||
FROM python:3.14-slim AS prod
|
FROM python:3.14-slim AS prod
|
||||||
|
|
||||||
LABEL org.opencontainers.image.title="the-great-excel-project-prod"
|
LABEL org.opencontainers.image.title="The-DisExcel-project-prod"
|
||||||
LABEL org.opencontainers.image.source="https://git.homyk.space/MH.Dmitrii/The_Great_Excel_Project"
|
LABEL org.opencontainers.image.source="https://git.homyk.space/MH.Dmitrii/The_DisExcel_project"
|
||||||
|
|
||||||
WORKDIR /home/excel-project
|
WORKDIR /home/excel-project
|
||||||
|
|
||||||
COPY pyproject.toml poetry.lock main.py docker/entrypoint.sh ./
|
ENV VIRTUAL_ENV=/opt/venv
|
||||||
|
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|
||||||
|
COPY --from=builder /opt/venv /opt/venv
|
||||||
|
|
||||||
|
COPY pyproject.toml poetry.lock main.py docker/entrypoint.sh alembic.ini ./
|
||||||
COPY src/ ./src/
|
COPY src/ ./src/
|
||||||
|
|
||||||
RUN chmod +x ./entrypoint.sh
|
RUN chmod +x ./entrypoint.sh
|
||||||
|
|
||||||
RUN pip install --no-cache-dir --break-system-packages poetry \
|
|
||||||
&& poetry config virtualenvs.create false \
|
|
||||||
&& poetry install --no-root --no-interaction --only main
|
|
||||||
|
|
||||||
RUN groupadd --gid 1000 appuser \
|
RUN groupadd --gid 1000 appuser \
|
||||||
&& useradd --uid 1000 --gid appuser --shell /bin/bash --create-home appuser \
|
&& useradd --uid 1000 --gid appuser --shell /bin/bash --create-home appuser \
|
||||||
&& chown -R appuser:appuser /home/excel-project
|
&& chown -R appuser:appuser /home/excel-project
|
||||||
|
|
||||||
USER appuser
|
USER appuser
|
||||||
|
|
||||||
ENTRYPOINT ["./entrypoint.sh", "--prod"]
|
ENTRYPOINT ["./entrypoint.sh", "--prod"]
|
||||||
@@ -21,6 +21,11 @@ if [ -z "$MODE" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if ! alembic upgrade head; then
|
||||||
|
echo "Migration failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "$MODE" = "dev" ]; then
|
if [ "$MODE" = "dev" ]; then
|
||||||
WORKERS="${WORKERS:-1}"
|
WORKERS="${WORKERS:-1}"
|
||||||
exec gunicorn \
|
exec gunicorn \
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ from pathlib import Path
|
|||||||
# import uvicorn
|
# import uvicorn
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from src.cache.redis_client import redis_client
|
||||||
|
from src.database.users.crud import Seed
|
||||||
|
from src.logging.logger import LoggingMiddleware, ProcessingTimeMiddleware
|
||||||
from src.web.protected_routes.auth_routes import router as protected_router
|
from src.web.protected_routes.auth_routes import router as protected_router
|
||||||
from src.web.protected_routes.protected_user_action_routes import (
|
from src.web.protected_routes.protected_user_action_routes import (
|
||||||
router as protected_user_action_routes,
|
router as protected_user_action_routes,
|
||||||
@@ -13,11 +16,14 @@ from src.web.protected_routes.protected_user_action_routes import (
|
|||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
create_dirs()
|
create_dirs()
|
||||||
|
await create_first_user()
|
||||||
yield
|
yield
|
||||||
print("shutting down")
|
await redis_client.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
app=FastAPI(root_path="/", lifespan=lifespan)
|
app=FastAPI(root_path="/", lifespan=lifespan)
|
||||||
|
app.add_middleware(LoggingMiddleware)
|
||||||
|
app.add_middleware(ProcessingTimeMiddleware)
|
||||||
app.include_router(router=protected_router)
|
app.include_router(router=protected_router)
|
||||||
app.include_router(router=protected_user_action_routes)
|
app.include_router(router=protected_user_action_routes)
|
||||||
|
|
||||||
@@ -27,12 +33,18 @@ async def root()->dict:
|
|||||||
return {"root":"hello, this is root"}
|
return {"root":"hello, this is root"}
|
||||||
|
|
||||||
|
|
||||||
def create_dirs():
|
def create_dirs()->None:
|
||||||
|
|
||||||
dirs_to_create=("./DB",
|
dirs_to_create=("./DB",
|
||||||
"./uploads/upload",
|
"./uploads/upload",
|
||||||
"./uploads/upload_bad",
|
"./uploads/upload_bad",
|
||||||
"./uploads/upload_finished")
|
"./uploads/upload_finished",
|
||||||
|
"./logs")
|
||||||
|
|
||||||
for x in dirs_to_create:
|
for x in dirs_to_create:
|
||||||
Path(x).mkdir(parents=True, exist_ok=True)
|
Path(x).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
async def create_first_user()->None:
|
||||||
|
seed=Seed()
|
||||||
|
await seed.seed()
|
||||||
|
|
||||||
@@ -3,6 +3,8 @@ ALLURE:=.venv/allure-2.44.0/bin/allure #linux&macos
|
|||||||
#ALLURE=.venv\allure-2.44.0\bin\allure #Windows
|
#ALLURE=.venv\allure-2.44.0\bin\allure #Windows
|
||||||
|
|
||||||
NUM_DOWN ?= 1
|
NUM_DOWN ?= 1
|
||||||
|
#make run-dev BUILD=--build
|
||||||
|
BUILD ?=
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
@@ -15,23 +17,27 @@ help:
|
|||||||
##
|
##
|
||||||
.PHONY: run
|
.PHONY: run
|
||||||
run: ## Run dev local application
|
run: ## Run dev local application
|
||||||
${VENV} uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
docker compose -f docker/compose-dev.yaml --env-file configs/.env --profile db --profile redis up -d && ${VENV} uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|
||||||
|
.PHONY: down
|
||||||
|
down: ## Down dev local db
|
||||||
|
docker compose -f docker/compose-dev.yaml --env-file configs/.env --profile db --profile redis down
|
||||||
|
|
||||||
.PHONY: run-dev
|
.PHONY: run-dev
|
||||||
run-dev: ## Run dev application
|
run-dev: ## Run dev application
|
||||||
docker compose -f docker/compose-dev.yaml --profile dev up -d
|
docker compose -f docker/compose-dev.yaml --env-file configs/.env --profile dev up -d ${BUILD}
|
||||||
|
|
||||||
.PHONY: run-prod
|
.PHONY: run-prod
|
||||||
run-prod: ## Run prod application
|
run-prod: ## Run prod application
|
||||||
docker compose -f docker/compose-dev.yaml --profile prod up -d
|
docker compose -f docker/compose-dev.yaml --env-file configs/.env --profile prod up -d ${BUILD}
|
||||||
|
|
||||||
.PHONY: down-dev
|
.PHONY: down-dev
|
||||||
down-dev: ## Down dev application
|
down-dev: ## Down dev application
|
||||||
docker compose -f docker/compose-dev.yaml --profile dev down
|
docker compose -f docker/compose-dev.yaml --env-file configs/.env --profile dev down
|
||||||
|
|
||||||
.PHONY: down-prod
|
.PHONY: down-prod
|
||||||
down-prod: ## Down prod application
|
down-prod: ## Down prod application
|
||||||
docker compose -f docker/compose-dev.yaml --profile prod down
|
docker compose -f docker/compose-dev.yaml --env-file configs/.env --profile prod down
|
||||||
|
|
||||||
##
|
##
|
||||||
## Migration section
|
## Migration section
|
||||||
|
|||||||
Generated
+228
-71
@@ -1,21 +1,17 @@
|
|||||||
# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 2.4.1 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiosqlite"
|
name = "aiofiles"
|
||||||
version = "0.22.1"
|
version = "25.1.0"
|
||||||
description = "asyncio bridge to the standard sqlite3 module"
|
description = "File support for asyncio."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb"},
|
{file = "aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695"},
|
||||||
{file = "aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650"},
|
{file = "aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
|
||||||
dev = ["attribution (==1.8.0)", "black (==25.11.0)", "build (>=1.2)", "coverage[toml] (==7.10.7)", "flake8 (==7.3.0)", "flake8-bugbear (==24.12.12)", "flit (==3.12.0)", "mypy (==1.19.0)", "ufmt (==2.8.0)", "usort (==1.0.8.post1)"]
|
|
||||||
docs = ["sphinx (==8.1.3)", "sphinx-mdinclude (==0.6.2)"]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alembic"
|
name = "alembic"
|
||||||
version = "1.18.5"
|
version = "1.18.5"
|
||||||
@@ -126,6 +122,76 @@ files = [
|
|||||||
astroid = ["astroid (>=2,<5)"]
|
astroid = ["astroid (>=2,<5)"]
|
||||||
test = ["astroid (>=2,<5)", "pytest (<9.0)", "pytest-cov", "pytest-xdist"]
|
test = ["astroid (>=2,<5)", "pytest (<9.0)", "pytest-cov", "pytest-xdist"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "asyncpg"
|
||||||
|
version = "0.31.0"
|
||||||
|
description = "An asyncio PostgreSQL driver"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9.0"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "asyncpg-0.31.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:831712dd3cf117eec68575a9b50da711893fd63ebe277fc155ecae1c6c9f0f61"},
|
||||||
|
{file = "asyncpg-0.31.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0b17c89312c2f4ccea222a3a6571f7df65d4ba2c0e803339bfc7bed46a96d3be"},
|
||||||
|
{file = "asyncpg-0.31.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3faa62f997db0c9add34504a68ac2c342cfee4d57a0c3062fcf0d86c7f9cb1e8"},
|
||||||
|
{file = "asyncpg-0.31.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ea599d45c361dfbf398cb67da7fd052affa556a401482d3ff1ee99bd68808a1"},
|
||||||
|
{file = "asyncpg-0.31.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:795416369c3d284e1837461909f58418ad22b305f955e625a4b3a2521d80a5f3"},
|
||||||
|
{file = "asyncpg-0.31.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a8d758dac9d2e723e173d286ef5e574f0b350ec00e9186fce84d0fc5f6a8e6b8"},
|
||||||
|
{file = "asyncpg-0.31.0-cp310-cp310-win32.whl", hash = "sha256:2d076d42eb583601179efa246c5d7ae44614b4144bc1c7a683ad1222814ed095"},
|
||||||
|
{file = "asyncpg-0.31.0-cp310-cp310-win_amd64.whl", hash = "sha256:9ea33213ac044171f4cac23740bed9a3805abae10e7025314cfbd725ec670540"},
|
||||||
|
{file = "asyncpg-0.31.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eee690960e8ab85063ba93af2ce128c0f52fd655fdff9fdb1a28df01329f031d"},
|
||||||
|
{file = "asyncpg-0.31.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2657204552b75f8288de08ca60faf4a99a65deef3a71d1467454123205a88fab"},
|
||||||
|
{file = "asyncpg-0.31.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a429e842a3a4b4ea240ea52d7fe3f82d5149853249306f7ff166cb9948faa46c"},
|
||||||
|
{file = "asyncpg-0.31.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0807be46c32c963ae40d329b3a686356e417f674c976c07fa49f1b30303f109"},
|
||||||
|
{file = "asyncpg-0.31.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e5d5098f63beeae93512ee513d4c0c53dc12e9aa2b7a1af5a81cddf93fe4e4da"},
|
||||||
|
{file = "asyncpg-0.31.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37fc6c00a814e18eef51833545d1891cac9aa69140598bb076b4cd29b3e010b9"},
|
||||||
|
{file = "asyncpg-0.31.0-cp311-cp311-win32.whl", hash = "sha256:5a4af56edf82a701aece93190cc4e094d2df7d33f6e915c222fb09efbb5afc24"},
|
||||||
|
{file = "asyncpg-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:480c4befbdf079c14c9ca43c8c5e1fe8b6296c96f1f927158d4f1e750aacc047"},
|
||||||
|
{file = "asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad"},
|
||||||
|
{file = "asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d"},
|
||||||
|
{file = "asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a"},
|
||||||
|
{file = "asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671"},
|
||||||
|
{file = "asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec"},
|
||||||
|
{file = "asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20"},
|
||||||
|
{file = "asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8"},
|
||||||
|
{file = "asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186"},
|
||||||
|
{file = "asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b"},
|
||||||
|
{file = "asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e"},
|
||||||
|
{file = "asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403"},
|
||||||
|
{file = "asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4"},
|
||||||
|
{file = "asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2"},
|
||||||
|
{file = "asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602"},
|
||||||
|
{file = "asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696"},
|
||||||
|
{file = "asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d"},
|
||||||
|
{file = "asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3"},
|
||||||
|
{file = "asyncpg-0.31.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ebb3cde58321a1f89ce41812be3f2a98dddedc1e76d0838aba1d724f1e4e1a95"},
|
||||||
|
{file = "asyncpg-0.31.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e6974f36eb9a224d8fb428bcf66bd411aa12cf57c2967463178149e73d4de366"},
|
||||||
|
{file = "asyncpg-0.31.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc2b685f400ceae428f79f78b58110470d7b4466929a7f78d455964b17ad1008"},
|
||||||
|
{file = "asyncpg-0.31.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb223567dea5f47c45d347f2bde5486be8d9f40339f27217adb3fb1c3be51298"},
|
||||||
|
{file = "asyncpg-0.31.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:22be6e02381bab3101cd502d9297ac71e2f966c86e20e78caead9934c98a8af6"},
|
||||||
|
{file = "asyncpg-0.31.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:37a58919cfef2448a920df00d1b2f821762d17194d0dbf355d6dde8d952c04f9"},
|
||||||
|
{file = "asyncpg-0.31.0-cp39-cp39-win32.whl", hash = "sha256:c1a9c5b71d2371a2290bc93336cd05ba4ec781683cab292adbddc084f89443c6"},
|
||||||
|
{file = "asyncpg-0.31.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1e1ab5bc65373d92dd749d7308c5b26fb2dc0fbe5d3bf68a32b676aa3bcd24a"},
|
||||||
|
{file = "asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
gssauth = ["gssapi ; platform_system != \"Windows\"", "sspilib ; platform_system == \"Windows\""]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
version = "26.1.0"
|
version = "26.1.0"
|
||||||
@@ -1477,6 +1543,83 @@ files = [
|
|||||||
dev = ["abi3audit", "black", "check-manifest", "colorama ; os_name == \"nt\"", "coverage", "packaging", "psleak", "pylint", "pyperf", "pypinfo", "pyreadline3 ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""]
|
dev = ["abi3audit", "black", "check-manifest", "colorama ; os_name == \"nt\"", "coverage", "packaging", "psleak", "pylint", "pyperf", "pypinfo", "pyreadline3 ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""]
|
||||||
test = ["psleak", "pytest", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "setuptools", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""]
|
test = ["psleak", "pytest", "pytest-instafail", "pytest-xdist", "pywin32 ; os_name == \"nt\" and implementation_name != \"pypy\"", "setuptools", "wheel ; os_name == \"nt\" and implementation_name != \"pypy\"", "wmi ; os_name == \"nt\" and implementation_name != \"pypy\""]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg2-binary"
|
||||||
|
version = "2.9.12"
|
||||||
|
description = "psycopg2 - Python-PostgreSQL Database Adapter"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b818ceff717f98851a64bffd4c5eb5b3059ae280276dcecc52ac658dcf006a4"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2fa0d7caca8635c56e373055094eeda3208d901d55dd0ff5abc1d4e47f82b56"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:864c261b3690e1207d14bbfe0a61e27567981b80c47a778561e49f676f7ce433"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c5ee5213445dd45312459029b8c4c0a695461eb517b753d2582315bd07995f5e"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f9cae1f848779b5b01f417e762c40d026ea93eb0648249a604728cda991dde3"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:63a3ebbd543d3d1eda088ac99164e8c5bac15293ee91f20281fd17d050aee1c4"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d6fcbba8c9fed08a73b8ac61ea79e4821e45b1e92bb466230c5e746bbf3d5256"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:36512911ebb2b60a0c3e44d0bb5048c1980aced91235d133b7874f3d1d93487c"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:8ffdb59fe88f99589e34354a130217aa1fd2d615612402d6edc8b3dbc7a44463"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a46fe069b65255df410f856d842bc235f90e22ffdf532dda625fd4213d3fd9b1"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab29414b25dcb698bf26bf213e3348abdcd07bbd5de032a5bec15bd75b298b03"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5c8ce6c61bd1b1f6b9c24ee32211599f6166af2c55abb19456090a21fd16554b"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b4a9eaa6e7f4ff91bec10aa3fb296878e75187bced5cc4bafe17dc40915e1326"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c6528cefc8e50fcc6f4a107e27a672058b36cc5736d665476aeb413ba88dbb06"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e4e184b1fb6072bf05388aa41c697e1b2d01b3473f107e7ec44f186a32cfd0b8"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4766ab678563054d3f1d064a4db19cc4b5f9e3a8d9018592a8285cf200c248f3"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5a0253224780c978746cb9be55a946bcdaf40fe3519c0f622924cdabdafe2c39"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0dc9228d47c46bda253d2ecd6bb93b56a9f2d7ad33b684a1fa3622bf74ffe30c"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f921f3cd87035ef7df233383011d7a53ea1d346224752c1385f1edfd790ceb6a"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3d999bd982a723113c1a45b55a7a6a90d64d0ed2278020ed625c490ff7bef96c"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29d4d134bd0ab46ffb04e94aa3c5fa3ef582e9026609165e2f758ff76fc3a3be"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp311-cp311-win_amd64.whl", hash = "sha256:cb4a1dacdd48077150dc762a9e5ddbf32c256d66cb46f80839391aa458774936"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7c729a73c7b1b84de3582f73cdd27d905121dc2c531f3d9a3c32a3011033b965"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4413d0caef93c5cf50b96863df4c2efe8c269bf2267df353225595e7e15e8df7"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4dfcf8e45ebb0c663be34a3442f65e17311f3367089cd4e5e3a3e8e62c978777"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c41321a14dd74aceb6a9a643b9253a334521babfa763fa873e33d89cfa122fb5"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83946ba43979ebfdc99a3cd0ee775c89f221df026984ba19d46133d8d75d3cd9"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:411e85815652d13560fbe731878daa5d92378c4995a22302071890ec3397d019"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c8ad4c08e00f7679559eaed7aff1edfffc60c086b976f93972f686384a95e2c"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:00814e40fa23c2b37ef0a1e3c749d89982c73a9cb5046137f0752a22d432e82f"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:98062447aebc20ed20add1f547a364fd0ef8933640d5372ff1873f8deb9b61be"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:66a7685d7e548f10fb4ce32fb01a7b7f4aa702134de92a292c7bd9e0d3dbd290"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:b6937f5fe4e180aeee87de907a2fa982ded6f7f15d7218f78a083e4e1d68f2a0"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f3b3de8a74ef8db215f22edffb19e32dc6fa41340456de7ec99efdc8a7b3ec2"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1006fb62f0f0bc5ce256a832356c6262e91be43f5e4eb15b5eaf38079464caf2"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:840066105706cd2eb29b9a1c2329620056582a4bf3e8169dec5c447042d0869f"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:863f5d12241ebe1c76a72a04c2113b6dc905f90b9cef0e9be0efd994affd9354"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a99eaab34a9010f1a086b126de467466620a750634d114d20455f3a824aae033"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd7dc5463ccd61845ac37b7012d0f35a1548df9febe14f8dd549be4a0bc81e"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:54a0dfecab1b48731f934e06139dfe11e24219fb6d0ceb32177cf0375f14c7b5"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:96937c9c5d891f772430f418a7a8b4691a90c3e6b93cf72b5bd7cad8cbca32a5"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:77b348775efd4cdab410ec6609d81ccecd1139c90265fa583a7255c8064bc03d"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:527e6342b3e44c2f0544f6b8e927d60de7f163f5723b8f1dfa7d2a84298738cd"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:f12ae41fcafadb39b2785e64a40f9db05d6de2ac114077457e0e7c597f3af980"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ee2d84ef5eb6c04702d2e9c372ad557fb027f26a5d82804f749dfb14c7fdd2ab"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cfa2517c94ea3af6deb46f81e1bbd884faa63e28481eb2f889989dd8d95e5f03"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ba3df2fc42a1cfa45b72cf096d4acb2b885937eedc61461081d53538d4a82a86"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:718e1fc18edf573b02cb8aea868de8d8d33f99ce9620206aa9144b67b0985e94"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c7cb4cbf894a1d36c720d713de507952c7c58f66d30834708f03dbe5c822ccf"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:049366c6d884bdcd65d66e6ca1fdbebe670b56c6c9ba46f164e6667e90881964"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fb1828cf3da68f99e45ebce1355d65d2d12b6a78fb5dfb16247aad6bdef5f5d2"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:127467c6e476dd876634f17c3d870530e73ff454ff99bff73d36e80af28e1115"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:ace94261f43850e9e79f6c56636c5e0147978ab79eda5e5e5ebf13ae146fc8fe"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a7e39a65b7d2a20e4ba2e0aaad1960b61cc2888d6ab047769f8347bd3c9ad915"},
|
||||||
|
{file = "psycopg2_binary-2.9.12-cp39-cp39-win_amd64.whl", hash = "sha256:f625abb7020e4af3432d95342daa1aa0db3fa369eed19807aa596367ba791b10"},
|
||||||
|
{file = "psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ptyprocess"
|
name = "ptyprocess"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -1869,6 +2012,26 @@ files = [
|
|||||||
{file = "python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e"},
|
{file = "python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "redis"
|
||||||
|
version = "8.1.0"
|
||||||
|
description = "Python client for Redis database and key-value store"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "redis-8.1.0-py3-none-any.whl", hash = "sha256:a4fe1aac3d3b3cc791d4b3d5931c5a956045dc951ee74d1c913ee3ac4d2ee9fb"},
|
||||||
|
{file = "redis-8.1.0.tar.gz", hash = "sha256:6e1a19beef9225c83efd689c7e6b7da2d5215b1f42cd13b7fc3714d0a09c7b25"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
circuit-breaker = ["pybreaker (>=1.4.0)"]
|
||||||
|
hiredis = ["hiredis (>=3.2.0)"]
|
||||||
|
jwt = ["pyjwt (>=2.13.0)"]
|
||||||
|
ocsp = ["cryptography (>=36.0.1)", "pyopenssl (>=20.0.1)", "requests (>=2.31.0)"]
|
||||||
|
otel = ["opentelemetry-api (>=1.39.1)", "opentelemetry-exporter-otlp-proto-http (>=1.39.1)", "opentelemetry-sdk (>=1.39.1)"]
|
||||||
|
xxhash = ["xxhash (>=3.6.0,<3.7.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.34.2"
|
version = "2.34.2"
|
||||||
@@ -2022,74 +2185,68 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlalchemy"
|
name = "sqlalchemy"
|
||||||
version = "2.0.51"
|
version = "2.0.52"
|
||||||
description = "Database Abstraction Library"
|
description = "Database Abstraction Library"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "sqlalchemy-2.0.51-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e8203d2fbd5c6254692ef0a72c740d75b2f3c7ca345404f4c1a4604813c77c0"},
|
{file = "sqlalchemy-2.0.52-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a7438774e1091192fc50a2bd8ceff5c596912d00ecd46587e88effdea7826101"},
|
||||||
{file = "sqlalchemy-2.0.51-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1af05726b3d0cdba1c55284bf408fd3b792e690fe2399bfb8304565551cda652"},
|
{file = "sqlalchemy-2.0.52-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c1b7ed45bf87b214e0a9def9c2313949067efe6269db5ef18d542ee13250af7"},
|
||||||
{file = "sqlalchemy-2.0.51-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e54ff2dd657f2e3e0fbf2b097db1182f7bfea263eca4353f00065bae2a67c3d"},
|
{file = "sqlalchemy-2.0.52-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:309cc8ba50fc5d2174189dfcd49cdf7aa711f8346afcff19f2642ae4fc449c14"},
|
||||||
{file = "sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1e47b1199c2e832e325eacabc8d32d2487f58c9358f97e9a00f5eb93c5680d84"},
|
{file = "sqlalchemy-2.0.52-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2f9eccf8793c8c3f8dd2dfd11b9e400cb27d1d19370ef732b66017e212107822"},
|
||||||
{file = "sqlalchemy-2.0.51-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c68568f3facf8f66fa76c60e0ced69b67666ffa9941d1d0a3756fda196049080"},
|
{file = "sqlalchemy-2.0.52-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9255ceb65a80c1b001129060b63ee776a2e9c288be3b662be36dfbb888fffdcd"},
|
||||||
{file = "sqlalchemy-2.0.51-cp310-cp310-win32.whl", hash = "sha256:0592bdadf86ddcabfd72d9ab66ea8a5d8d2cc6be1cc51fa7e66c03868ac5eac1"},
|
{file = "sqlalchemy-2.0.52-cp310-cp310-win32.whl", hash = "sha256:2e15b1d1116a64fc399b8c2694a83f3e792fdc58df28514a81e1dc4f8cf22729"},
|
||||||
{file = "sqlalchemy-2.0.51-cp310-cp310-win_amd64.whl", hash = "sha256:740cf6f35351b1ac3d82369152acf1d51d37e3dcf85d4dc0a22ca01410eabe2a"},
|
{file = "sqlalchemy-2.0.52-cp310-cp310-win_amd64.whl", hash = "sha256:11560064cc4696e772298b6221ede59e646386d9f2a85d549365473b972f7850"},
|
||||||
{file = "sqlalchemy-2.0.51-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1aa10c0daee6705294d181daadaa793221e1a59ed55000a3fab1d42b088ce4ba"},
|
{file = "sqlalchemy-2.0.52-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0c3ce43907374889f3352bdcc6195c970148a2cb71574cd0237a5071a37fb6c"},
|
||||||
{file = "sqlalchemy-2.0.51-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5b2ed6d828f1f09bd812861f4f59ca3bc3803f9df871f4555187f0faf018604"},
|
{file = "sqlalchemy-2.0.52-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a0d48c4b80717c61385b4e966e087c839a66cfd7b780641dcb428f4dba65608"},
|
||||||
{file = "sqlalchemy-2.0.51-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:436728ce18a80f6951a1e11cc6112c2ede9faf20766f1a26195a7c441ca12dbd"},
|
{file = "sqlalchemy-2.0.52-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938325a5373267afc53bfbe72983b20fbd64ca47842aac62433c3da1137ecff1"},
|
||||||
{file = "sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc261707bf5739aea8a541593f3cc1d463c2701fb05fbcbba0ce031b69a21260"},
|
{file = "sqlalchemy-2.0.52-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f8438a98d49424acf69d0d53c0a522951dfe49a6f2d86417fbb37ad3066ab43"},
|
||||||
{file = "sqlalchemy-2.0.51-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a6d26094615306d116dd5e4a51b0304c99dd2356fc569eed6922a80a6bd3b265"},
|
{file = "sqlalchemy-2.0.52-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4699dbb8d396d199e7e78fd4d525e3ad3d6008a9c8c0160b87e74c606c2c3736"},
|
||||||
{file = "sqlalchemy-2.0.51-cp311-cp311-win32.whl", hash = "sha256:ca8435d13829b92f4a97362d91975154a4015db3a2634154e1754e9a915e6b86"},
|
{file = "sqlalchemy-2.0.52-cp311-cp311-win32.whl", hash = "sha256:cef328349452ae152637df4d11ce5a0919ecdf0a363e16c830c3518ee33bde72"},
|
||||||
{file = "sqlalchemy-2.0.51-cp311-cp311-win_amd64.whl", hash = "sha256:4a011ea4510683319ce4ed274b56ee05194b39b6da9d09ca7a39388f0fa84dcc"},
|
{file = "sqlalchemy-2.0.52-cp311-cp311-win_amd64.whl", hash = "sha256:f1c850792a3b25a3ad74dade3f05e4f402cdebfea27438bcadafaa1617f77bcc"},
|
||||||
{file = "sqlalchemy-2.0.51-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d78702b26ba1c18b2d0fb2ea940ba7f17a9581b42e8361ff93920ebbee1235a"},
|
{file = "sqlalchemy-2.0.52-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be8c49131665dfe2cc74c498aa1240ffb548d0fd901325dd11c2c7a18956f727"},
|
||||||
{file = "sqlalchemy-2.0.51-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581921d849d6e6f994d560389192955e80e2950e18fcdfe2ccea863e01158e6e"},
|
{file = "sqlalchemy-2.0.52-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b2d9e507a458832adcfbd8af6e2036ddf069b7710b799448542ebccae2dceee"},
|
||||||
{file = "sqlalchemy-2.0.51-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d21ce524ab86c23046e992a5b81cb54c21079c6df6e78b8fc77d77cac70a6b9"},
|
{file = "sqlalchemy-2.0.52-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8738008376d22f30f411ea3efecf39b51110b6996d80bb73786f30bcfdd5fd3b"},
|
||||||
{file = "sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c5d98a2709840027f5a347c3af0a7c3d5f6c1ff93af2ca1c54494e23cba8f389"},
|
{file = "sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37a4d548327b6cab9c7d8cdb4e0e82feabee0110c4d150059068e2d1cfbd99ee"},
|
||||||
{file = "sqlalchemy-2.0.51-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1181256e0f16479691b5616d36375dc2620ad8332b25978763c3d206ad3f3f1d"},
|
{file = "sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e49f51a5d59857a7a0dcaf9469febf7197d9394bd88f00d69c2c4e848112cdbf"},
|
||||||
{file = "sqlalchemy-2.0.51-cp312-cp312-win32.whl", hash = "sha256:9f380393be5abeb6815f68fd39271b95127173511b6706b0a630a9995d53f8f5"},
|
{file = "sqlalchemy-2.0.52-cp312-cp312-win32.whl", hash = "sha256:afda3ec521d0517d0de783fc70030775841900896d832de5bbd066549290470e"},
|
||||||
{file = "sqlalchemy-2.0.51-cp312-cp312-win_amd64.whl", hash = "sha256:2cf39aabdf48e87c1c2c2ed6d20d33ffa0733b3071ce9c5f66357947dd009080"},
|
{file = "sqlalchemy-2.0.52-cp312-cp312-win_amd64.whl", hash = "sha256:2d5e53e36e37129fe0be8b9d08b6e4052c10a963ee6cda56c8c10dcc194b99ca"},
|
||||||
{file = "sqlalchemy-2.0.51-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c2056838b6685b72fdb36c99996cf862753461a62f2e84f4196371d3b2d6a07"},
|
{file = "sqlalchemy-2.0.52-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2eb3c6a64b1bfe6704777cfd504e7b8ad093a5f3e03ce67663a5e6742f294e43"},
|
||||||
{file = "sqlalchemy-2.0.51-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483b11bd46bf35fc14c52faf338b04300c9e6ce554bce9b11be85bfec3bc3195"},
|
{file = "sqlalchemy-2.0.52-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:923bb183c1dc64fdf7b717965e3d59938ec4f8b8710b419a21ce403e5da9a9e1"},
|
||||||
{file = "sqlalchemy-2.0.51-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1bed1ee8b01da6088210aa9412023326fb98a599ba502e6118308601dcbef77f"},
|
{file = "sqlalchemy-2.0.52-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:651d6d8782e80679e6151707c7b490834d46ada526328895abf567f25e63d29c"},
|
||||||
{file = "sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72ca54c952107ba5cd58854b67a5a6268631289d21651a1235396f3b98b47400"},
|
{file = "sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b08cddb8989775e3c88799d86704bdfc3ee6e9846118201aa5997f16f27e3a15"},
|
||||||
{file = "sqlalchemy-2.0.51-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b3e693d15533a45cd5906f0589f9c35090bef6ef45bf1e8195c424aa0ae06a8d"},
|
{file = "sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab66fa9618269390d4dfa222f2f2f88f7bc4bf5da13905131b818217db7e8057"},
|
||||||
{file = "sqlalchemy-2.0.51-cp313-cp313-win32.whl", hash = "sha256:b93ab07b5292dbe7e6b8da89475275e7042744283921344b56105f3eeb0f828b"},
|
{file = "sqlalchemy-2.0.52-cp313-cp313-win32.whl", hash = "sha256:c63bda077685c85ca513286547a531ba57e7a68cf0a7ed3bafcc2bbd18896f4d"},
|
||||||
{file = "sqlalchemy-2.0.51-cp313-cp313-win_amd64.whl", hash = "sha256:0f053118c30e53161857a953e4de667d90e274980dccbe5dd3829bbbeece72a5"},
|
{file = "sqlalchemy-2.0.52-cp313-cp313-win_amd64.whl", hash = "sha256:9876b09b9f1ce7398b0ffece585c0a911244c53191187341f6bcae640e133751"},
|
||||||
{file = "sqlalchemy-2.0.51-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6ea306caaae6bd5afd0a46050003c88f6bf33227377a49298c498c3cb88ff491"},
|
{file = "sqlalchemy-2.0.52-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:410d52be41d17f1a236d19520fbe776257dc16516ed06bd16d433311842aefd9"},
|
||||||
{file = "sqlalchemy-2.0.51-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45a496d6bc05dec41dcd4c3a2b183723f47473255c159cd80b503c8f246424d"},
|
{file = "sqlalchemy-2.0.52-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfe9ce533dbe4d0a2ae1486546619bd30b76bcd670539a44d910361376175f5e"},
|
||||||
{file = "sqlalchemy-2.0.51-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4004ada0aafe8ae1991b2cd1d99c6d9146126e123bd6f883c260d974aa012e54"},
|
{file = "sqlalchemy-2.0.52-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:812bae5138bfc0aa46fb0686da0fc7f581f68e2bbb05bc24c3713bebaedd1437"},
|
||||||
{file = "sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f6bcad487aee1c638d707235682fc96f741de00663619881ab235400d03289e"},
|
{file = "sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:50bff43b632a56fbf5ed9afdd76307e1512b62051bcd5afb341ae67205bbb6c8"},
|
||||||
{file = "sqlalchemy-2.0.51-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:39a76529db6305693d8d4affa58ad5b5e2e18edd62daea628b29b97930b3513d"},
|
{file = "sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:49565daf5af554f538e23aef1fc81a95a4e49658f152285e45c02f5fc44f04cd"},
|
||||||
{file = "sqlalchemy-2.0.51-cp314-cp314-win32.whl", hash = "sha256:08a204d8b5638717c26a24df18fcf40af45a6b22e35b70b1d62f0113c2e278e8"},
|
{file = "sqlalchemy-2.0.52-cp314-cp314-win32.whl", hash = "sha256:ab9da41e61b9979b910499d633b241df20c51ee5037e5405b11c2faac3cbe1a2"},
|
||||||
{file = "sqlalchemy-2.0.51-cp314-cp314-win_amd64.whl", hash = "sha256:96747bfbadb055466e5b46d572618170046b45ce5a4879167f50d70a5319a499"},
|
{file = "sqlalchemy-2.0.52-cp314-cp314-win_amd64.whl", hash = "sha256:a593db51b3bae75db17a5738ad5f992244b3a03863f83c28117ee482c6a3f76d"},
|
||||||
{file = "sqlalchemy-2.0.51-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5ea1a213be1fcd5e49d9904c3b9939211ded90bc2a64e93f4c01963474285de"},
|
{file = "sqlalchemy-2.0.52-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1e61d08bdf4ee2f41024569e3400de7d6734ba498144766b11260936ccfa582"},
|
||||||
{file = "sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c6b36ed71f41942bdcd2ad2522be46bfce09d5705be5640ecf19bbc7660e4b7"},
|
{file = "sqlalchemy-2.0.52-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1b92a1e23ed40022081217b40d2d1feba4f77064e69ef4f39f68bcbbd148452a"},
|
||||||
{file = "sqlalchemy-2.0.51-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c2c62877097e1a0db401fba5cb4debee33265e5b2a55c4ccb489c02c53b4f72"},
|
{file = "sqlalchemy-2.0.52-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77a247d3fd179f6583171e7e0e98f40dc6642ed4f655557515a5a7e25923e9a4"},
|
||||||
{file = "sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0378d055e9e8cd6ce4d8dff683bdd3d7d413533c4ee51d67a2b1e0f9eacc0f23"},
|
{file = "sqlalchemy-2.0.52-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd9206024b8602e7518bbaf44016c29e0045722f09328d8e654941023920d0b3"},
|
||||||
{file = "sqlalchemy-2.0.51-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6e46fc36029eff666391e0531e5387b62ce6c4f1d8e50b3fb3099eaca1b42522"},
|
{file = "sqlalchemy-2.0.52-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:46f0c46f0d360d727b84660b26c62b295d82306ec2c82b701e97747d2c6dcbe1"},
|
||||||
{file = "sqlalchemy-2.0.51-cp314-cp314t-win32.whl", hash = "sha256:9161cfc9efce70d1715f47d6ff40f79c6778c00d53be4fbc09d70301e4b83ba7"},
|
{file = "sqlalchemy-2.0.52-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df8f213ceb485d8227b74935eb87ba0d80169a8401eba7835da6e30d6727dac4"},
|
||||||
{file = "sqlalchemy-2.0.51-cp314-cp314t-win_amd64.whl", hash = "sha256:159bb6ba32059f57ad7375a8f50d844dd2f19d14954ecf820cd33e20debd46b2"},
|
{file = "sqlalchemy-2.0.52-cp38-cp38-win32.whl", hash = "sha256:f2b09029ef6f260409eefa5dc2b8276f6c3d7b892bfb50d50e8f852257d4a6b4"},
|
||||||
{file = "sqlalchemy-2.0.51-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bb1f5062f98b0b3290e72b707747fdd7e0f22d6956b236ba7ca7f5c9971d2da2"},
|
{file = "sqlalchemy-2.0.52-cp38-cp38-win_amd64.whl", hash = "sha256:765f439da5bc8696973bc0c8a31fae0912ac3ff1cb9d66246a6b2728ee4fbbc8"},
|
||||||
{file = "sqlalchemy-2.0.51-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:247acaa29ccef6250dfd6a3eedf8f94ddf23564180a39fe362e32ae9dbdbde46"},
|
{file = "sqlalchemy-2.0.52-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b89e93bb89eabdbea9d5d3fa2d6cc6544e733c33064339f91e5292480cf130e"},
|
||||||
{file = "sqlalchemy-2.0.51-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c95ef01f53233a305a874a44a63fbfb1d81cd79b49de0f8529b3548cde437e37"},
|
{file = "sqlalchemy-2.0.52-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf993f065bc04caa5000b339e8d9d6f3d9d00251511f850147c516c9e07115f"},
|
||||||
{file = "sqlalchemy-2.0.51-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fa268106c8987639a17a18514cfe0cd9bf17420ab887e1e1bf486da8836135b1"},
|
{file = "sqlalchemy-2.0.52-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cce4922535db73f9dbb91e3db2b3e851ac629467fd1ebd8e354a60e369521c63"},
|
||||||
{file = "sqlalchemy-2.0.51-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:b7f08588854bbb724041d9ae9d980d40040c922382e1d9a2ecb390edc4fd5032"},
|
{file = "sqlalchemy-2.0.52-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2f5fa2b2aca75d2c7f36db3a8dd04717b6fbfd1a964fb32bdeae16698e475ab3"},
|
||||||
{file = "sqlalchemy-2.0.51-cp38-cp38-win32.whl", hash = "sha256:6b588fd681ddf0c196b8df1ea49a8913514894b2b8f945a9511b4b48871f99c8"},
|
{file = "sqlalchemy-2.0.52-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f4d4f7afc682961dc567db70e00a7b5bd81ccd3743c46199b0257f0744902dde"},
|
||||||
{file = "sqlalchemy-2.0.51-cp38-cp38-win_amd64.whl", hash = "sha256:ca216e8af5c05e326efc7e28716ac2381a7cf9791749f5ee1849dccdc99c9b00"},
|
{file = "sqlalchemy-2.0.52-cp39-cp39-win32.whl", hash = "sha256:de89de5b5798cafdd7ef7b7b804acec246d6152922128fd9d156cd1701271aff"},
|
||||||
{file = "sqlalchemy-2.0.51-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aa18ae738b5170e253ad0bb6c4b0f07585081e8a6e50893e4d911d47b39a0904"},
|
{file = "sqlalchemy-2.0.52-cp39-cp39-win_amd64.whl", hash = "sha256:3c95c3044edddb65e4a2f7194ec52ca5a9736f72d33ca3a6fa4196aedcc689fd"},
|
||||||
{file = "sqlalchemy-2.0.51-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59cab3686b1bc039dd9cded2f8d0c08a246e84e76bd4ab5b4f18c7cdae293825"},
|
{file = "sqlalchemy-2.0.52-py3-none-any.whl", hash = "sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89"},
|
||||||
{file = "sqlalchemy-2.0.51-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111604e637da87031255ddc26c7d7bc22bc6af6f5d459ccff3af1b4660233a85"},
|
{file = "sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97"},
|
||||||
{file = "sqlalchemy-2.0.51-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ad30ae663711786303fbcd46a47516302d201ee49a877cb3fac61f672895110a"},
|
|
||||||
{file = "sqlalchemy-2.0.51-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b21f0e7efc7a5c509e953784e9d1575ebb8b4318960e7e7d7a93bb803626cf64"},
|
|
||||||
{file = "sqlalchemy-2.0.51-cp39-cp39-win32.whl", hash = "sha256:a42ad6afcbaaa777241e347aa2e29155993045a0d6b7db74da61053ffe875fe0"},
|
|
||||||
{file = "sqlalchemy-2.0.51-cp39-cp39-win_amd64.whl", hash = "sha256:2a97eaad21c84b4ef8010b11eeba9fe6153eb0b3df3ff8b6abc309df1b978ef7"},
|
|
||||||
{file = "sqlalchemy-2.0.51-py3-none-any.whl", hash = "sha256:bb024d8b621d0be75f4f44ecc7c950450026e76d66dc8f791bb5331d7fed59d5"},
|
|
||||||
{file = "sqlalchemy-2.0.51.tar.gz", hash = "sha256:804dccd8a4a6242c4e30ad961e540e18a588f6527202f2d6791b01845d59fdc9"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
greenlet = {version = ">=1", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""}
|
greenlet = {version = ">=1", optional = true, markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\" or extra == \"asyncio\""}
|
||||||
typing-extensions = ">=4.6.0"
|
typing-extensions = ">=4.6.0"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
@@ -2097,7 +2254,7 @@ aiomysql = ["aiomysql (>=0.2.0)", "greenlet (>=1)"]
|
|||||||
aioodbc = ["aioodbc", "greenlet (>=1)"]
|
aioodbc = ["aioodbc", "greenlet (>=1)"]
|
||||||
aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"]
|
aiosqlite = ["aiosqlite", "greenlet (>=1)", "typing_extensions (!=3.10.0.1)"]
|
||||||
asyncio = ["greenlet (>=1)"]
|
asyncio = ["greenlet (>=1)"]
|
||||||
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (>=1)"]
|
asyncmy = ["asyncmy (>=0.2.12)", "greenlet (>=1)"]
|
||||||
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"]
|
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5,!=1.1.10)"]
|
||||||
mssql = ["pyodbc"]
|
mssql = ["pyodbc"]
|
||||||
mssql-pymssql = ["pymssql"]
|
mssql-pymssql = ["pymssql"]
|
||||||
@@ -2263,4 +2420,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.13"
|
python-versions = ">=3.13"
|
||||||
content-hash = "e1edf156eafb0e85fb3c090f3f17595ef56216f6bb9a2881f553619a19d83f3f"
|
content-hash = "30e907fcd3da88ca5c08edcb5cf49e0011f4cc702052583dd4496ca89b40e26b"
|
||||||
|
|||||||
+11
-4
@@ -16,13 +16,16 @@ dependencies = [
|
|||||||
"fastapi (>=0.139.1,<0.140.0)",
|
"fastapi (>=0.139.1,<0.140.0)",
|
||||||
"pydantic[email] (>=2.13.4,<3.0.0)",
|
"pydantic[email] (>=2.13.4,<3.0.0)",
|
||||||
"pydantic-settings (>=2.14.2,<3.0.0)",
|
"pydantic-settings (>=2.14.2,<3.0.0)",
|
||||||
"sqlalchemy (>=2.0.51,<3.0.0)",
|
"sqlalchemy[asyncio] (>=2.0.52,<3.0.0)",
|
||||||
"pandas (>=3.0.3,<4.0.0)",
|
"pandas (>=3.0.3,<4.0.0)",
|
||||||
"bcrypt (>=5.0.0,<6.0.0)",
|
"bcrypt (>=5.0.0,<6.0.0)",
|
||||||
"python-jose (>=3.5.0,<4.0.0)",
|
"python-jose (>=3.5.0,<4.0.0)",
|
||||||
"python-multipart (>=0.0.32,<0.0.33)",
|
"python-multipart (>=0.0.32,<0.0.33)",
|
||||||
"aiosqlite (>=0.22.1,<0.23.0)",
|
|
||||||
"greenlet (>=3.5.4,<4.0.0)",
|
"greenlet (>=3.5.4,<4.0.0)",
|
||||||
|
"aiofiles (>=25.1.0,<26.0.0)",
|
||||||
|
"asyncpg (>=0.31.0,<0.32.0)",
|
||||||
|
"psycopg2-binary (>=2.9.12,<3.0.0)",
|
||||||
|
"redis (>=8.1.0,<9.0.0)",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
@@ -46,10 +49,14 @@ omit = [
|
|||||||
"*/database/*",
|
"*/database/*",
|
||||||
"*/errors/*",
|
"*/errors/*",
|
||||||
"__init__.py",
|
"__init__.py",
|
||||||
"*/docker/*"
|
"*/docker/*",
|
||||||
|
"*/rate_limit.py",
|
||||||
|
"*/logger.py"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.coverage.report]
|
[tool.coverage.report]
|
||||||
exclude_lines = [
|
exclude_lines = [
|
||||||
"pragma: no cover",
|
"pragma: no cover",
|
||||||
]
|
]
|
||||||
|
[tool.ruff.lint]
|
||||||
|
ignore=["B008"]
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# The_DisExcel_project
|
||||||
|
|
||||||
|
A FastAPI project combining Excel and digital data ("The Great Excel project that is going to be built from Excel and digital projects").
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- Python >= 3.13
|
||||||
|
- FastAPI + Uvicorn / Gunicorn
|
||||||
|
- SQLAlchemy 2.0 (async) + Alembic (migrations)
|
||||||
|
- PostgreSQL (asyncpg, psycopg2)
|
||||||
|
- Pydantic 2 / Pydantic Settings
|
||||||
|
- Poetry — dependency management
|
||||||
|
- Docker, Ansible — deployment
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── database/ # DB CRUD operations
|
||||||
|
├── errors/ # HTTP errors
|
||||||
|
├── logging/ # logging middleware
|
||||||
|
├── migrations/ # Alembic migrations
|
||||||
|
├── models/ # Pydantic and SQLAlchemy models, configs
|
||||||
|
├── reports/ # reports
|
||||||
|
├── service/ # business logic (auth, users_crud)
|
||||||
|
└── web/ # routes (protected_routes)
|
||||||
|
```
|
||||||
|
|
||||||
|
Layers are connected top to bottom: `web → service → database → models`.
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
- JWT access + refresh tokens
|
||||||
|
- Refresh token is stored in the DB as a SHA256 hash, with rotation and revocation support
|
||||||
|
- Passwords are hashed with bcrypt
|
||||||
|
- RBAC: direct user permissions + permissions via groups, checked through `require_permissions()`
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── unit/
|
||||||
|
├── integrated/
|
||||||
|
└── e2e/
|
||||||
|
```
|
||||||
|
|
||||||
|
Uses pytest, pytest-asyncio, pytest-cov, pytest-mock, allure-pytest.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
poetry install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running migrations
|
||||||
|
|
||||||
|
```bash
|
||||||
|
alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
Pipeline is set up via Gitea Actions (`.gitea/workflows/ci.yml`).
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
The repository includes ready-made `docker/` and `ansible/` configs for containerization and deployment.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MH.Dmitrii's project
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
#redis dir
|
||||||
Vendored
+24
@@ -0,0 +1,24 @@
|
|||||||
|
from src.cache.redis_client import redis_client
|
||||||
|
from src.errors.http_errors.errors import Errors
|
||||||
|
|
||||||
|
|
||||||
|
class RateLimit:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.errors=Errors()
|
||||||
|
|
||||||
|
async def rate_limit(self, ip:str)->None:
|
||||||
|
|
||||||
|
key=f"action attempt {ip}"
|
||||||
|
attempts = await redis_client.incrby(key)
|
||||||
|
|
||||||
|
if attempts == 1:
|
||||||
|
await redis_client.expire(key, 60)
|
||||||
|
|
||||||
|
if attempts>5:
|
||||||
|
raise self.errors.rate_limit_error(detail="too many attempts", retry_after=60)
|
||||||
|
|
||||||
|
async def check_rate_limit(self, client_ip:str) -> None:
|
||||||
|
await self.rate_limit(client_ip)
|
||||||
|
|
||||||
|
|
||||||
|
rate_limiter=RateLimit()
|
||||||
Vendored
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import redis.asyncio as redis
|
||||||
|
|
||||||
|
from src.models.configs_read.env import env_settings
|
||||||
|
|
||||||
|
|
||||||
|
class RedisClient:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.client = redis.Redis(
|
||||||
|
host=env_settings.REDIS_HOST,
|
||||||
|
port=env_settings.REDIS_PORT,
|
||||||
|
password=env_settings.REDIS_PASSWORD,
|
||||||
|
decode_responses=True,
|
||||||
|
max_connections=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __getattr__(self, name):
|
||||||
|
return getattr(self.client, name)
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
await self.client.aclose()
|
||||||
|
|
||||||
|
redis_client = RedisClient()
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ from src.models.database_models.model import (
|
|||||||
engine,
|
engine,
|
||||||
)
|
)
|
||||||
from src.models.pydantic_models.model import UserOutDB
|
from src.models.pydantic_models.model import UserOutDB
|
||||||
|
from src.service.auth.jwt import HashService
|
||||||
|
|
||||||
|
|
||||||
class UsersCrudActions:
|
class UsersCrudActions:
|
||||||
@@ -121,4 +122,35 @@ class UsersCrudActions:
|
|||||||
user_edit.direct_permissions=list(groups)
|
user_edit.direct_permissions=list(groups)
|
||||||
|
|
||||||
await session.flush()
|
await session.flush()
|
||||||
return UserOutDB.model_validate(user_edit)
|
return UserOutDB.model_validate(user_edit)
|
||||||
|
|
||||||
|
class Seed:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.Session=async_sessionmaker(bind=engine)
|
||||||
|
self.hash_service=HashService()
|
||||||
|
|
||||||
|
async def seed(self) -> None:
|
||||||
|
async with self.Session() as session, session.begin():
|
||||||
|
|
||||||
|
existing = (await session.execute(select(User).limit(1))).scalar_one_or_none()
|
||||||
|
if existing is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
admin_permission = Permissions(permission="admin")
|
||||||
|
session.add(admin_permission)
|
||||||
|
|
||||||
|
admin_group = PermissionsGroups(group="admin_group", permissions=[admin_permission])
|
||||||
|
session.add(admin_group)
|
||||||
|
|
||||||
|
admin_user = User(
|
||||||
|
first_name="Admin",
|
||||||
|
last_name="Admin",
|
||||||
|
middle_name="Admin",
|
||||||
|
email="admin@admin.com",
|
||||||
|
hashed_password=self.hash_service.plain_to_hash("1234"),
|
||||||
|
direct_permissions=[admin_permission],
|
||||||
|
group=[admin_group],
|
||||||
|
)
|
||||||
|
session.add(admin_user)
|
||||||
|
|
||||||
|
print("Seed completed: admin user and permissions are created, credentials: email - admin@admin.com, password - 1234")
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#logger decorators and middlewares
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .logger import LoggerDB
|
||||||
|
|
||||||
|
sql_logger = logging.getLogger("sqlalchemy.engine")
|
||||||
|
sql_logger.setLevel(logging.INFO)
|
||||||
|
sql_logger.addHandler(LoggerDB())
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from time import gmtime, perf_counter, strftime
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from starlette.concurrency import iterate_in_threadpool
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
from starlette.responses import Response, StreamingResponse
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessingTimeMiddleware(BaseHTTPMiddleware):
|
||||||
|
async def dispatch(self, request: Request, call_next)->Response:
|
||||||
|
start_time = perf_counter()
|
||||||
|
response = await call_next(request)
|
||||||
|
process_time = perf_counter() - start_time
|
||||||
|
response.headers["X-Process-Time"] = str(process_time)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingMiddleware(BaseHTTPMiddleware):
|
||||||
|
async def dispatch(self, request: Request, call_next) -> Response:
|
||||||
|
current_time = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
|
||||||
|
file_time = strftime("%b_%Y", gmtime())
|
||||||
|
client_ip = request.headers.get('x-forwarded-for', '').split(',')[0].strip() or (request.client.host if request.client else 'unknown')
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await call_next(request)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
body = str(exc)
|
||||||
|
async with aiofiles.open(f"./logs/endpoints_log_{file_time}.txt", "a") as file:
|
||||||
|
await file.write(f"[{current_time}] [500] [{body}] [{client_ip}]\n")
|
||||||
|
return JSONResponse(status_code=500, content={"detail": "Internal Server Error"})
|
||||||
|
|
||||||
|
streaming_response = cast(StreamingResponse, response)
|
||||||
|
|
||||||
|
chunks = []
|
||||||
|
async for chunk in streaming_response.body_iterator:
|
||||||
|
chunks.append(chunk.encode() if isinstance(chunk, str) else bytes(chunk))
|
||||||
|
|
||||||
|
body_bytes = b"".join(chunks)
|
||||||
|
streaming_response.body_iterator = iterate_in_threadpool(iter([body_bytes]))
|
||||||
|
|
||||||
|
try:
|
||||||
|
parsed = json.loads(body_bytes)
|
||||||
|
body = parsed.get("detail", None) if not isinstance(parsed, bool) else None
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
body = None
|
||||||
|
|
||||||
|
async with aiofiles.open(f"./logs/endpoints_log_{file_time}.txt", "a") as file:
|
||||||
|
await file.write(f"[{current_time}] [{response.status_code}] [{body}] [{client_ip}]\n")
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class LoggerDB(logging.Handler):
|
||||||
|
|
||||||
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
|
msg = self.format(record)
|
||||||
|
asyncio.create_task(self._write(msg))
|
||||||
|
|
||||||
|
async def _write(self, msg: str) -> None:
|
||||||
|
|
||||||
|
file_time = strftime("%b_%Y", gmtime())
|
||||||
|
current_time = strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
|
||||||
|
|
||||||
|
async with aiofiles.open(f"./logs/sql_log_{file_time}.txt", "a") as f:
|
||||||
|
await f.write(f"[{current_time}] {msg}\n")
|
||||||
@@ -6,7 +6,7 @@ from sqlalchemy import create_engine, pool
|
|||||||
from src.models.database_models import Model
|
from src.models.database_models import Model
|
||||||
from src.models.database_models.model import engine
|
from src.models.database_models.model import engine
|
||||||
|
|
||||||
sync_url = engine.url.render_as_string(hide_password=False).replace("+aiosqlite", "")
|
sync_url = engine.url.render_as_string(hide_password=False).replace("+asyncpg", "")
|
||||||
sync_engine = create_engine(sync_url, poolclass=pool.NullPool)
|
sync_engine = create_engine(sync_url, poolclass=pool.NullPool)
|
||||||
|
|
||||||
# this is the Alembic Config object, which provides
|
# this is the Alembic Config object, which provides
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 23dd6d3efe4b
|
|
||||||
Revises: 2f92088cdce4
|
|
||||||
Create Date: 2026-07-23 13:05:12.553687
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '23dd6d3efe4b'
|
|
||||||
down_revision: Union[str, Sequence[str], None] = '2f92088cdce4'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
pass
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
pass
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 2f92088cdce4
|
|
||||||
Revises: 75074097a2a3
|
|
||||||
Create Date: 2026-07-23 11:05:02.409697
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '2f92088cdce4'
|
|
||||||
down_revision: Union[str, Sequence[str], None] = '75074097a2a3'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
pass
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
pass
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 385d4efec15f
|
|
||||||
Revises: 23dd6d3efe4b
|
|
||||||
Create Date: 2026-07-23 15:03:33.582896
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '385d4efec15f'
|
|
||||||
down_revision: Union[str, Sequence[str], None] = '23dd6d3efe4b'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('refresh_tokens', schema=None) as batch_op:
|
|
||||||
batch_op.alter_column('id',
|
|
||||||
existing_type=sa.INTEGER(),
|
|
||||||
type_=sa.Uuid(),
|
|
||||||
existing_nullable=False)
|
|
||||||
batch_op.alter_column('replaced_by',
|
|
||||||
existing_type=sa.INTEGER(),
|
|
||||||
type_=sa.Uuid(),
|
|
||||||
existing_nullable=True)
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('refresh_tokens', schema=None) as batch_op:
|
|
||||||
batch_op.alter_column('replaced_by',
|
|
||||||
existing_type=sa.Uuid(),
|
|
||||||
type_=sa.INTEGER(),
|
|
||||||
existing_nullable=True)
|
|
||||||
batch_op.alter_column('id',
|
|
||||||
existing_type=sa.Uuid(),
|
|
||||||
type_=sa.INTEGER(),
|
|
||||||
existing_nullable=False)
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 439a77f8a4d4
|
|
||||||
Revises: 5e60c8fbc553
|
|
||||||
Create Date: 2026-07-22 15:19:15.853280
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '439a77f8a4d4'
|
|
||||||
down_revision: Union[str, Sequence[str], None] = '5e60c8fbc553'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('markets',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('name', sa.String(length=64), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_markets')),
|
|
||||||
sa.UniqueConstraint('name', name=op.f('uq_markets_name'))
|
|
||||||
)
|
|
||||||
with op.batch_alter_table('markets', schema=None) as batch_op:
|
|
||||||
batch_op.create_index(batch_op.f('ix_markets_id'), ['id'], unique=False)
|
|
||||||
|
|
||||||
with op.batch_alter_table('reports', schema=None) as batch_op:
|
|
||||||
batch_op.add_column(sa.Column('doc_id', sa.Uuid(), nullable=False))
|
|
||||||
batch_op.add_column(sa.Column('uploaded_at', sa.TIMESTAMP(), nullable=False))
|
|
||||||
batch_op.add_column(sa.Column('doc_date', sa.TIMESTAMP(), nullable=False))
|
|
||||||
batch_op.add_column(sa.Column('status', sa.String(length=64), nullable=False))
|
|
||||||
batch_op.add_column(sa.Column('user_id', sa.Uuid(), nullable=False))
|
|
||||||
batch_op.add_column(sa.Column('market_id', sa.Integer(), nullable=False))
|
|
||||||
batch_op.create_index(batch_op.f('ix_reports_market_id'), ['market_id'], unique=False)
|
|
||||||
batch_op.create_index(batch_op.f('ix_reports_user_id'), ['user_id'], unique=False)
|
|
||||||
batch_op.create_unique_constraint(batch_op.f('uq_reports_doc_id'), ['doc_id'])
|
|
||||||
batch_op.create_foreign_key(batch_op.f('fk_reports_user_id_users'), 'users', ['user_id'], ['id'], ondelete='CASCADE')
|
|
||||||
batch_op.create_foreign_key(batch_op.f('fk_reports_market_id_markets'), 'markets', ['market_id'], ['id'], ondelete='CASCADE')
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('reports', schema=None) as batch_op:
|
|
||||||
batch_op.drop_constraint(batch_op.f('fk_reports_market_id_markets'), type_='foreignkey')
|
|
||||||
batch_op.drop_constraint(batch_op.f('fk_reports_user_id_users'), type_='foreignkey')
|
|
||||||
batch_op.drop_constraint(batch_op.f('uq_reports_doc_id'), type_='unique')
|
|
||||||
batch_op.drop_index(batch_op.f('ix_reports_user_id'))
|
|
||||||
batch_op.drop_index(batch_op.f('ix_reports_market_id'))
|
|
||||||
batch_op.drop_column('market_id')
|
|
||||||
batch_op.drop_column('user_id')
|
|
||||||
batch_op.drop_column('status')
|
|
||||||
batch_op.drop_column('doc_date')
|
|
||||||
batch_op.drop_column('uploaded_at')
|
|
||||||
batch_op.drop_column('doc_id')
|
|
||||||
|
|
||||||
with op.batch_alter_table('markets', schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f('ix_markets_id'))
|
|
||||||
|
|
||||||
op.drop_table('markets')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 74814eb1b7f8
|
|
||||||
Revises: 8c136ff14180
|
|
||||||
Create Date: 2026-07-23 21:06:37.254211
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '74814eb1b7f8'
|
|
||||||
down_revision: Union[str, Sequence[str], None] = '8c136ff14180'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
pass
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
pass
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 75074097a2a3
|
|
||||||
Revises: 8c300c4d43ea
|
|
||||||
Create Date: 2026-07-22 16:11:08.077455
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '75074097a2a3'
|
|
||||||
down_revision: Union[str, Sequence[str], None] = '8c300c4d43ea'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
op.create_table('accountant_settings',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('name', sa.String(length=64), nullable=False),
|
|
||||||
sa.Column('database_key', sa.Uuid(), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_accountant_settings')),
|
|
||||||
sa.UniqueConstraint('database_key', name=op.f('uq_accountant_settings_database_key'))
|
|
||||||
)
|
|
||||||
with op.batch_alter_table('accountant_settings', schema=None) as batch_op:
|
|
||||||
batch_op.create_index(batch_op.f('ix_accountant_settings_id'), ['id'], unique=False)
|
|
||||||
batch_op.create_index(batch_op.f('ix_accountant_settings_name'), ['name'], unique=True)
|
|
||||||
|
|
||||||
op.create_table('doc_types',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('name', sa.String(length=64), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_doc_types'))
|
|
||||||
)
|
|
||||||
with op.batch_alter_table('doc_types', schema=None) as batch_op:
|
|
||||||
batch_op.create_index(batch_op.f('ix_doc_types_id'), ['id'], unique=False)
|
|
||||||
batch_op.create_index(batch_op.f('ix_doc_types_name'), ['name'], unique=True)
|
|
||||||
|
|
||||||
op.create_table('fabric',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('name', sa.String(length=64), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_fabric'))
|
|
||||||
)
|
|
||||||
with op.batch_alter_table('fabric', schema=None) as batch_op:
|
|
||||||
batch_op.create_index(batch_op.f('ix_fabric_id'), ['id'], unique=False)
|
|
||||||
batch_op.create_index(batch_op.f('ix_fabric_name'), ['name'], unique=True)
|
|
||||||
|
|
||||||
op.create_table('goods',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('article', sa.String(length=16), nullable=False),
|
|
||||||
sa.Column('price', sa.Numeric(precision=10, scale=2), nullable=False),
|
|
||||||
sa.Column('tnvd', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('doc_type_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('fabric_id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('status', sa.Boolean(), nullable=False),
|
|
||||||
sa.ForeignKeyConstraint(['doc_type_id'], ['doc_types.id'], name=op.f('fk_goods_doc_type_id_doc_types'), ondelete='CASCADE'),
|
|
||||||
sa.ForeignKeyConstraint(['fabric_id'], ['fabric.id'], name=op.f('fk_goods_fabric_id_fabric'), ondelete='CASCADE'),
|
|
||||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_goods'))
|
|
||||||
)
|
|
||||||
with op.batch_alter_table('goods', schema=None) as batch_op:
|
|
||||||
batch_op.create_index(batch_op.f('ix_goods_article'), ['article'], unique=True)
|
|
||||||
batch_op.create_index(batch_op.f('ix_goods_doc_type_id'), ['doc_type_id'], unique=False)
|
|
||||||
batch_op.create_index(batch_op.f('ix_goods_fabric_id'), ['fabric_id'], unique=False)
|
|
||||||
batch_op.create_index(batch_op.f('ix_goods_id'), ['id'], unique=False)
|
|
||||||
batch_op.create_index(batch_op.f('ix_goods_price'), ['price'], unique=False)
|
|
||||||
batch_op.create_index(batch_op.f('ix_goods_tnvd'), ['tnvd'], unique=True)
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('goods', schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f('ix_goods_tnvd'))
|
|
||||||
batch_op.drop_index(batch_op.f('ix_goods_price'))
|
|
||||||
batch_op.drop_index(batch_op.f('ix_goods_id'))
|
|
||||||
batch_op.drop_index(batch_op.f('ix_goods_fabric_id'))
|
|
||||||
batch_op.drop_index(batch_op.f('ix_goods_doc_type_id'))
|
|
||||||
batch_op.drop_index(batch_op.f('ix_goods_article'))
|
|
||||||
|
|
||||||
op.drop_table('goods')
|
|
||||||
with op.batch_alter_table('fabric', schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f('ix_fabric_name'))
|
|
||||||
batch_op.drop_index(batch_op.f('ix_fabric_id'))
|
|
||||||
|
|
||||||
op.drop_table('fabric')
|
|
||||||
with op.batch_alter_table('doc_types', schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f('ix_doc_types_name'))
|
|
||||||
batch_op.drop_index(batch_op.f('ix_doc_types_id'))
|
|
||||||
|
|
||||||
op.drop_table('doc_types')
|
|
||||||
with op.batch_alter_table('accountant_settings', schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f('ix_accountant_settings_name'))
|
|
||||||
batch_op.drop_index(batch_op.f('ix_accountant_settings_id'))
|
|
||||||
|
|
||||||
op.drop_table('accountant_settings')
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 8c136ff14180
|
|
||||||
Revises: 385d4efec15f
|
|
||||||
Create Date: 2026-07-23 17:56:26.345954
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '8c136ff14180'
|
|
||||||
down_revision: Union[str, Sequence[str], None] = '385d4efec15f'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
pass
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
pass
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: 8c300c4d43ea
|
|
||||||
Revises: 439a77f8a4d4
|
|
||||||
Create Date: 2026-07-22 15:20:34.871724
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = '8c300c4d43ea'
|
|
||||||
down_revision: Union[str, Sequence[str], None] = '439a77f8a4d4'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('reports', schema=None) as batch_op:
|
|
||||||
batch_op.alter_column('uploaded_at',
|
|
||||||
existing_type=sa.TIMESTAMP(),
|
|
||||||
nullable=True)
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('reports', schema=None) as batch_op:
|
|
||||||
batch_op.alter_column('uploaded_at',
|
|
||||||
existing_type=sa.TIMESTAMP(),
|
|
||||||
nullable=False)
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
"""empty message
|
|
||||||
|
|
||||||
Revision ID: ebe80cd28822
|
|
||||||
Revises: 74814eb1b7f8
|
|
||||||
Create Date: 2026-08-21 13:52:49.788041
|
|
||||||
|
|
||||||
"""
|
|
||||||
from typing import Sequence, Union
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
import sqlalchemy as sa
|
|
||||||
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
|
||||||
revision: str = 'ebe80cd28822'
|
|
||||||
down_revision: Union[str, Sequence[str], None] = '74814eb1b7f8'
|
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
|
||||||
|
|
||||||
|
|
||||||
def upgrade() -> None:
|
|
||||||
"""Upgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('user_direct_permissions', schema=None) as batch_op:
|
|
||||||
batch_op.drop_constraint(batch_op.f('fk_user_direct_permissions_user_id_users'), type_='foreignkey')
|
|
||||||
batch_op.create_foreign_key(batch_op.f('fk_user_direct_permissions_user_id_users'), 'users', ['user_id'], ['id'], ondelete='CASCADE')
|
|
||||||
|
|
||||||
with op.batch_alter_table('user_group', schema=None) as batch_op:
|
|
||||||
batch_op.drop_constraint(batch_op.f('fk_user_group_user_id_users'), type_='foreignkey')
|
|
||||||
batch_op.create_foreign_key(batch_op.f('fk_user_group_user_id_users'), 'users', ['user_id'], ['id'], ondelete='CASCADE')
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
"""Downgrade schema."""
|
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
|
||||||
with op.batch_alter_table('user_group', schema=None) as batch_op:
|
|
||||||
batch_op.drop_constraint(batch_op.f('fk_user_group_user_id_users'), type_='foreignkey')
|
|
||||||
batch_op.create_foreign_key(batch_op.f('fk_user_group_user_id_users'), 'users', ['user_id'], ['id'])
|
|
||||||
|
|
||||||
with op.batch_alter_table('user_direct_permissions', schema=None) as batch_op:
|
|
||||||
batch_op.drop_constraint(batch_op.f('fk_user_direct_permissions_user_id_users'), type_='foreignkey')
|
|
||||||
batch_op.create_foreign_key(batch_op.f('fk_user_direct_permissions_user_id_users'), 'users', ['user_id'], ['id'])
|
|
||||||
|
|
||||||
# ### end Alembic commands ###
|
|
||||||
+124
-26
@@ -1,18 +1,18 @@
|
|||||||
"""empty message
|
"""empty message
|
||||||
|
|
||||||
Revision ID: 5e60c8fbc553
|
Revision ID: f4018d3509eb
|
||||||
Revises:
|
Revises:
|
||||||
Create Date: 2026-07-17 20:02:01.237457
|
Create Date: 2026-08-28 12:45:18.014540
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from typing import Sequence, Union
|
from typing import Sequence, Union
|
||||||
|
|
||||||
from alembic import op
|
from alembic import op
|
||||||
import sqlalchemy as sa
|
import sqlalchemy as sa
|
||||||
from sqlalchemy.sql import text
|
|
||||||
|
|
||||||
# revision identifiers, used by Alembic.
|
# revision identifiers, used by Alembic.
|
||||||
revision: str = '5e60c8fbc553'
|
revision: str = 'f4018d3509eb'
|
||||||
down_revision: Union[str, Sequence[str], None] = None
|
down_revision: Union[str, Sequence[str], None] = None
|
||||||
branch_labels: Union[str, Sequence[str], None] = None
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
depends_on: Union[str, Sequence[str], None] = None
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
@@ -21,6 +21,35 @@ depends_on: Union[str, Sequence[str], None] = None
|
|||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
"""Upgrade schema."""
|
"""Upgrade schema."""
|
||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('accountant_settings',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=64), nullable=False),
|
||||||
|
sa.Column('database_key', sa.Uuid(), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_accountant_settings')),
|
||||||
|
sa.UniqueConstraint('database_key', name=op.f('uq_accountant_settings_database_key'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('accountant_settings', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_accountant_settings_id'), ['id'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_accountant_settings_name'), ['name'], unique=True)
|
||||||
|
|
||||||
|
op.create_table('doc_types',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=64), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_doc_types'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('doc_types', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_doc_types_id'), ['id'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_doc_types_name'), ['name'], unique=True)
|
||||||
|
|
||||||
|
op.create_table('fabric',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=64), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_fabric'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('fabric', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_fabric_id'), ['id'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_fabric_name'), ['name'], unique=True)
|
||||||
|
|
||||||
op.create_table('groups_of_permissions',
|
op.create_table('groups_of_permissions',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('group', sa.String(length=255), nullable=False),
|
sa.Column('group', sa.String(length=255), nullable=False),
|
||||||
@@ -30,6 +59,15 @@ def upgrade() -> None:
|
|||||||
with op.batch_alter_table('groups_of_permissions', schema=None) as batch_op:
|
with op.batch_alter_table('groups_of_permissions', schema=None) as batch_op:
|
||||||
batch_op.create_index(batch_op.f('ix_groups_of_permissions_id'), ['id'], unique=False)
|
batch_op.create_index(batch_op.f('ix_groups_of_permissions_id'), ['id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('markets',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(length=64), nullable=False),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_markets')),
|
||||||
|
sa.UniqueConstraint('name', name=op.f('uq_markets_name'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('markets', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_markets_id'), ['id'], unique=False)
|
||||||
|
|
||||||
op.create_table('permissions',
|
op.create_table('permissions',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('permission', sa.String(length=255), nullable=False),
|
sa.Column('permission', sa.String(length=255), nullable=False),
|
||||||
@@ -38,17 +76,6 @@ def upgrade() -> None:
|
|||||||
)
|
)
|
||||||
with op.batch_alter_table('permissions', schema=None) as batch_op:
|
with op.batch_alter_table('permissions', schema=None) as batch_op:
|
||||||
batch_op.create_index(batch_op.f('ix_permissions_id'), ['id'], unique=False)
|
batch_op.create_index(batch_op.f('ix_permissions_id'), ['id'], unique=False)
|
||||||
op.execute(text("INSERT INTO permissions (permission) VALUES ('admin');"))
|
|
||||||
|
|
||||||
op.create_table('reports',
|
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
|
||||||
sa.Column('filename', sa.String(length=255), nullable=False),
|
|
||||||
sa.Column('created_at', sa.TIMESTAMP(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
|
||||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_reports'))
|
|
||||||
)
|
|
||||||
with op.batch_alter_table('reports', schema=None) as batch_op:
|
|
||||||
batch_op.create_index(batch_op.f('ix_reports_filename'), ['filename'], unique=False)
|
|
||||||
batch_op.create_index(batch_op.f('ix_reports_id'), ['id'], unique=False)
|
|
||||||
|
|
||||||
op.create_table('users',
|
op.create_table('users',
|
||||||
sa.Column('id', sa.Uuid(), nullable=False),
|
sa.Column('id', sa.Uuid(), nullable=False),
|
||||||
@@ -66,6 +93,26 @@ def upgrade() -> None:
|
|||||||
batch_op.create_index(batch_op.f('ix_users_last_name'), ['last_name'], unique=False)
|
batch_op.create_index(batch_op.f('ix_users_last_name'), ['last_name'], unique=False)
|
||||||
batch_op.create_index(batch_op.f('ix_users_middle_name'), ['middle_name'], unique=False)
|
batch_op.create_index(batch_op.f('ix_users_middle_name'), ['middle_name'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('goods',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('article', sa.String(length=16), nullable=False),
|
||||||
|
sa.Column('price', sa.Numeric(precision=10, scale=2), nullable=False),
|
||||||
|
sa.Column('tnvd', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('doc_type_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('fabric_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('status', sa.Boolean(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['doc_type_id'], ['doc_types.id'], name=op.f('fk_goods_doc_type_id_doc_types'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['fabric_id'], ['fabric.id'], name=op.f('fk_goods_fabric_id_fabric'), ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_goods'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('goods', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_goods_article'), ['article'], unique=True)
|
||||||
|
batch_op.create_index(batch_op.f('ix_goods_doc_type_id'), ['doc_type_id'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_goods_fabric_id'), ['fabric_id'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_goods_id'), ['id'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_goods_price'), ['price'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_goods_tnvd'), ['tnvd'], unique=True)
|
||||||
|
|
||||||
op.create_table('group_permission',
|
op.create_table('group_permission',
|
||||||
sa.Column('group_id', sa.Integer(), nullable=False),
|
sa.Column('group_id', sa.Integer(), nullable=False),
|
||||||
sa.Column('permission_id', sa.Integer(), nullable=False),
|
sa.Column('permission_id', sa.Integer(), nullable=False),
|
||||||
@@ -74,15 +121,15 @@ def upgrade() -> None:
|
|||||||
sa.PrimaryKeyConstraint('group_id', 'permission_id', name=op.f('pk_group_permission'))
|
sa.PrimaryKeyConstraint('group_id', 'permission_id', name=op.f('pk_group_permission'))
|
||||||
)
|
)
|
||||||
op.create_table('refresh_tokens',
|
op.create_table('refresh_tokens',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Uuid(), nullable=False),
|
||||||
sa.Column('user_id', sa.Uuid(), nullable=False),
|
sa.Column('user_id', sa.Uuid(), nullable=False),
|
||||||
sa.Column('token_hash', sa.String(length=255), nullable=False),
|
sa.Column('token_hash', sa.String(length=255), nullable=False),
|
||||||
sa.Column('device_info', sa.String(length=255), nullable=False),
|
sa.Column('device_info', sa.String(length=255), nullable=False),
|
||||||
sa.Column('ip_address', sa.String(length=45), nullable=False),
|
sa.Column('ip_address', sa.String(length=45), nullable=False),
|
||||||
sa.Column('is_revoked', sa.Boolean(), nullable=False),
|
sa.Column('is_revoked', sa.Boolean(), nullable=False),
|
||||||
sa.Column('expires_at', sa.TIMESTAMP(), nullable=False),
|
sa.Column('expires_at', sa.TIMESTAMP(timezone=True), nullable=False),
|
||||||
sa.Column('created_at', sa.TIMESTAMP(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
|
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
sa.Column('replaced_by', sa.Integer(), nullable=True),
|
sa.Column('replaced_by', sa.Uuid(), nullable=True),
|
||||||
sa.ForeignKeyConstraint(['replaced_by'], ['refresh_tokens.id'], name=op.f('fk_refresh_tokens_replaced_by_refresh_tokens')),
|
sa.ForeignKeyConstraint(['replaced_by'], ['refresh_tokens.id'], name=op.f('fk_refresh_tokens_replaced_by_refresh_tokens')),
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_refresh_tokens_user_id_users'), ondelete='CASCADE'),
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_refresh_tokens_user_id_users'), ondelete='CASCADE'),
|
||||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_refresh_tokens')),
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_refresh_tokens')),
|
||||||
@@ -92,18 +139,39 @@ def upgrade() -> None:
|
|||||||
batch_op.create_index(batch_op.f('ix_refresh_tokens_id'), ['id'], unique=False)
|
batch_op.create_index(batch_op.f('ix_refresh_tokens_id'), ['id'], unique=False)
|
||||||
batch_op.create_index(batch_op.f('ix_refresh_tokens_user_id'), ['user_id'], unique=False)
|
batch_op.create_index(batch_op.f('ix_refresh_tokens_user_id'), ['user_id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('reports',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('doc_id', sa.Uuid(), nullable=False),
|
||||||
|
sa.Column('filename', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=False),
|
||||||
|
sa.Column('uploaded_at', sa.TIMESTAMP(timezone=True), nullable=True),
|
||||||
|
sa.Column('doc_date', sa.TIMESTAMP(timezone=True), nullable=False),
|
||||||
|
sa.Column('status', sa.String(length=64), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Uuid(), nullable=False),
|
||||||
|
sa.Column('market_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['market_id'], ['markets.id'], name=op.f('fk_reports_market_id_markets'), ondelete='CASCADE'),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_reports_user_id_users'), ondelete='CASCADE'),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_reports')),
|
||||||
|
sa.UniqueConstraint('doc_id', name=op.f('uq_reports_doc_id'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('reports', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_reports_filename'), ['filename'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_reports_id'), ['id'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_reports_market_id'), ['market_id'], unique=False)
|
||||||
|
batch_op.create_index(batch_op.f('ix_reports_user_id'), ['user_id'], unique=False)
|
||||||
|
|
||||||
op.create_table('user_direct_permissions',
|
op.create_table('user_direct_permissions',
|
||||||
sa.Column('user_id', sa.Uuid(), nullable=False),
|
sa.Column('user_id', sa.Uuid(), nullable=False),
|
||||||
sa.Column('permission_id', sa.Integer(), nullable=False),
|
sa.Column('permission_id', sa.Integer(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], name=op.f('fk_user_direct_permissions_permission_id_permissions')),
|
sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], name=op.f('fk_user_direct_permissions_permission_id_permissions')),
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_user_direct_permissions_user_id_users')),
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_user_direct_permissions_user_id_users'), ondelete='CASCADE'),
|
||||||
sa.PrimaryKeyConstraint('user_id', 'permission_id', name=op.f('pk_user_direct_permissions'))
|
sa.PrimaryKeyConstraint('user_id', 'permission_id', name=op.f('pk_user_direct_permissions'))
|
||||||
)
|
)
|
||||||
op.create_table('user_group',
|
op.create_table('user_group',
|
||||||
sa.Column('user_id', sa.Uuid(), nullable=False),
|
sa.Column('user_id', sa.Uuid(), nullable=False),
|
||||||
sa.Column('permission_group_id', sa.Integer(), nullable=False),
|
sa.Column('permission_group_id', sa.Integer(), nullable=False),
|
||||||
sa.ForeignKeyConstraint(['permission_group_id'], ['groups_of_permissions.id'], name=op.f('fk_user_group_permission_group_id_groups_of_permissions')),
|
sa.ForeignKeyConstraint(['permission_group_id'], ['groups_of_permissions.id'], name=op.f('fk_user_group_permission_group_id_groups_of_permissions')),
|
||||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_user_group_user_id_users')),
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_user_group_user_id_users'), ondelete='CASCADE'),
|
||||||
sa.PrimaryKeyConstraint('user_id', 'permission_group_id', name=op.f('pk_user_group'))
|
sa.PrimaryKeyConstraint('user_id', 'permission_group_id', name=op.f('pk_user_group'))
|
||||||
)
|
)
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
@@ -114,12 +182,28 @@ def downgrade() -> None:
|
|||||||
# ### commands auto generated by Alembic - please adjust! ###
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
op.drop_table('user_group')
|
op.drop_table('user_group')
|
||||||
op.drop_table('user_direct_permissions')
|
op.drop_table('user_direct_permissions')
|
||||||
|
with op.batch_alter_table('reports', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_reports_user_id'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_reports_market_id'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_reports_id'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_reports_filename'))
|
||||||
|
|
||||||
|
op.drop_table('reports')
|
||||||
with op.batch_alter_table('refresh_tokens', schema=None) as batch_op:
|
with op.batch_alter_table('refresh_tokens', schema=None) as batch_op:
|
||||||
batch_op.drop_index(batch_op.f('ix_refresh_tokens_user_id'))
|
batch_op.drop_index(batch_op.f('ix_refresh_tokens_user_id'))
|
||||||
batch_op.drop_index(batch_op.f('ix_refresh_tokens_id'))
|
batch_op.drop_index(batch_op.f('ix_refresh_tokens_id'))
|
||||||
|
|
||||||
op.drop_table('refresh_tokens')
|
op.drop_table('refresh_tokens')
|
||||||
op.drop_table('group_permission')
|
op.drop_table('group_permission')
|
||||||
|
with op.batch_alter_table('goods', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_goods_tnvd'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_goods_price'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_goods_id'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_goods_fabric_id'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_goods_doc_type_id'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_goods_article'))
|
||||||
|
|
||||||
|
op.drop_table('goods')
|
||||||
with op.batch_alter_table('users', schema=None) as batch_op:
|
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||||
batch_op.drop_index(batch_op.f('ix_users_middle_name'))
|
batch_op.drop_index(batch_op.f('ix_users_middle_name'))
|
||||||
batch_op.drop_index(batch_op.f('ix_users_last_name'))
|
batch_op.drop_index(batch_op.f('ix_users_last_name'))
|
||||||
@@ -127,17 +211,31 @@ def downgrade() -> None:
|
|||||||
batch_op.drop_index(batch_op.f('ix_users_email'))
|
batch_op.drop_index(batch_op.f('ix_users_email'))
|
||||||
|
|
||||||
op.drop_table('users')
|
op.drop_table('users')
|
||||||
with op.batch_alter_table('reports', schema=None) as batch_op:
|
|
||||||
batch_op.drop_index(batch_op.f('ix_reports_id'))
|
|
||||||
batch_op.drop_index(batch_op.f('ix_reports_filename'))
|
|
||||||
|
|
||||||
op.drop_table('reports')
|
|
||||||
with op.batch_alter_table('permissions', schema=None) as batch_op:
|
with op.batch_alter_table('permissions', schema=None) as batch_op:
|
||||||
batch_op.drop_index(batch_op.f('ix_permissions_id'))
|
batch_op.drop_index(batch_op.f('ix_permissions_id'))
|
||||||
|
|
||||||
op.drop_table('permissions')
|
op.drop_table('permissions')
|
||||||
|
with op.batch_alter_table('markets', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_markets_id'))
|
||||||
|
|
||||||
|
op.drop_table('markets')
|
||||||
with op.batch_alter_table('groups_of_permissions', schema=None) as batch_op:
|
with op.batch_alter_table('groups_of_permissions', schema=None) as batch_op:
|
||||||
batch_op.drop_index(batch_op.f('ix_groups_of_permissions_id'))
|
batch_op.drop_index(batch_op.f('ix_groups_of_permissions_id'))
|
||||||
|
|
||||||
op.drop_table('groups_of_permissions')
|
op.drop_table('groups_of_permissions')
|
||||||
|
with op.batch_alter_table('fabric', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_fabric_name'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_fabric_id'))
|
||||||
|
|
||||||
|
op.drop_table('fabric')
|
||||||
|
with op.batch_alter_table('doc_types', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_doc_types_name'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_doc_types_id'))
|
||||||
|
|
||||||
|
op.drop_table('doc_types')
|
||||||
|
with op.batch_alter_table('accountant_settings', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_accountant_settings_name'))
|
||||||
|
batch_op.drop_index(batch_op.f('ix_accountant_settings_id'))
|
||||||
|
|
||||||
|
op.drop_table('accountant_settings')
|
||||||
# ### end Alembic commands ###
|
# ### end Alembic commands ###
|
||||||
@@ -10,6 +10,18 @@ class Env(Base):
|
|||||||
ACCESS_TOKEN_EXPIRE_MINUTES:int
|
ACCESS_TOKEN_EXPIRE_MINUTES:int
|
||||||
REFRESH_TOKEN_EXPIRE_DAYS:int
|
REFRESH_TOKEN_EXPIRE_DAYS:int
|
||||||
|
|
||||||
|
DB_USER:str
|
||||||
|
DB_PASSWORD:str
|
||||||
|
DB_POSTGRESS:str
|
||||||
|
DB_HOST:str
|
||||||
|
DB_PORT:str
|
||||||
|
|
||||||
|
REDIS_PASSWORD:str
|
||||||
|
REDIS_PORT:int
|
||||||
|
REDIS_HOST:str
|
||||||
|
|
||||||
|
PROD_MODE:bool
|
||||||
|
|
||||||
model_config=SettingsConfigDict(env_file="configs/.env", extra=None)
|
model_config=SettingsConfigDict(env_file="configs/.env", extra=None)
|
||||||
|
|
||||||
env_settings=Env() # type: ignore[call-arg]
|
env_settings=Env() # type: ignore[call-arg]
|
||||||
@@ -15,7 +15,14 @@ from sqlalchemy import (
|
|||||||
from sqlalchemy.ext.asyncio import create_async_engine
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||||
|
|
||||||
engine = create_async_engine("sqlite+aiosqlite:///DB/database.db", echo=True)
|
from src.models.configs_read.env import env_settings
|
||||||
|
|
||||||
|
engine = create_async_engine(f"postgresql+asyncpg://{env_settings.DB_USER}:{env_settings.DB_PASSWORD}@{env_settings.DB_HOST}:{env_settings.DB_PORT}/{env_settings.DB_POSTGRESS}",
|
||||||
|
pool_size=20, # сколько соединений держать открытыми постоянно
|
||||||
|
max_overflow=10, # сколько доп. соединений можно создать при пиковой нагрузке
|
||||||
|
pool_timeout=30, # сколько ждать свободное соединение, прежде чем упасть с ошибкой
|
||||||
|
pool_pre_ping=True, # проверять соединение перед использованием (ловит "протухшие" соединения)
|
||||||
|
)
|
||||||
|
|
||||||
'''remember as a boilerplate, or just cp/pst'''
|
'''remember as a boilerplate, or just cp/pst'''
|
||||||
class Model(DeclarativeBase):
|
class Model(DeclarativeBase):
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from uuid import UUID
|
|||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
|
|
||||||
|
from src.cache.redis_client import redis_client
|
||||||
from src.database.auth.refresh_tokens import JwtCrudActions
|
from src.database.auth.refresh_tokens import JwtCrudActions
|
||||||
from src.database.users.crud import UsersCrudActions
|
from src.database.users.crud import UsersCrudActions
|
||||||
from src.errors.http_errors.errors import Errors
|
from src.errors.http_errors.errors import Errors
|
||||||
@@ -52,8 +53,13 @@ class CurrentUserService:
|
|||||||
async def get_current_user(self, token:str, *permissions: str)->UserOut:
|
async def get_current_user(self, token:str, *permissions: str)->UserOut:
|
||||||
|
|
||||||
payload= await self.jwt_service.jwt_decode(token)
|
payload= await self.jwt_service.jwt_decode(token)
|
||||||
|
|
||||||
sub=payload.get("sub")
|
sub=payload.get("sub")
|
||||||
|
jti_access=payload.get("jti")
|
||||||
|
|
||||||
|
if jti_access and await redis_client.get(f"revoked_access_token:{jti_access}"):
|
||||||
|
raise self.error.credentials_error(detail="Token has been revoked")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sub=UUID(sub)
|
sub=UUID(sub)
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
@@ -167,21 +173,34 @@ class CurrentUserService:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def logout(self, refresh_token:str)->bool:
|
async def logout(self, refresh_token:str, access_token:str)->bool:
|
||||||
|
|
||||||
'''decode current refresh token'''
|
'''decode current refresh token'''
|
||||||
payload=await self.jwt_service.jwt_decode(refresh_token)
|
payload_refresh=await self.jwt_service.jwt_decode(refresh_token)
|
||||||
|
|
||||||
if (jti:=payload.get("jti")) is None:
|
'''decode current access token'''
|
||||||
raise self.error.credentials_error(detail="Jwt token is incorrect")
|
payload_access=await self.jwt_service.jwt_decode(access_token)
|
||||||
|
|
||||||
|
|
||||||
|
if (jti_refresh:=payload_refresh.get("jti")) is None:
|
||||||
|
raise self.error.credentials_error(detail="Jwt refresh token is incorrect")
|
||||||
|
|
||||||
|
if (jti_access:=payload_access.get("jti")) is None or (exp_access:=payload_access.get("exp")) is None:
|
||||||
|
raise self.error.credentials_error(detail="Jwt access token is incorrect")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jti=UUID(jti)
|
jti_refresh=UUID(jti_refresh)
|
||||||
|
jti_access=UUID(jti_access)
|
||||||
except (ValueError, TypeError, AttributeError) as e:
|
except (ValueError, TypeError, AttributeError) as e:
|
||||||
raise self.error.credentials_error(detail="Jwt token is incorrect") from e
|
raise self.error.credentials_error(detail="Jwt token is incorrect") from e
|
||||||
|
|
||||||
|
if jti_access and exp_access:
|
||||||
|
exp_datetime = datetime.fromtimestamp(exp_access, tz=UTC)
|
||||||
|
remaining_ttl = max(int((exp_datetime - datetime.now(UTC)).total_seconds()), 1)
|
||||||
|
await redis_client.set(f"revoked_access_token:{jti_access}", "1", ex=remaining_ttl) #revoke tokens and set them to redis until their exp ends
|
||||||
|
|
||||||
'''logout by assigning revoked flag'''
|
'''logout by assigning revoked flag'''
|
||||||
if await self.jwt_db_actions.logout(jti):
|
if await self.jwt_db_actions.logout(jti_refresh):
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
raise self.error.not_found_error(detail="Refresh Token Not Found")
|
raise self.error.not_found_error(detail="Refresh Token Not Found")
|
||||||
|
|||||||
@@ -35,14 +35,17 @@ class JwtService:
|
|||||||
if not (data.get("sub")) or data.get("sub") == "":
|
if not (data.get("sub")) or data.get("sub") == "":
|
||||||
raise self.error.credentials_error(detail="Jwt token is incorrect")
|
raise self.error.credentials_error(detail="Jwt token is incorrect")
|
||||||
|
|
||||||
|
|
||||||
async def create_access_token(self, data:dict)->str:
|
async def create_access_token(self, data:dict)->str:
|
||||||
|
|
||||||
user_info=data.copy()
|
user_info=data.copy()
|
||||||
|
jti=str(uuid4())
|
||||||
|
|
||||||
await self._validate_sub(user_info)
|
await self._validate_sub(user_info)
|
||||||
|
|
||||||
user_info.update({"exp": datetime.now(UTC)+timedelta(minutes=env_settings.ACCESS_TOKEN_EXPIRE_MINUTES),
|
user_info.update({"exp": datetime.now(UTC)+timedelta(minutes=env_settings.ACCESS_TOKEN_EXPIRE_MINUTES),
|
||||||
"token_type":"access"})
|
"token_type":"access",
|
||||||
|
"jti":jti})
|
||||||
return jwt.encode(user_info, env_settings.SECRET_KEY, env_settings.ALGORITHM)
|
return jwt.encode(user_info, env_settings.SECRET_KEY, env_settings.ALGORITHM)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from fastapi import APIRouter, Cookie, Depends, Request, Response
|
from fastapi import APIRouter, Cookie, Depends, HTTPException, Request, Response
|
||||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
|
|
||||||
|
from src.cache.rate_limit import rate_limiter
|
||||||
from src.models.configs_read.env import env_settings
|
from src.models.configs_read.env import env_settings
|
||||||
from src.models.pydantic_models.model import UserOut
|
from src.models.pydantic_models.model import UserOut
|
||||||
from src.service.auth.auth import CurrentUserService, auth_service
|
from src.service.auth.auth import CurrentUserService, auth_service
|
||||||
@@ -12,22 +13,32 @@ oauth2_schema=OAuth2PasswordBearer(tokenUrl="/protected/token", refreshUrl="/pro
|
|||||||
def require_permissions(*permissions: str): #permissions check dependency
|
def require_permissions(*permissions: str): #permissions check dependency
|
||||||
async def checker(
|
async def checker(
|
||||||
token: str = Depends(oauth2_schema),
|
token: str = Depends(oauth2_schema),
|
||||||
auth: CurrentUserService = Depends(auth_service), #noqa: B008
|
auth: CurrentUserService = Depends(auth_service),
|
||||||
) -> UserOut:
|
) -> UserOut:
|
||||||
return UserOut.model_validate(await auth.get_current_user(token, *permissions))
|
return UserOut.model_validate(await auth.get_current_user(token, *permissions))
|
||||||
return checker
|
return checker
|
||||||
|
|
||||||
|
|
||||||
@router.post("/token")
|
@router.post("/token")
|
||||||
async def get_access_token(request: Request,response:Response,auth:CurrentUserService=Depends(auth_service), form_data:OAuth2PasswordRequestForm=Depends())->dict: # noqa: B008
|
async def get_access_token(request: Request,
|
||||||
|
response:Response,
|
||||||
|
auth:CurrentUserService=Depends(auth_service),
|
||||||
|
form_data:OAuth2PasswordRequestForm=Depends(),
|
||||||
|
)->dict:
|
||||||
|
|
||||||
access_token, refresh_token=await auth.login(form_data_email=form_data.username, form_data_password=form_data.password, request=request)
|
client_ip = request.headers.get('x-forwarded-for', '').split(',')[0].strip() or (request.client.host if request.client else 'unknown')
|
||||||
|
|
||||||
|
try:
|
||||||
|
access_token, refresh_token=await auth.login(form_data_email=form_data.username, form_data_password=form_data.password, request=request)
|
||||||
|
except HTTPException:
|
||||||
|
await rate_limiter.rate_limit(client_ip)
|
||||||
|
raise
|
||||||
|
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key="refresh_token",
|
key="refresh_token",
|
||||||
value=refresh_token,
|
value=refresh_token,
|
||||||
httponly=True,
|
httponly=True,
|
||||||
secure=True,
|
secure=env_settings.PROD_MODE,
|
||||||
samesite="strict",
|
samesite="strict",
|
||||||
max_age=env_settings.REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
|
max_age=env_settings.REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
|
||||||
)
|
)
|
||||||
@@ -35,7 +46,10 @@ async def get_access_token(request: Request,response:Response,auth:CurrentUserSe
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/refresh")
|
@router.post("/refresh")
|
||||||
async def get_refresh_token(request:Request,response:Response, refresh_token: str = Cookie(), auth:CurrentUserService=Depends(auth_service))->dict: # noqa: B008
|
async def get_refresh_token(request:Request,
|
||||||
|
response:Response,
|
||||||
|
refresh_token: str = Cookie(),
|
||||||
|
auth:CurrentUserService=Depends(auth_service))->dict:
|
||||||
|
|
||||||
access_token, refresh_token= await auth.refresh_token(refresh_token=refresh_token,request=request)
|
access_token, refresh_token= await auth.refresh_token(refresh_token=refresh_token,request=request)
|
||||||
|
|
||||||
@@ -43,7 +57,7 @@ async def get_refresh_token(request:Request,response:Response, refresh_token: st
|
|||||||
key="refresh_token",
|
key="refresh_token",
|
||||||
value=refresh_token,
|
value=refresh_token,
|
||||||
httponly=True,
|
httponly=True,
|
||||||
secure=True,
|
secure=env_settings.PROD_MODE,
|
||||||
samesite="strict",
|
samesite="strict",
|
||||||
max_age=env_settings.REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
|
max_age=env_settings.REFRESH_TOKEN_EXPIRE_DAYS * 24 * 60 * 60
|
||||||
)
|
)
|
||||||
@@ -51,10 +65,15 @@ async def get_refresh_token(request:Request,response:Response, refresh_token: st
|
|||||||
return {"access_token":access_token, "token_type": "bearer"}
|
return {"access_token":access_token, "token_type": "bearer"}
|
||||||
|
|
||||||
@router.get("/logout")
|
@router.get("/logout")
|
||||||
async def logout(response:Response,refresh_token: str = Cookie(),auth:CurrentUserService=Depends(auth_service),current_user:UserOut=Depends(require_permissions()))->bool: # noqa: B008
|
async def logout(response:Response,
|
||||||
|
refresh_token: str = Cookie(),
|
||||||
|
access_token: str = Depends(oauth2_schema),
|
||||||
|
auth:CurrentUserService=Depends(auth_service),
|
||||||
|
current_user:UserOut=Depends(require_permissions()))->bool:
|
||||||
|
|
||||||
response.delete_cookie("refresh_token")
|
response.delete_cookie("refresh_token")
|
||||||
return await auth.logout(refresh_token)
|
return await auth.logout(refresh_token, access_token)
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
async def protected(current_user:UserOut=Depends(require_permissions()))->dict: # noqa: B008
|
async def protected(current_user:UserOut=Depends(require_permissions()))->dict:
|
||||||
return {"protected router": "Hello, this is a protected router"}
|
return {"protected router": "Hello, this is a protected router"}
|
||||||
|
|||||||
@@ -7,22 +7,22 @@ from src.web.protected_routes.auth_routes import require_permissions
|
|||||||
router=APIRouter(prefix="/user")
|
router=APIRouter(prefix="/user")
|
||||||
|
|
||||||
@router.get("/get_by_email")
|
@router.get("/get_by_email")
|
||||||
async def get_current_user_by_email(email:str, crud:CrudService=Depends(crud_service), current_user=Depends(require_permissions("admin")))->UserOut: # noqa: B008
|
async def get_current_user_by_email(email:str, crud:CrudService=Depends(crud_service), current_user=Depends(require_permissions("admin")))->UserOut:
|
||||||
return await crud.get_user_by_email(email)
|
return await crud.get_user_by_email(email)
|
||||||
|
|
||||||
@router.post("/create_user")
|
@router.post("/create_user")
|
||||||
async def create_user(data:UserCreate, crud:CrudService=Depends(crud_service), current_user=Depends(require_permissions("admin")))->UserOut: #noqa: B008
|
async def create_user(data:UserCreate, crud:CrudService=Depends(crud_service), current_user=Depends(require_permissions("admin")))->UserOut:
|
||||||
return await crud.create_user(data)
|
return await crud.create_user(data)
|
||||||
|
|
||||||
@router.post("/delete_user_soft")
|
@router.post("/delete_user_soft")
|
||||||
async def delete_user_soft(email:str, crud:CrudService=Depends(crud_service), current_user=Depends(require_permissions("admin")))->bool: #noqa: B008
|
async def delete_user_soft(email:str, crud:CrudService=Depends(crud_service), current_user=Depends(require_permissions("admin")))->bool:
|
||||||
return await crud.delete_user_soft(email)
|
return await crud.delete_user_soft(email)
|
||||||
|
|
||||||
@router.post("/delete_user_hard")
|
@router.post("/delete_user_hard")
|
||||||
async def delete_user_hard(email:str, crud:CrudService=Depends(crud_service), current_user=Depends(require_permissions("admin")))->bool: #noqa: B008
|
async def delete_user_hard(email:str, crud:CrudService=Depends(crud_service), current_user=Depends(require_permissions("admin")))->bool:
|
||||||
return await crud.delete_user_hard(email, current_user)
|
return await crud.delete_user_hard(email, current_user)
|
||||||
|
|
||||||
@router.patch("/patch_user")
|
@router.patch("/patch_user")
|
||||||
async def patch_user(email:str, data:UserUpdate, crud:CrudService=Depends(crud_service), current_user=Depends(require_permissions("admin")))->UserOut: #noqa: B008
|
async def patch_user(email:str, data:UserUpdate, crud:CrudService=Depends(crud_service), current_user=Depends(require_permissions("admin")))->UserOut:
|
||||||
return await crud.update_user(email, data)
|
return await crud.update_user(email, data)
|
||||||
|
|
||||||
+17
-1
@@ -1,6 +1,9 @@
|
|||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
|
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
|
from src.models.configs_read.env import env_settings
|
||||||
from src.service.auth.jwt import HashService, JwtService
|
from src.service.auth.jwt import HashService, JwtService
|
||||||
|
from src.service.users_crud.users_crud import CrudService
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture
|
@pytest_asyncio.fixture
|
||||||
@@ -11,4 +14,17 @@ async def jwt_service()->JwtService:
|
|||||||
@pytest_asyncio.fixture
|
@pytest_asyncio.fixture
|
||||||
async def hash_service()->HashService:
|
async def hash_service()->HashService:
|
||||||
hash_service=HashService()
|
hash_service=HashService()
|
||||||
return hash_service
|
return hash_service
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def crud_service():
|
||||||
|
test_engine = create_async_engine(f"postgresql+asyncpg://{env_settings.DB_USER}:{env_settings.DB_PASSWORD}@{env_settings.DB_HOST}:{env_settings.DB_PORT}/{env_settings.DB_POSTGRESS}",
|
||||||
|
pool_size=20,
|
||||||
|
max_overflow=10,
|
||||||
|
pool_timeout=30,
|
||||||
|
pool_pre_ping=True
|
||||||
|
)
|
||||||
|
crud_service = CrudService()
|
||||||
|
crud_service.crud_db_actions.Session = async_sessionmaker(bind=test_engine)
|
||||||
|
yield crud_service
|
||||||
|
await test_engine.dispose()
|
||||||
+40
-2
@@ -4,7 +4,7 @@ import requests_async
|
|||||||
from httpx import HTTPStatusError
|
from httpx import HTTPStatusError
|
||||||
|
|
||||||
|
|
||||||
class TestAuth:
|
class TestPermissions:
|
||||||
|
|
||||||
async def test_get_access_token_positive(self, target_url:str)->None:
|
async def test_get_access_token_positive(self, target_url:str)->None:
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ class TestAuth:
|
|||||||
|
|
||||||
response = await requests_async.post(f"{target_url}/protected/token")
|
response = await requests_async.post(f"{target_url}/protected/token")
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
assert exc_info.value.response.status_code != 403
|
assert exc_info.value.response.status_code != 403
|
||||||
assert exc_info.value.response.status_code != 401
|
assert exc_info.value.response.status_code != 401
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ class TestAuth:
|
|||||||
assert exc_info.value.response.status_code != 403
|
assert exc_info.value.response.status_code != 403
|
||||||
assert exc_info.value.response.status_code != 401
|
assert exc_info.value.response.status_code != 401
|
||||||
|
|
||||||
async def test_get_root_positive(self, target_url:str)->None:
|
async def test_get_root_unauthorized(self, target_url:str)->None:
|
||||||
|
|
||||||
with allure.step("get_root"), pytest.raises(HTTPStatusError) as exc_info:
|
with allure.step("get_root"), pytest.raises(HTTPStatusError) as exc_info:
|
||||||
|
|
||||||
@@ -43,4 +44,41 @@ class TestAuth:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
assert exc_info.value.response.status_code != 403
|
assert exc_info.value.response.status_code != 403
|
||||||
assert exc_info.value.response.status_code != 401
|
assert exc_info.value.response.status_code != 401
|
||||||
|
|
||||||
|
|
||||||
|
class TestRedis:
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("wrong_user_data, expected_status",[
|
||||||
|
pytest.param({"username":"Wrong_user", "password":"Wrong_password"},429,id="Wrong_user_creds")
|
||||||
|
])
|
||||||
|
async def test_rate_limit_positive(self, wrong_user_data:dict, target_url:str, expected_status:int):
|
||||||
|
|
||||||
|
with allure.step("logging with invalid creds"):
|
||||||
|
|
||||||
|
for i in range(5):
|
||||||
|
with pytest.raises(HTTPStatusError) as exc_info:
|
||||||
|
response = await requests_async.post(target_url + "/protected/token", data=wrong_user_data)
|
||||||
|
response.raise_for_status()
|
||||||
|
assert exc_info.value.response.status_code == 401, f"Attempt {i+1} should be 401"
|
||||||
|
|
||||||
|
with allure.step("verify rate_limit works"),pytest.raises(HTTPStatusError) as exc_info:
|
||||||
|
response = await requests_async.post(target_url + "/protected/token", data=wrong_user_data)
|
||||||
|
response.raise_for_status()
|
||||||
|
assert exc_info.value.response.status_code == expected_status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("test_user_fixture", [([], [])], indirect=True)
|
||||||
|
async def test_logout_revokes_access_token(self,test_user_fixture, target_url:str)->None:
|
||||||
|
|
||||||
|
session=test_user_fixture[0]
|
||||||
|
|
||||||
|
with allure.step("logout"):
|
||||||
|
|
||||||
|
response = await session.get(f"{target_url}/protected/logout")
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
with allure.step("verify token is revoked"), pytest.raises(HTTPStatusError) as exc_info:
|
||||||
|
response = await session.get(f"{target_url}/protected")
|
||||||
|
response.raise_for_status()
|
||||||
|
assert exc_info.value.response.status_code==401
|
||||||
|
|
||||||
@@ -1,22 +1,23 @@
|
|||||||
import pytest_asyncio
|
import pytest_asyncio
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
|
|
||||||
|
from src.cache.redis_client import RedisClient
|
||||||
from src.service.auth.auth import CurrentUserService
|
from src.service.auth.auth import CurrentUserService
|
||||||
from src.service.users_crud.users_crud import CrudService
|
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture
|
@pytest_asyncio.fixture
|
||||||
async def current_user_service()->CurrentUserService:
|
async def current_user_service(monkeypatch):
|
||||||
current_user_service=CurrentUserService()
|
|
||||||
return current_user_service
|
test_redis = RedisClient()
|
||||||
|
monkeypatch.setattr("src.service.auth.auth.redis_client", test_redis)
|
||||||
|
|
||||||
|
service = CurrentUserService()
|
||||||
|
|
||||||
|
yield service
|
||||||
|
await test_redis.aclose()
|
||||||
|
|
||||||
@pytest_asyncio.fixture
|
@pytest_asyncio.fixture
|
||||||
async def requests(mocker):
|
async def requests(mocker):
|
||||||
fake_request = mocker.MagicMock(spec=Request)
|
fake_request = mocker.MagicMock(spec=Request)
|
||||||
fake_request.headers = {"user-agent": "pytest-agent", "x-forwarded-for":"127.0.0.1"}
|
fake_request.headers = {"user-agent": "pytest-agent", "x-forwarded-for":"127.0.0.1"}
|
||||||
return fake_request
|
return fake_request
|
||||||
|
|
||||||
@pytest_asyncio.fixture
|
|
||||||
async def crud_service()->CrudService:
|
|
||||||
crud_service=CrudService()
|
|
||||||
return crud_service
|
|
||||||
@@ -118,7 +118,8 @@ class TestAuth:
|
|||||||
|
|
||||||
with allure.step("create fake refresh token"):
|
with allure.step("create fake refresh token"):
|
||||||
|
|
||||||
token=await jwt_service.create_refresh_token({"sub":str(uuid4())})
|
refresh_token=await jwt_service.create_refresh_token({"sub":str(uuid4())})
|
||||||
|
access_token=await jwt_service.create_access_token({"sub":str(uuid4)})
|
||||||
|
|
||||||
with allure.step("patching db call functions"):
|
with allure.step("patching db call functions"):
|
||||||
|
|
||||||
@@ -126,7 +127,7 @@ class TestAuth:
|
|||||||
|
|
||||||
with allure.step("test logout with fake data"):
|
with allure.step("test logout with fake data"):
|
||||||
|
|
||||||
status=await current_user_service.logout(token[0])
|
status=await current_user_service.logout(refresh_token[0], access_token)
|
||||||
assert status is True
|
assert status is True
|
||||||
|
|
||||||
@pytest.mark.parametrize("jti,db_result, expected_exception, expected_status",[
|
@pytest.mark.parametrize("jti,db_result, expected_exception, expected_status",[
|
||||||
@@ -148,11 +149,11 @@ class TestAuth:
|
|||||||
|
|
||||||
with allure.step("create fake refresh token"):
|
with allure.step("create fake refresh token"):
|
||||||
|
|
||||||
token=await fake_create_refresh_token({"sub":str(uuid4()), "jti":jti, "token_type":"refresh", "exp":datetime.now(UTC)+timedelta(days=45)})
|
refresh_token=await fake_create_refresh_token({"sub":str(uuid4()), "jti":jti, "token_type":"refresh", "exp":datetime.now(UTC)+timedelta(days=45)})
|
||||||
|
access_token=await fake_create_refresh_token({"sub":str(uuid4()), "jti":jti, "token_type":"refresh", "exp":datetime.now(UTC)+timedelta(minutes=30)})
|
||||||
with allure.step("test logout with fake data"), pytest.raises(expected_exception) as exc_info:
|
with allure.step("test logout with fake data"), pytest.raises(expected_exception) as exc_info:
|
||||||
|
|
||||||
await current_user_service.logout(token)
|
await current_user_service.logout(refresh_token, access_token)
|
||||||
|
|
||||||
if expected_exception is HTTPException:
|
if expected_exception is HTTPException:
|
||||||
assert exc_info.value.status_code==expected_status
|
assert exc_info.value.status_code==expected_status
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import pytest_asyncio
|
|
||||||
|
|
||||||
from src.service.users_crud.users_crud import CrudService
|
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture
|
|
||||||
async def crud_service()->CrudService:
|
|
||||||
crud_service=CrudService()
|
|
||||||
return crud_service
|
|
||||||
Reference in New Issue
Block a user