Compare commits
28
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5dde797a1b | ||
|
|
cfd3078216 | ||
|
|
73e983c6a5 | ||
|
|
8bc3ff7b54 | ||
|
|
b3083b0e82 | ||
|
|
1eb9935a15 | ||
|
|
0a4a21f2e7 | ||
|
|
4486f62e17 | ||
|
|
33aa3cb7a4 | ||
|
|
d3244666af | ||
|
|
6c5b85223e | ||
|
|
0f8d816e7f | ||
|
|
e23a6fc569 | ||
|
|
50201542da | ||
|
|
5ec033a21d | ||
|
|
2da58c7483 | ||
|
|
4e14972cf6 | ||
|
|
8fa72daa6b | ||
|
|
3705ac4f0c | ||
|
|
439d57554c | ||
|
|
f87f54de55 | ||
|
|
00403191f5 | ||
|
|
d24b99b8b0 | ||
|
|
ef1e39d506 | ||
|
|
2781317797 | ||
|
|
f81ba19da4 | ||
|
|
7a4df2933d | ||
|
|
5522447b07 |
@@ -0,0 +1,18 @@
|
|||||||
|
# виртуальное окружение
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# кэш питона
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
*.pytest_cache
|
||||||
|
|
||||||
|
# IDE и редакторы
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS мусор
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -25,3 +25,5 @@ Thumbs.db
|
|||||||
#Примеры документов
|
#Примеры документов
|
||||||
input/
|
input/
|
||||||
output/
|
output/
|
||||||
|
allure-results/
|
||||||
|
.coverage
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
HOST="change_me"
|
||||||
|
PORT="change_me"
|
||||||
|
TEST_USERNAME="change_me"
|
||||||
|
TEST_PASSWORD="change_me"
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
name: excel-project
|
||||||
|
|
||||||
|
services:
|
||||||
|
backend-dev:
|
||||||
|
profiles: ["dev"]
|
||||||
|
image: "${DOCKER_REGISTRY:-local}/excel-dev:${IMAGE_TAG:-local}"
|
||||||
|
container_name: backend-dev
|
||||||
|
build:
|
||||||
|
dockerfile: ./docker/dockerfile
|
||||||
|
context: ../
|
||||||
|
target: dev
|
||||||
|
init: true #Manage processes and reap zombies
|
||||||
|
ipc: private #Inter-Process Communication (IPC) namespace for high-performance applications
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ../src
|
||||||
|
target: /home/excel-project/src
|
||||||
|
- type: bind
|
||||||
|
source: ../main.py
|
||||||
|
target: /home/excel-project/main.py
|
||||||
|
- type: bind
|
||||||
|
source: ../configs
|
||||||
|
target: /home/excel-project/configs
|
||||||
|
- type: bind
|
||||||
|
source: ../DB
|
||||||
|
target: /home/excel-project/DB
|
||||||
|
- type: bind
|
||||||
|
source: ../uploads
|
||||||
|
target: /home/excel-project/uploads
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
ports:
|
||||||
|
- "80:8000"
|
||||||
|
entrypoint: ["./entrypoint.sh", "--dev"]
|
||||||
|
|
||||||
|
backend-prod:
|
||||||
|
profiles: ["prod"]
|
||||||
|
image: "${DOCKER_REGISTRY:-local}/excel-prod:${IMAGE_TAG:-local}"
|
||||||
|
container_name: backend-prod
|
||||||
|
build:
|
||||||
|
dockerfile: ./docker/dockerfile
|
||||||
|
context: ../
|
||||||
|
target: prod
|
||||||
|
init: true #Manage processes and reap zombies
|
||||||
|
ipc: private #Inter-Process Communication (IPC) namespace for high-performance applications
|
||||||
|
volumes:
|
||||||
|
- type: bind
|
||||||
|
source: ../configs
|
||||||
|
target: /home/excel-project/configs
|
||||||
|
- type: bind
|
||||||
|
source: ../DB
|
||||||
|
target: /home/excel-project/DB
|
||||||
|
- type: bind
|
||||||
|
source: ../uploads
|
||||||
|
target: /home/excel-project/uploads
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:8000"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
name: "${BACKEND_NETWORK:-backend_network}"
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# --- Stage 1: Python Backend dev ---
|
||||||
|
FROM python:3.14-slim AS dev
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="the-great-excel-project-dev"
|
||||||
|
LABEL org.opencontainers.image.source="https://git.homyk.space/MH.Dmitrii/The_Great_Excel_Project"
|
||||||
|
|
||||||
|
WORKDIR /home/excel-project
|
||||||
|
|
||||||
|
COPY pyproject.toml poetry.lock docker/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
|
||||||
|
|
||||||
|
# --- Stage 2: Python Backend prod ---
|
||||||
|
|
||||||
|
FROM python:3.14-slim AS prod
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.title="the-great-excel-project-prod"
|
||||||
|
LABEL org.opencontainers.image.source="https://git.homyk.space/MH.Dmitrii/The_Great_Excel_Project"
|
||||||
|
|
||||||
|
WORKDIR /home/excel-project
|
||||||
|
|
||||||
|
COPY pyproject.toml poetry.lock main.py docker/entrypoint.sh ./
|
||||||
|
COPY src/ ./src/
|
||||||
|
|
||||||
|
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 \
|
||||||
|
&& useradd --uid 1000 --gid appuser --shell /bin/bash --create-home appuser \
|
||||||
|
&& chown -R appuser:appuser /home/excel-project
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
ENTRYPOINT ["./entrypoint.sh", "--prod"]
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
MODE=""
|
||||||
|
WORKERS=""
|
||||||
|
|
||||||
|
while [ -n "$1" ]; do
|
||||||
|
case "$1" in
|
||||||
|
--dev) MODE="dev" ;;
|
||||||
|
--prod) MODE="prod" ;;
|
||||||
|
--workers)
|
||||||
|
shift
|
||||||
|
WORKERS="$1"
|
||||||
|
;;
|
||||||
|
*) echo "$1 is not an option" ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -z "$MODE" ]; then
|
||||||
|
echo "Usage: entrypoint.sh --dev|--prod [--workers N]"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$MODE" = "dev" ]; then
|
||||||
|
WORKERS="${WORKERS:-1}"
|
||||||
|
exec gunicorn \
|
||||||
|
--workers "$WORKERS" \
|
||||||
|
--worker-class uvicorn.workers.UvicornWorker \
|
||||||
|
--worker-connections 1000 \
|
||||||
|
--reload \
|
||||||
|
--bind 0.0.0.0:8000 \
|
||||||
|
main:app
|
||||||
|
else
|
||||||
|
WORKERS="${WORKERS:-4}"
|
||||||
|
exec gunicorn \
|
||||||
|
--workers "$WORKERS" \
|
||||||
|
--worker-class uvicorn.workers.UvicornWorker \
|
||||||
|
--worker-connections 1000 \
|
||||||
|
--bind 0.0.0.0:8000 \
|
||||||
|
main:app
|
||||||
|
fi
|
||||||
@@ -1,29 +1,38 @@
|
|||||||
from fastapi import FastAPI
|
from contextlib import asynccontextmanager
|
||||||
from src.web.protected_routes.routes import router as protected_router
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import uvicorn
|
|
||||||
|
|
||||||
app=FastAPI(root_path="/")
|
# import uvicorn
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
from src.web.protected_routes.auth_routes import router as protected_router
|
||||||
|
from src.web.protected_routes.protected_user_action_routes import (
|
||||||
|
router as protected_user_action_routes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
create_dirs()
|
||||||
|
yield
|
||||||
|
print("shutting down")
|
||||||
|
|
||||||
|
|
||||||
|
app=FastAPI(root_path="/", lifespan=lifespan)
|
||||||
app.include_router(router=protected_router)
|
app.include_router(router=protected_router)
|
||||||
|
app.include_router(router=protected_user_action_routes)
|
||||||
|
|
||||||
|
|
||||||
@app.get("")
|
@app.get("")
|
||||||
def root()->dict:
|
async def root()->dict:
|
||||||
return {"root":"hello, this is root"}
|
return {"root":"hello, this is root"}
|
||||||
|
|
||||||
|
|
||||||
def create_dirs():
|
def create_dirs():
|
||||||
|
|
||||||
dirs_to_create=("./DB",
|
dirs_to_create=("./DB",
|
||||||
"./upload",
|
"./uploads/upload",
|
||||||
"./upload_bad",
|
"./uploads/upload_bad",
|
||||||
"./upload_finished")
|
"./uploads/upload_finished")
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
def main():
|
|
||||||
create_dirs()
|
|
||||||
uvicorn.run("main:app", reload=True)
|
|
||||||
|
|
||||||
if __name__=="__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,11 +1,82 @@
|
|||||||
VENV=source .venv/bin/activate;
|
VENV:=source .venv/bin/activate;
|
||||||
|
ALLURE:=.venv/allure-2.44.0/bin/allure #linux&macos
|
||||||
|
#ALLURE=.venv\allure-2.44.0\bin\allure #Windows
|
||||||
|
|
||||||
.PHONY:
|
NUM_DOWN ?= 1
|
||||||
run, m_gen, m_up
|
|
||||||
|
|
||||||
run:
|
.DEFAULT_GOAL := help
|
||||||
${VENV} python3 main.py
|
|
||||||
m_gen:
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
@grep -E '(^[a-zA-Z0-9_-]+:.*?##.*$$)|(^##)' Makefile | awk 'BEGIN {FS = ":.*?## "}{printf "[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m## /[33m/' | sed -e 's/\[32m/ [32m/' | sed -e 's/\[33m/[33m/'
|
||||||
|
|
||||||
|
##
|
||||||
|
## Init section
|
||||||
|
##
|
||||||
|
.PHONY: run
|
||||||
|
run: ## Run dev local application
|
||||||
|
${VENV} uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|
||||||
|
.PHONY: run-dev
|
||||||
|
run-dev: ## Run dev application
|
||||||
|
docker compose -f docker/compose-dev.yaml --profile dev up -d
|
||||||
|
|
||||||
|
.PHONY: run-prod
|
||||||
|
run-prod: ## Run prod application
|
||||||
|
docker compose -f docker/compose-dev.yaml --profile prod up -d
|
||||||
|
|
||||||
|
.PHONY: down-dev
|
||||||
|
down-dev: ## Down dev application
|
||||||
|
docker compose -f docker/compose-dev.yaml --profile dev down
|
||||||
|
|
||||||
|
.PHONY: down-prod
|
||||||
|
down-prod: ## Down prod application
|
||||||
|
docker compose -f docker/compose-dev.yaml --profile prod down
|
||||||
|
|
||||||
|
##
|
||||||
|
## Migration section
|
||||||
|
##
|
||||||
|
|
||||||
|
.PHONY: m_gen
|
||||||
|
m_gen: ## Generate alembic new revision
|
||||||
${VENV} alembic revision --autogenerate
|
${VENV} alembic revision --autogenerate
|
||||||
m_up:
|
|
||||||
|
.PHONY: m_up
|
||||||
|
m_up: ## Set new alembic revision
|
||||||
${VENV} alembic upgrade head
|
${VENV} alembic upgrade head
|
||||||
|
|
||||||
|
.PHONY: m_down
|
||||||
|
m_down: ## Downgrade alembic revision
|
||||||
|
${VENV} alembic downgrade -${NUM_DOWN}
|
||||||
|
|
||||||
|
.PHONY: m_history
|
||||||
|
m_history: ## List history of migrations
|
||||||
|
${VENV} alembic history
|
||||||
|
|
||||||
|
.PHONY: m_current
|
||||||
|
m_current: ## Current migration
|
||||||
|
${VENV} alembic current
|
||||||
|
|
||||||
|
.PHONY: m_stamp
|
||||||
|
m_stamp: ## Stamp head
|
||||||
|
${VENV} alembic stamp head
|
||||||
|
|
||||||
|
##
|
||||||
|
## Test section
|
||||||
|
##
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test: ## Run tests
|
||||||
|
${VENV} pytest
|
||||||
|
|
||||||
|
.PHONY: allure
|
||||||
|
allure: ## Generate allure report
|
||||||
|
${VENV} ${ALLURE} generate tests/allure-results/reports --single-file -o tests/allure-results/html --clean
|
||||||
|
|
||||||
|
.PHONY: coverage
|
||||||
|
coverage: ## Run pytest coverage
|
||||||
|
${VENV} pytest --cov=src tests/ --cov-report=term-missing
|
||||||
|
|
||||||
|
.PHONY: clear
|
||||||
|
clear: ## Delete old test results
|
||||||
|
rm -rf ./tests/allure-results/reports
|
||||||
Generated
+420
-122
@@ -1,5 +1,21 @@
|
|||||||
# 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]]
|
||||||
|
name = "aiosqlite"
|
||||||
|
version = "0.22.1"
|
||||||
|
description = "asyncio bridge to the standard sqlite3 module"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
files = [
|
||||||
|
{file = "aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb"},
|
||||||
|
{file = "aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[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"
|
||||||
@@ -20,6 +36,38 @@ typing-extensions = ">=4.12"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
tz = ["tzdata"]
|
tz = ["tzdata"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allure-pytest"
|
||||||
|
version = "2.16.0"
|
||||||
|
description = "Allure pytest integration"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "allure_pytest-2.16.0-py3-none-any.whl", hash = "sha256:e5035a3b1f541b0c2ee566822df6fec20e0628b8536780e5bdfe39060ac0c71b"},
|
||||||
|
{file = "allure_pytest-2.16.0.tar.gz", hash = "sha256:3cc883595b1ce4280b0b9a5fdaa0ce3bb5cdbaa1b43c7269d8b82e825e6e107c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
allure-python-commons = "2.16.0"
|
||||||
|
pytest = ">=4.5.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allure-python-commons"
|
||||||
|
version = "2.16.0"
|
||||||
|
description = "Contains the API for end users as well as helper functions and classes to build Allure adapters for Python test frameworks"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "allure_python_commons-2.16.0-py3-none-any.whl", hash = "sha256:6d42a500078aca8a2e68823075c1ffc2396987bb268d62b19af82390b205ce88"},
|
||||||
|
{file = "allure_python_commons-2.16.0.tar.gz", hash = "sha256:ecdc92bafea074bab96b5f2c4eb3100825340188f5aece608ae80eced709b36f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
attrs = ">=16.0.0"
|
||||||
|
pluggy = ">=0.4.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-doc"
|
name = "annotated-doc"
|
||||||
version = "0.0.4"
|
version = "0.0.4"
|
||||||
@@ -50,7 +98,7 @@ version = "4.14.2"
|
|||||||
description = "High-level concurrency and networking framework on top of asyncio or Trio"
|
description = "High-level concurrency and networking framework on top of asyncio or Trio"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494"},
|
{file = "anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494"},
|
||||||
{file = "anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f"},
|
{file = "anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f"},
|
||||||
@@ -68,7 +116,7 @@ version = "3.0.2"
|
|||||||
description = "Annotate AST trees with source code positions"
|
description = "Annotate AST trees with source code positions"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933"},
|
{file = "asttokens-3.0.2-py3-none-any.whl", hash = "sha256:9da13157f5b28becde0bd374fc677dcd3c290614264eff096f167c469cd9f933"},
|
||||||
{file = "asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2"},
|
{file = "asttokens-3.0.2.tar.gz", hash = "sha256:3ecdbd8f2cc195f53ccada3a613538bb5f9ef6f6869129f13e03c30a677b8fe2"},
|
||||||
@@ -78,6 +126,18 @@ 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 = "attrs"
|
||||||
|
version = "26.1.0"
|
||||||
|
description = "Classes Without Boilerplate"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309"},
|
||||||
|
{file = "attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bcrypt"
|
name = "bcrypt"
|
||||||
version = "5.0.0"
|
version = "5.0.0"
|
||||||
@@ -161,7 +221,7 @@ version = "2026.7.22"
|
|||||||
description = "Python package for providing Mozilla's CA Bundle."
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"},
|
{file = "certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775"},
|
||||||
{file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"},
|
{file = "certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55"},
|
||||||
@@ -173,7 +233,7 @@ version = "3.4.9"
|
|||||||
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a"},
|
{file = "charset_normalizer-3.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a"},
|
||||||
{file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616"},
|
{file = "charset_normalizer-3.4.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616"},
|
||||||
@@ -291,12 +351,116 @@ version = "0.4.6"
|
|||||||
description = "Cross-platform colored terminal text."
|
description = "Cross-platform colored terminal text."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
markers = "sys_platform == \"win32\" or platform_system == \"Windows\""
|
|
||||||
files = [
|
files = [
|
||||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
]
|
]
|
||||||
|
markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coverage"
|
||||||
|
version = "7.15.2"
|
||||||
|
description = "Code coverage measurement for Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "coverage-7.15.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b5bd92ff1ec22e535eab0de75fa6db021992791f461a2aceb7822c625a1187d"},
|
||||||
|
{file = "coverage-7.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:44826758cfe73fcd0e6af5deb4ba6d5417cc1d13df3acb35c93484a11160f846"},
|
||||||
|
{file = "coverage-7.15.2-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:09f5c6ec5901f667bd97dd140b5b9a2586b10efec66f46fb1e6d8135f8b95bdf"},
|
||||||
|
{file = "coverage-7.15.2-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1d16e3a7104ea84f03e614611b3edbf6fb6892554b3ab0fe7fbb3f2b2ef04376"},
|
||||||
|
{file = "coverage-7.15.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d46e62cb35d91e6e2589fda6d28074426b0e276422b5d2ebef2c6b11dc60dbfd"},
|
||||||
|
{file = "coverage-7.15.2-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dfd3db045e95960ae3683059571e597fda7cc610106a8916f77c5839048c1deb"},
|
||||||
|
{file = "coverage-7.15.2-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:affd532502d34c0472d0cdb181325c89f1d2c44992fef0c17e88e7b1576259a1"},
|
||||||
|
{file = "coverage-7.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d17d7512151fedfcc64c1821a8977fc9be0dbf495754669afcab7b57abc98ae9"},
|
||||||
|
{file = "coverage-7.15.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e26ff680768b8095e8874aabe0e9d3a47a2a9f176a8340d05f8604c56457c23a"},
|
||||||
|
{file = "coverage-7.15.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:7e8f27131dc7cd53de2c137dd207b3720919320b3c20d499dc30aa9ee6173287"},
|
||||||
|
{file = "coverage-7.15.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:728a33676d4c3f0db977990a4bd421dcaa3be3e53b5b6273036fff6666008e89"},
|
||||||
|
{file = "coverage-7.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:29c052f7c83ccfcc5c577eaae025d2e4a9bb80daf03c0ac31c996e83b000ce88"},
|
||||||
|
{file = "coverage-7.15.2-cp310-cp310-win32.whl", hash = "sha256:1268ac8fb9ddcd783d3948dbabaf80a5d53bfdaa0575e873e2139a692f797443"},
|
||||||
|
{file = "coverage-7.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:9f4432898c4bf2fba0435bbe35dd4437d7264565e5a88a21f5b49d8662a6b629"},
|
||||||
|
{file = "coverage-7.15.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f1ec6f304b156669cfde653b4e9a953f5de87e247ea02ac599bce0ab2744036"},
|
||||||
|
{file = "coverage-7.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d3361879d736f469f45723c11ea1a5bbdaf1f6928f0e632c940378b5aa9b660"},
|
||||||
|
{file = "coverage-7.15.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c6a98d698f9e2c8008d0370ec7fc452ebfcc530002ae2d0061170d768b992589"},
|
||||||
|
{file = "coverage-7.15.2-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50dd325e18ec25bfcc10cd7f99b04df1ab9ec76b0918c260e60817ad0643dee"},
|
||||||
|
{file = "coverage-7.15.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67d7602480a47bdf5b675635403625553ebaa70d5a62a657c035149fd401cea0"},
|
||||||
|
{file = "coverage-7.15.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cee0f89f4767a6057c8fbf168f8135f18be651300496086bd873e3189fed0487"},
|
||||||
|
{file = "coverage-7.15.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a29ec5305a7335aacee2d799e3422e91e1c8a12474986e2b3b07e315c91be82f"},
|
||||||
|
{file = "coverage-7.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:48ccc6395958eda89093ecdc35644c86f23a8b23a7f4d44958812b721aad67c1"},
|
||||||
|
{file = "coverage-7.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:81f382c5a94b434ec1f6da607edb904c76d7212e618cd4d1bc9f97bed4120ef5"},
|
||||||
|
{file = "coverage-7.15.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bbc808daf4f5cd567af8075ecc72d21c6dfef9a254709a621a84c217c935ebc0"},
|
||||||
|
{file = "coverage-7.15.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a4c46b247b5d4b78f613bd89fea926d32b25c6cc61a50bd1e99ba310348f3dad"},
|
||||||
|
{file = "coverage-7.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:094dd37f3ef7b2da8b068b583d1f4c40f91c65197e16c52a71962d5d537fc5db"},
|
||||||
|
{file = "coverage-7.15.2-cp311-cp311-win32.whl", hash = "sha256:a63b9e190711134d581c4d703df5df09851b1acf99792c7aacbbe9f41f0283c9"},
|
||||||
|
{file = "coverage-7.15.2-cp311-cp311-win_amd64.whl", hash = "sha256:8bb9f4b4279187560796a4cdaca3b0a93dd97e48ee667df005f4ed9a97403688"},
|
||||||
|
{file = "coverage-7.15.2-cp311-cp311-win_arm64.whl", hash = "sha256:8c726b232659cbd2ae57ade46509eb068c9bd7a06df9fcbff6fe484870006934"},
|
||||||
|
{file = "coverage-7.15.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1adac78e5abc7c5438f7a209c9ca69d06542f0bf481d728b6989ea80b813fdf9"},
|
||||||
|
{file = "coverage-7.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b868acc62aa5de3be7a9d05c2333bf8359ca987e43f9cb30ff8fbda6a024ab73"},
|
||||||
|
{file = "coverage-7.15.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6f6966fc30e6f06ca8f98fb0ce51eda6b111b3ee8d066a8b1ec9e77fa06ab55d"},
|
||||||
|
{file = "coverage-7.15.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:68af907f595ab01a78f794932ff3bdf929c316d3000810d38dbc247129e26f8b"},
|
||||||
|
{file = "coverage-7.15.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afa29e2eff3d5729267e2cb2fd4ce9d61c952932fb2694e34ccb5d9540c6a296"},
|
||||||
|
{file = "coverage-7.15.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbf44513ceb1589e31948e20eafbde9deaface90e1a1afa5f5f77b4423d17ce6"},
|
||||||
|
{file = "coverage-7.15.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9deddf09eecb717b7f980414b43d90a5b22ff3967d2949ab29cb0aa83d9e9098"},
|
||||||
|
{file = "coverage-7.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae901f7e55ba405c84ee1cab3d3e962e4e871e4a2bcb9c90911adbd69b42ac5a"},
|
||||||
|
{file = "coverage-7.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a0f47002c6eeb7c280228467a4cb0cc15ca2103a8421b986b2d3ec04a0f9bd8b"},
|
||||||
|
{file = "coverage-7.15.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1cd7a5beb7af3e864a13b1f0fb26efd3695da43ef0daf71e586adfffaf34d5b2"},
|
||||||
|
{file = "coverage-7.15.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:97a5c5457a9fb1d6c4e06cfb5dc835871fbfb6a6a51addc9e925bdeff5ef7440"},
|
||||||
|
{file = "coverage-7.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0901cfe6c13bcd2302da4f83e884555d2a22bda6e4c476f09ef204ba20ca536e"},
|
||||||
|
{file = "coverage-7.15.2-cp312-cp312-win32.whl", hash = "sha256:b171bdd71cb7ff792bf32e376173b0ace7e7963e7e57c58dfc42063a6a7174cd"},
|
||||||
|
{file = "coverage-7.15.2-cp312-cp312-win_amd64.whl", hash = "sha256:582edc45c2040543fef83341be23c43024a3ab3ae0c2d8bc498a06282905ad40"},
|
||||||
|
{file = "coverage-7.15.2-cp312-cp312-win_arm64.whl", hash = "sha256:a638db90c61cd219aeee65e83a24fdaa57269a741ae0cf773309208ac862cee3"},
|
||||||
|
{file = "coverage-7.15.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1121caa19159a38b5463eaae4b1e1fde81e525b15ecc5e000cd5b1a108f743a8"},
|
||||||
|
{file = "coverage-7.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a300c6934e0989c327b9e8a1e110329da4641149f872bbe9f70168be66da76c1"},
|
||||||
|
{file = "coverage-7.15.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2617f8799d268fabdeef42a7e89ac3a23e1deee9025427db2df970f99a89a578"},
|
||||||
|
{file = "coverage-7.15.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7dc2950a2992cd676d35c20ae63522836deeb034f08874699d14068710af3dc1"},
|
||||||
|
{file = "coverage-7.15.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9e36686f7a442185db2400b3df171aac520869faf9deb59df687d28659eda2a6"},
|
||||||
|
{file = "coverage-7.15.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d29ca7bd67af6e12e74632d65f026eabc1364da5c254494cd914446a28a3ef7"},
|
||||||
|
{file = "coverage-7.15.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:db9c8438057e5b0f6a22a0af99c0c1d26b57fbbdbd1be5861ddb8f897fcc3a2d"},
|
||||||
|
{file = "coverage-7.15.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:63022c4c8dec1d0342f05c3ede99842fe3d007689acc45e86f123a1746e4a026"},
|
||||||
|
{file = "coverage-7.15.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6c0be82b4d4aa5b2704e08518e2252f3e3d110164bcca826816801052e48a7aa"},
|
||||||
|
{file = "coverage-7.15.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4510fb9cdf6bb02dfa6af0be4a534b8102d086e22e4a33f8836df663da3d660d"},
|
||||||
|
{file = "coverage-7.15.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:42ec3d989421b174a2ab607c1539f24127ad362757b7f1c0c0d7a2993f7eb37b"},
|
||||||
|
{file = "coverage-7.15.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8f91bce78e32343af184c3b7fa28fcf5a9e2641f4b6623d392038f804939188"},
|
||||||
|
{file = "coverage-7.15.2-cp313-cp313-win32.whl", hash = "sha256:434e68d531858205895eb0d74b73d20b84260de426387d53c422a5acda2cf050"},
|
||||||
|
{file = "coverage-7.15.2-cp313-cp313-win_amd64.whl", hash = "sha256:26c3b04a6377fd7c09800921fa934e3a17c0020439cd59df73e73ae1d4b6a78c"},
|
||||||
|
{file = "coverage-7.15.2-cp313-cp313-win_arm64.whl", hash = "sha256:3ed010aa1b69cda8e827aabfca9866216c980e2dca82ab9a78c5f83689964c8b"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:40f633c5c5fc783732f6312280122e859538fa24461235597c13d803ea9a108a"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:075560438765b7a2ef43bf7aa7758661b53d889df47f062a31bda6c1ade553a2"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:25fd15dd40a0a2c51a500d664ca29053c09c3259d998407bf982b6e114696138"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b9a6367e4aff723e8ee8190836836124284e8fcd4265e307c844010cfa074f3f"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9854ca62c152874b2060772503535be2e8f53f70b8aaa7686b094888d872f984"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:913b6c56e110da40e035bbd168353bf7aaa2544a5eaccea5d98a4629aac156c7"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aaccad4129d735a8a4d526f26929894c9a4e8ef7034566f210b176749d6906e3"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a164b50081fc7357331c4024ef4d17b78ba325f8380d05f5a69599a7e05257ee"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bfd341ccf78128e72c094bc70cc25b3ef309c33c7c2c66ba3ed4309549e02de1"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1473b3ba8e7ee0f076117b1a72c23f579a2b9e2bb742f48a8d86ea27ca93f91a"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:17c432b5f73ad52ef46fb06019f6fa7c66ce381961cf0f7dfd1d3a4bd3a98145"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:77f0ef5011df53a4bd1b35211ab122287f8d9b8d7aa1c4553e5c2deb24b1d446"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314-win32.whl", hash = "sha256:f653e5d7248c1191ec988a85c72edeab46c3ff44f90639a4ed4874ec0be90243"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314-win_amd64.whl", hash = "sha256:9911f31aad8906abe337c271343485cf20df5e70df5d2f57f9f136e7b55f26bc"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314-win_arm64.whl", hash = "sha256:e38def96ad59853824c97953fdcd2c320a84ba3ce99b417db78af8bb6c3db635"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:835ec4e20b45f0a7f63ed78f94065aca00de033403df8377bfe8b9c6abc0a7be"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7466cc7ab6dc0db871d264bf99e8779f0917ee63d40730af0552f71535a6e072"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e370c12133095ff18432de8c044962be85a5a96d90c6fcbce8e17e76236d2328"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fe41909c9515c3bfdb5f02c4d1f857dba322d9a9a1178069b91eea77889df63a"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6aa28cfb6488e5453b5b762d65f73aa586380f6693a04d58078ce228a29b06c0"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bcc0aae933921d03096f53b0b03eeb702129fd406dee59f08d2efacc68681fa5"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7c63387e21ab21f512c69c9756a8c7dadd322c7275edb064064433c9a09c3743"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e55510bc98ae943cece9e667a6c0fe94c6a92913720dea34243657a17993d0c"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2ff08701be2d1556fc78b326c80a3e8042da09352ecb3819105f8e386c8a3071"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:38c9518b7103826c403a461544e3c2e77151e8676d06eaed85911a97e962584a"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:dee88b1ed88587abd8c0269a1fc1f4cc77f7750d1dfde2869e2a123af420e67d"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fbeeeecea279727f8ac16c8e1133ddfeee793e985c86ae343d6a5ce744eef8c"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314t-win32.whl", hash = "sha256:cb0fddaa6884be6aae36ced9544b5e90f7d5f03845a2853bf47a14953a4e8688"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314t-win_amd64.whl", hash = "sha256:77f091ea3a9cc611cd29f433565476bc1936c084ac8eee00ea0e7e70c27e4199"},
|
||||||
|
{file = "coverage-7.15.2-cp314-cp314t-win_arm64.whl", hash = "sha256:6fc448c377d6eeb00a47c673494bd9bae29280ca53987e1869e67ebedfe20658"},
|
||||||
|
{file = "coverage-7.15.2-py3-none-any.whl", hash = "sha256:eb6bcae8d1a9d305351ecb108232441d11c5cfe9de840a04388ba5d2db8d735c"},
|
||||||
|
{file = "coverage-7.15.2.tar.gz", hash = "sha256:3df60dc267f0a2ca23cb7a9ab1109c62b9335ffbf519fcfe167157c28c09b81d"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decorator"
|
name = "decorator"
|
||||||
@@ -304,7 +468,7 @@ version = "5.3.1"
|
|||||||
description = "Decorators for Humans"
|
description = "Decorators for Humans"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c"},
|
{file = "decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c"},
|
||||||
{file = "decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82"},
|
{file = "decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82"},
|
||||||
@@ -316,7 +480,7 @@ version = "0.7.1"
|
|||||||
description = "XML bomb protection for Python stdlib modules"
|
description = "XML bomb protection for Python stdlib modules"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
|
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
|
||||||
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
|
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
|
||||||
@@ -384,7 +548,7 @@ version = "2.2.1"
|
|||||||
description = "Get the currently executing AST node of a frame, and other information"
|
description = "Get the currently executing AST node of a frame, and other information"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017"},
|
{file = "executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017"},
|
||||||
{file = "executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4"},
|
{file = "executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4"},
|
||||||
@@ -419,92 +583,91 @@ standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[stand
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "greenlet"
|
name = "greenlet"
|
||||||
version = "3.5.3"
|
version = "3.5.4"
|
||||||
description = "Lightweight in-process concurrent programming"
|
description = "Lightweight in-process concurrent programming"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
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\""
|
|
||||||
files = [
|
files = [
|
||||||
{file = "greenlet-3.5.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c180d22d325fb613956b443c3c6f4406eb70e6defc70d3974da2a7b59e06f48c"},
|
{file = "greenlet-3.5.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:ac5bf81d79d2c8eeb2ef6359b2e1687a1e9ebf46c2b1f970da9a9255df51d190"},
|
||||||
{file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:483d08c11181c83a6ce1a7a61df0f624a208ec40817a3bb2302714592eee4f04"},
|
{file = "greenlet-3.5.4-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89f3738167bab8c1084b94e23023d41d247117ac149fa0fbcb5bd4cf6262b353"},
|
||||||
{file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1dae6e0091eae084317e411f047f0b7cb241c6db570f7c45fd6b900a274914ce"},
|
{file = "greenlet-3.5.4-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9a5e3406e3ed8125ae1a3b37c12f3434e2b1f0fa053197c5557895b4fb09606"},
|
||||||
{file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0f6ff50ff8dbd51fae9b37f4101648b04ea0df19b3f50ab2beb5061e7716a5c8"},
|
{file = "greenlet-3.5.4-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a2d614cb2372c7101a12ea8b96dd56f81c986d247c5a73db67063f3ed1ca4a52"},
|
||||||
{file = "greenlet-3.5.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcd2d72ccd70a1ec68ba6ef93e7fbb4420ef9997dabc7010d893bd4015e0bec"},
|
{file = "greenlet-3.5.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4ab9f0704bccf6d3b38e0d2130b7b33271cff11453690da074fa280c3aa8e8e7"},
|
||||||
{file = "greenlet-3.5.3-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:37bf9c538f5ae6e63d643f88dec37c0c83bdf0e2ebc62961dedcf458822f7b71"},
|
{file = "greenlet-3.5.4-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:188e4d142f243051d92a1f5c244a741da02dddc070a0620c842804d7b56d008c"},
|
||||||
{file = "greenlet-3.5.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:73f152c895e09907e0dbe24f6c2db37beb085cd63db91c3825a0fcd0064124a8"},
|
{file = "greenlet-3.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2cdaadc3d31445a8f782bde3cd37e49a2c2a9c6da6daf76a3e34c683b271a3c7"},
|
||||||
{file = "greenlet-3.5.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8bdb43e1a1d1873721acab2be99c5befd4d2044ddfd52e4d610801019880a702"},
|
{file = "greenlet-3.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:70bdfacdc183dac838b2a0aaff2dd6134a457c52fe68a9c6bbab435483d2b9df"},
|
||||||
{file = "greenlet-3.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:0909f9355a9f24845d3299f3112e266a06afb68302041989fd26bd68894933db"},
|
{file = "greenlet-3.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:69173331fbc5d64bfac0065d7e22c39cfcd089e9b18d125bdcd5079363b09616"},
|
||||||
{file = "greenlet-3.5.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:aca9b4ce85b152b5524ef7d88170efdff80dc0032aa8b75f9aaf7f3479ea95b4"},
|
{file = "greenlet-3.5.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e883de250e299654b1f1680f72a1a9f9ba62c9bd1bce84099c90657349a8dfbb"},
|
||||||
{file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f71be4920368fe1fabeeaa53d1e3548337e2b223d9565f8ad5e392a75ba23fc"},
|
{file = "greenlet-3.5.4-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32802705c2c1ff25e8237b3bdacf2594fa02be80af8a66703eb7853ea7e68686"},
|
||||||
{file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d77e67f65f98449e3fb83f795b5d0a8437aead2f874ca89c96576caf4be3af6"},
|
{file = "greenlet-3.5.4-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57aa201b351f7c7c75627c60d29e4d5b97a07d37efeb62b903466fca42c097d7"},
|
||||||
{file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e18619ba655ac05d78d80fc83cac4ba892bd6927b99e3b8237aee861aaacc8bb"},
|
{file = "greenlet-3.5.4-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9667862a2e38ad379f11b845daeda22c8989186def44f06962c9c4c05e556da7"},
|
||||||
{file = "greenlet-3.5.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8540f1e6205bd13ca0ce685581037219ca54a1b41a0a15d228c6c9b8ad5903d7"},
|
{file = "greenlet-3.5.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3fe76c2cac86b4f7a1e92865ac0a54384deb05c92986287c1a7110d9bd53071"},
|
||||||
{file = "greenlet-3.5.3-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:d27c0c653a60d9535f690226474a5cc1036a8b0d7b57504d1c4f89c44a07a80c"},
|
{file = "greenlet-3.5.4-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:ae53534b5dec0f4c2ec26f898f538dc8ea1ca3ef2927d597a9439e40a09da937"},
|
||||||
{file = "greenlet-3.5.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7ef56fe650f50575bf843acde967b9c567687f3c22340941a899b7bc56e956a8"},
|
{file = "greenlet-3.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1e1a4a684b16c45ba324e60b32a4386a87722bcb815d2a149d2182f9b401ca72"},
|
||||||
{file = "greenlet-3.5.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5121af01cf911e70056c00d4b46d5e9b5d1415550038573d744138bacb59e6b8"},
|
{file = "greenlet-3.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e849e6e139b9671adeac505f72fc05f4af7fd1921faef40295e214fc3b361b59"},
|
||||||
{file = "greenlet-3.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:0f41e4a05a3c0cb31b17023eff28dd111e1d16bf7d7d00406cd7df23f31398a7"},
|
{file = "greenlet-3.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:dc418cf4c873357964d6624445ed09472e50def990c65dd4e76fc3ba8cd9cef6"},
|
||||||
{file = "greenlet-3.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:ec6f1af59f6b5f3fc9678e2ea062d8377d22ac644f7844cb7a292910cf12ff44"},
|
{file = "greenlet-3.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:c38c902a0986eba1f6e7ba1ab39ad5195926abde90f3fe080e08212db62176da"},
|
||||||
{file = "greenlet-3.5.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:719757059f5a53fd0dde23f78cffeafcdd97b21c850ddb7ca684a3c1a1f122e2"},
|
{file = "greenlet-3.5.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3d66250e8b09f182ede05490998c818b5961f7a3640332d44c4927caec7bbfe4"},
|
||||||
{file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:efa9f765dd09f9d0cdac651ffdf631ee59ec5dc6ee7a73e0c012ba9c52fbdf5b"},
|
{file = "greenlet-3.5.4-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c90e930c9c192e5b3ee9fb8bcd920ea3926155e2e3ded39fc697323addecee17"},
|
||||||
{file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7faba15ac005376e02a0384504e0243be3370ce010296a44a820feb342b505ab"},
|
{file = "greenlet-3.5.4-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:791fdfeeb9c6e0c7b10fa151bf110d2a6974866f13dcb5b1c7efae698245893a"},
|
||||||
{file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5795cd1101371140551c645f2d408b8d3c01a5a29cf8a9bce6e759c983682d23"},
|
{file = "greenlet-3.5.4-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b7c895310363f310361e0fe2072af85269d2a2a285cd04c0c59e79a5e3670dcf"},
|
||||||
{file = "greenlet-3.5.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87142215824be6ac05e2e8e2786eec307ccbc27c36723c3881959df654af6861"},
|
{file = "greenlet-3.5.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f88193799d43dbf8c8a806d6405c9c52fe2af40bf75072a606357b33cc336c7f"},
|
||||||
{file = "greenlet-3.5.3-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:af4923b3096e26a36d7e9cf24ab88083a20f97d191e3b97f253731ce9b41b28c"},
|
{file = "greenlet-3.5.4-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:13b980043cb1b3134e81ea469da1250ddcc6bfe6d245bbaa59168d9cdc8f228f"},
|
||||||
{file = "greenlet-3.5.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215275b1b49320987352e6c1b054acca0064f965a2c66992bed9a6f7d913f149"},
|
{file = "greenlet-3.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b7a5f095767c4493afcd06067f2bb3b8716e3f3f9e92b99c88e7e99f885b3d4d"},
|
||||||
{file = "greenlet-3.5.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b1b0eed82364b0e32c4ea0f221452d33e6bb17ae094d9f72aed9851812747ea"},
|
{file = "greenlet-3.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:42afdc1ab5f66da8c586c32af9224a74a706b4f0ea0dc3a4188a0860a09c65c9"},
|
||||||
{file = "greenlet-3.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:cde8adafa2365676f74a979744629589999093bc86e2484214f58e61df08902c"},
|
{file = "greenlet-3.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:60149df8f462d1b230038e6590c23c3b4768bb5d6c022b3b6e82532b34b0b8a3"},
|
||||||
{file = "greenlet-3.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:c4e7b79d83805475f0102008843f6eb45fd3bb0b2e88c774adab5fbaab27117d"},
|
{file = "greenlet-3.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:77d6ce04fed0d9aeed42e0f37923cc43eba9b027bdd9c34546bb4ccd143d0fe0"},
|
||||||
{file = "greenlet-3.5.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c8d87c2134d871df96ecdea9cec7cbaab286dadab0f56476e57aaf9e8ac11550"},
|
{file = "greenlet-3.5.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:24e61b88cb7e1b1d794b32a10cc346ac779681d6d74ff137a3e0a444d2bf1f02"},
|
||||||
{file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2d185dd1621757e70c3861cceffd5317ab4e7ed7eb09c82994828468527ade5"},
|
{file = "greenlet-3.5.4-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:870d730fec833f5a06906a32596cc099b9161594642a92a520b7a88911c95356"},
|
||||||
{file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1c514a468149bf8fbbab874188a3535cd8a48a3e353eb53a3d424296f8dbacd3"},
|
{file = "greenlet-3.5.4-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec5ff0d1878df6af3bf9b638a5a92a7d5693291de77c91bff10fa48519c604ef"},
|
||||||
{file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ad04dd75458c6300b047c61b8639092433d205a25a14e310d6582a480efcca1"},
|
{file = "greenlet-3.5.4-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07bd44616608d873d06735b63ef1a88191d6ca57c8d291d6559c71bc14c0893c"},
|
||||||
{file = "greenlet-3.5.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:915f887cf2682b66419b879423a2e072634aa7b7dce6f3ada4957cfced3f1e9a"},
|
{file = "greenlet-3.5.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d84d993f6e575c950d91a23c1345d18fe1a4310d447bf630849d7809196b52f0"},
|
||||||
{file = "greenlet-3.5.3-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:afaabdd554cd7ae9bbb3ca070b0d7fdfd207dbf1d16865f7233837709d354bda"},
|
{file = "greenlet-3.5.4-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:3529a8a933582ad19e224792cac7372489526576b75b4c124e8e4f29948f4861"},
|
||||||
{file = "greenlet-3.5.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:766cfd421c13e450feb340cd472a3ed9957d438727b7b4593ad7c76c5d2b0deb"},
|
{file = "greenlet-3.5.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:58023945f421093de5e6fa108c0985a8659d43f49e0216da25099369a121bcbd"},
|
||||||
{file = "greenlet-3.5.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2ecda9ec22edf38fa389369eaed8c3d37c05f3c54e69f69438dbb2cc1de1458b"},
|
{file = "greenlet-3.5.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bae2728e1897aa8df8cb1af38cd48b3a743aefe29372de7b8b7a9f532501e69f"},
|
||||||
{file = "greenlet-3.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:c82304750f057167ff60d188df1d0cc1764ce9567eadf03e6a7443bcedd0b30b"},
|
{file = "greenlet-3.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:ca5726c0b08ca35ae873557266a78b2c3f3b2b7d7401aa5ff886c2045dd0111c"},
|
||||||
{file = "greenlet-3.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:dc133a1569ee667b2a6ef56ce551084aeefd87a5acbc4736d336d1e2edc6cfc4"},
|
{file = "greenlet-3.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:7c1303791d603080cac6fc3b34df51c3b75b723739c282c8029e48a0d241672f"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:fd2e02fa07485778536a036222d616ab957b1d533f36b3ed98ce725d9c9d3117"},
|
{file = "greenlet-3.5.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7e8afa5eac028f8140ceafe5ceec66e6aa127ddcb21452d2a564dcd2900b5f22"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df0a0628d1597eb0897b62f55d1343f772405fd25f3b2a796c76874b0c2e22e8"},
|
{file = "greenlet-3.5.4-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73b37afe369021423ea53dd3123e04bffa7e93ac64429b9f50835b2e4fcae7cf"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ebd933a6adabc298bab47731a130fe6bfb888bd934eee37810f151159544540d"},
|
{file = "greenlet-3.5.4-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ef964f56dfcb6f9bbef2a190d9126795eac408716aeae47b5e7c73c32aafca9"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d19fe6c39ebff9259f07bcc685d3290f8fa4ea2278e51dd0008e4d6b0f2d814"},
|
{file = "greenlet-3.5.4-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cef589bc65fae02d10bca2ac341191c5b33acc2967892ebf4fcbd10eabb7a74c"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b9d501b40e80b70e32323c799dd9b420a5577a9601469d362ae1ffb690f3a7c"},
|
{file = "greenlet-3.5.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c53ff01a5c53a40f2c16820ebc56d7c61a77f5fbe009dadd96292d5682f80f8"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:962c5df2db8cb446da51edf1ca5296c389d93b99c9d8aa2ee4c7d0d8f1218260"},
|
{file = "greenlet-3.5.4-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:dfc41ae893d9ceaf22c824f2153a88b30651b20e8758c2cd9ac143f23640563c"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a1fad1d11e7d6aab184107baa8e4ece11ccba3ec9599cd7efa5ff4d70d43256a"},
|
{file = "greenlet-3.5.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecca4d80d55a01ad6b23b33262662956149fbb7b2c6be2910f1705921958cbf3"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fad5aec764399f1b5cc347ad250a59660f20c8f8888ea6bae1f93b769cce1154"},
|
{file = "greenlet-3.5.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffbc533e0eaf8e80d8471411646ab88fe58f641d508c0b02b24494479f4d9ec"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:7669aa24cf2a1041d6f7899575b494a3ab4cf68bfcc8609b1dc0be7272db835e"},
|
{file = "greenlet-3.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:305f69e6c4523d7f6979ed001cff4e5853c063e5da04880296603aa0227e544c"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:5b4807c4082c9d1b6d9eed56fcd041863e37f2228106eef24c30ca096e238605"},
|
{file = "greenlet-3.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:f260930bbbbcf9caee661211235a5111c86dfe5832fdf6ae4570da1e0995320f"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:271a8ea7c1024e8a0d7dd2be66dd66dda8a07193f41a17b9e924f7600f5b62be"},
|
{file = "greenlet-3.5.4-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:41ddab54e4b238f4a6c323f39b4e59e176affd5a94d461a9fb7583dac74240a3"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19131729ae0ddc3c2e1ef85e650169b5e37ee32e400f215f78b94d7b0d567310"},
|
{file = "greenlet-3.5.4-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3dabe3e2809013052c68bdf0b7fa5f5f2859c43a80803131ad61af9cabd7867"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1540dd8e5fc2a5aec40fbb98ef8e149fa47c89a4b4a1cf2575a14d3d1869d7a8"},
|
{file = "greenlet-3.5.4-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:27d3f00718634d4520a3a150154ac5da36f257869d41321953375b90bfbbc72c"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b897d97759425953f69a9c0fac67f8fe333ec0ce7377ef186fb2b0c3ad5e354d"},
|
{file = "greenlet-3.5.4-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39169a11d87a6a263afda3e9a27d1df16d0f919d40a4837cc73986c9884c0dd8"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e81fa194a1d20967877bdf9c7794db2bc99063e5be36aee710c08f04c5bb087f"},
|
{file = "greenlet-3.5.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbd60b5763c6543c1827e48faaf14ea9bfbad245f52b1a4d76a2a2d8884c6c66"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:3236754d423955ea08e9bb5f6c04a7895f9e22c290b66aa7653fcb922d839eb0"},
|
{file = "greenlet-3.5.4-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:bd3d1145f603b2db19feb9078c2e6855eb7c67e15580c010ed815cee519b86fd"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:55cf4d777485d43110e47133cbba6d74a8885a87ec1227ef0267f9ee80c5aa21"},
|
{file = "greenlet-3.5.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f00f910f0e7b35416c63b23ad78b769aeccfc1775f712b43c4ee525624a2eef7"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:12a248ba75f6a9a236375f52296c498c89ff1d8badf32deb9eca7abd5853f7da"},
|
{file = "greenlet-3.5.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:91c26423753b92caf41ab3f98fd547d7374d4d9fc2d85be041886c1579d9255e"},
|
||||||
{file = "greenlet-3.5.3-cp314-cp314t-win_amd64.whl", hash = "sha256:efc6bd60ea02e085862c74a3ef64b147ffc6f1a5ea7d9f26e7a939943f68c1e3"},
|
{file = "greenlet-3.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:ee032b91fd8ec29ec6c4cea2b8c561b178435134bd0752c7334b94e9c736c132"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:ea03f2f04367845d6b58eeed276e1e56e51f0b97d8ad5a88a7d20a91dc9056cc"},
|
{file = "greenlet-3.5.4-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:178111881dd7a6c946471fda85485ec796e1043c2b939f694b096e2ecf986809"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78dbef602fda6d97d957eb7937f70c9ce9e9527330347f8f6b6f9e554a9e7a47"},
|
{file = "greenlet-3.5.4-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d92df08dd65fede97fc37aad36c2e9dcda3b31c467f8e0c2c096456cb818e927"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f73857adb8fee13fa56c172bd11262f888c0c648f9fea113e777bb2c7904a81"},
|
{file = "greenlet-3.5.4-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:99e8f8c4ebc4fd80aa26c1280ae9ad43a0976e786349703a181cf0bae60413e5"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cefa9cef4b371f9844c6053db71f1138bc6807bab1578b0dae5149c1f1141357"},
|
{file = "greenlet-3.5.4-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1f17e362d78e37559e0506c5a7d066bdd45073c36a0127a543e8a0df27242ff3"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:232fec92e823addaf02d9472cf7381e24a1d046a6ced1103c5caa4c21b9dfc1d"},
|
{file = "greenlet-3.5.4-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:394de08dad5ffcb1f50c2159d93e398d9d2da3ed437645eaa54771fa720db9f0"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:6219b6d04dbf6ba6084d77dc609e8473060dc55f759cbf626d512122781fa128"},
|
{file = "greenlet-3.5.4-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:cd320d998cbaa032932830448e39abf3c6a12901295e386e8114db926e10cffb"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:2421c3564da9429d5586d46ca31ebb26516b5498a802cf65c041a8e8a8980d34"},
|
{file = "greenlet-3.5.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:c883d61f2282d72c767a14936641b3efcbde9d82f1080712aaea0b1d3126cb88"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e0f0d160f0b2e558e6c75f7930967183255dc9735e5f5b8cae58ee09c9576d8b"},
|
{file = "greenlet-3.5.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2a924f15d17957e252a810acefcb5942f5ca712298e8b6fcaed9a307d357522c"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315-win_amd64.whl", hash = "sha256:dd99329bbc15ca78dcc583dba05d0b1b0bae01ab6c2174989f5aaee3e41ac930"},
|
{file = "greenlet-3.5.4-cp315-cp315-win_amd64.whl", hash = "sha256:ed17e5f3420360d5b459de8462efb52060399a5326a613d4cde31cef63ef95da"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315-win_arm64.whl", hash = "sha256:499fef2acede88c1864a57bb586b4bf533c81e1b82df7ab93451cdb47dfec227"},
|
{file = "greenlet-3.5.4-cp315-cp315-win_arm64.whl", hash = "sha256:f908898d6fa484ce4b6f447ce70ea99b52c503fee419e53cf74d60a16bc9e667"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:176bc16a721fa5fc294d70b87b4dfa5fbdd251b3da5d5372735ecef9bd7d6d0c"},
|
{file = "greenlet-3.5.4-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:1833637f17d5e7472548a48575c394fe39f1b1890d676d162d86593610f44d8c"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:629b614d2b786e89c50440e246f33eea78f58a962d0bdbbcc809e6d13605903f"},
|
{file = "greenlet-3.5.4-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12cda9122e03341f1cb6b8207a19d7a9d375e52f1b4e9243918375f40fd7b4b9"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b2e857ae16f5f72142edf75f9f176fe7526ba19a2841df1420516f83831c9f2"},
|
{file = "greenlet-3.5.4-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d83ae0e32d14957ab7170785a20f582635c8474deab1bfbb552b17e769a6ce25"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:16d192579ed281051396dddd7f7754dac6259e6b1fb26378c87b66622f8e3f91"},
|
{file = "greenlet-3.5.4-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:123aa379c962ed5fe90a880327e0c3066124ac64ec99e12a238be9fd8eb3db3d"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e515757e2e36bcbf1fad09a46e1557e8b1ae1797d4b44d09da7deed88ad28608"},
|
{file = "greenlet-3.5.4-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f1467de1bb767f75db0aa34c195e3a496d8d1278c796e70c24ce205d3e99cde"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:4399eb8d041f20b68d943918bc55502a93d6fdc0a37c14da7881c04139acee9d"},
|
{file = "greenlet-3.5.4-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:adf2244d7f69409925a8f22ed22cc5f93cdfe5c9dc87ff3476be2c2aaae61a05"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:b363d46ed1ea431825fdb01471bb024fc08399bad1572a616e853c7684415adb"},
|
{file = "greenlet-3.5.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0fa53040b78b578120eecdc0265e3f1051487cc425d11a2b7c761daadf4feaa8"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:e44da2f5bbdaabaf7d80b73dbb430c7035771e9f244e3c8b769715c9d8fa0a16"},
|
{file = "greenlet-3.5.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:60e0bc961d367df506660e9ac0177a76bc6d81305300704b0977d1634f76efe2"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315t-win_amd64.whl", hash = "sha256:8ff8bed3e3baa20a3ea261ce00526f1898ad4801d4886fd2220580ee0ad8fadf"},
|
{file = "greenlet-3.5.4-cp315-cp315t-win_amd64.whl", hash = "sha256:f680e549edb3eaf21eea4e7fe101e15ec180c74b7879ab46adc080f22d4015d2"},
|
||||||
{file = "greenlet-3.5.3-cp315-cp315t-win_arm64.whl", hash = "sha256:b7068bd09f761f3f5b4d214c2bed063186b2a86148c740b3873e3f56d79bac31"},
|
{file = "greenlet-3.5.4-cp315-cp315t-win_arm64.whl", hash = "sha256:08fc36de8442d5c3e95b044550dbea9bf144d31ec0cc58e36fb241cb6ef6a994"},
|
||||||
{file = "greenlet-3.5.3.tar.gz", hash = "sha256:a61efc018fd3eb317eeca31aba90ee9e7f26f22884a79b6c6ec715bf71bb62f1"},
|
{file = "greenlet-3.5.4.tar.gz", hash = "sha256:0232ae1de90a8e07867bb127d7a6ba2301e859145489f25cda8a6096dabe1d20"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
@@ -540,19 +703,41 @@ version = "0.16.0"
|
|||||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
|
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
|
||||||
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
|
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore"
|
||||||
|
version = "1.0.9"
|
||||||
|
description = "A minimal low-level HTTP client."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
|
||||||
|
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
certifi = "*"
|
||||||
|
h11 = ">=0.16"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||||
|
http2 = ["h2 (>=3,<5)"]
|
||||||
|
socks = ["socksio (==1.*)"]
|
||||||
|
trio = ["trio (>=0.22.0,<1.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpie"
|
name = "httpie"
|
||||||
version = "3.2.4"
|
version = "3.2.4"
|
||||||
description = "HTTPie: modern, user-friendly command-line HTTP client for the API era."
|
description = "HTTPie: modern, user-friendly command-line HTTP client for the API era."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "httpie-3.2.4-py3-none-any.whl", hash = "sha256:4bd0435cc4b9bca59501bc65089de96f3e93b393803f32a81951db62050ebf0b"},
|
{file = "httpie-3.2.4-py3-none-any.whl", hash = "sha256:4bd0435cc4b9bca59501bc65089de96f3e93b393803f32a81951db62050ebf0b"},
|
||||||
{file = "httpie-3.2.4.tar.gz", hash = "sha256:302ad436c3dc14fd0d1b19d4572ef8d62b146bcd94b505f3c2521f701e2e7a2a"},
|
{file = "httpie-3.2.4.tar.gz", hash = "sha256:302ad436c3dc14fd0d1b19d4572ef8d62b146bcd94b505f3c2521f701e2e7a2a"},
|
||||||
@@ -574,13 +759,39 @@ setuptools = "*"
|
|||||||
dev = ["Jinja2", "flake8", "flake8-comprehensions", "flake8-deprecated", "flake8-mutable", "flake8-tuple", "pyopenssl", "pytest", "pytest-cov", "pytest-httpbin (>=0.0.6)", "pytest-mock", "pyyaml", "responses", "twine", "werkzeug (<2.1.0)", "wheel"]
|
dev = ["Jinja2", "flake8", "flake8-comprehensions", "flake8-deprecated", "flake8-mutable", "flake8-tuple", "pyopenssl", "pytest", "pytest-cov", "pytest-httpbin (>=0.0.6)", "pytest-mock", "pyyaml", "responses", "twine", "werkzeug (<2.1.0)", "wheel"]
|
||||||
test = ["pytest", "pytest-httpbin (>=0.0.6)", "pytest-mock", "responses", "werkzeug (<2.1.0)"]
|
test = ["pytest", "pytest-httpbin (>=0.0.6)", "pytest-mock", "responses", "werkzeug (<2.1.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.28.1"
|
||||||
|
description = "The next generation HTTP client."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
|
||||||
|
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
anyio = "*"
|
||||||
|
certifi = "*"
|
||||||
|
httpcore = "==1.*"
|
||||||
|
idna = "*"
|
||||||
|
socksio = {version = "==1.*", optional = true, markers = "extra == \"socks\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
|
||||||
|
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||||
|
http2 = ["h2 (>=3,<5)"]
|
||||||
|
socks = ["socksio (==1.*)"]
|
||||||
|
zstd = ["zstandard (>=0.18.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.18"
|
version = "3.18"
|
||||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"},
|
{file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"},
|
||||||
{file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"},
|
{file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"},
|
||||||
@@ -595,7 +806,7 @@ version = "2.3.0"
|
|||||||
description = "brain-dead simple config-ini parsing"
|
description = "brain-dead simple config-ini parsing"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
|
{file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
|
||||||
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
|
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
|
||||||
@@ -607,7 +818,7 @@ version = "9.15.0"
|
|||||||
description = "IPython: Productive Interactive Computing"
|
description = "IPython: Productive Interactive Computing"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.11"
|
python-versions = ">=3.11"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "ipython-9.15.0-py3-none-any.whl", hash = "sha256:515ad9c3cdf0c932a5a9f6245419e8aba706b7bd03c3e1d3a1c83d9351d6aa6e"},
|
{file = "ipython-9.15.0-py3-none-any.whl", hash = "sha256:515ad9c3cdf0c932a5a9f6245419e8aba706b7bd03c3e1d3a1c83d9351d6aa6e"},
|
||||||
{file = "ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756"},
|
{file = "ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756"},
|
||||||
@@ -640,7 +851,7 @@ version = "1.1.1"
|
|||||||
description = "Defines a variety of Pygments lexers for highlighting IPython code."
|
description = "Defines a variety of Pygments lexers for highlighting IPython code."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"},
|
{file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"},
|
||||||
{file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"},
|
{file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"},
|
||||||
@@ -655,7 +866,7 @@ version = "0.20.0"
|
|||||||
description = "An autocompletion tool for Python that can be used for text editors."
|
description = "An autocompletion tool for Python that can be used for text editors."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67"},
|
{file = "jedi-0.20.0-py2.py3-none-any.whl", hash = "sha256:7bdd9c2634f56713299976f4cbd59cb3fa92165cc5e05ea811fb253480728b67"},
|
||||||
{file = "jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011"},
|
{file = "jedi-0.20.0.tar.gz", hash = "sha256:c3f4ccbd276696f4b19c54618d4fb18f9fc24b0aef02acf704b23f487daa1011"},
|
||||||
@@ -694,7 +905,7 @@ version = "4.2.0"
|
|||||||
description = "Python port of markdown-it. Markdown parsing, done right!"
|
description = "Python port of markdown-it. Markdown parsing, done right!"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"},
|
{file = "markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"},
|
||||||
{file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"},
|
{file = "markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49"},
|
||||||
@@ -817,7 +1028,7 @@ version = "0.2.2"
|
|||||||
description = "Inline Matplotlib backend for Jupyter"
|
description = "Inline Matplotlib backend for Jupyter"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6"},
|
{file = "matplotlib_inline-0.2.2-py3-none-any.whl", hash = "sha256:3c821cf1c209f59fb2d2d64abbf5b23b67bcb2210d663f9918dd851c6da1fcf6"},
|
||||||
{file = "matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79"},
|
{file = "matplotlib_inline-0.2.2.tar.gz", hash = "sha256:72f3fe8fce36b70d4a5b612f899090cd0401deddc4ea90e1572b9f4bfb058c79"},
|
||||||
@@ -835,7 +1046,7 @@ version = "0.1.2"
|
|||||||
description = "Markdown URL utilities"
|
description = "Markdown URL utilities"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
|
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
|
||||||
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
||||||
@@ -847,7 +1058,7 @@ version = "6.7.1"
|
|||||||
description = "multidict implementation"
|
description = "multidict implementation"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"},
|
{file = "multidict-6.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c93c3db7ea657dd4637d57e74ab73de31bccefe144d3d4ce370052035bc85fb5"},
|
||||||
{file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"},
|
{file = "multidict-6.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:974e72a2474600827abaeda71af0c53d9ebbc3c2eb7da37b37d7829ae31232d8"},
|
||||||
@@ -1057,7 +1268,7 @@ version = "26.2"
|
|||||||
description = "Core utilities for Python packages"
|
description = "Core utilities for Python packages"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"},
|
{file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"},
|
||||||
{file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"},
|
{file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"},
|
||||||
@@ -1161,7 +1372,7 @@ version = "0.8.7"
|
|||||||
description = "A Python Parser"
|
description = "A Python Parser"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c"},
|
{file = "parso-0.8.7-py2.py3-none-any.whl", hash = "sha256:a8926eb2a1b915486941fdbd31e86a4baf88fe8c210f25f2f35ecec5b574ca1c"},
|
||||||
{file = "parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1"},
|
{file = "parso-0.8.7.tar.gz", hash = "sha256:eaaac4c9fdd5e9e8852dc778d2d7405897ec510f2a298071453e5e3a07914bb1"},
|
||||||
@@ -1177,7 +1388,7 @@ version = "4.9.0"
|
|||||||
description = "Pexpect allows easy control of interactive console applications."
|
description = "Pexpect allows easy control of interactive console applications."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""
|
markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""
|
||||||
files = [
|
files = [
|
||||||
{file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
|
{file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
|
||||||
@@ -1193,7 +1404,7 @@ version = "26.1.2"
|
|||||||
description = "The PyPA recommended tool for installing Python packages."
|
description = "The PyPA recommended tool for installing Python packages."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab"},
|
{file = "pip-26.1.2-py3-none-any.whl", hash = "sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab"},
|
||||||
{file = "pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605"},
|
{file = "pip-26.1.2.tar.gz", hash = "sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605"},
|
||||||
@@ -1205,7 +1416,7 @@ version = "1.6.0"
|
|||||||
description = "plugin and hook calling mechanisms for python"
|
description = "plugin and hook calling mechanisms for python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
|
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
|
||||||
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
|
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
|
||||||
@@ -1221,7 +1432,7 @@ version = "3.0.52"
|
|||||||
description = "Library for building powerful interactive command lines in Python"
|
description = "Library for building powerful interactive command lines in Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"},
|
{file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"},
|
||||||
{file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"},
|
{file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"},
|
||||||
@@ -1236,7 +1447,7 @@ version = "7.2.2"
|
|||||||
description = "Cross-platform lib for process and system monitoring."
|
description = "Cross-platform lib for process and system monitoring."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
markers = "sys_platform != \"emscripten\" and sys_platform != \"cygwin\""
|
markers = "sys_platform != \"emscripten\" and sys_platform != \"cygwin\""
|
||||||
files = [
|
files = [
|
||||||
{file = "psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b"},
|
{file = "psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b"},
|
||||||
@@ -1272,7 +1483,7 @@ version = "0.7.0"
|
|||||||
description = "Run a subprocess in a pseudo terminal"
|
description = "Run a subprocess in a pseudo terminal"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""
|
markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""
|
||||||
files = [
|
files = [
|
||||||
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
|
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
|
||||||
@@ -1285,7 +1496,7 @@ version = "0.2.3"
|
|||||||
description = "Safely evaluate AST nodes without side effects"
|
description = "Safely evaluate AST nodes without side effects"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"},
|
{file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"},
|
||||||
{file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"},
|
{file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"},
|
||||||
@@ -1492,7 +1703,7 @@ version = "2.20.0"
|
|||||||
description = "Pygments is a syntax highlighting package written in Python."
|
description = "Pygments is a syntax highlighting package written in Python."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
|
{file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
|
||||||
{file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
|
{file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
|
||||||
@@ -1507,7 +1718,7 @@ version = "1.7.1"
|
|||||||
description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
|
description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"},
|
{file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"},
|
||||||
{file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"},
|
{file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"},
|
||||||
@@ -1520,7 +1731,7 @@ version = "9.1.1"
|
|||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"},
|
{file = "pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"},
|
||||||
{file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"},
|
{file = "pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313"},
|
||||||
@@ -1536,6 +1747,63 @@ pygments = ">=2.7.2"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
|
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-asyncio"
|
||||||
|
version = "1.4.0"
|
||||||
|
description = "Pytest support for asyncio"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1"},
|
||||||
|
{file = "pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pytest = ">=8.4,<10"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)", "sphinx-tabs (>=3.5)"]
|
||||||
|
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-cov"
|
||||||
|
version = "7.1.0"
|
||||||
|
description = "Pytest plugin for measuring coverage."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"},
|
||||||
|
{file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
coverage = {version = ">=7.10.6", extras = ["toml"]}
|
||||||
|
pluggy = ">=1.2"
|
||||||
|
pytest = ">=7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
testing = ["process-tests", "pytest-xdist", "virtualenv"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-mock"
|
||||||
|
version = "3.15.1"
|
||||||
|
description = "Thin-wrapper around the mock package for easier use with pytest"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"},
|
||||||
|
{file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pytest = ">=6.2.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["pre-commit", "pytest-asyncio", "tox"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
version = "2.9.0.post0"
|
version = "2.9.0.post0"
|
||||||
@@ -1607,7 +1875,7 @@ version = "2.34.2"
|
|||||||
description = "Python HTTP for Humans."
|
description = "Python HTTP for Humans."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"},
|
{file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"},
|
||||||
{file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"},
|
{file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"},
|
||||||
@@ -1624,13 +1892,31 @@ urllib3 = ">=1.26,<3"
|
|||||||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"]
|
use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests-async"
|
||||||
|
version = "0.2.4"
|
||||||
|
description = "Simple async HTTP client with requests-like interface, powered by httpx"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "requests_async-0.2.4-py3-none-any.whl", hash = "sha256:a967efb84eb03a2053a847b1bfbac5917df8100d101368d883e20ae9ab15f7a2"},
|
||||||
|
{file = "requests_async-0.2.4.tar.gz", hash = "sha256:7eaa42cbfe4d0f1a5f1ef78625c625248cac5c5323afcbd76e7a2a26b85b56b9"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
httpx = {version = ">=0.23.0", extras = ["socks"]}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["pytest (>=7.0.0)", "pytest-asyncio (>=0.21.0)", "pytest-cov (>=4.0.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests-toolbelt"
|
name = "requests-toolbelt"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
description = "A utility belt for advanced users of python-requests"
|
description = "A utility belt for advanced users of python-requests"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
|
{file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"},
|
||||||
{file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
|
{file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"},
|
||||||
@@ -1645,7 +1931,7 @@ version = "15.0.0"
|
|||||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9.0"
|
python-versions = ">=3.9.0"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"},
|
{file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"},
|
||||||
{file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"},
|
{file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"},
|
||||||
@@ -1695,7 +1981,7 @@ version = "83.0.0"
|
|||||||
description = "Most extensible Python build backend with support for C/C++ extension modules"
|
description = "Most extensible Python build backend with support for C/C++ extension modules"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3"},
|
{file = "setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3"},
|
||||||
{file = "setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef"},
|
{file = "setuptools-83.0.0.tar.gz", hash = "sha256:025bccbbf0fa05b6192bc64ae1e7b16e001fd6d6d4d5de03c97b1c1ade523bef"},
|
||||||
@@ -1722,6 +2008,18 @@ files = [
|
|||||||
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
|
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socksio"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Sans-I/O implementation of SOCKS4, SOCKS4A, and SOCKS5."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3"},
|
||||||
|
{file = "socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlalchemy"
|
name = "sqlalchemy"
|
||||||
version = "2.0.51"
|
version = "2.0.51"
|
||||||
@@ -1825,7 +2123,7 @@ version = "0.6.3"
|
|||||||
description = "Extract data from python stack frames and tracebacks for informative displays"
|
description = "Extract data from python stack frames and tracebacks for informative displays"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
|
{file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
|
||||||
{file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
|
{file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
|
||||||
@@ -1863,7 +2161,7 @@ version = "5.15.1"
|
|||||||
description = "Traitlets Python configuration system"
|
description = "Traitlets Python configuration system"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92"},
|
{file = "traitlets-5.15.1-py3-none-any.whl", hash = "sha256:770a53705f84b81ac107e83a1b3328ff2dae16094d8fc3cfc004e4b22dfd8e92"},
|
||||||
{file = "traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722"},
|
{file = "traitlets-5.15.1.tar.gz", hash = "sha256:7b1c07854fe25acb39e009bae49f11b79ff6cbb2f27999104e9110e7a6b53722"},
|
||||||
@@ -1919,7 +2217,7 @@ version = "2.7.0"
|
|||||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"},
|
{file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"},
|
||||||
{file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"},
|
{file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"},
|
||||||
@@ -1956,7 +2254,7 @@ version = "0.8.2"
|
|||||||
description = "Measures the displayed width of unicode strings in a terminal"
|
description = "Measures the displayed width of unicode strings in a terminal"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85"},
|
{file = "wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85"},
|
||||||
{file = "wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda"},
|
{file = "wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda"},
|
||||||
@@ -1965,4 +2263,4 @@ files = [
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.13"
|
python-versions = ">=3.13"
|
||||||
content-hash = "22aa39f5ef7ad5d2415b44f3908af10f00430ba0b9be5071910d691fa9a9cb34"
|
content-hash = "e1edf156eafb0e85fb3c090f3f17595ef56216f6bb9a2881f553619a19d83f3f"
|
||||||
|
|||||||
+27
-3
@@ -8,9 +8,9 @@ authors = [
|
|||||||
license = "MH.Dmitrii's project"
|
license = "MH.Dmitrii's project"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"alembic (>=1.18.5,<2.0.0)",
|
"alembic (>=1.18.5,<2.0.0)",
|
||||||
"pytest (>=9.1.1,<10.0.0)",
|
|
||||||
"uvicorn (>=0.51.0,<0.52.0)",
|
"uvicorn (>=0.51.0,<0.52.0)",
|
||||||
"gunicorn (>=26.0.0,<27.0.0)",
|
"gunicorn (>=26.0.0,<27.0.0)",
|
||||||
"fastapi (>=0.139.1,<0.140.0)",
|
"fastapi (>=0.139.1,<0.140.0)",
|
||||||
@@ -21,11 +21,35 @@ dependencies = [
|
|||||||
"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)",
|
||||||
"ipython (>=9.15.0,<10.0.0)",
|
"aiosqlite (>=0.22.1,<0.23.0)",
|
||||||
"httpie (>=3.2.4,<4.0.0)"
|
"greenlet (>=3.5.4,<4.0.0)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
pytest = ">=9.1.1,<10.0.0"
|
||||||
|
pytest-cov = ">=7.1.0,<8.0.0"
|
||||||
|
pytest-mock = ">=3.15.1,<4.0.0"
|
||||||
|
allure-pytest = ">=2.16.0,<3.0.0"
|
||||||
|
ipython = ">=9.15.0,<10.0.0"
|
||||||
|
httpie = ">=3.2.4,<4.0.0"
|
||||||
|
pytest-asyncio = ">=1.4.0,<2.0.0"
|
||||||
|
requests-async = ">=0.2.4,<0.3.0"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
omit = [
|
||||||
|
# "*/models/*",
|
||||||
|
"*/migrations/*",
|
||||||
|
"*/database/*",
|
||||||
|
"*/errors/*",
|
||||||
|
"__init__.py",
|
||||||
|
"*/docker/*"
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
exclude_lines = [
|
||||||
|
"pragma: no cover",
|
||||||
|
]
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
[pytest]
|
||||||
|
addopts =
|
||||||
|
-l
|
||||||
|
-v
|
||||||
|
-s
|
||||||
|
--alluredir=tests/allure-results/reports/
|
||||||
|
testpaths =
|
||||||
|
tests
|
||||||
|
markers=
|
||||||
|
unit: unit tests
|
||||||
|
integra: integrations test
|
||||||
|
e2e: e2e tests
|
||||||
|
smoke: smoke tests
|
||||||
|
|
||||||
|
asyncio_mode = auto
|
||||||
|
asyncio_default_fixture_loop_scope = function
|
||||||
@@ -1,62 +1,67 @@
|
|||||||
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from src.models.database_models.model import engine, RefreshTokens
|
|
||||||
from sqlalchemy import and_, not_, select
|
from sqlalchemy import and_, not_, select, update
|
||||||
from sqlalchemy.orm import sessionmaker
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||||
|
|
||||||
|
from src.models.database_models.model import RefreshTokens, engine
|
||||||
from src.models.pydantic_models.model import RefreshTokensOut
|
from src.models.pydantic_models.model import RefreshTokensOut
|
||||||
|
|
||||||
|
|
||||||
class JwtCrudActions:
|
class JwtCrudActions:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.Session=sessionmaker(bind=engine)
|
self.Session=async_sessionmaker(bind=engine)
|
||||||
|
|
||||||
def get_token_by_user_id(self, user_id:UUID)->RefreshTokensOut|None:
|
async def get_token_by_user_id(self, user_id:UUID)->RefreshTokensOut|None:
|
||||||
with self.Session() as session:
|
async with self.Session() as session, session.begin():
|
||||||
with session.begin():
|
|
||||||
query=select(RefreshTokens).where(and_(RefreshTokens.user_id==user_id, not_(RefreshTokens.is_revoked)))
|
query=select(RefreshTokens).where(and_(RefreshTokens.user_id==user_id, not_(RefreshTokens.is_revoked)))
|
||||||
response=session.scalars(query).one_or_none()
|
response= (await session.scalars(query)).first()
|
||||||
if response is None:
|
if response is None:
|
||||||
return None
|
return None
|
||||||
return RefreshTokensOut.model_validate(response)
|
return RefreshTokensOut.model_validate(response)
|
||||||
|
|
||||||
def get_token_by_id(self, id:UUID)->RefreshTokensOut|None:
|
async def get_token_by_id(self, token_id:UUID)->RefreshTokensOut|None:
|
||||||
with self.Session() as session:
|
async with self.Session() as session, session.begin():
|
||||||
with session.begin():
|
query=select(RefreshTokens).where(RefreshTokens.id==token_id)
|
||||||
query=select(RefreshTokens).where(RefreshTokens.id==id)
|
response= (await session.scalars(query)).one_or_none()
|
||||||
response=session.scalars(query).one_or_none()
|
|
||||||
if response is None:
|
if response is None:
|
||||||
return None
|
return None
|
||||||
return RefreshTokensOut.model_validate(response)
|
return RefreshTokensOut.model_validate(response)
|
||||||
|
|
||||||
def create_token(self, data:dict)->None:
|
async def create_token(self, data:dict)->None:
|
||||||
with self.Session() as session:
|
async with self.Session() as session, session.begin():
|
||||||
with session.begin():
|
|
||||||
new_token=RefreshTokens(**data)
|
new_token=RefreshTokens(**data)
|
||||||
response=session.add(new_token)
|
session.add(new_token)
|
||||||
return response
|
|
||||||
|
|
||||||
def update_token(self, old_jti:UUID, new_jti:UUID)->bool:
|
|
||||||
with self.Session() as session:
|
async def create_and_update_token(self, data: dict, old_jti: UUID, new_jti: UUID) -> bool:
|
||||||
with session.begin():
|
async with self.Session() as session, session.begin():
|
||||||
query=select(RefreshTokens).where(RefreshTokens.id==old_jti)
|
new_token = RefreshTokens(**data)
|
||||||
response=session.scalars(query).one()
|
|
||||||
response.is_revoked=True
|
query = (
|
||||||
response.replaced_by=new_jti
|
update(RefreshTokens)
|
||||||
|
.where(RefreshTokens.id == old_jti, RefreshTokens.is_revoked.is_(False))
|
||||||
|
.values(is_revoked=True, replaced_by=new_jti)
|
||||||
|
.returning(RefreshTokens.id)
|
||||||
|
)
|
||||||
|
updated_id = (await session.execute(query)).scalar_one_or_none()
|
||||||
|
|
||||||
|
if updated_id is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
session.add(new_token)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def revoke_all(self, user_id:UUID)->bool:
|
|
||||||
with self.Session() as session:
|
async def revoke_all(self, user_id:UUID)->bool:
|
||||||
with session.begin():
|
async with self.Session() as session, session.begin():
|
||||||
query=select(RefreshTokens).where(RefreshTokens.user_id==user_id)
|
await session.execute(update(RefreshTokens).where(RefreshTokens.user_id==user_id).values(is_revoked=True)) #bulk update
|
||||||
response=session.scalars(query).all()
|
|
||||||
for record in response:
|
|
||||||
record.is_revoked=True
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def logout(self,id:UUID)->bool:
|
async def logout(self,token_id:UUID)->bool:
|
||||||
with self.Session() as session:
|
async with self.Session() as session, session.begin():
|
||||||
with session.begin():
|
query=select(RefreshTokens).where(RefreshTokens.id == token_id)
|
||||||
query=select(RefreshTokens).where(RefreshTokens.id == id)
|
response= (await session.scalars(query)).one_or_none()
|
||||||
response=session.scalars(query).one_or_none()
|
|
||||||
if response is None:
|
if response is None:
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
|
|||||||
+111
-13
@@ -1,26 +1,124 @@
|
|||||||
from sqlalchemy import select
|
|
||||||
from src.models.database_models.model import User, engine
|
|
||||||
from src.models.pydantic_models.model import UserOutDB
|
|
||||||
from sqlalchemy.orm import sessionmaker
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from sqlalchemy import delete, select, update
|
||||||
|
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||||
|
|
||||||
|
from src.models.database_models.model import (
|
||||||
|
Permissions,
|
||||||
|
PermissionsGroups,
|
||||||
|
User,
|
||||||
|
engine,
|
||||||
|
)
|
||||||
|
from src.models.pydantic_models.model import UserOutDB
|
||||||
|
|
||||||
|
|
||||||
class UsersCrudActions:
|
class UsersCrudActions:
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.Session=sessionmaker(bind=engine)
|
self.Session=async_sessionmaker(bind=engine)
|
||||||
|
|
||||||
|
async def get_user_by_email(self, email:str)->UserOutDB|None:
|
||||||
|
|
||||||
|
async with self.Session() as session, session.begin():
|
||||||
|
|
||||||
def get_user_by_email(self, email:str)->UserOutDB|None:
|
|
||||||
with self.Session() as session:
|
|
||||||
with session.begin():
|
|
||||||
query=select(User).where(User.email==email)
|
query=select(User).where(User.email==email)
|
||||||
response=session.scalars(query).one_or_none()
|
response=(await session.scalars(query)).one_or_none()
|
||||||
|
|
||||||
if response is None:
|
if response is None:
|
||||||
return None
|
return None
|
||||||
return UserOutDB.model_validate(response)
|
return UserOutDB.model_validate(response)
|
||||||
|
|
||||||
def get_user_by_id(self, id:UUID)->UserOutDB|None:
|
async def get_user_by_id(self, id:UUID)->UserOutDB|None:
|
||||||
with self.Session() as session:
|
|
||||||
with session.begin():
|
async with self.Session() as session, session.begin():
|
||||||
|
|
||||||
query=select(User).where(User.id==id)
|
query=select(User).where(User.id==id)
|
||||||
response=session.scalars(query).one_or_none()
|
response=(await session.scalars(query)).one_or_none()
|
||||||
|
|
||||||
if response is None:
|
if response is None:
|
||||||
return None
|
return None
|
||||||
return UserOutDB.model_validate(response)
|
return UserOutDB.model_validate(response)
|
||||||
|
|
||||||
|
async def create_user(self, data:dict)->UserOutDB|None:
|
||||||
|
async with self.Session() as session, session.begin():
|
||||||
|
|
||||||
|
groups_name=data.pop("group", None)
|
||||||
|
permissions_name=data.pop("direct_permissions", None)
|
||||||
|
|
||||||
|
new_user=User(**data)
|
||||||
|
|
||||||
|
if groups_name:
|
||||||
|
query=select(PermissionsGroups).where(PermissionsGroups.group.in_(groups_name))
|
||||||
|
response=(await session.scalars(query)).all()
|
||||||
|
if response is None:
|
||||||
|
new_user.group=[]
|
||||||
|
else:
|
||||||
|
new_user.group=list(response)
|
||||||
|
else:
|
||||||
|
new_user.group=[]
|
||||||
|
|
||||||
|
if permissions_name:
|
||||||
|
query=select(Permissions).where(Permissions.permission.in_(permissions_name))
|
||||||
|
response=(await session.scalars(query)).all()
|
||||||
|
if response is None:
|
||||||
|
new_user.direct_permissions=[]
|
||||||
|
else:
|
||||||
|
new_user.direct_permissions=list(response)
|
||||||
|
else:
|
||||||
|
new_user.direct_permissions=[]
|
||||||
|
|
||||||
|
session.add(new_user)
|
||||||
|
await session.flush()
|
||||||
|
return UserOutDB.model_validate(new_user)
|
||||||
|
|
||||||
|
async def delete_user_soft(self, user_email:str)->bool|None:
|
||||||
|
|
||||||
|
async with self.Session() as session, session.begin():
|
||||||
|
|
||||||
|
query=select(User).where(User.email == user_email)
|
||||||
|
response=(await session.scalars(query)).one_or_none()
|
||||||
|
|
||||||
|
if response is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
response.status=False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_user_hard(self, user_email:str)->bool|None:
|
||||||
|
|
||||||
|
async with self.Session() as session, session.begin():
|
||||||
|
|
||||||
|
query=delete(User).where(User.email == user_email).returning(User)
|
||||||
|
response=(await session.scalars(query)).one_or_none()
|
||||||
|
|
||||||
|
if response is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def update_user_partially(self, user_email:str, data:dict)->UserOutDB|None:
|
||||||
|
|
||||||
|
async with self.Session() as session, session.begin():
|
||||||
|
|
||||||
|
groups_name=data.pop("group", None)
|
||||||
|
permissions_name=data.pop("direct_permissions", None)
|
||||||
|
|
||||||
|
query = update(User).where(User.email == user_email).values(**data).returning(User)
|
||||||
|
user_edit=(await session.scalars(query)).one_or_none()
|
||||||
|
|
||||||
|
if user_edit is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if groups_name is not None:
|
||||||
|
query=select(PermissionsGroups).where(PermissionsGroups.group.in_(permissions_name))
|
||||||
|
groups=(await session.scalars(query)).all()
|
||||||
|
user_edit.group=list(groups)
|
||||||
|
|
||||||
|
if permissions_name is not None:
|
||||||
|
query=select(Permissions).where(Permissions.permission.in_(permissions_name))
|
||||||
|
groups=(await session.scalars(query)).all()
|
||||||
|
user_edit.direct_permissions=list(groups)
|
||||||
|
|
||||||
|
await session.flush()
|
||||||
|
return UserOutDB.model_validate(user_edit)
|
||||||
@@ -12,3 +12,29 @@ class Errors:
|
|||||||
|
|
||||||
def not_found_error(self, detail:str)->HTTPException:
|
def not_found_error(self, detail:str)->HTTPException:
|
||||||
raise HTTPException(status_code=404, detail=detail, headers={"Cache-Control": "no-store, max-age=0"})
|
raise HTTPException(status_code=404, detail=detail, headers={"Cache-Control": "no-store, max-age=0"})
|
||||||
|
|
||||||
|
def conflict_error(self, detail:str)->HTTPException:
|
||||||
|
raise HTTPException(status_code=409, detail=detail)
|
||||||
|
|
||||||
|
def bad_request_error(self, detail:str) -> HTTPException:
|
||||||
|
raise HTTPException(status_code=400, detail=detail)
|
||||||
|
|
||||||
|
def validation_error(self, detail:str) -> HTTPException:
|
||||||
|
raise HTTPException(status_code=422, detail=detail)
|
||||||
|
|
||||||
|
def rate_limit_error(self, detail:str, retry_after: int = 60) -> HTTPException:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=429,
|
||||||
|
detail=detail,
|
||||||
|
headers={"Retry-After":str(retry_after)}
|
||||||
|
)
|
||||||
|
|
||||||
|
def internal_server_error(self, detail:str = "Internal server error") -> HTTPException:
|
||||||
|
raise HTTPException(status_code=500, detail=detail)
|
||||||
|
|
||||||
|
def service_unavailable_error(self, detail:str, retry_after: int = 30) -> HTTPException:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=503,
|
||||||
|
detail=detail,
|
||||||
|
headers={"Retry-After":str(retry_after)}
|
||||||
|
)
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
class Base(BaseSettings):
|
class Base(BaseSettings):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -11,4 +12,4 @@ class Env(Base):
|
|||||||
|
|
||||||
model_config=SettingsConfigDict(env_file="configs/.env", extra=None)
|
model_config=SettingsConfigDict(env_file="configs/.env", extra=None)
|
||||||
|
|
||||||
env_settings=Env()
|
env_settings=Env() # type: ignore[call-arg]
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
from src.models.database_models.model import Model
|
from uuid import UUID, uuid1
|
||||||
|
|
||||||
from sqlalchemy import String
|
from sqlalchemy import String
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
from uuid import UUID, uuid1
|
|
||||||
|
from src.models.database_models.model import Model
|
||||||
|
|
||||||
|
|
||||||
class AccountantSettings(Model):
|
class AccountantSettings(Model):
|
||||||
__tablename__="accountant_settings"
|
__tablename__="accountant_settings"
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from sqlalchemy import TIMESTAMP, String, func, ForeignKey
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
from src.models.database_models.model import Model
|
|
||||||
from uuid import UUID, uuid1
|
from uuid import UUID, uuid1
|
||||||
|
|
||||||
|
from sqlalchemy import TIMESTAMP, ForeignKey, String, func
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from src.models.database_models.model import Model
|
||||||
|
|
||||||
|
|
||||||
class Stored(Model):
|
class Stored(Model):
|
||||||
__tablename__="reports"
|
__tablename__="reports"
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from src.models.database_models.model import Model
|
|
||||||
from sqlalchemy import Boolean, ForeignKey, Integer, Numeric, String
|
from sqlalchemy import Boolean, ForeignKey, Integer, Numeric, String
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from src.models.database_models.model import Model
|
||||||
|
|
||||||
|
|
||||||
class Nomenclature(Model):
|
class Nomenclature(Model):
|
||||||
__tablename__="goods"
|
__tablename__="goods"
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
from sqlalchemy import TIMESTAMP, Table, create_engine, String, Boolean, MetaData, Column, ForeignKey, func, Uuid
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase, relationship
|
|
||||||
from uuid import UUID, uuid4
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
engine = create_engine("sqlite:///DB/database.db", echo=True)
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
|
from sqlalchemy import (
|
||||||
|
TIMESTAMP,
|
||||||
|
Boolean,
|
||||||
|
Column,
|
||||||
|
ForeignKey,
|
||||||
|
MetaData,
|
||||||
|
String,
|
||||||
|
Table,
|
||||||
|
Uuid,
|
||||||
|
func,
|
||||||
|
)
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
|
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
engine = create_async_engine("sqlite+aiosqlite:///DB/database.db", echo=True)
|
||||||
|
|
||||||
'''remember as a boilerplate, or just cp/pst'''
|
'''remember as a boilerplate, or just cp/pst'''
|
||||||
class Model(DeclarativeBase):
|
class Model(DeclarativeBase):
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from src.models.pydantic_models.model import Base
|
|
||||||
from pydantic import Field
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from src.models.pydantic_models.model import Base
|
||||||
|
|
||||||
|
|
||||||
class AccountantCreate(Base):
|
class AccountantCreate(Base):
|
||||||
|
|
||||||
name:Annotated[str, Field(...,max_length=64,description="name of the accountant setting")]
|
name:Annotated[str, Field(...,max_length=64,description="name of the accountant setting")]
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
from src.models.pydantic_models.model import Base
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|
||||||
|
from src.models.pydantic_models.model import Base
|
||||||
|
|
||||||
|
|
||||||
class ReportCreate(Base):
|
class ReportCreate(Base):
|
||||||
filename:Annotated[str,Field(..., min_length=2, max_length=255, description="name of the report")]
|
filename:Annotated[str,Field(..., min_length=2, max_length=255, description="name of the report")]
|
||||||
doc_date:Annotated[datetime, Field(..., description="ts of the report")]
|
doc_date:Annotated[datetime, Field(..., description="ts of the report")]
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from src.models.pydantic_models.model import Base
|
|
||||||
from pydantic import Field
|
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
|
from pydantic import Field
|
||||||
|
|
||||||
|
from src.models.pydantic_models.model import Base
|
||||||
|
|
||||||
|
|
||||||
class NomenclatureCreate(Base):
|
class NomenclatureCreate(Base):
|
||||||
|
|
||||||
article:Annotated[str, Field(...,min_length=5,max_length=16, description="name of the article")]
|
article:Annotated[str, Field(...,min_length=5,max_length=16, description="name of the article")]
|
||||||
|
|||||||
@@ -1,14 +1,29 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from pydantic import BaseModel, EmailStr, Field
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from pydantic import AfterValidator, BaseModel, EmailStr, Field
|
||||||
|
|
||||||
|
|
||||||
|
def validate_password(password: str) -> str:
|
||||||
|
PUNCTUATION: set[str] = {"$", "@", "#", "%", "!", "^", "&", "*", "(", ")", "-", "_", "+", "=", "{", "}", "[", "]"}
|
||||||
|
if len(password) < 8 or len(password) > 72:
|
||||||
|
raise ValueError("Password must be 8-72 characters")
|
||||||
|
if (
|
||||||
|
not any(c.isupper() for c in password)
|
||||||
|
or not any(c.islower() for c in password)
|
||||||
|
or not any(c.isdigit() for c in password)
|
||||||
|
or not any(c in PUNCTUATION for c in password)
|
||||||
|
):
|
||||||
|
raise ValueError("Password must contain uppercase, lowercase, digit and special char")
|
||||||
|
return password
|
||||||
|
|
||||||
|
PasswordStr = Annotated[str, AfterValidator(validate_password)]
|
||||||
|
|
||||||
class Base(BaseModel):
|
class Base(BaseModel):
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PermissionsCreate(Base):
|
class PermissionsCreate(Base):
|
||||||
permission:Annotated[str, Field(..., max_length=30, description="permission name")]
|
permission:Annotated[str, Field(..., max_length=30, description="permission name")]
|
||||||
|
|
||||||
@@ -34,8 +49,7 @@ class UserCreate(Base):
|
|||||||
last_name:Annotated[str, Field(...,max_length=64, description="last name of the user")]
|
last_name:Annotated[str, Field(...,max_length=64, description="last name of the user")]
|
||||||
middle_name:Annotated[str, Field(...,max_length=64, description="middle name of the user")]
|
middle_name:Annotated[str, Field(...,max_length=64, description="middle name of the user")]
|
||||||
email:Annotated[EmailStr, Field(...,min_length=5, max_length=255, description="email of the user")]
|
email:Annotated[EmailStr, Field(...,min_length=5, max_length=255, description="email of the user")]
|
||||||
plain_password:Annotated[str, Field(...,min_length=8,max_length=72, description="plain password of the user")]
|
plain_password:Annotated[PasswordStr, Field(..., description="plain password of the user")]
|
||||||
status:Annotated[bool, Field(..., description="status of the user")]
|
|
||||||
|
|
||||||
direct_permissions:Annotated[list[str], Field(..., description="permissions of the user")]
|
direct_permissions:Annotated[list[str], Field(..., description="permissions of the user")]
|
||||||
group:Annotated[list[str], Field(..., description="permissions groups of the user")]
|
group:Annotated[list[str], Field(..., description="permissions groups of the user")]
|
||||||
@@ -62,6 +76,7 @@ class UserUpdate(Base):
|
|||||||
last_name:Annotated[str|None, Field(None, max_length=64,description="last name of the user")]
|
last_name:Annotated[str|None, Field(None, max_length=64,description="last name of the user")]
|
||||||
middle_name:Annotated[str|None, Field(None, max_length=64,description="middle name of the user")]
|
middle_name:Annotated[str|None, Field(None, max_length=64,description="middle name of the user")]
|
||||||
email:Annotated[EmailStr|None, Field(None, min_length=5, max_length=255, description="email of the user")]
|
email:Annotated[EmailStr|None, Field(None, min_length=5, max_length=255, description="email of the user")]
|
||||||
|
plain_password:Annotated[PasswordStr|None, Field(None, description="plain password of the user")]
|
||||||
status:Annotated[bool|None, Field(None, description="status of the user")]
|
status:Annotated[bool|None, Field(None, description="status of the user")]
|
||||||
direct_permissions:Annotated[list[str]|None, Field(None, description="permissions of the user")]
|
direct_permissions:Annotated[list[str]|None, Field(None, description="permissions of the user")]
|
||||||
group:Annotated[list[str]|None, Field(None, description="permissions groups of the user")]
|
group:Annotated[list[str]|None, Field(None, description="permissions groups of the user")]
|
||||||
|
|||||||
+73
-65
@@ -1,32 +1,35 @@
|
|||||||
from datetime import datetime, timedelta, timezone
|
import asyncio
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from .jwt import Jwt, Hashes
|
|
||||||
from src.database.users.crud import UsersCrudActions
|
|
||||||
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.errors.http_errors.errors import Errors
|
from src.errors.http_errors.errors import Errors
|
||||||
from src.models.pydantic_models.model import RefreshTokensCreate, UserOut
|
|
||||||
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 RefreshTokensCreate, UserOut
|
||||||
|
|
||||||
|
from .jwt import HashService, JwtService
|
||||||
|
|
||||||
|
|
||||||
class CurrentUser:
|
class CurrentUserService:
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.jwt_service=Jwt()
|
self.jwt_service=JwtService()
|
||||||
self.hash=Hashes()
|
self.hash=HashService()
|
||||||
self.crud_db_actions=UsersCrudActions()
|
self.crud_db_actions=UsersCrudActions()
|
||||||
self.jwt_db_actions=JwtCrudActions()
|
self.jwt_db_actions=JwtCrudActions()
|
||||||
self.error=Errors()
|
self.error=Errors()
|
||||||
|
|
||||||
def _check(self, form_data_email:str, form_data_password:str,):
|
async def _check(self, form_data_email:str, form_data_password:str,):
|
||||||
'''check user by email'''
|
'''check user by email'''
|
||||||
user=self.crud_db_actions.get_user_by_email(form_data_email)
|
user=await self.crud_db_actions.get_user_by_email(form_data_email)
|
||||||
|
|
||||||
if user is None:
|
if user is None:
|
||||||
raise self.error.credentials_error(detail="Wrong credentials")
|
raise self.error.credentials_error(detail="Wrong credentials")
|
||||||
|
|
||||||
if not self.hash.verify_password(plain_password=form_data_password, hashed_password=user.hashed_password):
|
if not await asyncio.to_thread(self.hash.verify_password, plain_password=form_data_password, hashed_password=user.hashed_password):
|
||||||
raise self.error.credentials_error(detail="Wrong credentials")
|
raise self.error.credentials_error(detail="Wrong credentials")
|
||||||
|
|
||||||
if user.status is False:
|
if user.status is False:
|
||||||
@@ -34,9 +37,21 @@ class CurrentUser:
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
def get_current_user(self, token:str)->UserOut:
|
async def _token_record_create(self, jti:UUID,user_id:UUID,token:str, request:Request)->RefreshTokensCreate:
|
||||||
|
|
||||||
payload=self.jwt_service.jwt_decode(token)
|
return RefreshTokensCreate(
|
||||||
|
id=jti,
|
||||||
|
user_id=user_id,
|
||||||
|
token_hash=self.hash.token_to_hash(token),
|
||||||
|
device_info=request.headers.get("user-agent", "unknown"),
|
||||||
|
ip_address=request.headers.get("x-forwarded-for", "").split(",")[0].strip() or (request.client.host if request.client else "unknown"),
|
||||||
|
expires_at=datetime.now(UTC)+timedelta(days=env_settings.REFRESH_TOKEN_EXPIRE_DAYS)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_user(self, token:str)->UserOut:
|
||||||
|
|
||||||
|
payload= await self.jwt_service.jwt_decode(token)
|
||||||
sub=payload.get("sub")
|
sub=payload.get("sub")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -44,8 +59,10 @@ class CurrentUser:
|
|||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) 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 not (payload.get("token_type")=="access"):
|
||||||
|
raise self.error.credentials_error(detail="Jwt token type is incorrect")
|
||||||
|
|
||||||
user=self.crud_db_actions.get_user_by_id(sub)
|
user=await self.crud_db_actions.get_user_by_id(sub)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise self.error.not_found_error(detail="User with this email address not found")
|
raise self.error.not_found_error(detail="User with this email address not found")
|
||||||
|
|
||||||
@@ -56,15 +73,15 @@ class CurrentUser:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def create_access_token(self, user_id:UUID)->str:
|
async def create_access_token(self, user_id:UUID)->str:
|
||||||
'''create new access token if all the checks are successful'''
|
'''create new access token if all the checks are successful'''
|
||||||
return self.jwt_service.create_access_token({"sub":str(user_id)})
|
return await self.jwt_service.create_access_token({"sub":str(user_id)})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def create_refresh_token(self,user_id:UUID, request:Request)->str:
|
async def create_refresh_token(self,user_id:UUID, request:Request)->str:
|
||||||
|
|
||||||
token, jti=self.jwt_service.create_refresh_token({"sub":str(user_id)})
|
token, jti= await self.jwt_service.create_refresh_token({"sub":str(user_id)})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jti=UUID(jti)
|
jti=UUID(jti)
|
||||||
@@ -72,23 +89,18 @@ class CurrentUser:
|
|||||||
raise self.error.credentials_error(detail="Jwt token is incorrect") from e
|
raise self.error.credentials_error(detail="Jwt token is incorrect") from e
|
||||||
|
|
||||||
'''create new refresh token if all the checks are successful'''
|
'''create new refresh token if all the checks are successful'''
|
||||||
token_record=RefreshTokensCreate(
|
token_record=await self._token_record_create(jti=jti, user_id=user_id, token=token, request=request)
|
||||||
id=jti,
|
|
||||||
user_id=user_id,
|
|
||||||
token_hash=self.hash.token_to_hash(token),
|
await self.jwt_db_actions.create_token(RefreshTokensCreate.model_dump(token_record))
|
||||||
device_info=request.headers.get("user-agent", "unknown"),
|
|
||||||
ip_address=request.headers.get("x-forwarded-for", "").split(",")[0].strip() or (request.client.host if request.client else "unknown"),
|
|
||||||
expires_at=datetime.now(timezone.utc)+timedelta(days=env_settings.REFRESH_TOKEN_EXPIRE_DAYS)
|
|
||||||
)
|
|
||||||
self.jwt_db_actions.create_token(RefreshTokensCreate.model_dump(token_record))
|
|
||||||
|
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
def refresh_token(self, refresh_token:str, request:Request)->tuple[str, str]:
|
async def refresh_token(self, refresh_token:str, request:Request)->tuple[str, str]:
|
||||||
|
|
||||||
'''decode old refresh token'''
|
'''decode old refresh token'''
|
||||||
old_refresh_token=self.jwt_service.jwt_decode(refresh_token)
|
old_refresh_token= await self.jwt_service.jwt_decode(refresh_token)
|
||||||
sub=old_refresh_token.get("sub")
|
sub=old_refresh_token.get("sub")
|
||||||
|
|
||||||
if (old_jti:=old_refresh_token.get("jti")) is None:
|
if (old_jti:=old_refresh_token.get("jti")) is None:
|
||||||
@@ -102,23 +114,25 @@ class CurrentUser:
|
|||||||
|
|
||||||
|
|
||||||
'''old refresh token check'''
|
'''old refresh token check'''
|
||||||
old_record=self.jwt_db_actions.get_token_by_id(old_jti)
|
|
||||||
|
if (old_refresh_token.get("token_type")=="access"):
|
||||||
|
raise self.error.credentials_error(detail="Jwt token type is incorrect")
|
||||||
|
|
||||||
|
|
||||||
|
old_record=await self.jwt_db_actions.get_token_by_id(old_jti)
|
||||||
if old_record is None:
|
if old_record is None:
|
||||||
raise self.error.not_found_error(detail="Token not found")
|
raise self.error.not_found_error(detail="Token not found")
|
||||||
if old_record.is_revoked:
|
|
||||||
self.jwt_db_actions.revoke_all(old_record.user_id)
|
|
||||||
raise self.error.credentials_error(detail="Reuse token detected")
|
|
||||||
|
|
||||||
|
|
||||||
'''sqlite constraints about timezone'''
|
'''sqlite constraints about timezone'''
|
||||||
expires_at=old_record.expires_at
|
expires_at=old_record.expires_at
|
||||||
if expires_at.tzinfo is None:
|
if expires_at.tzinfo is None:
|
||||||
expires_at = expires_at.replace(tzinfo=timezone.utc)
|
expires_at = expires_at.replace(tzinfo=UTC)
|
||||||
if expires_at<datetime.now(timezone.utc):
|
if expires_at<datetime.now(UTC):
|
||||||
raise self.error.credentials_error(detail="Token expired")
|
raise self.error.credentials_error(detail="Token expired")
|
||||||
|
|
||||||
'''user check'''
|
'''user check'''
|
||||||
user = self.crud_db_actions.get_user_by_id(sub)
|
user = await self.crud_db_actions.get_user_by_id(sub)
|
||||||
if user is None:
|
if user is None:
|
||||||
raise self.error.not_found_error(detail="User not found")
|
raise self.error.not_found_error(detail="User not found")
|
||||||
if user.status is False:
|
if user.status is False:
|
||||||
@@ -126,64 +140,58 @@ class CurrentUser:
|
|||||||
|
|
||||||
|
|
||||||
'''create new refresh token if all the checks are successful'''
|
'''create new refresh token if all the checks are successful'''
|
||||||
new_refresh_token, new_jti=self.jwt_service.create_refresh_token({"sub":str(sub)})
|
new_refresh_token, new_jti= await self.jwt_service.create_refresh_token({"sub":str(sub)})
|
||||||
new_access_token=self.create_access_token(user_id=sub)
|
new_access_token=await self.create_access_token(user_id=sub)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
new_jti=UUID(new_jti)
|
new_jti=UUID(new_jti)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError) as e:
|
||||||
raise self.error.credentials_error(detail="Jwt token is incorrect")
|
raise self.error.credentials_error(detail="Jwt token is incorrect") from e
|
||||||
|
|
||||||
|
|
||||||
'''create database record with the new token'''
|
'''create database record with the new token'''
|
||||||
new_token_record=RefreshTokensCreate(
|
new_token_record=await self._token_record_create(jti=new_jti, user_id=sub, token=new_refresh_token, request=request)
|
||||||
id=new_jti,
|
|
||||||
user_id=sub,
|
|
||||||
token_hash=self.hash.token_to_hash(new_refresh_token),
|
|
||||||
device_info=request.headers.get("user-agent", "unknown"),
|
|
||||||
ip_address=request.headers.get("x-forwarded-for", "").split(",")[0].strip() or (request.client.host if request.client else "unknown"),
|
|
||||||
expires_at=datetime.now(timezone.utc)+timedelta(days=env_settings.REFRESH_TOKEN_EXPIRE_DAYS),
|
|
||||||
)
|
|
||||||
self.jwt_db_actions.create_token(RefreshTokensCreate.model_dump(new_token_record))
|
|
||||||
|
|
||||||
'''update old token to deactivate it and assign replaced_by'''
|
success = await self.jwt_db_actions.create_and_update_token(RefreshTokensCreate.model_dump(new_token_record), old_jti, new_jti)
|
||||||
self.jwt_db_actions.update_token(old_jti, new_jti)
|
|
||||||
|
|
||||||
return (new_access_token,new_refresh_token)
|
if not success:
|
||||||
|
raise self.error.not_found_error(detail="Token not found")
|
||||||
|
|
||||||
|
return (new_access_token, new_refresh_token)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def logout(self, refresh_token:str)->bool:
|
async def logout(self, refresh_token:str)->bool:
|
||||||
|
|
||||||
'''decode current refresh token'''
|
'''decode current refresh token'''
|
||||||
payload=self.jwt_service.jwt_decode(refresh_token)
|
payload=await self.jwt_service.jwt_decode(refresh_token)
|
||||||
|
|
||||||
if (jti:=payload.get("jti")) is None:
|
if (jti:=payload.get("jti")) is None:
|
||||||
raise self.error.credentials_error(detail="Jwt token is incorrect")
|
raise self.error.credentials_error(detail="Jwt token is incorrect")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jti=UUID(jti)
|
jti=UUID(jti)
|
||||||
except (ValueError, TypeError) 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
|
||||||
|
|
||||||
current_token = self.jwt_db_actions.get_token_by_id(jti)
|
'''logout by assigning revoked flag'''
|
||||||
if current_token is None:
|
if await self.jwt_db_actions.logout(jti):
|
||||||
|
return True
|
||||||
|
else:
|
||||||
raise self.error.not_found_error(detail="Refresh Token Not Found")
|
raise self.error.not_found_error(detail="Refresh Token Not Found")
|
||||||
|
|
||||||
'''logout by assigning revoked flag'''
|
|
||||||
return self.jwt_db_actions.logout(jti)
|
|
||||||
|
|
||||||
|
|
||||||
|
async def login(self, form_data_email:str, form_data_password:str, request:Request)->tuple[str, str]:
|
||||||
def login(self, form_data_email:str, form_data_password:str, request:Request)->tuple[str, str]:
|
|
||||||
'''revoke all the old refresh tokens'''
|
'''revoke all the old refresh tokens'''
|
||||||
user = self._check(form_data_email, form_data_password)
|
user = await self._check(form_data_email, form_data_password)
|
||||||
self.jwt_db_actions.revoke_all(user_id=user.id)
|
await self.jwt_db_actions.revoke_all(user_id=user.id)
|
||||||
|
|
||||||
'''create access and refresh tokens'''
|
'''create access and refresh tokens'''
|
||||||
access_token=self.create_access_token(user_id=user.id)
|
access_token=await self.create_access_token(user_id=user.id)
|
||||||
refresh_token=self.create_refresh_token(user_id=user.id,request=request)
|
refresh_token=await self.create_refresh_token(user_id=user.id,request=request)
|
||||||
|
|
||||||
return (access_token, refresh_token)
|
return (access_token, refresh_token)
|
||||||
|
|
||||||
auth=CurrentUser()
|
async def auth_service()->CurrentUserService:
|
||||||
|
return CurrentUserService()
|
||||||
+27
-15
@@ -1,13 +1,15 @@
|
|||||||
from jose import JWTError, jwt
|
|
||||||
import bcrypt
|
|
||||||
from src.errors.http_errors.errors import Errors
|
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
from src.models.configs_read.env import env_settings
|
|
||||||
from uuid import uuid4
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import bcrypt
|
||||||
|
from jose import JWTError, jwt
|
||||||
|
|
||||||
|
from src.errors.http_errors.errors import Errors
|
||||||
|
from src.models.configs_read.env import env_settings
|
||||||
|
|
||||||
'''Hash/Check hash'''
|
'''Hash/Check hash'''
|
||||||
class Hashes:
|
class HashService:
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
pass
|
pass
|
||||||
@@ -23,25 +25,35 @@ class Hashes:
|
|||||||
|
|
||||||
|
|
||||||
'''jwt'''
|
'''jwt'''
|
||||||
class Jwt:
|
class JwtService:
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
|
||||||
self.error=Errors()
|
self.error=Errors()
|
||||||
|
|
||||||
def create_access_token(self, data:dict)->str:
|
async def _validate_sub(self,data:dict)->None:
|
||||||
|
if not (data.get("sub")) or data.get("sub") == "":
|
||||||
|
raise self.error.credentials_error(detail="Jwt token is incorrect")
|
||||||
|
|
||||||
|
async def create_access_token(self, data:dict)->str:
|
||||||
|
|
||||||
user_info=data.copy()
|
user_info=data.copy()
|
||||||
user_info.update({"exp": datetime.now(timezone.utc)+timedelta(minutes=env_settings.ACCESS_TOKEN_EXPIRE_MINUTES),
|
|
||||||
|
await self._validate_sub(user_info)
|
||||||
|
|
||||||
|
user_info.update({"exp": datetime.now(UTC)+timedelta(minutes=env_settings.ACCESS_TOKEN_EXPIRE_MINUTES),
|
||||||
"token_type":"access"})
|
"token_type":"access"})
|
||||||
return jwt.encode(user_info, env_settings.SECRET_KEY, env_settings.ALGORITHM)
|
return jwt.encode(user_info, env_settings.SECRET_KEY, env_settings.ALGORITHM)
|
||||||
|
|
||||||
|
|
||||||
def create_refresh_token(self, data:dict)->tuple[str, str]:
|
async def create_refresh_token(self, data:dict)->tuple[str, str]:
|
||||||
|
|
||||||
user_info=data.copy()
|
user_info=data.copy()
|
||||||
jti=str(uuid4())
|
jti=str(uuid4())
|
||||||
user_info.update({"exp":datetime.now(timezone.utc)+timedelta(days=env_settings.REFRESH_TOKEN_EXPIRE_DAYS),
|
|
||||||
|
await self._validate_sub(user_info)
|
||||||
|
|
||||||
|
user_info.update({"exp":datetime.now(UTC)+timedelta(days=env_settings.REFRESH_TOKEN_EXPIRE_DAYS),
|
||||||
"token_type":"refresh",
|
"token_type":"refresh",
|
||||||
"jti":jti
|
"jti":jti
|
||||||
})
|
})
|
||||||
@@ -50,12 +62,12 @@ class Jwt:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
def jwt_decode(self, token:str)->dict:
|
async def jwt_decode(self, token:str)->dict:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload=jwt.decode(token, env_settings.SECRET_KEY, algorithms=[env_settings.ALGORITHM])
|
payload=jwt.decode(token, env_settings.SECRET_KEY, algorithms=[env_settings.ALGORITHM], options={"require_exp": True} )
|
||||||
|
|
||||||
if (payload.get("sub")) is None:
|
if not (payload.get("sub")) or not (payload.get("token_type")):
|
||||||
raise self.error.credentials_error(detail="Jwt token is incorrect")
|
raise self.error.credentials_error(detail="Jwt token is incorrect")
|
||||||
|
|
||||||
except JWTError as e:
|
except JWTError as e:
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
from src.database.users.crud import UsersCrudActions
|
||||||
|
from src.errors.http_errors.errors import Errors
|
||||||
|
from src.models.pydantic_models.model import UserCreate, UserOut, UserUpdate
|
||||||
|
from src.service.auth.jwt import HashService
|
||||||
|
|
||||||
|
|
||||||
|
class CrudService:
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.errors=Errors()
|
||||||
|
self.crud_db_actions=UsersCrudActions()
|
||||||
|
self.hash_service=HashService()
|
||||||
|
|
||||||
|
|
||||||
|
async def _plain_to_hash(self, user_data:dict)->dict:
|
||||||
|
|
||||||
|
user_data["hashed_password"]=user_data.pop("plain_password")
|
||||||
|
user_data["hashed_password"]= await asyncio.to_thread(self.hash_service.plain_to_hash, user_data["hashed_password"])
|
||||||
|
|
||||||
|
return user_data
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_by_email(self, email:str)->UserOut:
|
||||||
|
|
||||||
|
user_entity=await self.crud_db_actions.get_user_by_email(email)
|
||||||
|
|
||||||
|
if not user_entity:
|
||||||
|
raise self.errors.not_found_error(detail="User wasn't found")
|
||||||
|
return UserOut.model_validate(user_entity)
|
||||||
|
|
||||||
|
|
||||||
|
async def create_user(self, data:UserCreate)->UserOut:
|
||||||
|
|
||||||
|
user_data=UserCreate.model_dump(data)
|
||||||
|
|
||||||
|
user_data=await self._plain_to_hash(user_data)
|
||||||
|
|
||||||
|
user_entity=await self.crud_db_actions.create_user(user_data)
|
||||||
|
|
||||||
|
if not user_entity:
|
||||||
|
raise self.errors.validation_error(detail="User creation gone wrong")
|
||||||
|
return UserOut.model_validate(user_entity)
|
||||||
|
|
||||||
|
|
||||||
|
async def delete_user_soft(self, email:str)->bool:
|
||||||
|
|
||||||
|
user_entity=await self.crud_db_actions.delete_user_soft(email)
|
||||||
|
|
||||||
|
if not user_entity:
|
||||||
|
raise self.errors.not_found_error(detail="User not found")
|
||||||
|
return user_entity
|
||||||
|
|
||||||
|
async def delete_user_hard(self, email:str, current_user)->bool:
|
||||||
|
|
||||||
|
user_entity=await self.crud_db_actions.delete_user_hard(email)
|
||||||
|
|
||||||
|
if not user_entity:
|
||||||
|
raise self.errors.not_found_error(detail="User not found")
|
||||||
|
return user_entity
|
||||||
|
|
||||||
|
async def update_user(self, email:str, data:UserUpdate)->UserOut:
|
||||||
|
|
||||||
|
user_data=UserUpdate.model_dump(data, exclude_unset=True)
|
||||||
|
|
||||||
|
if not user_data:
|
||||||
|
raise self.errors.bad_request_error(detail="User info to update can not be empty")
|
||||||
|
|
||||||
|
if user_data.get("plain_password"):
|
||||||
|
user_data=await self._plain_to_hash(user_data)
|
||||||
|
|
||||||
|
user_entity=await self.crud_db_actions.update_user_partially(email, user_data)
|
||||||
|
|
||||||
|
if not user_entity:
|
||||||
|
raise self.errors.not_found_error(detail="User not found")
|
||||||
|
return UserOut.model_validate(user_entity)
|
||||||
|
|
||||||
|
async def crud_service()->CrudService:
|
||||||
|
return CrudService()
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
from fastapi import APIRouter, Depends, Request, Response, Cookie
|
from fastapi import APIRouter, Cookie, Depends, Request, Response
|
||||||
from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
|
|
||||||
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 auth
|
|
||||||
|
|
||||||
router=APIRouter(prefix="/protected")
|
router=APIRouter(prefix="/protected")
|
||||||
oauth2_schema=OAuth2PasswordBearer(tokenUrl="/protected/token", refreshUrl="/protected/refresh")
|
oauth2_schema=OAuth2PasswordBearer(tokenUrl="/protected/token", refreshUrl="/protected/refresh")
|
||||||
|
|
||||||
@router.post("/token")
|
@router.post("/token")
|
||||||
async def get_access_token(request: Request,response:Response, form_data:OAuth2PasswordRequestForm=Depends())->dict:
|
async def get_access_token(request: Request,response:Response,auth:CurrentUserService=Depends(auth_service), form_data:OAuth2PasswordRequestForm=Depends())->dict: # noqa: B008
|
||||||
|
|
||||||
access_token, refresh_token=auth.login(form_data_email=form_data.username, form_data_password=form_data.password, request=request)
|
access_token, refresh_token=await auth.login(form_data_email=form_data.username, form_data_password=form_data.password, request=request)
|
||||||
|
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key="refresh_token",
|
key="refresh_token",
|
||||||
@@ -25,9 +25,9 @@ async def get_access_token(request: Request,response:Response, form_data:OAuth2P
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/refresh")
|
@router.post("/refresh")
|
||||||
async def get_refresh_token(request:Request,response:Response, refresh_token: str = Cookie())->dict:
|
async def get_refresh_token(request:Request,response:Response, refresh_token: str = Cookie(), auth:CurrentUserService=Depends(auth_service))->dict: # noqa: B008
|
||||||
|
|
||||||
access_token, refresh_token= auth.refresh_token(refresh_token=refresh_token,request=request)
|
access_token, refresh_token= await auth.refresh_token(refresh_token=refresh_token,request=request)
|
||||||
|
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key="refresh_token",
|
key="refresh_token",
|
||||||
@@ -41,15 +41,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"}
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user(token:str = Depends(oauth2_schema)) -> UserOut:
|
async def get_current_user(token:str = Depends(oauth2_schema), auth:CurrentUserService=Depends(auth_service)) -> UserOut: # noqa: B008
|
||||||
return UserOut.model_validate(auth.get_current_user(token))
|
return UserOut.model_validate(await auth.get_current_user(token))
|
||||||
|
|
||||||
|
|
||||||
@router.get("/logout")
|
@router.get("/logout")
|
||||||
async def logout(response:Response,refresh_token: str = Cookie(),current_user:UserOut=Depends(get_current_user))->bool:
|
async def logout(response:Response,refresh_token: str = Cookie(),auth:CurrentUserService=Depends(auth_service),current_user:UserOut=Depends(get_current_user))->bool: # noqa: B008
|
||||||
response.delete_cookie("refresh_token")
|
response.delete_cookie("refresh_token")
|
||||||
return auth.logout(refresh_token)
|
return await auth.logout(refresh_token)
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
async def protected(current_user:UserOut=Depends(get_current_user))->dict:
|
async def protected(current_user:UserOut=Depends(get_current_user))->dict: # noqa: B008
|
||||||
return {"protected router": "Hello, this is a protected router"}
|
return {"protected router": "Hello, this is a protected router"}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from src.models.pydantic_models.model import UserCreate, UserOut, UserUpdate
|
||||||
|
from src.service.users_crud.users_crud import CrudService, crud_service
|
||||||
|
from src.web.protected_routes.auth_routes import get_current_user
|
||||||
|
|
||||||
|
router=APIRouter(prefix="/user")
|
||||||
|
|
||||||
|
@router.get("/get_by_email")
|
||||||
|
async def get_current_user_by_email(email:str, crud:CrudService=Depends(crud_service), current_user=Depends(get_current_user))->UserOut: # noqa: B008
|
||||||
|
return await crud.get_user_by_email(email)
|
||||||
|
|
||||||
|
@router.post("/create_user")
|
||||||
|
async def create_user(data:UserCreate, crud:CrudService=Depends(crud_service), current_user=Depends(get_current_user))->UserOut: #noqa: B008
|
||||||
|
return await crud.create_user(data)
|
||||||
|
|
||||||
|
@router.post("/delete_user_soft")
|
||||||
|
async def delete_user_soft(email:str, crud:CrudService=Depends(crud_service), current_user=Depends(get_current_user))->bool: #noqa: B008
|
||||||
|
return await crud.delete_user_soft(email)
|
||||||
|
|
||||||
|
@router.post("/delete_user_hard")
|
||||||
|
async def delete_user_hard(email:str, crud:CrudService=Depends(crud_service), current_user=Depends(get_current_user))->bool: #noqa: B008
|
||||||
|
return await crud.delete_user_hard(email, current_user)
|
||||||
|
|
||||||
|
@router.patch("/patch_user")
|
||||||
|
async def patch_user(email:str, data:UserUpdate, crud:CrudService=Depends(crud_service), current_user=Depends(get_current_user))->UserOut: #noqa: B008
|
||||||
|
return await crud.update_user(email, data)
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import pytest_asyncio
|
||||||
|
|
||||||
|
from src.service.auth.jwt import HashService, JwtService
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def jwt_service()->JwtService:
|
||||||
|
jwt_service=JwtService()
|
||||||
|
return jwt_service
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def hash_service()->HashService:
|
||||||
|
hash_service=HashService()
|
||||||
|
return hash_service
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest_asyncio
|
||||||
|
import requests_async
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Env(BaseSettings):
|
||||||
|
HOST:str
|
||||||
|
PORT:str
|
||||||
|
TEST_USERNAME:str
|
||||||
|
TEST_PASSWORD:str
|
||||||
|
|
||||||
|
model_config=SettingsConfigDict(env_file="configs/.e2e.env", extra=None)
|
||||||
|
|
||||||
|
e2e_settings=Env() # type: ignore[call-arg]
|
||||||
|
|
||||||
|
class MySession(requests_async.AsyncSession):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.headers = {}
|
||||||
|
self.token = None
|
||||||
|
|
||||||
|
async def request(self, method:str, url:str, **kwargs):
|
||||||
|
if self.token:
|
||||||
|
self.headers['Authorization'] = f"Bearer {self.token}"
|
||||||
|
kwargs.setdefault('headers', self.headers)
|
||||||
|
return await super().request(method, url, **kwargs)
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture(scope="function")
|
||||||
|
async def auth_fixture(target_url:str):
|
||||||
|
|
||||||
|
payload = {"username": e2e_settings.TEST_USERNAME, "password": e2e_settings.TEST_PASSWORD}
|
||||||
|
|
||||||
|
async with MySession() as session:
|
||||||
|
response = await session.post(target_url + "/protected/token", data=payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
session.token = response.json()["access_token"]
|
||||||
|
yield session
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture(scope="function")
|
||||||
|
async def test_user_fixture(request, auth_fixture: MySession, target_url:str):
|
||||||
|
|
||||||
|
test_id=uuid4()
|
||||||
|
|
||||||
|
direct_permission_param, group_param = request.param
|
||||||
|
|
||||||
|
new_user_record={
|
||||||
|
"first_name":f"TEST_{test_id}",
|
||||||
|
"last_name":f"TEST_{test_id}",
|
||||||
|
"middle_name":f"TEST_{test_id}",
|
||||||
|
"email":f"TEST_{test_id}@d.d",
|
||||||
|
"plain_password":"Test1234!",
|
||||||
|
"direct_permissions":direct_permission_param,
|
||||||
|
"group":group_param
|
||||||
|
}
|
||||||
|
response=await auth_fixture.post(f"{target_url}/user/create_user", json=new_user_record)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
|
||||||
|
async with MySession() as session:
|
||||||
|
|
||||||
|
payload={"username": new_user_record.get("email"), "password": new_user_record.get("plain_password")}
|
||||||
|
|
||||||
|
response = await session.post(target_url + "/protected/token", data=payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
session.token = response.json()["access_token"]
|
||||||
|
|
||||||
|
yield (session, new_user_record)
|
||||||
|
|
||||||
|
response=await auth_fixture.post(f"{target_url}/user/delete_user_hard", params={"email":new_user_record.get("email")})
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture()
|
||||||
|
async def target_url()->str:
|
||||||
|
return f"http://{e2e_settings.HOST}:{e2e_settings.PORT}"
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import allure
|
||||||
|
import pytest
|
||||||
|
from httpx import HTTPStatusError
|
||||||
|
|
||||||
|
from tests.e2e.conftest import MySession
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integra
|
||||||
|
class TestCrud:
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("test_user_fixture", [(["admin"], ["admin_group"])], indirect=True)
|
||||||
|
async def test_get_user_by_email_positive(self,test_user_fixture, target_url:str)->None:
|
||||||
|
|
||||||
|
session, new_user_record=test_user_fixture
|
||||||
|
|
||||||
|
with allure.step("Get user by email"):
|
||||||
|
|
||||||
|
email = new_user_record.get("email") #get email from the fixture in yield sector
|
||||||
|
|
||||||
|
response = await session.get(f"{target_url}/user/get_by_email",params={"email":email})
|
||||||
|
response.raise_for_status()
|
||||||
|
response=response.json()
|
||||||
|
|
||||||
|
with allure.step("Validate response"):
|
||||||
|
|
||||||
|
assert response.get("email")==email
|
||||||
|
assert "TEST_" in response.get("first_name")
|
||||||
|
assert "TEST_" in response.get("last_name")
|
||||||
|
assert "TEST_" in response.get("middle_name")
|
||||||
|
assert response.get("direct_permissions") != []
|
||||||
|
assert response.get("group") != []
|
||||||
|
assert not response.get("hashed_password") or not response.get("plain_password") or not response.get("password")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("email, expected_status", [
|
||||||
|
pytest.param("test@test.test", 404, id="non_existed_email"),
|
||||||
|
pytest.param("test",404, id="wrong_email"),
|
||||||
|
pytest.param("@d", 404, id="wrong_email")
|
||||||
|
])
|
||||||
|
async def test_get_user_by_email_negative(self, email:str, expected_status:int, auth_fixture:MySession,target_url:str)->None:
|
||||||
|
|
||||||
|
with allure.step("Get user by email"):
|
||||||
|
|
||||||
|
with pytest.raises(HTTPStatusError) as exc_info:
|
||||||
|
response = await auth_fixture.get(f"{target_url}/user/get_by_email", params={"email": email})
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
assert exc_info.value.response.status_code == expected_status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("new_user_record",[
|
||||||
|
pytest.param({ "first_name":"TEST",
|
||||||
|
"last_name":"TEST",
|
||||||
|
"middle_name":"TEST",
|
||||||
|
"email":f"TEST_{uuid4()}@d.d",
|
||||||
|
"plain_password":"Test1234!",
|
||||||
|
"direct_permissions":[],
|
||||||
|
"group":[]}, id="Positive_user_creation_with_all_the_fields"),
|
||||||
|
|
||||||
|
pytest.param({ "first_name":"TEST",
|
||||||
|
"last_name":"TEST",
|
||||||
|
"middle_name":"TEST",
|
||||||
|
"email":f"TEST_{uuid4()}@d.d",
|
||||||
|
"plain_password":"Test1234!",
|
||||||
|
"direct_permissions":["WRONG_PERMISSIONS"],
|
||||||
|
"group":["WRONG_GROUP"]},id="Positive_wrong_permissions"),
|
||||||
|
])
|
||||||
|
async def test_create_delete_user_hard_positive(self, new_user_record:dict,auth_fixture:MySession, target_url:str)->None:
|
||||||
|
|
||||||
|
with allure.step("Create new test user and check for the new user"):
|
||||||
|
|
||||||
|
response=await auth_fixture.post(f"{target_url}/user/create_user", json=new_user_record)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with allure.step("Check for the new user"):
|
||||||
|
|
||||||
|
response = await auth_fixture.get(f"{target_url}/user/get_by_email",params={"email":new_user_record.get("email")})
|
||||||
|
response.raise_for_status()
|
||||||
|
response=response.json()
|
||||||
|
|
||||||
|
assert response.get("email")==new_user_record["email"]
|
||||||
|
assert response.get("first_name")==new_user_record["first_name"]
|
||||||
|
assert response.get("last_name")==new_user_record["last_name"]
|
||||||
|
assert response.get("middle_name")==new_user_record["middle_name"]
|
||||||
|
assert response.get("direct_permissions") == new_user_record["direct_permissions"] or response.get("direct_permissions") == []
|
||||||
|
assert response.get("group") == new_user_record["group"] or response.get("group") == []
|
||||||
|
assert not response.get("hashed_password") or not response.get("plain_password") or not response.get("password")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
with allure.step("delete new user"):
|
||||||
|
response=await auth_fixture.post(f"{target_url}/user/delete_user_hard", params={"email":new_user_record["email"]})
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("new_user_record, expected_status", [
|
||||||
|
pytest.param({ "first_name":"TEST",
|
||||||
|
"last_name":"TEST",
|
||||||
|
"middle_name":"TEST",
|
||||||
|
"email":"WRONGEMAIL",
|
||||||
|
"plain_password":"Test1234!",
|
||||||
|
"direct_permissions":[],
|
||||||
|
"group":[]},422,id="Non_existed_email"),
|
||||||
|
|
||||||
|
pytest.param({ "first_name":"TEST",
|
||||||
|
"last_name":"TEST",
|
||||||
|
"middle_name":"TEST",
|
||||||
|
"email":"TEST1@d.d",
|
||||||
|
"plain_password":"1234",
|
||||||
|
"direct_permissions":[],
|
||||||
|
"group":[]},422,id="Wrong_password"),
|
||||||
|
|
||||||
|
pytest.param({ "first_name":"TEST",
|
||||||
|
"email":"TEST1@d.d",
|
||||||
|
"plain_password":"Test1234!",
|
||||||
|
},422,id="Not_all_the_fields"),
|
||||||
|
])
|
||||||
|
async def test_create_user_negative(self, new_user_record:dict, auth_fixture:MySession, expected_status:int, target_url:str):
|
||||||
|
|
||||||
|
with allure.step("Preparing data to create new user negative"):
|
||||||
|
|
||||||
|
user_created=False
|
||||||
|
|
||||||
|
try:
|
||||||
|
with allure.step("Create new test user and check for the new user"):
|
||||||
|
|
||||||
|
response=await auth_fixture.post(f"{target_url}/user/create_user", json=new_user_record)
|
||||||
|
if response.status_code < 400:
|
||||||
|
user_created = True
|
||||||
|
|
||||||
|
with pytest.raises(HTTPStatusError) as exc_info:
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
assert exc_info.value.response.status_code == expected_status
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if user_created:
|
||||||
|
with allure.step("delete new user"):
|
||||||
|
|
||||||
|
response = await auth_fixture.post(
|
||||||
|
f"{target_url}/user/delete_user_hard",
|
||||||
|
params={"email": new_user_record["email"]}
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("new_user_record",[
|
||||||
|
pytest.param({ "first_name":"TEST",
|
||||||
|
"last_name":"TEST",
|
||||||
|
"middle_name":"TEST",
|
||||||
|
"email":f"TEST_{uuid4()}@d.d",
|
||||||
|
"plain_password":"Test1234!",
|
||||||
|
"direct_permissions":[],
|
||||||
|
"group":[]}, id="Positive_user_delete_soft"),
|
||||||
|
])
|
||||||
|
async def test_user_create_delete_soft_positive(self, new_user_record:dict, auth_fixture:MySession, target_url:str)->None:
|
||||||
|
|
||||||
|
with allure.step("Create new test user and check for the new user"):
|
||||||
|
|
||||||
|
response=await auth_fixture.post(f"{target_url}/user/create_user", json=new_user_record)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with allure.step("Check for the new user"):
|
||||||
|
|
||||||
|
response = await auth_fixture.get(f"{target_url}/user/get_by_email",params={"email":new_user_record.get("email")})
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
with allure.step("Delete user soft"):
|
||||||
|
response=await auth_fixture.post(f"{target_url}/user/delete_user_soft", params={"email":new_user_record["email"]})
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
finally:
|
||||||
|
with allure.step("delete new user"):
|
||||||
|
response=await auth_fixture.post(f"{target_url}/user/delete_user_hard", params={"email":new_user_record["email"]})
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("email, expected_status, ",[
|
||||||
|
pytest.param("Test", 404,id="Wrong_email")
|
||||||
|
])
|
||||||
|
async def test_user_delete_soft_negative(self, email:str,expected_status:int, auth_fixture:MySession, target_url:str)->None:
|
||||||
|
|
||||||
|
with allure.step("Delete user soft"):
|
||||||
|
|
||||||
|
with pytest.raises(HTTPStatusError) as exc_info:
|
||||||
|
|
||||||
|
response=await auth_fixture.post(f"{target_url}/user/delete_user_soft", params={"email":email})
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
assert exc_info.value.response.status_code == expected_status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("email, expected_status, ",[
|
||||||
|
pytest.param("Test", 404,id="Wrong_email")
|
||||||
|
])
|
||||||
|
async def test_user_delete_hard_negative(self, email:str,expected_status:int, auth_fixture:MySession, target_url:str)->None:
|
||||||
|
|
||||||
|
with allure.step("Delete user hard"):
|
||||||
|
|
||||||
|
with pytest.raises(HTTPStatusError) as exc_info:
|
||||||
|
|
||||||
|
response=await auth_fixture.post(f"{target_url}/user/delete_user_soft", params={"email":email})
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
assert exc_info.value.response.status_code == expected_status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("test_user_fixture", [
|
||||||
|
(["admin"], ["admin_group"])
|
||||||
|
], indirect=True)
|
||||||
|
@pytest.mark.parametrize("user_record_to_update",[
|
||||||
|
pytest.param({"first_name": "Test_New"},id="Positive_user_update_partially")
|
||||||
|
])
|
||||||
|
async def test_user_update_partially_positive(self, test_user_fixture, user_record_to_update:dict, target_url:str)->None:
|
||||||
|
|
||||||
|
session, new_user_record=test_user_fixture
|
||||||
|
|
||||||
|
with allure.step("Update user"):
|
||||||
|
response= await session.patch(f"{target_url}/user/patch_user", json=user_record_to_update, params={"email":new_user_record["email"]})
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
with allure.step("Check for the updated user"):
|
||||||
|
response=await session.get(f"{target_url}/user/get_by_email", params={"email":new_user_record["email"]})
|
||||||
|
response.raise_for_status()
|
||||||
|
response=response.json()
|
||||||
|
|
||||||
|
actual_permissions = [item.get("permission") for item in response.get("direct_permissions")] #unpacking json like {group:[{},{}]}
|
||||||
|
actual_groups =[item.get("group") for item in response.get("group")]
|
||||||
|
|
||||||
|
|
||||||
|
assert response.get("email")==new_user_record["email"]
|
||||||
|
assert response.get("first_name")==user_record_to_update["first_name"]
|
||||||
|
assert response.get("last_name")==new_user_record["last_name"]
|
||||||
|
assert response.get("middle_name")==new_user_record["middle_name"]
|
||||||
|
assert actual_permissions == new_user_record["direct_permissions"]
|
||||||
|
assert actual_groups == new_user_record["group"]
|
||||||
|
assert not response.get("hashed_password") or not response.get("plain_password") or not response.get("password")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("test_user_fixture", [
|
||||||
|
([], [])
|
||||||
|
], indirect=True)
|
||||||
|
@pytest.mark.parametrize("user_record_to_update, expected_exception, expected_status",[
|
||||||
|
pytest.param({"plain_password": "Wrong_pass"},HTTPStatusError,422,id="Wrong_password"),
|
||||||
|
pytest.param({"email": "Wrong_email"},HTTPStatusError,422,id="Wrong_email"),
|
||||||
|
pytest.param({},HTTPStatusError, 400,id="Negative_user_update_nothing")
|
||||||
|
])
|
||||||
|
async def test_user_update_partially_negative(self, test_user_fixture, user_record_to_update:dict, expected_exception, expected_status:int, target_url:str)->None:
|
||||||
|
|
||||||
|
session, new_user_record=test_user_fixture
|
||||||
|
|
||||||
|
with allure.step("Update user"), pytest.raises(expected_exception) as exc_info:
|
||||||
|
|
||||||
|
response= await session.patch(f"{target_url}/user/patch_user", json=user_record_to_update, params={"email":new_user_record["email"]})
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
assert exc_info.value.response.status_code == expected_status
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import pytest_asyncio
|
||||||
|
from fastapi import Request
|
||||||
|
|
||||||
|
from src.service.auth.auth import CurrentUserService
|
||||||
|
from src.service.users_crud.users_crud import CrudService
|
||||||
|
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def current_user_service()->CurrentUserService:
|
||||||
|
current_user_service=CurrentUserService()
|
||||||
|
return current_user_service
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def requests(mocker):
|
||||||
|
fake_request = mocker.MagicMock(spec=Request)
|
||||||
|
fake_request.headers = {"user-agent": "pytest-agent", "x-forwarded-for":"127.0.0.1"}
|
||||||
|
return fake_request
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def crud_service()->CrudService:
|
||||||
|
crud_service=CrudService()
|
||||||
|
return crud_service
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import allure
|
||||||
|
import pytest
|
||||||
|
from fastapi import HTTPException, Request
|
||||||
|
from jose import jwt
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from src.models.configs_read.env import env_settings
|
||||||
|
from src.service.auth.auth import CurrentUserService
|
||||||
|
from src.service.auth.jwt import HashService, JwtService
|
||||||
|
|
||||||
|
current_user_service=CurrentUserService()
|
||||||
|
|
||||||
|
@pytest.mark.integra
|
||||||
|
class TestAuth:
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("user_data",[
|
||||||
|
pytest.param(SimpleNamespace(first_name="test",last_name="test",middle_name="test",email="d@d.d",direct_permissions=[],group=[],status=True), id="correct_data")
|
||||||
|
])
|
||||||
|
async def test_get_current_user_positive(self,current_user_service:CurrentUserService, jwt_service:JwtService, monkeypatch, user_data:SimpleNamespace)->None:
|
||||||
|
|
||||||
|
with allure.step("create token"):
|
||||||
|
|
||||||
|
token=await jwt_service.create_access_token({"sub":str(uuid4())})
|
||||||
|
|
||||||
|
with allure.step("patching db call functions"):
|
||||||
|
|
||||||
|
monkeypatch.setattr(current_user_service.crud_db_actions, "get_user_by_id", AsyncMock(return_value=user_data))
|
||||||
|
|
||||||
|
with allure.step("test get_current_user_with_fake_token"):
|
||||||
|
|
||||||
|
test_result= await current_user_service.get_current_user(token)
|
||||||
|
|
||||||
|
assert test_result.first_name==user_data.first_name
|
||||||
|
assert test_result.last_name==user_data.last_name
|
||||||
|
assert test_result.middle_name==user_data.middle_name
|
||||||
|
assert test_result.email==user_data.email
|
||||||
|
assert test_result.direct_permissions==user_data.direct_permissions
|
||||||
|
assert test_result.group==user_data.group
|
||||||
|
assert not hasattr(test_result, "password") or not hasattr(test_result, "plain_password") or not hasattr(test_result, "hashed_password")
|
||||||
|
assert not hasattr(test_result, "status")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("user_data, uuid, expected_exception,expected_status",[
|
||||||
|
pytest.param(SimpleNamespace(first_name="test",last_name="test",middle_name="test",email="d@d.d",direct_permissions=[],group=[],status=False), uuid4(), HTTPException,401, id="false_status"),
|
||||||
|
pytest.param(SimpleNamespace(first_name="test",last_name="test",middle_name="test",email="d@d.d",direct_permissions=[],group=[],status=True),1234, HTTPException,401, id="wrong_id"),
|
||||||
|
pytest.param(SimpleNamespace(status=True),uuid4(), ValidationError,None,id="empty_model_data")
|
||||||
|
])
|
||||||
|
async def test_get_current_user_negative(self,current_user_service:CurrentUserService, jwt_service:JwtService, monkeypatch, user_data:SimpleNamespace,expected_exception, expected_status:int, uuid)->None:
|
||||||
|
|
||||||
|
with allure.step("create token"):
|
||||||
|
|
||||||
|
token=await jwt_service.create_access_token({"sub":str(uuid)})
|
||||||
|
|
||||||
|
with allure.step("patching db call functions"):
|
||||||
|
|
||||||
|
monkeypatch.setattr(current_user_service.crud_db_actions, "get_user_by_id", AsyncMock(return_value=user_data))
|
||||||
|
|
||||||
|
with allure.step("test get_current_user_with_fake_token"), pytest.raises(expected_exception) as exc_info:
|
||||||
|
await current_user_service.get_current_user(token)
|
||||||
|
|
||||||
|
if expected_exception is HTTPException:
|
||||||
|
assert exc_info.value.status_code == expected_status
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("user_data, form_data_email,form_data_password",[
|
||||||
|
pytest.param(SimpleNamespace(id=uuid4(),hashed_password="1234", first_name="test",last_name="test",middle_name="test",email="d@d.d",direct_permissions=[],group=[],status=True), "d@d.d", "1234", id="correct_data"),
|
||||||
|
])
|
||||||
|
async def test_login_positive(self, jwt_service:JwtService,current_user_service:CurrentUserService, monkeypatch, user_data:SimpleNamespace, hash_service:HashService, form_data_email:str, form_data_password:str, requests)->None:
|
||||||
|
|
||||||
|
with allure.step("patching db call functions"):
|
||||||
|
|
||||||
|
user_data.hashed_password=hash_service.plain_to_hash(user_data.hashed_password)
|
||||||
|
monkeypatch.setattr(current_user_service.crud_db_actions, "get_user_by_email", AsyncMock(return_value=user_data))
|
||||||
|
monkeypatch.setattr(current_user_service.jwt_db_actions, "revoke_all", AsyncMock(return_value=True))
|
||||||
|
monkeypatch.setattr(current_user_service.jwt_db_actions, "create_token", AsyncMock(return_value=True))
|
||||||
|
|
||||||
|
fake_request = requests
|
||||||
|
|
||||||
|
with allure.step("test login_with_fake_data"):
|
||||||
|
access, refresh=await current_user_service.login(form_data_email, form_data_password,fake_request)
|
||||||
|
parts_a=access.split(".")
|
||||||
|
parts_b=refresh.split(".")
|
||||||
|
assert isinstance(access, str)
|
||||||
|
assert len(parts_a)==3
|
||||||
|
assert isinstance(refresh, str)
|
||||||
|
assert len(parts_b)==3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("user_data, form_data_email,form_data_password, expected_exception, expected_status",[
|
||||||
|
pytest.param(SimpleNamespace(id=uuid4(),hashed_password="1234", first_name="test",last_name="test",middle_name="test",email="d@d.d",direct_permissions=[],group=[],status=True), "d@d.d", "wrong_password", HTTPException,401, id="wrong_password"),
|
||||||
|
pytest.param(SimpleNamespace(id=uuid4(),hashed_password="1234", first_name="test",last_name="test",middle_name="test",email="d@d.d",direct_permissions=[],group=[],status=False), "d@d.d", "1234",HTTPException,401, id="false_status"),
|
||||||
|
pytest.param(SimpleNamespace(id=1234,hashed_password="1234", first_name="test",last_name="test",middle_name="test",email="d@d.d",direct_permissions=[],group=[],status=True), "d@d.d", "1234",ValidationError,None, id="wrong_id"),
|
||||||
|
])
|
||||||
|
async def test_login_negative(self, current_user_service:CurrentUserService, user_data:SimpleNamespace, jwt_service:JwtService, monkeypatch, requests, hash_service:HashService, form_data_email:str, form_data_password:str, expected_exception, expected_status:int):
|
||||||
|
|
||||||
|
with allure.step("patching db call functions"):
|
||||||
|
|
||||||
|
user_data.hashed_password=hash_service.plain_to_hash(user_data.hashed_password)
|
||||||
|
monkeypatch.setattr(current_user_service.crud_db_actions, "get_user_by_email", AsyncMock(return_value=user_data))
|
||||||
|
monkeypatch.setattr(current_user_service.jwt_db_actions, "revoke_all", AsyncMock(return_value=True))
|
||||||
|
monkeypatch.setattr(current_user_service.jwt_db_actions, "create_token", AsyncMock(return_value=True))
|
||||||
|
|
||||||
|
fake_request = requests
|
||||||
|
|
||||||
|
with allure.step("test login_with_fake_data"), pytest.raises(expected_exception) as exc_info:
|
||||||
|
|
||||||
|
await current_user_service.login(form_data_email, form_data_password,fake_request)
|
||||||
|
|
||||||
|
if expected_exception is HTTPException:
|
||||||
|
assert exc_info.value.status_code == expected_status
|
||||||
|
|
||||||
|
|
||||||
|
async def test_logout_positive(self, jwt_service:JwtService, monkeypatch, current_user_service:CurrentUserService)->None:
|
||||||
|
|
||||||
|
with allure.step("create fake refresh token"):
|
||||||
|
|
||||||
|
token=await jwt_service.create_refresh_token({"sub":str(uuid4())})
|
||||||
|
|
||||||
|
with allure.step("patching db call functions"):
|
||||||
|
|
||||||
|
monkeypatch.setattr(current_user_service.jwt_db_actions, "logout", AsyncMock(return_value=True))
|
||||||
|
|
||||||
|
with allure.step("test logout with fake data"):
|
||||||
|
|
||||||
|
status=await current_user_service.logout(token[0])
|
||||||
|
assert status is True
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("jti,db_result, expected_exception, expected_status",[
|
||||||
|
pytest.param(None, True, HTTPException,401, id="jti_none"),
|
||||||
|
pytest.param(1234, True, HTTPException,401, id="jti_int"),
|
||||||
|
pytest.param(str(uuid4()), False, HTTPException,404,id="db_result_none"),
|
||||||
|
])
|
||||||
|
async def test_logout_negative(self, jwt_service:JwtService, monkeypatch, current_user_service:CurrentUserService, expected_exception, jti, db_result, expected_status:int)->None:
|
||||||
|
|
||||||
|
with allure.step("patching db call functions"):
|
||||||
|
|
||||||
|
monkeypatch.setattr(current_user_service.jwt_db_actions, "logout", AsyncMock(return_value=db_result) )
|
||||||
|
|
||||||
|
async def fake_create_refresh_token(data:dict)->str:
|
||||||
|
return jwt.encode(data, env_settings.SECRET_KEY, env_settings.ALGORITHM)
|
||||||
|
|
||||||
|
monkeypatch.setattr(jwt_service, "create_refresh_token", fake_create_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)})
|
||||||
|
|
||||||
|
with allure.step("test logout with fake data"), pytest.raises(expected_exception) as exc_info:
|
||||||
|
|
||||||
|
await current_user_service.logout(token)
|
||||||
|
|
||||||
|
if expected_exception is HTTPException:
|
||||||
|
assert exc_info.value.status_code==expected_status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("db_result_token, user_data_result_db", [
|
||||||
|
pytest.param(SimpleNamespace(is_revoked=False, expires_at=datetime.now(UTC)+timedelta(days=15)),SimpleNamespace(status=True), id="correct_data")
|
||||||
|
])
|
||||||
|
async def test_refresh_token_positive(self, monkeypatch, current_user_service:CurrentUserService, db_result_token, requests:Request, jwt_service:JwtService,user_data_result_db )->None:
|
||||||
|
|
||||||
|
with allure.step("patching db call functions"):
|
||||||
|
|
||||||
|
monkeypatch.setattr(current_user_service.jwt_db_actions,"get_token_by_id", AsyncMock(return_value=db_result_token))
|
||||||
|
monkeypatch.setattr(current_user_service.jwt_db_actions, "revoke_all", AsyncMock(return_value=True))
|
||||||
|
monkeypatch.setattr(current_user_service.crud_db_actions, "get_user_by_id", AsyncMock(return_value=user_data_result_db))
|
||||||
|
monkeypatch.setattr(current_user_service.jwt_db_actions,"create_and_update_token", AsyncMock(return_value=True))
|
||||||
|
|
||||||
|
fake_request = requests
|
||||||
|
|
||||||
|
async def fake_create_refresh_token(data:dict)->str:
|
||||||
|
return jwt.encode(data, env_settings.SECRET_KEY, env_settings.ALGORITHM)
|
||||||
|
|
||||||
|
monkeypatch.setattr(jwt_service, "create_refresh_token", fake_create_refresh_token)
|
||||||
|
|
||||||
|
with allure.step("create fake refresh token"):
|
||||||
|
|
||||||
|
token=await fake_create_refresh_token({"sub":str(uuid4()), "jti":str(uuid4()), "token_type":"refresh", "exp":datetime.now(UTC)+timedelta(days=45)})
|
||||||
|
|
||||||
|
with allure.step("test refresh token with fake data"):
|
||||||
|
|
||||||
|
new_access_token, new_refresh_token = await current_user_service.refresh_token(token, fake_request)
|
||||||
|
parts_a=new_access_token.split(".")
|
||||||
|
parts_b=new_refresh_token.split(".")
|
||||||
|
assert isinstance(new_access_token, str)
|
||||||
|
assert len(parts_a)==3
|
||||||
|
assert isinstance(new_refresh_token, str)
|
||||||
|
assert len(parts_b)==3
|
||||||
|
assert new_access_token!=new_refresh_token
|
||||||
|
assert new_access_token!=token
|
||||||
|
assert new_refresh_token!=token
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("db_result_token, user_data_result_db, update_result, fake_token_data,expected_exception, expected_status", [
|
||||||
|
pytest.param(SimpleNamespace(is_revoked=True,user_id="123", expires_at=datetime.now(UTC)+timedelta(days=15)),SimpleNamespace(status=True),False,{"sub":str(uuid4()), "jti":str(uuid4()), "token_type":"refresh", "exp":datetime.now(UTC)+timedelta(days=45)}, HTTPException,404,id="false_revoke_status"),
|
||||||
|
pytest.param(SimpleNamespace(is_revoked=False,user_id="123", expires_at=datetime.now(UTC)+timedelta(days=15)),SimpleNamespace(status=True),True,{"sub":str(uuid4()), "token_type":"refresh", "exp":datetime.now(UTC)+timedelta(days=45)}, HTTPException,401, id="jti_missing"),
|
||||||
|
pytest.param(None,SimpleNamespace(status=True),True,{"sub":str(uuid4()), "jti":str(uuid4()),"token_type":"refresh", "exp":datetime.now(UTC)+timedelta(days=45)}, HTTPException, 404,id="token_missing"),
|
||||||
|
pytest.param(SimpleNamespace(is_revoked=False,user_id="123", expires_at=datetime.now(UTC)+timedelta(days=15)),SimpleNamespace(status=False),True,{"sub":str(uuid4()), "jti":str(uuid4()), "token_type":"refresh", "exp":datetime.now(UTC)+timedelta(days=45)}, HTTPException,401,id="false_user_status"),
|
||||||
|
pytest.param(SimpleNamespace(is_revoked=False, user_id="123",expires_at=datetime.now(UTC)+timedelta(days=15)),None,True,{"sub":str(uuid4()), "jti":str(uuid4()), "token_type":"refresh", "exp":datetime.now(UTC)+timedelta(days=45)}, HTTPException,404,id="user_missing"),
|
||||||
|
pytest.param(SimpleNamespace(is_revoked=False,user_id="123", expires_at=datetime.now(UTC)-timedelta(days=15)),SimpleNamespace(status=True),True,{"sub":str(uuid4()), "jti":str(uuid4()), "token_type":"refresh", "exp":datetime.now(UTC)+timedelta(days=45)}, HTTPException,401,id="wrong_exp")
|
||||||
|
])
|
||||||
|
async def test_refresh_token_negative(self, monkeypatch, current_user_service:CurrentUserService, db_result_token, requests, jwt_service:JwtService,user_data_result_db, expected_exception, fake_token_data, update_result, expected_status:int)->None:
|
||||||
|
with allure.step("patching db call functions"):
|
||||||
|
monkeypatch.setattr(current_user_service.jwt_db_actions,"get_token_by_id", AsyncMock(return_value=db_result_token))
|
||||||
|
monkeypatch.setattr(current_user_service.jwt_db_actions, "revoke_all", AsyncMock(return_value=True))
|
||||||
|
monkeypatch.setattr(current_user_service.crud_db_actions, "get_user_by_id", AsyncMock(return_value=user_data_result_db))
|
||||||
|
monkeypatch.setattr(current_user_service.jwt_db_actions, "create_and_update_token", AsyncMock(return_value=update_result))
|
||||||
|
|
||||||
|
fake_request = requests
|
||||||
|
|
||||||
|
async def fake_create_refresh_token(data:dict)->str:
|
||||||
|
return jwt.encode(data, env_settings.SECRET_KEY, env_settings.ALGORITHM)
|
||||||
|
|
||||||
|
monkeypatch.setattr(jwt_service, "create_refresh_token", fake_create_refresh_token)
|
||||||
|
|
||||||
|
with allure.step("create fake refresh token"):
|
||||||
|
token=await fake_create_refresh_token(fake_token_data)
|
||||||
|
|
||||||
|
with allure.step("test refresh token with fake data"), pytest.raises(expected_exception) as exc_info:
|
||||||
|
await current_user_service.refresh_token(token, fake_request)
|
||||||
|
|
||||||
|
if expected_exception is HTTPException:
|
||||||
|
assert exc_info.value.status_code==expected_status
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
import allure
|
||||||
|
import pytest
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from src.models.pydantic_models.model import UserCreate, UserUpdate
|
||||||
|
from src.service.users_crud.users_crud import CrudService
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.integra
|
||||||
|
class TestCrud:
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("user_data", [
|
||||||
|
pytest.param(SimpleNamespace(first_name="test",last_name="test",middle_name="test",email="d@d.d",direct_permissions=[],group=[], plain_password="Test1234!"),id="Create_user_positive")
|
||||||
|
])
|
||||||
|
async def test_create_user_positive(self, crud_service:CrudService, monkeypatch, user_data:SimpleNamespace)->None:
|
||||||
|
|
||||||
|
with allure.step("Patching functions"):
|
||||||
|
monkeypatch.setattr(crud_service.crud_db_actions, "create_user", AsyncMock(return_value=user_data))
|
||||||
|
|
||||||
|
with allure.step("Test Create User"):
|
||||||
|
await crud_service.create_user(UserCreate.model_validate(user_data))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("user_data,expected_exception,expected_status ", [
|
||||||
|
pytest.param(SimpleNamespace(first_name="test",last_name="test",middle_name="test",email="d@d.d",direct_permissions=[],group=[], plain_password="Test1234!"),HTTPException,422,id="Create_user_None")
|
||||||
|
])
|
||||||
|
async def test_create_user_negative(self, crud_service:CrudService, monkeypatch, user_data:SimpleNamespace, expected_exception, expected_status:int)->None:
|
||||||
|
|
||||||
|
with allure.step("Patching functions"):
|
||||||
|
monkeypatch.setattr(crud_service.crud_db_actions, "create_user", AsyncMock(return_value=None))
|
||||||
|
|
||||||
|
with allure.step("Test Create User"), pytest.raises(expected_exception) as exc_info:
|
||||||
|
await crud_service.create_user(UserCreate.model_validate(user_data))
|
||||||
|
|
||||||
|
if expected_exception is HTTPException:
|
||||||
|
assert exc_info.value.status_code == expected_status
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("user_data", [
|
||||||
|
pytest.param(SimpleNamespace(first_name="test",last_name="test",middle_name="test",email="d@d.d",direct_permissions=[],group=[], plain_password="Test1234!"),id="Create_user_positive")
|
||||||
|
])
|
||||||
|
async def test_update_user_positive(self, crud_service:CrudService, monkeypatch, user_data:SimpleNamespace)->None:
|
||||||
|
|
||||||
|
with allure.step("Patching functions"):
|
||||||
|
monkeypatch.setattr(crud_service.crud_db_actions, "update_user_partially", AsyncMock(return_value=user_data))
|
||||||
|
|
||||||
|
with allure.step("Test Update User"):
|
||||||
|
await crud_service.update_user(user_data.email,UserUpdate.model_validate(user_data))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("user_data,expected_exception,expected_status ", [
|
||||||
|
pytest.param(SimpleNamespace(first_name="test",last_name="test",middle_name="test",email="d@d.d",direct_permissions=[],group=[], plain_password="Test1234!"),HTTPException,404,id="Update_user_None"),
|
||||||
|
pytest.param(SimpleNamespace(),HTTPException,400,id="Empty_user_data")
|
||||||
|
])
|
||||||
|
async def test_update_user_negative(self, crud_service:CrudService, monkeypatch, user_data:SimpleNamespace, expected_exception, expected_status:int)->None:
|
||||||
|
|
||||||
|
with allure.step("Patching functions"):
|
||||||
|
monkeypatch.setattr(crud_service.crud_db_actions, "update_user_partially", AsyncMock(return_value=None))
|
||||||
|
|
||||||
|
with allure.step("Test Update User"), pytest.raises(expected_exception) as exc_info:
|
||||||
|
await crud_service.update_user("d@d.d",UserUpdate.model_validate(user_data))
|
||||||
|
|
||||||
|
if expected_exception is HTTPException:
|
||||||
|
assert exc_info.value.status_code == expected_status
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
import allure
|
||||||
|
import pytest
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
from src.service.users_crud.users_crud import CrudService
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
class TestCrud:
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("user_data",[
|
||||||
|
pytest.param(SimpleNamespace(first_name="test",last_name="test",middle_name="test",email="d@d.d",direct_permissions=[],group=[],status=True),id="Get_user_by_email_positive")
|
||||||
|
])
|
||||||
|
async def test_get_user_by_email_positive(self, monkeypatch, user_data:SimpleNamespace, crud_service:CrudService)->None:
|
||||||
|
|
||||||
|
with allure.step("Patching functions"):
|
||||||
|
monkeypatch.setattr(crud_service.crud_db_actions, "get_user_by_email",AsyncMock(return_value=user_data))
|
||||||
|
|
||||||
|
with allure.step("Test get_by_email"):
|
||||||
|
test_result = await crud_service.get_user_by_email(user_data.email)
|
||||||
|
|
||||||
|
assert test_result.email==user_data.email
|
||||||
|
assert test_result.first_name==user_data.first_name
|
||||||
|
assert test_result.last_name==user_data.last_name
|
||||||
|
assert test_result.middle_name==user_data.middle_name
|
||||||
|
assert test_result.email==user_data.email
|
||||||
|
assert test_result.email==user_data.email
|
||||||
|
assert not hasattr(test_result, "password") or not hasattr(test_result, "plain_password") or not hasattr(test_result, "hashed_password")
|
||||||
|
assert test_result.direct_permissions==user_data.direct_permissions
|
||||||
|
assert test_result.group==user_data.group
|
||||||
|
assert not hasattr(test_result, "status")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("email, expected_exception, expected_status",[
|
||||||
|
pytest.param("Wrong_email", HTTPException, 404,id="Wrong_email"),
|
||||||
|
pytest.param("",HTTPException, 404,id="Empty_email"),
|
||||||
|
])
|
||||||
|
async def test_get_user_by_email_negative(self, email, crud_service:CrudService, expected_exception, expected_status:int)->None:
|
||||||
|
|
||||||
|
with allure.step("Test get_by_email"), pytest.raises(expected_exception) as exc_info:
|
||||||
|
|
||||||
|
await crud_service.get_user_by_email(email)
|
||||||
|
|
||||||
|
if expected_exception is HTTPException:
|
||||||
|
assert exc_info.value.status_code==expected_status
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("user_data",[
|
||||||
|
pytest.param(SimpleNamespace(first_name="test",last_name="test",middle_name="test",email="d@d.d",direct_permissions=[],group=[],status=True),id="Get_user_by_email_positive")
|
||||||
|
])
|
||||||
|
async def test_delete_user_soft_positive(self, monkeypatch, user_data:SimpleNamespace, crud_service:CrudService)->None:
|
||||||
|
|
||||||
|
with allure.step("Patching functions"):
|
||||||
|
monkeypatch.setattr(crud_service.crud_db_actions, "delete_user_soft",AsyncMock(return_value=True))
|
||||||
|
|
||||||
|
with allure.step("Test get_by_email"):
|
||||||
|
test_result = await crud_service.delete_user_soft(user_data.email)
|
||||||
|
|
||||||
|
assert test_result == True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("email, expected_exception, expected_status",[
|
||||||
|
pytest.param("Wrong_email", HTTPException, 404,id="Wrong_email"),
|
||||||
|
pytest.param("",HTTPException, 404,id="Empty_email"),
|
||||||
|
])
|
||||||
|
async def test_delete_user_soft_negative(self, email, crud_service:CrudService, expected_exception, expected_status:int)->None:
|
||||||
|
|
||||||
|
with allure.step("Test get_by_email"), pytest.raises(expected_exception) as exc_info:
|
||||||
|
|
||||||
|
await crud_service.delete_user_soft(email)
|
||||||
|
|
||||||
|
if expected_exception is HTTPException:
|
||||||
|
assert exc_info.value.status_code==expected_status
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("user_data",[
|
||||||
|
pytest.param(SimpleNamespace(first_name="test",last_name="test",middle_name="test",email="d@d.d",direct_permissions=[],group=[],status=True),id="Get_user_by_email_positive")
|
||||||
|
])
|
||||||
|
async def test_delete_user_hard_positive(self, monkeypatch, user_data:SimpleNamespace, crud_service:CrudService)->None:
|
||||||
|
|
||||||
|
with allure.step("Patching functions"):
|
||||||
|
monkeypatch.setattr(crud_service.crud_db_actions, "delete_user_hard",AsyncMock(return_value=True))
|
||||||
|
|
||||||
|
with allure.step("Test get_by_email"):
|
||||||
|
test_result = await crud_service.delete_user_hard(user_data.email, "current_user")
|
||||||
|
|
||||||
|
assert test_result==True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("email, expected_exception, expected_status",[
|
||||||
|
pytest.param("Wrong_email", HTTPException, 404,id="Wrong_email"),
|
||||||
|
pytest.param("",HTTPException, 404,id="Empty_email"),
|
||||||
|
])
|
||||||
|
async def test_delete_user_hard_negative(self, email, crud_service:CrudService, expected_exception, expected_status:int)->None:
|
||||||
|
|
||||||
|
with allure.step("Test get_by_email"), pytest.raises(expected_exception) as exc_info:
|
||||||
|
|
||||||
|
await crud_service.delete_user_hard(email, "current_user")
|
||||||
|
|
||||||
|
if expected_exception is HTTPException:
|
||||||
|
assert exc_info.value.status_code==expected_status
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
|
import allure
|
||||||
|
import pytest
|
||||||
|
from fastapi import HTTPException
|
||||||
|
from jose import jwt
|
||||||
|
|
||||||
|
from src.models.configs_read.env import env_settings
|
||||||
|
from src.service.auth.jwt import HashService, JwtService
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
class TestJwt:
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data", [
|
||||||
|
pytest.param({"sub": "123"}, id="full_sub")
|
||||||
|
])
|
||||||
|
async def test_access_create_positive(self, jwt_service:JwtService, data:dict)->None:
|
||||||
|
|
||||||
|
with allure.step("create correct access token"):
|
||||||
|
token = await jwt_service.create_access_token(data)
|
||||||
|
parts=token.split(".")
|
||||||
|
assert isinstance(token, str)
|
||||||
|
assert len(parts)==3
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data, expected_exception, expected_status",[
|
||||||
|
pytest.param("", AttributeError,None,id="not_dict_value"),
|
||||||
|
pytest.param({"sub":""},HTTPException,401,id="empty_value"),
|
||||||
|
pytest.param({"":""},HTTPException,401,id="empty_key_value")
|
||||||
|
])
|
||||||
|
async def test_access_create_negative(self, jwt_service:JwtService, data:dict, expected_exception, expected_status:int)->None:
|
||||||
|
with allure.step("create invalid access token"),pytest.raises(expected_exception) as exc_info:
|
||||||
|
await jwt_service.create_access_token(data)
|
||||||
|
if expected_exception is HTTPException:
|
||||||
|
assert expected_status == exc_info.value.status_code
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data", [
|
||||||
|
pytest.param({"sub": "123"}, id="full_sub")
|
||||||
|
])
|
||||||
|
async def test_refresh_create_positive(self, jwt_service:JwtService, data:dict)->None:
|
||||||
|
|
||||||
|
with allure.step("create correct access token"):
|
||||||
|
token = await jwt_service.create_refresh_token(data)
|
||||||
|
parts=token[0].split(".")
|
||||||
|
assert isinstance(token[0], str)
|
||||||
|
assert isinstance(token[1], str)
|
||||||
|
assert len(parts)==3
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data, expected_exception",[
|
||||||
|
pytest.param("", AttributeError,id="not_dict_value"),
|
||||||
|
pytest.param({"sub":""},HTTPException, id="empty_value"),
|
||||||
|
pytest.param({"":""},HTTPException, id="empty_key_value")
|
||||||
|
])
|
||||||
|
async def test_refresh_create_negative(self, jwt_service:JwtService, data, expected_exception)->None:
|
||||||
|
with allure.step("create invalid access token"), pytest.raises(expected_exception):
|
||||||
|
await jwt_service.create_refresh_token(data)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data", [
|
||||||
|
pytest.param({"sub": "123", "exp":datetime.now(UTC)+timedelta(minutes=15), "token_type":"access"}, id="correct_data"),
|
||||||
|
])
|
||||||
|
async def test_jwt_decode_positive(self, data:dict, monkeypatch, jwt_service:JwtService)->None:
|
||||||
|
|
||||||
|
with allure.step("patch a create token function"):
|
||||||
|
async def fake_create_access_token(data:dict)->str:
|
||||||
|
return jwt.encode(data, env_settings.SECRET_KEY, env_settings.ALGORITHM)
|
||||||
|
monkeypatch.setattr(jwt_service, "create_access_token", fake_create_access_token)
|
||||||
|
|
||||||
|
with allure.step("create and decode correct token"):
|
||||||
|
fake_token = await jwt_service.create_access_token(data)
|
||||||
|
payload=await jwt_service.jwt_decode(fake_token)
|
||||||
|
assert payload.get("sub")
|
||||||
|
assert payload.get("exp")
|
||||||
|
assert payload.get("token_type")
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("data, expected_exception, expected_status", [
|
||||||
|
pytest.param({"sub": "123", "exp":datetime.now(UTC)-timedelta(minutes=15), "token_type":"access"}, HTTPException,401, id="wrong_exp"),
|
||||||
|
pytest.param({"sub": "123", "exp":datetime.now(UTC)+timedelta(minutes=15)}, HTTPException, 401,id="no_token_type"),
|
||||||
|
pytest.param({"sub": "123", "token_type":"access"}, HTTPException,401,id="no_exp"),
|
||||||
|
pytest.param({}, HTTPException, 401,id="empty_data"),
|
||||||
|
pytest.param("", AttributeError, None,id="not_dict_data")
|
||||||
|
])
|
||||||
|
async def test_jwt_decode_invalid(self,jwt_service:JwtService, expected_exception, data, monkeypatch, expected_status)->None:
|
||||||
|
with allure.step("patch a create token function"):
|
||||||
|
async def fake_create_access_token(data:dict)->str:
|
||||||
|
return jwt.encode(data, env_settings.SECRET_KEY, env_settings.ALGORITHM)
|
||||||
|
monkeypatch.setattr(jwt_service, "create_access_token", fake_create_access_token)
|
||||||
|
|
||||||
|
with allure.step("create and decode invalid token"), pytest.raises(expected_exception) as exc_info:
|
||||||
|
fake_token=await jwt_service.create_access_token(data)
|
||||||
|
await jwt_service.jwt_decode(fake_token)
|
||||||
|
|
||||||
|
if expected_exception is HTTPException:
|
||||||
|
assert expected_status == exc_info.value.status_code
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("time, key, algorithm", [
|
||||||
|
pytest.param(15, "wrong_key", "HS256",id="wrong_key"),
|
||||||
|
pytest.param(-15, "correct_key", "HS256", id="wrong_time"),
|
||||||
|
pytest.param(15, "correct_key", "HS512", id="wrong_algorithm"),
|
||||||
|
])
|
||||||
|
async def test_jwt_decode_wrong_env(self, monkeypatch, time:int, key:str, algorithm:str, jwt_service)->None:
|
||||||
|
|
||||||
|
with allure.step("patch a create token function"):
|
||||||
|
async def fake_create_access_token(data:dict, key:str, algorithm:str)->str:
|
||||||
|
data.update({"exp":datetime.now(UTC)+timedelta(minutes=time)})
|
||||||
|
return jwt.encode(data, key, algorithm)
|
||||||
|
monkeypatch.setattr(jwt_service, "create_access_token", fake_create_access_token)
|
||||||
|
|
||||||
|
with allure.step("create and decode token with wrong data inside"), pytest.raises(HTTPException):
|
||||||
|
fake_token=await jwt_service.create_access_token({"sub": "123", "token_type":"access"}, key, algorithm)
|
||||||
|
await jwt_service.jwt_decode(fake_token)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("password",[
|
||||||
|
pytest.param("plain_password", id="correct_plain_password")
|
||||||
|
])
|
||||||
|
async def test_hash_and_veryfy_positive(self, password:str, hash_service:HashService)->None:
|
||||||
|
|
||||||
|
with allure.step("encode password"):
|
||||||
|
encoded_password=hash_service.plain_to_hash(password)
|
||||||
|
assert isinstance(encoded_password, str)
|
||||||
|
assert encoded_password!=password
|
||||||
|
|
||||||
|
with allure.step("decode password"):
|
||||||
|
assert hash_service.verify_password(password, encoded_password) is True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def test_verify_wrong_password(self, hash_service:HashService)->None:
|
||||||
|
|
||||||
|
with allure.step("encode password"):
|
||||||
|
encoded_password=hash_service.plain_to_hash("plain_password")
|
||||||
|
|
||||||
|
with allure.step("decode password"):
|
||||||
|
assert hash_service.verify_password("wrong_password", encoded_password) is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_token_to_hash_determistic(self, hash_service:HashService)->None:
|
||||||
|
assert hash_service.token_to_hash("abc")==hash_service.token_to_hash("abc")
|
||||||
|
|
||||||
|
async def test_token_to_hash_different_input(self, hash_service:HashService)->None:
|
||||||
|
assert hash_service.token_to_hash("abc")!=hash_service.token_to_hash("xyz")
|
||||||
|
|
||||||
Reference in New Issue
Block a user