├── .coverage ├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── README.md ├── alembic.ini ├── front ├── button │ └── button.py ├── header.py └── header │ └── header.py ├── main.py ├── migrations ├── README ├── env.py ├── script.py.mako └── versions │ ├── 2024_03_14_1433-3722e79049fb_added_users_table.py │ ├── 2024_03_14_1732-53937fceb381_changed_user_refresh_token_to_nullable.py │ ├── 2024_03_17_1708-d1c5aa9b4ecc_made_user_email_unique.py │ ├── 2024_03_26-6040e6a7be5b_added_work_and_supply_tables.py │ ├── 2024_03_26-e4ca7f85e87c_added_work_supply.py │ ├── 2024_03_27-71671dbc53c0_added_master.py │ ├── 2024_03_28-0f2b136116fd_added_timezone.py │ ├── 2024_03_28-c717f3475dc3_added_master_to_order_table.py │ └── 2024_03_28-db5c44566279_added_user.py ├── poetry.lock ├── pyproject.toml ├── src ├── __init__.py ├── auth │ ├── __init__.py │ ├── config.py │ ├── dependencies.py │ ├── models.py │ ├── router.py │ ├── schemas.py │ ├── service.py │ ├── user_rep.py │ └── utils.py ├── config.py ├── database.py ├── dependencies.py ├── errors │ ├── __init__.py │ ├── excaptions.py │ ├── handlers.py │ └── schema.py ├── masters │ ├── models.py │ ├── repository.py │ ├── router.py │ ├── schemas.py │ └── service.py ├── orders │ ├── models.py │ ├── repository.py │ ├── router.py │ ├── schemas.py │ └── service.py ├── repository.py ├── service.py ├── uow.py └── works │ ├── dependencies.py │ ├── models.py │ ├── repositories.py │ ├── router.py │ ├── schemas.py │ └── service.py └── tests ├── auth ├── test_auth_service.py ├── test_jwt_generation.py ├── test_password_hashing.py └── test_user_rep.py ├── conftest.py └── e2e └── test_auth.py /.coverage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Staks-sor/Fast_service/1a9cc753895e7337eac6e917c2371e61af4b3967/.coverage -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Start deploy 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | deploy: 9 | name: Deploy to server 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout repository 14 | - name: SSH to server and update repository 15 | run: | 16 | ssh 176.123.165.113 "cd /path/to/repository && git pull" 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .python-version 2 | .venv 3 | .idea 4 | **/.env 5 | **/.test.env 6 | **__pycache__** 7 | **cache** 8 | .vscode 9 | Makefile 10 | htmlcov -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Проект на FASTAPI для автосервиса 2 | 3 | Этот проект представляет собой веб-приложение на основе FASTAPI для автосервиса. Здесь вы найдете функционал, который поможет автосервису эффективно управлять своей работой, взаимодействовать с клиентами и отслеживать выполнение заказов. 4 | 5 | ## Основные функции: 6 | - Регистрация клиентов и автомобилей 7 | - Управление заказами и расписанием работы 8 | - Отслеживание статуса выполнения заказов 9 | - Коммуникация с клиентами через уведомления и обратную связь 10 | 11 | ## Технологии, использованные в проекте: 12 | - FASTAPI - быстрый веб-фреймворк для создания API 13 | - Python - основной язык программирования 14 | - HTML/CSS/JavaScript - для создания пользовательского интерфейса 15 | - База данных (например, SQLite) для хранения данных 16 | 17 | ## Установка и запуск: 18 | 1. Клонируйте репозиторий: 19 | 20 | git clone https://github.com/Staks-sor/Fast_service.git 21 | 22 | 2. Установите зависимости: 23 | 24 | pip install -r requirements.txt 25 | 26 | 3. Запустите приложение: 27 | 28 | uvicorn main:app --reload 29 | 30 | ## Использование poetry для зависимостей 31 | ### Установка 32 | 33 | **poetry** возможно установить двумя способами: 34 | 35 | 1. C помощью пакетного менеджера pip 36 | ``` 37 | pip install poetry 38 | ``` 39 | 40 | 2. скачать с оф. сайта и установить напрямую 41 | (данная команда работает на macOS, linux, Windows(WSL)) 42 | 43 | ``` bash 44 | curl -sSL https://install.python-poetry.org | python3 - 45 | ``` 46 | ### Конфигурация 47 | 48 | Всю информацию о конфигурации **poetry** можно найти на официальном [сайте](https://python-poetry.org/docs/configuration/). Здесь хочу лишь упомянуть настройку виртуального окружения. 49 | 50 | Для того чтобы виртуальное окружение создавалось внутри текущей директории, а не где то в директории кэша необходимо установить флаг **virtualenvs.in-project** 51 | 52 | ``` 53 | poetry config virtalenvs.in-project true 54 | ``` 55 | 56 | ### Запуск виртаульного окружения и установка зависимостей 57 | 58 | Для запуска виртуального окружения используется команда 59 | ``` 60 | poetry shell 61 | ``` 62 | а для выхода из него просто 63 | 64 | ``` 65 | exit 66 | ``` 67 | 68 | --- 69 | 70 | Все зависимости фиксируются в файлах **poetry.lock** и **pyproject.toml**. Для того чтобы установить все указанные в этих файлах зависимости используется команда 71 | 72 | ``` 73 | poetry install 74 | ``` 75 | В данном проекте зависимости разделены на группы (пока prod и dev). указанная выше команда установит лишь prod зависимости. Для установки всех зависимостей необходимо использовать команду 76 | 77 | ``` 78 | poetry install --with dev 79 | ``` 80 | ### Добавление новых зависимостей 81 | Для добавления новой зависимости в **prod**: 82 | ``` 83 | poetry add [package-name]@[version] 84 | ``` 85 | Версия опциональна, если она не указана будет установлена последняя версия пакета 86 | 87 | Например: 88 | ``` 89 | poetry add fastapi 90 | ``` 91 | --- 92 | Для добавления новых зависимостей в в группу **dev**: 93 | ``` 94 | poetry add pytest --group dev 95 | ``` -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | 4 | [alembic] 5 | # path to migration scripts 6 | script_location = migrations 7 | 8 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 9 | # Uncomment the line below if you want the files to be prepended with date and time 10 | # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file 11 | # for all available tokens 12 | file_template = %%(year)d_%%(month).2d_%%(day).2d-%%(rev)s_%%(slug)s 13 | 14 | # sys.path path, will be prepended to sys.path if present. 15 | # defaults to the current working directory. 16 | prepend_sys_path = . 17 | 18 | # timezone to use when rendering the date within the migration file 19 | # as well as the filename. 20 | # If specified, requires the python>=3.9 or backports.zoneinfo library. 21 | # Any required deps can installed by adding `alembic[tz]` to the pip requirements 22 | # string value is passed to ZoneInfo() 23 | # leave blank for localtime 24 | # timezone = 25 | 26 | # max length of characters to apply to the 27 | # "slug" field 28 | # truncate_slug_length = 40 29 | 30 | # set to 'true' to run the environment during 31 | # the 'revision' command, regardless of autogenerate 32 | # revision_environment = false 33 | 34 | # set to 'true' to allow .pyc and .pyo files without 35 | # a source .py file to be detected as revisions in the 36 | # versions/ directory 37 | # sourceless = false 38 | 39 | # version location specification; This defaults 40 | # to migrations/versions. When using multiple version 41 | # directories, initial revisions must be specified with --version-path. 42 | # The path separator used here should be the separator specified by "version_path_separator" below. 43 | # version_locations = %(here)s/bar:%(here)s/bat:migrations/versions 44 | 45 | # version path separator; As mentioned above, this is the character used to split 46 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 47 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 48 | # Valid values for version_path_separator are: 49 | # 50 | # version_path_separator = : 51 | # version_path_separator = ; 52 | # version_path_separator = space 53 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 54 | 55 | # set to 'true' to search source files recursively 56 | # in each "version_locations" directory 57 | # new in Alembic version 1.10 58 | # recursive_version_locations = false 59 | 60 | # the output encoding used when revision files 61 | # are written from script.py.mako 62 | # output_encoding = utf-8 63 | 64 | sqlalchemy.url = %(db_dsn)s 65 | 66 | 67 | 68 | [post_write_hooks] 69 | # post_write_hooks defines scripts or Python functions that are run 70 | # on newly generated revision scripts. See the documentation for further 71 | # detail and examples 72 | 73 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 74 | # hooks = black 75 | # black.type = console_scripts 76 | # black.entrypoint = black 77 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 78 | 79 | # lint with attempts to fix using "ruff" - use the exec runner, execute a binary 80 | # hooks = ruff 81 | # ruff.type = exec 82 | # ruff.executable = %(here)s/.venv/bin/ruff 83 | # ruff.options = --fix REVISION_SCRIPT_FILENAME 84 | 85 | # Logging configuration 86 | [loggers] 87 | keys = root,sqlalchemy,alembic 88 | 89 | [handlers] 90 | keys = console 91 | 92 | [formatters] 93 | keys = generic 94 | 95 | [logger_root] 96 | level = WARN 97 | handlers = console 98 | qualname = 99 | 100 | [logger_sqlalchemy] 101 | level = WARN 102 | handlers = 103 | qualname = sqlalchemy.engine 104 | 105 | [logger_alembic] 106 | level = INFO 107 | handlers = 108 | qualname = alembic 109 | 110 | [handler_console] 111 | class = StreamHandler 112 | args = (sys.stderr,) 113 | level = NOTSET 114 | formatter = generic 115 | 116 | [formatter_generic] 117 | format = %(levelname)-5.5s [%(name)s] %(message)s 118 | datefmt = %H:%M:%S 119 | -------------------------------------------------------------------------------- /front/button/button.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Staks-sor/Fast_service/1a9cc753895e7337eac6e917c2371e61af4b3967/front/button/button.py -------------------------------------------------------------------------------- /front/header.py: -------------------------------------------------------------------------------- 1 | import flet as ft 2 | 3 | 4 | def main(page: ft.Page): 5 | page.title = "хз" 6 | 7 | def route_change(route): 8 | page.views.clear() 9 | page.views.append( 10 | ft.View( 11 | "/", 12 | [ 13 | ft.AppBar(title=ft.Text("Автосервис"), bgcolor=ft.colors.SURFACE_VARIANT), 14 | 15 | ], 16 | ) 17 | ) 18 | 19 | page.update() 20 | 21 | def view_pop(view): 22 | page.views.pop() 23 | top_view = page.views[-1] 24 | page.go(top_view.route) 25 | 26 | page.on_route_change = route_change 27 | page.on_view_pop = view_pop 28 | page.go(page.route) 29 | 30 | 31 | ft.app(target=main, view=ft.AppView.WEB_BROWSER) 32 | -------------------------------------------------------------------------------- /front/header/header.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | import flet as ft 4 | from flet import View, Page, AppBar, ElevatedButton, Text 5 | from flet import RouteChangeEvent, ViewPopEvent, CrossAxisAlignment, MainAxisAlignment 6 | import requests 7 | from flet_core import TextAlign 8 | 9 | 10 | def main(page: ft.Page): 11 | # Устанавливаем заголовок страницы 12 | page.title = "Главная" 13 | 14 | # Функция для создания модальных окон 15 | def modal_window(): 16 | # Окно для регистрации 17 | dlg_reg_user = ft.AlertDialog( 18 | adaptive=True, 19 | title=ft.Text("Регистрация", text_align=TextAlign.CENTER), 20 | actions=[ 21 | ft.TextField(label="Имя", autofocus=True, col=10, scale=0.9), 22 | ft.TextField(label="Email", scale=0.9), 23 | ft.TextField(label="Пароль", password=True, scale=0.9), 24 | ft.OutlinedButton( 25 | content=ft.Text("Регистрация"), 26 | on_click=lambda e: registrator(e, dlg_reg_user), 27 | scale=0.9 28 | ) 29 | ] 30 | ) 31 | # Окно для входа 32 | dlg_enter_user = ft.AlertDialog( 33 | adaptive=True, 34 | title=ft.Text("Вход", text_align=TextAlign.CENTER), 35 | actions=[ 36 | ft.TextField(hint_text="Email", autofocus=True, col=10, scale=0.9), 37 | ft.TextField(hint_text="Пароль", password=True, scale=0.9), 38 | ft.CupertinoActionSheetAction(content=ft.Text("Вход")), 39 | ] 40 | ) 41 | return dlg_reg_user, dlg_enter_user 42 | 43 | # Функция для открытия диалогового окна 44 | def open_dialog(dialog): 45 | page.dialog = dialog 46 | dialog.open = True 47 | page.update() 48 | 49 | # Функция для регистрации пользователя 50 | def registrator(e, dlg_reg_user): 51 | name = dlg_reg_user.actions[0].value 52 | email = dlg_reg_user.actions[1].value 53 | password = dlg_reg_user.actions[2].value 54 | url = "http://127.0.0.1:8000/auth/register" 55 | data = { 56 | "name": name, 57 | "email": email, 58 | "password": password 59 | } 60 | response = requests.post(url, json=data) 61 | response_json = response.json() 62 | if response.status_code == 200: 63 | 64 | # Успешная регистрация 65 | dlg_success_registration = ft.AlertDialog( 66 | adaptive=True, 67 | title=ft.Text("Вы успешно зарегистрированы", text_align=TextAlign.CENTER), 68 | 69 | ) 70 | 71 | open_dialog(dlg_success_registration) 72 | time.sleep(1) 73 | dlg_success_registration.open = False 74 | page.update() 75 | page.go("/new_page") 76 | else: 77 | # Ошибка при регистрации 78 | error_description = response_json['detail'][0]['msg'] 79 | dlg_error_registration = ft.AlertDialog( 80 | adaptive=True, 81 | title=ft.Text(error_description, text_align=TextAlign.CENTER) 82 | ) 83 | open_dialog(dlg_error_registration) 84 | time.sleep(2) 85 | open_dialog(dlg_reg_user) 86 | print("Ошибка при регистрации пользователя:", response.text) 87 | 88 | # Функция для изменения маршрута 89 | def create_main_view(dlg_reg_user, dlg_enter_user): 90 | return ft.View( 91 | route="/", 92 | controls=[ 93 | # Футер 94 | ft.BottomAppBar( 95 | bgcolor=ft.colors.SURFACE_VARIANT, 96 | shape=ft.NotchShape.CIRCULAR, 97 | height=45, 98 | content=Text("this is footer", size=10, text_align=TextAlign.CENTER) 99 | ), 100 | # Верхняя панель приложения 101 | ft.AppBar( 102 | title=ft.Text("Автосервис"), 103 | bgcolor=ft.colors.SURFACE_VARIANT, 104 | actions=[ 105 | ft.IconButton(ft.icons.WB_SUNNY_OUTLINED), 106 | # Кнопка "Регистрация" 107 | ft.OutlinedButton( 108 | content=ft.Text("Регистрация"), 109 | on_click=lambda e: open_dialog(dlg_reg_user), 110 | scale=0.9 111 | ), 112 | # Кнопка "Вход" 113 | ft.OutlinedButton( 114 | content=ft.Text("Вход"), 115 | on_click=lambda e: open_dialog(dlg_enter_user), 116 | scale=0.9 117 | ) 118 | ], 119 | ), 120 | ], 121 | ) 122 | 123 | def create_new_page_view(): 124 | 125 | return ft.View( 126 | route="/new_page", 127 | controls=[ 128 | # Футер 129 | ft.BottomAppBar( 130 | bgcolor=ft.colors.SURFACE_VARIANT, 131 | shape=ft.NotchShape.CIRCULAR, 132 | height=45, 133 | content=Text("this is footer", size=10, text_align=TextAlign.CENTER) 134 | ), 135 | # Верхняя панель приложения 136 | ft.AppBar( 137 | title=ft.Text("Новая страница"), 138 | bgcolor=ft.colors.SURFACE_VARIANT, 139 | actions=[ 140 | ft.IconButton(ft.icons.PHOTO), 141 | # Кнопка "Личный кабинет" 142 | ft.OutlinedButton( 143 | content=ft.Text("Личный кабинет"), 144 | scale=0.9 145 | ) 146 | ], 147 | ), 148 | ], 149 | ) 150 | 151 | def update_page(views): 152 | page.views.clear() 153 | page.views.extend(views) 154 | page.update() 155 | 156 | def route_change(e: RouteChangeEvent) -> None: 157 | # Создаем модальные окна для основной страницы 158 | dlg_reg_user, dlg_enter_user = modal_window() 159 | 160 | # Создаем представления для основной страницы и новой страницы 161 | main_view = create_main_view(dlg_reg_user, dlg_enter_user) 162 | new_page_view = create_new_page_view() 163 | 164 | # Обновляем страницу с новыми представлениями 165 | update_page([main_view, new_page_view]) 166 | 167 | # Функция для удаления представления из стека 168 | def view_pop(view): 169 | page.views.pop() 170 | top_view = page.views[-1] 171 | page.go(top_view.route) 172 | 173 | # Привязываем функции к событиям 174 | page.on_route_change = route_change 175 | page.on_view_pop = view_pop 176 | page.go(page.route) 177 | 178 | 179 | if __name__ == "__main__": 180 | ft.app(target=main, view=ft.AppView.WEB_BROWSER, port=8000) 181 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | from fastapi import FastAPI, Request 4 | from fastapi.responses import JSONResponse 5 | from fastapi.middleware.cors import CORSMiddleware 6 | 7 | from src import include_routers 8 | from src.errors import excaptions, handlers 9 | from src.masters.router import router as masters_router 10 | from src.orders.router import router as order_router 11 | from src.works.router import work_router 12 | 13 | print("gfgfgf") 14 | def get_app() -> FastAPI: 15 | app = FastAPI( 16 | title="Auto-service app", 17 | description="this app is supposed to help auto mechanics do their job", 18 | ) 19 | 20 | app.include_router(work_router) 21 | app.include_router(masters_router) 22 | app.include_router(order_router) 23 | 24 | @app.exception_handler(excaptions.ApplicationException) 25 | async def handle_application_exception( 26 | request: Request, exc: excaptions.ApplicationException 27 | ): 28 | return JSONResponse( 29 | status_code=exc.status_code, content={"error": exc.message} 30 | ) 31 | app.add_middleware( 32 | CORSMiddleware, 33 | allow_origins=["*"], 34 | allow_credentials=True, 35 | allow_methods=["*"], 36 | allow_headers=["*"], 37 | ) 38 | 39 | include_routers(app) 40 | 41 | return app 42 | 43 | 44 | def main(): 45 | app = get_app() 46 | uvicorn.run(app) 47 | 48 | 49 | if __name__ == "__main__": 50 | main() 51 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from alembic import context 4 | from sqlalchemy import engine_from_config, pool 5 | 6 | from src.auth.models import User # noqa: F401 7 | from src.config import settings 8 | from src.database import Base 9 | from src.masters.models import Master # noqa: F401 10 | from src.orders.models import Order, OrderWorks # noqa: F401 11 | from src.works.models import Supply, Work # noqa: F401 12 | 13 | # this is the Alembic Config object, which provides 14 | # access to the values within the .ini file in use. 15 | config = context.config 16 | section = config.config_ini_section 17 | config.set_section_option( 18 | section, "db_dsn", settings.postgres_dsn + "?async_fallback=True" 19 | ) 20 | 21 | # Interpret the config file for Python logging. 22 | # This line sets up loggers basically. 23 | if config.config_file_name is not None: 24 | fileConfig(config.config_file_name) 25 | 26 | # add your model's MetaData object here 27 | # for 'autogenerate' support 28 | # from myapp import mymodel 29 | # target_metadata = mymodel.Base.metadata 30 | target_metadata = Base.metadata 31 | 32 | # other values from the config, defined by the needs of env.py, 33 | # can be acquired: 34 | # my_important_option = config.get_main_option("my_important_option") 35 | # ... etc. 36 | 37 | 38 | def run_migrations_offline() -> None: 39 | """Run migrations in 'offline' mode. 40 | 41 | This configures the context with just a URL 42 | and not an Engine, though an Engine is acceptable 43 | here as well. By skipping the Engine creation 44 | we don't even need a DBAPI to be available. 45 | 46 | Calls to context.execute() here emit the given string to the 47 | script output. 48 | 49 | """ 50 | url = config.get_main_option("sqlalchemy.url") 51 | context.configure( 52 | url=url, 53 | target_metadata=target_metadata, 54 | literal_binds=True, 55 | dialect_opts={"paramstyle": "named"}, 56 | ) 57 | 58 | with context.begin_transaction(): 59 | context.run_migrations() 60 | 61 | 62 | def run_migrations_online() -> None: 63 | """Run migrations in 'online' mode. 64 | 65 | In this scenario we need to create an Engine 66 | and associate a connection with the context. 67 | 68 | """ 69 | connectable = engine_from_config( 70 | config.get_section(config.config_ini_section, {}), 71 | prefix="sqlalchemy.", 72 | poolclass=pool.NullPool, 73 | ) 74 | 75 | with connectable.connect() as connection: 76 | context.configure( 77 | connection=connection, target_metadata=target_metadata 78 | ) 79 | 80 | with context.begin_transaction(): 81 | context.run_migrations() 82 | 83 | 84 | if context.is_offline_mode(): 85 | run_migrations_offline() 86 | else: 87 | run_migrations_online() 88 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | ${imports if imports else ""} 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = ${repr(up_revision)} 16 | down_revision: Union[str, None] = ${repr(down_revision)} 17 | branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} 18 | depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} 19 | 20 | 21 | def upgrade() -> None: 22 | ${upgrades if upgrades else "pass"} 23 | 24 | 25 | def downgrade() -> None: 26 | ${downgrades if downgrades else "pass"} 27 | -------------------------------------------------------------------------------- /migrations/versions/2024_03_14_1433-3722e79049fb_added_users_table.py: -------------------------------------------------------------------------------- 1 | """added users table 2 | 3 | Revision ID: 3722e79049fb 4 | Revises: 5 | Create Date: 2024-03-14 14:33:03.876566 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = '3722e79049fb' 16 | down_revision: Union[str, None] = None 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.create_table('user', 24 | sa.Column('id', sa.Uuid(), nullable=False), 25 | sa.Column('name', sa.String(length=50), nullable=False), 26 | sa.Column('email', sa.String(length=70), nullable=False), 27 | sa.Column('hashed_password', sa.String(), nullable=False), 28 | sa.Column('is_admin', sa.Boolean(), nullable=False), 29 | sa.Column('is_active', sa.Boolean(), nullable=False), 30 | sa.Column('refresh_token', sa.String(), nullable=False), 31 | sa.PrimaryKeyConstraint('id') 32 | ) 33 | # ### end Alembic commands ### 34 | 35 | 36 | def downgrade() -> None: 37 | # ### commands auto generated by Alembic - please adjust! ### 38 | op.drop_table('user') 39 | # ### end Alembic commands ### 40 | -------------------------------------------------------------------------------- /migrations/versions/2024_03_14_1732-53937fceb381_changed_user_refresh_token_to_nullable.py: -------------------------------------------------------------------------------- 1 | """changed user.refresh_token to nullable 2 | 3 | Revision ID: 53937fceb381 4 | Revises: 3722e79049fb 5 | Create Date: 2024-03-14 17:32:24.609494 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = '53937fceb381' 16 | down_revision: Union[str, None] = '3722e79049fb' 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.alter_column('user', 'refresh_token', 24 | existing_type=sa.VARCHAR(), 25 | nullable=True) 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade() -> None: 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.alter_column('user', 'refresh_token', 32 | existing_type=sa.VARCHAR(), 33 | nullable=False) 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /migrations/versions/2024_03_17_1708-d1c5aa9b4ecc_made_user_email_unique.py: -------------------------------------------------------------------------------- 1 | """made user email unique 2 | 3 | Revision ID: d1c5aa9b4ecc 4 | Revises: 53937fceb381 5 | Create Date: 2024-03-17 17:08:50.570241 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = 'd1c5aa9b4ecc' 16 | down_revision: Union[str, None] = '53937fceb381' 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.create_unique_constraint(None, 'user', ['email']) 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade() -> None: 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | op.drop_constraint(None, 'user', type_='unique') 30 | # ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /migrations/versions/2024_03_26-6040e6a7be5b_added_work_and_supply_tables.py: -------------------------------------------------------------------------------- 1 | """added work and supply tables 2 | 3 | Revision ID: 6040e6a7be5b 4 | Revises: d1c5aa9b4ecc 5 | Create Date: 2024-03-26 21:08:41.045901 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = '6040e6a7be5b' 16 | down_revision: Union[str, None] = 'd1c5aa9b4ecc' 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.create_table('supply', 24 | sa.Column('title', sa.String(length=70), nullable=False), 25 | sa.Column('supply_type', sa.String(length=50), nullable=False), 26 | sa.Column('amount', sa.Integer(), nullable=False), 27 | sa.PrimaryKeyConstraint('title') 28 | ) 29 | op.create_table('work', 30 | sa.Column('id', sa.Uuid(), nullable=False), 31 | sa.Column('title', sa.String(length=70), nullable=False), 32 | sa.Column('description', sa.Text(), nullable=False), 33 | sa.Column('duration_in_minutes', sa.Integer(), nullable=False), 34 | sa.PrimaryKeyConstraint('id') 35 | ) 36 | # ### end Alembic commands ### 37 | 38 | 39 | def downgrade() -> None: 40 | # ### commands auto generated by Alembic - please adjust! ### 41 | op.drop_table('work') 42 | op.drop_table('supply') 43 | # ### end Alembic commands ### 44 | -------------------------------------------------------------------------------- /migrations/versions/2024_03_26-e4ca7f85e87c_added_work_supply.py: -------------------------------------------------------------------------------- 1 | """added work_supply 2 | 3 | Revision ID: e4ca7f85e87c 4 | Revises: 6040e6a7be5b 5 | Create Date: 2024-03-26 21:10:57.631207 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = 'e4ca7f85e87c' 16 | down_revision: Union[str, None] = '6040e6a7be5b' 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.create_table('work_supply', 24 | sa.Column('work_id', sa.Uuid(), nullable=False), 25 | sa.Column('supply_title', sa.String(length=70), nullable=False), 26 | sa.ForeignKeyConstraint(['supply_title'], ['supply.title'], ), 27 | sa.ForeignKeyConstraint(['work_id'], ['work.id'], ), 28 | sa.PrimaryKeyConstraint('work_id', 'supply_title') 29 | ) 30 | # ### end Alembic commands ### 31 | 32 | 33 | def downgrade() -> None: 34 | # ### commands auto generated by Alembic - please adjust! ### 35 | op.drop_table('work_supply') 36 | # ### end Alembic commands ### 37 | -------------------------------------------------------------------------------- /migrations/versions/2024_03_27-71671dbc53c0_added_master.py: -------------------------------------------------------------------------------- 1 | """added master 2 | 3 | Revision ID: 71671dbc53c0 4 | Revises: e4ca7f85e87c 5 | Create Date: 2024-03-27 20:23:39.831825 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = '71671dbc53c0' 16 | down_revision: Union[str, None] = 'e4ca7f85e87c' 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.create_table('master', 24 | sa.Column('id', sa.Uuid(), nullable=False), 25 | sa.Column('name', sa.String(length=50), nullable=False), 26 | sa.Column('s_name', sa.String(length=50), nullable=False), 27 | sa.Column('speciality', sa.String(), nullable=False), 28 | sa.Column('experience', sa.Integer(), nullable=False), 29 | sa.Column('phone_number', sa.String(length=20), nullable=False), 30 | sa.PrimaryKeyConstraint('id') 31 | ) 32 | # ### end Alembic commands ### 33 | 34 | 35 | def downgrade() -> None: 36 | # ### commands auto generated by Alembic - please adjust! ### 37 | op.drop_table('master') 38 | # ### end Alembic commands ### 39 | -------------------------------------------------------------------------------- /migrations/versions/2024_03_28-0f2b136116fd_added_timezone.py: -------------------------------------------------------------------------------- 1 | """added timezone 2 | 3 | Revision ID: 0f2b136116fd 4 | Revises: c717f3475dc3 5 | Create Date: 2024-03-28 18:14:30.831029 6 | 7 | """ 8 | 9 | from typing import Sequence, Union 10 | 11 | from alembic import op 12 | import sqlalchemy as sa 13 | from sqlalchemy.dialects import postgresql 14 | 15 | # revision identifiers, used by Alembic. 16 | revision: str = "0f2b136116fd" 17 | down_revision: Union[str, None] = "c717f3475dc3" 18 | branch_labels: Union[str, Sequence[str], None] = None 19 | depends_on: Union[str, Sequence[str], None] = None 20 | 21 | 22 | def upgrade() -> None: 23 | # ### commands auto generated by Alembic - please adjust! ### 24 | op.create_table( 25 | "orders_works", 26 | sa.Column("order_id", sa.Uuid(), nullable=False), 27 | sa.Column("work_id", sa.Uuid(), nullable=False), 28 | sa.ForeignKeyConstraint( 29 | ["order_id"], 30 | ["order.id"], 31 | ), 32 | sa.ForeignKeyConstraint( 33 | ["work_id"], 34 | ["work.id"], 35 | ), 36 | sa.PrimaryKeyConstraint("order_id", "work_id"), 37 | ) 38 | op.alter_column( 39 | "order", 40 | "ordered_at", 41 | existing_type=postgresql.TIMESTAMP(), 42 | type_=sa.DateTime(timezone=True), 43 | existing_nullable=False, 44 | existing_server_default=sa.text("now()"), 45 | ) 46 | op.alter_column( 47 | "order", 48 | "to_be_provided_at", 49 | existing_type=postgresql.TIMESTAMP(), 50 | type_=sa.DateTime(timezone=True), 51 | existing_nullable=False, 52 | ) 53 | # ### end Alembic commands ### 54 | 55 | 56 | def downgrade() -> None: 57 | # ### commands auto generated by Alembic - please adjust! ### 58 | op.alter_column( 59 | "order", 60 | "to_be_provided_at", 61 | existing_type=sa.DateTime(timezone=True), 62 | type_=postgresql.TIMESTAMP(), 63 | existing_nullable=False, 64 | ) 65 | op.alter_column( 66 | "order", 67 | "ordered_at", 68 | existing_type=sa.DateTime(timezone=True), 69 | type_=postgresql.TIMESTAMP(), 70 | existing_nullable=False, 71 | existing_server_default=sa.text("now()"), 72 | ) 73 | op.drop_table("orders_works") 74 | # ### end Alembic commands ### 75 | -------------------------------------------------------------------------------- /migrations/versions/2024_03_28-c717f3475dc3_added_master_to_order_table.py: -------------------------------------------------------------------------------- 1 | """added master to order table 2 | 3 | Revision ID: c717f3475dc3 4 | Revises: db5c44566279 5 | Create Date: 2024-03-28 16:59:43.685111 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = 'c717f3475dc3' 16 | down_revision: Union[str, None] = 'db5c44566279' 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.add_column('order', sa.Column('master_id', sa.Uuid(), nullable=False)) 24 | op.create_foreign_key(None, 'order', 'master', ['master_id'], ['id']) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade() -> None: 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | op.drop_constraint(None, 'order', type_='foreignkey') 31 | op.drop_column('order', 'master_id') 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /migrations/versions/2024_03_28-db5c44566279_added_user.py: -------------------------------------------------------------------------------- 1 | """added user 2 | 3 | Revision ID: db5c44566279 4 | Revises: 71671dbc53c0 5 | Create Date: 2024-03-28 16:16:57.033551 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = 'db5c44566279' 16 | down_revision: Union[str, None] = '71671dbc53c0' 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.create_table('order', 24 | sa.Column('id', sa.Uuid(), nullable=False), 25 | sa.Column('price', sa.Integer(), nullable=False), 26 | sa.Column('ordered_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False), 27 | sa.Column('to_be_provided_at', sa.DateTime(), nullable=False), 28 | sa.Column('status', sa.Enum('created', 'acepted', 'in_progress', 'finished', 'canceled', name='orderstatus'), nullable=False), 29 | sa.Column('user_id', sa.Uuid(), nullable=False), 30 | sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), 31 | sa.PrimaryKeyConstraint('id') 32 | ) 33 | # ### end Alembic commands ### 34 | 35 | 36 | def downgrade() -> None: 37 | # ### commands auto generated by Alembic - please adjust! ### 38 | op.drop_table('order') 39 | # ### end Alembic commands ### 40 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "alembic" 5 | version = "1.13.1" 6 | description = "A database migration tool for SQLAlchemy." 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"}, 11 | {file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"}, 12 | ] 13 | 14 | [package.dependencies] 15 | Mako = "*" 16 | SQLAlchemy = ">=1.3.0" 17 | typing-extensions = ">=4" 18 | 19 | [package.extras] 20 | tz = ["backports.zoneinfo"] 21 | 22 | [[package]] 23 | name = "annotated-types" 24 | version = "0.6.0" 25 | description = "Reusable constraint types to use with typing.Annotated" 26 | optional = false 27 | python-versions = ">=3.8" 28 | files = [ 29 | {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, 30 | {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, 31 | ] 32 | 33 | [[package]] 34 | name = "anyio" 35 | version = "4.3.0" 36 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 37 | optional = false 38 | python-versions = ">=3.8" 39 | files = [ 40 | {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, 41 | {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, 42 | ] 43 | 44 | [package.dependencies] 45 | idna = ">=2.8" 46 | sniffio = ">=1.1" 47 | 48 | [package.extras] 49 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 50 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 51 | trio = ["trio (>=0.23)"] 52 | 53 | [[package]] 54 | name = "async-timeout" 55 | version = "4.0.3" 56 | description = "Timeout context manager for asyncio programs" 57 | optional = false 58 | python-versions = ">=3.7" 59 | files = [ 60 | {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, 61 | {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, 62 | ] 63 | 64 | [[package]] 65 | name = "asyncpg" 66 | version = "0.29.0" 67 | description = "An asyncio PostgreSQL driver" 68 | optional = false 69 | python-versions = ">=3.8.0" 70 | files = [ 71 | {file = "asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169"}, 72 | {file = "asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385"}, 73 | {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22"}, 74 | {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610"}, 75 | {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397"}, 76 | {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb"}, 77 | {file = "asyncpg-0.29.0-cp310-cp310-win32.whl", hash = "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449"}, 78 | {file = "asyncpg-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772"}, 79 | {file = "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"}, 80 | {file = "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"}, 81 | {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"}, 82 | {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"}, 83 | {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"}, 84 | {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"}, 85 | {file = "asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"}, 86 | {file = "asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"}, 87 | {file = "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"}, 88 | {file = "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"}, 89 | {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"}, 90 | {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"}, 91 | {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"}, 92 | {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"}, 93 | {file = "asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"}, 94 | {file = "asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"}, 95 | {file = "asyncpg-0.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9"}, 96 | {file = "asyncpg-0.29.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc"}, 97 | {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548"}, 98 | {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7"}, 99 | {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775"}, 100 | {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9"}, 101 | {file = "asyncpg-0.29.0-cp38-cp38-win32.whl", hash = "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408"}, 102 | {file = "asyncpg-0.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3"}, 103 | {file = "asyncpg-0.29.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da"}, 104 | {file = "asyncpg-0.29.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3"}, 105 | {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090"}, 106 | {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83"}, 107 | {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810"}, 108 | {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c"}, 109 | {file = "asyncpg-0.29.0-cp39-cp39-win32.whl", hash = "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2"}, 110 | {file = "asyncpg-0.29.0-cp39-cp39-win_amd64.whl", hash = "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8"}, 111 | {file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"}, 112 | ] 113 | 114 | [package.dependencies] 115 | async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""} 116 | 117 | [package.extras] 118 | docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] 119 | test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"] 120 | 121 | [[package]] 122 | name = "bcrypt" 123 | version = "4.1.2" 124 | description = "Modern password hashing for your software and your servers" 125 | optional = false 126 | python-versions = ">=3.7" 127 | files = [ 128 | {file = "bcrypt-4.1.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ac621c093edb28200728a9cca214d7e838529e557027ef0581685909acd28b5e"}, 129 | {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea505c97a5c465ab8c3ba75c0805a102ce526695cd6818c6de3b1a38f6f60da1"}, 130 | {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57fa9442758da926ed33a91644649d3e340a71e2d0a5a8de064fb621fd5a3326"}, 131 | {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb3bd3321517916696233b5e0c67fd7d6281f0ef48e66812db35fc963a422a1c"}, 132 | {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6cad43d8c63f34b26aef462b6f5e44fdcf9860b723d2453b5d391258c4c8e966"}, 133 | {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:44290ccc827d3a24604f2c8bcd00d0da349e336e6503656cb8192133e27335e2"}, 134 | {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:732b3920a08eacf12f93e6b04ea276c489f1c8fb49344f564cca2adb663b3e4c"}, 135 | {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c28973decf4e0e69cee78c68e30a523be441972c826703bb93099868a8ff5b5"}, 136 | {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b8df79979c5bae07f1db22dcc49cc5bccf08a0380ca5c6f391cbb5790355c0b0"}, 137 | {file = "bcrypt-4.1.2-cp37-abi3-win32.whl", hash = "sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369"}, 138 | {file = "bcrypt-4.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:9800ae5bd5077b13725e2e3934aa3c9c37e49d3ea3d06318010aa40f54c63551"}, 139 | {file = "bcrypt-4.1.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:71b8be82bc46cedd61a9f4ccb6c1a493211d031415a34adde3669ee1b0afbb63"}, 140 | {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e3c6642077b0c8092580c819c1684161262b2e30c4f45deb000c38947bf483"}, 141 | {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387e7e1af9a4dd636b9505a465032f2f5cb8e61ba1120e79a0e1cd0b512f3dfc"}, 142 | {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f70d9c61f9c4ca7d57f3bfe88a5ccf62546ffbadf3681bb1e268d9d2e41c91a7"}, 143 | {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2a298db2a8ab20056120b45e86c00a0a5eb50ec4075b6142db35f593b97cb3fb"}, 144 | {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ba55e40de38a24e2d78d34c2d36d6e864f93e0d79d0b6ce915e4335aa81d01b1"}, 145 | {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3566a88234e8de2ccae31968127b0ecccbb4cddb629da744165db72b58d88ca4"}, 146 | {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b90e216dc36864ae7132cb151ffe95155a37a14e0de3a8f64b49655dd959ff9c"}, 147 | {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:69057b9fc5093ea1ab00dd24ede891f3e5e65bee040395fb1e66ee196f9c9b4a"}, 148 | {file = "bcrypt-4.1.2-cp39-abi3-win32.whl", hash = "sha256:02d9ef8915f72dd6daaef40e0baeef8a017ce624369f09754baf32bb32dba25f"}, 149 | {file = "bcrypt-4.1.2-cp39-abi3-win_amd64.whl", hash = "sha256:be3ab1071662f6065899fe08428e45c16aa36e28bc42921c4901a191fda6ee42"}, 150 | {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d75fc8cd0ba23f97bae88a6ec04e9e5351ff3c6ad06f38fe32ba50cbd0d11946"}, 151 | {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a97e07e83e3262599434816f631cc4c7ca2aa8e9c072c1b1a7fec2ae809a1d2d"}, 152 | {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e51c42750b7585cee7892c2614be0d14107fad9581d1738d954a262556dd1aab"}, 153 | {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba4e4cc26610581a6329b3937e02d319f5ad4b85b074846bf4fef8a8cf51e7bb"}, 154 | {file = "bcrypt-4.1.2.tar.gz", hash = "sha256:33313a1200a3ae90b75587ceac502b048b840fc69e7f7a0905b5f87fac7a1258"}, 155 | ] 156 | 157 | [package.extras] 158 | tests = ["pytest (>=3.2.1,!=3.3.0)"] 159 | typecheck = ["mypy"] 160 | 161 | [[package]] 162 | name = "certifi" 163 | version = "2024.2.2" 164 | description = "Python package for providing Mozilla's CA Bundle." 165 | optional = false 166 | python-versions = ">=3.6" 167 | files = [ 168 | {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, 169 | {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, 170 | ] 171 | 172 | [[package]] 173 | name = "cffi" 174 | version = "1.16.0" 175 | description = "Foreign Function Interface for Python calling C code." 176 | optional = false 177 | python-versions = ">=3.8" 178 | files = [ 179 | {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, 180 | {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, 181 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, 182 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, 183 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, 184 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, 185 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, 186 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, 187 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, 188 | {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, 189 | {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, 190 | {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, 191 | {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, 192 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, 193 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, 194 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, 195 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, 196 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, 197 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, 198 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, 199 | {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, 200 | {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, 201 | {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, 202 | {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, 203 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, 204 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, 205 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, 206 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, 207 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, 208 | {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, 209 | {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, 210 | {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, 211 | {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, 212 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, 213 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, 214 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, 215 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, 216 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, 217 | {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, 218 | {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, 219 | {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, 220 | {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, 221 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, 222 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, 223 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, 224 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, 225 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, 226 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, 227 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, 228 | {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, 229 | {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, 230 | {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, 231 | ] 232 | 233 | [package.dependencies] 234 | pycparser = "*" 235 | 236 | [[package]] 237 | name = "click" 238 | version = "8.1.7" 239 | description = "Composable command line interface toolkit" 240 | optional = false 241 | python-versions = ">=3.7" 242 | files = [ 243 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 244 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 245 | ] 246 | 247 | [package.dependencies] 248 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 249 | 250 | [[package]] 251 | name = "colorama" 252 | version = "0.4.6" 253 | description = "Cross-platform colored terminal text." 254 | optional = false 255 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 256 | files = [ 257 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 258 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 259 | ] 260 | 261 | [[package]] 262 | name = "coverage" 263 | version = "7.4.4" 264 | description = "Code coverage measurement for Python" 265 | optional = false 266 | python-versions = ">=3.8" 267 | files = [ 268 | {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, 269 | {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, 270 | {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, 271 | {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, 272 | {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, 273 | {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, 274 | {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, 275 | {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, 276 | {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, 277 | {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, 278 | {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, 279 | {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, 280 | {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, 281 | {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, 282 | {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, 283 | {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, 284 | {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, 285 | {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, 286 | {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, 287 | {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, 288 | {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, 289 | {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, 290 | {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, 291 | {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, 292 | {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, 293 | {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, 294 | {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, 295 | {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, 296 | {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, 297 | {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, 298 | {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, 299 | {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, 300 | {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, 301 | {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, 302 | {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, 303 | {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, 304 | {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, 305 | {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, 306 | {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, 307 | {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, 308 | {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, 309 | {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, 310 | {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, 311 | {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, 312 | {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, 313 | {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, 314 | {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, 315 | {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, 316 | {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, 317 | {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, 318 | {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, 319 | {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, 320 | ] 321 | 322 | [package.extras] 323 | toml = ["tomli"] 324 | 325 | [[package]] 326 | name = "cryptography" 327 | version = "42.0.5" 328 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 329 | optional = false 330 | python-versions = ">=3.7" 331 | files = [ 332 | {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16"}, 333 | {file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec"}, 334 | {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb"}, 335 | {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4"}, 336 | {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278"}, 337 | {file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"}, 338 | {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee"}, 339 | {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1"}, 340 | {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d"}, 341 | {file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da"}, 342 | {file = "cryptography-42.0.5-cp37-abi3-win32.whl", hash = "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74"}, 343 | {file = "cryptography-42.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940"}, 344 | {file = "cryptography-42.0.5-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8"}, 345 | {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1"}, 346 | {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e"}, 347 | {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc"}, 348 | {file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a"}, 349 | {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7"}, 350 | {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922"}, 351 | {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc"}, 352 | {file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30"}, 353 | {file = "cryptography-42.0.5-cp39-abi3-win32.whl", hash = "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413"}, 354 | {file = "cryptography-42.0.5-cp39-abi3-win_amd64.whl", hash = "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400"}, 355 | {file = "cryptography-42.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8"}, 356 | {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2"}, 357 | {file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c"}, 358 | {file = "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576"}, 359 | {file = "cryptography-42.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6"}, 360 | {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e"}, 361 | {file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac"}, 362 | {file = "cryptography-42.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd"}, 363 | {file = "cryptography-42.0.5.tar.gz", hash = "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1"}, 364 | ] 365 | 366 | [package.dependencies] 367 | cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} 368 | 369 | [package.extras] 370 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 371 | docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] 372 | nox = ["nox"] 373 | pep8test = ["check-sdist", "click", "mypy", "ruff"] 374 | sdist = ["build"] 375 | ssh = ["bcrypt (>=3.1.5)"] 376 | test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 377 | test-randomorder = ["pytest-randomly"] 378 | 379 | [[package]] 380 | name = "dnspython" 381 | version = "2.6.1" 382 | description = "DNS toolkit" 383 | optional = false 384 | python-versions = ">=3.8" 385 | files = [ 386 | {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, 387 | {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, 388 | ] 389 | 390 | [package.extras] 391 | dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] 392 | dnssec = ["cryptography (>=41)"] 393 | doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] 394 | doq = ["aioquic (>=0.9.25)"] 395 | idna = ["idna (>=3.6)"] 396 | trio = ["trio (>=0.23)"] 397 | wmi = ["wmi (>=1.5.1)"] 398 | 399 | [[package]] 400 | name = "email-validator" 401 | version = "2.1.1" 402 | description = "A robust email address syntax and deliverability validation library." 403 | optional = false 404 | python-versions = ">=3.8" 405 | files = [ 406 | {file = "email_validator-2.1.1-py3-none-any.whl", hash = "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"}, 407 | {file = "email_validator-2.1.1.tar.gz", hash = "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84"}, 408 | ] 409 | 410 | [package.dependencies] 411 | dnspython = ">=2.0.0" 412 | idna = ">=2.0.0" 413 | 414 | [[package]] 415 | name = "faker" 416 | version = "24.3.0" 417 | description = "Faker is a Python package that generates fake data for you." 418 | optional = false 419 | python-versions = ">=3.8" 420 | files = [ 421 | {file = "Faker-24.3.0-py3-none-any.whl", hash = "sha256:9978025e765ba79f8bf6154c9630a9c2b7f9c9b0f175d4ad5e04b19a82a8d8d6"}, 422 | {file = "Faker-24.3.0.tar.gz", hash = "sha256:5fb5aa9749d09971e04a41281ae3ceda9414f683d4810a694f8a8eebb8f9edec"}, 423 | ] 424 | 425 | [package.dependencies] 426 | python-dateutil = ">=2.4" 427 | 428 | [[package]] 429 | name = "fastapi" 430 | version = "0.110.0" 431 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 432 | optional = false 433 | python-versions = ">=3.8" 434 | files = [ 435 | {file = "fastapi-0.110.0-py3-none-any.whl", hash = "sha256:87a1f6fb632a218222c5984be540055346a8f5d8a68e8f6fb647b1dc9934de4b"}, 436 | {file = "fastapi-0.110.0.tar.gz", hash = "sha256:266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3"}, 437 | ] 438 | 439 | [package.dependencies] 440 | pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" 441 | starlette = ">=0.36.3,<0.37.0" 442 | typing-extensions = ">=4.8.0" 443 | 444 | [package.extras] 445 | all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] 446 | 447 | [[package]] 448 | name = "flat" 449 | version = "0.3.2" 450 | description = "Generative infrastructure for Python" 451 | optional = false 452 | python-versions = "*" 453 | files = [ 454 | {file = "Flat-0.3.2.tar.gz", hash = "sha256:e113acf6a4fbe7800f21036fe6368602883b0421bc00b5df278d625f8c815db4"}, 455 | ] 456 | 457 | [[package]] 458 | name = "greenlet" 459 | version = "3.0.3" 460 | description = "Lightweight in-process concurrent programming" 461 | optional = false 462 | python-versions = ">=3.7" 463 | files = [ 464 | {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, 465 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, 466 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, 467 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, 468 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, 469 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, 470 | {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, 471 | {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, 472 | {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, 473 | {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, 474 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, 475 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, 476 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, 477 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, 478 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, 479 | {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, 480 | {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, 481 | {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, 482 | {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, 483 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, 484 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, 485 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, 486 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, 487 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, 488 | {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, 489 | {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, 490 | {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, 491 | {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, 492 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, 493 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, 494 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, 495 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, 496 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, 497 | {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, 498 | {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, 499 | {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, 500 | {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, 501 | {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, 502 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, 503 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, 504 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, 505 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, 506 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, 507 | {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, 508 | {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, 509 | {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, 510 | {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, 511 | {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, 512 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, 513 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, 514 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, 515 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, 516 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, 517 | {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, 518 | {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, 519 | {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, 520 | {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, 521 | {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, 522 | ] 523 | 524 | [package.extras] 525 | docs = ["Sphinx", "furo"] 526 | test = ["objgraph", "psutil"] 527 | 528 | [[package]] 529 | name = "h11" 530 | version = "0.14.0" 531 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 532 | optional = false 533 | python-versions = ">=3.7" 534 | files = [ 535 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 536 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 537 | ] 538 | 539 | [[package]] 540 | name = "httpcore" 541 | version = "1.0.4" 542 | description = "A minimal low-level HTTP client." 543 | optional = false 544 | python-versions = ">=3.8" 545 | files = [ 546 | {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, 547 | {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, 548 | ] 549 | 550 | [package.dependencies] 551 | certifi = "*" 552 | h11 = ">=0.13,<0.15" 553 | 554 | [package.extras] 555 | asyncio = ["anyio (>=4.0,<5.0)"] 556 | http2 = ["h2 (>=3,<5)"] 557 | socks = ["socksio (==1.*)"] 558 | trio = ["trio (>=0.22.0,<0.25.0)"] 559 | 560 | [[package]] 561 | name = "httpx" 562 | version = "0.27.0" 563 | description = "The next generation HTTP client." 564 | optional = false 565 | python-versions = ">=3.8" 566 | files = [ 567 | {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, 568 | {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, 569 | ] 570 | 571 | [package.dependencies] 572 | anyio = "*" 573 | certifi = "*" 574 | httpcore = "==1.*" 575 | idna = "*" 576 | sniffio = "*" 577 | 578 | [package.extras] 579 | brotli = ["brotli", "brotlicffi"] 580 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 581 | http2 = ["h2 (>=3,<5)"] 582 | socks = ["socksio (==1.*)"] 583 | 584 | [[package]] 585 | name = "idna" 586 | version = "3.6" 587 | description = "Internationalized Domain Names in Applications (IDNA)" 588 | optional = false 589 | python-versions = ">=3.5" 590 | files = [ 591 | {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, 592 | {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, 593 | ] 594 | 595 | [[package]] 596 | name = "iniconfig" 597 | version = "2.0.0" 598 | description = "brain-dead simple config-ini parsing" 599 | optional = false 600 | python-versions = ">=3.7" 601 | files = [ 602 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 603 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 604 | ] 605 | 606 | [[package]] 607 | name = "mako" 608 | version = "1.3.2" 609 | description = "A super-fast templating language that borrows the best ideas from the existing templating languages." 610 | optional = false 611 | python-versions = ">=3.8" 612 | files = [ 613 | {file = "Mako-1.3.2-py3-none-any.whl", hash = "sha256:32a99d70754dfce237019d17ffe4a282d2d3351b9c476e90d8a60e63f133b80c"}, 614 | {file = "Mako-1.3.2.tar.gz", hash = "sha256:2a0c8ad7f6274271b3bb7467dd37cf9cc6dab4bc19cb69a4ef10669402de698e"}, 615 | ] 616 | 617 | [package.dependencies] 618 | MarkupSafe = ">=0.9.2" 619 | 620 | [package.extras] 621 | babel = ["Babel"] 622 | lingua = ["lingua"] 623 | testing = ["pytest"] 624 | 625 | [[package]] 626 | name = "markupsafe" 627 | version = "2.1.5" 628 | description = "Safely add untrusted strings to HTML/XML markup." 629 | optional = false 630 | python-versions = ">=3.7" 631 | files = [ 632 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, 633 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, 634 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, 635 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, 636 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, 637 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, 638 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, 639 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, 640 | {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, 641 | {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, 642 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, 643 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, 644 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, 645 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, 646 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, 647 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, 648 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, 649 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, 650 | {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, 651 | {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, 652 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, 653 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, 654 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, 655 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, 656 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, 657 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, 658 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, 659 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, 660 | {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, 661 | {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, 662 | {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, 663 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, 664 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, 665 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, 666 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, 667 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, 668 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, 669 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, 670 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, 671 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, 672 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, 673 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, 674 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, 675 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, 676 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, 677 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, 678 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, 679 | {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, 680 | {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, 681 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, 682 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, 683 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, 684 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, 685 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, 686 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, 687 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, 688 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, 689 | {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, 690 | {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, 691 | {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, 692 | ] 693 | 694 | [[package]] 695 | name = "packaging" 696 | version = "24.0" 697 | description = "Core utilities for Python packages" 698 | optional = false 699 | python-versions = ">=3.7" 700 | files = [ 701 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 702 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 703 | ] 704 | 705 | [[package]] 706 | name = "pluggy" 707 | version = "1.4.0" 708 | description = "plugin and hook calling mechanisms for python" 709 | optional = false 710 | python-versions = ">=3.8" 711 | files = [ 712 | {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, 713 | {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, 714 | ] 715 | 716 | [package.extras] 717 | dev = ["pre-commit", "tox"] 718 | testing = ["pytest", "pytest-benchmark"] 719 | 720 | [[package]] 721 | name = "psycopg-binary" 722 | version = "3.1.18" 723 | description = "PostgreSQL database adapter for Python -- C optimisation distribution" 724 | optional = false 725 | python-versions = ">=3.7" 726 | files = [ 727 | {file = "psycopg_binary-3.1.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c323103dfa663b88204cf5f028e83c77d7a715f9b6f51d2bbc8184b99ddd90a"}, 728 | {file = "psycopg_binary-3.1.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:887f8d856c91510148be942c7acd702ccf761a05f59f8abc123c22ab77b5a16c"}, 729 | {file = "psycopg_binary-3.1.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d322ba72cde4ca2eefc2196dad9ad7e52451acd2f04e3688d590290625d0c970"}, 730 | {file = "psycopg_binary-3.1.18-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:489aa4fe5a0b653b68341e9e44af247dedbbc655326854aa34c163ef1bcb3143"}, 731 | {file = "psycopg_binary-3.1.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ff0948457bfa8c0d35c46e3a75193906d1c275538877ba65907fd67aa059ad"}, 732 | {file = "psycopg_binary-3.1.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b15e3653c82384b043d820fc637199b5c6a36b37fa4a4943e0652785bb2bad5d"}, 733 | {file = "psycopg_binary-3.1.18-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f8ff3bc08b43f36fdc24fedb86d42749298a458c4724fb588c4d76823ac39f54"}, 734 | {file = "psycopg_binary-3.1.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1729d0e3dfe2546d823841eb7a3d003144189d6f5e138ee63e5227f8b75276a5"}, 735 | {file = "psycopg_binary-3.1.18-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:13bcd3742112446037d15e360b27a03af4b5afcf767f5ee374ef8f5dd7571b31"}, 736 | {file = "psycopg_binary-3.1.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:320047e3d3554b857e16c2b6b615a85e0db6a02426f4d203a4594a2f125dfe57"}, 737 | {file = "psycopg_binary-3.1.18-cp310-cp310-win_amd64.whl", hash = "sha256:888a72c2aca4316ca6d4a619291b805677bae99bba2f6e31a3c18424a48c7e4d"}, 738 | {file = "psycopg_binary-3.1.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e4de16a637ec190cbee82e0c2dc4860fed17a23a35f7a1e6dc479a5c6876722"}, 739 | {file = "psycopg_binary-3.1.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6432047b8b24ef97e3fbee1d1593a0faaa9544c7a41a2c67d1f10e7621374c83"}, 740 | {file = "psycopg_binary-3.1.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d684227ef8212e27da5f2aff9d4d303cc30b27ac1702d4f6881935549486dd5"}, 741 | {file = "psycopg_binary-3.1.18-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67284e2e450dc7a9e4d76e78c0bd357dc946334a3d410defaeb2635607f632cd"}, 742 | {file = "psycopg_binary-3.1.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c9b6bd7fb5c6638cb32469674707649b526acfe786ba6d5a78ca4293d87bae4"}, 743 | {file = "psycopg_binary-3.1.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7121acc783c4e86d2d320a7fb803460fab158a7f0a04c5e8c5d49065118c1e73"}, 744 | {file = "psycopg_binary-3.1.18-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e28ff8f3de7b56588c2a398dc135fd9f157d12c612bd3daa7e6ba9872337f6f5"}, 745 | {file = "psycopg_binary-3.1.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c84a0174109f329eeda169004c7b7ca2e884a6305acab4a39600be67f915ed38"}, 746 | {file = "psycopg_binary-3.1.18-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:531381f6647fc267383dca88dbe8a70d0feff433a8e3d0c4939201fea7ae1b82"}, 747 | {file = "psycopg_binary-3.1.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b293e01057e63c3ac0002aa132a1071ce0fdb13b9ee2b6b45d3abdb3525c597d"}, 748 | {file = "psycopg_binary-3.1.18-cp311-cp311-win_amd64.whl", hash = "sha256:780a90bcb69bf27a8b08bc35b958e974cb6ea7a04cdec69e737f66378a344d68"}, 749 | {file = "psycopg_binary-3.1.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:87dd9154b757a5fbf6d590f6f6ea75f4ad7b764a813ae04b1d91a70713f414a1"}, 750 | {file = "psycopg_binary-3.1.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f876ebbf92db70125f6375f91ab4bc6b27648aa68f90d661b1fc5affb4c9731c"}, 751 | {file = "psycopg_binary-3.1.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:258d2f0cb45e4574f8b2fe7c6d0a0e2eb58903a4fd1fbaf60954fba82d595ab7"}, 752 | {file = "psycopg_binary-3.1.18-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd27f713f2e5ef3fd6796e66c1a5203a27a30ecb847be27a78e1df8a9a5ae68c"}, 753 | {file = "psycopg_binary-3.1.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c38a4796abf7380f83b1653c2711cb2449dd0b2e5aca1caa75447d6fa5179c69"}, 754 | {file = "psycopg_binary-3.1.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2f7f95746efd1be2dc240248cc157f4315db3fd09fef2adfcc2a76e24aa5741"}, 755 | {file = "psycopg_binary-3.1.18-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4085f56a8d4fc8b455e8f44380705c7795be5317419aa5f8214f315e4205d804"}, 756 | {file = "psycopg_binary-3.1.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:2e2484ae835dedc80cdc7f1b1a939377dc967fed862262cfd097aa9f50cade46"}, 757 | {file = "psycopg_binary-3.1.18-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3c2b039ae0c45eee4cd85300ef802c0f97d0afc78350946a5d0ec77dd2d7e834"}, 758 | {file = "psycopg_binary-3.1.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f54978c4b646dec77fefd8485fa82ec1a87807f334004372af1aaa6de9539a5"}, 759 | {file = "psycopg_binary-3.1.18-cp312-cp312-win_amd64.whl", hash = "sha256:9ffcbbd389e486d3fd83d30107bbf8b27845a295051ccabde240f235d04ed921"}, 760 | {file = "psycopg_binary-3.1.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c76659ae29a84f2c14f56aad305dd00eb685bd88f8c0a3281a9a4bc6bd7d2aa7"}, 761 | {file = "psycopg_binary-3.1.18-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7afcd6f1d55992f26d9ff7b0bd4ee6b475eb43aa3f054d67d32e09f18b0065"}, 762 | {file = "psycopg_binary-3.1.18-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:639dd78ac09b144b0119076783cb64e1128cc8612243e9701d1503c816750b2e"}, 763 | {file = "psycopg_binary-3.1.18-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1cf59e0bb12e031a48bb628aae32df3d0c98fd6c759cb89f464b1047f0ca9c8"}, 764 | {file = "psycopg_binary-3.1.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e262398e5d51563093edf30612cd1e20fedd932ad0994697d7781ca4880cdc3d"}, 765 | {file = "psycopg_binary-3.1.18-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:59701118c7d8842e451f1e562d08e8708b3f5d14974eefbce9374badd723c4ae"}, 766 | {file = "psycopg_binary-3.1.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:dea4a59da7850192fdead9da888e6b96166e90608cf39e17b503f45826b16f84"}, 767 | {file = "psycopg_binary-3.1.18-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4575da95fc441244a0e2ebaf33a2b2f74164603341d2046b5cde0a9aa86aa7e2"}, 768 | {file = "psycopg_binary-3.1.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:812726266ab96de681f2c7dbd6b734d327f493a78357fcc16b2ac86ff4f4e080"}, 769 | {file = "psycopg_binary-3.1.18-cp37-cp37m-win_amd64.whl", hash = "sha256:3e7ce4d988112ca6c75765c7f24c83bdc476a6a5ce00878df6c140ca32c3e16d"}, 770 | {file = "psycopg_binary-3.1.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:02bd4da45d5ee9941432e2e9bf36fa71a3ac21c6536fe7366d1bd3dd70d6b1e7"}, 771 | {file = "psycopg_binary-3.1.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39242546383f6b97032de7af30edb483d237a0616f6050512eee7b218a2aa8ee"}, 772 | {file = "psycopg_binary-3.1.18-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d46ae44d66bf6058a812467f6ae84e4e157dee281bfb1cfaeca07dee07452e85"}, 773 | {file = "psycopg_binary-3.1.18-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad35ac7fd989184bf4d38a87decfb5a262b419e8ba8dcaeec97848817412c64a"}, 774 | {file = "psycopg_binary-3.1.18-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:247474af262bdd5559ee6e669926c4f23e9cf53dae2d34c4d991723c72196404"}, 775 | {file = "psycopg_binary-3.1.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ebecbf2406cd6875bdd2453e31067d1bd8efe96705a9489ef37e93b50dc6f09"}, 776 | {file = "psycopg_binary-3.1.18-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1859aeb2133f5ecdd9cbcee155f5e38699afc06a365f903b1512c765fd8d457e"}, 777 | {file = "psycopg_binary-3.1.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:da917f6df8c6b2002043193cb0d74cc173b3af7eb5800ad69c4e1fbac2a71c30"}, 778 | {file = "psycopg_binary-3.1.18-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9e24e7b6a68a51cc3b162d0339ae4e1263b253e887987d5c759652f5692b5efe"}, 779 | {file = "psycopg_binary-3.1.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e252d66276c992319ed6cd69a3ffa17538943954075051e992143ccbf6dc3d3e"}, 780 | {file = "psycopg_binary-3.1.18-cp38-cp38-win_amd64.whl", hash = "sha256:5d6e860edf877d4413e4a807e837d55e3a7c7df701e9d6943c06e460fa6c058f"}, 781 | {file = "psycopg_binary-3.1.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eea5f14933177ffe5c40b200f04f814258cc14b14a71024ad109f308e8bad414"}, 782 | {file = "psycopg_binary-3.1.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:824a1bfd0db96cc6bef2d1e52d9e0963f5bf653dd5bc3ab519a38f5e6f21c299"}, 783 | {file = "psycopg_binary-3.1.18-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a87e9eeb80ce8ec8c2783f29bce9a50bbcd2e2342a340f159c3326bf4697afa1"}, 784 | {file = "psycopg_binary-3.1.18-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91074f78a9f890af5f2c786691575b6b93a4967ad6b8c5a90101f7b8c1a91d9c"}, 785 | {file = "psycopg_binary-3.1.18-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e05f6825f8db4428782135e6986fec79b139210398f3710ed4aa6ef41473c008"}, 786 | {file = "psycopg_binary-3.1.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f68ac2364a50d4cf9bb803b4341e83678668f1881a253e1224574921c69868c"}, 787 | {file = "psycopg_binary-3.1.18-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7ac1785d67241d5074f8086705fa68e046becea27964267ab3abd392481d7773"}, 788 | {file = "psycopg_binary-3.1.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:cd2a9f7f0d4dacc5b9ce7f0e767ae6cc64153264151f50698898c42cabffec0c"}, 789 | {file = "psycopg_binary-3.1.18-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:3e4b0bb91da6f2238dbd4fbb4afc40dfb4f045bb611b92fce4d381b26413c686"}, 790 | {file = "psycopg_binary-3.1.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:74e498586b72fb819ca8ea82107747d0cb6e00ae685ea6d1ab3f929318a8ce2d"}, 791 | {file = "psycopg_binary-3.1.18-cp39-cp39-win_amd64.whl", hash = "sha256:d4422af5232699f14b7266a754da49dc9bcd45eba244cf3812307934cd5d6679"}, 792 | ] 793 | 794 | [[package]] 795 | name = "psycopg2-binary" 796 | version = "2.9.9" 797 | description = "psycopg2 - Python-PostgreSQL Database Adapter" 798 | optional = false 799 | python-versions = ">=3.7" 800 | files = [ 801 | {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, 802 | {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, 803 | {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, 804 | {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, 805 | {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, 806 | {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, 807 | {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, 808 | {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, 809 | {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, 810 | {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, 811 | {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, 812 | {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, 813 | {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, 814 | {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, 815 | {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, 816 | {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, 817 | {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, 818 | {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, 819 | {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, 820 | {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, 821 | {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, 822 | {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, 823 | {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, 824 | {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, 825 | {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, 826 | {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, 827 | {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, 828 | {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, 829 | {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, 830 | {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, 831 | {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, 832 | {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, 833 | {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, 834 | {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, 835 | {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, 836 | {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, 837 | {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, 838 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, 839 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, 840 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, 841 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, 842 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, 843 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, 844 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, 845 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, 846 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, 847 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, 848 | {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, 849 | {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, 850 | {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, 851 | {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, 852 | {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, 853 | {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, 854 | {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, 855 | {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, 856 | {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, 857 | {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, 858 | {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, 859 | {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, 860 | {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, 861 | {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, 862 | {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, 863 | {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, 864 | {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, 865 | {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, 866 | {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, 867 | {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, 868 | {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, 869 | {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, 870 | {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, 871 | {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, 872 | {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, 873 | ] 874 | 875 | [[package]] 876 | name = "pycparser" 877 | version = "2.21" 878 | description = "C parser in Python" 879 | optional = false 880 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 881 | files = [ 882 | {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, 883 | {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, 884 | ] 885 | 886 | [[package]] 887 | name = "pydantic" 888 | version = "2.6.4" 889 | description = "Data validation using Python type hints" 890 | optional = false 891 | python-versions = ">=3.8" 892 | files = [ 893 | {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, 894 | {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, 895 | ] 896 | 897 | [package.dependencies] 898 | annotated-types = ">=0.4.0" 899 | email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} 900 | pydantic-core = "2.16.3" 901 | typing-extensions = ">=4.6.1" 902 | 903 | [package.extras] 904 | email = ["email-validator (>=2.0.0)"] 905 | 906 | [[package]] 907 | name = "pydantic-core" 908 | version = "2.16.3" 909 | description = "" 910 | optional = false 911 | python-versions = ">=3.8" 912 | files = [ 913 | {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, 914 | {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, 915 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, 916 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, 917 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, 918 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, 919 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, 920 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, 921 | {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, 922 | {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, 923 | {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, 924 | {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, 925 | {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, 926 | {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, 927 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, 928 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, 929 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, 930 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, 931 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, 932 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, 933 | {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, 934 | {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, 935 | {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, 936 | {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, 937 | {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, 938 | {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, 939 | {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, 940 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, 941 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, 942 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, 943 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, 944 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, 945 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, 946 | {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, 947 | {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, 948 | {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, 949 | {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, 950 | {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, 951 | {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, 952 | {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, 953 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, 954 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, 955 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, 956 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, 957 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, 958 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, 959 | {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, 960 | {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, 961 | {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, 962 | {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, 963 | {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, 964 | {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, 965 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, 966 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, 967 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, 968 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, 969 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, 970 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, 971 | {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, 972 | {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, 973 | {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, 974 | {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, 975 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, 976 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, 977 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, 978 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, 979 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, 980 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, 981 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, 982 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, 983 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, 984 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, 985 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, 986 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, 987 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, 988 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, 989 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, 990 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, 991 | {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, 992 | ] 993 | 994 | [package.dependencies] 995 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 996 | 997 | [[package]] 998 | name = "pydantic-settings" 999 | version = "2.2.1" 1000 | description = "Settings management using Pydantic" 1001 | optional = false 1002 | python-versions = ">=3.8" 1003 | files = [ 1004 | {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, 1005 | {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, 1006 | ] 1007 | 1008 | [package.dependencies] 1009 | pydantic = ">=2.3.0" 1010 | python-dotenv = ">=0.21.0" 1011 | 1012 | [package.extras] 1013 | toml = ["tomli (>=2.0.1)"] 1014 | yaml = ["pyyaml (>=6.0.1)"] 1015 | 1016 | [[package]] 1017 | name = "pyjwt" 1018 | version = "2.8.0" 1019 | description = "JSON Web Token implementation in Python" 1020 | optional = false 1021 | python-versions = ">=3.7" 1022 | files = [ 1023 | {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, 1024 | {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, 1025 | ] 1026 | 1027 | [package.dependencies] 1028 | cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} 1029 | 1030 | [package.extras] 1031 | crypto = ["cryptography (>=3.4.0)"] 1032 | dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] 1033 | docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] 1034 | tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] 1035 | 1036 | [[package]] 1037 | name = "pytest" 1038 | version = "8.1.1" 1039 | description = "pytest: simple powerful testing with Python" 1040 | optional = false 1041 | python-versions = ">=3.8" 1042 | files = [ 1043 | {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, 1044 | {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, 1045 | ] 1046 | 1047 | [package.dependencies] 1048 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 1049 | iniconfig = "*" 1050 | packaging = "*" 1051 | pluggy = ">=1.4,<2.0" 1052 | 1053 | [package.extras] 1054 | testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 1055 | 1056 | [[package]] 1057 | name = "pytest-asyncio" 1058 | version = "0.23.6" 1059 | description = "Pytest support for asyncio" 1060 | optional = false 1061 | python-versions = ">=3.8" 1062 | files = [ 1063 | {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"}, 1064 | {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"}, 1065 | ] 1066 | 1067 | [package.dependencies] 1068 | pytest = ">=7.0.0,<9" 1069 | 1070 | [package.extras] 1071 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] 1072 | testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] 1073 | 1074 | [[package]] 1075 | name = "pytest-cov" 1076 | version = "4.1.0" 1077 | description = "Pytest plugin for measuring coverage." 1078 | optional = false 1079 | python-versions = ">=3.7" 1080 | files = [ 1081 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 1082 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 1083 | ] 1084 | 1085 | [package.dependencies] 1086 | coverage = {version = ">=5.2.1", extras = ["toml"]} 1087 | pytest = ">=4.6" 1088 | 1089 | [package.extras] 1090 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 1091 | 1092 | [[package]] 1093 | name = "pytest-dotenv" 1094 | version = "0.5.2" 1095 | description = "A py.test plugin that parses environment files before running tests" 1096 | optional = false 1097 | python-versions = "*" 1098 | files = [ 1099 | {file = "pytest-dotenv-0.5.2.tar.gz", hash = "sha256:2dc6c3ac6d8764c71c6d2804e902d0ff810fa19692e95fe138aefc9b1aa73732"}, 1100 | {file = "pytest_dotenv-0.5.2-py3-none-any.whl", hash = "sha256:40a2cece120a213898afaa5407673f6bd924b1fa7eafce6bda0e8abffe2f710f"}, 1101 | ] 1102 | 1103 | [package.dependencies] 1104 | pytest = ">=5.0.0" 1105 | python-dotenv = ">=0.9.1" 1106 | 1107 | [[package]] 1108 | name = "python-dateutil" 1109 | version = "2.9.0.post0" 1110 | description = "Extensions to the standard Python datetime module" 1111 | optional = false 1112 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 1113 | files = [ 1114 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 1115 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 1116 | ] 1117 | 1118 | [package.dependencies] 1119 | six = ">=1.5" 1120 | 1121 | [[package]] 1122 | name = "python-dotenv" 1123 | version = "1.0.1" 1124 | description = "Read key-value pairs from a .env file and set them as environment variables" 1125 | optional = false 1126 | python-versions = ">=3.8" 1127 | files = [ 1128 | {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, 1129 | {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, 1130 | ] 1131 | 1132 | [package.extras] 1133 | cli = ["click (>=5.0)"] 1134 | 1135 | [[package]] 1136 | name = "python-multipart" 1137 | version = "0.0.9" 1138 | description = "A streaming multipart parser for Python" 1139 | optional = false 1140 | python-versions = ">=3.8" 1141 | files = [ 1142 | {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, 1143 | {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, 1144 | ] 1145 | 1146 | [package.extras] 1147 | dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] 1148 | 1149 | [[package]] 1150 | name = "six" 1151 | version = "1.16.0" 1152 | description = "Python 2 and 3 compatibility utilities" 1153 | optional = false 1154 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 1155 | files = [ 1156 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1157 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1158 | ] 1159 | 1160 | [[package]] 1161 | name = "sniffio" 1162 | version = "1.3.1" 1163 | description = "Sniff out which async library your code is running under" 1164 | optional = false 1165 | python-versions = ">=3.7" 1166 | files = [ 1167 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 1168 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 1169 | ] 1170 | 1171 | [[package]] 1172 | name = "sqlalchemy" 1173 | version = "2.0.28" 1174 | description = "Database Abstraction Library" 1175 | optional = false 1176 | python-versions = ">=3.7" 1177 | files = [ 1178 | {file = "SQLAlchemy-2.0.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0b148ab0438f72ad21cb004ce3bdaafd28465c4276af66df3b9ecd2037bf252"}, 1179 | {file = "SQLAlchemy-2.0.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bbda76961eb8f27e6ad3c84d1dc56d5bc61ba8f02bd20fcf3450bd421c2fcc9c"}, 1180 | {file = "SQLAlchemy-2.0.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feea693c452d85ea0015ebe3bb9cd15b6f49acc1a31c28b3c50f4db0f8fb1e71"}, 1181 | {file = "SQLAlchemy-2.0.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5da98815f82dce0cb31fd1e873a0cb30934971d15b74e0d78cf21f9e1b05953f"}, 1182 | {file = "SQLAlchemy-2.0.28-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a5adf383c73f2d49ad15ff363a8748319ff84c371eed59ffd0127355d6ea1da"}, 1183 | {file = "SQLAlchemy-2.0.28-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56856b871146bfead25fbcaed098269d90b744eea5cb32a952df00d542cdd368"}, 1184 | {file = "SQLAlchemy-2.0.28-cp310-cp310-win32.whl", hash = "sha256:943aa74a11f5806ab68278284a4ddd282d3fb348a0e96db9b42cb81bf731acdc"}, 1185 | {file = "SQLAlchemy-2.0.28-cp310-cp310-win_amd64.whl", hash = "sha256:c6c4da4843e0dabde41b8f2e8147438330924114f541949e6318358a56d1875a"}, 1186 | {file = "SQLAlchemy-2.0.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46a3d4e7a472bfff2d28db838669fc437964e8af8df8ee1e4548e92710929adc"}, 1187 | {file = "SQLAlchemy-2.0.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3dd67b5d69794cfe82862c002512683b3db038b99002171f624712fa71aeaa"}, 1188 | {file = "SQLAlchemy-2.0.28-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61e2e41656a673b777e2f0cbbe545323dbe0d32312f590b1bc09da1de6c2a02"}, 1189 | {file = "SQLAlchemy-2.0.28-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0315d9125a38026227f559488fe7f7cee1bd2fbc19f9fd637739dc50bb6380b2"}, 1190 | {file = "SQLAlchemy-2.0.28-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af8ce2d31679006e7b747d30a89cd3ac1ec304c3d4c20973f0f4ad58e2d1c4c9"}, 1191 | {file = "SQLAlchemy-2.0.28-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:81ba314a08c7ab701e621b7ad079c0c933c58cdef88593c59b90b996e8b58fa5"}, 1192 | {file = "SQLAlchemy-2.0.28-cp311-cp311-win32.whl", hash = "sha256:1ee8bd6d68578e517943f5ebff3afbd93fc65f7ef8f23becab9fa8fb315afb1d"}, 1193 | {file = "SQLAlchemy-2.0.28-cp311-cp311-win_amd64.whl", hash = "sha256:ad7acbe95bac70e4e687a4dc9ae3f7a2f467aa6597049eeb6d4a662ecd990bb6"}, 1194 | {file = "SQLAlchemy-2.0.28-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d3499008ddec83127ab286c6f6ec82a34f39c9817f020f75eca96155f9765097"}, 1195 | {file = "SQLAlchemy-2.0.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b66fcd38659cab5d29e8de5409cdf91e9986817703e1078b2fdaad731ea66f5"}, 1196 | {file = "SQLAlchemy-2.0.28-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea30da1e76cb1acc5b72e204a920a3a7678d9d52f688f087dc08e54e2754c67"}, 1197 | {file = "SQLAlchemy-2.0.28-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:124202b4e0edea7f08a4db8c81cc7859012f90a0d14ba2bf07c099aff6e96462"}, 1198 | {file = "SQLAlchemy-2.0.28-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e23b88c69497a6322b5796c0781400692eca1ae5532821b39ce81a48c395aae9"}, 1199 | {file = "SQLAlchemy-2.0.28-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b6303bfd78fb3221847723104d152e5972c22367ff66edf09120fcde5ddc2e2"}, 1200 | {file = "SQLAlchemy-2.0.28-cp312-cp312-win32.whl", hash = "sha256:a921002be69ac3ab2cf0c3017c4e6a3377f800f1fca7f254c13b5f1a2f10022c"}, 1201 | {file = "SQLAlchemy-2.0.28-cp312-cp312-win_amd64.whl", hash = "sha256:b4a2cf92995635b64876dc141af0ef089c6eea7e05898d8d8865e71a326c0385"}, 1202 | {file = "SQLAlchemy-2.0.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e91b5e341f8c7f1e5020db8e5602f3ed045a29f8e27f7f565e0bdee3338f2c7"}, 1203 | {file = "SQLAlchemy-2.0.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45c7b78dfc7278329f27be02c44abc0d69fe235495bb8e16ec7ef1b1a17952db"}, 1204 | {file = "SQLAlchemy-2.0.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eba73ef2c30695cb7eabcdb33bb3d0b878595737479e152468f3ba97a9c22a4"}, 1205 | {file = "SQLAlchemy-2.0.28-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5df5d1dafb8eee89384fb7a1f79128118bc0ba50ce0db27a40750f6f91aa99d5"}, 1206 | {file = "SQLAlchemy-2.0.28-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2858bbab1681ee5406650202950dc8f00e83b06a198741b7c656e63818633526"}, 1207 | {file = "SQLAlchemy-2.0.28-cp37-cp37m-win32.whl", hash = "sha256:9461802f2e965de5cff80c5a13bc945abea7edaa1d29360b485c3d2b56cdb075"}, 1208 | {file = "SQLAlchemy-2.0.28-cp37-cp37m-win_amd64.whl", hash = "sha256:a6bec1c010a6d65b3ed88c863d56b9ea5eeefdf62b5e39cafd08c65f5ce5198b"}, 1209 | {file = "SQLAlchemy-2.0.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:843a882cadebecc655a68bd9a5b8aa39b3c52f4a9a5572a3036fb1bb2ccdc197"}, 1210 | {file = "SQLAlchemy-2.0.28-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dbb990612c36163c6072723523d2be7c3eb1517bbdd63fe50449f56afafd1133"}, 1211 | {file = "SQLAlchemy-2.0.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7e4baf9161d076b9a7e432fce06217b9bd90cfb8f1d543d6e8c4595627edb9"}, 1212 | {file = "SQLAlchemy-2.0.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0a5354cb4de9b64bccb6ea33162cb83e03dbefa0d892db88a672f5aad638a75"}, 1213 | {file = "SQLAlchemy-2.0.28-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fffcc8edc508801ed2e6a4e7b0d150a62196fd28b4e16ab9f65192e8186102b6"}, 1214 | {file = "SQLAlchemy-2.0.28-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aca7b6d99a4541b2ebab4494f6c8c2f947e0df4ac859ced575238e1d6ca5716b"}, 1215 | {file = "SQLAlchemy-2.0.28-cp38-cp38-win32.whl", hash = "sha256:8c7f10720fc34d14abad5b647bc8202202f4948498927d9f1b4df0fb1cf391b7"}, 1216 | {file = "SQLAlchemy-2.0.28-cp38-cp38-win_amd64.whl", hash = "sha256:243feb6882b06a2af68ecf4bec8813d99452a1b62ba2be917ce6283852cf701b"}, 1217 | {file = "SQLAlchemy-2.0.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc4974d3684f28b61b9a90fcb4c41fb340fd4b6a50c04365704a4da5a9603b05"}, 1218 | {file = "SQLAlchemy-2.0.28-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87724e7ed2a936fdda2c05dbd99d395c91ea3c96f029a033a4a20e008dd876bf"}, 1219 | {file = "SQLAlchemy-2.0.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68722e6a550f5de2e3cfe9da6afb9a7dd15ef7032afa5651b0f0c6b3adb8815d"}, 1220 | {file = "SQLAlchemy-2.0.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328529f7c7f90adcd65aed06a161851f83f475c2f664a898af574893f55d9e53"}, 1221 | {file = "SQLAlchemy-2.0.28-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:df40c16a7e8be7413b885c9bf900d402918cc848be08a59b022478804ea076b8"}, 1222 | {file = "SQLAlchemy-2.0.28-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:426f2fa71331a64f5132369ede5171c52fd1df1bd9727ce621f38b5b24f48750"}, 1223 | {file = "SQLAlchemy-2.0.28-cp39-cp39-win32.whl", hash = "sha256:33157920b233bc542ce497a81a2e1452e685a11834c5763933b440fedd1d8e2d"}, 1224 | {file = "SQLAlchemy-2.0.28-cp39-cp39-win_amd64.whl", hash = "sha256:2f60843068e432311c886c5f03c4664acaef507cf716f6c60d5fde7265be9d7b"}, 1225 | {file = "SQLAlchemy-2.0.28-py3-none-any.whl", hash = "sha256:78bb7e8da0183a8301352d569900d9d3594c48ac21dc1c2ec6b3121ed8b6c986"}, 1226 | {file = "SQLAlchemy-2.0.28.tar.gz", hash = "sha256:dd53b6c4e6d960600fd6532b79ee28e2da489322fcf6648738134587faf767b6"}, 1227 | ] 1228 | 1229 | [package.dependencies] 1230 | greenlet = {version = "!=0.4.17", 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\""} 1231 | typing-extensions = ">=4.6.0" 1232 | 1233 | [package.extras] 1234 | aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] 1235 | aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] 1236 | aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] 1237 | asyncio = ["greenlet (!=0.4.17)"] 1238 | asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] 1239 | mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] 1240 | mssql = ["pyodbc"] 1241 | mssql-pymssql = ["pymssql"] 1242 | mssql-pyodbc = ["pyodbc"] 1243 | mypy = ["mypy (>=0.910)"] 1244 | mysql = ["mysqlclient (>=1.4.0)"] 1245 | mysql-connector = ["mysql-connector-python"] 1246 | oracle = ["cx_oracle (>=8)"] 1247 | oracle-oracledb = ["oracledb (>=1.0.1)"] 1248 | postgresql = ["psycopg2 (>=2.7)"] 1249 | postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] 1250 | postgresql-pg8000 = ["pg8000 (>=1.29.1)"] 1251 | postgresql-psycopg = ["psycopg (>=3.0.7)"] 1252 | postgresql-psycopg2binary = ["psycopg2-binary"] 1253 | postgresql-psycopg2cffi = ["psycopg2cffi"] 1254 | postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] 1255 | pymysql = ["pymysql"] 1256 | sqlcipher = ["sqlcipher3_binary"] 1257 | 1258 | [[package]] 1259 | name = "starlette" 1260 | version = "0.36.3" 1261 | description = "The little ASGI library that shines." 1262 | optional = false 1263 | python-versions = ">=3.8" 1264 | files = [ 1265 | {file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"}, 1266 | {file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"}, 1267 | ] 1268 | 1269 | [package.dependencies] 1270 | anyio = ">=3.4.0,<5" 1271 | 1272 | [package.extras] 1273 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] 1274 | 1275 | [[package]] 1276 | name = "typing-extensions" 1277 | version = "4.10.0" 1278 | description = "Backported and Experimental Type Hints for Python 3.8+" 1279 | optional = false 1280 | python-versions = ">=3.8" 1281 | files = [ 1282 | {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, 1283 | {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, 1284 | ] 1285 | 1286 | [[package]] 1287 | name = "uvicorn" 1288 | version = "0.28.0" 1289 | description = "The lightning-fast ASGI server." 1290 | optional = false 1291 | python-versions = ">=3.8" 1292 | files = [ 1293 | {file = "uvicorn-0.28.0-py3-none-any.whl", hash = "sha256:6623abbbe6176204a4226e67607b4d52cc60ff62cda0ff177613645cefa2ece1"}, 1294 | {file = "uvicorn-0.28.0.tar.gz", hash = "sha256:cab4473b5d1eaeb5a0f6375ac4bc85007ffc75c3cc1768816d9e5d589857b067"}, 1295 | ] 1296 | 1297 | [package.dependencies] 1298 | click = ">=7.0" 1299 | h11 = ">=0.8" 1300 | 1301 | [package.extras] 1302 | standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] 1303 | 1304 | [metadata] 1305 | lock-version = "2.0" 1306 | python-versions = "^3.11" 1307 | content-hash = "6f6aec774627bd1ae25155a21df671bb381ebd35eb2dbcf5c4b7abf402011892" 1308 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fast-service" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["ilya-4real "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | fastapi = "^0.110.0" 11 | sqlalchemy = "^2.0.28" 12 | pydantic-settings = "^2.2.1" 13 | alembic = "^1.13.1" 14 | psycopg-binary = "^3.1.18" 15 | pydantic = {extras = ["email"], version = "^2.6.4"} 16 | python-multipart = "^0.0.9" 17 | bcrypt = "^4.1.2" 18 | pyjwt = {extras = ["crypto"], version = "^2.8.0"} 19 | asyncpg = "^0.29.0" 20 | flat = "^0.3.2" 21 | 22 | 23 | [tool.poetry.group.dev.dependencies] 24 | uvicorn = "^0.28.0" 25 | pytest = "^8.1.1" 26 | pytest-asyncio = "^0.23.6" 27 | faker = "^24.3.0" 28 | pytest-dotenv = "^0.5.2" 29 | httpx = "^0.27.0" 30 | pytest-cov = "^4.1.0" 31 | psycopg2-binary = "^2.9.9" 32 | 33 | [tool.pytest.ini_options] 34 | pythonpath =["."] 35 | asyncio_mode="auto" 36 | testpaths = ["tests"] 37 | env_override_existing_values=1 38 | env_files = ["src/.test.env"] 39 | 40 | 41 | [build-system] 42 | requires = ["poetry-core"] 43 | build-backend = "poetry.core.masonry.api" 44 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, APIRouter 2 | 3 | from .auth import auth_router 4 | 5 | 6 | def include_routers(app: FastAPI) -> None: 7 | routers: list[APIRouter] = [ 8 | auth_router 9 | ] 10 | 11 | for router in routers: 12 | app.include_router(router) 13 | -------------------------------------------------------------------------------- /src/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from .router import router as auth_router 2 | -------------------------------------------------------------------------------- /src/auth/config.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Staks-sor/Fast_service/1a9cc753895e7337eac6e917c2371e61af4b3967/src/auth/config.py -------------------------------------------------------------------------------- /src/auth/dependencies.py: -------------------------------------------------------------------------------- 1 | from datetime import UTC, datetime 2 | from typing import Annotated 3 | 4 | from fastapi import Depends, HTTPException, status 5 | 6 | from src.auth import utils 7 | from src.auth.schemas import UserResponse 8 | from src.auth.service import AuthService 9 | from src.uow import UoW, UoWInterface 10 | 11 | UowDep = Annotated[UoWInterface, Depends(UoW)] 12 | 13 | oauth_scheme = utils.OAuthPasswordWithCookie( 14 | tokenUrl="auth/login", auto_error=True 15 | ) 16 | 17 | 18 | async def get_current_user( 19 | uow: UowDep, token: str = Depends(oauth_scheme) 20 | ) -> UserResponse: 21 | payload = utils.decode_token(token, "access") 22 | if payload.exp < datetime.now(UTC): 23 | raise HTTPException( 24 | status.HTTP_401_UNAUTHORIZED, detail="not authenticated" 25 | ) 26 | user = await AuthService.get_user(payload.uuid, uow) 27 | return user 28 | -------------------------------------------------------------------------------- /src/auth/models.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID, uuid4 2 | 3 | from sqlalchemy import String 4 | from sqlalchemy.orm import Mapped, mapped_column, relationship 5 | 6 | from src.database import Base 7 | 8 | 9 | class User(Base): 10 | __tablename__ = "user" 11 | 12 | id: Mapped[UUID] = mapped_column(default=uuid4, primary_key=True) 13 | name: Mapped[str] = mapped_column(String(50), nullable=False) 14 | email: Mapped[str] = mapped_column(String(70), nullable=False, unique=True) 15 | hashed_password: Mapped[str] = mapped_column(nullable=False) 16 | is_admin: Mapped[bool] = mapped_column(default=False) 17 | is_active: Mapped[bool] = mapped_column(default=True) 18 | refresh_token: Mapped[str] = mapped_column(nullable=True) 19 | 20 | orders: Mapped[list["Order"]] = relationship(back_populates="user") # type: ignore # noqa: F821 21 | -------------------------------------------------------------------------------- /src/auth/router.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from fastapi import ( 4 | APIRouter, 5 | Depends, 6 | HTTPException, 7 | Request, 8 | Response, 9 | status, 10 | ) 11 | from fastapi.security import OAuth2PasswordRequestForm 12 | 13 | from src.auth.dependencies import UowDep, get_current_user 14 | from src.auth.schemas import UserCreateSchema, UserResponse 15 | from src.auth.service import AuthService 16 | from src.config import settings 17 | 18 | router = APIRouter(prefix="/auth", tags=["authentication"]) 19 | 20 | 21 | @router.post("/register") 22 | async def register_user(user: UserCreateSchema, uow: UowDep): 23 | await AuthService.add_user(user, uow) 24 | 25 | 26 | @router.post("/login") 27 | async def log_user_in( 28 | credentials: Annotated[OAuth2PasswordRequestForm, Depends()], 29 | response: Response, 30 | uow: UowDep, 31 | ): 32 | tokens = await AuthService.authenticate_user(credentials, uow) 33 | if tokens: 34 | response.set_cookie( 35 | key="access_token", 36 | value=tokens.access_token, 37 | max_age=settings.access_token_expiration * 60, 38 | httponly=True, 39 | ) 40 | response.set_cookie( 41 | key="refresh_token", 42 | value=tokens.refresh_token, 43 | max_age=settings.refresh_token_expiration * 60 * 60 * 24, 44 | httponly=True, 45 | ) 46 | 47 | 48 | @router.post("/refresh") 49 | async def refresh_tokens(request: Request, response: Response, uow: UowDep): 50 | current_token = request.cookies.get("refresh_token") 51 | 52 | if not current_token: 53 | raise HTTPException( 54 | status.HTTP_401_UNAUTHORIZED, detail="invalid token" 55 | ) 56 | 57 | new_tokens = await AuthService.refresh_tokens(current_token, uow) 58 | 59 | response.set_cookie( 60 | key="access_token", 61 | value=new_tokens.access_token, 62 | max_age=settings.access_token_expiration * 60, 63 | httponly=True, 64 | ) 65 | response.set_cookie( 66 | key="refresh_token", 67 | value=new_tokens.refresh_token, 68 | max_age=settings.refresh_token_expiration * 60 * 60 * 24, 69 | httponly=True, 70 | ) 71 | 72 | 73 | @router.post("/abort") 74 | async def abort_refresh_token( 75 | response: Response, 76 | uow: UowDep, 77 | user: UserResponse = Depends(get_current_user), 78 | ): 79 | await AuthService.abort_refresh_token(user.id, uow) 80 | 81 | response.delete_cookie("access_token") 82 | response.delete_cookie("refresh_token") 83 | 84 | 85 | @router.get("/me", response_model=UserResponse) 86 | async def get_me(user=Depends(get_current_user)): 87 | return user 88 | -------------------------------------------------------------------------------- /src/auth/schemas.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from uuid import UUID, uuid4 3 | 4 | from pydantic import BaseModel, EmailStr, ConfigDict, Field 5 | 6 | 7 | class UserCreateSchema(BaseModel): 8 | id: UUID = Field(default_factory=uuid4) 9 | name: str 10 | email: EmailStr 11 | password: str 12 | 13 | 14 | class UserToken(BaseModel): 15 | uuid: str 16 | exp: datetime 17 | 18 | 19 | class UserResponse(BaseModel): 20 | model_config = ConfigDict(from_attributes=True, extra="ignore") 21 | id: UUID 22 | name: str 23 | email: EmailStr 24 | is_active: bool 25 | -------------------------------------------------------------------------------- /src/auth/service.py: -------------------------------------------------------------------------------- 1 | from datetime import UTC, datetime 2 | from typing import NamedTuple 3 | from uuid import UUID 4 | 5 | from fastapi import HTTPException, status 6 | from fastapi.security import OAuth2PasswordRequestForm 7 | 8 | from src.auth import utils 9 | from src.auth.models import User 10 | from src.auth.schemas import UserCreateSchema, UserResponse 11 | from src.config import settings 12 | from src.uow import UoWInterface 13 | 14 | 15 | class Tokens(NamedTuple): 16 | access_token: str 17 | refresh_token: str 18 | 19 | 20 | class AuthService: 21 | @classmethod 22 | async def add_user(cls, user: UserCreateSchema, uow: UoWInterface): 23 | user_dict = user.model_dump(exclude="password") # type: ignore 24 | user_dict["hashed_password"] = utils.hash_password(user.password) 25 | 26 | async with uow: 27 | await uow.users.add_one(user_dict, User.id) 28 | await uow.commit() 29 | 30 | @classmethod 31 | async def get_user(cls, uuid: str, uow: UoWInterface): 32 | async with uow as uow: 33 | user = await uow.users.get_one(User.id, uuid) 34 | return UserResponse.model_validate(user) 35 | 36 | @classmethod 37 | async def authenticate_user( 38 | cls, credentials: OAuth2PasswordRequestForm, uow: UoWInterface 39 | ) -> Tokens: 40 | async with uow as uow: 41 | user = await uow.users.get_one(User.email, credentials.username) 42 | if not user or not utils.check_password( 43 | credentials.password, user.hashed_password 44 | ): 45 | raise HTTPException( 46 | status_code=status.HTTP_401_UNAUTHORIZED, 47 | detail="invalid credentials", 48 | ) 49 | user_id = user.id 50 | access_token = utils.generate_token( 51 | str(user_id), settings.access_token_expiration, "access" 52 | ) 53 | refresh_token = utils.generate_token( 54 | str(user_id), settings.refresh_token_expiration, "refresh" 55 | ) 56 | await uow.users.update_one( 57 | User.id, user_id, refresh_token=refresh_token 58 | ) 59 | await uow.commit() 60 | return Tokens(access_token, refresh_token) 61 | 62 | @classmethod 63 | async def refresh_tokens(cls, refresh_token: str, uow: UoWInterface): 64 | payload = utils.decode_token(refresh_token, "refresh") 65 | user_id = payload.uuid 66 | async with uow as uow: 67 | user = await uow.users.get_one(User.id, user_id) 68 | if not user or payload.exp < datetime.now(UTC): 69 | raise HTTPException( 70 | status.HTTP_401_UNAUTHORIZED, detail="not authenticated" 71 | ) 72 | new_access_token = utils.generate_token( 73 | str(user.id), settings.access_token_expiration, "access" 74 | ) 75 | new_refresh_token = utils.generate_token( 76 | str(user.id), settings.refresh_token_expiration, "refresh" 77 | ) 78 | await uow.users.update_one( 79 | User.id, user_id, refresh_token=new_refresh_token 80 | ) 81 | await uow.commit() 82 | return Tokens(new_access_token, new_refresh_token) 83 | 84 | @classmethod 85 | async def abort_refresh_token(cls, user_id: UUID, uow: UoWInterface): 86 | async with uow as uow: 87 | await uow.users.update_one(User.id, user_id, refresh_token=None) 88 | await uow.commit() 89 | -------------------------------------------------------------------------------- /src/auth/user_rep.py: -------------------------------------------------------------------------------- 1 | from src.auth.models import User 2 | from src.repository import SQLAlchemyRepository 3 | 4 | 5 | class UserRepository(SQLAlchemyRepository[User]): 6 | model = User 7 | pass 8 | -------------------------------------------------------------------------------- /src/auth/utils.py: -------------------------------------------------------------------------------- 1 | from datetime import UTC, datetime, timedelta 2 | from typing import Dict, Literal, Optional 3 | 4 | import bcrypt 5 | import jwt 6 | from fastapi import HTTPException, Request, status 7 | from fastapi.openapi.models import OAuthFlowPassword 8 | from fastapi.openapi.models import OAuthFlows as OAuthFlowsModel 9 | from fastapi.security import OAuth2 10 | 11 | from src.auth.schemas import UserToken 12 | from src.config import settings 13 | 14 | 15 | class OAuthPasswordWithCookie(OAuth2): 16 | def __init__( 17 | self, 18 | tokenUrl: str, 19 | scheme_name: str | None = None, 20 | scopes: Optional[Dict[str, str]] = None, 21 | auto_error: bool = True, 22 | ): 23 | if not scopes: 24 | scopes = {} 25 | pass_flow = OAuthFlowPassword(tokenUrl=tokenUrl, scopes=scopes) 26 | flows = OAuthFlowsModel(password=pass_flow) 27 | super().__init__( 28 | flows=flows, 29 | scheme_name=scheme_name, 30 | auto_error=auto_error, 31 | ) 32 | 33 | def __call__(self, request: Request) -> Optional[str]: 34 | authorization: str | None = request.cookies.get("access_token") 35 | if not authorization: 36 | if self.auto_error: 37 | raise HTTPException( 38 | status_code=status.HTTP_401_UNAUTHORIZED, 39 | detail="not authenticated", 40 | ) 41 | else: 42 | return None 43 | return authorization 44 | 45 | 46 | def generate_token( 47 | user_uuid: str, expires_in: int, type: Literal["access", "refresh"] 48 | ): 49 | expires = ( 50 | timedelta(minutes=expires_in) 51 | if type == "access" 52 | else timedelta(days=expires_in) 53 | ) 54 | payload = {"uuid": user_uuid, "exp": datetime.now(UTC) + expires} 55 | if type == "access": 56 | token = jwt.encode( 57 | payload, settings.jwt_access_secret, settings.jwt_algorithm 58 | ) 59 | else: 60 | token = jwt.encode( 61 | payload, settings.jwt_refresh_secret, settings.jwt_algorithm 62 | ) 63 | return token 64 | 65 | 66 | def decode_token(token: str, type: Literal["access", "refresh"]): 67 | try: 68 | if type == "access": 69 | payload = jwt.decode( 70 | token, settings.jwt_access_secret, [settings.jwt_algorithm] 71 | ) 72 | else: 73 | payload = jwt.decode( 74 | token, settings.jwt_refresh_secret, [settings.jwt_algorithm] 75 | ) 76 | except jwt.InvalidTokenError as e: 77 | print(e, type) 78 | raise HTTPException( 79 | status_code=status.HTTP_403_FORBIDDEN, detail="invalid token" 80 | ) 81 | return UserToken( 82 | uuid=payload["uuid"], exp=datetime.fromtimestamp(payload["exp"], UTC) 83 | ) 84 | 85 | 86 | def hash_password(password: str): 87 | password_encoded = password.encode() 88 | salt = bcrypt.gensalt() 89 | return bcrypt.hashpw(password_encoded, salt=salt).decode() 90 | 91 | 92 | def check_password(plain_password: str, hashed_password: str): 93 | return bcrypt.checkpw(plain_password.encode(), hashed_password.encode()) 94 | -------------------------------------------------------------------------------- /src/config.py: -------------------------------------------------------------------------------- 1 | from typing import Literal 2 | 3 | from pydantic_settings import BaseSettings, SettingsConfigDict 4 | 5 | 6 | class Settings(BaseSettings): 7 | MODE: str 8 | model_config = SettingsConfigDict(env_file="src/.env", extra="allow") 9 | 10 | postgres_password: str 11 | postgres_username: str 12 | postgres_host: str 13 | postgres_port: int 14 | postgres_db: str 15 | jwt_access_secret: str 16 | jwt_refresh_secret: str 17 | jwt_algorithm: str 18 | access_token_expiration: int 19 | refresh_token_expiration: int 20 | 21 | MODE: Literal["TEST", "DEV"] 22 | 23 | @property 24 | def postgres_dsn(self): 25 | return f"postgresql+asyncpg://{self.postgres_username}:{self.postgres_password}@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}" 26 | 27 | 28 | class Config: 29 | env_file = '.env' 30 | 31 | 32 | settings = Settings() # type: ignore 33 | -------------------------------------------------------------------------------- /src/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import NullPool 2 | from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine 3 | from sqlalchemy.orm import DeclarativeBase 4 | 5 | from src.config import settings 6 | 7 | if settings.MODE == "DEV": 8 | engine = create_async_engine(settings.postgres_dsn, echo=True) 9 | elif settings.MODE == "TEST": 10 | engine = create_async_engine(settings.postgres_dsn, poolclass=NullPool) 11 | 12 | session = async_sessionmaker(bind=engine) 13 | 14 | 15 | class Base(DeclarativeBase): 16 | pass 17 | 18 | repr_num: int = 3 19 | repr_cols: tuple | tuple[str, ...] = () 20 | 21 | def __repr__(self) -> str: 22 | cols = [] 23 | for idx, col in enumerate(self.__table__.columns.keys()): 24 | if col in self.repr_cols or idx < self.repr_num: 25 | cols.append(f"{col}={getattr(self, col)}") 26 | 27 | return f"{self.__class__.__name__}=({', '.join(cols)})" 28 | -------------------------------------------------------------------------------- /src/dependencies.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from fastapi import Depends 4 | 5 | from src.uow import UoW, UoWInterface 6 | 7 | UowDep = Annotated[UoWInterface, Depends(UoW)] 8 | -------------------------------------------------------------------------------- /src/errors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Staks-sor/Fast_service/1a9cc753895e7337eac6e917c2371e61af4b3967/src/errors/__init__.py -------------------------------------------------------------------------------- /src/errors/excaptions.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from fastapi import HTTPException 4 | 5 | 6 | class HTTPError(HTTPException): 7 | def __init__( 8 | self, 9 | status_code: int, 10 | *, 11 | errors: list[str], 12 | detail: str | None = None, 13 | headers: dict[str, str] | None = None, 14 | ): 15 | ( 16 | super().__init__( 17 | status_code=status_code, detail=detail, headers=headers 18 | ), 19 | ) 20 | self.errors = errors 21 | 22 | 23 | @dataclass 24 | class ApplicationException(Exception): 25 | status_code: int 26 | message_text: str 27 | 28 | @property 29 | def message(self): 30 | return f"An error has been occured: {self.message_text}" 31 | 32 | 33 | class RepositoryException(ApplicationException): 34 | pass 35 | -------------------------------------------------------------------------------- /src/errors/handlers.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | from fastapi.responses import JSONResponse 3 | 4 | from .excaptions import ApplicationException, HTTPError 5 | 6 | 7 | def http_error_handler(request: Request, exc: ApplicationException): 8 | return JSONResponse(status_code=exc.status_code, content=exc.message_text) 9 | -------------------------------------------------------------------------------- /src/errors/schema.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class ErrorSchema(BaseModel): 5 | error: str 6 | -------------------------------------------------------------------------------- /src/masters/models.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | 3 | from sqlalchemy import String 4 | from sqlalchemy.orm import Mapped, mapped_column, relationship 5 | 6 | from src.database import Base 7 | 8 | 9 | class Master(Base): 10 | __tablename__ = "master" 11 | id: Mapped[UUID] = mapped_column(primary_key=True) 12 | name: Mapped[str] = mapped_column(String(50), nullable=False) 13 | s_name: Mapped[str] = mapped_column(String(50), nullable=False) 14 | speciality: Mapped[str] 15 | experience: Mapped[int] = mapped_column(nullable=False) 16 | phone_number: Mapped[str] = mapped_column(String(20), nullable=False) 17 | 18 | orders: Mapped[list["Order"]] = relationship(back_populates="master") # type: ignore # noqa: F821 19 | -------------------------------------------------------------------------------- /src/masters/repository.py: -------------------------------------------------------------------------------- 1 | from src.masters.models import Master 2 | from src.repository import SQLAlchemyRepository 3 | 4 | 5 | class MasterRepository(SQLAlchemyRepository[Master]): 6 | model = Master 7 | -------------------------------------------------------------------------------- /src/masters/router.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from src.dependencies import UowDep 4 | from src.masters.schemas import MasterCreateSchema 5 | from src.masters.service import MasterService 6 | 7 | router = APIRouter(prefix="/masters", tags=["masters"]) 8 | 9 | 10 | @router.post("/") 11 | async def add_master(master: MasterCreateSchema, uow: UowDep): 12 | master_id = await MasterService.add_new_master(master, uow) 13 | return {"master id": master_id} 14 | -------------------------------------------------------------------------------- /src/masters/schemas.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID, uuid4 2 | 3 | from pydantic import BaseModel, Field 4 | 5 | 6 | class MasterCreateSchema(BaseModel): 7 | id: UUID = Field(default_factory=uuid4) 8 | name: str 9 | s_name: str 10 | speciality: str 11 | experience: int 12 | phone_number: str = Field(min_length=7, max_length=20) 13 | -------------------------------------------------------------------------------- /src/masters/service.py: -------------------------------------------------------------------------------- 1 | from src.masters.models import Master 2 | from src.masters.schemas import MasterCreateSchema 3 | from src.uow import UoWInterface 4 | 5 | 6 | class MasterService: 7 | @classmethod 8 | async def add_new_master( 9 | cls, master: MasterCreateSchema, uow: UoWInterface 10 | ): 11 | async with uow: 12 | master_id = await uow.masters.add_one( 13 | master.model_dump(), Master.id 14 | ) 15 | await uow.commit() 16 | return master_id 17 | -------------------------------------------------------------------------------- /src/orders/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from enum import Enum 3 | from uuid import UUID 4 | 5 | from sqlalchemy import DateTime, ForeignKey, func 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship 7 | 8 | from src.auth.models import User 9 | from src.database import Base 10 | 11 | 12 | class OrderStatus(Enum): 13 | created = "created" 14 | acepted = "accepted" 15 | in_progress = "in_progress" 16 | finished = "finished" 17 | canceled = "canceled" 18 | 19 | 20 | class Order(Base): 21 | __tablename__ = "order" 22 | id: Mapped[UUID] = mapped_column(primary_key=True) 23 | price: Mapped[int] 24 | ordered_at: Mapped[datetime] = mapped_column( 25 | DateTime(timezone=True), server_default=func.now() 26 | ) 27 | to_be_provided_at: Mapped[datetime] = mapped_column( 28 | DateTime(timezone=True), nullable=False 29 | ) 30 | status: Mapped[OrderStatus] = mapped_column(default="created") 31 | user_id: Mapped[UUID] = mapped_column( 32 | ForeignKey("user.id"), nullable=False 33 | ) 34 | master_id: Mapped[UUID] = mapped_column(ForeignKey("master.id")) 35 | 36 | master: Mapped["Master"] = relationship(back_populates="orders") # type: ignore # noqa: F821 37 | user: Mapped[User] = relationship(back_populates="orders") 38 | works: Mapped[list["Work"]] = relationship( # type: ignore # noqa: F821 39 | back_populates="orders", secondary="orders_works" 40 | ) 41 | 42 | 43 | class OrderWorks(Base): 44 | __tablename__ = "orders_works" 45 | order_id: Mapped[UUID] = mapped_column( 46 | ForeignKey("order.id"), primary_key=True 47 | ) 48 | work_id: Mapped[UUID] = mapped_column( 49 | ForeignKey("work.id"), primary_key=True 50 | ) 51 | -------------------------------------------------------------------------------- /src/orders/repository.py: -------------------------------------------------------------------------------- 1 | from typing import Sequence 2 | from uuid import UUID 3 | 4 | from sqlalchemy import select 5 | 6 | from src.orders.models import Order 7 | from src.repository import SQLAlchemyRepository 8 | 9 | 10 | class OrderRepository(SQLAlchemyRepository[Order]): 11 | model = Order 12 | 13 | async def get_orders_by_user_id( 14 | self, user_id: str | UUID 15 | ) -> Sequence[Order]: 16 | query = select(self.model).where(self.model.user_id == user_id) 17 | 18 | result = await self.session.execute(query) 19 | return result.scalars().all() 20 | -------------------------------------------------------------------------------- /src/orders/router.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends 2 | 3 | from src.auth.dependencies import get_current_user 4 | from src.auth.schemas import UserResponse 5 | from src.dependencies import UowDep 6 | from src.orders.schemas import CreateOrderSchema 7 | from src.orders.service import OrderService 8 | 9 | router = APIRouter(prefix="/orders", tags=["orders"]) 10 | 11 | 12 | @router.post("/") 13 | async def create_order( 14 | order: CreateOrderSchema, 15 | uow: UowDep, 16 | user: UserResponse = Depends(get_current_user), 17 | ): 18 | await OrderService.add_new_order(order, user.id, uow) 19 | -------------------------------------------------------------------------------- /src/orders/schemas.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from uuid import UUID, uuid4 3 | 4 | from pydantic import BaseModel, Field 5 | 6 | 7 | class CreateOrderSchema(BaseModel): 8 | id: UUID = Field(default_factory=uuid4) 9 | to_be_provided_at: datetime 10 | works_ids: list[UUID] 11 | master_id: UUID 12 | -------------------------------------------------------------------------------- /src/orders/service.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | 3 | from src.orders.models import Order 4 | from src.orders.schemas import CreateOrderSchema 5 | from src.uow import UoWInterface 6 | 7 | 8 | class OrderService: 9 | @classmethod 10 | async def add_new_order( 11 | cls, order: CreateOrderSchema, user_id: UUID, uow: UoWInterface 12 | ): 13 | async with uow: 14 | new_order = order.model_dump(exclude={"works_ids"}) 15 | new_order["user_id"] = user_id 16 | new_order["price"] = 0 17 | print(new_order) 18 | await uow.orders.add_one(new_order, Order.id) 19 | await uow.commit() 20 | -------------------------------------------------------------------------------- /src/repository.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from typing import Any, Generic, Sequence, Type, TypeVar 3 | 4 | from sqlalchemy import delete, insert, select, update 5 | from sqlalchemy.exc import IntegrityError 6 | from sqlalchemy.ext.asyncio import AsyncSession 7 | from sqlalchemy.orm import InstrumentedAttribute 8 | 9 | from src.database import Base 10 | from src.errors.excaptions import RepositoryException 11 | 12 | ModelType = TypeVar("ModelType", bound=Base) 13 | 14 | 15 | class AbstractRepository(ABC): 16 | @abstractmethod 17 | async def get_one(self, filter_by, filter_value): 18 | raise NotImplementedError 19 | 20 | @abstractmethod 21 | async def get_all(self, limit: int = 0, offset: int = 10): 22 | raise NotImplementedError 23 | 24 | @abstractmethod 25 | async def add_one(self, data: dict[str, Any], returning_value): 26 | raise NotImplementedError 27 | 28 | @abstractmethod 29 | async def delete_one(self, filter_by, filter_value): 30 | raise NotImplementedError 31 | 32 | @abstractmethod 33 | async def update_one(self, filter_by, filter_value, **new_data): 34 | raise NotImplementedError 35 | 36 | 37 | class SQLAlchemyRepository(AbstractRepository, Generic[ModelType]): 38 | model: Type[ModelType] 39 | 40 | def __init__(self, session: AsyncSession) -> None: 41 | self.session = session 42 | 43 | async def add_one( 44 | self, data: dict[str, Any], returning_value: InstrumentedAttribute 45 | ): 46 | stmt = insert(self.model).values(**data).returning(returning_value) 47 | try: 48 | result = await self.session.execute(stmt) 49 | except IntegrityError: 50 | raise RepositoryException( 51 | 400, "object with this id already exists" 52 | ) 53 | return result.scalar_one() 54 | 55 | async def get_one( 56 | self, filter_by: InstrumentedAttribute, filter_value: Any 57 | ) -> ModelType | None: 58 | query = select(self.model).where(filter_by == filter_value) 59 | result = await self.session.execute(query) 60 | return result.scalar_one_or_none() 61 | 62 | async def get_all( 63 | self, limit: int = 0, offset: int = 10 64 | ) -> Sequence[ModelType]: 65 | query = select(self.model).limit(limit).offset(offset) 66 | result = await self.session.execute(query) 67 | return result.scalars().all() 68 | 69 | async def update_one( 70 | self, filter_by: InstrumentedAttribute, filter_value: Any, **new_data 71 | ): 72 | stmt = ( 73 | update(self.model) 74 | .where(filter_by == filter_value) 75 | .values(**new_data) 76 | ) 77 | await self.session.execute(stmt) 78 | 79 | async def delete_one( 80 | self, filter_by: InstrumentedAttribute, filter_value: Any 81 | ) -> None: 82 | stmt = delete(self.model).where(filter_by == filter_value) 83 | await self.session.execute(stmt) 84 | -------------------------------------------------------------------------------- /src/service.py: -------------------------------------------------------------------------------- 1 | from .uow import UoW 2 | 3 | 4 | class Service: 5 | 6 | @classmethod 7 | def get_uow(cls) -> UoW: 8 | return UoW() 9 | -------------------------------------------------------------------------------- /src/uow.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | from src.auth.user_rep import UserRepository 4 | from src.database import session 5 | from src.masters.repository import MasterRepository 6 | from src.orders.repository import OrderRepository 7 | from src.works.repositories import SupplyRepository, WorkRepository 8 | 9 | 10 | class UoWInterface: 11 | users: UserRepository 12 | works: WorkRepository 13 | supplies: SupplyRepository 14 | masters: MasterRepository 15 | orders: OrderRepository 16 | 17 | async def __aenter__(self): 18 | raise NotImplementedError 19 | 20 | async def __aexit__(self, *args): 21 | raise NotImplementedError 22 | 23 | @abstractmethod 24 | async def commit(self): 25 | raise NotImplementedError 26 | 27 | @abstractmethod 28 | async def rollback(self): 29 | raise NotImplementedError 30 | 31 | 32 | class UoW(UoWInterface): 33 | def __init__(self) -> None: 34 | self.sessionmaker = session 35 | 36 | async def __aenter__(self): 37 | self.session = self.sessionmaker() 38 | self.users = UserRepository(self.session) 39 | self.works = WorkRepository(self.session) 40 | self.supplies = SupplyRepository(self.session) 41 | self.masters = MasterRepository(self.session) 42 | self.orders = OrderRepository(self.session) 43 | return self 44 | 45 | async def __aexit__(self, *args): 46 | await self.rollback() 47 | await self.session.close() 48 | 49 | async def commit(self): 50 | await self.session.commit() 51 | 52 | async def rollback(self): 53 | await self.session.rollback() 54 | -------------------------------------------------------------------------------- /src/works/dependencies.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Staks-sor/Fast_service/1a9cc753895e7337eac6e917c2371e61af4b3967/src/works/dependencies.py -------------------------------------------------------------------------------- /src/works/models.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | 3 | from sqlalchemy import ForeignKey, String, Text 4 | from sqlalchemy.orm import Mapped, mapped_column, relationship 5 | 6 | from src.database import Base 7 | 8 | 9 | class Work(Base): 10 | __tablename__ = "work" 11 | id: Mapped[UUID] = mapped_column(primary_key=True) 12 | title: Mapped[str] = mapped_column(String(70)) 13 | description: Mapped[str] = mapped_column(Text()) 14 | duration_in_minutes: Mapped[int] 15 | 16 | supplies: Mapped[list["Supply"]] = relationship( 17 | back_populates="works", secondary="work_supply" 18 | ) 19 | orders: Mapped[list["Order"]] = relationship( # type: ignore # noqa: F821 20 | back_populates="works", secondary="orders_works" 21 | ) 22 | 23 | 24 | class Supply(Base): 25 | __tablename__ = "supply" 26 | 27 | title: Mapped[str] = mapped_column(String(70), primary_key=True) 28 | supply_type: Mapped[str] = mapped_column(String(50)) 29 | amount: Mapped[int] 30 | works: Mapped[list["Work"]] = relationship( 31 | back_populates="supplies", secondary="work_supply" 32 | ) 33 | 34 | 35 | class WorkSupply(Base): 36 | __tablename__ = "work_supply" 37 | work_id: Mapped[UUID] = mapped_column( 38 | ForeignKey(Work.id), primary_key=True 39 | ) 40 | supply_title: Mapped[str] = mapped_column( 41 | ForeignKey(Supply.title), primary_key=True 42 | ) 43 | -------------------------------------------------------------------------------- /src/works/repositories.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Sequence 2 | 3 | from sqlalchemy import select 4 | from sqlalchemy.orm import joinedload, selectinload 5 | 6 | from src.repository import SQLAlchemyRepository 7 | from src.works.models import Supply, Work 8 | 9 | 10 | class WorkRepository(SQLAlchemyRepository[Work]): 11 | model = Work 12 | 13 | async def get_work_with_required_supplies( 14 | self, work_id: str 15 | ) -> Work | None: 16 | query = ( 17 | select(self.model) 18 | .where(self.model.id == work_id) 19 | .options(selectinload(self.model.supplies)) 20 | ) 21 | result = await self.session.execute(query) 22 | return result.unique().scalar_one_or_none() 23 | 24 | async def get_all_works_with_supplies(self): 25 | query = select(self.model).options(selectinload(self.model.supplies)) 26 | result = await self.session.execute(query) 27 | return result.unique().scalars() 28 | 29 | async def add_supplies_to_work( 30 | self, work_id: str, supplies: Iterable[Supply] 31 | ): 32 | query = ( 33 | select(self.model) 34 | .where(self.model.id == work_id) 35 | .options(selectinload(self.model.supplies)) 36 | ) 37 | query_result = await self.session.execute(query) 38 | work = query_result.scalar_one() 39 | for supply in supplies: 40 | work.supplies.append(supply) 41 | 42 | 43 | class SupplyRepository(SQLAlchemyRepository[Supply]): 44 | model = Supply 45 | 46 | async def get_supply_with_work(self, supply_title) -> Supply | None: 47 | query = ( 48 | select(self.model) 49 | .where(self.model.title == supply_title) 50 | .options(selectinload(self.model.works)) 51 | ) 52 | 53 | result = await self.session.execute(query) 54 | return result.unique().scalar_one_or_none() 55 | 56 | async def get_supplies_by_titles( 57 | self, titles: list[str] 58 | ) -> Sequence[Supply]: 59 | query = select(self.model).where(self.model.title.in_(titles)) 60 | result = await self.session.execute(query) 61 | return result.scalars().all() 62 | -------------------------------------------------------------------------------- /src/works/router.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, status 2 | 3 | from src.dependencies import UowDep 4 | from src.errors.schema import ErrorSchema 5 | from src.works.schemas import ( 6 | CreateSupplySchema, 7 | CreateWorkSchema, 8 | WorkCreatedSchema, 9 | WorkResponceSchema, 10 | ) 11 | from src.works.service import SupplyService, WorkService 12 | 13 | work_router = APIRouter(prefix="", tags=["works"]) 14 | 15 | 16 | @work_router.post( 17 | "/works", 18 | responses={ 19 | status.HTTP_201_CREATED: {"model": WorkCreatedSchema}, 20 | status.HTTP_400_BAD_REQUEST: {"model": ErrorSchema}, 21 | }, 22 | ) 23 | async def add_new_work(work_schema: CreateWorkSchema, uow: UowDep): 24 | """creeates new work with provided supplies ids""" 25 | work_id = await WorkService.add_work(work_schema, uow) 26 | if work_id: 27 | return WorkCreatedSchema(work_id=work_id) 28 | 29 | 30 | @work_router.get("/works") 31 | async def get_all_works(uow: UowDep): 32 | works = await WorkService.get_all_works(uow) 33 | return works 34 | 35 | 36 | @work_router.get("/works/{work_id}", response_model=WorkResponceSchema) 37 | async def get_work_by_id(work_id: str, uow: UowDep): 38 | """returns all available works with required supplies""" 39 | result = await WorkService.get_one_work(work_id, uow) 40 | return result 41 | 42 | 43 | @work_router.post("/supplies") 44 | async def add_new_supply(supply_schema: CreateSupplySchema, uow: UowDep): 45 | new_supply_title = await SupplyService.add_supply(supply_schema, uow) 46 | return {"supply title": new_supply_title} 47 | 48 | 49 | @work_router.get("/supplies/", response_model=list[CreateSupplySchema]) 50 | async def get_all_supplies(uow: UowDep, offset: int, limit: int): 51 | return await SupplyService.get_all_supplies(uow, limit, offset) 52 | -------------------------------------------------------------------------------- /src/works/schemas.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from uuid import UUID, uuid4 3 | 4 | from pydantic import BaseModel, ConfigDict, Field, TypeAdapter 5 | 6 | 7 | class CreateWorkSchema(BaseModel): 8 | id: UUID = Field(default_factory=uuid4) 9 | title: str 10 | description: str 11 | duration_in_minutes: int 12 | supplies: list[str] 13 | 14 | 15 | class CreateSupplySchema(BaseModel): 16 | model_config = ConfigDict(from_attributes=True) 17 | title: str 18 | supply_type: str 19 | amount: int 20 | 21 | 22 | class WorkResponceSchema(CreateWorkSchema): 23 | model_config = ConfigDict(from_attributes=True) 24 | id: UUID 25 | supplies: list[Optional[CreateSupplySchema]] = [] 26 | 27 | 28 | list_of_works = TypeAdapter(list[WorkResponceSchema]) 29 | list_of_supplies = TypeAdapter(list[CreateSupplySchema]) 30 | 31 | 32 | class SupplyResponceSchema(CreateSupplySchema): 33 | works: list[WorkResponceSchema] 34 | 35 | 36 | class WorkCreatedSchema(BaseModel): 37 | work_id: str 38 | -------------------------------------------------------------------------------- /src/works/service.py: -------------------------------------------------------------------------------- 1 | from src.uow import UoWInterface 2 | from src.works.models import Supply, Work, WorkSupply 3 | from src.works.schemas import ( 4 | CreateSupplySchema, 5 | CreateWorkSchema, 6 | WorkResponceSchema, 7 | ) 8 | 9 | 10 | class WorkService: 11 | @classmethod 12 | async def add_work(cls, new_work: CreateWorkSchema, uow: UoWInterface): 13 | async with uow: 14 | supplies = await uow.supplies.get_supplies_by_titles( 15 | new_work.supplies 16 | ) 17 | work_id = await uow.works.add_one( 18 | new_work.model_dump(exclude={"supplies"}), Work.id 19 | ) 20 | 21 | await uow.works.add_supplies_to_work(work_id, supplies) 22 | await uow.commit() 23 | return str(work_id) 24 | 25 | @classmethod 26 | async def get_all_works(cls, uow: UoWInterface): 27 | async with uow: 28 | works = await uow.works.get_all_works_with_supplies() 29 | dto = [WorkResponceSchema.model_validate(work) for work in works] 30 | return dto 31 | 32 | @classmethod 33 | async def get_one_work(cls, work_id: str, uow: UoWInterface): 34 | async with uow: 35 | work = await uow.works.get_work_with_required_supplies(work_id) 36 | return WorkResponceSchema.model_validate(work) 37 | 38 | 39 | class SupplyService: 40 | @classmethod 41 | async def add_supply( 42 | cls, new_supply: CreateSupplySchema, uow: UoWInterface 43 | ): 44 | async with uow: 45 | supply_title = await uow.supplies.add_one( 46 | new_supply.model_dump(), Supply.title 47 | ) 48 | await uow.commit() 49 | return supply_title 50 | 51 | @classmethod 52 | async def get_all_supplies( 53 | cls, uow: UoWInterface, limit: int, offset: int 54 | ): 55 | async with uow: 56 | supplies = await uow.supplies.get_all(limit, offset) 57 | dto = [ 58 | CreateSupplySchema.model_validate(supply) 59 | for supply in supplies 60 | ] 61 | 62 | return dto 63 | -------------------------------------------------------------------------------- /tests/auth/test_auth_service.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | from uuid import uuid4 3 | 4 | import bcrypt 5 | import jwt 6 | import pytest 7 | from faker import Faker 8 | from fastapi import HTTPException 9 | from fastapi.security import OAuth2PasswordRequestForm 10 | from pydantic import BaseModel 11 | 12 | from src.auth.models import User 13 | from src.auth.schemas import UserCreateSchema 14 | from src.auth.service import AuthService 15 | from src.config import settings 16 | from src.uow import UoW 17 | 18 | fake_jwt_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" 19 | 20 | 21 | class FakeUserModel(BaseModel): 22 | id: str 23 | hashed_password: str 24 | refresh_token: str = "" 25 | 26 | 27 | uow = UoW() 28 | 29 | fake = Faker() 30 | 31 | 32 | class FakeUserRepository: 33 | model = User 34 | 35 | def __init__(self) -> None: 36 | self._users = {} 37 | 38 | async def add_one(self, usermodel: dict, returning_value) -> None: 39 | user_id = usermodel["id"] 40 | self._users[user_id] = usermodel 41 | 42 | async def get_one(self, filter_by, filter_value): 43 | try: 44 | return self._users[filter_value] 45 | except KeyError: 46 | return None 47 | 48 | async def get_all(self): 49 | return list(self._users) 50 | 51 | async def update_one(self, filter_by, filter_value, **data): 52 | self._users[filter_value] = data 53 | 54 | async def delete_one(self, uuid: str): 55 | self._users[uuid] = None 56 | 57 | 58 | class FakeUoW: 59 | def __init__(self) -> None: 60 | self._commit = False 61 | self.users = FakeUserRepository() 62 | 63 | async def commit(self): 64 | self._commit = True 65 | 66 | async def rollback(self): ... 67 | 68 | async def __aenter__(self): 69 | return self 70 | 71 | async def __aexit__(self, *args): 72 | await self.rollback() 73 | 74 | 75 | @pytest.fixture(scope="function") 76 | async def fake_uow(): 77 | return FakeUoW() 78 | 79 | 80 | class TestAuthService: 81 | async def test_add_user(self, fake_uow): 82 | user_id = uuid4() 83 | user_name = fake.name() 84 | user_email = fake.email() 85 | user_password = fake.password() 86 | new_user = UserCreateSchema( 87 | id=user_id, 88 | name=user_name, 89 | email=user_email, 90 | password=user_password, 91 | ) 92 | 93 | await AuthService.add_user(new_user, fake_uow) # type: ignore 94 | 95 | assert len(fake_uow.users._users) == 1 96 | assert fake_uow.users._users[user_id]["name"] == user_name 97 | assert fake_uow.users._users[user_id]["email"] == user_email 98 | encrypted_pass = fake_uow.users._users[user_id]["hashed_password"] 99 | assert bcrypt.checkpw( 100 | new_user.password.encode(), encrypted_pass.encode() 101 | ) 102 | 103 | async def test_get_user(self, fake_uow): 104 | user_id = uuid4() 105 | user_name = fake.name() 106 | user_email = fake.email() 107 | created_user = { 108 | "id": user_id, 109 | "name": user_name, 110 | "email": user_email, 111 | "is_active": True, 112 | } 113 | fake_uow.users._users[str(user_id)] = created_user 114 | 115 | user_from_service = await AuthService.get_user(str(user_id), fake_uow) # type: ignore 116 | assert user_from_service.id == user_id 117 | assert user_from_service.email == user_email 118 | assert user_from_service.name == user_name 119 | 120 | async def test_successful_authenticate_user(self, fake_uow): 121 | user_id = str(uuid4()) 122 | user_name = fake.name() 123 | salt = bcrypt.gensalt() 124 | user_pass = "pass1234" 125 | user_pass_hashed = bcrypt.hashpw(user_pass.encode(), salt).decode() 126 | credentials = OAuth2PasswordRequestForm( 127 | username=user_name, password=user_pass 128 | ) 129 | fake_uow.users._users[user_name] = FakeUserModel( 130 | id=user_id, hashed_password=user_pass_hashed 131 | ) 132 | 133 | tokens = await AuthService.authenticate_user(credentials, fake_uow) # type: ignore 134 | payload = jwt.decode( 135 | tokens.refresh_token, 136 | settings.jwt_refresh_secret, 137 | [settings.jwt_algorithm], 138 | ) 139 | assert payload["uuid"] == user_id 140 | assert uow.commit 141 | 142 | async def test_fail_authenticate_user(self, fake_uow): 143 | with pytest.raises(HTTPException): 144 | credentials = OAuth2PasswordRequestForm( 145 | username="fake", password="pass" 146 | ) 147 | await AuthService.authenticate_user(credentials, fake_uow) # type: ignore 148 | 149 | async def test_fake_refresh_tokens(self, fake_uow): 150 | with pytest.raises(HTTPException): 151 | await AuthService.refresh_tokens(fake_jwt_token, fake_uow) # type: ignore 152 | 153 | async def test_expired_refresh_tokens(self, fake_uow): 154 | local_uow = FakeUoW() 155 | user_id = uuid4() 156 | expiration = datetime.now() - timedelta(days=1) 157 | user_dict = {"uuid": str(user_id), "exp": expiration} 158 | prepared_token = jwt.encode(user_dict, settings.jwt_refresh_secret) 159 | 160 | local_uow.users._users[prepared_token] = FakeUserModel( 161 | id=str(user_id), hashed_password="safssoiionsv" 162 | ) 163 | 164 | with pytest.raises(HTTPException): 165 | await AuthService.refresh_tokens(prepared_token, local_uow) # type: ignore 166 | 167 | async def test_no_refresh_token_in_storage(self, fake_uow): 168 | prepared_token = jwt.encode( 169 | {"uuid": str(uuid4()), "exp": datetime.now() + timedelta(days=1)}, 170 | settings.jwt_refresh_secret, 171 | ) 172 | fake_uow.users._users[prepared_token] = None 173 | 174 | with pytest.raises(HTTPException): 175 | await AuthService.refresh_tokens(prepared_token, fake_uow) 176 | 177 | async def test_successful_refresh_tokens(self, fake_uow, refresh_token): 178 | user_id = refresh_token[1] 179 | fake_uow.users._users[user_id] = FakeUserModel( 180 | id="unknown", hashed_password="strongly hashed" 181 | ) 182 | 183 | tokens = await AuthService.refresh_tokens(refresh_token[0], fake_uow) 184 | 185 | access_payload = jwt.decode( 186 | tokens.access_token, 187 | settings.jwt_access_secret, 188 | [settings.jwt_algorithm], 189 | ) 190 | 191 | refresh_payload = jwt.decode( 192 | tokens.refresh_token, 193 | settings.jwt_refresh_secret, 194 | [settings.jwt_algorithm], 195 | ) 196 | 197 | assert ( 198 | datetime.now().minute 199 | - datetime.fromtimestamp(access_payload["exp"]).minute 200 | <= settings.access_token_expiration 201 | ) 202 | 203 | assert ( 204 | datetime.now().day 205 | - datetime.fromtimestamp(refresh_payload["exp"]).day 206 | <= settings.refresh_token_expiration 207 | ) 208 | 209 | async def test_abort_token(self, fake_uow): 210 | fake_record = fake.name() 211 | user_id = uuid4() 212 | fake_uow.users._users[str(user_id)] = fake_record 213 | 214 | await AuthService.abort_refresh_token(user_id, fake_uow) 215 | assert fake_uow.users._users[user_id]["refresh_token"] is None 216 | -------------------------------------------------------------------------------- /tests/auth/test_jwt_generation.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from uuid import uuid4 3 | 4 | import pytest 5 | from fastapi import HTTPException 6 | 7 | from src.auth import utils 8 | from src.auth.schemas import UserToken 9 | from src.config import settings 10 | 11 | 12 | @pytest.fixture(scope="module") 13 | def fake_uuid() -> str: 14 | return str(uuid4()) 15 | 16 | 17 | @pytest.fixture(scope="module") 18 | def generated_access_token(fake_uuid) -> str: 19 | return utils.generate_token(fake_uuid, 30, "access") 20 | 21 | 22 | @pytest.fixture(scope="module") 23 | def genereated_refresh_token(fake_uuid) -> str: 24 | return utils.generate_token(fake_uuid, 30, "refresh") 25 | 26 | 27 | @pytest.fixture 28 | def fake_token(): 29 | return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" 30 | 31 | 32 | def test_decode_access_token(generated_access_token, fake_uuid): 33 | payload = utils.decode_token(generated_access_token, "access") 34 | assert isinstance(payload, UserToken) 35 | assert payload.uuid == fake_uuid 36 | assert ( 37 | datetime.now().minute - payload.exp.minute <= settings.access_token_expiration 38 | ) 39 | 40 | 41 | def test_decode_refresh_token(genereated_refresh_token, fake_uuid): 42 | payload = utils.decode_token(genereated_refresh_token, "refresh") 43 | assert isinstance(payload, UserToken) 44 | assert payload.uuid == fake_uuid 45 | assert datetime.now().day - payload.exp.day <= settings.refresh_token_expiration 46 | 47 | 48 | def test_decode_fake_access_token(fake_token): 49 | with pytest.raises(HTTPException): 50 | utils.decode_token(fake_token, "access") 51 | -------------------------------------------------------------------------------- /tests/auth/test_password_hashing.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.auth import utils 4 | 5 | 6 | @pytest.fixture 7 | def fake_db_pass() -> str: 8 | return utils.hash_password("pass1234") 9 | 10 | 11 | @pytest.fixture() 12 | def plain_pass(): 13 | return "pass1234" 14 | 15 | 16 | def test_success_hash(plain_pass, fake_db_pass): 17 | assert utils.check_password(plain_pass, fake_db_pass) 18 | 19 | 20 | @pytest.mark.parametrize("fake_pass", [("pa$s1234"), ("pass!234")]) 21 | def test_wrong_pass(fake_pass, fake_db_pass): 22 | assert not utils.check_password(fake_pass, fake_db_pass) 23 | -------------------------------------------------------------------------------- /tests/auth/test_user_rep.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | import pytest 4 | from faker import Faker 5 | from sqlalchemy import insert, select, text 6 | 7 | from src.auth.models import User 8 | from src.auth.schemas import UserCreateSchema 9 | from src.auth.user_rep import UserRepository 10 | from tests.conftest import session_maker 11 | 12 | fake = Faker() 13 | 14 | 15 | @pytest.fixture(scope="function") 16 | async def inserted_user(): 17 | user = { 18 | "id": str(uuid4()), 19 | "name": fake.name(), 20 | "email": fake.email(), 21 | "hashed_password": "SDLFVSDOIJFkmdsfa24", 22 | } 23 | async with session_maker() as session: 24 | stmt = insert(User).values(user) 25 | await session.execute(stmt) 26 | await session.commit() 27 | return user["id"] 28 | 29 | 30 | @pytest.fixture(scope="function") 31 | def users(): 32 | users = [ 33 | UserCreateSchema( 34 | name=fake.name(), email=fake.email(), password="pass1234" 35 | ), 36 | UserCreateSchema( 37 | name=fake.name(), email=fake.email(), password="pass1234" 38 | ), 39 | UserCreateSchema( 40 | name=fake.name(), email=fake.email(), password="pass1234" 41 | ), 42 | ] 43 | return users 44 | 45 | 46 | @pytest.fixture 47 | async def inserted_users(create_tables): 48 | number_of_users = 3 49 | async with session_maker() as session: 50 | clean_up_table_stmt = text('truncate table "user";') 51 | await session.execute(clean_up_table_stmt) 52 | await session.commit() 53 | 54 | for i in range(number_of_users): 55 | stmt = insert(User).values( 56 | { 57 | "name": fake.name(), 58 | "email": fake.email(), 59 | "hashed_password": fake.password(), 60 | } 61 | ) 62 | await session.execute(stmt) 63 | await session.commit() 64 | return number_of_users 65 | 66 | 67 | @pytest.mark.usefixtures("create_tables") 68 | class TestUserRepsitory: 69 | async def test_add_user(self, users): 70 | async with session_maker() as s: 71 | for user in users: 72 | user_dict = user.model_dump(exclude="password") 73 | user_dict["hashed_password"] = "psdafasdf" 74 | await UserRepository(s).add_one(user_dict, User.id) 75 | 76 | stmt = text('select count(*) from "user";') 77 | res = await s.execute(stmt) 78 | 79 | assert len(users) == res.scalar_one() 80 | 81 | async def test_update_user(self, inserted_user): 82 | new_name = "georgiy" 83 | new_email = "georgiy@mail.com" 84 | 85 | async with session_maker() as session: 86 | await UserRepository(session).update_one( 87 | User.id, inserted_user, name=new_name, email=new_email 88 | ) 89 | 90 | query = select(User).where(User.id == inserted_user) 91 | result = await session.execute(query) 92 | user = result.scalar_one() 93 | assert user.name == new_name 94 | assert user.email == new_email 95 | 96 | async def test_user_delete(self, inserted_user): 97 | async with session_maker() as session: 98 | await UserRepository(session).delete_one(User.id, inserted_user) 99 | 100 | query = select(User).where(User.id == inserted_user) 101 | result = await session.execute(query) 102 | 103 | assert result.scalar_one_or_none() is None 104 | 105 | async def test_get_all_users(self, inserted_users): 106 | async with session_maker() as session: 107 | users_in_repo = await UserRepository(session).get_all() 108 | 109 | assert len(users_in_repo) == inserted_users 110 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from datetime import UTC, datetime, timedelta 2 | from uuid import uuid4 3 | 4 | import jwt 5 | import pytest 6 | from sqlalchemy import NullPool 7 | from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine 8 | 9 | from src.auth.models import User # noqa: F401 10 | from src.config import settings 11 | from src.database import Base 12 | 13 | engine = create_async_engine(settings.postgres_dsn, poolclass=NullPool) 14 | 15 | session_maker = async_sessionmaker( 16 | bind=engine, class_=AsyncSession, expire_on_commit=False 17 | ) 18 | 19 | 20 | @pytest.fixture(scope="session") 21 | async def create_tables(): 22 | assert settings.MODE == "TEST" 23 | async with engine.begin() as conn: 24 | await conn.run_sync(Base.metadata.create_all) 25 | 26 | yield 27 | 28 | async with engine.begin() as conn: 29 | await conn.run_sync(Base.metadata.drop_all) 30 | 31 | 32 | @pytest.fixture(scope="function") 33 | def access_token() -> tuple[str, str]: 34 | user_id = str(uuid4()) 35 | payload = { 36 | "uuid": user_id, 37 | "exp": datetime.now(UTC) + timedelta(minutes=settings.access_token_expiration), 38 | } 39 | return jwt.encode(payload, settings.jwt_access_secret), user_id 40 | 41 | 42 | @pytest.fixture(scope="function") 43 | def refresh_token(access_token): 44 | _, user_id = access_token 45 | payload = { 46 | "uuid": user_id, 47 | "exp": datetime.now(UTC) + timedelta(days=settings.refresh_token_expiration), 48 | } 49 | return jwt.encode( 50 | payload, settings.jwt_refresh_secret, settings.jwt_algorithm 51 | ), user_id 52 | -------------------------------------------------------------------------------- /tests/e2e/test_auth.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from uuid import uuid4 3 | 4 | import bcrypt 5 | import pytest 6 | from faker import Faker 7 | from httpx import ASGITransport, AsyncClient 8 | from sqlalchemy import text 9 | 10 | from main import get_app 11 | from tests.conftest import session_maker 12 | 13 | fake = Faker() 14 | 15 | 16 | @dataclass 17 | class UserModel: 18 | id: str 19 | name: str 20 | email: str 21 | hashed_password: str 22 | is_active: bool 23 | is_admin: bool 24 | refresh_token: str 25 | 26 | 27 | @pytest.fixture 28 | def password(): 29 | return fake.password() 30 | 31 | 32 | @pytest.fixture 33 | def hashed_password(password): 34 | return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() 35 | 36 | 37 | @pytest.fixture 38 | def created_user(hashed_password, refresh_token): 39 | refresh_token, user_id = refresh_token 40 | return UserModel( 41 | id=user_id, 42 | name=fake.name(), 43 | email=fake.email(), 44 | hashed_password=hashed_password, 45 | is_active=True, 46 | is_admin=False, 47 | refresh_token=refresh_token[0], 48 | ) 49 | 50 | 51 | @pytest.fixture(scope="session") 52 | def app(): 53 | return get_app() 54 | 55 | 56 | @pytest.fixture(scope="function") 57 | async def client(app): 58 | async with AsyncClient( 59 | transport=ASGITransport(app), base_url="http://localhost:8000" 60 | ) as test_client: 61 | yield test_client 62 | 63 | 64 | @pytest.fixture(scope="function") 65 | async def inserted_user(create_tables, created_user): 66 | query_params = { 67 | "id": created_user.id, 68 | "name": created_user.name, 69 | "email": created_user.email, 70 | "hashed_password": created_user.hashed_password, 71 | "is_admin": created_user.is_active, 72 | "is_active": created_user.is_admin, 73 | "refresh_token": created_user.refresh_token, 74 | } 75 | print(created_user.refresh_token) 76 | query = text( 77 | """insert into "user"(id, name, email, hashed_password, is_admin, is_active, refresh_token) 78 | values(:id, :name, :email, :hashed_password, :is_admin, :is_active, :refresh_token)""" 79 | ) 80 | async with session_maker() as session: 81 | await session.execute(query, query_params) 82 | await session.commit() 83 | 84 | 85 | async def test_successful_register_user(create_tables, client): 86 | user_name = fake.name() 87 | user_email = fake.email() 88 | user_pass = fake.password() 89 | responce = await client.post( 90 | "/auth/register", 91 | json={"name": user_name, "email": user_email, "password": user_pass}, 92 | ) 93 | 94 | assert responce.status_code == 200 95 | async with session_maker() as session: 96 | query = text('select * from "user" where email = :email;') 97 | result = await session.execute(query, {"email": user_email}) 98 | row = result.one_or_none() 99 | assert user_email in row 100 | assert user_name in row 101 | assert bcrypt.checkpw(user_pass.encode(), row[3].encode()) # type: ignore 102 | 103 | 104 | async def test_invalid_email_register(create_tables, client): 105 | user_name = fake.name() 106 | user_email = "wrongemail" 107 | user_pass = fake.password() 108 | 109 | responce = await client.post( 110 | "/auth/register", 111 | json={"name": user_name, "email": user_email, "password": user_pass}, 112 | ) 113 | assert responce.status_code == 422 114 | 115 | 116 | async def test_successful_login( 117 | create_tables, client, inserted_user, password, created_user 118 | ): 119 | form_data = { 120 | "username": created_user.email, 121 | "password": password, 122 | } 123 | responce = await client.post("/auth/login", data=form_data) 124 | assert responce.status_code == 200 125 | assert responce.cookies.get("access_token") is not None 126 | assert responce.cookies.get("refresh_token") is not None 127 | 128 | 129 | async def test_successful_refresh_tokens( 130 | create_tables, access_token, refresh_token, client, inserted_user, created_user 131 | ): 132 | client.cookies.set("access_token", access_token[0]) 133 | client.cookies.set("refresh_token", refresh_token[0]) 134 | print(refresh_token[0]) 135 | print(client.cookies.get("refresh_token")) 136 | responce = await client.post("/auth/refresh") 137 | assert responce.status_code == 200 138 | 139 | 140 | async def test_without_token_refresh_tokens(create_tables, client): 141 | responce = await client.post("/auth/refresh") 142 | 143 | assert responce.status_code == 401 144 | 145 | 146 | async def test_abort_tokens( 147 | create_tables, inserted_user, client, refresh_token, access_token 148 | ): 149 | refr_token, user_id = refresh_token 150 | client.cookies.set("access_token", access_token[0]) 151 | client.cookies.set("refresh_token", refr_token) 152 | 153 | responce = await client.post("/auth/abort") 154 | assert responce.status_code == 200 155 | assert responce.cookies.get("refresh_token") is None 156 | assert responce.cookies.get("access_token") is None 157 | --------------------------------------------------------------------------------