diff --git a/.gitignore b/.gitignore index 7bf21e4..750af55 100644 --- a/.gitignore +++ b/.gitignore @@ -264,4 +264,7 @@ coverage.xml .mypy_cache/ # .key -SECRET.key \ No newline at end of file +SECRET.key + +# poetry lock +poetry.lock diff --git a/README.md b/README.md index 2dbbbe6..5918016 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@
Logo -

Python Fast API boilerplate

+

Product Fast API boilerplate

Fast API boiler plate project @@ -151,6 +151,154 @@ Requirement of Project alembic downgrade -1 ``` +# Poetry Installation Guide + +This guide provides detailed instructions on how to set up and use [Poetry](https://python-poetry.org/) for managing dependencies and environments in your Python project. + +## What is Poetry? + +Poetry is a dependency management and packaging tool for Python. It simplifies the process of managing project dependencies, virtual environments, and publishing packages. + +Key features: +- Dependency resolution. +- Virtual environment management. +- Project packaging and publishing. + +## Prerequisites + +- **Python**: Ensure Python is installed on your system. Poetry supports Python 3.7 and above. +- **Pip**: The Python package manager should also be installed. + +You can verify installations using the following commands: +```bash +python --version +pip --version +``` + +## Installing Poetry + +### 1. Using the Official Installer + +Run the following command to install Poetry: + +```bash +curl -sSL https://install.python-poetry.org | python3 - +``` + +### 2. Verifying Installation + +Once installed, verify Poetry by running: + +```bash +poetry --version +``` + +This should display the installed version of Poetry. + +### 3. Adding Poetry to Your PATH + +If Poetry is not recognized, ensure it is added to your system PATH. By default, Poetry is installed in: +- **Unix/macOS**: `$HOME/.local/bin` +- **Windows**: `%APPDATA%\Python\Scripts` + +Add this directory to your PATH. + +## Setting Up Poetry in a Project + +### 1. Initialize a New Project + +Navigate to your project directory and run: + +```bash +poetry init +``` + +Follow the prompts to define your project metadata (e.g., package name, version, description). + +### 2. Adding Dependencies + +To add dependencies: + +```bash +poetry add +``` + +Example: + +```bash +poetry add requests +``` + +To add development dependencies: + +```bash +poetry add --dev +``` + +### 3. Installing Dependencies + +Install all dependencies defined in `pyproject.toml`: + +```bash +poetry install +``` + +### 4. Using Virtual Environments + +Poetry automatically creates a virtual environment for your project. To activate it: + +```bash +poetry shell +``` + +To deactivate, simply exit the shell: + +```bash +exit +``` + +## Managing Your Project + +### Updating Dependencies + +To update dependencies to their latest compatible versions: + +```bash +poetry update +``` + +### Listing Installed Packages + +To list all installed packages and their versions: + +```bash +poetry show +``` + +### Publishing Your Package + +If you’re packaging your project, publish it to PyPI with: + +```bash +poetry publish --build +``` + +## Uninstalling Poetry + +To uninstall Poetry, remove its files: + +```bash +curl -sSL https://install.python-poetry.org | python3 - --uninstall +``` + +## Additional Resources + +- [Poetry Documentation](https://python-poetry.org/docs/) +- [Poetry GitHub Repository](https://github.com/python-poetry/poetry) + +With this setup, you can efficiently manage Python project dependencies and environments using Poetry. Happy coding! + + ## Run the server in development mode Add environment variables (given in .env) by running following command in cmd/terminal: diff --git a/apps/server.py b/apps/server.py index d214d14..498f37d 100644 --- a/apps/server.py +++ b/apps/server.py @@ -6,6 +6,9 @@ from fastapi.responses import JSONResponse # from core.config import config +from middleware.response_log_middleware import ResponseLogMiddleware +from middleware.rate_limiting_middleware import RateLimitMiddleware +from apps.v1.api.auth.view import authrouter from config import project_path, LoggingConfig from core import CustomException from core.utils import constant_variable @@ -13,8 +16,9 @@ def init_routers(app_: FastAPI) -> None: - pass - # app_.include_router(user_router) + app_.include_router( + authrouter, prefix=f"{constant_variable.API_V1}/auth", tags=["Authentication"] + ) def init_listeners(app_: FastAPI) -> None: @@ -26,8 +30,7 @@ async def custom_exception_handler(request: Request, exc: CustomException): status_code=exc.status, content=content, ) - - + def make_middleware() -> list[Middleware]: middleware = [ Middleware( @@ -40,6 +43,14 @@ def make_middleware() -> list[Middleware]: Middleware( S3PathMiddleware, config_path=f"{project_path.S3_ROOT}/s3_paths_config.json" ), + Middleware( + ResponseLogMiddleware + ), + Middleware( + RateLimitMiddleware, + rate_limit=constant_variable.RATE_LIMIT, + time_window=constant_variable.TIME_WINDOW + ) ] return middleware @@ -49,8 +60,8 @@ def make_middleware() -> list[Middleware]: def create_app() -> FastAPI: app_ = FastAPI( - title="Hide", - description="Hide API", + title="VC_Product Fastapi Boilerplate", + description="FastAPI", version="1.0.0", # docs_url=None if config.ENV == "production" else "/docs", # redoc_url=None if config.ENV == "production" else "/redoc", diff --git a/apps/v1/api/auth/models/attribute.py b/apps/v1/api/auth/models/attribute.py index e69de29..225485f 100644 --- a/apps/v1/api/auth/models/attribute.py +++ b/apps/v1/api/auth/models/attribute.py @@ -0,0 +1,8 @@ +from core.db.db_attribute import BaseAttributes +from core.utils import constant_variable as constant + + +class SsoType(BaseAttributes): + google = constant.STATUS_ONE + facebook = constant.STATUS_TWO + apple = constant.STATUS_THREE diff --git a/apps/v1/api/auth/models/methods/method1.py b/apps/v1/api/auth/models/methods/method1.py index 558bccc..b180c36 100644 --- a/apps/v1/api/auth/models/methods/method1.py +++ b/apps/v1/api/auth/models/methods/method1.py @@ -1,21 +1,50 @@ """This module contains database operations methods.""" -from sqlalchemy.orm import Session +from sqlalchemy.future import select +from sqlalchemy.ext.asyncio import AsyncSession -class UserAuthMethod(): + +class UserAuthMethod: """This class defines methods to authenticate users.""" def __init__(self, model) -> None: self.model = model - def find_by_email(self, db: Session, email: str): - """This funtion will return the email object""" - return db.query(self.model).filter( - self.model.email == email - ).first() + async def find_by_email(self, db: AsyncSession, email: str): + """This function will return the user object by email asynchronously.""" + async with db: # Ensure the session context + stmt = select(self.model).where(self.model.email == email) + result = await db.execute(stmt) + return result.scalars().first() + + async def find_by_username(self, db: AsyncSession, username: str): + """This function will return the username object""" + async with db: # Ensure the session context + stmt = select(self.model).where(self.model.username == username) + result = await db.execute(stmt) + return result.scalars().first() + + async def find_user_by_otp_referenceId( + self, db: AsyncSession, otp_referenceId: str + ): + """This function will return the username object""" + async with db: # Ensure the session context + stmt = select(self.model).where( + self.model.otp_referenceId == otp_referenceId + ) + result = await db.execute(stmt) + return result.scalars().first() + + async def find_verified_phone_user(self, db: AsyncSession, phone: str): + """This function will return the username object""" + async with db: # Ensure the session context + stmt = select(self.model).where(self.model.phone == phone) + result = await db.execute(stmt) + return result.scalars().first() - def find_by_username(self, db: Session, username: str): + async def find_verified_email_user(self, db: AsyncSession, email: str): """This function will return the username object""" - return db.query(self.model).filter( - self.model.username == username - ).first() + async with db: # Ensure the session context + stmt = select(self.model).where(self.model.email == email) + result = await db.execute(stmt) + return result.scalars().first() diff --git a/apps/v1/api/auth/models/model.py b/apps/v1/api/auth/models/model.py index b8ff3fc..c3427af 100644 --- a/apps/v1/api/auth/models/model.py +++ b/apps/v1/api/auth/models/model.py @@ -1,8 +1,8 @@ """This module contains database model implementations.""" -from datetime import datetime - -from sqlalchemy import Column, DateTime, Integer, String +from core.utils.helper import DateTimeUtils +from sqlalchemy import Column, DateTime, Integer, String, Boolean, ForeignKey +from core.utils import constant_variable as constant from config.db_config import Base @@ -10,18 +10,76 @@ class Users(Base): """ Table used for stored the users information """ + __tablename__ = "users" - id = Column(Integer, primary_key=True, nullable=False) - uuid = Column(String(100), nullable=False, unique=True, doc='unique_id') - first_name = Column(String(255), doc='First name of user', nullable=True) - last_name = Column(String(255), doc='Last name of user', nullable=True) - email = Column(String(100), doc='Email ID of the user', nullable=True) - username = Column(String(100), doc='Username of the user', nullable=True) - password = Column(String(100), doc='Password of the user', nullable=True) - created_at = Column(DateTime, default=datetime.utcnow, nullable=False, - doc='its generate automatically when data create') - updated_at = Column(DateTime, nullable=True, - onupdate=datetime.utcnow, doc='its generate automatically when data update') - deleted_at = Column(DateTime, nullable=True, - doc='its generate automatically when data deleted') + id = Column(Integer, primary_key=True, autoincrement=True) + uuid = Column(String(100), nullable=False, unique=True, doc="unique_id") + first_name = Column(String(255), doc="First name of user", nullable=True) + last_name = Column(String(255), doc="Last name of user", nullable=True) + email = Column(String(100), doc="Email ID of the user", nullable=True) + username = Column(String(100), doc="Username of the user", nullable=True) + password = Column(String(100), doc="Password of the user", nullable=True) + created_at = Column( + DateTime, + default=DateTimeUtils().get_time, + nullable=False, + doc="its generate automatically when data create", + ) + updated_at = Column( + DateTime, + nullable=True, + onupdate=DateTimeUtils().get_time, + doc="its generate automatically when data update", + ) + deleted_at = Column( + DateTime, nullable=True, doc="its generate automatically when data deleted" + ) + + +class UserSocialLogin(Base): + """ + Table used for stored the users social login information + """ + + __tablename__ = "users_social_login" + + id = Column( + Integer, primary_key=constant.STATUS_TRUE, nullable=constant.STATUS_FALSE + ) + uuid = Column( + String(100), + nullable=constant.STATUS_FALSE, + unique=constant.STATUS_TRUE, + doc="unique_id", + ) + user_uuid = Column(String(100), ForeignKey("users.uuid", ondelete="CASCADE")) + provider = Column( + String(50), doc="social login provider name", nullable=constant.STATUS_TRUE + ) + provider_token = Column( + String(255), doc="social login provider token", nullable=constant.STATUS_TRUE + ) + status = Column( + Boolean, + default=constant.STATUS_TRUE, + doc="This flag is define userlogin active or not active", + ) + created_at = Column( + DateTime, + nullable=constant.STATUS_FALSE, + default=DateTimeUtils().get_time, + doc="its generate automatically when userlogin create", + ) + updated_at = Column( + DateTime, + nullable=constant.STATUS_TRUE, + default=DateTimeUtils().get_time, + onupdate=DateTimeUtils().get_time, + doc="its generate automatically when userlogin update", + ) + deleted_at = Column( + DateTime, + nullable=constant.STATUS_TRUE, + doc="its generate automatically when userlogin deleted", + ) diff --git a/apps/v1/api/auth/schema.py b/apps/v1/api/auth/schema.py index 8b2e550..69fbf06 100644 --- a/apps/v1/api/auth/schema.py +++ b/apps/v1/api/auth/schema.py @@ -1,24 +1,220 @@ """This module is for swager and request parameter schema""" -from pydantic import BaseModel +from typing import Optional +from pydantic import BaseModel, ConfigDict, EmailStr, field_validator +from core.utils import constant_variable as constant +from core.utils.validation import ValidationMethods -class UserAuth(BaseModel): + +class CreateUser(BaseModel): """This class is for user schema.""" + first_name: str last_name: str - email: str + email: EmailStr password: str username: str class Config: """This class is the schema for user configuration.""" - from_attributes = True + + from_attributes = constant.STATUS_TRUE + extra = "forbid" json_schema_extra = { "example": { - "first_name": "John", - "last_name": "Smith", - "email": "jhohnsmith@example.com", - "password": "Abc@123", - "username": "Jhon123" - } + "first_name": "John", + "last_name": "Smith", + "email": "johnsmith@example.com", + "password": "Abc@123", + "username": "John123", + } + } + + @field_validator("password") + def password_validation(cls, v): + return ValidationMethods().validate_password(v) + + +class LoginUser(BaseModel): + """This class is for user schema.""" + + email: EmailStr + password: str + + class Config: + """This class is the schema for user configuration.""" + + from_attributes = constant.STATUS_TRUE + extra = "forbid" + json_schema_extra = { + "example": {"email": "johnsmith@example.com", "password": "Abc@123"} } + + @field_validator("password") + def password_validation(cls, v): + return ValidationMethods().validate_password(v) + + +class ForgotPassword(BaseModel): + email: EmailStr + + model_config = ConfigDict( + from_attributes=constant.STATUS_TRUE, + extra="forbid", + json_schema_extra={ + "example": { + "email": "johnsmith@gmail.com", + } + }, + ) + + +class ResetPassword(BaseModel): + token: str + new_password: str + confirm_password: str + + model_config = ConfigDict( + from_attributes=constant.STATUS_TRUE, + extra="forbid", + json_schema_extra={ + "example": { + "token": "1234567890", + "new_password": "Test@123", + "confirm_password": "Test@123", + } + }, + ) + + @field_validator("new_password") + def password_validation(cls, value): + return ValidationMethods().validate_password(value) + + @field_validator("confirm_password") + def password_validation(cls, value): + return ValidationMethods().validate_password(value) + + +class SocialLogin(BaseModel): + provider: int + provider_token: str + + class Config: + model_config = ConfigDict( + from_attributes=constant.STATUS_TRUE, + extra="forbid", + json_schema_extra={ + "example": { + "provider": "1234567890", + "provider_token": "Test@123", + } + }, + ) + + @field_validator("provider_token") + def provider_token_must_be_required(cls, v): + return ValidationMethods().not_null_validator(v, "provider_token") + + +class EmailVerification(BaseModel): + otp: str + otp_referenceId: str + is_exist: bool + password: str + + class Config: + model_config = ConfigDict( + from_attributes=constant.STATUS_TRUE, + extra="forbid", + json_schema_extra={ + "example": { + "otp": "123456", + "otp_referenceId": "123456", + "is_exist": True, + "password": "Test@123", + } + }, + ) + + @field_validator("otp") + def otp_must_be_required(cls, v): + return ValidationMethods().not_null_validator(v, "otp") + + @field_validator("otp_referenceId") + def otp_referenceId_must_be_required(cls, v): + return ValidationMethods().not_null_validator(v, "otp_referenceId") + + @field_validator("password") + def password_validation(cls, v): + return ValidationMethods().validate_password(v) + + +class VerifyOtp(BaseModel): + user_type: int + phone_no: str + otp: str + is_exist: bool + password: str + otp_referenceId: str + + class Config: + model_config = ConfigDict( + from_attributes=constant.STATUS_TRUE, + extra="forbid", + json_schema_extra={ + "example": { + "user_type": "123456", + "phone_no": "123456", + "otp": "123456", + "is_exist": True, + "password": "Test@123", + "otp_referenceId": "123456", + } + }, + ) + + @field_validator("otp") + def otp_must_be_required(cls, v): + return ValidationMethods().not_null_validator(v, "otp") + + @field_validator("otp_referenceId") + def otp_referenceId_must_be_required(cls, v): + return ValidationMethods().not_null_validator(v, "otp_referenceId") + + @field_validator("password") + def password_validation(cls, v): + return ValidationMethods().validate_password(v) + + +class ResendOtp(BaseModel): + email: Optional[EmailStr] = constant.STATUS_NULL + country_code: Optional[str] = constant.STATUS_NULL + phone_no: Optional[str] = constant.STATUS_NULL + + class Config: + model_config = ConfigDict( + from_attributes=constant.STATUS_TRUE, + extra="forbid", + json_schema_extra={ + "example": { + "email": "johnsmith@example.com", + "country_code": "+91", + "phone_no": "1234567890", + } + }, + ) + + +@field_validator("phone_no") +def phone_no_must_be_required(cls, v): + return ValidationMethods().not_null_validator(v, "phone_no") + + +@field_validator("country_code") +def country_code_must_be_required(cls, v): + return ValidationMethods().not_null_validator(v, "country_code") + + +@field_validator("email") +def email_must_be_required(cls, v): + return ValidationMethods().not_null_validator(v, "email") diff --git a/apps/v1/api/auth/services/email_verification_service.py b/apps/v1/api/auth/services/email_verification_service.py new file mode 100644 index 0000000..3c28a62 --- /dev/null +++ b/apps/v1/api/auth/services/email_verification_service.py @@ -0,0 +1,54 @@ +from fastapi import status +from fastapi.encoders import jsonable_encoder +from sqlalchemy.ext.asyncio import AsyncSession + +from apps.v1.api.auth.models.methods.method1 import UserAuthMethod +from apps.v1.api.auth.models.model import Users +from core.utils import constant_variable as constant +from core.utils import db_method, message_variable +from core.utils.standard_response import StandardResponse + + +class EmailVerificationService: + + def user_verification_service(self, db: AsyncSession, body: dict): + """ + TODO: Impliment verification Logic as per requirement + """ + try: + body = body.dict() + otp_referenceId = body["otp_referenceId"] + otp = body["otp"] + password = body["password"] + + # Get the user + if not ( + user_object := UserAuthMethod(Users).find_user_by_otp_referenceId( + db, otp_referenceId + ) + ): + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_USER_NOT_FOUND, + ).make + + user_data = jsonable_encoder(user_object) + if user_data["otp_referenceId"] != otp_referenceId: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.INVALID_OTP, + ).make + + data = {} + db.commit() # Commit the transaction + return StandardResponse( + status.HTTP_200_OK, data, message_variable.SUCCESS_USER_VERIFIED + ).make + except Exception as e: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.SOMETHING_WENT_WRONG, + ).make diff --git a/apps/v1/api/auth/services/forgot_password_service.py b/apps/v1/api/auth/services/forgot_password_service.py new file mode 100644 index 0000000..8ec6f7d --- /dev/null +++ b/apps/v1/api/auth/services/forgot_password_service.py @@ -0,0 +1,38 @@ +from fastapi import status +from sqlalchemy.ext.asyncio import AsyncSession + +from apps.v1.api.auth.models.methods.method1 import UserAuthMethod +from apps.v1.api.auth.models.model import Users +from core.utils.token_authentication import JWTOAuth2 +from core.utils import constant_variable, message_variable +from core.utils.standard_response import StandardResponse + + +class ForgotPasswordService: + + async def forgot_password_service(self, db: AsyncSession, body): + try: + user_auth_method = UserAuthMethod(Users) + user_object = await user_auth_method.find_by_email(db, body.email) + if not user_object: + return StandardResponse( + False, + status.HTTP_400_BAD_REQUEST, + constant_variable.STATUS_NULL, + message_variable.INVALID_USER_CREDENTIAL, + ).make + reset_link = JWTOAuth2().encode_reset_password_link(body.email) + db.commit() # Commit the transaction + return StandardResponse( + True, + status.HTTP_200_OK, + data=reset_link, + message=message_variable.SUCCESS_RESET_LINK, + ).make + except Exception as e: + return StandardResponse( + False, + status.HTTP_400_BAD_REQUEST, + constant_variable.STATUS_NULL, + message_variable.GENERAL_TRY_AGAIN, + ) diff --git a/apps/v1/api/auth/services/otp_resend_verification_service.py b/apps/v1/api/auth/services/otp_resend_verification_service.py new file mode 100644 index 0000000..1091638 --- /dev/null +++ b/apps/v1/api/auth/services/otp_resend_verification_service.py @@ -0,0 +1,141 @@ +import os +from fastapi import status +from fastapi.encoders import jsonable_encoder +from sqlalchemy.ext.asyncio import AsyncSession +from core.utils.token_authentication import JWTOAuth2 +from apps.v1.api.auth.models.methods.method1 import UserAuthMethod +from apps.v1.api.auth.models.model import Users +from core.utils import constant_variable as constant +from core.utils import ( + db_method, + message_variable, + otp_service, +) +from core.utils.standard_response import StandardResponse + + +class ResendOtpVerificationService: + + async def otp_resend_verification_service(self, db: AsyncSession, body: dict): + try: + body = body.dict() + user_type = int(body["user_type"]) + otp = str(body["otp"]) + ref_id = str(body["otp_referenceId"]) + if user_type == constant.STATUS_ZERO: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_INVALID_REQUEST_BODY, + ).make + + if "email" in body and body["email"]: + otp_referenceId = body["otp_referenceId"] + if not ( + user_object := UserAuthMethod(Users).find_user_by_otp_referenceId( + db, otp_referenceId + ) + ): + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_USER_NOT_FOUND, + ).make + user_data = jsonable_encoder(user_object) + + if user_data["otp_referenceId"] != otp_referenceId: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.INVALID_OTP, + ).make + + if user_data["email"] != os.environ.get( + "TESTING_EMAIL" + ) and otp != os.environ.get("TESTING_OTP"): + if not (otp_service.OTPUtils().verify_otp(user_data["email"], otp)): + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.INVALID_OTP, + ).make + + elif ("phone_no" in body and body["phone_no"]) and ( + "country_code" in body and body["country_code"] + ): + country_code = body["country_code"] + phone_no = body["phone_no"] + user_object = UserAuthMethod(Users).find_verified_phone_user( + db, phone_no, user_type + ) + + if not user_object: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_USER_NOT_FOUND, + ).make + + if ( + user_object.phone_number != os.environ.get("TESTING_MOBILE") + and otp != os.environ.get("TESTING_OTP") + and os.environ.get("OTP_LIVE_MODE") == "True" + ): + country_code = "+".join(user_object.country_code) or "+91" + + if os.environ.get("MOBILE_OTP_SERVICE") == "KALEYRA": + if not (otp_service.OTPUtils().verify_kaleyra_otp(otp, ref_id)): + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_OTP_VERIFICATION, + ).make + elif os.environ.get("MOBILE_OTP_SERVICE") == "TWILLIO": + if not ( + otp_service.OTPUtils().verify_otp( + f"{country_code}{user_object.phone_number}", + otp, + ) + ): + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_OTP_VERIFICATION, + ).make + else: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_SMS_SERVICE, + ).make + else: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_INVALID_REQUEST_BODY, + ).make + + if not (db_method.DataBaseMethod(Users).save(user_object, db)): + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.GENERIC_ERROR, + ).make + token = JWTOAuth2().encode_access_token(identity=user_object.email) + user_data = jsonable_encoder(user_object) + + data = { + "data": user_data, + "access_token": token["access_token"], + "refresh_token": token["refresh_token"], + } + db.commit() # Commit the transaction + return StandardResponse( + status.HTTP_200_OK, data, message_variable.SUCCESS_USER_LOGGED_IN + ).make + except Exception as e: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.GENERAL_TRY_AGAIN, + ).make diff --git a/apps/v1/api/auth/services/otp_verification_service.py b/apps/v1/api/auth/services/otp_verification_service.py new file mode 100644 index 0000000..2825aed --- /dev/null +++ b/apps/v1/api/auth/services/otp_verification_service.py @@ -0,0 +1,104 @@ +import os +from fastapi import status +from fastapi.encoders import jsonable_encoder +from sqlalchemy.ext.asyncio import AsyncSession +from core.utils.token_authentication import JWTOAuth2 +from apps.v1.api.auth.models.methods.method1 import UserAuthMethod +from apps.v1.api.auth.models.model import Users +from core.utils import constant_variable as constant +from core.utils import ( + db_method, + constant_variable, + message_variable, + helper, + otp_service, + sms_service, +) +from core.utils.standard_response import StandardResponse + + +class OtpVerificationService: + async def otp_verification_service(self, db: AsyncSession, body: dict): + try: + body = body.dict() + user_type = int(body["user_type"]) + password = body["password"] + ref_id = str(body["otp_referenceId"]) + + if user_type == constant.STATUS_ZERO: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_INVALID_REQUEST_BODY, + ).make + + phone_no = body["phone_no"] + otp = str(body["otp"]) + user_object = UserAuthMethod(Users).find_verified_phone_user( + db, phone_no, user_type + ) + + if not user_object: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_USER_NOT_FOUND, + ).make + + if ( + user_object.phone_number != os.environ.get("TESTING_MOBILE") + and otp != os.environ.get("TESTING_OTP") + and os.environ.get("OTP_LIVE_MODE") == "True" + ): + country_code = "+".join(user_object.country_code) or "+91" + + if os.environ.get("MOBILE_OTP_SERVICE") == "KALEYRA": + if not (otp_service.OTPUtils().verify_kaleyra_otp(otp, ref_id)): + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_OTP_VERIFICATION, + ).make + elif os.environ.get("MOBILE_OTP_SERVICE") == "TWILLIO": + if not ( + otp_service.OTPUtils().verify_otp( + f"{country_code}{user_object.phone_number}", + otp, + ) + ): + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_OTP_VERIFICATION, + ).make + else: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_SMS_SERVICE, + ).make + + user_object.password = helper.PasswordUtils().hash_password(password) + if user_object.verified_at is constant.STATUS_NULL: + user_object.verified_at = helper.DateTimeUtils().get_time() + + if not (db_method.DataBaseMethod(Users).save(user_object, db)): + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.GENERIC_ERROR, + ).make + + token = JWTOAuth2().encode_access_token(identity=user_object.email) + user_data = jsonable_encoder(user_object) + data = {"data": user_data, "access_token": token["access_token"]} + db.commit() # Commit the transaction + return StandardResponse( + status.HTTP_200_OK, data, message_variable.SUCCESS_USER_LOGGED_IN + ).make + except Exception as e: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant_variable.STATUS_NULL, + message_variable.GENERAL_TRY_AGAIN, + ).make diff --git a/apps/v1/api/auth/services/resend_otp_service.py b/apps/v1/api/auth/services/resend_otp_service.py new file mode 100644 index 0000000..5182dca --- /dev/null +++ b/apps/v1/api/auth/services/resend_otp_service.py @@ -0,0 +1,126 @@ +import os +import pyotp +from fastapi import status +from fastapi.encoders import jsonable_encoder +from sqlalchemy.ext.asyncio import AsyncSession +from core.utils.token_authentication import JWTOAuth2 +from apps.v1.api.auth.models.methods.method1 import UserAuthMethod +from apps.v1.api.auth.models.model import Users +from core.utils import constant_variable as constant +from core.utils import ( + db_method, + constant_variable, + message_variable, + helper, + otp_service, + sms_service, +) +from core.utils.standard_response import StandardResponse + + +class ResendOTPService: + + async def otp_resend_service(self, db: AsyncSession, body: dict): + try: + body = body.dict() + response = {} + if "email" in body and body["email"]: + email = body["email"] + user_object = UserAuthMethod(Users).find_verified_email_user(db, email) + + if not user_object: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_USER_NOT_FOUND, + ).make + + channel_name = constant.EMAIL_CHANNEL + otp_source = email + user_object.otp_referenceId = pyotp.random_base32() + response["email"] = body["email"] + response["otp_referenceId"] = user_object.otp_referenceId + elif ("phone_no" in body and body["phone_no"]) and ( + "country_code" in body and body["country_code"] + ): + phone_no = body["phone_no"] + user_object = UserAuthMethod(Users).find_verified_phone_user( + db, phone_no + ) + + if not user_object: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_USER_NOT_FOUND, + ).make + + channel_name = constant.SMS_CHANNEL + country_code = body["country_code"] or "+91" + response["phone_no"] = phone_no + response["country_code"] = country_code + otp_source = f"{country_code}{phone_no}" + else: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_INVALID_REQUEST_BODY, + ).make + + # OTP generate + if os.environ.get("OTP_LIVE_MODE") == "True": + if ( + os.environ.get("MOBILE_OTP_SERVICE") == "KALEYRA" + and channel_name == constant.SMS_CHANNEL + ): + if not ( + otp_send_status := otp_service.OTPUtils().generate_otp_kaleyra( + otp_source + ) + ): + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_OTP_SEND, + ).make + otp_referenceId = otp_send_status["data"]["verify_id"] + user_object.otp_referenceId = otp_referenceId + response["otp_referenceId"] = otp_referenceId + elif (os.environ.get("MOBILE_OTP_SERVICE") == "TWILLIO") or ( + os.environ.get("EMAIL_OTP_SERVICE") == "TWILLIO" + and channel_name == constant.EMAIL_CHANNEL + ): + if not ( + otp_send_status := otp_service.OTPUtils().send_otp( + otp_source, channel_name + ) + ): + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_OTP_SEND, + ).make + else: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.ERROR_SMS_SERVICE, + ).make + + # save user data + if not (db_method.DataBaseMethod(Users).save(user_object, db)): + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.GENERIC_ERROR, + ).make + db.commit() # Commit the transaction + return StandardResponse( + status.HTTP_200_OK, response, message_variable.SUCCESS_OTP_SENT + ).make + except Exception as e: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant.STATUS_NULL, + message_variable.GENERAL_TRY_AGAIN, + ).make diff --git a/apps/v1/api/auth/services/reset_password_service.py b/apps/v1/api/auth/services/reset_password_service.py new file mode 100644 index 0000000..0faef63 --- /dev/null +++ b/apps/v1/api/auth/services/reset_password_service.py @@ -0,0 +1,69 @@ +import bcrypt +from fastapi import status +from fastapi.encoders import jsonable_encoder +from sqlalchemy.ext.asyncio import AsyncSession + +from apps.v1.api.auth.models.methods.method1 import UserAuthMethod +from apps.v1.api.auth.models.model import Users +from core.utils.token_authentication import JWTOAuth2 +from core.utils import db_method, constant_variable, message_variable +from core.utils.standard_response import StandardResponse + + +class ResetPasswordService: + async def reset_password_service(self, db: AsyncSession, body): + try: + decoded_token = JWTOAuth2().decode_reset_password_token(body.token) + user_auth_method = UserAuthMethod(Users) + user_object = await user_auth_method.find_by_email( + db, decoded_token.get("receiver_email") + ) + if not user_object: + return StandardResponse( + False, + status.HTTP_400_BAD_REQUEST, + constant_variable.STATUS_NULL, + message_variable.INVALID_USER_CREDENTIAL, + ).make + + # Check if new password and confirm password match + if body.new_password != body.confirm_password: + return StandardResponse( + status=status.HTTP_400_BAD_REQUEST, + data=None, + message=message_variable.ERROR_PASSWORD_MISMATCH, + ).make + + hashed_password = bcrypt.hashpw( + body.new_password.encode("utf-8"), bcrypt.gensalt() + ) + body = body.dict() + body["password"] = hashed_password + user_object = Users(**body) + if not ( + client_save := await db_method.DataBaseMethod(Users).save( + user_object, db + ) + ): + return StandardResponse( + False, + status.HTTP_400_BAD_REQUEST, + constant_variable.STATUS_NULL, + message_variable.GENERIC_ERROR, + ).make + user_data = jsonable_encoder(user_object) + del user_data["password"] + db.commit() # Commit the transaction + if client_save is not None: + return StandardResponse( + True, + status.HTTP_201_CREATED, + data=user_data, + message=message_variable.SUCCESS_USER_CREATE, + ).make + except Exception as e: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant_variable.STATUS_NULL, + message_variable.GENERAL_TRY_AGAIN, + ).make diff --git a/apps/v1/api/auth/services/service.py b/apps/v1/api/auth/services/service.py deleted file mode 100644 index c0201a9..0000000 --- a/apps/v1/api/auth/services/service.py +++ /dev/null @@ -1,105 +0,0 @@ -"""This module contains API's specific functionality.""" -import uuid - -from fastapi import status -from fastapi.encoders import jsonable_encoder -from sqlalchemy.orm import Session - -from apps.v1.api.auth.models.methods.method1 import UserAuthMethod -from apps.v1.api.auth.models.model import Users -from core.utils import db_method -from core.utils import ValidationMethods -from apps.constant import constant -from core.utils.message_variable import ErrorMessage, InfoMessage -from core.utils.standard_response import StandardResponse - - -class UserAuthService: - """This class represents the user creation service""" - def create_user_service(self, db: Session, body: dict): - """This function is used to create user - Args: - db (Session): database connection - body (dict): dictionary to user information data - - Returns: - response (dict): user object representing the user - """ - username = body['username'] - email = body['email'] or None - password = body['password'] - - # check Email exists in body or not - if "email" not in body or not email: - return StandardResponse( - False, status.HTTP_400_BAD_REQUEST, None, ErrorMessage.emailRequired - ) - - # check Email exists in Db or not - if (user_object := UserAuthMethod(Users).find_by_email(db, email)): - return StandardResponse( - False, status.HTTP_400_BAD_REQUEST, - constant.STATUS_NULL, ErrorMessage.emailAlreadyExist - ).make - - # For password validation - if not ValidationMethods().password_validator(password): - return StandardResponse( - False, status.HTTP_400_BAD_REQUEST, - constant.STATUS_NULL, ErrorMessage.invalidPasswordFormat - ).make - - user_object = Users( - uuid = uuid.uuid4(), - first_name=body['first_name'], - last_name=body['last_name'], - username=username, - email=email, - password=password - ) - - # Store user object in database - if not(db_method.BaseMethods(Users).save(user_object, db)): - return StandardResponse( - False, status.HTTP_400_BAD_REQUEST, - constant.STATUS_NULL, ErrorMessage.userNotSaved - ) - - # check email exists or not - if user_object := UserAuthMethod(Users).find_by_email(db, email): - data = { - "username": user_object.username, - "first_name": user_object.first_name, - "last_name": user_object.last_name, - "email": user_object.email, - "password": user_object.password, - } - - else: - return StandardResponse( - False, status.HTTP_400_BAD_REQUEST, - constant.STATUS_NULL, ErrorMessage.userInvalid - ).make - - return StandardResponse( - True, status.HTTP_200_OK, data, InfoMessage.userCreated - ).make - - def get_user_service(self, db): - """This function returns the user service list.""" - if not (user_object := db_method.BaseMethods(Users).find_by_uuid(db)): - return StandardResponse( - False, - status.HTTP_400_BAD_REQUEST, - None, - ErrorMessage.userNotFound - ) - # convert the object data into json - user_data = jsonable_encoder(user_object) - - return StandardResponse( - True, - status.HTTP_200_OK, - user_data, - InfoMessage.retriveInfoSuccessfully - ) diff --git a/apps/v1/api/auth/services/social_login_service.py b/apps/v1/api/auth/services/social_login_service.py new file mode 100644 index 0000000..e854026 --- /dev/null +++ b/apps/v1/api/auth/services/social_login_service.py @@ -0,0 +1,82 @@ +from fastapi import status +from google.auth.transport import requests +from google.oauth2 import id_token +from fastapi.encoders import jsonable_encoder +from sqlalchemy.ext.asyncio import AsyncSession + +from core.utils.token_authentication import JWTOAuth2 +from core.utils import constant_variable, message_variable +from core.utils.standard_response import StandardResponse +from config import social_login +from apps.v1.api.auth.models.attribute import SsoType + + +class SsologinService: + """This class represents the user creation service""" + + def get_google_account(self, access_token: str): + """ + This function is used to get the user's facebook profile. + Args: + access_token(str): This is the token of user's + to fetch the data from facebook. + + return: + dict: It returns the user's profile information like, + email, full_name, etc,. + """ + try: + # Specify the CLIENT_ID of the app that accesses the backend + profile = id_token.verify_oauth2_token( + access_token, requests.Request(), social_login.GOOGLE_CLIENT_ID + ) + return profile + + except Exception as e: + profile = constant_variable.STATUS_NULL + + async def sso_login_user_service(self, db: AsyncSession, body): + try: + body = body.dict() + # TODO: as per database architecture retrive user object from sso login table + user_data = {} + # Login with google + provider_token = body["provider_token"] + provider = body["provider"] + if provider == SsoType.google.value: + if not (profile := self.get_google_account(provider_token)): + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant_variable.STATUS_NULL, + message_variable.INVALID_PROVIDER_TOKEN, + ).make + + email, full_name = ( + profile["email"], + profile["given_name"], + ) + provider_unique_id = profile["sub"] + access_token = JWTOAuth2().encode_access_token( + identity={ + "email": profile["email"], + "id": profile["uuid"], + "user_type": "user", + } + ) + # user_data = jsonable_encoder(user_object) + user_data["access_token"] = access_token + del user_data["password"] + db.commit() # Commit the transaction + return StandardResponse( + True, + status.HTTP_200_OK, + data=user_data, + message=message_variable.SUCCESS_USER_LOGIN, + ).make + except Exception as e: + return StandardResponse( + False, + status.HTTP_400_BAD_REQUEST, + constant_variable.STATUS_NULL, + message_variable.GENERAL_TRY_AGAIN, + ) diff --git a/apps/v1/api/auth/services/user_login_service.py b/apps/v1/api/auth/services/user_login_service.py new file mode 100644 index 0000000..5c88301 --- /dev/null +++ b/apps/v1/api/auth/services/user_login_service.py @@ -0,0 +1,61 @@ +import bcrypt +from fastapi import status +from fastapi.encoders import jsonable_encoder +from sqlalchemy.ext.asyncio import AsyncSession + +from apps.v1.api.auth.models.methods.method1 import UserAuthMethod +from apps.v1.api.auth.models.model import Users +from core.utils.token_authentication import JWTOAuth2 +from core.utils import constant_variable, message_variable +from core.utils.standard_response import StandardResponse + + +class LoginService: + + async def login_user_service(self, db: AsyncSession, body): + try: + user_auth_method = UserAuthMethod(Users) + user_object = await user_auth_method.find_by_email(db, body.email) + if not user_object: + return StandardResponse( + False, + status.HTTP_400_BAD_REQUEST, + constant_variable.STATUS_NULL, + message_variable.INVALID_USER_CREDENTIAL, + ).make + if not bcrypt.checkpw( + body.password.encode("utf-8"), user_object.password.encode("utf-8") + ): + return StandardResponse( + False, + status.HTTP_400_BAD_REQUEST, + constant_variable.STATUS_NULL, + message_variable.INVALID_USER_CREDENTIAL, + ).make + + # generate token + access_token = JWTOAuth2().encode_access_token( + identity=user_object.email + # identity={ + # "email": user_object.email, + # "id": user_object.id, + # "user_type": "user", + # } + ) + user_data = jsonable_encoder(user_object) + user_data["access_token"] = access_token + del user_data["password"] + db.commit() # Commit the transaction + return StandardResponse( + True, + status.HTTP_200_OK, + data=user_data, + message=message_variable.SUCCESS_USER_LOGIN, + ).make + except Exception as e: + return StandardResponse( + False, + status.HTTP_400_BAD_REQUEST, + constant_variable.STATUS_NULL, + message_variable.GENERAL_TRY_AGAIN, + ) diff --git a/apps/v1/api/auth/services/user_signup_service.py b/apps/v1/api/auth/services/user_signup_service.py new file mode 100644 index 0000000..0926b93 --- /dev/null +++ b/apps/v1/api/auth/services/user_signup_service.py @@ -0,0 +1,64 @@ +import uuid +import bcrypt +from fastapi import status +from fastapi.encoders import jsonable_encoder +from sqlalchemy.ext.asyncio import AsyncSession + +from apps.v1.api.auth.models.methods.method1 import UserAuthMethod +from apps.v1.api.auth.models.model import Users +from core.utils import db_method, constant_variable, message_variable +from core.utils.standard_response import StandardResponse + + +class UserAuthService: + """This class represents the user creation service""" + + async def signup_user_service(self, db: AsyncSession, body): + try: + body = body.dict() + # Check body's Email already exist + user_auth_method = UserAuthMethod(Users) + if user_object := await user_auth_method.find_by_email(db, body["email"]): + return StandardResponse( + False, + status.HTTP_400_BAD_REQUEST, + constant_variable.STATUS_NULL, + message_variable.EMAIL_ALLREADY_EXISTS, + ).make + + # For password hashing + hashed_password = bcrypt.hashpw( + body["password"].encode("utf-8"), bcrypt.gensalt() + ) + + body["password"] = hashed_password + body["uuid"] = uuid.uuid4() + # Add the body into the database + user_object = Users(**body) + if not ( + client_save := await db_method.DataBaseMethod(Users).save( + user_object, db + ) + ): + return StandardResponse( + False, + status.HTTP_400_BAD_REQUEST, + constant_variable.STATUS_NULL, + message_variable.GENERIC_ERROR, + ).make + user_data = jsonable_encoder(user_object) + del user_data["password"] + db.commit() # Commit the transaction + if client_save is not None: + return StandardResponse( + True, + status.HTTP_201_CREATED, + data=user_data, + message=message_variable.SUCCESS_USER_CREATE, + ).make + except Exception as e: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, + constant_variable.STATUS_NULL, + message_variable.GENERAL_TRY_AGAIN, + ).make diff --git a/apps/v1/api/auth/test.py b/apps/v1/api/auth/tests/__init__.py similarity index 100% rename from apps/v1/api/auth/test.py rename to apps/v1/api/auth/tests/__init__.py diff --git a/apps/v1/api/auth/tests/conftest.py b/apps/v1/api/auth/tests/conftest.py new file mode 100644 index 0000000..2f85cbc --- /dev/null +++ b/apps/v1/api/auth/tests/conftest.py @@ -0,0 +1,88 @@ +## TODO: Refactor this test to use pytest-asyncio +import pytest +import uuid +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import StaticPool +from fastapi.testclient import TestClient +from apps.server import app +from config import db_config +from config.db_config import Base + +# from app.database import Base, get_db + + +get_db = db_config.get_db +# SQLite database URL for testing +SQLITE_DATABASE_URL = "sqlite:///./test_db.db" + +# Create a SQLAlchemy engine +engine = create_engine( + SQLITE_DATABASE_URL, + connect_args={"check_same_thread": False}, + poolclass=StaticPool, +) + +# Create a sessionmaker to manage sessions +TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# Create tables in the database +Base.metadata.create_all(bind=engine) + + +@pytest.fixture(scope="function") +def db_session(): + """Create a new database session with a rollback at the end of the test.""" + connection = engine.connect() + transaction = connection.begin() + session = TestingSessionLocal(bind=connection) + yield session + session.close() + transaction.rollback() + connection.close() + + +@pytest.fixture(scope="function") +def test_client(db_session): + """Create a test client that uses the override_get_db fixture to return a session.""" + + def override_get_db(): + try: + yield db_session + finally: + db_session.close() + + app.dependency_overrides[get_db] = override_get_db + with TestClient(app) as test_client: + yield test_client + + +# Fixture to generate a random user id +@pytest.fixture() +def user_id() -> uuid.UUID: + """Generate a random user id.""" + return str(uuid.uuid4()) + + +# Fixture to generate a user payload +@pytest.fixture() +def user_payload(user_id): + """Generate a user payload.""" + return { + "id": user_id, + "first_name": "John", + "last_name": "Doe", + "email": "test@example.com", + "password": "Test@123", # Hashed password + } + + +@pytest.fixture() +def user_payload_updated(user_id): + """Generate an updated user payload.""" + return { + "first_name": "Jane", + "last_name": "Doe", + "address": "321 Farmville", + "activated": True, + } diff --git a/apps/v1/api/auth/tests/test_crud_api.py b/apps/v1/api/auth/tests/test_crud_api.py new file mode 100644 index 0000000..4c2d8bc --- /dev/null +++ b/apps/v1/api/auth/tests/test_crud_api.py @@ -0,0 +1,110 @@ +import time + + +# TODO: This is a basic test to check if the API is working modify it to fit your needs. +# TODO: Refactor this test to use pytest-asyncio +def test_root(test_client): + response = test_client.get("/api/healthchecker") + assert response.status_code == 200 + assert response.json() == {"message": "The API is LIVE!!"} + + +def test_create_get_user(test_client, user_payload): + response = test_client.post("/api/users/", json=user_payload) + response_json = response.json() + assert response.status_code == 201 + + # Get the created user + response = test_client.get(f"/api/users/{user_payload['id']}") + assert response.status_code == 200 + response_json = response.json() + assert response_json["Status"] == "Success" + assert response_json["User"]["id"] == user_payload["id"] + assert response_json["User"]["address"] == "123 Farmville" + assert response_json["User"]["first_name"] == "John" + assert response_json["User"]["last_name"] == "Doe" + + +def test_create_update_user(test_client, user_payload, user_payload_updated): + response = test_client.post("/api/users/", json=user_payload) + response_json = response.json() + assert response.status_code == 201 + + # Update the created user + time.sleep( + 1 + ) # Sleep for 1 second to ensure updatedAt is different (datetime precision is low in SQLite) + response = test_client.patch( + f"/api/users/{user_payload['id']}", json=user_payload_updated + ) + response_json = response.json() + assert response.status_code == 202 + assert response_json["Status"] == "Success" + assert response_json["User"]["id"] == user_payload["id"] + assert response_json["User"]["address"] == "321 Farmville" + assert response_json["User"]["first_name"] == "Jane" + assert response_json["User"]["last_name"] == "Doe" + assert response_json["User"]["activated"] is True + assert ( + response_json["User"]["updatedAt"] is not None + and response_json["User"]["updatedAt"] > response_json["User"]["createdAt"] + ) + + +def test_create_delete_user(test_client, user_payload): + response = test_client.post("/api/users/", json=user_payload) + response_json = response.json() + assert response.status_code == 201 + + # Delete the created user + response = test_client.delete(f"/api/users/{user_payload['id']}") + response_json = response.json() + assert response.status_code == 202 + assert response_json["Status"] == "Success" + assert response_json["Message"] == "User deleted successfully" + + # Get the deleted user + response = test_client.get(f"/api/users/{user_payload['id']}") + assert response.status_code == 404 + response_json = response.json() + assert ( + response_json["detail"] == f"No User with this id: `{user_payload['id']}` found" + ) + + +def test_get_user_not_found(test_client, user_id): + response = test_client.get(f"/api/users/{user_id}") + assert response.status_code == 404 + response_json = response.json() + assert response_json["detail"] == f"No User with this id: `{user_id}` found" + + +def test_create_user_wrong_payload(test_client): + response = test_client.post("/api/users/", json={}) + assert response.status_code == 422 + + +def test_update_user_wrong_payload(test_client, user_id, user_payload_updated): + user_payload_updated["first_name"] = ( + True # first_name should be a string not a boolean + ) + response = test_client.patch(f"/api/users/{user_id}", json=user_payload_updated) + assert response.status_code == 422 + response_json = response.json() + assert response_json == { + "detail": [ + { + "type": "string_type", + "loc": ["body", "first_name"], + "msg": "Input should be a valid string", + "input": True, + } + ] + } + + +def test_update_user_doesnt_exist(test_client, user_id, user_payload_updated): + response = test_client.patch(f"/api/users/{user_id}", json=user_payload_updated) + assert response.status_code == 404 + response_json = response.json() + assert response_json["detail"] == f"No User with this id: `{user_id}` found" diff --git a/apps/v1/api/auth/view.py b/apps/v1/api/auth/view.py index 42199e5..8ea8d24 100644 --- a/apps/v1/api/auth/view.py +++ b/apps/v1/api/auth/view.py @@ -1,44 +1,126 @@ """This module is responsible to contain API's endpoint""" -from fastapi import APIRouter, Depends, status -from sqlalchemy.orm import Session + +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession from apps.v1.api.auth import schema -from apps.v1.api.auth.service import UserAuthService -from apps.constant import constant -from core.utils.standard_response import StandardResponse +from apps.v1.api.auth.services import ( + user_login_service, + social_login_service, + user_signup_service, + reset_password_service, + forgot_password_service, + email_verification_service, + otp_verification_service, + resend_otp_service, + otp_resend_verification_service, +) +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from middleware import authentication_middleware from config import db_config ## Load API's -router = APIRouter() +authrouter = APIRouter() getdb = db_config.get_db +oauth2_scheme = HTTPBearer() + ## Define verison 1 API's here -class UserCrudApi(): +class UsersAuthentication: """This class is for user's CRUD operation with version 1 API's""" - @router.get('/users') - async def list_user(db: Session = Depends(getdb)): - """This API is for list user. - Args: None - Returns: - response: will return users list.""" - try: - response = UserAuthService().get_user_service(db) - return response - except Exception: - return StandardResponse(False, status.HTTP_400_BAD_REQUEST, None, constant.ERROR_MSG) - - @router.post('/create/user') - async def create_user(body: schema.UserAuth, - db: Session = Depends(getdb)): - """This API is for create user. - Args: - body(dict) : user's data - Returns: - response: will return the user's data""" - try: - # as per pydantic version 2. - body = body.model_dump() - response = UserAuthService().create_user_service(db, body) - return response - except Exception: - return StandardResponse(False, status.HTTP_400_BAD_REQUEST, None, constant.ERROR_MSG) \ No newline at end of file + + @authrouter.post("/signup") + async def signup_user(body: schema.CreateUser, db: AsyncSession = Depends(getdb)): + + response = await user_signup_service.UserAuthService().signup_user_service( + db, body + ) + return response + + @authrouter.post("/login") + async def login_user( + body: schema.LoginUser, + db: AsyncSession = Depends(getdb), + ): + + response = await user_login_service.LoginService().login_user_service(db, body) + return response + + @authrouter.post("/login/sso") + async def login_sso_user( + body: schema.LoginUser, + db: AsyncSession = Depends(getdb), + ): + + response = await social_login_service.SsologinService().sso_login_user_service( + db, body + ) + return response + + @authrouter.post("/forgot/password") + async def forgot_password( + body: schema.ForgotPassword, + db: AsyncSession = Depends(getdb), + ): + + response = await forgot_password_service.ForgotPasswordService().forgot_password_service( + db, body + ) + return response + + @authrouter.post("/reset/password") + async def reset_password( + body: schema.ResetPassword, + db: AsyncSession = Depends(getdb), + ): + + response = ( + await reset_password_service.ResetPasswordService().reset_password_service( + db, body + ) + ) + return response + + @authrouter.post("/user/verification") + async def user_verification( + body: schema.EmailVerification, + db: AsyncSession = Depends(getdb), + ): + + response = await email_verification_service.EmailVerificationService().user_verification_service( + db, body + ) + return response + + @authrouter.post("/otp/verification") + async def otp_verification( + body: schema.VerifyOtp, + db: AsyncSession = Depends(getdb), + ): + + response = await otp_verification_service.OtpVerificationService().otp_verification_service( + db, body + ) + return response + + @authrouter.post("/otp/resend") + async def otp_resend( + body: schema.ResendOtp, + db: AsyncSession = Depends(getdb), + ): + + response = await resend_otp_service.ResendOTPService().otp_resend_service( + db, body + ) + return response + + @authrouter.post("/otp/resend/verification") + async def otp_resend_verification( + body: schema.VerifyOtp, + db: AsyncSession = Depends(getdb), + ): + + response = await otp_resend_verification_service.ResendOtpVerificationService().otp_resend_verification_service( + db, body + ) + return response diff --git a/asgi.py b/asgi.py index 6d75b43..495c7fd 100644 --- a/asgi.py +++ b/asgi.py @@ -31,7 +31,7 @@ def main(env: str, debug: bool): app="asgi:app", host=str(os.environ.get("SERVER_HOST", "localhost")), port=int(os.environ.get("SERVER_PORT", 8000)), - reload=bool(os.environ.get("SERVER_DEBUG", constant_variable.STATUS_FALSE)), + reload=bool(os.environ.get("SERVER_DEBUG", constant_variable.STATUS_TRUE)), workers=1, log_config=LoggingConfig().get_config(), ) diff --git a/celery_tasks/__init__.py b/celery_tasks/__init__.py new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/celery_tasks/__init__.py @@ -0,0 +1 @@ + diff --git a/celery_tasks/tasks/__init__.py b/celery_tasks/tasks/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/celery_tasks/tasks/example_tasks.py b/celery_tasks/tasks/example_tasks.py new file mode 100644 index 0000000..e69de29 diff --git a/celery_tasks/worker.py b/celery_tasks/worker.py new file mode 100644 index 0000000..48d7e27 --- /dev/null +++ b/celery_tasks/worker.py @@ -0,0 +1,27 @@ +""" +Module to initialize the celery app and redis backend along with broker. +""" + +from celery import Celery +from config import redis_config + +# Initialize Celery app with Redis broker and backend +celery_app = Celery( + __name__, + broker=f"{redis_config.REDIS_BROKER_URL}/0", + backend=f"{redis_config.REDIS_BROKER_URL}/0", + include=["celery_task.tasks"], +) + +celery_app.conf.task_queues = { + 'sequential_queue': { + 'exchange': 'sequential', + 'exchange_type': 'direct', + 'binding_key': 'sequential_queue', + } +} + +celery_app.conf.task_routes = { + 'celery_task.tasks.fetch_and_prepare_records_data': {"queue": "sequential_queue"}, +} + diff --git a/config/celery_config.py b/config/celery_config.py new file mode 100644 index 0000000..e69de29 diff --git a/config/db_config.py b/config/db_config.py index 80f619d..7a5c437 100644 --- a/config/db_config.py +++ b/config/db_config.py @@ -67,3 +67,9 @@ async def session_factory() -> AsyncGenerator[AsyncSession, None]: yield _session finally: await _session.close() + + +# Define FastAPI Dependencyasync def +async def get_db() -> AsyncGenerator[AsyncSession, None]: + async with session_factory() as session: + yield session diff --git a/config/jwt_config.py b/config/jwt_config.py new file mode 100644 index 0000000..67c16d6 --- /dev/null +++ b/config/jwt_config.py @@ -0,0 +1,23 @@ +""" +JWT Configuration Module for FastAPI + +- This module manages configuration settings related to +JSON Web Tokens (JWT) in a FastAPI application. +- It provides information such as the issuer, token lifetime, +algorithm, and secret key used for token generation and verification. +""" + +import datetime +import os + +from dotenv import load_dotenv + +load_dotenv() + +# JWT info +JWT_LIFETIME = datetime.timedelta(days=7) +JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY") +JWT_REFRESH_SECRET_KEY = os.environ.get("JWT_REFRESH_SECRET_KEY") +JWT_ALGORITHM = os.environ.get("JWT_ALGORITHM") +AUTHJWT_SECRET_KEY = os.environ.get("AUTHJWT_SECRET_KEY") +AUTHJWT_EXPIRES_TIME = bool(os.environ.get("THJWT_EXPIRES_TIME")) diff --git a/config/kaleyra_config.py b/config/kaleyra_config.py new file mode 100644 index 0000000..82f6e20 --- /dev/null +++ b/config/kaleyra_config.py @@ -0,0 +1,16 @@ +import os + +from config import env_config + +KALEYRA_URL = os.environ.get("KALEYRA_URL") +APIKEY = os.environ.get("APIKEY") +SMSTYPE = os.environ.get("SMSTYPE") +SENDERID = os.environ.get("SENDERID") + + +CONTENT_TYPE = os.environ.get("CONTENT_TYPE") +OTPTYPE = os.environ.get("OTPTYPE") +SMS_TEMPLATE_ID = os.environ.get("SMS_TEMPLATE_ID") +OTP_TEMPLATE_ID = os.environ.get("OTP_TEMPLATE_ID") +SID = os.environ.get("SID") +FLOW_ID = os.environ.get("FLOW_ID") diff --git a/config/redis_config.py b/config/redis_config.py index ce23848..dab704f 100644 --- a/config/redis_config.py +++ b/config/redis_config.py @@ -6,6 +6,9 @@ REDIS_HOST = os.getenv("REDIS_HOST", "localhost") REDIS_PORT = int(os.getenv("REDIS_PORT", 6379)) +REDIS_BROKER_URL = os.environ.get('REDIS_BROKER_URL') + + redis_client = redis.StrictRedis( host=REDIS_HOST, diff --git a/config/signing_cookies.py b/config/signing_cookies.py new file mode 100644 index 0000000..99bc842 --- /dev/null +++ b/config/signing_cookies.py @@ -0,0 +1,19 @@ +""" +Cookie Signing Configuration Module for FastAPI + +- This module manages configuration settings related to cookie +signing in a FastAPI application. +- It includes parameters such as the secret key used for signing +cookies and the algorithm for the cookie signature. +""" + +import os + +from dotenv import load_dotenv + +load_dotenv() + +# # Secret key for signing cookies +SIGNIN_SECRET_KEY = os.environ.get("SIGNIN_SECRET_KEY") +ITSDANGEROUS_KEY = os.environ.get("ITSDANGEROUS_KEY") +RESET_KEY = os.environ.get("RESET_KEY") diff --git a/config/social_login.py b/config/social_login.py new file mode 100644 index 0000000..ac0bc43 --- /dev/null +++ b/config/social_login.py @@ -0,0 +1,3 @@ +import os + +GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID") diff --git a/config/twilio_config.py b/config/twilio_config.py new file mode 100644 index 0000000..923027f --- /dev/null +++ b/config/twilio_config.py @@ -0,0 +1,8 @@ +import os + +from config import env_config + +TWILIO_ACCOUNT_SID = os.environ.get("TWILIO_ACCOUNT_SID") +TWILIO_AUTH_TOKEN = os.environ.get("TWILIO_AUTH_TOKEN") +TWILIO_SERVICE_SID = os.environ.get("TWILIO_SERVICE_SID") +TWILIO_SIMPLE_MESSAGE_SERVICE_SID = os.environ.get("TWILIO_SIMPLE_MESSAGE_SERVICE_SID") diff --git a/core/db/db_attribute.py b/core/db/db_attribute.py new file mode 100644 index 0000000..26f9a26 --- /dev/null +++ b/core/db/db_attribute.py @@ -0,0 +1,25 @@ +from enum import Enum + +from core.utils import constant_variable as constant + + +class BaseAttributes(Enum): + @classmethod + def fetch_dict(cls): + user_status_dict = {i.name: i.value for i in cls} + return user_status_dict + + @classmethod + def fetch_by_name(cls, name): + return cls[name].value + + @classmethod + def fetch_by_id(cls, id: int): + if user_status := [i.name for i in cls if i.value == id]: + return user_status[0] + return constant.STATUS_NULL + + +class Status(BaseAttributes): + inactive = constant.STATUS_ZERO + active = constant.STATUS_ONE diff --git a/core/utils/constant_variable.py b/core/utils/constant_variable.py index 52a85ad..a7e0537 100644 --- a/core/utils/constant_variable.py +++ b/core/utils/constant_variable.py @@ -23,6 +23,15 @@ STATUS_NINE = 9 STATUS_TEN = 10 +# Http status code +STATUS_CODE_200 = 200 +STATUS_CODE_202 = 202 +STATUS_CODE_201 = 201 +STATUS_CODE_204 = 204 +STATUS_CODE_400 = 400 +STATUS_CODE_401 = 401 +STATUS_CODE_500 = 500 + ## Constant Date ## DATE_FORMAT = "%Y-%m-%d" TIME_FORMAT = "%H:%M:%S" @@ -81,3 +90,11 @@ DEFAULT_LANGUAGE = "en" TIMEZONE_UTC = "UTC" TIMEZONE_EST = "US/Eastern" + +# Rate limits +RATE_LIMIT = 100 +TIME_WINDOW = 60 + +# Channel to send the messages +SMS_CHANNEL = "sms" +EMAIL_CHANNEL = "email" diff --git a/core/utils/db_method.py b/core/utils/db_method.py index 93a1a74..fb69437 100644 --- a/core/utils/db_method.py +++ b/core/utils/db_method.py @@ -1,5 +1,5 @@ from fastapi import Depends -from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession from config import db_config from core.utils import constant_variable @@ -13,32 +13,33 @@ class DataBaseMethod: def __init__(self, model): self.model = model - def save(self, validate_data, db: Session = Depends(getdb)): - """This function creates new object + async def save(self, validate_data, db: AsyncSession = Depends(getdb)): + """This function creates a new object asynchronously. Arguments: self(db): database session - validate_data (dict): validate data + validate_data (dict): validated data to be saved Returns: - Returns the creates object + Returns the status of the operation (True/False). """ try: - db.add(validate_data) - db.flush() # Changed this to a flush - db.refresh(validate_data) + async with db.begin(): # Start a transaction asynchronously + db.add(validate_data) # Add the object to the session + await db.commit() + await db.flush() # Asynchronously flush the changes to the database return constant_variable.STATUS_TRUE except Exception as err: print(err) - db.rollback() + # Rollback is handled automatically when the transaction is not committed. return constant_variable.STATUS_FALSE - def save_all(self, validate_data: list, db: Session = Depends(getdb)): + async def save_all(self, validate_data: list, db: AsyncSession = Depends(getdb)): """Saves bulk data in the database.""" try: db.add_all(validate_data) - db.flush() # Changed this to a flush - db.refresh(validate_data) + await db.commit() + await db.flush() return constant_variable.STATUS_TRUE except Exception as err: print(err) @@ -46,12 +47,14 @@ def save_all(self, validate_data: list, db: Session = Depends(getdb)): db.close() return constant_variable.STATUS_FALSE - def bulk_insert_mapping(self, validate_data: dict, db: Session = Depends(getdb)): + async def bulk_insert_mapping( + self, validate_data: dict, db: AsyncSession = Depends(getdb) + ): """Saves bulk data in the database with its mapping""" try: db.bulk_insert_mappings(self.model, validate_data) - db.flush() # Changed this to a flush - db.refresh(validate_data) + await db.commit() + await db.flush() return constant_variable.STATUS_TRUE except Exception as err: print(err) @@ -59,7 +62,7 @@ def bulk_insert_mapping(self, validate_data: dict, db: Session = Depends(getdb)) db.close() return constant_variable.STATUS_FALSE - def destroy(self, instance: object, db: Session = Depends(getdb)): + async def destroy(self, instance: object, db: AsyncSession = Depends(getdb)): """This function take a ID and destroy the object Arguments: @@ -72,8 +75,8 @@ def destroy(self, instance: object, db: Session = Depends(getdb)): """ try: db.delete(instance=instance) - db.flush() # Changed this to a flush - db.refresh(instance) + await db.commit() + await db.flush() return constant_variable.STATUS_TRUE except Exception as e: return constant_variable.STATUS_FALSE diff --git a/core/utils/message_variable.py b/core/utils/message_variable.py index 6c2c9eb..a0b13f5 100644 --- a/core/utils/message_variable.py +++ b/core/utils/message_variable.py @@ -9,6 +9,13 @@ SUCCESS_VERIFICATION_SENT = "Verification email sent successfully!" SUCCESS_OPERATION_COMPLETED = "Operation completed successfully!" SUCCESS_FILE_UPLOADED = "File uploaded successfully!" +EMAIL_ALLREADY_EXISTS = "Email already exists!" +SUCCESS_USER_LOGIN = "User logged in successfully!" +SUCCESS_RESET_LINK = "User reset link sent successfully!" +SUCCESS_USER_VERIFIED = "User verified successfully!" +SUCCESS_USER_LOGGED_IN = "Logged in successfully!" +SUCCESS_OTP_SENT = "OTP sent successfully!" + ## Error message ## ERROR_USER_CREATE = "Error occurred while creating the user!" @@ -21,6 +28,17 @@ ERROR_FILE_UPLOAD = "Error occurred while uploading the file!" ERROR_VALIDATION = "Validation failed. Please check the input!" ERROR_INTERNAL_SERVER = "An internal server error occurred!" +GENERIC_ERROR = "An error ocured while saving data" +SOMETHING_WENT_WRONG = "Woops, something's not quite right, please try again!" +INVALID_USER_CREDENTIAL = "Invalid user credentials!" +INVALID_PROVIDER_TOKEN = "Invalid provider token!" +INVALID_OTP = "Invalid OTP!" +ERROR_USER_NOT_FOUND = "User not found!" +ERROR_INVALID_REQUEST_BODY = "Invalid request body!" +ERROR_SMS_SERVICE = "SMS service not found!" +ERROR_OTP_VERIFICATION = "OTP verification failed!" +ERROR_OTP_SEND = "Error sending OTP to user!" + ## Info message ## INFO_NO_RECORDS = "No records found!" @@ -40,6 +58,8 @@ VALIDATION_INVALID_EMAIL = "Invalid email address format!" VALIDATION_INVALID_DATE = "Invalid date format. Use YYYY-MM-DD!" VALIDATION_INVALID_FILE_TYPE = "Invalid file type. Allowed types are: {}!" +INVALID_AUTH_TOKEN = "Invalid authentication token!" +TOKEN_REQUIRED = "Token is required to perform this operation!" ## Generic error message ## GENERAL_PROCESSING = "Processing your request. Please wait..." diff --git a/core/utils/otp_service.py b/core/utils/otp_service.py new file mode 100644 index 0000000..c017530 --- /dev/null +++ b/core/utils/otp_service.py @@ -0,0 +1,101 @@ +import json +import pyotp +import requests +from twilio.rest import Client + +from core.utils import constant_variable as constant +from config import env_config, kaleyra_config, twilio_config + + +class OTPUtils: + def send_otp(self, data: str, channel_name: str): + try: + # Send the OTP via channel name SMS/Email + client = Client( + twilio_config.TWILIO_ACCOUNT_SID, twilio_config.TWILIO_AUTH_TOKEN + ) + + verification = client.verify.v2.services( + twilio_config.TWILIO_SERVICE_SID + ).verifications.create(to=data, channel=channel_name) + return constant.STATUS_TRUE + except Exception as e: + print("TWILIO error", e) + return constant.STATUS_FALSE + + def verify_otp(self, data: str, otp: str): + try: + # Verify OTP + client = Client( + twilio_config.TWILIO_ACCOUNT_SID, twilio_config.TWILIO_AUTH_TOKEN + ) + + verification_check = client.verify.v2.services( + twilio_config.TWILIO_SERVICE_SID + ).verification_checks.create(to=data, code=otp) + if verification_check.status != "approved": + return constant.STATUS_FALSE + return constant.STATUS_TRUE + except Exception as e: + print("TWILIO error", e) + return constant.STATUS_FALSE + + def generate_otp_kaleyra(self, data: str): + try: + url = f"{kaleyra_config.KALEYRA_URL}v1/{kaleyra_config.SID}/verify" + headers = { + "Content-Type": kaleyra_config.CONTENT_TYPE, + "api-key": kaleyra_config.APIKEY, + } + payload = json.dumps( + {"flow_id": kaleyra_config.FLOW_ID, "to": {"mobile": data}} + ) + response = requests.request("POST", url, headers=headers, data=payload) + otp_data = response.json() + if response.status_code not in [ + constant.STATUS_CODE_202, + constant.STATUS_CODE_200, + ]: + return constant.STATUS_FALSE + return otp_data + except Exception as e: + print("KALEYRA error", e) + return constant.STATUS_FALSE + + def verify_kaleyra_otp(self, otp: str, ref_id: str): + try: + url = f"{kaleyra_config.KALEYRA_URL}v1/{kaleyra_config.SID}/verify/validate" + headers = { + "Content-Type": kaleyra_config.CONTENT_TYPE, + "api-key": kaleyra_config.APIKEY, + } + payload = json.dumps({"verify_id": ref_id, "otp": otp}) + response = requests.request("POST", url, headers=headers, data=payload) + if response.status_code not in [ + constant.STATUS_CODE_202, + constant.STATUS_CODE_200, + ]: + return constant.STATUS_FALSE + return constant.STATUS_TRUE + except Exception as e: + print("KALEYRA error", e) + return constant.STATUS_FALSE + + +class EmailOTPUtils: + def generate_otp(self, otp_referenceId): + try: + secret_key = otp_referenceId + totp = pyotp.TOTP(secret_key, digits=constant.STATUS_SIX, interval=120) + return totp.now() + except Exception: + return constant.STATUS_NULL + + def verify_otp(self, otp: str, otp_referenceId: str): + try: + # Verify OTP + secret_key = otp_referenceId + totp = pyotp.TOTP(secret_key, digits=constant.STATUS_SIX, interval=120) + return totp.verify(otp) + except Exception: + return constant.STATUS_FALSE diff --git a/core/utils/sms_service.py b/core/utils/sms_service.py new file mode 100644 index 0000000..cf05306 --- /dev/null +++ b/core/utils/sms_service.py @@ -0,0 +1,73 @@ +import os +import json +import requests +from twilio.rest import Client + +from core.utils import constant_variable as constant +from config import kaleyra_config, twilio_config + + +class SendSMS: + def send_sms_twilio(self, phone_number, message, subject): + """Publishes a text message directly to a phone number without need for a + subscription. + :param phone_number: The phone number that receives the message. This must be in E.164 format. + For example, a United States phonenumber might be +12065550101. + :param message: The message to send. + :return: The ID of the message. + """ + client = Client( + twilio_config.TWILIO_ACCOUNT_SID, twilio_config.TWILIO_AUTH_TOKEN + ) + + try: + response = client.messages.create( + messaging_service_sid=twilio_config.TWILIO_SIMPLE_MESSAGE_SERVICE_SID, + body=message, + to=phone_number, + ) + message_id = response.sid + # logger.info("Published message to %s.", phone_number) + except Exception as e: + print("TWILIO error", e) + return False + else: + return message_id + + def send_sms_Kaleyra(self, phone_number: str, ticket_link: str): + try: + origin_url = f"{kaleyra_config.KALEYRA_URL}v1/{kaleyra_config.SID}/messages" + headers = { + "channel": "SMS", + "api-key": kaleyra_config.APIKEY, + "Content-Type": kaleyra_config.CONTENT_TYPE, + } + slug = os.environ.get("BUSINESS_SLUG") + payload = json.dumps( + { + "to": phone_number, + "sender": kaleyra_config.SENDERID, + "type": kaleyra_config.SMSTYPE, + "body": f"EveryTicket: Thank you for purchase at {slug}, " + + "Use link {url} to get digital tickets! -VIITORCLOUD.", + "url_data": { + "shorten_url": constant.STATUS_ONE, + "url": ticket_link, + "track_user": constant.STATUS_ONE, + }, + } + ) + response = requests.request( + "POST", origin_url, headers=headers, data=payload + ) + response_content = response.json() + response_id = response_content.get("id") + if response.status_code not in [ + constant.STATUS_CODE_202, + constant.STATUS_CODE_200, + ]: + return constant.STATUS_FALSE + return response_id + except Exception as e: + print("KALEYRA error", e) + return constant.STATUS_FALSE diff --git a/core/utils/token_authentication.py b/core/utils/token_authentication.py new file mode 100644 index 0000000..3dd90e9 --- /dev/null +++ b/core/utils/token_authentication.py @@ -0,0 +1,128 @@ +"""This module defines classes and methods for OAuth2 +authentication and token management. + +It contains a class `JWTOAuth2` with methods for generating access and +refresh tokens, encoding and decoding reset password and email confirmation tokens, +and verifying access tokens. +""" + +import datetime +import os +import jwt +from fastapi import APIRouter, status +from config import project_path +from core.utils.standard_response import StandardResponse +from config import jwt_config, signing_cookies +from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer + +oauth2router = APIRouter(tags="JWTOAuth2") +# Initialization for encoding +key = URLSafeTimedSerializer(signing_cookies.ITSDANGEROUS_KEY) +BACKEND_URL = os.environ.get("BACKEND_URL") + + +class JWTOAuth2: + """Class for JWT-based OAuth2 authentication and token management.""" + + def encode_access_token(self, identity: dict): + """ + Generates the Auth Token + + Args: + identity (tuple): Identity of the token to be decoded + + Returns: + JWT Token string + """ + try: + now = datetime.datetime.now() + exp_time = ( + now + jwt_config.JWT_LIFETIME + ) # Assuming JWT_LIFETIME is a timedelta + + payload = { + "iss": "Your-Issuer", # Set your issuer here + "exp": exp_time.timestamp(), # Convert to timestamp + "iat": now.timestamp(), # Created date of token + "sub": identity, # The subject of the token (the user whom it identifies) + } + + return jwt.encode( + payload, + signing_cookies.SIGNIN_SECRET_KEY, + algorithm=jwt_config.JWT_ALGORITHM, + ) + except: + return StandardResponse( + status=status.HTTP_400_BAD_REQUEST, + data=None, + message="Failed to encode!", + ).make + + def verify_access_token(self, token): + """Verify the validity of an access token. + + Args: + token (str): The access token to verify. + + Returns: + Any: The subject of the token if verification is successful. + + Raises: + jwt.ExpiredSignatureError: If the token is expired. + jwt.InvalidSignatureError: If the signature is invalid. + """ + payload = jwt.decode( + token, signing_cookies.SIGNIN_SECRET_KEY, algorithms=jwt_config.JWT_ALGORITHM + ) + return payload + + def decode_reset_password_token(self, token: str, max_age: int = None): + """ + This function is used to decode reset tokens + + Args: + token (str): Encoded token string + + Returns: + + Decoded token information + """ + # Default token expire time is 24hr (86400 Sec) + if not max_age: + max_age = 86400 + + try: + payload = key.loads(token, max_age=max_age) + + return payload + except SignatureExpired: + raise SignatureExpired("Your token is expired!") + except BadSignature: + raise BadSignature("Your token is invalid") + + def encode_reset_password_link(self, receiver_email: str): + """ + Generates the reset password link using reset token + + Args: + receiver_email(str): The email address of user + holder (bool): Whether a holder user or business user + + Returns: + reset password link + """ + + try: + reset_token = key.dumps( + { + "receiver_email": receiver_email, + "reset_key": signing_cookies.RESET_KEY, + } + ) + + return f"{BACKEND_URL}auth/reset/{reset_token}" + except: + return StandardResponse( + status.HTTP_400_BAD_REQUEST, data=None, message="Failed to encode!" + ).make diff --git a/middleware/authentication_middleware.py b/middleware/authentication_middleware.py new file mode 100644 index 0000000..53cd996 --- /dev/null +++ b/middleware/authentication_middleware.py @@ -0,0 +1,37 @@ +from fastapi import Depends, status, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession +from starlette.middleware.authentication import ( + AuthenticationMiddleware as BaseAuthenticationMiddleware, +) +from core.utils import constant_variable, message_variable +from core.utils.token_authentication import JWTOAuth2 +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from config import db_config + +oauth2_scheme = HTTPBearer() +getdb = db_config.get_db + + +async def authenticate( + authorize: HTTPAuthorizationCredentials = Depends(oauth2_scheme), + db: AsyncSession = Depends(getdb), +): + + # TODO: Modify middleware as per requirement + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=message_variable.INVALID_AUTH_TOKEN, + ) + + token_required_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=message_variable.TOKEN_REQUIRED, + ) + # try: + + token_data = JWTOAuth2().verify_access_token( + authorize.credentials + ) # This will raise an exception if the token is missing or invalid + sub = token_data.get("sub") + user_id = sub["id"] + return user_id diff --git a/middleware/rate_limiting_middleware.py b/middleware/rate_limiting_middleware.py new file mode 100644 index 0000000..d92e57d --- /dev/null +++ b/middleware/rate_limiting_middleware.py @@ -0,0 +1,40 @@ +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import JSONResponse +from time import time + + +class RateLimitMiddleware(BaseHTTPMiddleware): + def __init__(self, app, rate_limit: int, time_window: int = 60): + """ + :param app: The FastAPI app + :param rate_limit: Maximum number of requests allowed + :param time_window: Time window in seconds for rate limiting (default is 60 seconds) + """ + super().__init__(app) + self.rate_limit = rate_limit + self.time_window = time_window + self.request_count = 0 + self.window_start_time = time() + + async def dispatch(self, request: Request, call_next): + current_time = time() + + # Check if the time window has expired + if current_time - self.window_start_time > self.time_window: + # Reset the window + self.window_start_time = current_time + self.request_count = 0 + + # Enforce rate limiting + if self.request_count >= self.rate_limit: + return JSONResponse( + content={"error": "Rate limit exceeded"}, status_code=429 + ) + + # Increment request count + self.request_count += 1 + + # Process the request + response = await call_next(request) + return response \ No newline at end of file diff --git a/middleware/request_size_middleware.py b/middleware/request_size_middleware.py new file mode 100644 index 0000000..eb032fc --- /dev/null +++ b/middleware/request_size_middleware.py @@ -0,0 +1,17 @@ +from fastapi import status, HTTPException +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request + + +class LimitRequestSizeMiddleware(BaseHTTPMiddleware): + def __init__(self, app, max_body_size: int): + super().__init__(app) + self.max_body_size = max_body_size + + async def dispatch(self, request: Request, call_next): + if int(request.headers.get("content-length", 0)) > self.max_body_size: + raise HTTPException( + detail="Request entity too large", + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + ) + return await call_next(request) \ No newline at end of file diff --git a/middleware/response_log_middleware.py b/middleware/response_log_middleware.py new file mode 100644 index 0000000..5d3f575 --- /dev/null +++ b/middleware/response_log_middleware.py @@ -0,0 +1,34 @@ +from pydantic import BaseModel, Field, ConfigDict +from starlette.datastructures import Headers +from starlette.types import ASGIApp, Message, Receive, Scope, Send + + +class ResponseInfo(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + headers: Headers | None = Field(default=None, title="Response header") + body: str = Field(default="", title="Response body") + status_code: int | None = Field(default=None, title="Status code") + + +class ResponseLogMiddleware: + def __init__(self, app: ASGIApp) -> None: + self.app = app + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope["type"] != "http": + return await self.app(scope, receive, send) + + response_info = ResponseInfo() + + async def _logging_send(message: Message) -> None: + if message.get("type") == "http.response.start": + response_info.headers = Headers(raw=message.get("headers")) + response_info.status_code = message.get("status") + elif message.get("type") == "http.response.body": + if body := message.get("body"): + response_info.body += body.decode("utf8") + + await send(message) + + await self.app(scope, receive, _logging_send) diff --git a/poetry.lock b/poetry.lock index f0e6df5..3636306 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,126 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.4" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, + {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, +] + +[[package]] +name = "aiohttp" +version = "3.11.10" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.9" +files = [ + {file = "aiohttp-3.11.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cbad88a61fa743c5d283ad501b01c153820734118b65aee2bd7dbb735475ce0d"}, + {file = "aiohttp-3.11.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80886dac673ceaef499de2f393fc80bb4481a129e6cb29e624a12e3296cc088f"}, + {file = "aiohttp-3.11.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:61b9bae80ed1f338c42f57c16918853dc51775fb5cb61da70d590de14d8b5fb4"}, + {file = "aiohttp-3.11.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e2e576caec5c6a6b93f41626c9c02fc87cd91538b81a3670b2e04452a63def6"}, + {file = "aiohttp-3.11.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02c13415b5732fb6ee7ff64583a5e6ed1c57aa68f17d2bda79c04888dfdc2769"}, + {file = "aiohttp-3.11.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfce37f31f20800a6a6620ce2cdd6737b82e42e06e6e9bd1b36f546feb3c44f"}, + {file = "aiohttp-3.11.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bbbfff4c679c64e6e23cb213f57cc2c9165c9a65d63717108a644eb5a7398df"}, + {file = "aiohttp-3.11.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49c7dbbc1a559ae14fc48387a115b7d4bbc84b4a2c3b9299c31696953c2a5219"}, + {file = "aiohttp-3.11.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68386d78743e6570f054fe7949d6cb37ef2b672b4d3405ce91fafa996f7d9b4d"}, + {file = "aiohttp-3.11.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9ef405356ba989fb57f84cac66f7b0260772836191ccefbb987f414bcd2979d9"}, + {file = "aiohttp-3.11.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5d6958671b296febe7f5f859bea581a21c1d05430d1bbdcf2b393599b1cdce77"}, + {file = "aiohttp-3.11.10-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:99b7920e7165be5a9e9a3a7f1b680f06f68ff0d0328ff4079e5163990d046767"}, + {file = "aiohttp-3.11.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0dc49f42422163efb7e6f1df2636fe3db72713f6cd94688e339dbe33fe06d61d"}, + {file = "aiohttp-3.11.10-cp310-cp310-win32.whl", hash = "sha256:40d1c7a7f750b5648642586ba7206999650208dbe5afbcc5284bcec6579c9b91"}, + {file = "aiohttp-3.11.10-cp310-cp310-win_amd64.whl", hash = "sha256:68ff6f48b51bd78ea92b31079817aff539f6c8fc80b6b8d6ca347d7c02384e33"}, + {file = "aiohttp-3.11.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:77c4aa15a89847b9891abf97f3d4048f3c2d667e00f8a623c89ad2dccee6771b"}, + {file = "aiohttp-3.11.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:909af95a72cedbefe5596f0bdf3055740f96c1a4baa0dd11fd74ca4de0b4e3f1"}, + {file = "aiohttp-3.11.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:386fbe79863eb564e9f3615b959e28b222259da0c48fd1be5929ac838bc65683"}, + {file = "aiohttp-3.11.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3de34936eb1a647aa919655ff8d38b618e9f6b7f250cc19a57a4bf7fd2062b6d"}, + {file = "aiohttp-3.11.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c9527819b29cd2b9f52033e7fb9ff08073df49b4799c89cb5754624ecd98299"}, + {file = "aiohttp-3.11.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a96e3e03300b41f261bbfd40dfdbf1c301e87eab7cd61c054b1f2e7c89b9e8"}, + {file = "aiohttp-3.11.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f5635f7b74bcd4f6f72fcd85bea2154b323a9f05226a80bc7398d0c90763b0"}, + {file = "aiohttp-3.11.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:03b6002e20938fc6ee0918c81d9e776bebccc84690e2b03ed132331cca065ee5"}, + {file = "aiohttp-3.11.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6362cc6c23c08d18ddbf0e8c4d5159b5df74fea1a5278ff4f2c79aed3f4e9f46"}, + {file = "aiohttp-3.11.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3691ed7726fef54e928fe26344d930c0c8575bc968c3e239c2e1a04bd8cf7838"}, + {file = "aiohttp-3.11.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31d5093d3acd02b31c649d3a69bb072d539d4c7659b87caa4f6d2bcf57c2fa2b"}, + {file = "aiohttp-3.11.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8b3cf2dc0f0690a33f2d2b2cb15db87a65f1c609f53c37e226f84edb08d10f52"}, + {file = "aiohttp-3.11.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbbaea811a2bba171197b08eea288b9402faa2bab2ba0858eecdd0a4105753a3"}, + {file = "aiohttp-3.11.10-cp311-cp311-win32.whl", hash = "sha256:4b2c7ac59c5698a7a8207ba72d9e9c15b0fc484a560be0788b31312c2c5504e4"}, + {file = "aiohttp-3.11.10-cp311-cp311-win_amd64.whl", hash = "sha256:974d3a2cce5fcfa32f06b13ccc8f20c6ad9c51802bb7f829eae8a1845c4019ec"}, + {file = "aiohttp-3.11.10-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b78f053a7ecfc35f0451d961dacdc671f4bcbc2f58241a7c820e9d82559844cf"}, + {file = "aiohttp-3.11.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab7485222db0959a87fbe8125e233b5a6f01f4400785b36e8a7878170d8c3138"}, + {file = "aiohttp-3.11.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cf14627232dfa8730453752e9cdc210966490992234d77ff90bc8dc0dce361d5"}, + {file = "aiohttp-3.11.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:076bc454a7e6fd646bc82ea7f98296be0b1219b5e3ef8a488afbdd8e81fbac50"}, + {file = "aiohttp-3.11.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:482cafb7dc886bebeb6c9ba7925e03591a62ab34298ee70d3dd47ba966370d2c"}, + {file = "aiohttp-3.11.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf3d1a519a324af764a46da4115bdbd566b3c73fb793ffb97f9111dbc684fc4d"}, + {file = "aiohttp-3.11.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24213ba85a419103e641e55c27dc7ff03536c4873470c2478cce3311ba1eee7b"}, + {file = "aiohttp-3.11.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b99acd4730ad1b196bfb03ee0803e4adac371ae8efa7e1cbc820200fc5ded109"}, + {file = "aiohttp-3.11.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:14cdb5a9570be5a04eec2ace174a48ae85833c2aadc86de68f55541f66ce42ab"}, + {file = "aiohttp-3.11.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:7e97d622cb083e86f18317282084bc9fbf261801b0192c34fe4b1febd9f7ae69"}, + {file = "aiohttp-3.11.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:012f176945af138abc10c4a48743327a92b4ca9adc7a0e078077cdb5dbab7be0"}, + {file = "aiohttp-3.11.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44224d815853962f48fe124748227773acd9686eba6dc102578defd6fc99e8d9"}, + {file = "aiohttp-3.11.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c87bf31b7fdab94ae3adbe4a48e711bfc5f89d21cf4c197e75561def39e223bc"}, + {file = "aiohttp-3.11.10-cp312-cp312-win32.whl", hash = "sha256:06a8e2ee1cbac16fe61e51e0b0c269400e781b13bcfc33f5425912391a542985"}, + {file = "aiohttp-3.11.10-cp312-cp312-win_amd64.whl", hash = "sha256:be2b516f56ea883a3e14dda17059716593526e10fb6303189aaf5503937db408"}, + {file = "aiohttp-3.11.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8cc5203b817b748adccb07f36390feb730b1bc5f56683445bfe924fc270b8816"}, + {file = "aiohttp-3.11.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ef359ebc6949e3a34c65ce20230fae70920714367c63afd80ea0c2702902ccf"}, + {file = "aiohttp-3.11.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9bca390cb247dbfaec3c664326e034ef23882c3f3bfa5fbf0b56cad0320aaca5"}, + {file = "aiohttp-3.11.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:811f23b3351ca532af598405db1093f018edf81368e689d1b508c57dcc6b6a32"}, + {file = "aiohttp-3.11.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddf5f7d877615f6a1e75971bfa5ac88609af3b74796ff3e06879e8422729fd01"}, + {file = "aiohttp-3.11.10-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ab29b8a0beb6f8eaf1e5049252cfe74adbaafd39ba91e10f18caeb0e99ffb34"}, + {file = "aiohttp-3.11.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c49a76c1038c2dd116fa443eba26bbb8e6c37e924e2513574856de3b6516be99"}, + {file = "aiohttp-3.11.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f3dc0e330575f5b134918976a645e79adf333c0a1439dcf6899a80776c9ab39"}, + {file = "aiohttp-3.11.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:efb15a17a12497685304b2d976cb4939e55137df7b09fa53f1b6a023f01fcb4e"}, + {file = "aiohttp-3.11.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:db1d0b28fcb7f1d35600150c3e4b490775251dea70f894bf15c678fdd84eda6a"}, + {file = "aiohttp-3.11.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:15fccaf62a4889527539ecb86834084ecf6e9ea70588efde86e8bc775e0e7542"}, + {file = "aiohttp-3.11.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:593c114a2221444f30749cc5e5f4012488f56bd14de2af44fe23e1e9894a9c60"}, + {file = "aiohttp-3.11.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7852bbcb4d0d2f0c4d583f40c3bc750ee033265d80598d0f9cb6f372baa6b836"}, + {file = "aiohttp-3.11.10-cp313-cp313-win32.whl", hash = "sha256:65e55ca7debae8faaffee0ebb4b47a51b4075f01e9b641c31e554fd376595c6c"}, + {file = "aiohttp-3.11.10-cp313-cp313-win_amd64.whl", hash = "sha256:beb39a6d60a709ae3fb3516a1581777e7e8b76933bb88c8f4420d875bb0267c6"}, + {file = "aiohttp-3.11.10-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0580f2e12de2138f34debcd5d88894786453a76e98febaf3e8fe5db62d01c9bf"}, + {file = "aiohttp-3.11.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a55d2ad345684e7c3dd2c20d2f9572e9e1d5446d57200ff630e6ede7612e307f"}, + {file = "aiohttp-3.11.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:04814571cb72d65a6899db6099e377ed00710bf2e3eafd2985166f2918beaf59"}, + {file = "aiohttp-3.11.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e44a9a3c053b90c6f09b1bb4edd880959f5328cf63052503f892c41ea786d99f"}, + {file = "aiohttp-3.11.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:502a1464ccbc800b4b1995b302efaf426e8763fadf185e933c2931df7db9a199"}, + {file = "aiohttp-3.11.10-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:613e5169f8ae77b1933e42e418a95931fb4867b2991fc311430b15901ed67079"}, + {file = "aiohttp-3.11.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cca22a61b7fe45da8fc73c3443150c3608750bbe27641fc7558ec5117b27fdf"}, + {file = "aiohttp-3.11.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86a5dfcc39309470bd7b68c591d84056d195428d5d2e0b5ccadfbaf25b026ebc"}, + {file = "aiohttp-3.11.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:77ae58586930ee6b2b6f696c82cf8e78c8016ec4795c53e36718365f6959dc82"}, + {file = "aiohttp-3.11.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:78153314f26d5abef3239b4a9af20c229c6f3ecb97d4c1c01b22c4f87669820c"}, + {file = "aiohttp-3.11.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:98283b94cc0e11c73acaf1c9698dea80c830ca476492c0fe2622bd931f34b487"}, + {file = "aiohttp-3.11.10-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:53bf2097e05c2accc166c142a2090e4c6fd86581bde3fd9b2d3f9e93dda66ac1"}, + {file = "aiohttp-3.11.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5532f0441fc09c119e1dca18fbc0687e64fbeb45aa4d6a87211ceaee50a74c4"}, + {file = "aiohttp-3.11.10-cp39-cp39-win32.whl", hash = "sha256:47ad15a65fb41c570cd0ad9a9ff8012489e68176e7207ec7b82a0940dddfd8be"}, + {file = "aiohttp-3.11.10-cp39-cp39-win_amd64.whl", hash = "sha256:c6b9e6d7e41656d78e37ce754813fa44b455c3d0d0dced2a047def7dc5570b74"}, + {file = "aiohttp-3.11.10.tar.gz", hash = "sha256:b1fc6b45010a8d0ff9e88f9f2418c6fd408c99c211257334aff41597ebece42e"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.3.0" +aiosignal = ">=1.1.2" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +propcache = ">=0.2.0" +yarl = ">=1.17.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] + +[[package]] +name = "aiohttp-retry" +version = "2.8.3" +description = "Simple retry client for aiohttp" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiohttp_retry-2.8.3-py3-none-any.whl", hash = "sha256:3aeeead8f6afe48272db93ced9440cf4eda8b6fd7ee2abb25357b7eb28525b45"}, + {file = "aiohttp_retry-2.8.3.tar.gz", hash = "sha256:9a8e637e31682ad36e1ff9f8bcba912fcfc7d7041722bc901a4b948da4d71ea9"}, +] + +[package.dependencies] +aiohttp = "*" [[package]] name = "aiomysql" @@ -18,6 +140,20 @@ PyMySQL = ">=1.0" rsa = ["PyMySQL[rsa] (>=1.0)"] sa = ["sqlalchemy (>=1.3,<1.4)"] +[[package]] +name = "aiosignal" +version = "1.3.2" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.9" +files = [ + {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, + {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + [[package]] name = "aiosmtplib" version = "3.0.2" @@ -33,6 +169,39 @@ files = [ docs = ["furo (>=2023.9.10)", "sphinx (>=7.0.0)", "sphinx-autodoc-typehints (>=1.24.0)", "sphinx-copybutton (>=0.5.0)"] uvloop = ["uvloop (>=0.18)"] +[[package]] +name = "alembic" +version = "1.14.0" +description = "A database migration tool for SQLAlchemy." +optional = false +python-versions = ">=3.8" +files = [ + {file = "alembic-1.14.0-py3-none-any.whl", hash = "sha256:99bd884ca390466db5e27ffccff1d179ec5c05c965cfefc0607e69f9e411cb25"}, + {file = "alembic-1.14.0.tar.gz", hash = "sha256:b00892b53b3642d0b8dbedba234dbf1924b69be83a9a769d5a624b01094e304b"}, +] + +[package.dependencies] +Mako = "*" +SQLAlchemy = ">=1.3.0" +typing-extensions = ">=4" + +[package.extras] +tz = ["backports.zoneinfo"] + +[[package]] +name = "amqp" +version = "5.3.1" +description = "Low-level AMQP client for Python (fork of amqplib)." +optional = false +python-versions = ">=3.6" +files = [ + {file = "amqp-5.3.1-py3-none-any.whl", hash = "sha256:43b3319e1b4e7d1251833a93d672b4af1e40f3d632d479b98661a95f117880a2"}, + {file = "amqp-5.3.1.tar.gz", hash = "sha256:cddc00c725449522023bad949f70fff7b48f0b1ade74d170a6f10ab044739432"}, +] + +[package.dependencies] +vine = ">=5.0.0,<6.0.0" + [[package]] name = "annotated-types" version = "0.7.0" @@ -65,6 +234,101 @@ doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", 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", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] +[[package]] +name = "apscheduler" +version = "3.11.0" +description = "In-process task scheduler with Cron-like capabilities" +optional = false +python-versions = ">=3.8" +files = [ + {file = "APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da"}, + {file = "apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133"}, +] + +[package.dependencies] +tzlocal = ">=3.0" + +[package.extras] +doc = ["packaging", "sphinx", "sphinx-rtd-theme (>=1.3.0)"] +etcd = ["etcd3", "protobuf (<=3.21.0)"] +gevent = ["gevent"] +mongodb = ["pymongo (>=3.0)"] +redis = ["redis (>=3.0)"] +rethinkdb = ["rethinkdb (>=2.4.0)"] +sqlalchemy = ["sqlalchemy (>=1.4)"] +test = ["APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]", "PySide6", "anyio (>=4.5.2)", "gevent", "pytest", "pytz", "twisted"] +tornado = ["tornado (>=4.3)"] +twisted = ["twisted"] +zookeeper = ["kazoo"] + +[[package]] +name = "attrs" +version = "24.3.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +files = [ + {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, + {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "bcrypt" +version = "4.2.1" +description = "Modern password hashing for your software and your servers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "bcrypt-4.2.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea"}, + {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6"}, + {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139"}, + {file = "bcrypt-4.2.1-cp37-abi3-win32.whl", hash = "sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005"}, + {file = "bcrypt-4.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526"}, + {file = "bcrypt-4.2.1-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99"}, + {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84"}, + {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d"}, + {file = "bcrypt-4.2.1-cp39-abi3-win32.whl", hash = "sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf"}, + {file = "bcrypt-4.2.1-cp39-abi3-win_amd64.whl", hash = "sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c"}, + {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76132c176a6d9953cdc83c296aeaed65e1a708485fd55abf163e0d9f8f16ce0e"}, + {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e158009a54c4c8bc91d5e0da80920d048f918c61a581f0a63e4e93bb556d362f"}, + {file = "bcrypt-4.2.1.tar.gz", hash = "sha256:6765386e3ab87f569b276988742039baab087b2cdb01e809d74e74503c2faafe"}, +] + +[package.extras] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] + +[[package]] +name = "billiard" +version = "4.2.1" +description = "Python multiprocessing fork with improvements and bugfixes" +optional = false +python-versions = ">=3.7" +files = [ + {file = "billiard-4.2.1-py3-none-any.whl", hash = "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb"}, + {file = "billiard-4.2.1.tar.gz", hash = "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f"}, +] + [[package]] name = "bleach" version = "6.2.0" @@ -95,17 +359,17 @@ files = [ [[package]] name = "boto3" -version = "1.35.79" +version = "1.35.80" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.79-py3-none-any.whl", hash = "sha256:a673b0b6378c9ccbf045a31a43195b175e12aa5c37fb7635fcbfc8f48fb857b3"}, - {file = "boto3-1.35.79.tar.gz", hash = "sha256:1fa26217cd33ded82e55aed4460cd55f7223fa647916aa0d3c5d6828e6ec7135"}, + {file = "boto3-1.35.80-py3-none-any.whl", hash = "sha256:21a3b18c3a7fd20e463708fe3fa035983105dc7f3a1c274e1903e1583ab91159"}, + {file = "boto3-1.35.80.tar.gz", hash = "sha256:50dae461ab5fbedfb81b690895d48a918fed0d5fdff37be1c4232770c0dc9712"}, ] [package.dependencies] -botocore = ">=1.35.79,<1.36.0" +botocore = ">=1.35.80,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -114,13 +378,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.79" +version = "1.35.80" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.79-py3-none-any.whl", hash = "sha256:e6b10bb9a357e3f5ca2e60f6dd15a85d311b9a476eb21b3c0c2a3b364a2897c8"}, - {file = "botocore-1.35.79.tar.gz", hash = "sha256:245bfdda1b1508539ddd1819c67a8a2cc81780adf0715d3de418d64c4247f346"}, + {file = "botocore-1.35.80-py3-none-any.whl", hash = "sha256:36e589dccb62380abd628b08fecfa2f7c89b99f41ec9fc42c467c94008c0be4a"}, + {file = "botocore-1.35.80.tar.gz", hash = "sha256:b8dfceca58891cb2711bd6455ec4f7159051f3796e0f64adef9bb334f19d8a92"}, ] [package.dependencies] @@ -131,6 +395,73 @@ urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version > [package.extras] crt = ["awscrt (==0.22.0)"] +[[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 = "celery" +version = "5.4.0" +description = "Distributed Task Queue." +optional = false +python-versions = ">=3.8" +files = [ + {file = "celery-5.4.0-py3-none-any.whl", hash = "sha256:369631eb580cf8c51a82721ec538684994f8277637edde2dfc0dacd73ed97f64"}, + {file = "celery-5.4.0.tar.gz", hash = "sha256:504a19140e8d3029d5acad88330c541d4c3f64c789d85f94756762d8bca7e706"}, +] + +[package.dependencies] +billiard = ">=4.2.0,<5.0" +click = ">=8.1.2,<9.0" +click-didyoumean = ">=0.3.0" +click-plugins = ">=1.1.1" +click-repl = ">=0.2.0" +kombu = ">=5.3.4,<6.0" +python-dateutil = ">=2.8.2" +tzdata = ">=2022.7" +vine = ">=5.1.0,<6.0" + +[package.extras] +arangodb = ["pyArango (>=2.0.2)"] +auth = ["cryptography (==42.0.5)"] +azureblockblob = ["azure-storage-blob (>=12.15.0)"] +brotli = ["brotli (>=1.0.0)", "brotlipy (>=0.7.0)"] +cassandra = ["cassandra-driver (>=3.25.0,<4)"] +consul = ["python-consul2 (==0.1.5)"] +cosmosdbsql = ["pydocumentdb (==2.3.5)"] +couchbase = ["couchbase (>=3.0.0)"] +couchdb = ["pycouchdb (==1.14.2)"] +django = ["Django (>=2.2.28)"] +dynamodb = ["boto3 (>=1.26.143)"] +elasticsearch = ["elastic-transport (<=8.13.0)", "elasticsearch (<=8.13.0)"] +eventlet = ["eventlet (>=0.32.0)"] +gcs = ["google-cloud-storage (>=2.10.0)"] +gevent = ["gevent (>=1.5.0)"] +librabbitmq = ["librabbitmq (>=2.0.0)"] +memcache = ["pylibmc (==1.6.3)"] +mongodb = ["pymongo[srv] (>=4.0.2)"] +msgpack = ["msgpack (==1.0.8)"] +pymemcache = ["python-memcached (>=1.61)"] +pyro = ["pyro4 (==4.82)"] +pytest = ["pytest-celery[all] (>=1.0.0)"] +redis = ["redis (>=4.5.2,!=4.5.5,<6.0.0)"] +s3 = ["boto3 (>=1.26.143)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +solar = ["ephem (==4.1.5)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "kombu[sqs] (>=5.3.4)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] +tblib = ["tblib (>=1.3.0)", "tblib (>=1.5.0)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=1.3.1)"] +zstd = ["zstandard (==0.22.0)"] + [[package]] name = "certifi" version = "2024.8.30" @@ -221,6 +552,120 @@ files = [ [package.dependencies] pycparser = "*" +[[package]] +name = "charset-normalizer" +version = "3.4.0" +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.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +] + [[package]] name = "click" version = "8.1.7" @@ -235,6 +680,55 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[[package]] +name = "click-didyoumean" +version = "0.3.1" +description = "Enables git-like *did-you-mean* feature in click" +optional = false +python-versions = ">=3.6.2" +files = [ + {file = "click_didyoumean-0.3.1-py3-none-any.whl", hash = "sha256:5c4bb6007cfea5f2fd6583a2fb6701a22a41eb98957e63d0fac41c10e7c3117c"}, + {file = "click_didyoumean-0.3.1.tar.gz", hash = "sha256:4f82fdff0dbe64ef8ab2279bd6aa3f6a99c3b28c05aa09cbfc07c9d7fbb5a463"}, +] + +[package.dependencies] +click = ">=7" + +[[package]] +name = "click-plugins" +version = "1.1.1" +description = "An extension module for click to enable registering CLI commands via setuptools entry-points." +optional = false +python-versions = "*" +files = [ + {file = "click-plugins-1.1.1.tar.gz", hash = "sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b"}, + {file = "click_plugins-1.1.1-py2.py3-none-any.whl", hash = "sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"}, +] + +[package.dependencies] +click = ">=4.0" + +[package.extras] +dev = ["coveralls", "pytest (>=3.6)", "pytest-cov", "wheel"] + +[[package]] +name = "click-repl" +version = "0.3.0" +description = "REPL plugin for Click" +optional = false +python-versions = ">=3.6" +files = [ + {file = "click-repl-0.3.0.tar.gz", hash = "sha256:17849c23dba3d667247dc4defe1757fff98694e90fe37474f3feebb69ced26a9"}, + {file = "click_repl-0.3.0-py3-none-any.whl", hash = "sha256:fb7e06deb8da8de86180a33a9da97ac316751c094c6899382da7feeeeb51b812"}, +] + +[package.dependencies] +click = ">=7.0" +prompt-toolkit = ">=3.0.36" + +[package.extras] +testing = ["pytest (>=7.2.1)", "pytest-cov (>=4.0.0)", "tox (>=4.4.3)"] + [[package]] name = "colorama" version = "0.4.6" @@ -259,7 +753,6 @@ files = [ {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, @@ -270,7 +763,6 @@ files = [ {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, @@ -419,6 +911,131 @@ starlette = ">=0.24,<1.0" httpx = ["httpx[httpx] (>=0.23,<0.24)"] redis = ["redis[redis] (>=4.3,<5.0)"] +[[package]] +name = "frozenlist" +version = "1.5.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5b6a66c18b5b9dd261ca98dffcb826a525334b2f29e7caa54e182255c5f6a65a"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d1b3eb7b05ea246510b43a7e53ed1653e55c2121019a97e60cad7efb881a97bb"}, + {file = "frozenlist-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15538c0cbf0e4fa11d1e3a71f823524b0c46299aed6e10ebb4c2089abd8c3bec"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79225373c317ff1e35f210dd5f1344ff31066ba8067c307ab60254cd3a78ad5"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9272fa73ca71266702c4c3e2d4a28553ea03418e591e377a03b8e3659d94fa76"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:498524025a5b8ba81695761d78c8dd7382ac0b052f34e66939c42df860b8ff17"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:92b5278ed9d50fe610185ecd23c55d8b307d75ca18e94c0e7de328089ac5dcba"}, + {file = "frozenlist-1.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f3c8c1dacd037df16e85227bac13cca58c30da836c6f936ba1df0c05d046d8d"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2ac49a9bedb996086057b75bf93538240538c6d9b38e57c82d51f75a73409d2"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e66cc454f97053b79c2ab09c17fbe3c825ea6b4de20baf1be28919460dd7877f"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:5a3ba5f9a0dfed20337d3e966dc359784c9f96503674c2faf015f7fe8e96798c"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6321899477db90bdeb9299ac3627a6a53c7399c8cd58d25da094007402b039ab"}, + {file = "frozenlist-1.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76e4753701248476e6286f2ef492af900ea67d9706a0155335a40ea21bf3b2f5"}, + {file = "frozenlist-1.5.0-cp310-cp310-win32.whl", hash = "sha256:977701c081c0241d0955c9586ffdd9ce44f7a7795df39b9151cd9a6fd0ce4cfb"}, + {file = "frozenlist-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:189f03b53e64144f90990d29a27ec4f7997d91ed3d01b51fa39d2dbe77540fd4"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5"}, + {file = "frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45"}, + {file = "frozenlist-1.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c2623347b933fcb9095841f1cc5d4ff0b278addd743e0e966cb3d460278840d"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2"}, + {file = "frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf"}, + {file = "frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942"}, + {file = "frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d"}, + {file = "frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6"}, + {file = "frozenlist-1.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000a77d6034fbad9b6bb880f7ec073027908f1b40254b5d6f26210d2dab1240e"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631"}, + {file = "frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f"}, + {file = "frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8"}, + {file = "frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0"}, + {file = "frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840"}, + {file = "frozenlist-1.5.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b7dc0c4338e6b8b091e8faf0db3168a37101943e687f373dce00959583f7439"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9"}, + {file = "frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03"}, + {file = "frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c"}, + {file = "frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:dd94994fc91a6177bfaafd7d9fd951bc8689b0a98168aa26b5f543868548d3ca"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0da8bbec082bf6bf18345b180958775363588678f64998c2b7609e34719b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:73f2e31ea8dd7df61a359b731716018c2be196e5bb3b74ddba107f694fbd7604"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:828afae9f17e6de596825cf4228ff28fbdf6065974e5ac1410cecc22f699d2b3"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1577515d35ed5649d52ab4319db757bb881ce3b2b796d7283e6634d99ace307"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2150cc6305a2c2ab33299453e2968611dacb970d2283a14955923062c8d00b10"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a72b7a6e3cd2725eff67cd64c8f13335ee18fc3c7befc05aed043d24c7b9ccb9"}, + {file = "frozenlist-1.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c16d2fa63e0800723139137d667e1056bee1a1cf7965153d2d104b62855e9b99"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:17dcc32fc7bda7ce5875435003220a457bcfa34ab7924a49a1c19f55b6ee185c"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97160e245ea33d8609cd2b8fd997c850b56db147a304a262abc2b3be021a9171"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f1e6540b7fa044eee0bb5111ada694cf3dc15f2b0347ca125ee9ca984d5e9e6e"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:91d6c171862df0a6c61479d9724f22efb6109111017c87567cfeb7b5d1449fdf"}, + {file = "frozenlist-1.5.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c1fac3e2ace2eb1052e9f7c7db480818371134410e1f5c55d65e8f3ac6d1407e"}, + {file = "frozenlist-1.5.0-cp38-cp38-win32.whl", hash = "sha256:b97f7b575ab4a8af9b7bc1d2ef7f29d3afee2226bd03ca3875c16451ad5a7723"}, + {file = "frozenlist-1.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:374ca2dabdccad8e2a76d40b1d037f5bd16824933bf7bcea3e59c891fd4a0923"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9bbcdfaf4af7ce002694a4e10a0159d5a8d20056a12b05b45cea944a4953f972"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1893f948bf6681733aaccf36c5232c231e3b5166d607c5fa77773611df6dc336"}, + {file = "frozenlist-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2b5e23253bb709ef57a8e95e6ae48daa9ac5f265637529e4ce6b003a37b2621f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f253985bb515ecd89629db13cb58d702035ecd8cfbca7d7a7e29a0e6d39af5f"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04a5c6babd5e8fb7d3c871dc8b321166b80e41b637c31a995ed844a6139942b6"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9fe0f1c29ba24ba6ff6abf688cb0b7cf1efab6b6aa6adc55441773c252f7411"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226d72559fa19babe2ccd920273e767c96a49b9d3d38badd7c91a0fdeda8ea08"}, + {file = "frozenlist-1.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b731db116ab3aedec558573c1a5eec78822b32292fe4f2f0345b7f697745c2"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:366d8f93e3edfe5a918c874702f78faac300209a4d5bf38352b2c1bdc07a766d"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1b96af8c582b94d381a1c1f51ffaedeb77c821c690ea5f01da3d70a487dd0a9b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c03eff4a41bd4e38415cbed054bbaff4a075b093e2394b6915dca34a40d1e38b"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:50cf5e7ee9b98f22bdecbabf3800ae78ddcc26e4a435515fc72d97903e8488e0"}, + {file = "frozenlist-1.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1e76bfbc72353269c44e0bc2cfe171900fbf7f722ad74c9a7b638052afe6a00c"}, + {file = "frozenlist-1.5.0-cp39-cp39-win32.whl", hash = "sha256:666534d15ba8f0fda3f53969117383d5dc021266b3c1a42c9ec4855e4b58b9d3"}, + {file = "frozenlist-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c28f4b5dbef8a0d8aad0d4de24d1e9e981728628afaf4ea0792f5d0939372f0"}, + {file = "frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3"}, + {file = "frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817"}, +] + +[[package]] +name = "google-auth" +version = "2.37.0" +description = "Google Authentication Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "google_auth-2.37.0-py2.py3-none-any.whl", hash = "sha256:42664f18290a6be591be5329a96fe30184be1a1badb7292a7f686a9659de9ca0"}, + {file = "google_auth-2.37.0.tar.gz", hash = "sha256:0054623abf1f9c83492c63d3f47e77f0a544caa3d40b2d98e099a611c2dd5d00"}, +] + +[package.dependencies] +cachetools = ">=2.0.0,<6.0" +pyasn1-modules = ">=0.2.1" +rsa = ">=3.1.4,<5" + +[package.extras] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] +enterprise-cert = ["cryptography", "pyopenssl"] +pyjwt = ["cryptography (>=38.0.3)", "pyjwt (>=2.0)"] +pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +reauth = ["pyu2f (>=0.1.5)"] +requests = ["requests (>=2.20.0,<3.0.0.dev0)"] + [[package]] name = "greenlet" version = "3.1.1" @@ -630,6 +1247,17 @@ files = [ [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 = "itsdangerous" version = "2.2.0" @@ -669,6 +1297,52 @@ files = [ {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, ] +[[package]] +name = "jwt" +version = "1.3.1" +description = "JSON Web Token library for Python 3." +optional = false +python-versions = ">= 3.6" +files = [ + {file = "jwt-1.3.1-py3-none-any.whl", hash = "sha256:61c9170f92e736b530655e75374681d4fcca9cfa8763ab42be57353b2b203494"}, +] + +[package.dependencies] +cryptography = ">=3.1,<3.4.0 || >3.4.0" + +[[package]] +name = "kombu" +version = "5.4.2" +description = "Messaging library for Python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "kombu-5.4.2-py3-none-any.whl", hash = "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763"}, + {file = "kombu-5.4.2.tar.gz", hash = "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf"}, +] + +[package.dependencies] +amqp = ">=5.1.1,<6.0.0" +tzdata = {version = "*", markers = "python_version >= \"3.9\""} +vine = "5.1.0" + +[package.extras] +azureservicebus = ["azure-servicebus (>=7.10.0)"] +azurestoragequeues = ["azure-identity (>=1.12.0)", "azure-storage-queue (>=12.6.0)"] +confluentkafka = ["confluent-kafka (>=2.2.0)"] +consul = ["python-consul2 (==0.1.5)"] +librabbitmq = ["librabbitmq (>=2.0.0)"] +mongodb = ["pymongo (>=4.1.1)"] +msgpack = ["msgpack (==1.1.0)"] +pyro = ["pyro4 (==4.82)"] +qpid = ["qpid-python (>=0.26)", "qpid-tools (>=0.26)"] +redis = ["redis (>=4.5.2,!=4.5.5,!=5.0.2)"] +slmq = ["softlayer-messaging (>=1.0.3)"] +sqlalchemy = ["sqlalchemy (>=1.4.48,<2.1)"] +sqs = ["boto3 (>=1.26.143)", "pycurl (>=7.43.0.5)", "urllib3 (>=1.26.16)"] +yaml = ["PyYAML (>=3.10)"] +zookeeper = ["kazoo (>=2.8.0)"] + [[package]] name = "limits" version = "3.14.1" @@ -707,6 +1381,25 @@ files = [ {file = "logging-0.4.9.6.tar.gz", hash = "sha256:26f6b50773f085042d301085bd1bf5d9f3735704db9f37c1ce6d8b85c38f2417"}, ] +[[package]] +name = "mako" +version = "1.3.8" +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.8-py3-none-any.whl", hash = "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627"}, + {file = "mako-1.3.8.tar.gz", hash = "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8"}, +] + +[package.dependencies] +MarkupSafe = ">=0.9.2" + +[package.extras] +babel = ["Babel"] +lingua = ["lingua"] +testing = ["pytest"] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -812,6 +1505,107 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "multidict" +version = "6.1.0" +description = "multidict implementation" +optional = false +python-versions = ">=3.8" +files = [ + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, + {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, + {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, + {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, + {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, +] + [[package]] name = "orjson" version = "3.10.12" @@ -936,6 +1730,151 @@ files = [ {file = "pdfkit-1.0.0.tar.gz", hash = "sha256:992f821e1e18fc8a0e701ecae24b51a2d598296a180caee0a24c0af181da02a9"}, ] +[[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 = "prompt-toolkit" +version = "3.0.48" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, + {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "propcache" +version = "0.2.1" +description = "Accelerated property cache" +optional = false +python-versions = ">=3.9" +files = [ + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b"}, + {file = "propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4"}, + {file = "propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e"}, + {file = "propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034"}, + {file = "propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518"}, + {file = "propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246"}, + {file = "propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30"}, + {file = "propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6"}, + {file = "propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587"}, + {file = "propcache-0.2.1-cp39-cp39-win32.whl", hash = "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb"}, + {file = "propcache-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1"}, + {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"}, + {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.1" +description = "A collection of ASN.1-based protocols modules" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, + {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, +] + +[package.dependencies] +pyasn1 = ">=0.4.6,<0.7.0" + [[package]] name = "pycparser" version = "2.22" @@ -1081,13 +2020,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pydantic-settings" -version = "2.6.1" +version = "2.7.0" description = "Settings management using Pydantic" optional = false python-versions = ">=3.8" files = [ - {file = "pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87"}, - {file = "pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0"}, + {file = "pydantic_settings-2.7.0-py3-none-any.whl", hash = "sha256:e00c05d5fa6cbbb227c84bd7487c5c1065084119b750df7c8c1a554aed236eb5"}, + {file = "pydantic_settings-2.7.0.tar.gz", hash = "sha256:ac4bfd4a36831a48dbf8b2d9325425b549a0a6f18cea118436d728eb4f1c4d66"}, ] [package.dependencies] @@ -1113,6 +2052,23 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyjwt" +version = "2.10.1" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, +] + +[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 = "pymysql" version = "1.1.1" @@ -1128,6 +2084,40 @@ files = [ ed25519 = ["PyNaCl (>=1.4.0)"] rsa = ["cryptography"] +[[package]] +name = "pyotp" +version = "2.9.0" +description = "Python One Time Password Library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyotp-2.9.0-py3-none-any.whl", hash = "sha256:81c2e5865b8ac55e825b0358e496e1d9387c811e85bb40e71a3b29b288963612"}, + {file = "pyotp-2.9.0.tar.gz", hash = "sha256:346b6642e0dbdde3b4ff5a930b664ca82abfa116356ed48cc42c7d6590d36f63"}, +] + +[package.extras] +test = ["coverage", "mypy", "ruff", "wheel"] + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[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 = "python-dateutil" version = "2.9.0.post0" @@ -1255,6 +2245,27 @@ files = [ hiredis = ["hiredis (>=3.0.0)"] ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==23.2.1)", "requests (>=2.31.0)"] +[[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 = "rich" version = "13.9.4" @@ -1289,6 +2300,20 @@ click = ">=8.1.7" rich = ">=13.7.1" typing-extensions = ">=4.12.2" +[[package]] +name = "rsa" +version = "4.9" +description = "Pure-Python RSA implementation" +optional = false +python-versions = ">=3.6,<4" +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] + +[package.dependencies] +pyasn1 = ">=0.1.3" + [[package]] name = "s3transfer" version = "0.10.4" @@ -1471,6 +2496,23 @@ files = [ {file = "style-1.1.0.tar.gz", hash = "sha256:8eb365fc15039b19b728bd4e6e85fb7daf24e7aeeec6a15a666f97484c564005"}, ] +[[package]] +name = "twilio" +version = "9.4.1" +description = "Twilio API client and TwiML generator" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "twilio-9.4.1-py2.py3-none-any.whl", hash = "sha256:2447e041cec11167d7765aaa62ab1dae3b82b712245ca9a966096acd8b9f426f"}, + {file = "twilio-9.4.1.tar.gz", hash = "sha256:e24c640696ccc726bba14160951da3cfc6b4bcb772fdcb3e8c16dc3cc851ef12"}, +] + +[package.dependencies] +aiohttp = ">=3.8.4" +aiohttp-retry = "2.8.3" +PyJWT = ">=2.0.0,<3.0.0" +requests = ">=2.0.0" + [[package]] name = "typer" version = "0.15.1" @@ -1499,6 +2541,34 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] + +[[package]] +name = "tzlocal" +version = "5.2" +description = "tzinfo object for the local timezone" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tzlocal-5.2-py3-none-any.whl", hash = "sha256:49816ef2fe65ea8ac19d19aa7a1ae0551c834303d5014c6d5a62e4cbda8047b8"}, + {file = "tzlocal-5.2.tar.gz", hash = "sha256:8d399205578f1a9342816409cc1e46a93ebd5755e39ea2d85334bea911bf0e6e"}, +] + +[package.dependencies] +tzdata = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] + [[package]] name = "ujson" version = "5.10.0" @@ -1692,6 +2762,17 @@ dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] 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 = ["aiohttp (>=3.10.5)", "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 = "vine" +version = "5.1.0" +description = "Python promises." +optional = false +python-versions = ">=3.6" +files = [ + {file = "vine-5.1.0-py3-none-any.whl", hash = "sha256:40fdf3c48b2cfe1c38a49e9ae2da6fda88e4794c810050a728bd7413811fb1dc"}, + {file = "vine-5.1.0.tar.gz", hash = "sha256:8b62e981d35c41049211cf62a0a1242d8c1ee9bd15bb196ce38aefd6799e61e0"}, +] + [[package]] name = "watchfiles" version = "1.0.3" @@ -1775,6 +2856,17 @@ files = [ [package.dependencies] anyio = ">=3.0.0" +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + [[package]] name = "webencodings" version = "0.5.1" @@ -1938,7 +3030,103 @@ files = [ {file = "wrapt-1.17.0.tar.gz", hash = "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801"}, ] +[[package]] +name = "yarl" +version = "1.18.3" +description = "Yet another URL library" +optional = false +python-versions = ">=3.9" +files = [ + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"}, + {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"}, + {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"}, + {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"}, + {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, + {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, + {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"}, + {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"}, + {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"}, + {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"}, + {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"}, + {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, + {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" +propcache = ">=0.2.0" + [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "ed691696f7c56dc711f04b74bc4c05321b16ac809274600250b37d44244d69e6" +content-hash = "5c3a9d328ac4f1152edb830063ce18e7f92367c4f39c867eb1a52302e766cb80" diff --git a/product_structure.txt b/product_structure.txt index 79b2293..8f01d02 100644 --- a/product_structure.txt +++ b/product_structure.txt @@ -29,7 +29,9 @@ python-fastapi-boilerplate/ │ │ │ │ ├─ services/ │ │ │ │ │ ├─ service.py │ │ │ │ │ └─ __init__.py -│ │ │ │ ├─ test.py +│ │ │ │ ├─ tests/ +│ │ │ │ │ ├─ conftest.py +│ │ │ │ │ ├─ test.py │ │ │ │ ├─ view.py │ │ │ │ ├─ __init__.py │ │ │ └─ __init__.py @@ -51,6 +53,7 @@ python-fastapi-boilerplate/ │ ├─ project_path.py │ ├─ redis_config.py │ ├─ session_config.py +| ├─ celery_config.py │ ├─ __init__.py ├─ core/ │ ├─ db/ @@ -85,6 +88,12 @@ python-fastapi-boilerplate/ │ ├─ s3_middleware.py │ ├─ session_middleware.py │ ├─ __init__.py +├─ celery_tasks/ +│ ├─ tasks/ +│ │ ├─ example_task.py +│ │ └─ __init__.py +│ ├─ worker.py +│ └─ __init__.py ├─ poetry.lock ├─ pyproject.toml └─ README.md diff --git a/pyproject.toml b/pyproject.toml index 961afb5..eb338ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,18 @@ pdfkit = "^1.0.0" logging = "^0.4.9.6" update = "^0.0.1" anyio = "^4.7.0" +click = "^8.1.7" +alembic = "^1.14.0" +bcrypt = "^4.2.1" +jwt = "^1.3.1" +pyjwt = "^2.10.1" +google-auth = "^2.37.0" +requests = "^2.32.3" +pyotp = "^2.9.0" +twilio = "^9.4.1" +pytest = "^8.3.4" +celery = "^5.4.0" +apscheduler = "^3.11.0" [build-system] diff --git a/scheduler_service.py/constant.py b/scheduler_service.py/constant.py new file mode 100644 index 0000000..0de69e8 --- /dev/null +++ b/scheduler_service.py/constant.py @@ -0,0 +1,88 @@ +import datetime as dt +from datetime import datetime, timedelta, timezone + +import pytz + +# API response status +STATUS_SUCCESS = "success" +STATUS_FAIL = "fail" +STATUS_ERROR = "error" + +# Hours +# FOUR = datetime.time(4, 00, 00) +# FOUR = dt.timedelta(11, 34, 59) + +# Boolean status code +STATUS_ZERO = 0 +STATUS_DOUBLEZERO = 00 +STATUS_ONE = 1 +STATUS_TWO = 2 +STATUS_THREE = 3 +STATUS_FOUR = 4 +STATUS_FIVE = 5 +STATUS_SIX = 6 +STATUS_SEVEN = 7 +STATUS_EIGHT = 8 +STATUS_NINE = 9 +STATUS_TEN = 10 +TWELVE = 12 +TWENTY_TWO = 22 +THIRTY = 30 +ONE_TWENTY = 120 +STATUS_TRUE = True +STATUS_FALSE = False +STATUS_NULL = None + +# Entity status +STATUS_ACTIVE = "active" +STATUS_INACTIVE = "inactive" +STATUS_PUBLISHED = "published" +STATUS_UNPUBLISHED = "unpublished" +STATUS_CREATE = "create" +STATUS_UPDATE = "update" +STATUS_DELETE = "delete" +STATUS_ALL = "all" +STATUS_NEW = "new" + +# Image upload path +REQUEST_LETTER_UPLOAD_PATH = "/letters/" + +# Activity log action +INSERT_LOG = "insert" +UPDATE_LOG = "update" +DELETE_LOG = "delete" +SEND_FOR_APPROVAL_LOG = "send_for_approval" + +# Roles +ROLE_SUPERADMIN = "superadmin" +ROLE_ADMIN = "admin" +ROLE_RECEPTIONIST = "receptionist" +ROLE_USER = "user" +ROLE_STAFF = "staff" + +# Database +BASE_CONFIG_NAME = "base" +DEVELOPMENT_CONFIG_NAME = "development" +PRODUCTION_CONFIG_NAME = "production" +TEST_CONFIG_NAME = "test" + +# Http status code +STATUS_CODE_200 = 200 +STATUS_CODE_201 = 201 +STATUS_CODE_204 = 204 +STATUS_CODE_400 = 400 +STATUS_CODE_401 = 401 +STATUS_CODE_500 = 500 + +# API version prefix +API_V1 = "v1" +API_V2 = "v2" + +# DateTime + +# Channel to send the messages +SMS_CHANNEL = "sms" +EMAIL_CHANNEL = "email" + +PERCENTAGE = "percentage" +FLAT = "flat" diff --git a/scheduler_service.py/env_config.py b/scheduler_service.py/env_config.py new file mode 100644 index 0000000..91b03cc --- /dev/null +++ b/scheduler_service.py/env_config.py @@ -0,0 +1,14 @@ +from os.path import abspath, dirname, join + +from dotenv import load_dotenv + +# ##### PATH CONFIGURATION ################################ + +# fetch FastAPI's project directory +BASE_DIR = dirname(abspath(__file__)) + +# ##### ENV CONFIGURATION ################################ + +# Take environment variables from .env file +dotenv_path = join(BASE_DIR, ".env") +load_dotenv(dotenv_path) diff --git a/scheduler_service.py/main.py b/scheduler_service.py/main.py new file mode 100644 index 0000000..be5b427 --- /dev/null +++ b/scheduler_service.py/main.py @@ -0,0 +1,13 @@ +import os +import constant +import uvicorn +from scheduler import app + +# Run the application +if __name__ == "__main__": + uvicorn.run( + "main:app", + host=os.environ.get("SERVER_HOST"), + port=int(os.environ.get("SERVER_PORT")), + reload=constant.STATUS_TRUE, + ) diff --git a/scheduler_service.py/scheduler.py b/scheduler_service.py/scheduler.py new file mode 100644 index 0000000..6677c59 --- /dev/null +++ b/scheduler_service.py/scheduler.py @@ -0,0 +1,27 @@ +import asyncio + +import constant +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger +from fastapi import FastAPI +from scheduler_job import ScheduerJob + +app = FastAPI() +scheduler = BackgroundScheduler() + + +async def schedule_job(): + # Add schedulers to background for inactive/delete the past event + scheduler.add_job( + ScheduerJob().delete_past_events, + CronTrigger( + day_of_week="mon-sun", + hour=constant.STATUS_ONE, + minute=constant.THIRTY, + timezone="Asia/Kolkata", + ), + ) + scheduler.start() + + +asyncio.run(schedule_job()) \ No newline at end of file diff --git a/scheduler_service.py/scheduler_job.py b/scheduler_service.py/scheduler_job.py new file mode 100644 index 0000000..11acb6e --- /dev/null +++ b/scheduler_service.py/scheduler_job.py @@ -0,0 +1,20 @@ +import os +import requests + + +class ScheduerJob: + def delete_past_events(self): + try: + url = f"{os.environ.get('BACKEND_URL')}v1/cap/delete/past/event" + + payload = {} + headers = { + "Accept": "application/json", + "Cookie": "csrftoken=tFHOXldbBtoChLw6sKtladcULiJNz1fQUW2vu7byl9Cwrnr1H61NwydEAtQQENlw", + } + + response = requests.request("POST", url, headers=headers, data=payload) + + print(response.text) + except Exception as e: + print(e) \ No newline at end of file