diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 00000000..3f43f832 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,9 @@ +COGNITO_CLIENT_ID= +COGNITO_CLIENT_SECRET= +COGNITO_REGION= +COGNITO_REDIRECT_URI=http://localhost:34828/signin +COGNITO_USER_POOL_ID= +COGNITO_ACCESS_ID= +COGNITO_ACCESS_KEY= +ROOT_URL=http://localhost:34828 +DATABASE_URL=sqlite:///./homeuniteus.db \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 00000000..cf8d83c2 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,75 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Local database +homeuniteus.db + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# 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/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ +venv/ +.venv/ +.python-version +.pytest_cache + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +#Ipython Notebook +.ipynb_checkpoints + +# Local Setup +.DS_Store +.vscode +personal.py +.env \ No newline at end of file diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 00000000..095f2e91 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,63 @@ +# Home Unite Us FastAPI Server + +## Overview + +This is the _Home Unite Us_ web API server. + +This server uses: + +- [FastAPI](https://fastapi.tiangolo.com/) - Web framework for API development +- [SQLAlchemy](https://www.sqlalchemy.org/) - ORM for database operations +- [Pydantic](https://docs.pydantic.dev/latest/) - Data validation and serialization +- [Poetry](https://python-poetry.org/docs/) - Dependency management + +## Requirements + +You will need Python 3.8+ to install Poetry. + +Run `python -V` to check the Python version. + +**Note**: On some systems, you might need to use the `python3` and `pip3` commands. + +[Poetry](https://python-poetry.org/docs/#installation) is used to manage the project dependencies. Follow the [installation instructions](https://python-poetry.org/docs/#installation) to run the CLI globally. + +## Usage - Development + +### Getting Started + +#### Configuration + +The API configuration must be specified before running the application. Configuration variables are specified as entries within a `.env` file located within the `api-v2` directory. To get started, create a `.env` file within `/api-v2` and copy the values from `.env.example` into the new `.env` file. You may have to contact someone from the development team to get the necessary values. + +#### Setup and Run + +Once the `.env` file has been configured and Poetry is installed, run the following commands in the `api-v2` directory to install the required development dependencies and run the application. + +```shell +poetry install # Installs all dependencies + +poetry shell # Activates the virtual environment + +poetry run fastapi dev app/main.py # Runs this server in developer mode +``` + +Your server is now running at: +``` +http://127.0.0.1:8000 +``` + +And your API docs at: +``` +http://127.0.0.1:8000/docs +``` + +To exit the virtual environment, within the shell run: +```shell +exit +``` + +## Conventions + +### API Endpoints + +A path segment with spaces must be replace the spaces with a hyphen `-`. For example, `https://dev.homeunite.us/api/housing-orgs`. diff --git a/api/alembic.ini b/backend/alembic.ini similarity index 100% rename from api/alembic.ini rename to backend/alembic.ini diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 00000000..8901a3f3 --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,91 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +import sys +import os +print(os.getcwd()) +sys.path.append(os.getcwd()) + +from app import models as db + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = db.Base.metadata + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + print("ONLINE") + # Check for an existing connection before creating a new engine. + # pytest-alembic will hook into alembic by creating a connection + # with the test engine configuration. + connectable = context.config.attributes.get("connection", None) + if connectable is None: + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/api/alembic/script.py.mako b/backend/alembic/script.py.mako similarity index 100% rename from api/alembic/script.py.mako rename to backend/alembic/script.py.mako diff --git a/api/alembic/versions/3ceec084158f_.py b/backend/alembic/versions/3ceec084158f_.py similarity index 100% rename from api/alembic/versions/3ceec084158f_.py rename to backend/alembic/versions/3ceec084158f_.py diff --git a/api/alembic/versions/cfc4e41b69d3_initial_form_api.py b/backend/alembic/versions/cfc4e41b69d3_initial_form_api.py similarity index 100% rename from api/alembic/versions/cfc4e41b69d3_initial_form_api.py rename to backend/alembic/versions/cfc4e41b69d3_initial_form_api.py diff --git a/backend/alembic/versions/e4c8bb426528_add_user_types.py b/backend/alembic/versions/e4c8bb426528_add_user_types.py new file mode 100644 index 00000000..42cce722 --- /dev/null +++ b/backend/alembic/versions/e4c8bb426528_add_user_types.py @@ -0,0 +1,72 @@ +"""Add user types + +Revision ID: e4c8bb426528 +Revises: ec8b1c17739a +Create Date: 2024-03-10 21:47:13.942845 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy import text +from app.user_roles import UserRole + +# revision identifiers, used by Alembic. +revision = 'e4c8bb426528' +down_revision = 'ec8b1c17739a' +branch_labels = None +depends_on = None + +def upgrade() -> None: + ''' + 1. Add one table: + 1. role - Store available application user roles + 2. Prepopulate the role table with four role types: Admin, Host, Guest, Coordinator + 3. Update the user table to add the first, middle, last name, and role_id columns. + * All existing users will be given the first, last name "UNKNOWN" + * Assign all existing users to the Guest role. + 4. Drop the host table. + * There is no way to map host users back to the user table. We would need a user id foreign + key, or at least an email address. + ''' + role_table = op.create_table('role', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.bulk_insert(role_table, + [{'name': UserRole.ADMIN.value}, + {'name': UserRole.HOST.value}, + {'name': UserRole.GUEST.value}, + {'name': UserRole.COORDINATOR.value}]) + op.create_index(op.f('ix_role_id'), 'role', ['id']) + + conn = op.get_bind() + guest_role_id = conn.execute(text("SELECT id FROM role WHERE name = 'Guest'")).fetchone()[0] + + with op.batch_alter_table('user', schema=None) as batch_op: + # Each existing user will get the first and last names "Unknown" by default + # and they will be assigned to the "Guest" user role. + batch_op.add_column(sa.Column('firstName', sa.String(length=255), nullable=False, server_default='Unknown')) + batch_op.add_column(sa.Column('middleName', sa.String(length=255), nullable=True)) + batch_op.add_column(sa.Column('lastName', sa.String(length=255), nullable=True)) + batch_op.add_column(sa.Column('role_id', sa.Integer, nullable=False, server_default=str(guest_role_id))) + batch_op.create_foreign_key('fk_user_role_id', 'role', ['role_id'], ['id']) + + op.drop_table('host') + +def downgrade() -> None: + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.drop_constraint('fk_user_role_id', type_='foreignkey') + batch_op.drop_column('lastName') + batch_op.drop_column('middleName') + batch_op.drop_column('firstName') + + op.drop_index(op.f('ix_role_id'), table_name='role') + op.drop_table('role') + op.create_table('host', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_host_id'), 'host', ['id']) diff --git a/api/alembic/versions/ec8b1c17739a_drop_unused_tables.py b/backend/alembic/versions/ec8b1c17739a_drop_unused_tables.py similarity index 100% rename from api/alembic/versions/ec8b1c17739a_drop_unused_tables.py rename to backend/alembic/versions/ec8b1c17739a_drop_unused_tables.py diff --git a/api/openapi_server/__init__.py b/backend/app/__init__.py similarity index 100% rename from api/openapi_server/__init__.py rename to backend/app/__init__.py diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 00000000..38e3b937 --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,21 @@ +from functools import lru_cache +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file=".env") + + COGNITO_CLIENT_ID: str + COGNITO_CLIENT_SECRET: str + COGNITO_REGION: str + COGNITO_REDIRECT_URI: str + COGNITO_USER_POOL_ID: str + COGNITO_ACCESS_ID: str + COGNITO_ACCESS_KEY: str + ROOT_URL: str + DATABASE_URL: str + + +@lru_cache +def get_settings(): + return Settings() diff --git a/backend/app/core/db.py b/backend/app/core/db.py new file mode 100644 index 00000000..4da47064 --- /dev/null +++ b/backend/app/core/db.py @@ -0,0 +1,32 @@ +"""Shared database components.""" +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, DeclarativeBase + + +_db_engine = None +_DbSessionFactory = None + + +class Base(DeclarativeBase): + pass + + +def init_db(engine): + Base.metadata.create_all(bind=engine, checkfirst=True) + + +def db_engine(settings): + global _db_engine + if _db_engine is None: + _db_engine = create_engine(settings.DATABASE_URL, + connect_args={"check_same_thread": False}) + return _db_engine + + +def db_session_factory(engine): + global _DbSessionFactory + if _DbSessionFactory is None: + _DbSessionFactory = sessionmaker(autocommit=False, + autoflush=False, + bind=engine) + return _DbSessionFactory diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 00000000..169c0821 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,20 @@ +from fastapi import FastAPI +from contextlib import asynccontextmanager + +from app.modules.router import api_router +import app.core.db as db +import app.core.config as config + + +@asynccontextmanager +async def lifespan(app: FastAPI): + settings = config.get_settings() + engine = db.db_engine(settings) + import app.seed + db.init_db(engine) + yield + + +app = FastAPI(lifespan=lifespan) + +app.include_router(api_router, prefix="/api") \ No newline at end of file diff --git a/api/openapi_server/configs/__init__.py b/backend/app/modules/__init__.py similarity index 100% rename from api/openapi_server/configs/__init__.py rename to backend/app/modules/__init__.py diff --git a/api/openapi_server/controllers/__init__.py b/backend/app/modules/access/__init__.py similarity index 100% rename from api/openapi_server/controllers/__init__.py rename to backend/app/modules/access/__init__.py diff --git a/backend/app/modules/access/auth_controller.py b/backend/app/modules/access/auth_controller.py new file mode 100644 index 00000000..8e261d78 --- /dev/null +++ b/backend/app/modules/access/auth_controller.py @@ -0,0 +1,303 @@ +import logging +import jwt +import boto3 + + +from fastapi import Depends, APIRouter, HTTPException, Response, Request +from fastapi.security import HTTPBearer +from fastapi.responses import RedirectResponse, JSONResponse +from botocore.exceptions import ClientError + +from app.modules.access.schemas import ( + UserCreate, UserSignInRequest, UserSignInResponse, ForgotPasswordRequest, ConfirmForgotPasswordResponse, + ConfirmForgotPasswordRequest, RefreshTokenResponse) + +from app.modules.access.crud import create_user, delete_user, get_user +from app.modules.deps import (SettingsDep, DbSessionDep, CognitoIdpDep, + SecretHashFuncDep, requires_auth, allow_roles, + role_to_cognito_group_map) + +router = APIRouter() + + +# Helper function to set session cookies +def set_session_cookie(response: Response, auth_response: dict): + refresh_token = auth_response["AuthenticationResult"]["RefreshToken"] + id_token = auth_response["AuthenticationResult"]["IdToken"] + + response.set_cookie("refresh_token", refresh_token, httponly=True) + response.set_cookie("id_token", id_token, httponly=True) + +@router.get('/signup/confirm') +def confirm_sign_up(code: str, email: str, settings: SettingsDep, cognito_client: CognitoIdpDep, calc_secret_hash: SecretHashFuncDep): + secret_hash = calc_secret_hash(email) + + try: + cognito_client.confirm_sign_up( + ClientId=settings.COGNITO_CLIENT_ID, + SecretHash=secret_hash, + Username=email, + ConfirmationCode=code + ) + + return RedirectResponse(f"{settings.ROOT_URL}/email-verification-success") + except Exception as e: + return RedirectResponse(f"{settings.ROOT_URL}/email-verification-error") + +@router.post("/signup", description="Sign up a new user") +def signup(body: UserCreate, + settings: SettingsDep, + db: DbSessionDep, + cognito_client: CognitoIdpDep, + calc_secret_hash: SecretHashFuncDep) -> JSONResponse: + + # Create user in database + try: + user = create_user(db, body) + except Exception as e: + raise HTTPException(status_code=400, detail="Failed to create user") + + if user is None: + raise HTTPException(status_code=400, detail="User already exists") + + # Add user to cognito + try: + cognito_client.sign_up( + ClientId=settings.COGNITO_CLIENT_ID, + SecretHash=calc_secret_hash(body.email), + Username=user.email, + Password=body.password, + ClientMetadata={"url": settings.ROOT_URL}, + ) + except Exception as e: + logging.error(f"Failed to create user: {e}") + delete_user(db, user.id) + raise HTTPException(status_code=400, detail="Failed to create user") + + # Add user to group + try: + cognito_client.admin_add_user_to_group( + UserPoolId=settings.COGNITO_USER_POOL_ID, + Username=user.email, + GroupName=role_to_cognito_group_map[body.role], + ) + except Exception as e: + cognito_client.admin_delete_user( + UserPoolId=settings.COGNITO_USER_POOL_ID, + Username=user.email + ) + delete_user(db, user.id) + raise HTTPException(status_code=400, detail="Failed to confirm user") + + return JSONResponse(content={"message": "User sign up successful"}) + + +@router.post("/signin", description="Sign in a user and start a new session", response_model=UserSignInResponse) +def signin(body: UserSignInRequest, + response: Response, + settings: SettingsDep, + db: DbSessionDep, + cognito_client: CognitoIdpDep, + calc_secret_hash: SecretHashFuncDep): + + try: + auth_response = cognito_client.initiate_auth( + ClientId=settings.COGNITO_CLIENT_ID, + AuthFlow="USER_PASSWORD_AUTH", + AuthParameters={ + "USERNAME": body.email, + "PASSWORD": body.password, + "SECRET_HASH": calc_secret_hash(body.email), + }, + ) + except ClientError as e: + raise HTTPException( + status_code=400, + detail={ + "code": e.response["Error"]["Code"], + "message": e.response["Error"]["Message"], + }, + ) + + if (auth_response.get("ChallengeName") + and auth_response["ChallengeName"] == "NEW_PASSWORD_REQUIRED"): + userId = auth_response["ChallengeParameters"]["USER_ID_FOR_SRP"] + sessionId = auth_response["Session"] + root_url = settings.ROOT_URL + return RedirectResponse( + f"{root_url}/create-password?userId={userId}&sessionId={sessionId}" + ) + + user = get_user(db, body.email) + if user is None: + raise HTTPException(status_code=400, detail="User not found") + + set_session_cookie(response, auth_response) + + return { + "user": user, + "token": auth_response["AuthenticationResult"]["AccessToken"], + } + +@router.post( + "/signout", dependencies=[ + Depends(HTTPBearer()), + Depends(requires_auth) + ]) +def signout(request: Request, cognito_client: CognitoIdpDep) -> JSONResponse: + access_token = request.headers.get("Authorization").split(" ")[1] + + + # Signout user + response = cognito_client.global_sign_out( + AccessToken=access_token + ) + + response = JSONResponse(content={"message": "User signed out successfully"}) + + # Remove refresh token cookie + response.delete_cookie("refresh_token") + response.delete_cookie("id_token") + + # send response + return response + + +@router.get("/session", description="Get the current session and user info upon page refresh", response_model=UserSignInResponse) +def current_session( + request: Request, + settings: SettingsDep, + db: DbSessionDep, + cognito_client: CognitoIdpDep, + calc_secret_hash: SecretHashFuncDep): + + id_token = request.cookies.get('id_token') + refresh_token = request.cookies.get('refresh_token') + + if None in (refresh_token, id_token): + raise HTTPException(status_code=401, + detail="Missing refresh token or id token") + + decoded_id_token = jwt.decode(id_token, + algorithms=["RS256"], + options={"verify_signature": False}) + + user = get_user(db, decoded_id_token['email']) + + try: + auth_response = cognito_client.initiate_auth( + ClientId=settings.COGNITO_CLIENT_ID, + AuthFlow='REFRESH_TOKEN', + AuthParameters={ + 'REFRESH_TOKEN': refresh_token, + # DO NOT CHANGE TO EMAIL. THE REFRESH TOKEN AUTH FLOW REQUIRES the use of the 'cognito:username' instead of email + 'SECRET_HASH': calc_secret_hash(decoded_id_token["cognito:username"]) + }) + except ClientError as e: + code = e.response['Error']['Code'] + message = e.response['Error']['Message'] + raise HTTPException(status_code=400, + detail={ + "code": code, + "message": message + }) + + return { + "user": user, + "token": auth_response['AuthenticationResult']['AccessToken'], + } + + +@router.get("/refresh", description="Refresh the current access token during session", response_model=RefreshTokenResponse) +def refresh(request: Request, + settings: SettingsDep, + cognito_client: CognitoIdpDep, + calc_secret_hash: SecretHashFuncDep): + refresh_token = request.cookies.get('refresh_token') + id_token = request.cookies.get('id_token') + + if None in (refresh_token, id_token): + raise HTTPException(status_code=401, + detail="Missing refresh token or id token") + + decoded_id_token = jwt.decode(id_token, + algorithms=["RS256"], + options={"verify_signature": False}) + + try: + response = cognito_client.initiate_auth( + ClientId=settings.COGNITO_CLIENT_ID, + AuthFlow='REFRESH_TOKEN', + AuthParameters={ + 'REFRESH_TOKEN': refresh_token, + # DO NOT CHANGE TO EMAIL. THE REFRESH TOKEN AUTH FLOW REQUIRES the use of the 'cognito:username' instead of email + 'SECRET_HASH': calc_secret_hash(decoded_id_token["cognito:username"]) + }) + except ClientError as e: + code = e.response['Error']['Code'] + message = e.response['Error']['Message'] + raise HTTPException(status_code=400, + detail={ + "code": code, + "message": message + }) + + access_token = response['AuthenticationResult']['AccessToken'] + + # Return access token + return {"token": access_token} + + +@router.post( + "/forgot-password", + description="Handles forgot password requests by hashing credentialsand sending to AWS Cognito", + ) +def forgot_password(body: ForgotPasswordRequest, + settings: SettingsDep, + cognito_client: CognitoIdpDep, + calc_secret_hash: SecretHashFuncDep) -> JSONResponse: + secret_hash = calc_secret_hash(body.email) + + try: + cognito_client.forgot_password(ClientId=settings.COGNITO_CLIENT_ID, + SecretHash=secret_hash, + Username=body.email) + except boto3.exceptions.Boto3Error as e: + code = e.response['Error']['Code'] + message = e.response['Error']['Message'] + raise HTTPException(status_code=401, + detail={ + "code": code, + "message": message + }) + + return JSONResponse(content={"message": "Password reset instructions sent"}) + + +@router.post("/forgot-password/confirm", + description="Handles forgot password confirmation code requests by receiving the confirmation code and sending to AWS Cognito to verify", + response_model=ConfirmForgotPasswordResponse) +def confirm_forgot_password(body: ConfirmForgotPasswordRequest, + settings: SettingsDep, + cognito_client: CognitoIdpDep, + calc_secret_hash: SecretHashFuncDep) -> JSONResponse: + + secret_hash = calc_secret_hash(body.email) + + try: + cognito_client.confirm_forgot_password( + ClientId=settings.COGNITO_CLIENT_ID, + SecretHash=secret_hash, + Username=body.email, + ConfirmationCode=body.code, + Password=body.password) + except boto3.exceptions.Boto3Error as e: + code = e.response['Error']['Code'] + message = e.response['Error']['Message'] + raise HTTPException(status_code=401, + detail={ + "code": code, + "message": message + }) + + return {"message": "Password reset successful"} diff --git a/backend/app/modules/access/crud.py b/backend/app/modules/access/crud.py new file mode 100644 index 00000000..6cdb72ca --- /dev/null +++ b/backend/app/modules/access/crud.py @@ -0,0 +1,40 @@ +from sqlalchemy.orm import Session + +import app.modules.access.models as models +import app.modules.access.schemas as schemas + + +def get_role(db: Session, role: int): + return db.query(models.Role).filter(models.Role.type == role.value).first() + + +def get_user(db: Session, email: str): + return db.query(models.User).filter(models.User.email == email).first() + +def get_user_by_id(db: Session, user_id: int): + return db.query(models.User).filter(models.User.id == user_id).first() + +def create_user(db: Session, user: schemas.UserCreate): + role = get_role(db, user.role) + if role is None: + raise ValueError("Invalid role") + + db_user = models.User( + email=user.email, + firstName=user.firstName, + middleName=user.middleName, + lastName=user.lastName, + roleId=role.id, + ) + + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + + +def delete_user(db: Session, user_id: int): + user = db.query(models.User).filter(models.User.id == user_id).first() + db.delete(user) + db.commit() + return user diff --git a/backend/app/modules/access/models.py b/backend/app/modules/access/models.py new file mode 100644 index 00000000..36877ead --- /dev/null +++ b/backend/app/modules/access/models.py @@ -0,0 +1,57 @@ +"""Model.""" + +from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy.orm import relationship +from sqlalchemy.orm import validates as validates_sqlachemy +from sqlalchemy import create_engine, text +from sqlalchemy.engine import Engine +from sqlalchemy.exc import SQLAlchemyError + +from app.core.db import Base + + +class User(Base): + __tablename__ = "user" + id = Column(Integer, primary_key=True, index=True) + email = Column(String, nullable=False, unique=True) + firstName = Column(String(255), nullable=False) + middleName = Column(String(255), nullable=True) + lastName = Column(String(255), nullable=True) + roleId = Column(Integer, ForeignKey("role.id"), nullable=False) + + role = relationship("Role", back_populates="users") + + @validates_sqlachemy("firstName") + def validate_first_name(self, key, value): + if not value or not value.strip(): + raise ValueError( + f"{key} must contain at least one non-space character") + return value.strip() + + +class Role(Base): + __tablename__ = "role" + id = Column(Integer, primary_key=True, index=True) + type = Column(String, nullable=False, unique=True) + + users = relationship("User", back_populates="role") + + +class UnmatchedGuestCase(Base): + __tablename__ = "unmatched_guest_case" + id = Column(Integer, primary_key=True, index=True) + guest_id = Column(Integer, ForeignKey('user.id'), nullable=False) + coordinator_id = Column(Integer, ForeignKey('user.id'), nullable=False) + status_id = Column(Integer, + ForeignKey('unmatched_guest_case_status.id'), + nullable=False) + status = relationship("UnmatchedGuestCaseStatus", back_populates="cases") + + +class UnmatchedGuestCaseStatus(Base): + __tablename__ = "unmatched_guest_case_status" + id = Column(Integer, primary_key=True, index=True) + status_text = Column(String(255), nullable=False, unique=True) + cases = relationship("UnmatchedGuestCase", back_populates="status") + + diff --git a/backend/app/modules/access/schemas.py b/backend/app/modules/access/schemas.py new file mode 100644 index 00000000..49202f78 --- /dev/null +++ b/backend/app/modules/access/schemas.py @@ -0,0 +1,104 @@ +from pydantic import BaseModel, ConfigDict, EmailStr + +from enum import Enum + + +class UserRoleEnum(str, Enum): + ADMIN = "admin" + GUEST = "guest" + HOST = "host" + COORDINATOR = "coordinator" + + +class RoleBase(BaseModel): + id: int + type: UserRoleEnum + + model_config = ConfigDict(from_attributes=True) + + +class UserBase(BaseModel): + email: EmailStr + firstName: str + middleName: str | None = None + lastName: str | None = None + + +class UserCreate(UserBase): + password: str + role: UserRoleEnum + + +class User(UserBase): + id: int + role: RoleBase + + model_config = ConfigDict(from_attributes=True) + + +class UserSignInRequest(BaseModel): + email: EmailStr + password: str + + +class UserSignInResponse(BaseModel): + user: User + token: str + + +class RefreshTokenResponse(BaseModel): + token: str + +class ForgotPasswordRequest(BaseModel): + email: EmailStr + +class ConfirmForgotPasswordRequest(BaseModel): + email: EmailStr + code: str + password: str + +class ConfirmForgotPasswordResponse(BaseModel): + message: str + + +# class SmartNested(Nested): +# ''' +# Schema attribute used to serialize nested attributes to +# primary keys, unless they are already loaded. This +# enables serialization of complex nested relationships. +# Modified from +# https://marshmallow-sqlalchemy.readthedocs.io/en/latest/recipes.html#smart-nested-field +# ''' + +# def serialize(self, attr, obj, accessor=None): +# if hasattr(obj, attr): +# value = getattr(obj, attr, None) +# if value is None: +# return None +# elif hasattr(value, 'id'): +# return {"id": value.id} +# else: +# return super(SmartNested, self).serialize(attr, obj, accessor) +# else: +# raise AttributeError( +# f"{obj.__class__.__name__} object has no attribute '{attr}'") + +# class RoleSchema(BaseModel): + +# model_config = ConfigDict(from_attributes=True) + +# class UnmatchedCaseSchema(BaseModel): + +# model_config = ConfigDict(from_attributes=True) + +# class UnmatchedCaseStatusSchema(BaseModel): + +# model_config = ConfigDict(from_attributes=True) + +# class UserSchema(BaseModel): +# model_config = ConfigDict(from_attributes=True) + +# user_schema = UserSchema() +# users_schema = UserSchema(many=True) +# unmatched_cs_schema = UnmatchedCaseStatusSchema() +# unmatched_c_schema = UnmatchedCaseSchema() diff --git a/backend/app/modules/access/user_repo.py b/backend/app/modules/access/user_repo.py new file mode 100644 index 00000000..cec2082d --- /dev/null +++ b/backend/app/modules/access/user_repo.py @@ -0,0 +1,85 @@ +from app.modules.access.models import UnmatchedGuestCase, UnmatchedGuestCaseStatus, User, Role +from app.modules.access.user_roles import UmatchedCaseStatus, UserRole + + +class UnmatchedCaseRepository: + + def __init__(self, session): + self.session = session + + def add_case(self, guest_id: int, + coordinator_id: int) -> UnmatchedGuestCase: + status_id = self.session.query(UnmatchedGuestCaseStatus).filter_by( + status_text=UmatchedCaseStatus.IN_PROGRESS).first().id + new_guest_case = UnmatchedGuestCase(guest_id=guest_id, + coordinator_id=coordinator_id, + status_id=status_id) + self.session.add(new_guest_case) + self.session.commit() + + return new_guest_case + + def delete_case_for_guest(self, guest_id: int) -> bool: + guest_case = self.session.query(UnmatchedGuestCaseStatus).filter_by( + guest_id=guest_id).first() + if guest_case: + self.session.delete(guest_case) + self.session.commit() + return True + return False + + def get_case_for_guest(self, guest_id: int) -> UnmatchedGuestCase: + return self.session.query(UnmatchedGuestCase).filter_by( + guest_id=guest_id).first() + + +class UserRepository: + + def __init__(self, session): + self.session = session + + def _get_role(self, role: UserRole) -> Role: + db_role = self.session.query(Role).filter_by(type=role.value).first() + if not db_role: + raise ValueError(f"{role.value} is not a valid user role type") + return db_role + + def add_user(self, + email: str, + role: UserRole, + firstName: str, + middleName: str = None, + lastName: str = None) -> User: + new_role = self._get_role(role) + new_user = User(email=email, + firstName=firstName, + middleName=middleName, + lastName=lastName, + roleId=new_role.id) + self.session.add(new_user) + self.session.commit() + + return new_user + + def delete_user(self, user_id: int) -> bool: + user = self.session.query(User).filter_by(id=user_id).first() + if user: + self.session.delete(user) + self.session.commit() + return True + return False + + def get_user_by_id(self, id: int) -> User: + return self.session.query(User).filter_by(id=id).first() + + def get_user(self, email: str) -> User: + return self.session.query(User).filter_by(email=email).first() + + def get_all_users(self) -> list[User]: + return self.session.query(User).all() + + def get_user_id(self, email: str) -> int: + return self.session.query(User).filter_by(email=email).first().id + + def get_users_with_role(self, role: UserRole) -> list[User]: + return self.session.query(User).filter_by(role=self._get_role(role)) diff --git a/backend/app/modules/access/user_roles.py b/backend/app/modules/access/user_roles.py new file mode 100644 index 00000000..cc46272a --- /dev/null +++ b/backend/app/modules/access/user_roles.py @@ -0,0 +1,13 @@ +from enum import Enum + + +class UserRole(Enum): + ADMIN = "admin" + GUEST = "guest" + HOST = "host" + COORDINATOR = "coordinator" + + +class UmatchedCaseStatus(Enum): + IN_PROGRESS = "In Progress" + COMPLETE = "Complete" diff --git a/backend/app/modules/access/users_controller.py b/backend/app/modules/access/users_controller.py new file mode 100644 index 00000000..1080411d --- /dev/null +++ b/backend/app/modules/access/users_controller.py @@ -0,0 +1,91 @@ +import jwt + +from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi.security import HTTPBearer +from sqlalchemy.exc import IntegrityError + +from app.modules.access.schemas import User +from app.modules.access.crud import get_user, get_user_by_id, delete_user +from app.modules.deps import DbSessionDep, CognitoIdpDep, SettingsDep + +router = APIRouter() + +@router.get("/current", dependencies=[Depends(HTTPBearer())], response_model=User) +def get_current_user(request: Request, db: DbSessionDep): + """Get user route. + + This route is used to get the current user info. + """ + id_token = request.cookies.get('id_token') + if (id_token is None): + raise HTTPException(status_code=401, detail="Missing id token") + + decoded = jwt.decode(id_token, + algorithms=["RS256"], + options={"verify_signature": False}) + email = decoded['email'] + if (email is None): + raise HTTPException(status_code=401, detail="Email not found in token") + + user = get_user(db, email) + + return user + +@router.delete("/{user_id}", dependencies=[Depends(HTTPBearer())]) +def delete_user_by_id(user_id: str, db: DbSessionDep, cognito_client: CognitoIdpDep, settings: SettingsDep): + + try: + user = get_user_by_id(db, user_id) + + #TODO: Add back once unmatched cases are implemented + # role = db_session.query(Role).filter_by(id=user.role_id).first() + + # if role.name == UserRole.GUEST.value: + # unmatched_cases_repo = UnmatchedCaseRepository(db_session) + # unmatched_cases_repo.delete_case_for_guest(user.id) + + # unmatched_cases = [] + # if role.name == UserRole.COORDINATOR.value: + # unmatched_cases = db_session.query(UnmatchedGuestCase).filter_by(coordinator_id=user.id).all() + + + # if len(unmatched_cases) > 0: + # user_repo = UserRepository(db_session) + # guests_by_id = {x.id: x for x in user_repo.get_users_with_role(UserRole.GUEST)} + # guest_emails_with_ids = [{ + # 'id': x.guest_id, + # 'email': guests_by_id[x.guest_id].email, + # } for x in unmatched_cases] + + # guest_emails_with_ids_strs = [f'{g["email"]} (#{g["id"]})' for g in guest_emails_with_ids] + + # return { + # "message": f"Coordinator is associated with {len(unmatched_cases)} case(s). Move these Guest(s) to a different Coordinator before attempting to delete this account", + # "items":guest_emails_with_ids_strs + # }, 400 + + except Exception as error: + raise HTTPException(status_code=400, detail=error) + except IntegrityError: + db.rollback() + raise HTTPException(status_code=422, detail={ + "message": "An error occured while removing user to database." + }) + + # delete user from cognito + try: + cognito_client.admin_delete_user( + UserPoolId=settings.COGNITO_USER_POOL_ID, + Username=user.email + ) + except Exception as e: + raise HTTPException(status_code=401, detail="An error occured while removing user from cognito.") + + # delete user from database + try: + delete_user(db, user_id) + except IntegrityError: + db.rollback() + raise HTTPException(status_code=422, detail="An error occured while removing user to database.") + + return {"message": "User deleted successfully"} diff --git a/backend/app/modules/deps.py b/backend/app/modules/deps.py new file mode 100644 index 00000000..cf7d5c1f --- /dev/null +++ b/backend/app/modules/deps.py @@ -0,0 +1,117 @@ +import boto3 +import jwt +import time +import hmac +import base64 + +from typing import Annotated, Any, Callable + +from fastapi import Depends, Request, HTTPException +from fastapi.security import SecurityScopes +from sqlalchemy.orm import Session + +import app.core.db as db +import app.core.config as config + +SettingsDep = Annotated[config.Settings, Depends(config.get_settings)] + + +def db_engine(settings: SettingsDep): + return db.db_engine(settings) + + +DbEngineDep = Annotated[Any, Depends(db_engine)] + + +def db_session(engine: DbEngineDep): + session_factory = db.db_session_factory(engine) + session = session_factory() + try: + yield session + finally: + session.close() + + +DbSessionDep = Annotated[Session, Depends(db_session)] + + +def get_cognito_client(settings: SettingsDep): + return boto3.client( + "cognito-idp", + region_name=settings.COGNITO_REGION, + aws_access_key_id=settings.COGNITO_ACCESS_ID, + aws_secret_access_key=settings.COGNITO_ACCESS_KEY, + ) + + +CognitoIdpDep = Annotated[Any, Depends(get_cognito_client)] + + +def requires_auth(request: Request, cognito_client: CognitoIdpDep): + # Check for Authorization header + auth_header = request.headers.get("Authorization") + if auth_header is None: + raise HTTPException(status_code=401, + detail="Missing Authorization header") + + # Check for Bearer token + token = auth_header.split(" ")[1] + if token is None: + raise HTTPException(status_code=401, detail="Missing token") + + # Decode token + decoded_access_token = jwt.decode(token, + algorithms=["RS256"], + options={"verify_signature": False}) + + # Check if token is expired + if decoded_access_token["exp"] < time.time(): + raise HTTPException(status_code=401, detail="Token expired") + + try: + cognito_client.get_user(AccessToken=token) + except Exception: + raise HTTPException(status_code=401, detail="Invalid token") + + return True + + +role_to_cognito_group_map = { + "admin": "Admins", + "host": "Hosts", + "coordinator": "Coordinators", + "guest": "Guests", +} + + +def allow_roles(request: Request, security_scopes: SecurityScopes): + id_token = request.cookies.get("id_token") + if id_token is None: + raise HTTPException(status_code=401, detail="Missing id_token") + + decoded_id_token = jwt.decode(id_token, + algorithms=["RS256"], + options={"verify_signature": False}) + + groups = decoded_id_token.get("cognito:groups") + contains_group = any(role_to_cognito_group_map[scope] in groups + for scope in security_scopes.scopes) + + if not contains_group: + raise HTTPException(status_code=403, detail="Unauthorized") + return True + + +def secret_hash_func(settings: SettingsDep): + + def hash(username: str) -> str: + message = username + settings.COGNITO_CLIENT_ID + secret = bytearray(settings.COGNITO_CLIENT_SECRET, "utf-8") + dig = hmac.new(secret, msg=message.encode("utf-8"), + digestmod="sha256").digest() + return base64.b64encode(dig).decode() + + return hash + + +SecretHashFuncDep = Annotated[Callable, Depends(secret_hash_func)] diff --git a/api/openapi_server/repositories/__init__.py b/backend/app/modules/intake_profile/__init__.py similarity index 100% rename from api/openapi_server/repositories/__init__.py rename to backend/app/modules/intake_profile/__init__.py diff --git a/backend/app/modules/intake_profile/controller.py b/backend/app/modules/intake_profile/controller.py new file mode 100644 index 00000000..c183f0d9 --- /dev/null +++ b/backend/app/modules/intake_profile/controller.py @@ -0,0 +1,31 @@ + +from fastapi import Depends, APIRouter, HTTPException, Response, Security +# from fastapi.responses import RedirectResponse + +# from app.modules.deps import ( +# DbSessionDep, +# CognitoIdpDep, +# ) + +router = APIRouter() + + +# @router.post("/guest/") +# def post_guest_intake_profile(body, guest: Depends(aim_guest)): +# forms_repo = FormsRepository(DataAccessLayer.session()) + +# form_id = forms_repo.add_form(body) +# form = forms_repo.get_form_json(form_id) +# if form: +# return form, 200 +# return {}, 404 + + +# @router.get("/guest/{form_id}") +# def get_guest_intake_profile(form_id, guest: Depends(aim_guest)): +# forms_repo = FormsRepository(DataAccessLayer.session()) + +# form = forms_repo.get_form_json(form_id) +# if form: +# return form, 200 +# return f"Form with id {form_id} does not exist.", 404 diff --git a/backend/app/modules/intake_profile/forms/forms.py b/backend/app/modules/intake_profile/forms/forms.py new file mode 100644 index 00000000..66bd8bc9 --- /dev/null +++ b/backend/app/modules/intake_profile/forms/forms.py @@ -0,0 +1,79 @@ +from sqlalchemy import Column, Integer, String, ForeignKey, Text, Boolean, DateTime +from sqlalchemy.sql import func +from app.core.db import Base + + +class Form(Base): + __tablename__ = 'forms' + form_id = Column(Integer, primary_key=True) + title = Column(String(255), nullable=False) + description = Column(Text) + created_at = Column(DateTime, default=func.current_timestamp()) + + def get_field_ids(self) -> List[int]: + return [ + field.field_id for group in self.field_groups + for field in group.fields + ] + + +class FieldProperties(Base): + __tablename__ = 'field_properties' + properties_id = Column(Integer, primary_key=True) + description = Column(Text) + field_type = Column(String(50), nullable=False) + choices = Column(JSON) + + __table_args__ = (CheckConstraint( + "field_type IN ('date', 'dropdown', 'multiple_choice', 'email', 'file_upload', 'group', 'long_text', 'number', 'short_text', 'yes_no')", + name='chk_field_type'), ) + + +class FieldValidations(Base): + __tablename__ = 'field_validations' + validations_id = Column(Integer, primary_key=True) + required = Column(Boolean, nullable=False, default=False) + max_length = Column(Integer) # NULL if not applicable + + +class FieldGroup(Base): + __tablename__ = 'field_groups' + group_id = Column(Integer, primary_key=True) + form_id = Column(Integer, ForeignKey('forms.form_id'), nullable=False) + title = Column(String(255), nullable=False) + description = Column(Text) + form = relationship("Form", back_populates="field_groups") + + +class Field(Base): + __tablename__ = 'fields' + field_id = Column(Integer, primary_key=True) + ref = Column(String(255), nullable=False) + properties_id = Column(Integer, + ForeignKey('field_properties.properties_id'), + nullable=False) + validations_id = Column(Integer, + ForeignKey('field_validations.validations_id'), + nullable=False) + group_id = Column(Integer, ForeignKey('field_groups.group_id')) + properties = relationship("FieldProperties") + validations = relationship("FieldValidations") + group = relationship("FieldGroup", back_populates="fields") + + +class Response(Base): + __tablename__ = 'responses' + answer_id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey('user.id'), nullable=False) + field_id = Column(Integer, ForeignKey('fields.field_id'), nullable=False) + answer_text = Column(Text) + user = relationship("User") + field = relationship("Field") + + +Form.field_groups = relationship("FieldGroup", + order_by=FieldGroup.group_id, + back_populates="form") +FieldGroup.fields = relationship("Field", + order_by=Field.field_id, + back_populates="group") diff --git a/backend/app/modules/intake_profile/model.py b/backend/app/modules/intake_profile/model.py new file mode 100644 index 00000000..c4530b09 --- /dev/null +++ b/backend/app/modules/intake_profile/model.py @@ -0,0 +1,21 @@ + +from enum import Enum, auto + + +class IntakeProfileStatus(Enum): + NEW = auto() + IN_PROGRESS = auto() + APPROVED = auto() + DENIED = auto() + + +class IntakeProfile: + + def __init__(self, form_id: int): + if form_id is None: + raise Exception("IntakeProfile is not valid without a Form") + self.intake_form_id: form_id = form_id + self.attachments: list[bytes] = [] + self.status: IntakeProfileStatus = IntakeProfileStatus.NEW + + diff --git a/backend/app/modules/intake_profile/repository.py b/backend/app/modules/intake_profile/repository.py new file mode 100644 index 00000000..1be46562 --- /dev/null +++ b/backend/app/modules/intake_profile/repository.py @@ -0,0 +1,30 @@ +"""Defines a SQLAlchemy-backed Repository for the IntakeProfile.""" +from typing import Optional + +from sqlalchemy.orm import Session +from model import IntakeProfile + + +class IntakeProfileRepository: + """Repository backed by SQLAlchemy for data persistence.""" + + def __init__(self, session: Session): + """Initialize an IntakeProfile Repository with a SQLAlchemy Session. + + An Exception will be thrown if a Session is not used to create an + object of this class. + """ + if session is None: + raise Exception( + "IntakeProfileRepository is not valid without a SQLAlchemy Session" + ) + self.session = session + + def add(self, intake_profile: IntakeProfile): + """Add the given IntakeProfile to the repository.""" + with self.session as session: + session.add(intake_profile) + + def get(self, intake_profile_id: int) -> Optional[IntakeProfile]: + """Get an IntakeProfile with the given identifier.""" + return self.session.query(IntakeProfile).get(intake_profile_id) diff --git a/backend/app/modules/intake_profile/schemas.py b/backend/app/modules/intake_profile/schemas.py new file mode 100644 index 00000000..9fa4a3c0 --- /dev/null +++ b/backend/app/modules/intake_profile/schemas.py @@ -0,0 +1,100 @@ +from pydantic import BaseModel, ConfigDict + + +class FieldValidationsSchema(BaseModel): + + model_config = ConfigDict(from_attributes=True) + + required: bool + max_length: int + + +# class FieldPropertiesSchema(BaseModel): + +# class Meta: +# model = FieldProperties +# include_relationships = True +# load_instance = True +# exclude = ('properties_id', ) + +# description = auto_field() +# field_type = auto_field() +# choices = auto_field() + + +# class FieldSchema(BaseModel): + +# class Meta: +# model = Field +# include_relationships = True +# load_instance = True +# exclude = ('properties_id', 'validations_id', 'group_id') + +# field_id = auto_field(dump_only=True) +# ref = auto_field() +# properties = SmartNested(FieldPropertiesSchema) +# validations = SmartNested(FieldValidationsSchema) + + +# class FieldGroupSchema(BaseModel): + +# class Meta: +# model = FieldGroup +# include_relationships = True +# load_instance = True +# exclude = ('group_id', 'form_id') + +# title = auto_field() +# description = auto_field() +# fields = SmartNested(FieldSchema, many=True) + + +# class FormSchema(BaseModel): + +# class Meta: +# model = Form +# include_relationships = True +# load_instance = True +# exclude = ('form_id', ) + +# title = auto_field() +# description = auto_field() +# field_groups = SmartNested(FieldGroupSchema, many=True) + + +# class ResponseSchema(BaseModel): + +# class Meta: +# model = Response +# include_relationship = True +# load_instance = True +# exclude = ('answer_id', ) + +# user_id = auto_field(load_only=True) +# field_id = auto_field(load_only=True) +# answer_text = auto_field() +# user = SmartNested(UserSchema, only=['name'], required=False, missing=None) +# field = SmartNested(FieldSchema, +# only=['field_id', 'ref', 'properties'], +# required=False, +# missing=None) + +# @post_load +# def make_response(self, data, **kwargs): +# if data.user is None: +# user = self._session.query(User).get(data.user_id) +# if not user: +# raise ValidationError('User not found', 'user_id') +# data.user = user + +# if data.field is None: +# field = self._session.query(Field).get(data.field_id) +# if not field: +# raise ValidationError('Field not found', 'field_id') +# data.field = field + +# return data + + +# form_schema = FormSchema() +# response_schema = ResponseSchema(many=True) diff --git a/api/tests/__init__.py b/backend/app/modules/matching/__init__.py similarity index 100% rename from api/tests/__init__.py rename to backend/app/modules/matching/__init__.py diff --git a/backend/app/modules/matching/controller.py b/backend/app/modules/matching/controller.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/modules/matching/model.py b/backend/app/modules/matching/model.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/modules/matching/schemas.py b/backend/app/modules/matching/schemas.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/modules/onboarding/__init__.py b/backend/app/modules/onboarding/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/modules/onboarding/controller.py b/backend/app/modules/onboarding/controller.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/modules/onboarding/model.py b/backend/app/modules/onboarding/model.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/modules/onboarding/schemas.py b/backend/app/modules/onboarding/schemas.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/modules/relationship_management/__init__.py b/backend/app/modules/relationship_management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/modules/relationship_management/controller.py b/backend/app/modules/relationship_management/controller.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/modules/relationship_management/model.py b/backend/app/modules/relationship_management/model.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/modules/relationship_management/schemas.py b/backend/app/modules/relationship_management/schemas.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/modules/router.py b/backend/app/modules/router.py new file mode 100644 index 00000000..57d65048 --- /dev/null +++ b/backend/app/modules/router.py @@ -0,0 +1,22 @@ +from fastapi import APIRouter + +from app.modules.access import auth_controller, users_controller +from app.modules.intake_profile import controller as intake_profile +from app.modules.tenant_housing_orgs import controller as housing_org + + +api_router = APIRouter() + + +api_router.include_router( + auth_controller.router, prefix="/auth", tags=["auth"] +) +api_router.include_router( + users_controller.router, prefix="/users", tags=["users"] +) +api_router.include_router( + intake_profile.router, prefix="/intake-profile", tags=["intake_profile"] +) +api_router.include_router( + housing_org.router, prefix="/housing-orgs", tags=["tenant_housing_orgs"] +) diff --git a/backend/app/modules/tenant_housing_orgs/__init__.py b/backend/app/modules/tenant_housing_orgs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/modules/tenant_housing_orgs/controller.py b/backend/app/modules/tenant_housing_orgs/controller.py new file mode 100644 index 00000000..8b2dcb77 --- /dev/null +++ b/backend/app/modules/tenant_housing_orgs/controller.py @@ -0,0 +1,103 @@ +"""Controller (or "Resource") that represents a Housing Org(anization). + +This module implements the HTTP interface that represents a Housing Org. +""" +from . import crud, models, schemas + +from typing import Any +from fastapi import APIRouter, Request, Response, HTTPException, status +from fastapi.responses import RedirectResponse + +from app.modules.deps import DbSessionDep + +router = APIRouter() + + +@router.post("/", + status_code=status.HTTP_201_CREATED, + response_model=schemas.HousingOrg) +def create_housing_org( + housing_org: schemas.HousingOrg, + request: Request, + session: DbSessionDep) -> Any: + """Create a housing org. + + A housing org is created if it is not already in + the database. + + Return the newly created housing org. + If the Housing Org with the given name exists, a redirect response is given. + """ + with session.begin(): + db_org = crud.read_housing_org_by_name(session, housing_org.org_name) + if db_org: + redirect_url = request.url_for('get_housing_org', + **{'housing_org_id': db_org.housing_org_id}) + return RedirectResponse(url=redirect_url, + status_code=status.HTTP_303_SEE_OTHER) + + new_housing_org = models.HousingOrg(org_name=housing_org.org_name) + crud.create_housing_org(session, new_housing_org) + + session.refresh(new_housing_org) + return new_housing_org + + +@router.get("/{housing_org_id}") +def get_housing_org(housing_org_id: int, session: DbSessionDep) -> schemas.HousingOrg | None: + """Get details about a housing org from an ID.""" + housing_org = crud.read_housing_org_by_id(session, housing_org_id) + if not housing_org: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, + detail="Housing Org not found") + return housing_org + + +@router.get("/") +def get_housing_orgs(session: DbSessionDep) -> list[ + schemas.HousingOrg]: + """Get a list of all housing orgs.""" + return crud.read_housing_orgs(session) + + +@router.put("/{housing_org_id}", status_code=status.HTTP_200_OK) +def put_housing_org( + housing_org_id: int, + body: schemas.HousingOrg, + response: Response, + session: DbSessionDep) -> schemas.HousingOrg: + """Create or Update a Housing Org with the given ID. + + If the representation contains a Housing Org ID that does match the ID given + in the path, then a HTTP 409 Conflict will be returned. + """ + if body.housing_org_id is not None and body.housing_org_id != housing_org_id: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail= + "The Housing Org ID in the path mismatches the ID in the request body." + ) + + housing_org = models.HousingOrg(housing_org_id=housing_org_id, org_name=body.org_name) + + with session.begin(): + was_created = crud.upsert_housing_org(session, housing_org) + + if was_created: + response.status_code = status.HTTP_201_CREATED + + return housing_org + + +@router.delete("/{housing_org_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_housing_org(housing_org_id: int, + session: DbSessionDep): + """Delete a housing org. + + :param housing_org_id: The ID of the housing org to delete. + """ + with session.begin(): + housing_org = crud.read_housing_org_by_id(session, housing_org_id) + if not housing_org: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + crud.delete_housing_org(session, housing_org) diff --git a/backend/app/modules/tenant_housing_orgs/crud.py b/backend/app/modules/tenant_housing_orgs/crud.py new file mode 100644 index 00000000..0c298cf4 --- /dev/null +++ b/backend/app/modules/tenant_housing_orgs/crud.py @@ -0,0 +1,59 @@ +"""A simple CRUD implementation for the HousingOrg data model.""" + +from sqlalchemy.orm import Session +from sqlalchemy import select + +from . import models + + +def create_housing_org(session: Session, new_housing_org: models.HousingOrg): + """Create a Housing Org.""" + session.add(new_housing_org) + + +def read_housing_org_by_id(session: Session, + housing_org_id: int) -> models.HousingOrg: + """Read a HousingOrg by ID.""" + return session.get(models.HousingOrg, housing_org_id) + + +def read_housing_org_by_name(session: Session, + org_name: str) -> models.HousingOrg: + """Read a HousingOrg by name.""" + query = select( + models.HousingOrg).filter(models.HousingOrg.org_name == org_name) + return session.scalars(query).one_or_none() + + +def read_housing_orgs(session: Session) -> list[models.HousingOrg]: + """Read all HousingOrgs returned as a list.""" + return session.scalars(select(models.HousingOrg)).all() + + +def upsert_housing_org(session: Session, + housing_org: models.HousingOrg) -> bool: + """Upsert (Update or Insert) a HousingOrg. + + If a HousingOrg exists, it will be updated and the function + will return False. + + If a HousingOrg does not exist, it will be added to the database + and the function will return True. + """ + was_created = False + + db_housing_org = session.query( + models.HousingOrg).filter_by(housing_org_id=housing_org.housing_org_id).first() + if db_housing_org: + db_housing_org.org_name = housing_org.org_name + else: + session.add(housing_org) + was_created = True + + return was_created + + +def delete_housing_org(session: Session, housing_org: models.HousingOrg): + """Delete a HousingOrg.""" + housing_org = session.get(models.HousingOrg, housing_org.housing_org_id) + session.delete(housing_org) diff --git a/backend/app/modules/tenant_housing_orgs/models.py b/backend/app/modules/tenant_housing_orgs/models.py new file mode 100644 index 00000000..efe07f25 --- /dev/null +++ b/backend/app/modules/tenant_housing_orgs/models.py @@ -0,0 +1,34 @@ +"""SQLAlchemy models for the Housing Org.""" + +from typing import Annotated +from typing import List +from sqlalchemy import ForeignKey, String +from sqlalchemy.orm import Mapped +from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import relationship + +from app.core.db import Base + +intpk = Annotated[int, mapped_column(primary_key=True)] + + +class HousingOrg(Base): + __tablename__ = "housing_orgs" + + housing_org_id: Mapped[intpk] + org_name: Mapped[str] = mapped_column(String, nullable=False, unique=True) + programs: Mapped[List["HousingProgram"]] = relationship( + back_populates="housing_org") + + def __repr__(self): + return f"HousingOrg(housing_org_id={self.housing_org_id},org_name='{self.org_name}')" + + +class HousingProgram(Base): + __tablename__ = "housing_programs" + + housing_program_id: Mapped[intpk] + program_name: Mapped[str] = mapped_column(String, nullable=False) + housing_org_id: Mapped[int] = mapped_column( + ForeignKey('housing_orgs.housing_org_id'), nullable=False) + housing_org: Mapped["HousingOrg"] = relationship(back_populates="programs") diff --git a/backend/app/modules/tenant_housing_orgs/schemas.py b/backend/app/modules/tenant_housing_orgs/schemas.py new file mode 100644 index 00000000..876c6b14 --- /dev/null +++ b/backend/app/modules/tenant_housing_orgs/schemas.py @@ -0,0 +1,9 @@ +"""Pydantic schemas for the Housing Org.""" +from pydantic import BaseModel, ConfigDict + + +class HousingOrg(BaseModel): + housing_org_id: int | None = None + org_name: str + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/app/seed.py b/backend/app/seed.py new file mode 100644 index 00000000..238a55de --- /dev/null +++ b/backend/app/seed.py @@ -0,0 +1,23 @@ +from sqlalchemy import event + +from app.modules.access.models import Role + +INITIAL_ROLES = [ + {"type": "admin"}, + {"type": "guest"}, + {"type": "host"}, + {"type": "coordinator"}, +] + + +def initialize_table(target, connection, **kw): + """Initialize a DB table. + + This method receives a table, a connection and inserts data to that table. + """ + for role in INITIAL_ROLES: + connection.execute(target.insert(), role) + return + + +event.listen(Role.__table__, "after_create", initialize_table) diff --git a/backend/poetry.lock b/backend/poetry.lock new file mode 100644 index 00000000..64665e43 --- /dev/null +++ b/backend/poetry.lock @@ -0,0 +1,2084 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "alembic" +version = "1.13.2" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.8" +files = [ + {file = "alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"}, + {file = "alembic-1.13.2.tar.gz", hash = "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" +typing-extensions = ">=4" + +[package.extras] +tz = ["backports.zoneinfo"] + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.4.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +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 = "boto3" +version = "1.35.19" +description = "The AWS SDK for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "boto3-1.35.19-py3-none-any.whl", hash = "sha256:84b3fe1727945bc3cada832d969ddb3dc0d08fce1677064ca8bdc13a89c1a143"}, + {file = "boto3-1.35.19.tar.gz", hash = "sha256:9979fe674780a0b7100eae9156d74ee374cd1638a9f61c77277e3ce712f3e496"}, +] + +[package.dependencies] +botocore = ">=1.35.19,<1.36.0" +jmespath = ">=0.7.1,<2.0.0" +s3transfer = ">=0.10.0,<0.11.0" + +[package.extras] +crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] + +[[package]] +name = "botocore" +version = "1.35.19" +description = "Low-level, data-driven core of boto 3." +optional = false +python-versions = ">=3.8" +files = [ + {file = "botocore-1.35.19-py3-none-any.whl", hash = "sha256:c83f7f0cacfe7c19b109b363ebfa8736e570d24922f16ed371681f58ebab44a9"}, + {file = "botocore-1.35.19.tar.gz", hash = "sha256:42d6d8db7250cbd7899f786f9861e02cab17dc238f64d6acb976098ed9809625"}, +] + +[package.dependencies] +jmespath = ">=0.7.1,<2.0.0" +python-dateutil = ">=2.1,<3.0.0" +urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} + +[package.extras] +crt = ["awscrt (==0.21.5)"] + +[[package]] +name = "cachetools" +version = "5.5.0" +description = "Extensible memoizing collections and decorators" +optional = false +python-versions = ">=3.7" +files = [ + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, +] + +[[package]] +name = "certifi" +version = "2024.8.30" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, +] + +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + +[[package]] +name = "chardet" +version = "5.2.0" +description = "Universal encoding detector for Python 3" +optional = false +python-versions = ">=3.7" +files = [ + {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, + {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.6.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "cryptography" +version = "43.0.1" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-43.0.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277"}, + {file = "cryptography-43.0.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042"}, + {file = "cryptography-43.0.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494"}, + {file = "cryptography-43.0.1-cp37-abi3-win32.whl", hash = "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2"}, + {file = "cryptography-43.0.1-cp37-abi3-win_amd64.whl", hash = "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d"}, + {file = "cryptography-43.0.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c"}, + {file = "cryptography-43.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa"}, + {file = "cryptography-43.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4"}, + {file = "cryptography-43.0.1-cp39-abi3-win32.whl", hash = "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47"}, + {file = "cryptography-43.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289"}, + {file = "cryptography-43.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172"}, + {file = "cryptography-43.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2"}, + {file = "cryptography-43.0.1.tar.gz", hash = "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "cryptography-vectors (==43.0.1)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "dnspython" +version = "2.6.1" +description = "DNS toolkit" +optional = false +python-versions = ">=3.8" +files = [ + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, +] + +[package.extras] +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.2.0" +description = "A robust email address syntax and deliverability validation library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, + {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, +] + +[package.dependencies] +dnspython = ">=2.0.0" +idna = ">=2.0.0" + +[[package]] +name = "fastapi" +version = "0.113.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.113.0-py3-none-any.whl", hash = "sha256:c8d364485b6361fb643d53920a18d58a696e189abcb901ec03b487e35774c476"}, + {file = "fastapi-0.113.0.tar.gz", hash = "sha256:b7cf9684dc154dfc93f8b718e5850577b529889096518df44defa41e73caf50f"}, +] + +[package.dependencies] +email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"standard\""} +fastapi-cli = {version = ">=0.0.5", extras = ["standard"], optional = true, markers = "extra == \"standard\""} +httpx = {version = ">=0.23.0", optional = true, markers = "extra == \"standard\""} +jinja2 = {version = ">=2.11.2", optional = true, markers = "extra == \"standard\""} +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" +python-multipart = {version = ">=0.0.7", optional = true, markers = "extra == \"standard\""} +starlette = ">=0.37.2,<0.39.0" +typing-extensions = ">=4.8.0" +uvicorn = {version = ">=0.12.0", extras = ["standard"], optional = true, markers = "extra == \"standard\""} + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "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)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "fastapi-cli" +version = "0.0.5" +description = "Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi_cli-0.0.5-py3-none-any.whl", hash = "sha256:e94d847524648c748a5350673546bbf9bcaeb086b33c24f2e82e021436866a46"}, + {file = "fastapi_cli-0.0.5.tar.gz", hash = "sha256:d30e1239c6f46fcb95e606f02cdda59a1e2fa778a54b64686b3ff27f6211ff9f"}, +] + +[package.dependencies] +typer = ">=0.12.3" +uvicorn = {version = ">=0.15.0", extras = ["standard"]} + +[package.extras] +standard = ["uvicorn[standard] (>=0.15.0)"] + +[[package]] +name = "filelock" +version = "3.16.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.16.0-py3-none-any.whl", hash = "sha256:f6ed4c963184f4c84dd5557ce8fece759a3724b37b80c6c4f20a2f63a4dc6609"}, + {file = "filelock-3.16.0.tar.gz", hash = "sha256:81de9eb8453c769b63369f87f11131a7ab04e367f8d97ad39dc230daa07e3bec"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.1.1)", "pytest (>=8.3.2)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.3)"] +typing = ["typing-extensions (>=4.12.2)"] + +[[package]] +name = "greenlet" +version = "3.1.0" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a814dc3100e8a046ff48faeaa909e80cdb358411a3d6dd5293158425c684eda8"}, + {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a771dc64fa44ebe58d65768d869fcfb9060169d203446c1d446e844b62bdfdca"}, + {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e49a65d25d7350cca2da15aac31b6f67a43d867448babf997fe83c7505f57bc"}, + {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cd8518eade968bc52262d8c46727cfc0826ff4d552cf0430b8d65aaf50bb91d"}, + {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76dc19e660baea5c38e949455c1181bc018893f25372d10ffe24b3ed7341fb25"}, + {file = "greenlet-3.1.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0a5b1c22c82831f56f2f7ad9bbe4948879762fe0d59833a4a71f16e5fa0f682"}, + {file = "greenlet-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2651dfb006f391bcb240635079a68a261b227a10a08af6349cba834a2141efa1"}, + {file = "greenlet-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3e7e6ef1737a819819b1163116ad4b48d06cfdd40352d813bb14436024fcda99"}, + {file = "greenlet-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:ffb08f2a1e59d38c7b8b9ac8083c9c8b9875f0955b1e9b9b9a965607a51f8e54"}, + {file = "greenlet-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9730929375021ec90f6447bff4f7f5508faef1c02f399a1953870cdb78e0c345"}, + {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:713d450cf8e61854de9420fb7eea8ad228df4e27e7d4ed465de98c955d2b3fa6"}, + {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c3446937be153718250fe421da548f973124189f18fe4575a0510b5c928f0cc"}, + {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ddc7bcedeb47187be74208bc652d63d6b20cb24f4e596bd356092d8000da6d6"}, + {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44151d7b81b9391ed759a2f2865bbe623ef00d648fed59363be2bbbd5154656f"}, + {file = "greenlet-3.1.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cea1cca3be76c9483282dc7760ea1cc08a6ecec1f0b6ca0a94ea0d17432da19"}, + {file = "greenlet-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:619935a44f414274a2c08c9e74611965650b730eb4efe4b2270f91df5e4adf9a"}, + {file = "greenlet-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:221169d31cada333a0c7fd087b957c8f431c1dba202c3a58cf5a3583ed973e9b"}, + {file = "greenlet-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:01059afb9b178606b4b6e92c3e710ea1635597c3537e44da69f4531e111dd5e9"}, + {file = "greenlet-3.1.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:24fc216ec7c8be9becba8b64a98a78f9cd057fd2dc75ae952ca94ed8a893bf27"}, + {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d07c28b85b350564bdff9f51c1c5007dfb2f389385d1bc23288de51134ca303"}, + {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:243a223c96a4246f8a30ea470c440fe9db1f5e444941ee3c3cd79df119b8eebf"}, + {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26811df4dc81271033a7836bc20d12cd30938e6bd2e9437f56fa03da81b0f8fc"}, + {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9d86401550b09a55410f32ceb5fe7efcd998bd2dad9e82521713cb148a4a15f"}, + {file = "greenlet-3.1.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26d9c1c4f1748ccac0bae1dbb465fb1a795a75aba8af8ca871503019f4285e2a"}, + {file = "greenlet-3.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:cd468ec62257bb4544989402b19d795d2305eccb06cde5da0eb739b63dc04665"}, + {file = "greenlet-3.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a53dfe8f82b715319e9953330fa5c8708b610d48b5c59f1316337302af5c0811"}, + {file = "greenlet-3.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:28fe80a3eb673b2d5cc3b12eea468a5e5f4603c26aa34d88bf61bba82ceb2f9b"}, + {file = "greenlet-3.1.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:76b3e3976d2a452cba7aa9e453498ac72240d43030fdc6d538a72b87eaff52fd"}, + {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655b21ffd37a96b1e78cc48bf254f5ea4b5b85efaf9e9e2a526b3c9309d660ca"}, + {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6f4c2027689093775fd58ca2388d58789009116844432d920e9147f91acbe64"}, + {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76e5064fd8e94c3f74d9fd69b02d99e3cdb8fc286ed49a1f10b256e59d0d3a0b"}, + {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4bf607f690f7987ab3291406e012cd8591a4f77aa54f29b890f9c331e84989"}, + {file = "greenlet-3.1.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:037d9ac99540ace9424cb9ea89f0accfaff4316f149520b4ae293eebc5bded17"}, + {file = "greenlet-3.1.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:90b5bbf05fe3d3ef697103850c2ce3374558f6fe40fd57c9fac1bf14903f50a5"}, + {file = "greenlet-3.1.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:726377bd60081172685c0ff46afbc600d064f01053190e4450857483c4d44484"}, + {file = "greenlet-3.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:d46d5069e2eeda111d6f71970e341f4bd9aeeee92074e649ae263b834286ecc0"}, + {file = "greenlet-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81eeec4403a7d7684b5812a8aaa626fa23b7d0848edb3a28d2eb3220daddcbd0"}, + {file = "greenlet-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a3dae7492d16e85ea6045fd11cb8e782b63eac8c8d520c3a92c02ac4573b0a6"}, + {file = "greenlet-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b5ea3664eed571779403858d7cd0a9b0ebf50d57d2cdeafc7748e09ef8cd81a"}, + {file = "greenlet-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a22f4e26400f7f48faef2d69c20dc055a1f3043d330923f9abe08ea0aecc44df"}, + {file = "greenlet-3.1.0-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13ff8c8e54a10472ce3b2a2da007f915175192f18e6495bad50486e87c7f6637"}, + {file = "greenlet-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9671e7282d8c6fcabc32c0fb8d7c0ea8894ae85cee89c9aadc2d7129e1a9954"}, + {file = "greenlet-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:184258372ae9e1e9bddce6f187967f2e08ecd16906557c4320e3ba88a93438c3"}, + {file = "greenlet-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:a0409bc18a9f85321399c29baf93545152d74a49d92f2f55302f122007cfda00"}, + {file = "greenlet-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9eb4a1d7399b9f3c7ac68ae6baa6be5f9195d1d08c9ddc45ad559aa6b556bce6"}, + {file = "greenlet-3.1.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:a8870983af660798dc1b529e1fd6f1cefd94e45135a32e58bd70edd694540f33"}, + {file = "greenlet-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfcfb73aed40f550a57ea904629bdaf2e562c68fa1164fa4588e752af6efdc3f"}, + {file = "greenlet-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f9482c2ed414781c0af0b35d9d575226da6b728bd1a720668fa05837184965b7"}, + {file = "greenlet-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d58ec349e0c2c0bc6669bf2cd4982d2f93bf067860d23a0ea1fe677b0f0b1e09"}, + {file = "greenlet-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd65695a8df1233309b701dec2539cc4b11e97d4fcc0f4185b4a12ce54db0491"}, + {file = "greenlet-3.1.0-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:665b21e95bc0fce5cab03b2e1d90ba9c66c510f1bb5fdc864f3a377d0f553f6b"}, + {file = "greenlet-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d3c59a06c2c28a81a026ff11fbf012081ea34fb9b7052f2ed0366e14896f0a1d"}, + {file = "greenlet-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5415b9494ff6240b09af06b91a375731febe0090218e2898d2b85f9b92abcda0"}, + {file = "greenlet-3.1.0-cp38-cp38-win32.whl", hash = "sha256:1544b8dd090b494c55e60c4ff46e238be44fdc472d2589e943c241e0169bcea2"}, + {file = "greenlet-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:7f346d24d74c00b6730440f5eb8ec3fe5774ca8d1c9574e8e57c8671bb51b910"}, + {file = "greenlet-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:db1b3ccb93488328c74e97ff888604a8b95ae4f35f4f56677ca57a4fc3a4220b"}, + {file = "greenlet-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44cd313629ded43bb3b98737bba2f3e2c2c8679b55ea29ed73daea6b755fe8e7"}, + {file = "greenlet-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fad7a051e07f64e297e6e8399b4d6a3bdcad3d7297409e9a06ef8cbccff4f501"}, + {file = "greenlet-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3967dcc1cd2ea61b08b0b276659242cbce5caca39e7cbc02408222fb9e6ff39"}, + {file = "greenlet-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d45b75b0f3fd8d99f62eb7908cfa6d727b7ed190737dec7fe46d993da550b81a"}, + {file = "greenlet-3.1.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2d004db911ed7b6218ec5c5bfe4cf70ae8aa2223dffbb5b3c69e342bb253cb28"}, + {file = "greenlet-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9505a0c8579899057cbefd4ec34d865ab99852baf1ff33a9481eb3924e2da0b"}, + {file = "greenlet-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fd6e94593f6f9714dbad1aaba734b5ec04593374fa6638df61592055868f8b8"}, + {file = "greenlet-3.1.0-cp39-cp39-win32.whl", hash = "sha256:d0dd943282231480aad5f50f89bdf26690c995e8ff555f26d8a5b9887b559bcc"}, + {file = "greenlet-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:ac0adfdb3a21dc2a24ed728b61e72440d297d0fd3a577389df566651fcd08f97"}, + {file = "greenlet-3.1.0.tar.gz", hash = "sha256:b395121e9bbe8d02a750886f108d540abe66075e61e22f7353d9acb0b81be0f0"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.26.0)"] + +[[package]] +name = "httptools" +version = "0.6.1" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, + {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, + {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, + {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"}, + {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"}, + {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"}, + {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, +] + +[package.extras] +test = ["Cython (>=0.29.24,<0.30.0)"] + +[[package]] +name = "httpx" +version = "0.27.2" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "idna" +version = "3.9" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.9-py3-none-any.whl", hash = "sha256:69297d5da0cc9281c77efffb4e730254dd45943f45bbfb461de5991713989b1e"}, + {file = "idna-3.9.tar.gz", hash = "sha256:e5c5dafde284f26e9e0f28f6ea2d6400abd5ca099864a67f576f3981c6476124"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.4" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "jmespath" +version = "1.0.1" +description = "JSON Matching Expressions" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, + {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, +] + +[[package]] +name = "joserfc" +version = "1.0.0" +description = "The ultimate Python library for JOSE RFCs, including JWS, JWE, JWK, JWA, JWT" +optional = false +python-versions = ">=3.8" +files = [ + {file = "joserfc-1.0.0-py3-none-any.whl", hash = "sha256:1de2c3ac203db8fceb2e84c1e78ba357030b195c21af046a1411711927654a09"}, + {file = "joserfc-1.0.0.tar.gz", hash = "sha256:298a9820c76576f8ca63375d1851cc092f3f225508305c7a36c4632cec38f7bc"}, +] + +[package.dependencies] +cryptography = "*" + +[package.extras] +drafts = ["pycryptodome"] + +[[package]] +name = "mako" +version = "1.3.5" +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.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"}, + {file = "Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {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]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "moto" +version = "5.0.14" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "moto-5.0.14-py2.py3-none-any.whl", hash = "sha256:c738ffe85d3844ef37b865951736c4faf2e0f3e4f05db87bdad97a6c01b88174"}, + {file = "moto-5.0.14.tar.gz", hash = "sha256:0f849243269fd03372426c302b18cb605302da32620d7f0266be6a40735b2acd"}, +] + +[package.dependencies] +boto3 = ">=1.9.201" +botocore = ">=1.14.0" +cryptography = ">=3.3.1" +Jinja2 = ">=2.10.1" +joserfc = {version = ">=0.9.0", optional = true, markers = "extra == \"cognitoidp\""} +python-dateutil = ">=2.1,<3.0.0" +requests = ">=2.5" +responses = ">=0.15.0" +werkzeug = ">=0.5,<2.2.0 || >2.2.0,<2.2.1 || >2.2.1" +xmltodict = "*" + +[package.extras] +all = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "jsonpath-ng", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.6)", "pyparsing (>=3.0.7)", "setuptools"] +apigateway = ["PyYAML (>=5.1)", "joserfc (>=0.9.0)", "openapi-spec-validator (>=0.5.0)"] +apigatewayv2 = ["PyYAML (>=5.1)", "openapi-spec-validator (>=0.5.0)"] +appsync = ["graphql-core"] +awslambda = ["docker (>=3.0.0)"] +batch = ["docker (>=3.0.0)"] +cloudformation = ["PyYAML (>=5.1)", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.6)", "pyparsing (>=3.0.7)", "setuptools"] +cognitoidp = ["joserfc (>=0.9.0)"] +dynamodb = ["docker (>=3.0.0)", "py-partiql-parser (==0.5.6)"] +dynamodbstreams = ["docker (>=3.0.0)", "py-partiql-parser (==0.5.6)"] +events = ["jsonpath-ng"] +glue = ["pyparsing (>=3.0.7)"] +iotdata = ["jsondiff (>=1.1.2)"] +proxy = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=2.5.1)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "jsonpath-ng", "multipart", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.6)", "pyparsing (>=3.0.7)", "setuptools"] +resourcegroupstaggingapi = ["PyYAML (>=5.1)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.6)", "pyparsing (>=3.0.7)"] +s3 = ["PyYAML (>=5.1)", "py-partiql-parser (==0.5.6)"] +s3crc32c = ["PyYAML (>=5.1)", "crc32c", "py-partiql-parser (==0.5.6)"] +server = ["PyYAML (>=5.1)", "antlr4-python3-runtime", "aws-xray-sdk (>=0.93,!=0.96)", "cfn-lint (>=0.40.0)", "docker (>=3.0.0)", "flask (!=2.2.0,!=2.2.1)", "flask-cors", "graphql-core", "joserfc (>=0.9.0)", "jsondiff (>=1.1.2)", "jsonpath-ng", "openapi-spec-validator (>=0.5.0)", "py-partiql-parser (==0.5.6)", "pyparsing (>=3.0.7)", "setuptools"] +ssm = ["PyYAML (>=5.1)"] +stepfunctions = ["antlr4-python3-runtime", "jsonpath-ng"] +xray = ["aws-xray-sdk (>=0.93,!=0.96)", "setuptools"] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.3" +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.3.3-py3-none-any.whl", hash = "sha256:50a5450e2e84f44539718293cbb1da0a0885c9d14adf21b77bae4e66fc99d9b5"}, + {file = "platformdirs-4.3.3.tar.gz", hash = "sha256:d4e0b7d8ec176b341fb03cb11ca12d0276faa8c485f9cd218f613840463fc2c0"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "psycopg2-binary" +version = "2.9.9" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "psycopg2-binary-2.9.9.tar.gz", hash = "sha256:7f01846810177d829c7692f1f5ada8096762d9172af1b1a28d4ab5b77c923c1c"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c2470da5418b76232f02a2fcd2229537bb2d5a7096674ce61859c3229f2eb202"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6af2a6d4b7ee9615cbb162b0738f6e1fd1f5c3eda7e5da17861eacf4c717ea7"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75723c3c0fbbf34350b46a3199eb50638ab22a0228f93fb472ef4d9becc2382b"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83791a65b51ad6ee6cf0845634859d69a038ea9b03d7b26e703f94c7e93dbcf9"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ef4854e82c09e84cc63084a9e4ccd6d9b154f1dbdd283efb92ecd0b5e2b8c84"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed1184ab8f113e8d660ce49a56390ca181f2981066acc27cf637d5c1e10ce46e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d2997c458c690ec2bc6b0b7ecbafd02b029b7b4283078d3b32a852a7ce3ddd98"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b58b4710c7f4161b5e9dcbe73bb7c62d65670a87df7bcce9e1faaad43e715245"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:0c009475ee389757e6e34611d75f6e4f05f0cf5ebb76c6037508318e1a1e0d7e"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8dbf6d1bc73f1d04ec1734bae3b4fb0ee3cb2a493d35ede9badbeb901fb40f6f"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win32.whl", hash = "sha256:3f78fd71c4f43a13d342be74ebbc0666fe1f555b8837eb113cb7416856c79682"}, + {file = "psycopg2_binary-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:876801744b0dee379e4e3c38b76fc89f88834bb15bf92ee07d94acd06ec890a0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ee825e70b1a209475622f7f7b776785bd68f34af6e7a46e2e42f27b659b5bc26"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1ea665f8ce695bcc37a90ee52de7a7980be5161375d42a0b6c6abedbf0d81f0f"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:143072318f793f53819048fdfe30c321890af0c3ec7cb1dfc9cc87aa88241de2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c332c8d69fb64979ebf76613c66b985414927a40f8defa16cf1bc028b7b0a7b0"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7fc5a5acafb7d6ccca13bfa8c90f8c51f13d8fb87d95656d3950f0158d3ce53"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:977646e05232579d2e7b9c59e21dbe5261f403a88417f6a6512e70d3f8a046be"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b6356793b84728d9d50ead16ab43c187673831e9d4019013f1402c41b1db9b27"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bc7bb56d04601d443f24094e9e31ae6deec9ccb23581f75343feebaf30423359"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:77853062a2c45be16fd6b8d6de2a99278ee1d985a7bd8b103e97e41c034006d2"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:78151aa3ec21dccd5cdef6c74c3e73386dcdfaf19bced944169697d7ac7482fc"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win32.whl", hash = "sha256:dc4926288b2a3e9fd7b50dc6a1909a13bbdadfc67d93f3374d984e56f885579d"}, + {file = "psycopg2_binary-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:b76bedd166805480ab069612119ea636f5ab8f8771e640ae103e05a4aae3e417"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8532fd6e6e2dc57bcb3bc90b079c60de896d2128c5d9d6f24a63875a95a088cf"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0605eaed3eb239e87df0d5e3c6489daae3f7388d455d0c0b4df899519c6a38d"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f8544b092a29a6ddd72f3556a9fcf249ec412e10ad28be6a0c0d948924f2212"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d423c8d8a3c82d08fe8af900ad5b613ce3632a1249fd6a223941d0735fce493"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e5afae772c00980525f6d6ecf7cbca55676296b580c0e6abb407f15f3706996"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e6f98446430fdf41bd36d4faa6cb409f5140c1c2cf58ce0bbdaf16af7d3f119"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c77e3d1862452565875eb31bdb45ac62502feabbd53429fdc39a1cc341d681ba"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:cb16c65dcb648d0a43a2521f2f0a2300f40639f6f8c1ecbc662141e4e3e1ee07"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:911dda9c487075abd54e644ccdf5e5c16773470a6a5d3826fda76699410066fb"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57fede879f08d23c85140a360c6a77709113efd1c993923c59fde17aa27599fe"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win32.whl", hash = "sha256:64cf30263844fa208851ebb13b0732ce674d8ec6a0c86a4e160495d299ba3c93"}, + {file = "psycopg2_binary-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:81ff62668af011f9a48787564ab7eded4e9fb17a4a6a74af5ffa6a457400d2ab"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2293b001e319ab0d869d660a704942c9e2cce19745262a8aba2115ef41a0a42a"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ef7df18daf2c4c07e2695e8cfd5ee7f748a1d54d802330985a78d2a5a6dca9"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a602ea5aff39bb9fac6308e9c9d82b9a35c2bf288e184a816002c9fae930b77"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8359bf4791968c5a78c56103702000105501adb557f3cf772b2c207284273984"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:275ff571376626195ab95a746e6a04c7df8ea34638b99fc11160de91f2fef503"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:f9b5571d33660d5009a8b3c25dc1db560206e2d2f89d3df1cb32d72c0d117d52"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:420f9bbf47a02616e8554e825208cb947969451978dceb77f95ad09c37791dae"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:4154ad09dac630a0f13f37b583eae260c6aa885d67dfbccb5b02c33f31a6d420"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a148c5d507bb9b4f2030a2025c545fccb0e1ef317393eaba42e7eabd28eb6041"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:68fc1f1ba168724771e38bee37d940d2865cb0f562380a1fb1ffb428b75cb692"}, + {file = "psycopg2_binary-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:281309265596e388ef483250db3640e5f414168c5a67e9c665cafce9492eda2f"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:60989127da422b74a04345096c10d416c2b41bd7bf2a380eb541059e4e999980"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:246b123cc54bb5361588acc54218c8c9fb73068bf227a4a531d8ed56fa3ca7d6"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34eccd14566f8fe14b2b95bb13b11572f7c7d5c36da61caf414d23b91fcc5d94"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d0ef97766055fec15b5de2c06dd8e7654705ce3e5e5eed3b6651a1d2a9a152"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f82c171b4ccd83bbaf35aa05e44e690113bd4f3b7b6cc54d2219b132f3ae55"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ead20f7913a9c1e894aebe47cccf9dc834e1618b7aa96155d2091a626e59c972"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ca49a8119c6cbd77375ae303b0cfd8c11f011abbbd64601167ecca18a87e7cdd"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:323ba25b92454adb36fa425dc5cf6f8f19f78948cbad2e7bc6cdf7b0d7982e59"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:1236ed0952fbd919c100bc839eaa4a39ebc397ed1c08a97fc45fee2a595aa1b3"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:729177eaf0aefca0994ce4cffe96ad3c75e377c7b6f4efa59ebf003b6d398716"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win32.whl", hash = "sha256:804d99b24ad523a1fe18cc707bf741670332f7c7412e9d49cb5eab67e886b9b5"}, + {file = "psycopg2_binary-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:a6cdcc3ede532f4a4b96000b6362099591ab4a3e913d70bcbac2b56c872446f7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:72dffbd8b4194858d0941062a9766f8297e8868e1dd07a7b36212aaa90f49472"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:30dcc86377618a4c8f3b72418df92e77be4254d8f89f14b8e8f57d6d43603c0f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31a34c508c003a4347d389a9e6fcc2307cc2150eb516462a7a17512130de109e"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15208be1c50b99203fe88d15695f22a5bed95ab3f84354c494bcb1d08557df67"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1873aade94b74715be2246321c8650cabf5a0d098a95bab81145ffffa4c13876"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a58c98a7e9c021f357348867f537017057c2ed7f77337fd914d0bedb35dace7"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4686818798f9194d03c9129a4d9a702d9e113a89cb03bffe08c6cf799e053291"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ebdc36bea43063116f0486869652cb2ed7032dbc59fbcb4445c4862b5c1ecf7f"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:ca08decd2697fdea0aea364b370b1249d47336aec935f87b8bbfd7da5b2ee9c1"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ac05fb791acf5e1a3e39402641827780fe44d27e72567a000412c648a85ba860"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win32.whl", hash = "sha256:9dba73be7305b399924709b91682299794887cbbd88e38226ed9f6712eabee90"}, + {file = "psycopg2_binary-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:f7ae5d65ccfbebdfa761585228eb4d0df3a8b15cfb53bd953e713e09fbb12957"}, +] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + +[[package]] +name = "pydantic" +version = "2.9.1" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.9.1-py3-none-any.whl", hash = "sha256:7aff4db5fdf3cf573d4b3c30926a510a10e19a0774d38fc4967f78beb6deb612"}, + {file = "pydantic-2.9.1.tar.gz", hash = "sha256:1363c7d975c7036df0db2b4a61f2e062fbc0aa5ab5f2772e0ffc7191a4f4bce2"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.23.3" +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.23.3" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.23.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7f10a5d1b9281392f1bf507d16ac720e78285dfd635b05737c3911637601bae6"}, + {file = "pydantic_core-2.23.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3c09a7885dd33ee8c65266e5aa7fb7e2f23d49d8043f089989726391dd7350c5"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6470b5a1ec4d1c2e9afe928c6cb37eb33381cab99292a708b8cb9aa89e62429b"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9172d2088e27d9a185ea0a6c8cebe227a9139fd90295221d7d495944d2367700"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86fc6c762ca7ac8fbbdff80d61b2c59fb6b7d144aa46e2d54d9e1b7b0e780e01"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0cb80fd5c2df4898693aa841425ea1727b1b6d2167448253077d2a49003e0ed"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03667cec5daf43ac4995cefa8aaf58f99de036204a37b889c24a80927b629cec"}, + {file = "pydantic_core-2.23.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:047531242f8e9c2db733599f1c612925de095e93c9cc0e599e96cf536aaf56ba"}, + {file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5499798317fff7f25dbef9347f4451b91ac2a4330c6669821c8202fd354c7bee"}, + {file = "pydantic_core-2.23.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bbb5e45eab7624440516ee3722a3044b83fff4c0372efe183fd6ba678ff681fe"}, + {file = "pydantic_core-2.23.3-cp310-none-win32.whl", hash = "sha256:8b5b3ed73abb147704a6e9f556d8c5cb078f8c095be4588e669d315e0d11893b"}, + {file = "pydantic_core-2.23.3-cp310-none-win_amd64.whl", hash = "sha256:2b603cde285322758a0279995b5796d64b63060bfbe214b50a3ca23b5cee3e83"}, + {file = "pydantic_core-2.23.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c889fd87e1f1bbeb877c2ee56b63bb297de4636661cc9bbfcf4b34e5e925bc27"}, + {file = "pydantic_core-2.23.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea85bda3189fb27503af4c45273735bcde3dd31c1ab17d11f37b04877859ef45"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7f7f72f721223f33d3dc98a791666ebc6a91fa023ce63733709f4894a7dc611"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b2b55b0448e9da68f56b696f313949cda1039e8ec7b5d294285335b53104b61"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c24574c7e92e2c56379706b9a3f07c1e0c7f2f87a41b6ee86653100c4ce343e5"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2b05e6ccbee333a8f4b8f4d7c244fdb7a979e90977ad9c51ea31261e2085ce0"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2c409ce1c219c091e47cb03feb3c4ed8c2b8e004efc940da0166aaee8f9d6c8"}, + {file = "pydantic_core-2.23.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d965e8b325f443ed3196db890d85dfebbb09f7384486a77461347f4adb1fa7f8"}, + {file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f56af3a420fb1ffaf43ece3ea09c2d27c444e7c40dcb7c6e7cf57aae764f2b48"}, + {file = "pydantic_core-2.23.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b01a078dd4f9a52494370af21aa52964e0a96d4862ac64ff7cea06e0f12d2c5"}, + {file = "pydantic_core-2.23.3-cp311-none-win32.whl", hash = "sha256:560e32f0df04ac69b3dd818f71339983f6d1f70eb99d4d1f8e9705fb6c34a5c1"}, + {file = "pydantic_core-2.23.3-cp311-none-win_amd64.whl", hash = "sha256:c744fa100fdea0d000d8bcddee95213d2de2e95b9c12be083370b2072333a0fa"}, + {file = "pydantic_core-2.23.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:e0ec50663feedf64d21bad0809f5857bac1ce91deded203efc4a84b31b2e4305"}, + {file = "pydantic_core-2.23.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:db6e6afcb95edbe6b357786684b71008499836e91f2a4a1e55b840955b341dbb"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98ccd69edcf49f0875d86942f4418a4e83eb3047f20eb897bffa62a5d419c8fa"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a678c1ac5c5ec5685af0133262103defb427114e62eafeda12f1357a12140162"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01491d8b4d8db9f3391d93b0df60701e644ff0894352947f31fff3e52bd5c801"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fcf31facf2796a2d3b7fe338fe8640aa0166e4e55b4cb108dbfd1058049bf4cb"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7200fd561fb3be06827340da066df4311d0b6b8eb0c2116a110be5245dceb326"}, + {file = "pydantic_core-2.23.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc1636770a809dee2bd44dd74b89cc80eb41172bcad8af75dd0bc182c2666d4c"}, + {file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:67a5def279309f2e23014b608c4150b0c2d323bd7bccd27ff07b001c12c2415c"}, + {file = "pydantic_core-2.23.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:748bdf985014c6dd3e1e4cc3db90f1c3ecc7246ff5a3cd4ddab20c768b2f1dab"}, + {file = "pydantic_core-2.23.3-cp312-none-win32.whl", hash = "sha256:255ec6dcb899c115f1e2a64bc9ebc24cc0e3ab097775755244f77360d1f3c06c"}, + {file = "pydantic_core-2.23.3-cp312-none-win_amd64.whl", hash = "sha256:40b8441be16c1e940abebed83cd006ddb9e3737a279e339dbd6d31578b802f7b"}, + {file = "pydantic_core-2.23.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6daaf5b1ba1369a22c8b050b643250e3e5efc6a78366d323294aee54953a4d5f"}, + {file = "pydantic_core-2.23.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d015e63b985a78a3d4ccffd3bdf22b7c20b3bbd4b8227809b3e8e75bc37f9cb2"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3fc572d9b5b5cfe13f8e8a6e26271d5d13f80173724b738557a8c7f3a8a3791"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f6bd91345b5163ee7448bee201ed7dd601ca24f43f439109b0212e296eb5b423"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc379c73fd66606628b866f661e8785088afe2adaba78e6bbe80796baf708a63"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbdce4b47592f9e296e19ac31667daed8753c8367ebb34b9a9bd89dacaa299c9"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc3cf31edf405a161a0adad83246568647c54404739b614b1ff43dad2b02e6d5"}, + {file = "pydantic_core-2.23.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e22b477bf90db71c156f89a55bfe4d25177b81fce4aa09294d9e805eec13855"}, + {file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0a0137ddf462575d9bce863c4c95bac3493ba8e22f8c28ca94634b4a1d3e2bb4"}, + {file = "pydantic_core-2.23.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:203171e48946c3164fe7691fc349c79241ff8f28306abd4cad5f4f75ed80bc8d"}, + {file = "pydantic_core-2.23.3-cp313-none-win32.whl", hash = "sha256:76bdab0de4acb3f119c2a4bff740e0c7dc2e6de7692774620f7452ce11ca76c8"}, + {file = "pydantic_core-2.23.3-cp313-none-win_amd64.whl", hash = "sha256:37ba321ac2a46100c578a92e9a6aa33afe9ec99ffa084424291d84e456f490c1"}, + {file = "pydantic_core-2.23.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d063c6b9fed7d992bcbebfc9133f4c24b7a7f215d6b102f3e082b1117cddb72c"}, + {file = "pydantic_core-2.23.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6cb968da9a0746a0cf521b2b5ef25fc5a0bee9b9a1a8214e0a1cfaea5be7e8a4"}, + {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edbefe079a520c5984e30e1f1f29325054b59534729c25b874a16a5048028d16"}, + {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbaaf2ef20d282659093913da9d402108203f7cb5955020bd8d1ae5a2325d1c4"}, + {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb539d7e5dc4aac345846f290cf504d2fd3c1be26ac4e8b5e4c2b688069ff4cf"}, + {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e6f33503c5495059148cc486867e1d24ca35df5fc064686e631e314d959ad5b"}, + {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04b07490bc2f6f2717b10c3969e1b830f5720b632f8ae2f3b8b1542394c47a8e"}, + {file = "pydantic_core-2.23.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03795b9e8a5d7fda05f3873efc3f59105e2dcff14231680296b87b80bb327295"}, + {file = "pydantic_core-2.23.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c483dab0f14b8d3f0df0c6c18d70b21b086f74c87ab03c59250dbf6d3c89baba"}, + {file = "pydantic_core-2.23.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8b2682038e255e94baf2c473dca914a7460069171ff5cdd4080be18ab8a7fd6e"}, + {file = "pydantic_core-2.23.3-cp38-none-win32.whl", hash = "sha256:f4a57db8966b3a1d1a350012839c6a0099f0898c56512dfade8a1fe5fb278710"}, + {file = "pydantic_core-2.23.3-cp38-none-win_amd64.whl", hash = "sha256:13dd45ba2561603681a2676ca56006d6dee94493f03d5cadc055d2055615c3ea"}, + {file = "pydantic_core-2.23.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:82da2f4703894134a9f000e24965df73cc103e31e8c31906cc1ee89fde72cbd8"}, + {file = "pydantic_core-2.23.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dd9be0a42de08f4b58a3cc73a123f124f65c24698b95a54c1543065baca8cf0e"}, + {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89b731f25c80830c76fdb13705c68fef6a2b6dc494402987c7ea9584fe189f5d"}, + {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6de1ec30c4bb94f3a69c9f5f2182baeda5b809f806676675e9ef6b8dc936f28"}, + {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb68b41c3fa64587412b104294b9cbb027509dc2f6958446c502638d481525ef"}, + {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c3980f2843de5184656aab58698011b42763ccba11c4a8c35936c8dd6c7068c"}, + {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94f85614f2cba13f62c3c6481716e4adeae48e1eaa7e8bac379b9d177d93947a"}, + {file = "pydantic_core-2.23.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:510b7fb0a86dc8f10a8bb43bd2f97beb63cffad1203071dc434dac26453955cd"}, + {file = "pydantic_core-2.23.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1eba2f7ce3e30ee2170410e2171867ea73dbd692433b81a93758ab2de6c64835"}, + {file = "pydantic_core-2.23.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b259fd8409ab84b4041b7b3f24dcc41e4696f180b775961ca8142b5b21d0e70"}, + {file = "pydantic_core-2.23.3-cp39-none-win32.whl", hash = "sha256:40d9bd259538dba2f40963286009bf7caf18b5112b19d2b55b09c14dde6db6a7"}, + {file = "pydantic_core-2.23.3-cp39-none-win_amd64.whl", hash = "sha256:5a8cd3074a98ee70173a8633ad3c10e00dcb991ecec57263aacb4095c5efb958"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f399e8657c67313476a121a6944311fab377085ca7f490648c9af97fc732732d"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6b5547d098c76e1694ba85f05b595720d7c60d342f24d5aad32c3049131fa5c4"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0dda0290a6f608504882d9f7650975b4651ff91c85673341789a476b1159f211"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b6e5da855e9c55a0c67f4db8a492bf13d8d3316a59999cfbaf98cc6e401961"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:09e926397f392059ce0afdcac920df29d9c833256354d0c55f1584b0b70cf07e"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:87cfa0ed6b8c5bd6ae8b66de941cece179281239d482f363814d2b986b79cedc"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e61328920154b6a44d98cabcb709f10e8b74276bc709c9a513a8c37a18786cc4"}, + {file = "pydantic_core-2.23.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ce3317d155628301d649fe5e16a99528d5680af4ec7aa70b90b8dacd2d725c9b"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e89513f014c6be0d17b00a9a7c81b1c426f4eb9224b15433f3d98c1a071f8433"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4f62c1c953d7ee375df5eb2e44ad50ce2f5aff931723b398b8bc6f0ac159791a"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2718443bc671c7ac331de4eef9b673063b10af32a0bb385019ad61dcf2cc8f6c"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d90e08b2727c5d01af1b5ef4121d2f0c99fbee692c762f4d9d0409c9da6541"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b676583fc459c64146debea14ba3af54e540b61762dfc0613dc4e98c3f66eeb"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:50e4661f3337977740fdbfbae084ae5693e505ca2b3130a6d4eb0f2281dc43b8"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:68f4cf373f0de6abfe599a38307f4417c1c867ca381c03df27c873a9069cda25"}, + {file = "pydantic_core-2.23.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:59d52cf01854cb26c46958552a21acb10dd78a52aa34c86f284e66b209db8cab"}, + {file = "pydantic_core-2.23.3.tar.gz", hash = "sha256:3cb0f65d8b4121c1b015c60104a685feb929a29d7cf204387c7f2688c7974690"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pydantic-settings" +version = "2.5.2" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_settings-2.5.2-py3-none-any.whl", hash = "sha256:2c912e55fd5794a59bf8c832b9de832dcfdf4778d79ff79b708744eed499a907"}, + {file = "pydantic_settings-2.5.2.tar.gz", hash = "sha256:f90b139682bee4d2065273d5185d71d37ea46cfe57e1b5ae184fc6a0b2484ca0"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" + +[package.extras] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + +[[package]] +name = "pygments" +version = "2.18.0" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyjwt" +version = "2.9.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, + {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, +] + +[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", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + +[[package]] +name = "pyproject-api" +version = "1.7.1" +description = "API to interact with the python pyproject.toml based projects" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyproject_api-1.7.1-py3-none-any.whl", hash = "sha256:2dc1654062c2b27733d8fd4cdda672b22fe8741ef1dde8e3a998a9547b071eeb"}, + {file = "pyproject_api-1.7.1.tar.gz", hash = "sha256:7ebc6cd10710f89f4cf2a2731710a98abce37ebff19427116ff2174c9236a827"}, +] + +[package.dependencies] +packaging = ">=24.1" + +[package.extras] +docs = ["furo (>=2024.5.6)", "sphinx-autodoc-typehints (>=2.2.1)"] +testing = ["covdefaults (>=2.3)", "pytest (>=8.2.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=70.1)"] + +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-alembic" +version = "0.11.1" +description = "A pytest plugin for verifying alembic migrations." +optional = false +python-versions = "<4,>=3.6" +files = [ + {file = "pytest_alembic-0.11.1-py3-none-any.whl", hash = "sha256:f83e8c1534d50ced053aa4b1dbf6e261f4674aa626cb852fc1dcb565049ae152"}, + {file = "pytest_alembic-0.11.1.tar.gz", hash = "sha256:a920d8770b5be77326c5c1b2bd8d4d4a0dd8fc2c2d57abbcd1fec28a21131b85"}, +] + +[package.dependencies] +alembic = "*" +pytest = ">=6.0" +sqlalchemy = "*" + +[[package]] +name = "pytest-cov" +version = "5.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-randomly" +version = "3.15.0" +description = "Pytest plugin to randomly order tests and control random.seed." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_randomly-3.15.0-py3-none-any.whl", hash = "sha256:0516f4344b29f4e9cdae8bce31c4aeebf59d0b9ef05927c33354ff3859eeeca6"}, + {file = "pytest_randomly-3.15.0.tar.gz", hash = "sha256:b908529648667ba5e54723088edd6f82252f540cc340d748d1fa985539687047"}, +] + +[package.dependencies] +pytest = "*" + +[[package]] +name = "python-dateutil" +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.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] +six = ">=1.5" + +[[package]] +name = "python-dotenv" +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.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "python-multipart" +version = "0.0.9" +description = "A streaming multipart parser for Python" +optional = false +python-versions = ">=3.8" +files = [ + {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.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" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "responses" +version = "0.25.3" +description = "A utility library for mocking out the `requests` Python library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "responses-0.25.3-py3-none-any.whl", hash = "sha256:521efcbc82081ab8daa588e08f7e8a64ce79b91c39f6e62199b19159bea7dbcb"}, + {file = "responses-0.25.3.tar.gz", hash = "sha256:617b9247abd9ae28313d57a75880422d55ec63c29d33d629697590a034358dba"}, +] + +[package.dependencies] +pyyaml = "*" +requests = ">=2.30.0,<3.0" +urllib3 = ">=1.25.10,<3.0" + +[package.extras] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli", "tomli-w", "types-PyYAML", "types-requests"] + +[[package]] +name = "rich" +version = "13.8.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, + {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "s3transfer" +version = "0.10.2" +description = "An Amazon S3 Transfer Manager" +optional = false +python-versions = ">=3.8" +files = [ + {file = "s3transfer-0.10.2-py3-none-any.whl", hash = "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69"}, + {file = "s3transfer-0.10.2.tar.gz", hash = "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6"}, +] + +[package.dependencies] +botocore = ">=1.33.2,<2.0a.0" + +[package.extras] +crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[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.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {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.34" +description = "Database Abstraction Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "SQLAlchemy-2.0.34-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:95d0b2cf8791ab5fb9e3aa3d9a79a0d5d51f55b6357eecf532a120ba3b5524db"}, + {file = "SQLAlchemy-2.0.34-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:243f92596f4fd4c8bd30ab8e8dd5965afe226363d75cab2468f2c707f64cd83b"}, + {file = "SQLAlchemy-2.0.34-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ea54f7300553af0a2a7235e9b85f4204e1fc21848f917a3213b0e0818de9a24"}, + {file = "SQLAlchemy-2.0.34-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:173f5f122d2e1bff8fbd9f7811b7942bead1f5e9f371cdf9e670b327e6703ebd"}, + {file = "SQLAlchemy-2.0.34-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:196958cde924a00488e3e83ff917be3b73cd4ed8352bbc0f2989333176d1c54d"}, + {file = "SQLAlchemy-2.0.34-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd90c221ed4e60ac9d476db967f436cfcecbd4ef744537c0f2d5291439848768"}, + {file = "SQLAlchemy-2.0.34-cp310-cp310-win32.whl", hash = "sha256:3166dfff2d16fe9be3241ee60ece6fcb01cf8e74dd7c5e0b64f8e19fab44911b"}, + {file = "SQLAlchemy-2.0.34-cp310-cp310-win_amd64.whl", hash = "sha256:6831a78bbd3c40f909b3e5233f87341f12d0b34a58f14115c9e94b4cdaf726d3"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7db3db284a0edaebe87f8f6642c2b2c27ed85c3e70064b84d1c9e4ec06d5d84"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:430093fce0efc7941d911d34f75a70084f12f6ca5c15d19595c18753edb7c33b"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79cb400c360c7c210097b147c16a9e4c14688a6402445ac848f296ade6283bbc"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb1b30f31a36c7f3fee848391ff77eebdd3af5750bf95fbf9b8b5323edfdb4ec"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fddde2368e777ea2a4891a3fb4341e910a056be0bb15303bf1b92f073b80c02"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80bd73ea335203b125cf1d8e50fef06be709619eb6ab9e7b891ea34b5baa2287"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-win32.whl", hash = "sha256:6daeb8382d0df526372abd9cb795c992e18eed25ef2c43afe518c73f8cccb721"}, + {file = "SQLAlchemy-2.0.34-cp311-cp311-win_amd64.whl", hash = "sha256:5bc08e75ed11693ecb648b7a0a4ed80da6d10845e44be0c98c03f2f880b68ff4"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:53e68b091492c8ed2bd0141e00ad3089bcc6bf0e6ec4142ad6505b4afe64163e"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bcd18441a49499bf5528deaa9dee1f5c01ca491fc2791b13604e8f972877f812"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:165bbe0b376541092bf49542bd9827b048357f4623486096fc9aaa6d4e7c59a2"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3330415cd387d2b88600e8e26b510d0370db9b7eaf984354a43e19c40df2e2b"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97b850f73f8abbffb66ccbab6e55a195a0eb655e5dc74624d15cff4bfb35bd74"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee4c6917857fd6121ed84f56d1dc78eb1d0e87f845ab5a568aba73e78adf83"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-win32.whl", hash = "sha256:fbb034f565ecbe6c530dff948239377ba859420d146d5f62f0271407ffb8c580"}, + {file = "SQLAlchemy-2.0.34-cp312-cp312-win_amd64.whl", hash = "sha256:707c8f44931a4facd4149b52b75b80544a8d824162602b8cd2fe788207307f9a"}, + {file = "SQLAlchemy-2.0.34-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:24af3dc43568f3780b7e1e57c49b41d98b2d940c1fd2e62d65d3928b6f95f021"}, + {file = "SQLAlchemy-2.0.34-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e60ed6ef0a35c6b76b7640fe452d0e47acc832ccbb8475de549a5cc5f90c2c06"}, + {file = "SQLAlchemy-2.0.34-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413c85cd0177c23e32dee6898c67a5f49296640041d98fddb2c40888fe4daa2e"}, + {file = "SQLAlchemy-2.0.34-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:25691f4adfb9d5e796fd48bf1432272f95f4bbe5f89c475a788f31232ea6afba"}, + {file = "SQLAlchemy-2.0.34-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:526ce723265643dbc4c7efb54f56648cc30e7abe20f387d763364b3ce7506c82"}, + {file = "SQLAlchemy-2.0.34-cp37-cp37m-win32.whl", hash = "sha256:13be2cc683b76977a700948411a94c67ad8faf542fa7da2a4b167f2244781cf3"}, + {file = "SQLAlchemy-2.0.34-cp37-cp37m-win_amd64.whl", hash = "sha256:e54ef33ea80d464c3dcfe881eb00ad5921b60f8115ea1a30d781653edc2fd6a2"}, + {file = "SQLAlchemy-2.0.34-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:43f28005141165edd11fbbf1541c920bd29e167b8bbc1fb410d4fe2269c1667a"}, + {file = "SQLAlchemy-2.0.34-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b68094b165a9e930aedef90725a8fcfafe9ef95370cbb54abc0464062dbf808f"}, + {file = "SQLAlchemy-2.0.34-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1e03db964e9d32f112bae36f0cc1dcd1988d096cfd75d6a588a3c3def9ab2b"}, + {file = "SQLAlchemy-2.0.34-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:203d46bddeaa7982f9c3cc693e5bc93db476ab5de9d4b4640d5c99ff219bee8c"}, + {file = "SQLAlchemy-2.0.34-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ae92bebca3b1e6bd203494e5ef919a60fb6dfe4d9a47ed2453211d3bd451b9f5"}, + {file = "SQLAlchemy-2.0.34-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9661268415f450c95f72f0ac1217cc6f10256f860eed85c2ae32e75b60278ad8"}, + {file = "SQLAlchemy-2.0.34-cp38-cp38-win32.whl", hash = "sha256:895184dfef8708e15f7516bd930bda7e50ead069280d2ce09ba11781b630a434"}, + {file = "SQLAlchemy-2.0.34-cp38-cp38-win_amd64.whl", hash = "sha256:6e7cde3a2221aa89247944cafb1b26616380e30c63e37ed19ff0bba5e968688d"}, + {file = "SQLAlchemy-2.0.34-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dbcdf987f3aceef9763b6d7b1fd3e4ee210ddd26cac421d78b3c206d07b2700b"}, + {file = "SQLAlchemy-2.0.34-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ce119fc4ce0d64124d37f66a6f2a584fddc3c5001755f8a49f1ca0a177ef9796"}, + {file = "SQLAlchemy-2.0.34-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a17d8fac6df9835d8e2b4c5523666e7051d0897a93756518a1fe101c7f47f2f0"}, + {file = "SQLAlchemy-2.0.34-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ebc11c54c6ecdd07bb4efbfa1554538982f5432dfb8456958b6d46b9f834bb7"}, + {file = "SQLAlchemy-2.0.34-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e6965346fc1491a566e019a4a1d3dfc081ce7ac1a736536367ca305da6472a8"}, + {file = "SQLAlchemy-2.0.34-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:220574e78ad986aea8e81ac68821e47ea9202b7e44f251b7ed8c66d9ae3f4278"}, + {file = "SQLAlchemy-2.0.34-cp39-cp39-win32.whl", hash = "sha256:b75b00083e7fe6621ce13cfce9d4469c4774e55e8e9d38c305b37f13cf1e874c"}, + {file = "SQLAlchemy-2.0.34-cp39-cp39-win_amd64.whl", hash = "sha256:c29d03e0adf3cc1a8c3ec62d176824972ae29b67a66cbb18daff3062acc6faa8"}, + {file = "SQLAlchemy-2.0.34-py3-none-any.whl", hash = "sha256:7286c353ee6475613d8beff83167374006c6b3e3f0e6491bfe8ca610eb1dec0f"}, + {file = "sqlalchemy-2.0.34.tar.gz", hash = "sha256:10d8f36990dd929690666679b0f42235c159a7051534adb135728ee52828dd22"}, +] + +[package.dependencies] +greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" 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\")"} +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)"] +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)"] +mssql = ["pyodbc"] +mssql-pymssql = ["pymssql"] +mssql-pyodbc = ["pyodbc"] +mypy = ["mypy (>=0.910)"] +mysql = ["mysqlclient (>=1.4.0)"] +mysql-connector = ["mysql-connector-python"] +oracle = ["cx_oracle (>=8)"] +oracle-oracledb = ["oracledb (>=1.0.1)"] +postgresql = ["psycopg2 (>=2.7)"] +postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.29.1)"] +postgresql-psycopg = ["psycopg (>=3.0.7)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] +pymysql = ["pymysql"] +sqlcipher = ["sqlcipher3_binary"] + +[[package]] +name = "starlette" +version = "0.38.5" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.38.5-py3-none-any.whl", hash = "sha256:632f420a9d13e3ee2a6f18f437b0a9f1faecb0bc42e1942aa2ea0e379a4c4206"}, + {file = "starlette-0.38.5.tar.gz", hash = "sha256:04a92830a9b6eb1442c766199d62260c3d4dc9c4f9188360626b1e0273cb7077"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[[package]] +name = "tox" +version = "4.18.1" +description = "tox is a generic virtualenv management and test command line tool" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tox-4.18.1-py3-none-any.whl", hash = "sha256:35d472032ee1f73fe20c3e0e73d7073a4e85075c86ff02c576f9fc7c6a15a578"}, + {file = "tox-4.18.1.tar.gz", hash = "sha256:3c0c96bc3a568a5c7e66387a4cfcf8c875b52e09f4d47c9f7a277ec82f1a0b11"}, +] + +[package.dependencies] +cachetools = ">=5.5" +chardet = ">=5.2" +colorama = ">=0.4.6" +filelock = ">=3.15.4" +packaging = ">=24.1" +platformdirs = ">=4.2.2" +pluggy = ">=1.5" +pyproject-api = ">=1.7.1" +virtualenv = ">=20.26.3" + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-argparse-cli (>=1.17)", "sphinx-autodoc-typehints (>=2.4)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=24.8)"] +testing = ["build[virtualenv] (>=1.2.2)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.2)", "devpi-process (>=1)", "diff-cover (>=9.1.1)", "distlib (>=0.3.8)", "flaky (>=3.8.1)", "hatch-vcs (>=0.4)", "hatchling (>=1.25)", "psutil (>=6)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-xdist (>=3.6.1)", "re-assert (>=1.1)", "setuptools (>=74.1.2)", "time-machine (>=2.15)", "wheel (>=0.44)"] + +[[package]] +name = "typer" +version = "0.12.5" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.7" +files = [ + {file = "typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b"}, + {file = "typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722"}, +] + +[package.dependencies] +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "uvicorn" +version = "0.30.6" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, + {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "uvloop" +version = "0.20.0" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "uvloop-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9ebafa0b96c62881d5cafa02d9da2e44c23f9f0cd829f3a32a6aff771449c996"}, + {file = "uvloop-0.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:35968fc697b0527a06e134999eef859b4034b37aebca537daeb598b9d45a137b"}, + {file = "uvloop-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b16696f10e59d7580979b420eedf6650010a4a9c3bd8113f24a103dfdb770b10"}, + {file = "uvloop-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b04d96188d365151d1af41fa2d23257b674e7ead68cfd61c725a422764062ae"}, + {file = "uvloop-0.20.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94707205efbe809dfa3a0d09c08bef1352f5d3d6612a506f10a319933757c006"}, + {file = "uvloop-0.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89e8d33bb88d7263f74dc57d69f0063e06b5a5ce50bb9a6b32f5fcbe655f9e73"}, + {file = "uvloop-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e50289c101495e0d1bb0bfcb4a60adde56e32f4449a67216a1ab2750aa84f037"}, + {file = "uvloop-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e237f9c1e8a00e7d9ddaa288e535dc337a39bcbf679f290aee9d26df9e72bce9"}, + {file = "uvloop-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:746242cd703dc2b37f9d8b9f173749c15e9a918ddb021575a0205ec29a38d31e"}, + {file = "uvloop-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82edbfd3df39fb3d108fc079ebc461330f7c2e33dbd002d146bf7c445ba6e756"}, + {file = "uvloop-0.20.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80dc1b139516be2077b3e57ce1cb65bfed09149e1d175e0478e7a987863b68f0"}, + {file = "uvloop-0.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f44af67bf39af25db4c1ac27e82e9665717f9c26af2369c404be865c8818dcf"}, + {file = "uvloop-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4b75f2950ddb6feed85336412b9a0c310a2edbcf4cf931aa5cfe29034829676d"}, + {file = "uvloop-0.20.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:77fbc69c287596880ecec2d4c7a62346bef08b6209749bf6ce8c22bbaca0239e"}, + {file = "uvloop-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6462c95f48e2d8d4c993a2950cd3d31ab061864d1c226bbf0ee2f1a8f36674b9"}, + {file = "uvloop-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:649c33034979273fa71aa25d0fe120ad1777c551d8c4cd2c0c9851d88fcb13ab"}, + {file = "uvloop-0.20.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a609780e942d43a275a617c0839d85f95c334bad29c4c0918252085113285b5"}, + {file = "uvloop-0.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aea15c78e0d9ad6555ed201344ae36db5c63d428818b4b2a42842b3870127c00"}, + {file = "uvloop-0.20.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f0e94b221295b5e69de57a1bd4aeb0b3a29f61be6e1b478bb8a69a73377db7ba"}, + {file = "uvloop-0.20.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fee6044b64c965c425b65a4e17719953b96e065c5b7e09b599ff332bb2744bdf"}, + {file = "uvloop-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:265a99a2ff41a0fd56c19c3838b29bf54d1d177964c300dad388b27e84fd7847"}, + {file = "uvloop-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10c2956efcecb981bf9cfb8184d27d5d64b9033f917115a960b83f11bfa0d6b"}, + {file = "uvloop-0.20.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e7d61fe8e8d9335fac1bf8d5d82820b4808dd7a43020c149b63a1ada953d48a6"}, + {file = "uvloop-0.20.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2beee18efd33fa6fdb0976e18475a4042cd31c7433c866e8a09ab604c7c22ff2"}, + {file = "uvloop-0.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8c36fdf3e02cec92aed2d44f63565ad1522a499c654f07935c8f9d04db69e95"}, + {file = "uvloop-0.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a0fac7be202596c7126146660725157d4813aa29a4cc990fe51346f75ff8fde7"}, + {file = "uvloop-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0fba61846f294bce41eb44d60d58136090ea2b5b99efd21cbdf4e21927c56a"}, + {file = "uvloop-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95720bae002ac357202e0d866128eb1ac82545bcf0b549b9abe91b5178d9b541"}, + {file = "uvloop-0.20.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:36c530d8fa03bfa7085af54a48f2ca16ab74df3ec7108a46ba82fd8b411a2315"}, + {file = "uvloop-0.20.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e97152983442b499d7a71e44f29baa75b3b02e65d9c44ba53b10338e98dedb66"}, + {file = "uvloop-0.20.0.tar.gz", hash = "sha256:4603ca714a754fc8d9b197e325db25b2ea045385e8a3ad05d3463de725fdf469"}, +] + +[package.extras] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] + +[[package]] +name = "virtualenv" +version = "20.26.4" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.4-py3-none-any.whl", hash = "sha256:48f2695d9809277003f30776d155615ffc11328e6a0a8c1f0ec80188d7874a55"}, + {file = "virtualenv-20.26.4.tar.gz", hash = "sha256:c17f4e0f3e6036e9f26700446f85c76ab11df65ff6d8a9cbfad9f71aabfcf23c"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "watchfiles" +version = "0.24.0" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "watchfiles-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0"}, + {file = "watchfiles-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c"}, + {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361"}, + {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3"}, + {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571"}, + {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd"}, + {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a"}, + {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e"}, + {file = "watchfiles-0.24.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c"}, + {file = "watchfiles-0.24.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188"}, + {file = "watchfiles-0.24.0-cp310-none-win32.whl", hash = "sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735"}, + {file = "watchfiles-0.24.0-cp310-none-win_amd64.whl", hash = "sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04"}, + {file = "watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428"}, + {file = "watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c"}, + {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43"}, + {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327"}, + {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5"}, + {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61"}, + {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15"}, + {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823"}, + {file = "watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab"}, + {file = "watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec"}, + {file = "watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d"}, + {file = "watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c"}, + {file = "watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633"}, + {file = "watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a"}, + {file = "watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370"}, + {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6"}, + {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b"}, + {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e"}, + {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea"}, + {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f"}, + {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234"}, + {file = "watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef"}, + {file = "watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968"}, + {file = "watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444"}, + {file = "watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896"}, + {file = "watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418"}, + {file = "watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48"}, + {file = "watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90"}, + {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94"}, + {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e"}, + {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827"}, + {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df"}, + {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab"}, + {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f"}, + {file = "watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b"}, + {file = "watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18"}, + {file = "watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07"}, + {file = "watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366"}, + {file = "watchfiles-0.24.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318"}, + {file = "watchfiles-0.24.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05"}, + {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c"}, + {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83"}, + {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c"}, + {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b"}, + {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b"}, + {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91"}, + {file = "watchfiles-0.24.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b"}, + {file = "watchfiles-0.24.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22"}, + {file = "watchfiles-0.24.0-cp38-none-win32.whl", hash = "sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1"}, + {file = "watchfiles-0.24.0-cp38-none-win_amd64.whl", hash = "sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1"}, + {file = "watchfiles-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886"}, + {file = "watchfiles-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f"}, + {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855"}, + {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b"}, + {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430"}, + {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3"}, + {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a"}, + {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9"}, + {file = "watchfiles-0.24.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca"}, + {file = "watchfiles-0.24.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e"}, + {file = "watchfiles-0.24.0-cp39-none-win32.whl", hash = "sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da"}, + {file = "watchfiles-0.24.0-cp39-none-win_amd64.whl", hash = "sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f"}, + {file = "watchfiles-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f"}, + {file = "watchfiles-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b"}, + {file = "watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4"}, + {file = "watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a"}, + {file = "watchfiles-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be"}, + {file = "watchfiles-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5"}, + {file = "watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777"}, + {file = "watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e"}, + {file = "watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + +[[package]] +name = "websockets" +version = "13.0.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websockets-13.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1841c9082a3ba4a05ea824cf6d99570a6a2d8849ef0db16e9c826acb28089e8f"}, + {file = "websockets-13.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c5870b4a11b77e4caa3937142b650fbbc0914a3e07a0cf3131f35c0587489c1c"}, + {file = "websockets-13.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f1d3d1f2eb79fe7b0fb02e599b2bf76a7619c79300fc55f0b5e2d382881d4f7f"}, + {file = "websockets-13.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15c7d62ee071fa94a2fc52c2b472fed4af258d43f9030479d9c4a2de885fd543"}, + {file = "websockets-13.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6724b554b70d6195ba19650fef5759ef11346f946c07dbbe390e039bcaa7cc3d"}, + {file = "websockets-13.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56a952fa2ae57a42ba7951e6b2605e08a24801a4931b5644dfc68939e041bc7f"}, + {file = "websockets-13.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17118647c0ea14796364299e942c330d72acc4b248e07e639d34b75067b3cdd8"}, + {file = "websockets-13.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64a11aae1de4c178fa653b07d90f2fb1a2ed31919a5ea2361a38760192e1858b"}, + {file = "websockets-13.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0617fd0b1d14309c7eab6ba5deae8a7179959861846cbc5cb528a7531c249448"}, + {file = "websockets-13.0.1-cp310-cp310-win32.whl", hash = "sha256:11f9976ecbc530248cf162e359a92f37b7b282de88d1d194f2167b5e7ad80ce3"}, + {file = "websockets-13.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:c3c493d0e5141ec055a7d6809a28ac2b88d5b878bb22df8c621ebe79a61123d0"}, + {file = "websockets-13.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:699ba9dd6a926f82a277063603fc8d586b89f4cb128efc353b749b641fcddda7"}, + {file = "websockets-13.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf2fae6d85e5dc384bf846f8243ddaa9197f3a1a70044f59399af001fd1f51d4"}, + {file = "websockets-13.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:52aed6ef21a0f1a2a5e310fb5c42d7555e9c5855476bbd7173c3aa3d8a0302f2"}, + {file = "websockets-13.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8eb2b9a318542153674c6e377eb8cb9ca0fc011c04475110d3477862f15d29f0"}, + {file = "websockets-13.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5df891c86fe68b2c38da55b7aea7095beca105933c697d719f3f45f4220a5e0e"}, + {file = "websockets-13.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fac2d146ff30d9dd2fcf917e5d147db037a5c573f0446c564f16f1f94cf87462"}, + {file = "websockets-13.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b8ac5b46fd798bbbf2ac6620e0437c36a202b08e1f827832c4bf050da081b501"}, + {file = "websockets-13.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46af561eba6f9b0848b2c9d2427086cabadf14e0abdd9fde9d72d447df268418"}, + {file = "websockets-13.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b5a06d7f60bc2fc378a333978470dfc4e1415ee52f5f0fce4f7853eb10c1e9df"}, + {file = "websockets-13.0.1-cp311-cp311-win32.whl", hash = "sha256:556e70e4f69be1082e6ef26dcb70efcd08d1850f5d6c5f4f2bcb4e397e68f01f"}, + {file = "websockets-13.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:67494e95d6565bf395476e9d040037ff69c8b3fa356a886b21d8422ad86ae075"}, + {file = "websockets-13.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:f9c9e258e3d5efe199ec23903f5da0eeaad58cf6fccb3547b74fd4750e5ac47a"}, + {file = "websockets-13.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6b41a1b3b561f1cba8321fb32987552a024a8f67f0d05f06fcf29f0090a1b956"}, + {file = "websockets-13.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f73e676a46b0fe9426612ce8caeca54c9073191a77c3e9d5c94697aef99296af"}, + {file = "websockets-13.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f613289f4a94142f914aafad6c6c87903de78eae1e140fa769a7385fb232fdf"}, + {file = "websockets-13.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f52504023b1480d458adf496dc1c9e9811df4ba4752f0bc1f89ae92f4f07d0c"}, + {file = "websockets-13.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:139add0f98206cb74109faf3611b7783ceafc928529c62b389917a037d4cfdf4"}, + {file = "websockets-13.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:47236c13be337ef36546004ce8c5580f4b1150d9538b27bf8a5ad8edf23ccfab"}, + {file = "websockets-13.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c44ca9ade59b2e376612df34e837013e2b273e6c92d7ed6636d0556b6f4db93d"}, + {file = "websockets-13.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9bbc525f4be3e51b89b2a700f5746c2a6907d2e2ef4513a8daafc98198b92237"}, + {file = "websockets-13.0.1-cp312-cp312-win32.whl", hash = "sha256:3624fd8664f2577cf8de996db3250662e259bfbc870dd8ebdcf5d7c6ac0b5185"}, + {file = "websockets-13.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0513c727fb8adffa6d9bf4a4463b2bade0186cbd8c3604ae5540fae18a90cb99"}, + {file = "websockets-13.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1ee4cc030a4bdab482a37462dbf3ffb7e09334d01dd37d1063be1136a0d825fa"}, + {file = "websockets-13.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbb0b697cc0655719522406c059eae233abaa3243821cfdfab1215d02ac10231"}, + {file = "websockets-13.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:acbebec8cb3d4df6e2488fbf34702cbc37fc39ac7abf9449392cefb3305562e9"}, + {file = "websockets-13.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63848cdb6fcc0bf09d4a155464c46c64ffdb5807ede4fb251da2c2692559ce75"}, + {file = "websockets-13.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:872afa52a9f4c414d6955c365b6588bc4401272c629ff8321a55f44e3f62b553"}, + {file = "websockets-13.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05e70fec7c54aad4d71eae8e8cab50525e899791fc389ec6f77b95312e4e9920"}, + {file = "websockets-13.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e82db3756ccb66266504f5a3de05ac6b32f287faacff72462612120074103329"}, + {file = "websockets-13.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4e85f46ce287f5c52438bb3703d86162263afccf034a5ef13dbe4318e98d86e7"}, + {file = "websockets-13.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3fea72e4e6edb983908f0db373ae0732b275628901d909c382aae3b592589f2"}, + {file = "websockets-13.0.1-cp313-cp313-win32.whl", hash = "sha256:254ecf35572fca01a9f789a1d0f543898e222f7b69ecd7d5381d8d8047627bdb"}, + {file = "websockets-13.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:ca48914cdd9f2ccd94deab5bcb5ac98025a5ddce98881e5cce762854a5de330b"}, + {file = "websockets-13.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b74593e9acf18ea5469c3edaa6b27fa7ecf97b30e9dabd5a94c4c940637ab96e"}, + {file = "websockets-13.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:132511bfd42e77d152c919147078460c88a795af16b50e42a0bd14f0ad71ddd2"}, + {file = "websockets-13.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:165bedf13556f985a2aa064309baa01462aa79bf6112fbd068ae38993a0e1f1b"}, + {file = "websockets-13.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e801ca2f448850685417d723ec70298feff3ce4ff687c6f20922c7474b4746ae"}, + {file = "websockets-13.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30d3a1f041360f029765d8704eae606781e673e8918e6b2c792e0775de51352f"}, + {file = "websockets-13.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67648f5e50231b5a7f6d83b32f9c525e319f0ddc841be0de64f24928cd75a603"}, + {file = "websockets-13.0.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4f0426d51c8f0926a4879390f53c7f5a855e42d68df95fff6032c82c888b5f36"}, + {file = "websockets-13.0.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ef48e4137e8799998a343706531e656fdec6797b80efd029117edacb74b0a10a"}, + {file = "websockets-13.0.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:249aab278810bee585cd0d4de2f08cfd67eed4fc75bde623be163798ed4db2eb"}, + {file = "websockets-13.0.1-cp38-cp38-win32.whl", hash = "sha256:06c0a667e466fcb56a0886d924b5f29a7f0886199102f0a0e1c60a02a3751cb4"}, + {file = "websockets-13.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1f3cf6d6ec1142412d4535adabc6bd72a63f5f148c43fe559f06298bc21953c9"}, + {file = "websockets-13.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1fa082ea38d5de51dd409434edc27c0dcbd5fed2b09b9be982deb6f0508d25bc"}, + {file = "websockets-13.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4a365bcb7be554e6e1f9f3ed64016e67e2fa03d7b027a33e436aecf194febb63"}, + {file = "websockets-13.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:10a0dc7242215d794fb1918f69c6bb235f1f627aaf19e77f05336d147fce7c37"}, + {file = "websockets-13.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59197afd478545b1f73367620407b0083303569c5f2d043afe5363676f2697c9"}, + {file = "websockets-13.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d20516990d8ad557b5abeb48127b8b779b0b7e6771a265fa3e91767596d7d97"}, + {file = "websockets-13.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1a2e272d067030048e1fe41aa1ec8cfbbaabce733b3d634304fa2b19e5c897f"}, + {file = "websockets-13.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ad327ac80ba7ee61da85383ca8822ff808ab5ada0e4a030d66703cc025b021c4"}, + {file = "websockets-13.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:518f90e6dd089d34eaade01101fd8a990921c3ba18ebbe9b0165b46ebff947f0"}, + {file = "websockets-13.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:68264802399aed6fe9652e89761031acc734fc4c653137a5911c2bfa995d6d6d"}, + {file = "websockets-13.0.1-cp39-cp39-win32.whl", hash = "sha256:a5dc0c42ded1557cc7c3f0240b24129aefbad88af4f09346164349391dea8e58"}, + {file = "websockets-13.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b448a0690ef43db5ef31b3a0d9aea79043882b4632cfc3eaab20105edecf6097"}, + {file = "websockets-13.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:faef9ec6354fe4f9a2c0bbb52fb1ff852effc897e2a4501e25eb3a47cb0a4f89"}, + {file = "websockets-13.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:03d3f9ba172e0a53e37fa4e636b86cc60c3ab2cfee4935e66ed1d7acaa4625ad"}, + {file = "websockets-13.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d450f5a7a35662a9b91a64aefa852f0c0308ee256122f5218a42f1d13577d71e"}, + {file = "websockets-13.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f55b36d17ac50aa8a171b771e15fbe1561217510c8768af3d546f56c7576cdc"}, + {file = "websockets-13.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14b9c006cac63772b31abbcd3e3abb6228233eec966bf062e89e7fa7ae0b7333"}, + {file = "websockets-13.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b79915a1179a91f6c5f04ece1e592e2e8a6bd245a0e45d12fd56b2b59e559a32"}, + {file = "websockets-13.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f40de079779acbcdbb6ed4c65af9f018f8b77c5ec4e17a4b737c05c2db554491"}, + {file = "websockets-13.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:80e4ba642fc87fa532bac07e5ed7e19d56940b6af6a8c61d4429be48718a380f"}, + {file = "websockets-13.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a02b0161c43cc9e0232711eff846569fad6ec836a7acab16b3cf97b2344c060"}, + {file = "websockets-13.0.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6aa74a45d4cdc028561a7d6ab3272c8b3018e23723100b12e58be9dfa5a24491"}, + {file = "websockets-13.0.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00fd961943b6c10ee6f0b1130753e50ac5dcd906130dcd77b0003c3ab797d026"}, + {file = "websockets-13.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d93572720d781331fb10d3da9ca1067817d84ad1e7c31466e9f5e59965618096"}, + {file = "websockets-13.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:71e6e5a3a3728886caee9ab8752e8113670936a193284be9d6ad2176a137f376"}, + {file = "websockets-13.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c4a6343e3b0714e80da0b0893543bf9a5b5fa71b846ae640e56e9abc6fbc4c83"}, + {file = "websockets-13.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a678532018e435396e37422a95e3ab87f75028ac79570ad11f5bf23cd2a7d8c"}, + {file = "websockets-13.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6716c087e4aa0b9260c4e579bb82e068f84faddb9bfba9906cb87726fa2e870"}, + {file = "websockets-13.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e33505534f3f673270dd67f81e73550b11de5b538c56fe04435d63c02c3f26b5"}, + {file = "websockets-13.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:acab3539a027a85d568c2573291e864333ec9d912675107d6efceb7e2be5d980"}, + {file = "websockets-13.0.1-py3-none-any.whl", hash = "sha256:b80f0c51681c517604152eb6a572f5a9378f877763231fddb883ba2f968e8817"}, + {file = "websockets-13.0.1.tar.gz", hash = "sha256:4d6ece65099411cfd9a48d13701d7438d9c34f479046b34c50ff60bb8834e43e"}, +] + +[[package]] +name = "werkzeug" +version = "3.0.4" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.8" +files = [ + {file = "werkzeug-3.0.4-py3-none-any.whl", hash = "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c"}, + {file = "werkzeug-3.0.4.tar.gz", hash = "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "xmltodict" +version = "0.13.0" +description = "Makes working with XML feel like you are working with JSON" +optional = false +python-versions = ">=3.4" +files = [ + {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, + {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "1b6a30e72e81c23468cee1c6dfe89e08b755f042eba78c99716a8ebc1beab80c" diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 00000000..0b64c698 --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,67 @@ +[tool.poetry] +name = "homeuniteus-api" +version = "0.1.0" +description = "Web API for Home Unite Us" +authors = [ + "Home Unite Us ", +] +license = "MIT" +readme = "README.md" +package-mode = false + +[tool.poetry.dependencies] +python = "^3.12" +fastapi = {extras = ["standard"], version = "^0.113.0"} + +# SQLAlchemy is a Python SQL toolkit and Object Relational Mapper that is +# mainly used for its object relational mapper and database transactional +# features. +sqlalchemy = "^2.0.34" + +# alembic is a database migration tool provided by the SQLAlchemy project. +# alembic does not use SemVer. Changes in the middle number represents a +# "Significant Minor Release" that might be non-backward compatible. +# https://alembic.sqlalchemy.org/en/latest/front.html#versioning-scheme +alembic = "^1.13" + +# psycopg2 allows SQLAlchemy to communicate with PostgreSQL. +# PostgreSQL is this project's target production database. +psycopg2-binary = "^2.9" + +# boto3 is used for connecting to AWS resources +boto3 = "^1.35.13" + +# pydantic-settings is a Pydantic feature that is used to load settings/configurations +# from environment variables or secret files. +pydantic-settings = "^2.4.0" + +# email-validator validates e-mail addresses. It is used with Pydantic to validates +# an e-mail address in Pydantic schemas. +email-validator = "^2.2.0" + +# pyjwt is used to decode information contained in a JSON Web Token +pyjwt = {extras = ["crypto"], version = "^2.9.0"} + +[tool.poetry.group.test.dependencies] +# tox builds and runs tests in an isolated environment. +# It has its own configuration file named `tox.ini`. +tox = "^4.18" + +# pytest runs the tests implemented in this project. +pytest = "^8.3" + +# pytest-cov will report the amount of test coverage implemented. +pytest-cov = "^5.0" + +# pytest-randomly will cause tests to be run in random order. +pytest-randomly = "^3.15" + +# moto mocks out AWS Services +moto = {extras = ["cognitoidp"], version = "^5.0"} + +# pytest-alembic is a pytest plugin that verifies alembic migrations +pytest-alembic = "^0.11.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/cognito_setup.py b/backend/tests/cognito_setup.py new file mode 100644 index 00000000..88432ea2 --- /dev/null +++ b/backend/tests/cognito_setup.py @@ -0,0 +1,211 @@ +import re +import uuid + + +class AWSTemporaryUserpool(): + """Provide a temporary user pool for development and testing purposes. + + The provided userpool is empty. If mocking is enabled then changes to + the userpool will be destroyed when the application exists. If mocking + is not disabled then destroy() must be called to remove the temporary + user data from AWS Cognito. It is recommended to use the context manager + to avoid accidentally persisting development data on AWS. + """ + + def __init__(self, cognito_client): + self.cognito_client = cognito_client + self.tmp_userpool_id = None + self.tmp_client_id = None + self.tmp_client_secret = None + + def create(self): + unique_poolname = f"TestUserPool{str(uuid.uuid4())}" + mock_pool_resp = self.cognito_client.create_user_pool( + PoolName=unique_poolname, + UsernameAttributes=['email']) + mock_pool_id = mock_pool_resp['UserPool']['Id'] + + client_response = self.cognito_client.create_user_pool_client( + UserPoolId=mock_pool_id, + ClientName="MockUserPoolClient", + GenerateSecret=True, + ExplicitAuthFlows=[ + 'ALLOW_USER_PASSWORD_AUTH', # Enable USER_PASSWORD_AUTH flow + 'ALLOW_REFRESH_TOKEN_AUTH' # You can add other auth flows as needed + ]) + + self.tmp_userpool_id = mock_pool_id + self.tmp_client_id = client_response['UserPoolClient']['ClientId'] + self.tmp_client_secret = client_response['UserPoolClient']['ClientSecret'] + + self.cognito_client.create_group(GroupName='Admins', + UserPoolId=mock_pool_id) + self.cognito_client.create_group(GroupName='Hosts', + UserPoolId=mock_pool_id) + self.cognito_client.create_group(GroupName='Guests', + UserPoolId=mock_pool_id) + self.cognito_client.create_group(GroupName='Coordinators', + UserPoolId=mock_pool_id) + + def destroy(self): + self.cognito_client.delete_user_pool_client( + UserPoolId=self.tmp_userpool_id, + ClientId=self.tmp_client_id) + self.cognito_client.delete_user_pool(UserPoolId=self.tmp_userpool_id) + self.tmp_userpool_id = None + self.tmp_client_id = None + self.tmp_client_secret = None + + def __enter__(self): + self.create() + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.destroy() + return self + + +# class AWSMockService(): +# """Start and stop AWS Cognito mocking using moto. + +# The mocking service will stop when the context exits. The +# mocked AWS Cognito requests will not persist outside of the context. +# """ + +# TEST_USERS = [{ +# "email": "test@test.com", +# "password": "Test!123" +# }, { +# "email": "testhost@test.com", +# "password": "Test!123" +# }, { +# "email": "testcoordinator@test.com", +# "password": "Test!123" +# }] + +# def __init__(self, flask_app): +# from moto import mock_cognitoidp +# self.userpool = AWSTemporaryUserpool(flask_app) +# self.mock_service = mock_cognitoidp() +# self.app = flask_app +# self.app.after_request(self.auto_signup_user_after_request) +# self.app.before_request(self.create_test_users) +# self.test_users_created = False + +# def create_test_users(self): +# """Create a set of test users before the first request is made.""" +# if self.test_users_created == True: +# return + +# for user in AWSMockService.TEST_USERS: +# email = user["email"] + +# try: +# remove_user({"email": email}) +# except AuthError: +# # This error is expected if the local database +# # Does not have the test user yet. We can ignore it. +# pass + +# try: +# signUpAdmin({ +# "email": email, +# "password": user["password"], +# "firstName": "testuser_firstname", +# "lastName": "testuser_lastname" +# }) +# self._auto_signup_user(email) +# self.app.logger.info(f"Created test user: {email}") +# except AuthError as e: +# self.app.logger.warning( +# f"Failed to create test user: {email}: {e.error}") + +# self.test_users_created = True + +# def add_aws_userpool_user(self, email, password, attributes=None): +# """Adds a new user to the temporary user pool. + +# Adds the user with the given username, password, and attributes. +# Attributes should be a list of dictionaries, each containing a 'Name' and 'Value' key. +# """ +# if attributes is None: +# attributes = [] + +# try: +# response = self.app.boto_client.admin_create_user( +# UserPoolId=self.app.config["COGNITO_USER_POOL_ID"], +# Username=email, +# TemporaryPassword=password, +# UserAttributes=attributes, +# MessageAction='SUPPRESS') +# self._auto_signup_user(email) +# self.app.logger.info( +# f"Added user {email} to the temporary user pool") +# return response +# except Exception as e: +# self.app.logger.error(f"Failed to add user {email}: {str(e)}") +# raise + +# def _auto_signup_user(self, email) -> bool: +# """Auto-confirm a new user. + +# Return True if successful and false otherwise. +# """ +# confirm_response = self.app.boto_client.admin_confirm_sign_up( +# UserPoolId=self.app.config["COGNITO_USER_POOL_ID"], Username=email) +# if confirm_response['ResponseMetadata']['HTTPStatusCode'] == 200: +# self.app.logger.info(f"Auto-confirmed new user: {email}") +# return True +# else: +# self.app.logger.warning( +# f"Failed to auto-confirm new user: {email}") +# return False + +# def auto_signup_user_after_request(self, response): +# """Automatically verify new users by listening for signup. + +# Confirms the user if the signup was successful. +# """ +# # The alternative approaches are to use a lambda pre-signup +# # trigger to automatically verify new users, or to include +# # conditional login within our endpoint. The lambda approach +# # requires more overhead, and conditional logic within the endpoint +# # risks adding a bug to the production code. +# if request.endpoint and ('signup' in request.endpoint.lower() +# ) and 200 <= response.status_code < 300: +# email = request.json['email'] +# if self._auto_signup_user(email): +# new_response = response.get_json() +# new_response['UserConfirmed'] = True +# response.data = json.dumps(new_response) +# return response + +# def start(self): +# self.mock_service.start() +# self.app._boto_client = None +# self.app.config["COGNITO_REGION"] = "us-east-1" +# self.app.config["COGNITO_ACCESS_ID"] = self.mock_service.FAKE_KEYS[ +# 'AWS_ACCESS_KEY_ID'] +# self.app.config["COGNITO_ACCESS_KEY"] = self.mock_service.FAKE_KEYS[ +# 'AWS_SECRET_ACCESS_KEY'] +# self.userpool.create() + +# self.app.logger.info("Started mock AWS Cognito service") + +# def stop(self): +# self.userpool.destroy() +# self.mock_service.stop() +# self.app.config["COGNITO_REGION"] = None +# self.app.config["COGNITO_ACCESS_ID"] = None +# self.app.config["COGNITO_ACCESS_KEY"] = None +# self.app._boto_client = None + +# self.app.logger.info("Stopped mock AWS Cognito service") + +# def __enter__(self): +# self.start() +# return self + +# def __exit__(self, exc_type, exc_value, traceback): +# self.stop() +# return self diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py new file mode 100644 index 00000000..268f6133 --- /dev/null +++ b/backend/tests/conftest.py @@ -0,0 +1,83 @@ +import os + +import pytest + +from fastapi.testclient import TestClient + +import boto3 +from moto import mock_aws + +import sqlalchemy as sa +from sqlalchemy.orm import sessionmaker, Session +from sqlalchemy.pool import StaticPool + +from app.main import app as main_app +from app.core.db import Base +from app.core.config import get_settings +from app.modules.deps import db_session, get_cognito_client + +import tests.cognito_setup as cognito_setup + + +@pytest.fixture +def session_factory() -> Session: + SQLALCHEMY_DATABASE_URL = "sqlite+pysqlite:///:memory:" + + engine = sa.create_engine( + SQLALCHEMY_DATABASE_URL, + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + TestingSessionLocal = sessionmaker(autocommit=False, + autoflush=False, + bind=engine) + + import app.seed + Base.metadata.create_all(bind=engine) + + return TestingSessionLocal + + +@pytest.fixture(scope="module") +def api_settings(): + """Configure test settings.""" + os.environ["COGNITO_ACCESS_ID"] = "testing" + os.environ["COGNITO_ACCESS_KEY"] = "testing" + os.environ["COGNITO_CLIENT_ID"] = "testing" + os.environ["COGNITO_CLIENT_SECRET"] = "testing" + os.environ["COGNITO_REGION"] = "us-east-1" + os.environ["COGNITO_REDIRECT_URI"] = "testing" + os.environ["COGNITO_USER_POOL_ID"] = "testing" + os.environ["ROOT_URL"] = "testing" + os.environ["DATABASE_URL"] = "testing" + return get_settings() + + +@pytest.fixture(scope="module") +def cognito_client(api_settings): + """Return a mocked Cognito IDP client.""" + with mock_aws(): + client = boto3.client("cognito-idp", + region_name=api_settings.COGNITO_REGION) + with cognito_setup.AWSTemporaryUserpool(client) as temp_pool: + api_settings.COGNITO_USER_POOL_ID = temp_pool.tmp_userpool_id + api_settings.COGNITO_CLIENT_ID = temp_pool.tmp_client_id + api_settings.COGNITO_CLIENT_SECRET = temp_pool.tmp_client_secret + + yield client + + +@pytest.fixture +def client(session_factory) -> TestClient: + + def override_db_session(): + try: + session = session_factory() + yield session + finally: + session.close() + + main_app.dependency_overrides[db_session] = override_db_session + main_app.dependency_overrides[get_cognito_client] = lambda: None + + return TestClient(main_app) diff --git a/backend/tests/e2e/empty.py b/backend/tests/e2e/empty.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/integration/test_authentication.py b/backend/tests/integration/test_authentication.py new file mode 100644 index 00000000..5f5d5ae8 --- /dev/null +++ b/backend/tests/integration/test_authentication.py @@ -0,0 +1,376 @@ +import string +import secrets +import pytest + +from fastapi.testclient import TestClient + +from app.modules.deps import get_cognito_client +from app.modules.access.models import User + +PATH = "/api/auth" +secretsGenerator = secrets.SystemRandom() + + +@pytest.fixture +def client(client, cognito_client) -> TestClient: + client.app.dependency_overrides[ + get_cognito_client] = lambda: cognito_client + + return client + + +def signup_user(client: TestClient, + email: str, + password: str, + first_name: str = None, + middle_name: str = None, + last_name: str = None) -> None: + if not first_name: + first_name = "firstName" + if not last_name: + last_name = "lastName" + if not middle_name: + middle_name = "" + + signup_response = client.post(PATH + '/signup', + json={ + 'email': email, + 'password': password, + 'firstName': first_name, + 'middleName': middle_name, + 'lastName': last_name, + 'role': 'host', + }) + assert signup_response.status_code != 400, signup_response.text + assert signup_response.status_code == 200, signup_response.text + + +def confirm_user(cognito_client, api_settings, email: str) -> None: + confirm_response = cognito_client.admin_confirm_sign_up( + UserPoolId=api_settings.COGNITO_USER_POOL_ID, Username=email) + assert confirm_response['ResponseMetadata'][ + 'HTTPStatusCode'] == 200, confirm_response + + +def create_user(client: TestClient, + api_settings, + cognito_client, + email: str, + password: str, + firstName: str = None, + middleName: str = None, + lastName: str = None) -> None: + """Sign-up and confirm a new user. + + Fail the test if the signup or confirm operation fails. + """ + signup_user(client, email, password, firstName, middleName, lastName) + confirm_user(cognito_client, api_settings, email) + + +def signin_user(client: TestClient, email: str, password: str) -> str: + """ + Signin a user and return the JWT. Fail the test if the + signin operation fails. + """ + response = client.post(PATH + '/signin', + json={ + 'email': email, + 'password': password + }) + assert response.status_code == 200, "Signin failed" + assert "token" in response.json(), "Signin succeeded but no token provided" + return response.json()['token'] + + +def create_and_signin_user(client: TestClient, api_settings, cognito_client, + email: str, password: str) -> (str, str): + """ + Signup, confirm, and signin a new user. Return the JWT. + Fail the test if the signup, confirm, or signin operation fails. + """ + create_user(client, api_settings, cognito_client, email, password) + return signin_user(client, email, password) + + +def strip_punctuation(text): + return text.translate(str.maketrans("", "", string.punctuation)) + + +def test_signin_with_fake_credentials(client): + response = client.post(PATH + '/signin', + json={ + 'email': 'mdndndkde@email.com', + 'password': '_pp#FXo;h$i~' + }) + + body = response.json() + assert response.status_code == 400, body + assert body["detail"]["code"] == "UserNotFoundException", body + + +def test_signin_without_email_format(client): + """Test login using a malformed email.""" + response = client.post(PATH + '/signin', + json={ + 'email': 'not_an_email', + 'password': '_pp#FXo;h$i~' + }) + + assert response.status_code == 422 + assert "not a valid email address" in response.text, response + + +def test_signup_with_missing_fields(client): + """Test attempts to sign-up without all required fields.""" + BAD_SIGNUP_REQUESTS = [{ + 'email': 'inbox928@placeholder.org', + 'password': 'Fakepass%^&7!asdf' + }, { + 'email': 'inbox928@placeholder.org', + 'password': 'Fakepass%^&7!asdf', + 'lastName': 'test' + }, { + 'email': 'inbox928@placeholder.org', + 'firstName': 'test', + 'lastName': 'test' + }, { + 'password': 'Fakepass%^&7!asdf', + 'firstName': 'test', + 'lastName': 'test' + }, { + 'password': 'Fakepass%^&7!asdf', + 'role': 'guest' + }, {}] + + for req in BAD_SIGNUP_REQUESTS: + response = client.post(PATH + '/signup', json=req) + assert response.status_code == 422, req + assert 'Field required' in response.text, req + + +def test_refresh_without_cookie(client): + """Test attempts to use the refresh endpoint without a session cookie.""" + response = client.get(PATH + '/refresh') + assert response.status_code == 401 + assert "Missing refresh token" in response.json()['detail'] + + +def test_session_without_cookie(client): + """Test attempt to use the session endpoint without a session cookie. + """ + response = client.get( + PATH + '/session', + headers={"Authorization": "Bearer fake_jwt_token_here"}) + assert response.status_code == 401 + assert "Missing session cookies" in response.json()['detail'] + + +def test_incorrect_JWT_fail_auth(client): + """Test attempts to use an incorrect JWT with the user endpoint.""" + response = client.get( + '/api/user', headers={"Authorization": "Bearer fake_jwt_token_here"}) + assert response.status_code == 401 + assert "Missing id token" in response.json()['detail'] + + +def _signup_unconfirmed(signup_endpoint, role, client, expect_user_confirmed): + email = f'{secretsGenerator.randint(1_000, 2_000)}@email.com' + password = 'Fakepass%^&7!asdf' + signup_response = client.post(signup_endpoint, + json={ + 'email': email, + 'password': password, + "firstName": "valid name", + "role": role, + }) + + assert signup_response.status_code == 200, signup_response.text + assert signup_response.json( + )["UserConfirmed"] == expect_user_confirmed, signup_response.text + + signin_response = client.post(PATH + '/signin', + json={ + 'email': email, + 'password': password + }) + + if expect_user_confirmed: + assert signin_response.status_code == 200, signin_response.text + assert "token" in signin_response.json(), signin_response.text + else: + assert signin_response.status_code == 400, signin_response.text + assert signin_response.json()["detail"][ + "code"] == "UserNotConfirmedException", signin_response.text + + +def test_signup_unconfirmed_host(client): + """ + Use the host signup endpoint to + test that unconfirmed accounts cannot be used to login to the API. + Mocked users are automatically confirmed. + """ + _signup_unconfirmed(PATH + "/signup", "host", client, False) + + +def test_signup_unconfirmed_coordinator(client): + """ + Use the coordinator signup endpoint to + test that unconfirmed accounts cannot be used to login to the API. + Mocked users are automatically confirmed. + """ + _signup_unconfirmed(PATH + "/signup", "coordinator", client, False) + + +def test_signup_confirmed(client, api_settings, cognito_client): + """Test that confirmed accounts can be used to login to the API.""" + email = f'{secretsGenerator.randint(1_000, 2_000)}@email.com' + password = 'Fakepass%^&7!asdf' + create_user(client, api_settings, cognito_client, email, password) + + signin_response = client.post(PATH + '/signin', + json={ + 'email': email, + 'password': password + }) + assert signin_response.status_code == 200, "Signup attempt failed" + assert "token" in signin_response.json( + ), "Signin succeeded but no token provided" + assert len(signin_response.json()["token"]) > 0 + + +def test_weak_passwords_rejected(client): + """Test that attempting to sign-up a new user with a password + that does not meet AWS Cognito password complexity requirements + returns a valid error. + """ + email = f'{secretsGenerator.randint(1_000, 2_000)}@email.com' + password = 'weakpa55' + signup_response = client.post(PATH + '/signup', + json={ + 'email': email, + 'password': password, + 'firstName': 'unqiue', + 'lastName': 'name', + 'role': 'host' + }) + + assert signup_response.status_code == 400, "The weak password worked for signup!" + assert "Failed to create user" in signup_response.text, signup_response.text + + +def test_basic_auth_flow(client, api_settings, cognito_client): + """Create a new user, confirm it, and login using the + /signin endpoint, and use the returned JWT to access + a protected endpoint. + """ + EMAIL = f'{secretsGenerator.randint(1_000, 2_000)}@email.com' + PASSWORD = 'Fake4!@#$2589FFF' + FIRST_NAME = "test" + LAST_NAME = "test" + create_user(client, + api_settings, + cognito_client, + EMAIL, + PASSWORD, + firstName=FIRST_NAME, + lastName=LAST_NAME) + + response = client.post(PATH + '/signin', + json={ + 'email': EMAIL, + 'password': PASSWORD + }) + + assert response.status_code == 200, "Signin failed" + assert 'token' in response.json(), 'Signin succeeded but token field missing from response' + jwt = response.json()['token'] + assert jwt is not None, 'Signin succeeded but returned empty jwt' + assert len(jwt) > 0 + + response = client.get('/api/user', + headers={"Authorization": f"Bearer {jwt}"}) + + assert response.status_code == 200, '/user authentication failed' + assert response.json()['email'] == EMAIL + assert response.json()['firstName'] == FIRST_NAME + assert response.json()['middleName'] == '' + assert response.json()['lastName'] == LAST_NAME + + +def test_signin_returns_refresh_token(client, api_settings, cognito_client): + """Test that the /signin endpoint returns a session cookie. + + The session cookie stores the refresh token. + """ + EMAIL = f'{secretsGenerator.randint(1_000, 2_000)}@email.com' + PASSWORD = 'Fake4!@#$2589FFF' + create_user(client, api_settings, cognito_client, EMAIL, PASSWORD) + response = client.post(PATH + '/signin', + json={ + 'email': EMAIL, + 'password': PASSWORD + }) + + assert response.status_code == 200, "Signin failed" + all_cookies = response.cookies + assert all_cookies.get("refresh_token"), "Session cookie is empty" + + +def test_refresh_endpoint(client, api_settings, cognito_client): + """Test refreshing a JWT using the /refresh endpoint.""" + EMAIL = f'{secretsGenerator.randint(1_000, 2_000)}@email.com' + PASSWORD = 'Fake4!@#$2589FFF' + create_and_signin_user(client, api_settings, cognito_client, EMAIL, + PASSWORD) + + # The test_client automatically attaches the session cookie to the request + # The session cookie stores the refresh token. + response = client.get(PATH + '/refresh', ) + + assert response.status_code == 200, response.text + assert 'token' in response.json(), response.text + + +def test_session_endpoint(client, api_settings, cognito_client): + """Test refreshing a JWT using the /session endpoint.""" + EMAIL = f'{secretsGenerator.randint(1_000, 2_000)}@email.com' + PASSWORD = 'Fake4!@#$2589FFF' + jwt = create_and_signin_user(client, api_settings, cognito_client, EMAIL, + PASSWORD) + + # The test_client automatically attaches the session cookie to the request + # The session cookie stores the refresh token. + response = client.get(PATH + '/session', + headers={"Authorization": f"Bearer {jwt}"}) + + assert response.status_code == 200, response.text + assert 'token' in response.json(), response.text + + +def test_user_signup_rollback(client, api_settings, cognito_client, session_factory): + """Test that a failed sign-up with Cognito. + + Ensure the local DB entry of the user's email is deleted.""" + + rollback_email = 'test_user_signup_rollback@fake.com' + signup_response = client.post(PATH + '/signup', + json={ + 'email': rollback_email, + 'password': 'lol', + 'firstName': 'firstname', + 'lastName': 'lastname', + 'role': 'host', + }) + + assert signup_response.status_code == 400 + + with pytest.raises(cognito_client.exceptions.UserNotFoundException): + cognito_client.admin_delete_user( + UserPoolId=api_settings.COGNITO_USER_POOL_ID, + Username=rollback_email) + + with session_factory() as sess: + rolledback_user = sess.query(User).filter_by( + email=rollback_email).first() + assert rolledback_user is None diff --git a/backend/tests/integration/test_forms.py b/backend/tests/integration/test_forms.py new file mode 100644 index 00000000..0a9113c3 --- /dev/null +++ b/backend/tests/integration/test_forms.py @@ -0,0 +1,134 @@ +from types import MappingProxyType + +from app.repositories.forms import FormsRepository +from app.repositories.user_repo import UserRepository, UserRole + +TEST_FORM_READ_ONLY = MappingProxyType({ + "title": + "Employee Onboarding", + "description": + "Collect necessary employee data.", + "field_groups": [{ + "title": + "Personal Details", + "description": + "Please enter your personal details.", + "fields": [{ + "ref": "position", + "properties": { + "description": "Position in the company", + "field_type": "dropdown", + "choices": ['Manager', 'Developer', 'Designer'], + }, + "validations": { + "required": True, + "max_length": 12 + } + }, { + "ref": "service_length", + "properties": { + "description": "Years in the company", + "field_type": "number", + "choices": None, + }, + "validations": { + "required": False, + "max_length": None + } + }] + }, { + "title": + "Second Group", + "description": + "A second field group.", + "fields": [{ + "ref": "start date", + "properties": { + "description": "Start date", + "field_type": "date", + "choices": "11-22-2005", + }, + "validations": { + "required": True, + "max_length": 12 + } + }] + }] +}) + + +def assert_form_equal(actual_form: dict, expected_form: dict): + """ + Do a deep equality check of a form, excluding dynamically + assigned values like timestamps and primary key ids. + """ + actual_copy = actual_form.copy() + del actual_copy['created_at'] + for group in actual_copy['field_groups']: + del group['form'] + for field in group['fields']: + del field['field_id'] + del field['group'] + + assert actual_copy == expected_form + + +def test_add_form_valid_json(empty_db_session_provider): + form_json = dict(TEST_FORM_READ_ONLY) + + form_repo = FormsRepository(empty_db_session_provider.session()) + created_form_id = form_repo.add_form(form_json) + retrieved_form = form_repo.get_form_json(created_form_id) + + assert_form_equal(retrieved_form, form_json) + + +def test_add_get_responses(empty_db_session_provider): + with empty_db_session_provider.session() as session: + user_repo = UserRepository(session) + form_repo = FormsRepository(session) + + user_repo.add_user('fake@email.com', UserRole.COORDINATOR, 'firstname') + user_id = user_repo.get_user_id('fake@email.com') + created_form_id = form_repo.add_form(TEST_FORM_READ_ONLY) + retrieved_form = form_repo.get_form_json(created_form_id) + + def _get_field_id(lcl_form, ref): + for group in lcl_form['field_groups']: + for field in group['fields']: + if field['ref'] == ref: + return int(field['field_id']) + raise ValueError(f'ref {ref} not found in test form') + + expected_responses = [{ + "user_id": + user_id, + "field_id": + _get_field_id(retrieved_form, 'position'), + "answer_text": + "Designer" + }, { + "user_id": + user_id, + "field_id": + _get_field_id(retrieved_form, 'service_length'), + "answer_text": + "5" + }, { + "user_id": + user_id, + "field_id": + _get_field_id(retrieved_form, 'start date'), + "answer_text": + '2024-05-19' + }] + form_repo.add_user_responses(user_id, expected_responses) + + retrieved_answers = form_repo.get_user_responses( + user_id, created_form_id) + + assert len(retrieved_answers) == 3 + for expected, actual in zip(expected_responses, retrieved_answers): + assert expected['answer_text'] == actual['answer_text'] + assert expected['user_id'] == actual['user']['id'] + assert expected['field_id'] == actual['field']['field_id'] diff --git a/api/tests/test_forms_schema.py b/backend/tests/integration/test_forms_schema.py similarity index 100% rename from api/tests/test_forms_schema.py rename to backend/tests/integration/test_forms_schema.py diff --git a/api/tests/test_host_controller.py b/backend/tests/integration/test_host_controller.py similarity index 100% rename from api/tests/test_host_controller.py rename to backend/tests/integration/test_host_controller.py diff --git a/backend/tests/integration/test_housing_orgs_controller.py b/backend/tests/integration/test_housing_orgs_controller.py new file mode 100644 index 00000000..c568fb72 --- /dev/null +++ b/backend/tests/integration/test_housing_orgs_controller.py @@ -0,0 +1,240 @@ +from fastapi.testclient import TestClient + +PATH = "/api/housing-orgs" + + +def populate_test_database(client: TestClient, num_entries: int) -> list[int]: + """Add the given number of entries to the database. + + Add num_entries rows to the test database and return the + created ids. fail test if any of the creation requests + fails. + + note: orgs are created using sqlalchemy commands, + not api requests. + """ + ids = [] + for i in range(num_entries): + REQUESTED_ORG = {"org_name": f"org no {i}"} + response = client.post(PATH, json=REQUESTED_ORG) + assert response.status_code == 201, "Could not create housing org." + org = response.json() + assert org is not None, ( + f"test setup failure. failed to create org no {i}." + "cannot perform endpoint test!") + assert "housing_org_id" in org + ids.append(org["housing_org_id"]) + return ids + + +def test_create_housing_org(client): + """Test create a new housing org.""" + requested_org = {"org_name": "-123ASCII&"} + + # POST + response = client.post(PATH, json=requested_org) + assert response.status_code == 201 + response_obj = response.json() + assert response_obj["org_name"] == requested_org["org_name"] + + # GET + response = client.get(f"{PATH}/{response_obj['housing_org_id']}") + assert response.status_code == 200 + response_obj = response.json() + assert response_obj["housing_org_id"] == 1 + assert response_obj["org_name"] == requested_org["org_name"] + + +def test_create_duplicate_housing_org_redirects(client): + """Test create a duplicate housing org redirects.""" + requested_org = {"org_name": "-123ASCII&"} + + # POST 1 of 2 + response = client.post(PATH, json=requested_org) + assert response.status_code == 201 + response_obj = response.json() + assert response_obj["housing_org_id"] is not None + assert response_obj["org_name"] == requested_org["org_name"] + + org_id = response_obj["housing_org_id"] + + # POST 2 of 2 should redirect instead of creating a new one + # Explicitly turn on following redirects to get a HTTP 200. + # The wrong status code (307) was being returned when setting + # follow_redirect to False. At the time of this writting, it + # seems that something changed the controller's RedirectResponse + # status code. + response = client.post(PATH, follow_redirects=True, json=requested_org) + assert response.status_code == 200, "Should have redirected to existing resource." + response_obj = response.json() + assert response_obj["housing_org_id"] is not None + assert response_obj["org_name"] == requested_org["org_name"] + + +def test_create_with_extra_data(client): + """Test that sending an create POST request with extra + json entries in the body does not disrupt the update. + + We should safely ignore additional fields. + """ + create_request = { + "org_name": "A new org", + "extra_int": 1, + "extra_bool": True, + "extra_string": "I'm notta name" + } + + response = client.post(PATH, json=create_request) + response_body = response.json() + + assert response.status_code == 201 + assert "org_name" in response_body + assert "housing_org_id" in response_body + assert response_body["org_name"] == create_request["org_name"] + assert "extra_int" not in response_body, "We should not send back request json extra fields" + assert "extra_bool" not in response_body, "We should not send back request json extra fields" + assert "extra_string" not in response_body, "We should not send back request json extra fields" + + response = client.get(f"{PATH}/{response_body['housing_org_id']}") + assert response.status_code == 200, "POST succeeded but the housing org doesn't exist." + assert response_body["org_name"] == create_request["org_name"] + + +def test_create_bad_json_invalid_type(client): + bad_create_request = {"org_name": 1} + response = client.post(PATH, json=bad_create_request) + + assert response.status_code == 422 + + +def test_create_bad_json_missing_name(client): + bad_create_request = {"org_namez": 1} + response = client.post(PATH, json=bad_create_request) + + assert response.status_code == 422 + + +def test_delete_housing_org(client: TestClient): + """ + Test deleting a housing org that we know exists, + using a delete request. + """ + ids = populate_test_database(client=client, num_entries=1) + path = f"{PATH}/{ids[0]}" + response = client.delete(path) + assert response.status_code == 204 + + response = client.get(path) + assert response.status_code == 404, "Housing org was not deleted." + + +def test_delete_nonexistant_org(client: TestClient): + """ + Test that deleting a nonexistant org responds with the + correct status code and does not modify the db. + """ + NUM_ROWS = 4 + populate_test_database(client=client, num_entries=NUM_ROWS) + + response = client.get(PATH) + response_body = response.json() + assert response.status_code == 200, "Housing orgs endpoint failure." + assert len(response_body) == NUM_ROWS + + response = client.delete(f"{PATH}/{999}") + assert response.status_code == 404 + + response = client.get(PATH) + response_body = response.json() + assert response.status_code == 200, "Housing orgs endpoint failure." + assert len(response_body) == NUM_ROWS + + +def test_get_nonexistent_org(client: TestClient): + populate_test_database(client=client, num_entries=8) + response = client.get(f"{PATH}/999") + response_body = response.json() + + assert response.status_code == 404 + assert "org_name" not in response_body + + +def test_get_housing_orgs(client: TestClient): + """Test case for get_housing_orgs + + Get a list of housing orgs. + """ + expected_org_count = 12 + populate_test_database(client=client, num_entries=expected_org_count) + + response = client.get(PATH) + response_body = response.json() + + assert response.status_code == 200 + assert len(response_body) == expected_org_count + + +def test_get_housing_org_empty_db(client): + response = client.get(PATH) + response_body = response.json() + + assert response.status_code == 200 + assert len(response_body) == 0 + + +def test_put_update_housing_org(client: TestClient): + """Test case for update_housing_org + + Update a housing org + """ + ids = populate_test_database(client=client, num_entries=1) + updated_org = {"org_name": "Rebranded Org~~~"} + + response = client.put(f"{PATH}/{ids[0]}", json=updated_org) + + assert response.status_code == 200 + + response_obj = response.json() + assert response_obj["org_name"] == updated_org["org_name"] + assert response_obj["housing_org_id"] == ids[0] + + +def test_put_create_housing_org_no_id(client: TestClient): + put_body = {"org_name": "New Housing Org Name"} + response = client.put(f"{PATH}/999", json=put_body) + assert response.status_code == 201 + + +def test_put_create_housing_org_mismatch_id(client: TestClient): + failed_put_body = {"housing_org_id": 1, "org_name": "New Housing Org Name"} + response = client.put(f"{PATH}/{999}", json=failed_put_body) + assert response.status_code == 409 + + +def test_put_with_extra_data(client: TestClient): + """ + Test that sending an update PUT request with extra + json entries in the body does not disrupt the update. + + We should safely ignore additional fields. + """ + ids = populate_test_database(client=client, num_entries=1) + update_request = { + "org_name": "A brand new name", + "extra_int": 1, + "extra_bool": True, + "extra_string": "I'm notta name" + } + response = client.put(f"{PATH}/{ids[0]}", json=update_request) + response_body = response.json() + + assert response.status_code == 200 + + assert "org_name" in response_body + assert "housing_org_id" in response_body + assert "extra_int" not in response_body, "We should not send back request json extra fields" + assert "extra_bool" not in response_body, "We should not send back request json extra fields" + assert "extra_string" not in response_body, "We should not send back request json extra fields" + + assert response_body["org_name"] == update_request["org_name"] + assert response_body["housing_org_id"] == ids[0] diff --git a/backend/tests/integration/test_user_repo.py b/backend/tests/integration/test_user_repo.py new file mode 100644 index 00000000..c1bd4b51 --- /dev/null +++ b/backend/tests/integration/test_user_repo.py @@ -0,0 +1,69 @@ +import pytest +from sqlalchemy.orm import Session +from sqlalchemy.exc import IntegrityError + +from app.modules.access.models import User +from app.modules.access.user_roles import UserRole +from app.modules.access.user_repo import UserRepository + +def test_user_role_required(session_factory: Session): + with session_factory() as empty_db_session: + new_user = User(email="realemail@fakedomain.com", + firstName="realemail@fakedomain.com", + middleName="realemail@fakedomain.com", + lastName="realemail@fakedomain.com") + empty_db_session.add(new_user) + with pytest.raises(IntegrityError, match="NOT NULL constraint failed"): + empty_db_session.commit() + + with pytest.raises(TypeError): + repo = UserRepository(empty_db_session) + repo.add_user(email="realemail@fakedomain.com", + firstName="realemail@fakedomain.com", + middleName="realemail@fakedomain.com", + lastName="realemail@fakedomain.com") + + +def test_add_user_firstname_only(session_factory: Session): + """Verify that user middle and last name are not required. + + In some cultures, such as Indonesian and Icelandic, people may have only one name. + """ + with session_factory() as empty_db_session: + repo = UserRepository(empty_db_session) + new_user = repo.add_user(email="realemail@fakedomain.com", + firstName="name", + role=UserRole.GUEST) + assert new_user.role.type == UserRole.GUEST.value + assert new_user.firstName == "name" + assert new_user.middleName == None + assert new_user.lastName == None + assert new_user.email == "realemail@fakedomain.com" + + +def test_single_char_name(session_factory: Session): + """Verify that user names can be just one character, per the + US Web Design System Guidance. + """ + with session_factory() as empty_db_session: + repo = UserRepository(empty_db_session) + new_user = repo.add_user(email="realemail@fakedomain.com", + firstName="n", + role=UserRole.GUEST) + assert new_user.role.type == UserRole.GUEST.value + assert new_user.firstName == "n" + assert new_user.middleName == None + assert new_user.lastName == None + assert new_user.email == "realemail@fakedomain.com" + + +def test_firstname_required(session_factory: Session): + """Test that the firstname must at least contain one non-space character.""" + with session_factory() as empty_db_session: + repo = UserRepository(empty_db_session) + with pytest.raises( + ValueError, + match="firstName must contain at least one non-space character"): + repo.add_user(email="realemail@fakedomain.com", + firstName=" ", + role=UserRole.GUEST) diff --git a/backend/tests/test_alembic_migration.py b/backend/tests/test_alembic_migration.py new file mode 100644 index 00000000..12f1baff --- /dev/null +++ b/backend/tests/test_alembic_migration.py @@ -0,0 +1,35 @@ +from app.user_roles import UserRole +from app.repositories.user_repo import UserRepository + +# Importing these tests will register them within our test project +# These tests do an excellent job of detecting errors in the alembic +# downgrade and upgrade scripts. +from pytest_alembic.tests import test_single_head_revision +from pytest_alembic.tests import test_upgrade +from pytest_alembic.tests import test_model_definitions_match_ddl +from pytest_alembic.tests import test_up_down_consistency + + +def test_db_session_version(empty_db_session): + """ + Test that the pytest in-memory database is at the most + up-to-date alembic migration version. This will ensure all + the require database objects and pre-populated fields will + be available. + """ + # Adding a new database revision will break this test case + + # Before updating to the new revision please add additional + # test cases below that check the integrity of your new migration + assert DataAccessLayer.revision_id() == 'cfc4e41b69d3' + + +def test_user_roles_available(empty_db_session): + """ + Test that all of the UserRole types are pre-populated within + the Role table after migrating the database to the HEAD revision. + """ + user_repo = UserRepository(empty_db_session) + for role in UserRole: + db_role = user_repo._get_role(role) + assert db_role.name == role.value diff --git a/backend/tests/unit/access/__init_.py b/backend/tests/unit/access/__init_.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/unit/intake_profile/__init__.py b/backend/tests/unit/intake_profile/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/unit/matching/__init__.py b/backend/tests/unit/matching/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/unit/onboarding/__init__.py b/backend/tests/unit/onboarding/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/unit/relationship_management/__init__.py b/backend/tests/unit/relationship_management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tests/unit/tenant_housing_provider/__init__.py b/backend/tests/unit/tenant_housing_provider/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/tox.ini b/backend/tox.ini new file mode 100644 index 00000000..f45b9c48 --- /dev/null +++ b/backend/tox.ini @@ -0,0 +1,20 @@ +[tox] +env_list = + py312 +minversion = 4.6.4 + +[testenv] +description = run tests with mocking using pytest + +skip_install = true +allowlist_externals = poetry +commands_pre = + poetry install +commands = + poetry run pytest {tty:--color=yes} {posargs:tests} --cov=app #--mode=debug + +[testenv:releasetest] +description = run tests without mocking using pytest +passenv = COGNITO_REGION,COGNITO_ACCESS_ID,COGNITO_ACCESS_KEY +commands = + poetry run pytest {tty:--color=yes} {posargs} --cov=openapi_server --mode=release \ No newline at end of file diff --git a/api/.devcontainer/Dockerfile b/flask-api/.devcontainer/Dockerfile similarity index 100% rename from api/.devcontainer/Dockerfile rename to flask-api/.devcontainer/Dockerfile diff --git a/api/.devcontainer/devcontainer.json b/flask-api/.devcontainer/devcontainer.json similarity index 100% rename from api/.devcontainer/devcontainer.json rename to flask-api/.devcontainer/devcontainer.json diff --git a/api/.dockerignore b/flask-api/.dockerignore similarity index 100% rename from api/.dockerignore rename to flask-api/.dockerignore diff --git a/api/.env.dev.example b/flask-api/.env.dev.example similarity index 100% rename from api/.env.dev.example rename to flask-api/.env.dev.example diff --git a/api/.env.prod.example b/flask-api/.env.prod.example similarity index 100% rename from api/.env.prod.example rename to flask-api/.env.prod.example diff --git a/api/.env.staging.example b/flask-api/.env.staging.example similarity index 100% rename from api/.env.staging.example rename to flask-api/.env.staging.example diff --git a/api/.gitignore b/flask-api/.gitignore similarity index 100% rename from api/.gitignore rename to flask-api/.gitignore diff --git a/api/.openapi-generator-ignore b/flask-api/.openapi-generator-ignore similarity index 100% rename from api/.openapi-generator-ignore rename to flask-api/.openapi-generator-ignore diff --git a/api/.openapi-generator/FILES b/flask-api/.openapi-generator/FILES similarity index 100% rename from api/.openapi-generator/FILES rename to flask-api/.openapi-generator/FILES diff --git a/api/.openapi-generator/VERSION b/flask-api/.openapi-generator/VERSION similarity index 100% rename from api/.openapi-generator/VERSION rename to flask-api/.openapi-generator/VERSION diff --git a/api/.travis.yml b/flask-api/.travis.yml similarity index 100% rename from api/.travis.yml rename to flask-api/.travis.yml diff --git a/api/Dockerfile b/flask-api/Dockerfile similarity index 100% rename from api/Dockerfile rename to flask-api/Dockerfile diff --git a/api/README.md b/flask-api/README.md similarity index 100% rename from api/README.md rename to flask-api/README.md diff --git a/flask-api/alembic.ini b/flask-api/alembic.ini new file mode 100644 index 00000000..ef70e5f2 --- /dev/null +++ b/flask-api/alembic.ini @@ -0,0 +1,110 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# 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 +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +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() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/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" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# 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 # 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 +# output_encoding = utf-8 + +sqlalchemy.url = sqlite:///./homeuniteus.db + + +[post_write_hooks] +# post_write_hooks 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 +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/api/alembic/env.py b/flask-api/alembic/env.py similarity index 100% rename from api/alembic/env.py rename to flask-api/alembic/env.py diff --git a/flask-api/alembic/script.py.mako b/flask-api/alembic/script.py.mako new file mode 100644 index 00000000..55df2863 --- /dev/null +++ b/flask-api/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/flask-api/alembic/versions/3ceec084158f_.py b/flask-api/alembic/versions/3ceec084158f_.py new file mode 100644 index 00000000..b5518d1b --- /dev/null +++ b/flask-api/alembic/versions/3ceec084158f_.py @@ -0,0 +1,367 @@ +"""empty message + +Revision ID: 3ceec084158f +Revises: +Create Date: 2023-03-13 16:58:30.782837 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.engine.reflection import Inspector + +# revision identifiers, used by Alembic. +revision = '3ceec084158f' +down_revision = None +branch_labels = None +depends_on = None + +def create_missing_table(name: str, *create_args) -> bool: + "Create the table if it is not already present in the database." + conn = op.get_bind() + inspector = Inspector.from_engine(conn) + if name not in inspector.get_table_names(): + op.create_table(name, *create_args) + return True + return False + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + if create_missing_table('applicant_type', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('applicant_type_description', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id')): + op.create_index(op.f('ix_applicant_type_id'), 'applicant_type', ['id'], unique=False) + + if create_missing_table('case_status', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('status_description', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_case_status_id'), 'case_status', ['id'], unique=False) + + if create_missing_table('guest_group', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('group_name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_guest_group_id'), 'guest_group', ['id'], unique=False) + + if create_missing_table('host', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_host_id'), 'host', ['id'], unique=False) + + if create_missing_table('host_household', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('household_name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_host_household_id'), 'host_household', ['id'], unique=False) + + if create_missing_table('housing_program_service_provider', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('provider_name', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_housing_program_service_provider_id'), 'housing_program_service_provider', ['id'], unique=False) + + if create_missing_table('image_tag_type', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('tag_text', sa.String(), nullable=False), + sa.Column('tag_description', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_image_tag_type_id'), 'image_tag_type', ['id'], unique=False) + + if create_missing_table('intake_question_type', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('type_description', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_intake_question_type_id'), 'intake_question_type', ['id'], unique=False) + + if create_missing_table('match_status', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('status_description', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_match_status_id'), 'match_status', ['id'], unique=False) + + if create_missing_table('user', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('email', sa.String(), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email') + ): + op.create_index(op.f('ix_user_id'), 'user', ['id'], unique=False) + + if create_missing_table('applicant_status', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('applicant_type', sa.Integer(), nullable=False), + sa.Column('status_description', sa.String(), nullable=False), + sa.ForeignKeyConstraint(['applicant_type'], ['applicant_type.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_applicant_status_id'), 'applicant_status', ['id'], unique=False) + + if create_missing_table('group_match_result', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('guest_group', sa.Integer(), nullable=False), + sa.Column('host_household', sa.Integer(), nullable=False), + sa.Column('match_status', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['guest_group'], ['guest_group.id'], ), + sa.ForeignKeyConstraint(['host_household'], ['host_household.id'], ), + sa.ForeignKeyConstraint(['match_status'], ['match_status.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_group_match_result_id'), 'group_match_result', ['id'], unique=False) + + if create_missing_table('housing_program', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('program_name', sa.String(), nullable=False), + sa.Column('service_provider', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['service_provider'], ['housing_program_service_provider.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_housing_program_id'), 'housing_program', ['id'], unique=False) + + if create_missing_table('applicant', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('applicant_type', sa.Integer(), nullable=False), + sa.Column('applicant_status', sa.Integer(), nullable=False), + sa.Column('user', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['applicant_status'], ['applicant_status.id'], ), + sa.ForeignKeyConstraint(['applicant_type'], ['applicant_type.id'], ), + sa.ForeignKeyConstraint(['user'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_applicant_id'), 'applicant', ['id'], unique=False) + + if create_missing_table('intake_question_set', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('question_set_name', sa.String(), nullable=False), + sa.Column('housing_program', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['housing_program'], ['housing_program.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_intake_question_set_id'), 'intake_question_set', ['id'], unique=False) + + if create_missing_table('program_coordinator', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user', sa.Integer(), nullable=False), + sa.Column('housing_program', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['housing_program'], ['housing_program.id'], ), + sa.ForeignKeyConstraint(['user'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_program_coordinator_id'), 'program_coordinator', ['id'], unique=False) + + if create_missing_table('applicant_status_log', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('log_description', sa.String(), nullable=False), + sa.Column('logtime', sa.DateTime(), nullable=False), + sa.Column('applicant', sa.Integer(), nullable=False), + sa.Column('src_status', sa.Integer(), nullable=False), + sa.Column('dest_status', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['applicant'], ['applicant.id'], ), + sa.ForeignKeyConstraint(['dest_status'], ['applicant_status.id'], ), + sa.ForeignKeyConstraint(['src_status'], ['applicant_status.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_applicant_status_log_id'), 'applicant_status_log', ['id'], unique=False) + + if create_missing_table('applicant_uploaded_image', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('applicant', sa.Integer(), nullable=False), + sa.Column('image_data', sa.LargeBinary(length=5242880), nullable=False), + sa.ForeignKeyConstraint(['applicant'], ['applicant.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_applicant_uploaded_image_id'), 'applicant_uploaded_image', ['id'], unique=False) + + if create_missing_table('guest_group_member', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('guest_group', sa.Integer(), nullable=False), + sa.Column('applicant', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['applicant'], ['applicant.id'], ), + sa.ForeignKeyConstraint(['guest_group'], ['guest_group.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_guest_group_member_id'), 'guest_group_member', ['id'], unique=False) + + if create_missing_table('host_household_member', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('host_household', sa.Integer(), nullable=False), + sa.Column('applicant', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['applicant'], ['applicant.id'], ), + sa.ForeignKeyConstraint(['host_household'], ['host_household.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_host_household_member_id'), 'host_household_member', ['id'], unique=False) + + if create_missing_table('housing_program_pariticipant', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('applicant', sa.Integer(), nullable=False), + sa.Column('housing_program', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['applicant'], ['applicant.id'], ), + sa.ForeignKeyConstraint(['housing_program'], ['housing_program.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_housing_program_pariticipant_id'), 'housing_program_pariticipant', ['id'], unique=False) + + if create_missing_table('intake_question', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('applicant_type', sa.Integer(), nullable=False), + sa.Column('intake_question_type', sa.Integer(), nullable=False), + sa.Column('intake_question_set', sa.Integer(), nullable=False), + sa.Column('question_text', sa.String(), nullable=False), + sa.ForeignKeyConstraint(['applicant_type'], ['applicant_type.id'], ), + sa.ForeignKeyConstraint(['intake_question_set'], ['intake_question_set.id'], ), + sa.ForeignKeyConstraint(['intake_question_type'], ['intake_question_type.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_intake_question_id'), 'intake_question', ['id'], unique=False) + + if create_missing_table('match_result', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('applicant_a', sa.Integer(), nullable=False), + sa.Column('applicant_b', sa.Integer(), nullable=False), + sa.Column('match_status', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['applicant_a'], ['applicant.id'], ), + sa.ForeignKeyConstraint(['applicant_b'], ['applicant.id'], ), + sa.ForeignKeyConstraint(['match_status'], ['match_status.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_match_result_id'), 'match_result', ['id'], unique=False) + + if create_missing_table('program_case', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('coordinator', sa.Integer(), nullable=False), + sa.Column('case_status', sa.Integer(), nullable=False), + sa.Column('host_household', sa.Integer(), nullable=False), + sa.Column('guest_group', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['case_status'], ['case_status.id'], ), + sa.ForeignKeyConstraint(['coordinator'], ['program_coordinator.id'], ), + sa.ForeignKeyConstraint(['guest_group'], ['guest_group.id'], ), + sa.ForeignKeyConstraint(['host_household'], ['host_household.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_program_case_id'), 'program_case', ['id'], unique=False) + + if create_missing_table('image_tag', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('applicant', sa.Integer(), nullable=False), + sa.Column('image_tag_type', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['applicant'], ['applicant_uploaded_image.id'], ), + sa.ForeignKeyConstraint(['image_tag_type'], ['image_tag_type.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_image_tag_id'), 'image_tag', ['id'], unique=False) + + if create_missing_table('intake_response_value', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('intake_question', sa.Integer(), nullable=False), + sa.Column('response_text', sa.String(), nullable=False), + sa.ForeignKeyConstraint(['intake_question'], ['intake_question.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_intake_response_value_id'), 'intake_response_value', ['id'], unique=False) + + if create_missing_table('program_case_log', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('log_description', sa.String(), nullable=False), + sa.Column('logtime', sa.DateTime(), nullable=False), + sa.Column('program_case', sa.Integer(), nullable=False), + sa.Column('src_status', sa.Integer(), nullable=False), + sa.Column('dest_status', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['dest_status'], ['case_status.id'], ), + sa.ForeignKeyConstraint(['program_case'], ['program_case.id'], ), + sa.ForeignKeyConstraint(['src_status'], ['case_status.id'], ), + sa.PrimaryKeyConstraint('id') + ): + op.create_index(op.f('ix_program_case_log_id'), 'program_case_log', ['id'], unique=False) + + if create_missing_table('match_fail_condition', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('response_value_a', sa.Integer(), nullable=False), + sa.Column('response_value_b', sa.Integer(), nullable=False), + sa.Column('reason_text', sa.String(), nullable=False), + sa.ForeignKeyConstraint(['response_value_a'], ['intake_response_value.id'], ), + sa.ForeignKeyConstraint(['response_value_b'], ['intake_response_value.id'], ), + sa.PrimaryKeyConstraint('id')): + op.create_index(op.f('ix_match_fail_condition_id'), 'match_fail_condition', ['id'], unique=False) + + if create_missing_table('match_failure', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('match_result', sa.Integer(), nullable=False), + sa.Column('failed_condition', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['failed_condition'], ['match_fail_condition.id'], ), + sa.ForeignKeyConstraint(['match_result'], ['match_result.id'], ), + sa.PrimaryKeyConstraint('id')): + op.create_index(op.f('ix_match_failure_id'), 'match_failure', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_match_failure_id'), table_name='match_failure') + op.drop_table('match_failure') + op.drop_index(op.f('ix_match_fail_condition_id'), table_name='match_fail_condition') + op.drop_table('match_fail_condition') + op.drop_index(op.f('ix_program_case_log_id'), table_name='program_case_log') + op.drop_table('program_case_log') + op.drop_index(op.f('ix_intake_response_value_id'), table_name='intake_response_value') + op.drop_table('intake_response_value') + op.drop_index(op.f('ix_image_tag_id'), table_name='image_tag') + op.drop_table('image_tag') + op.drop_index(op.f('ix_program_case_id'), table_name='program_case') + op.drop_table('program_case') + op.drop_index(op.f('ix_match_result_id'), table_name='match_result') + op.drop_table('match_result') + op.drop_index(op.f('ix_intake_question_id'), table_name='intake_question') + op.drop_table('intake_question') + op.drop_index(op.f('ix_housing_program_pariticipant_id'), table_name='housing_program_pariticipant') + op.drop_table('housing_program_pariticipant') + op.drop_index(op.f('ix_host_household_member_id'), table_name='host_household_member') + op.drop_table('host_household_member') + op.drop_index(op.f('ix_guest_group_member_id'), table_name='guest_group_member') + op.drop_table('guest_group_member') + op.drop_index(op.f('ix_applicant_uploaded_image_id'), table_name='applicant_uploaded_image') + op.drop_table('applicant_uploaded_image') + op.drop_index(op.f('ix_applicant_status_log_id'), table_name='applicant_status_log') + op.drop_table('applicant_status_log') + op.drop_index(op.f('ix_program_coordinator_id'), table_name='program_coordinator') + op.drop_table('program_coordinator') + op.drop_index(op.f('ix_intake_question_set_id'), table_name='intake_question_set') + op.drop_table('intake_question_set') + op.drop_index(op.f('ix_applicant_id'), table_name='applicant') + op.drop_table('applicant') + op.drop_index(op.f('ix_housing_program_id'), table_name='housing_program') + op.drop_table('housing_program') + op.drop_index(op.f('ix_group_match_result_id'), table_name='group_match_result') + op.drop_table('group_match_result') + op.drop_index(op.f('ix_applicant_status_id'), table_name='applicant_status') + op.drop_table('applicant_status') + op.drop_index(op.f('ix_user_id'), table_name='user') + op.drop_table('user') + op.drop_index(op.f('ix_match_status_id'), table_name='match_status') + op.drop_table('match_status') + op.drop_index(op.f('ix_intake_question_type_id'), table_name='intake_question_type') + op.drop_table('intake_question_type') + op.drop_index(op.f('ix_image_tag_type_id'), table_name='image_tag_type') + op.drop_table('image_tag_type') + op.drop_index(op.f('ix_housing_program_service_provider_id'), table_name='housing_program_service_provider') + op.drop_table('housing_program_service_provider') + op.drop_index(op.f('ix_host_household_id'), table_name='host_household') + op.drop_table('host_household') + op.drop_index(op.f('ix_host_id'), table_name='host') + op.drop_table('host') + op.drop_index(op.f('ix_guest_group_id'), table_name='guest_group') + op.drop_table('guest_group') + op.drop_index(op.f('ix_case_status_id'), table_name='case_status') + op.drop_table('case_status') + op.drop_index(op.f('ix_applicant_type_id'), table_name='applicant_type') + op.drop_table('applicant_type') + # ### end Alembic commands ### diff --git a/flask-api/alembic/versions/cfc4e41b69d3_initial_form_api.py b/flask-api/alembic/versions/cfc4e41b69d3_initial_form_api.py new file mode 100644 index 00000000..8379e0d9 --- /dev/null +++ b/flask-api/alembic/versions/cfc4e41b69d3_initial_form_api.py @@ -0,0 +1,85 @@ +"""initial_form_api + +Revision ID: cfc4e41b69d3 +Revises: e4c8bb426528 +Create Date: 2024-05-05 17:14:51.771328 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'cfc4e41b69d3' +down_revision = 'e4c8bb426528' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table('field_properties', + sa.Column('properties_id', sa.Integer(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('field_type', sa.String(length=50), nullable=False), + sa.Column('choices', sa.JSON(), nullable=True), + sa.CheckConstraint("field_type IN ('date', 'dropdown', 'multiple_choice', 'email', 'file_upload', 'group', 'long_text', 'number', 'short_text', 'yes_no')", name='chk_field_type'), + sa.PrimaryKeyConstraint('properties_id') + ) + op.create_table('field_validations', + sa.Column('validations_id', sa.Integer(), nullable=False), + sa.Column('required', sa.Boolean(), nullable=False), + sa.Column('max_length', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('validations_id') + ) + op.create_table('forms', + sa.Column('form_id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('form_id') + ) + op.create_table('field_groups', + sa.Column('group_id', sa.Integer(), nullable=False), + sa.Column('form_id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['form_id'], ['forms.form_id'], ), + sa.PrimaryKeyConstraint('group_id') + ) + op.create_table('fields', + sa.Column('field_id', sa.Integer(), nullable=False), + sa.Column('ref', sa.String(length=255), nullable=False), + sa.Column('properties_id', sa.Integer(), nullable=False), + sa.Column('validations_id', sa.Integer(), nullable=False), + sa.Column('group_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['group_id'], ['field_groups.group_id'], ), + sa.ForeignKeyConstraint(['properties_id'], ['field_properties.properties_id'], ), + sa.ForeignKeyConstraint(['validations_id'], ['field_validations.validations_id'], ), + sa.PrimaryKeyConstraint('field_id') + ) + op.create_table('responses', + sa.Column('answer_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('field_id', sa.String(length=255), nullable=False), + sa.Column('answer_text', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['field_id'], ['fields.field_id'], ), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ), + sa.PrimaryKeyConstraint('answer_id') + ) + with op.batch_alter_table('role', schema=None) as batch_op: + batch_op.create_unique_constraint('role', ['name']) + with op.batch_alter_table('user', schema=None) as batch_op: + batch_op.alter_column('lastName', + existing_type=sa.VARCHAR(length=255), + nullable=True, + existing_server_default=sa.text("'Unknown'")) + +def downgrade() -> None: + with op.batch_alter_table('role', schema=None) as batch_op: + batch_op.drop_constraint('role', type_='unique') + op.drop_table('responses') + op.drop_table('fields') + op.drop_table('field_groups') + op.drop_table('forms') + op.drop_table('field_validations') + op.drop_table('field_properties') diff --git a/api/alembic/versions/e4c8bb426528_add_user_types.py b/flask-api/alembic/versions/e4c8bb426528_add_user_types.py similarity index 100% rename from api/alembic/versions/e4c8bb426528_add_user_types.py rename to flask-api/alembic/versions/e4c8bb426528_add_user_types.py diff --git a/flask-api/alembic/versions/ec8b1c17739a_drop_unused_tables.py b/flask-api/alembic/versions/ec8b1c17739a_drop_unused_tables.py new file mode 100644 index 00000000..a6713646 --- /dev/null +++ b/flask-api/alembic/versions/ec8b1c17739a_drop_unused_tables.py @@ -0,0 +1,299 @@ +"""Drop unused tables + +Revision ID: ec8b1c17739a +Revises: 3ceec084158f +Create Date: 2024-03-10 15:54:55.578328 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ec8b1c17739a' +down_revision = '3ceec084158f' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index('ix_housing_program_pariticipant_id', table_name='housing_program_pariticipant') + op.drop_table('housing_program_pariticipant') + op.drop_index('ix_applicant_status_id', table_name='applicant_status') + op.drop_table('applicant_status') + op.drop_index('ix_applicant_type_id', table_name='applicant_type') + op.drop_table('applicant_type') + op.drop_index('ix_match_failure_id', table_name='match_failure') + op.drop_table('match_failure') + op.drop_index('ix_program_case_log_id', table_name='program_case_log') + op.drop_table('program_case_log') + op.drop_index('ix_intake_response_value_id', table_name='intake_response_value') + op.drop_table('intake_response_value') + op.drop_index('ix_case_status_id', table_name='case_status') + op.drop_table('case_status') + op.drop_index('ix_image_tag_type_id', table_name='image_tag_type') + op.drop_table('image_tag_type') + op.drop_index('ix_applicant_uploaded_image_id', table_name='applicant_uploaded_image') + op.drop_table('applicant_uploaded_image') + op.drop_index('ix_match_fail_condition_id', table_name='match_fail_condition') + op.drop_table('match_fail_condition') + op.drop_index('ix_host_household_member_id', table_name='host_household_member') + op.drop_table('host_household_member') + op.drop_index('ix_applicant_id', table_name='applicant') + op.drop_table('applicant') + op.drop_index('ix_intake_question_id', table_name='intake_question') + op.drop_table('intake_question') + op.drop_index('ix_image_tag_id', table_name='image_tag') + op.drop_table('image_tag') + op.drop_index('ix_match_status_id', table_name='match_status') + op.drop_table('match_status') + op.drop_index('ix_guest_group_id', table_name='guest_group') + op.drop_table('guest_group') + op.drop_index('ix_applicant_status_log_id', table_name='applicant_status_log') + op.drop_table('applicant_status_log') + op.drop_index('ix_host_household_id', table_name='host_household') + op.drop_table('host_household') + op.drop_index('ix_match_result_id', table_name='match_result') + op.drop_table('match_result') + op.drop_index('ix_intake_question_set_id', table_name='intake_question_set') + op.drop_table('intake_question_set') + op.drop_index('ix_program_case_id', table_name='program_case') + op.drop_table('program_case') + op.drop_index('ix_intake_question_type_id', table_name='intake_question_type') + op.drop_table('intake_question_type') + op.drop_index('ix_guest_group_member_id', table_name='guest_group_member') + op.drop_table('guest_group_member') + op.drop_index('ix_group_match_result_id', table_name='group_match_result') + op.drop_table('group_match_result') + op.drop_index('ix_program_coordinator_id', table_name='program_coordinator') + op.drop_table('program_coordinator') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('program_coordinator', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('user', sa.INTEGER(), nullable=False), + sa.Column('housing_program', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['housing_program'], ['housing_program.id'], ), + sa.ForeignKeyConstraint(['user'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_program_coordinator_id', 'program_coordinator', ['id'], unique=False) + op.create_table('group_match_result', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('guest_group', sa.INTEGER(), nullable=False), + sa.Column('host_household', sa.INTEGER(), nullable=False), + sa.Column('match_status', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['guest_group'], ['guest_group.id'], ), + sa.ForeignKeyConstraint(['host_household'], ['host_household.id'], ), + sa.ForeignKeyConstraint(['match_status'], ['match_status.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_group_match_result_id', 'group_match_result', ['id'], unique=False) + op.create_table('guest_group_member', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('guest_group', sa.INTEGER(), nullable=False), + sa.Column('applicant', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['applicant'], ['applicant.id'], ), + sa.ForeignKeyConstraint(['guest_group'], ['guest_group.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_guest_group_member_id', 'guest_group_member', ['id'], unique=False) + op.create_table('intake_question_type', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('type_description', sa.VARCHAR(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_intake_question_type_id', 'intake_question_type', ['id'], unique=False) + op.create_table('program_case', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('coordinator', sa.INTEGER(), nullable=False), + sa.Column('case_status', sa.INTEGER(), nullable=False), + sa.Column('host_household', sa.INTEGER(), nullable=False), + sa.Column('guest_group', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['case_status'], ['case_status.id'], ), + sa.ForeignKeyConstraint(['coordinator'], ['program_coordinator.id'], ), + sa.ForeignKeyConstraint(['guest_group'], ['guest_group.id'], ), + sa.ForeignKeyConstraint(['host_household'], ['host_household.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_program_case_id', 'program_case', ['id'], unique=False) + op.create_table('intake_question_set', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('question_set_name', sa.VARCHAR(), nullable=False), + sa.Column('housing_program', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['housing_program'], ['housing_program.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_intake_question_set_id', 'intake_question_set', ['id'], unique=False) + op.create_table('match_result', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('applicant_a', sa.INTEGER(), nullable=False), + sa.Column('applicant_b', sa.INTEGER(), nullable=False), + sa.Column('match_status', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['applicant_a'], ['applicant.id'], ), + sa.ForeignKeyConstraint(['applicant_b'], ['applicant.id'], ), + sa.ForeignKeyConstraint(['match_status'], ['match_status.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_match_result_id', 'match_result', ['id'], unique=False) + op.create_table('host_household', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('household_name', sa.VARCHAR(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_host_household_id', 'host_household', ['id'], unique=False) + op.create_table('applicant_status_log', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('log_description', sa.VARCHAR(), nullable=False), + sa.Column('logtime', sa.DATETIME(), nullable=False), + sa.Column('applicant', sa.INTEGER(), nullable=False), + sa.Column('src_status', sa.INTEGER(), nullable=False), + sa.Column('dest_status', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['applicant'], ['applicant.id'], ), + sa.ForeignKeyConstraint(['dest_status'], ['applicant_status.id'], ), + sa.ForeignKeyConstraint(['src_status'], ['applicant_status.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_applicant_status_log_id', 'applicant_status_log', ['id'], unique=False) + op.create_table('guest_group', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('group_name', sa.VARCHAR(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_guest_group_id', 'guest_group', ['id'], unique=False) + op.create_table('match_status', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('status_description', sa.VARCHAR(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_match_status_id', 'match_status', ['id'], unique=False) + op.create_table('image_tag', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('applicant', sa.INTEGER(), nullable=False), + sa.Column('image_tag_type', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['applicant'], ['applicant_uploaded_image.id'], ), + sa.ForeignKeyConstraint(['image_tag_type'], ['image_tag_type.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_image_tag_id', 'image_tag', ['id'], unique=False) + op.create_table('intake_question', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('applicant_type', sa.INTEGER(), nullable=False), + sa.Column('intake_question_type', sa.INTEGER(), nullable=False), + sa.Column('intake_question_set', sa.INTEGER(), nullable=False), + sa.Column('question_text', sa.VARCHAR(), nullable=False), + sa.ForeignKeyConstraint(['applicant_type'], ['applicant_type.id'], ), + sa.ForeignKeyConstraint(['intake_question_set'], ['intake_question_set.id'], ), + sa.ForeignKeyConstraint(['intake_question_type'], ['intake_question_type.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_intake_question_id', 'intake_question', ['id'], unique=False) + op.create_table('applicant', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('applicant_type', sa.INTEGER(), nullable=False), + sa.Column('applicant_status', sa.INTEGER(), nullable=False), + sa.Column('user', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['applicant_status'], ['applicant_status.id'], ), + sa.ForeignKeyConstraint(['applicant_type'], ['applicant_type.id'], ), + sa.ForeignKeyConstraint(['user'], ['user.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_applicant_id', 'applicant', ['id'], unique=False) + op.create_table('host_household_member', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('host_household', sa.INTEGER(), nullable=False), + sa.Column('applicant', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['applicant'], ['applicant.id'], ), + sa.ForeignKeyConstraint(['host_household'], ['host_household.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_host_household_member_id', 'host_household_member', ['id'], unique=False) + op.create_table('match_fail_condition', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('response_value_a', sa.INTEGER(), nullable=False), + sa.Column('response_value_b', sa.INTEGER(), nullable=False), + sa.Column('reason_text', sa.VARCHAR(), nullable=False), + sa.ForeignKeyConstraint(['response_value_a'], ['intake_response_value.id'], ), + sa.ForeignKeyConstraint(['response_value_b'], ['intake_response_value.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_match_fail_condition_id', 'match_fail_condition', ['id'], unique=False) + op.create_table('applicant_uploaded_image', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('applicant', sa.INTEGER(), nullable=False), + sa.Column('image_data', sa.BLOB(), nullable=False), + sa.ForeignKeyConstraint(['applicant'], ['applicant.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_applicant_uploaded_image_id', 'applicant_uploaded_image', ['id'], unique=False) + op.create_table('image_tag_type', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('tag_text', sa.VARCHAR(), nullable=False), + sa.Column('tag_description', sa.VARCHAR(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_image_tag_type_id', 'image_tag_type', ['id'], unique=False) + op.create_table('case_status', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('status_description', sa.VARCHAR(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_case_status_id', 'case_status', ['id'], unique=False) + op.create_table('intake_response_value', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('intake_question', sa.INTEGER(), nullable=False), + sa.Column('response_text', sa.VARCHAR(), nullable=False), + sa.ForeignKeyConstraint(['intake_question'], ['intake_question.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_intake_response_value_id', 'intake_response_value', ['id'], unique=False) + op.create_table('program_case_log', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('log_description', sa.VARCHAR(), nullable=False), + sa.Column('logtime', sa.DATETIME(), nullable=False), + sa.Column('program_case', sa.INTEGER(), nullable=False), + sa.Column('src_status', sa.INTEGER(), nullable=False), + sa.Column('dest_status', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['dest_status'], ['case_status.id'], ), + sa.ForeignKeyConstraint(['program_case'], ['program_case.id'], ), + sa.ForeignKeyConstraint(['src_status'], ['case_status.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_program_case_log_id', 'program_case_log', ['id'], unique=False) + op.create_table('match_failure', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('match_result', sa.INTEGER(), nullable=False), + sa.Column('failed_condition', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['failed_condition'], ['match_fail_condition.id'], ), + sa.ForeignKeyConstraint(['match_result'], ['match_result.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_match_failure_id', 'match_failure', ['id'], unique=False) + op.create_table('applicant_type', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('applicant_type_description', sa.VARCHAR(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_applicant_type_id', 'applicant_type', ['id'], unique=False) + op.create_table('applicant_status', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('applicant_type', sa.INTEGER(), nullable=False), + sa.Column('status_description', sa.VARCHAR(), nullable=False), + sa.ForeignKeyConstraint(['applicant_type'], ['applicant_type.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_applicant_status_id', 'applicant_status', ['id'], unique=False) + op.create_table('housing_program_pariticipant', + sa.Column('id', sa.INTEGER(), nullable=False), + sa.Column('applicant', sa.INTEGER(), nullable=False), + sa.Column('housing_program', sa.INTEGER(), nullable=False), + sa.ForeignKeyConstraint(['applicant'], ['applicant.id'], ), + sa.ForeignKeyConstraint(['housing_program'], ['housing_program.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('ix_housing_program_pariticipant_id', 'housing_program_pariticipant', ['id'], unique=False) + # ### end Alembic commands ### diff --git a/api/git_push.sh b/flask-api/git_push.sh similarity index 100% rename from api/git_push.sh rename to flask-api/git_push.sh diff --git a/flask-api/openapi_server/__init__.py b/flask-api/openapi_server/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/openapi_server/__main__.py b/flask-api/openapi_server/__main__.py similarity index 100% rename from api/openapi_server/__main__.py rename to flask-api/openapi_server/__main__.py diff --git a/api/openapi_server/app.py b/flask-api/openapi_server/app.py similarity index 100% rename from api/openapi_server/app.py rename to flask-api/openapi_server/app.py diff --git a/flask-api/openapi_server/configs/__init__.py b/flask-api/openapi_server/configs/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/openapi_server/configs/development.py b/flask-api/openapi_server/configs/development.py similarity index 100% rename from api/openapi_server/configs/development.py rename to flask-api/openapi_server/configs/development.py diff --git a/api/openapi_server/configs/huu_config.py b/flask-api/openapi_server/configs/huu_config.py similarity index 100% rename from api/openapi_server/configs/huu_config.py rename to flask-api/openapi_server/configs/huu_config.py diff --git a/api/openapi_server/configs/mock_aws.py b/flask-api/openapi_server/configs/mock_aws.py similarity index 100% rename from api/openapi_server/configs/mock_aws.py rename to flask-api/openapi_server/configs/mock_aws.py diff --git a/api/openapi_server/configs/production.py b/flask-api/openapi_server/configs/production.py similarity index 100% rename from api/openapi_server/configs/production.py rename to flask-api/openapi_server/configs/production.py diff --git a/api/openapi_server/configs/registry.py b/flask-api/openapi_server/configs/registry.py similarity index 100% rename from api/openapi_server/configs/registry.py rename to flask-api/openapi_server/configs/registry.py diff --git a/api/openapi_server/configs/staging.py b/flask-api/openapi_server/configs/staging.py similarity index 100% rename from api/openapi_server/configs/staging.py rename to flask-api/openapi_server/configs/staging.py diff --git a/flask-api/openapi_server/controllers/__init__.py b/flask-api/openapi_server/controllers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/openapi_server/controllers/admin_controller.py b/flask-api/openapi_server/controllers/admin_controller.py similarity index 100% rename from api/openapi_server/controllers/admin_controller.py rename to flask-api/openapi_server/controllers/admin_controller.py diff --git a/api/openapi_server/controllers/auth_controller.py b/flask-api/openapi_server/controllers/auth_controller.py similarity index 100% rename from api/openapi_server/controllers/auth_controller.py rename to flask-api/openapi_server/controllers/auth_controller.py diff --git a/api/openapi_server/controllers/coordinator_controller.py b/flask-api/openapi_server/controllers/coordinator_controller.py similarity index 100% rename from api/openapi_server/controllers/coordinator_controller.py rename to flask-api/openapi_server/controllers/coordinator_controller.py diff --git a/api/openapi_server/controllers/forms_controller.py b/flask-api/openapi_server/controllers/forms_controller.py similarity index 100% rename from api/openapi_server/controllers/forms_controller.py rename to flask-api/openapi_server/controllers/forms_controller.py diff --git a/api/openapi_server/controllers/host_controller.py b/flask-api/openapi_server/controllers/host_controller.py similarity index 100% rename from api/openapi_server/controllers/host_controller.py rename to flask-api/openapi_server/controllers/host_controller.py diff --git a/api/openapi_server/controllers/responses_controller.py b/flask-api/openapi_server/controllers/responses_controller.py similarity index 100% rename from api/openapi_server/controllers/responses_controller.py rename to flask-api/openapi_server/controllers/responses_controller.py diff --git a/api/openapi_server/controllers/security_controller.py b/flask-api/openapi_server/controllers/security_controller.py similarity index 100% rename from api/openapi_server/controllers/security_controller.py rename to flask-api/openapi_server/controllers/security_controller.py diff --git a/api/openapi_server/controllers/service_provider_controller.py b/flask-api/openapi_server/controllers/service_provider_controller.py similarity index 100% rename from api/openapi_server/controllers/service_provider_controller.py rename to flask-api/openapi_server/controllers/service_provider_controller.py diff --git a/api/openapi_server/controllers/users_controller.py b/flask-api/openapi_server/controllers/users_controller.py similarity index 100% rename from api/openapi_server/controllers/users_controller.py rename to flask-api/openapi_server/controllers/users_controller.py diff --git a/api/openapi_server/exceptions.py b/flask-api/openapi_server/exceptions.py similarity index 100% rename from api/openapi_server/exceptions.py rename to flask-api/openapi_server/exceptions.py diff --git a/api/openapi_server/models/README.md b/flask-api/openapi_server/models/README.md similarity index 100% rename from api/openapi_server/models/README.md rename to flask-api/openapi_server/models/README.md diff --git a/api/openapi_server/models/__init__.py b/flask-api/openapi_server/models/__init__.py similarity index 100% rename from api/openapi_server/models/__init__.py rename to flask-api/openapi_server/models/__init__.py diff --git a/api/openapi_server/models/database.py b/flask-api/openapi_server/models/database.py similarity index 100% rename from api/openapi_server/models/database.py rename to flask-api/openapi_server/models/database.py index f8fe9664..4647cbe7 100644 --- a/api/openapi_server/models/database.py +++ b/flask-api/openapi_server/models/database.py @@ -28,6 +28,12 @@ def validate_first_name(self, key, value): raise ValueError(f"{key} must contain at least one non-space character") return value.strip() +class Role(Base): + __tablename__ = "role" + id = Column(Integer, primary_key=True, index=True) + name = Column(String, nullable=False, unique=True) + users = relationship("User", back_populates="role") + class UnmatchedGuestCase(Base): __tablename__ = "unmatched_guest_case" id = Column(Integer, primary_key=True, index=True) @@ -42,12 +48,6 @@ class UnmatchedGuestCaseStatus(Base): status_text = Column(String(255), nullable=False, unique=True) cases = relationship("UnmatchedGuestCase", back_populates="status") -class Role(Base): - __tablename__ = "role" - id = Column(Integer, primary_key=True, index=True) - name = Column(String, nullable=False, unique=True) - users = relationship("User", back_populates="role") - class HousingProgramServiceProvider(Base): __tablename__ = "housing_program_service_provider" diff --git a/api/openapi_server/models/schema.py b/flask-api/openapi_server/models/schema.py similarity index 100% rename from api/openapi_server/models/schema.py rename to flask-api/openapi_server/models/schema.py diff --git a/api/openapi_server/models/user_roles.py b/flask-api/openapi_server/models/user_roles.py similarity index 100% rename from api/openapi_server/models/user_roles.py rename to flask-api/openapi_server/models/user_roles.py diff --git a/api/openapi_server/openapi/openapi.yaml b/flask-api/openapi_server/openapi/openapi.yaml similarity index 100% rename from api/openapi_server/openapi/openapi.yaml rename to flask-api/openapi_server/openapi/openapi.yaml diff --git a/api/openapi_server/openapi/parameters/_index.yaml b/flask-api/openapi_server/openapi/parameters/_index.yaml similarity index 100% rename from api/openapi_server/openapi/parameters/_index.yaml rename to flask-api/openapi_server/openapi/parameters/_index.yaml diff --git a/api/openapi_server/openapi/paths/auth/authConfirm.yaml b/flask-api/openapi_server/openapi/paths/auth/authConfirm.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authConfirm.yaml rename to flask-api/openapi_server/openapi/paths/auth/authConfirm.yaml diff --git a/api/openapi_server/openapi/paths/auth/authConfirmForgotPassword.yaml b/flask-api/openapi_server/openapi/paths/auth/authConfirmForgotPassword.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authConfirmForgotPassword.yaml rename to flask-api/openapi_server/openapi/paths/auth/authConfirmForgotPassword.yaml diff --git a/api/openapi_server/openapi/paths/auth/authConfirmInvite.yaml b/flask-api/openapi_server/openapi/paths/auth/authConfirmInvite.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authConfirmInvite.yaml rename to flask-api/openapi_server/openapi/paths/auth/authConfirmInvite.yaml diff --git a/api/openapi_server/openapi/paths/auth/authForgotPassword.yaml b/flask-api/openapi_server/openapi/paths/auth/authForgotPassword.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authForgotPassword.yaml rename to flask-api/openapi_server/openapi/paths/auth/authForgotPassword.yaml diff --git a/api/openapi_server/openapi/paths/auth/authGoogle.yaml b/flask-api/openapi_server/openapi/paths/auth/authGoogle.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authGoogle.yaml rename to flask-api/openapi_server/openapi/paths/auth/authGoogle.yaml diff --git a/api/openapi_server/openapi/paths/auth/authGoogleSignIn.yaml b/flask-api/openapi_server/openapi/paths/auth/authGoogleSignIn.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authGoogleSignIn.yaml rename to flask-api/openapi_server/openapi/paths/auth/authGoogleSignIn.yaml diff --git a/api/openapi_server/openapi/paths/auth/authGoogleSignUp.yaml b/flask-api/openapi_server/openapi/paths/auth/authGoogleSignUp.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authGoogleSignUp.yaml rename to flask-api/openapi_server/openapi/paths/auth/authGoogleSignUp.yaml diff --git a/api/openapi_server/openapi/paths/auth/authInvite.yaml b/flask-api/openapi_server/openapi/paths/auth/authInvite.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authInvite.yaml rename to flask-api/openapi_server/openapi/paths/auth/authInvite.yaml diff --git a/api/openapi_server/openapi/paths/auth/authNewPassword.yaml b/flask-api/openapi_server/openapi/paths/auth/authNewPassword.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authNewPassword.yaml rename to flask-api/openapi_server/openapi/paths/auth/authNewPassword.yaml diff --git a/api/openapi_server/openapi/paths/auth/authPrivate.yaml b/flask-api/openapi_server/openapi/paths/auth/authPrivate.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authPrivate.yaml rename to flask-api/openapi_server/openapi/paths/auth/authPrivate.yaml diff --git a/api/openapi_server/openapi/paths/auth/authRefresh.yaml b/flask-api/openapi_server/openapi/paths/auth/authRefresh.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authRefresh.yaml rename to flask-api/openapi_server/openapi/paths/auth/authRefresh.yaml diff --git a/api/openapi_server/openapi/paths/auth/authResendConfirmationCode.yaml b/flask-api/openapi_server/openapi/paths/auth/authResendConfirmationCode.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authResendConfirmationCode.yaml rename to flask-api/openapi_server/openapi/paths/auth/authResendConfirmationCode.yaml diff --git a/api/openapi_server/openapi/paths/auth/authSession.yaml b/flask-api/openapi_server/openapi/paths/auth/authSession.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authSession.yaml rename to flask-api/openapi_server/openapi/paths/auth/authSession.yaml diff --git a/api/openapi_server/openapi/paths/auth/authSignUpCoordinator.yaml b/flask-api/openapi_server/openapi/paths/auth/authSignUpCoordinator.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authSignUpCoordinator.yaml rename to flask-api/openapi_server/openapi/paths/auth/authSignUpCoordinator.yaml diff --git a/api/openapi_server/openapi/paths/auth/authSignUpHost.yaml b/flask-api/openapi_server/openapi/paths/auth/authSignUpHost.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authSignUpHost.yaml rename to flask-api/openapi_server/openapi/paths/auth/authSignUpHost.yaml diff --git a/api/openapi_server/openapi/paths/auth/authSignin.yaml b/flask-api/openapi_server/openapi/paths/auth/authSignin.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authSignin.yaml rename to flask-api/openapi_server/openapi/paths/auth/authSignin.yaml diff --git a/api/openapi_server/openapi/paths/auth/authSignout.yaml b/flask-api/openapi_server/openapi/paths/auth/authSignout.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authSignout.yaml rename to flask-api/openapi_server/openapi/paths/auth/authSignout.yaml diff --git a/api/openapi_server/openapi/paths/auth/authUser.yaml b/flask-api/openapi_server/openapi/paths/auth/authUser.yaml similarity index 100% rename from api/openapi_server/openapi/paths/auth/authUser.yaml rename to flask-api/openapi_server/openapi/paths/auth/authUser.yaml diff --git a/api/openapi_server/openapi/paths/forms.yaml b/flask-api/openapi_server/openapi/paths/forms.yaml similarity index 100% rename from api/openapi_server/openapi/paths/forms.yaml rename to flask-api/openapi_server/openapi/paths/forms.yaml diff --git a/api/openapi_server/openapi/paths/host.yaml b/flask-api/openapi_server/openapi/paths/host.yaml similarity index 100% rename from api/openapi_server/openapi/paths/host.yaml rename to flask-api/openapi_server/openapi/paths/host.yaml diff --git a/api/openapi_server/openapi/paths/post-form.yaml b/flask-api/openapi_server/openapi/paths/post-form.yaml similarity index 100% rename from api/openapi_server/openapi/paths/post-form.yaml rename to flask-api/openapi_server/openapi/paths/post-form.yaml diff --git a/api/openapi_server/openapi/paths/responses.yaml b/flask-api/openapi_server/openapi/paths/responses.yaml similarity index 100% rename from api/openapi_server/openapi/paths/responses.yaml rename to flask-api/openapi_server/openapi/paths/responses.yaml diff --git a/api/openapi_server/openapi/paths/serviceProvider.yaml b/flask-api/openapi_server/openapi/paths/serviceProvider.yaml similarity index 100% rename from api/openapi_server/openapi/paths/serviceProvider.yaml rename to flask-api/openapi_server/openapi/paths/serviceProvider.yaml diff --git a/api/openapi_server/openapi/paths/serviceProviders.yaml b/flask-api/openapi_server/openapi/paths/serviceProviders.yaml similarity index 100% rename from api/openapi_server/openapi/paths/serviceProviders.yaml rename to flask-api/openapi_server/openapi/paths/serviceProviders.yaml diff --git a/api/openapi_server/openapi/paths/users.yaml b/flask-api/openapi_server/openapi/paths/users.yaml similarity index 100% rename from api/openapi_server/openapi/paths/users.yaml rename to flask-api/openapi_server/openapi/paths/users.yaml diff --git a/api/openapi_server/openapi/responses/_index.yaml b/flask-api/openapi_server/openapi/responses/_index.yaml similarity index 100% rename from api/openapi_server/openapi/responses/_index.yaml rename to flask-api/openapi_server/openapi/responses/_index.yaml diff --git a/api/openapi_server/openapi/schemas/_index.yaml b/flask-api/openapi_server/openapi/schemas/_index.yaml similarity index 100% rename from api/openapi_server/openapi/schemas/_index.yaml rename to flask-api/openapi_server/openapi/schemas/_index.yaml diff --git a/flask-api/openapi_server/repositories/__init__.py b/flask-api/openapi_server/repositories/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/openapi_server/repositories/base.py b/flask-api/openapi_server/repositories/base.py similarity index 100% rename from api/openapi_server/repositories/base.py rename to flask-api/openapi_server/repositories/base.py diff --git a/api/openapi_server/repositories/forms.py b/flask-api/openapi_server/repositories/forms.py similarity index 100% rename from api/openapi_server/repositories/forms.py rename to flask-api/openapi_server/repositories/forms.py diff --git a/api/openapi_server/repositories/service_provider_repository.py b/flask-api/openapi_server/repositories/service_provider_repository.py similarity index 100% rename from api/openapi_server/repositories/service_provider_repository.py rename to flask-api/openapi_server/repositories/service_provider_repository.py diff --git a/api/openapi_server/repositories/user_repo.py b/flask-api/openapi_server/repositories/user_repo.py similarity index 100% rename from api/openapi_server/repositories/user_repo.py rename to flask-api/openapi_server/repositories/user_repo.py diff --git a/api/openapi_server/typing_utils.py b/flask-api/openapi_server/typing_utils.py similarity index 100% rename from api/openapi_server/typing_utils.py rename to flask-api/openapi_server/typing_utils.py diff --git a/api/openapi_server/util.py b/flask-api/openapi_server/util.py similarity index 100% rename from api/openapi_server/util.py rename to flask-api/openapi_server/util.py diff --git a/api/pyproject.toml b/flask-api/pyproject.toml similarity index 100% rename from api/pyproject.toml rename to flask-api/pyproject.toml diff --git a/api/requirements-dev.txt b/flask-api/requirements-dev.txt similarity index 100% rename from api/requirements-dev.txt rename to flask-api/requirements-dev.txt diff --git a/api/requirements.txt b/flask-api/requirements.txt similarity index 100% rename from api/requirements.txt rename to flask-api/requirements.txt diff --git a/flask-api/tests/__init__.py b/flask-api/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/api/tests/conftest.py b/flask-api/tests/conftest.py similarity index 100% rename from api/tests/conftest.py rename to flask-api/tests/conftest.py diff --git a/api/tests/setup_utils.py b/flask-api/tests/setup_utils.py similarity index 100% rename from api/tests/setup_utils.py rename to flask-api/tests/setup_utils.py diff --git a/api/tests/test_alembic_migration.py b/flask-api/tests/test_alembic_migration.py similarity index 100% rename from api/tests/test_alembic_migration.py rename to flask-api/tests/test_alembic_migration.py diff --git a/api/tests/test_authentication.py b/flask-api/tests/test_authentication.py similarity index 100% rename from api/tests/test_authentication.py rename to flask-api/tests/test_authentication.py diff --git a/api/tests/test_configs.py b/flask-api/tests/test_configs.py similarity index 100% rename from api/tests/test_configs.py rename to flask-api/tests/test_configs.py diff --git a/api/tests/test_forms_repo.py b/flask-api/tests/test_forms_repo.py similarity index 99% rename from api/tests/test_forms_repo.py rename to flask-api/tests/test_forms_repo.py index 14f665a1..8b24c021 100644 --- a/api/tests/test_forms_repo.py +++ b/flask-api/tests/test_forms_repo.py @@ -124,4 +124,4 @@ def _get_field_id(lcl_form, ref): for expected, actual in zip(expected_responses, retrieved_answers): assert expected['answer_text'] == actual['answer_text'] assert expected['user_id'] == actual['user']['id'] - assert expected['field_id'] == actual['field']['field_id'] \ No newline at end of file + assert expected['field_id'] == actual['field']['field_id'] diff --git a/flask-api/tests/test_forms_schema.py b/flask-api/tests/test_forms_schema.py new file mode 100644 index 00000000..66aef7b3 --- /dev/null +++ b/flask-api/tests/test_forms_schema.py @@ -0,0 +1,238 @@ +from types import MappingProxyType +import pytest +from marshmallow import ValidationError + +from openapi_server.models.schema import ( + form_schema, + FieldSchema, + FieldValidationsSchema, + FieldPropertiesSchema, + FieldGroupSchema +) + +VALID_FORM_JSON = MappingProxyType({ + "title": "Employee Onboarding", + "description": "Collect necessary employee data.", + "field_groups": [ + { + "title": "Personal Details", + "description": "Please enter your personal details.", + "fields": [ + { + "ref": "position", + "properties": { + "description": "Position in the company", + "field_type": "dropdown", + "choices": ['Manager', 'Developer', 'Designer'], + }, + "validations": { + "required": True, + "max_length": 12 + } + }, + { + "ref": "service_length", + "properties": { + "description": "Years in the company", + "field_type": "number", + "choices": None, + }, + "validations": { + "required": False, + "max_length": None + } + } + ] + }, + { + "title": "Second Group", + "description": "A second field group.", + "fields": [ + { + "ref": "start date", + "properties": { + "description": "Start date", + "field_type": "date", + "choices": "11-22-2005", + }, + "validations": { + "required": True, + "max_length": 12 + } + } + ] + } + ] + } + ) + + +def test_serialize_form_no_questions(empty_db_session): + form_json = {"title": "mytitle", "description": "mydesc", "field_groups": []} + form = form_schema.load(form_json, session=empty_db_session) + + assert "mytitle" == form.title + assert "mydesc" == form.description + assert list() == form.field_groups + +def test_deserialize_field_validations(empty_db_session): + validation_json = { + "required": True, + "max_length": None + } + validation = FieldValidationsSchema().load(validation_json, session=empty_db_session) + assert validation.required + assert validation.max_length is None + +def test_deserialize_field_property(empty_db_session): + property_json = { + "description": "sample desc", + "field_type": "long_text", + "choices": ['one', 'two','three'] + } + property = FieldPropertiesSchema().load(property_json, session=empty_db_session) + assert property_json["field_type"] == property.field_type + assert property_json["description"] == property.description + +def test_deserialize_field(empty_db_session): + single_field_json = { + "ref": "position", + "properties": { + "description": "Position in the company", + "field_type": "dropdown", + "choices": ['Manager', 'Developer', 'Designer'], + }, + "validations": { + "required": True, + "max_length": 12 + } + } + field = FieldSchema().load(single_field_json, session=empty_db_session) + assert single_field_json["ref"] == field.ref + assert single_field_json["properties"]["description"] == field.properties.description + assert single_field_json["properties"]["choices"] == field.properties.choices + assert single_field_json["validations"]["max_length"] == field.validations.max_length + assert field.validations.required + +def test_deserialize_fields(empty_db_session): + multiple_fields = [ + { + "ref": "position", + "properties": { + "description": "Position in the company", + "field_type": "dropdown", + "choices": ['Manager', 'Developer', 'Designer'], + }, + "validations": { + "required": True, + "max_length": 12 + } + }, + { + "ref": "service_length", + "properties": { + "description": "Years in the company", + "field_type": "number", + "choices": None, + }, + "validations": { + "required": False, + "max_length": None + } + } + ] + fields = FieldSchema(many=True).load(multiple_fields, session=empty_db_session) + assert 2 == len(fields) + for expected, actual in zip(multiple_fields, fields): + assert expected['properties']['description'] == actual.properties.description + assert expected['properties']['field_type'] == actual.properties.field_type + +def test_deserialize_field_group(empty_db_session): + group_json = [ + { + "title": "Personal Details", + "description": "Please enter your personal details.", + "fields": [ + { + "ref": "position", + "properties": { + "description": "Position in the company", + "field_type": "dropdown", + "choices": ['Manager', 'Developer', 'Designer'], + }, + "validations": { + "required": True, + "max_length": 12 + } + }, + { + "ref": "service_length", + "properties": { + "description": "Years in the company", + "field_type": "number", + "choices": None, + }, + "validations": { + "required": False, + "max_length": None + } + } + ] + }, + { + "title": "Second Group", + "description": "A second field group.", + "fields": [ + { + "ref": "start date", + "properties": { + "description": "Start date", + "field_type": "date", + "choices": "11-22-2005", + }, + "validations": { + "required": True, + "max_length": 12 + } + } + ] + } + ] + groups = FieldGroupSchema(many=True).load(group_json, session=empty_db_session) + assert len(group_json) == len(groups) + for expected_group, actual_group in zip(group_json, groups): + assert expected_group['title'] == actual_group.title + assert expected_group['description'] == actual_group.description + for expected_fields, actual_fields in zip(expected_group['fields'], actual_group.fields): + assert expected_fields['ref'] == actual_fields.ref + assert expected_fields['validations']['required'] == actual_fields.validations.required + assert expected_fields['validations']['max_length'] == actual_fields.validations.max_length + assert expected_fields['properties']['description'] == actual_fields.properties.description + assert expected_fields['properties']['field_type'] == actual_fields.properties.field_type + assert expected_fields['properties']['choices'] == actual_fields.properties.choices + +def test_deserialize_form_happypath(empty_db_session): + form_json = dict(VALID_FORM_JSON) + form = form_schema.load(form_json, session=empty_db_session) + assert form_json["title"] == form.title + assert form_json["description"] == form.description + assert 2 == len(form.field_groups) + for expected, actual in zip(form_json["field_groups"], form.field_groups): + assert expected["title"] == actual.title + assert expected["description"] == actual.description + assert len(expected["fields"]) == len(actual.fields) + + +def test_deserialize_form_extra_key(empty_db_session): + invalid_form_json = dict(VALID_FORM_JSON) + invalid_form_json['extra_key'] = 'extra_value' + + with pytest.raises(ValidationError, match=r"Unknown field"): + form_schema.load(invalid_form_json, session=empty_db_session) + +def test_deserialize_form_missing_key(empty_db_session): + invalid_form_json = dict(VALID_FORM_JSON) + del invalid_form_json['title'] + + with pytest.raises(ValidationError, match=r"Missing data for required field"): + form_schema.load(invalid_form_json, session=empty_db_session) \ No newline at end of file diff --git a/flask-api/tests/test_host_controller.py b/flask-api/tests/test_host_controller.py new file mode 100644 index 00000000..bca493f7 --- /dev/null +++ b/flask-api/tests/test_host_controller.py @@ -0,0 +1,66 @@ +from openapi_server.models.database import User, DataAccessLayer +from openapi_server.repositories.user_repo import UserRepository +from openapi_server.models.user_roles import UserRole + +def test_signup_host(client): + """ + Test creating a new host using a simulated post request. Verify that the + response is correct, and that the app database was properly updated. + """ + + NEW_HOST = { + "email" : "test@email.com", + "password": "Test!@123", + "firstName": "Josh", + "middleName": "Ray", + "lastName": "Douglas" + } + response = client.post( + '/api/auth/signup/host', + json=NEW_HOST) + + assert response.status_code == 200, f'Response body is: {response.json}' + + # Make sure the database was updated to persist the values + with DataAccessLayer.session() as session: + user_repo = UserRepository(session) + test_host = user_repo.get_user(NEW_HOST['email']) + assert test_host is not None + assert test_host.email == NEW_HOST['email'] + assert test_host.firstName == NEW_HOST['firstName'] + assert test_host.middleName == NEW_HOST['middleName'] + assert test_host.lastName == NEW_HOST['lastName'] + assert test_host.role.name == UserRole.HOST.value + +def test_get_hosts(client): + """ + Test that get_hosts returns all hosts available in the database. The endpoint + should properly filter out all other user roles. + """ + # Arrange + with DataAccessLayer.session() as session: + user_repo = UserRepository(session) + user_repo.add_user(email="host0@email.com", role=UserRole.HOST, firstName="host0", middleName = None, lastName="host_last0") + user_repo.add_user(email="host1@email.com", role=UserRole.HOST, firstName="host1", middleName = None, lastName="host_last1") + user_repo.add_user(email="host2@email.com", role=UserRole.HOST, firstName="host2", middleName = None, lastName="host_last2") + user_repo.add_user(email="guest1@email.com", role=UserRole.GUEST, firstName="guest0", middleName = None, lastName="guest_last0") + user_repo.add_user(email="Admin2@email.com", role=UserRole.ADMIN, firstName="Admin0", middleName = None, lastName="cdmin_last0") + user_repo.add_user(email="Coordinator3@email.com", role=UserRole.COORDINATOR, firstName="coodinator0", middleName = None, lastName="coordinator_last0") + + # Act + response = client.get('/api/host') + + # Assert + assert response.status_code == 200, f'Response body is: {response.json}' + assert isinstance(response.json, list) + assert len(response.json) == 3 + host_emails_set = set() + for host in response.json: + assert 'host' in host["email"] + assert 'host' in host["firstName"] + assert 'host_last' in host["lastName"] + assert host["role"]["name"] == UserRole.HOST.value + assert host["middleName"] == None + host_emails_set.add(host["email"]) + + assert len(host_emails_set) == 3, "Duplicate hosts were returned!" \ No newline at end of file diff --git a/api/tests/test_mocking.py b/flask-api/tests/test_mocking.py similarity index 100% rename from api/tests/test_mocking.py rename to flask-api/tests/test_mocking.py diff --git a/api/tests/test_schema.py b/flask-api/tests/test_schema.py similarity index 100% rename from api/tests/test_schema.py rename to flask-api/tests/test_schema.py diff --git a/api/tests/test_service_provider_controller.py b/flask-api/tests/test_service_provider_controller.py similarity index 100% rename from api/tests/test_service_provider_controller.py rename to flask-api/tests/test_service_provider_controller.py diff --git a/api/tests/test_service_provider_repository.py b/flask-api/tests/test_service_provider_repository.py similarity index 100% rename from api/tests/test_service_provider_repository.py rename to flask-api/tests/test_service_provider_repository.py diff --git a/api/tests/test_user_repo.py b/flask-api/tests/test_user_repo.py similarity index 100% rename from api/tests/test_user_repo.py rename to flask-api/tests/test_user_repo.py diff --git a/api/tox.ini b/flask-api/tox.ini similarity index 100% rename from api/tox.ini rename to flask-api/tox.ini diff --git a/app/.devcontainer/Dockerfile b/frontend/.devcontainer/Dockerfile similarity index 100% rename from app/.devcontainer/Dockerfile rename to frontend/.devcontainer/Dockerfile diff --git a/app/.devcontainer/devcontainer.json b/frontend/.devcontainer/devcontainer.json similarity index 100% rename from app/.devcontainer/devcontainer.json rename to frontend/.devcontainer/devcontainer.json diff --git a/app/.env.devcontainer b/frontend/.env.devcontainer similarity index 100% rename from app/.env.devcontainer rename to frontend/.env.devcontainer diff --git a/app/.env.example b/frontend/.env.example similarity index 100% rename from app/.env.example rename to frontend/.env.example diff --git a/app/.eslintignore b/frontend/.eslintignore similarity index 100% rename from app/.eslintignore rename to frontend/.eslintignore diff --git a/app/.eslintrc b/frontend/.eslintrc similarity index 100% rename from app/.eslintrc rename to frontend/.eslintrc diff --git a/app/.gitignore b/frontend/.gitignore similarity index 100% rename from app/.gitignore rename to frontend/.gitignore diff --git a/app/.husky/pre-commit b/frontend/.husky/pre-commit similarity index 100% rename from app/.husky/pre-commit rename to frontend/.husky/pre-commit diff --git a/app/.prettierignore b/frontend/.prettierignore similarity index 100% rename from app/.prettierignore rename to frontend/.prettierignore diff --git a/app/.prettierrc b/frontend/.prettierrc similarity index 100% rename from app/.prettierrc rename to frontend/.prettierrc diff --git a/app/Dockerfile b/frontend/Dockerfile similarity index 100% rename from app/Dockerfile rename to frontend/Dockerfile diff --git a/app/README.md b/frontend/README.md similarity index 100% rename from app/README.md rename to frontend/README.md diff --git a/app/check-node-version.mjs b/frontend/check-node-version.mjs similarity index 100% rename from app/check-node-version.mjs rename to frontend/check-node-version.mjs diff --git a/app/cypress.config.ts b/frontend/cypress.config.ts similarity index 100% rename from app/cypress.config.ts rename to frontend/cypress.config.ts diff --git a/app/cypress/e2e/create-new-password.cy.ts b/frontend/cypress/e2e/create-new-password.cy.ts similarity index 100% rename from app/cypress/e2e/create-new-password.cy.ts rename to frontend/cypress/e2e/create-new-password.cy.ts diff --git a/app/cypress/e2e/forgot-password.cy.ts b/frontend/cypress/e2e/forgot-password.cy.ts similarity index 100% rename from app/cypress/e2e/forgot-password.cy.ts rename to frontend/cypress/e2e/forgot-password.cy.ts diff --git a/app/cypress/e2e/sign-up.cy.ts b/frontend/cypress/e2e/sign-up.cy.ts similarity index 100% rename from app/cypress/e2e/sign-up.cy.ts rename to frontend/cypress/e2e/sign-up.cy.ts diff --git a/app/cypress/e2e/test-authentication.cy.ts b/frontend/cypress/e2e/test-authentication.cy.ts similarity index 100% rename from app/cypress/e2e/test-authentication.cy.ts rename to frontend/cypress/e2e/test-authentication.cy.ts diff --git a/app/cypress/fixtures/example.json b/frontend/cypress/fixtures/example.json similarity index 100% rename from app/cypress/fixtures/example.json rename to frontend/cypress/fixtures/example.json diff --git a/app/cypress/support/commands.ts b/frontend/cypress/support/commands.ts similarity index 100% rename from app/cypress/support/commands.ts rename to frontend/cypress/support/commands.ts diff --git a/app/cypress/support/component-index.html b/frontend/cypress/support/component-index.html similarity index 100% rename from app/cypress/support/component-index.html rename to frontend/cypress/support/component-index.html diff --git a/app/cypress/support/component.ts b/frontend/cypress/support/component.ts similarity index 100% rename from app/cypress/support/component.ts rename to frontend/cypress/support/component.ts diff --git a/app/cypress/support/e2e.ts b/frontend/cypress/support/e2e.ts similarity index 100% rename from app/cypress/support/e2e.ts rename to frontend/cypress/support/e2e.ts diff --git a/app/cypress/tsconfig.json b/frontend/cypress/tsconfig.json similarity index 100% rename from app/cypress/tsconfig.json rename to frontend/cypress/tsconfig.json diff --git a/app/index.html b/frontend/index.html similarity index 100% rename from app/index.html rename to frontend/index.html diff --git a/app/openapi-config.ts b/frontend/openapi-config.ts similarity index 100% rename from app/openapi-config.ts rename to frontend/openapi-config.ts diff --git a/app/package-lock.json b/frontend/package-lock.json similarity index 100% rename from app/package-lock.json rename to frontend/package-lock.json diff --git a/app/package.json b/frontend/package.json similarity index 100% rename from app/package.json rename to frontend/package.json diff --git a/app/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js similarity index 100% rename from app/public/mockServiceWorker.js rename to frontend/public/mockServiceWorker.js diff --git a/app/src/app/authSlice.ts b/frontend/src/app/authSlice.ts similarity index 85% rename from app/src/app/authSlice.ts rename to frontend/src/app/authSlice.ts index cce1fe6d..5fd56b4c 100644 --- a/app/src/app/authSlice.ts +++ b/frontend/src/app/authSlice.ts @@ -1,6 +1,6 @@ import {createSlice, PayloadAction} from '@reduxjs/toolkit'; -import {User} from '../services/auth'; +import {User, userAPI} from '../services/user'; import {RootState} from './store'; import {authApi} from '../services/auth'; @@ -34,9 +34,12 @@ export const authSlice = createSlice({ extraReducers: builder => { builder // Add a matcher to update auth state with user returned from the user query - .addMatcher(authApi.endpoints.user.matchFulfilled, (state, {payload}) => { - state.user = payload.user; - }) + .addMatcher( + userAPI.endpoints.currentUser.matchFulfilled, + (state, {payload}) => { + state.user = payload; + }, + ) .addMatcher( authApi.endpoints.session.matchFulfilled, (state, {payload}) => { diff --git a/app/src/app/helpers.ts b/frontend/src/app/helpers.ts similarity index 100% rename from app/src/app/helpers.ts rename to frontend/src/app/helpers.ts diff --git a/app/src/app/hooks/store.ts b/frontend/src/app/hooks/store.ts similarity index 100% rename from app/src/app/hooks/store.ts rename to frontend/src/app/hooks/store.ts diff --git a/app/src/app/hooks/useAuth.ts b/frontend/src/app/hooks/useAuth.ts similarity index 100% rename from app/src/app/hooks/useAuth.ts rename to frontend/src/app/hooks/useAuth.ts diff --git a/app/src/app/store.ts b/frontend/src/app/store.ts similarity index 100% rename from app/src/app/store.ts rename to frontend/src/app/store.ts diff --git a/app/src/components/Icons/CoordinatorIcon.tsx b/frontend/src/components/Icons/CoordinatorIcon.tsx similarity index 100% rename from app/src/components/Icons/CoordinatorIcon.tsx rename to frontend/src/components/Icons/CoordinatorIcon.tsx diff --git a/app/src/components/Icons/GuestIcon.tsx b/frontend/src/components/Icons/GuestIcon.tsx similarity index 100% rename from app/src/components/Icons/GuestIcon.tsx rename to frontend/src/components/Icons/GuestIcon.tsx diff --git a/app/src/components/Icons/HostIcon.tsx b/frontend/src/components/Icons/HostIcon.tsx similarity index 100% rename from app/src/components/Icons/HostIcon.tsx rename to frontend/src/components/Icons/HostIcon.tsx diff --git a/app/src/components/Icons/InProgressIcon.tsx b/frontend/src/components/Icons/InProgressIcon.tsx similarity index 100% rename from app/src/components/Icons/InProgressIcon.tsx rename to frontend/src/components/Icons/InProgressIcon.tsx diff --git a/app/src/components/Icons/index.ts b/frontend/src/components/Icons/index.ts similarity index 100% rename from app/src/components/Icons/index.ts rename to frontend/src/components/Icons/index.ts diff --git a/app/src/components/authentication/AccountVerificationForm.tsx b/frontend/src/components/authentication/AccountVerificationForm.tsx similarity index 100% rename from app/src/components/authentication/AccountVerificationForm.tsx rename to frontend/src/components/authentication/AccountVerificationForm.tsx diff --git a/app/src/components/authentication/CodeField.tsx b/frontend/src/components/authentication/CodeField.tsx similarity index 100% rename from app/src/components/authentication/CodeField.tsx rename to frontend/src/components/authentication/CodeField.tsx diff --git a/app/src/components/authentication/FormContainer.tsx b/frontend/src/components/authentication/FormContainer.tsx similarity index 100% rename from app/src/components/authentication/FormContainer.tsx rename to frontend/src/components/authentication/FormContainer.tsx diff --git a/app/src/components/authentication/NewPasswordForm.tsx b/frontend/src/components/authentication/NewPasswordForm.tsx similarity index 100% rename from app/src/components/authentication/NewPasswordForm.tsx rename to frontend/src/components/authentication/NewPasswordForm.tsx diff --git a/app/src/components/authentication/PasswordField.tsx b/frontend/src/components/authentication/PasswordField.tsx similarity index 100% rename from app/src/components/authentication/PasswordField.tsx rename to frontend/src/components/authentication/PasswordField.tsx diff --git a/app/src/components/authentication/ProtectedRoute.tsx b/frontend/src/components/authentication/ProtectedRoute.tsx similarity index 87% rename from app/src/components/authentication/ProtectedRoute.tsx rename to frontend/src/components/authentication/ProtectedRoute.tsx index a71bea81..759acb60 100644 --- a/app/src/components/authentication/ProtectedRoute.tsx +++ b/frontend/src/components/authentication/ProtectedRoute.tsx @@ -2,11 +2,11 @@ import React from 'react'; import {Navigate, useLocation} from 'react-router-dom'; import {useAuth} from '../../app/hooks/useAuth'; import {Loading} from '../common'; -import {useUserQuery} from '../../services/auth'; +import {useCurrentUserQuery} from '../../services/user'; export const ProtectedRoute = ({children}: {children: JSX.Element}) => { const {user} = useAuth(); - const {isLoading} = useUserQuery(); + const {isLoading} = useCurrentUserQuery(); const location = useLocation(); // show loader while fetching data unless user already exists and is logged in diff --git a/app/src/components/authentication/ResetPasswordContext.tsx b/frontend/src/components/authentication/ResetPasswordContext.tsx similarity index 100% rename from app/src/components/authentication/ResetPasswordContext.tsx rename to frontend/src/components/authentication/ResetPasswordContext.tsx diff --git a/app/src/components/authentication/SignInForm.tsx b/frontend/src/components/authentication/SignInForm.tsx similarity index 87% rename from app/src/components/authentication/SignInForm.tsx rename to frontend/src/components/authentication/SignInForm.tsx index 032de992..3c3f7cac 100644 --- a/app/src/components/authentication/SignInForm.tsx +++ b/frontend/src/components/authentication/SignInForm.tsx @@ -1,12 +1,12 @@ import { Button, Stack, - Divider, + // Divider, Link, TextField, CircularProgress, } from '@mui/material'; -import GoogleIcon from '@mui/icons-material/Google'; +// import GoogleIcon from '@mui/icons-material/Google'; import {useFormik} from 'formik'; import {object, string} from 'yup'; @@ -14,8 +14,7 @@ import {SignInRequest} from '../../services/auth'; import {PasswordField} from './PasswordField'; interface SignInFormProps { - signInIsLoading: boolean; - getTokenIsLoading: boolean; + isLoading: boolean; onSubmit: ({email, password}: SignInRequest) => Promise; } @@ -24,11 +23,7 @@ const validationSchema = object({ password: string().required('password is required'), }); -export const SignInForm = ({ - onSubmit, - signInIsLoading, - getTokenIsLoading, -}: SignInFormProps) => { +export const SignInForm = ({onSubmit, isLoading}: SignInFormProps) => { const { handleSubmit, handleChange, @@ -89,19 +84,20 @@ export const SignInForm = ({ - or - + */} ); }; diff --git a/app/src/components/authentication/SignUpForm.tsx b/frontend/src/components/authentication/SignUpForm.tsx similarity index 79% rename from app/src/components/authentication/SignUpForm.tsx rename to frontend/src/components/authentication/SignUpForm.tsx index 4efcd2d3..14111422 100644 --- a/app/src/components/authentication/SignUpForm.tsx +++ b/frontend/src/components/authentication/SignUpForm.tsx @@ -1,39 +1,29 @@ import React from 'react'; import { - Divider, + // Divider, Stack, Button, TextField, CircularProgress, } from '@mui/material'; -import GoogleIcon from '@mui/icons-material/Google'; +// import GoogleIcon from '@mui/icons-material/Google'; import {useFormik} from 'formik'; -import {SignUpHostRequest, SignUpCoordinatorRequest} from '../../services/auth'; +import {SignUpRequest} from '../../services/auth'; import {PasswordValidation} from '../common/PasswordValidation'; import {signUpVaildationSchema} from '../../utils/PasswordValidationSchema'; import {PasswordField} from './PasswordField'; export interface SignUpFormProps { - // sign up according to host/coordinator + isLoading: boolean; onSubmit: ({ email, password, firstName, lastName, - }: SignUpHostRequest | SignUpCoordinatorRequest) => Promise; - type: string; - getTokenIsLoading: boolean; - signUpHostIsLoading: boolean; - signUpCoordinatorIsLoading: boolean; + }: Omit) => Promise; } -export const SignUpForm = ({ - onSubmit, - type, - getTokenIsLoading, - signUpHostIsLoading, - signUpCoordinatorIsLoading, -}: SignUpFormProps) => { +export const SignUpForm = ({onSubmit, isLoading}: SignUpFormProps) => { const { handleSubmit, handleChange, @@ -119,22 +109,18 @@ export const SignUpForm = ({ variant="contained" size="large" type="submit" - disabled={ - !isValid || - !dirty || - signUpHostIsLoading || - signUpCoordinatorIsLoading - } + disabled={!isValid || !dirty || isLoading} fullWidth > Sign up - {signUpHostIsLoading || signUpCoordinatorIsLoading ? ( + {isLoading ? ( ) : null} - or - + */} ); }; diff --git a/app/src/components/authentication/__tests__/CodeField.test.tsx b/frontend/src/components/authentication/__tests__/CodeField.test.tsx similarity index 100% rename from app/src/components/authentication/__tests__/CodeField.test.tsx rename to frontend/src/components/authentication/__tests__/CodeField.test.tsx diff --git a/app/src/components/authentication/__tests__/SignInForm.test.tsx b/frontend/src/components/authentication/__tests__/SignInForm.test.tsx similarity index 100% rename from app/src/components/authentication/__tests__/SignInForm.test.tsx rename to frontend/src/components/authentication/__tests__/SignInForm.test.tsx diff --git a/app/src/components/authentication/__tests__/SignUpForm.test.tsx b/frontend/src/components/authentication/__tests__/SignUpForm.test.tsx similarity index 100% rename from app/src/components/authentication/__tests__/SignUpForm.test.tsx rename to frontend/src/components/authentication/__tests__/SignUpForm.test.tsx diff --git a/app/src/components/authentication/hooks/useAuthenticateWithOAuth.ts b/frontend/src/components/authentication/hooks/useAuthenticateWithOAuth.ts similarity index 93% rename from app/src/components/authentication/hooks/useAuthenticateWithOAuth.ts rename to frontend/src/components/authentication/hooks/useAuthenticateWithOAuth.ts index 393e3be9..caf3b3cb 100644 --- a/app/src/components/authentication/hooks/useAuthenticateWithOAuth.ts +++ b/frontend/src/components/authentication/hooks/useAuthenticateWithOAuth.ts @@ -14,10 +14,10 @@ import { // TODO: Maybe store this in a more global location? with routes? export const redirectsByRole = { - Guest: '/guest', - Host: '/host', - Coordinator: '/coordinator', - Admin: '/coordinator', + guest: '/guest', + host: '/host', + coordinator: '/coordinator', + admin: '/coordinator', }; interface UseAuthenticateWithOAuth { @@ -55,7 +55,7 @@ export const useAuthenticateWithOAuth = ({ .then(response => { const {token, user} = response; dispatch(setCredentials({user, token})); - navigate(redirectsByRole[user.role.name]); + navigate(redirectsByRole[user.role.type]); }) .catch(err => { if (isFetchBaseQueryError(err)) { diff --git a/app/src/components/authentication/index.ts b/frontend/src/components/authentication/index.ts similarity index 100% rename from app/src/components/authentication/index.ts rename to frontend/src/components/authentication/index.ts diff --git a/app/src/components/common/AppContainer.tsx b/frontend/src/components/common/AppContainer.tsx similarity index 100% rename from app/src/components/common/AppContainer.tsx rename to frontend/src/components/common/AppContainer.tsx diff --git a/app/src/components/common/AppLayout.tsx b/frontend/src/components/common/AppLayout.tsx similarity index 100% rename from app/src/components/common/AppLayout.tsx rename to frontend/src/components/common/AppLayout.tsx diff --git a/app/src/components/common/AuthenticatedHeader.tsx b/frontend/src/components/common/AuthenticatedHeader.tsx similarity index 95% rename from app/src/components/common/AuthenticatedHeader.tsx rename to frontend/src/components/common/AuthenticatedHeader.tsx index 52f58bb2..273b230e 100644 --- a/app/src/components/common/AuthenticatedHeader.tsx +++ b/frontend/src/components/common/AuthenticatedHeader.tsx @@ -7,9 +7,9 @@ import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; import {Tooltip, Avatar, Menu, MenuItem, Stack} from '@mui/material'; import logo from '../../img/favicon.png'; -import {UserRole, useSignOutMutation} from '../../services/auth'; -import {selectCurrentUser} from '../../../src/app/authSlice'; -import {User} from '../../services/auth'; +import {useSignOutMutation} from '../../services/auth'; +import {selectCurrentUser} from '../../app/authSlice'; +import {User, UserRole} from '../../services/user'; import {useSelector} from 'react-redux'; import {Link, useNavigate} from 'react-router-dom'; diff --git a/app/src/components/common/Avatar.tsx b/frontend/src/components/common/Avatar.tsx similarity index 100% rename from app/src/components/common/Avatar.tsx rename to frontend/src/components/common/Avatar.tsx diff --git a/app/src/components/common/GuestInviteButton.tsx b/frontend/src/components/common/GuestInviteButton.tsx similarity index 100% rename from app/src/components/common/GuestInviteButton.tsx rename to frontend/src/components/common/GuestInviteButton.tsx diff --git a/app/src/components/common/Header.tsx b/frontend/src/components/common/Header.tsx similarity index 100% rename from app/src/components/common/Header.tsx rename to frontend/src/components/common/Header.tsx diff --git a/app/src/components/common/HomeLink.tsx b/frontend/src/components/common/HomeLink.tsx similarity index 100% rename from app/src/components/common/HomeLink.tsx rename to frontend/src/components/common/HomeLink.tsx diff --git a/app/src/components/common/Loading.tsx b/frontend/src/components/common/Loading.tsx similarity index 100% rename from app/src/components/common/Loading.tsx rename to frontend/src/components/common/Loading.tsx diff --git a/app/src/components/common/PasswordValidation.tsx b/frontend/src/components/common/PasswordValidation.tsx similarity index 100% rename from app/src/components/common/PasswordValidation.tsx rename to frontend/src/components/common/PasswordValidation.tsx diff --git a/app/src/components/common/ProgressBar.tsx b/frontend/src/components/common/ProgressBar.tsx similarity index 100% rename from app/src/components/common/ProgressBar.tsx rename to frontend/src/components/common/ProgressBar.tsx diff --git a/app/src/components/common/UiPlaceholder.tsx b/frontend/src/components/common/UiPlaceholder.tsx similarity index 100% rename from app/src/components/common/UiPlaceholder.tsx rename to frontend/src/components/common/UiPlaceholder.tsx diff --git a/app/src/components/common/__tests__/GuestInviteButton.test.tsx b/frontend/src/components/common/__tests__/GuestInviteButton.test.tsx similarity index 100% rename from app/src/components/common/__tests__/GuestInviteButton.test.tsx rename to frontend/src/components/common/__tests__/GuestInviteButton.test.tsx diff --git a/app/src/components/common/index.ts b/frontend/src/components/common/index.ts similarity index 100% rename from app/src/components/common/index.ts rename to frontend/src/components/common/index.ts diff --git a/app/src/components/dashboard/CoordinatorContact.tsx b/frontend/src/components/dashboard/CoordinatorContact.tsx similarity index 100% rename from app/src/components/dashboard/CoordinatorContact.tsx rename to frontend/src/components/dashboard/CoordinatorContact.tsx diff --git a/app/src/components/dashboard/DashboardTask.tsx b/frontend/src/components/dashboard/DashboardTask.tsx similarity index 100% rename from app/src/components/dashboard/DashboardTask.tsx rename to frontend/src/components/dashboard/DashboardTask.tsx diff --git a/app/src/components/dashboard/DashboardTaskAccordion.tsx b/frontend/src/components/dashboard/DashboardTaskAccordion.tsx similarity index 100% rename from app/src/components/dashboard/DashboardTaskAccordion.tsx rename to frontend/src/components/dashboard/DashboardTaskAccordion.tsx diff --git a/app/src/components/dashboard/__tests__/CoordinatorContact.test.tsx b/frontend/src/components/dashboard/__tests__/CoordinatorContact.test.tsx similarity index 100% rename from app/src/components/dashboard/__tests__/CoordinatorContact.test.tsx rename to frontend/src/components/dashboard/__tests__/CoordinatorContact.test.tsx diff --git a/app/src/components/dashboard/__tests__/DashboardTask.test.tsx b/frontend/src/components/dashboard/__tests__/DashboardTask.test.tsx similarity index 100% rename from app/src/components/dashboard/__tests__/DashboardTask.test.tsx rename to frontend/src/components/dashboard/__tests__/DashboardTask.test.tsx diff --git a/app/src/components/dashboard/__tests__/DashboardTaskAccordion.test.tsx b/frontend/src/components/dashboard/__tests__/DashboardTaskAccordion.test.tsx similarity index 100% rename from app/src/components/dashboard/__tests__/DashboardTaskAccordion.test.tsx rename to frontend/src/components/dashboard/__tests__/DashboardTaskAccordion.test.tsx diff --git a/app/src/components/dashboard/index.ts b/frontend/src/components/dashboard/index.ts similarity index 100% rename from app/src/components/dashboard/index.ts rename to frontend/src/components/dashboard/index.ts diff --git a/app/src/components/intake-profile/IntakeProfileGroups.tsx b/frontend/src/components/intake-profile/IntakeProfileGroups.tsx similarity index 99% rename from app/src/components/intake-profile/IntakeProfileGroups.tsx rename to frontend/src/components/intake-profile/IntakeProfileGroups.tsx index 342c15cf..e2d3a523 100644 --- a/app/src/components/intake-profile/IntakeProfileGroups.tsx +++ b/frontend/src/components/intake-profile/IntakeProfileGroups.tsx @@ -19,11 +19,10 @@ import {useOutletContext} from 'react-router-dom'; import {InitialValues} from 'src/views/IntakeProfile'; import {AdditionalGuestsField} from './fields/AdditionaGuestsField'; import {FieldGroup, Fields, Guest, Pet} from 'src/services/profile'; -import {AdditionalPetsField} from './AdditionalPetsField'; +import {AdditionalPetsField} from './fields/AdditionalPetsField'; import {phoneRegExp} from '../../views/IntakeProfile/constants/index'; import {DatePickerField} from './fields/DatePickerField'; - export interface OutletContext { groupId: string; fieldGroups: FieldGroup[]; diff --git a/app/src/components/intake-profile/ProfileReview.tsx b/frontend/src/components/intake-profile/ProfileReview.tsx similarity index 100% rename from app/src/components/intake-profile/ProfileReview.tsx rename to frontend/src/components/intake-profile/ProfileReview.tsx diff --git a/app/src/components/intake-profile/ProfileSidebar.tsx b/frontend/src/components/intake-profile/ProfileSidebar.tsx similarity index 100% rename from app/src/components/intake-profile/ProfileSidebar.tsx rename to frontend/src/components/intake-profile/ProfileSidebar.tsx diff --git a/app/src/components/intake-profile/SidebarButton.tsx b/frontend/src/components/intake-profile/SidebarButton.tsx similarity index 100% rename from app/src/components/intake-profile/SidebarButton.tsx rename to frontend/src/components/intake-profile/SidebarButton.tsx diff --git a/app/src/components/intake-profile/fields/AdditionaGuestsField.tsx b/frontend/src/components/intake-profile/fields/AdditionaGuestsField.tsx similarity index 100% rename from app/src/components/intake-profile/fields/AdditionaGuestsField.tsx rename to frontend/src/components/intake-profile/fields/AdditionaGuestsField.tsx diff --git a/app/src/components/intake-profile/fields/AdditionalPetsField.tsx b/frontend/src/components/intake-profile/fields/AdditionalPetsField.tsx similarity index 100% rename from app/src/components/intake-profile/fields/AdditionalPetsField.tsx rename to frontend/src/components/intake-profile/fields/AdditionalPetsField.tsx diff --git a/app/src/components/intake-profile/fields/DatePickerField.tsx b/frontend/src/components/intake-profile/fields/DatePickerField.tsx similarity index 100% rename from app/src/components/intake-profile/fields/DatePickerField.tsx rename to frontend/src/components/intake-profile/fields/DatePickerField.tsx diff --git a/app/src/components/intake-profile/hooks/useStatusStyling.tsx b/frontend/src/components/intake-profile/hooks/useStatusStyling.tsx similarity index 100% rename from app/src/components/intake-profile/hooks/useStatusStyling.tsx rename to frontend/src/components/intake-profile/hooks/useStatusStyling.tsx diff --git a/app/src/components/layout/CoordinatorDashboardLayout.tsx b/frontend/src/components/layout/CoordinatorDashboardLayout.tsx similarity index 100% rename from app/src/components/layout/CoordinatorDashboardLayout.tsx rename to frontend/src/components/layout/CoordinatorDashboardLayout.tsx diff --git a/app/src/components/layout/DashboardLayout.tsx b/frontend/src/components/layout/DashboardLayout.tsx similarity index 100% rename from app/src/components/layout/DashboardLayout.tsx rename to frontend/src/components/layout/DashboardLayout.tsx diff --git a/app/src/components/layout/GuestDashboardLayout.tsx b/frontend/src/components/layout/GuestDashboardLayout.tsx similarity index 100% rename from app/src/components/layout/GuestDashboardLayout.tsx rename to frontend/src/components/layout/GuestDashboardLayout.tsx diff --git a/app/src/components/layout/index.ts b/frontend/src/components/layout/index.ts similarity index 100% rename from app/src/components/layout/index.ts rename to frontend/src/components/layout/index.ts diff --git a/app/src/favicon.svg b/frontend/src/favicon.svg similarity index 100% rename from app/src/favicon.svg rename to frontend/src/favicon.svg diff --git a/app/src/img/admin-icon.svg b/frontend/src/img/admin-icon.svg similarity index 100% rename from app/src/img/admin-icon.svg rename to frontend/src/img/admin-icon.svg diff --git a/app/src/img/avatar.png b/frontend/src/img/avatar.png similarity index 100% rename from app/src/img/avatar.png rename to frontend/src/img/avatar.png diff --git a/app/src/img/favicon.png b/frontend/src/img/favicon.png similarity index 100% rename from app/src/img/favicon.png rename to frontend/src/img/favicon.png diff --git a/app/src/img/guest-icon.svg b/frontend/src/img/guest-icon.svg similarity index 100% rename from app/src/img/guest-icon.svg rename to frontend/src/img/guest-icon.svg diff --git a/app/src/img/host-icon.svg b/frontend/src/img/host-icon.svg similarity index 100% rename from app/src/img/host-icon.svg rename to frontend/src/img/host-icon.svg diff --git a/app/src/img/huu.svg b/frontend/src/img/huu.svg similarity index 100% rename from app/src/img/huu.svg rename to frontend/src/img/huu.svg diff --git a/app/src/img/spy.png b/frontend/src/img/spy.png similarity index 100% rename from app/src/img/spy.png rename to frontend/src/img/spy.png diff --git a/app/src/index.css b/frontend/src/index.css similarity index 100% rename from app/src/index.css rename to frontend/src/index.css diff --git a/app/src/logo.svg b/frontend/src/logo.svg similarity index 100% rename from app/src/logo.svg rename to frontend/src/logo.svg diff --git a/app/src/main.tsx b/frontend/src/main.tsx similarity index 100% rename from app/src/main.tsx rename to frontend/src/main.tsx diff --git a/app/src/services/api.ts b/frontend/src/services/api.ts similarity index 94% rename from app/src/services/api.ts rename to frontend/src/services/api.ts index b58082b9..94a1c0dd 100644 --- a/app/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -31,7 +31,8 @@ const baseQueryWithReAuth: BaseQueryFn< > = async (args, api, extraOptions) => { let result = await baseQuery(args, api, extraOptions); // if token is expired, try to refresh it - if (result.error && result.error.status === 401) { + // TODO: Possibly refactor to only try again when error is of a certaion type/status + if (result.error) { // make request to retrieve new tokens const refreshResult = await baseQuery('auth/refresh', api, extraOptions); @@ -42,7 +43,6 @@ const baseQueryWithReAuth: BaseQueryFn< // to give us a flexible but strongly-typed interface to rely on const {data} = refreshResult as {data: {token: string}}; // store new token - // api.dispatch(tokenReceived(refreshResult.data.token)); api.dispatch(tokenReceived(data.token)); // retry the intiail query result = await baseQuery(args, api, extraOptions); diff --git a/app/src/services/auth.ts b/frontend/src/services/auth.ts similarity index 77% rename from app/src/services/auth.ts rename to frontend/src/services/auth.ts index 3d0b453c..82a98665 100644 --- a/app/src/services/auth.ts +++ b/frontend/src/services/auth.ts @@ -1,40 +1,17 @@ import {api} from './api'; +import {User} from './user'; -export interface UserRole { - name: 'Guest' | 'Host' | 'Coordinator' | 'Admin'; -} -export interface User { - email: string; - firstName: string; - lastName: string; - role: UserRole; -} - -export interface UserResponse { - user: User; -} - -export interface SignUpHostResponse { +export interface SignUpResponse { user: User; token: string; } -export interface SignUpHostRequest { - firstName: string; - lastName: string; - email: string; - password: string; -} -export interface SignUpCoordinatorResponse { - user: User; - token: string; -} - -export interface SignUpCoordinatorRequest { +export interface SignUpRequest { firstName: string; lastName: string; email: string; password: string; + role: string; } export interface SignInResponse { @@ -94,18 +71,9 @@ export interface ResendConfirmationCodeResponse { const authApi = api.injectEndpoints({ endpoints: build => ({ - signUpHost: build.mutation({ - query: credentials => ({ - url: '/auth/signup/host', - method: 'POST', - withCredentials: true, - body: credentials, - }), - }), - // prettier-ignore - signUpCoordinator: build.mutation({ + signUp: build.mutation({ query: credentials => ({ - url: '/auth/signup/coordinator', + url: '/auth/signup', method: 'POST', withCredentials: true, body: credentials, @@ -151,7 +119,7 @@ const authApi = api.injectEndpoints({ }), forgotPassword: build.mutation({ query: credentials => ({ - url: 'auth/forgot_password', + url: 'auth/forgot-password', method: 'POST', withCredentials: true, body: credentials, @@ -159,7 +127,7 @@ const authApi = api.injectEndpoints({ }), confirmForgotPassword: build.mutation({ query: credentials => ({ - url: 'auth/forgot_password/confirm', + url: 'auth/forgot-password/confirm', method: 'POST', withCredentials: true, body: credentials, @@ -193,13 +161,6 @@ const authApi = api.injectEndpoints({ withCredentials: true, }), }), - user: build.query({ - query: () => ({ - url: 'auth/user', - method: 'GET', - withCredentials: true, - }), - }), resendConfirmationCode: build.mutation< ResendConfirmationCodeResponse, ResendConfirmationCodeRequest @@ -217,8 +178,7 @@ const authApi = api.injectEndpoints({ export {authApi}; export const { - useSignUpHostMutation, - useSignUpCoordinatorMutation, + useSignUpMutation, useSignInMutation, useSignOutMutation, useVerificationMutation, @@ -228,7 +188,6 @@ export const { useForgotPasswordMutation, useConfirmForgotPasswordMutation, useSessionMutation, - useUserQuery, usePrivateQuery, useResendConfirmationCodeMutation, } = authApi; diff --git a/app/src/services/coordinator.ts b/frontend/src/services/coordinator.ts similarity index 100% rename from app/src/services/coordinator.ts rename to frontend/src/services/coordinator.ts diff --git a/app/src/services/host.ts b/frontend/src/services/host.ts similarity index 100% rename from app/src/services/host.ts rename to frontend/src/services/host.ts diff --git a/app/src/services/profile.ts b/frontend/src/services/profile.ts similarity index 100% rename from app/src/services/profile.ts rename to frontend/src/services/profile.ts diff --git a/frontend/src/services/user.ts b/frontend/src/services/user.ts new file mode 100644 index 00000000..5656e154 --- /dev/null +++ b/frontend/src/services/user.ts @@ -0,0 +1,27 @@ +import {api} from './api'; + +export interface UserRole { + type: 'guest' | 'host' | 'coordinator' | 'admin'; +} +export interface User { + email: string; + firstName: string; + lastName: string; + role: UserRole; +} + +const userAPI = api.injectEndpoints({ + endpoints: build => ({ + currentUser: build.query({ + query: () => ({ + url: 'users/current', + method: 'GET', + withCredentials: true, + }), + }), + }), +}); + +export {userAPI}; + +export const {useCurrentUserQuery} = userAPI; diff --git a/app/src/theme/index.ts b/frontend/src/theme/index.ts similarity index 100% rename from app/src/theme/index.ts rename to frontend/src/theme/index.ts diff --git a/app/src/theme/overrides/Button.ts b/frontend/src/theme/overrides/Button.ts similarity index 100% rename from app/src/theme/overrides/Button.ts rename to frontend/src/theme/overrides/Button.ts diff --git a/app/src/theme/overrides/Divider.ts b/frontend/src/theme/overrides/Divider.ts similarity index 100% rename from app/src/theme/overrides/Divider.ts rename to frontend/src/theme/overrides/Divider.ts diff --git a/app/src/theme/overrides/InputLabel.ts b/frontend/src/theme/overrides/InputLabel.ts similarity index 100% rename from app/src/theme/overrides/InputLabel.ts rename to frontend/src/theme/overrides/InputLabel.ts diff --git a/app/src/theme/overrides/Link.tsx b/frontend/src/theme/overrides/Link.tsx similarity index 100% rename from app/src/theme/overrides/Link.tsx rename to frontend/src/theme/overrides/Link.tsx diff --git a/app/src/theme/overrides/OutlinedInput.ts b/frontend/src/theme/overrides/OutlinedInput.ts similarity index 100% rename from app/src/theme/overrides/OutlinedInput.ts rename to frontend/src/theme/overrides/OutlinedInput.ts diff --git a/app/src/theme/overrides/index.ts b/frontend/src/theme/overrides/index.ts similarity index 100% rename from app/src/theme/overrides/index.ts rename to frontend/src/theme/overrides/index.ts diff --git a/app/src/theme/theme.ts b/frontend/src/theme/theme.ts similarity index 100% rename from app/src/theme/theme.ts rename to frontend/src/theme/theme.ts diff --git a/app/src/types/index.d.ts b/frontend/src/types/index.d.ts similarity index 100% rename from app/src/types/index.d.ts rename to frontend/src/types/index.d.ts diff --git a/app/src/utils/PasswordValidationSchema.tsx b/frontend/src/utils/PasswordValidationSchema.tsx similarity index 100% rename from app/src/utils/PasswordValidationSchema.tsx rename to frontend/src/utils/PasswordValidationSchema.tsx diff --git a/app/src/utils/test/__mocks__/fileMock.ts b/frontend/src/utils/test/__mocks__/fileMock.ts similarity index 100% rename from app/src/utils/test/__mocks__/fileMock.ts rename to frontend/src/utils/test/__mocks__/fileMock.ts diff --git a/app/src/utils/test/browser.ts b/frontend/src/utils/test/browser.ts similarity index 97% rename from app/src/utils/test/browser.ts rename to frontend/src/utils/test/browser.ts index 9999dc0e..67277d73 100644 --- a/app/src/utils/test/browser.ts +++ b/frontend/src/utils/test/browser.ts @@ -14,7 +14,7 @@ export const enableMocking = async () => { onUnhandledRequest(req, print) { // Ignore any requests from these URLs. const excludedRoutes = [ - '/api/auth/user', + '/api/user', '/api/auth/session', '/api/auth/refresh', ]; diff --git a/app/src/utils/test/db/profile.ts b/frontend/src/utils/test/db/profile.ts similarity index 100% rename from app/src/utils/test/db/profile.ts rename to frontend/src/utils/test/db/profile.ts diff --git a/app/src/utils/test/handlers/auth.ts b/frontend/src/utils/test/handlers/auth.ts similarity index 100% rename from app/src/utils/test/handlers/auth.ts rename to frontend/src/utils/test/handlers/auth.ts diff --git a/app/src/utils/test/handlers/profile.ts b/frontend/src/utils/test/handlers/profile.ts similarity index 100% rename from app/src/utils/test/handlers/profile.ts rename to frontend/src/utils/test/handlers/profile.ts diff --git a/app/src/utils/test/server.ts b/frontend/src/utils/test/server.ts similarity index 100% rename from app/src/utils/test/server.ts rename to frontend/src/utils/test/server.ts diff --git a/app/src/utils/test/setupTests.ts b/frontend/src/utils/test/setupTests.ts similarity index 100% rename from app/src/utils/test/setupTests.ts rename to frontend/src/utils/test/setupTests.ts diff --git a/app/src/utils/test/test-utils.tsx b/frontend/src/utils/test/test-utils.tsx similarity index 100% rename from app/src/utils/test/test-utils.tsx rename to frontend/src/utils/test/test-utils.tsx diff --git a/app/src/views/AccountVerification.tsx b/frontend/src/views/AccountVerification.tsx similarity index 100% rename from app/src/views/AccountVerification.tsx rename to frontend/src/views/AccountVerification.tsx diff --git a/app/src/views/ConfirmSignUp.tsx b/frontend/src/views/ConfirmSignUp.tsx similarity index 100% rename from app/src/views/ConfirmSignUp.tsx rename to frontend/src/views/ConfirmSignUp.tsx diff --git a/app/src/views/CoordinatorDashboard.tsx b/frontend/src/views/CoordinatorDashboard.tsx similarity index 100% rename from app/src/views/CoordinatorDashboard.tsx rename to frontend/src/views/CoordinatorDashboard.tsx diff --git a/app/src/views/EmailVerificationError.tsx b/frontend/src/views/EmailVerificationError.tsx similarity index 100% rename from app/src/views/EmailVerificationError.tsx rename to frontend/src/views/EmailVerificationError.tsx diff --git a/app/src/views/EmailVerificationSuccess.tsx b/frontend/src/views/EmailVerificationSuccess.tsx similarity index 100% rename from app/src/views/EmailVerificationSuccess.tsx rename to frontend/src/views/EmailVerificationSuccess.tsx diff --git a/app/src/views/ForgotPassword.tsx b/frontend/src/views/ForgotPassword.tsx similarity index 100% rename from app/src/views/ForgotPassword.tsx rename to frontend/src/views/ForgotPassword.tsx diff --git a/app/src/views/ForgotPasswordCode.tsx b/frontend/src/views/ForgotPasswordCode.tsx similarity index 100% rename from app/src/views/ForgotPasswordCode.tsx rename to frontend/src/views/ForgotPasswordCode.tsx diff --git a/app/src/views/ForgotPasswordSuccess.tsx b/frontend/src/views/ForgotPasswordSuccess.tsx similarity index 100% rename from app/src/views/ForgotPasswordSuccess.tsx rename to frontend/src/views/ForgotPasswordSuccess.tsx diff --git a/app/src/views/GuestApplicationTracker.tsx b/frontend/src/views/GuestApplicationTracker.tsx similarity index 100% rename from app/src/views/GuestApplicationTracker.tsx rename to frontend/src/views/GuestApplicationTracker.tsx diff --git a/app/src/views/GuestContacts.tsx b/frontend/src/views/GuestContacts.tsx similarity index 100% rename from app/src/views/GuestContacts.tsx rename to frontend/src/views/GuestContacts.tsx diff --git a/app/src/views/GuestDocuments.tsx b/frontend/src/views/GuestDocuments.tsx similarity index 100% rename from app/src/views/GuestDocuments.tsx rename to frontend/src/views/GuestDocuments.tsx diff --git a/app/src/views/GuestSettings.tsx b/frontend/src/views/GuestSettings.tsx similarity index 100% rename from app/src/views/GuestSettings.tsx rename to frontend/src/views/GuestSettings.tsx diff --git a/app/src/views/GuestTasks.tsx b/frontend/src/views/GuestTasks.tsx similarity index 100% rename from app/src/views/GuestTasks.tsx rename to frontend/src/views/GuestTasks.tsx diff --git a/app/src/views/Home.tsx b/frontend/src/views/Home.tsx similarity index 100% rename from app/src/views/Home.tsx rename to frontend/src/views/Home.tsx diff --git a/app/src/views/HostApplicationTracker.tsx b/frontend/src/views/HostApplicationTracker.tsx similarity index 100% rename from app/src/views/HostApplicationTracker.tsx rename to frontend/src/views/HostApplicationTracker.tsx diff --git a/app/src/views/HostsList.tsx b/frontend/src/views/HostsList.tsx similarity index 100% rename from app/src/views/HostsList.tsx rename to frontend/src/views/HostsList.tsx diff --git a/app/src/views/IntakeProfile/constants/buildValidationSchema.ts b/frontend/src/views/IntakeProfile/constants/buildValidationSchema.ts similarity index 100% rename from app/src/views/IntakeProfile/constants/buildValidationSchema.ts rename to frontend/src/views/IntakeProfile/constants/buildValidationSchema.ts diff --git a/app/src/views/IntakeProfile/constants/createInitialValues.ts b/frontend/src/views/IntakeProfile/constants/createInitialValues.ts similarity index 100% rename from app/src/views/IntakeProfile/constants/createInitialValues.ts rename to frontend/src/views/IntakeProfile/constants/createInitialValues.ts diff --git a/app/src/views/IntakeProfile/constants/index.ts b/frontend/src/views/IntakeProfile/constants/index.ts similarity index 99% rename from app/src/views/IntakeProfile/constants/index.ts rename to frontend/src/views/IntakeProfile/constants/index.ts index e9e3eb67..aa7c7304 100644 --- a/app/src/views/IntakeProfile/constants/index.ts +++ b/frontend/src/views/IntakeProfile/constants/index.ts @@ -1,4 +1,3 @@ - import {faker} from '@faker-js/faker'; import {array, object, string} from 'yup'; import {InitialValues} from '..'; @@ -189,4 +188,3 @@ export const buildValidationSchema = ( [groupId]: object().shape({...schema.fields}), }); }; - diff --git a/app/src/views/IntakeProfile/index.tsx b/frontend/src/views/IntakeProfile/index.tsx similarity index 100% rename from app/src/views/IntakeProfile/index.tsx rename to frontend/src/views/IntakeProfile/index.tsx diff --git a/app/src/views/LoadingComponent.tsx b/frontend/src/views/LoadingComponent.tsx similarity index 100% rename from app/src/views/LoadingComponent.tsx rename to frontend/src/views/LoadingComponent.tsx diff --git a/app/src/views/NewPassword.tsx b/frontend/src/views/NewPassword.tsx similarity index 100% rename from app/src/views/NewPassword.tsx rename to frontend/src/views/NewPassword.tsx diff --git a/app/src/views/ResetPassword.tsx b/frontend/src/views/ResetPassword.tsx similarity index 100% rename from app/src/views/ResetPassword.tsx rename to frontend/src/views/ResetPassword.tsx diff --git a/app/src/views/SelectAccountType.tsx b/frontend/src/views/SelectAccountType.tsx similarity index 100% rename from app/src/views/SelectAccountType.tsx rename to frontend/src/views/SelectAccountType.tsx diff --git a/app/src/views/Settings.tsx b/frontend/src/views/Settings.tsx similarity index 100% rename from app/src/views/Settings.tsx rename to frontend/src/views/Settings.tsx diff --git a/app/src/views/SignIn.tsx b/frontend/src/views/SignIn.tsx similarity index 95% rename from app/src/views/SignIn.tsx rename to frontend/src/views/SignIn.tsx index 83eccdbf..63df4a07 100644 --- a/app/src/views/SignIn.tsx +++ b/frontend/src/views/SignIn.tsx @@ -57,7 +57,7 @@ export const SignIn = () => { dispatch(setCredentials({user, token})); - navigate(redirectsByRole[user.role.name]); + navigate(redirectsByRole[user.role.type]); } catch (err) { if (isFetchBaseQueryError(err)) { // you can access all properties of `FetchBaseQueryError` here @@ -102,8 +102,7 @@ export const SignIn = () => { diff --git a/app/src/views/SignUp.tsx b/frontend/src/views/SignUp.tsx similarity index 75% rename from app/src/views/SignUp.tsx rename to frontend/src/views/SignUp.tsx index dc632032..91f9712c 100644 --- a/app/src/views/SignUp.tsx +++ b/frontend/src/views/SignUp.tsx @@ -12,11 +12,9 @@ import CloseIcon from '@mui/icons-material/Close'; import {useNavigate, useParams} from 'react-router-dom'; import {SignUpForm} from '../components/authentication/SignUpForm'; import { - SignUpHostRequest, - SignUpCoordinatorRequest, - useSignUpHostMutation, - useSignUpCoordinatorMutation, useGoogleSignUpMutation, + useSignUpMutation, + SignUpRequest, } from '../services/auth'; import {isErrorWithMessage, isFetchBaseQueryError} from '../app/helpers'; import {FormContainer} from '../components/authentication'; @@ -27,10 +25,8 @@ export const SignUp = () => { const {type} = useParams(); const navigate = useNavigate(); - const [signUpHost, {isLoading: signUpHostIsLoading}] = - useSignUpHostMutation(); - const [signUpCoordinator, {isLoading: signUpCoordinatorIsLoading}] = - useSignUpCoordinatorMutation(); + const [signUp, {isLoading: signUpIsLoading}] = useSignUpMutation(); + const [googleSignUp, {isLoading: getTokenIsLoading}] = useGoogleSignUpMutation(); // get type from params @@ -51,25 +47,19 @@ export const SignUp = () => { password, firstName, lastName, - }: SignUpHostRequest | SignUpCoordinatorRequest) => { - try { - if (type === 'host') { - await signUpHost({ - firstName, - lastName, - email, - password, - }).unwrap(); - } + }: Omit) => { + if (!type) { + throw new Error('User type is required'); + } - if (type === 'coordinator') { - await signUpCoordinator({ - firstName, - lastName, - email, - password, - }).unwrap(); - } + try { + await signUp({ + email, + password, + firstName, + lastName, + role: type, + }); navigate(`/signup/success?email=${email}`); } catch (err) { @@ -116,11 +106,7 @@ export const SignUp = () => { diff --git a/app/src/views/SystemAdminDashboard.tsx b/frontend/src/views/SystemAdminDashboard.tsx similarity index 100% rename from app/src/views/SystemAdminDashboard.tsx rename to frontend/src/views/SystemAdminDashboard.tsx diff --git a/app/src/views/__tests__/ForgotPasswordCode.test.tsx b/frontend/src/views/__tests__/ForgotPasswordCode.test.tsx similarity index 100% rename from app/src/views/__tests__/ForgotPasswordCode.test.tsx rename to frontend/src/views/__tests__/ForgotPasswordCode.test.tsx diff --git a/app/src/views/__tests__/ResetPassword.test.tsx b/frontend/src/views/__tests__/ResetPassword.test.tsx similarity index 100% rename from app/src/views/__tests__/ResetPassword.test.tsx rename to frontend/src/views/__tests__/ResetPassword.test.tsx diff --git a/app/src/views/index.ts b/frontend/src/views/index.ts similarity index 100% rename from app/src/views/index.ts rename to frontend/src/views/index.ts diff --git a/app/src/vite-env.d.ts b/frontend/src/vite-env.d.ts similarity index 100% rename from app/src/vite-env.d.ts rename to frontend/src/vite-env.d.ts diff --git a/app/static/landingpage.html b/frontend/static/landingpage.html similarity index 100% rename from app/static/landingpage.html rename to frontend/static/landingpage.html diff --git a/app/tsconfig.json b/frontend/tsconfig.json similarity index 100% rename from app/tsconfig.json rename to frontend/tsconfig.json diff --git a/app/tsconfig.node.json b/frontend/tsconfig.node.json similarity index 100% rename from app/tsconfig.node.json rename to frontend/tsconfig.node.json diff --git a/app/vite.config.ts b/frontend/vite.config.ts similarity index 96% rename from app/vite.config.ts rename to frontend/vite.config.ts index 9c8ecad2..a4653185 100644 --- a/app/vite.config.ts +++ b/frontend/vite.config.ts @@ -11,7 +11,7 @@ function huuApiBaseUrl(envHuuApiBaseUrl: string, mode: string): URL | never { return new URL(envHuuApiBaseUrl); } catch { if (mode === 'development' || mode === 'test') { - return new URL('http://localhost:38429/api'); + return new URL('http://localhost:8000/api'); } else { throw new Error('VITE_HUU_API_BASE_URL is not configured with a URL'); } @@ -43,7 +43,7 @@ export default defineConfig(({mode}) => { }, plugins: [react()], server: { - port: 38428, + port: 34828, proxy: { '/api': { target: apiBaseUrl.origin,