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 @@

-
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