├── .env.template ├── .gitignore ├── .pre-commit-config.yaml ├── .vscode ├── launch.json └── settings.json ├── Dockerfile ├── LICENCE ├── Makefile ├── README.md ├── __init__.py ├── app ├── .dockerignore ├── __init__.py ├── alembic.ini ├── base │ ├── crud.py │ ├── db.py │ ├── exceptions.py │ ├── models.py │ └── routers.py ├── config.py ├── main.py ├── migrations │ ├── README │ ├── env.py │ ├── script.py.mako │ └── versions │ │ └── 0a389a1b9dd0_band_and_song_models.py ├── songs │ ├── __init__.py │ ├── crud.py │ ├── models.py │ ├── routes.py │ ├── schemas.py │ └── services.py └── tooling.py ├── docker-compose.yml ├── poetry.lock ├── pyproject.toml └── tests ├── conftest.py └── songs ├── conftest.py ├── factories.py ├── test_api_songs.py └── test_songs.py /.env.template: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgresql+asyncpg://postgres:postgres@db:5432/fanspark 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | .idea 162 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | default_language_version: 4 | python: python3.12 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.4.0 8 | hooks: 9 | - id: check-added-large-files 10 | - id: check-toml 11 | - id: check-yaml 12 | args: 13 | - --unsafe 14 | - id: end-of-file-fixer 15 | - id: trailing-whitespace 16 | - repo: https://github.com/charliermarsh/ruff-pre-commit 17 | rev: v0.1.2 18 | hooks: 19 | - id: ruff 20 | args: 21 | - --fix 22 | - id: ruff-format 23 | ci: 24 | autofix_commit_msg: 🎨 [pre-commit.ci] Auto format from pre-commit.com hooks 25 | autoupdate_commit_msg: ⬆ [pre-commit.ci] pre-commit autoupdate 26 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Python: FastAPI", 6 | "type": "python", 7 | "request": "launch", 8 | "module": "uvicorn", 9 | "args": [ 10 | "app.main:app", 11 | "--reload" 12 | ], 13 | "justMyCode": false 14 | }, 15 | { 16 | "name": "Debug Tests", 17 | "type": "python", 18 | "justMyCode": false, 19 | "request": "test", 20 | "console": "integratedTerminal", 21 | "env": { 22 | "PYTEST_ADDOPTS": "--no-cov" 23 | }, 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "." 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true, 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # pull official base image 2 | FROM python:3.12-slim-bookworm 3 | 4 | # set environment variables 5 | ENV PYTHONDONTWRITEBYTECODE 1 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | # set working directory 9 | WORKDIR /backend 10 | 11 | 12 | RUN apt update && apt install -y curl 13 | 14 | 15 | # Install Poetry 16 | RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python && \ 17 | cd /usr/local/bin && \ 18 | ln -s /opt/poetry/bin/poetry && \ 19 | poetry config virtualenvs.create false 20 | 21 | # Copy poetry.lock* in case it doesn't exist in the repo 22 | COPY pyproject.toml poetry.lock* /backend/ 23 | # RUN poetry install 24 | 25 | # Allow installing dev dependencies to run tests 26 | ARG INSTALL_DEV=false 27 | RUN bash -c "if [ $INSTALL_DEV == 'true' ] ; then poetry install --no-root ; else poetry install --no-root --only main ; fi" 28 | 29 | # add app 30 | ADD app app 31 | 32 | CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"] 33 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Pablo Iaria 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for managing a FastAPI application with Alembic and Docker 2 | 3 | # Variables 4 | MIN_PYTHON_VERSION = 3.12 5 | 6 | DOCKER_COMPOSE = docker compose 7 | COMPOSE_FILE = -f docker-compose.yml 8 | ALEMBIC = alembic -c /backend/app/alembic.ini 9 | RUFF = ruff 10 | MAKEFLAGS += --no-print-directory 11 | SHELL := /bin/bash 12 | 13 | GREEN=\033[0;32m 14 | RED=\033[0;31m 15 | NC=\033[0m # No Color 16 | 17 | 18 | # Help 19 | # Show this help. 20 | help: 21 | @echo "Usage: make " 22 | @echo "" 23 | @echo "Targets:" 24 | @awk '/^# / { help_message = substr($$0, 3); next } /^[a-zA-Z_-]+:/ { if (help_message) print " \033[36m" $$1 "\033[0m" help_message; help_message = "" }' $(MAKEFILE_LIST) 25 | 26 | ##@ Docker 27 | # Start Docker containers 28 | up: 29 | $(DOCKER_COMPOSE) $(COMPOSE_FILE) build 30 | $(DOCKER_COMPOSE) $(COMPOSE_FILE) up --remove-orphans 31 | 32 | # Stop Docker containers 33 | down: 34 | $(DOCKER_COMPOSE) $(COMPOSE_FILE) down --remove-orphans 35 | 36 | ##@ FastAPI 37 | # Run backend application 38 | run-backend: 39 | $(DOCKER_COMPOSE) $(COMPOSE_FILE) up backend 40 | 41 | # Run database service 42 | run-db: 43 | $(DOCKER_COMPOSE) $(COMPOSE_FILE) up db -d 44 | 45 | # Reset database service deleting all data 46 | reset-db: 47 | $(DOCKER_COMPOSE) $(COMPOSE_FILE) stop db 48 | $(DOCKER_COMPOSE) $(COMPOSE_FILE) rm -f db 49 | make run-db 50 | 51 | ##@ Alembic 52 | # Show current Alembic revision 53 | alembic-current: 54 | $(DOCKER_COMPOSE) $(COMPOSE_FILE) run backend $(ALEMBIC) current 55 | 56 | 57 | # Upgrade to the latest Alembic revision 58 | alembic-upgrade: 59 | $(DOCKER_COMPOSE) $(COMPOSE_FILE) run --rm backend $(ALEMBIC) upgrade head 60 | 61 | # Downgrade to the previous Alembic revision 62 | alembic-downgrade: 63 | $(DOCKER_COMPOSE) $(COMPOSE_FILE) run --rm backend $(ALEMBIC) downgrade -1 64 | 65 | # Downgrade to the previous Alembic revision 66 | alembic-history: 67 | $(DOCKER_COMPOSE) $(COMPOSE_FILE) run backend $(ALEMBIC) history -i --verbose 68 | 69 | ##@ Alembic 70 | # Create a new Alembic revision 71 | alembic-revision: 72 | $(DOCKER_COMPOSE) $(COMPOSE_FILE) run --rm backend $(ALEMBIC) revision --autogenerate -m "$(m)" 73 | 74 | # Create a new Alembic revision with a message 75 | migrate: 76 | make alembic-revision m="enter_migration_message_here" 77 | 78 | ##@ Testing 79 | 80 | # Run all tests with pytest 81 | test: 82 | @pytest 83 | 84 | # Run model tests with pytest 85 | test-model: 86 | @pytest -k 'not test_api' 87 | 88 | # Run integration tests with pytest 89 | test-api: 90 | @pytest -k 'test_api' 91 | 92 | 93 | ##@ Linting 94 | # Lint code with Ruff 95 | lint: 96 | $(RUFF) --fix . 97 | 98 | # Phony targets 99 | .PHONY: help up down run-backend run-db reset-db alembic-current alembic-upgrade alembic-downgrade alembic-revision migrate test lint 100 | 101 | # Set the default goal to 'help' when no target is given 102 | .DEFAULT_GOAL := help 103 | 104 | # check-python-version 105 | check-python-version: 106 | @echo "Checking Python ${MIN_PYTHON_VERSION}..." 107 | @(python --version 2>&1 | grep -q "Python ${MIN_PYTHON_VERSION}" ) || (python3 --version 2>&1 | grep -q "Python ${MIN_PYTHON_VERSION}" ) || \ 108 | (echo -e "${RED}Error: Python ${MIN_PYTHON_VERSION} is not installed" && exit 1) 109 | 110 | check-docker-version: 111 | @echo "Checking if Docker 2 is installed..." 112 | @(docker compose version 2>&1) || \ 113 | (echo -e "${RED}Error: Docker version 2 is not installed." && exit 1) 114 | 115 | check-poetry: 116 | @echo "Checking if Poetry is installed..." 117 | @command -v poetry >/dev/null 2>&1 || { echo "Installing Poetry..."; curl -sSL https://install.python-poetry.org | python3 -; } 118 | 119 | ##@ Environment 120 | # Setup development environment for the first time 121 | setup-environment: check-docker-version check-python-version check-poetry 122 | @echo "Setting up the environment..." 123 | @bash -c "poetry env use python3.12 && poetry install --no-root && pre-commit install" 124 | @echo -e "${GREEN}Environment is ready. Now run 'poetry shell' from the commandline to activate the environment${NC}" 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Template 2 | 3 | This is a simple project template using the following components: 4 | - FastAPI 5 | - SQLModel -> SQLAlchemy 6 | - Alembic for schema Migrations 7 | - pytest 8 | 9 | Also has a GenericCRUD (for interacting with the db model), GenericCrudRouter (for automatic expose rest API) 10 | 11 | 12 | ## Getting Started with the Project 13 | Before starting up the backend or running any commands, you need to set up the environment. This can be easily done using the make command. Simply run the following command in your terminal: 14 | ```sh 15 | make setup-environment 16 | ``` 17 | The following steps are executed: 18 | 19 | 1. **Check Docker Version**: 20 | - The command checks if Docker version 2 is installed on your system. If it is not installed, it will display an error message indicating that Docker version 2 is not installed. 21 | 22 | 2. **Check Python Version**: 23 | - It verifies whether Python version 3.12 is installed. If Python 3.12 is not found, an error message is displayed. 24 | 25 | 3. **Check and Install Poetry**: 26 | - The command checks for the presence of Poetry, a Python dependency management and packaging tool. If Poetry is not installed, it is automatically installed using a script from the official Poetry website. 27 | 28 | 4. **Setting Up the Environment**: 29 | - After performing the checks, it sets up the Python environment using Poetry. This step involves using Python 3.12 for the environment and installing all dependencies specified in the project's `pyproject.toml` file. 30 | 31 | And then to open a terminal for the project run: 32 | ```sh 33 | poetry shell 34 | ``` 35 | 36 | # Running locally 37 | ## vscode 38 | ## Running the Project Locally with Visual Studio Code (VSCode) 39 | 40 | To run the project locally using VSCode, follow these steps: 41 | 42 | 1. **Open the Project in VSCode**: 43 | - Start by opening the main folder in VSCode. 44 | 45 | 2. **Setup the Environment**: 46 | - Before running the project, ensure that you have set up the environment. Run the `make setup-environment` command in the terminal to prepare your environment. 47 | 48 | 3. **Activate the Python Environment**: 49 | - In the VSCode terminal, activate the Python environment created by Poetry by running: 50 | ```sh 51 | poetry shell 52 | ``` 53 | - This step is crucial to ensure that VSCode uses the correct Python interpreter and dependencies. 54 | 55 | 4. **Install VSCode Extensions**: 56 | - Install recommended VSCode extensions for Python and Docker to enhance your development experience. These extensions provide features like IntelliSense, code navigation, and Docker integration (Optional). 57 | 58 | 5. **Run the Project**: 59 | - Press Ctrl+`F5` to run the project. 60 | - Select `FastAPI` from the dropdown list of configurations. 61 | - Entrer `app.main:app` as the app entrypoint. 62 | - Optionally you could also specify to watch for code changes using the `--reload` parameter. 63 | Coverage should not run along with pytest because they collide, so edit the `.vscode/launch.json` file like this: 64 | ```json 65 | { 66 | "version": "0.2.0", 67 | "configurations": [ 68 | { 69 | "name": "Python: FastAPI", 70 | "type": "python", 71 | "request": "launch", 72 | "module": "uvicorn", 73 | "args": [ 74 | "app.main:app", 75 | "--reload" 76 | ], 77 | "justMyCode": false 78 | }, 79 | { 80 | "name": "Debug Tests", 81 | "type": "python", 82 | "justMyCode": false, 83 | "request": "test", 84 | "console": "integratedTerminal", 85 | "env": { 86 | "PYTEST_ADDOPTS": "--no-cov" 87 | }, 88 | } 89 | ] 90 | } 91 | ``` 92 | 93 | 6. **Access the Application**: 94 | - Once the project is running, you can access the application through the specified URL in a web browser. The URL will depend on how the project is configured (e.g., `http://localhost:8000` for a web application). 95 | 96 | 7. **Debugging**: 97 | - To debug your application, you can use the debugging features provided by VSCode. Set breakpoints in your code and start a debug session using the debug panel in VSCode. 98 | 99 | 100 | ## pycharm 101 | ## Running the Project Locally in PyCharm 102 | 103 | To run the project locally using PyCharm, follow these steps: 104 | 105 | 1. **Open the Project in PyCharm**: 106 | - Start by opening the main folder in PyCharm. 107 | 108 | 2. **Setup the Environment**: 109 | - Before running the project, ensure that you have set up the environment. In the PyCharm terminal, run the `make setup-environment` command to prepare your environment. PyCharm will suggest to use pyproject.toml, this is also valid. 110 | 111 | 3. **Activate the Python Environment**: 112 | - In the PyCharm terminal, activate the Python environment created by Poetry by running: 113 | ```sh 114 | poetry shell 115 | ``` 116 | - This step is essential to ensure that PyCharm uses the correct Python interpreter and dependencies. 117 | 118 | 4. **Configure PyCharm for the Project**: 119 | - Configure your PyCharm to recognize the Python interpreter set up by Poetry. Go to `File > Settings > Project: > Python Interpreter`, and select the Python interpreter from the virtual environment created by Poetry. 120 | - Optionally, install PyCharm extensions or plugins that facilitate Python and FastAPI development. 121 | 122 | 5. **Run the Project**: 123 | - Right-click on the file containing the main entry point of your FastAPI application (typically `main.py` or similar) in PyCharm and select `Run 'filename'`. 124 | - To enable automatic reloading on code changes, ensure the `--reload` parameter is included in the run configuration. 125 | 126 | 6. **Access the Application**: 127 | - Once the project is running, access the application through the specified URL in a web browser, such as `http://localhost:8000`. 128 | 129 | 130 | ## Starting up the backend 131 | 132 | To start the backend run the following command: 133 | 134 | ```sh 135 | make up 136 | ``` 137 | 138 | To stop the aplication run the folowwing command: 139 | 140 | ```sh 141 | make down 142 | ``` 143 | ## Testing 144 | To run the tests the database is used and each test case is wrapped in a transaction. This ensures that all databases operations are rollbacked after the tests are completed. To run all the tests execute this command: 145 | ```sh 146 | make tests 147 | ``` 148 | 149 | ## Generic CRUD 150 | 151 | The GenericCRUD class provides a set of standard CRUD (Create, Read, Update, Delete) operations for a given SQLAlchemy model. This class simplifies the process of interfacing with the database by abstracting common operations. 152 | 153 | ### Usage 154 | 155 | To use the GenericCRUD class, you need to define a SQLAlchemy model and corresponding Pydantic schema classes for create and update operations. Here's an example for a hypothetical Item model: 156 | 157 | ```python 158 | from sqlmodel import SQLModel 159 | 160 | from base.models import TimestampModel, UUIDModel 161 | 162 | 163 | class SongBase(SQLModel): 164 | name: str 165 | artist: str 166 | year: int | None = None 167 | 168 | class Song(SongBase, TimestampModel, UUIDModel, SoftDeleteModel, table=True): 169 | ... 170 | 171 | class SongCreate(SongBase): 172 | ... 173 | 174 | class SongUpdate(SongBase): 175 | ... 176 | 177 | ``` 178 | 179 | ### Creating a CRUD Object 180 | 181 | ```python 182 | item_crud = GenericCRUD[Item, ItemCreate, ItemUpdate](Item) 183 | ``` 184 | 185 | ## Understanding the Models: `TimestampModel`, `UUIDModel`, and `SoftDeleteModel` 186 | 187 | In the given code, three models are defined: `TimestampModel`, `UUIDModel`, and `SoftDeleteModel`. Each model serves a specific purpose. 188 | 189 | ### `UUIDModel` 190 | 191 | - **Purpose**: The `UUIDModel` is designed to provide a unique identifier for each record in a database table. 192 | - **Attributes**: 193 | - `id`: A field that stores a unique identifier for each record. It uses the UUID (Universally Unique Identifier) format. 194 | - **Characteristics**: 195 | - The UUID is generated using Python's `uuid.uuid4()` function, which creates a random, unique UUID. 196 | - The `id` field is marked as a primary key and is indexed for faster queries. 197 | - The field is non-nullable, meaning it must always have a value. 198 | - It is set to be unique across the model. 199 | 200 | ### `TimestampModel` 201 | 202 | - **Purpose**: The `TimestampModel` provides timestamp fields for tracking the creation and last update times of a record. 203 | - **Attributes**: 204 | - `created_at`: The datetime when the record was created. 205 | - `updated_at`: The datetime when the record was last updated. 206 | - **Characteristics**: 207 | - Both fields use `datetime.utcnow` as the default value, which sets the time to the current UTC time when the record is created. 208 | - The fields are non-nullable. 209 | - The `updated_at` field is designed to be updated whenever the record is modified, though the mechanism for this update (e.g., database triggers) might need to be enabled separately. 210 | 211 | ### `SoftDeleteModel` 212 | 213 | - **Purpose**: The `SoftDeleteModel` is used for soft deletion of records, a technique where records are not physically deleted from the database but are marked as deleted. 214 | - **Attributes**: 215 | - `deleted_at`: The datetime when the record was marked as deleted. 216 | - **Characteristics**: 217 | - This field is nullable, meaning it can hold a null value to indicate the record has not been deleted. 218 | - When a record is "soft deleted," this field is set to the current datetime. 219 | - Queries can then be written to exclude records where `deleted_at` is not null, effectively hiding soft-deleted records from normal use. 220 | 221 | ## Generic Router 222 | 223 | ### Overview 224 | 225 | The `GenericCrudRouter` class is a customizable router for creating CRUD (Create, Read, Update, Delete) endpoints in a FastAPI application. It simplifies the process of setting up standard CRUD operations for a given SQLAlchemy model and corresponding Pydantic schemas. 226 | 227 | ### Features 228 | 229 | - **Automated Route Creation**: Automatically creates standard CRUD routes for a specified SQLAlchemy model. 230 | - **Customizable**: Easily define custom Pydantic schemas for different operations (Create, Read, Update). 231 | - **Pagination**: Supports pagination for retrieving lists of items. 232 | 233 | ### How It Works 234 | 235 | The `GenericCrudRouter` class takes a SQLAlchemy model and Pydantic schema classes as inputs and generates standard CRUD routes. These routes include: 236 | 237 | - `GET /s`: Retrieve a paginated list of items. 238 | - `GET /s/{id}`: Retrieve a single item by ID. 239 | - `POST /s`: Create a new item. 240 | - `PUT /s/{id}`: Update an existing item by ID. 241 | - `DELETE /s/{id}`: Delete an existing item by ID. 242 | 243 | ### Usage 244 | 245 | To use the `GenericCrudRouter`, import the class and create an instance by passing the SQLAlchemy model and the Pydantic schema classes for the Create, Update, and Read operations. 246 | 247 | Here is an example of using the `GenericCrudRouter` for a `Song` model: 248 | 249 | ```python 250 | from base.routers import GenericCrudRouter 251 | from songs.models import Song, SongCreate, SongUpdate 252 | 253 | # Create a CRUD router for the Song model 254 | router = GenericCrudRouter(Song, Song, SongUpdate, SongCreate) 255 | ``` 256 | 257 | ## Setting Up Ruff for Code Linting 258 | 259 | ### Installation 260 | 261 | To improve code quality and consistency, we use `ruff` as a linting tool in our project. Follow these steps to install and configure `ruff`: 262 | 263 | ### Configuring Pre-Commit Hook 264 | 265 | After installing project dependencies, you need to set it up as a pre-commit hook: 266 | 267 | **Install the Pre-Commit Hook**: Run the following command to set up the git hook scripts: 268 | 269 | ```bash 270 | pre-commit install 271 | ``` 272 | 273 | This command installs the pre-commit hook into your `.git/hooks/pre-commit`. 274 | 275 | ### Running Ruff 276 | 277 | With the pre-commit hook installed, `ruff` will automatically run on the staged files each time you commit. To manually run `ruff` on all files in the project, you can use the following command: 278 | 279 | ```bash 280 | pre-commit run ruff --all-files 281 | ``` 282 | 283 | ### Updating Ruff Version 284 | 285 | To update to a newer version of `ruff`, change the `rev` value in the `.pre-commit-config.yaml` file to the desired version, and then update the pre-commit hooks with: 286 | 287 | ```bash 288 | pre-commit autoupdate 289 | ``` 290 | 291 | This will update your hooks to the latest versions specified in the configuration file. 292 | 293 | ## Testing 294 | When writing tests: 295 | - **`test_api_some_entity.py`**: This file should contain tests that focus on the API layer of the songs functionality. Here, you should write tests that make requests to your API endpoints and assert the responses. 296 | - **`test_some_entity.py`**: This file is intended for unit tests that directly interact with the model or services functionalities, independent of the API layer. These tests are crucial for ensuring the internal logic of your application works as expected. 297 | 298 | ### Testing 299 | ## Writting tests 300 | Suppose we want to write tests for the Song model you could have a folder structure like this: 301 | ``` 302 | tests 303 | └── songs 304 | ├── factories.py 305 | ├── test_api_songs.py 306 | └── test_songs.py 307 | ``` 308 | Where: 309 | - **factories.py**: Contains the factories for creating instances of your Pydantic models with test data. 310 | ```python 311 | from app.songs.models import SongCreate 312 | 313 | 314 | class SongCreationFactory(ModelFactory[SongCreate]): 315 | __model__ = SongCreate 316 | 317 | ``` 318 | - **test_api_songs.py**: Contains tests that make requests to your API endpoints and assert the responses. 319 | ```python 320 | async def test_create(api_client: TestClient): 321 | result = api_client.get("/songs") 322 | assert result 323 | assert result.status_code == 200 324 | 325 | result = result.json() 326 | assert len(result["items"]) == 0 327 | assert result["total"] == 0 328 | assert result["limit"] == 50 329 | assert result["offset"] == 0 330 | ``` 331 | `api_client` is injected by pytest dependency injection. 332 | - 333 | - **test_songs.py**: Contains tests that directly interact with the model or services functionalities, independent of the API layer. These tests are crucial for ensuring the internal logic of your application works as expected. 334 | ```python 335 | @pytest.mark.asyncio 336 | async def test_crud_create(db: AsyncSession): 337 | song_data = SongCreationFactory.build() 338 | result = await song_crud.create( 339 | db, 340 | obj_in=song_data.model_dump(), 341 | ) 342 | assert result 343 | assert result.id 344 | ``` 345 | 346 | ## Running tests 347 | The Makefile of the project includes commands to facilitate running tests easily. 348 | 349 | 1. **Running All Tests**: 350 | - To run all tests, use the following command in the terminal: 351 | ```sh 352 | make test 353 | ``` 354 | - This will execute all test cases, including both unit tests and API tests. 355 | 356 | 2. **Running API Tests**: 357 | - If you want to run only the API tests, use: 358 | ```sh 359 | make test-api 360 | ``` 361 | - This command runs tests with a naming pattern that matches 'test_api', ensuring only API tests are executed. 362 | 363 | 3. **Running Models Tests**: 364 | - If you want to run only the API tests, use: 365 | ```sh 366 | make test-model 367 | ``` 368 | - This command runs tests with a naming pattern that does not matches 'test_api', ensuring only model tests are executed. 369 | 370 | 371 | ## Alembic Workflow 372 | 373 | Using Alembic involves several common steps for managing database migrations in your FastAPI application. Here's a general workflow you can follow: 374 | 375 | 1. Initial Setup 376 | Ensure Docker is running. 377 | 2. Starting the Services 378 | Run `make up` to start all services defined in your docker-compose.yml, including your database service. 379 | 3. Creating a New Migration 380 | Whenever you make changes to your database models, you'll need to create a new migration script. 381 | Run `make alembic-revision m="description_of_changes"` to create a new migration script. Replace `description_of_changes` with a brief description of the changes your migration introduces. 382 | 4. Reviewing the Migration Script 383 | Alembic will generate a new migration script in your migrations directory (versions folder under the alembic directory). 384 | Review and, if necessary, manually edit the generated script to ensure it accurately represents the desired database schema changes. 385 | 5. Applying Migrations 386 | Run `make alembic-upgrade` to apply the latest migration to your database. This will bring your database schema up to date with your current model definitions. 387 | 6. Checking Migration Status 388 | You can check the current state of migrations in your database by running `make alembic-current`. This will show you the current revision of the database. 389 | 7. Rolling Back Migrations 390 | If you need to undo the last migration, you can run `make alembic-downgrade`. This command reverts the last applied migration. 391 | 8. Stopping the Services 392 | Once you're done, you can stop all services by running `make down`. 393 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iariap/fastapi-sqlmodel-alembic/8eb9dfd6ad07975e81ecb9fa46d0e9df12d21a62/__init__.py -------------------------------------------------------------------------------- /app/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | Dockerfile 3 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iariap/fastapi-sqlmodel-alembic/8eb9dfd6ad07975e81ecb9fa46d0e9df12d21a62/app/__init__.py -------------------------------------------------------------------------------- /app/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = app/migrations 6 | 7 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 8 | # Uncomment the line below if you want the files to be prepended with date and time 9 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 10 | 11 | # sys.path path, will be prepended to sys.path if present. 12 | # defaults to the current working directory. 13 | prepend_sys_path = . 14 | 15 | # timezone to use when rendering the date within the migration file 16 | # as well as the filename. 17 | # If specified, requires the python-dateutil library that can be 18 | # installed by adding `alembic[tz]` to the pip requirements 19 | # string value is passed to dateutil.tz.gettz() 20 | # leave blank for localtime 21 | # timezone = 22 | 23 | # max length of characters to apply to the 24 | # "slug" field 25 | # truncate_slug_length = 40 26 | 27 | # set to 'true' to run the environment during 28 | # the 'revision' command, regardless of autogenerate 29 | # revision_environment = false 30 | 31 | # set to 'true' to allow .pyc and .pyo files without 32 | # a source .py file to be detected as revisions in the 33 | # versions/ directory 34 | # sourceless = false 35 | 36 | # version location specification; This defaults 37 | # to migrations/versions. When using multiple version 38 | # directories, initial revisions must be specified with --version-path. 39 | # The path separator used here should be the separator specified by "version_path_separator" below. 40 | # version_locations = %(here)s/bar:%(here)s/bat:migrations/versions 41 | 42 | # version path separator; As mentioned above, this is the character used to split 43 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 44 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 45 | # Valid values for version_path_separator are: 46 | # 47 | # version_path_separator = : 48 | # version_path_separator = ; 49 | # version_path_separator = space 50 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 51 | 52 | # set to 'true' to search source files recursively 53 | # in each "version_locations" directory 54 | # new in Alembic version 1.10 55 | # recursive_version_locations = false 56 | 57 | # the output encoding used when revision files 58 | # are written from script.py.mako 59 | # output_encoding = utf-8 60 | 61 | sqlalchemy.url = postgresql+asyncpg://postgres:postgres@db:5432/fanspark 62 | 63 | 64 | [post_write_hooks] 65 | # post_write_hooks defines scripts or Python functions that are run 66 | # on newly generated revision scripts. See the documentation for further 67 | # detail and examples 68 | 69 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 70 | # hooks = black 71 | # black.type = console_scripts 72 | # black.entrypoint = black 73 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 74 | 75 | # Logging configuration 76 | [loggers] 77 | keys = root,sqlalchemy,alembic 78 | 79 | [handlers] 80 | keys = console 81 | 82 | [formatters] 83 | keys = generic 84 | 85 | [logger_root] 86 | level = WARN 87 | handlers = console 88 | qualname = 89 | 90 | [logger_sqlalchemy] 91 | level = WARN 92 | handlers = 93 | qualname = sqlalchemy.engine 94 | 95 | [logger_alembic] 96 | level = INFO 97 | handlers = 98 | qualname = alembic 99 | 100 | [handler_console] 101 | class = StreamHandler 102 | args = (sys.stderr,) 103 | level = NOTSET 104 | formatter = generic 105 | 106 | [formatter_generic] 107 | format = %(levelname)-5.5s [%(name)s] %(message)s 108 | datefmt = %H:%M:%S 109 | -------------------------------------------------------------------------------- /app/base/crud.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from typing import Any, Dict, Type 3 | 4 | from fastapi.encoders import jsonable_encoder 5 | from fastapi_pagination import LimitOffsetPage 6 | from fastapi_pagination.ext.sqlalchemy import paginate as fap_paginate 7 | from pydantic import BaseModel 8 | from sqlmodel import select 9 | from sqlmodel.ext.asyncio.session import AsyncSession 10 | 11 | from app.base.models import SoftDeleteModel 12 | 13 | 14 | class GenericCRUD[ 15 | ModelType: BaseModel, 16 | CreateSchemaType: BaseModel, 17 | UpdateSchemaType: BaseModel, 18 | ]: 19 | def __init__(self, model_type: Type[ModelType]): 20 | """ 21 | CRUD object with default methods to Create, Read, Update, Delete (CRUD). 22 | 23 | **Parameters** 24 | 25 | * `model`: A SQLAlchemy model class 26 | * `schema`: A Pydantic model (schema) class 27 | """ 28 | self.model_type = model_type 29 | 30 | def apply_soft_delete_filtering(self, statement): 31 | """If the model is soft delete, then filter out the deleted items""" 32 | if issubclass(self.model_type, SoftDeleteModel): 33 | statement = statement.filter(self.model_type.deleted_at == None) 34 | return statement 35 | 36 | async def get(self, db: AsyncSession, id: Any) -> ModelType: 37 | statement = select(self.model_type).filter(self.model_type.id == id) 38 | statement = self.apply_soft_delete_filtering(statement) 39 | 40 | result = await db.exec(statement) 41 | answer = result.one() 42 | return answer 43 | 44 | async def create(self, db: AsyncSession, *, obj_in: CreateSchemaType) -> ModelType: 45 | obj_in_data = jsonable_encoder(obj_in) 46 | db_obj = self.model_type(**obj_in_data) # type: ignore 47 | db.add(db_obj) 48 | await db.commit() 49 | await db.refresh(db_obj) 50 | return db_obj 51 | 52 | async def update( 53 | self, 54 | db: AsyncSession, 55 | *, 56 | id: Any, 57 | obj_in: UpdateSchemaType | Dict[str, Any], 58 | ) -> ModelType: 59 | db_obj = await self.get(db, id) 60 | obj_data = jsonable_encoder(db_obj) 61 | if isinstance(obj_in, dict): 62 | update_data = obj_in 63 | else: 64 | update_data = obj_in.model_dump(exclude_unset=True) 65 | for field in obj_data: 66 | if field in update_data: 67 | setattr(db_obj, field, update_data[field]) 68 | db.add(db_obj) 69 | await db.commit() 70 | await db.refresh(db_obj) 71 | return db_obj 72 | 73 | async def remove(self, db: AsyncSession, *, id: int) -> ModelType: 74 | obj = await self.get(db, id) 75 | 76 | if issubclass(self.model_type, SoftDeleteModel): 77 | obj.deleted_at = datetime.utcnow() 78 | else: 79 | await db.delete(obj) 80 | await db.commit() 81 | return obj 82 | 83 | async def paginate(self, db: AsyncSession) -> LimitOffsetPage[ModelType]: 84 | statement = select(self.model_type).order_by(self.model_type.id) 85 | 86 | statement = self.apply_soft_delete_filtering(statement) 87 | 88 | return await fap_paginate( 89 | db, 90 | statement, 91 | subquery_count=False, 92 | ) 93 | 94 | async def get_all(self, db: AsyncSession) -> list[ModelType]: 95 | statement = select(self.model_type).order_by(self.model_type.id) 96 | 97 | statement = self.apply_soft_delete_filtering(statement) 98 | 99 | result = await db.exec(statement) 100 | return result.all() 101 | -------------------------------------------------------------------------------- /app/base/db.py: -------------------------------------------------------------------------------- 1 | from typing import Annotated 2 | 3 | from fastapi import Depends 4 | from sqlalchemy.ext.asyncio import create_async_engine 5 | from sqlalchemy.orm import sessionmaker 6 | from sqlmodel import SQLModel 7 | from sqlmodel.ext.asyncio.session import AsyncSession 8 | 9 | from app.config import settings 10 | 11 | engine = create_async_engine(settings.DATABASE_URL, echo=True, future=True) 12 | SessionLocal = sessionmaker( 13 | autocommit=False, autoflush=False, bind=engine, class_=AsyncSession 14 | ) 15 | 16 | 17 | async def init_db(): 18 | print("Creating database...") 19 | async with engine.begin() as conn: 20 | # await conn.run_sync(SQLModel.metadata.drop_all) 21 | await conn.run_sync(SQLModel.metadata.create_all) 22 | 23 | 24 | async def get_session() -> AsyncSession: 25 | async with SessionLocal() as session: 26 | try: 27 | yield session 28 | await session.commit() 29 | except Exception as e: 30 | await session.rollback() 31 | raise e 32 | 33 | 34 | DBSession = Annotated[AsyncSession, Depends(get_session)] 35 | -------------------------------------------------------------------------------- /app/base/exceptions.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | from fastapi.responses import JSONResponse 3 | from sqlalchemy.exc import NoResultFound 4 | 5 | 6 | def add_exceptions_handlers(app: FastAPI): 7 | @app.exception_handler(NoResultFound) 8 | def handle_NoResultFound(request: Request, exc: NoResultFound): 9 | return JSONResponse( 10 | status_code=404, 11 | content={"message": "Not found"}, 12 | ) 13 | -------------------------------------------------------------------------------- /app/base/models.py: -------------------------------------------------------------------------------- 1 | import uuid as uuid_pkg 2 | from datetime import datetime 3 | 4 | from sqlmodel import Field, SQLModel, text 5 | 6 | 7 | class UUIDModel(SQLModel): 8 | id: uuid_pkg.UUID = Field( 9 | default_factory=uuid_pkg.uuid4, 10 | primary_key=True, 11 | index=True, 12 | nullable=False, 13 | sa_column_kwargs={ 14 | # "server_default": text("gen_random_uuid()"), 15 | "unique": True, 16 | }, 17 | ) 18 | 19 | 20 | class TimestampModel(SQLModel): 21 | created_at: datetime = Field( 22 | nullable=False, 23 | sa_column_kwargs={"server_default": text("current_timestamp")}, 24 | ) 25 | 26 | updated_at: datetime = Field( 27 | nullable=True, 28 | sa_column_kwargs={ 29 | "onupdate": text("current_timestamp"), 30 | }, 31 | ) 32 | 33 | 34 | class SoftDeleteModel(SQLModel): 35 | deleted_at: datetime = Field(nullable=True) 36 | -------------------------------------------------------------------------------- /app/base/routers.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from fastapi_pagination import LimitOffsetPage 3 | from pydantic import BaseModel 4 | 5 | from app.base.crud import GenericCRUD 6 | from app.base.db import DBSession 7 | 8 | 9 | class GenericCrudRouter(APIRouter): 10 | def __init__( 11 | self, 12 | model_type: BaseModel, 13 | GetSchemaType: BaseModel, 14 | CreateSchemaType: BaseModel, 15 | UpdateSchemaType: BaseModel, 16 | ): 17 | """ 18 | CRUD object with default methods to Create, Read, Update, Delete (CRUD). 19 | 20 | **Parameters** 21 | 22 | * `model`: A SQLAlchemy model class 23 | * `schema`: A Pydantic model (schema) class 24 | """ 25 | obj_name = f"{model_type.__name__.lower()}s" 26 | super().__init__(prefix=f"/{obj_name}", tags=[obj_name.capitalize()]) 27 | self.crud = GenericCRUD(model_type) 28 | 29 | @self.get("", name=f"Gets all {model_type.__name__.capitalize()}s") 30 | async def get_all( 31 | db: DBSession, 32 | ) -> LimitOffsetPage[GetSchemaType]: 33 | return await self.crud.paginate(db) 34 | 35 | @self.get( 36 | "/{id}", 37 | name=f"Gets an existing {model_type.__name__.lower()} by id", 38 | ) 39 | async def get_by_id( 40 | id: str, 41 | db: DBSession, 42 | ) -> GetSchemaType: 43 | return await self.crud.get(db, id) 44 | 45 | @self.post("", name=f"Creates a new {model_type.__name__.lower()}") 46 | async def create( 47 | obj_in: CreateSchemaType, 48 | db: DBSession, 49 | ) -> GetSchemaType: 50 | return await self.crud.create(db, obj_in=obj_in) 51 | 52 | @self.put("/{id}", name=f"Updates an existing {model_type.__name__.lower()}") 53 | async def update( 54 | id: str, 55 | obj_in: UpdateSchemaType, 56 | db: DBSession, 57 | ) -> GetSchemaType: 58 | return await self.crud.update(db, id=id, obj_in=obj_in) 59 | 60 | @self.delete( 61 | "/{id}", 62 | status_code=203, 63 | name=f"Deletes the {model_type.__name__.lower()} by id", 64 | ) 65 | async def delete( 66 | id: str, 67 | db: DBSession, 68 | ): 69 | await self.crud.remove(db, id=id) 70 | return None 71 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings, SettingsConfigDict 2 | 3 | 4 | class Settings(BaseSettings): 5 | model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8") 6 | DATABASE_URL: str 7 | OPENAPI_URL: str = "/openapi.json" 8 | 9 | 10 | settings = Settings() 11 | -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from fastapi import FastAPI 3 | from fastapi_pagination import add_pagination 4 | 5 | from app.base.exceptions import add_exceptions_handlers 6 | from app.config import settings 7 | from app.songs.routes import router as songs_router 8 | from app.tooling import router as tooling_router 9 | 10 | app = FastAPI(openapi_url=settings.OPENAPI_URL) 11 | 12 | 13 | app.include_router(songs_router) 14 | app.include_router(tooling_router) 15 | 16 | add_pagination(app) 17 | add_exceptions_handlers(app) 18 | 19 | if __name__ == "__main__": 20 | uvicorn.run("app.main:app", reload=True, workers=2) 21 | -------------------------------------------------------------------------------- /app/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration with an async dbapi. 2 | -------------------------------------------------------------------------------- /app/migrations/env.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from logging.config import fileConfig 3 | 4 | from alembic import context 5 | from sqlalchemy import pool 6 | from sqlalchemy.engine import Connection 7 | from sqlalchemy.ext.asyncio import async_engine_from_config 8 | from sqlmodel import SQLModel # NEW 9 | 10 | # this is the Alembic Config object, which provides 11 | # access to the values within the .ini file in use. 12 | config = context.config 13 | 14 | # Interpret the config file for Python logging. 15 | # This line sets up loggers basically. 16 | if config.config_file_name is not None: 17 | fileConfig(config.config_file_name) 18 | 19 | # add your model's MetaData object here 20 | # for 'autogenerate' support 21 | 22 | # target_metadata = mymodel.Base.metadata 23 | target_metadata = SQLModel.metadata # UPDATED 24 | 25 | # other values from the config, defined by the needs of env.py, 26 | # can be acquired: 27 | # my_important_option = config.get_main_option("my_important_option") 28 | # ... etc. 29 | 30 | 31 | def run_migrations_offline() -> None: 32 | """Run migrations in 'offline' mode. 33 | 34 | This configures the context with just a URL 35 | and not an Engine, though an Engine is acceptable 36 | here as well. By skipping the Engine creation 37 | we don't even need a DBAPI to be available. 38 | 39 | Calls to context.execute() here emit the given string to the 40 | script output. 41 | 42 | """ 43 | url = config.get_main_option("sqlalchemy.url") 44 | context.configure( 45 | url=url, 46 | target_metadata=target_metadata, 47 | literal_binds=True, 48 | dialect_opts={"paramstyle": "named"}, 49 | ) 50 | 51 | with context.begin_transaction(): 52 | context.run_migrations() 53 | 54 | 55 | def do_run_migrations(connection: Connection) -> None: 56 | context.configure(connection=connection, target_metadata=target_metadata) 57 | 58 | with context.begin_transaction(): 59 | context.run_migrations() 60 | 61 | 62 | async def run_async_migrations() -> None: 63 | """In this scenario we need to create an Engine 64 | and associate a connection with the context. 65 | 66 | """ 67 | 68 | connectable = async_engine_from_config( 69 | config.get_section(config.config_ini_section, {}), 70 | prefix="sqlalchemy.", 71 | poolclass=pool.NullPool, 72 | ) 73 | 74 | async with connectable.connect() as connection: 75 | await connection.run_sync(do_run_migrations) 76 | 77 | await connectable.dispose() 78 | 79 | 80 | def run_migrations_online() -> None: 81 | """Run migrations in 'online' mode.""" 82 | asyncio.run(run_async_migrations()) 83 | 84 | 85 | if context.is_offline_mode(): 86 | run_migrations_offline() 87 | else: 88 | run_migrations_online() 89 | -------------------------------------------------------------------------------- /app/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 alembic import op 9 | import sqlalchemy as sa 10 | import sqlmodel # NEW 11 | ${imports if imports else ""} 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = ${repr(up_revision)} 15 | down_revision = ${repr(down_revision)} 16 | branch_labels = ${repr(branch_labels)} 17 | depends_on = ${repr(depends_on)} 18 | 19 | 20 | def upgrade() -> None: 21 | ${upgrades if upgrades else "pass"} 22 | 23 | 24 | def downgrade() -> None: 25 | ${downgrades if downgrades else "pass"} 26 | -------------------------------------------------------------------------------- /app/migrations/versions/0a389a1b9dd0_band_and_song_models.py: -------------------------------------------------------------------------------- 1 | """band_and_song_models 2 | 3 | Revision ID: 0a389a1b9dd0 4 | Revises: 5 | Create Date: 2024-01-12 14:45:06.932777 6 | 7 | """ 8 | import sqlalchemy as sa 9 | import sqlmodel # NEW 10 | from alembic import op 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "0a389a1b9dd0" 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade() -> None: 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table( 22 | "band", 23 | sa.Column("deleted_at", sa.DateTime(), nullable=True), 24 | sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False), 25 | sa.Column( 26 | "created_at", 27 | sa.DateTime(), 28 | server_default=sa.text("current_timestamp"), 29 | nullable=False, 30 | ), 31 | sa.Column("updated_at", sa.DateTime(), nullable=True), 32 | sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), 33 | sa.PrimaryKeyConstraint("id"), 34 | ) 35 | op.create_index(op.f("ix_band_id"), "band", ["id"], unique=True) 36 | op.create_table( 37 | "song", 38 | sa.Column("deleted_at", sa.DateTime(), nullable=True), 39 | sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False), 40 | sa.Column( 41 | "created_at", 42 | sa.DateTime(), 43 | server_default=sa.text("current_timestamp"), 44 | nullable=False, 45 | ), 46 | sa.Column("updated_at", sa.DateTime(), nullable=True), 47 | sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False), 48 | sa.Column("artist", sqlmodel.sql.sqltypes.AutoString(), nullable=False), 49 | sa.Column("year", sa.Integer(), nullable=True), 50 | sa.Column("band_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False), 51 | sa.ForeignKeyConstraint( 52 | ["band_id"], 53 | ["band.id"], 54 | ), 55 | sa.PrimaryKeyConstraint("id"), 56 | ) 57 | op.create_index(op.f("ix_song_id"), "song", ["id"], unique=True) 58 | # ### end Alembic commands ### 59 | 60 | 61 | def downgrade() -> None: 62 | # ### commands auto generated by Alembic - please adjust! ### 63 | op.drop_index(op.f("ix_song_id"), table_name="song") 64 | op.drop_table("song") 65 | op.drop_index(op.f("ix_band_id"), table_name="band") 66 | op.drop_table("band") 67 | # ### end Alembic commands ### 68 | -------------------------------------------------------------------------------- /app/songs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iariap/fastapi-sqlmodel-alembic/8eb9dfd6ad07975e81ecb9fa46d0e9df12d21a62/app/songs/__init__.py -------------------------------------------------------------------------------- /app/songs/crud.py: -------------------------------------------------------------------------------- 1 | from app.base.crud import GenericCRUD 2 | from app.songs.models import Band, Song 3 | from app.songs.schemas import BandCreate, BandUpdate, SongCreate, SongUpdate 4 | 5 | 6 | class CRUDSong(GenericCRUD[Song, SongCreate, SongUpdate]): 7 | ... 8 | 9 | 10 | class CRUDBand(GenericCRUD[Band, BandCreate, BandUpdate]): 11 | ... 12 | 13 | 14 | song_crud = CRUDSong(Song) 15 | band_crud = CRUDBand(Band) 16 | -------------------------------------------------------------------------------- /app/songs/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from sqlmodel import Field, Relationship, SQLModel 4 | 5 | from app.base.models import SoftDeleteModel, TimestampModel, UUIDModel 6 | 7 | 8 | # Band models 9 | class BandBase(SQLModel): 10 | name: str 11 | 12 | 13 | class Band(BandBase, TimestampModel, UUIDModel, SoftDeleteModel, table=True): 14 | songs: list["Song"] = Relationship(back_populates="band") 15 | 16 | 17 | # Song models 18 | class SongBase(SQLModel): 19 | name: str 20 | artist: str 21 | year: int | None = None 22 | 23 | 24 | class Song(SongBase, TimestampModel, UUIDModel, SoftDeleteModel, table=True): 25 | band_id: uuid.UUID = Field(foreign_key="band.id") 26 | band: Band = Relationship(back_populates="songs") 27 | -------------------------------------------------------------------------------- /app/songs/routes.py: -------------------------------------------------------------------------------- 1 | from app.base.routers import GenericCrudRouter 2 | from app.songs.models import Song 3 | from app.songs.schemas import SongCreate, SongRead, SongUpdate 4 | 5 | router = GenericCrudRouter(Song, SongRead, SongUpdate, SongCreate) 6 | -------------------------------------------------------------------------------- /app/songs/schemas.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from app.songs.models import BandBase, SongBase 4 | 5 | 6 | # band schemas 7 | class BandCreate(BandBase): 8 | ... 9 | 10 | 11 | class BandUpdate(BandBase): 12 | ... 13 | 14 | 15 | class BandRead(BandBase): 16 | id: uuid.UUID 17 | 18 | 19 | # song schemas 20 | class SongRead(SongBase): 21 | band: BandRead 22 | 23 | 24 | class SongCreate(SongBase): 25 | band_id: uuid.UUID 26 | 27 | 28 | class SongUpdate(SongBase): 29 | ... 30 | -------------------------------------------------------------------------------- /app/songs/services.py: -------------------------------------------------------------------------------- 1 | # complete this class with proper business logic 2 | -------------------------------------------------------------------------------- /app/tooling.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | 3 | from app.base.db import init_db 4 | 5 | router = APIRouter(tags=["Tooling"]) 6 | 7 | 8 | @router.get("/ping") 9 | async def pong(): 10 | return {"ping": "pong!"} 11 | 12 | 13 | @router.post("/initdb") 14 | async def init_db_route() -> None: 15 | await init_db() 16 | 17 | 18 | # @router.get("/env") 19 | # async def env() -> None: 20 | # return settings 21 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | 5 | backend: 6 | build: 7 | context: . 8 | args: 9 | INSTALL_DEV: ${INSTALL_DEV-true} 10 | command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 2 --reload 11 | volumes: 12 | - ./app:/backend/app 13 | ports: 14 | - 8000:8000 15 | - 5678:5678 16 | env_file: 17 | - ./.env 18 | depends_on: 19 | - db 20 | 21 | db: 22 | image: postgres 23 | ports: 24 | - 5432:5432 25 | environment: 26 | - POSTGRES_USER=postgres 27 | - POSTGRES_PASSWORD=postgres 28 | - POSTGRES_DB=fanspark 29 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "aiosqlite" 5 | version = "0.19.0" 6 | description = "asyncio bridge to the standard sqlite3 module" 7 | optional = false 8 | python-versions = ">=3.7" 9 | files = [ 10 | {file = "aiosqlite-0.19.0-py3-none-any.whl", hash = "sha256:edba222e03453e094a3ce605db1b970c4b3376264e56f32e2a4959f948d66a96"}, 11 | {file = "aiosqlite-0.19.0.tar.gz", hash = "sha256:95ee77b91c8d2808bd08a59fbebf66270e9090c3d92ffbf260dc0db0b979577d"}, 12 | ] 13 | 14 | [package.extras] 15 | dev = ["aiounittest (==1.4.1)", "attribution (==1.6.2)", "black (==23.3.0)", "coverage[toml] (==7.2.3)", "flake8 (==5.0.4)", "flake8-bugbear (==23.3.12)", "flit (==3.7.1)", "mypy (==1.2.0)", "ufmt (==2.1.0)", "usort (==1.0.6)"] 16 | docs = ["sphinx (==6.1.3)", "sphinx-mdinclude (==0.5.3)"] 17 | 18 | [[package]] 19 | name = "alembic" 20 | version = "1.13.1" 21 | description = "A database migration tool for SQLAlchemy." 22 | optional = false 23 | python-versions = ">=3.8" 24 | files = [ 25 | {file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"}, 26 | {file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"}, 27 | ] 28 | 29 | [package.dependencies] 30 | Mako = "*" 31 | SQLAlchemy = ">=1.3.0" 32 | typing-extensions = ">=4" 33 | 34 | [package.extras] 35 | tz = ["backports.zoneinfo"] 36 | 37 | [[package]] 38 | name = "annotated-types" 39 | version = "0.6.0" 40 | description = "Reusable constraint types to use with typing.Annotated" 41 | optional = false 42 | python-versions = ">=3.8" 43 | files = [ 44 | {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, 45 | {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, 46 | ] 47 | 48 | [[package]] 49 | name = "anyio" 50 | version = "4.3.0" 51 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 52 | optional = false 53 | python-versions = ">=3.8" 54 | files = [ 55 | {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, 56 | {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, 57 | ] 58 | 59 | [package.dependencies] 60 | idna = ">=2.8" 61 | sniffio = ">=1.1" 62 | 63 | [package.extras] 64 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 65 | 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)"] 66 | trio = ["trio (>=0.23)"] 67 | 68 | [[package]] 69 | name = "asyncpg" 70 | version = "0.29.0" 71 | description = "An asyncio PostgreSQL driver" 72 | optional = false 73 | python-versions = ">=3.8.0" 74 | files = [ 75 | {file = "asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169"}, 76 | {file = "asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385"}, 77 | {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22"}, 78 | {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610"}, 79 | {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397"}, 80 | {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb"}, 81 | {file = "asyncpg-0.29.0-cp310-cp310-win32.whl", hash = "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449"}, 82 | {file = "asyncpg-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772"}, 83 | {file = "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"}, 84 | {file = "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"}, 85 | {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"}, 86 | {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"}, 87 | {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"}, 88 | {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"}, 89 | {file = "asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"}, 90 | {file = "asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"}, 91 | {file = "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"}, 92 | {file = "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"}, 93 | {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"}, 94 | {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"}, 95 | {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"}, 96 | {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"}, 97 | {file = "asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"}, 98 | {file = "asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"}, 99 | {file = "asyncpg-0.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9"}, 100 | {file = "asyncpg-0.29.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc"}, 101 | {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548"}, 102 | {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7"}, 103 | {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775"}, 104 | {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9"}, 105 | {file = "asyncpg-0.29.0-cp38-cp38-win32.whl", hash = "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408"}, 106 | {file = "asyncpg-0.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3"}, 107 | {file = "asyncpg-0.29.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da"}, 108 | {file = "asyncpg-0.29.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3"}, 109 | {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090"}, 110 | {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83"}, 111 | {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810"}, 112 | {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c"}, 113 | {file = "asyncpg-0.29.0-cp39-cp39-win32.whl", hash = "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2"}, 114 | {file = "asyncpg-0.29.0-cp39-cp39-win_amd64.whl", hash = "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8"}, 115 | {file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"}, 116 | ] 117 | 118 | [package.extras] 119 | docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] 120 | test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"] 121 | 122 | [[package]] 123 | name = "certifi" 124 | version = "2024.2.2" 125 | description = "Python package for providing Mozilla's CA Bundle." 126 | optional = false 127 | python-versions = ">=3.6" 128 | files = [ 129 | {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, 130 | {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, 131 | ] 132 | 133 | [[package]] 134 | name = "cfgv" 135 | version = "3.4.0" 136 | description = "Validate configuration and produce human readable error messages." 137 | optional = false 138 | python-versions = ">=3.8" 139 | files = [ 140 | {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, 141 | {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, 142 | ] 143 | 144 | [[package]] 145 | name = "click" 146 | version = "8.1.7" 147 | description = "Composable command line interface toolkit" 148 | optional = false 149 | python-versions = ">=3.7" 150 | files = [ 151 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 152 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 153 | ] 154 | 155 | [package.dependencies] 156 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 157 | 158 | [[package]] 159 | name = "colorama" 160 | version = "0.4.6" 161 | description = "Cross-platform colored terminal text." 162 | optional = false 163 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 164 | files = [ 165 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 166 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 167 | ] 168 | 169 | [[package]] 170 | name = "coverage" 171 | version = "7.4.3" 172 | description = "Code coverage measurement for Python" 173 | optional = false 174 | python-versions = ">=3.8" 175 | files = [ 176 | {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, 177 | {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, 178 | {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, 179 | {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, 180 | {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, 181 | {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, 182 | {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, 183 | {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, 184 | {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, 185 | {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, 186 | {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, 187 | {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, 188 | {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, 189 | {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, 190 | {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, 191 | {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, 192 | {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, 193 | {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, 194 | {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, 195 | {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, 196 | {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, 197 | {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, 198 | {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, 199 | {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, 200 | {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, 201 | {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, 202 | {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, 203 | {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, 204 | {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, 205 | {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, 206 | {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"}, 207 | {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"}, 208 | {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"}, 209 | {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"}, 210 | {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"}, 211 | {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"}, 212 | {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"}, 213 | {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"}, 214 | {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"}, 215 | {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"}, 216 | {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"}, 217 | {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"}, 218 | {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"}, 219 | {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"}, 220 | {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"}, 221 | {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"}, 222 | {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"}, 223 | {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"}, 224 | {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"}, 225 | {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"}, 226 | {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, 227 | {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, 228 | ] 229 | 230 | [package.extras] 231 | toml = ["tomli"] 232 | 233 | [[package]] 234 | name = "distlib" 235 | version = "0.3.8" 236 | description = "Distribution utilities" 237 | optional = false 238 | python-versions = "*" 239 | files = [ 240 | {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, 241 | {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, 242 | ] 243 | 244 | [[package]] 245 | name = "faker" 246 | version = "24.1.0" 247 | description = "Faker is a Python package that generates fake data for you." 248 | optional = false 249 | python-versions = ">=3.8" 250 | files = [ 251 | {file = "Faker-24.1.0-py3-none-any.whl", hash = "sha256:89ae0932f4f269754790569828859eaa0ae2ce73d1f3eb1f30ae7c20d4daf5ce"}, 252 | {file = "Faker-24.1.0.tar.gz", hash = "sha256:4fb0c16c71ad35d278a5fa7a4106a5c26c2b2b5c5efc47c1d67635db90b6071e"}, 253 | ] 254 | 255 | [package.dependencies] 256 | python-dateutil = ">=2.4" 257 | 258 | [[package]] 259 | name = "fastapi" 260 | version = "0.109.2" 261 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 262 | optional = false 263 | python-versions = ">=3.8" 264 | files = [ 265 | {file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"}, 266 | {file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"}, 267 | ] 268 | 269 | [package.dependencies] 270 | 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" 271 | starlette = ">=0.36.3,<0.37.0" 272 | typing-extensions = ">=4.8.0" 273 | 274 | [package.extras] 275 | 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)"] 276 | 277 | [[package]] 278 | name = "fastapi-pagination" 279 | version = "0.12.19" 280 | description = "FastAPI pagination" 281 | optional = false 282 | python-versions = ">=3.8,<4.0" 283 | files = [ 284 | {file = "fastapi_pagination-0.12.19-py3-none-any.whl", hash = "sha256:67838b21e2f62fae739117d130f8153a362d1fc266c2d738543e71d446618635"}, 285 | {file = "fastapi_pagination-0.12.19.tar.gz", hash = "sha256:d947d0c91589dc2fc99a15409707eb24272970a044489d21363641af03516fd7"}, 286 | ] 287 | 288 | [package.dependencies] 289 | fastapi = ">=0.93.0" 290 | pydantic = ">=1.9.1" 291 | typing-extensions = ">=4.8.0,<5.0.0" 292 | 293 | [package.extras] 294 | all = ["SQLAlchemy (>=1.3.20)", "asyncpg (>=0.24.0)", "beanie (>=1.25.0)", "bunnet (>=1.1.0,<2.0.0)", "databases (>=0.6.0)", "django (<5.0.0)", "mongoengine (>=0.23.1,<0.29.0)", "motor (>=2.5.1,<4.0.0)", "orm (>=0.3.1)", "ormar (>=0.11.2)", "piccolo (>=0.89,<0.122)", "pony (>=0.7.16,<0.8.0)", "scylla-driver (>=3.25.6,<4.0.0)", "sqlakeyset (>=2.0.1680321678,<3.0.0)", "sqlmodel (>=0.0.8,<0.0.15)", "tortoise-orm (>=0.16.18,<0.21.0)"] 295 | asyncpg = ["SQLAlchemy (>=1.3.20)", "asyncpg (>=0.24.0)"] 296 | beanie = ["beanie (>=1.25.0)"] 297 | bunnet = ["bunnet (>=1.1.0,<2.0.0)"] 298 | databases = ["databases (>=0.6.0)"] 299 | django = ["databases (>=0.6.0)", "django (<5.0.0)"] 300 | mongoengine = ["mongoengine (>=0.23.1,<0.29.0)"] 301 | motor = ["motor (>=2.5.1,<4.0.0)"] 302 | orm = ["databases (>=0.6.0)", "orm (>=0.3.1)"] 303 | ormar = ["ormar (>=0.11.2)"] 304 | piccolo = ["piccolo (>=0.89,<0.122)"] 305 | scylla-driver = ["scylla-driver (>=3.25.6,<4.0.0)"] 306 | sqlalchemy = ["SQLAlchemy (>=1.3.20)", "sqlakeyset (>=2.0.1680321678,<3.0.0)"] 307 | sqlmodel = ["sqlakeyset (>=2.0.1680321678,<3.0.0)", "sqlmodel (>=0.0.8,<0.0.15)"] 308 | tortoise = ["tortoise-orm (>=0.16.18,<0.21.0)"] 309 | 310 | [[package]] 311 | name = "filelock" 312 | version = "3.13.1" 313 | description = "A platform independent file lock." 314 | optional = false 315 | python-versions = ">=3.8" 316 | files = [ 317 | {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, 318 | {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, 319 | ] 320 | 321 | [package.extras] 322 | docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] 323 | testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] 324 | typing = ["typing-extensions (>=4.8)"] 325 | 326 | [[package]] 327 | name = "greenlet" 328 | version = "3.0.3" 329 | description = "Lightweight in-process concurrent programming" 330 | optional = false 331 | python-versions = ">=3.7" 332 | files = [ 333 | {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, 334 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, 335 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, 336 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, 337 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, 338 | {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, 339 | {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, 340 | {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, 341 | {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, 342 | {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, 343 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, 344 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, 345 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, 346 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, 347 | {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, 348 | {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, 349 | {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, 350 | {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, 351 | {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, 352 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, 353 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, 354 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, 355 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, 356 | {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, 357 | {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, 358 | {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, 359 | {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, 360 | {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, 361 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, 362 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, 363 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, 364 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, 365 | {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, 366 | {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, 367 | {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, 368 | {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, 369 | {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, 370 | {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, 371 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, 372 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, 373 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, 374 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, 375 | {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, 376 | {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, 377 | {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, 378 | {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, 379 | {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, 380 | {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, 381 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, 382 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, 383 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, 384 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, 385 | {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, 386 | {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, 387 | {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, 388 | {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, 389 | {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, 390 | {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, 391 | ] 392 | 393 | [package.extras] 394 | docs = ["Sphinx", "furo"] 395 | test = ["objgraph", "psutil"] 396 | 397 | [[package]] 398 | name = "h11" 399 | version = "0.14.0" 400 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 401 | optional = false 402 | python-versions = ">=3.7" 403 | files = [ 404 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 405 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 406 | ] 407 | 408 | [[package]] 409 | name = "httpcore" 410 | version = "1.0.4" 411 | description = "A minimal low-level HTTP client." 412 | optional = false 413 | python-versions = ">=3.8" 414 | files = [ 415 | {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, 416 | {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, 417 | ] 418 | 419 | [package.dependencies] 420 | certifi = "*" 421 | h11 = ">=0.13,<0.15" 422 | 423 | [package.extras] 424 | asyncio = ["anyio (>=4.0,<5.0)"] 425 | http2 = ["h2 (>=3,<5)"] 426 | socks = ["socksio (==1.*)"] 427 | trio = ["trio (>=0.22.0,<0.25.0)"] 428 | 429 | [[package]] 430 | name = "httpx" 431 | version = "0.26.0" 432 | description = "The next generation HTTP client." 433 | optional = false 434 | python-versions = ">=3.8" 435 | files = [ 436 | {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"}, 437 | {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"}, 438 | ] 439 | 440 | [package.dependencies] 441 | anyio = "*" 442 | certifi = "*" 443 | httpcore = "==1.*" 444 | idna = "*" 445 | sniffio = "*" 446 | 447 | [package.extras] 448 | brotli = ["brotli", "brotlicffi"] 449 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 450 | http2 = ["h2 (>=3,<5)"] 451 | socks = ["socksio (==1.*)"] 452 | 453 | [[package]] 454 | name = "identify" 455 | version = "2.5.35" 456 | description = "File identification library for Python" 457 | optional = false 458 | python-versions = ">=3.8" 459 | files = [ 460 | {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, 461 | {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, 462 | ] 463 | 464 | [package.extras] 465 | license = ["ukkonen"] 466 | 467 | [[package]] 468 | name = "idna" 469 | version = "3.6" 470 | description = "Internationalized Domain Names in Applications (IDNA)" 471 | optional = false 472 | python-versions = ">=3.5" 473 | files = [ 474 | {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, 475 | {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, 476 | ] 477 | 478 | [[package]] 479 | name = "iniconfig" 480 | version = "2.0.0" 481 | description = "brain-dead simple config-ini parsing" 482 | optional = false 483 | python-versions = ">=3.7" 484 | files = [ 485 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 486 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 487 | ] 488 | 489 | [[package]] 490 | name = "mako" 491 | version = "1.3.2" 492 | description = "A super-fast templating language that borrows the best ideas from the existing templating languages." 493 | optional = false 494 | python-versions = ">=3.8" 495 | files = [ 496 | {file = "Mako-1.3.2-py3-none-any.whl", hash = "sha256:32a99d70754dfce237019d17ffe4a282d2d3351b9c476e90d8a60e63f133b80c"}, 497 | {file = "Mako-1.3.2.tar.gz", hash = "sha256:2a0c8ad7f6274271b3bb7467dd37cf9cc6dab4bc19cb69a4ef10669402de698e"}, 498 | ] 499 | 500 | [package.dependencies] 501 | MarkupSafe = ">=0.9.2" 502 | 503 | [package.extras] 504 | babel = ["Babel"] 505 | lingua = ["lingua"] 506 | testing = ["pytest"] 507 | 508 | [[package]] 509 | name = "markupsafe" 510 | version = "2.1.5" 511 | description = "Safely add untrusted strings to HTML/XML markup." 512 | optional = false 513 | python-versions = ">=3.7" 514 | files = [ 515 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, 516 | {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, 517 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, 518 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, 519 | {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, 520 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, 521 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, 522 | {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, 523 | {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, 524 | {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, 525 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, 526 | {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, 527 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, 528 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, 529 | {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, 530 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, 531 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, 532 | {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, 533 | {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, 534 | {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, 535 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, 536 | {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, 537 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, 538 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, 539 | {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, 540 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, 541 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, 542 | {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, 543 | {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, 544 | {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, 545 | {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, 546 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, 547 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, 548 | {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, 549 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, 550 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, 551 | {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, 552 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, 553 | {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, 554 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, 555 | {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, 556 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, 557 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, 558 | {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, 559 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, 560 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, 561 | {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, 562 | {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, 563 | {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, 564 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, 565 | {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, 566 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, 567 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, 568 | {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, 569 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, 570 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, 571 | {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, 572 | {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, 573 | {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, 574 | {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, 575 | ] 576 | 577 | [[package]] 578 | name = "nest-asyncio" 579 | version = "1.6.0" 580 | description = "Patch asyncio to allow nested event loops" 581 | optional = false 582 | python-versions = ">=3.5" 583 | files = [ 584 | {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, 585 | {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, 586 | ] 587 | 588 | [[package]] 589 | name = "nodeenv" 590 | version = "1.8.0" 591 | description = "Node.js virtual environment builder" 592 | optional = false 593 | python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" 594 | files = [ 595 | {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, 596 | {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, 597 | ] 598 | 599 | [package.dependencies] 600 | setuptools = "*" 601 | 602 | [[package]] 603 | name = "packaging" 604 | version = "24.0" 605 | description = "Core utilities for Python packages" 606 | optional = false 607 | python-versions = ">=3.7" 608 | files = [ 609 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 610 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 611 | ] 612 | 613 | [[package]] 614 | name = "platformdirs" 615 | version = "4.2.0" 616 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 617 | optional = false 618 | python-versions = ">=3.8" 619 | files = [ 620 | {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, 621 | {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, 622 | ] 623 | 624 | [package.extras] 625 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 626 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 627 | 628 | [[package]] 629 | name = "pluggy" 630 | version = "1.4.0" 631 | description = "plugin and hook calling mechanisms for python" 632 | optional = false 633 | python-versions = ">=3.8" 634 | files = [ 635 | {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, 636 | {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, 637 | ] 638 | 639 | [package.extras] 640 | dev = ["pre-commit", "tox"] 641 | testing = ["pytest", "pytest-benchmark"] 642 | 643 | [[package]] 644 | name = "polyfactory" 645 | version = "2.15.0" 646 | description = "Mock data generation factories" 647 | optional = false 648 | python-versions = "<4.0,>=3.8" 649 | files = [ 650 | {file = "polyfactory-2.15.0-py3-none-any.whl", hash = "sha256:ff5b6a8742cbd6fbde9f81310b9732d5421fbec31916d6ede5a977753110fbe9"}, 651 | {file = "polyfactory-2.15.0.tar.gz", hash = "sha256:a3ff5263756ad74acf4001f04c1b6aab7d1197cbaa070352df79573a8dcd85ec"}, 652 | ] 653 | 654 | [package.dependencies] 655 | faker = "*" 656 | typing-extensions = ">=4.6.0" 657 | 658 | [package.extras] 659 | attrs = ["attrs (>=22.2.0)"] 660 | beanie = ["beanie", "pydantic[email]"] 661 | full = ["attrs", "beanie", "msgspec", "odmantic", "pydantic", "sqlalchemy"] 662 | msgspec = ["msgspec"] 663 | odmantic = ["odmantic (<1.0.0)", "pydantic[email]"] 664 | pydantic = ["pydantic[email]"] 665 | sqlalchemy = ["sqlalchemy (>=1.4.29)"] 666 | 667 | [[package]] 668 | name = "pre-commit" 669 | version = "3.6.2" 670 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 671 | optional = false 672 | python-versions = ">=3.9" 673 | files = [ 674 | {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, 675 | {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, 676 | ] 677 | 678 | [package.dependencies] 679 | cfgv = ">=2.0.0" 680 | identify = ">=1.0.0" 681 | nodeenv = ">=0.11.1" 682 | pyyaml = ">=5.1" 683 | virtualenv = ">=20.10.0" 684 | 685 | [[package]] 686 | name = "pydantic" 687 | version = "2.6.3" 688 | description = "Data validation using Python type hints" 689 | optional = false 690 | python-versions = ">=3.8" 691 | files = [ 692 | {file = "pydantic-2.6.3-py3-none-any.whl", hash = "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a"}, 693 | {file = "pydantic-2.6.3.tar.gz", hash = "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f"}, 694 | ] 695 | 696 | [package.dependencies] 697 | annotated-types = ">=0.4.0" 698 | pydantic-core = "2.16.3" 699 | typing-extensions = ">=4.6.1" 700 | 701 | [package.extras] 702 | email = ["email-validator (>=2.0.0)"] 703 | 704 | [[package]] 705 | name = "pydantic-core" 706 | version = "2.16.3" 707 | description = "" 708 | optional = false 709 | python-versions = ">=3.8" 710 | files = [ 711 | {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, 712 | {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, 713 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, 714 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, 715 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, 716 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, 717 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, 718 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, 719 | {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, 720 | {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, 721 | {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, 722 | {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, 723 | {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, 724 | {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, 725 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, 726 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, 727 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, 728 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, 729 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, 730 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, 731 | {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, 732 | {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, 733 | {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, 734 | {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, 735 | {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, 736 | {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, 737 | {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, 738 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, 739 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, 740 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, 741 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, 742 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, 743 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, 744 | {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, 745 | {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, 746 | {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, 747 | {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, 748 | {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, 749 | {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, 750 | {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, 751 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, 752 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, 753 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, 754 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, 755 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, 756 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, 757 | {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, 758 | {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, 759 | {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, 760 | {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, 761 | {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, 762 | {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, 763 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, 764 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, 765 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, 766 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, 767 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, 768 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, 769 | {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, 770 | {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, 771 | {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, 772 | {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, 773 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, 774 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, 775 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, 776 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, 777 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, 778 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, 779 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, 780 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, 781 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, 782 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, 783 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, 784 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, 785 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, 786 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, 787 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, 788 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, 789 | {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, 790 | ] 791 | 792 | [package.dependencies] 793 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 794 | 795 | [[package]] 796 | name = "pydantic-settings" 797 | version = "2.2.1" 798 | description = "Settings management using Pydantic" 799 | optional = false 800 | python-versions = ">=3.8" 801 | files = [ 802 | {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, 803 | {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, 804 | ] 805 | 806 | [package.dependencies] 807 | pydantic = ">=2.3.0" 808 | python-dotenv = ">=0.21.0" 809 | 810 | [package.extras] 811 | toml = ["tomli (>=2.0.1)"] 812 | yaml = ["pyyaml (>=6.0.1)"] 813 | 814 | [[package]] 815 | name = "pytest" 816 | version = "7.4.4" 817 | description = "pytest: simple powerful testing with Python" 818 | optional = false 819 | python-versions = ">=3.7" 820 | files = [ 821 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 822 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 823 | ] 824 | 825 | [package.dependencies] 826 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 827 | iniconfig = "*" 828 | packaging = "*" 829 | pluggy = ">=0.12,<2.0" 830 | 831 | [package.extras] 832 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 833 | 834 | [[package]] 835 | name = "pytest-asyncio" 836 | version = "0.23.5.post1" 837 | description = "Pytest support for asyncio" 838 | optional = false 839 | python-versions = ">=3.8" 840 | files = [ 841 | {file = "pytest-asyncio-0.23.5.post1.tar.gz", hash = "sha256:b9a8806bea78c21276bc34321bbf234ba1b2ea5b30d9f0ce0f2dea45e4685813"}, 842 | {file = "pytest_asyncio-0.23.5.post1-py3-none-any.whl", hash = "sha256:30f54d27774e79ac409778889880242b0403d09cabd65b727ce90fe92dd5d80e"}, 843 | ] 844 | 845 | [package.dependencies] 846 | pytest = ">=7.0.0,<9" 847 | 848 | [package.extras] 849 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] 850 | testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] 851 | 852 | [[package]] 853 | name = "pytest-cov" 854 | version = "4.1.0" 855 | description = "Pytest plugin for measuring coverage." 856 | optional = false 857 | python-versions = ">=3.7" 858 | files = [ 859 | {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, 860 | {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, 861 | ] 862 | 863 | [package.dependencies] 864 | coverage = {version = ">=5.2.1", extras = ["toml"]} 865 | pytest = ">=4.6" 866 | 867 | [package.extras] 868 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 869 | 870 | [[package]] 871 | name = "python-dateutil" 872 | version = "2.9.0.post0" 873 | description = "Extensions to the standard Python datetime module" 874 | optional = false 875 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 876 | files = [ 877 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 878 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 879 | ] 880 | 881 | [package.dependencies] 882 | six = ">=1.5" 883 | 884 | [[package]] 885 | name = "python-dotenv" 886 | version = "1.0.1" 887 | description = "Read key-value pairs from a .env file and set them as environment variables" 888 | optional = false 889 | python-versions = ">=3.8" 890 | files = [ 891 | {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, 892 | {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, 893 | ] 894 | 895 | [package.extras] 896 | cli = ["click (>=5.0)"] 897 | 898 | [[package]] 899 | name = "pyyaml" 900 | version = "6.0.1" 901 | description = "YAML parser and emitter for Python" 902 | optional = false 903 | python-versions = ">=3.6" 904 | files = [ 905 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, 906 | {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, 907 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, 908 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, 909 | {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, 910 | {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, 911 | {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, 912 | {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, 913 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, 914 | {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, 915 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, 916 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, 917 | {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, 918 | {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, 919 | {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, 920 | {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, 921 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, 922 | {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, 923 | {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, 924 | {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, 925 | {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, 926 | {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, 927 | {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, 928 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, 929 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, 930 | {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, 931 | {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, 932 | {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, 933 | {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, 934 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, 935 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, 936 | {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, 937 | {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, 938 | {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, 939 | {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, 940 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, 941 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, 942 | {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, 943 | {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, 944 | {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, 945 | {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, 946 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, 947 | {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, 948 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, 949 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, 950 | {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, 951 | {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, 952 | {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, 953 | {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, 954 | {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, 955 | ] 956 | 957 | [[package]] 958 | name = "ruff" 959 | version = "0.1.15" 960 | description = "An extremely fast Python linter and code formatter, written in Rust." 961 | optional = false 962 | python-versions = ">=3.7" 963 | files = [ 964 | {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, 965 | {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, 966 | {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, 967 | {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, 968 | {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, 969 | {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, 970 | {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, 971 | {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, 972 | {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, 973 | {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, 974 | {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, 975 | {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, 976 | {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, 977 | {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, 978 | {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, 979 | {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, 980 | {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, 981 | ] 982 | 983 | [[package]] 984 | name = "setuptools" 985 | version = "69.1.1" 986 | description = "Easily download, build, install, upgrade, and uninstall Python packages" 987 | optional = false 988 | python-versions = ">=3.8" 989 | files = [ 990 | {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, 991 | {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, 992 | ] 993 | 994 | [package.extras] 995 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] 996 | testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] 997 | testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] 998 | 999 | [[package]] 1000 | name = "six" 1001 | version = "1.16.0" 1002 | description = "Python 2 and 3 compatibility utilities" 1003 | optional = false 1004 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 1005 | files = [ 1006 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 1007 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 1008 | ] 1009 | 1010 | [[package]] 1011 | name = "sniffio" 1012 | version = "1.3.1" 1013 | description = "Sniff out which async library your code is running under" 1014 | optional = false 1015 | python-versions = ">=3.7" 1016 | files = [ 1017 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 1018 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 1019 | ] 1020 | 1021 | [[package]] 1022 | name = "sqlalchemy" 1023 | version = "2.0.28" 1024 | description = "Database Abstraction Library" 1025 | optional = false 1026 | python-versions = ">=3.7" 1027 | files = [ 1028 | {file = "SQLAlchemy-2.0.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0b148ab0438f72ad21cb004ce3bdaafd28465c4276af66df3b9ecd2037bf252"}, 1029 | {file = "SQLAlchemy-2.0.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bbda76961eb8f27e6ad3c84d1dc56d5bc61ba8f02bd20fcf3450bd421c2fcc9c"}, 1030 | {file = "SQLAlchemy-2.0.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feea693c452d85ea0015ebe3bb9cd15b6f49acc1a31c28b3c50f4db0f8fb1e71"}, 1031 | {file = "SQLAlchemy-2.0.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5da98815f82dce0cb31fd1e873a0cb30934971d15b74e0d78cf21f9e1b05953f"}, 1032 | {file = "SQLAlchemy-2.0.28-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a5adf383c73f2d49ad15ff363a8748319ff84c371eed59ffd0127355d6ea1da"}, 1033 | {file = "SQLAlchemy-2.0.28-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56856b871146bfead25fbcaed098269d90b744eea5cb32a952df00d542cdd368"}, 1034 | {file = "SQLAlchemy-2.0.28-cp310-cp310-win32.whl", hash = "sha256:943aa74a11f5806ab68278284a4ddd282d3fb348a0e96db9b42cb81bf731acdc"}, 1035 | {file = "SQLAlchemy-2.0.28-cp310-cp310-win_amd64.whl", hash = "sha256:c6c4da4843e0dabde41b8f2e8147438330924114f541949e6318358a56d1875a"}, 1036 | {file = "SQLAlchemy-2.0.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46a3d4e7a472bfff2d28db838669fc437964e8af8df8ee1e4548e92710929adc"}, 1037 | {file = "SQLAlchemy-2.0.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3dd67b5d69794cfe82862c002512683b3db038b99002171f624712fa71aeaa"}, 1038 | {file = "SQLAlchemy-2.0.28-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61e2e41656a673b777e2f0cbbe545323dbe0d32312f590b1bc09da1de6c2a02"}, 1039 | {file = "SQLAlchemy-2.0.28-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0315d9125a38026227f559488fe7f7cee1bd2fbc19f9fd637739dc50bb6380b2"}, 1040 | {file = "SQLAlchemy-2.0.28-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af8ce2d31679006e7b747d30a89cd3ac1ec304c3d4c20973f0f4ad58e2d1c4c9"}, 1041 | {file = "SQLAlchemy-2.0.28-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:81ba314a08c7ab701e621b7ad079c0c933c58cdef88593c59b90b996e8b58fa5"}, 1042 | {file = "SQLAlchemy-2.0.28-cp311-cp311-win32.whl", hash = "sha256:1ee8bd6d68578e517943f5ebff3afbd93fc65f7ef8f23becab9fa8fb315afb1d"}, 1043 | {file = "SQLAlchemy-2.0.28-cp311-cp311-win_amd64.whl", hash = "sha256:ad7acbe95bac70e4e687a4dc9ae3f7a2f467aa6597049eeb6d4a662ecd990bb6"}, 1044 | {file = "SQLAlchemy-2.0.28-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d3499008ddec83127ab286c6f6ec82a34f39c9817f020f75eca96155f9765097"}, 1045 | {file = "SQLAlchemy-2.0.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b66fcd38659cab5d29e8de5409cdf91e9986817703e1078b2fdaad731ea66f5"}, 1046 | {file = "SQLAlchemy-2.0.28-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea30da1e76cb1acc5b72e204a920a3a7678d9d52f688f087dc08e54e2754c67"}, 1047 | {file = "SQLAlchemy-2.0.28-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:124202b4e0edea7f08a4db8c81cc7859012f90a0d14ba2bf07c099aff6e96462"}, 1048 | {file = "SQLAlchemy-2.0.28-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e23b88c69497a6322b5796c0781400692eca1ae5532821b39ce81a48c395aae9"}, 1049 | {file = "SQLAlchemy-2.0.28-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b6303bfd78fb3221847723104d152e5972c22367ff66edf09120fcde5ddc2e2"}, 1050 | {file = "SQLAlchemy-2.0.28-cp312-cp312-win32.whl", hash = "sha256:a921002be69ac3ab2cf0c3017c4e6a3377f800f1fca7f254c13b5f1a2f10022c"}, 1051 | {file = "SQLAlchemy-2.0.28-cp312-cp312-win_amd64.whl", hash = "sha256:b4a2cf92995635b64876dc141af0ef089c6eea7e05898d8d8865e71a326c0385"}, 1052 | {file = "SQLAlchemy-2.0.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e91b5e341f8c7f1e5020db8e5602f3ed045a29f8e27f7f565e0bdee3338f2c7"}, 1053 | {file = "SQLAlchemy-2.0.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45c7b78dfc7278329f27be02c44abc0d69fe235495bb8e16ec7ef1b1a17952db"}, 1054 | {file = "SQLAlchemy-2.0.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eba73ef2c30695cb7eabcdb33bb3d0b878595737479e152468f3ba97a9c22a4"}, 1055 | {file = "SQLAlchemy-2.0.28-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5df5d1dafb8eee89384fb7a1f79128118bc0ba50ce0db27a40750f6f91aa99d5"}, 1056 | {file = "SQLAlchemy-2.0.28-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2858bbab1681ee5406650202950dc8f00e83b06a198741b7c656e63818633526"}, 1057 | {file = "SQLAlchemy-2.0.28-cp37-cp37m-win32.whl", hash = "sha256:9461802f2e965de5cff80c5a13bc945abea7edaa1d29360b485c3d2b56cdb075"}, 1058 | {file = "SQLAlchemy-2.0.28-cp37-cp37m-win_amd64.whl", hash = "sha256:a6bec1c010a6d65b3ed88c863d56b9ea5eeefdf62b5e39cafd08c65f5ce5198b"}, 1059 | {file = "SQLAlchemy-2.0.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:843a882cadebecc655a68bd9a5b8aa39b3c52f4a9a5572a3036fb1bb2ccdc197"}, 1060 | {file = "SQLAlchemy-2.0.28-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dbb990612c36163c6072723523d2be7c3eb1517bbdd63fe50449f56afafd1133"}, 1061 | {file = "SQLAlchemy-2.0.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7e4baf9161d076b9a7e432fce06217b9bd90cfb8f1d543d6e8c4595627edb9"}, 1062 | {file = "SQLAlchemy-2.0.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0a5354cb4de9b64bccb6ea33162cb83e03dbefa0d892db88a672f5aad638a75"}, 1063 | {file = "SQLAlchemy-2.0.28-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fffcc8edc508801ed2e6a4e7b0d150a62196fd28b4e16ab9f65192e8186102b6"}, 1064 | {file = "SQLAlchemy-2.0.28-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aca7b6d99a4541b2ebab4494f6c8c2f947e0df4ac859ced575238e1d6ca5716b"}, 1065 | {file = "SQLAlchemy-2.0.28-cp38-cp38-win32.whl", hash = "sha256:8c7f10720fc34d14abad5b647bc8202202f4948498927d9f1b4df0fb1cf391b7"}, 1066 | {file = "SQLAlchemy-2.0.28-cp38-cp38-win_amd64.whl", hash = "sha256:243feb6882b06a2af68ecf4bec8813d99452a1b62ba2be917ce6283852cf701b"}, 1067 | {file = "SQLAlchemy-2.0.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc4974d3684f28b61b9a90fcb4c41fb340fd4b6a50c04365704a4da5a9603b05"}, 1068 | {file = "SQLAlchemy-2.0.28-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87724e7ed2a936fdda2c05dbd99d395c91ea3c96f029a033a4a20e008dd876bf"}, 1069 | {file = "SQLAlchemy-2.0.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68722e6a550f5de2e3cfe9da6afb9a7dd15ef7032afa5651b0f0c6b3adb8815d"}, 1070 | {file = "SQLAlchemy-2.0.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328529f7c7f90adcd65aed06a161851f83f475c2f664a898af574893f55d9e53"}, 1071 | {file = "SQLAlchemy-2.0.28-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:df40c16a7e8be7413b885c9bf900d402918cc848be08a59b022478804ea076b8"}, 1072 | {file = "SQLAlchemy-2.0.28-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:426f2fa71331a64f5132369ede5171c52fd1df1bd9727ce621f38b5b24f48750"}, 1073 | {file = "SQLAlchemy-2.0.28-cp39-cp39-win32.whl", hash = "sha256:33157920b233bc542ce497a81a2e1452e685a11834c5763933b440fedd1d8e2d"}, 1074 | {file = "SQLAlchemy-2.0.28-cp39-cp39-win_amd64.whl", hash = "sha256:2f60843068e432311c886c5f03c4664acaef507cf716f6c60d5fde7265be9d7b"}, 1075 | {file = "SQLAlchemy-2.0.28-py3-none-any.whl", hash = "sha256:78bb7e8da0183a8301352d569900d9d3594c48ac21dc1c2ec6b3121ed8b6c986"}, 1076 | {file = "SQLAlchemy-2.0.28.tar.gz", hash = "sha256:dd53b6c4e6d960600fd6532b79ee28e2da489322fcf6648738134587faf767b6"}, 1077 | ] 1078 | 1079 | [package.dependencies] 1080 | 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\""} 1081 | typing-extensions = ">=4.6.0" 1082 | 1083 | [package.extras] 1084 | aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] 1085 | aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] 1086 | aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] 1087 | asyncio = ["greenlet (!=0.4.17)"] 1088 | asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] 1089 | mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] 1090 | mssql = ["pyodbc"] 1091 | mssql-pymssql = ["pymssql"] 1092 | mssql-pyodbc = ["pyodbc"] 1093 | mypy = ["mypy (>=0.910)"] 1094 | mysql = ["mysqlclient (>=1.4.0)"] 1095 | mysql-connector = ["mysql-connector-python"] 1096 | oracle = ["cx_oracle (>=8)"] 1097 | oracle-oracledb = ["oracledb (>=1.0.1)"] 1098 | postgresql = ["psycopg2 (>=2.7)"] 1099 | postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] 1100 | postgresql-pg8000 = ["pg8000 (>=1.29.1)"] 1101 | postgresql-psycopg = ["psycopg (>=3.0.7)"] 1102 | postgresql-psycopg2binary = ["psycopg2-binary"] 1103 | postgresql-psycopg2cffi = ["psycopg2cffi"] 1104 | postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] 1105 | pymysql = ["pymysql"] 1106 | sqlcipher = ["sqlcipher3_binary"] 1107 | 1108 | [[package]] 1109 | name = "sqlmodel" 1110 | version = "0.0.14" 1111 | description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness." 1112 | optional = false 1113 | python-versions = ">=3.7,<4.0" 1114 | files = [ 1115 | {file = "sqlmodel-0.0.14-py3-none-any.whl", hash = "sha256:accea3ff5d878e41ac439b11e78613ed61ce300cfcb860e87a2d73d4884cbee4"}, 1116 | {file = "sqlmodel-0.0.14.tar.gz", hash = "sha256:0bff8fc94af86b44925aa813f56cf6aabdd7f156b73259f2f60692c6a64ac90e"}, 1117 | ] 1118 | 1119 | [package.dependencies] 1120 | pydantic = ">=1.10.13,<3.0.0" 1121 | SQLAlchemy = ">=2.0.0,<2.1.0" 1122 | 1123 | [[package]] 1124 | name = "starlette" 1125 | version = "0.36.3" 1126 | description = "The little ASGI library that shines." 1127 | optional = false 1128 | python-versions = ">=3.8" 1129 | files = [ 1130 | {file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"}, 1131 | {file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"}, 1132 | ] 1133 | 1134 | [package.dependencies] 1135 | anyio = ">=3.4.0,<5" 1136 | 1137 | [package.extras] 1138 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] 1139 | 1140 | [[package]] 1141 | name = "typing-extensions" 1142 | version = "4.10.0" 1143 | description = "Backported and Experimental Type Hints for Python 3.8+" 1144 | optional = false 1145 | python-versions = ">=3.8" 1146 | files = [ 1147 | {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, 1148 | {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, 1149 | ] 1150 | 1151 | [[package]] 1152 | name = "uvicorn" 1153 | version = "0.22.0" 1154 | description = "The lightning-fast ASGI server." 1155 | optional = false 1156 | python-versions = ">=3.7" 1157 | files = [ 1158 | {file = "uvicorn-0.22.0-py3-none-any.whl", hash = "sha256:e9434d3bbf05f310e762147f769c9f21235ee118ba2d2bf1155a7196448bd996"}, 1159 | {file = "uvicorn-0.22.0.tar.gz", hash = "sha256:79277ae03db57ce7d9aa0567830bbb51d7a612f54d6e1e3e92da3ef24c2c8ed8"}, 1160 | ] 1161 | 1162 | [package.dependencies] 1163 | click = ">=7.0" 1164 | h11 = ">=0.8" 1165 | 1166 | [package.extras] 1167 | 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)"] 1168 | 1169 | [[package]] 1170 | name = "virtualenv" 1171 | version = "20.25.1" 1172 | description = "Virtual Python Environment builder" 1173 | optional = false 1174 | python-versions = ">=3.7" 1175 | files = [ 1176 | {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, 1177 | {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, 1178 | ] 1179 | 1180 | [package.dependencies] 1181 | distlib = ">=0.3.7,<1" 1182 | filelock = ">=3.12.2,<4" 1183 | platformdirs = ">=3.9.1,<5" 1184 | 1185 | [package.extras] 1186 | docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] 1187 | test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] 1188 | 1189 | [metadata] 1190 | lock-version = "2.0" 1191 | python-versions = "^3.12" 1192 | content-hash = "0c901a80ef3c9c859b448f092314ca6eb0df5908697ceb9aa1926e1ef7aea13b" 1193 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fanspark" 3 | description = "FanSpark proposal template" 4 | version = "0.0.1" 5 | authors=["the sparkers"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.12" 9 | fastapi="^0.109.0" 10 | uvicorn="^0.22.0" 11 | sqlmodel="^0.0.14" 12 | SQLAlchemy="^2.0.25" 13 | alembic="^1.13.1" 14 | asyncpg="^0.29.0" 15 | pydantic-settings="^2.1.0" 16 | fastapi-pagination="^0.12.14" 17 | httpx = "^0.26.0" 18 | 19 | [tool.poetry.group.dev.dependencies] 20 | pre-commit="^3.6.0" 21 | ruff="^0.1.11" 22 | nest_asyncio="^1.5.8" 23 | pytest-cov="^4.1.0" 24 | pytest="^7.4.4" 25 | pytest-asyncio="^0.23.3" 26 | aiosqlite = "^0.19.0" 27 | polyfactory = "^2.13.0" 28 | 29 | 30 | [tool.hatch.build.targets.wheel] 31 | packages = ["backend"] 32 | 33 | [tool.pytest.ini_options] 34 | addopts = "-vv -rA --disable-warnings --cov=app --cov-report term-missing" 35 | testpaths = [ 36 | "tests", 37 | ] 38 | asyncio_mode = "auto" 39 | pythonpath = [ 40 | "." 41 | ] 42 | 43 | 44 | [tool.ruff] 45 | select = [ 46 | "E", # pycodestyle errors 47 | "W", # pycodestyle warnings 48 | "F", # pyflakes 49 | "I", # isort 50 | "C", # flake8-comprehensions 51 | "B", # flake8-bugbear 52 | "UP", # pyupgrade 53 | ] 54 | ignore = [ 55 | "B008", # do not perform function calls in argument defaults 56 | "C901", # too complex 57 | "W191", # indentation contains tabs 58 | "E711", # `None` should be `cond is None` 59 | ] 60 | 61 | [tool.ruff.per-file-ignores] 62 | "__init__.py" = ["F401"] 63 | 64 | 65 | [tool.ruff.pyupgrade] 66 | # Preserve types, even if a file imports `from __future__ import annotations`. 67 | keep-runtime-typing = true 68 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from alembic.config import Config 3 | from fastapi.testclient import TestClient 4 | from sqlalchemy.ext.asyncio import create_async_engine 5 | from sqlalchemy.orm import sessionmaker 6 | from sqlmodel import SQLModel 7 | from sqlmodel.ext.asyncio.session import AsyncSession 8 | 9 | from app.base.db import get_session 10 | from app.main import app 11 | 12 | # Async engine for in-memory SQLite database 13 | # db_url = "sqlite+aiosqlite:///:memory:" 14 | # engine = create_async_engine(db_url, echo=True) 15 | 16 | __config_path__ = "app/alembic.ini" 17 | __migration_path__ = "app/migrations" 18 | 19 | cfg = Config(__config_path__) 20 | cfg.set_main_option("script_location", __migration_path__) 21 | 22 | 23 | @pytest.fixture(scope="session") 24 | async def sqlite_engine(): 25 | engine = create_async_engine("sqlite+aiosqlite:///:memory:") 26 | async with engine.begin() as conn: 27 | await conn.run_sync(SQLModel.metadata.create_all) 28 | try: 29 | yield engine 30 | finally: 31 | await engine.dispose() 32 | 33 | 34 | @pytest.fixture(scope="function") 35 | async def db(sqlite_engine): 36 | """ 37 | Fixture that returns a SQLAlchemy session with a SAVEPOINT, and the rollback to it 38 | after the test completes. 39 | """ 40 | connection = await sqlite_engine.connect() 41 | trans = await connection.begin() 42 | 43 | Session: AsyncSession = sessionmaker( 44 | connection, expire_on_commit=False, class_=AsyncSession 45 | ) 46 | session = Session() 47 | 48 | try: 49 | yield session 50 | finally: 51 | await session.close() 52 | await trans.rollback() 53 | await connection.close() 54 | 55 | 56 | @pytest.fixture(scope="function") 57 | def api_client(db) -> TestClient: 58 | # replace the app dependency to get test database 59 | app.dependency_overrides[get_session] = lambda: db 60 | 61 | return TestClient(app) 62 | -------------------------------------------------------------------------------- /tests/songs/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from sqlalchemy.ext.asyncio import AsyncSession 3 | 4 | from app.songs.crud import band_crud, song_crud 5 | from tests.songs.factories import BandCreationFactory, SongCreationFactory 6 | 7 | 8 | @pytest.fixture(scope="function") 9 | @pytest.mark.asyncio 10 | async def beatles_song(db: AsyncSession): 11 | the_beatles_data = BandCreationFactory.build(name="The Beatles") 12 | the_beatles = await band_crud.create(db, obj_in=the_beatles_data.model_dump()) 13 | song_data = SongCreationFactory.build(band_id=the_beatles.id) 14 | song = await song_crud.create( 15 | db, 16 | obj_in=song_data.model_dump(), 17 | ) 18 | return song 19 | -------------------------------------------------------------------------------- /tests/songs/factories.py: -------------------------------------------------------------------------------- 1 | from polyfactory.factories.pydantic_factory import ModelFactory 2 | 3 | from app.songs.schemas import BandCreate, SongCreate 4 | 5 | 6 | class SongCreationFactory(ModelFactory[SongCreate]): 7 | __model__ = SongCreate 8 | 9 | 10 | class BandCreationFactory(ModelFactory[BandCreate]): 11 | __model__ = BandCreate 12 | -------------------------------------------------------------------------------- /tests/songs/test_api_songs.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fastapi.testclient import TestClient 3 | 4 | 5 | @pytest.mark.asyncio 6 | async def test_get_songs_empty(api_client: TestClient): 7 | result = api_client.get("/songs") 8 | result.raise_for_status() 9 | result = result.json() 10 | assert len(result["items"]) == 0 11 | assert result["total"] == 0 12 | assert result["limit"] == 50 13 | assert result["offset"] == 0 14 | 15 | 16 | @pytest.mark.asyncio 17 | async def test_get_one_song(api_client: TestClient, beatles_song): 18 | result = api_client.get("/songs") 19 | result.raise_for_status() 20 | result = result.json() 21 | assert len(result["items"]) == 1 22 | assert result["total"] == 1 23 | assert result["limit"] == 50 24 | assert result["offset"] == 0 25 | -------------------------------------------------------------------------------- /tests/songs/test_songs.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import AsyncMock, patch 2 | 3 | import pytest 4 | from sqlalchemy.ext.asyncio import AsyncSession 5 | 6 | from app.songs.crud import band_crud, song_crud 7 | from tests.songs.factories import BandCreationFactory, SongCreationFactory 8 | 9 | 10 | @pytest.mark.asyncio 11 | async def test_crud_create(db: AsyncSession): 12 | the_beatles_data = BandCreationFactory.build(name="The Beatles") 13 | the_beatles = await band_crud.create(db, obj_in=the_beatles_data.model_dump()) 14 | song_data = SongCreationFactory.build(band_id=the_beatles.id) 15 | result = await song_crud.create( 16 | db, 17 | obj_in=song_data.model_dump(), 18 | ) 19 | assert result 20 | assert result.id 21 | 22 | 23 | @pytest.mark.asyncio 24 | async def test_crud_update(db: AsyncSession, beatles_song): 25 | assert not beatles_song.updated_at, "Song.updated_at should be empty" 26 | 27 | song = await song_crud.update( 28 | db, 29 | id=beatles_song.id, 30 | obj_in={"name": "New name"}, 31 | ) 32 | 33 | assert song 34 | assert song.updated_at, "Song.updated_at should NOT be empty" 35 | 36 | 37 | @pytest.mark.asyncio 38 | async def test_crud_delete(db: AsyncSession, beatles_song): 39 | song = await song_crud.remove(db, id=beatles_song.id) 40 | 41 | assert song 42 | assert song.deleted_at, "Song.deleted_at should NOT be empty" 43 | 44 | 45 | @pytest.mark.asyncio 46 | async def test_crud_get_all_should_be_empty(db: AsyncSession): 47 | result = await song_crud.get_all(db) 48 | assert not result 49 | 50 | 51 | @pytest.mark.asyncio 52 | async def test_crud_get_all_filtering_deleted(db: AsyncSession): 53 | songs_data = SongCreationFactory.batch(5) 54 | last_song = None 55 | for song_data in songs_data: 56 | last_song = await song_crud.create( 57 | db, 58 | obj_in=song_data.model_dump(), 59 | ) 60 | songs = await song_crud.get_all(db) 61 | assert len(songs) == 5 62 | 63 | await song_crud.remove(db, id=last_song.id) 64 | 65 | songs = await song_crud.get_all(db) 66 | assert len(songs) == 4 67 | 68 | 69 | @pytest.mark.asyncio 70 | @patch("app.songs.crud.song_crud.get_all", AsyncMock(return_value="Hola")) 71 | async def test(): 72 | result = await song_crud.get_all(None) 73 | assert result == "Hola" 74 | --------------------------------------------------------------------------------