From 6eb0790b26ca3740afce0862d159c9f7916eb181 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Fri, 19 Jan 2024 02:31:11 +0100 Subject: [PATCH 01/41] add mypy, rewrite config and security, create proper refresh token flow, remove passlib MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- {{cookiecutter.project_name}}/alembic.ini | 42 +- {{cookiecutter.project_name}}/alembic/env.py | 16 +- ...2023020440_init_user_model_07c71f4389b6.py | 36 - ...nit_user_and_refresh_token_21f30f70dab2.py | 47 + {{cookiecutter.project_name}}/app/api/deps.py | 40 +- .../app/api/endpoints/auth.py | 97 +- .../app/api/endpoints/users.py | 12 +- {{cookiecutter.project_name}}/app/conftest.py | 11 - .../app/core/config.py | 101 +- .../app/core/security.py | 89 -- .../app/core/security/__init__.py | 0 .../app/core/security/jwt.py | 60 + .../app/core/security/password.py | 11 + .../app/core/session.py | 11 +- .../app/initial_data.py | 42 - {{cookiecutter.project_name}}/app/main.py | 18 +- {{cookiecutter.project_name}}/app/models.py | 29 +- .../app/schemas/responses.py | 6 +- .../app/tests/conftest.py | 18 +- .../app/tests/test_auth.py | 6 +- .../app/tests/test_users.py | 14 +- {{cookiecutter.project_name}}/init.sh | 3 - {{cookiecutter.project_name}}/poetry.lock | 1005 +++++++++-------- {{cookiecutter.project_name}}/pyproject.toml | 35 +- 24 files changed, 899 insertions(+), 850 deletions(-) delete mode 100644 {{cookiecutter.project_name}}/alembic/versions/2023020440_init_user_model_07c71f4389b6.py create mode 100644 {{cookiecutter.project_name}}/alembic/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py delete mode 100644 {{cookiecutter.project_name}}/app/conftest.py delete mode 100644 {{cookiecutter.project_name}}/app/core/security.py create mode 100644 {{cookiecutter.project_name}}/app/core/security/__init__.py create mode 100644 {{cookiecutter.project_name}}/app/core/security/jwt.py create mode 100644 {{cookiecutter.project_name}}/app/core/security/password.py delete mode 100644 {{cookiecutter.project_name}}/app/initial_data.py diff --git a/{{cookiecutter.project_name}}/alembic.ini b/{{cookiecutter.project_name}}/alembic.ini index cd05815..8ef6727 100644 --- a/{{cookiecutter.project_name}}/alembic.ini +++ b/{{cookiecutter.project_name}}/alembic.ini @@ -4,7 +4,8 @@ # path to migration scripts script_location = alembic -# template used to generate migration files +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time file_template = %%(year)d%%(month).2d%%(day).2d%%(minute).2d_%%(slug)s_%%(rev)s # sys.path path, will be prepended to sys.path if present. @@ -13,9 +14,9 @@ prepend_sys_path = . # timezone to use when rendering the date within the migration file # as well as the filename. -# If specified, requires the python-dateutil library that can be -# installed by adding `alembic[tz]` to the pip requirements -# string value is passed to dateutil.tz.gettz() +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() # leave blank for localtime # timezone = @@ -33,18 +34,25 @@ truncate_slug_length = 40 # sourceless = false # version location specification; This defaults -# to alembic/versions. When using multiple version +# to ${script_location}/versions. When using multiple version # directories, initial revisions must be specified with --version-path. -# The path separator used here should be the separator specified by "version_path_separator" -# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:${script_location}/versions # version path separator; As mentioned above, this is the character used to split -# version_locations. Valid values are: +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: # # version_path_separator = : # version_path_separator = ; # version_path_separator = space -version_path_separator = os # default: use os.pathsep +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false # the output encoding used when revision files # are written from script.py.mako @@ -52,18 +60,20 @@ version_path_separator = os # default: use os.pathsep sqlalchemy.url = driver://user:pass@localhost/dbname - -[post_write_hooks] -# post_write_hooks defines scripts or Python functions that are run +# [post_write_hooks] +# This section defines scripts or Python functions that are run # on newly generated revision scripts. See the documentation for further # detail and examples -# format using "black" - use the console_scripts runner, against the "black" entrypoint -hooks = black - +# format using "black" - use the console_scripts runner, +# against the "black" entrypoint +hooks = black,ruff black.type = console_scripts black.entrypoint = black black.options = REVISION_SCRIPT_FILENAME +ruff.type = console_scripts +ruff.executable = ruff +ruff.options = --fix REVISION_SCRIPT_FILENAME # Logging configuration [loggers] @@ -98,4 +108,4 @@ formatter = generic [formatter_generic] format = %(levelname)-5.5s [%(name)s] %(message)s -datefmt = %H:%M:%S +datefmt = %H:%M:%S \ No newline at end of file diff --git a/{{cookiecutter.project_name}}/alembic/env.py b/{{cookiecutter.project_name}}/alembic/env.py index b8bb0cc..1bad14b 100644 --- a/{{cookiecutter.project_name}}/alembic/env.py +++ b/{{cookiecutter.project_name}}/alembic/env.py @@ -1,11 +1,11 @@ import asyncio from logging.config import fileConfig -from sqlalchemy import engine_from_config, pool +from sqlalchemy import Connection, engine_from_config, pool from sqlalchemy.ext.asyncio import AsyncEngine from alembic import context -from app.core import config as app_config +from app.core.config import get_settings # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -29,11 +29,11 @@ # ... etc. -def get_database_uri(): - return app_config.settings.DEFAULT_SQLALCHEMY_DATABASE_URI +def get_database_uri() -> str: + return get_settings().sqlalchemy_database_uri.get_secret_value() -def run_migrations_offline(): +def run_migrations_offline() -> None: """Run migrations in 'offline' mode. This configures the context with just a URL @@ -59,7 +59,7 @@ def run_migrations_offline(): context.run_migrations() -def do_run_migrations(connection): +def do_run_migrations(connection: Connection | None) -> None: context.configure( connection=connection, target_metadata=target_metadata, compare_type=True ) @@ -68,7 +68,7 @@ def do_run_migrations(connection): context.run_migrations() -async def run_migrations_online(): +async def run_migrations_online() -> None: """Run migrations in 'online' mode. In this scenario we need to create an Engine @@ -84,7 +84,7 @@ async def run_migrations_online(): prefix="sqlalchemy.", poolclass=pool.NullPool, future=True, - ) # type: ignore + ) ) async with connectable.connect() as connection: await connection.run_sync(do_run_migrations) diff --git a/{{cookiecutter.project_name}}/alembic/versions/2023020440_init_user_model_07c71f4389b6.py b/{{cookiecutter.project_name}}/alembic/versions/2023020440_init_user_model_07c71f4389b6.py deleted file mode 100644 index 90302fc..0000000 --- a/{{cookiecutter.project_name}}/alembic/versions/2023020440_init_user_model_07c71f4389b6.py +++ /dev/null @@ -1,36 +0,0 @@ -"""init_user_model - -Revision ID: 07c71f4389b6 -Revises: -Create Date: 2023-02-04 23:40:00.426237 - -""" -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision = "07c71f4389b6" -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "user_model", - sa.Column("id", sa.UUID(), nullable=False), - sa.Column("email", sa.String(length=254), nullable=False), - sa.Column("hashed_password", sa.String(length=128), nullable=False), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index(op.f("ix_user_model_email"), "user_model", ["email"], unique=True) - # ### end Alembic commands ### - - -def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f("ix_user_model_email"), table_name="user_model") - op.drop_table("user_model") - # ### end Alembic commands ### diff --git a/{{cookiecutter.project_name}}/alembic/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py b/{{cookiecutter.project_name}}/alembic/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py new file mode 100644 index 0000000..d9c867b --- /dev/null +++ b/{{cookiecutter.project_name}}/alembic/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py @@ -0,0 +1,47 @@ +"""init_user_and_refresh_token + +Revision ID: 21f30f70dab2 +Revises: +Create Date: 2024-01-19 01:39:35.369361 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '21f30f70dab2' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('user_account', + sa.Column('user_id', sa.Uuid(as_uuid=False), nullable=False), + sa.Column('email', sa.String(length=256), nullable=False), + sa.Column('hashed_password', sa.String(length=128), nullable=False), + sa.PrimaryKeyConstraint('user_id') + ) + op.create_index(op.f('ix_user_account_email'), 'user_account', ['email'], unique=True) + op.create_table('refresh_token', + sa.Column('id', sa.BigInteger(), nullable=False), + sa.Column('refresh_token', sa.String(length=512), nullable=False), + sa.Column('used', sa.Boolean(), nullable=False), + sa.Column('exp', sa.BigInteger(), nullable=False), + sa.Column('user_id', sa.Uuid(as_uuid=False), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['user_account.user_id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_refresh_token_refresh_token'), 'refresh_token', ['refresh_token'], unique=True) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_refresh_token_refresh_token'), table_name='refresh_token') + op.drop_table('refresh_token') + op.drop_index(op.f('ix_user_account_email'), table_name='user_account') + op.drop_table('user_account') + # ### end Alembic commands ### diff --git a/{{cookiecutter.project_name}}/app/api/deps.py b/{{cookiecutter.project_name}}/app/api/deps.py index cbb7c39..0e2dd0a 100644 --- a/{{cookiecutter.project_name}}/app/api/deps.py +++ b/{{cookiecutter.project_name}}/app/api/deps.py @@ -1,5 +1,6 @@ import time from collections.abc import AsyncGenerator +from typing import Annotated import jwt from fastapi import Depends, HTTPException, status @@ -8,10 +9,11 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.core import config, security +from app.core.security.jwt import verify_jwt_token from app.core.session import async_session from app.models import User -reusable_oauth2 = OAuth2PasswordBearer(tokenUrl="auth/access-token") +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/access-token") async def get_session() -> AsyncGenerator[AsyncSession, None]: @@ -20,35 +22,25 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]: async def get_current_user( - session: AsyncSession = Depends(get_session), token: str = Depends(reusable_oauth2) + token: Annotated[str, Depends(oauth2_scheme)], + session: AsyncSession = Depends(get_session), ) -> User: try: - payload = jwt.decode( - token, config.settings.SECRET_KEY, algorithms=[security.JWT_ALGORITHM] - ) - except jwt.DecodeError: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Could not validate credentials.", - ) - # JWT guarantees payload will be unchanged (and thus valid), no errors here - token_data = security.JWTTokenPayload(**payload) - - if token_data.refresh: + token_payload = verify_jwt_token(token) + except ValueError as err: raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Could not validate credentials, cannot use refresh token", - ) - now = int(time.time()) - if now < token_data.issued_at or now > token_data.expires_at: - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Could not validate credentials, token expired or not yet valid", + status_code=status.HTTP_401_UNAUTHORIZED, + detail=f"Could not validate credentials: {err}", ) - result = await session.execute(select(User).where(User.id == token_data.sub)) + result = await session.execute( + select(User).where(User.user_id == token_payload.sub) + ) user = result.scalars().first() if not user: - raise HTTPException(status_code=404, detail="User not found.") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found", + ) return user diff --git a/{{cookiecutter.project_name}}/app/api/endpoints/auth.py b/{{cookiecutter.project_name}}/app/api/endpoints/auth.py index b992a44..89621b9 100644 --- a/{{cookiecutter.project_name}}/app/api/endpoints/auth.py +++ b/{{cookiecutter.project_name}}/app/api/endpoints/auth.py @@ -1,15 +1,20 @@ +import asyncio +import random import time + import jwt from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from pydantic import ValidationError -from sqlalchemy import select +from sqlalchemy import delete, select, update from sqlalchemy.ext.asyncio import AsyncSession - +import secrets from app.api import deps from app.core import config, security -from app.models import User +from app.core.security.password import verify_password +from app.core.security.jwt import create_jwt_token +from app.models import User, RefreshToken from app.schemas.requests import RefreshTokenRequest from app.schemas.responses import AccessTokenResponse @@ -20,58 +25,82 @@ async def login_access_token( session: AsyncSession = Depends(deps.get_session), form_data: OAuth2PasswordRequestForm = Depends(), -): +) -> AccessTokenResponse: """OAuth2 compatible token, get an access token for future requests using username and password""" + wait_delay_ms = random.randint(800, 1000) + min_end_time = time.time() + wait_delay_ms / 1000 + result = await session.execute(select(User).where(User.email == form_data.username)) user = result.scalars().first() if user is None: + await asyncio.sleep(min_end_time - time.time()) raise HTTPException(status_code=400, detail="Incorrect email or password") - if not security.verify_password(form_data.password, user.hashed_password): + if not verify_password(form_data.password, user.hashed_password): + await asyncio.sleep(min_end_time - time.time()) raise HTTPException(status_code=400, detail="Incorrect email or password") - return security.generate_access_token_response(str(user.id)) + jwt_token = create_jwt_token(user_id=user.user_id) + + refresh_token = RefreshToken( + user_id=user.user_id, + refresh_token=secrets.token_urlsafe(32), + exp=int(time.time() + config.get_settings().security.refresh_token_expire_secs), + ) + session.add(refresh_token) + await session.commit() + + await asyncio.sleep(min_end_time - time.time()) + return AccessTokenResponse( + access_token=jwt_token.access_token, + expires_at=jwt_token.payload.iat, + refresh_token=refresh_token.refresh_token, + refresh_token_expires_at=refresh_token.exp, + ) @router.post("/refresh-token", response_model=AccessTokenResponse) async def refresh_token( input: RefreshTokenRequest, session: AsyncSession = Depends(deps.get_session), -): +) -> AccessTokenResponse: """OAuth2 compatible token, get an access token for future requests using refresh token""" - try: - payload = jwt.decode( - input.refresh_token, - config.settings.SECRET_KEY, - algorithms=[security.JWT_ALGORITHM], - ) - except (jwt.DecodeError, ValidationError): - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Could not validate credentials, unknown error", - ) - # JWT guarantees payload will be unchanged (and thus valid), no errors here - token_data = security.JWTTokenPayload(**payload) + result = await session.execute( + select(RefreshToken).where(RefreshToken.refresh_token == input.refresh_token) + ) + token = result.scalars().first() - if not token_data.refresh: + if token is None or time.time() > token.exp: raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Could not validate credentials, cannot use access token", + status_code=status.HTTP_404_NOT_FOUND, detail="Token not found" + ) + if token.used: + await session.execute( + delete(RefreshToken).where(RefreshToken.user_id == token.user_id) ) - now = int(time.time()) - if now < token_data.issued_at or now > token_data.expires_at: raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail="Could not validate credentials, token expired or not yet valid", + status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) - result = await session.execute(select(User).where(User.id == token_data.sub)) - user = result.scalars().first() - - if user is None: - raise HTTPException(status_code=404, detail="User not found") - - return security.generate_access_token_response(str(user.id)) + token.used = True + session.add(token) + + jwt_token = create_jwt_token(user_id=token.user_id) + + refresh_token = RefreshToken( + user_id=token.user_id, + refresh_token=secrets.token_urlsafe(32), + exp=int(time.time() + config.get_settings().security.refresh_token_expire_secs), + ) + session.add(refresh_token) + await session.commit() + + return AccessTokenResponse( + access_token=jwt_token.access_token, + expires_at=jwt_token.payload.iat, + refresh_token=refresh_token.refresh_token, + refresh_token_expires_at=refresh_token.exp, + ) diff --git a/{{cookiecutter.project_name}}/app/api/endpoints/users.py b/{{cookiecutter.project_name}}/app/api/endpoints/users.py index d933fca..8cbc689 100644 --- a/{{cookiecutter.project_name}}/app/api/endpoints/users.py +++ b/{{cookiecutter.project_name}}/app/api/endpoints/users.py @@ -3,7 +3,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.api import deps -from app.core.security import get_password_hash +from app.core.security.password import get_password_hash from app.models import User from app.schemas.requests import UserCreateRequest, UserUpdatePasswordRequest from app.schemas.responses import UserResponse @@ -14,7 +14,7 @@ @router.get("/me", response_model=UserResponse) async def read_current_user( current_user: User = Depends(deps.get_current_user), -): +) -> User: """Get current user""" return current_user @@ -23,9 +23,9 @@ async def read_current_user( async def delete_current_user( current_user: User = Depends(deps.get_current_user), session: AsyncSession = Depends(deps.get_session), -): +) -> None: """Delete current user""" - await session.execute(delete(User).where(User.id == current_user.id)) + await session.execute(delete(User).where(User.user_id == current_user.user_id)) await session.commit() @@ -34,7 +34,7 @@ async def reset_current_user_password( user_update_password: UserUpdatePasswordRequest, session: AsyncSession = Depends(deps.get_session), current_user: User = Depends(deps.get_current_user), -): +) -> User: """Update current user password""" current_user.hashed_password = get_password_hash(user_update_password.password) session.add(current_user) @@ -46,7 +46,7 @@ async def reset_current_user_password( async def register_new_user( new_user: UserCreateRequest, session: AsyncSession = Depends(deps.get_session), -): +) -> User: """Create new user""" result = await session.execute(select(User).where(User.email == new_user.email)) if result.scalars().first() is not None: diff --git a/{{cookiecutter.project_name}}/app/conftest.py b/{{cookiecutter.project_name}}/app/conftest.py deleted file mode 100644 index 9d48255..0000000 --- a/{{cookiecutter.project_name}}/app/conftest.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -Used to execute code before running tests, in this case we want to use test database. -We don't want to mess up dev database. - -Put here any Pytest related code (it will be executed before `app/tests/...`) -""" - -import os - -# This will ensure using test database -os.environ["ENVIRONMENT"] = "PYTEST" diff --git a/{{cookiecutter.project_name}}/app/core/config.py b/{{cookiecutter.project_name}}/app/core/config.py index 6471502..ef048e2 100644 --- a/{{cookiecutter.project_name}}/app/core/config.py +++ b/{{cookiecutter.project_name}}/app/core/config.py @@ -21,83 +21,60 @@ Note, complex types like lists are read as json-encoded strings. """ -import tomllib from functools import cached_property from pathlib import Path -from typing import Literal +from functools import lru_cache -from pydantic import AnyHttpUrl, EmailStr, PostgresDsn, computed_field +from pydantic import BaseModel, AnyHttpUrl, PostgresDsn, SecretStr, computed_field from pydantic_settings import BaseSettings, SettingsConfigDict PROJECT_DIR = Path(__file__).parent.parent.parent -with open(f"{PROJECT_DIR}/pyproject.toml", "rb") as f: - PYPROJECT_CONTENT = tomllib.load(f)["tool"]["poetry"] + + +class Security(BaseModel): + jwt_issuer: str = "my-app" + jwt_secret_key: SecretStr + jwt_access_token_expire_secs: int = 24 * 3600 # 1d + refresh_token_expire_secs: int = 28 * 24 * 3600 # 28d + allowed_hosts: list[str] = ["localhost", "127.0.0.1"] + backend_cors_origins: list[AnyHttpUrl] = [] + + +class Database(BaseModel): + hostname: str = "postgres" + username: str = "postgres" + password: SecretStr + port: int = 5432 + db: str = "postgres" class Settings(BaseSettings): - # CORE SETTINGS - SECRET_KEY: str - ENVIRONMENT: Literal["DEV", "PYTEST", "STG", "PRD"] = "DEV" - SECURITY_BCRYPT_ROUNDS: int = 12 - ACCESS_TOKEN_EXPIRE_MINUTES: int = 11520 # 8 days - REFRESH_TOKEN_EXPIRE_MINUTES: int = 40320 # 28 days - BACKEND_CORS_ORIGINS: list[AnyHttpUrl] = [] - ALLOWED_HOSTS: list[str] = ["localhost", "127.0.0.1"] - - # PROJECT NAME, VERSION AND DESCRIPTION - PROJECT_NAME: str = PYPROJECT_CONTENT["name"] - VERSION: str = PYPROJECT_CONTENT["version"] - DESCRIPTION: str = PYPROJECT_CONTENT["description"] - - # POSTGRESQL DEFAULT DATABASE - DEFAULT_DATABASE_HOSTNAME: str - DEFAULT_DATABASE_USER: str - DEFAULT_DATABASE_PASSWORD: str - DEFAULT_DATABASE_PORT: int - DEFAULT_DATABASE_DB: str - - # POSTGRESQL TEST DATABASE - TEST_DATABASE_HOSTNAME: str = "postgres" - TEST_DATABASE_USER: str = "postgres" - TEST_DATABASE_PASSWORD: str = "postgres" - TEST_DATABASE_PORT: int = 5432 - TEST_DATABASE_DB: str = "postgres" - - # FIRST SUPERUSER - FIRST_SUPERUSER_EMAIL: EmailStr - FIRST_SUPERUSER_PASSWORD: str - - @computed_field - @cached_property - def DEFAULT_SQLALCHEMY_DATABASE_URI(self) -> str: - return str( - PostgresDsn.build( - scheme="postgresql+asyncpg", - username=self.DEFAULT_DATABASE_USER, - password=self.DEFAULT_DATABASE_PASSWORD, - host=self.DEFAULT_DATABASE_HOSTNAME, - port=self.DEFAULT_DATABASE_PORT, - path=self.DEFAULT_DATABASE_DB, - ) - ) + security: Security + database: Database - @computed_field + @computed_field # type: ignore[misc] @cached_property - def TEST_SQLALCHEMY_DATABASE_URI(self) -> str: - return str( - PostgresDsn.build( - scheme="postgresql+asyncpg", - username=self.TEST_DATABASE_USER, - password=self.TEST_DATABASE_PASSWORD, - host=self.TEST_DATABASE_HOSTNAME, - port=self.TEST_DATABASE_PORT, - path=self.TEST_DATABASE_DB, + def sqlalchemy_database_uri(self) -> SecretStr: + return SecretStr( + str( + PostgresDsn.build( + scheme="postgresql+asyncpg", + username=self.database.username, + password=self.database.password.get_secret_value(), + host=self.database.hostname, + port=self.database.port, + path=self.database.db, + ) ) ) model_config = SettingsConfigDict( - env_file=f"{PROJECT_DIR}/.env", case_sensitive=True + env_file=f"{PROJECT_DIR}/.env", + case_sensitive=False, + env_nested_delimiter="__", ) -settings: Settings = Settings() # type: ignore +@lru_cache(maxsize=1) +def get_settings() -> Settings: + return Settings() # type: ignore diff --git a/{{cookiecutter.project_name}}/app/core/security.py b/{{cookiecutter.project_name}}/app/core/security.py deleted file mode 100644 index 62fb6dc..0000000 --- a/{{cookiecutter.project_name}}/app/core/security.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Black-box security shortcuts to generate JWT tokens and password hashing and verifcation.""" - -import time - -import jwt -from passlib.context import CryptContext -from pydantic import BaseModel - -from app.core import config -from app.schemas.responses import AccessTokenResponse - -JWT_ALGORITHM = "HS256" -ACCESS_TOKEN_EXPIRE_SECS = config.settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60 -REFRESH_TOKEN_EXPIRE_SECS = config.settings.REFRESH_TOKEN_EXPIRE_MINUTES * 60 -PWD_CONTEXT = CryptContext( - schemes=["bcrypt"], - deprecated="auto", - bcrypt__rounds=config.settings.SECURITY_BCRYPT_ROUNDS, -) - - -class JWTTokenPayload(BaseModel): - sub: str | int - refresh: bool - issued_at: int - expires_at: int - - -def create_jwt_token(subject: str | int, exp_secs: int, refresh: bool): - """Creates jwt access or refresh token for user. - - Args: - subject: anything unique to user, id or email etc. - exp_secs: expire time in seconds - refresh: if True, this is refresh token - """ - - issued_at = int(time.time()) - expires_at = issued_at + exp_secs - - to_encode: dict[str, int | str | bool] = { - "issued_at": issued_at, - "expires_at": expires_at, - "sub": subject, - "refresh": refresh, - } - encoded_jwt = jwt.encode( - to_encode, - key=config.settings.SECRET_KEY, - algorithm=JWT_ALGORITHM, - ) - return encoded_jwt, expires_at, issued_at - - -def generate_access_token_response(subject: str | int): - """Generate tokens and return AccessTokenResponse""" - access_token, expires_at, issued_at = create_jwt_token( - subject, ACCESS_TOKEN_EXPIRE_SECS, refresh=False - ) - refresh_token, refresh_expires_at, refresh_issued_at = create_jwt_token( - subject, REFRESH_TOKEN_EXPIRE_SECS, refresh=True - ) - return AccessTokenResponse( - token_type="Bearer", - access_token=access_token, - expires_at=expires_at, - issued_at=issued_at, - refresh_token=refresh_token, - refresh_token_expires_at=refresh_expires_at, - refresh_token_issued_at=refresh_issued_at, - ) - - -def verify_password(plain_password: str, hashed_password: str) -> bool: - """Verifies plain and hashed password matches - - Applies passlib context based on bcrypt algorithm on plain passoword. - It takes about 0.3s for default 12 rounds of SECURITY_BCRYPT_DEFAULT_ROUNDS. - """ - return PWD_CONTEXT.verify(plain_password, hashed_password) - - -def get_password_hash(password: str) -> str: - """Creates hash from password - - Applies passlib context based on bcrypt algorithm on plain passoword. - It takes about 0.3s for default 12 rounds of SECURITY_BCRYPT_DEFAULT_ROUNDS. - """ - return PWD_CONTEXT.hash(password) diff --git a/{{cookiecutter.project_name}}/app/core/security/__init__.py b/{{cookiecutter.project_name}}/app/core/security/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/{{cookiecutter.project_name}}/app/core/security/jwt.py b/{{cookiecutter.project_name}}/app/core/security/jwt.py new file mode 100644 index 0000000..97ab2ac --- /dev/null +++ b/{{cookiecutter.project_name}}/app/core/security/jwt.py @@ -0,0 +1,60 @@ +import time + +import jwt +from pydantic import BaseModel + +from app.core.config import get_settings + +JWT_ALGORITHM = "HS256" + + +# https://www.rfc-editor.org/rfc/rfc7519#section-4.1 +class JWTTokenPayload(BaseModel): + iss: str + sub: str + exp: int + iat: int + + +class JWTToken(BaseModel): + payload: JWTTokenPayload + access_token: str + + +def create_jwt_token(user_id: str) -> JWTToken: + iat = int(time.time()) + exp = iat + get_settings().security.jwt_access_token_expire_secs + + token_payload = JWTTokenPayload( + iss=get_settings().security.jwt_issuer, + sub=user_id, + exp=exp, + iat=iat, + ) + + access_token = jwt.encode( + token_payload.model_dump(), + key=get_settings().security.jwt_secret_key.get_secret_value(), + algorithm=JWT_ALGORITHM, + ) + + return JWTToken(payload=token_payload, access_token=access_token) + + +def verify_jwt_token(token: str) -> JWTTokenPayload: + try: + raw_payload = jwt.decode( + token, + get_settings().security.jwt_secret_key.get_secret_value(), + algorithms=[JWT_ALGORITHM], + ) + except jwt.DecodeError: + raise ValueError("invalid token") + + token_payload = JWTTokenPayload(**raw_payload) + + now = int(time.time()) + if now < token_payload.iat or now > token_payload.exp: + raise ValueError("token expired") + + return token_payload diff --git a/{{cookiecutter.project_name}}/app/core/security/password.py b/{{cookiecutter.project_name}}/app/core/security/password.py new file mode 100644 index 0000000..4882741 --- /dev/null +++ b/{{cookiecutter.project_name}}/app/core/security/password.py @@ -0,0 +1,11 @@ +import bcrypt + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return bcrypt.checkpw( + plain_password.encode("utf-8"), hashed_password.encode("utf-8") + ) + + +def get_password_hash(password: str) -> str: + return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() diff --git a/{{cookiecutter.project_name}}/app/core/session.py b/{{cookiecutter.project_name}}/app/core/session.py index 366773a..efd4ca5 100644 --- a/{{cookiecutter.project_name}}/app/core/session.py +++ b/{{cookiecutter.project_name}}/app/core/session.py @@ -6,13 +6,10 @@ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine -from app.core import config +from app.core.config import get_settings -if config.settings.ENVIRONMENT == "PYTEST": - sqlalchemy_database_uri = config.settings.TEST_SQLALCHEMY_DATABASE_URI -else: - sqlalchemy_database_uri = config.settings.DEFAULT_SQLALCHEMY_DATABASE_URI - -async_engine = create_async_engine(sqlalchemy_database_uri, pool_pre_ping=True) +async_engine = create_async_engine( + get_settings().sqlalchemy_database_uri.get_secret_value(), pool_pre_ping=True +) async_session = async_sessionmaker(async_engine, expire_on_commit=False) diff --git a/{{cookiecutter.project_name}}/app/initial_data.py b/{{cookiecutter.project_name}}/app/initial_data.py deleted file mode 100644 index ad26c29..0000000 --- a/{{cookiecutter.project_name}}/app/initial_data.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Put here any Python code that must be runned before application startup. -It is included in `init.sh` script. - -By defualt `main` create a superuser if not exists -""" - -import asyncio - -from sqlalchemy import select - -from app.core import config, security -from app.core.session import async_session -from app.models import User - - -async def main() -> None: - print("Start initial data") - async with async_session() as session: - result = await session.execute( - select(User).where(User.email == config.settings.FIRST_SUPERUSER_EMAIL) - ) - user = result.scalars().first() - - if user is None: - new_superuser = User( - email=config.settings.FIRST_SUPERUSER_EMAIL, - hashed_password=security.get_password_hash( - config.settings.FIRST_SUPERUSER_PASSWORD - ), - ) - session.add(new_superuser) - await session.commit() - print("Superuser was created") - else: - print("Superuser already exists in database") - - print("Initial data created") - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/{{cookiecutter.project_name}}/app/main.py b/{{cookiecutter.project_name}}/app/main.py index bfe7a21..2a7217d 100644 --- a/{{cookiecutter.project_name}}/app/main.py +++ b/{{cookiecutter.project_name}}/app/main.py @@ -5,25 +5,31 @@ from fastapi.middleware.trustedhost import TrustedHostMiddleware from app.api.api import api_router -from app.core import config +from app.core.config import get_settings app = FastAPI( - title=config.settings.PROJECT_NAME, - version=config.settings.VERSION, - description=config.settings.DESCRIPTION, + title="minimal fastapi postgres template", + version="6.0.0", + description="https://github.com/rafsaf/minimal-fastapi-postgres-template", openapi_url="/openapi.json", docs_url="/", ) + app.include_router(api_router) # Sets all CORS enabled origins app.add_middleware( CORSMiddleware, - allow_origins=[str(origin) for origin in config.settings.BACKEND_CORS_ORIGINS], + allow_origins=[ + str(origin) for origin in get_settings().security.backend_cors_origins + ], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Guards against HTTP Host Header attacks -app.add_middleware(TrustedHostMiddleware, allowed_hosts=config.settings.ALLOWED_HOSTS) +app.add_middleware( + TrustedHostMiddleware, + allowed_hosts=get_settings().security.allowed_hosts, +) diff --git a/{{cookiecutter.project_name}}/app/models.py b/{{cookiecutter.project_name}}/app/models.py index aba9385..a4255b3 100644 --- a/{{cookiecutter.project_name}}/app/models.py +++ b/{{cookiecutter.project_name}}/app/models.py @@ -15,9 +15,8 @@ """ import uuid -from sqlalchemy import String -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column +from sqlalchemy import Boolean, String, ForeignKey, Uuid, BigInteger +from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship class Base(DeclarativeBase): @@ -25,12 +24,28 @@ class Base(DeclarativeBase): class User(Base): - __tablename__ = "user_model" + __tablename__ = "user_account" - id: Mapped[str] = mapped_column( - UUID(as_uuid=False), primary_key=True, default=lambda _: str(uuid.uuid4()) + user_id: Mapped[str] = mapped_column( + Uuid(as_uuid=False), primary_key=True, default=lambda _: str(uuid.uuid4()) ) email: Mapped[str] = mapped_column( - String(254), nullable=False, unique=True, index=True + String(256), nullable=False, unique=True, index=True ) hashed_password: Mapped[str] = mapped_column(String(128), nullable=False) + refresh_tokens: Mapped[list["RefreshToken"]] = relationship(back_populates="user") + + +class RefreshToken(Base): + __tablename__ = "refresh_token" + + id: Mapped[int] = mapped_column(BigInteger, primary_key=True) + refresh_token: Mapped[str] = mapped_column( + String(512), nullable=False, unique=True, index=True + ) + used: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) + exp: Mapped[int] = mapped_column(BigInteger, nullable=False) + user_id: Mapped[str] = mapped_column( + ForeignKey("user_account.user_id", ondelete="CASCADE"), + ) + user: Mapped["User"] = relationship(back_populates="refresh_tokens") diff --git a/{{cookiecutter.project_name}}/app/schemas/responses.py b/{{cookiecutter.project_name}}/app/schemas/responses.py index 52db66b..77fd722 100644 --- a/{{cookiecutter.project_name}}/app/schemas/responses.py +++ b/{{cookiecutter.project_name}}/app/schemas/responses.py @@ -6,15 +6,13 @@ class BaseResponse(BaseModel): class AccessTokenResponse(BaseResponse): - token_type: str + token_type: str = "Bearer" access_token: str expires_at: int - issued_at: int refresh_token: str refresh_token_expires_at: int - refresh_token_issued_at: int class UserResponse(BaseResponse): - id: str + user_id: str email: EmailStr diff --git a/{{cookiecutter.project_name}}/app/tests/conftest.py b/{{cookiecutter.project_name}}/app/tests/conftest.py index c7d816f..6873016 100644 --- a/{{cookiecutter.project_name}}/app/tests/conftest.py +++ b/{{cookiecutter.project_name}}/app/tests/conftest.py @@ -1,5 +1,6 @@ import asyncio from collections.abc import AsyncGenerator +from typing import Generator import pytest import pytest_asyncio @@ -22,7 +23,7 @@ @pytest.fixture(scope="session") -def event_loop(): +def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) yield loop @@ -30,7 +31,7 @@ def event_loop(): @pytest_asyncio.fixture(scope="session") -async def test_db_setup_sessionmaker(): +async def test_db_setup_sessionmaker() -> None: # assert if we use TEST_DB URL for 100% assert config.settings.ENVIRONMENT == "PYTEST" @@ -41,14 +42,11 @@ async def test_db_setup_sessionmaker(): @pytest_asyncio.fixture(autouse=True) -async def session(test_db_setup_sessionmaker) -> AsyncGenerator[AsyncSession, None]: +async def session(test_db_setup_sessionmaker: None) -> AsyncGenerator[AsyncSession, None]: async with async_session() as session: yield session - - # delete all data from all tables after test - for name, table in Base.metadata.tables.items(): - await session.execute(delete(table)) - await session.commit() + await session.rollback() + await session.close() @pytest_asyncio.fixture(scope="session") @@ -59,7 +57,7 @@ async def client() -> AsyncGenerator[AsyncClient, None]: @pytest_asyncio.fixture -async def default_user(test_db_setup_sessionmaker) -> User: +async def default_user(test_db_setup_sessionmaker: None) -> User: async with async_session() as session: result = await session.execute( select(User).where(User.email == default_user_email) @@ -79,5 +77,5 @@ async def default_user(test_db_setup_sessionmaker) -> User: @pytest.fixture -def default_user_headers(default_user: User): +def default_user_headers(default_user: User) -> dict[str, str]: return {"Authorization": f"Bearer {default_user_access_token}"} diff --git a/{{cookiecutter.project_name}}/app/tests/test_auth.py b/{{cookiecutter.project_name}}/app/tests/test_auth.py index 6f8bd90..7a1ed34 100644 --- a/{{cookiecutter.project_name}}/app/tests/test_auth.py +++ b/{{cookiecutter.project_name}}/app/tests/test_auth.py @@ -5,7 +5,7 @@ from app.tests.conftest import default_user_email, default_user_password -async def test_auth_access_token(client: AsyncClient, default_user: User): +async def test_auth_access_token(client: AsyncClient, default_user: User) -> None: response = await client.post( app.url_path_for("login_access_token"), data={ @@ -25,7 +25,7 @@ async def test_auth_access_token(client: AsyncClient, default_user: User): assert "refresh_token_issued_at" in token -async def test_auth_access_token_fail_no_user(client: AsyncClient): +async def test_auth_access_token_fail_no_user(client: AsyncClient) -> None: response = await client.post( app.url_path_for("login_access_token"), data={ @@ -39,7 +39,7 @@ async def test_auth_access_token_fail_no_user(client: AsyncClient): assert response.json() == {"detail": "Incorrect email or password"} -async def test_auth_refresh_token(client: AsyncClient, default_user: User): +async def test_auth_refresh_token(client: AsyncClient, default_user: User) -> None: response = await client.post( app.url_path_for("login_access_token"), data={ diff --git a/{{cookiecutter.project_name}}/app/tests/test_users.py b/{{cookiecutter.project_name}}/app/tests/test_users.py index 1be021d..c30024b 100644 --- a/{{cookiecutter.project_name}}/app/tests/test_users.py +++ b/{{cookiecutter.project_name}}/app/tests/test_users.py @@ -11,7 +11,7 @@ ) -async def test_read_current_user(client: AsyncClient, default_user_headers): +async def test_read_current_user(client: AsyncClient, default_user_headers: dict[str, str]) -> None: response = await client.get( app.url_path_for("read_current_user"), headers=default_user_headers ) @@ -23,8 +23,8 @@ async def test_read_current_user(client: AsyncClient, default_user_headers): async def test_delete_current_user( - client: AsyncClient, default_user_headers, session: AsyncSession -): + client: AsyncClient, default_user_headers: dict[str, str], session: AsyncSession +) -> None: response = await client.delete( app.url_path_for("delete_current_user"), headers=default_user_headers ) @@ -35,8 +35,8 @@ async def test_delete_current_user( async def test_reset_current_user_password( - client: AsyncClient, default_user_headers, session: AsyncSession -): + client: AsyncClient, default_user_headers: dict[str, str], session: AsyncSession +) -> None: response = await client.post( app.url_path_for("reset_current_user_password"), headers=default_user_headers, @@ -50,8 +50,8 @@ async def test_reset_current_user_password( async def test_register_new_user( - client: AsyncClient, default_user_headers, session: AsyncSession -): + client: AsyncClient, default_user_headers: dict[str, str], session: AsyncSession +) -> None: response = await client.post( app.url_path_for("register_new_user"), headers=default_user_headers, diff --git a/{{cookiecutter.project_name}}/init.sh b/{{cookiecutter.project_name}}/init.sh index 5811845..3cf97f5 100755 --- a/{{cookiecutter.project_name}}/init.sh +++ b/{{cookiecutter.project_name}}/init.sh @@ -2,6 +2,3 @@ echo "Run migrations" alembic upgrade head - -echo "Create initial data in DB" -python -m app.initial_data diff --git a/{{cookiecutter.project_name}}/poetry.lock b/{{cookiecutter.project_name}}/poetry.lock index bd4daa9..1a7125f 100644 --- a/{{cookiecutter.project_name}}/poetry.lock +++ b/{{cookiecutter.project_name}}/poetry.lock @@ -1,14 +1,14 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "alembic" -version = "1.12.1" +version = "1.13.1" description = "A database migration tool for SQLAlchemy." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "alembic-1.12.1-py3-none-any.whl", hash = "sha256:47d52e3dfb03666ed945becb723d6482e52190917fdb47071440cfdba05d92cb"}, - {file = "alembic-1.12.1.tar.gz", hash = "sha256:bca5877e9678b454706347bc10b97cb7d67f300320fa5c3a94423e8266e2823f"}, + {file = "alembic-1.13.1-py3-none-any.whl", hash = "sha256:2edcc97bed0bd3272611ce3a98d98279e9c209e7186e43e75bbb1b2bdfdbcc43"}, + {file = "alembic-1.13.1.tar.gz", hash = "sha256:4932c8558bf68f2ee92b9bbcb8218671c627064d5b08939437af6d77dc05e595"}, ] [package.dependencies] @@ -17,7 +17,7 @@ SQLAlchemy = ">=1.3.0" typing-extensions = ">=4" [package.extras] -tz = ["python-dateutil"] +tz = ["backports.zoneinfo"] [[package]] name = "annotated-types" @@ -32,13 +32,13 @@ files = [ [[package]] name = "anyio" -version = "3.7.1" +version = "4.2.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, - {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, + {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, + {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, ] [package.dependencies] @@ -46,9 +46,9 @@ idna = ">=2.8" sniffio = ">=1.1" [package.extras] -doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (<0.22)"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +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)"] +trio = ["trio (>=0.23)"] [[package]] name = "asyncpg" @@ -106,32 +106,38 @@ test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"] [[package]] name = "bcrypt" -version = "4.0.1" +version = "4.1.2" description = "Modern password hashing for your software and your servers" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "bcrypt-4.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:b1023030aec778185a6c16cf70f359cbb6e0c289fd564a7cfa29e727a1c38f8f"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:08d2947c490093a11416df18043c27abe3921558d2c03e2076ccb28a116cb6d0"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0eaa47d4661c326bfc9d08d16debbc4edf78778e6aaba29c1bc7ce67214d4410"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ae88eca3024bb34bb3430f964beab71226e761f51b912de5133470b649d82344"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:a522427293d77e1c29e303fc282e2d71864579527a04ddcfda6d4f8396c6c36a"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:fbdaec13c5105f0c4e5c52614d04f0bca5f5af007910daa8b6b12095edaa67b3"}, - {file = "bcrypt-4.0.1-cp36-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:ca3204d00d3cb2dfed07f2d74a25f12fc12f73e606fcaa6975d1f7ae69cacbb2"}, - {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:089098effa1bc35dc055366740a067a2fc76987e8ec75349eb9484061c54f535"}, - {file = "bcrypt-4.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e9a51bbfe7e9802b5f3508687758b564069ba937748ad7b9e890086290d2f79e"}, - {file = "bcrypt-4.0.1-cp36-abi3-win32.whl", hash = "sha256:2caffdae059e06ac23fce178d31b4a702f2a3264c20bfb5ff541b338194d8fab"}, - {file = "bcrypt-4.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:8a68f4341daf7522fe8d73874de8906f3a339048ba406be6ddc1b3ccb16fc0d9"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf4fa8b2ca74381bb5442c089350f09a3f17797829d958fad058d6e44d9eb83c"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:67a97e1c405b24f19d08890e7ae0c4f7ce1e56a712a016746c8b2d7732d65d4b"}, - {file = "bcrypt-4.0.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b3b85202d95dd568efcb35b53936c5e3b3600c7cdcc6115ba461df3a8e89f38d"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbb03eec97496166b704ed663a53680ab57c5084b2fc98ef23291987b525cb7d"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:5ad4d32a28b80c5fa6671ccfb43676e8c1cc232887759d1cd7b6f56ea4355215"}, - {file = "bcrypt-4.0.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b57adba8a1444faf784394de3436233728a1ecaeb6e07e8c22c8848f179b893c"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:705b2cea8a9ed3d55b4491887ceadb0106acf7c6387699fca771af56b1cdeeda"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:2b3ac11cf45161628f1f3733263e63194f22664bf4d0c0f3ab34099c02134665"}, - {file = "bcrypt-4.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3100851841186c25f127731b9fa11909ab7b1df6fc4b9f8353f4f1fd952fbf71"}, - {file = "bcrypt-4.0.1.tar.gz", hash = "sha256:27d375903ac8261cfe4047f6709d16f7d18d39b1ec92aaf72af989552a650ebd"}, + {file = "bcrypt-4.1.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:ac621c093edb28200728a9cca214d7e838529e557027ef0581685909acd28b5e"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea505c97a5c465ab8c3ba75c0805a102ce526695cd6818c6de3b1a38f6f60da1"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57fa9442758da926ed33a91644649d3e340a71e2d0a5a8de064fb621fd5a3326"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb3bd3321517916696233b5e0c67fd7d6281f0ef48e66812db35fc963a422a1c"}, + {file = "bcrypt-4.1.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6cad43d8c63f34b26aef462b6f5e44fdcf9860b723d2453b5d391258c4c8e966"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:44290ccc827d3a24604f2c8bcd00d0da349e336e6503656cb8192133e27335e2"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:732b3920a08eacf12f93e6b04ea276c489f1c8fb49344f564cca2adb663b3e4c"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1c28973decf4e0e69cee78c68e30a523be441972c826703bb93099868a8ff5b5"}, + {file = "bcrypt-4.1.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b8df79979c5bae07f1db22dcc49cc5bccf08a0380ca5c6f391cbb5790355c0b0"}, + {file = "bcrypt-4.1.2-cp37-abi3-win32.whl", hash = "sha256:fbe188b878313d01b7718390f31528be4010fed1faa798c5a1d0469c9c48c369"}, + {file = "bcrypt-4.1.2-cp37-abi3-win_amd64.whl", hash = "sha256:9800ae5bd5077b13725e2e3934aa3c9c37e49d3ea3d06318010aa40f54c63551"}, + {file = "bcrypt-4.1.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:71b8be82bc46cedd61a9f4ccb6c1a493211d031415a34adde3669ee1b0afbb63"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e3c6642077b0c8092580c819c1684161262b2e30c4f45deb000c38947bf483"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:387e7e1af9a4dd636b9505a465032f2f5cb8e61ba1120e79a0e1cd0b512f3dfc"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f70d9c61f9c4ca7d57f3bfe88a5ccf62546ffbadf3681bb1e268d9d2e41c91a7"}, + {file = "bcrypt-4.1.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2a298db2a8ab20056120b45e86c00a0a5eb50ec4075b6142db35f593b97cb3fb"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ba55e40de38a24e2d78d34c2d36d6e864f93e0d79d0b6ce915e4335aa81d01b1"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3566a88234e8de2ccae31968127b0ecccbb4cddb629da744165db72b58d88ca4"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b90e216dc36864ae7132cb151ffe95155a37a14e0de3a8f64b49655dd959ff9c"}, + {file = "bcrypt-4.1.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:69057b9fc5093ea1ab00dd24ede891f3e5e65bee040395fb1e66ee196f9c9b4a"}, + {file = "bcrypt-4.1.2-cp39-abi3-win32.whl", hash = "sha256:02d9ef8915f72dd6daaef40e0baeef8a017ce624369f09754baf32bb32dba25f"}, + {file = "bcrypt-4.1.2-cp39-abi3-win_amd64.whl", hash = "sha256:be3ab1071662f6065899fe08428e45c16aa36e28bc42921c4901a191fda6ee42"}, + {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d75fc8cd0ba23f97bae88a6ec04e9e5351ff3c6ad06f38fe32ba50cbd0d11946"}, + {file = "bcrypt-4.1.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:a97e07e83e3262599434816f631cc4c7ca2aa8e9c072c1b1a7fec2ae809a1d2d"}, + {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e51c42750b7585cee7892c2614be0d14107fad9581d1738d954a262556dd1aab"}, + {file = "bcrypt-4.1.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba4e4cc26610581a6329b3937e02d319f5ad4b85b074846bf4fef8a8cf51e7bb"}, + {file = "bcrypt-4.1.2.tar.gz", hash = "sha256:33313a1200a3ae90b75587ceac502b048b840fc69e7f7a0905b5f87fac7a1258"}, ] [package.extras] @@ -140,29 +146,33 @@ typecheck = ["mypy"] [[package]] name = "black" -version = "23.10.1" +version = "23.12.1" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"}, - {file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"}, - {file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"}, - {file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"}, - {file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"}, - {file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"}, - {file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"}, - {file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"}, - {file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"}, - {file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"}, - {file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"}, - {file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"}, - {file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"}, - {file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"}, - {file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"}, - {file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"}, - {file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"}, - {file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"}, + {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, + {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, + {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, + {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, + {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, + {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, + {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, + {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, + {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, + {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, + {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, + {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, + {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, + {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, + {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, + {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, + {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, + {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, + {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, + {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, + {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, + {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, ] [package.dependencies] @@ -174,19 +184,19 @@ platformdirs = ">=2" [package.extras] colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2023.7.22" +version = "2023.11.17" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, + {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] [[package]] @@ -291,63 +301,63 @@ files = [ [[package]] name = "coverage" -version = "7.3.2" +version = "7.4.0" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf"}, - {file = "coverage-7.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c"}, - {file = "coverage-7.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148"}, - {file = "coverage-7.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9"}, - {file = "coverage-7.3.2-cp310-cp310-win32.whl", hash = "sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f"}, - {file = "coverage-7.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c"}, - {file = "coverage-7.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3"}, - {file = "coverage-7.3.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c"}, - {file = "coverage-7.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312"}, - {file = "coverage-7.3.2-cp311-cp311-win32.whl", hash = "sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640"}, - {file = "coverage-7.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836"}, - {file = "coverage-7.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"}, - {file = "coverage-7.3.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84"}, - {file = "coverage-7.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a"}, - {file = "coverage-7.3.2-cp312-cp312-win32.whl", hash = "sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb"}, - {file = "coverage-7.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738"}, - {file = "coverage-7.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c"}, - {file = "coverage-7.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901"}, - {file = "coverage-7.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76"}, - {file = "coverage-7.3.2-cp38-cp38-win32.whl", hash = "sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92"}, - {file = "coverage-7.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce"}, - {file = "coverage-7.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25"}, - {file = "coverage-7.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc"}, - {file = "coverage-7.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083"}, - {file = "coverage-7.3.2-cp39-cp39-win32.whl", hash = "sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce"}, - {file = "coverage-7.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f"}, - {file = "coverage-7.3.2-pp38.pp39.pp310-none-any.whl", hash = "sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637"}, - {file = "coverage-7.3.2.tar.gz", hash = "sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef"}, + {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, + {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, + {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, + {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, + {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, + {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, + {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, + {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, + {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, + {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, + {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, + {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, + {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, + {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, + {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, + {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, + {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, + {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, + {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, + {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, + {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, + {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, + {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, + {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, + {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, + {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, + {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, + {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, ] [package.extras] @@ -355,34 +365,34 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "41.0.5" +version = "41.0.7" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" files = [ - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:da6a0ff8f1016ccc7477e6339e1d50ce5f59b88905585f77193ebd5068f1e797"}, - {file = "cryptography-41.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b948e09fe5fb18517d99994184854ebd50b57248736fd4c720ad540560174ec5"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d38e6031e113b7421db1de0c1b1f7739564a88f1684c6b89234fbf6c11b75147"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e270c04f4d9b5671ebcc792b3ba5d4488bf7c42c3c241a3748e2599776f29696"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ec3b055ff8f1dce8e6ef28f626e0972981475173d7973d63f271b29c8a2897da"}, - {file = "cryptography-41.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7d208c21e47940369accfc9e85f0de7693d9a5d843c2509b3846b2db170dfd20"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8254962e6ba1f4d2090c44daf50a547cd5f0bf446dc658a8e5f8156cae0d8548"}, - {file = "cryptography-41.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a48e74dad1fb349f3dc1d449ed88e0017d792997a7ad2ec9587ed17405667e6d"}, - {file = "cryptography-41.0.5-cp37-abi3-win32.whl", hash = "sha256:d3977f0e276f6f5bf245c403156673db103283266601405376f075c849a0b936"}, - {file = "cryptography-41.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:73801ac9736741f220e20435f84ecec75ed70eda90f781a148f1bad546963d81"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3be3ca726e1572517d2bef99a818378bbcf7d7799d5372a46c79c29eb8d166c1"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e886098619d3815e0ad5790c973afeee2c0e6e04b4da90b88e6bd06e2a0b1b72"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:573eb7128cbca75f9157dcde974781209463ce56b5804983e11a1c462f0f4e88"}, - {file = "cryptography-41.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0c327cac00f082013c7c9fb6c46b7cc9fa3c288ca702c74773968173bda421bf"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:227ec057cd32a41c6651701abc0328135e472ed450f47c2766f23267b792a88e"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:22892cc830d8b2c89ea60148227631bb96a7da0c1b722f2aac8824b1b7c0b6b8"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5a70187954ba7292c7876734183e810b728b4f3965fbe571421cb2434d279179"}, - {file = "cryptography-41.0.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:88417bff20162f635f24f849ab182b092697922088b477a7abd6664ddd82291d"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c707f7afd813478e2019ae32a7c49cd932dd60ab2d2a93e796f68236b7e1fbf1"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:580afc7b7216deeb87a098ef0674d6ee34ab55993140838b14c9b83312b37b86"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1e91467c65fe64a82c689dc6cf58151158993b13eb7a7f3f4b7f395636723"}, - {file = "cryptography-41.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d2a6a598847c46e3e321a7aef8af1436f11c27f1254933746304ff014664d84"}, - {file = "cryptography-41.0.5.tar.gz", hash = "sha256:392cb88b597247177172e02da6b7a63deeff1937fa6fec3bbf902ebd75d97ec7"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, + {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, + {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, + {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, + {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, + {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, + {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, + {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, + {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, + {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, ] [package.dependencies] @@ -400,13 +410,13 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "distlib" -version = "0.3.7" +version = "0.3.8" description = "Distribution utilities" optional = false python-versions = "*" files = [ - {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"}, - {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"}, + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] [[package]] @@ -445,19 +455,18 @@ idna = ">=2.0.0" [[package]] name = "fastapi" -version = "0.104.1" +version = "0.109.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi-0.104.1-py3-none-any.whl", hash = "sha256:752dc31160cdbd0436bb93bad51560b57e525cbb1d4bbf6f4904ceee75548241"}, - {file = "fastapi-0.104.1.tar.gz", hash = "sha256:e5e4540a7c5e1dcfbbcf5b903c234feddcdcd881f191977a1c5dfd917487e7ae"}, + {file = "fastapi-0.109.0-py3-none-any.whl", hash = "sha256:8c77515984cd8e8cfeb58364f8cc7a28f0692088475e2614f7bf03275eba9093"}, + {file = "fastapi-0.109.0.tar.gz", hash = "sha256:b978095b9ee01a5cf49b19f4bc1ac9b8ca83aa076e770ef8fd9af09a2b88d191"}, ] [package.dependencies] -anyio = ">=3.7.1,<4.0.0" 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" -starlette = ">=0.27.0,<0.28.0" +starlette = ">=0.35.0,<0.36.0" typing-extensions = ">=4.8.0" [package.extras] @@ -481,72 +490,73 @@ typing = ["typing-extensions (>=4.8)"] [[package]] name = "greenlet" -version = "3.0.1" +version = "3.0.3" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" files = [ - {file = "greenlet-3.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f89e21afe925fcfa655965ca8ea10f24773a1791400989ff32f467badfe4a064"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28e89e232c7593d33cac35425b58950789962011cc274aa43ef8865f2e11f46d"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8ba29306c5de7717b5761b9ea74f9c72b9e2b834e24aa984da99cbfc70157fd"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19bbdf1cce0346ef7341705d71e2ecf6f41a35c311137f29b8a2dc2341374565"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599daf06ea59bfedbec564b1692b0166a0045f32b6f0933b0dd4df59a854caf2"}, - {file = "greenlet-3.0.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b641161c302efbb860ae6b081f406839a8b7d5573f20a455539823802c655f63"}, - {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d57e20ba591727da0c230ab2c3f200ac9d6d333860d85348816e1dca4cc4792e"}, - {file = "greenlet-3.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5805e71e5b570d490938d55552f5a9e10f477c19400c38bf1d5190d760691846"}, - {file = "greenlet-3.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:52e93b28db27ae7d208748f45d2db8a7b6a380e0d703f099c949d0f0d80b70e9"}, - {file = "greenlet-3.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f7bfb769f7efa0eefcd039dd19d843a4fbfbac52f1878b1da2ed5793ec9b1a65"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91e6c7db42638dc45cf2e13c73be16bf83179f7859b07cfc139518941320be96"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1757936efea16e3f03db20efd0cd50a1c86b06734f9f7338a90c4ba85ec2ad5a"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19075157a10055759066854a973b3d1325d964d498a805bb68a1f9af4aaef8ec"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9d21aaa84557d64209af04ff48e0ad5e28c5cca67ce43444e939579d085da72"}, - {file = "greenlet-3.0.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2847e5d7beedb8d614186962c3d774d40d3374d580d2cbdab7f184580a39d234"}, - {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:97e7ac860d64e2dcba5c5944cfc8fa9ea185cd84061c623536154d5a89237884"}, - {file = "greenlet-3.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b2c02d2ad98116e914d4f3155ffc905fd0c025d901ead3f6ed07385e19122c94"}, - {file = "greenlet-3.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:22f79120a24aeeae2b4471c711dcf4f8c736a2bb2fabad2a67ac9a55ea72523c"}, - {file = "greenlet-3.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:100f78a29707ca1525ea47388cec8a049405147719f47ebf3895e7509c6446aa"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60d5772e8195f4e9ebf74046a9121bbb90090f6550f81d8956a05387ba139353"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:daa7197b43c707462f06d2c693ffdbb5991cbb8b80b5b984007de431493a319c"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea6b8aa9e08eea388c5f7a276fabb1d4b6b9d6e4ceb12cc477c3d352001768a9"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d11ebbd679e927593978aa44c10fc2092bc454b7d13fdc958d3e9d508aba7d0"}, - {file = "greenlet-3.0.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbd4c177afb8a8d9ba348d925b0b67246147af806f0b104af4d24f144d461cd5"}, - {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20107edf7c2c3644c67c12205dc60b1bb11d26b2610b276f97d666110d1b511d"}, - {file = "greenlet-3.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8bef097455dea90ffe855286926ae02d8faa335ed8e4067326257cb571fc1445"}, - {file = "greenlet-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:b2d3337dcfaa99698aa2377c81c9ca72fcd89c07e7eb62ece3f23a3fe89b2ce4"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80ac992f25d10aaebe1ee15df45ca0d7571d0f70b645c08ec68733fb7a020206"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:337322096d92808f76ad26061a8f5fccb22b0809bea39212cd6c406f6a7060d2"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9934adbd0f6e476f0ecff3c94626529f344f57b38c9a541f87098710b18af0a"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc4d815b794fd8868c4d67602692c21bf5293a75e4b607bb92a11e821e2b859a"}, - {file = "greenlet-3.0.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41bdeeb552d814bcd7fb52172b304898a35818107cc8778b5101423c9017b3de"}, - {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e6061bf1e9565c29002e3c601cf68569c450be7fc3f7336671af7ddb4657166"}, - {file = "greenlet-3.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:fa24255ae3c0ab67e613556375a4341af04a084bd58764731972bcbc8baeba36"}, - {file = "greenlet-3.0.1-cp37-cp37m-win32.whl", hash = "sha256:b489c36d1327868d207002391f662a1d163bdc8daf10ab2e5f6e41b9b96de3b1"}, - {file = "greenlet-3.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f33f3258aae89da191c6ebaa3bc517c6c4cbc9b9f689e5d8452f7aedbb913fa8"}, - {file = "greenlet-3.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:d2905ce1df400360463c772b55d8e2518d0e488a87cdea13dd2c71dcb2a1fa16"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a02d259510b3630f330c86557331a3b0e0c79dac3d166e449a39363beaae174"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55d62807f1c5a1682075c62436702aaba941daa316e9161e4b6ccebbbf38bda3"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fcc780ae8edbb1d050d920ab44790201f027d59fdbd21362340a85c79066a74"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eddd98afc726f8aee1948858aed9e6feeb1758889dfd869072d4465973f6bfd"}, - {file = "greenlet-3.0.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eabe7090db68c981fca689299c2d116400b553f4b713266b130cfc9e2aa9c5a9"}, - {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f2f6d303f3dee132b322a14cd8765287b8f86cdc10d2cb6a6fae234ea488888e"}, - {file = "greenlet-3.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d923ff276f1c1f9680d32832f8d6c040fe9306cbfb5d161b0911e9634be9ef0a"}, - {file = "greenlet-3.0.1-cp38-cp38-win32.whl", hash = "sha256:0b6f9f8ca7093fd4433472fd99b5650f8a26dcd8ba410e14094c1e44cd3ceddd"}, - {file = "greenlet-3.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:990066bff27c4fcf3b69382b86f4c99b3652bab2a7e685d968cd4d0cfc6f67c6"}, - {file = "greenlet-3.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ce85c43ae54845272f6f9cd8320d034d7a946e9773c693b27d620edec825e376"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89ee2e967bd7ff85d84a2de09df10e021c9b38c7d91dead95b406ed6350c6997"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87c8ceb0cf8a5a51b8008b643844b7f4a8264a2c13fcbcd8a8316161725383fe"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6a8c9d4f8692917a3dc7eb25a6fb337bff86909febe2f793ec1928cd97bedfc"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fbc5b8f3dfe24784cee8ce0be3da2d8a79e46a276593db6868382d9c50d97b1"}, - {file = "greenlet-3.0.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85d2b77e7c9382f004b41d9c72c85537fac834fb141b0296942d52bf03fe4a3d"}, - {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:696d8e7d82398e810f2b3622b24e87906763b6ebfd90e361e88eb85b0e554dc8"}, - {file = "greenlet-3.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:329c5a2e5a0ee942f2992c5e3ff40be03e75f745f48847f118a3cfece7a28546"}, - {file = "greenlet-3.0.1-cp39-cp39-win32.whl", hash = "sha256:cf868e08690cb89360eebc73ba4be7fb461cfbc6168dd88e2fbbe6f31812cd57"}, - {file = "greenlet-3.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:ac4a39d1abae48184d420aa8e5e63efd1b75c8444dd95daa3e03f6c6310e9619"}, - {file = "greenlet-3.0.1.tar.gz", hash = "sha256:816bd9488a94cba78d93e1abb58000e8266fa9cc2aa9ccdd6eb0696acb24005b"}, + {file = "greenlet-3.0.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9da2bd29ed9e4f15955dd1595ad7bc9320308a3b766ef7f837e23ad4b4aac31a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d353cadd6083fdb056bb46ed07e4340b0869c305c8ca54ef9da3421acbdf6881"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dca1e2f3ca00b84a396bc1bce13dd21f680f035314d2379c4160c98153b2059b"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ed7fb269f15dc662787f4119ec300ad0702fa1b19d2135a37c2c4de6fadfd4a"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd4f49ae60e10adbc94b45c0b5e6a179acc1736cf7a90160b404076ee283cf83"}, + {file = "greenlet-3.0.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73a411ef564e0e097dbe7e866bb2dda0f027e072b04da387282b02c308807405"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7f362975f2d179f9e26928c5b517524e89dd48530a0202570d55ad6ca5d8a56f"}, + {file = "greenlet-3.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:649dde7de1a5eceb258f9cb00bdf50e978c9db1b996964cd80703614c86495eb"}, + {file = "greenlet-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:68834da854554926fbedd38c76e60c4a2e3198c6fbed520b106a8986445caaf9"}, + {file = "greenlet-3.0.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b1b5667cced97081bf57b8fa1d6bfca67814b0afd38208d52538316e9422fc61"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52f59dd9c96ad2fc0d5724107444f76eb20aaccb675bf825df6435acb7703559"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afaff6cf5200befd5cec055b07d1c0a5a06c040fe5ad148abcd11ba6ab9b114e"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe754d231288e1e64323cfad462fcee8f0288654c10bdf4f603a39ed923bef33"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2797aa5aedac23af156bbb5a6aa2cd3427ada2972c828244eb7d1b9255846379"}, + {file = "greenlet-3.0.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7f009caad047246ed379e1c4dbcb8b020f0a390667ea74d2387be2998f58a22"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c5e1536de2aad7bf62e27baf79225d0d64360d4168cf2e6becb91baf1ed074f3"}, + {file = "greenlet-3.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:894393ce10ceac937e56ec00bb71c4c2f8209ad516e96033e4b3b1de270e200d"}, + {file = "greenlet-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:1ea188d4f49089fc6fb283845ab18a2518d279c7cd9da1065d7a84e991748728"}, + {file = "greenlet-3.0.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:70fb482fdf2c707765ab5f0b6655e9cfcf3780d8d87355a063547b41177599be"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4d1ac74f5c0c0524e4a24335350edad7e5f03b9532da7ea4d3c54d527784f2e"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149e94a2dd82d19838fe4b2259f1b6b9957d5ba1b25640d2380bea9c5df37676"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15d79dd26056573940fcb8c7413d84118086f2ec1a8acdfa854631084393efcc"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b7db1ebff4ba09aaaeae6aa491daeb226c8150fc20e836ad00041bcb11230"}, + {file = "greenlet-3.0.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fcd2469d6a2cf298f198f0487e0a5b1a47a42ca0fa4dfd1b6862c999f018ebbf"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1f672519db1796ca0d8753f9e78ec02355e862d0998193038c7073045899f305"}, + {file = "greenlet-3.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2516a9957eed41dd8f1ec0c604f1cdc86758b587d964668b5b196a9db5bfcde6"}, + {file = "greenlet-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:bba5387a6975598857d86de9eac14210a49d554a77eb8261cc68b7d082f78ce2"}, + {file = "greenlet-3.0.3-cp37-cp37m-macosx_11_0_universal2.whl", hash = "sha256:5b51e85cb5ceda94e79d019ed36b35386e8c37d22f07d6a751cb659b180d5274"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:daf3cb43b7cf2ba96d614252ce1684c1bccee6b2183a01328c98d36fcd7d5cb0"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99bf650dc5d69546e076f413a87481ee1d2d09aaaaaca058c9251b6d8c14783f"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2dd6e660effd852586b6a8478a1d244b8dc90ab5b1321751d2ea15deb49ed414"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3391d1e16e2a5a1507d83e4a8b100f4ee626e8eca43cf2cadb543de69827c4c"}, + {file = "greenlet-3.0.3-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1f145462f1fa6e4a4ae3c0f782e580ce44d57c8f2c7aae1b6fa88c0b2efdb41"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1a7191e42732df52cb5f39d3527217e7ab73cae2cb3694d241e18f53d84ea9a7"}, + {file = "greenlet-3.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0448abc479fab28b00cb472d278828b3ccca164531daab4e970a0458786055d6"}, + {file = "greenlet-3.0.3-cp37-cp37m-win32.whl", hash = "sha256:b542be2440edc2d48547b5923c408cbe0fc94afb9f18741faa6ae970dbcb9b6d"}, + {file = "greenlet-3.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:01bc7ea167cf943b4c802068e178bbf70ae2e8c080467070d01bfa02f337ee67"}, + {file = "greenlet-3.0.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1996cb9306c8595335bb157d133daf5cf9f693ef413e7673cb07e3e5871379ca"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc0f794e6ad661e321caa8d2f0a55ce01213c74722587256fb6566049a8b04"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9db1c18f0eaad2f804728c67d6c610778456e3e1cc4ab4bbd5eeb8e6053c6fc"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7170375bcc99f1a2fbd9c306f5be8764eaf3ac6b5cb968862cad4c7057756506"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b66c9c1e7ccabad3a7d037b2bcb740122a7b17a53734b7d72a344ce39882a1b"}, + {file = "greenlet-3.0.3-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:098d86f528c855ead3479afe84b49242e174ed262456c342d70fc7f972bc13c4"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:81bb9c6d52e8321f09c3d165b2a78c680506d9af285bfccbad9fb7ad5a5da3e5"}, + {file = "greenlet-3.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd096eb7ffef17c456cfa587523c5f92321ae02427ff955bebe9e3c63bc9f0da"}, + {file = "greenlet-3.0.3-cp38-cp38-win32.whl", hash = "sha256:d46677c85c5ba00a9cb6f7a00b2bfa6f812192d2c9f7d9c4f6a55b60216712f3"}, + {file = "greenlet-3.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:419b386f84949bf0e7c73e6032e3457b82a787c1ab4a0e43732898a761cc9dbf"}, + {file = "greenlet-3.0.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:da70d4d51c8b306bb7a031d5cff6cc25ad253affe89b70352af5f1cb68e74b53"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086152f8fbc5955df88382e8a75984e2bb1c892ad2e3c80a2508954e52295257"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d73a9fe764d77f87f8ec26a0c85144d6a951a6c438dfe50487df5595c6373eac"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7dcbe92cc99f08c8dd11f930de4d99ef756c3591a5377d1d9cd7dd5e896da71"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1551a8195c0d4a68fac7a4325efac0d541b48def35feb49d803674ac32582f61"}, + {file = "greenlet-3.0.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64d7675ad83578e3fc149b617a444fab8efdafc9385471f868eb5ff83e446b8b"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b37eef18ea55f2ffd8f00ff8fe7c8d3818abd3e25fb73fae2ca3b672e333a7a6"}, + {file = "greenlet-3.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:77457465d89b8263bca14759d7c1684df840b6811b2499838cc5b040a8b5b113"}, + {file = "greenlet-3.0.3-cp39-cp39-win32.whl", hash = "sha256:57e8974f23e47dac22b83436bdcf23080ade568ce77df33159e019d161ce1d1e"}, + {file = "greenlet-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:c5ee858cfe08f34712f548c3c363e807e7186f03ad7a5039ebadb29e8c6be067"}, + {file = "greenlet-3.0.3.tar.gz", hash = "sha256:43374442353259554ce33599da8b692d5aa96f8976d567d4badf263371fbe491"}, ] [package.extras] -docs = ["Sphinx"] +docs = ["Sphinx", "furo"] test = ["objgraph", "psutil"] [[package]] @@ -562,13 +572,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.1" +version = "1.0.2" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.1-py3-none-any.whl", hash = "sha256:c5e97ef177dca2023d0b9aad98e49507ef5423e9f1d94ffe2cfe250aa28e63b0"}, - {file = "httpcore-1.0.1.tar.gz", hash = "sha256:fce1ddf9b606cfb98132ab58865c3728c52c8e4c3c46e2aabb3674464a186e92"}, + {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, + {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, ] [package.dependencies] @@ -631,19 +641,19 @@ test = ["Cython (>=0.29.24,<0.30.0)"] [[package]] name = "httpx" -version = "0.25.1" +version = "0.26.0" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.25.1-py3-none-any.whl", hash = "sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a"}, - {file = "httpx-0.25.1.tar.gz", hash = "sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0"}, + {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"}, + {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"}, ] [package.dependencies] anyio = "*" certifi = "*" -httpcore = "*" +httpcore = "==1.*" idna = "*" sniffio = "*" @@ -655,13 +665,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "identify" -version = "2.5.31" +version = "2.5.33" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.31-py2.py3-none-any.whl", hash = "sha256:90199cb9e7bd3c5407a9b7e81b4abec4bb9d249991c79439ec8af740afc6293d"}, - {file = "identify-2.5.31.tar.gz", hash = "sha256:7736b3c7a28233637e3c36550646fc6389bedd74ae84cb788200cc8e2dd60b75"}, + {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, + {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, ] [package.extras] @@ -669,13 +679,13 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, ] [[package]] @@ -691,13 +701,13 @@ files = [ [[package]] name = "mako" -version = "1.2.4" +version = "1.3.0" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Mako-1.2.4-py3-none-any.whl", hash = "sha256:c97c79c018b9165ac9922ae4f32da095ffd3c4e6872b45eded42926deea46818"}, - {file = "Mako-1.2.4.tar.gz", hash = "sha256:d60a3903dc3bb01a18ad6a89cdbe2e4eadc69c0bc8ef1e3773ba53d44c3f7a34"}, + {file = "Mako-1.3.0-py3-none-any.whl", hash = "sha256:57d4e997349f1a92035aa25c17ace371a4213f2ca42f99bee9a602500cfd54d9"}, + {file = "Mako-1.3.0.tar.gz", hash = "sha256:e3a9d388fd00e87043edbe8792f45880ac0114e9c4adc69f6e9bfb2c55e3b11b"}, ] [package.dependencies] @@ -767,6 +777,52 @@ files = [ {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] +[[package]] +name = "mypy" +version = "1.8.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"}, + {file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"}, + {file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"}, + {file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"}, + {file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"}, + {file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"}, + {file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"}, + {file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"}, + {file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"}, + {file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"}, + {file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"}, + {file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"}, + {file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"}, + {file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"}, + {file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"}, + {file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"}, + {file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"}, + {file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"}, + {file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"}, + {file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"}, + {file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"}, + {file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"}, + {file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -803,46 +859,26 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] -[[package]] -name = "passlib" -version = "1.7.4" -description = "comprehensive password hashing framework supporting over 30 schemes" -optional = false -python-versions = "*" -files = [ - {file = "passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1"}, - {file = "passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04"}, -] - -[package.dependencies] -bcrypt = {version = ">=3.1.0", optional = true, markers = "extra == \"bcrypt\""} - -[package.extras] -argon2 = ["argon2-cffi (>=18.2.0)"] -bcrypt = ["bcrypt (>=3.1.0)"] -build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"] -totp = ["cryptography"] - [[package]] name = "pathspec" -version = "0.11.2" +version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, - {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] [[package]] name = "platformdirs" -version = "3.11.0" +version = "4.1.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, - {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, + {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, + {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, ] [package.extras] @@ -866,13 +902,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "3.5.0" +version = "3.6.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"}, - {file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"}, + {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, + {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, ] [package.dependencies] @@ -895,19 +931,19 @@ files = [ [[package]] name = "pydantic" -version = "2.4.2" +version = "2.5.3" description = "Data validation using Python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-2.4.2-py3-none-any.whl", hash = "sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"}, - {file = "pydantic-2.4.2.tar.gz", hash = "sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7"}, + {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"}, + {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"}, ] [package.dependencies] annotated-types = ">=0.4.0" email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} -pydantic-core = "2.10.1" +pydantic-core = "2.14.6" typing-extensions = ">=4.6.1" [package.extras] @@ -915,117 +951,116 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.10.1" +version = "2.14.6" description = "" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic_core-2.10.1-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63"}, - {file = "pydantic_core-2.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e"}, - {file = "pydantic_core-2.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e"}, - {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e"}, - {file = "pydantic_core-2.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6"}, - {file = "pydantic_core-2.10.1-cp310-none-win32.whl", hash = "sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b"}, - {file = "pydantic_core-2.10.1-cp310-none-win_amd64.whl", hash = "sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0"}, - {file = "pydantic_core-2.10.1-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea"}, - {file = "pydantic_core-2.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8"}, - {file = "pydantic_core-2.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4"}, - {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607"}, - {file = "pydantic_core-2.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f"}, - {file = "pydantic_core-2.10.1-cp311-none-win32.whl", hash = "sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6"}, - {file = "pydantic_core-2.10.1-cp311-none-win_amd64.whl", hash = "sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27"}, - {file = "pydantic_core-2.10.1-cp311-none-win_arm64.whl", hash = "sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325"}, - {file = "pydantic_core-2.10.1-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921"}, - {file = "pydantic_core-2.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901"}, - {file = "pydantic_core-2.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d"}, - {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f"}, - {file = "pydantic_core-2.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c"}, - {file = "pydantic_core-2.10.1-cp312-none-win32.whl", hash = "sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f"}, - {file = "pydantic_core-2.10.1-cp312-none-win_amd64.whl", hash = "sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430"}, - {file = "pydantic_core-2.10.1-cp312-none-win_arm64.whl", hash = "sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15"}, - {file = "pydantic_core-2.10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f"}, - {file = "pydantic_core-2.10.1-cp37-none-win32.whl", hash = "sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c"}, - {file = "pydantic_core-2.10.1-cp37-none-win_amd64.whl", hash = "sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e"}, - {file = "pydantic_core-2.10.1-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc"}, - {file = "pydantic_core-2.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302"}, - {file = "pydantic_core-2.10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e"}, - {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561"}, - {file = "pydantic_core-2.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de"}, - {file = "pydantic_core-2.10.1-cp38-none-win32.whl", hash = "sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee"}, - {file = "pydantic_core-2.10.1-cp38-none-win_amd64.whl", hash = "sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e"}, - {file = "pydantic_core-2.10.1-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970"}, - {file = "pydantic_core-2.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a"}, - {file = "pydantic_core-2.10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429"}, - {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7"}, - {file = "pydantic_core-2.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595"}, - {file = "pydantic_core-2.10.1-cp39-none-win32.whl", hash = "sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a"}, - {file = "pydantic_core-2.10.1-cp39-none-win_amd64.whl", hash = "sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357"}, - {file = "pydantic_core-2.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2"}, - {file = "pydantic_core-2.10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132"}, - {file = "pydantic_core-2.10.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7"}, - {file = "pydantic_core-2.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776"}, - {file = "pydantic_core-2.10.1.tar.gz", hash = "sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82"}, + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"}, + {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"}, + {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"}, + {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"}, + {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"}, + {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"}, + {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"}, + {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"}, + {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"}, + {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"}, + {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"}, + {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"}, + {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"}, + {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"}, + {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"}, + {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"}, + {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"}, + {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"}, + {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"}, + {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"}, + {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"}, + {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"}, + {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"}, + {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"}, + {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"}, + {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"}, + {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"}, + {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"}, + {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"}, + {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"}, + {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"}, + {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"}, + {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"}, + {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"}, + {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"}, ] [package.dependencies] @@ -1033,17 +1068,17 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-settings" -version = "2.0.3" +version = "2.1.0" description = "Settings management using Pydantic" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic_settings-2.0.3-py3-none-any.whl", hash = "sha256:ddd907b066622bd67603b75e2ff791875540dc485b7307c4fffc015719da8625"}, - {file = "pydantic_settings-2.0.3.tar.gz", hash = "sha256:962dc3672495aad6ae96a4390fac7e593591e144625e5112d359f8f67fb75945"}, + {file = "pydantic_settings-2.1.0-py3-none-any.whl", hash = "sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a"}, + {file = "pydantic_settings-2.1.0.tar.gz", hash = "sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c"}, ] [package.dependencies] -pydantic = ">=2.0.1" +pydantic = ">=2.3.0" python-dotenv = ">=0.21.0" [[package]] @@ -1068,13 +1103,13 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pytest" -version = "7.4.3" +version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -1104,6 +1139,41 @@ pytest = ">=7.0.0" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "pytest-env" +version = "1.1.3" +description = "pytest plugin that allows you to add environment variables." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_env-1.1.3-py3-none-any.whl", hash = "sha256:aada77e6d09fcfb04540a6e462c58533c37df35fa853da78707b17ec04d17dfc"}, + {file = "pytest_env-1.1.3.tar.gz", hash = "sha256:fcd7dc23bb71efd3d35632bde1bbe5ee8c8dc4489d6617fb010674880d96216b"}, +] + +[package.dependencies] +pytest = ">=7.4.3" + +[package.extras] +test = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "pytest-mock (>=3.12)"] + [[package]] name = "python-dotenv" version = "1.0.0" @@ -1183,43 +1253,43 @@ files = [ [[package]] name = "ruff" -version = "0.1.4" +version = "0.1.13" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.4-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:864958706b669cce31d629902175138ad8a069d99ca53514611521f532d91495"}, - {file = "ruff-0.1.4-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:9fdd61883bb34317c788af87f4cd75dfee3a73f5ded714b77ba928e418d6e39e"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4eaca8c9cc39aa7f0f0d7b8fe24ecb51232d1bb620fc4441a61161be4a17539"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a9a1301dc43cbf633fb603242bccd0aaa34834750a14a4c1817e2e5c8d60de17"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78e8db8ab6f100f02e28b3d713270c857d370b8d61871d5c7d1702ae411df683"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:80fea754eaae06335784b8ea053d6eb8e9aac75359ebddd6fee0858e87c8d510"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6bc02a480d4bfffd163a723698da15d1a9aec2fced4c06f2a753f87f4ce6969c"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862811b403063765b03e716dac0fda8fdbe78b675cd947ed5873506448acea4"}, - {file = "ruff-0.1.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58826efb8b3efbb59bb306f4b19640b7e366967a31c049d49311d9eb3a4c60cb"}, - {file = "ruff-0.1.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fdfd453fc91d9d86d6aaa33b1bafa69d114cf7421057868f0b79104079d3e66e"}, - {file = "ruff-0.1.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e8791482d508bd0b36c76481ad3117987301b86072158bdb69d796503e1c84a8"}, - {file = "ruff-0.1.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01206e361021426e3c1b7fba06ddcb20dbc5037d64f6841e5f2b21084dc51800"}, - {file = "ruff-0.1.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:645591a613a42cb7e5c2b667cbefd3877b21e0252b59272ba7212c3d35a5819f"}, - {file = "ruff-0.1.4-py3-none-win32.whl", hash = "sha256:99908ca2b3b85bffe7e1414275d004917d1e0dfc99d497ccd2ecd19ad115fd0d"}, - {file = "ruff-0.1.4-py3-none-win_amd64.whl", hash = "sha256:1dfd6bf8f6ad0a4ac99333f437e0ec168989adc5d837ecd38ddb2cc4a2e3db8a"}, - {file = "ruff-0.1.4-py3-none-win_arm64.whl", hash = "sha256:d98ae9ebf56444e18a3e3652b3383204748f73e247dea6caaf8b52d37e6b32da"}, - {file = "ruff-0.1.4.tar.gz", hash = "sha256:21520ecca4cc555162068d87c747b8f95e1e95f8ecfcbbe59e8dd00710586315"}, + {file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e3fd36e0d48aeac672aa850045e784673449ce619afc12823ea7868fcc41d8ba"}, + {file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9fb6b3b86450d4ec6a6732f9f60c4406061b6851c4b29f944f8c9d91c3611c7a"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b13ba5d7156daaf3fd08b6b993360a96060500aca7e307d95ecbc5bb47a69296"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ebb40442f7b531e136d334ef0851412410061e65d61ca8ce90d894a094feb22"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226b517f42d59a543d6383cfe03cccf0091e3e0ed1b856c6824be03d2a75d3b6"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f0312ba1061e9b8c724e9a702d3c8621e3c6e6c2c9bd862550ab2951ac75c16"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f59bcf5217c661254bd6bc42d65a6fd1a8b80c48763cb5c2293295babd945dd"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6894b00495e00c27b6ba61af1fc666f17de6140345e5ef27dd6e08fb987259d"}, + {file = "ruff-0.1.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1600942485c6e66119da294c6294856b5c86fd6df591ce293e4a4cc8e72989"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ee3febce7863e231a467f90e681d3d89210b900d49ce88723ce052c8761be8c7"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dcaab50e278ff497ee4d1fe69b29ca0a9a47cd954bb17963628fa417933c6eb1"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f57de973de4edef3ad3044d6a50c02ad9fc2dff0d88587f25f1a48e3f72edf5e"}, + {file = "ruff-0.1.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a36fa90eb12208272a858475ec43ac811ac37e91ef868759770b71bdabe27b6"}, + {file = "ruff-0.1.13-py3-none-win32.whl", hash = "sha256:a623349a505ff768dad6bd57087e2461be8db58305ebd5577bd0e98631f9ae69"}, + {file = "ruff-0.1.13-py3-none-win_amd64.whl", hash = "sha256:f988746e3c3982bea7f824c8fa318ce7f538c4dfefec99cd09c8770bd33e6539"}, + {file = "ruff-0.1.13-py3-none-win_arm64.whl", hash = "sha256:6bbbc3042075871ec17f28864808540a26f0f79a4478c357d3e3d2284e832998"}, + {file = "ruff-0.1.13.tar.gz", hash = "sha256:e261f1baed6291f434ffb1d5c6bd8051d1c2a26958072d38dfbec39b3dda7352"}, ] [[package]] name = "setuptools" -version = "68.2.2" +version = "69.0.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, - {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, + {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, + {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +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"] 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)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] @@ -1236,70 +1306,70 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.23" +version = "2.0.25" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-win32.whl", hash = "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8"}, - {file = "SQLAlchemy-2.0.23-cp310-cp310-win_amd64.whl", hash = "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-win32.whl", hash = "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855"}, - {file = "SQLAlchemy-2.0.23-cp311-cp311-win_amd64.whl", hash = "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-win32.whl", hash = "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846"}, - {file = "SQLAlchemy-2.0.23-cp312-cp312-win_amd64.whl", hash = "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-win32.whl", hash = "sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18"}, - {file = "SQLAlchemy-2.0.23-cp37-cp37m-win_amd64.whl", hash = "sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-win32.whl", hash = "sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306"}, - {file = "SQLAlchemy-2.0.23-cp38-cp38-win_amd64.whl", hash = "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-win32.whl", hash = "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884"}, - {file = "SQLAlchemy-2.0.23-cp39-cp39-win_amd64.whl", hash = "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b"}, - {file = "SQLAlchemy-2.0.23-py3-none-any.whl", hash = "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d"}, - {file = "SQLAlchemy-2.0.23.tar.gz", hash = "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4344d059265cc8b1b1be351bfb88749294b87a8b2bbe21dfbe066c4199541ebd"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9e2e59cbcc6ba1488404aad43de005d05ca56e069477b33ff74e91b6319735"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84daa0a2055df9ca0f148a64fdde12ac635e30edbca80e87df9b3aaf419e144a"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc8b7dabe8e67c4832891a5d322cec6d44ef02f432b4588390017f5cec186a84"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5693145220517b5f42393e07a6898acdfe820e136c98663b971906120549da5"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db854730a25db7c956423bb9fb4bdd1216c839a689bf9cc15fada0a7fb2f4570"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-win32.whl", hash = "sha256:14a6f68e8fc96e5e8f5647ef6cda6250c780612a573d99e4d881581432ef1669"}, + {file = "SQLAlchemy-2.0.25-cp310-cp310-win_amd64.whl", hash = "sha256:87f6e732bccd7dcf1741c00f1ecf33797383128bd1c90144ac8adc02cbb98643"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:342d365988ba88ada8af320d43df4e0b13a694dbd75951f537b2d5e4cb5cd002"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f37c0caf14b9e9b9e8f6dbc81bc56db06acb4363eba5a633167781a48ef036ed"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa9373708763ef46782d10e950b49d0235bfe58facebd76917d3f5cbf5971aed"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24f571990c05f6b36a396218f251f3e0dda916e0c687ef6fdca5072743208f5"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75432b5b14dc2fff43c50435e248b45c7cdadef73388e5610852b95280ffd0e9"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:884272dcd3ad97f47702965a0e902b540541890f468d24bd1d98bcfe41c3f018"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-win32.whl", hash = "sha256:e607cdd99cbf9bb80391f54446b86e16eea6ad309361942bf88318bcd452363c"}, + {file = "SQLAlchemy-2.0.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d505815ac340568fd03f719446a589162d55c52f08abd77ba8964fbb7eb5b5f"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0dacf67aee53b16f365c589ce72e766efaabd2b145f9de7c917777b575e3659d"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b801154027107461ee992ff4b5c09aa7cc6ec91ddfe50d02bca344918c3265c6"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a21853f5daeb50412d459cfb13cb82c089ad4c04ec208cd14dddd99fc23b39"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29049e2c299b5ace92cbed0c1610a7a236f3baf4c6b66eb9547c01179f638ec5"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b64b183d610b424a160b0d4d880995e935208fc043d0302dd29fee32d1ee3f95"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f7a7d7fcc675d3d85fbf3b3828ecd5990b8d61bd6de3f1b260080b3beccf215"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-win32.whl", hash = "sha256:cf18ff7fc9941b8fc23437cc3e68ed4ebeff3599eec6ef5eebf305f3d2e9a7c2"}, + {file = "SQLAlchemy-2.0.25-cp312-cp312-win_amd64.whl", hash = "sha256:91f7d9d1c4dd1f4f6e092874c128c11165eafcf7c963128f79e28f8445de82d5"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bb209a73b8307f8fe4fe46f6ad5979649be01607f11af1eb94aa9e8a3aaf77f0"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:798f717ae7c806d67145f6ae94dc7c342d3222d3b9a311a784f371a4333212c7"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd402169aa00df3142149940b3bf9ce7dde075928c1886d9a1df63d4b8de62"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0d3cab3076af2e4aa5693f89622bef7fa770c6fec967143e4da7508b3dceb9b9"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:74b080c897563f81062b74e44f5a72fa44c2b373741a9ade701d5f789a10ba23"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-win32.whl", hash = "sha256:87d91043ea0dc65ee583026cb18e1b458d8ec5fc0a93637126b5fc0bc3ea68c4"}, + {file = "SQLAlchemy-2.0.25-cp37-cp37m-win_amd64.whl", hash = "sha256:75f99202324383d613ddd1f7455ac908dca9c2dd729ec8584c9541dd41822a2c"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:420362338681eec03f53467804541a854617faed7272fe71a1bfdb07336a381e"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c88f0c7dcc5f99bdb34b4fd9b69b93c89f893f454f40219fe923a3a2fd11625"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3be4987e3ee9d9a380b66393b77a4cd6d742480c951a1c56a23c335caca4ce3"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a159111a0f58fb034c93eeba211b4141137ec4b0a6e75789ab7a3ef3c7e7e3"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8b8cb63d3ea63b29074dcd29da4dc6a97ad1349151f2d2949495418fd6e48db9"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:736ea78cd06de6c21ecba7416499e7236a22374561493b456a1f7ffbe3f6cdb4"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-win32.whl", hash = "sha256:10331f129982a19df4284ceac6fe87353ca3ca6b4ca77ff7d697209ae0a5915e"}, + {file = "SQLAlchemy-2.0.25-cp38-cp38-win_amd64.whl", hash = "sha256:c55731c116806836a5d678a70c84cb13f2cedba920212ba7dcad53260997666d"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:605b6b059f4b57b277f75ace81cc5bc6335efcbcc4ccb9066695e515dbdb3900"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:665f0a3954635b5b777a55111ababf44b4fc12b1f3ba0a435b602b6387ffd7cf"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecf6d4cda1f9f6cb0b45803a01ea7f034e2f1aed9475e883410812d9f9e3cfcf"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51db269513917394faec5e5c00d6f83829742ba62e2ac4fa5c98d58be91662f"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:790f533fa5c8901a62b6fef5811d48980adeb2f51f1290ade8b5e7ba990ba3de"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1b1180cda6df7af84fe72e4530f192231b1f29a7496951db4ff38dac1687202d"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-win32.whl", hash = "sha256:555651adbb503ac7f4cb35834c5e4ae0819aab2cd24857a123370764dc7d7e24"}, + {file = "SQLAlchemy-2.0.25-cp39-cp39-win_amd64.whl", hash = "sha256:dc55990143cbd853a5d038c05e79284baedf3e299661389654551bd02a6a68d7"}, + {file = "SQLAlchemy-2.0.25-py3-none-any.whl", hash = "sha256:a86b4240e67d4753dc3092d9511886795b3c2852abe599cffe108952f7af7ac3"}, + {file = "SQLAlchemy-2.0.25.tar.gz", hash = "sha256:a2c69a7664fb2d54b8682dd774c3b54f67f84fa123cf84dda2a5f40dcaa04e08"}, ] [package.dependencies] 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\""} -typing-extensions = ">=4.2.0" +typing-extensions = ">=4.6.0" [package.extras] aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] aioodbc = ["aioodbc", "greenlet (!=0.4.17)"] -aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing-extensions (!=3.10.0.1)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] asyncmy = ["asyncmy (>=0.2.3,!=0.2.4,!=0.2.6)", "greenlet (!=0.4.17)"] mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2,!=1.1.5)"] @@ -1309,7 +1379,7 @@ mssql-pyodbc = ["pyodbc"] mypy = ["mypy (>=0.910)"] mysql = ["mysqlclient (>=1.4.0)"] mysql-connector = ["mysql-connector-python"] -oracle = ["cx-oracle (>=8)"] +oracle = ["cx_oracle (>=8)"] oracle-oracledb = ["oracledb (>=1.0.1)"] postgresql = ["psycopg2 (>=2.7)"] postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] @@ -1319,17 +1389,17 @@ postgresql-psycopg2binary = ["psycopg2-binary"] postgresql-psycopg2cffi = ["psycopg2cffi"] postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] -sqlcipher = ["sqlcipher3-binary"] +sqlcipher = ["sqlcipher3_binary"] [[package]] name = "starlette" -version = "0.27.0" +version = "0.35.1" description = "The little ASGI library that shines." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "starlette-0.27.0-py3-none-any.whl", hash = "sha256:918416370e846586541235ccd38a474c08b80443ed31c578a418e2209b3eef91"}, - {file = "starlette-0.27.0.tar.gz", hash = "sha256:6a6b0d042acb8d469a01eba54e9cda6cbd24ac602c4cd016723117d6a7e73b75"}, + {file = "starlette-0.35.1-py3-none-any.whl", hash = "sha256:50bbbda9baa098e361f398fda0928062abbaf1f54f4fadcbe17c092a01eb9a25"}, + {file = "starlette-0.35.1.tar.gz", hash = "sha256:3e2639dac3520e4f58734ed22553f950d3f3cb1001cd2eaac4d57e8cdc5f66bc"}, ] [package.dependencies] @@ -1338,26 +1408,37 @@ anyio = ">=3.4.0,<5" [package.extras] full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +[[package]] +name = "types-passlib" +version = "1.7.7.20240106" +description = "Typing stubs for passlib" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-passlib-1.7.7.20240106.tar.gz", hash = "sha256:2231ae83d1dd9e485b7ec6041d81b4f9c66403d1767360e860605a90db48ea27"}, + {file = "types_passlib-1.7.7.20240106-py3-none-any.whl", hash = "sha256:347aa64d4c2bc239f3765fe38fc79dad3d67f9def7b3ea721daaaaa835a91dad"}, +] + [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.9.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] [[package]] name = "uvicorn" -version = "0.24.0" +version = "0.26.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.24.0-py3-none-any.whl", hash = "sha256:3d19f13dfd2c2af1bfe34dd0f7155118ce689425fdf931177abe832ca44b8a04"}, - {file = "uvicorn-0.24.0.tar.gz", hash = "sha256:368d5d81520a51be96431845169c225d771c9dd22a58613e1a181e6c4512ac33"}, + {file = "uvicorn-0.26.0-py3-none-any.whl", hash = "sha256:cdb58ef6b8188c6c174994b2b1ba2150a9a8ae7ea5fb2f1b856b94a815d6071d"}, + {file = "uvicorn-0.26.0.tar.gz", hash = "sha256:48bfd350fce3c5c57af5fb4995fded8fb50da3b4feb543eb18ad7e0d54589602"}, ] [package.dependencies] @@ -1420,19 +1501,19 @@ test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)" [[package]] name = "virtualenv" -version = "20.24.6" +version = "20.25.0" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.24.6-py3-none-any.whl", hash = "sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381"}, - {file = "virtualenv-20.24.6.tar.gz", hash = "sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af"}, + {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, + {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<4" +platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] @@ -1609,4 +1690,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "f1e3649ddc3c8aa560343011f41d356a1f9cf074f6e73c02bec8e94969e992fc" +content-hash = "8394909ca43af6556bf3aaed1f188595b9b21fb26144bba4cae60a0780ee2bc1" diff --git a/{{cookiecutter.project_name}}/pyproject.toml b/{{cookiecutter.project_name}}/pyproject.toml index e50b773..7cc6f2c 100644 --- a/{{cookiecutter.project_name}}/pyproject.toml +++ b/{{cookiecutter.project_name}}/pyproject.toml @@ -9,23 +9,28 @@ python = "^3.12" alembic = "^1.12.1" asyncpg = "^0.29.0" -fastapi = "^0.104.1" -passlib = { extras = ["bcrypt"], version = "^1.7.4" } -pydantic = { extras = ["dotenv", "email"], version = "^2.4.2" } -pydantic-settings = "^2.0.3" +fastapi = "^0.109.0" +bcrypt = "^4.1.2" +pydantic = { extras = ["dotenv", "email"], version = "^2.5.3" } +pydantic-settings = "^2.1.0" pyjwt = { extras = ["crypto"], version = "^2.8.0" } python-multipart = "^0.0.6" sqlalchemy = "^2.0.23" + [tool.poetry.group.dev.dependencies] black = "^23.10.1" coverage = "^7.3.2" -httpx = "^0.25.1" +httpx = "^0.26.0" pre-commit = "^3.5.0" pytest = "^7.4.3" -pytest-asyncio = "^0.21.1" +pytest-asyncio = "0.21.1" ruff = "^0.1.4" -uvicorn = { extras = ["standard"], version = "^0.24.0" } +uvicorn = { extras = ["standard"], version = "^0.26.0" } +mypy = "^1.8.0" +pytest-cov = "^4.1.0" +pytest-env = "^1.1.3" +types-passlib = "^1.7.7.20240106" [build-system] @@ -33,16 +38,20 @@ build-backend = "poetry.core.masonry.api" requires = ["poetry-core>=1.0.0"] [tool.pytest.ini_options] -addopts = "-v" +addopts = "-v --cov --cov-report xml --cov-report term-missing" asyncio_mode = "auto" -filterwarnings = [ - # Passlib 1.7.4 depends on crypt - https://foss.heptapod.net/python-libs/passlib/-/issues/148 - "ignore:'crypt' is deprecated and slated for removal", -] -markers = ["pytest.mark.asyncio"] +env = ["ENVIRONMENT=PYTEST"] minversion = "6.0" testpaths = ["app/tests"] +[tool.coverage.run] +omit = ["app/tests/*"] +source = ["app"] + +[tool.mypy] +python_version = "3.12" +strict = true + [tool.ruff] target-version = "py312" # pycodestyle, pyflakes, isort, pylint, pyupgrade From 4d5d65693a2fb79e8932198294a9e7192d0f264b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Fri, 19 Jan 2024 02:43:32 +0100 Subject: [PATCH 02/41] Move project to main dir, remove cookiecutter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- .../.env.example => .env.example | 0 .github/workflows/build_docker_image.yml | 42 ------ .../workflows/manual_build_docker_image.yml | 9 +- .gitignore | 134 ++++++++++++++++- ...mit-config.yaml => .pre-commit-config.yaml | 0 .../Dockerfile => Dockerfile | 0 .../alembic.ini => alembic.ini | 0 .../alembic => alembic}/README | 0 .../alembic => alembic}/env.py | 0 .../alembic => alembic}/script.py.mako | 0 ...nit_user_and_refresh_token_21f30f70dab2.py | 0 {tests => app}/__init__.py | 0 .../app => app/api}/__init__.py | 0 .../app/api/api.py => app/api/api_router.py | 0 .../app => app}/api/deps.py | 0 .../app/api => app/api/endpoints}/__init__.py | 0 .../app => app}/api/endpoints/auth.py | 0 .../app => app}/api/endpoints/users.py | 0 .../api/endpoints => app/core}/__init__.py | 0 .../app => app}/core/config.py | 0 .../core => app/core/security}/__init__.py | 0 .../app => app}/core/security/jwt.py | 0 .../app => app}/core/security/password.py | 0 .../app => app}/core/session.py | 0 .../app => app}/main.py | 2 +- .../app => app}/models.py | 0 .../core/security => app/schemas}/__init__.py | 0 .../app => app}/schemas/requests.py | 0 .../app => app}/schemas/responses.py | 0 .../app/schemas => app/tests}/__init__.py | 0 .../app => app}/tests/conftest.py | 0 .../app => app}/tests/test_auth.py | 0 .../app => app}/tests/test_users.py | 0 cookiecutter.json | 3 - .../docker-compose.yml => docker-compose.yml | 0 ...template-fastapi-users-openapi-example.png | Bin 160836 -> 0 bytes hooks/post_gen_project.py | 19 --- .../init.sh => init.sh | 0 .../poetry.lock => poetry.lock | 0 .../pyproject.toml => pyproject.toml | 0 setup.cfg | 13 -- tests/create_minimal_project.py | 23 --- {{cookiecutter.project_name}}/.env.template | 21 --- {{cookiecutter.project_name}}/.gitignore | 137 ------------------ .../app/tests/__init__.py | 0 .../docker-compose.dev.yml | 36 ----- .../requirements-dev.txt | 58 -------- .../requirements.txt | 27 ---- 48 files changed, 132 insertions(+), 392 deletions(-) rename {{cookiecutter.project_name}}/.env.example => .env.example (100%) delete mode 100644 .github/workflows/build_docker_image.yml rename {{cookiecutter.project_name}}/.pre-commit-config.yaml => .pre-commit-config.yaml (100%) rename {{cookiecutter.project_name}}/Dockerfile => Dockerfile (100%) rename {{cookiecutter.project_name}}/alembic.ini => alembic.ini (100%) rename {{{cookiecutter.project_name}}/alembic => alembic}/README (100%) rename {{{cookiecutter.project_name}}/alembic => alembic}/env.py (100%) rename {{{cookiecutter.project_name}}/alembic => alembic}/script.py.mako (100%) rename {{{cookiecutter.project_name}}/alembic => alembic}/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py (100%) rename {tests => app}/__init__.py (100%) rename {{{cookiecutter.project_name}}/app => app/api}/__init__.py (100%) rename {{cookiecutter.project_name}}/app/api/api.py => app/api/api_router.py (100%) rename {{{cookiecutter.project_name}}/app => app}/api/deps.py (100%) rename {{{cookiecutter.project_name}}/app/api => app/api/endpoints}/__init__.py (100%) rename {{{cookiecutter.project_name}}/app => app}/api/endpoints/auth.py (100%) rename {{{cookiecutter.project_name}}/app => app}/api/endpoints/users.py (100%) rename {{{cookiecutter.project_name}}/app/api/endpoints => app/core}/__init__.py (100%) rename {{{cookiecutter.project_name}}/app => app}/core/config.py (100%) rename {{{cookiecutter.project_name}}/app/core => app/core/security}/__init__.py (100%) rename {{{cookiecutter.project_name}}/app => app}/core/security/jwt.py (100%) rename {{{cookiecutter.project_name}}/app => app}/core/security/password.py (100%) rename {{{cookiecutter.project_name}}/app => app}/core/session.py (100%) rename {{{cookiecutter.project_name}}/app => app}/main.py (95%) rename {{{cookiecutter.project_name}}/app => app}/models.py (100%) rename {{{cookiecutter.project_name}}/app/core/security => app/schemas}/__init__.py (100%) rename {{{cookiecutter.project_name}}/app => app}/schemas/requests.py (100%) rename {{{cookiecutter.project_name}}/app => app}/schemas/responses.py (100%) rename {{{cookiecutter.project_name}}/app/schemas => app/tests}/__init__.py (100%) rename {{{cookiecutter.project_name}}/app => app}/tests/conftest.py (100%) rename {{{cookiecutter.project_name}}/app => app}/tests/test_auth.py (100%) rename {{{cookiecutter.project_name}}/app => app}/tests/test_users.py (100%) delete mode 100644 cookiecutter.json rename {{cookiecutter.project_name}}/docker-compose.yml => docker-compose.yml (100%) delete mode 100644 docs/template-fastapi-users-openapi-example.png delete mode 100644 hooks/post_gen_project.py rename {{cookiecutter.project_name}}/init.sh => init.sh (100%) rename {{cookiecutter.project_name}}/poetry.lock => poetry.lock (100%) rename {{cookiecutter.project_name}}/pyproject.toml => pyproject.toml (100%) delete mode 100644 setup.cfg delete mode 100644 tests/create_minimal_project.py delete mode 100644 {{cookiecutter.project_name}}/.env.template delete mode 100644 {{cookiecutter.project_name}}/.gitignore delete mode 100644 {{cookiecutter.project_name}}/app/tests/__init__.py delete mode 100644 {{cookiecutter.project_name}}/docker-compose.dev.yml delete mode 100644 {{cookiecutter.project_name}}/requirements-dev.txt delete mode 100644 {{cookiecutter.project_name}}/requirements.txt diff --git a/{{cookiecutter.project_name}}/.env.example b/.env.example similarity index 100% rename from {{cookiecutter.project_name}}/.env.example rename to .env.example diff --git a/.github/workflows/build_docker_image.yml b/.github/workflows/build_docker_image.yml deleted file mode 100644 index f840b0f..0000000 --- a/.github/workflows/build_docker_image.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Push minimal template docker image to dockerhub - -on: - workflow_run: - workflows: - - "Run tests" - branches: - - main - types: - - completed - -jobs: - push_image_to_dockerhub: - if: ${{ github.event.workflow_run.conclusion == 'success' }} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: "3.12" - - - name: Generate projects from templates using cookiecutter - # minimal_project folder - run: | - pip install cookiecutter - python tests/create_minimal_project.py - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_USER }} - password: ${{ secrets.DOCKER_PASS }} - - - name: Build and push image - uses: docker/build-push-action@v3 - with: - file: minimal_project/Dockerfile - context: minimal_project - push: true - tags: rafsaf/minimal-fastapi-postgres-template:latest diff --git a/.github/workflows/manual_build_docker_image.yml b/.github/workflows/manual_build_docker_image.yml index 1ca1a1c..2cc3102 100644 --- a/.github/workflows/manual_build_docker_image.yml +++ b/.github/workflows/manual_build_docker_image.yml @@ -19,12 +19,6 @@ jobs: with: python-version: "3.12" - - name: Generate projects from templates using cookiecutter - # minimal_project folder - run: | - pip install cookiecutter - python tests/create_minimal_project.py - - name: Login to DockerHub uses: docker/login-action@v1 with: @@ -34,7 +28,6 @@ jobs: - name: Build and push image uses: docker/build-push-action@v3 with: - file: minimal_project/Dockerfile - context: minimal_project + file: Dockerfile push: true tags: rafsaf/minimal-fastapi-postgres-template:${{ github.event.inputs.tag }} diff --git a/.gitignore b/.gitignore index 562d81d..9d9a06e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,131 @@ -.vscode +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +.env + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +log.txt + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments + .venv -venv -.venv1 -.venv2 \ No newline at end of file +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ \ No newline at end of file diff --git a/{{cookiecutter.project_name}}/.pre-commit-config.yaml b/.pre-commit-config.yaml similarity index 100% rename from {{cookiecutter.project_name}}/.pre-commit-config.yaml rename to .pre-commit-config.yaml diff --git a/{{cookiecutter.project_name}}/Dockerfile b/Dockerfile similarity index 100% rename from {{cookiecutter.project_name}}/Dockerfile rename to Dockerfile diff --git a/{{cookiecutter.project_name}}/alembic.ini b/alembic.ini similarity index 100% rename from {{cookiecutter.project_name}}/alembic.ini rename to alembic.ini diff --git a/{{cookiecutter.project_name}}/alembic/README b/alembic/README similarity index 100% rename from {{cookiecutter.project_name}}/alembic/README rename to alembic/README diff --git a/{{cookiecutter.project_name}}/alembic/env.py b/alembic/env.py similarity index 100% rename from {{cookiecutter.project_name}}/alembic/env.py rename to alembic/env.py diff --git a/{{cookiecutter.project_name}}/alembic/script.py.mako b/alembic/script.py.mako similarity index 100% rename from {{cookiecutter.project_name}}/alembic/script.py.mako rename to alembic/script.py.mako diff --git a/{{cookiecutter.project_name}}/alembic/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py b/alembic/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py similarity index 100% rename from {{cookiecutter.project_name}}/alembic/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py rename to alembic/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py diff --git a/tests/__init__.py b/app/__init__.py similarity index 100% rename from tests/__init__.py rename to app/__init__.py diff --git a/{{cookiecutter.project_name}}/app/__init__.py b/app/api/__init__.py similarity index 100% rename from {{cookiecutter.project_name}}/app/__init__.py rename to app/api/__init__.py diff --git a/{{cookiecutter.project_name}}/app/api/api.py b/app/api/api_router.py similarity index 100% rename from {{cookiecutter.project_name}}/app/api/api.py rename to app/api/api_router.py diff --git a/{{cookiecutter.project_name}}/app/api/deps.py b/app/api/deps.py similarity index 100% rename from {{cookiecutter.project_name}}/app/api/deps.py rename to app/api/deps.py diff --git a/{{cookiecutter.project_name}}/app/api/__init__.py b/app/api/endpoints/__init__.py similarity index 100% rename from {{cookiecutter.project_name}}/app/api/__init__.py rename to app/api/endpoints/__init__.py diff --git a/{{cookiecutter.project_name}}/app/api/endpoints/auth.py b/app/api/endpoints/auth.py similarity index 100% rename from {{cookiecutter.project_name}}/app/api/endpoints/auth.py rename to app/api/endpoints/auth.py diff --git a/{{cookiecutter.project_name}}/app/api/endpoints/users.py b/app/api/endpoints/users.py similarity index 100% rename from {{cookiecutter.project_name}}/app/api/endpoints/users.py rename to app/api/endpoints/users.py diff --git a/{{cookiecutter.project_name}}/app/api/endpoints/__init__.py b/app/core/__init__.py similarity index 100% rename from {{cookiecutter.project_name}}/app/api/endpoints/__init__.py rename to app/core/__init__.py diff --git a/{{cookiecutter.project_name}}/app/core/config.py b/app/core/config.py similarity index 100% rename from {{cookiecutter.project_name}}/app/core/config.py rename to app/core/config.py diff --git a/{{cookiecutter.project_name}}/app/core/__init__.py b/app/core/security/__init__.py similarity index 100% rename from {{cookiecutter.project_name}}/app/core/__init__.py rename to app/core/security/__init__.py diff --git a/{{cookiecutter.project_name}}/app/core/security/jwt.py b/app/core/security/jwt.py similarity index 100% rename from {{cookiecutter.project_name}}/app/core/security/jwt.py rename to app/core/security/jwt.py diff --git a/{{cookiecutter.project_name}}/app/core/security/password.py b/app/core/security/password.py similarity index 100% rename from {{cookiecutter.project_name}}/app/core/security/password.py rename to app/core/security/password.py diff --git a/{{cookiecutter.project_name}}/app/core/session.py b/app/core/session.py similarity index 100% rename from {{cookiecutter.project_name}}/app/core/session.py rename to app/core/session.py diff --git a/{{cookiecutter.project_name}}/app/main.py b/app/main.py similarity index 95% rename from {{cookiecutter.project_name}}/app/main.py rename to app/main.py index 2a7217d..de3e87c 100644 --- a/{{cookiecutter.project_name}}/app/main.py +++ b/app/main.py @@ -4,7 +4,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.trustedhost import TrustedHostMiddleware -from app.api.api import api_router +from app.api.api_router import api_router from app.core.config import get_settings app = FastAPI( diff --git a/{{cookiecutter.project_name}}/app/models.py b/app/models.py similarity index 100% rename from {{cookiecutter.project_name}}/app/models.py rename to app/models.py diff --git a/{{cookiecutter.project_name}}/app/core/security/__init__.py b/app/schemas/__init__.py similarity index 100% rename from {{cookiecutter.project_name}}/app/core/security/__init__.py rename to app/schemas/__init__.py diff --git a/{{cookiecutter.project_name}}/app/schemas/requests.py b/app/schemas/requests.py similarity index 100% rename from {{cookiecutter.project_name}}/app/schemas/requests.py rename to app/schemas/requests.py diff --git a/{{cookiecutter.project_name}}/app/schemas/responses.py b/app/schemas/responses.py similarity index 100% rename from {{cookiecutter.project_name}}/app/schemas/responses.py rename to app/schemas/responses.py diff --git a/{{cookiecutter.project_name}}/app/schemas/__init__.py b/app/tests/__init__.py similarity index 100% rename from {{cookiecutter.project_name}}/app/schemas/__init__.py rename to app/tests/__init__.py diff --git a/{{cookiecutter.project_name}}/app/tests/conftest.py b/app/tests/conftest.py similarity index 100% rename from {{cookiecutter.project_name}}/app/tests/conftest.py rename to app/tests/conftest.py diff --git a/{{cookiecutter.project_name}}/app/tests/test_auth.py b/app/tests/test_auth.py similarity index 100% rename from {{cookiecutter.project_name}}/app/tests/test_auth.py rename to app/tests/test_auth.py diff --git a/{{cookiecutter.project_name}}/app/tests/test_users.py b/app/tests/test_users.py similarity index 100% rename from {{cookiecutter.project_name}}/app/tests/test_users.py rename to app/tests/test_users.py diff --git a/cookiecutter.json b/cookiecutter.json deleted file mode 100644 index 866dd9b..0000000 --- a/cookiecutter.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "project_name": "my_project_name" -} diff --git a/{{cookiecutter.project_name}}/docker-compose.yml b/docker-compose.yml similarity index 100% rename from {{cookiecutter.project_name}}/docker-compose.yml rename to docker-compose.yml diff --git a/docs/template-fastapi-users-openapi-example.png b/docs/template-fastapi-users-openapi-example.png deleted file mode 100644 index 5373b6f4b1bbc63687bd642ccf08c398676344ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160836 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 z 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 "3.12" and python_version < "4.0" -annotated-types==0.6.0 ; python_version >= "3.12" and python_version < "4.0" -anyio==3.7.1 ; python_version >= "3.12" and python_version < "4.0" -asyncpg==0.29.0 ; python_version >= "3.12" and python_version < "4.0" -bcrypt==4.0.1 ; python_version >= "3.12" and python_version < "4.0" -black==23.10.1 ; python_version >= "3.12" and python_version < "4.0" -certifi==2023.7.22 ; python_version >= "3.12" and python_version < "4.0" -cffi==1.16.0 ; python_version >= "3.12" and python_version < "4.0" -cfgv==3.4.0 ; python_version >= "3.12" and python_version < "4.0" -click==8.1.7 ; python_version >= "3.12" and python_version < "4.0" -colorama==0.4.6 ; python_version >= "3.12" and python_version < "4.0" and (sys_platform == "win32" or platform_system == "Windows") -coverage==7.3.2 ; python_version >= "3.12" and python_version < "4.0" -cryptography==41.0.5 ; python_version >= "3.12" and python_version < "4.0" -distlib==0.3.7 ; python_version >= "3.12" and python_version < "4.0" -dnspython==2.4.2 ; python_version >= "3.12" and python_version < "4.0" -email-validator==2.1.0.post1 ; python_version >= "3.12" and python_version < "4.0" -fastapi==0.104.1 ; python_version >= "3.12" and python_version < "4.0" -filelock==3.13.1 ; python_version >= "3.12" and python_version < "4.0" -greenlet==3.0.1 ; python_version >= "3.12" and python_version < "4.0" and (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") -h11==0.14.0 ; python_version >= "3.12" and python_version < "4.0" -httpcore==1.0.1 ; python_version >= "3.12" and python_version < "4.0" -httptools==0.6.1 ; python_version >= "3.12" and python_version < "4.0" -httpx==0.25.1 ; python_version >= "3.12" and python_version < "4.0" -identify==2.5.31 ; python_version >= "3.12" and python_version < "4.0" -idna==3.4 ; python_version >= "3.12" and python_version < "4.0" -iniconfig==2.0.0 ; python_version >= "3.12" and python_version < "4.0" -mako==1.2.4 ; python_version >= "3.12" and python_version < "4.0" -markupsafe==2.1.3 ; python_version >= "3.12" and python_version < "4.0" -mypy-extensions==1.0.0 ; python_version >= "3.12" and python_version < "4.0" -nodeenv==1.8.0 ; python_version >= "3.12" and python_version < "4.0" -packaging==23.2 ; python_version >= "3.12" and python_version < "4.0" -passlib[bcrypt]==1.7.4 ; python_version >= "3.12" and python_version < "4.0" -pathspec==0.11.2 ; python_version >= "3.12" and python_version < "4.0" -platformdirs==3.11.0 ; python_version >= "3.12" and python_version < "4.0" -pluggy==1.3.0 ; python_version >= "3.12" and python_version < "4.0" -pre-commit==3.5.0 ; python_version >= "3.12" and python_version < "4.0" -pycparser==2.21 ; python_version >= "3.12" and python_version < "4.0" -pydantic-core==2.10.1 ; python_version >= "3.12" and python_version < "4.0" -pydantic-settings==2.0.3 ; python_version >= "3.12" and python_version < "4.0" -pydantic==2.4.2 ; python_version >= "3.12" and python_version < "4.0" -pydantic[dotenv,email]==2.4.2 ; python_version >= "3.12" and python_version < "4.0" -pyjwt[crypto]==2.8.0 ; python_version >= "3.12" and python_version < "4.0" -pytest-asyncio==0.21.1 ; python_version >= "3.12" and python_version < "4.0" -pytest==7.4.3 ; python_version >= "3.12" and python_version < "4.0" -python-dotenv==1.0.0 ; python_version >= "3.12" and python_version < "4.0" -python-multipart==0.0.6 ; python_version >= "3.12" and python_version < "4.0" -pyyaml==6.0.1 ; python_version >= "3.12" and python_version < "4.0" -ruff==0.1.4 ; python_version >= "3.12" and python_version < "4.0" -setuptools==68.2.2 ; python_version >= "3.12" and python_version < "4.0" -sniffio==1.3.0 ; python_version >= "3.12" and python_version < "4.0" -sqlalchemy==2.0.23 ; python_version >= "3.12" and python_version < "4.0" -starlette==0.27.0 ; python_version >= "3.12" and python_version < "4.0" -typing-extensions==4.8.0 ; python_version >= "3.12" and python_version < "4.0" -uvicorn[standard]==0.24.0 ; python_version >= "3.12" and python_version < "4.0" -uvloop==0.19.0 ; (sys_platform != "win32" and sys_platform != "cygwin") and platform_python_implementation != "PyPy" and python_version >= "3.12" and python_version < "4.0" -virtualenv==20.24.6 ; python_version >= "3.12" and python_version < "4.0" -watchfiles==0.21.0 ; python_version >= "3.12" and python_version < "4.0" -websockets==12.0 ; python_version >= "3.12" and python_version < "4.0" diff --git a/{{cookiecutter.project_name}}/requirements.txt b/{{cookiecutter.project_name}}/requirements.txt deleted file mode 100644 index 2fd8afd..0000000 --- a/{{cookiecutter.project_name}}/requirements.txt +++ /dev/null @@ -1,27 +0,0 @@ -alembic==1.12.1 ; python_version >= "3.12" and python_version < "4.0" -annotated-types==0.6.0 ; python_version >= "3.12" and python_version < "4.0" -anyio==3.7.1 ; python_version >= "3.12" and python_version < "4.0" -asyncpg==0.29.0 ; python_version >= "3.12" and python_version < "4.0" -bcrypt==4.0.1 ; python_version >= "3.12" and python_version < "4.0" -cffi==1.16.0 ; python_version >= "3.12" and python_version < "4.0" -cryptography==41.0.5 ; python_version >= "3.12" and python_version < "4.0" -dnspython==2.4.2 ; python_version >= "3.12" and python_version < "4.0" -email-validator==2.1.0.post1 ; python_version >= "3.12" and python_version < "4.0" -fastapi==0.104.1 ; python_version >= "3.12" and python_version < "4.0" -greenlet==3.0.1 ; python_version >= "3.12" and python_version < "4.0" and (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") -idna==3.4 ; python_version >= "3.12" and python_version < "4.0" -mako==1.2.4 ; python_version >= "3.12" and python_version < "4.0" -markupsafe==2.1.3 ; python_version >= "3.12" and python_version < "4.0" -passlib[bcrypt]==1.7.4 ; python_version >= "3.12" and python_version < "4.0" -pycparser==2.21 ; python_version >= "3.12" and python_version < "4.0" -pydantic-core==2.10.1 ; python_version >= "3.12" and python_version < "4.0" -pydantic-settings==2.0.3 ; python_version >= "3.12" and python_version < "4.0" -pydantic==2.4.2 ; python_version >= "3.12" and python_version < "4.0" -pydantic[dotenv,email]==2.4.2 ; python_version >= "3.12" and python_version < "4.0" -pyjwt[crypto]==2.8.0 ; python_version >= "3.12" and python_version < "4.0" -python-dotenv==1.0.0 ; python_version >= "3.12" and python_version < "4.0" -python-multipart==0.0.6 ; python_version >= "3.12" and python_version < "4.0" -sniffio==1.3.0 ; python_version >= "3.12" and python_version < "4.0" -sqlalchemy==2.0.23 ; python_version >= "3.12" and python_version < "4.0" -starlette==0.27.0 ; python_version >= "3.12" and python_version < "4.0" -typing-extensions==4.8.0 ; python_version >= "3.12" and python_version < "4.0" From d21e408f744fdb4546dec34f99a06a9841faca54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Fri, 19 Jan 2024 13:41:30 +0100 Subject: [PATCH 03/41] remove cryptography, update .env.example, run pre-commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- .env.example | 28 +--- .../workflows/manual_build_docker_image.yml | 2 +- .github/workflows/tests.yml | 31 ++-- .gitignore | 3 + .pre-commit-config.yaml | 16 +- Dockerfile | 12 +- ...nit_user_and_refresh_token_21f30f70dab2.py | 59 ++++--- app/api/deps.py | 3 - app/api/endpoints/auth.py | 14 +- app/core/config.py | 5 +- app/core/session.py | 1 - app/models.py | 2 +- app/tests/conftest.py | 23 ++- app/tests/test_users.py | 8 +- docker-compose.yml | 20 +-- poetry.lock | 147 +++--------------- pyproject.toml | 4 +- 17 files changed, 122 insertions(+), 256 deletions(-) diff --git a/.env.example b/.env.example index 8803f10..9e2d8cd 100644 --- a/.env.example +++ b/.env.example @@ -1,21 +1,9 @@ -SECRET_KEY=DVnFmhwvjEhJZpuhndxjhlezxQPJmBIIkMDEmFREWQADPcUnrG -ENVIRONMENT=DEV -ACCESS_TOKEN_EXPIRE_MINUTES=11520 -REFRESH_TOKEN_EXPIRE_MINUTES=40320 -BACKEND_CORS_ORIGINS=["http://localhost:3000","http://localhost:8001"] -ALLOWED_HOSTS=["localhost", "127.0.0.1"] +SECURITY__JWT_SECRET_KEY=DVnFmhwvjEhJZpuhndxjhlezxQPJmBIIkMDEmFREWQADPcUnrG +SECURITY__BACKEND_CORS_ORIGINS=["http://localhost:3000","http://localhost:8001"] +SECURITY__ALLOWED_HOSTS=["localhost", "127.0.0.1"] -DEFAULT_DATABASE_HOSTNAME=localhost -DEFAULT_DATABASE_USER=rDGJeEDqAz -DEFAULT_DATABASE_PASSWORD=XsPQhCoEfOQZueDjsILetLDUvbvSxAMnrVtgVZpmdcSssUgbvs -DEFAULT_DATABASE_PORT=5387 -DEFAULT_DATABASE_DB=default_db - -TEST_DATABASE_HOSTNAME=localhost -TEST_DATABASE_USER=test -TEST_DATABASE_PASSWORD=ywRCUjJijmQoBmWxIfLldOoITPzajPSNvTvHyugQoSqGwNcvQE -TEST_DATABASE_PORT=37270 -TEST_DATABASE_DB=test_db - -FIRST_SUPERUSER_EMAIL=example@example.com -FIRST_SUPERUSER_PASSWORD=OdLknKQJMUwuhpAVHvRC \ No newline at end of file +DATABASE__HOSTNAME=localhost +DATABASE__USERNAME=rDGJeEDqAz +DATABASE__PASSWORD=XsPQhCoEfOQZueDjsILetLDUvbvSxAMnrVtgVZpmdcSssUgbvs +DATABASE__PORT=5455 +DATABASE__DB=default_db \ No newline at end of file diff --git a/.github/workflows/manual_build_docker_image.yml b/.github/workflows/manual_build_docker_image.yml index 2cc3102..0c667cf 100644 --- a/.github/workflows/manual_build_docker_image.yml +++ b/.github/workflows/manual_build_docker_image.yml @@ -1,4 +1,4 @@ -name: Manual push minimal template docker image to dockerhub +name: Manual push docker image to dockerhub on: workflow_dispatch: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 88743f8..92d4758 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,40 +26,27 @@ jobs: with: python-version: "3.12" - # Below will create fresh template in path: minimal_project - - name: Generate project from template using cookiecutter - run: | - pip install cookiecutter - python tests/create_minimal_project.py - - name: Install Poetry uses: snok/install-poetry@v1 with: virtualenvs-create: true virtualenvs-in-project: true - # run tests from folder minimal_project - name: Load cached venv - id: cached-poetry-dependencies-template + id: cached-poetry-dependencies uses: actions/cache@v2 with: - path: minimal_project/.venv - key: venv-${{ runner.os }}-${{ hashFiles('minimal_project/poetry.lock') }} + path: .venv + key: venv-${{ runner.os }}-${{ hashFiles('poetry.lock') }} - - name: Install template minimal dependencies - if: steps.cached-poetry-dependencies-template.outputs.cache-hit != 'true' + - name: Run poetry install + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' run: | - cd minimal_project poetry install --no-interaction --no-root - - name: Run template minimal flake8 and then tests + - name: Run tests env: - TEST_DATABASE_HOSTNAME: localhost - TEST_DATABASE_PASSWORD: postgres - TEST_DATABASE_PORT: 5432 - TEST_DATABASE_USER: postgres - TEST_DATABASE_DB: postgres + DATABASE__HOSTNAME: localhost + DATABASE__PASSWORD: postgres run: | - cd minimal_project - poetry run ruff app - poetry run coverage run -m pytest + poetry run pytest diff --git a/.gitignore b/.gitignore index 9d9a06e..53e2c95 100644 --- a/.gitignore +++ b/.gitignore @@ -127,5 +127,8 @@ venv.bak/ .dmypy.json dmypy.json +# ruff +.ruff_cache + # Pyre type checker .pyre/ \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ddf613c..44797ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,24 +5,12 @@ repos: - id: check-yaml - repo: https://github.com/psf/black - rev: "23.10.1" + rev: "23.12.1" hooks: - id: black - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.4 + rev: v0.1.13 hooks: - id: ruff args: [--fix] - - - repo: https://github.com/python-poetry/poetry - rev: "1.7.0" - hooks: - - id: poetry-export - args: ["-o", "requirements.txt", "--without-hashes"] - - - repo: https://github.com/python-poetry/poetry - rev: "1.7.0" - hooks: - - id: poetry-export - args: ["-o", "requirements-dev.txt", "--without-hashes", "--with=dev"] diff --git a/Dockerfile b/Dockerfile index 3719523..451a6ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,19 @@ -FROM python:3.12.0-slim-bullseye +FROM python:3.12.1-slim-bullseye as base ENV PYTHONUNBUFFERED 1 WORKDIR /build +# Create requirements.txt file +FROM base as poetry +RUN pip install poetry==1.7.1 +COPY poetry.lock pyproject.toml ./ +RUN poetry export -o /requirements.txt --without-hashes + +FROM base as common +COPY --from=poetry /requirements.txt . # Create venv, add it to path and install requirements RUN python -m venv /venv ENV PATH="/venv/bin:$PATH" - -COPY requirements.txt . RUN pip install -r requirements.txt # Install uvicorn server diff --git a/alembic/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py b/alembic/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py index d9c867b..2045c4c 100644 --- a/alembic/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py +++ b/alembic/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py @@ -1,47 +1,58 @@ """init_user_and_refresh_token Revision ID: 21f30f70dab2 -Revises: +Revises: Create Date: 2024-01-19 01:39:35.369361 """ -from alembic import op import sqlalchemy as sa +from alembic import op # revision identifiers, used by Alembic. -revision = '21f30f70dab2' +revision = "21f30f70dab2" down_revision = None branch_labels = None depends_on = None -def upgrade(): +def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.create_table('user_account', - sa.Column('user_id', sa.Uuid(as_uuid=False), nullable=False), - sa.Column('email', sa.String(length=256), nullable=False), - sa.Column('hashed_password', sa.String(length=128), nullable=False), - sa.PrimaryKeyConstraint('user_id') + op.create_table( + "user_account", + sa.Column("user_id", sa.Uuid(as_uuid=False), nullable=False), + sa.Column("email", sa.String(length=256), nullable=False), + sa.Column("hashed_password", sa.String(length=128), nullable=False), + sa.PrimaryKeyConstraint("user_id"), + ) + op.create_index( + op.f("ix_user_account_email"), "user_account", ["email"], unique=True + ) + op.create_table( + "refresh_token", + sa.Column("id", sa.BigInteger(), nullable=False), + sa.Column("refresh_token", sa.String(length=512), nullable=False), + sa.Column("used", sa.Boolean(), nullable=False), + sa.Column("exp", sa.BigInteger(), nullable=False), + sa.Column("user_id", sa.Uuid(as_uuid=False), nullable=False), + sa.ForeignKeyConstraint( + ["user_id"], ["user_account.user_id"], ondelete="CASCADE" + ), + sa.PrimaryKeyConstraint("id"), ) - op.create_index(op.f('ix_user_account_email'), 'user_account', ['email'], unique=True) - op.create_table('refresh_token', - sa.Column('id', sa.BigInteger(), nullable=False), - sa.Column('refresh_token', sa.String(length=512), nullable=False), - sa.Column('used', sa.Boolean(), nullable=False), - sa.Column('exp', sa.BigInteger(), nullable=False), - sa.Column('user_id', sa.Uuid(as_uuid=False), nullable=False), - sa.ForeignKeyConstraint(['user_id'], ['user_account.user_id'], ondelete='CASCADE'), - sa.PrimaryKeyConstraint('id') + op.create_index( + op.f("ix_refresh_token_refresh_token"), + "refresh_token", + ["refresh_token"], + unique=True, ) - op.create_index(op.f('ix_refresh_token_refresh_token'), 'refresh_token', ['refresh_token'], unique=True) # ### end Alembic commands ### -def downgrade(): +def downgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_index(op.f('ix_refresh_token_refresh_token'), table_name='refresh_token') - op.drop_table('refresh_token') - op.drop_index(op.f('ix_user_account_email'), table_name='user_account') - op.drop_table('user_account') + op.drop_index(op.f("ix_refresh_token_refresh_token"), table_name="refresh_token") + op.drop_table("refresh_token") + op.drop_index(op.f("ix_user_account_email"), table_name="user_account") + op.drop_table("user_account") # ### end Alembic commands ### diff --git a/app/api/deps.py b/app/api/deps.py index 0e2dd0a..a1e6838 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -1,14 +1,11 @@ -import time from collections.abc import AsyncGenerator from typing import Annotated -import jwt from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.core import config, security from app.core.security.jwt import verify_jwt_token from app.core.session import async_session from app.models import User diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index 89621b9..0d0e291 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -1,20 +1,18 @@ import asyncio import random +import secrets import time - -import jwt from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm -from pydantic import ValidationError -from sqlalchemy import delete, select, update +from sqlalchemy import delete, select from sqlalchemy.ext.asyncio import AsyncSession -import secrets + from app.api import deps -from app.core import config, security -from app.core.security.password import verify_password +from app.core import config from app.core.security.jwt import create_jwt_token -from app.models import User, RefreshToken +from app.core.security.password import verify_password +from app.models import RefreshToken, User from app.schemas.requests import RefreshTokenRequest from app.schemas.responses import AccessTokenResponse diff --git a/app/core/config.py b/app/core/config.py index ef048e2..0854cab 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -21,11 +21,10 @@ Note, complex types like lists are read as json-encoded strings. """ -from functools import cached_property +from functools import cached_property, lru_cache from pathlib import Path -from functools import lru_cache -from pydantic import BaseModel, AnyHttpUrl, PostgresDsn, SecretStr, computed_field +from pydantic import AnyHttpUrl, BaseModel, PostgresDsn, SecretStr, computed_field from pydantic_settings import BaseSettings, SettingsConfigDict PROJECT_DIR = Path(__file__).parent.parent.parent diff --git a/app/core/session.py b/app/core/session.py index efd4ca5..26c2f28 100644 --- a/app/core/session.py +++ b/app/core/session.py @@ -8,7 +8,6 @@ from app.core.config import get_settings - async_engine = create_async_engine( get_settings().sqlalchemy_database_uri.get_secret_value(), pool_pre_ping=True ) diff --git a/app/models.py b/app/models.py index a4255b3..8987cd8 100644 --- a/app/models.py +++ b/app/models.py @@ -15,7 +15,7 @@ """ import uuid -from sqlalchemy import Boolean, String, ForeignKey, Uuid, BigInteger +from sqlalchemy import BigInteger, Boolean, ForeignKey, String, Uuid from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship diff --git a/app/tests/conftest.py b/app/tests/conftest.py index 6873016..8f047dd 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -1,14 +1,14 @@ import asyncio -from collections.abc import AsyncGenerator -from typing import Generator +from collections.abc import AsyncGenerator, Generator import pytest import pytest_asyncio from httpx import AsyncClient -from sqlalchemy import delete, select +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.core import config, security +from app.core.security.password import get_password_hash +from app.core.security.jwt import create_jwt_token from app.core.session import async_engine, async_session from app.main import app from app.models import Base, User @@ -16,10 +16,8 @@ default_user_id = "b75365d9-7bf9-4f54-add5-aeab333a087b" default_user_email = "geralt@wiedzmin.pl" default_user_password = "geralt" -default_user_password_hash = security.get_password_hash(default_user_password) -default_user_access_token = security.create_jwt_token( - str(default_user_id), 60 * 60 * 24, refresh=False -)[0] +default_user_password_hash = get_password_hash(default_user_password) +default_user_access_token = create_jwt_token(default_user_id) @pytest.fixture(scope="session") @@ -32,9 +30,6 @@ def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]: @pytest_asyncio.fixture(scope="session") async def test_db_setup_sessionmaker() -> None: - # assert if we use TEST_DB URL for 100% - assert config.settings.ENVIRONMENT == "PYTEST" - # always drop and create test db tables between tests session async with async_engine.begin() as conn: await conn.run_sync(Base.metadata.drop_all) @@ -42,7 +37,9 @@ async def test_db_setup_sessionmaker() -> None: @pytest_asyncio.fixture(autouse=True) -async def session(test_db_setup_sessionmaker: None) -> AsyncGenerator[AsyncSession, None]: +async def session( + test_db_setup_sessionmaker: None, +) -> AsyncGenerator[AsyncSession, None]: async with async_session() as session: yield session await session.rollback() @@ -68,7 +65,7 @@ async def default_user(test_db_setup_sessionmaker: None) -> User: email=default_user_email, hashed_password=default_user_password_hash, ) - new_user.id = default_user_id + new_user.user_id = default_user_id session.add(new_user) await session.commit() await session.refresh(new_user) diff --git a/app/tests/test_users.py b/app/tests/test_users.py index c30024b..8ab8626 100644 --- a/app/tests/test_users.py +++ b/app/tests/test_users.py @@ -11,7 +11,9 @@ ) -async def test_read_current_user(client: AsyncClient, default_user_headers: dict[str, str]) -> None: +async def test_read_current_user( + client: AsyncClient, default_user_headers: dict[str, str] +) -> None: response = await client.get( app.url_path_for("read_current_user"), headers=default_user_headers ) @@ -29,7 +31,7 @@ async def test_delete_current_user( app.url_path_for("delete_current_user"), headers=default_user_headers ) assert response.status_code == codes.NO_CONTENT - result = await session.execute(select(User).where(User.id == default_user_id)) + result = await session.execute(select(User).where(User.user_id == default_user_id)) user = result.scalars().first() assert user is None @@ -43,7 +45,7 @@ async def test_reset_current_user_password( json={"password": "testxxxxxx"}, ) assert response.status_code == codes.OK - result = await session.execute(select(User).where(User.id == default_user_id)) + result = await session.execute(select(User).where(User.user_id == default_user_id)) user = result.scalars().first() assert user is not None assert user.hashed_password != default_user_password_hash diff --git a/docker-compose.yml b/docker-compose.yml index b339f17..787efff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,6 @@ -version: "3.7" - # For local development, only database is running # -# docker-compose up -d +# docker compose up -d # uvicorn app.main:app --reload # @@ -13,13 +11,13 @@ services: volumes: - default_database_data:/var/lib/postgresql/data environment: - - POSTGRES_DB=${DEFAULT_DATABASE_DB} - - POSTGRES_USER=${DEFAULT_DATABASE_USER} - - POSTGRES_PASSWORD=${DEFAULT_DATABASE_PASSWORD} + - POSTGRES_DB=${DATABASE__DB} + - POSTGRES_USER=${DATABASE__USERNAME} + - POSTGRES_PASSWORD=${DATABASE__PASSWORD} env_file: - .env ports: - - "${DEFAULT_DATABASE_PORT}:5432" + - "${DATABASE__PORT}:5432" test_database: restart: unless-stopped @@ -27,13 +25,9 @@ services: volumes: - test_database_data:/var/lib/postgresql/data environment: - - POSTGRES_DB=${TEST_DATABASE_DB} - - POSTGRES_USER=${TEST_DATABASE_USER} - - POSTGRES_PASSWORD=${TEST_DATABASE_PASSWORD} - env_file: - - .env + - POSTGRES_PASSWORD=postgres ports: - - "${TEST_DATABASE_PORT}:5432" + - "31234:5432" volumes: test_database_data: diff --git a/poetry.lock b/poetry.lock index 1a7125f..aa2dcd2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "alembic" @@ -199,70 +199,6 @@ files = [ {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] -[[package]] -name = "cffi" -version = "1.16.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, -] - -[package.dependencies] -pycparser = "*" - [[package]] name = "cfgv" version = "3.4.0" @@ -363,51 +299,6 @@ files = [ [package.extras] toml = ["tomli"] -[[package]] -name = "cryptography" -version = "41.0.7" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = ">=3.7" -files = [ - {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, - {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a1b41bc97f1ad230a41657d9155113c7521953869ae57ac39ac7f1bb471469a"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:841df4caa01008bad253bce2a6f7b47f86dc9f08df4b433c404def869f590a15"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5429ec739a29df2e29e15d082f1d9ad683701f0ec7709ca479b3ff2708dae65a"}, - {file = "cryptography-41.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:43f2552a2378b44869fe8827aa19e69512e3245a219104438692385b0ee119d1"}, - {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:af03b32695b24d85a75d40e1ba39ffe7db7ffcb099fe507b39fd41a565f1b157"}, - {file = "cryptography-41.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:49f0805fc0b2ac8d4882dd52f4a3b935b210935d500b6b805f321addc8177406"}, - {file = "cryptography-41.0.7-cp37-abi3-win32.whl", hash = "sha256:f983596065a18a2183e7f79ab3fd4c475205b839e02cbc0efbbf9666c4b3083d"}, - {file = "cryptography-41.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:90452ba79b8788fa380dfb587cca692976ef4e757b194b093d845e8d99f612f2"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:079b85658ea2f59c4f43b70f8119a52414cdb7be34da5d019a77bf96d473b960"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:b640981bf64a3e978a56167594a0e97db71c89a479da8e175d8bb5be5178c003"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e3114da6d7f95d2dee7d3f4eec16dacff819740bbab931aff8648cb13c5ff5e7"}, - {file = "cryptography-41.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d5ec85080cce7b0513cfd233914eb8b7bbd0633f1d1703aa28d1dd5a72f678ec"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a698cb1dac82c35fcf8fe3417a3aaba97de16a01ac914b89a0889d364d2f6be"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:37a138589b12069efb424220bf78eac59ca68b95696fc622b6ccc1c0a197204a"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:68a2dec79deebc5d26d617bfdf6e8aab065a4f34934b22d3b5010df3ba36612c"}, - {file = "cryptography-41.0.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:09616eeaef406f99046553b8a40fbf8b1e70795a91885ba4c96a70793de5504a"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:48a0476626da912a44cc078f9893f292f0b3e4c739caf289268168d8f4702a39"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c7f3201ec47d5207841402594f1d7950879ef890c0c495052fa62f58283fde1a"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c5ca78485a255e03c32b513f8c2bc39fedb7f5c5f8535545bdc223a03b24f248"}, - {file = "cryptography-41.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6c391c021ab1f7a82da5d8d0b3cee2f4b2c455ec86c8aebbc84837a631ff309"}, - {file = "cryptography-41.0.7.tar.gz", hash = "sha256:13f93ce9bea8016c253b34afc6bd6a75993e5c40672ed5405a9c832f0d4a00bc"}, -] - -[package.dependencies] -cffi = ">=1.12" - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] -nox = ["nox"] -pep8test = ["black", "check-sdist", "mypy", "ruff"] -sdist = ["build"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] -test-randomorder = ["pytest-randomly"] - [[package]] name = "distlib" version = "0.3.8" @@ -745,6 +636,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -918,17 +819,6 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" -[[package]] -name = "pycparser" -version = "2.21" -description = "C parser in Python" -optional = false -python-versions = "*" -files = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] - [[package]] name = "pydantic" version = "2.5.3" @@ -1092,9 +982,6 @@ files = [ {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, ] -[package.dependencies] -cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} - [package.extras] crypto = ["cryptography (>=3.4.0)"] dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] @@ -1214,6 +1101,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1221,8 +1109,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1239,6 +1134,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1246,6 +1142,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1690,4 +1587,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "8394909ca43af6556bf3aaed1f188595b9b21fb26144bba4cae60a0780ee2bc1" +content-hash = "85eff04b05715f86f6be8b2d0f840caf38f97b19d898074564e7c9aafbedb1e4" diff --git a/pyproject.toml b/pyproject.toml index 7cc6f2c..659bf79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,9 @@ fastapi = "^0.109.0" bcrypt = "^4.1.2" pydantic = { extras = ["dotenv", "email"], version = "^2.5.3" } pydantic-settings = "^2.1.0" -pyjwt = { extras = ["crypto"], version = "^2.8.0" } python-multipart = "^0.0.6" sqlalchemy = "^2.0.23" +pyjwt = "^2.8.0" [tool.poetry.group.dev.dependencies] @@ -38,7 +38,7 @@ build-backend = "poetry.core.masonry.api" requires = ["poetry-core>=1.0.0"] [tool.pytest.ini_options] -addopts = "-v --cov --cov-report xml --cov-report term-missing" +addopts = "-v --cov --cov-report xml --cov-report term-missing --cov-fail-under=100" asyncio_mode = "auto" env = ["ENVIRONMENT=PYTEST"] minversion = "6.0" From 44728a66074960ff89827ea29674c577d619f74d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Sat, 20 Jan 2024 16:24:12 +0100 Subject: [PATCH 04/41] add freezegun, write test_auth tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/tests/test_auth.py | 83 ++++++++++++++++++++++++++++++++++++------ poetry.lock | 41 ++++++++++++++++++++- pyproject.toml | 1 + 3 files changed, 112 insertions(+), 13 deletions(-) diff --git a/app/tests/test_auth.py b/app/tests/test_auth.py index 7a1ed34..8260773 100644 --- a/app/tests/test_auth.py +++ b/app/tests/test_auth.py @@ -1,11 +1,20 @@ -from httpx import AsyncClient, codes +from datetime import datetime, timezone +from httpx import AsyncClient +from fastapi import status +from sqlalchemy import select from app.main import app -from app.models import User +from app.models import RefreshToken, User +from app.core.config import get_settings +from app.core.security.jwt import verify_jwt_token from app.tests.conftest import default_user_email, default_user_password +from freezegun import freeze_time +from sqlalchemy.ext.asyncio import AsyncSession -async def test_auth_access_token(client: AsyncClient, default_user: User) -> None: +async def test_login_access_token_return_success_response( + client: AsyncClient, default_user: User +) -> None: response = await client.post( app.url_path_for("login_access_token"), data={ @@ -14,15 +23,65 @@ async def test_auth_access_token(client: AsyncClient, default_user: User) -> Non }, headers={"Content-Type": "application/x-www-form-urlencoded"}, ) - assert response.status_code == codes.OK + + assert response.status_code == status.HTTP_200_OK + token = response.json() assert token["token_type"] == "Bearer" - assert "access_token" in token - assert "expires_at" in token - assert "issued_at" in token - assert "refresh_token" in token - assert "refresh_token_expires_at" in token - assert "refresh_token_issued_at" in token + + +@freeze_time("2024-01-19") +async def test_login_access_token_return_valid_jwt_access_token( + client: AsyncClient, default_user: User +) -> None: + response = await client.post( + app.url_path_for("login_access_token"), + data={ + "username": default_user_email, + "password": default_user_password, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + + token = response.json() + expected_exp = int( + datetime.now(tz=timezone.utc).timestamp() + + get_settings().security.jwt_access_token_expire_secs + ) + assert token["expires_in"] == expected_exp + token_payload = verify_jwt_token(token["access_token"]) + assert token_payload.sub == default_user.user_id + assert token_payload.iat == int(datetime.now(tz=timezone.utc).timestamp()) + assert token_payload.exp == expected_exp + + +@freeze_time("2024-01-19") +async def test_login_access_token_return_valid_refresh_token( + client: AsyncClient, default_user: User, session: AsyncSession +) -> None: + response = await client.post( + app.url_path_for("login_access_token"), + data={ + "username": default_user_email, + "password": default_user_password, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + + token = response.json() + expected_exp = int( + datetime.now(tz=timezone.utc).timestamp() + + get_settings().security.refresh_token_expire_secs + ) + assert token["refresh_token_expires_in"] == expected_exp + result = await session.execute( + select(RefreshToken).where(RefreshToken.refresh_token == token["refresh_token"]) + ) + refresh_token = result.scalars().first() + assert refresh_token is not None + assert refresh_token.user_id == default_user.user_id + assert refresh_token.exp == expected_exp + assert not refresh_token.used async def test_auth_access_token_fail_no_user(client: AsyncClient) -> None: @@ -35,7 +94,7 @@ async def test_auth_access_token_fail_no_user(client: AsyncClient) -> None: headers={"Content-Type": "application/x-www-form-urlencoded"}, ) - assert response.status_code == codes.BAD_REQUEST + assert response.status_code == status.HTTP_404_NOT_FOUND assert response.json() == {"detail": "Incorrect email or password"} @@ -53,7 +112,7 @@ async def test_auth_refresh_token(client: AsyncClient, default_user: User) -> No new_token_response = await client.post( app.url_path_for("refresh_token"), json={"refresh_token": refresh_token} ) - assert new_token_response.status_code == codes.OK + assert new_token_response.status_code == status.HTTP_200_OK token = new_token_response.json() assert token["token_type"] == "Bearer" assert "access_token" in token diff --git a/poetry.lock b/poetry.lock index aa2dcd2..68487e5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -379,6 +379,20 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 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)"] typing = ["typing-extensions (>=4.8)"] +[[package]] +name = "freezegun" +version = "1.4.0" +description = "Let your Python tests travel through time" +optional = false +python-versions = ">=3.7" +files = [ + {file = "freezegun-1.4.0-py3-none-any.whl", hash = "sha256:55e0fc3c84ebf0a96a5aa23ff8b53d70246479e9a68863f1fcac5a3e52f19dd6"}, + {file = "freezegun-1.4.0.tar.gz", hash = "sha256:10939b0ba0ff5adaecf3b06a5c2f73071d9678e507c5eaedb23c761d56ac774b"}, +] + +[package.dependencies] +python-dateutil = ">=2.7" + [[package]] name = "greenlet" version = "3.0.3" @@ -1061,6 +1075,20 @@ pytest = ">=7.4.3" [package.extras] test = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "pytest-mock (>=3.12)"] +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "1.0.0" @@ -1190,6 +1218,17 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments 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)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sniffio" version = "1.3.0" @@ -1587,4 +1626,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "85eff04b05715f86f6be8b2d0f840caf38f97b19d898074564e7c9aafbedb1e4" +content-hash = "5f2672d42d6a30209a2046e383afdd2a8884760956119d40602fb8a636219636" diff --git a/pyproject.toml b/pyproject.toml index 659bf79..22387fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ mypy = "^1.8.0" pytest-cov = "^4.1.0" pytest-env = "^1.1.3" types-passlib = "^1.7.7.20240106" +freezegun = "^1.4.0" [build-system] From 983937e62a35ab66acaaac79cf1601b475d6c43c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 29 Jan 2024 23:48:09 +0100 Subject: [PATCH 05/41] fix pytest tests, finish test_auth_login_access_token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- .github/workflows/tests.yml | 2 +- app/api/endpoints/auth.py | 13 +- app/core/config.py | 1 + app/core/security/password.py | 9 +- app/tests/test_auth.py | 123 --------- app/tests/test_auth/__init__.py | 0 .../test_auth/test_auth_login_access_token.py | 191 +++++++++++++ .../test_auth/test_auth_refresh_token.py | 0 poetry.lock | 257 ++++++++++++++---- pyproject.toml | 14 +- 10 files changed, 420 insertions(+), 190 deletions(-) delete mode 100644 app/tests/test_auth.py create mode 100644 app/tests/test_auth/__init__.py create mode 100644 app/tests/test_auth/test_auth_login_access_token.py create mode 100644 app/tests/test_auth/test_auth_refresh_token.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 92d4758..93052a1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -17,7 +17,7 @@ jobs: --health-timeout 5s --health-retries 5 ports: - - 5432:5432 + - 31234:5432 steps: - uses: actions/checkout@v2 diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index 0d0e291..e989bbe 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -11,7 +11,7 @@ from app.api import deps from app.core import config from app.core.security.jwt import create_jwt_token -from app.core.security.password import verify_password +from app.core.security.password import verify_password, DUMMY_PASSWORD from app.models import RefreshToken, User from app.schemas.requests import RefreshTokenRequest from app.schemas.responses import AccessTokenResponse @@ -26,18 +26,16 @@ async def login_access_token( ) -> AccessTokenResponse: """OAuth2 compatible token, get an access token for future requests using username and password""" - wait_delay_ms = random.randint(800, 1000) - min_end_time = time.time() + wait_delay_ms / 1000 - result = await session.execute(select(User).where(User.email == form_data.username)) user = result.scalars().first() if user is None: - await asyncio.sleep(min_end_time - time.time()) + # this is naive method to not return early + verify_password(form_data.password, DUMMY_PASSWORD) + raise HTTPException(status_code=400, detail="Incorrect email or password") if not verify_password(form_data.password, user.hashed_password): - await asyncio.sleep(min_end_time - time.time()) raise HTTPException(status_code=400, detail="Incorrect email or password") jwt_token = create_jwt_token(user_id=user.user_id) @@ -50,10 +48,9 @@ async def login_access_token( session.add(refresh_token) await session.commit() - await asyncio.sleep(min_end_time - time.time()) return AccessTokenResponse( access_token=jwt_token.access_token, - expires_at=jwt_token.payload.iat, + expires_at=jwt_token.payload.exp, refresh_token=refresh_token.refresh_token, refresh_token_expires_at=refresh_token.exp, ) diff --git a/app/core/config.py b/app/core/config.py index 0854cab..0b1e983 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -35,6 +35,7 @@ class Security(BaseModel): jwt_secret_key: SecretStr jwt_access_token_expire_secs: int = 24 * 3600 # 1d refresh_token_expire_secs: int = 28 * 24 * 3600 # 28d + password_bcrypt_rounds: int = 12 allowed_hosts: list[str] = ["localhost", "127.0.0.1"] backend_cors_origins: list[AnyHttpUrl] = [] diff --git a/app/core/security/password.py b/app/core/security/password.py index 4882741..7d04b50 100644 --- a/app/core/security/password.py +++ b/app/core/security/password.py @@ -1,4 +1,5 @@ import bcrypt +from app.core.config import get_settings def verify_password(plain_password: str, hashed_password: str) -> bool: @@ -8,4 +9,10 @@ def verify_password(plain_password: str, hashed_password: str) -> bool: def get_password_hash(password: str) -> str: - return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() + return bcrypt.hashpw( + password.encode(), + bcrypt.gensalt(get_settings().security.password_bcrypt_rounds), + ).decode() + + +DUMMY_PASSWORD = get_password_hash("") diff --git a/app/tests/test_auth.py b/app/tests/test_auth.py deleted file mode 100644 index 8260773..0000000 --- a/app/tests/test_auth.py +++ /dev/null @@ -1,123 +0,0 @@ -from datetime import datetime, timezone -from httpx import AsyncClient - -from fastapi import status -from sqlalchemy import select -from app.main import app -from app.models import RefreshToken, User -from app.core.config import get_settings -from app.core.security.jwt import verify_jwt_token -from app.tests.conftest import default_user_email, default_user_password -from freezegun import freeze_time -from sqlalchemy.ext.asyncio import AsyncSession - - -async def test_login_access_token_return_success_response( - client: AsyncClient, default_user: User -) -> None: - response = await client.post( - app.url_path_for("login_access_token"), - data={ - "username": default_user_email, - "password": default_user_password, - }, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) - - assert response.status_code == status.HTTP_200_OK - - token = response.json() - assert token["token_type"] == "Bearer" - - -@freeze_time("2024-01-19") -async def test_login_access_token_return_valid_jwt_access_token( - client: AsyncClient, default_user: User -) -> None: - response = await client.post( - app.url_path_for("login_access_token"), - data={ - "username": default_user_email, - "password": default_user_password, - }, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) - - token = response.json() - expected_exp = int( - datetime.now(tz=timezone.utc).timestamp() - + get_settings().security.jwt_access_token_expire_secs - ) - assert token["expires_in"] == expected_exp - token_payload = verify_jwt_token(token["access_token"]) - assert token_payload.sub == default_user.user_id - assert token_payload.iat == int(datetime.now(tz=timezone.utc).timestamp()) - assert token_payload.exp == expected_exp - - -@freeze_time("2024-01-19") -async def test_login_access_token_return_valid_refresh_token( - client: AsyncClient, default_user: User, session: AsyncSession -) -> None: - response = await client.post( - app.url_path_for("login_access_token"), - data={ - "username": default_user_email, - "password": default_user_password, - }, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) - - token = response.json() - expected_exp = int( - datetime.now(tz=timezone.utc).timestamp() - + get_settings().security.refresh_token_expire_secs - ) - assert token["refresh_token_expires_in"] == expected_exp - result = await session.execute( - select(RefreshToken).where(RefreshToken.refresh_token == token["refresh_token"]) - ) - refresh_token = result.scalars().first() - assert refresh_token is not None - assert refresh_token.user_id == default_user.user_id - assert refresh_token.exp == expected_exp - assert not refresh_token.used - - -async def test_auth_access_token_fail_no_user(client: AsyncClient) -> None: - response = await client.post( - app.url_path_for("login_access_token"), - data={ - "username": "xxx", - "password": "yyy", - }, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) - - assert response.status_code == status.HTTP_404_NOT_FOUND - assert response.json() == {"detail": "Incorrect email or password"} - - -async def test_auth_refresh_token(client: AsyncClient, default_user: User) -> None: - response = await client.post( - app.url_path_for("login_access_token"), - data={ - "username": default_user_email, - "password": default_user_password, - }, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - ) - refresh_token = response.json()["refresh_token"] - - new_token_response = await client.post( - app.url_path_for("refresh_token"), json={"refresh_token": refresh_token} - ) - assert new_token_response.status_code == status.HTTP_200_OK - token = new_token_response.json() - assert token["token_type"] == "Bearer" - assert "access_token" in token - assert "expires_at" in token - assert "issued_at" in token - assert "refresh_token" in token - assert "refresh_token_expires_at" in token - assert "refresh_token_issued_at" in token diff --git a/app/tests/test_auth/__init__.py b/app/tests/test_auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tests/test_auth/test_auth_login_access_token.py b/app/tests/test_auth/test_auth_login_access_token.py new file mode 100644 index 0000000..e90d2fe --- /dev/null +++ b/app/tests/test_auth/test_auth_login_access_token.py @@ -0,0 +1,191 @@ +from datetime import datetime, timezone +from httpx import AsyncClient + +from fastapi import status +import pytest +from sqlalchemy import func, select +from app.main import app +from app.models import RefreshToken, User +from app.core.config import get_settings +from app.core.security.jwt import verify_jwt_token +from app.tests.conftest import default_user_password +from sqlalchemy.ext.asyncio import AsyncSession + + +async def test_login_access_token_has_response_code_200( + client: AsyncClient, + default_user: User, +) -> None: + response = await client.post( + app.url_path_for("login_access_token"), + data={ + "username": default_user.email, + "password": default_user_password, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + + assert response.status_code == status.HTTP_200_OK + + +async def test_login_access_token_jwt_has_valid_token_type( + client: AsyncClient, + default_user: User, +) -> None: + response = await client.post( + app.url_path_for("login_access_token"), + data={ + "username": default_user.email, + "password": default_user_password, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + + token = response.json() + assert token["token_type"] == "Bearer" + + +async def test_login_access_token_jwt_has_valid_expire_time( + client: AsyncClient, + default_user: User, +) -> None: + current_time = int(datetime.now(tz=timezone.utc).timestamp()) + response = await client.post( + app.url_path_for("login_access_token"), + data={ + "username": default_user.email, + "password": default_user_password, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + + token = response.json() + assert ( + current_time + get_settings().security.jwt_access_token_expire_secs + <= token["expires_at"] + <= current_time + get_settings().security.jwt_access_token_expire_secs + 1 + ) + + +async def test_login_access_token_returns_valid_jwt_access_token( + client: AsyncClient, + default_user: User, +) -> None: + now = int(datetime.now(tz=timezone.utc).timestamp()) + response = await client.post( + app.url_path_for("login_access_token"), + data={ + "username": default_user.email, + "password": default_user_password, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + + token = response.json() + token_payload = verify_jwt_token(token["access_token"]) + + assert token_payload.sub == default_user.user_id + assert token_payload.iat >= now + assert token_payload.exp == token["expires_at"] + + +async def test_login_access_token_refresh_token_has_valid_expire_time( + client: AsyncClient, + default_user: User, +) -> None: + current_time = int(datetime.now(tz=timezone.utc).timestamp()) + response = await client.post( + app.url_path_for("login_access_token"), + data={ + "username": default_user.email, + "password": default_user_password, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + + token = response.json() + assert ( + current_time + get_settings().security.refresh_token_expire_secs + <= token["refresh_token_expires_at"] + <= current_time + get_settings().security.refresh_token_expire_secs + 1 + ) + + +async def test_login_access_token_refresh_token_exists_in_db( + client: AsyncClient, + default_user: User, + session: AsyncSession, +) -> None: + response = await client.post( + app.url_path_for("login_access_token"), + data={ + "username": default_user.email, + "password": default_user_password, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + + token = response.json() + + token_db_count = await session.scalar( + select(func.count()).where(RefreshToken.refresh_token == token["refresh_token"]) + ) + assert token_db_count == 1 + + +async def test_login_access_token_refresh_token_in_db_has_valid_fields( + client: AsyncClient, + default_user: User, + session: AsyncSession, +) -> None: + response = await client.post( + app.url_path_for("login_access_token"), + data={ + "username": default_user.email, + "password": default_user_password, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + + token = response.json() + result = await session.scalars( + select(RefreshToken).where(RefreshToken.refresh_token == token["refresh_token"]) + ) + refresh_token = result.one() + + assert refresh_token.user_id == default_user.user_id + assert refresh_token.exp == token["refresh_token_expires_at"] + assert not refresh_token.used + + +async def test_auth_access_token_fail_for_not_existing_user( + client: AsyncClient, +) -> None: + response = await client.post( + app.url_path_for("login_access_token"), + data={ + "username": "non-existing", + "password": "bla", + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == {"detail": "Incorrect email or password"} + + +async def test_auth_access_token_fail_for_invalid_password( + client: AsyncClient, + default_user: User, +) -> None: + response = await client.post( + app.url_path_for("login_access_token"), + data={ + "username": default_user.email, + "password": "invalid", + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == {"detail": "Incorrect email or password"} diff --git a/app/tests/test_auth/test_auth_refresh_token.py b/app/tests/test_auth/test_auth_refresh_token.py new file mode 100644 index 0000000..e69de29 diff --git a/poetry.lock b/poetry.lock index 68487e5..fb42fc7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "alembic" @@ -199,6 +199,70 @@ files = [ {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "cfgv" version = "3.4.0" @@ -380,18 +444,66 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pyt typing = ["typing-extensions (>=4.8)"] [[package]] -name = "freezegun" -version = "1.4.0" -description = "Let your Python tests travel through time" +name = "gevent" +version = "23.9.1" +description = "Coroutine-based network library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "freezegun-1.4.0-py3-none-any.whl", hash = "sha256:55e0fc3c84ebf0a96a5aa23ff8b53d70246479e9a68863f1fcac5a3e52f19dd6"}, - {file = "freezegun-1.4.0.tar.gz", hash = "sha256:10939b0ba0ff5adaecf3b06a5c2f73071d9678e507c5eaedb23c761d56ac774b"}, + {file = "gevent-23.9.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:a3c5e9b1f766a7a64833334a18539a362fb563f6c4682f9634dea72cbe24f771"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b101086f109168b23fa3586fccd1133494bdb97f86920a24dc0b23984dc30b69"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36a549d632c14684bcbbd3014a6ce2666c5f2a500f34d58d32df6c9ea38b6535"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:272cffdf535978d59c38ed837916dfd2b5d193be1e9e5dcc60a5f4d5025dd98a"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcb8612787a7f4626aa881ff15ff25439561a429f5b303048f0fca8a1c781c39"}, + {file = "gevent-23.9.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d57737860bfc332b9b5aa438963986afe90f49645f6e053140cfa0fa1bdae1ae"}, + {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5f3c781c84794926d853d6fb58554dc0dcc800ba25c41d42f6959c344b4db5a6"}, + {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dbb22a9bbd6a13e925815ce70b940d1578dbe5d4013f20d23e8a11eddf8d14a7"}, + {file = "gevent-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:707904027d7130ff3e59ea387dddceedb133cc742b00b3ffe696d567147a9c9e"}, + {file = "gevent-23.9.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:45792c45d60f6ce3d19651d7fde0bc13e01b56bb4db60d3f32ab7d9ec467374c"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e24c2af9638d6c989caffc691a039d7c7022a31c0363da367c0d32ceb4a0648"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1ead6863e596a8cc2a03e26a7a0981f84b6b3e956101135ff6d02df4d9a6b07"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65883ac026731ac112184680d1f0f1e39fa6f4389fd1fc0bf46cc1388e2599f9"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7af500da05363e66f122896012acb6e101a552682f2352b618e541c941a011"}, + {file = "gevent-23.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c3e5d2fa532e4d3450595244de8ccf51f5721a05088813c1abd93ad274fe15e7"}, + {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c84d34256c243b0a53d4335ef0bc76c735873986d478c53073861a92566a8d71"}, + {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ada07076b380918829250201df1d016bdafb3acf352f35e5693b59dceee8dd2e"}, + {file = "gevent-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:921dda1c0b84e3d3b1778efa362d61ed29e2b215b90f81d498eb4d8eafcd0b7a"}, + {file = "gevent-23.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ed7a048d3e526a5c1d55c44cb3bc06cfdc1947d06d45006cc4cf60dedc628904"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c1abc6f25f475adc33e5fc2dbcc26a732608ac5375d0d306228738a9ae14d3b"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4368f341a5f51611411ec3fc62426f52ac3d6d42eaee9ed0f9eebe715c80184e"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52b4abf28e837f1865a9bdeef58ff6afd07d1d888b70b6804557e7908032e599"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52e9f12cd1cda96603ce6b113d934f1aafb873e2c13182cf8e86d2c5c41982ea"}, + {file = "gevent-23.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:de350fde10efa87ea60d742901e1053eb2127ebd8b59a7d3b90597eb4e586599"}, + {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303"}, + {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dd6c32ab977ecf7c7b8c2611ed95fa4aaebd69b74bf08f4b4960ad516861517d"}, + {file = "gevent-23.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:455e5ee8103f722b503fa45dedb04f3ffdec978c1524647f8ba72b4f08490af1"}, + {file = "gevent-23.9.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7ccf0fd378257cb77d91c116e15c99e533374a8153632c48a3ecae7f7f4f09fe"}, + {file = "gevent-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d163d59f1be5a4c4efcdd13c2177baaf24aadf721fdf2e1af9ee54a998d160f5"}, + {file = "gevent-23.9.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7532c17bc6c1cbac265e751b95000961715adef35a25d2b0b1813aa7263fb397"}, + {file = "gevent-23.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:78eebaf5e73ff91d34df48f4e35581ab4c84e22dd5338ef32714264063c57507"}, + {file = "gevent-23.9.1-cp38-cp38-win32.whl", hash = "sha256:f632487c87866094546a74eefbca2c74c1d03638b715b6feb12e80120960185a"}, + {file = "gevent-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:62d121344f7465e3739989ad6b91f53a6ca9110518231553fe5846dbe1b4518f"}, + {file = "gevent-23.9.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:bf456bd6b992eb0e1e869e2fd0caf817f0253e55ca7977fd0e72d0336a8c1c6a"}, + {file = "gevent-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43daf68496c03a35287b8b617f9f91e0e7c0d042aebcc060cadc3f049aadd653"}, + {file = "gevent-23.9.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7c28e38dcde327c217fdafb9d5d17d3e772f636f35df15ffae2d933a5587addd"}, + {file = "gevent-23.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543"}, + {file = "gevent-23.9.1-cp39-cp39-win32.whl", hash = "sha256:2c7b5c9912378e5f5ccf180d1fdb1e83f42b71823483066eddbe10ef1a2fcaa2"}, + {file = "gevent-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:a2898b7048771917d85a1d548fd378e8a7b2ca963db8e17c6d90c76b495e0e2b"}, + {file = "gevent-23.9.1.tar.gz", hash = "sha256:72c002235390d46f94938a96920d8856d4ffd9ddf62a303a0d7c118894097e34"}, ] [package.dependencies] -python-dateutil = ">=2.7" +cffi = {version = ">=1.12.2", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""} +greenlet = {version = ">=3.0rc3", markers = "platform_python_implementation == \"CPython\" and python_version >= \"3.11\""} +"zope.event" = "*" +"zope.interface" = "*" + +[package.extras] +dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"] +docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"] +monitor = ["psutil (>=5.7.0)"] +recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)"] +test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idna", "objgraph", "psutil (>=5.7.0)", "requests", "setuptools"] [[package]] name = "greenlet" @@ -650,16 +762,6 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -833,6 +935,17 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = "*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + [[package]] name = "pydantic" version = "2.5.3" @@ -1075,20 +1188,6 @@ pytest = ">=7.4.3" [package.extras] test = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "pytest-mock (>=3.12)"] -[[package]] -name = "python-dateutil" -version = "2.8.2" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] - -[package.dependencies] -six = ">=1.5" - [[package]] name = "python-dotenv" version = "1.0.0" @@ -1129,7 +1228,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1137,15 +1235,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1162,7 +1253,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1170,7 +1260,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1218,17 +1307,6 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments 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)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - [[package]] name = "sniffio" version = "1.3.0" @@ -1623,7 +1701,78 @@ files = [ {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, ] +[[package]] +name = "zope-event" +version = "5.0" +description = "Very basic event publishing system" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zope.event-5.0-py3-none-any.whl", hash = "sha256:2832e95014f4db26c47a13fdaef84cef2f4df37e66b59d8f1f4a8f319a632c26"}, + {file = "zope.event-5.0.tar.gz", hash = "sha256:bac440d8d9891b4068e2b5a2c5e2c9765a9df762944bda6955f96bb9b91e67cd"}, +] + +[package.dependencies] +setuptools = "*" + +[package.extras] +docs = ["Sphinx"] +test = ["zope.testrunner"] + +[[package]] +name = "zope-interface" +version = "6.1" +description = "Interfaces for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zope.interface-6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb"}, + {file = "zope.interface-6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92"}, + {file = "zope.interface-6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3"}, + {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd"}, + {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41"}, + {file = "zope.interface-6.1-cp310-cp310-win_amd64.whl", hash = "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f"}, + {file = "zope.interface-6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1"}, + {file = "zope.interface-6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736"}, + {file = "zope.interface-6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605"}, + {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8"}, + {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de"}, + {file = "zope.interface-6.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1"}, + {file = "zope.interface-6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a"}, + {file = "zope.interface-6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7"}, + {file = "zope.interface-6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d"}, + {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff"}, + {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0"}, + {file = "zope.interface-6.1-cp312-cp312-win_amd64.whl", hash = "sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b"}, + {file = "zope.interface-6.1-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:2f8d89721834524a813f37fa174bac074ec3d179858e4ad1b7efd4401f8ac45d"}, + {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13b7d0f2a67eb83c385880489dbb80145e9d344427b4262c49fbf2581677c11c"}, + {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef43ee91c193f827e49599e824385ec7c7f3cd152d74cb1dfe02cb135f264d83"}, + {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e441e8b7d587af0414d25e8d05e27040d78581388eed4c54c30c0c91aad3a379"}, + {file = "zope.interface-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89b28772fc2562ed9ad871c865f5320ef761a7fcc188a935e21fe8b31a38ca9"}, + {file = "zope.interface-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f"}, + {file = "zope.interface-6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1"}, + {file = "zope.interface-6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56"}, + {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b"}, + {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43"}, + {file = "zope.interface-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d"}, + {file = "zope.interface-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179"}, + {file = "zope.interface-6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941"}, + {file = "zope.interface-6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3"}, + {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d"}, + {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac"}, + {file = "zope.interface-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40"}, + {file = "zope.interface-6.1.tar.gz", hash = "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309"}, +] + +[package.dependencies] +setuptools = "*" + +[package.extras] +docs = ["Sphinx", "repoze.sphinx.autointerface", "sphinx-rtd-theme"] +test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] +testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] + [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "5f2672d42d6a30209a2046e383afdd2a8884760956119d40602fb8a636219636" +content-hash = "4e2b0adab623b7e9a87cdad3973b286045b546c21ede257055301740de52f7b4" diff --git a/pyproject.toml b/pyproject.toml index 22387fc..67ff0f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] authors = ["admin "] description = "FastAPI project generated using minimal-fastapi-postgres-template." -name = "{{cookiecutter.project_name}}" +name = "app" version = "0.1.0-alpha" [tool.poetry.dependencies] @@ -31,7 +31,7 @@ mypy = "^1.8.0" pytest-cov = "^4.1.0" pytest-env = "^1.1.3" types-passlib = "^1.7.7.20240106" -freezegun = "^1.4.0" +gevent = "^23.9.1" [build-system] @@ -41,13 +41,21 @@ requires = ["poetry-core>=1.0.0"] [tool.pytest.ini_options] addopts = "-v --cov --cov-report xml --cov-report term-missing --cov-fail-under=100" asyncio_mode = "auto" -env = ["ENVIRONMENT=PYTEST"] +env = [ + "ENVIRONMENT=PYTEST", + "DATABASE__PORT=31234", + "DATABASE__USERNAME=postgres", + "DATABASE__DB=postgres", + "DATABASE__PASSWORD=postgres", + "SECURITY__PASSWORD_BCRYPT_ROUNDS=4", +] minversion = "6.0" testpaths = ["app/tests"] [tool.coverage.run] omit = ["app/tests/*"] source = ["app"] +concurrency = ["gevent"] [tool.mypy] python_version = "3.12" From e708d996bb6fdc181dd91e70460cf7c03cb1abbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 29 Jan 2024 23:52:37 +0100 Subject: [PATCH 06/41] run pre-commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/api/endpoints/auth.py | 4 +--- app/core/security/password.py | 1 + app/tests/conftest.py | 2 +- .../test_auth/test_auth_login_access_token.py | 18 +++++++++--------- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index e989bbe..f6a36de 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -1,5 +1,3 @@ -import asyncio -import random import secrets import time @@ -11,7 +9,7 @@ from app.api import deps from app.core import config from app.core.security.jwt import create_jwt_token -from app.core.security.password import verify_password, DUMMY_PASSWORD +from app.core.security.password import DUMMY_PASSWORD, verify_password from app.models import RefreshToken, User from app.schemas.requests import RefreshTokenRequest from app.schemas.responses import AccessTokenResponse diff --git a/app/core/security/password.py b/app/core/security/password.py index 7d04b50..6e1087a 100644 --- a/app/core/security/password.py +++ b/app/core/security/password.py @@ -1,4 +1,5 @@ import bcrypt + from app.core.config import get_settings diff --git a/app/tests/conftest.py b/app/tests/conftest.py index 8f047dd..b8f6267 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -7,8 +7,8 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.core.security.password import get_password_hash from app.core.security.jwt import create_jwt_token +from app.core.security.password import get_password_hash from app.core.session import async_engine, async_session from app.main import app from app.models import Base, User diff --git a/app/tests/test_auth/test_auth_login_access_token.py b/app/tests/test_auth/test_auth_login_access_token.py index e90d2fe..aaaa41a 100644 --- a/app/tests/test_auth/test_auth_login_access_token.py +++ b/app/tests/test_auth/test_auth_login_access_token.py @@ -1,15 +1,15 @@ -from datetime import datetime, timezone -from httpx import AsyncClient +from datetime import UTC, datetime from fastapi import status -import pytest +from httpx import AsyncClient from sqlalchemy import func, select -from app.main import app -from app.models import RefreshToken, User +from sqlalchemy.ext.asyncio import AsyncSession + from app.core.config import get_settings from app.core.security.jwt import verify_jwt_token +from app.main import app +from app.models import RefreshToken, User from app.tests.conftest import default_user_password -from sqlalchemy.ext.asyncio import AsyncSession async def test_login_access_token_has_response_code_200( @@ -49,7 +49,7 @@ async def test_login_access_token_jwt_has_valid_expire_time( client: AsyncClient, default_user: User, ) -> None: - current_time = int(datetime.now(tz=timezone.utc).timestamp()) + current_time = int(datetime.now(tz=UTC).timestamp()) response = await client.post( app.url_path_for("login_access_token"), data={ @@ -71,7 +71,7 @@ async def test_login_access_token_returns_valid_jwt_access_token( client: AsyncClient, default_user: User, ) -> None: - now = int(datetime.now(tz=timezone.utc).timestamp()) + now = int(datetime.now(tz=UTC).timestamp()) response = await client.post( app.url_path_for("login_access_token"), data={ @@ -93,7 +93,7 @@ async def test_login_access_token_refresh_token_has_valid_expire_time( client: AsyncClient, default_user: User, ) -> None: - current_time = int(datetime.now(tz=timezone.utc).timestamp()) + current_time = int(datetime.now(tz=UTC).timestamp()) response = await client.post( app.url_path_for("login_access_token"), data={ From 2feb0eb3864fa299866600273a4be3ec4a4df608 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Wed, 21 Feb 2024 18:26:50 +0100 Subject: [PATCH 07/41] add freeze time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- .../test_auth/test_auth_login_access_token.py | 4 ++ poetry.lock | 65 ++++++++++++++++++- pyproject.toml | 1 + 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/app/tests/test_auth/test_auth_login_access_token.py b/app/tests/test_auth/test_auth_login_access_token.py index aaaa41a..2f202d2 100644 --- a/app/tests/test_auth/test_auth_login_access_token.py +++ b/app/tests/test_auth/test_auth_login_access_token.py @@ -10,8 +10,10 @@ from app.main import app from app.models import RefreshToken, User from app.tests.conftest import default_user_password +from freezegun import freeze_time +@freeze_time("2023-01-01") async def test_login_access_token_has_response_code_200( client: AsyncClient, default_user: User, @@ -28,6 +30,7 @@ async def test_login_access_token_has_response_code_200( assert response.status_code == status.HTTP_200_OK +@freeze_time("2023-01-01") async def test_login_access_token_jwt_has_valid_token_type( client: AsyncClient, default_user: User, @@ -45,6 +48,7 @@ async def test_login_access_token_jwt_has_valid_token_type( assert token["token_type"] == "Bearer" +@freeze_time("2023-01-01") async def test_login_access_token_jwt_has_valid_expire_time( client: AsyncClient, default_user: User, diff --git a/poetry.lock b/poetry.lock index fb42fc7..7935b27 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "alembic" @@ -443,6 +443,20 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 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)"] typing = ["typing-extensions (>=4.8)"] +[[package]] +name = "freezegun" +version = "1.4.0" +description = "Let your Python tests travel through time" +optional = false +python-versions = ">=3.7" +files = [ + {file = "freezegun-1.4.0-py3-none-any.whl", hash = "sha256:55e0fc3c84ebf0a96a5aa23ff8b53d70246479e9a68863f1fcac5a3e52f19dd6"}, + {file = "freezegun-1.4.0.tar.gz", hash = "sha256:10939b0ba0ff5adaecf3b06a5c2f73071d9678e507c5eaedb23c761d56ac774b"}, +] + +[package.dependencies] +python-dateutil = ">=2.7" + [[package]] name = "gevent" version = "23.9.1" @@ -762,6 +776,16 @@ files = [ {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, + {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, @@ -940,7 +964,7 @@ name = "pycparser" version = "2.21" description = "C parser in Python" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, @@ -1188,6 +1212,20 @@ pytest = ">=7.4.3" [package.extras] test = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "pytest-mock (>=3.12)"] +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-dotenv" version = "1.0.0" @@ -1228,6 +1266,7 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1235,8 +1274,15 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1253,6 +1299,7 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1260,6 +1307,7 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1307,6 +1355,17 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments 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)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sniffio" version = "1.3.0" @@ -1775,4 +1834,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "4e2b0adab623b7e9a87cdad3973b286045b546c21ede257055301740de52f7b4" +content-hash = "e6c170662a2d87f6764555ee4a114215381d4b32fce6320f14d5e27705a06d2e" diff --git a/pyproject.toml b/pyproject.toml index 67ff0f5..38b391c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ pytest-cov = "^4.1.0" pytest-env = "^1.1.3" types-passlib = "^1.7.7.20240106" gevent = "^23.9.1" +freezegun = "^1.4.0" [build-system] From 86e002466af9c3fa98773839bd65c41223567894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Sat, 2 Mar 2024 17:28:44 +0100 Subject: [PATCH 08/41] test for refresh token, change database uri method in settings, add new create and update fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- alembic/env.py | 2 +- ...add_create_and_update_time_b70e5f695bd2.py | 22 + app/core/config.py | 27 +- app/core/session.py | 2 +- app/models.py | 11 +- .../test_auth/test_auth_login_access_token.py | 20 +- poetry.lock | 801 +++++++++--------- 7 files changed, 439 insertions(+), 446 deletions(-) create mode 100644 alembic/versions/2024030244_add_create_and_update_time_b70e5f695bd2.py diff --git a/alembic/env.py b/alembic/env.py index 1bad14b..3d74954 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -30,7 +30,7 @@ def get_database_uri() -> str: - return get_settings().sqlalchemy_database_uri.get_secret_value() + return get_settings().sqlalchemy_database_uri.render_as_string(hide_password=False) def run_migrations_offline() -> None: diff --git a/alembic/versions/2024030244_add_create_and_update_time_b70e5f695bd2.py b/alembic/versions/2024030244_add_create_and_update_time_b70e5f695bd2.py new file mode 100644 index 0000000..16571db --- /dev/null +++ b/alembic/versions/2024030244_add_create_and_update_time_b70e5f695bd2.py @@ -0,0 +1,22 @@ +"""add create and update time + +Revision ID: b70e5f695bd2 +Revises: 21f30f70dab2 +Create Date: 2024-03-02 16:44:19.587386 + +""" + + +# revision identifiers, used by Alembic. +revision = "b70e5f695bd2" +down_revision = "21f30f70dab2" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/app/core/config.py b/app/core/config.py index 0b1e983..5f13779 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -21,11 +21,12 @@ Note, complex types like lists are read as json-encoded strings. """ -from functools import cached_property, lru_cache +from functools import lru_cache from pathlib import Path -from pydantic import AnyHttpUrl, BaseModel, PostgresDsn, SecretStr, computed_field +from pydantic import AnyHttpUrl, BaseModel, SecretStr, computed_field from pydantic_settings import BaseSettings, SettingsConfigDict +from sqlalchemy.engine.url import URL PROJECT_DIR = Path(__file__).parent.parent.parent @@ -53,19 +54,15 @@ class Settings(BaseSettings): database: Database @computed_field # type: ignore[misc] - @cached_property - def sqlalchemy_database_uri(self) -> SecretStr: - return SecretStr( - str( - PostgresDsn.build( - scheme="postgresql+asyncpg", - username=self.database.username, - password=self.database.password.get_secret_value(), - host=self.database.hostname, - port=self.database.port, - path=self.database.db, - ) - ) + @property + def sqlalchemy_database_uri(self) -> URL: + return URL.create( + drivername="postgresql+asyncpg", + username=self.database.username, + password=self.database.password.get_secret_value(), + host=self.database.hostname, + port=self.database.port, + database=self.database.db, ) model_config = SettingsConfigDict( diff --git a/app/core/session.py b/app/core/session.py index 26c2f28..202d9b6 100644 --- a/app/core/session.py +++ b/app/core/session.py @@ -9,6 +9,6 @@ from app.core.config import get_settings async_engine = create_async_engine( - get_settings().sqlalchemy_database_uri.get_secret_value(), pool_pre_ping=True + get_settings().sqlalchemy_database_uri, pool_pre_ping=True ) async_session = async_sessionmaker(async_engine, expire_on_commit=False) diff --git a/app/models.py b/app/models.py index 8987cd8..ad86333 100644 --- a/app/models.py +++ b/app/models.py @@ -13,14 +13,21 @@ # apply all migrations alembic upgrade head """ + import uuid +from datetime import datetime -from sqlalchemy import BigInteger, Boolean, ForeignKey, String, Uuid +from sqlalchemy import BigInteger, Boolean, DateTime, ForeignKey, String, Uuid, func from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship class Base(DeclarativeBase): - pass + create_time: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now() + ) + update_time: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) class User(Base): diff --git a/app/tests/test_auth/test_auth_login_access_token.py b/app/tests/test_auth/test_auth_login_access_token.py index 2f202d2..53cf6b8 100644 --- a/app/tests/test_auth/test_auth_login_access_token.py +++ b/app/tests/test_auth/test_auth_login_access_token.py @@ -1,6 +1,7 @@ from datetime import UTC, datetime from fastapi import status +from freezegun import freeze_time from httpx import AsyncClient from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession @@ -10,7 +11,6 @@ from app.main import app from app.models import RefreshToken, User from app.tests.conftest import default_user_password -from freezegun import freeze_time @freeze_time("2023-01-01") @@ -53,7 +53,6 @@ async def test_login_access_token_jwt_has_valid_expire_time( client: AsyncClient, default_user: User, ) -> None: - current_time = int(datetime.now(tz=UTC).timestamp()) response = await client.post( app.url_path_for("login_access_token"), data={ @@ -64,10 +63,10 @@ async def test_login_access_token_jwt_has_valid_expire_time( ) token = response.json() + current_timestamp = int(datetime.now(tz=UTC).timestamp()) assert ( - current_time + get_settings().security.jwt_access_token_expire_secs - <= token["expires_at"] - <= current_time + get_settings().security.jwt_access_token_expire_secs + 1 + token["expires_at"] + == current_timestamp + get_settings().security.jwt_access_token_expire_secs ) @@ -97,7 +96,6 @@ async def test_login_access_token_refresh_token_has_valid_expire_time( client: AsyncClient, default_user: User, ) -> None: - current_time = int(datetime.now(tz=UTC).timestamp()) response = await client.post( app.url_path_for("login_access_token"), data={ @@ -108,10 +106,10 @@ async def test_login_access_token_refresh_token_has_valid_expire_time( ) token = response.json() + current_time = int(datetime.now(tz=UTC).timestamp()) assert ( - current_time + get_settings().security.refresh_token_expire_secs - <= token["refresh_token_expires_at"] - <= current_time + get_settings().security.refresh_token_expire_secs + 1 + token["refresh_token_expires_at"] + == current_time + get_settings().security.refresh_token_expire_secs ) @@ -162,7 +160,7 @@ async def test_login_access_token_refresh_token_in_db_has_valid_fields( assert not refresh_token.used -async def test_auth_access_token_fail_for_not_existing_user( +async def test_auth_access_token_fail_for_not_existing_user_with_message( client: AsyncClient, ) -> None: response = await client.post( @@ -178,7 +176,7 @@ async def test_auth_access_token_fail_for_not_existing_user( assert response.json() == {"detail": "Incorrect email or password"} -async def test_auth_access_token_fail_for_invalid_password( +async def test_auth_access_token_fail_for_invalid_password_with_message( client: AsyncClient, default_user: User, ) -> None: diff --git a/poetry.lock b/poetry.lock index 7935b27..8fc7acb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "alembic" @@ -32,13 +32,13 @@ files = [ [[package]] name = "anyio" -version = "4.2.0" +version = "4.3.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" files = [ - {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, - {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, ] [package.dependencies] @@ -190,13 +190,13 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2023.11.17" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] @@ -301,63 +301,63 @@ files = [ [[package]] name = "coverage" -version = "7.4.0" +version = "7.4.3" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, - {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, - {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, - {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, - {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, - {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, - {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, - {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, - {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, - {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, - {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, - {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, - {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, - {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, - {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, - {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, - {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, - {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, - {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, - {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, - {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, - {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, + {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, + {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, + {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, + {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"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, + {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, + {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, + {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, + {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, + {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, + {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, + {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"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, + {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, + {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, + {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, + {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, + {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, + {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, + {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"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, + {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, + {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, + {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, + {file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"}, + {file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"}, + {file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"}, + {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"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"}, + {file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"}, + {file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"}, + {file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"}, + {file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"}, + {file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"}, + {file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"}, + {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"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"}, + {file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"}, + {file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"}, + {file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"}, + {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, + {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, ] [package.extras] @@ -376,32 +376,33 @@ files = [ [[package]] name = "dnspython" -version = "2.4.2" +version = "2.6.1" description = "DNS toolkit" optional = false -python-versions = ">=3.8,<4.0" +python-versions = ">=3.8" files = [ - {file = "dnspython-2.4.2-py3-none-any.whl", hash = "sha256:57c6fbaaeaaf39c891292012060beb141791735dbb4004798328fc2c467402d8"}, - {file = "dnspython-2.4.2.tar.gz", hash = "sha256:8dcfae8c7460a2f84b4072e26f1c9f4101ca20c071649cb7c34e8b6a93d58984"}, + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, ] [package.extras] -dnssec = ["cryptography (>=2.6,<42.0)"] -doh = ["h2 (>=4.1.0)", "httpcore (>=0.17.3)", "httpx (>=0.24.1)"] -doq = ["aioquic (>=0.9.20)"] -idna = ["idna (>=2.1,<4.0)"] -trio = ["trio (>=0.14,<0.23)"] -wmi = ["wmi (>=1.5.1,<2.0.0)"] +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=41)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=0.9.25)"] +idna = ["idna (>=3.6)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] [[package]] name = "email-validator" -version = "2.1.0.post1" +version = "2.1.1" description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.8" files = [ - {file = "email_validator-2.1.0.post1-py3-none-any.whl", hash = "sha256:c973053efbeddfef924dc0bd93f6e77a1ea7ee0fce935aea7103c7a3d6d2d637"}, - {file = "email_validator-2.1.0.post1.tar.gz", hash = "sha256:a4b0bd1cf55f073b924258d19321b1f3aa74b4b5a71a42c305575dba920e1a44"}, + {file = "email_validator-2.1.1-py3-none-any.whl", hash = "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"}, + {file = "email_validator-2.1.1.tar.gz", hash = "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84"}, ] [package.dependencies] @@ -410,22 +411,22 @@ idna = ">=2.0.0" [[package]] name = "fastapi" -version = "0.109.0" +version = "0.109.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi-0.109.0-py3-none-any.whl", hash = "sha256:8c77515984cd8e8cfeb58364f8cc7a28f0692088475e2614f7bf03275eba9093"}, - {file = "fastapi-0.109.0.tar.gz", hash = "sha256:b978095b9ee01a5cf49b19f4bc1ac9b8ca83aa076e770ef8fd9af09a2b88d191"}, + {file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"}, + {file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"}, ] [package.dependencies] 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" -starlette = ">=0.35.0,<0.36.0" +starlette = ">=0.36.3,<0.37.0" typing-extensions = ">=4.8.0" [package.extras] -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.5)", "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)"] +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)"] [[package]] name = "filelock" @@ -603,13 +604,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.2" +version = "1.0.4" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, - {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, + {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, + {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, ] [package.dependencies] @@ -620,7 +621,7 @@ h11 = ">=0.13,<0.15" asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<0.23.0)"] +trio = ["trio (>=0.22.0,<0.25.0)"] [[package]] name = "httptools" @@ -696,13 +697,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "identify" -version = "2.5.33" +version = "2.5.35" description = "File identification library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, - {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, + {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, + {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, ] [package.extras] @@ -732,13 +733,13 @@ files = [ [[package]] name = "mako" -version = "1.3.0" +version = "1.3.2" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false python-versions = ">=3.8" files = [ - {file = "Mako-1.3.0-py3-none-any.whl", hash = "sha256:57d4e997349f1a92035aa25c17ace371a4213f2ca42f99bee9a602500cfd54d9"}, - {file = "Mako-1.3.0.tar.gz", hash = "sha256:e3a9d388fd00e87043edbe8792f45880ac0114e9c4adc69f6e9bfb2c55e3b11b"}, + {file = "Mako-1.3.2-py3-none-any.whl", hash = "sha256:32a99d70754dfce237019d17ffe4a282d2d3351b9c476e90d8a60e63f133b80c"}, + {file = "Mako-1.3.2.tar.gz", hash = "sha256:2a0c8ad7f6274271b3bb7467dd37cf9cc6dab4bc19cb69a4ef10669402de698e"}, ] [package.dependencies] @@ -751,71 +752,71 @@ testing = ["pytest"] [[package]] name = "markupsafe" -version = "2.1.3" +version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.7" files = [ - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win32.whl", hash = "sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431"}, - {file = "MarkupSafe-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win32.whl", hash = "sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb"}, - {file = "MarkupSafe-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win32.whl", hash = "sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007"}, - {file = "MarkupSafe-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win32.whl", hash = "sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0"}, - {file = "MarkupSafe-2.1.3-cp37-cp37m-win_amd64.whl", hash = "sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win32.whl", hash = "sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5"}, - {file = "MarkupSafe-2.1.3-cp38-cp38-win_amd64.whl", hash = "sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win32.whl", hash = "sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2"}, - {file = "MarkupSafe-2.1.3-cp39-cp39-win_amd64.whl", hash = "sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba"}, - {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] @@ -913,28 +914,28 @@ files = [ [[package]] name = "platformdirs" -version = "4.1.0" +version = "4.2.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, - {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, ] [package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] [[package]] name = "pluggy" -version = "1.3.0" +version = "1.4.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, - {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, ] [package.extras] @@ -943,13 +944,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "3.6.0" +version = "3.6.2" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ - {file = "pre_commit-3.6.0-py2.py3-none-any.whl", hash = "sha256:c255039ef399049a5544b6ce13d135caba8f2c28c3b4033277a788f434308376"}, - {file = "pre_commit-3.6.0.tar.gz", hash = "sha256:d30bad9abf165f7785c15a21a1f46da7d0677cb00ee7ff4c579fd38922efe15d"}, + {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, + {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, ] [package.dependencies] @@ -964,7 +965,7 @@ name = "pycparser" version = "2.21" description = "C parser in Python" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = "*" files = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, @@ -972,19 +973,19 @@ files = [ [[package]] name = "pydantic" -version = "2.5.3" +version = "2.6.3" description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic-2.5.3-py3-none-any.whl", hash = "sha256:d0caf5954bee831b6bfe7e338c32b9e30c85dfe080c843680783ac2b631673b4"}, - {file = "pydantic-2.5.3.tar.gz", hash = "sha256:b3ef57c62535b0941697cce638c08900d87fcb67e29cfa99e8a68f747f393f7a"}, + {file = "pydantic-2.6.3-py3-none-any.whl", hash = "sha256:72c6034df47f46ccdf81869fddb81aade68056003900a8724a4f160700016a2a"}, + {file = "pydantic-2.6.3.tar.gz", hash = "sha256:e07805c4c7f5c6826e33a1d4c9d47950d7eaf34868e2690f8594d2e30241f11f"}, ] [package.dependencies] annotated-types = ">=0.4.0" email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} -pydantic-core = "2.14.6" +pydantic-core = "2.16.3" typing-extensions = ">=4.6.1" [package.extras] @@ -992,116 +993,90 @@ email = ["email-validator (>=2.0.0)"] [[package]] name = "pydantic-core" -version = "2.14.6" +version = "2.16.3" description = "" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic_core-2.14.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:72f9a942d739f09cd42fffe5dc759928217649f070056f03c70df14f5770acf9"}, - {file = "pydantic_core-2.14.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6a31d98c0d69776c2576dda4b77b8e0c69ad08e8b539c25c7d0ca0dc19a50d6c"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa90562bc079c6c290f0512b21768967f9968e4cfea84ea4ff5af5d917016e4"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:370ffecb5316ed23b667d99ce4debe53ea664b99cc37bfa2af47bc769056d534"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f85f3843bdb1fe80e8c206fe6eed7a1caeae897e496542cee499c374a85c6e08"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9862bf828112e19685b76ca499b379338fd4c5c269d897e218b2ae8fcb80139d"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:036137b5ad0cb0004c75b579445a1efccd072387a36c7f217bb8efd1afbe5245"}, - {file = "pydantic_core-2.14.6-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92879bce89f91f4b2416eba4429c7b5ca22c45ef4a499c39f0c5c69257522c7c"}, - {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0c08de15d50fa190d577e8591f0329a643eeaed696d7771760295998aca6bc66"}, - {file = "pydantic_core-2.14.6-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:36099c69f6b14fc2c49d7996cbf4f87ec4f0e66d1c74aa05228583225a07b590"}, - {file = "pydantic_core-2.14.6-cp310-none-win32.whl", hash = "sha256:7be719e4d2ae6c314f72844ba9d69e38dff342bc360379f7c8537c48e23034b7"}, - {file = "pydantic_core-2.14.6-cp310-none-win_amd64.whl", hash = "sha256:36fa402dcdc8ea7f1b0ddcf0df4254cc6b2e08f8cd80e7010d4c4ae6e86b2a87"}, - {file = "pydantic_core-2.14.6-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dea7fcd62915fb150cdc373212141a30037e11b761fbced340e9db3379b892d4"}, - {file = "pydantic_core-2.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffff855100bc066ff2cd3aa4a60bc9534661816b110f0243e59503ec2df38421"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b027c86c66b8627eb90e57aee1f526df77dc6d8b354ec498be9a757d513b92b"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00b1087dabcee0b0ffd104f9f53d7d3eaddfaa314cdd6726143af6bc713aa27e"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:75ec284328b60a4e91010c1acade0c30584f28a1f345bc8f72fe8b9e46ec6a96"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e1f4744eea1501404b20b0ac059ff7e3f96a97d3e3f48ce27a139e053bb370b"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2602177668f89b38b9f84b7b3435d0a72511ddef45dc14446811759b82235a1"}, - {file = "pydantic_core-2.14.6-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c8edaea3089bf908dd27da8f5d9e395c5b4dc092dbcce9b65e7156099b4b937"}, - {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:478e9e7b360dfec451daafe286998d4a1eeaecf6d69c427b834ae771cad4b622"}, - {file = "pydantic_core-2.14.6-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b6ca36c12a5120bad343eef193cc0122928c5c7466121da7c20f41160ba00ba2"}, - {file = "pydantic_core-2.14.6-cp311-none-win32.whl", hash = "sha256:2b8719037e570639e6b665a4050add43134d80b687288ba3ade18b22bbb29dd2"}, - {file = "pydantic_core-2.14.6-cp311-none-win_amd64.whl", hash = "sha256:78ee52ecc088c61cce32b2d30a826f929e1708f7b9247dc3b921aec367dc1b23"}, - {file = "pydantic_core-2.14.6-cp311-none-win_arm64.whl", hash = "sha256:a19b794f8fe6569472ff77602437ec4430f9b2b9ec7a1105cfd2232f9ba355e6"}, - {file = "pydantic_core-2.14.6-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:667aa2eac9cd0700af1ddb38b7b1ef246d8cf94c85637cbb03d7757ca4c3fdec"}, - {file = "pydantic_core-2.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cdee837710ef6b56ebd20245b83799fce40b265b3b406e51e8ccc5b85b9099b7"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c5bcf3414367e29f83fd66f7de64509a8fd2368b1edf4351e862910727d3e51"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:26a92ae76f75d1915806b77cf459811e772d8f71fd1e4339c99750f0e7f6324f"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a983cca5ed1dd9a35e9e42ebf9f278d344603bfcb174ff99a5815f953925140a"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cb92f9061657287eded380d7dc455bbf115430b3aa4741bdc662d02977e7d0af"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ace1e220b078c8e48e82c081e35002038657e4b37d403ce940fa679e57113b"}, - {file = "pydantic_core-2.14.6-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef633add81832f4b56d3b4c9408b43d530dfca29e68fb1b797dcb861a2c734cd"}, - {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e90d6cc4aad2cc1f5e16ed56e46cebf4877c62403a311af20459c15da76fd91"}, - {file = "pydantic_core-2.14.6-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e8a5ac97ea521d7bde7621d86c30e86b798cdecd985723c4ed737a2aa9e77d0c"}, - {file = "pydantic_core-2.14.6-cp312-none-win32.whl", hash = "sha256:f27207e8ca3e5e021e2402ba942e5b4c629718e665c81b8b306f3c8b1ddbb786"}, - {file = "pydantic_core-2.14.6-cp312-none-win_amd64.whl", hash = "sha256:b3e5fe4538001bb82e2295b8d2a39356a84694c97cb73a566dc36328b9f83b40"}, - {file = "pydantic_core-2.14.6-cp312-none-win_arm64.whl", hash = "sha256:64634ccf9d671c6be242a664a33c4acf12882670b09b3f163cd00a24cffbd74e"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:24368e31be2c88bd69340fbfe741b405302993242ccb476c5c3ff48aeee1afe0"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e33b0834f1cf779aa839975f9d8755a7c2420510c0fa1e9fa0497de77cd35d2c"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6af4b3f52cc65f8a0bc8b1cd9676f8c21ef3e9132f21fed250f6958bd7223bed"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15687d7d7f40333bd8266f3814c591c2e2cd263fa2116e314f60d82086e353a"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:095b707bb287bfd534044166ab767bec70a9bba3175dcdc3371782175c14e43c"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94fc0e6621e07d1e91c44e016cc0b189b48db053061cc22d6298a611de8071bb"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce830e480f6774608dedfd4a90c42aac4a7af0a711f1b52f807130c2e434c06"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a306cdd2ad3a7d795d8e617a58c3a2ed0f76c8496fb7621b6cd514eb1532cae8"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2f5fa187bde8524b1e37ba894db13aadd64faa884657473b03a019f625cee9a8"}, - {file = "pydantic_core-2.14.6-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:438027a975cc213a47c5d70672e0d29776082155cfae540c4e225716586be75e"}, - {file = "pydantic_core-2.14.6-cp37-none-win32.whl", hash = "sha256:f96ae96a060a8072ceff4cfde89d261837b4294a4f28b84a28765470d502ccc6"}, - {file = "pydantic_core-2.14.6-cp37-none-win_amd64.whl", hash = "sha256:e646c0e282e960345314f42f2cea5e0b5f56938c093541ea6dbf11aec2862391"}, - {file = "pydantic_core-2.14.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:db453f2da3f59a348f514cfbfeb042393b68720787bbef2b4c6068ea362c8149"}, - {file = "pydantic_core-2.14.6-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3860c62057acd95cc84044e758e47b18dcd8871a328ebc8ccdefd18b0d26a21b"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36026d8f99c58d7044413e1b819a67ca0e0b8ebe0f25e775e6c3d1fabb3c38fb"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ed1af8692bd8d2a29d702f1a2e6065416d76897d726e45a1775b1444f5928a7"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:314ccc4264ce7d854941231cf71b592e30d8d368a71e50197c905874feacc8a8"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:982487f8931067a32e72d40ab6b47b1628a9c5d344be7f1a4e668fb462d2da42"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dbe357bc4ddda078f79d2a36fc1dd0494a7f2fad83a0a684465b6f24b46fe80"}, - {file = "pydantic_core-2.14.6-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f6ffc6701a0eb28648c845f4945a194dc7ab3c651f535b81793251e1185ac3d"}, - {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7f5025db12fc6de7bc1104d826d5aee1d172f9ba6ca936bf6474c2148ac336c1"}, - {file = "pydantic_core-2.14.6-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:dab03ed811ed1c71d700ed08bde8431cf429bbe59e423394f0f4055f1ca0ea60"}, - {file = "pydantic_core-2.14.6-cp38-none-win32.whl", hash = "sha256:dfcbebdb3c4b6f739a91769aea5ed615023f3c88cb70df812849aef634c25fbe"}, - {file = "pydantic_core-2.14.6-cp38-none-win_amd64.whl", hash = "sha256:99b14dbea2fdb563d8b5a57c9badfcd72083f6006caf8e126b491519c7d64ca8"}, - {file = "pydantic_core-2.14.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:4ce8299b481bcb68e5c82002b96e411796b844d72b3e92a3fbedfe8e19813eab"}, - {file = "pydantic_core-2.14.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b9a9d92f10772d2a181b5ca339dee066ab7d1c9a34ae2421b2a52556e719756f"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd9e98b408384989ea4ab60206b8e100d8687da18b5c813c11e92fd8212a98e0"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4f86f1f318e56f5cbb282fe61eb84767aee743ebe32c7c0834690ebea50c0a6b"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86ce5fcfc3accf3a07a729779d0b86c5d0309a4764c897d86c11089be61da160"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dcf1978be02153c6a31692d4fbcc2a3f1db9da36039ead23173bc256ee3b91b"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eedf97be7bc3dbc8addcef4142f4b4164066df0c6f36397ae4aaed3eb187d8ab"}, - {file = "pydantic_core-2.14.6-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d5f916acf8afbcab6bacbb376ba7dc61f845367901ecd5e328fc4d4aef2fcab0"}, - {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8a14c192c1d724c3acbfb3f10a958c55a2638391319ce8078cb36c02283959b9"}, - {file = "pydantic_core-2.14.6-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0348b1dc6b76041516e8a854ff95b21c55f5a411c3297d2ca52f5528e49d8411"}, - {file = "pydantic_core-2.14.6-cp39-none-win32.whl", hash = "sha256:de2a0645a923ba57c5527497daf8ec5df69c6eadf869e9cd46e86349146e5975"}, - {file = "pydantic_core-2.14.6-cp39-none-win_amd64.whl", hash = "sha256:aca48506a9c20f68ee61c87f2008f81f8ee99f8d7f0104bff3c47e2d148f89d9"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:d5c28525c19f5bb1e09511669bb57353d22b94cf8b65f3a8d141c389a55dec95"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:78d0768ee59baa3de0f4adac9e3748b4b1fffc52143caebddfd5ea2961595277"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b93785eadaef932e4fe9c6e12ba67beb1b3f1e5495631419c784ab87e975670"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a874f21f87c485310944b2b2734cd6d318765bcbb7515eead33af9641816506e"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b89f4477d915ea43b4ceea6756f63f0288941b6443a2b28c69004fe07fde0d0d"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:172de779e2a153d36ee690dbc49c6db568d7b33b18dc56b69a7514aecbcf380d"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:dfcebb950aa7e667ec226a442722134539e77c575f6cfaa423f24371bb8d2e94"}, - {file = "pydantic_core-2.14.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:55a23dcd98c858c0db44fc5c04fc7ed81c4b4d33c653a7c45ddaebf6563a2f66"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4241204e4b36ab5ae466ecec5c4c16527a054c69f99bba20f6f75232a6a534e2"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e574de99d735b3fc8364cba9912c2bec2da78775eba95cbb225ef7dda6acea24"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1302a54f87b5cd8528e4d6d1bf2133b6aa7c6122ff8e9dc5220fbc1e07bffebd"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8e81e4b55930e5ffab4a68db1af431629cf2e4066dbdbfef65348b8ab804ea8"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c99462ffc538717b3e60151dfaf91125f637e801f5ab008f81c402f1dff0cd0f"}, - {file = "pydantic_core-2.14.6-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e4cf2d5829f6963a5483ec01578ee76d329eb5caf330ecd05b3edd697e7d768a"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:cf10b7d58ae4a1f07fccbf4a0a956d705356fea05fb4c70608bb6fa81d103cda"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:399ac0891c284fa8eb998bcfa323f2234858f5d2efca3950ae58c8f88830f145"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c6a5c79b28003543db3ba67d1df336f253a87d3112dac3a51b94f7d48e4c0e1"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:599c87d79cab2a6a2a9df4aefe0455e61e7d2aeede2f8577c1b7c0aec643ee8e"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e166ad47ba900f2542a80d83f9fc65fe99eb63ceec4debec160ae729824052"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a0b5db001b98e1c649dd55afa928e75aa4087e587b9524a4992316fa23c9fba"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:747265448cb57a9f37572a488a57d873fd96bf51e5bb7edb52cfb37124516da4"}, - {file = "pydantic_core-2.14.6-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:7ebe3416785f65c28f4f9441e916bfc8a54179c8dea73c23023f7086fa601c5d"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:86c963186ca5e50d5c8287b1d1c9d3f8f024cbe343d048c5bd282aec2d8641f2"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e0641b506486f0b4cd1500a2a65740243e8670a2549bb02bc4556a83af84ae03"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71d72ca5eaaa8d38c8df16b7deb1a2da4f650c41b58bb142f3fb75d5ad4a611f"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27e524624eace5c59af499cd97dc18bb201dc6a7a2da24bfc66ef151c69a5f2a"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3dde6cac75e0b0902778978d3b1646ca9f438654395a362cb21d9ad34b24acf"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:00646784f6cd993b1e1c0e7b0fdcbccc375d539db95555477771c27555e3c556"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:23598acb8ccaa3d1d875ef3b35cb6376535095e9405d91a3d57a8c7db5d29341"}, - {file = "pydantic_core-2.14.6-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7f41533d7e3cf9520065f610b41ac1c76bc2161415955fbcead4981b22c7611e"}, - {file = "pydantic_core-2.14.6.tar.gz", hash = "sha256:1fd0c1d395372843fba13a51c28e3bb9d59bd7aebfeb17358ffaaa1e4dbbe948"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, + {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, + {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, + {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, + {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, + {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, + {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, + {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, + {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, + {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, + {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, + {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, + {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, + {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, + {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, + {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, + {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, + {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, + {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, + {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, + {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, + {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, + {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, + {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, + {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, + {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, + {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, + {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, + {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, + {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, ] [package.dependencies] @@ -1109,19 +1084,23 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-settings" -version = "2.1.0" +version = "2.2.1" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_settings-2.1.0-py3-none-any.whl", hash = "sha256:7621c0cb5d90d1140d2f0ef557bdf03573aac7035948109adf2574770b77605a"}, - {file = "pydantic_settings-2.1.0.tar.gz", hash = "sha256:26b1492e0a24755626ac5e6d715e9077ab7ad4fb5f19a8b7ed7011d52f36141c"}, + {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, + {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, ] [package.dependencies] pydantic = ">=2.3.0" python-dotenv = ">=0.21.0" +[package.extras] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + [[package]] name = "pyjwt" version = "2.8.0" @@ -1214,13 +1193,13 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "pytest-mock (>=3.12)"] [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -1228,13 +1207,13 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "1.0.0" +version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.8" files = [ - {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, - {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, ] [package.extras] @@ -1266,7 +1245,6 @@ files = [ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, @@ -1274,15 +1252,8 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, @@ -1299,7 +1270,6 @@ files = [ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, @@ -1307,7 +1277,6 @@ files = [ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, @@ -1315,45 +1284,45 @@ files = [ [[package]] name = "ruff" -version = "0.1.13" +version = "0.1.15" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e3fd36e0d48aeac672aa850045e784673449ce619afc12823ea7868fcc41d8ba"}, - {file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9fb6b3b86450d4ec6a6732f9f60c4406061b6851c4b29f944f8c9d91c3611c7a"}, - {file = "ruff-0.1.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b13ba5d7156daaf3fd08b6b993360a96060500aca7e307d95ecbc5bb47a69296"}, - {file = "ruff-0.1.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ebb40442f7b531e136d334ef0851412410061e65d61ca8ce90d894a094feb22"}, - {file = "ruff-0.1.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226b517f42d59a543d6383cfe03cccf0091e3e0ed1b856c6824be03d2a75d3b6"}, - {file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f0312ba1061e9b8c724e9a702d3c8621e3c6e6c2c9bd862550ab2951ac75c16"}, - {file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f59bcf5217c661254bd6bc42d65a6fd1a8b80c48763cb5c2293295babd945dd"}, - {file = "ruff-0.1.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6894b00495e00c27b6ba61af1fc666f17de6140345e5ef27dd6e08fb987259d"}, - {file = "ruff-0.1.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1600942485c6e66119da294c6294856b5c86fd6df591ce293e4a4cc8e72989"}, - {file = "ruff-0.1.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ee3febce7863e231a467f90e681d3d89210b900d49ce88723ce052c8761be8c7"}, - {file = "ruff-0.1.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dcaab50e278ff497ee4d1fe69b29ca0a9a47cd954bb17963628fa417933c6eb1"}, - {file = "ruff-0.1.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f57de973de4edef3ad3044d6a50c02ad9fc2dff0d88587f25f1a48e3f72edf5e"}, - {file = "ruff-0.1.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a36fa90eb12208272a858475ec43ac811ac37e91ef868759770b71bdabe27b6"}, - {file = "ruff-0.1.13-py3-none-win32.whl", hash = "sha256:a623349a505ff768dad6bd57087e2461be8db58305ebd5577bd0e98631f9ae69"}, - {file = "ruff-0.1.13-py3-none-win_amd64.whl", hash = "sha256:f988746e3c3982bea7f824c8fa318ce7f538c4dfefec99cd09c8770bd33e6539"}, - {file = "ruff-0.1.13-py3-none-win_arm64.whl", hash = "sha256:6bbbc3042075871ec17f28864808540a26f0f79a4478c357d3e3d2284e832998"}, - {file = "ruff-0.1.13.tar.gz", hash = "sha256:e261f1baed6291f434ffb1d5c6bd8051d1c2a26958072d38dfbec39b3dda7352"}, + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, + {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, + {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, + {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, + {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, ] [[package]] name = "setuptools" -version = "69.0.3" +version = "69.1.1" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, - {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, + {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, + {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, ] [package.extras] 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"] -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)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +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"] +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"] [[package]] name = "six" @@ -1368,71 +1337,71 @@ files = [ [[package]] name = "sniffio" -version = "1.3.0" +version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] name = "sqlalchemy" -version = "2.0.25" +version = "2.0.27" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4344d059265cc8b1b1be351bfb88749294b87a8b2bbe21dfbe066c4199541ebd"}, - {file = "SQLAlchemy-2.0.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6f9e2e59cbcc6ba1488404aad43de005d05ca56e069477b33ff74e91b6319735"}, - {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84daa0a2055df9ca0f148a64fdde12ac635e30edbca80e87df9b3aaf419e144a"}, - {file = "SQLAlchemy-2.0.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc8b7dabe8e67c4832891a5d322cec6d44ef02f432b4588390017f5cec186a84"}, - {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5693145220517b5f42393e07a6898acdfe820e136c98663b971906120549da5"}, - {file = "SQLAlchemy-2.0.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db854730a25db7c956423bb9fb4bdd1216c839a689bf9cc15fada0a7fb2f4570"}, - {file = "SQLAlchemy-2.0.25-cp310-cp310-win32.whl", hash = "sha256:14a6f68e8fc96e5e8f5647ef6cda6250c780612a573d99e4d881581432ef1669"}, - {file = "SQLAlchemy-2.0.25-cp310-cp310-win_amd64.whl", hash = "sha256:87f6e732bccd7dcf1741c00f1ecf33797383128bd1c90144ac8adc02cbb98643"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:342d365988ba88ada8af320d43df4e0b13a694dbd75951f537b2d5e4cb5cd002"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f37c0caf14b9e9b9e8f6dbc81bc56db06acb4363eba5a633167781a48ef036ed"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa9373708763ef46782d10e950b49d0235bfe58facebd76917d3f5cbf5971aed"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d24f571990c05f6b36a396218f251f3e0dda916e0c687ef6fdca5072743208f5"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:75432b5b14dc2fff43c50435e248b45c7cdadef73388e5610852b95280ffd0e9"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:884272dcd3ad97f47702965a0e902b540541890f468d24bd1d98bcfe41c3f018"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-win32.whl", hash = "sha256:e607cdd99cbf9bb80391f54446b86e16eea6ad309361942bf88318bcd452363c"}, - {file = "SQLAlchemy-2.0.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d505815ac340568fd03f719446a589162d55c52f08abd77ba8964fbb7eb5b5f"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0dacf67aee53b16f365c589ce72e766efaabd2b145f9de7c917777b575e3659d"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b801154027107461ee992ff4b5c09aa7cc6ec91ddfe50d02bca344918c3265c6"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59a21853f5daeb50412d459cfb13cb82c089ad4c04ec208cd14dddd99fc23b39"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29049e2c299b5ace92cbed0c1610a7a236f3baf4c6b66eb9547c01179f638ec5"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b64b183d610b424a160b0d4d880995e935208fc043d0302dd29fee32d1ee3f95"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4f7a7d7fcc675d3d85fbf3b3828ecd5990b8d61bd6de3f1b260080b3beccf215"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-win32.whl", hash = "sha256:cf18ff7fc9941b8fc23437cc3e68ed4ebeff3599eec6ef5eebf305f3d2e9a7c2"}, - {file = "SQLAlchemy-2.0.25-cp312-cp312-win_amd64.whl", hash = "sha256:91f7d9d1c4dd1f4f6e092874c128c11165eafcf7c963128f79e28f8445de82d5"}, - {file = "SQLAlchemy-2.0.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bb209a73b8307f8fe4fe46f6ad5979649be01607f11af1eb94aa9e8a3aaf77f0"}, - {file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:798f717ae7c806d67145f6ae94dc7c342d3222d3b9a311a784f371a4333212c7"}, - {file = "SQLAlchemy-2.0.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fdd402169aa00df3142149940b3bf9ce7dde075928c1886d9a1df63d4b8de62"}, - {file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0d3cab3076af2e4aa5693f89622bef7fa770c6fec967143e4da7508b3dceb9b9"}, - {file = "SQLAlchemy-2.0.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:74b080c897563f81062b74e44f5a72fa44c2b373741a9ade701d5f789a10ba23"}, - {file = "SQLAlchemy-2.0.25-cp37-cp37m-win32.whl", hash = "sha256:87d91043ea0dc65ee583026cb18e1b458d8ec5fc0a93637126b5fc0bc3ea68c4"}, - {file = "SQLAlchemy-2.0.25-cp37-cp37m-win_amd64.whl", hash = "sha256:75f99202324383d613ddd1f7455ac908dca9c2dd729ec8584c9541dd41822a2c"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:420362338681eec03f53467804541a854617faed7272fe71a1bfdb07336a381e"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c88f0c7dcc5f99bdb34b4fd9b69b93c89f893f454f40219fe923a3a2fd11625"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3be4987e3ee9d9a380b66393b77a4cd6d742480c951a1c56a23c335caca4ce3"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2a159111a0f58fb034c93eeba211b4141137ec4b0a6e75789ab7a3ef3c7e7e3"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8b8cb63d3ea63b29074dcd29da4dc6a97ad1349151f2d2949495418fd6e48db9"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:736ea78cd06de6c21ecba7416499e7236a22374561493b456a1f7ffbe3f6cdb4"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-win32.whl", hash = "sha256:10331f129982a19df4284ceac6fe87353ca3ca6b4ca77ff7d697209ae0a5915e"}, - {file = "SQLAlchemy-2.0.25-cp38-cp38-win_amd64.whl", hash = "sha256:c55731c116806836a5d678a70c84cb13f2cedba920212ba7dcad53260997666d"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:605b6b059f4b57b277f75ace81cc5bc6335efcbcc4ccb9066695e515dbdb3900"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:665f0a3954635b5b777a55111ababf44b4fc12b1f3ba0a435b602b6387ffd7cf"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecf6d4cda1f9f6cb0b45803a01ea7f034e2f1aed9475e883410812d9f9e3cfcf"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51db269513917394faec5e5c00d6f83829742ba62e2ac4fa5c98d58be91662f"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:790f533fa5c8901a62b6fef5811d48980adeb2f51f1290ade8b5e7ba990ba3de"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:1b1180cda6df7af84fe72e4530f192231b1f29a7496951db4ff38dac1687202d"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-win32.whl", hash = "sha256:555651adbb503ac7f4cb35834c5e4ae0819aab2cd24857a123370764dc7d7e24"}, - {file = "SQLAlchemy-2.0.25-cp39-cp39-win_amd64.whl", hash = "sha256:dc55990143cbd853a5d038c05e79284baedf3e299661389654551bd02a6a68d7"}, - {file = "SQLAlchemy-2.0.25-py3-none-any.whl", hash = "sha256:a86b4240e67d4753dc3092d9511886795b3c2852abe599cffe108952f7af7ac3"}, - {file = "SQLAlchemy-2.0.25.tar.gz", hash = "sha256:a2c69a7664fb2d54b8682dd774c3b54f67f84fa123cf84dda2a5f40dcaa04e08"}, + {file = "SQLAlchemy-2.0.27-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d04e579e911562f1055d26dab1868d3e0bb905db3bccf664ee8ad109f035618a"}, + {file = "SQLAlchemy-2.0.27-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa67d821c1fd268a5a87922ef4940442513b4e6c377553506b9db3b83beebbd8"}, + {file = "SQLAlchemy-2.0.27-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c7a596d0be71b7baa037f4ac10d5e057d276f65a9a611c46970f012752ebf2d"}, + {file = "SQLAlchemy-2.0.27-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:954d9735ee9c3fa74874c830d089a815b7b48df6f6b6e357a74130e478dbd951"}, + {file = "SQLAlchemy-2.0.27-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5cd20f58c29bbf2680039ff9f569fa6d21453fbd2fa84dbdb4092f006424c2e6"}, + {file = "SQLAlchemy-2.0.27-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:03f448ffb731b48323bda68bcc93152f751436ad6037f18a42b7e16af9e91c07"}, + {file = "SQLAlchemy-2.0.27-cp310-cp310-win32.whl", hash = "sha256:d997c5938a08b5e172c30583ba6b8aad657ed9901fc24caf3a7152eeccb2f1b4"}, + {file = "SQLAlchemy-2.0.27-cp310-cp310-win_amd64.whl", hash = "sha256:eb15ef40b833f5b2f19eeae65d65e191f039e71790dd565c2af2a3783f72262f"}, + {file = "SQLAlchemy-2.0.27-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c5bad7c60a392850d2f0fee8f355953abaec878c483dd7c3836e0089f046bf6"}, + {file = "SQLAlchemy-2.0.27-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3012ab65ea42de1be81fff5fb28d6db893ef978950afc8130ba707179b4284a"}, + {file = "SQLAlchemy-2.0.27-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbcd77c4d94b23e0753c5ed8deba8c69f331d4fd83f68bfc9db58bc8983f49cd"}, + {file = "SQLAlchemy-2.0.27-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d177b7e82f6dd5e1aebd24d9c3297c70ce09cd1d5d37b43e53f39514379c029c"}, + {file = "SQLAlchemy-2.0.27-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:680b9a36029b30cf063698755d277885d4a0eab70a2c7c6e71aab601323cba45"}, + {file = "SQLAlchemy-2.0.27-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1306102f6d9e625cebaca3d4c9c8f10588735ef877f0360b5cdb4fdfd3fd7131"}, + {file = "SQLAlchemy-2.0.27-cp311-cp311-win32.whl", hash = "sha256:5b78aa9f4f68212248aaf8943d84c0ff0f74efc65a661c2fc68b82d498311fd5"}, + {file = "SQLAlchemy-2.0.27-cp311-cp311-win_amd64.whl", hash = "sha256:15e19a84b84528f52a68143439d0c7a3a69befcd4f50b8ef9b7b69d2628ae7c4"}, + {file = "SQLAlchemy-2.0.27-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0de1263aac858f288a80b2071990f02082c51d88335a1db0d589237a3435fe71"}, + {file = "SQLAlchemy-2.0.27-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce850db091bf7d2a1f2fdb615220b968aeff3849007b1204bf6e3e50a57b3d32"}, + {file = "SQLAlchemy-2.0.27-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dfc936870507da96aebb43e664ae3a71a7b96278382bcfe84d277b88e379b18"}, + {file = "SQLAlchemy-2.0.27-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4fbe6a766301f2e8a4519f4500fe74ef0a8509a59e07a4085458f26228cd7cc"}, + {file = "SQLAlchemy-2.0.27-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4535c49d961fe9a77392e3a630a626af5baa967172d42732b7a43496c8b28876"}, + {file = "SQLAlchemy-2.0.27-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0fb3bffc0ced37e5aa4ac2416f56d6d858f46d4da70c09bb731a246e70bff4d5"}, + {file = "SQLAlchemy-2.0.27-cp312-cp312-win32.whl", hash = "sha256:7f470327d06400a0aa7926b375b8e8c3c31d335e0884f509fe272b3c700a7254"}, + {file = "SQLAlchemy-2.0.27-cp312-cp312-win_amd64.whl", hash = "sha256:f9374e270e2553653d710ece397df67db9d19c60d2647bcd35bfc616f1622dcd"}, + {file = "SQLAlchemy-2.0.27-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e97cf143d74a7a5a0f143aa34039b4fecf11343eed66538610debc438685db4a"}, + {file = "SQLAlchemy-2.0.27-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7b5a3e2120982b8b6bd1d5d99e3025339f7fb8b8267551c679afb39e9c7c7f1"}, + {file = "SQLAlchemy-2.0.27-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e36aa62b765cf9f43a003233a8c2d7ffdeb55bc62eaa0a0380475b228663a38f"}, + {file = "SQLAlchemy-2.0.27-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5ada0438f5b74c3952d916c199367c29ee4d6858edff18eab783b3978d0db16d"}, + {file = "SQLAlchemy-2.0.27-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b1d9d1bfd96eef3c3faedb73f486c89e44e64e40e5bfec304ee163de01cf996f"}, + {file = "SQLAlchemy-2.0.27-cp37-cp37m-win32.whl", hash = "sha256:ca891af9f3289d24a490a5fde664ea04fe2f4984cd97e26de7442a4251bd4b7c"}, + {file = "SQLAlchemy-2.0.27-cp37-cp37m-win_amd64.whl", hash = "sha256:fd8aafda7cdff03b905d4426b714601c0978725a19efc39f5f207b86d188ba01"}, + {file = "SQLAlchemy-2.0.27-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec1f5a328464daf7a1e4e385e4f5652dd9b1d12405075ccba1df842f7774b4fc"}, + {file = "SQLAlchemy-2.0.27-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ad862295ad3f644e3c2c0d8b10a988e1600d3123ecb48702d2c0f26771f1c396"}, + {file = "SQLAlchemy-2.0.27-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48217be1de7d29a5600b5c513f3f7664b21d32e596d69582be0a94e36b8309cb"}, + {file = "SQLAlchemy-2.0.27-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e56afce6431450442f3ab5973156289bd5ec33dd618941283847c9fd5ff06bf"}, + {file = "SQLAlchemy-2.0.27-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:611068511b5531304137bcd7fe8117c985d1b828eb86043bd944cebb7fae3910"}, + {file = "SQLAlchemy-2.0.27-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b86abba762ecfeea359112b2bb4490802b340850bbee1948f785141a5e020de8"}, + {file = "SQLAlchemy-2.0.27-cp38-cp38-win32.whl", hash = "sha256:30d81cc1192dc693d49d5671cd40cdec596b885b0ce3b72f323888ab1c3863d5"}, + {file = "SQLAlchemy-2.0.27-cp38-cp38-win_amd64.whl", hash = "sha256:120af1e49d614d2525ac247f6123841589b029c318b9afbfc9e2b70e22e1827d"}, + {file = "SQLAlchemy-2.0.27-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d07ee7793f2aeb9b80ec8ceb96bc8cc08a2aec8a1b152da1955d64e4825fcbac"}, + {file = "SQLAlchemy-2.0.27-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb0845e934647232b6ff5150df37ceffd0b67b754b9fdbb095233deebcddbd4a"}, + {file = "SQLAlchemy-2.0.27-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fc19ae2e07a067663dd24fca55f8ed06a288384f0e6e3910420bf4b1270cc51"}, + {file = "SQLAlchemy-2.0.27-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b90053be91973a6fb6020a6e44382c97739736a5a9d74e08cc29b196639eb979"}, + {file = "SQLAlchemy-2.0.27-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2f5c9dfb0b9ab5e3a8a00249534bdd838d943ec4cfb9abe176a6c33408430230"}, + {file = "SQLAlchemy-2.0.27-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33e8bde8fff203de50399b9039c4e14e42d4d227759155c21f8da4a47fc8053c"}, + {file = "SQLAlchemy-2.0.27-cp39-cp39-win32.whl", hash = "sha256:d873c21b356bfaf1589b89090a4011e6532582b3a8ea568a00e0c3aab09399dd"}, + {file = "SQLAlchemy-2.0.27-cp39-cp39-win_amd64.whl", hash = "sha256:ff2f1b7c963961d41403b650842dc2039175b906ab2093635d8319bef0b7d620"}, + {file = "SQLAlchemy-2.0.27-py3-none-any.whl", hash = "sha256:1ab4e0448018d01b142c916cc7119ca573803a4745cfe341b8f95657812700ac"}, + {file = "SQLAlchemy-2.0.27.tar.gz", hash = "sha256:86a6ed69a71fe6b88bf9331594fa390a2adda4a49b5c06f98e47bf0d392534f8"}, ] [package.dependencies] @@ -1466,20 +1435,20 @@ sqlcipher = ["sqlcipher3_binary"] [[package]] name = "starlette" -version = "0.35.1" +version = "0.36.3" description = "The little ASGI library that shines." optional = false python-versions = ">=3.8" files = [ - {file = "starlette-0.35.1-py3-none-any.whl", hash = "sha256:50bbbda9baa098e361f398fda0928062abbaf1f54f4fadcbe17c092a01eb9a25"}, - {file = "starlette-0.35.1.tar.gz", hash = "sha256:3e2639dac3520e4f58734ed22553f950d3f3cb1001cd2eaac4d57e8cdc5f66bc"}, + {file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"}, + {file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"}, ] [package.dependencies] anyio = ">=3.4.0,<5" [package.extras] -full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] [[package]] name = "types-passlib" @@ -1494,13 +1463,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.9.0" +version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] @@ -1574,13 +1543,13 @@ test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)" [[package]] name = "virtualenv" -version = "20.25.0" +version = "20.25.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, - {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, + {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, + {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, ] [package.dependencies] @@ -1780,54 +1749,54 @@ test = ["zope.testrunner"] [[package]] name = "zope-interface" -version = "6.1" +version = "6.2" description = "Interfaces for Python" optional = false python-versions = ">=3.7" files = [ - {file = "zope.interface-6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:43b576c34ef0c1f5a4981163b551a8781896f2a37f71b8655fd20b5af0386abb"}, - {file = "zope.interface-6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:67be3ca75012c6e9b109860820a8b6c9a84bfb036fbd1076246b98e56951ca92"}, - {file = "zope.interface-6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b9bc671626281f6045ad61d93a60f52fd5e8209b1610972cf0ef1bbe6d808e3"}, - {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe81def9cf3e46f16ce01d9bfd8bea595e06505e51b7baf45115c77352675fd"}, - {file = "zope.interface-6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6dc998f6de015723196a904045e5a2217f3590b62ea31990672e31fbc5370b41"}, - {file = "zope.interface-6.1-cp310-cp310-win_amd64.whl", hash = "sha256:239a4a08525c080ff833560171d23b249f7f4d17fcbf9316ef4159f44997616f"}, - {file = "zope.interface-6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ffdaa5290422ac0f1688cb8adb1b94ca56cee3ad11f29f2ae301df8aecba7d1"}, - {file = "zope.interface-6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34c15ca9248f2e095ef2e93af2d633358c5f048c49fbfddf5fdfc47d5e263736"}, - {file = "zope.interface-6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b012d023b4fb59183909b45d7f97fb493ef7a46d2838a5e716e3155081894605"}, - {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97806e9ca3651588c1baaebb8d0c5ee3db95430b612db354c199b57378312ee8"}, - {file = "zope.interface-6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fddbab55a2473f1d3b8833ec6b7ac31e8211b0aa608df5ab09ce07f3727326de"}, - {file = "zope.interface-6.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0da79117952a9a41253696ed3e8b560a425197d4e41634a23b1507efe3273f1"}, - {file = "zope.interface-6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8bb9c990ca9027b4214fa543fd4025818dc95f8b7abce79d61dc8a2112b561a"}, - {file = "zope.interface-6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51b64432eed4c0744241e9ce5c70dcfecac866dff720e746d0a9c82f371dfa7"}, - {file = "zope.interface-6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa6fd016e9644406d0a61313e50348c706e911dca29736a3266fc9e28ec4ca6d"}, - {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0c8cf55261e15590065039696607f6c9c1aeda700ceee40c70478552d323b3ff"}, - {file = "zope.interface-6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e30506bcb03de8983f78884807e4fd95d8db6e65b69257eea05d13d519b83ac0"}, - {file = "zope.interface-6.1-cp312-cp312-win_amd64.whl", hash = "sha256:e33e86fd65f369f10608b08729c8f1c92ec7e0e485964670b4d2633a4812d36b"}, - {file = "zope.interface-6.1-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:2f8d89721834524a813f37fa174bac074ec3d179858e4ad1b7efd4401f8ac45d"}, - {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13b7d0f2a67eb83c385880489dbb80145e9d344427b4262c49fbf2581677c11c"}, - {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef43ee91c193f827e49599e824385ec7c7f3cd152d74cb1dfe02cb135f264d83"}, - {file = "zope.interface-6.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e441e8b7d587af0414d25e8d05e27040d78581388eed4c54c30c0c91aad3a379"}, - {file = "zope.interface-6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89b28772fc2562ed9ad871c865f5320ef761a7fcc188a935e21fe8b31a38ca9"}, - {file = "zope.interface-6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:70d2cef1bf529bff41559be2de9d44d47b002f65e17f43c73ddefc92f32bf00f"}, - {file = "zope.interface-6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ad54ed57bdfa3254d23ae04a4b1ce405954969c1b0550cc2d1d2990e8b439de1"}, - {file = "zope.interface-6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef467d86d3cfde8b39ea1b35090208b0447caaabd38405420830f7fd85fbdd56"}, - {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6af47f10cfc54c2ba2d825220f180cc1e2d4914d783d6fc0cd93d43d7bc1c78b"}, - {file = "zope.interface-6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9559138690e1bd4ea6cd0954d22d1e9251e8025ce9ede5d0af0ceae4a401e43"}, - {file = "zope.interface-6.1-cp38-cp38-win_amd64.whl", hash = "sha256:964a7af27379ff4357dad1256d9f215047e70e93009e532d36dcb8909036033d"}, - {file = "zope.interface-6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:387545206c56b0315fbadb0431d5129c797f92dc59e276b3ce82db07ac1c6179"}, - {file = "zope.interface-6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:57d0a8ce40ce440f96a2c77824ee94bf0d0925e6089df7366c2272ccefcb7941"}, - {file = "zope.interface-6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ebc4d34e7620c4f0da7bf162c81978fce0ea820e4fa1e8fc40ee763839805f3"}, - {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a804abc126b33824a44a7aa94f06cd211a18bbf31898ba04bd0924fbe9d282d"}, - {file = "zope.interface-6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f294a15f7723fc0d3b40701ca9b446133ec713eafc1cc6afa7b3d98666ee1ac"}, - {file = "zope.interface-6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a41f87bb93b8048fe866fa9e3d0c51e27fe55149035dcf5f43da4b56732c0a40"}, - {file = "zope.interface-6.1.tar.gz", hash = "sha256:2fdc7ccbd6eb6b7df5353012fbed6c3c5d04ceaca0038f75e601060e95345309"}, + {file = "zope.interface-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:506f5410b36e5ba494136d9fa04c548eaf1a0d9c442b0b0e7a0944db7620e0ab"}, + {file = "zope.interface-6.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b386b8b9d2b6a5e1e4eadd4e62335571244cb9193b7328c2b6e38b64cfda4f0e"}, + {file = "zope.interface-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb0b3f2cb606981c7432f690db23506b1db5899620ad274e29dbbbdd740e797"}, + {file = "zope.interface-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7916380abaef4bb4891740879b1afcba2045aee51799dfd6d6ca9bdc71f35f"}, + {file = "zope.interface-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b240883fb43160574f8f738e6d09ddbdbf8fa3e8cea051603d9edfd947d9328"}, + {file = "zope.interface-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:8af82afc5998e1f307d5e72712526dba07403c73a9e287d906a8aa2b1f2e33dd"}, + {file = "zope.interface-6.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d45d2ba8195850e3e829f1f0016066a122bfa362cc9dc212527fc3d51369037"}, + {file = "zope.interface-6.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76e0531d86523be7a46e15d379b0e975a9db84316617c0efe4af8338dc45b80c"}, + {file = "zope.interface-6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59f7374769b326a217d0b2366f1c176a45a4ff21e8f7cebb3b4a3537077eff85"}, + {file = "zope.interface-6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25e0af9663eeac6b61b231b43c52293c2cb7f0c232d914bdcbfd3e3bd5c182ad"}, + {file = "zope.interface-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14e02a6fc1772b458ebb6be1c276528b362041217b9ca37e52ecea2cbdce9fac"}, + {file = "zope.interface-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:02adbab560683c4eca3789cc0ac487dcc5f5a81cc48695ec247f00803cafe2fe"}, + {file = "zope.interface-6.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8f5d2c39f3283e461de3655e03faf10e4742bb87387113f787a7724f32db1e48"}, + {file = "zope.interface-6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:75d2ec3d9b401df759b87bc9e19d1b24db73083147089b43ae748aefa63067ef"}, + {file = "zope.interface-6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa994e8937e8ccc7e87395b7b35092818905cf27c651e3ff3e7f29729f5ce3ce"}, + {file = "zope.interface-6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ede888382882f07b9e4cd942255921ffd9f2901684198b88e247c7eabd27a000"}, + {file = "zope.interface-6.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2606955a06c6852a6cff4abeca38346ed01e83f11e960caa9a821b3626a4467b"}, + {file = "zope.interface-6.2-cp312-cp312-win_amd64.whl", hash = "sha256:ac7c2046d907e3b4e2605a130d162b1b783c170292a11216479bb1deb7cadebe"}, + {file = "zope.interface-6.2-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:febceb04ee7dd2aef08c2ff3d6f8a07de3052fc90137c507b0ede3ea80c21440"}, + {file = "zope.interface-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fc711acc4a1c702ca931fdbf7bf7c86f2a27d564c85c4964772dadf0e3c52f5"}, + {file = "zope.interface-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:396f5c94654301819a7f3a702c5830f0ea7468d7b154d124ceac823e2419d000"}, + {file = "zope.interface-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd374927c00764fcd6fe1046bea243ebdf403fba97a937493ae4be2c8912c2b"}, + {file = "zope.interface-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a3046e8ab29b590d723821d0785598e0b2e32b636a0272a38409be43e3ae0550"}, + {file = "zope.interface-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:de125151a53ecdb39df3cb3deb9951ed834dd6a110a9e795d985b10bb6db4532"}, + {file = "zope.interface-6.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f444de0565db46d26c9fa931ca14f497900a295bd5eba480fc3fad25af8c763e"}, + {file = "zope.interface-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2fefad268ff5c5b314794e27e359e48aeb9c8bb2cbb5748a071757a56f6bb8f"}, + {file = "zope.interface-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:97785604824981ec8c81850dd25c8071d5ce04717a34296eeac771231fbdd5cd"}, + {file = "zope.interface-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7b2bed4eea047a949296e618552d3fed00632dc1b795ee430289bdd0e3717f3"}, + {file = "zope.interface-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:d54f66c511ea01b9ef1d1a57420a93fbb9d48a08ec239f7d9c581092033156d0"}, + {file = "zope.interface-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5ee9789a20b0081dc469f65ff6c5007e67a940d5541419ca03ef20c6213dd099"}, + {file = "zope.interface-6.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af27b3fe5b6bf9cd01b8e1c5ddea0a0d0a1b8c37dc1c7452f1e90bf817539c6d"}, + {file = "zope.interface-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bce517b85f5debe07b186fc7102b332676760f2e0c92b7185dd49c138734b70"}, + {file = "zope.interface-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ae9793f114cee5c464cc0b821ae4d36e1eba961542c6086f391a61aee167b6f"}, + {file = "zope.interface-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e87698e2fea5ca2f0a99dff0a64ce8110ea857b640de536c76d92aaa2a91ff3a"}, + {file = "zope.interface-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:b66335bbdbb4c004c25ae01cc4a54fd199afbc1fd164233813c6d3c2293bb7e1"}, + {file = "zope.interface-6.2.tar.gz", hash = "sha256:3b6c62813c63c543a06394a636978b22dffa8c5410affc9331ce6cdb5bfa8565"}, ] [package.dependencies] setuptools = "*" [package.extras] -docs = ["Sphinx", "repoze.sphinx.autointerface", "sphinx-rtd-theme"] +docs = ["Sphinx", "repoze.sphinx.autointerface", "sphinx_rtd_theme"] test = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] From c45731c253e5aaadaac8b78ed8ed6913052794f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Sat, 2 Mar 2024 18:35:03 +0100 Subject: [PATCH 09/41] auth token test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/api/endpoints/auth.py | 15 ++-- .../test_auth/test_auth_refresh_token.py | 71 +++++++++++++++++++ 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index f6a36de..f72ce61 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -3,7 +3,7 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm -from sqlalchemy import delete, select +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.api import deps @@ -56,27 +56,20 @@ async def login_access_token( @router.post("/refresh-token", response_model=AccessTokenResponse) async def refresh_token( - input: RefreshTokenRequest, + data: RefreshTokenRequest, session: AsyncSession = Depends(deps.get_session), ) -> AccessTokenResponse: """OAuth2 compatible token, get an access token for future requests using refresh token""" result = await session.execute( - select(RefreshToken).where(RefreshToken.refresh_token == input.refresh_token) + select(RefreshToken).where(RefreshToken.refresh_token == data.refresh_token) ) token = result.scalars().first() - if token is None or time.time() > token.exp: + if token is None or time.time() > token.exp or token.used: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Token not found" ) - if token.used: - await session.execute( - delete(RefreshToken).where(RefreshToken.user_id == token.user_id) - ) - raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" - ) token.used = True session.add(token) diff --git a/app/tests/test_auth/test_auth_refresh_token.py b/app/tests/test_auth/test_auth_refresh_token.py index e69de29..6e0d6e3 100644 --- a/app/tests/test_auth/test_auth_refresh_token.py +++ b/app/tests/test_auth/test_auth_refresh_token.py @@ -0,0 +1,71 @@ +import time + +from fastapi import status +from httpx import AsyncClient +from sqlalchemy.ext.asyncio import AsyncSession + +from app.main import app +from app.models import RefreshToken, User + + +async def test_refresh_token_fails_with_message_when_token_does_not_exist( + client: AsyncClient, +) -> None: + response = await client.post( + app.url_path_for("refresh_token"), + json={ + "refresh_token": "blaxx", + }, + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.json() == {"detail": "Token not found"} + + +async def test_refresh_token_fails_with_message_when_token_is_expired( + client: AsyncClient, + default_user: User, + session: AsyncSession, +) -> None: + test_refresh_token = RefreshToken( + user_id=default_user.user_id, + refresh_token="blaxx", + exp=int(time.time()) - 1, + ) + session.add(test_refresh_token) + await session.commit() + + response = await client.post( + app.url_path_for("refresh_token"), + json={ + "refresh_token": "blaxx", + }, + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.json() == {"detail": "Token not found"} + + +async def test_refresh_token_fails_with_message_when_token_is_used( + client: AsyncClient, + default_user: User, + session: AsyncSession, +) -> None: + test_refresh_token = RefreshToken( + user_id=default_user.user_id, + refresh_token="blaxx", + exp=int(time.time()) + 1000, + used=True, + ) + session.add(test_refresh_token) + await session.commit() + + response = await client.post( + app.url_path_for("refresh_token"), + json={ + "refresh_token": "blaxx", + }, + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.json() == {"detail": "Token not found"} From 3fddcf300c5e9ecc66abf8f0b321f0427a0cec40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Sat, 2 Mar 2024 23:39:26 +0100 Subject: [PATCH 10/41] refactor conftest, use proper transaction rollback after tests! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/api/deps.py | 4 +- app/core/database_session.py | 21 ++++++++ app/core/session.py | 14 ----- app/tests/conftest.py | 101 ++++++++++++++++++++++------------- 4 files changed, 88 insertions(+), 52 deletions(-) create mode 100644 app/core/database_session.py delete mode 100644 app/core/session.py diff --git a/app/api/deps.py b/app/api/deps.py index a1e6838..f1b1c8d 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -6,15 +6,15 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from app.core import database_session from app.core.security.jwt import verify_jwt_token -from app.core.session import async_session from app.models import User oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/access-token") async def get_session() -> AsyncGenerator[AsyncSession, None]: - async with async_session() as session: + async with database_session.ASYNC_SESSIONMAKER() as session: yield session diff --git a/app/core/database_session.py b/app/core/database_session.py new file mode 100644 index 0000000..87869c7 --- /dev/null +++ b/app/core/database_session.py @@ -0,0 +1,21 @@ +# SQLAlchemy async engine and sessions tools +# +# https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html +# +# for pool size configuration: +# https://docs.sqlalchemy.org/en/20/core/pooling.html#sqlalchemy.pool.Pool + + +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine + +from app.core.config import get_settings + +ASYNC_ENGINE = create_async_engine( + get_settings().sqlalchemy_database_uri, + pool_pre_ping=True, + pool_size=5, + max_overflow=10, + pool_timeout=30.0, + pool_recycle=600, +) +ASYNC_SESSIONMAKER = async_sessionmaker(ASYNC_ENGINE, expire_on_commit=False) diff --git a/app/core/session.py b/app/core/session.py deleted file mode 100644 index 202d9b6..0000000 --- a/app/core/session.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -SQLAlchemy async engine and sessions tools - -https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html -""" - -from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine - -from app.core.config import get_settings - -async_engine = create_async_engine( - get_settings().sqlalchemy_database_uri, pool_pre_ping=True -) -async_session = async_sessionmaker(async_engine, expire_on_commit=False) diff --git a/app/tests/conftest.py b/app/tests/conftest.py index b8f6267..4253c6f 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -1,16 +1,19 @@ import asyncio from collections.abc import AsyncGenerator, Generator +from typing import Any import pytest import pytest_asyncio from httpx import AsyncClient -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.ext.asyncio import ( + AsyncSession, + async_sessionmaker, +) +from app.core import database_session from app.core.security.jwt import create_jwt_token from app.core.security.password import get_password_hash -from app.core.session import async_engine, async_session -from app.main import app +from app.main import app as fastapi_app from app.models import Base, User default_user_id = "b75365d9-7bf9-4f54-add5-aeab333a087b" @@ -28,51 +31,77 @@ def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]: loop.close() -@pytest_asyncio.fixture(scope="session") -async def test_db_setup_sessionmaker() -> None: +@pytest_asyncio.fixture(scope="session", autouse=True) +async def fixture_db_setup_tables_and_start_broad_connection() -> None: # always drop and create test db tables between tests session - async with async_engine.begin() as conn: + async with database_session.ASYNC_ENGINE.begin() as conn: await conn.run_sync(Base.metadata.drop_all) await conn.run_sync(Base.metadata.create_all) -@pytest_asyncio.fixture(autouse=True) -async def session( - test_db_setup_sessionmaker: None, +@pytest_asyncio.fixture(scope="function") +async def fixture_mock_async_session_factory( + fixture_db_setup_tables_and_start_broad_connection: None, + monkeypatch: pytest.MonkeyPatch, +) -> AsyncGenerator[None, None]: + # we want to monkeypatch sessionmaker with one bound to session + # that we will rollback on function scope + async with database_session.ASYNC_ENGINE.connect() as conn: + async with conn.begin() as transaction: + session = AsyncSession(bind=conn, expire_on_commit=False) + + # trick sessionmaker instance to use our crafted session + # that will have rollback on the end of each test + # note, magic methods goes directly to class __call__ definition + # so we need ugly hack with class overwrite + # maybe it can be done better + class mock_async_sessionmaker(async_sessionmaker): + def __call__(self, **local_kw: Any) -> Any: + return session + + session_factory_mock = mock_async_sessionmaker( + bind=database_session.ASYNC_ENGINE, expire_on_commit=False + ) + monkeypatch.setattr( + database_session, + "ASYNC_SESSIONMAKER", + session_factory_mock, + ) + + yield + + await session.close() + await transaction.rollback() + + +@pytest_asyncio.fixture(name="session") +async def fixture_session( + fixture_mock_async_session_factory: None, ) -> AsyncGenerator[AsyncSession, None]: - async with async_session() as session: + async with database_session.ASYNC_SESSIONMAKER() as session: yield session - await session.rollback() - await session.close() -@pytest_asyncio.fixture(scope="session") -async def client() -> AsyncGenerator[AsyncClient, None]: - async with AsyncClient(app=app, base_url="http://test") as client: +@pytest_asyncio.fixture(name="client") +async def fixture_client(session: AsyncSession) -> AsyncGenerator[AsyncClient, None]: + async with AsyncClient(app=fastapi_app, base_url="http://test") as client: client.headers.update({"Host": "localhost"}) yield client -@pytest_asyncio.fixture -async def default_user(test_db_setup_sessionmaker: None) -> User: - async with async_session() as session: - result = await session.execute( - select(User).where(User.email == default_user_email) - ) - user = result.scalars().first() - if user is None: - new_user = User( - email=default_user_email, - hashed_password=default_user_password_hash, - ) - new_user.user_id = default_user_id - session.add(new_user) - await session.commit() - await session.refresh(new_user) - return new_user - return user +@pytest_asyncio.fixture(name="default_user") +async def fixture_default_user(session: AsyncSession) -> User: + default_user = User( + user_id=default_user_id, + email=default_user_email, + hashed_password=default_user_password_hash, + ) + session.add(default_user) + await session.commit() + await session.refresh(default_user) + return default_user -@pytest.fixture -def default_user_headers(default_user: User) -> dict[str, str]: +@pytest.fixture(name="default_user_headers") +def fixture_default_user_headers(default_user: User) -> dict[str, str]: return {"Authorization": f"Bearer {default_user_access_token}"} From a8bfdc3ba97fc65bd507243b0b74cdb794b985a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Sun, 3 Mar 2024 01:54:37 +0100 Subject: [PATCH 11/41] remove test database, make conftest fixture_setup_new_test_database MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/api/deps.py | 2 +- app/core/database_session.py | 35 +++++++--- app/tests/conftest.py | 122 +++++++++++++++++++++-------------- app/tests/test_users.py | 117 +++++++++++++++++---------------- docker-compose.yml | 17 +---- poetry.lock | 35 +++++++--- pyproject.toml | 12 +--- 7 files changed, 187 insertions(+), 153 deletions(-) diff --git a/app/api/deps.py b/app/api/deps.py index f1b1c8d..9a0c50c 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -14,7 +14,7 @@ async def get_session() -> AsyncGenerator[AsyncSession, None]: - async with database_session.ASYNC_SESSIONMAKER() as session: + async with database_session.get_async_session() as session: yield session diff --git a/app/core/database_session.py b/app/core/database_session.py index 87869c7..ebf0a1e 100644 --- a/app/core/database_session.py +++ b/app/core/database_session.py @@ -6,16 +6,31 @@ # https://docs.sqlalchemy.org/en/20/core/pooling.html#sqlalchemy.pool.Pool -from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine +from sqlalchemy.engine.url import URL +from sqlalchemy.ext.asyncio import ( + AsyncEngine, + AsyncSession, + async_sessionmaker, + create_async_engine, +) from app.core.config import get_settings -ASYNC_ENGINE = create_async_engine( - get_settings().sqlalchemy_database_uri, - pool_pre_ping=True, - pool_size=5, - max_overflow=10, - pool_timeout=30.0, - pool_recycle=600, -) -ASYNC_SESSIONMAKER = async_sessionmaker(ASYNC_ENGINE, expire_on_commit=False) + +def new_async_engine(uri: URL) -> AsyncEngine: + return create_async_engine( + uri, + pool_pre_ping=True, + pool_size=5, + max_overflow=10, + pool_timeout=30.0, + pool_recycle=600, + ) + + +_ASYNC_ENGINE = new_async_engine(get_settings().sqlalchemy_database_uri) +_ASYNC_SESSIONMAKER = async_sessionmaker(_ASYNC_ENGINE, expire_on_commit=False) + + +def get_async_session() -> AsyncSession: + return _ASYNC_SESSIONMAKER() diff --git a/app/tests/conftest.py b/app/tests/conftest.py index 4253c6f..f997a7f 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -1,9 +1,10 @@ import asyncio +import os from collections.abc import AsyncGenerator, Generator -from typing import Any import pytest import pytest_asyncio +import sqlalchemy from httpx import AsyncClient from sqlalchemy.ext.asyncio import ( AsyncSession, @@ -11,6 +12,7 @@ ) from app.core import database_session +from app.core.config import get_settings from app.core.security.jwt import create_jwt_token from app.core.security.password import get_password_hash from app.main import app as fastapi_app @@ -19,7 +21,6 @@ default_user_id = "b75365d9-7bf9-4f54-add5-aeab333a087b" default_user_email = "geralt@wiedzmin.pl" default_user_password = "geralt" -default_user_password_hash = get_password_hash(default_user_password) default_user_access_token = create_jwt_token(default_user_id) @@ -32,69 +33,90 @@ def event_loop() -> Generator[asyncio.AbstractEventLoop, None, None]: @pytest_asyncio.fixture(scope="session", autouse=True) -async def fixture_db_setup_tables_and_start_broad_connection() -> None: - # always drop and create test db tables between tests session - async with database_session.ASYNC_ENGINE.begin() as conn: - await conn.run_sync(Base.metadata.drop_all) +async def fixture_setup_new_test_database() -> None: + worker_name = os.getenv("PYTEST_XDIST_WORKER", "gw0") + test_db_name = f"test_db_{worker_name}" + + # create new test db using connection to current database + conn = await database_session._ASYNC_ENGINE.connect() + await conn.execution_options(isolation_level="AUTOCOMMIT") + await conn.execute(sqlalchemy.text(f"DROP DATABASE IF EXISTS {test_db_name}")) + await conn.execute(sqlalchemy.text(f"CREATE DATABASE {test_db_name}")) + await conn.close() + + session_mpatch = pytest.MonkeyPatch() + session_mpatch.setenv("DATABASE__DB", test_db_name) + session_mpatch.setenv("SECURITY__PASSWORD_BCRYPT_ROUNDS", "4") + + # force settings to use now monkeypatched environments + get_settings.cache_clear() + + # monkeypatch test database engine + engine = database_session.new_async_engine( + get_settings().sqlalchemy_database_uri.set(database=test_db_name) + ) + + session_mpatch.setattr( + database_session, + "_ASYNC_ENGINE", + engine, + ) + session_mpatch.setattr( + database_session, + "_ASYNC_SESSIONMAKER", + async_sessionmaker(engine, expire_on_commit=False), + ) + + # create app tables in test database + async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) -@pytest_asyncio.fixture(scope="function") -async def fixture_mock_async_session_factory( - fixture_db_setup_tables_and_start_broad_connection: None, +@pytest_asyncio.fixture(name="default_hashed_password", scope="session") +async def fixture_default_hashed_password() -> str: + return get_password_hash(default_user_password) + + +@pytest_asyncio.fixture(name="session", scope="function") +async def fixture_session_with_rollback( monkeypatch: pytest.MonkeyPatch, -) -> AsyncGenerator[None, None]: - # we want to monkeypatch sessionmaker with one bound to session - # that we will rollback on function scope - async with database_session.ASYNC_ENGINE.connect() as conn: - async with conn.begin() as transaction: - session = AsyncSession(bind=conn, expire_on_commit=False) - - # trick sessionmaker instance to use our crafted session - # that will have rollback on the end of each test - # note, magic methods goes directly to class __call__ definition - # so we need ugly hack with class overwrite - # maybe it can be done better - class mock_async_sessionmaker(async_sessionmaker): - def __call__(self, **local_kw: Any) -> Any: - return session - - session_factory_mock = mock_async_sessionmaker( - bind=database_session.ASYNC_ENGINE, expire_on_commit=False - ) - monkeypatch.setattr( - database_session, - "ASYNC_SESSIONMAKER", - session_factory_mock, - ) - - yield - - await session.close() - await transaction.rollback() - - -@pytest_asyncio.fixture(name="session") -async def fixture_session( - fixture_mock_async_session_factory: None, ) -> AsyncGenerator[AsyncSession, None]: - async with database_session.ASYNC_SESSIONMAKER() as session: - yield session + # we want to monkeypatch get_async_session with one bound to session + # that we will always rollback on function scope + + connection = await database_session._ASYNC_ENGINE.connect() + transaction = await connection.begin() + + session = AsyncSession(bind=connection, expire_on_commit=False) + + monkeypatch.setattr( + database_session, + "get_async_session", + lambda: session, + ) + + yield session + + await session.close() + await transaction.rollback() + await connection.close() -@pytest_asyncio.fixture(name="client") +@pytest_asyncio.fixture(name="client", scope="function") async def fixture_client(session: AsyncSession) -> AsyncGenerator[AsyncClient, None]: async with AsyncClient(app=fastapi_app, base_url="http://test") as client: client.headers.update({"Host": "localhost"}) yield client -@pytest_asyncio.fixture(name="default_user") -async def fixture_default_user(session: AsyncSession) -> User: +@pytest_asyncio.fixture(name="default_user", scope="function") +async def fixture_default_user( + session: AsyncSession, default_hashed_password: str +) -> User: default_user = User( user_id=default_user_id, email=default_user_email, - hashed_password=default_user_password_hash, + hashed_password=default_hashed_password, ) session.add(default_user) await session.commit() @@ -102,6 +124,6 @@ async def fixture_default_user(session: AsyncSession) -> User: return default_user -@pytest.fixture(name="default_user_headers") +@pytest.fixture(name="default_user_headers", scope="function") def fixture_default_user_headers(default_user: User) -> dict[str, str]: return {"Authorization": f"Bearer {default_user_access_token}"} diff --git a/app/tests/test_users.py b/app/tests/test_users.py index 8ab8626..f18f49b 100644 --- a/app/tests/test_users.py +++ b/app/tests/test_users.py @@ -1,68 +1,67 @@ -from httpx import AsyncClient, codes -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession +# from httpx import AsyncClient, codes +# from sqlalchemy import select +# from sqlalchemy.ext.asyncio import AsyncSession -from app.main import app -from app.models import User -from app.tests.conftest import ( - default_user_email, - default_user_id, - default_user_password_hash, -) +# from app.main import app +# from app.models import User +# from app.tests.conftest import ( +# default_user_email, +# default_user_id, +# ) -async def test_read_current_user( - client: AsyncClient, default_user_headers: dict[str, str] -) -> None: - response = await client.get( - app.url_path_for("read_current_user"), headers=default_user_headers - ) - assert response.status_code == codes.OK - assert response.json() == { - "id": default_user_id, - "email": default_user_email, - } +# async def test_read_current_user( +# client: AsyncClient, default_user_headers: dict[str, str] +# ) -> None: +# response = await client.get( +# app.url_path_for("read_current_user"), headers=default_user_headers +# ) +# assert response.status_code == codes.OK +# assert response.json() == { +# "id": default_user_id, +# "email": default_user_email, +# } -async def test_delete_current_user( - client: AsyncClient, default_user_headers: dict[str, str], session: AsyncSession -) -> None: - response = await client.delete( - app.url_path_for("delete_current_user"), headers=default_user_headers - ) - assert response.status_code == codes.NO_CONTENT - result = await session.execute(select(User).where(User.user_id == default_user_id)) - user = result.scalars().first() - assert user is None +# async def test_delete_current_user( +# client: AsyncClient, default_user_headers: dict[str, str], session: AsyncSession +# ) -> None: +# response = await client.delete( +# app.url_path_for("delete_current_user"), headers=default_user_headers +# ) +# assert response.status_code == codes.NO_CONTENT +# result = await session.execute(select(User).where(User.user_id == default_user_id)) +# user = result.scalars().first() +# assert user is None -async def test_reset_current_user_password( - client: AsyncClient, default_user_headers: dict[str, str], session: AsyncSession -) -> None: - response = await client.post( - app.url_path_for("reset_current_user_password"), - headers=default_user_headers, - json={"password": "testxxxxxx"}, - ) - assert response.status_code == codes.OK - result = await session.execute(select(User).where(User.user_id == default_user_id)) - user = result.scalars().first() - assert user is not None - assert user.hashed_password != default_user_password_hash +# async def test_reset_current_user_password( +# client: AsyncClient, default_user_headers: dict[str, str], session: AsyncSession +# ) -> None: +# response = await client.post( +# app.url_path_for("reset_current_user_password"), +# headers=default_user_headers, +# json={"password": "testxxxxxx"}, +# ) +# assert response.status_code == codes.OK +# result = await session.execute(select(User).where(User.user_id == default_user_id)) +# user = result.scalars().first() +# assert user is not None +# assert user.hashed_password != default_user_password_hash -async def test_register_new_user( - client: AsyncClient, default_user_headers: dict[str, str], session: AsyncSession -) -> None: - response = await client.post( - app.url_path_for("register_new_user"), - headers=default_user_headers, - json={ - "email": "qwe@example.com", - "password": "asdasdasd", - }, - ) - assert response.status_code == codes.OK - result = await session.execute(select(User).where(User.email == "qwe@example.com")) - user = result.scalars().first() - assert user is not None +# async def test_register_new_user( +# client: AsyncClient, default_user_headers: dict[str, str], session: AsyncSession +# ) -> None: +# response = await client.post( +# app.url_path_for("register_new_user"), +# headers=default_user_headers, +# json={ +# "email": "qwe@example.com", +# "password": "asdasdasd", +# }, +# ) +# assert response.status_code == codes.OK +# result = await session.execute(select(User).where(User.email == "qwe@example.com")) +# user = result.scalars().first() +# assert user is not None diff --git a/docker-compose.yml b/docker-compose.yml index 787efff..836081d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,11 +5,11 @@ # services: - default_database: + postgres_database: restart: unless-stopped image: postgres:latest volumes: - - default_database_data:/var/lib/postgresql/data + - postgres_database:/var/lib/postgresql/data environment: - POSTGRES_DB=${DATABASE__DB} - POSTGRES_USER=${DATABASE__USERNAME} @@ -19,16 +19,5 @@ services: ports: - "${DATABASE__PORT}:5432" - test_database: - restart: unless-stopped - image: postgres:latest - volumes: - - test_database_data:/var/lib/postgresql/data - environment: - - POSTGRES_PASSWORD=postgres - ports: - - "31234:5432" - volumes: - test_database_data: - default_database_data: + postgres_database: diff --git a/poetry.lock b/poetry.lock index 8fc7acb..b7ea0ba 100644 --- a/poetry.lock +++ b/poetry.lock @@ -409,6 +409,20 @@ files = [ dnspython = ">=2.0.0" idna = ">=2.0.0" +[[package]] +name = "execnet" +version = "2.0.2" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.7" +files = [ + {file = "execnet-2.0.2-py3-none-any.whl", hash = "sha256:88256416ae766bc9e8895c76a87928c0012183da3cc4fc18016e6f050e025f41"}, + {file = "execnet-2.0.2.tar.gz", hash = "sha256:cc59bc4423742fd71ad227122eb0dd44db51efb3dc4095b45ac9a08c770096af"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + [[package]] name = "fastapi" version = "0.109.2" @@ -1175,21 +1189,24 @@ pytest = ">=4.6" testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] -name = "pytest-env" -version = "1.1.3" -description = "pytest plugin that allows you to add environment variables." +name = "pytest-xdist" +version = "3.5.0" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "pytest_env-1.1.3-py3-none-any.whl", hash = "sha256:aada77e6d09fcfb04540a6e462c58533c37df35fa853da78707b17ec04d17dfc"}, - {file = "pytest_env-1.1.3.tar.gz", hash = "sha256:fcd7dc23bb71efd3d35632bde1bbe5ee8c8dc4489d6617fb010674880d96216b"}, + {file = "pytest-xdist-3.5.0.tar.gz", hash = "sha256:cbb36f3d67e0c478baa57fa4edc8843887e0f6cfc42d677530a36d7472b32d8a"}, + {file = "pytest_xdist-3.5.0-py3-none-any.whl", hash = "sha256:d075629c7e00b611df89f490a5063944bee7a4362a5ff11c7cc7824a03dfce24"}, ] [package.dependencies] -pytest = ">=7.4.3" +execnet = ">=1.1" +pytest = ">=6.2.0" [package.extras] -test = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "pytest-mock (>=3.12)"] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] [[package]] name = "python-dateutil" @@ -1803,4 +1820,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "e6c170662a2d87f6764555ee4a114215381d4b32fce6320f14d5e27705a06d2e" +content-hash = "64d81c75ff5880d4d77e2edd83b993eb032935ad0db3045778022b12d75400bd" diff --git a/pyproject.toml b/pyproject.toml index 38b391c..e1b8f23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ pydantic-settings = "^2.1.0" python-multipart = "^0.0.6" sqlalchemy = "^2.0.23" pyjwt = "^2.8.0" +pytest-xdist = "^3.5.0" [tool.poetry.group.dev.dependencies] @@ -29,7 +30,6 @@ ruff = "^0.1.4" uvicorn = { extras = ["standard"], version = "^0.26.0" } mypy = "^1.8.0" pytest-cov = "^4.1.0" -pytest-env = "^1.1.3" types-passlib = "^1.7.7.20240106" gevent = "^23.9.1" freezegun = "^1.4.0" @@ -40,16 +40,8 @@ build-backend = "poetry.core.masonry.api" requires = ["poetry-core>=1.0.0"] [tool.pytest.ini_options] -addopts = "-v --cov --cov-report xml --cov-report term-missing --cov-fail-under=100" +addopts = "-v -n auto --cov --cov-report xml --cov-report term-missing --cov-fail-under=100" asyncio_mode = "auto" -env = [ - "ENVIRONMENT=PYTEST", - "DATABASE__PORT=31234", - "DATABASE__USERNAME=postgres", - "DATABASE__DB=postgres", - "DATABASE__PASSWORD=postgres", - "SECURITY__PASSWORD_BCRYPT_ROUNDS=4", -] minversion = "6.0" testpaths = ["app/tests"] From a2d0f9e6e61812554394730943372541a7cd76b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Sun, 3 Mar 2024 03:12:20 +0100 Subject: [PATCH 12/41] init api messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/api/api_messages.py | 3 +++ app/api/api_router.py | 44 ++++++++++++++++++++++++++++++++++++++-- app/api/deps.py | 13 ++++-------- app/core/security/jwt.py | 12 +++++++++-- app/main.py | 3 ++- poetry.lock | 2 +- pyproject.toml | 2 +- 7 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 app/api/api_messages.py diff --git a/app/api/api_messages.py b/app/api/api_messages.py new file mode 100644 index 0000000..d17921b --- /dev/null +++ b/app/api/api_messages.py @@ -0,0 +1,3 @@ +JWT_ERROR_INVALID_TOKEN = "Token invalid" +JWT_ERROR_EXPIRED_TOKEN = "Token expired" +JWT_ERROR_USER_REMOVED = "User removed" diff --git a/app/api/api_router.py b/app/api/api_router.py index 79b9497..72689b5 100644 --- a/app/api/api_router.py +++ b/app/api/api_router.py @@ -1,7 +1,47 @@ from fastapi import APIRouter +from app.api import api_messages from app.api.endpoints import auth, users -api_router = APIRouter() -api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) +auth_router = APIRouter() +auth_router.include_router(auth.router, prefix="/auth", tags=["auth"]) + +api_router = APIRouter( + responses={ + 401: { + "description": "No `Authorization` access token header or token is invalid", + "content": { + "application/json": { + "examples": { + "not authenticated": { + "summary": "No authorization token header", + "value": {"detail": "Not authenticated"}, + }, + "invalid token": { + "summary": api_messages.JWT_ERROR_INVALID_TOKEN, + "value": {"detail": api_messages.JWT_ERROR_INVALID_TOKEN}, + }, + } + } + }, + }, + 403: { + "description": "Access token is expired or user was removed", + "content": { + "application/json": { + "examples": { + "expired token": { + "summary": api_messages.JWT_ERROR_EXPIRED_TOKEN, + "value": {"detail": api_messages.JWT_ERROR_EXPIRED_TOKEN}, + }, + "removed user": { + "summary": api_messages.JWT_ERROR_USER_REMOVED, + "value": {"detail": api_messages.JWT_ERROR_USER_REMOVED}, + }, + }, + } + }, + }, + } +) api_router.include_router(users.router, prefix="/users", tags=["users"]) diff --git a/app/api/deps.py b/app/api/deps.py index 9a0c50c..6ffc41c 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -6,6 +6,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession +from app.api import api_messages from app.core import database_session from app.core.security.jwt import verify_jwt_token from app.models import User @@ -22,13 +23,7 @@ async def get_current_user( token: Annotated[str, Depends(oauth2_scheme)], session: AsyncSession = Depends(get_session), ) -> User: - try: - token_payload = verify_jwt_token(token) - except ValueError as err: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=f"Could not validate credentials: {err}", - ) + token_payload = verify_jwt_token(token) result = await session.execute( select(User).where(User.user_id == token_payload.sub) @@ -37,7 +32,7 @@ async def get_current_user( if not user: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="User not found", + status_code=status.HTTP_403_FORBIDDEN, + detail=api_messages.JWT_ERROR_USER_REMOVED, ) return user diff --git a/app/core/security/jwt.py b/app/core/security/jwt.py index 97ab2ac..6464455 100644 --- a/app/core/security/jwt.py +++ b/app/core/security/jwt.py @@ -1,8 +1,10 @@ import time import jwt +from fastapi import HTTPException, status from pydantic import BaseModel +from app.api import api_messages from app.core.config import get_settings JWT_ALGORITHM = "HS256" @@ -49,12 +51,18 @@ def verify_jwt_token(token: str) -> JWTTokenPayload: algorithms=[JWT_ALGORITHM], ) except jwt.DecodeError: - raise ValueError("invalid token") + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=api_messages.JWT_ERROR_INVALID_TOKEN, + ) token_payload = JWTTokenPayload(**raw_payload) now = int(time.time()) if now < token_payload.iat or now > token_payload.exp: - raise ValueError("token expired") + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=api_messages.JWT_ERROR_EXPIRED_TOKEN, + ) return token_payload diff --git a/app/main.py b/app/main.py index de3e87c..efb500d 100644 --- a/app/main.py +++ b/app/main.py @@ -4,7 +4,7 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.trustedhost import TrustedHostMiddleware -from app.api.api_router import api_router +from app.api.api_router import api_router, auth_router from app.core.config import get_settings app = FastAPI( @@ -16,6 +16,7 @@ ) app.include_router(api_router) +app.include_router(auth_router) # Sets all CORS enabled origins app.add_middleware( diff --git a/poetry.lock b/poetry.lock index b7ea0ba..87f7a4c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1820,4 +1820,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "64d81c75ff5880d4d77e2edd83b993eb032935ad0db3045778022b12d75400bd" +content-hash = "03b575ed5be6b81575a20f0400f33df69ee8e56f9213bdfa7f41ca1dd424a3bc" diff --git a/pyproject.toml b/pyproject.toml index e1b8f23..b66477b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ pydantic-settings = "^2.1.0" python-multipart = "^0.0.6" sqlalchemy = "^2.0.23" pyjwt = "^2.8.0" -pytest-xdist = "^3.5.0" [tool.poetry.group.dev.dependencies] @@ -33,6 +32,7 @@ pytest-cov = "^4.1.0" types-passlib = "^1.7.7.20240106" gevent = "^23.9.1" freezegun = "^1.4.0" +pytest-xdist = "^3.5.0" [build-system] From 80559c64846d357274fce445e3311c2bc425d147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Sun, 3 Mar 2024 03:14:19 +0100 Subject: [PATCH 13/41] remove minversion line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b66477b..090a9cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,7 +42,6 @@ requires = ["poetry-core>=1.0.0"] [tool.pytest.ini_options] addopts = "-v -n auto --cov --cov-report xml --cov-report term-missing --cov-fail-under=100" asyncio_mode = "auto" -minversion = "6.0" testpaths = ["app/tests"] [tool.coverage.run] From 2545ecbbc926b9418c64e8b8119038af3fa71cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Sun, 3 Mar 2024 11:24:51 +0100 Subject: [PATCH 14/41] add ruff format support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- .pre-commit-config.yaml | 12 ++++----- docker-compose.yml | 8 +++--- poetry.lock | 57 +---------------------------------------- pyproject.toml | 3 ++- 4 files changed, 13 insertions(+), 67 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 44797ff..9a32335 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,13 +4,13 @@ repos: hooks: - id: check-yaml - - repo: https://github.com/psf/black - rev: "23.12.1" - hooks: - - id: black - - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.13 + rev: v0.3.0 hooks: - id: ruff args: [--fix] + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.0 + hooks: + - id: ruff-format diff --git a/docker-compose.yml b/docker-compose.yml index 836081d..eeec521 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,11 +5,11 @@ # services: - postgres_database: + postgres_db: restart: unless-stopped - image: postgres:latest + image: postgres:16 volumes: - - postgres_database:/var/lib/postgresql/data + - postgres_db:/var/lib/postgresql/data environment: - POSTGRES_DB=${DATABASE__DB} - POSTGRES_USER=${DATABASE__USERNAME} @@ -20,4 +20,4 @@ services: - "${DATABASE__PORT}:5432" volumes: - postgres_database: + postgres_db: diff --git a/poetry.lock b/poetry.lock index 87f7a4c..4c6d5fe 100644 --- a/poetry.lock +++ b/poetry.lock @@ -144,50 +144,6 @@ files = [ tests = ["pytest (>=3.2.1,!=3.3.0)"] typecheck = ["mypy"] -[[package]] -name = "black" -version = "23.12.1" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, - {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, - {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, - {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, - {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, - {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, - {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, - {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, - {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, - {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, - {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, - {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, - {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, - {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, - {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, - {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, - {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, - {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, - {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, - {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, - {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, - {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "certifi" version = "2024.2.2" @@ -915,17 +871,6 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "platformdirs" version = "4.2.0" @@ -1820,4 +1765,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "03b575ed5be6b81575a20f0400f33df69ee8e56f9213bdfa7f41ca1dd424a3bc" +content-hash = "143f149033e1e1eeb4b3296db73974864330f9f52b0accf960f1d9d6ab060d2d" diff --git a/pyproject.toml b/pyproject.toml index 090a9cb..6d5457b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,6 @@ pyjwt = "^2.8.0" [tool.poetry.group.dev.dependencies] -black = "^23.10.1" coverage = "^7.3.2" httpx = "^0.26.0" pre-commit = "^3.5.0" @@ -55,6 +54,8 @@ strict = true [tool.ruff] target-version = "py312" + +[tool.ruff.lint] # pycodestyle, pyflakes, isort, pylint, pyupgrade select = ["E", "W", "F", "I", "PL", "UP"] ignore = ["E501"] From 2602ad166ebd8119a73bf781ddf16dedf3e969cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Sun, 3 Mar 2024 11:51:04 +0100 Subject: [PATCH 15/41] fix post_write_hooks in alembic, recreate migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- .pre-commit-config.yaml | 6 ++-- alembic.ini | 14 ++++---- ...add_create_and_update_time_b70e5f695bd2.py | 22 ------------- ...it_user_and_refresh_token_c79b0938ea4b.py} | 33 ++++++++++++++++--- 4 files changed, 38 insertions(+), 37 deletions(-) delete mode 100644 alembic/versions/2024030244_add_create_and_update_time_b70e5f695bd2.py rename alembic/versions/{2024011939_init_user_and_refresh_token_21f30f70dab2.py => 2024030345_init_user_and_refresh_token_c79b0938ea4b.py} (68%) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9a32335..919bdcd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,10 +7,10 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.3.0 hooks: - - id: ruff - args: [--fix] + - id: ruff-format - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.3.0 hooks: - - id: ruff-format + - id: ruff + args: [--fix] diff --git a/alembic.ini b/alembic.ini index 8ef6727..7b1c563 100644 --- a/alembic.ini +++ b/alembic.ini @@ -60,20 +60,18 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne sqlalchemy.url = driver://user:pass@localhost/dbname -# [post_write_hooks] +[post_write_hooks] +hooks = pre_commit +pre_commit.type = console_scripts +pre_commit.entrypoint = pre-commit +pre_commit.options = run --files REVISION_SCRIPT_FILENAME # This section defines scripts or Python functions that are run # on newly generated revision scripts. See the documentation for further # detail and examples # format using "black" - use the console_scripts runner, # against the "black" entrypoint -hooks = black,ruff -black.type = console_scripts -black.entrypoint = black -black.options = REVISION_SCRIPT_FILENAME -ruff.type = console_scripts -ruff.executable = ruff -ruff.options = --fix REVISION_SCRIPT_FILENAME + # Logging configuration [loggers] diff --git a/alembic/versions/2024030244_add_create_and_update_time_b70e5f695bd2.py b/alembic/versions/2024030244_add_create_and_update_time_b70e5f695bd2.py deleted file mode 100644 index 16571db..0000000 --- a/alembic/versions/2024030244_add_create_and_update_time_b70e5f695bd2.py +++ /dev/null @@ -1,22 +0,0 @@ -"""add create and update time - -Revision ID: b70e5f695bd2 -Revises: 21f30f70dab2 -Create Date: 2024-03-02 16:44:19.587386 - -""" - - -# revision identifiers, used by Alembic. -revision = "b70e5f695bd2" -down_revision = "21f30f70dab2" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - pass - - -def downgrade() -> None: - pass diff --git a/alembic/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py b/alembic/versions/2024030345_init_user_and_refresh_token_c79b0938ea4b.py similarity index 68% rename from alembic/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py rename to alembic/versions/2024030345_init_user_and_refresh_token_c79b0938ea4b.py index 2045c4c..d8bffc9 100644 --- a/alembic/versions/2024011939_init_user_and_refresh_token_21f30f70dab2.py +++ b/alembic/versions/2024030345_init_user_and_refresh_token_c79b0938ea4b.py @@ -1,16 +1,17 @@ -"""init_user_and_refresh_token +"""init user and refresh token -Revision ID: 21f30f70dab2 +Revision ID: c79b0938ea4b Revises: -Create Date: 2024-01-19 01:39:35.369361 +Create Date: 2024-03-03 11:45:21.361225 """ + import sqlalchemy as sa from alembic import op # revision identifiers, used by Alembic. -revision = "21f30f70dab2" +revision = "c79b0938ea4b" down_revision = None branch_labels = None depends_on = None @@ -23,6 +24,18 @@ def upgrade() -> None: sa.Column("user_id", sa.Uuid(as_uuid=False), nullable=False), sa.Column("email", sa.String(length=256), nullable=False), sa.Column("hashed_password", sa.String(length=128), nullable=False), + sa.Column( + "create_time", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "update_time", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), sa.PrimaryKeyConstraint("user_id"), ) op.create_index( @@ -35,6 +48,18 @@ def upgrade() -> None: sa.Column("used", sa.Boolean(), nullable=False), sa.Column("exp", sa.BigInteger(), nullable=False), sa.Column("user_id", sa.Uuid(as_uuid=False), nullable=False), + sa.Column( + "create_time", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), + sa.Column( + "update_time", + sa.DateTime(timezone=True), + server_default=sa.text("now()"), + nullable=False, + ), sa.ForeignKeyConstraint( ["user_id"], ["user_account.user_id"], ondelete="CASCADE" ), From 90e35260428c81ef013eede489af463715f91f34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Sun, 3 Mar 2024 11:58:55 +0100 Subject: [PATCH 16/41] fix typo in conftest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/tests/conftest.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/tests/conftest.py b/app/tests/conftest.py index f997a7f..c8a60df 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -52,9 +52,7 @@ async def fixture_setup_new_test_database() -> None: get_settings.cache_clear() # monkeypatch test database engine - engine = database_session.new_async_engine( - get_settings().sqlalchemy_database_uri.set(database=test_db_name) - ) + engine = database_session.new_async_engine(get_settings().sqlalchemy_database_uri) session_mpatch.setattr( database_session, From 35c5a922849a60b2b23ea3821c682570bcf68a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Sun, 3 Mar 2024 18:51:49 +0100 Subject: [PATCH 17/41] auth endpoint messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/api/api_messages.py | 4 ++ app/api/endpoints/auth.py | 83 ++++++++++++++++++++++++++++++++++----- 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/app/api/api_messages.py b/app/api/api_messages.py index d17921b..f5f5750 100644 --- a/app/api/api_messages.py +++ b/app/api/api_messages.py @@ -1,3 +1,7 @@ JWT_ERROR_INVALID_TOKEN = "Token invalid" JWT_ERROR_EXPIRED_TOKEN = "Token expired" JWT_ERROR_USER_REMOVED = "User removed" +PASSWORD_INVALID = "Incorrect email or password" +REFRESH_TOKEN_NOT_FOUND = "Refresh token not found" +REFRESH_TOKEN_EXPIRED = "Refresh token expired" +REFRESH_TOKEN_ALREADY_USED = "Refresh token already used" diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index f72ce61..70973e9 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -1,12 +1,13 @@ import secrets import time +from typing import Any from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession -from app.api import deps +from app.api import api_messages, deps from app.core import config from app.core.security.jwt import create_jwt_token from app.core.security.password import DUMMY_PASSWORD, verify_password @@ -16,8 +17,49 @@ router = APIRouter() - -@router.post("/access-token", response_model=AccessTokenResponse) +ACCESS_TOKEN_RESPONSES: dict[int | str, dict[str, Any]] = { + 400: { + "description": "Invalid email or password", + "content": { + "application/json": {"example": {"detail": api_messages.PASSWORD_INVALID}} + }, + }, +} + +REFRESH_TOKEN_RESPONES: dict[int | str, dict[str, Any]] = { + 400: { + "description": "Refresh token expired or is already used", + "content": { + "application/json": { + "examples": { + "refresh token expired": { + "summary": api_messages.REFRESH_TOKEN_EXPIRED, + "value": {"detail": api_messages.REFRESH_TOKEN_EXPIRED}, + }, + "refresh token already used": { + "summary": api_messages.REFRESH_TOKEN_ALREADY_USED, + "value": {"detail": api_messages.REFRESH_TOKEN_ALREADY_USED}, + }, + } + } + }, + }, + 404: { + "description": "Refresh token does not exist", + "content": { + "application/json": { + "example": {"detail": api_messages.REFRESH_TOKEN_NOT_FOUND} + } + }, + }, +} + + +@router.post( + "/access-token", + response_model=AccessTokenResponse, + responses=ACCESS_TOKEN_RESPONSES, +) async def login_access_token( session: AsyncSession = Depends(deps.get_session), form_data: OAuth2PasswordRequestForm = Depends(), @@ -31,10 +73,16 @@ async def login_access_token( # this is naive method to not return early verify_password(form_data.password, DUMMY_PASSWORD) - raise HTTPException(status_code=400, detail="Incorrect email or password") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=api_messages.PASSWORD_INVALID, + ) if not verify_password(form_data.password, user.hashed_password): - raise HTTPException(status_code=400, detail="Incorrect email or password") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=api_messages.PASSWORD_INVALID, + ) jwt_token = create_jwt_token(user_id=user.user_id) @@ -54,7 +102,11 @@ async def login_access_token( ) -@router.post("/refresh-token", response_model=AccessTokenResponse) +@router.post( + "/refresh-token", + response_model=AccessTokenResponse, + responses=REFRESH_TOKEN_RESPONES, +) async def refresh_token( data: RefreshTokenRequest, session: AsyncSession = Depends(deps.get_session), @@ -62,13 +114,26 @@ async def refresh_token( """OAuth2 compatible token, get an access token for future requests using refresh token""" result = await session.execute( - select(RefreshToken).where(RefreshToken.refresh_token == data.refresh_token) + select(RefreshToken) + .where(RefreshToken.refresh_token == data.refresh_token) + .with_for_update() ) token = result.scalars().first() - if token is None or time.time() > token.exp or token.used: + if token is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=api_messages.REFRESH_TOKEN_NOT_FOUND, + ) + elif time.time() > token.exp: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=api_messages.REFRESH_TOKEN_EXPIRED, + ) + elif token.used: raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Token not found" + status_code=status.HTTP_400_BAD_REQUEST, + detail=api_messages.REFRESH_TOKEN_ALREADY_USED, ) token.used = True From c82232ea4172fd79833a24c26e46e53c0e042529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 4 Mar 2024 13:57:51 +0100 Subject: [PATCH 18/41] add test auth refresh token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/api/endpoints/auth.py | 10 +- .../test_auth/test_auth_login_access_token.py | 9 +- .../test_auth/test_auth_refresh_token.py | 209 +++++++++++++++++- 3 files changed, 212 insertions(+), 16 deletions(-) diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index 70973e9..e50c5e3 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from app.api import api_messages, deps -from app.core import config +from app.core.config import get_settings from app.core.security.jwt import create_jwt_token from app.core.security.password import DUMMY_PASSWORD, verify_password from app.models import RefreshToken, User @@ -26,7 +26,7 @@ }, } -REFRESH_TOKEN_RESPONES: dict[int | str, dict[str, Any]] = { +REFRESH_TOKEN_RESPONSES: dict[int | str, dict[str, Any]] = { 400: { "description": "Refresh token expired or is already used", "content": { @@ -89,7 +89,7 @@ async def login_access_token( refresh_token = RefreshToken( user_id=user.user_id, refresh_token=secrets.token_urlsafe(32), - exp=int(time.time() + config.get_settings().security.refresh_token_expire_secs), + exp=int(time.time() + get_settings().security.refresh_token_expire_secs), ) session.add(refresh_token) await session.commit() @@ -105,7 +105,7 @@ async def login_access_token( @router.post( "/refresh-token", response_model=AccessTokenResponse, - responses=REFRESH_TOKEN_RESPONES, + responses=REFRESH_TOKEN_RESPONSES, ) async def refresh_token( data: RefreshTokenRequest, @@ -144,7 +144,7 @@ async def refresh_token( refresh_token = RefreshToken( user_id=token.user_id, refresh_token=secrets.token_urlsafe(32), - exp=int(time.time() + config.get_settings().security.refresh_token_expire_secs), + exp=int(time.time() + get_settings().security.refresh_token_expire_secs), ) session.add(refresh_token) await session.commit() diff --git a/app/tests/test_auth/test_auth_login_access_token.py b/app/tests/test_auth/test_auth_login_access_token.py index 53cf6b8..2a259f5 100644 --- a/app/tests/test_auth/test_auth_login_access_token.py +++ b/app/tests/test_auth/test_auth_login_access_token.py @@ -6,6 +6,7 @@ from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession +from app.api import api_messages from app.core.config import get_settings from app.core.security.jwt import verify_jwt_token from app.main import app @@ -13,7 +14,6 @@ from app.tests.conftest import default_user_password -@freeze_time("2023-01-01") async def test_login_access_token_has_response_code_200( client: AsyncClient, default_user: User, @@ -30,7 +30,6 @@ async def test_login_access_token_has_response_code_200( assert response.status_code == status.HTTP_200_OK -@freeze_time("2023-01-01") async def test_login_access_token_jwt_has_valid_token_type( client: AsyncClient, default_user: User, @@ -74,7 +73,6 @@ async def test_login_access_token_returns_valid_jwt_access_token( client: AsyncClient, default_user: User, ) -> None: - now = int(datetime.now(tz=UTC).timestamp()) response = await client.post( app.url_path_for("login_access_token"), data={ @@ -84,6 +82,7 @@ async def test_login_access_token_returns_valid_jwt_access_token( headers={"Content-Type": "application/x-www-form-urlencoded"}, ) + now = int(datetime.now(tz=UTC).timestamp()) token = response.json() token_payload = verify_jwt_token(token["access_token"]) @@ -173,7 +172,7 @@ async def test_auth_access_token_fail_for_not_existing_user_with_message( ) assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json() == {"detail": "Incorrect email or password"} + assert response.json() == {"detail": api_messages.PASSWORD_INVALID} async def test_auth_access_token_fail_for_invalid_password_with_message( @@ -190,4 +189,4 @@ async def test_auth_access_token_fail_for_invalid_password_with_message( ) assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json() == {"detail": "Incorrect email or password"} + assert response.json() == {"detail": api_messages.PASSWORD_INVALID} diff --git a/app/tests/test_auth/test_auth_refresh_token.py b/app/tests/test_auth/test_auth_refresh_token.py index 6e0d6e3..0e13ef5 100644 --- a/app/tests/test_auth/test_auth_refresh_token.py +++ b/app/tests/test_auth/test_auth_refresh_token.py @@ -1,9 +1,14 @@ +from datetime import UTC, datetime import time from fastapi import status from httpx import AsyncClient +from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession - +from freezegun import freeze_time +from app.api import api_messages +from app.core.config import get_settings +from app.core.security.jwt import verify_jwt_token from app.main import app from app.models import RefreshToken, User @@ -19,7 +24,7 @@ async def test_refresh_token_fails_with_message_when_token_does_not_exist( ) assert response.status_code == status.HTTP_404_NOT_FOUND - assert response.json() == {"detail": "Token not found"} + assert response.json() == {"detail": api_messages.REFRESH_TOKEN_NOT_FOUND} async def test_refresh_token_fails_with_message_when_token_is_expired( @@ -42,8 +47,8 @@ async def test_refresh_token_fails_with_message_when_token_is_expired( }, ) - assert response.status_code == status.HTTP_404_NOT_FOUND - assert response.json() == {"detail": "Token not found"} + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == {"detail": api_messages.REFRESH_TOKEN_EXPIRED} async def test_refresh_token_fails_with_message_when_token_is_used( @@ -67,5 +72,197 @@ async def test_refresh_token_fails_with_message_when_token_is_used( }, ) - assert response.status_code == status.HTTP_404_NOT_FOUND - assert response.json() == {"detail": "Token not found"} + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == {"detail": api_messages.REFRESH_TOKEN_ALREADY_USED} + + +async def test_refresh_token_success_response_200( + client: AsyncClient, + default_user: User, + session: AsyncSession, +) -> None: + test_refresh_token = RefreshToken( + user_id=default_user.user_id, + refresh_token="blaxx", + exp=int(time.time()) + 1000, + used=False, + ) + session.add(test_refresh_token) + await session.commit() + + response = await client.post( + app.url_path_for("refresh_token"), + json={ + "refresh_token": "blaxx", + }, + ) + + assert response.status_code == status.HTTP_200_OK + + +async def test_refresh_token_success_old_token_is_used( + client: AsyncClient, + default_user: User, + session: AsyncSession, +) -> None: + test_refresh_token = RefreshToken( + user_id=default_user.user_id, + refresh_token="blaxx", + exp=int(time.time()) + 1000, + used=False, + ) + session.add(test_refresh_token) + await session.commit() + + await client.post( + app.url_path_for("refresh_token"), + json={ + "refresh_token": "blaxx", + }, + ) + + await session.refresh(test_refresh_token) + assert test_refresh_token.used + + +async def test_refresh_token_success_jwt_has_valid_token_type( + client: AsyncClient, + default_user: User, + session: AsyncSession, +) -> None: + test_refresh_token = RefreshToken( + user_id=default_user.user_id, + refresh_token="blaxx", + exp=int(time.time()) + 1000, + used=False, + ) + session.add(test_refresh_token) + await session.commit() + + response = await client.post( + app.url_path_for("refresh_token"), + json={ + "refresh_token": "blaxx", + }, + ) + + token = response.json() + assert token["token_type"] == "Bearer" + + +@freeze_time("2023-01-01") +async def test_refresh_token_success_jwt_has_valid_expire_time( + client: AsyncClient, + default_user: User, + session: AsyncSession, +) -> None: + test_refresh_token = RefreshToken( + user_id=default_user.user_id, + refresh_token="blaxx", + exp=int(time.time()) + 1000, + used=False, + ) + session.add(test_refresh_token) + await session.commit() + + response = await client.post( + app.url_path_for("refresh_token"), + json={ + "refresh_token": "blaxx", + }, + ) + + token = response.json() + current_timestamp = int(datetime.now(tz=UTC).timestamp()) + assert ( + token["expires_at"] + == current_timestamp + get_settings().security.jwt_access_token_expire_secs + ) + + +async def test_refresh_token_success_jwt_has_valid_access_token( + client: AsyncClient, + default_user: User, + session: AsyncSession, +) -> None: + test_refresh_token = RefreshToken( + user_id=default_user.user_id, + refresh_token="blaxx", + exp=int(time.time()) + 1000, + used=False, + ) + session.add(test_refresh_token) + await session.commit() + + response = await client.post( + app.url_path_for("refresh_token"), + json={ + "refresh_token": "blaxx", + }, + ) + + now = int(datetime.now(tz=UTC).timestamp()) + token = response.json() + token_payload = verify_jwt_token(token["access_token"]) + + assert token_payload.sub == default_user.user_id + assert token_payload.iat >= now + assert token_payload.exp == token["expires_at"] + + +@freeze_time("2023-01-01") +async def test_refresh_token_success_refresh_token_has_valid_expire_time( + client: AsyncClient, + default_user: User, + session: AsyncSession, +) -> None: + test_refresh_token = RefreshToken( + user_id=default_user.user_id, + refresh_token="blaxx", + exp=int(time.time()) + 1000, + used=False, + ) + session.add(test_refresh_token) + await session.commit() + + response = await client.post( + app.url_path_for("refresh_token"), + json={ + "refresh_token": "blaxx", + }, + ) + + token = response.json() + current_time = int(datetime.now(tz=UTC).timestamp()) + assert ( + token["refresh_token_expires_at"] + == current_time + get_settings().security.refresh_token_expire_secs + ) + + +async def test_refresh_token_success_new_refresh_token_is_in_db( + client: AsyncClient, + default_user: User, + session: AsyncSession, +) -> None: + test_refresh_token = RefreshToken( + user_id=default_user.user_id, + refresh_token="blaxx", + exp=int(time.time()) + 1000, + used=False, + ) + session.add(test_refresh_token) + await session.commit() + + response = await client.post( + app.url_path_for("refresh_token"), + json={ + "refresh_token": "blaxx", + }, + ) + + token = response.json() + token_db_count = await session.scalar( + select(func.count()).where(RefreshToken.refresh_token == token["refresh_token"]) + ) + assert token_db_count == 1 From 77a6c9c37f3ddae2838aa97e30463e155239b0c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 4 Mar 2024 14:10:33 +0100 Subject: [PATCH 19/41] fix refresh tokens tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/api/endpoints/auth.py | 2 +- app/tests/test_auth/test_auth_refresh_token.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index e50c5e3..e3da3ba 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -151,7 +151,7 @@ async def refresh_token( return AccessTokenResponse( access_token=jwt_token.access_token, - expires_at=jwt_token.payload.iat, + expires_at=jwt_token.payload.exp, refresh_token=refresh_token.refresh_token, refresh_token_expires_at=refresh_token.exp, ) diff --git a/app/tests/test_auth/test_auth_refresh_token.py b/app/tests/test_auth/test_auth_refresh_token.py index 0e13ef5..b39c73e 100644 --- a/app/tests/test_auth/test_auth_refresh_token.py +++ b/app/tests/test_auth/test_auth_refresh_token.py @@ -1,11 +1,12 @@ -from datetime import UTC, datetime import time +from datetime import UTC, datetime from fastapi import status +from freezegun import freeze_time from httpx import AsyncClient from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession -from freezegun import freeze_time + from app.api import api_messages from app.core.config import get_settings from app.core.security.jwt import verify_jwt_token @@ -121,7 +122,10 @@ async def test_refresh_token_success_old_token_is_used( }, ) - await session.refresh(test_refresh_token) + result = await session.execute( + select(RefreshToken).where(RefreshToken.refresh_token == "blaxx") + ) + test_refresh_token = result.scalar_one() assert test_refresh_token.used From 03b42067511311167ea4f8a5c41cdf4368728c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 4 Mar 2024 14:29:44 +0100 Subject: [PATCH 20/41] users api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/api/api_messages.py | 1 + app/api/endpoints/auth.py | 8 +++----- app/api/endpoints/users.py | 39 +++++++++++++++++++++++++----------- app/core/database_session.py | 2 +- 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/app/api/api_messages.py b/app/api/api_messages.py index f5f5750..6000de8 100644 --- a/app/api/api_messages.py +++ b/app/api/api_messages.py @@ -5,3 +5,4 @@ REFRESH_TOKEN_NOT_FOUND = "Refresh token not found" REFRESH_TOKEN_EXPIRED = "Refresh token expired" REFRESH_TOKEN_ALREADY_USED = "Refresh token already used" +EMAIL_ADDRESS_ALREADY_USED = "Cannot use this email address" diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index e3da3ba..58d7a1e 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -59,13 +59,12 @@ "/access-token", response_model=AccessTokenResponse, responses=ACCESS_TOKEN_RESPONSES, + description="OAuth2 compatible token, get an access token for future requests using username and password", ) async def login_access_token( session: AsyncSession = Depends(deps.get_session), form_data: OAuth2PasswordRequestForm = Depends(), ) -> AccessTokenResponse: - """OAuth2 compatible token, get an access token for future requests using username and password""" - result = await session.execute(select(User).where(User.email == form_data.username)) user = result.scalars().first() @@ -106,17 +105,16 @@ async def login_access_token( "/refresh-token", response_model=AccessTokenResponse, responses=REFRESH_TOKEN_RESPONSES, + description="OAuth2 compatible token, get an access token for future requests using refresh token", ) async def refresh_token( data: RefreshTokenRequest, session: AsyncSession = Depends(deps.get_session), ) -> AccessTokenResponse: - """OAuth2 compatible token, get an access token for future requests using refresh token""" - result = await session.execute( select(RefreshToken) .where(RefreshToken.refresh_token == data.refresh_token) - .with_for_update() + .with_for_update(skip_locked=True) ) token = result.scalars().first() diff --git a/app/api/endpoints/users.py b/app/api/endpoints/users.py index 8cbc689..a7dc710 100644 --- a/app/api/endpoints/users.py +++ b/app/api/endpoints/users.py @@ -1,8 +1,9 @@ -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import delete, select +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession -from app.api import deps +from app.api import api_messages, deps from app.core.security.password import get_password_hash from app.models import User from app.schemas.requests import UserCreateRequest, UserUpdatePasswordRequest @@ -11,50 +12,64 @@ router = APIRouter() -@router.get("/me", response_model=UserResponse) +@router.get("/me", response_model=UserResponse, description="Get current user") async def read_current_user( current_user: User = Depends(deps.get_current_user), ) -> User: - """Get current user""" return current_user -@router.delete("/me", status_code=204) +@router.delete("/me", status_code=204, description="Delete current user") async def delete_current_user( current_user: User = Depends(deps.get_current_user), session: AsyncSession = Depends(deps.get_session), ) -> None: - """Delete current user""" await session.execute(delete(User).where(User.user_id == current_user.user_id)) await session.commit() -@router.post("/reset-password", response_model=UserResponse) +@router.post( + "/reset-password", + response_model=UserResponse, + description="Update current user password", +) async def reset_current_user_password( user_update_password: UserUpdatePasswordRequest, session: AsyncSession = Depends(deps.get_session), current_user: User = Depends(deps.get_current_user), ) -> User: - """Update current user password""" current_user.hashed_password = get_password_hash(user_update_password.password) session.add(current_user) await session.commit() return current_user -@router.post("/register", response_model=UserResponse) +@router.post("/register", response_model=UserResponse, description="Create new user") async def register_new_user( new_user: UserCreateRequest, session: AsyncSession = Depends(deps.get_session), ) -> User: - """Create new user""" result = await session.execute(select(User).where(User.email == new_user.email)) if result.scalars().first() is not None: - raise HTTPException(status_code=400, detail="Cannot use this email address") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=api_messages.EMAIL_ADDRESS_ALREADY_USED, + ) + user = User( email=new_user.email, hashed_password=get_password_hash(new_user.password), ) session.add(user) - await session.commit() + + try: + await session.commit() + except IntegrityError: + await session.rollback() + + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=api_messages.EMAIL_ADDRESS_ALREADY_USED, + ) + return user diff --git a/app/core/database_session.py b/app/core/database_session.py index ebf0a1e..125ab20 100644 --- a/app/core/database_session.py +++ b/app/core/database_session.py @@ -32,5 +32,5 @@ def new_async_engine(uri: URL) -> AsyncEngine: _ASYNC_SESSIONMAKER = async_sessionmaker(_ASYNC_ENGINE, expire_on_commit=False) -def get_async_session() -> AsyncSession: +def get_async_session() -> AsyncSession: # pragma: no cover return _ASYNC_SESSIONMAKER() From 193550475af306e4b620679b65b35a453d19da2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 4 Mar 2024 17:04:56 +0100 Subject: [PATCH 21/41] finish api tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/api/api_router.py | 13 +--- app/api/deps.py | 2 +- app/api/endpoints/auth.py | 47 ++++++++++++- app/api/endpoints/users.py | 51 +++----------- app/core/security/jwt.py | 18 ++--- app/tests/conftest.py | 2 +- app/tests/test_api_router_jwt_errors.py | 66 ++++++++++++++++++ ...n_access_token.py => test_access_token.py} | 2 +- .../test_auth/test_auth_refresh_token.py | 2 +- app/tests/test_auth/test_register_new_user.py | 63 +++++++++++++++++ app/tests/test_users.py | 67 ------------------- app/tests/test_users/__init__.py | 0 .../test_users/test_delete_current_user.py | 36 ++++++++++ .../test_users/test_read_current_user.py | 33 +++++++++ app/tests/test_users/test_reset_password.py | 40 +++++++++++ pyproject.toml | 2 +- 16 files changed, 307 insertions(+), 137 deletions(-) create mode 100644 app/tests/test_api_router_jwt_errors.py rename app/tests/test_auth/{test_auth_login_access_token.py => test_access_token.py} (98%) create mode 100644 app/tests/test_auth/test_register_new_user.py delete mode 100644 app/tests/test_users.py create mode 100644 app/tests/test_users/__init__.py create mode 100644 app/tests/test_users/test_delete_current_user.py create mode 100644 app/tests/test_users/test_read_current_user.py create mode 100644 app/tests/test_users/test_reset_password.py diff --git a/app/api/api_router.py b/app/api/api_router.py index 72689b5..7c4f117 100644 --- a/app/api/api_router.py +++ b/app/api/api_router.py @@ -9,7 +9,7 @@ api_router = APIRouter( responses={ 401: { - "description": "No `Authorization` access token header or token is invalid", + "description": "No `Authorization` access token header, token is invalid or user removed", "content": { "application/json": { "examples": { @@ -21,15 +21,6 @@ "summary": api_messages.JWT_ERROR_INVALID_TOKEN, "value": {"detail": api_messages.JWT_ERROR_INVALID_TOKEN}, }, - } - } - }, - }, - 403: { - "description": "Access token is expired or user was removed", - "content": { - "application/json": { - "examples": { "expired token": { "summary": api_messages.JWT_ERROR_EXPIRED_TOKEN, "value": {"detail": api_messages.JWT_ERROR_EXPIRED_TOKEN}, @@ -38,7 +29,7 @@ "summary": api_messages.JWT_ERROR_USER_REMOVED, "value": {"detail": api_messages.JWT_ERROR_USER_REMOVED}, }, - }, + } } }, }, diff --git a/app/api/deps.py b/app/api/deps.py index 6ffc41c..8c37590 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -32,7 +32,7 @@ async def get_current_user( if not user: raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, + status_code=status.HTTP_401_UNAUTHORIZED, detail=api_messages.JWT_ERROR_USER_REMOVED, ) return user diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index 58d7a1e..df17944 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -5,15 +5,20 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.security import OAuth2PasswordRequestForm from sqlalchemy import select +from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from app.api import api_messages, deps from app.core.config import get_settings from app.core.security.jwt import create_jwt_token -from app.core.security.password import DUMMY_PASSWORD, verify_password +from app.core.security.password import ( + DUMMY_PASSWORD, + get_password_hash, + verify_password, +) from app.models import RefreshToken, User -from app.schemas.requests import RefreshTokenRequest -from app.schemas.responses import AccessTokenResponse +from app.schemas.requests import RefreshTokenRequest, UserCreateRequest +from app.schemas.responses import AccessTokenResponse, UserResponse router = APIRouter() @@ -153,3 +158,39 @@ async def refresh_token( refresh_token=refresh_token.refresh_token, refresh_token_expires_at=refresh_token.exp, ) + + +@router.post( + "/register", + response_model=UserResponse, + description="Create new user", + status_code=status.HTTP_201_CREATED, +) +async def register_new_user( + new_user: UserCreateRequest, + session: AsyncSession = Depends(deps.get_session), +) -> User: + result = await session.execute(select(User).where(User.email == new_user.email)) + if result.scalars().first() is not None: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=api_messages.EMAIL_ADDRESS_ALREADY_USED, + ) + + user = User( + email=new_user.email, + hashed_password=get_password_hash(new_user.password), + ) + session.add(user) + + try: + await session.commit() + except IntegrityError: # pragma: no cover + await session.rollback() + + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=api_messages.EMAIL_ADDRESS_ALREADY_USED, + ) + + return user diff --git a/app/api/endpoints/users.py b/app/api/endpoints/users.py index a7dc710..72f9b44 100644 --- a/app/api/endpoints/users.py +++ b/app/api/endpoints/users.py @@ -1,12 +1,11 @@ -from fastapi import APIRouter, Depends, HTTPException, status -from sqlalchemy import delete, select -from sqlalchemy.exc import IntegrityError +from fastapi import APIRouter, Depends, status +from sqlalchemy import delete from sqlalchemy.ext.asyncio import AsyncSession -from app.api import api_messages, deps +from app.api import deps from app.core.security.password import get_password_hash from app.models import User -from app.schemas.requests import UserCreateRequest, UserUpdatePasswordRequest +from app.schemas.requests import UserUpdatePasswordRequest from app.schemas.responses import UserResponse router = APIRouter() @@ -19,7 +18,11 @@ async def read_current_user( return current_user -@router.delete("/me", status_code=204, description="Delete current user") +@router.delete( + "/me", + status_code=status.HTTP_204_NO_CONTENT, + description="Delete current user", +) async def delete_current_user( current_user: User = Depends(deps.get_current_user), session: AsyncSession = Depends(deps.get_session), @@ -30,46 +33,14 @@ async def delete_current_user( @router.post( "/reset-password", - response_model=UserResponse, + status_code=status.HTTP_204_NO_CONTENT, description="Update current user password", ) async def reset_current_user_password( user_update_password: UserUpdatePasswordRequest, session: AsyncSession = Depends(deps.get_session), current_user: User = Depends(deps.get_current_user), -) -> User: +) -> None: current_user.hashed_password = get_password_hash(user_update_password.password) session.add(current_user) await session.commit() - return current_user - - -@router.post("/register", response_model=UserResponse, description="Create new user") -async def register_new_user( - new_user: UserCreateRequest, - session: AsyncSession = Depends(deps.get_session), -) -> User: - result = await session.execute(select(User).where(User.email == new_user.email)) - if result.scalars().first() is not None: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=api_messages.EMAIL_ADDRESS_ALREADY_USED, - ) - - user = User( - email=new_user.email, - hashed_password=get_password_hash(new_user.password), - ) - session.add(user) - - try: - await session.commit() - except IntegrityError: - await session.rollback() - - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=api_messages.EMAIL_ADDRESS_ALREADY_USED, - ) - - return user diff --git a/app/core/security/jwt.py b/app/core/security/jwt.py index 6464455..a2b2c07 100644 --- a/app/core/security/jwt.py +++ b/app/core/security/jwt.py @@ -50,19 +50,15 @@ def verify_jwt_token(token: str) -> JWTTokenPayload: get_settings().security.jwt_secret_key.get_secret_value(), algorithms=[JWT_ALGORITHM], ) - except jwt.DecodeError: + except jwt.ExpiredSignatureError: raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=api_messages.JWT_ERROR_INVALID_TOKEN, + status_code=status.HTTP_401_UNAUTHORIZED, + detail=api_messages.JWT_ERROR_EXPIRED_TOKEN, ) - - token_payload = JWTTokenPayload(**raw_payload) - - now = int(time.time()) - if now < token_payload.iat or now > token_payload.exp: + except (jwt.DecodeError, jwt.InvalidTokenError): raise HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail=api_messages.JWT_ERROR_EXPIRED_TOKEN, + status_code=status.HTTP_401_UNAUTHORIZED, + detail=api_messages.JWT_ERROR_INVALID_TOKEN, ) - return token_payload + return JWTTokenPayload(**raw_payload) diff --git a/app/tests/conftest.py b/app/tests/conftest.py index c8a60df..0b2d2f4 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -21,7 +21,7 @@ default_user_id = "b75365d9-7bf9-4f54-add5-aeab333a087b" default_user_email = "geralt@wiedzmin.pl" default_user_password = "geralt" -default_user_access_token = create_jwt_token(default_user_id) +default_user_access_token = create_jwt_token(default_user_id).access_token @pytest.fixture(scope="session") diff --git a/app/tests/test_api_router_jwt_errors.py b/app/tests/test_api_router_jwt_errors.py new file mode 100644 index 0000000..69e7a56 --- /dev/null +++ b/app/tests/test_api_router_jwt_errors.py @@ -0,0 +1,66 @@ +import pytest +from fastapi import routing, status +from freezegun import freeze_time +from httpx import AsyncClient +from sqlalchemy import delete +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api import api_messages +from app.api.api_router import api_router +from app.core.security.jwt import create_jwt_token +from app.models import User + + +@pytest.mark.parametrize("api_route", api_router.routes) +async def test_api_routes_raise_401_on_jwt_decode_errors( + client: AsyncClient, + api_route: routing.APIRoute, +) -> None: + for method in api_route.methods: + response = await client.request( + method=method, + url=api_route.path, + headers={"Authorization": "Bearer garbage-invalid-jwt"}, + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.json() == {"detail": api_messages.JWT_ERROR_INVALID_TOKEN} + + +@pytest.mark.parametrize("api_route", api_router.routes) +async def test_api_routes_raise_401_on_jwt_expired_token( + client: AsyncClient, + default_user: User, + api_route: routing.APIRoute, +) -> None: + with freeze_time("2023-01-01"): + jwt = create_jwt_token(default_user.user_id) + with freeze_time("2023-02-01"): + for method in api_route.methods: + response = await client.request( + method=method, + url=api_route.path, + headers={"Authorization": f"Bearer {jwt.access_token}"}, + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.json() == {"detail": api_messages.JWT_ERROR_EXPIRED_TOKEN} + + +@pytest.mark.parametrize("api_route", api_router.routes) +async def test_api_routes_raise_401_on_jwt_user_deleted( + client: AsyncClient, + default_user_headers: dict[str, str], + default_user: User, + api_route: routing.APIRoute, + session: AsyncSession, +) -> None: + await session.execute(delete(User).where(User.user_id == default_user.user_id)) + await session.commit() + + for method in api_route.methods: + response = await client.request( + method=method, + url=api_route.path, + headers=default_user_headers, + ) + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response.json() == {"detail": api_messages.JWT_ERROR_USER_REMOVED} diff --git a/app/tests/test_auth/test_auth_login_access_token.py b/app/tests/test_auth/test_access_token.py similarity index 98% rename from app/tests/test_auth/test_auth_login_access_token.py rename to app/tests/test_auth/test_access_token.py index 2a259f5..3192a06 100644 --- a/app/tests/test_auth/test_auth_login_access_token.py +++ b/app/tests/test_auth/test_access_token.py @@ -14,7 +14,7 @@ from app.tests.conftest import default_user_password -async def test_login_access_token_has_response_code_200( +async def test_login_access_token_has_response_status_code( client: AsyncClient, default_user: User, ) -> None: diff --git a/app/tests/test_auth/test_auth_refresh_token.py b/app/tests/test_auth/test_auth_refresh_token.py index b39c73e..d82a8f1 100644 --- a/app/tests/test_auth/test_auth_refresh_token.py +++ b/app/tests/test_auth/test_auth_refresh_token.py @@ -77,7 +77,7 @@ async def test_refresh_token_fails_with_message_when_token_is_used( assert response.json() == {"detail": api_messages.REFRESH_TOKEN_ALREADY_USED} -async def test_refresh_token_success_response_200( +async def test_refresh_token_success_response_status_code( client: AsyncClient, default_user: User, session: AsyncSession, diff --git a/app/tests/test_auth/test_register_new_user.py b/app/tests/test_auth/test_register_new_user.py new file mode 100644 index 0000000..3e42cf1 --- /dev/null +++ b/app/tests/test_auth/test_register_new_user.py @@ -0,0 +1,63 @@ +from fastapi import status +from httpx import AsyncClient +from sqlalchemy import func, select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api import api_messages +from app.main import app +from app.models import User + + +async def test_register_new_user_status_code( + client: AsyncClient, +) -> None: + response = await client.post( + app.url_path_for("register_new_user"), + json={ + "email": "test@email.com", + "password": "testtesttest", + }, + ) + + assert response.status_code == status.HTTP_201_CREATED + + +async def test_register_new_user_creates_record_in_db( + client: AsyncClient, + session: AsyncSession, +) -> None: + await client.post( + app.url_path_for("register_new_user"), + json={ + "email": "test@email.com", + "password": "testtesttest", + }, + ) + + user_count = await session.scalar( + select(func.count()).where(User.email == "test@email.com") + ) + assert user_count == 1 + + +async def test_register_new_user_cannot_create_already_created_user( + client: AsyncClient, + session: AsyncSession, +) -> None: + user = User( + email="test@email.com", + hashed_password="bla", + ) + session.add(user) + await session.commit() + + response = await client.post( + app.url_path_for("register_new_user"), + json={ + "email": "test@email.com", + "password": "testtesttest", + }, + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == {"detail": api_messages.EMAIL_ADDRESS_ALREADY_USED} diff --git a/app/tests/test_users.py b/app/tests/test_users.py deleted file mode 100644 index f18f49b..0000000 --- a/app/tests/test_users.py +++ /dev/null @@ -1,67 +0,0 @@ -# from httpx import AsyncClient, codes -# from sqlalchemy import select -# from sqlalchemy.ext.asyncio import AsyncSession - -# from app.main import app -# from app.models import User -# from app.tests.conftest import ( -# default_user_email, -# default_user_id, -# ) - - -# async def test_read_current_user( -# client: AsyncClient, default_user_headers: dict[str, str] -# ) -> None: -# response = await client.get( -# app.url_path_for("read_current_user"), headers=default_user_headers -# ) -# assert response.status_code == codes.OK -# assert response.json() == { -# "id": default_user_id, -# "email": default_user_email, -# } - - -# async def test_delete_current_user( -# client: AsyncClient, default_user_headers: dict[str, str], session: AsyncSession -# ) -> None: -# response = await client.delete( -# app.url_path_for("delete_current_user"), headers=default_user_headers -# ) -# assert response.status_code == codes.NO_CONTENT -# result = await session.execute(select(User).where(User.user_id == default_user_id)) -# user = result.scalars().first() -# assert user is None - - -# async def test_reset_current_user_password( -# client: AsyncClient, default_user_headers: dict[str, str], session: AsyncSession -# ) -> None: -# response = await client.post( -# app.url_path_for("reset_current_user_password"), -# headers=default_user_headers, -# json={"password": "testxxxxxx"}, -# ) -# assert response.status_code == codes.OK -# result = await session.execute(select(User).where(User.user_id == default_user_id)) -# user = result.scalars().first() -# assert user is not None -# assert user.hashed_password != default_user_password_hash - - -# async def test_register_new_user( -# client: AsyncClient, default_user_headers: dict[str, str], session: AsyncSession -# ) -> None: -# response = await client.post( -# app.url_path_for("register_new_user"), -# headers=default_user_headers, -# json={ -# "email": "qwe@example.com", -# "password": "asdasdasd", -# }, -# ) -# assert response.status_code == codes.OK -# result = await session.execute(select(User).where(User.email == "qwe@example.com")) -# user = result.scalars().first() -# assert user is not None diff --git a/app/tests/test_users/__init__.py b/app/tests/test_users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tests/test_users/test_delete_current_user.py b/app/tests/test_users/test_delete_current_user.py new file mode 100644 index 0000000..5ca8e48 --- /dev/null +++ b/app/tests/test_users/test_delete_current_user.py @@ -0,0 +1,36 @@ +from fastapi import status +from httpx import AsyncClient +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.main import app +from app.models import User + + +async def test_delete_current_user_status_code( + client: AsyncClient, + default_user_headers: dict[str, str], +) -> None: + response = await client.delete( + app.url_path_for("delete_current_user"), + headers=default_user_headers, + ) + + assert response.status_code == status.HTTP_204_NO_CONTENT + + +async def test_delete_current_user_is_deleted_in_db( + client: AsyncClient, + default_user_headers: dict[str, str], + default_user: User, + session: AsyncSession, +) -> None: + await client.delete( + app.url_path_for("delete_current_user"), + headers=default_user_headers, + ) + + user = await session.scalar( + select(User).where(User.user_id == default_user.user_id) + ) + assert user is None diff --git a/app/tests/test_users/test_read_current_user.py b/app/tests/test_users/test_read_current_user.py new file mode 100644 index 0000000..3f5ef7b --- /dev/null +++ b/app/tests/test_users/test_read_current_user.py @@ -0,0 +1,33 @@ +from fastapi import status +from httpx import AsyncClient + +from app.main import app +from app.tests.conftest import ( + default_user_email, + default_user_id, +) + + +async def test_read_current_user_status_code( + client: AsyncClient, default_user_headers: dict[str, str] +) -> None: + response = await client.get( + app.url_path_for("read_current_user"), + headers=default_user_headers, + ) + + assert response.status_code == status.HTTP_200_OK + + +async def test_read_current_user_response( + client: AsyncClient, default_user_headers: dict[str, str] +) -> None: + response = await client.get( + app.url_path_for("read_current_user"), + headers=default_user_headers, + ) + + assert response.json() == { + "user_id": default_user_id, + "email": default_user_email, + } diff --git a/app/tests/test_users/test_reset_password.py b/app/tests/test_users/test_reset_password.py new file mode 100644 index 0000000..3ec720e --- /dev/null +++ b/app/tests/test_users/test_reset_password.py @@ -0,0 +1,40 @@ +from fastapi import status +from httpx import AsyncClient +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.core.security.password import verify_password +from app.main import app +from app.models import User + + +async def test_reset_current_user_password_status_code( + client: AsyncClient, + default_user_headers: dict[str, str], +) -> None: + response = await client.post( + app.url_path_for("reset_current_user_password"), + headers=default_user_headers, + json={"password": "test_pwd"}, + ) + + assert response.status_code == status.HTTP_204_NO_CONTENT + + +async def test_reset_current_user_password_is_changed_in_db( + client: AsyncClient, + default_user_headers: dict[str, str], + default_user: User, + session: AsyncSession, +) -> None: + await client.post( + app.url_path_for("reset_current_user_password"), + headers=default_user_headers, + json={"password": "test_pwd"}, + ) + + user = await session.scalar( + select(User).where(User.user_id == default_user.user_id) + ) + assert user is not None + assert verify_password("test_pwd", user.hashed_password) diff --git a/pyproject.toml b/pyproject.toml index 6d5457b..9a20bdf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,7 +39,7 @@ build-backend = "poetry.core.masonry.api" requires = ["poetry-core>=1.0.0"] [tool.pytest.ini_options] -addopts = "-v -n auto --cov --cov-report xml --cov-report term-missing --cov-fail-under=100" +addopts = "-vv -n auto --cov --cov-report xml --cov-report term-missing --cov-fail-under=100" asyncio_mode = "auto" testpaths = ["app/tests"] From e55694ce73303f3ad59b1353268f2bd177720c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 4 Mar 2024 17:20:34 +0100 Subject: [PATCH 22/41] improve module notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/core/config.py | 35 ++++++++++++++--------------------- app/main.py | 2 -- app/models.py | 23 +++++++++++------------ 3 files changed, 25 insertions(+), 35 deletions(-) diff --git a/app/core/config.py b/app/core/config.py index 5f13779..ed1d830 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -1,25 +1,18 @@ -""" -File with environment variables and general configuration logic. -`SECRET_KEY`, `ENVIRONMENT` etc. map to env variables with the same names. +# File with environment variables and general configuration logic. +# Env variables are combined in nested groups like "Security", "Database" etc. +# So environment variable (case-insensitive) for jwt_secret_key will be "security__jwt_secret_key" +# +# Pydantic priority ordering: +# +# 1. (Most important, will overwrite everything) - environment variables +# 2. `.env` file in root folder of project +# 3. Default values +# +# "sqlalchemy_database_uri" is computed field that will create valid database URL +# +# See https://pydantic-docs.helpmanual.io/usage/settings/ +# Note, complex types like lists are read as json-encoded strings. -Pydantic priority ordering: - -1. (Most important, will overwrite everything) - environment variables -2. `.env` file in root folder of project -3. Default values - -For project name, version, description we use pyproject.toml -For the rest, we use file `.env` (gitignored), see `.env.example` - -`DEFAULT_SQLALCHEMY_DATABASE_URI` and `TEST_SQLALCHEMY_DATABASE_URI`: -Both are ment to be validated at the runtime, do not change unless you know -what are you doing. All the two validators do is to build full URI (TCP protocol) -to databases to avoid typo bugs. - -See https://pydantic-docs.helpmanual.io/usage/settings/ - -Note, complex types like lists are read as json-encoded strings. -""" from functools import lru_cache from pathlib import Path diff --git a/app/main.py b/app/main.py index efb500d..359a440 100644 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,3 @@ -"""Main FastAPI app instance declaration.""" - from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.trustedhost import TrustedHostMiddleware diff --git a/app/models.py b/app/models.py index ad86333..d1fca6f 100644 --- a/app/models.py +++ b/app/models.py @@ -1,18 +1,17 @@ -""" -SQL Alchemy models declaration. -https://docs.sqlalchemy.org/en/14/orm/declarative_styles.html#example-two-dataclasses-with-declarative-table -Dataclass style for powerful autocompletion support. +# SQL Alchemy models declaration. +# https://docs.sqlalchemy.org/en/20/orm/quickstart.html#declare-models +# mapped_column syntax from SQLAlchemy 2.0. -https://alembic.sqlalchemy.org/en/latest/tutorial.html -Note, it is used by alembic migrations logic, see `alembic/env.py` +# https://alembic.sqlalchemy.org/en/latest/tutorial.html +# Note, it is used by alembic migrations logic, see `alembic/env.py` -Alembic shortcuts: -# create migration -alembic revision --autogenerate -m "migration_name" +# Alembic shortcuts: +# # create migration +# alembic revision --autogenerate -m "migration_name" + +# # apply all migrations +# alembic upgrade head -# apply all migrations -alembic upgrade head -""" import uuid from datetime import datetime From de114c614da66371d7c9d0d01bef5905b9b9bea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 4 Mar 2024 21:00:34 +0100 Subject: [PATCH 23/41] readme - update image link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- README.md | 2 +- docs/template-minimal-openapi-example.png | Bin 171365 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 docs/template-minimal-openapi-example.png diff --git a/README.md b/README.md index 62f7925..8c2b843 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ _Check out also online example: https://minimal-fastapi-postgres-template.rafsaf.pl, it's 100% code used in template (docker image) with added domain and https only._ -![template-fastapi-minimal-openapi-example](./docs/template-minimal-openapi-example.png) +![template-fastapi-minimal-openapi-example](https://drive.google.com/uc?export=view&id=1xEJx3fmkr1sOOzNQqMh-YWMAurFZYaTo) ## Quickstart diff --git a/docs/template-minimal-openapi-example.png b/docs/template-minimal-openapi-example.png deleted file mode 100644 index e50953d3379d9c160859fb774dd0853bbdd6144b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 171365 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 sxMUdcZ Date: Mon, 4 Mar 2024 21:03:55 +0100 Subject: [PATCH 24/41] make auth api above users in schema again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- README.md | 2 +- app/main.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8c2b843..cc74028 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ _Check out also online example: https://minimal-fastapi-postgres-template.rafsaf.pl, it's 100% code used in template (docker image) with added domain and https only._ -![template-fastapi-minimal-openapi-example](https://drive.google.com/uc?export=view&id=1xEJx3fmkr1sOOzNQqMh-YWMAurFZYaTo) +![template-fastapi-minimal-openapi-example](https://drive.google.com/uc?export=view&id=1rIXFJK8VyVrV7v4qgtPFryDd5FQrb4gr) ## Quickstart diff --git a/app/main.py b/app/main.py index 359a440..b2b0227 100644 --- a/app/main.py +++ b/app/main.py @@ -13,8 +13,8 @@ docs_url="/", ) -app.include_router(api_router) app.include_router(auth_router) +app.include_router(api_router) # Sets all CORS enabled origins app.add_middleware( From 1671f59e582012e4d31a44c32d226f513c400c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 4 Mar 2024 21:05:57 +0100 Subject: [PATCH 25/41] readme: make image look better MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cc74028..833319d 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ - [Minimal async FastAPI + PostgreSQL template](#minimal-async-fastapi--postgresql-template) - [Features](#features) + - [||](#) - [Quickstart](#quickstart) - [1. Install cookiecutter globally and cookiecutter this project](#1-install-cookiecutter-globally-and-cookiecutter-this-project) - [2. Install dependecies with poetry or without it](#2-install-dependecies-with-poetry-or-without-it) @@ -42,7 +43,9 @@ _Check out also online example: https://minimal-fastapi-postgres-template.rafsaf.pl, it's 100% code used in template (docker image) with added domain and https only._ -![template-fastapi-minimal-openapi-example](https://drive.google.com/uc?export=view&id=1rIXFJK8VyVrV7v4qgtPFryDd5FQrb4gr) +|![template-fastapi-minimal-openapi-example](https://drive.google.com/uc?export=view&id=1rIXFJK8VyVrV7v4qgtPFryDd5FQrb4gr)| +- + ## Quickstart From 6177d1860f0177aa93b1c7f2c5d13a7a585c5596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 4 Mar 2024 21:10:18 +0100 Subject: [PATCH 26/41] readme: next try for border image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 833319d..03099e8 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ - [Minimal async FastAPI + PostgreSQL template](#minimal-async-fastapi--postgresql-template) - [Features](#features) - - [||](#) - [Quickstart](#quickstart) - [1. Install cookiecutter globally and cookiecutter this project](#1-install-cookiecutter-globally-and-cookiecutter-this-project) - [2. Install dependecies with poetry or without it](#2-install-dependecies-with-poetry-or-without-it) @@ -43,8 +42,8 @@ _Check out also online example: https://minimal-fastapi-postgres-template.rafsaf.pl, it's 100% code used in template (docker image) with added domain and https only._ -|![template-fastapi-minimal-openapi-example](https://drive.google.com/uc?export=view&id=1rIXFJK8VyVrV7v4qgtPFryDd5FQrb4gr)| -- +![template-fastapi-minimal-openapi-example](https://drive.google.com/uc?export=view&id=1rIXFJK8VyVrV7v4qgtPFryDd5FQrb4gr) + ## Quickstart From 4f0f53bc4ed86291c256232b707603f547ddaa62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 4 Mar 2024 21:52:55 +0100 Subject: [PATCH 27/41] bump libs, fix depr warning in tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/tests/conftest.py | 9 +- poetry.lock | 261 +++++++++++++++++++++--------------------- pyproject.toml | 39 +++---- 3 files changed, 155 insertions(+), 154 deletions(-) diff --git a/app/tests/conftest.py b/app/tests/conftest.py index 0b2d2f4..ae7bef4 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -5,7 +5,7 @@ import pytest import pytest_asyncio import sqlalchemy -from httpx import AsyncClient +from httpx import ASGITransport, AsyncClient from sqlalchemy.ext.asyncio import ( AsyncSession, async_sessionmaker, @@ -102,9 +102,10 @@ async def fixture_session_with_rollback( @pytest_asyncio.fixture(name="client", scope="function") async def fixture_client(session: AsyncSession) -> AsyncGenerator[AsyncClient, None]: - async with AsyncClient(app=fastapi_app, base_url="http://test") as client: - client.headers.update({"Host": "localhost"}) - yield client + transport = ASGITransport(app=fastapi_app) # type: ignore + async with AsyncClient(transport=transport, base_url="http://test") as aclient: + aclient.headers.update({"Host": "localhost"}) + yield aclient @pytest_asyncio.fixture(name="default_user", scope="function") diff --git a/poetry.lock b/poetry.lock index 4c6d5fe..1600043 100644 --- a/poetry.lock +++ b/poetry.lock @@ -381,13 +381,13 @@ testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "fastapi" -version = "0.109.2" +version = "0.110.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"}, - {file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"}, + {file = "fastapi-0.110.0-py3-none-any.whl", hash = "sha256:87a1f6fb632a218222c5984be540055346a8f5d8a68e8f6fb647b1dc9934de4b"}, + {file = "fastapi-0.110.0.tar.gz", hash = "sha256:266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3"}, ] [package.dependencies] @@ -430,51 +430,52 @@ python-dateutil = ">=2.7" [[package]] name = "gevent" -version = "23.9.1" +version = "24.2.1" description = "Coroutine-based network library" optional = false python-versions = ">=3.8" files = [ - {file = "gevent-23.9.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:a3c5e9b1f766a7a64833334a18539a362fb563f6c4682f9634dea72cbe24f771"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b101086f109168b23fa3586fccd1133494bdb97f86920a24dc0b23984dc30b69"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36a549d632c14684bcbbd3014a6ce2666c5f2a500f34d58d32df6c9ea38b6535"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:272cffdf535978d59c38ed837916dfd2b5d193be1e9e5dcc60a5f4d5025dd98a"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcb8612787a7f4626aa881ff15ff25439561a429f5b303048f0fca8a1c781c39"}, - {file = "gevent-23.9.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d57737860bfc332b9b5aa438963986afe90f49645f6e053140cfa0fa1bdae1ae"}, - {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5f3c781c84794926d853d6fb58554dc0dcc800ba25c41d42f6959c344b4db5a6"}, - {file = "gevent-23.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dbb22a9bbd6a13e925815ce70b940d1578dbe5d4013f20d23e8a11eddf8d14a7"}, - {file = "gevent-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:707904027d7130ff3e59ea387dddceedb133cc742b00b3ffe696d567147a9c9e"}, - {file = "gevent-23.9.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:45792c45d60f6ce3d19651d7fde0bc13e01b56bb4db60d3f32ab7d9ec467374c"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e24c2af9638d6c989caffc691a039d7c7022a31c0363da367c0d32ceb4a0648"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e1ead6863e596a8cc2a03e26a7a0981f84b6b3e956101135ff6d02df4d9a6b07"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65883ac026731ac112184680d1f0f1e39fa6f4389fd1fc0bf46cc1388e2599f9"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7af500da05363e66f122896012acb6e101a552682f2352b618e541c941a011"}, - {file = "gevent-23.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:c3e5d2fa532e4d3450595244de8ccf51f5721a05088813c1abd93ad274fe15e7"}, - {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c84d34256c243b0a53d4335ef0bc76c735873986d478c53073861a92566a8d71"}, - {file = "gevent-23.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ada07076b380918829250201df1d016bdafb3acf352f35e5693b59dceee8dd2e"}, - {file = "gevent-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:921dda1c0b84e3d3b1778efa362d61ed29e2b215b90f81d498eb4d8eafcd0b7a"}, - {file = "gevent-23.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ed7a048d3e526a5c1d55c44cb3bc06cfdc1947d06d45006cc4cf60dedc628904"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c1abc6f25f475adc33e5fc2dbcc26a732608ac5375d0d306228738a9ae14d3b"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4368f341a5f51611411ec3fc62426f52ac3d6d42eaee9ed0f9eebe715c80184e"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:52b4abf28e837f1865a9bdeef58ff6afd07d1d888b70b6804557e7908032e599"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52e9f12cd1cda96603ce6b113d934f1aafb873e2c13182cf8e86d2c5c41982ea"}, - {file = "gevent-23.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:de350fde10efa87ea60d742901e1053eb2127ebd8b59a7d3b90597eb4e586599"}, - {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fde6402c5432b835fbb7698f1c7f2809c8d6b2bd9d047ac1f5a7c1d5aa569303"}, - {file = "gevent-23.9.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:dd6c32ab977ecf7c7b8c2611ed95fa4aaebd69b74bf08f4b4960ad516861517d"}, - {file = "gevent-23.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:455e5ee8103f722b503fa45dedb04f3ffdec978c1524647f8ba72b4f08490af1"}, - {file = "gevent-23.9.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:7ccf0fd378257cb77d91c116e15c99e533374a8153632c48a3ecae7f7f4f09fe"}, - {file = "gevent-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d163d59f1be5a4c4efcdd13c2177baaf24aadf721fdf2e1af9ee54a998d160f5"}, - {file = "gevent-23.9.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7532c17bc6c1cbac265e751b95000961715adef35a25d2b0b1813aa7263fb397"}, - {file = "gevent-23.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:78eebaf5e73ff91d34df48f4e35581ab4c84e22dd5338ef32714264063c57507"}, - {file = "gevent-23.9.1-cp38-cp38-win32.whl", hash = "sha256:f632487c87866094546a74eefbca2c74c1d03638b715b6feb12e80120960185a"}, - {file = "gevent-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:62d121344f7465e3739989ad6b91f53a6ca9110518231553fe5846dbe1b4518f"}, - {file = "gevent-23.9.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:bf456bd6b992eb0e1e869e2fd0caf817f0253e55ca7977fd0e72d0336a8c1c6a"}, - {file = "gevent-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43daf68496c03a35287b8b617f9f91e0e7c0d042aebcc060cadc3f049aadd653"}, - {file = "gevent-23.9.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7c28e38dcde327c217fdafb9d5d17d3e772f636f35df15ffae2d933a5587addd"}, - {file = "gevent-23.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fae8d5b5b8fa2a8f63b39f5447168b02db10c888a3e387ed7af2bd1b8612e543"}, - {file = "gevent-23.9.1-cp39-cp39-win32.whl", hash = "sha256:2c7b5c9912378e5f5ccf180d1fdb1e83f42b71823483066eddbe10ef1a2fcaa2"}, - {file = "gevent-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:a2898b7048771917d85a1d548fd378e8a7b2ca963db8e17c6d90c76b495e0e2b"}, - {file = "gevent-23.9.1.tar.gz", hash = "sha256:72c002235390d46f94938a96920d8856d4ffd9ddf62a303a0d7c118894097e34"}, + {file = "gevent-24.2.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6f947a9abc1a129858391b3d9334c45041c08a0f23d14333d5b844b6e5c17a07"}, + {file = "gevent-24.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bde283313daf0b34a8d1bab30325f5cb0f4e11b5869dbe5bc61f8fe09a8f66f3"}, + {file = "gevent-24.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1df555431f5cd5cc189a6ee3544d24f8c52f2529134685f1e878c4972ab026"}, + {file = "gevent-24.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:14532a67f7cb29fb055a0e9b39f16b88ed22c66b96641df8c04bdc38c26b9ea5"}, + {file = "gevent-24.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd23df885318391856415e20acfd51a985cba6919f0be78ed89f5db9ff3a31cb"}, + {file = "gevent-24.2.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ca80b121bbec76d7794fcb45e65a7eca660a76cc1a104ed439cdbd7df5f0b060"}, + {file = "gevent-24.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9913c45d1be52d7a5db0c63977eebb51f68a2d5e6fd922d1d9b5e5fd758cc98"}, + {file = "gevent-24.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:918cdf8751b24986f915d743225ad6b702f83e1106e08a63b736e3a4c6ead789"}, + {file = "gevent-24.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:3d5325ccfadfd3dcf72ff88a92fb8fc0b56cacc7225f0f4b6dcf186c1a6eeabc"}, + {file = "gevent-24.2.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:03aa5879acd6b7076f6a2a307410fb1e0d288b84b03cdfd8c74db8b4bc882fc5"}, + {file = "gevent-24.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8bb35ce57a63c9a6896c71a285818a3922d8ca05d150fd1fe49a7f57287b836"}, + {file = "gevent-24.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d7f87c2c02e03d99b95cfa6f7a776409083a9e4d468912e18c7680437b29222c"}, + {file = "gevent-24.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968581d1717bbcf170758580f5f97a2925854943c45a19be4d47299507db2eb7"}, + {file = "gevent-24.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7899a38d0ae7e817e99adb217f586d0a4620e315e4de577444ebeeed2c5729be"}, + {file = "gevent-24.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f5e8e8d60e18d5f7fd49983f0c4696deeddaf6e608fbab33397671e2fcc6cc91"}, + {file = "gevent-24.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fbfdce91239fe306772faab57597186710d5699213f4df099d1612da7320d682"}, + {file = "gevent-24.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cdf66977a976d6a3cfb006afdf825d1482f84f7b81179db33941f2fc9673bb1d"}, + {file = "gevent-24.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:1dffb395e500613e0452b9503153f8f7ba587c67dd4a85fc7cd7aa7430cb02cc"}, + {file = "gevent-24.2.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:6c47ae7d1174617b3509f5d884935e788f325eb8f1a7efc95d295c68d83cce40"}, + {file = "gevent-24.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7cac622e11b4253ac4536a654fe221249065d9a69feb6cdcd4d9af3503602e0"}, + {file = "gevent-24.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bf5b9c72b884c6f0c4ed26ef204ee1f768b9437330422492c319470954bc4cc7"}, + {file = "gevent-24.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5de3c676e57177b38857f6e3cdfbe8f38d1cd754b63200c0615eaa31f514b4f"}, + {file = "gevent-24.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4faf846ed132fd7ebfbbf4fde588a62d21faa0faa06e6f468b7faa6f436b661"}, + {file = "gevent-24.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:368a277bd9278ddb0fde308e6a43f544222d76ed0c4166e0d9f6b036586819d9"}, + {file = "gevent-24.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f8a04cf0c5b7139bc6368b461257d4a757ea2fe89b3773e494d235b7dd51119f"}, + {file = "gevent-24.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9d8d0642c63d453179058abc4143e30718b19a85cbf58c2744c9a63f06a1d388"}, + {file = "gevent-24.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:94138682e68ec197db42ad7442d3cf9b328069c3ad8e4e5022e6b5cd3e7ffae5"}, + {file = "gevent-24.2.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:8f4b8e777d39013595a7740b4463e61b1cfe5f462f1b609b28fbc1e4c4ff01e5"}, + {file = "gevent-24.2.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141a2b24ad14f7b9576965c0c84927fc85f824a9bb19f6ec1e61e845d87c9cd8"}, + {file = "gevent-24.2.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9202f22ef811053077d01f43cc02b4aaf4472792f9fd0f5081b0b05c926cca19"}, + {file = "gevent-24.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2955eea9c44c842c626feebf4459c42ce168685aa99594e049d03bedf53c2800"}, + {file = "gevent-24.2.1-cp38-cp38-win32.whl", hash = "sha256:44098038d5e2749b0784aabb27f1fcbb3f43edebedf64d0af0d26955611be8d6"}, + {file = "gevent-24.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:117e5837bc74a1673605fb53f8bfe22feb6e5afa411f524c835b2ddf768db0de"}, + {file = "gevent-24.2.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:2ae3a25ecce0a5b0cd0808ab716bfca180230112bb4bc89b46ae0061d62d4afe"}, + {file = "gevent-24.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7ceb59986456ce851160867ce4929edaffbd2f069ae25717150199f8e1548b8"}, + {file = "gevent-24.2.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:2e9ac06f225b696cdedbb22f9e805e2dd87bf82e8fa5e17756f94e88a9d37cf7"}, + {file = "gevent-24.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:90cbac1ec05b305a1b90ede61ef73126afdeb5a804ae04480d6da12c56378df1"}, + {file = "gevent-24.2.1-cp39-cp39-win32.whl", hash = "sha256:782a771424fe74bc7e75c228a1da671578c2ba4ddb2ca09b8f959abdf787331e"}, + {file = "gevent-24.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:3adfb96637f44010be8abd1b5e73b5070f851b817a0b182e601202f20fa06533"}, + {file = "gevent-24.2.1-pp310-pypy310_pp73-macosx_11_0_universal2.whl", hash = "sha256:7b00f8c9065de3ad226f7979154a7b27f3b9151c8055c162332369262fc025d8"}, + {file = "gevent-24.2.1.tar.gz", hash = "sha256:432fc76f680acf7cf188c2ee0f5d3ab73b63c1f03114c7cd8a34cebbe5aa2056"}, ] [package.dependencies] @@ -488,7 +489,7 @@ dnspython = ["dnspython (>=1.16.0,<2.0)", "idna"] docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"] monitor = ["psutil (>=5.7.0)"] recommended = ["cffi (>=1.12.2)", "dnspython (>=1.16.0,<2.0)", "idna", "psutil (>=5.7.0)"] -test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idna", "objgraph", "psutil (>=5.7.0)", "requests", "setuptools"] +test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idna", "objgraph", "psutil (>=5.7.0)", "requests"] [[package]] name = "greenlet" @@ -643,13 +644,13 @@ test = ["Cython (>=0.29.24,<0.30.0)"] [[package]] name = "httpx" -version = "0.26.0" +version = "0.27.0" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"}, - {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"}, + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, ] [package.dependencies] @@ -1079,20 +1080,20 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pytest" -version = "7.4.4" +version = "8.0.2" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, - {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, + {file = "pytest-8.0.2-py3-none-any.whl", hash = "sha256:edfaaef32ce5172d5466b5127b42e0d6d35ebbe4453f0e3505d96afd93f6b096"}, + {file = "pytest-8.0.2.tar.gz", hash = "sha256:d4051d623a2e0b7e51960ba963193b09ce6daeb9759a451844a21e4ddedfc1bd"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" +pluggy = ">=1.3.0,<2.0" [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] @@ -1183,17 +1184,17 @@ cli = ["click (>=5.0)"] [[package]] name = "python-multipart" -version = "0.0.6" +version = "0.0.9" description = "A streaming multipart parser for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "python_multipart-0.0.6-py3-none-any.whl", hash = "sha256:ee698bab5ef148b0a760751c261902cd096e57e10558e11aca17646b74ee1c18"}, - {file = "python_multipart-0.0.6.tar.gz", hash = "sha256:e9925a80bb668529f1b67c7fdb0a5dacdd7cbfc6fb0bff3ea443fe22bdd62132"}, + {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, + {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, ] [package.extras] -dev = ["atomicwrites (==1.2.1)", "attrs (==19.2.0)", "coverage (==6.5.0)", "hatch", "invoke (==1.7.3)", "more-itertools (==4.3.0)", "pbr (==4.3.0)", "pluggy (==1.0.0)", "py (==1.11.0)", "pytest (==7.2.0)", "pytest-cov (==4.0.0)", "pytest-timeout (==2.1.0)", "pyyaml (==5.1)"] +dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] [[package]] name = "pyyaml" @@ -1246,28 +1247,28 @@ files = [ [[package]] name = "ruff" -version = "0.1.15" +version = "0.3.0" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, - {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, - {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, - {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, - {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, - {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, - {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, - {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, + {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7deb528029bacf845bdbb3dbb2927d8ef9b4356a5e731b10eef171e3f0a85944"}, + {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e1e0d4381ca88fb2b73ea0766008e703f33f460295de658f5467f6f229658c19"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f7dbba46e2827dfcb0f0cc55fba8e96ba7c8700e0a866eb8cef7d1d66c25dcb"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23dbb808e2f1d68eeadd5f655485e235c102ac6f12ad31505804edced2a5ae77"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ef655c51f41d5fa879f98e40c90072b567c666a7114fa2d9fe004dffba00932"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d0d3d7ef3d4f06433d592e5f7d813314a34601e6c5be8481cccb7fa760aa243e"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b08b356d06a792e49a12074b62222f9d4ea2a11dca9da9f68163b28c71bf1dd4"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9343690f95710f8cf251bee1013bf43030072b9f8d012fbed6ad702ef70d360a"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1f3ed501a42f60f4dedb7805fa8d4534e78b4e196f536bac926f805f0743d49"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cc30a9053ff2f1ffb505a585797c23434d5f6c838bacfe206c0e6cf38c921a1e"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5da894a29ec018a8293d3d17c797e73b374773943e8369cfc50495573d396933"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:755c22536d7f1889be25f2baf6fedd019d0c51d079e8417d4441159f3bcd30c2"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd73fe7f4c28d317855da6a7bc4aa29a1500320818dd8f27df95f70a01b8171f"}, + {file = "ruff-0.3.0-py3-none-win32.whl", hash = "sha256:19eacceb4c9406f6c41af806418a26fdb23120dfe53583df76d1401c92b7c14b"}, + {file = "ruff-0.3.0-py3-none-win_amd64.whl", hash = "sha256:128265876c1d703e5f5e5a4543bd8be47c73a9ba223fd3989d4aa87dd06f312f"}, + {file = "ruff-0.3.0-py3-none-win_arm64.whl", hash = "sha256:e3a4a6d46aef0a84b74fcd201a4401ea9a6cd85614f6a9435f2d33dd8cefbf83"}, + {file = "ruff-0.3.0.tar.gz", hash = "sha256:0886184ba2618d815067cf43e005388967b67ab9c80df52b32ec1152ab49f53a"}, ] [[package]] @@ -1310,60 +1311,60 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.27" +version = "2.0.28" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.27-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d04e579e911562f1055d26dab1868d3e0bb905db3bccf664ee8ad109f035618a"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa67d821c1fd268a5a87922ef4940442513b4e6c377553506b9db3b83beebbd8"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c7a596d0be71b7baa037f4ac10d5e057d276f65a9a611c46970f012752ebf2d"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:954d9735ee9c3fa74874c830d089a815b7b48df6f6b6e357a74130e478dbd951"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5cd20f58c29bbf2680039ff9f569fa6d21453fbd2fa84dbdb4092f006424c2e6"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:03f448ffb731b48323bda68bcc93152f751436ad6037f18a42b7e16af9e91c07"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-win32.whl", hash = "sha256:d997c5938a08b5e172c30583ba6b8aad657ed9901fc24caf3a7152eeccb2f1b4"}, - {file = "SQLAlchemy-2.0.27-cp310-cp310-win_amd64.whl", hash = "sha256:eb15ef40b833f5b2f19eeae65d65e191f039e71790dd565c2af2a3783f72262f"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c5bad7c60a392850d2f0fee8f355953abaec878c483dd7c3836e0089f046bf6"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3012ab65ea42de1be81fff5fb28d6db893ef978950afc8130ba707179b4284a"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbcd77c4d94b23e0753c5ed8deba8c69f331d4fd83f68bfc9db58bc8983f49cd"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d177b7e82f6dd5e1aebd24d9c3297c70ce09cd1d5d37b43e53f39514379c029c"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:680b9a36029b30cf063698755d277885d4a0eab70a2c7c6e71aab601323cba45"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1306102f6d9e625cebaca3d4c9c8f10588735ef877f0360b5cdb4fdfd3fd7131"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-win32.whl", hash = "sha256:5b78aa9f4f68212248aaf8943d84c0ff0f74efc65a661c2fc68b82d498311fd5"}, - {file = "SQLAlchemy-2.0.27-cp311-cp311-win_amd64.whl", hash = "sha256:15e19a84b84528f52a68143439d0c7a3a69befcd4f50b8ef9b7b69d2628ae7c4"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0de1263aac858f288a80b2071990f02082c51d88335a1db0d589237a3435fe71"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce850db091bf7d2a1f2fdb615220b968aeff3849007b1204bf6e3e50a57b3d32"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dfc936870507da96aebb43e664ae3a71a7b96278382bcfe84d277b88e379b18"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4fbe6a766301f2e8a4519f4500fe74ef0a8509a59e07a4085458f26228cd7cc"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4535c49d961fe9a77392e3a630a626af5baa967172d42732b7a43496c8b28876"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0fb3bffc0ced37e5aa4ac2416f56d6d858f46d4da70c09bb731a246e70bff4d5"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-win32.whl", hash = "sha256:7f470327d06400a0aa7926b375b8e8c3c31d335e0884f509fe272b3c700a7254"}, - {file = "SQLAlchemy-2.0.27-cp312-cp312-win_amd64.whl", hash = "sha256:f9374e270e2553653d710ece397df67db9d19c60d2647bcd35bfc616f1622dcd"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e97cf143d74a7a5a0f143aa34039b4fecf11343eed66538610debc438685db4a"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7b5a3e2120982b8b6bd1d5d99e3025339f7fb8b8267551c679afb39e9c7c7f1"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e36aa62b765cf9f43a003233a8c2d7ffdeb55bc62eaa0a0380475b228663a38f"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5ada0438f5b74c3952d916c199367c29ee4d6858edff18eab783b3978d0db16d"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b1d9d1bfd96eef3c3faedb73f486c89e44e64e40e5bfec304ee163de01cf996f"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-win32.whl", hash = "sha256:ca891af9f3289d24a490a5fde664ea04fe2f4984cd97e26de7442a4251bd4b7c"}, - {file = "SQLAlchemy-2.0.27-cp37-cp37m-win_amd64.whl", hash = "sha256:fd8aafda7cdff03b905d4426b714601c0978725a19efc39f5f207b86d188ba01"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec1f5a328464daf7a1e4e385e4f5652dd9b1d12405075ccba1df842f7774b4fc"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ad862295ad3f644e3c2c0d8b10a988e1600d3123ecb48702d2c0f26771f1c396"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48217be1de7d29a5600b5c513f3f7664b21d32e596d69582be0a94e36b8309cb"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e56afce6431450442f3ab5973156289bd5ec33dd618941283847c9fd5ff06bf"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:611068511b5531304137bcd7fe8117c985d1b828eb86043bd944cebb7fae3910"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b86abba762ecfeea359112b2bb4490802b340850bbee1948f785141a5e020de8"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-win32.whl", hash = "sha256:30d81cc1192dc693d49d5671cd40cdec596b885b0ce3b72f323888ab1c3863d5"}, - {file = "SQLAlchemy-2.0.27-cp38-cp38-win_amd64.whl", hash = "sha256:120af1e49d614d2525ac247f6123841589b029c318b9afbfc9e2b70e22e1827d"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d07ee7793f2aeb9b80ec8ceb96bc8cc08a2aec8a1b152da1955d64e4825fcbac"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cb0845e934647232b6ff5150df37ceffd0b67b754b9fdbb095233deebcddbd4a"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fc19ae2e07a067663dd24fca55f8ed06a288384f0e6e3910420bf4b1270cc51"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b90053be91973a6fb6020a6e44382c97739736a5a9d74e08cc29b196639eb979"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2f5c9dfb0b9ab5e3a8a00249534bdd838d943ec4cfb9abe176a6c33408430230"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33e8bde8fff203de50399b9039c4e14e42d4d227759155c21f8da4a47fc8053c"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-win32.whl", hash = "sha256:d873c21b356bfaf1589b89090a4011e6532582b3a8ea568a00e0c3aab09399dd"}, - {file = "SQLAlchemy-2.0.27-cp39-cp39-win_amd64.whl", hash = "sha256:ff2f1b7c963961d41403b650842dc2039175b906ab2093635d8319bef0b7d620"}, - {file = "SQLAlchemy-2.0.27-py3-none-any.whl", hash = "sha256:1ab4e0448018d01b142c916cc7119ca573803a4745cfe341b8f95657812700ac"}, - {file = "SQLAlchemy-2.0.27.tar.gz", hash = "sha256:86a6ed69a71fe6b88bf9331594fa390a2adda4a49b5c06f98e47bf0d392534f8"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0b148ab0438f72ad21cb004ce3bdaafd28465c4276af66df3b9ecd2037bf252"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bbda76961eb8f27e6ad3c84d1dc56d5bc61ba8f02bd20fcf3450bd421c2fcc9c"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feea693c452d85ea0015ebe3bb9cd15b6f49acc1a31c28b3c50f4db0f8fb1e71"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5da98815f82dce0cb31fd1e873a0cb30934971d15b74e0d78cf21f9e1b05953f"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4a5adf383c73f2d49ad15ff363a8748319ff84c371eed59ffd0127355d6ea1da"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:56856b871146bfead25fbcaed098269d90b744eea5cb32a952df00d542cdd368"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-win32.whl", hash = "sha256:943aa74a11f5806ab68278284a4ddd282d3fb348a0e96db9b42cb81bf731acdc"}, + {file = "SQLAlchemy-2.0.28-cp310-cp310-win_amd64.whl", hash = "sha256:c6c4da4843e0dabde41b8f2e8147438330924114f541949e6318358a56d1875a"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46a3d4e7a472bfff2d28db838669fc437964e8af8df8ee1e4548e92710929adc"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3dd67b5d69794cfe82862c002512683b3db038b99002171f624712fa71aeaa"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61e2e41656a673b777e2f0cbbe545323dbe0d32312f590b1bc09da1de6c2a02"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0315d9125a38026227f559488fe7f7cee1bd2fbc19f9fd637739dc50bb6380b2"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:af8ce2d31679006e7b747d30a89cd3ac1ec304c3d4c20973f0f4ad58e2d1c4c9"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:81ba314a08c7ab701e621b7ad079c0c933c58cdef88593c59b90b996e8b58fa5"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-win32.whl", hash = "sha256:1ee8bd6d68578e517943f5ebff3afbd93fc65f7ef8f23becab9fa8fb315afb1d"}, + {file = "SQLAlchemy-2.0.28-cp311-cp311-win_amd64.whl", hash = "sha256:ad7acbe95bac70e4e687a4dc9ae3f7a2f467aa6597049eeb6d4a662ecd990bb6"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d3499008ddec83127ab286c6f6ec82a34f39c9817f020f75eca96155f9765097"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b66fcd38659cab5d29e8de5409cdf91e9986817703e1078b2fdaad731ea66f5"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bea30da1e76cb1acc5b72e204a920a3a7678d9d52f688f087dc08e54e2754c67"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:124202b4e0edea7f08a4db8c81cc7859012f90a0d14ba2bf07c099aff6e96462"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e23b88c69497a6322b5796c0781400692eca1ae5532821b39ce81a48c395aae9"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b6303bfd78fb3221847723104d152e5972c22367ff66edf09120fcde5ddc2e2"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-win32.whl", hash = "sha256:a921002be69ac3ab2cf0c3017c4e6a3377f800f1fca7f254c13b5f1a2f10022c"}, + {file = "SQLAlchemy-2.0.28-cp312-cp312-win_amd64.whl", hash = "sha256:b4a2cf92995635b64876dc141af0ef089c6eea7e05898d8d8865e71a326c0385"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8e91b5e341f8c7f1e5020db8e5602f3ed045a29f8e27f7f565e0bdee3338f2c7"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45c7b78dfc7278329f27be02c44abc0d69fe235495bb8e16ec7ef1b1a17952db"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3eba73ef2c30695cb7eabcdb33bb3d0b878595737479e152468f3ba97a9c22a4"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5df5d1dafb8eee89384fb7a1f79128118bc0ba50ce0db27a40750f6f91aa99d5"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2858bbab1681ee5406650202950dc8f00e83b06a198741b7c656e63818633526"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-win32.whl", hash = "sha256:9461802f2e965de5cff80c5a13bc945abea7edaa1d29360b485c3d2b56cdb075"}, + {file = "SQLAlchemy-2.0.28-cp37-cp37m-win_amd64.whl", hash = "sha256:a6bec1c010a6d65b3ed88c863d56b9ea5eeefdf62b5e39cafd08c65f5ce5198b"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:843a882cadebecc655a68bd9a5b8aa39b3c52f4a9a5572a3036fb1bb2ccdc197"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:dbb990612c36163c6072723523d2be7c3eb1517bbdd63fe50449f56afafd1133"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7e4baf9161d076b9a7e432fce06217b9bd90cfb8f1d543d6e8c4595627edb9"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0a5354cb4de9b64bccb6ea33162cb83e03dbefa0d892db88a672f5aad638a75"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:fffcc8edc508801ed2e6a4e7b0d150a62196fd28b4e16ab9f65192e8186102b6"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aca7b6d99a4541b2ebab4494f6c8c2f947e0df4ac859ced575238e1d6ca5716b"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-win32.whl", hash = "sha256:8c7f10720fc34d14abad5b647bc8202202f4948498927d9f1b4df0fb1cf391b7"}, + {file = "SQLAlchemy-2.0.28-cp38-cp38-win_amd64.whl", hash = "sha256:243feb6882b06a2af68ecf4bec8813d99452a1b62ba2be917ce6283852cf701b"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fc4974d3684f28b61b9a90fcb4c41fb340fd4b6a50c04365704a4da5a9603b05"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87724e7ed2a936fdda2c05dbd99d395c91ea3c96f029a033a4a20e008dd876bf"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68722e6a550f5de2e3cfe9da6afb9a7dd15ef7032afa5651b0f0c6b3adb8815d"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:328529f7c7f90adcd65aed06a161851f83f475c2f664a898af574893f55d9e53"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:df40c16a7e8be7413b885c9bf900d402918cc848be08a59b022478804ea076b8"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:426f2fa71331a64f5132369ede5171c52fd1df1bd9727ce621f38b5b24f48750"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-win32.whl", hash = "sha256:33157920b233bc542ce497a81a2e1452e685a11834c5763933b440fedd1d8e2d"}, + {file = "SQLAlchemy-2.0.28-cp39-cp39-win_amd64.whl", hash = "sha256:2f60843068e432311c886c5f03c4664acaef507cf716f6c60d5fde7265be9d7b"}, + {file = "SQLAlchemy-2.0.28-py3-none-any.whl", hash = "sha256:78bb7e8da0183a8301352d569900d9d3594c48ac21dc1c2ec6b3121ed8b6c986"}, + {file = "SQLAlchemy-2.0.28.tar.gz", hash = "sha256:dd53b6c4e6d960600fd6532b79ee28e2da489322fcf6648738134587faf767b6"}, ] [package.dependencies] @@ -1436,13 +1437,13 @@ files = [ [[package]] name = "uvicorn" -version = "0.26.0" +version = "0.27.1" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.26.0-py3-none-any.whl", hash = "sha256:cdb58ef6b8188c6c174994b2b1ba2150a9a8ae7ea5fb2f1b856b94a815d6071d"}, - {file = "uvicorn-0.26.0.tar.gz", hash = "sha256:48bfd350fce3c5c57af5fb4995fded8fb50da3b4feb543eb18ad7e0d54589602"}, + {file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"}, + {file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"}, ] [package.dependencies] @@ -1765,4 +1766,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "143f149033e1e1eeb4b3296db73974864330f9f52b0accf960f1d9d6ab060d2d" +content-hash = "61a46e3489b0d1f6c04e35ff79d63b922387af6ca09df0684a208ce773a3e10e" diff --git a/pyproject.toml b/pyproject.toml index 9a20bdf..816f8f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,32 +7,31 @@ version = "0.1.0-alpha" [tool.poetry.dependencies] python = "^3.12" -alembic = "^1.12.1" +alembic = "^1.13.1" asyncpg = "^0.29.0" -fastapi = "^0.109.0" bcrypt = "^4.1.2" -pydantic = { extras = ["dotenv", "email"], version = "^2.5.3" } -pydantic-settings = "^2.1.0" -python-multipart = "^0.0.6" -sqlalchemy = "^2.0.23" +fastapi = "^0.110.0" +pydantic = {extras = ["dotenv", "email"], version = "^2.6.3"} +pydantic-settings = "^2.2.1" pyjwt = "^2.8.0" - +python-multipart = "^0.0.9" +sqlalchemy = "^2.0.28" [tool.poetry.group.dev.dependencies] -coverage = "^7.3.2" -httpx = "^0.26.0" -pre-commit = "^3.5.0" -pytest = "^7.4.3" -pytest-asyncio = "0.21.1" -ruff = "^0.1.4" -uvicorn = { extras = ["standard"], version = "^0.26.0" } +coverage = "^7.4.3" +freezegun = "^1.4.0" +gevent = "^24.2.1" +httpx = "^0.27.0" mypy = "^1.8.0" +pre-commit = "^3.6.2" +pytest = "^8.0.2" +# do not bump pytest-asyncio until https://github.com/pytest-dev/pytest-asyncio/issues/706 resolved +pytest-asyncio = "0.21.1" pytest-cov = "^4.1.0" -types-passlib = "^1.7.7.20240106" -gevent = "^23.9.1" -freezegun = "^1.4.0" pytest-xdist = "^3.5.0" - +ruff = "^0.3.0" +types-passlib = "^1.7.7.20240106" +uvicorn = {extras = ["standard"], version = "^0.27.1"} [build-system] build-backend = "poetry.core.masonry.api" @@ -44,9 +43,9 @@ asyncio_mode = "auto" testpaths = ["app/tests"] [tool.coverage.run] +concurrency = ["gevent"] omit = ["app/tests/*"] source = ["app"] -concurrency = ["gevent"] [tool.mypy] python_version = "3.12" @@ -57,5 +56,5 @@ target-version = "py312" [tool.ruff.lint] # pycodestyle, pyflakes, isort, pylint, pyupgrade -select = ["E", "W", "F", "I", "PL", "UP"] ignore = ["E501"] +select = ["E", "F", "I", "PL", "UP", "W"] From b0bba3c4cfdb3ed122c2dee1a7c6a6b656b39fb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 4 Mar 2024 22:23:36 +0100 Subject: [PATCH 28/41] improve JWT messages and add comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/api/api_messages.py | 2 -- app/api/api_router.py | 8 ++------ app/core/security/jwt.py | 20 ++++++++++++-------- app/tests/test_api_router_jwt_errors.py | 4 ++-- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/app/api/api_messages.py b/app/api/api_messages.py index 6000de8..226f319 100644 --- a/app/api/api_messages.py +++ b/app/api/api_messages.py @@ -1,5 +1,3 @@ -JWT_ERROR_INVALID_TOKEN = "Token invalid" -JWT_ERROR_EXPIRED_TOKEN = "Token expired" JWT_ERROR_USER_REMOVED = "User removed" PASSWORD_INVALID = "Incorrect email or password" REFRESH_TOKEN_NOT_FOUND = "Refresh token not found" diff --git a/app/api/api_router.py b/app/api/api_router.py index 7c4f117..cc5e555 100644 --- a/app/api/api_router.py +++ b/app/api/api_router.py @@ -18,12 +18,8 @@ "value": {"detail": "Not authenticated"}, }, "invalid token": { - "summary": api_messages.JWT_ERROR_INVALID_TOKEN, - "value": {"detail": api_messages.JWT_ERROR_INVALID_TOKEN}, - }, - "expired token": { - "summary": api_messages.JWT_ERROR_EXPIRED_TOKEN, - "value": {"detail": api_messages.JWT_ERROR_EXPIRED_TOKEN}, + "summary": "Token validation failed, decode failed, it may be expired or malformed", + "value": {"detail": "Token invalid: {detailed error msg}"}, }, "removed user": { "summary": api_messages.JWT_ERROR_USER_REMOVED, diff --git a/app/core/security/jwt.py b/app/core/security/jwt.py index a2b2c07..485b4e7 100644 --- a/app/core/security/jwt.py +++ b/app/core/security/jwt.py @@ -4,12 +4,12 @@ from fastapi import HTTPException, status from pydantic import BaseModel -from app.api import api_messages from app.core.config import get_settings JWT_ALGORITHM = "HS256" +# Payload follows RFC 7519 # https://www.rfc-editor.org/rfc/rfc7519#section-4.1 class JWTTokenPayload(BaseModel): iss: str @@ -44,21 +44,25 @@ def create_jwt_token(user_id: str) -> JWTToken: def verify_jwt_token(token: str) -> JWTTokenPayload: + # Pay attention to verify_signature passed explicite, even if it is the default. + # Verification is based on expected payload fields like "exp", "iat" etc. + # so if you rename for example "exp" to "my_custom_exp", this is gonna break, + # jwt.ExpiredSignatureError will not be raised, that can potentialy + # be major security risk - not validating tokens at all. + # If unsure, jump into jwt.decode code, make sure tests are passing + # https://pyjwt.readthedocs.io/en/stable/usage.html#encoding-decoding-tokens-with-hs256 + try: raw_payload = jwt.decode( token, get_settings().security.jwt_secret_key.get_secret_value(), algorithms=[JWT_ALGORITHM], + options={"verify_signature": True}, ) - except jwt.ExpiredSignatureError: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=api_messages.JWT_ERROR_EXPIRED_TOKEN, - ) - except (jwt.DecodeError, jwt.InvalidTokenError): + except jwt.InvalidTokenError as e: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail=api_messages.JWT_ERROR_INVALID_TOKEN, + detail=f"Token invalid: {e}", ) return JWTTokenPayload(**raw_payload) diff --git a/app/tests/test_api_router_jwt_errors.py b/app/tests/test_api_router_jwt_errors.py index 69e7a56..c702fee 100644 --- a/app/tests/test_api_router_jwt_errors.py +++ b/app/tests/test_api_router_jwt_errors.py @@ -23,7 +23,7 @@ async def test_api_routes_raise_401_on_jwt_decode_errors( headers={"Authorization": "Bearer garbage-invalid-jwt"}, ) assert response.status_code == status.HTTP_401_UNAUTHORIZED - assert response.json() == {"detail": api_messages.JWT_ERROR_INVALID_TOKEN} + assert response.json() == {"detail": "Token invalid: Not enough segments"} @pytest.mark.parametrize("api_route", api_router.routes) @@ -42,7 +42,7 @@ async def test_api_routes_raise_401_on_jwt_expired_token( headers={"Authorization": f"Bearer {jwt.access_token}"}, ) assert response.status_code == status.HTTP_401_UNAUTHORIZED - assert response.json() == {"detail": api_messages.JWT_ERROR_EXPIRED_TOKEN} + assert response.json() == {"detail": "Token invalid: Signature has expired"} @pytest.mark.parametrize("api_route", api_router.routes) From 70393815278c1901a545d752bdff7b4464b6def0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 4 Mar 2024 22:31:25 +0100 Subject: [PATCH 29/41] use session.scalar instead of session.execute where possible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/api/endpoints/auth.py | 10 ++++------ app/tests/test_auth/test_auth_refresh_token.py | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/api/endpoints/auth.py b/app/api/endpoints/auth.py index df17944..ce500f2 100644 --- a/app/api/endpoints/auth.py +++ b/app/api/endpoints/auth.py @@ -70,8 +70,7 @@ async def login_access_token( session: AsyncSession = Depends(deps.get_session), form_data: OAuth2PasswordRequestForm = Depends(), ) -> AccessTokenResponse: - result = await session.execute(select(User).where(User.email == form_data.username)) - user = result.scalars().first() + user = await session.scalar(select(User).where(User.email == form_data.username)) if user is None: # this is naive method to not return early @@ -116,12 +115,11 @@ async def refresh_token( data: RefreshTokenRequest, session: AsyncSession = Depends(deps.get_session), ) -> AccessTokenResponse: - result = await session.execute( + token = await session.scalar( select(RefreshToken) .where(RefreshToken.refresh_token == data.refresh_token) .with_for_update(skip_locked=True) ) - token = result.scalars().first() if token is None: raise HTTPException( @@ -170,8 +168,8 @@ async def register_new_user( new_user: UserCreateRequest, session: AsyncSession = Depends(deps.get_session), ) -> User: - result = await session.execute(select(User).where(User.email == new_user.email)) - if result.scalars().first() is not None: + user = await session.scalar(select(User).where(User.email == new_user.email)) + if user is not None: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail=api_messages.EMAIL_ADDRESS_ALREADY_USED, diff --git a/app/tests/test_auth/test_auth_refresh_token.py b/app/tests/test_auth/test_auth_refresh_token.py index d82a8f1..1da7dbc 100644 --- a/app/tests/test_auth/test_auth_refresh_token.py +++ b/app/tests/test_auth/test_auth_refresh_token.py @@ -122,10 +122,10 @@ async def test_refresh_token_success_old_token_is_used( }, ) - result = await session.execute( + test_refresh_token = await session.scalar( select(RefreshToken).where(RefreshToken.refresh_token == "blaxx") ) - test_refresh_token = result.scalar_one() + assert test_refresh_token is not None assert test_refresh_token.used From 7e2c0d0effd65bb4e48df1d9eb7c132ef2a9be4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 4 Mar 2024 22:32:27 +0100 Subject: [PATCH 30/41] shorter get_current_user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/api/deps.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/api/deps.py b/app/api/deps.py index 8c37590..c52c243 100644 --- a/app/api/deps.py +++ b/app/api/deps.py @@ -25,12 +25,9 @@ async def get_current_user( ) -> User: token_payload = verify_jwt_token(token) - result = await session.execute( - select(User).where(User.user_id == token_payload.sub) - ) - user = result.scalars().first() + user = await session.scalar(select(User).where(User.user_id == token_payload.sub)) - if not user: + if user is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail=api_messages.JWT_ERROR_USER_REMOVED, From b24bfc9f37195ee3ba468a904caee2e333232ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 4 Mar 2024 22:34:42 +0100 Subject: [PATCH 31/41] add freeze time to 2 tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/tests/test_auth/test_access_token.py | 3 ++- app/tests/test_auth/test_auth_refresh_token.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/tests/test_auth/test_access_token.py b/app/tests/test_auth/test_access_token.py index 3192a06..e1e0d38 100644 --- a/app/tests/test_auth/test_access_token.py +++ b/app/tests/test_auth/test_access_token.py @@ -69,6 +69,7 @@ async def test_login_access_token_jwt_has_valid_expire_time( ) +@freeze_time("2023-01-01") async def test_login_access_token_returns_valid_jwt_access_token( client: AsyncClient, default_user: User, @@ -87,7 +88,7 @@ async def test_login_access_token_returns_valid_jwt_access_token( token_payload = verify_jwt_token(token["access_token"]) assert token_payload.sub == default_user.user_id - assert token_payload.iat >= now + assert token_payload.iat == now assert token_payload.exp == token["expires_at"] diff --git a/app/tests/test_auth/test_auth_refresh_token.py b/app/tests/test_auth/test_auth_refresh_token.py index 1da7dbc..6d03e82 100644 --- a/app/tests/test_auth/test_auth_refresh_token.py +++ b/app/tests/test_auth/test_auth_refresh_token.py @@ -184,6 +184,7 @@ async def test_refresh_token_success_jwt_has_valid_expire_time( ) +@freeze_time("2023-01-01") async def test_refresh_token_success_jwt_has_valid_access_token( client: AsyncClient, default_user: User, @@ -210,7 +211,7 @@ async def test_refresh_token_success_jwt_has_valid_access_token( token_payload = verify_jwt_token(token["access_token"]) assert token_payload.sub == default_user.user_id - assert token_payload.iat >= now + assert token_payload.iat == now assert token_payload.exp == token["expires_at"] From 326e1bfaa42762357be97ee699637082cf04ab12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 4 Mar 2024 22:48:46 +0100 Subject: [PATCH 32/41] unify tokens time to use time.time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/tests/test_auth/test_access_token.py | 8 ++++---- app/tests/test_auth/test_auth_refresh_token.py | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/tests/test_auth/test_access_token.py b/app/tests/test_auth/test_access_token.py index e1e0d38..58977ff 100644 --- a/app/tests/test_auth/test_access_token.py +++ b/app/tests/test_auth/test_access_token.py @@ -1,4 +1,4 @@ -from datetime import UTC, datetime +import time from fastapi import status from freezegun import freeze_time @@ -62,7 +62,7 @@ async def test_login_access_token_jwt_has_valid_expire_time( ) token = response.json() - current_timestamp = int(datetime.now(tz=UTC).timestamp()) + current_timestamp = int(time.time()) assert ( token["expires_at"] == current_timestamp + get_settings().security.jwt_access_token_expire_secs @@ -83,7 +83,7 @@ async def test_login_access_token_returns_valid_jwt_access_token( headers={"Content-Type": "application/x-www-form-urlencoded"}, ) - now = int(datetime.now(tz=UTC).timestamp()) + now = int(time.time()) token = response.json() token_payload = verify_jwt_token(token["access_token"]) @@ -106,7 +106,7 @@ async def test_login_access_token_refresh_token_has_valid_expire_time( ) token = response.json() - current_time = int(datetime.now(tz=UTC).timestamp()) + current_time = int(time.time()) assert ( token["refresh_token_expires_at"] == current_time + get_settings().security.refresh_token_expire_secs diff --git a/app/tests/test_auth/test_auth_refresh_token.py b/app/tests/test_auth/test_auth_refresh_token.py index 6d03e82..9933e42 100644 --- a/app/tests/test_auth/test_auth_refresh_token.py +++ b/app/tests/test_auth/test_auth_refresh_token.py @@ -1,5 +1,4 @@ import time -from datetime import UTC, datetime from fastapi import status from freezegun import freeze_time @@ -177,7 +176,7 @@ async def test_refresh_token_success_jwt_has_valid_expire_time( ) token = response.json() - current_timestamp = int(datetime.now(tz=UTC).timestamp()) + current_timestamp = int(time.time()) assert ( token["expires_at"] == current_timestamp + get_settings().security.jwt_access_token_expire_secs @@ -206,7 +205,7 @@ async def test_refresh_token_success_jwt_has_valid_access_token( }, ) - now = int(datetime.now(tz=UTC).timestamp()) + now = int(time.time()) token = response.json() token_payload = verify_jwt_token(token["access_token"]) @@ -238,7 +237,7 @@ async def test_refresh_token_success_refresh_token_has_valid_expire_time( ) token = response.json() - current_time = int(datetime.now(tz=UTC).timestamp()) + current_time = int(time.time()) assert ( token["refresh_token_expires_at"] == current_time + get_settings().security.refresh_token_expire_secs From a6bd5dc7e6dba663164b4e14f51dbb482c941c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Mon, 4 Mar 2024 23:46:00 +0100 Subject: [PATCH 33/41] jwt and password core tests added MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- app/core/security/jwt.py | 1 + app/tests/conftest.py | 7 ++ .../test_auth/test_auth_refresh_token.py | 6 +- app/tests/test_core/__init__.py | 0 app/tests/test_core/test_jwt.py | 84 +++++++++++++++++++ app/tests/test_core/test_password.py | 11 +++ 6 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 app/tests/test_core/__init__.py create mode 100644 app/tests/test_core/test_jwt.py create mode 100644 app/tests/test_core/test_password.py diff --git a/app/core/security/jwt.py b/app/core/security/jwt.py index 485b4e7..ac96fa4 100644 --- a/app/core/security/jwt.py +++ b/app/core/security/jwt.py @@ -58,6 +58,7 @@ def verify_jwt_token(token: str) -> JWTTokenPayload: get_settings().security.jwt_secret_key.get_secret_value(), algorithms=[JWT_ALGORITHM], options={"verify_signature": True}, + issuer=get_settings().security.jwt_issuer, ) except jwt.InvalidTokenError as e: raise HTTPException( diff --git a/app/tests/conftest.py b/app/tests/conftest.py index ae7bef4..fa53dc6 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -70,6 +70,13 @@ async def fixture_setup_new_test_database() -> None: await conn.run_sync(Base.metadata.create_all) +@pytest_asyncio.fixture(scope="function", autouse=True) +async def fixture_clean_get_settings_between_tests() -> AsyncGenerator[None, None]: + yield + + get_settings.cache_clear() + + @pytest_asyncio.fixture(name="default_hashed_password", scope="session") async def fixture_default_hashed_password() -> str: return get_password_hash(default_user_password) diff --git a/app/tests/test_auth/test_auth_refresh_token.py b/app/tests/test_auth/test_auth_refresh_token.py index 9933e42..4578feb 100644 --- a/app/tests/test_auth/test_auth_refresh_token.py +++ b/app/tests/test_auth/test_auth_refresh_token.py @@ -121,11 +121,11 @@ async def test_refresh_token_success_old_token_is_used( }, ) - test_refresh_token = await session.scalar( + used_test_refresh_token = await session.scalar( select(RefreshToken).where(RefreshToken.refresh_token == "blaxx") ) - assert test_refresh_token is not None - assert test_refresh_token.used + assert used_test_refresh_token is not None + assert used_test_refresh_token.used async def test_refresh_token_success_jwt_has_valid_token_type( diff --git a/app/tests/test_core/__init__.py b/app/tests/test_core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tests/test_core/test_jwt.py b/app/tests/test_core/test_jwt.py new file mode 100644 index 0000000..6079551 --- /dev/null +++ b/app/tests/test_core/test_jwt.py @@ -0,0 +1,84 @@ +import time + +import pytest +from fastapi import HTTPException +from freezegun import freeze_time +from pydantic import SecretStr + +from app.core.config import get_settings +from app.core.security import jwt + + +def test_jwt_access_token_can_be_decoded_back_into_user_id() -> None: + user_id = "test_user_id" + token = jwt.create_jwt_token(user_id) + + payload = jwt.verify_jwt_token(token=token.access_token) + assert payload.sub == user_id + + +@freeze_time("2024-01-01") +def test_jwt_payload_is_correct() -> None: + user_id = "test_user_id" + token = jwt.create_jwt_token(user_id) + + assert token.payload.iat == int(time.time()) + assert token.payload.sub == user_id + assert token.payload.iss == get_settings().security.jwt_issuer + assert ( + token.payload.exp + == int(time.time()) + get_settings().security.jwt_access_token_expire_secs + ) + + +def test_jwt_error_after_exp_time() -> None: + user_id = "test_user_id" + with freeze_time("2024-01-01"): + token = jwt.create_jwt_token(user_id) + with freeze_time("2024-02-01"): + with pytest.raises(HTTPException) as e: + jwt.verify_jwt_token(token=token.access_token) + + assert e.value.detail == "Token invalid: Signature has expired" + + +def test_jwt_error_before_iat_time() -> None: + user_id = "test_user_id" + with freeze_time("2024-01-01"): + token = jwt.create_jwt_token(user_id) + with freeze_time("2023-12-01"): + with pytest.raises(HTTPException) as e: + jwt.verify_jwt_token(token=token.access_token) + + assert e.value.detail == "Token invalid: The token is not yet valid (iat)" + + +def test_jwt_error_with_invalid_token() -> None: + with pytest.raises(HTTPException) as e: + jwt.verify_jwt_token(token="invalid!") + + assert e.value.detail == "Token invalid: Not enough segments" + + +def test_jwt_error_with_invalid_issuer() -> None: + user_id = "test_user_id" + token = jwt.create_jwt_token(user_id) + + get_settings().security.jwt_issuer = "another_issuer" + + with pytest.raises(HTTPException) as e: + jwt.verify_jwt_token(token=token.access_token) + + assert e.value.detail == "Token invalid: Invalid issuer" + + +def test_jwt_error_with_invalid_secret_key() -> None: + user_id = "test_user_id" + token = jwt.create_jwt_token(user_id) + + get_settings().security.jwt_secret_key = SecretStr("the secret has changed now!") + + with pytest.raises(HTTPException) as e: + jwt.verify_jwt_token(token=token.access_token) + + assert e.value.detail == "Token invalid: Signature verification failed" diff --git a/app/tests/test_core/test_password.py b/app/tests/test_core/test_password.py new file mode 100644 index 0000000..9364506 --- /dev/null +++ b/app/tests/test_core/test_password.py @@ -0,0 +1,11 @@ +from app.core.security.password import get_password_hash, verify_password + + +def test_hashed_password_is_verified() -> None: + pwd_hash = get_password_hash("my_password") + assert verify_password("my_password", pwd_hash) + + +def test_invalid_password_is_not_verified() -> None: + pwd_hash = get_password_hash("my_password") + assert not verify_password("my_password_invalid", pwd_hash) From 9b70ff7c51b80fda5dd370c36f20c4b0bb3c06d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Tue, 5 Mar 2024 00:14:38 +0100 Subject: [PATCH 34/41] add dependabot and update workflows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- .github/dependabot.yml | 25 +++++++++ .github/workflows/dev_build.yml | 36 +++++++++++++ .../workflows/manual_build_docker_image.yml | 33 ------------ .github/workflows/tests.yml | 31 ++++++----- .github/workflows/type_check.yml | 51 +++++++++++++++++++ Dockerfile | 4 +- 6 files changed, 132 insertions(+), 48 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/dev_build.yml delete mode 100644 .github/workflows/manual_build_docker_image.yml create mode 100644 .github/workflows/type_check.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..382c8f2 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,25 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + allow: + - dependency-type: "all" + groups: + all-dependencies: + patterns: + - "*" + exclude-patterns: + - "pytest-asyncio" + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + + - package-ecosystem: docker + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/dev_build.yml b/.github/workflows/dev_build.yml new file mode 100644 index 0000000..7a0845a --- /dev/null +++ b/.github/workflows/dev_build.yml @@ -0,0 +1,36 @@ +name: dev-build +on: + workflow_run: + workflows: ["tests"] + branches: [main] + types: + - completed + + workflow_dispatch: + inputs: + tag: + description: "Docker image tag" + required: true + default: "latest" + +env: + IMAGE_TAG: ${{ github.event.inputs.tag || 'latest' }} + +jobs: + dev_build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USER }} + password: ${{ secrets.DOCKER_PASS }} + + - name: Build and push image + uses: docker/build-push-action@v5 + with: + file: Dockerfile + push: true + tags: rafsaf/minimal-fastapi-postgres-template:${{ env.IMAGE_TAG }} diff --git a/.github/workflows/manual_build_docker_image.yml b/.github/workflows/manual_build_docker_image.yml deleted file mode 100644 index 0c667cf..0000000 --- a/.github/workflows/manual_build_docker_image.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Manual push docker image to dockerhub - -on: - workflow_dispatch: - inputs: - tag: - description: "Docker image tag" - required: true - default: "stable" - -jobs: - manual_push_image_to_dockerhub: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: "3.12" - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_USER }} - password: ${{ secrets.DOCKER_PASS }} - - - name: Build and push image - uses: docker/build-push-action@v3 - with: - file: Dockerfile - push: true - tags: rafsaf/minimal-fastapi-postgres-template:${{ github.event.inputs.tag }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 93052a1..ba62473 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,11 +1,14 @@ -name: Run tests - +name: tests on: push: + branches: + - "**" + tags-ignore: + - "*.*" jobs: - build: - runs-on: ubuntu-20.04 + tests: + runs-on: ubuntu-latest services: postgres: image: postgres @@ -17,35 +20,37 @@ jobs: --health-timeout 5s --health-retries 5 ports: - - 31234:5432 + - 5432:5432 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version: "3.12.2" - name: Install Poetry uses: snok/install-poetry@v1 with: virtualenvs-create: true - virtualenvs-in-project: true + virtualenvs-in-project: false + virtualenvs-path: /opt/venv - name: Load cached venv id: cached-poetry-dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: - path: .venv - key: venv-${{ runner.os }}-${{ hashFiles('poetry.lock') }} + path: /opt/venv + key: venv-${{ runner.os }}-python-3.12.2-${{ hashFiles('poetry.lock') }} - - name: Run poetry install + - name: Install dependencies and actiavte virtualenv if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' run: | poetry install --no-interaction --no-root - name: Run tests env: + SECURITY__JWT_SECRET_KEY: very-not-secret DATABASE__HOSTNAME: localhost DATABASE__PASSWORD: postgres run: | diff --git a/.github/workflows/type_check.yml b/.github/workflows/type_check.yml new file mode 100644 index 0000000..ffde1e6 --- /dev/null +++ b/.github/workflows/type_check.yml @@ -0,0 +1,51 @@ +name: type-check +on: + push: + branches: + - "**" + tags-ignore: + - "*.*" + +jobs: + type_check: + strategy: + matrix: + check: ["ruff check", "mypy --check", "ruff format --check"] + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12.2" + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: false + virtualenvs-path: /opt/venv + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4 + with: + path: /opt/venv + key: venv-${{ runner.os }}-python-3.12.2-${{ hashFiles('poetry.lock') }} + + - name: Install dependencies and actiavte virtualenv + if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' + run: | + poetry install --no-interaction --no-root + + - name: Run ${{ matrix.check }} + run: | + poetry run ${{ matrix.check }} . diff --git a/Dockerfile b/Dockerfile index 451a6ab..2023ffa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -FROM python:3.12.1-slim-bullseye as base +FROM python:3.12.2-slim-bullseye as base ENV PYTHONUNBUFFERED 1 WORKDIR /build # Create requirements.txt file FROM base as poetry -RUN pip install poetry==1.7.1 +RUN pip install poetry==1.8.2 COPY poetry.lock pyproject.toml ./ RUN poetry export -o /requirements.txt --without-hashes From 6e4209a33052a6e394ae66b07694e9de4f7c464a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Tue, 5 Mar 2024 00:25:40 +0100 Subject: [PATCH 35/41] readme - update Features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 03099e8..647261f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![License](https://img.shields.io/github/license/rafsaf/minimal-fastapi-postgres-template)](https://github.com/rafsaf/minimal-fastapi-postgres-template/blob/main/LICENSE) [![Python 3.12](https://img.shields.io/badge/python-3.12-blue)](https://docs.python.org/3/whatsnew/3.12.html) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![Black](https://img.shields.io/badge/code%20style-black-lightgrey)](https://github.com/psf/black) [![Tests](https://github.com/rafsaf/minimal-fastapi-postgres-template/actions/workflows/tests.yml/badge.svg)](https://github.com/rafsaf/minimal-fastapi-postgres-template/actions/workflows/tests.yml) # Minimal async FastAPI + PostgreSQL template @@ -28,15 +27,15 @@ ## Features -- [x] **SQLAlchemy 2.0 only**, async queries, best possible autocompletion support (SQLAlchemy 2.0.0 was released January 26, 2023) +- [x] SQLAlchemy 2.0, async queries, best possible autocompletion support - [x] Postgresql database under `asyncpg` - [x] [Alembic](https://alembic.sqlalchemy.org/en/latest/) migrations - [x] Very minimal project structure yet ready for quick start building new apps - [x] Refresh token endpoint (not only access like in official template) -- [x] Two databases in docker-compose.yml (second one for tests) and ready to go Dockerfile with [uvicorn](https://www.uvicorn.org/) webserver +- [x] Database in docker-compose.yml and ready to go Dockerfile with [uvicorn](https://www.uvicorn.org/) webserver - [x] [Poetry](https://python-poetry.org/docs/) and Python 3.12 based -- [x] `pre-commit` with poetry export and [ruff](https://github.com/astral-sh/ruff) -- [x] Rich setup for pytest async tests with few included and extensible `conftest.py` +- [x] `pre-commit` hooks with [ruff](https://github.com/astral-sh/ruff) +- [x] **Perfect** pytest asynchronous test setup with +40 tests and full coverage
    From 6a75069b21ad568281dbb315c703ddd717d5004e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Tue, 5 Mar 2024 00:40:34 +0100 Subject: [PATCH 36/41] Readme- Quickstart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- README.md | 88 ++++++++++++++++++++++++------------------------------- 1 file changed, 38 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 647261f..01727c7 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ - [Minimal async FastAPI + PostgreSQL template](#minimal-async-fastapi--postgresql-template) - [Features](#features) - [Quickstart](#quickstart) - - [1. Install cookiecutter globally and cookiecutter this project](#1-install-cookiecutter-globally-and-cookiecutter-this-project) - - [2. Install dependecies with poetry or without it](#2-install-dependecies-with-poetry-or-without-it) - - [3. Setup databases](#3-setup-databases) + - [1. Create repository from a template](#1-create-repository-from-a-template) + - [2. Install dependecies with Poetry](#2-install-dependecies-with-poetry) + - [3. Setup database and migrations](#3-setup-database-and-migrations) - [4. Now you can run app](#4-now-you-can-run-app) - [5. Activate pre-commit](#5-activate-pre-commit) - [6. Running tests](#6-running-tests) @@ -22,24 +22,26 @@ - [3. Create request and response schemas](#3-create-request-and-response-schemas) - [4. Create endpoints](#4-create-endpoints) - [5. Write tests](#5-write-tests) - - [Deployment strategies - via Docker image](#deployment-strategies---via-docker-image) - - [Docs URL, CORS and Allowed Hosts](#docs-url-cors-and-allowed-hosts) + - [Design](#design) + - [Deployment strategies - via Docker image](#deployment-strategies---via-docker-image) + - [Docs URL, CORS and Allowed Hosts](#docs-url-cors-and-allowed-hosts) + - [Test setup](#test-setup) + ## Features +- [x] Template repository - [x] SQLAlchemy 2.0, async queries, best possible autocompletion support -- [x] Postgresql database under `asyncpg` -- [x] [Alembic](https://alembic.sqlalchemy.org/en/latest/) migrations -- [x] Very minimal project structure yet ready for quick start building new apps +- [x] PostgreSQL 16 database under `asyncpg`, docker-compose.yml +- [x] Full [Alembic](https://alembic.sqlalchemy.org/en/latest/) migrations setup - [x] Refresh token endpoint (not only access like in official template) -- [x] Database in docker-compose.yml and ready to go Dockerfile with [uvicorn](https://www.uvicorn.org/) webserver -- [x] [Poetry](https://python-poetry.org/docs/) and Python 3.12 based -- [x] `pre-commit` hooks with [ruff](https://github.com/astral-sh/ruff) +- [x] Ready to go Dockerfile with [uvicorn](https://www.uvicorn.org/) webserver as an example +- [x] [Poetry](https://python-poetry.org/docs/), `mypy`, `pre-commit` hooks with [ruff](https://github.com/astral-sh/ruff) - [x] **Perfect** pytest asynchronous test setup with +40 tests and full coverage
    -_Check out also online example: https://minimal-fastapi-postgres-template.rafsaf.pl, it's 100% code used in template (docker image) with added domain and https only._ +_Check out also online example: https://minimal-fastapi-postgres-template.rafsaf.pl, it's 100% code used in template (docker image) with added my domain and https only._ ![template-fastapi-minimal-openapi-example](https://drive.google.com/uc?export=view&id=1rIXFJK8VyVrV7v4qgtPFryDd5FQrb4gr) @@ -47,39 +49,29 @@ _Check out also online example: https://minimal-fastapi-postgres-template.rafsaf ## Quickstart -### 1. Install cookiecutter globally and cookiecutter this project - -```bash -pip install cookiecutter +### 1. Create repository from a template -# And cookiecutter this project :) -cookiecutter https://github.com/rafsaf/minimal-fastapi-postgres-template +See [docs](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template). -``` - -### 2. Install dependecies with poetry or without it +### 2. Install dependecies with [Poetry](https://python-poetry.org/docs/) ```bash -cd project_name +cd your_project_name + ### Poetry install (python3.12) poetry install - -### Optionally there is also `requirements-dev.txt` file -python3.12 -m venv venv -source venv/bin/activate -pip install -r requirements-dev.txt ``` Note, be sure to use `python3.12` with this template with either poetry or standard venv & pip, if you need to stick to some earlier python version, you should adapt it yourself (remove new versions specific syntax for example `str | int` for python < 3.10 or `tomllib` for python < 3.11) -### 3. Setup databases +### 3. Setup database and migrations ```bash -### Setup two databases +### Setup database docker-compose up -d -### Alembic migrations upgrade and initial_data.py script -bash init.sh +### Run Alembic migrations +alembic upgrade head ``` ### 4. Now you can run app @@ -94,35 +86,27 @@ You should then use `git init` to initialize git repository and access OpenAPI s ### 5. Activate pre-commit -[pre-commit](https://pre-commit.com/) is de facto standard now for pre push activities like isort or black. +[pre-commit](https://pre-commit.com/) is de facto standard now for pre push activities like isort or black or its replacement ruff. -Refer to `.pre-commit-config.yaml` file to see my opinionated choices. +Refer to `.pre-commit-config.yaml` file to see my current opinionated choices. ```bash # Install pre-commit -pre-commit install +pre-commit install --install-hooks -# First initialization and run on all files +# Run on all files pre-commit run --all-files ``` ### 6. Running tests +Note, it will create databases during and run tests in many processes by default, based on how many CPU are available. + +For more details, see [Test setup](#test-setup). + ```bash -# Note, it will use second database declared in docker-compose.yml, not default one +# see pytest configuration flags in pyproject.toml pytest - -# collected 7 items - -# app/tests/test_auth.py::test_auth_access_token PASSED [ 14%] -# app/tests/test_auth.py::test_auth_access_token_fail_no_user PASSED [ 28%] -# app/tests/test_auth.py::test_auth_refresh_token PASSED [ 42%] -# app/tests/test_users.py::test_read_current_user PASSED [ 57%] -# app/tests/test_users.py::test_delete_current_user PASSED [ 71%] -# app/tests/test_users.py::test_reset_current_user_password PASSED [ 85%] -# app/tests/test_users.py::test_register_new_user PASSED [100%] -# -# ======================================================== 7 passed in 1.75s ======================================================== ```
    @@ -368,13 +352,15 @@ async def test_get_all_my_pets( ``` -## Deployment strategies - via Docker image +## Design + +### Deployment strategies - via Docker image This template has by default included `Dockerfile` with [Uvicorn](https://www.uvicorn.org/) webserver, because it's simple and just for showcase purposes, with direct relation to FastAPI and great ease of configuration. You should be able to run container(s) (over :8000 port) and then need to setup the proxy, loadbalancer, with https enbaled, so the app stays behind it. If you prefer other webservers for FastAPI, check out [Nginx Unit](https://unit.nginx.org/), [Daphne](https://github.com/django/daphne), [Hypercorn](https://pgjones.gitlab.io/hypercorn/index.html). -## Docs URL, CORS and Allowed Hosts +### Docs URL, CORS and Allowed Hosts There are some **opinionated** default settings in `/app/main.py` for documentation, CORS and allowed hosts. @@ -413,3 +399,5 @@ There are some **opinionated** default settings in `/app/main.py` for documentat ``` Prevents HTTP Host Headers attack, you shoud put here you server IP or (preferably) full domain under it's accessible like `example.com`. By default in .env there are two most popular records: `ALLOWED_HOSTS=["localhost", "127.0.0.1"]` + +### Test setup \ No newline at end of file From 5a19c5e301d6ecc80387627ed83817a47e1d5d10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Tue, 5 Mar 2024 00:42:59 +0100 Subject: [PATCH 37/41] readme -move check out phrase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 01727c7..afbed70 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Tests](https://github.com/rafsaf/minimal-fastapi-postgres-template/actions/workflows/tests.yml/badge.svg)](https://github.com/rafsaf/minimal-fastapi-postgres-template/actions/workflows/tests.yml) +_Check out online example: https://minimal-fastapi-postgres-template.rafsaf.pl, it's 100% code used in template (docker image) with added my domain and https only._ + # Minimal async FastAPI + PostgreSQL template - [Minimal async FastAPI + PostgreSQL template](#minimal-async-fastapi--postgresql-template) @@ -41,7 +43,7 @@
    -_Check out also online example: https://minimal-fastapi-postgres-template.rafsaf.pl, it's 100% code used in template (docker image) with added my domain and https only._ + ![template-fastapi-minimal-openapi-example](https://drive.google.com/uc?export=view&id=1rIXFJK8VyVrV7v4qgtPFryDd5FQrb4gr) From 8711ae0cf37b620cb7888ff1a358edb182e4a6aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Tue, 5 Mar 2024 01:13:22 +0100 Subject: [PATCH 38/41] readme - update about and step by step section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- README.md | 170 +++++++++++++++++++++++++++--------------------------- 1 file changed, 85 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index afbed70..66d4ac4 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ cd your_project_name poetry install ``` -Note, be sure to use `python3.12` with this template with either poetry or standard venv & pip, if you need to stick to some earlier python version, you should adapt it yourself (remove new versions specific syntax for example `str | int` for python < 3.10 or `tomllib` for python < 3.11) +Note, be sure to use `python3.12` with this template with either poetry or standard venv & pip, if you need to stick to some earlier python version, you should adapt it yourself (remove new versions specific syntax for example `str | int` for python < 3.10) ### 3. Setup database and migrations @@ -84,11 +84,11 @@ uvicorn app.main:app --reload ``` -You should then use `git init` to initialize git repository and access OpenAPI spec at http://localhost:8000/ by default. To customize docs url, cors and allowed hosts settings, read section about it. +You should then use `git init` (if needed) to initialize git repository and access OpenAPI spec at http://localhost:8000/ by default. To customize docs url, cors and allowed hosts settings, read [section about it](#docs-url-cors-and-allowed-hosts). ### 5. Activate pre-commit -[pre-commit](https://pre-commit.com/) is de facto standard now for pre push activities like isort or black or its replacement ruff. +[pre-commit](https://pre-commit.com/) is de facto standard now for pre push activities like isort or black or its nowadays replacement ruff. Refer to `.pre-commit-config.yaml` file to see my current opinionated choices. @@ -119,6 +119,14 @@ This project is heavily based on the official template https://github.com/tiango `2.0` style SQLAlchemy API is good enough so there is no need to write everything in `crud` and waste our time... The `core` folder was also rewritten. There is great base for writting tests in `tests`, but I didn't want to write hundreds of them, I noticed that usually after changes in the structure of the project, auto tests are useless and you have to write them from scratch anyway (delete old ones...), hence less than more. Similarly with the `User` model, it is very modest, with just `id` (uuid), `email` and `password_hash`, because it will be adapted to the project anyway. +2024 update: + +The template was adpoted to my current style and knowledge, the test based expanded to cover more, added mypy, ruff and test setup was completly rewritten to have three things: + +- run test in paraller in many processes for speed +- transactions rollback after every test +- create test databases instead of having another in docker-compose.yml +
    ## Step by step example - POST and GET endpoints @@ -132,48 +140,25 @@ I always enjoy to have some kind of an example in templates (even if I don't lik ### 1. Create SQLAlchemy model -We will add Pet model to `app/models.py`. To keep things clear, below is full result of models.py file. +We will add `Pet` model to `app/models.py`. ```python # app/models.py -import uuid - -from sqlalchemy import ForeignKey, Integer, String -from sqlalchemy.dialects.postgresql import UUID -from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column - - -class Base(DeclarativeBase): - pass - - -class User(Base): - __tablename__ = "user_model" - - id: Mapped[str] = mapped_column( - UUID(as_uuid=False), primary_key=True, default=lambda _: str(uuid.uuid4()) - ) - email: Mapped[str] = mapped_column( - String(254), nullable=False, unique=True, index=True - ) - hashed_password: Mapped[str] = mapped_column(String(128), nullable=False) - +... class Pet(Base): __tablename__ = "pet" - id: Mapped[int] = mapped_column(Integer, primary_key=True) + id: Mapped[int] = mapped_column(BigInteger, primary_key=True) user_id: Mapped[str] = mapped_column( - ForeignKey("user_model.id", ondelete="CASCADE"), + ForeignKey("user_account.user_id", ondelete="CASCADE"), ) pet_name: Mapped[str] = mapped_column(String(50), nullable=False) - - ``` -Note, we are using super powerful SQLAlchemy feature here - Mapped and mapped_column were first introduced in SQLAlchemy 2.0 on Feb 26, if this syntax is new for you, read carefully "what's new" part of documentation https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html. +Note, we are using super powerful SQLAlchemy feature here - Mapped and mapped_column were first introduced in SQLAlchemy 2.0, if this syntax is new for you, read carefully "what's new" part of documentation https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html.
    @@ -199,15 +184,13 @@ alembic upgrade head # INFO [alembic.runtime.migration] Running upgrade d1252175c146 -> 44b7b689ea5f, create_pet_model ``` -PS. Note, alembic is configured in a way that it work with async setup and also detects specific column changes. +PS. Note, alembic is configured in a way that it work with async setup and also detects specific column changes if using `--autogenerate` flag.
    ### 3. Create request and response schemas -I personally lately (after seeing clear benefits at work in Samsung) prefer less files than a lot of them for things like schemas. - -Thats why there are only 2 files: `requests.py` and `responses.py` in `schemas` folder and I would keep it that way even for few dozen of endpoints. Not to mention this is opinionated. +There are only 2 files: `requests.py` and `responses.py` in `schemas` folder and I would keep it that way even for few dozen of endpoints. Not to mention this is opinionated. ```python # app/schemas/requests.py @@ -238,9 +221,9 @@ class PetResponse(BaseResponse): ### 4. Create endpoints ```python -# /app/api/endpoints/pets.py +# app/api/endpoints/pets.py -from fastapi import APIRouter, Depends +from fastapi import APIRouter, Depends, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession @@ -252,46 +235,54 @@ from app.schemas.responses import PetResponse router = APIRouter() -@router.post("/create", response_model=PetResponse, status_code=201) +@router.post( + "/create", + response_model=PetResponse, + status_code=status.HTTP_201_CREATED, + description="Creates new pet. Only for logged users.", +) async def create_new_pet( - new_pet: PetCreateRequest, + data: PetCreateRequest, session: AsyncSession = Depends(deps.get_session), current_user: User = Depends(deps.get_current_user), -): - """Creates new pet. Only for logged users.""" +) -> Pet: + new_pet = Pet(user_id=current_user.user_id, pet_name=data.pet_name) - pet = Pet(user_id=current_user.id, pet_name=new_pet.pet_name) - - session.add(pet) + session.add(new_pet) await session.commit() - return pet + + return new_pet -@router.get("/me", response_model=list[PetResponse], status_code=200) +@router.get( + "/me", + response_model=list[PetResponse], + status_code=status.HTTP_200_OK, + description="Get list of pets for currently logged user.", +) async def get_all_my_pets( session: AsyncSession = Depends(deps.get_session), current_user: User = Depends(deps.get_current_user), -): - """Get list of pets for currently logged user.""" +) -> list[Pet]: + pets = await session.scalars( + select(Pet).where(Pet.user_id == current_user.user_id).order_by(Pet.pet_name) + ) - stmt = select(Pet).where(Pet.user_id == current_user.id).order_by(Pet.pet_name) - pets = await session.execute(stmt) - return pets.scalars().all() + return list(pets.all()) ``` Also, we need to add newly created endpoints to router. ```python -# /app/api/api.py +# app/api/api.py -from fastapi import APIRouter +... from app.api.endpoints import auth, pets, users -api_router = APIRouter() -api_router.include_router(auth.router, prefix="/auth", tags=["auth"]) -api_router.include_router(users.router, prefix="/users", tags=["users"]) +... + api_router.include_router(pets.router, prefix="/pets", tags=["pets"]) ``` @@ -300,9 +291,12 @@ api_router.include_router(pets.router, prefix="/pets", tags=["pets"]) ### 5. Write tests +We will write two really simple tests in combined file inside newly created `app/tests/test_pets` folder. + ```python -# /app/tests/test_pets.py +# app/tests/test_pets/test_pets.py +from fastapi import status from httpx import AsyncClient from sqlalchemy.ext.asyncio import AsyncSession @@ -311,24 +305,29 @@ from app.models import Pet, User async def test_create_new_pet( - client: AsyncClient, default_user_headers, default_user: User -): + client: AsyncClient, default_user_headers: dict[str, str], default_user: User +) -> None: response = await client.post( app.url_path_for("create_new_pet"), headers=default_user_headers, json={"pet_name": "Tadeusz"}, ) - assert response.status_code == 201 + assert response.status_code == status.HTTP_201_CREATED + result = response.json() - assert result["user_id"] == default_user.id + assert result["user_id"] == default_user.user_id assert result["pet_name"] == "Tadeusz" async def test_get_all_my_pets( - client: AsyncClient, default_user_headers, default_user: User, session: AsyncSession -): - pet1 = Pet(user_id=default_user.id, pet_name="Pet_1") - pet2 = Pet(user_id=default_user.id, pet_name="Pet_2") + client: AsyncClient, + default_user_headers: dict[str, str], + default_user: User, + session: AsyncSession, +) -> None: + pet1 = Pet(user_id=default_user.user_id, pet_name="Pet_1") + pet2 = Pet(user_id=default_user.user_id, pet_name="Pet_2") + session.add(pet1) session.add(pet2) await session.commit() @@ -337,7 +336,7 @@ async def test_get_all_my_pets( app.url_path_for("get_all_my_pets"), headers=default_user_headers, ) - assert response.status_code == 200 + assert response.status_code == status.HTTP_200_OK assert response.json() == [ { @@ -352,6 +351,7 @@ async def test_get_all_my_pets( }, ] + ``` ## Design @@ -368,29 +368,29 @@ There are some **opinionated** default settings in `/app/main.py` for documentat 1. Docs - ```python - app = FastAPI( - title=config.settings.PROJECT_NAME, - version=config.settings.VERSION, - description=config.settings.DESCRIPTION, - openapi_url="/openapi.json", - docs_url="/", - ) - ``` + ```python + app = FastAPI( + title="minimal fastapi postgres template", + version="6.0.0", + description="https://github.com/rafsaf/minimal-fastapi-postgres-template", + openapi_url="/openapi.json", + docs_url="/", + ) + ``` - Docs page is simpy `/` (by default in FastAPI it is `/docs`). Title, version and description are taken directly from `config` and then directly from `pyproject.toml` file. You can change it completely for the project, remove or use environment variables `PROJECT_NAME`, `VERSION`, `DESCRIPTION`. + Docs page is simpy `/` (by default in FastAPI it is `/docs`). You can change it completely for the project, just as title, version, etc. 2. CORS - ```python - app.add_middleware( - CORSMiddleware, - allow_origins=[str(origin) for origin in config.settings.BACKEND_CORS_ORIGINS], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) - ``` + ```python + app.add_middleware( + CORSMiddleware, + allow_origins=[str(origin) for origin in config.settings.BACKEND_CORS_ORIGINS], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + ``` If you are not sure what are CORS for, follow https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS. React and most frontend frameworks nowadays operate on `http://localhost:3000` thats why it's included in `BACKEND_CORS_ORIGINS` in .env file, before going production be sure to include your frontend domain here, like `https://my-fontend-app.example.com`. From 4ad1302edba07df87a603078254b267e3a27c65e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Tue, 5 Mar 2024 01:19:14 +0100 Subject: [PATCH 39/41] readme - better Running tests section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 66d4ac4..e3a6baa 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,6 @@ _Check out online example: https://minimal-fastapi-postgres-template.rafsaf.pl, - [Design](#design) - [Deployment strategies - via Docker image](#deployment-strategies---via-docker-image) - [Docs URL, CORS and Allowed Hosts](#docs-url-cors-and-allowed-hosts) - - [Test setup](#test-setup) ## Features @@ -39,7 +38,7 @@ _Check out online example: https://minimal-fastapi-postgres-template.rafsaf.pl, - [x] Refresh token endpoint (not only access like in official template) - [x] Ready to go Dockerfile with [uvicorn](https://www.uvicorn.org/) webserver as an example - [x] [Poetry](https://python-poetry.org/docs/), `mypy`, `pre-commit` hooks with [ruff](https://github.com/astral-sh/ruff) -- [x] **Perfect** pytest asynchronous test setup with +40 tests and full coverage +- [x] Perfect pytest asynchronous test setup with +40 tests and full coverage
    @@ -102,12 +101,14 @@ pre-commit run --all-files ### 6. Running tests -Note, it will create databases during and run tests in many processes by default, based on how many CPU are available. +Note, it will create databases for session and run tests in many processes by default (using pytest-xdist) to speed up execution, based on how many CPU are available in environment. -For more details, see [Test setup](#test-setup). +For more details about initial database setup, see logic `app/tests/conftest.py` file, `fixture_setup_new_test_database` function. + +Moreover, there is coverage pytest plugin with required code coverage level 100%. ```bash -# see pytest configuration flags in pyproject.toml +# see all pytest configuration flags in pyproject.toml pytest ``` @@ -402,4 +403,3 @@ There are some **opinionated** default settings in `/app/main.py` for documentat Prevents HTTP Host Headers attack, you shoud put here you server IP or (preferably) full domain under it's accessible like `example.com`. By default in .env there are two most popular records: `ALLOWED_HOSTS=["localhost", "127.0.0.1"]` -### Test setup \ No newline at end of file From 531e29bbf8e1f04a6be099ec84a0430a692c8479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Tue, 5 Mar 2024 01:21:02 +0100 Subject: [PATCH 40/41] readme - use (...) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e3a6baa..184d6e4 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ We will add `Pet` model to `app/models.py`. ```python # app/models.py -... +(...) class Pet(Base): __tablename__ = "pet" @@ -278,11 +278,11 @@ Also, we need to add newly created endpoints to router. ```python # app/api/api.py -... +(...) from app.api.endpoints import auth, pets, users -... +(...) api_router.include_router(pets.router, prefix="/pets", tags=["pets"]) From 4d34829c68aeccb8b22a3bfb394e40e2667d8c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Safin?= Date: Tue, 5 Mar 2024 01:33:57 +0100 Subject: [PATCH 41/41] readme - add license note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rafał Safin --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 184d6e4..f1a6475 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ _Check out online example: https://minimal-fastapi-postgres-template.rafsaf.pl, - [Design](#design) - [Deployment strategies - via Docker image](#deployment-strategies---via-docker-image) - [Docs URL, CORS and Allowed Hosts](#docs-url-cors-and-allowed-hosts) + - [License](#license) ## Features @@ -403,3 +404,7 @@ There are some **opinionated** default settings in `/app/main.py` for documentat Prevents HTTP Host Headers attack, you shoud put here you server IP or (preferably) full domain under it's accessible like `example.com`. By default in .env there are two most popular records: `ALLOWED_HOSTS=["localhost", "127.0.0.1"]` + +## License + +The code is under MIT License. It's here for educational purposes, created mainly to have a place where up-to-date Python and FastAPI software lives. Do whatever you want with this code. \ No newline at end of file