├── .dockerignore ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── api ├── .dockerignore ├── .env.example ├── Dockerfile ├── README.md ├── pyproject.toml ├── src │ ├── __init__.py │ ├── api │ │ ├── __init__.py │ │ ├── dependencies.py │ │ ├── exceptions.py │ │ ├── routes │ │ │ ├── __init__.py │ │ │ ├── home.py │ │ │ └── v1 │ │ │ │ ├── __init__.py │ │ │ │ ├── audio.py │ │ │ │ ├── chat_completions.py │ │ │ │ ├── embeddings.py │ │ │ │ ├── images_generations.py │ │ │ │ ├── models.py │ │ │ │ └── moderations.py │ │ └── utils.py │ ├── core │ │ ├── __init__.py │ │ ├── config.py │ │ └── db │ │ │ ├── __init__.py │ │ │ ├── exceptions.py │ │ │ └── managers │ │ │ ├── __init__.py │ │ │ ├── provider_manager.py │ │ │ └── user_manager.py │ ├── errors.py │ ├── main.py │ ├── models │ │ ├── __init__.py │ │ ├── audio.py │ │ ├── chat_completions.py │ │ ├── embeddings.py │ │ ├── images_generations.py │ │ └── moderations.py │ ├── providers │ │ ├── __init__.py │ │ ├── base_provider.py │ │ ├── openai.py │ │ └── utils.py │ ├── responses.py │ ├── tasks.py │ └── utils.py └── uv.lock ├── bot ├── .dockerignore ├── .env.example ├── Dockerfile ├── README.md ├── pyproject.toml ├── src │ ├── __main__.py │ ├── cogs │ │ ├── __init__.py │ │ ├── key_cog.py │ │ └── switcher_cog.py │ ├── config.py │ ├── db │ │ ├── __init__.py │ │ ├── exceptions.py │ │ └── user_manager.py │ ├── main.py │ └── utils.py └── uv.lock ├── credits.yml ├── deploy.sh ├── docker-compose.yml └── secrets ├── cloudflared.env └── db.env /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | app.egg-info 3 | *.pyc 4 | .mypy_cache 5 | .coverage 6 | htmlcov 7 | .cache 8 | .venv -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.sh text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | .venv -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Zukijourney 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example API 2 | 3 | Simple base code for creating your own AI reverse proxy. 4 | Uses Docker with Cloudflare Tunnels to deploy and FastAPI as the web framework. 5 | 6 | Heaviily inspired by [full-stack-fastapi-template](https://github.com/fastapi/full-stack-fastapi-template). 7 | 8 | ## Dependencies 9 | 10 | - Docker 11 | - Docker Compose 12 | 13 | ## How to Run 14 | 15 | ```bash 16 | chmod +x deploy.sh 17 | ./deploy.sh 18 | ``` -------------------------------------------------------------------------------- /api/.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | app.egg-info 3 | *.pyc 4 | .mypy_cache 5 | .coverage 6 | htmlcov 7 | .cache 8 | .venv -------------------------------------------------------------------------------- /api/.env.example: -------------------------------------------------------------------------------- 1 | DB_URL=mongodb://root:password@db:27017 2 | WEBHOOK_URL= -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/astral-sh/uv:latest AS uv 2 | 3 | # Builder stage 4 | FROM python:3.11-alpine AS builder 5 | 6 | RUN apk update && apk upgrade && rm -rf /var/cache/apk/* 7 | RUN apk add build-base 8 | 9 | ENV UV_LINK_MODE=copy 10 | 11 | RUN --mount=from=uv,source=/uv,target=/opt/uv \ 12 | /opt/uv venv /opt/venv 13 | 14 | ENV VIRTUAL_ENV=/opt/venv \ 15 | PATH="/opt/venv/bin:$PATH" 16 | 17 | COPY pyproject.toml . 18 | 19 | RUN --mount=type=cache,target=/root/.cache/uv \ 20 | --mount=from=uv,source=/uv,target=/opt/uv \ 21 | /opt/uv pip install -r pyproject.toml 22 | 23 | # Final stage 24 | FROM python:3.11-alpine AS final 25 | 26 | WORKDIR /app 27 | 28 | COPY . /app 29 | 30 | COPY --from=builder /opt/venv /opt/venv 31 | 32 | RUN --mount=from=uv,source=/uv,target=/tmp/uv \ 33 | cp /tmp/uv /opt/uv 34 | 35 | ENV PYTHONUNBUFFERED=1 \ 36 | PYTHONDONTWRITEBYTECODE=1 \ 37 | VIRTUAL_ENV=/opt/venv \ 38 | PATH="/opt/venv/bin:$PATH" \ 39 | PATH="/opt/uv:$PATH" 40 | 41 | EXPOSE 80 42 | 43 | CMD ["/opt/venv/bin/python3", "-m", "uvicorn", "src:app", "--host", "0.0.0.0", "--port", "80", "--loop", "asyncio"] -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # API -------------------------------------------------------------------------------- /api/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "api" 3 | version = "0.1.0" 4 | description = "" 5 | readme = "README.md" 6 | requires-python = ">=3.11" 7 | dependencies = [ 8 | "fastapi[standard]>=0.115.5", 9 | "motor>=3.6.0", 10 | "pydantic-settings>=2.6.1", 11 | "pydantic>=2.9.2", 12 | "pyyaml>=6.0.2", 13 | "slowapi>=0.1.9", 14 | "tiktoken>=0.8.0", 15 | "ujson>=5.10.0", 16 | "asgiref>=3.8.1", 17 | "httpx>=0.27.2", 18 | ] 19 | -------------------------------------------------------------------------------- /api/src/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import app 2 | 3 | __all__ = ['app'] -------------------------------------------------------------------------------- /api/src/api/__init__.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from .exceptions import ( 3 | ValidationError, 4 | AuthenticationError, 5 | AccessError 6 | ) 7 | from .routes import ( 8 | home_router, 9 | models_router, 10 | chat_router, 11 | image_router, 12 | embeddings_router, 13 | moderations_router, 14 | audio_router 15 | ) 16 | 17 | class RouterManager: 18 | def __init__(self): 19 | self.main_router = APIRouter() 20 | self.routers = [ 21 | home_router, 22 | models_router, 23 | chat_router, 24 | image_router, 25 | embeddings_router, 26 | moderations_router, 27 | audio_router 28 | ] 29 | 30 | def setup_routes(self) -> APIRouter: 31 | for router in self.routers: 32 | self.main_router.include_router(router) 33 | return self.main_router 34 | 35 | router_manager = RouterManager() 36 | main_router = router_manager.setup_routes() 37 | 38 | __all__ = [ 39 | 'main_router', 40 | 'ValidationError', 41 | 'AuthenticationError', 42 | 'AccessError' 43 | ] -------------------------------------------------------------------------------- /api/src/api/dependencies.py: -------------------------------------------------------------------------------- 1 | import time 2 | from fastapi import Request, Depends 3 | from typing import Set 4 | from .exceptions import AuthenticationError, AccessError, ValidationError 5 | from ..core import user_manager 6 | from ..providers import BaseProvider 7 | 8 | class AuthenticationHandler: 9 | @staticmethod 10 | async def _get_api_key(request: Request) -> str: 11 | key = request.headers.get('Authorization', '').replace('Bearer ', '') 12 | if not key: 13 | raise AuthenticationError('You need to provide a valid API key.') 14 | return key 15 | 16 | @staticmethod 17 | async def _validate_user(key: str) -> dict: 18 | user = await user_manager.get_user(key=key) 19 | if not user: 20 | raise AuthenticationError('The key you provided is invalid.') 21 | 22 | if user['banned']: 23 | raise AuthenticationError( 24 | 'The key you provided is banned.', 25 | status_code=403 26 | ) 27 | 28 | return user 29 | 30 | class UserAccessHandler: 31 | @staticmethod 32 | async def _check_premium_status(user: dict) -> None: 33 | if user['premium_tier'] > 0 and time.time() > user['premium_expiry']: 34 | user['premium_tier'] = 0 35 | await user_manager.update_user(user['user_id'], {'premium_tier': 0}) 36 | 37 | @staticmethod 38 | async def _validate_ip(request: Request, user: dict) -> None: 39 | if user['premium_tier'] == 0: 40 | current_ip = request.headers.get('CF-Connecting-IP') 41 | 42 | if not user['ip']: 43 | user['ip'] = current_ip 44 | await user_manager.update_user(user['user_id'], {'ip': current_ip}) 45 | 46 | if user['ip'] != current_ip: 47 | raise AccessError('Your IP is different than the locked one.') 48 | 49 | class RequestValidator: 50 | @staticmethod 51 | def _get_model_sets() -> tuple[Set[str], Set[str], Set[str]]: 52 | providers = BaseProvider.__subclasses__() 53 | all_models = set(BaseProvider.get_all_models()) 54 | paid_models = { 55 | model for provider in providers 56 | for model in provider.config.paid_models 57 | } 58 | return all_models, paid_models 59 | 60 | @staticmethod 61 | async def _get_request_body(request: Request) -> dict: 62 | try: 63 | if request.headers.get('Content-Type') == 'application/json': 64 | return await request.json() 65 | elif request.headers.get('Content-Type').startswith('multipart/form-data'): 66 | return await request.form() 67 | except Exception: 68 | raise ValidationError('Invalid body.') 69 | 70 | @staticmethod 71 | def _validate_model_access( 72 | model: str, 73 | user_tier: int, 74 | all_models: Set[str], 75 | paid_models: Set[str], 76 | ) -> None: 77 | if model not in all_models: 78 | raise ValidationError(f'The model `{model}` does not exist.') 79 | 80 | if model in paid_models and user_tier == 0: 81 | raise ValidationError(f'You don\'t have permission to use `{model}`.') 82 | 83 | async def authentication(request: Request) -> None: 84 | key = await AuthenticationHandler._get_api_key(request) 85 | user = await AuthenticationHandler._validate_user(key) 86 | request.state.user = user 87 | 88 | async def validate_user_access(request: Request) -> None: 89 | user = request.state.user 90 | await UserAccessHandler._check_premium_status(user) 91 | await UserAccessHandler._validate_ip(request, user) 92 | 93 | async def validate_request_body(request: Request) -> None: 94 | body = await RequestValidator._get_request_body(request) 95 | model = body.get('model') 96 | all_models, paid_models = RequestValidator._get_model_sets() 97 | 98 | RequestValidator._validate_model_access( 99 | model=model, 100 | user_tier=request.state.user.get('premium_tier', 0), 101 | all_models=all_models, 102 | paid_models=paid_models 103 | ) 104 | 105 | DEPENDENCIES = [ 106 | Depends(authentication), 107 | Depends(validate_user_access), 108 | Depends(validate_request_body) 109 | ] -------------------------------------------------------------------------------- /api/src/api/exceptions.py: -------------------------------------------------------------------------------- 1 | class InsufficientCreditsError(Exception): 2 | def __init__(self, available_credits: int, required_tokens: int): 3 | self.status_code = 429 4 | self.message = f'You have {available_credits} credits left. This request requires {required_tokens} credits.' 5 | super().__init__(self.message) 6 | 7 | class NoProviderAvailableError(Exception): 8 | def __init__(self): 9 | self.status_code = 503 10 | self.message = 'No provider available.' 11 | super().__init__(self.message) 12 | 13 | class AuthenticationError(Exception): 14 | def __init__(self, message: str, status_code: int = 401): 15 | self.status_code = status_code 16 | self.message = message 17 | super().__init__(self.message) 18 | 19 | class AccessError(Exception): 20 | def __init__(self, message: str): 21 | self.status_code = 403 22 | self.message = message 23 | super().__init__(self.message) 24 | 25 | class ValidationError(Exception): 26 | def __init__(self, message: str): 27 | self.status_code = 400 28 | self.message = message 29 | super().__init__(self.message) -------------------------------------------------------------------------------- /api/src/api/routes/__init__.py: -------------------------------------------------------------------------------- 1 | from .home import router as home_router 2 | from .v1 import ( 3 | models_router, 4 | chat_router, 5 | image_router, 6 | embeddings_router, 7 | moderations_router, 8 | audio_router 9 | ) 10 | 11 | __all__ = [ 12 | 'home_router', 13 | 'models_router', 14 | 'chat_router', 15 | 'image_router', 16 | 'embeddings_router', 17 | 'moderations_router', 18 | 'audio_router' 19 | ] -------------------------------------------------------------------------------- /api/src/api/routes/home.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from typing import Dict 3 | from ...responses import JSONResponse 4 | 5 | router = APIRouter() 6 | 7 | @router.get('/', response_class=JSONResponse) 8 | async def home() -> Dict[str, str]: 9 | return { 10 | 'message': 'Welcome to an instance of the Zukijourney Example API! The source code is available at: https://github.com/zukijourney/example-api' 11 | } -------------------------------------------------------------------------------- /api/src/api/routes/v1/__init__.py: -------------------------------------------------------------------------------- 1 | from .models import router as models_router 2 | from .chat_completions import router as chat_router 3 | from .images_generations import router as image_router 4 | from .embeddings import router as embeddings_router 5 | from .moderations import router as moderations_router 6 | from .audio import router as audio_router 7 | 8 | __all__ = [ 9 | 'models_router', 10 | 'chat_router', 11 | 'image_router', 12 | 'embeddings_router', 13 | 'moderations_router', 14 | 'audio_router' 15 | ] -------------------------------------------------------------------------------- /api/src/api/routes/v1/audio.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, Response, UploadFile, File, Form, HTTPException 2 | from ...dependencies import DEPENDENCIES 3 | from ....models import SpeechRequest 4 | from ....core import provider_manager 5 | from ....providers import BaseProvider 6 | from ...exceptions import InsufficientCreditsError, NoProviderAvailableError 7 | 8 | router = APIRouter(prefix='/v1') 9 | 10 | class AudioHandler: 11 | @staticmethod 12 | async def _get_provider(model: str) -> dict: 13 | provider = await provider_manager.get_best_provider(model) 14 | if not provider: 15 | raise NoProviderAvailableError() 16 | return provider 17 | 18 | @staticmethod 19 | def _validate_credits( 20 | available_credits: int, 21 | required_tokens: int 22 | ) -> None: 23 | if required_tokens > available_credits: 24 | raise InsufficientCreditsError( 25 | available_credits=available_credits, 26 | required_tokens=required_tokens 27 | ) 28 | 29 | @staticmethod 30 | def _get_token_count( 31 | provider_instance: BaseProvider, 32 | model: str 33 | ) -> int: 34 | return provider_instance.config.model_prices.get(model, 100) 35 | 36 | @router.post('/audio/speech', dependencies=DEPENDENCIES, response_model=None) 37 | async def audio_speech( 38 | request: Request, 39 | data: SpeechRequest 40 | ) -> Response: 41 | try: 42 | provider = await AudioHandler._get_provider(data.model) 43 | provider_instance = BaseProvider.get_provider_class(provider['name']) 44 | 45 | token_count = AudioHandler._get_token_count( 46 | provider_instance=provider_instance, 47 | model=data.model 48 | ) 49 | 50 | AudioHandler._validate_credits( 51 | available_credits=request.state.user['credits'], 52 | required_tokens=token_count 53 | ) 54 | 55 | request.state.provider = provider 56 | 57 | return await provider_instance.audio_speech( 58 | request, 59 | **data.model_dump(mode='json') 60 | ) 61 | 62 | except (InsufficientCreditsError, NoProviderAvailableError) as e: 63 | raise HTTPException( 64 | status_code=e.status_code, 65 | detail=str(e) 66 | ) 67 | except Exception: 68 | raise HTTPException( 69 | status_code=500, 70 | detail='An internal server error occurred while processing your request. Try again later.' 71 | ) 72 | 73 | @router.post('/audio/transcriptions', dependencies=DEPENDENCIES, response_model=None) 74 | async def audio_transcriptions( 75 | request: Request, 76 | model: str = Form(...), 77 | file: UploadFile = File(...) 78 | ) -> Response: 79 | try: 80 | provider = await AudioHandler._get_provider(model) 81 | provider_instance = BaseProvider.get_provider_class(provider['name']) 82 | 83 | token_count = AudioHandler._get_token_count( 84 | provider_instance=provider_instance, 85 | model=model 86 | ) 87 | 88 | AudioHandler._validate_credits( 89 | available_credits=request.state.user['credits'], 90 | required_tokens=token_count 91 | ) 92 | 93 | request.state.provider = provider 94 | 95 | return await provider_instance.audio_transcriptions( 96 | request, 97 | model, 98 | file 99 | ) 100 | 101 | except (InsufficientCreditsError, NoProviderAvailableError) as e: 102 | raise HTTPException( 103 | status_code=e.status_code, 104 | detail=str(e) 105 | ) 106 | except Exception: 107 | raise HTTPException( 108 | status_code=500, 109 | detail='An internal server error occurred while processing your request. Try again later.' 110 | ) 111 | 112 | @router.post('/audio/translations', dependencies=DEPENDENCIES, response_model=None) 113 | async def audio_translations( 114 | request: Request, 115 | model: str = Form(...), 116 | file: UploadFile = File(...) 117 | ) -> Response: 118 | try: 119 | provider = await AudioHandler._get_provider(model) 120 | provider_instance = BaseProvider.get_provider_class(provider['name']) 121 | 122 | token_count = AudioHandler._get_token_count( 123 | provider_instance=provider_instance, 124 | model=model 125 | ) 126 | 127 | AudioHandler._validate_credits( 128 | available_credits=request.state.user['credits'], 129 | required_tokens=token_count 130 | ) 131 | 132 | request.state.provider = provider 133 | 134 | return await provider_instance.audio_translations( 135 | request, 136 | model, 137 | file 138 | ) 139 | 140 | except (InsufficientCreditsError, NoProviderAvailableError) as e: 141 | raise HTTPException( 142 | status_code=e.status_code, 143 | detail=str(e) 144 | ) 145 | except Exception: 146 | raise HTTPException( 147 | status_code=500, 148 | detail='An internal server error occurred while processing your request. Try again later.' 149 | ) -------------------------------------------------------------------------------- /api/src/api/routes/v1/chat_completions.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, HTTPException 2 | from fastapi.responses import StreamingResponse 3 | from typing import Union, List 4 | from ....responses import JSONResponse 5 | from ...dependencies import DEPENDENCIES 6 | from ....models import ChatRequest, Message 7 | from ....utils import request_processor 8 | from ....core import provider_manager 9 | from ....providers import BaseProvider 10 | from ...exceptions import InsufficientCreditsError, NoProviderAvailableError 11 | 12 | router = APIRouter(prefix='/v1') 13 | 14 | class ChatCompletionsHandler: 15 | @staticmethod 16 | def _has_vision_requirement(messages: List[Message]) -> bool: 17 | return any( 18 | isinstance(message.content, list) and 19 | any(content.type == 'image_url' for content in message.content) 20 | for message in messages 21 | ) 22 | 23 | @staticmethod 24 | def _validate_credits(available_credits: int, required_tokens: int) -> None: 25 | if required_tokens > available_credits: 26 | raise InsufficientCreditsError( 27 | available_credits=available_credits, 28 | required_tokens=required_tokens 29 | ) 30 | 31 | @staticmethod 32 | async def _get_provider( 33 | model: str, 34 | vision_required: bool, 35 | tools_required: bool 36 | ) -> dict: 37 | provider = await provider_manager.get_best_provider( 38 | model=model, 39 | vision=vision_required, 40 | tools=tools_required 41 | ) 42 | 43 | if not provider: 44 | raise NoProviderAvailableError() 45 | 46 | return provider 47 | 48 | @router.post('/chat/completions', dependencies=DEPENDENCIES, response_model=None) 49 | async def chat_completions( 50 | request: Request, 51 | data: ChatRequest 52 | ) -> Union[JSONResponse, StreamingResponse]: 53 | try: 54 | token_count = request_processor.count_tokens(data) 55 | 56 | ChatCompletionsHandler._validate_credits( 57 | available_credits=request.state.user['credits'], 58 | required_tokens=token_count 59 | ) 60 | 61 | request.state.token_count = token_count 62 | 63 | vision_required = ChatCompletionsHandler._has_vision_requirement(data.messages) 64 | 65 | provider = await ChatCompletionsHandler._get_provider( 66 | model=data.model, 67 | vision_required=vision_required, 68 | tools_required=data.tool_choice and data.tools 69 | ) 70 | 71 | request.state.provider = provider 72 | 73 | provider_instance = BaseProvider.get_provider_class(provider['name']) 74 | return await provider_instance.chat_completions( 75 | request, 76 | **data.model_dump(mode='json') 77 | ) 78 | 79 | except (InsufficientCreditsError, NoProviderAvailableError) as e: 80 | raise HTTPException( 81 | status_code=e.status_code, 82 | detail=str(e) 83 | ) 84 | except Exception: 85 | raise HTTPException( 86 | status_code=500, 87 | detail='An internal server error occurred while processing your request. Try again later.' 88 | ) -------------------------------------------------------------------------------- /api/src/api/routes/v1/embeddings.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, HTTPException 2 | from ....responses import JSONResponse 3 | from ...dependencies import DEPENDENCIES 4 | from ....models import EmbeddingsRequest 5 | from ....core import provider_manager 6 | from ....providers import BaseProvider 7 | from ...exceptions import InsufficientCreditsError, NoProviderAvailableError 8 | 9 | router = APIRouter(prefix='/v1') 10 | 11 | class EmbeddingsHandler: 12 | @staticmethod 13 | async def _get_provider(model: str) -> dict: 14 | provider = await provider_manager.get_best_provider(model) 15 | if not provider: 16 | raise NoProviderAvailableError() 17 | return provider 18 | 19 | @staticmethod 20 | def _validate_credits( 21 | available_credits: int, 22 | required_tokens: int 23 | ) -> None: 24 | if required_tokens > available_credits: 25 | raise InsufficientCreditsError( 26 | available_credits=available_credits, 27 | required_tokens=required_tokens 28 | ) 29 | 30 | @staticmethod 31 | def _get_token_count( 32 | provider_instance: BaseProvider, 33 | model: str 34 | ) -> int: 35 | return provider_instance.config.model_prices.get(model, 100) 36 | 37 | @router.post('/embeddings', dependencies=DEPENDENCIES, response_model=None) 38 | async def embeddings( 39 | request: Request, 40 | data: EmbeddingsRequest 41 | ) -> JSONResponse: 42 | try: 43 | provider = await EmbeddingsHandler._get_provider(data.model) 44 | provider_instance = BaseProvider.get_provider_class(provider['name']) 45 | 46 | token_count = EmbeddingsHandler._get_token_count( 47 | provider_instance=provider_instance, 48 | model=data.model 49 | ) 50 | 51 | EmbeddingsHandler._validate_credits( 52 | available_credits=request.state.user['credits'], 53 | required_tokens=token_count 54 | ) 55 | 56 | request.state.provider = provider 57 | 58 | return await provider_instance.embeddings( 59 | request, 60 | **data.model_dump(mode='json') 61 | ) 62 | 63 | except (InsufficientCreditsError, NoProviderAvailableError) as e: 64 | raise HTTPException( 65 | status_code=e.status_code, 66 | detail=str(e) 67 | ) 68 | except Exception: 69 | raise HTTPException( 70 | status_code=500, 71 | detail='An internal server error occurred while processing your request. Try again later.' 72 | ) -------------------------------------------------------------------------------- /api/src/api/routes/v1/images_generations.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, HTTPException 2 | from ....responses import JSONResponse 3 | from ...dependencies import DEPENDENCIES 4 | from ....models import ImageRequest 5 | from ....core import provider_manager 6 | from ....providers import BaseProvider 7 | from ...exceptions import InsufficientCreditsError, NoProviderAvailableError 8 | 9 | router = APIRouter(prefix='/v1') 10 | 11 | class ImageGenerationHandler: 12 | @staticmethod 13 | async def _get_provider(model: str) -> dict: 14 | provider = await provider_manager.get_best_provider(model) 15 | if not provider: 16 | raise NoProviderAvailableError() 17 | return provider 18 | 19 | @staticmethod 20 | def _validate_credits( 21 | available_credits: int, 22 | required_tokens: int 23 | ) -> None: 24 | if required_tokens > available_credits: 25 | raise InsufficientCreditsError( 26 | available_credits=available_credits, 27 | required_tokens=required_tokens 28 | ) 29 | 30 | @staticmethod 31 | def _get_token_count( 32 | provider_instance: BaseProvider, 33 | model: str 34 | ) -> int: 35 | return provider_instance.config.model_prices.get(model, 100) 36 | 37 | @router.post('/images/generations', dependencies=DEPENDENCIES, response_model=None) 38 | async def images_generations( 39 | request: Request, 40 | data: ImageRequest 41 | ) -> JSONResponse: 42 | try: 43 | provider = await ImageGenerationHandler._get_provider(data.model) 44 | provider_instance = BaseProvider.get_provider_class(provider['name']) 45 | 46 | token_count = ImageGenerationHandler._get_token_count( 47 | provider_instance=provider_instance, 48 | model=data.model 49 | ) 50 | 51 | ImageGenerationHandler._validate_credits( 52 | available_credits=request.state.user['credits'], 53 | required_tokens=token_count 54 | ) 55 | 56 | request.state.provider = provider 57 | 58 | return await provider_instance.images_generations( 59 | request, 60 | **data.model_dump(mode='json') 61 | ) 62 | 63 | except (InsufficientCreditsError, NoProviderAvailableError) as e: 64 | raise HTTPException( 65 | status_code=e.status_code, 66 | detail=str(e) 67 | ) 68 | except Exception: 69 | raise HTTPException( 70 | status_code=500, 71 | detail='An internal server error occurred while processing your request. Try again later.' 72 | ) -------------------------------------------------------------------------------- /api/src/api/routes/v1/models.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter 2 | from typing import Dict, Any 3 | from ...utils import ModelListGenerator 4 | from ....responses import JSONResponse 5 | 6 | router = APIRouter() 7 | 8 | @router.get('/v1/models', response_class=JSONResponse) 9 | async def home() -> Dict[str, Any]: 10 | return { 11 | 'object': 'list', 12 | 'data': ModelListGenerator.generate() 13 | } -------------------------------------------------------------------------------- /api/src/api/routes/v1/moderations.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, HTTPException 2 | from ....responses import JSONResponse 3 | from ...dependencies import DEPENDENCIES 4 | from ....models import ModerationRequest 5 | from ....core import provider_manager 6 | from ....providers import BaseProvider 7 | from ...exceptions import InsufficientCreditsError, NoProviderAvailableError 8 | 9 | router = APIRouter(prefix='/v1') 10 | 11 | class ModerationHandler: 12 | @staticmethod 13 | async def _get_provider(model: str) -> dict: 14 | provider = await provider_manager.get_best_provider(model) 15 | if not provider: 16 | raise NoProviderAvailableError() 17 | return provider 18 | 19 | @staticmethod 20 | def _validate_credits( 21 | available_credits: int, 22 | required_tokens: int 23 | ) -> None: 24 | if required_tokens > available_credits: 25 | raise InsufficientCreditsError( 26 | available_credits=available_credits, 27 | required_tokens=required_tokens 28 | ) 29 | 30 | @staticmethod 31 | def _get_token_count( 32 | provider_instance: BaseProvider, 33 | model: str 34 | ) -> int: 35 | return provider_instance.config.model_prices.get(model, 100) 36 | 37 | @router.post('/moderations', dependencies=DEPENDENCIES, response_model=None) 38 | async def moderations( 39 | request: Request, 40 | data: ModerationRequest 41 | ) -> JSONResponse: 42 | try: 43 | provider = await ModerationHandler._get_provider(data.model) 44 | provider_instance = BaseProvider.get_provider_class(provider['name']) 45 | 46 | token_count = ModerationHandler._get_token_count( 47 | provider_instance=provider_instance, 48 | model=data.model 49 | ) 50 | 51 | ModerationHandler._validate_credits( 52 | available_credits=request.state.user['credits'], 53 | required_tokens=token_count 54 | ) 55 | 56 | request.state.provider = provider 57 | 58 | return await provider_instance.moderations( 59 | request, 60 | **data.model_dump(mode='json') 61 | ) 62 | 63 | except (InsufficientCreditsError, NoProviderAvailableError) as e: 64 | raise HTTPException( 65 | status_code=e.status_code, 66 | detail=str(e) 67 | ) 68 | except Exception: 69 | raise HTTPException( 70 | status_code=500, 71 | detail='An internal server error occurred while processing your request. Try again later.' 72 | ) -------------------------------------------------------------------------------- /api/src/api/utils.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Any, Set 2 | from dataclasses import dataclass 3 | from ..providers import BaseProvider 4 | 5 | @dataclass 6 | class ModelMetadata: 7 | free_models: Set[str] 8 | pricing: Dict[str, Any] 9 | multipliers: Dict[str, float] 10 | 11 | class ModelListGenerator: 12 | @staticmethod 13 | def _collect_model_metadata() -> ModelMetadata: 14 | providers = BaseProvider.__subclasses__() 15 | 16 | return ModelMetadata( 17 | free_models={ 18 | model 19 | for provider in providers 20 | for model in provider.config.free_models 21 | }, 22 | pricing={ 23 | model: pricing 24 | for provider in providers 25 | for model, pricing in provider.config.model_prices.items() 26 | }, 27 | multipliers={ 28 | model: multiplier 29 | for provider in providers 30 | for model, multiplier in provider.config.model_multipliers.items() 31 | } 32 | ) 33 | 34 | @staticmethod 35 | def _create_model_entry( 36 | model: str, 37 | metadata: ModelMetadata 38 | ) -> Dict[str, Any]: 39 | return { 40 | 'id': model, 41 | 'object': 'model', 42 | 'owned_by': 'zukijourney', 43 | 'is_free': model in metadata.free_models, 44 | 'pricing': { 45 | 'credits': metadata.pricing.get(model, 'per_token'), 46 | 'multiplier': metadata.multipliers.get(model, 1) 47 | } 48 | } 49 | 50 | @classmethod 51 | def generate(cls) -> List[Dict[str, Any]]: 52 | metadata = cls._collect_model_metadata() 53 | 54 | return [ 55 | cls._create_model_entry(model, metadata) 56 | for model in BaseProvider.get_all_models() 57 | ] -------------------------------------------------------------------------------- /api/src/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .db import user_manager, provider_manager 2 | from .config import settings 3 | 4 | __all__ = [ 5 | 'user_manager', 6 | 'provider_manager', 7 | 'settings' 8 | ] -------------------------------------------------------------------------------- /api/src/core/config.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings, SettingsConfigDict 2 | 3 | class Settings(BaseSettings): 4 | db_url: str 5 | webhook_url: str 6 | 7 | model_config = SettingsConfigDict( 8 | env_file='.env', 9 | env_ignore_empty=True, 10 | extra='ignore' 11 | ) 12 | 13 | settings = Settings() -------------------------------------------------------------------------------- /api/src/core/db/__init__.py: -------------------------------------------------------------------------------- 1 | from .managers import user_manager, provider_manager 2 | 3 | __all__ = ['user_manager', 'provider_manager'] -------------------------------------------------------------------------------- /api/src/core/db/exceptions.py: -------------------------------------------------------------------------------- 1 | class DatabaseError(Exception): 2 | def __init__(self, message: str): 3 | self.message = message 4 | super().__init__(self.message) -------------------------------------------------------------------------------- /api/src/core/db/managers/__init__.py: -------------------------------------------------------------------------------- 1 | from .user_manager import user_manager 2 | from .provider_manager import provider_manager 3 | 4 | __all__ = ['user_manager', 'provider_manager'] -------------------------------------------------------------------------------- /api/src/core/db/managers/provider_manager.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, Optional, List 2 | from motor.motor_asyncio import AsyncIOMotorClient 3 | from ...config import settings 4 | 5 | class ProviderDatabase: 6 | def __init__(self): 7 | self.client = AsyncIOMotorClient(settings.db_url) 8 | self.collection = self.client['db']['providers'] 9 | 10 | class ProviderManager: 11 | def __init__(self): 12 | self.db = ProviderDatabase() 13 | 14 | @staticmethod 15 | def _calculate_availability( 16 | usage: int = 0, 17 | failures: int = 0 18 | ) -> float: 19 | if usage > 0: 20 | return ((usage - failures) / usage) * 100 21 | return 100.0 - failures 22 | 23 | @staticmethod 24 | def _sort_providers( 25 | providers: List[Dict[str, Any]] 26 | ) -> List[Dict[str, Any]]: 27 | return sorted( 28 | providers, 29 | key=lambda x: ( 30 | -ProviderManager._calculate_availability( 31 | x.get('usage', 0), 32 | x.get('failures', 0) 33 | ), 34 | x['usage'], 35 | x['latency_avg'], 36 | not x['supports_real_streaming'] 37 | ) 38 | ) 39 | 40 | def _filter_providers( 41 | self, 42 | providers: List[Dict[str, Any]], 43 | vision: bool = False, 44 | tools: bool = False 45 | ) -> List[Dict[str, Any]]: 46 | if vision: 47 | providers = [p for p in providers if p['supports_vision']] 48 | if tools: 49 | providers = [p for p in providers if p['supports_tool_calling']] 50 | return providers 51 | 52 | async def get_best_provider( 53 | self, 54 | model: str, 55 | vision: bool = False, 56 | tools: bool = False 57 | ) -> Optional[Dict[str, Any]]: 58 | providers = await self.db.collection.find( 59 | {'models': {'$in': [model]}} 60 | ).to_list(length=None) 61 | 62 | if not providers: 63 | return None 64 | 65 | sorted_providers = self._sort_providers(providers) 66 | filtered_providers = self._filter_providers( 67 | sorted_providers, 68 | vision=vision, 69 | tools=tools 70 | ) 71 | 72 | return filtered_providers[0] if filtered_providers else None 73 | 74 | async def update_provider( 75 | self, 76 | name: str, 77 | new_data: Dict[str, Any] 78 | ) -> None: 79 | update_data = {k: v for k, v in new_data.items() if k != '_id'} 80 | 81 | await self.db.collection.find_one_and_update( 82 | filter={'name': name}, 83 | update={'$set': update_data}, 84 | upsert=True 85 | ) 86 | 87 | provider_manager = ProviderManager() -------------------------------------------------------------------------------- /api/src/core/db/managers/user_manager.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Any, Optional 2 | from motor.motor_asyncio import AsyncIOMotorClient 3 | from ..exceptions import DatabaseError 4 | from ...config import settings 5 | 6 | class UserDatabase: 7 | def __init__(self): 8 | self.client = AsyncIOMotorClient(settings.db_url) 9 | self.collection = self.client['db']['users'] 10 | 11 | class UserManager: 12 | def __init__(self): 13 | self.db = UserDatabase() 14 | 15 | async def get_user( 16 | self, 17 | user_id: Optional[int] = None, 18 | key: Optional[str] = None 19 | ) -> Optional[Dict[str, Any]]: 20 | try: 21 | query = {'user_id': user_id} if user_id else {'key': key} 22 | return await self.db.collection.find_one(query) 23 | except Exception as e: 24 | raise DatabaseError(f'Failed to retrieve user: {str(e)}') 25 | 26 | async def update_user( 27 | self, 28 | user_id: str, 29 | new_data: Dict[str, Any], 30 | upsert: bool = True 31 | ) -> Optional[Dict[str, Any]]: 32 | try: 33 | update_data = {k: v for k, v in new_data.items() if k != '_id'} 34 | 35 | return await self.db.collection.find_one_and_update( 36 | filter={'user_id': user_id}, 37 | update={'$set': update_data}, 38 | upsert=upsert, 39 | return_document=True 40 | ) 41 | except Exception as e: 42 | raise DatabaseError(f'Failed to update user: {str(e)}') 43 | 44 | user_manager = UserManager() -------------------------------------------------------------------------------- /api/src/errors.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Dict, Any, Callable, Awaitable, Type, Union, Optional 3 | from fastapi import FastAPI, Request, HTTPException 4 | from fastapi.exceptions import ValidationException 5 | from slowapi.errors import RateLimitExceeded 6 | from .api import ValidationError, AccessError, AuthenticationError 7 | from .responses import JSONResponse 8 | 9 | @dataclass 10 | class ErrorResponse: 11 | message: str 12 | error_type: str 13 | code: int 14 | status_code: int 15 | 16 | def to_dict(self) -> Dict[str, Any]: 17 | return { 18 | 'error': { 19 | 'message': self.message, 20 | 'type': self.error_type, 21 | 'code': self.code 22 | } 23 | } 24 | 25 | class ExceptionHandler: 26 | def __init__(self): 27 | self.handlers: Dict[ 28 | Type[Exception], 29 | Callable[[Request, Exception], Awaitable[JSONResponse]] 30 | ] = {} 31 | self._register_default_handlers() 32 | 33 | def _register_default_handlers(self) -> None: 34 | self.handlers.update({ 35 | Exception: self._handle_generic_exception, 36 | AccessError: self._handle_http_exception, 37 | AuthenticationError: self._handle_http_exception, 38 | HTTPException: self._handle_http_exception, 39 | ValidationError: self._handle_validation_exception, 40 | ValidationException: self._handle_validation_exception, 41 | RateLimitExceeded: self._handle_rate_limit_exceeded 42 | }) 43 | 44 | @staticmethod 45 | def _create_error_response( 46 | message: str, 47 | error_type: str = 'invalid_request_error', 48 | code: int = 400, 49 | status_code: Optional[int] = None 50 | ) -> ErrorResponse: 51 | return ErrorResponse( 52 | message=message, 53 | error_type=error_type, 54 | code=code, 55 | status_code=status_code or code 56 | ) 57 | 58 | @staticmethod 59 | def _create_json_response(error_response: ErrorResponse) -> JSONResponse: 60 | return JSONResponse( 61 | status_code=error_response.status_code, 62 | content=error_response.to_dict() 63 | ) 64 | 65 | async def _handle_generic_exception( 66 | self, 67 | _: Request, 68 | __: Exception 69 | ) -> JSONResponse: 70 | error_response = self._create_error_response( 71 | message='An internal server error occurred while processing your request. Try again later.', 72 | error_type='internal_server_error', 73 | code=500, 74 | status_code=500 75 | ) 76 | return self._create_json_response(error_response) 77 | 78 | async def _handle_validation_exception( 79 | self, 80 | _: Request, 81 | exc: Union[ValidationException, ValidationError, ValueError] 82 | ) -> JSONResponse: 83 | if isinstance(exc, ValidationException): 84 | error_response = self._create_error_response( 85 | message=exc.errors()[0]['msg'], 86 | error_type='invalid_request_error', 87 | code=400, 88 | status_code=400 89 | ) 90 | else: 91 | error_response = self._create_error_response( 92 | message=exc.message if hasattr(exc, 'message') else str(exc), 93 | error_type='invalid_request_error', 94 | code=400, 95 | status_code=400 96 | ) 97 | 98 | return self._create_json_response(error_response) 99 | 100 | async def _handle_http_exception( 101 | self, 102 | _: Request, 103 | exc: Union[HTTPException, AccessError, AuthenticationError] 104 | ) -> JSONResponse: 105 | error_response = self._create_error_response( 106 | message=exc.detail if hasattr(exc, 'detail') else exc.message, 107 | error_type='invalid_request_error', 108 | code=exc.status_code, 109 | status_code=exc.status_code 110 | ) 111 | return self._create_json_response(error_response) 112 | 113 | async def _handle_rate_limit_exceeded( 114 | self, 115 | _: Request, 116 | exc: RateLimitExceeded 117 | ) -> JSONResponse: 118 | error_response = self._create_error_response( 119 | message=f'Rate limit exceeded: {exc.detail}', 120 | error_type='invalid_request_error', 121 | code=400, 122 | status_code=exc.status_code 123 | ) 124 | return self._create_json_response(error_response) 125 | 126 | def register_handler( 127 | self, 128 | exception_class: Type[Exception], 129 | handler: Callable[[Request, Exception], Awaitable[JSONResponse]] 130 | ) -> None: 131 | self.handlers[exception_class] = handler 132 | 133 | def setup(self, app: FastAPI) -> None: 134 | for exception_class, handler in self.handlers.items(): 135 | app.add_exception_handler(exception_class, handler) -------------------------------------------------------------------------------- /api/src/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | from fastapi.middleware.cors import CORSMiddleware 3 | from slowapi import Limiter 4 | from slowapi.middleware import SlowAPIMiddleware 5 | from contextlib import asynccontextmanager 6 | from .tasks import CreditsService 7 | from .providers import BaseProvider 8 | from .api import main_router 9 | from .errors import ExceptionHandler 10 | from .utils import request_processor 11 | 12 | credits_service = CreditsService() 13 | base_provider = BaseProvider() 14 | 15 | @asynccontextmanager 16 | async def lifespan(_: FastAPI): 17 | await credits_service.start() 18 | await base_provider.import_modules() 19 | await base_provider.sync_to_db() 20 | yield 21 | await credits_service.stop() 22 | 23 | app = FastAPI(lifespan=lifespan) 24 | 25 | app.state.limiter = Limiter( 26 | key_func=request_processor.get_api_key, 27 | default_limits=[ 28 | '2/second', 29 | '30/minute' 30 | ] 31 | ) 32 | app.add_middleware(SlowAPIMiddleware) 33 | 34 | app.add_middleware( 35 | CORSMiddleware, 36 | allow_origins=['*'], 37 | allow_methods=['*'], 38 | allow_headers=['*'] 39 | ) 40 | 41 | ExceptionHandler().setup(app) 42 | 43 | app.include_router(main_router) -------------------------------------------------------------------------------- /api/src/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .chat_completions import ChatRequest, Message, TextContentPart, ImageContentPart 2 | from .images_generations import ImageRequest 3 | from .moderations import ModerationRequest 4 | from .audio import SpeechRequest 5 | from .embeddings import EmbeddingsRequest, EmbeddingsInput 6 | 7 | __all__ = [ 8 | 'ChatRequest', 9 | 'Message', 10 | 'TextContentPart', 11 | 'ImageContentPart', 12 | 'ImageRequest', 13 | 'ModerationRequest', 14 | 'SpeechRequest', 15 | 'EmbeddingsRequest', 16 | 'EmbeddingsInput' 17 | ] -------------------------------------------------------------------------------- /api/src/models/audio.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, field_validator 2 | from typing import Optional 3 | 4 | class SpeechRequest(BaseModel): 5 | model: str 6 | input: str 7 | voice: Optional[str] = None 8 | 9 | @field_validator('input') 10 | @classmethod 11 | def validate_input(cls, value: str) -> str: 12 | if not value: 13 | raise ValueError('The input field must not have an empty string') 14 | return value -------------------------------------------------------------------------------- /api/src/models/chat_completions.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field, field_validator 2 | from typing import List, Dict, Any, Optional, Union, Literal 3 | 4 | class ImageURL(BaseModel): 5 | detail: Literal['auto', 'low', 'high'] = 'auto' 6 | url: str 7 | 8 | class ImageContentPart(BaseModel): 9 | type: Literal['image_url'] 10 | image_url: ImageURL 11 | 12 | class TextContentPart(BaseModel): 13 | type: Literal['text'] 14 | text: str 15 | 16 | class Message(BaseModel): 17 | role: Literal['user', 'assistant', 'system'] 18 | content: Union[str, List[ImageContentPart], List[TextContentPart]] 19 | 20 | class ChatRequest(BaseModel): 21 | model: str 22 | messages: List[Message] = Field(min_length=1) 23 | stream: bool = False 24 | temperature: Optional[float] = Field(default=None, le=2.0, ge=0.0) 25 | top_p: Optional[float] = Field(default=None, le=1.0, ge=0.0) 26 | presence_penalty: Optional[float] = Field(default=None, le=1.0, ge=0.0) 27 | frequency_penalty: Optional[float] = Field(default=None, le=1.0, ge=0.0) 28 | max_tokens: Optional[int] = None 29 | max_completion_tokens: Optional[int] = None 30 | tool_choice: Optional[Literal['none', 'auto', 'required']] = None 31 | tools: Optional[List[Dict[str, Any]]] = None 32 | 33 | @field_validator('messages') 34 | @classmethod 35 | def validate_messages(cls, messages: List[Message]) -> List[Message]: 36 | if not any(msg.role == 'user' for msg in messages): 37 | raise ValueError('Messages must contain at least one user message') 38 | 39 | if messages[0].role == 'assistant': 40 | raise ValueError('First message must be from user or system') 41 | 42 | for msg in messages: 43 | if isinstance(msg.content, str): 44 | if not msg.content: 45 | raise ValueError('Message content cannot be empty') 46 | continue 47 | 48 | if msg.role != 'user' and any(isinstance(part, ImageContentPart) for part in msg.content): 49 | raise ValueError('Array image content only allowed for user messages') 50 | 51 | if not msg.content: 52 | raise ValueError('Message content array cannot be empty') 53 | 54 | for part in msg.content: 55 | if isinstance(part, ImageContentPart) and not part.image_url.url: 56 | raise ValueError('Image URL cannot be empty') 57 | if isinstance(part, TextContentPart) and not part.text: 58 | raise ValueError('Text content cannot be empty') 59 | 60 | return messages -------------------------------------------------------------------------------- /api/src/models/embeddings.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, field_validator 2 | from typing import List, Iterable, Optional, Union 3 | 4 | EmbeddingsInput = Union[str, List[str], Iterable[int], Iterable[Iterable[int]]] 5 | 6 | class EmbeddingsRequest(BaseModel): 7 | model: str 8 | input: EmbeddingsInput 9 | dimensions: Optional[int] = None 10 | 11 | @field_validator('input') 12 | @classmethod 13 | def validate_input(cls, value: EmbeddingsInput) -> EmbeddingsInput: 14 | if isinstance(value, str) or isinstance(value, list): 15 | if not value: 16 | raise ValueError('The input field must not have an empty string/array') 17 | elif isinstance(value, Iterable) and not isinstance(value, str): 18 | if all(isinstance(item, Iterable) for item in value) and not isinstance(value[0], str): 19 | if not all(sublist for sublist in value): 20 | raise ValueError('The input field must not have an empty sub-array') 21 | else: 22 | if not value: 23 | raise ValueError('The input field must not have an empty iterable') 24 | 25 | return value -------------------------------------------------------------------------------- /api/src/models/images_generations.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, field_validator 2 | from typing import Literal, Optional 3 | 4 | class ImageRequest(BaseModel): 5 | model: str 6 | prompt: str 7 | n: int = 1 8 | size: Literal['256x256', '512x512', '1024x1024', '1792x1024', '1024x1792'] = '1024x1024' 9 | negative_prompt: Optional[str] = None 10 | 11 | @field_validator('prompt') 12 | @classmethod 13 | def validate_prompt(cls, value: str) -> str: 14 | if not value: 15 | raise ValueError('The prompt field must not have an empty string') 16 | return value -------------------------------------------------------------------------------- /api/src/models/moderations.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, field_validator 2 | from typing import List, Union 3 | 4 | class ModerationRequest(BaseModel): 5 | model: str 6 | input: Union[str, List[str]] 7 | 8 | @field_validator('input') 9 | @classmethod 10 | def validate_input(cls, value: Union[str, List[str]]) -> Union[str, List[str]]: 11 | if isinstance(value, str): 12 | if not value: 13 | raise ValueError('The input field must not have an empty string') 14 | elif isinstance(value, list): 15 | if not value or any(not item for item in value): 16 | raise ValueError('The input field must not have an empty array') 17 | 18 | return value -------------------------------------------------------------------------------- /api/src/providers/__init__.py: -------------------------------------------------------------------------------- 1 | from .base_provider import BaseProvider 2 | 3 | __all__ = ['BaseProvider'] -------------------------------------------------------------------------------- /api/src/providers/base_provider.py: -------------------------------------------------------------------------------- 1 | import os 2 | import inspect 3 | import importlib 4 | from dataclasses import dataclass 5 | from motor.motor_asyncio import AsyncIOMotorClient 6 | from asgiref.sync import sync_to_async 7 | from typing import List, Dict, Optional, Type, ClassVar 8 | from ..core import settings 9 | 10 | @dataclass 11 | class ProviderConfig: 12 | name: str 13 | supports_vision: bool 14 | supports_real_streaming: bool 15 | supports_tool_calling: bool 16 | free_models: List[str] 17 | paid_models: List[str] 18 | model_prices: Dict[str, int] 19 | 20 | class BaseProvider: 21 | config: ClassVar[ProviderConfig] = ProviderConfig( 22 | name='', 23 | supports_vision=False, 24 | supports_real_streaming=False, 25 | supports_tool_calling=False, 26 | free_models=[], 27 | paid_models=[], 28 | model_prices={} 29 | ) 30 | 31 | def __init__(self): 32 | self.db = AsyncIOMotorClient(settings.db_url)['db']['providers'] 33 | 34 | @classmethod 35 | def get_provider_class(cls, name: str) -> Optional[Type['BaseProvider']]: 36 | return next( 37 | (p for p in cls.__subclasses__() if p.config.name == name), 38 | None 39 | ) 40 | 41 | @classmethod 42 | def get_all_models(cls) -> List[str]: 43 | all_models = set() 44 | for provider in cls.__subclasses__(): 45 | all_models.update( 46 | provider.config.free_models + 47 | provider.config.paid_models 48 | ) 49 | return list(all_models) 50 | 51 | @sync_to_async 52 | def import_modules(self): 53 | def get_module_name(file_path: str, base_module: str) -> str: 54 | rel_path = os.path.relpath(os.path.dirname(file_path), package_dir) 55 | 56 | if rel_path == '.': 57 | return f'{base_module}.{os.path.splitext(os.path.basename(file_path))[0]}' 58 | 59 | return f'{base_module}.{rel_path.replace(os.sep, ".")}.{os.path.splitext(os.path.basename(file_path))[0]}' 60 | 61 | root_file = inspect.getfile(self.__class__) 62 | package_dir = os.path.dirname(root_file) 63 | base_module = self.__module__.rsplit('.', 1)[0] 64 | 65 | for root, _, files in os.walk(package_dir): 66 | python_files = [f for f in files if f.endswith('.py') and f != '__init__.py'] 67 | 68 | for file in python_files: 69 | file_path = os.path.join(root, file) 70 | module_name = get_module_name(file_path, base_module) 71 | importlib.import_module(module_name) 72 | 73 | async def sync_to_db(self) -> None: 74 | try: 75 | existing_providers = await self.db.find({}).to_list(length=None) 76 | existing_names = {p['name'] for p in existing_providers} 77 | 78 | current_names = {p.config.name for p in self.__class__.__subclasses__()} 79 | 80 | for obsolete_name in existing_names - current_names: 81 | await self.db.delete_one({'name': obsolete_name}) 82 | 83 | for new_provider in current_names - existing_names: 84 | provider_class = self.get_provider_class(new_provider) 85 | config = provider_class.config 86 | all_models = ( 87 | config.free_models + 88 | config.paid_models 89 | ) 90 | await self.db.insert_one({ 91 | 'name': config.name, 92 | 'supports_vision': config.supports_vision, 93 | 'supports_real_streaming': config.supports_real_streaming, 94 | 'supports_tool_calling': config.supports_tool_calling, 95 | 'models': all_models, 96 | 'usage': 0, 97 | 'failures': 0, 98 | 'latency_avg': 0 99 | }) 100 | 101 | 102 | for provider_class in self.__class__.__subclasses__(): 103 | config = provider_class.config 104 | all_models = ( 105 | config.free_models + 106 | config.paid_models 107 | ) 108 | 109 | for db_provider in existing_providers: 110 | if db_provider['name'] == config.name and all_models != db_provider['models']: 111 | await self.db.update_one( 112 | {'name': config.name}, 113 | {'$set': {'models': all_models}}, 114 | upsert=True 115 | ) 116 | 117 | except Exception as e: 118 | raise Exception(f'Failed to sync providers: {str(e)}') 119 | 120 | @classmethod 121 | async def chat_completions(cls, **_) -> None: 122 | raise NotImplementedError 123 | 124 | @classmethod 125 | async def images_generations(cls, **_) -> None: 126 | raise NotImplementedError 127 | 128 | @classmethod 129 | async def embeddings(cls, **_) -> None: 130 | raise NotImplementedError 131 | 132 | @classmethod 133 | async def moderations(cls, **_) -> None: 134 | raise NotImplementedError 135 | 136 | @classmethod 137 | async def audio_speech(cls, **_) -> None: 138 | raise NotImplementedError 139 | 140 | @classmethod 141 | async def audio_transcriptions(cls, **_) -> None: 142 | raise NotImplementedError 143 | 144 | @classmethod 145 | async def audio_translations(cls, **_) -> None: 146 | raise NotImplementedError -------------------------------------------------------------------------------- /api/src/providers/openai.py: -------------------------------------------------------------------------------- 1 | import ujson 2 | import time 3 | import httpx 4 | from dataclasses import dataclass 5 | from fastapi import Request, Response, UploadFile 6 | from motor.motor_asyncio import AsyncIOMotorClient 7 | from typing import List, Dict, Any, Tuple, AsyncGenerator, Union 8 | from ..models import EmbeddingsInput 9 | from ..responses import JSONResponse, StreamingResponseWithStatusCode 10 | from ..core import settings, user_manager, provider_manager 11 | from ..utils import request_processor 12 | from .base_provider import BaseProvider, ProviderConfig 13 | from .utils import WebhookManager, ResponseGenerator, ErrorHandler 14 | 15 | @dataclass 16 | class OpenAIConfig: 17 | api_base_url: str = 'https://api.openai.com/v1' 18 | provider_id: str = 'oai' 19 | timeout: int = 100 20 | 21 | class OpenAI(BaseProvider): 22 | config = ProviderConfig( 23 | name='OpenAI', 24 | supports_vision=True, 25 | supports_tool_calling=True, 26 | supports_real_streaming=True, 27 | free_models=[ 28 | 'gpt-3.5-turbo', 29 | 'gpt-4o', 30 | 'gpt-4o-mini', 31 | 'tts-1', 32 | 'text-embedding-3-small', 33 | 'text-embedding-3-large', 34 | 'omni-moderation-latest', 35 | 'whisper-1' 36 | ], 37 | paid_models=[ 38 | 'gpt-4', 39 | 'gpt-4-turbo-preview', 40 | 'gpt-4-turbo', 41 | 'chatgpt-4o-latest', 42 | 'o1-mini', 43 | 'o1-preview', 44 | 'dall-e-3', 45 | 'tts-1-hd' 46 | ], 47 | model_prices={} 48 | ) 49 | 50 | def __init__(self): 51 | super().__init__() 52 | self.sub_providers_db = AsyncIOMotorClient(settings.db_url)['db']['sub_providers'] 53 | self.api_config = OpenAIConfig() 54 | 55 | async def _get_sub_provider(self, model: str) -> Dict[str, Any]: 56 | sub_providers = await self.sub_providers_db.find({ 57 | 'main_provider': self.config.name, 58 | 'models.api_name': {'$in': [model]} 59 | }).to_list(length=None) 60 | return min(sub_providers, key=lambda x: (x.get('usage', 0), x.get('last_used', 0))) 61 | 62 | async def _update_sub_provider(self, api_key: str, new_data: Dict[str, Any]) -> None: 63 | await self.sub_providers_db.find_one_and_update( 64 | filter={'api_key': api_key}, 65 | update={'$set': new_data}, 66 | upsert=True 67 | ) 68 | 69 | async def _disable_sub_provider(self, api_key: str) -> None: 70 | await self.sub_providers_db.delete_one(filter={'api_key': api_key}) 71 | 72 | def _generate_error_response(self, message: str = 'Something went wrong. Try again later.') -> JSONResponse: 73 | return JSONResponse( 74 | content={ 75 | 'error': { 76 | 'message': message, 77 | 'provider_id': self.api_config.provider_id, 78 | 'type': 'invalid_response_error', 79 | 'code': 500 80 | } 81 | }, 82 | status_code=500 83 | ) 84 | 85 | async def _handle_error(self, request: Request, model: str, status_code: int) -> None: 86 | await WebhookManager.send_to_webhook( 87 | request=request, 88 | is_error=True, 89 | model=model, 90 | pid=self.api_config.provider_id, 91 | exception=f'Status Code: {status_code}' 92 | ) 93 | request.state.provider['failures'] += 1 94 | await provider_manager.update_provider(self.config.name, request.state.provider) 95 | 96 | @classmethod 97 | @ErrorHandler.retry_provider(max_retries=3) 98 | async def chat_completions( 99 | cls, 100 | request: Request, 101 | model: str, 102 | messages: List[Dict[str, Any]], 103 | stream: bool, 104 | **kwargs 105 | ) -> Union[JSONResponse, StreamingResponseWithStatusCode]: 106 | instance = cls() 107 | 108 | sub_provider = await instance._get_sub_provider(model) 109 | model = next((m['provider_name'] for m in sub_provider['models'] if m['api_name'] == model), None) 110 | start = time.time() 111 | 112 | try: 113 | request.state.user['credits'] -= request.state.token_count 114 | await user_manager.update_user(request.state.user['user_id'], request.state.user) 115 | 116 | if not stream: 117 | return await instance._handle_non_streaming_chat( 118 | request=request, 119 | model=model, 120 | messages=messages, 121 | sub_provider=sub_provider, 122 | start=start, 123 | **kwargs 124 | ) 125 | return await instance._handle_streaming_chat( 126 | request=request, 127 | model=model, 128 | messages=messages, 129 | sub_provider=sub_provider, 130 | start=start, 131 | **kwargs 132 | ) 133 | except httpx.HTTPError as e: 134 | await WebhookManager.send_to_webhook(request, True, model, instance.api_config.provider_id, str(e)) 135 | await instance._handle_error(request, model, 500) 136 | return instance._generate_error_response() 137 | 138 | @classmethod 139 | async def images_generations( 140 | cls, 141 | request: Request, 142 | model: str, 143 | prompt: str, 144 | **kwargs 145 | ) -> JSONResponse: 146 | instance = cls() 147 | 148 | if 'negative_prompt' in kwargs: 149 | del kwargs['negative_prompt'] 150 | 151 | async with httpx.AsyncClient() as client: 152 | sub_provider = await instance._get_sub_provider(model) 153 | response = await client.post( 154 | url=f'{instance.api_config.api_base_url}/images/generations', 155 | headers={'Authorization': f'Bearer {sub_provider["api_key"]}'}, 156 | json={'model': model, 'prompt': prompt, **kwargs}, 157 | timeout=10000 158 | ) 159 | 160 | if response.status_code >= 400: 161 | if response.status_code in [401, 403, 429]: 162 | await instance._disable_sub_provider(sub_provider['api_key']) 163 | await instance._handle_error(request, model, response.status_code) 164 | return instance._generate_error_response() 165 | 166 | token_count = instance.config.model_prices.get(model, 10) 167 | request.state.user['credits'] -= token_count 168 | await user_manager.update_user(request.state.user['user_id'], request.state.user) 169 | 170 | return JSONResponse({ 171 | 'provider_id': instance.api_config.provider_id, 172 | **response.json() 173 | }) 174 | 175 | @classmethod 176 | async def embeddings( 177 | cls, 178 | request: Request, 179 | model: str, 180 | input: EmbeddingsInput, 181 | **kwargs 182 | ) -> JSONResponse: 183 | instance = cls() 184 | 185 | async with httpx.AsyncClient() as client: 186 | sub_provider = await instance._get_sub_provider(model) 187 | response = await client.post( 188 | url=f'{instance.api_config.api_base_url}/embeddings', 189 | headers={'Authorization': f'Bearer {sub_provider["api_key"]}'}, 190 | json={'model': model, 'input': input, **kwargs} 191 | ) 192 | 193 | if response.status_code >= 400: 194 | if response.status_code in [401, 403, 429]: 195 | await instance._disable_sub_provider(sub_provider['api_key']) 196 | await instance._handle_error(request, model, response.status_code) 197 | return instance._generate_error_response() 198 | 199 | request.state.user['credits'] -= 100 200 | await user_manager.update_user(request.state.user['user_id'], request.state.user) 201 | 202 | return JSONResponse({ 203 | 'provider_id': instance.api_config.provider_id, 204 | **response.json() 205 | }) 206 | 207 | @classmethod 208 | async def moderations( 209 | cls, 210 | request: Request, 211 | model: str, 212 | input: Union[str, List[str]] 213 | ) -> JSONResponse: 214 | instance = cls() 215 | 216 | async with httpx.AsyncClient() as client: 217 | sub_provider = await instance._get_sub_provider(model) 218 | response = await client.post( 219 | url=f'{instance.api_config.api_base_url}/moderations', 220 | headers={'Authorization': f'Bearer {sub_provider["api_key"]}'}, 221 | json={'model': model, 'input': input} 222 | ) 223 | 224 | if response.status_code >= 400: 225 | if response.status_code in [401, 403, 429]: 226 | await instance._disable_sub_provider(sub_provider['api_key']) 227 | await instance._handle_error(request, model, response.status_code) 228 | return instance._generate_error_response() 229 | 230 | request.state.user['credits'] -= 10 231 | await user_manager.update_user(request.state.user['user_id'], request.state.user) 232 | 233 | return JSONResponse({ 234 | 'provider_id': instance.api_config.provider_id, 235 | **response.json() 236 | }) 237 | 238 | @classmethod 239 | async def audio_speech( 240 | cls, 241 | request: Request, 242 | model: str, 243 | input: str, 244 | **kwargs 245 | ) -> Response: 246 | instance = cls() 247 | 248 | async with httpx.AsyncClient() as client: 249 | sub_provider = await instance._get_sub_provider(model) 250 | response = await client.post( 251 | url=f'{instance.api_config.api_base_url}/audio/speech', 252 | headers={'Authorization': f'Bearer {sub_provider["api_key"]}'}, 253 | json={'model': model, 'input': input, **kwargs} 254 | ) 255 | 256 | if response.status_code >= 400: 257 | if response.status_code in [401, 403, 429]: 258 | await instance._disable_sub_provider(sub_provider['api_key']) 259 | await instance._handle_error(request, model, response.status_code) 260 | return instance._generate_error_response() 261 | 262 | request.state.user['credits'] -= instance.config.model_prices.get(model, 10) 263 | await user_manager.update_user(request.state.user['user_id'], request.state.user) 264 | 265 | return Response( 266 | content=response.content, 267 | media_type='audio/mpeg', 268 | headers={'Content-Disposition': 'attachment;filename=audio.mp3'} 269 | ) 270 | 271 | @classmethod 272 | async def audio_transcriptions( 273 | cls, 274 | request: Request, 275 | model: str, 276 | file: UploadFile 277 | ) -> JSONResponse: 278 | instance = cls() 279 | 280 | async with httpx.AsyncClient() as client: 281 | sub_provider = await instance._get_sub_provider(model) 282 | response = await client.post( 283 | url=f'{instance.api_config.api_base_url}/audio/transcriptions', 284 | headers={'Authorization': f'Bearer {sub_provider["api_key"]}'}, 285 | files={'model': (None, model), 'file': (file.filename, file.file)} 286 | ) 287 | 288 | if response.is_error: 289 | if response.status_code in [401, 403, 429]: 290 | await instance._disable_sub_provider(sub_provider['api_key']) 291 | await instance._handle_error(request, model, response.status_code) 292 | return instance._generate_error_response() 293 | 294 | request.state.user['credits'] -= 100 295 | await user_manager.update_user(request.state.user['user_id'], request.state.user) 296 | 297 | return JSONResponse({ 298 | 'provider_id': instance.api_config.provider_id, 299 | **response.json() 300 | }) 301 | 302 | @classmethod 303 | async def audio_translations( 304 | cls, 305 | request: Request, 306 | model: str, 307 | file: UploadFile 308 | ) -> JSONResponse: 309 | instance = cls() 310 | 311 | async with httpx.AsyncClient() as client: 312 | sub_provider = await instance._get_sub_provider(model) 313 | response = await client.post( 314 | url=f'{instance.api_config.api_base_url}/audio/translations', 315 | headers={'Authorization': f'Bearer {sub_provider["api_key"]}'}, 316 | files={'model': (None, model), 'file': (file.filename, file.file)} 317 | ) 318 | 319 | if response.status_code >= 400: 320 | if response.status_code in [401, 403, 429]: 321 | await instance._disable_sub_provider(sub_provider['api_key']) 322 | await instance._handle_error(request, model, response.status_code) 323 | return instance._generate_error_response() 324 | 325 | request.state.user['credits'] -= 100 326 | await user_manager.update_user(request.state.user['user_id'], request.state.user) 327 | 328 | return JSONResponse({ 329 | 'provider_id': instance.api_config.provider_id, 330 | **response.json() 331 | }) 332 | 333 | async def _handle_non_streaming_chat( 334 | self, 335 | request: Request, 336 | model: str, 337 | messages: List[Dict[str, Any]], 338 | sub_provider: Dict[str, Any], 339 | start: float, 340 | **kwargs 341 | ) -> JSONResponse: 342 | async with httpx.AsyncClient(verify=False, timeout=self.api_config.timeout) as client: 343 | response = await client.post( 344 | url=f'{self.api_config.api_base_url}/chat/completions', 345 | headers={'Authorization': f'Bearer {sub_provider["api_key"]}'}, 346 | json={ 347 | 'model': model, 348 | 'messages': messages, 349 | 'stream': False, 350 | **kwargs 351 | } 352 | ) 353 | 354 | if response.status_code >= 400: 355 | if response.status_code in [401, 403, 429]: 356 | await self._disable_sub_provider(sub_provider['api_key']) 357 | await self._handle_error(request, model, response.status_code) 358 | return self._generate_error_response() 359 | 360 | await self._update_metrics(request, sub_provider, response, start) 361 | return JSONResponse({ 362 | 'provider_id': self.api_config.provider_id, 363 | **response.json() 364 | }) 365 | 366 | async def _handle_streaming_chat( 367 | self, 368 | request: Request, 369 | model: str, 370 | messages: List[Dict[str, Any]], 371 | sub_provider: Dict[str, Any], 372 | start: float, 373 | **kwargs 374 | ) -> StreamingResponseWithStatusCode: 375 | async def stream_response() -> AsyncGenerator[Tuple[str, int], None]: 376 | success = True 377 | try: 378 | async with httpx.AsyncClient(verify=False, timeout=self.api_config.timeout) as client: 379 | async with client.stream( 380 | method='POST', 381 | url=f'{self.api_config.api_base_url}/chat/completions', 382 | headers={'Authorization': f'Bearer {sub_provider["api_key"]}'}, 383 | json={ 384 | 'model': model, 385 | 'messages': messages, 386 | 'stream': True, 387 | **kwargs 388 | } 389 | ) as response: 390 | if response.status_code >= 400: 391 | if response.status_code in [401, 403, 429]: 392 | await self._disable_sub_provider(sub_provider['api_key']) 393 | success = False 394 | await self._handle_error(request, model, response.status_code) 395 | yield ResponseGenerator.generate_error('An error occurred. Try again later.', self.api_config.provider_id), 500 396 | else: 397 | await self._update_streaming_metrics(request, sub_provider, start) 398 | 399 | async for line in response.aiter_lines(): 400 | if line.startswith('data: ') and not line.startswith('data: [DONE]'): 401 | yield await self._process_stream_chunk(request, line) 402 | 403 | if success: 404 | yield 'data: [DONE]\n\n', 200 405 | except httpx.HTTPError: 406 | await self._handle_error(request, model, 500) 407 | yield ResponseGenerator.generate_error('Stream interrupted', self.api_config.provider_id), 500 408 | 409 | return StreamingResponseWithStatusCode( 410 | content=stream_response(), 411 | media_type='text/event-stream' 412 | ) 413 | 414 | async def _update_metrics( 415 | self, 416 | request: Request, 417 | sub_provider: Dict[str, Any], 418 | response: httpx.Response, 419 | start: float 420 | ) -> None: 421 | elapsed = time.time() - start 422 | json_response = response.json() 423 | 424 | word_count = sum( 425 | len(choice['message']['content']) 426 | for choice in json_response['choices'] 427 | if choice['message']['content'] 428 | ) 429 | token_count = sum( 430 | request_processor.count_tokens(choice['message']['content']) 431 | for choice in json_response['choices'] 432 | if choice['message']['content'] 433 | ) 434 | 435 | latency_avg = (elapsed / word_count) if elapsed > 0 else 0 436 | 437 | request.state.provider['usage'] += 1 438 | request.state.provider['latency_avg'] = ( 439 | (request.state.provider['latency_avg'] + latency_avg) / 2 440 | if request.state.provider['latency_avg'] != 0 441 | else latency_avg 442 | ) 443 | 444 | sub_provider['usage'] = sub_provider.get('usage', 0) + 1 445 | sub_provider['last_usage'] = time.time() 446 | 447 | await provider_manager.update_provider(self.config.name, request.state.provider) 448 | await self._update_sub_provider(sub_provider['api_key'], sub_provider) 449 | await self._update_user_credits(request, request.state.token_count + token_count) 450 | 451 | async def _update_streaming_metrics( 452 | self, 453 | request: Request, 454 | sub_provider: Dict[str, Any], 455 | start: float 456 | ) -> None: 457 | elapsed = time.time() - start 458 | 459 | request.state.provider['usage'] += 1 460 | request.state.provider['latency_avg'] = ( 461 | (request.state.provider['latency_avg'] + elapsed) / 2 462 | if request.state.provider['latency_avg'] != 0 463 | else elapsed 464 | ) 465 | 466 | sub_provider['usage'] = sub_provider.get('usage', 0) + 1 467 | sub_provider['last_usage'] = time.time() 468 | 469 | await provider_manager.update_provider(self.config.name, request.state.provider) 470 | await self._update_sub_provider(sub_provider['api_key'], sub_provider) 471 | 472 | async def _process_stream_chunk( 473 | self, 474 | request: Request, 475 | line: str 476 | ) -> Tuple[str, int]: 477 | parsed_chunk = { 478 | 'provider_id': self.api_config.provider_id, 479 | **ujson.loads(line[6:].strip()) 480 | } 481 | token_count = sum( 482 | request_processor.count_tokens(choice['delta'].get('content', '')) 483 | for choice in parsed_chunk['choices'] 484 | ) 485 | 486 | await self._update_user_credits(request, token_count) 487 | 488 | return f'data: {ujson.dumps(parsed_chunk)}\n\n', 200 489 | 490 | async def _update_user_credits(self, request: Request, token_count: int) -> None: 491 | request.state.user['credits'] -= token_count 492 | await user_manager.update_user(request.state.user['user_id'], request.state.user) -------------------------------------------------------------------------------- /api/src/providers/utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | import ujson 3 | import random 4 | import string 5 | import functools 6 | import httpx 7 | from dataclasses import dataclass 8 | from fastapi import Request, Response 9 | from typing import List, Dict, Any, Callable, Coroutine, Optional 10 | from ..core import settings 11 | 12 | @dataclass 13 | class WebhookConfig: 14 | success_color: int = 0x00FF00 15 | error_color: int = 0xFF0000 16 | admin_id: str = '325699845031723010' 17 | error_alert: str = '⚠️ **Error Alert**' 18 | 19 | class ErrorHandler: 20 | @staticmethod 21 | def retry_provider(max_retries: int) -> Callable[..., Callable]: 22 | def wrapper( 23 | func: Callable[..., Coroutine[Any, Any, Response]] 24 | ) -> Callable[..., Coroutine[Any, Any, Any]]: 25 | @functools.wraps(func) 26 | async def inner(*args, **kwargs) -> Any: 27 | for attempt in range(max_retries): 28 | response = await func(*args, **kwargs) 29 | 30 | if response.status_code == 200 or attempt == max_retries - 1: 31 | return response 32 | 33 | return inner 34 | return wrapper 35 | 36 | class MessageFormatter: 37 | @staticmethod 38 | def format_content(content: Any) -> str: 39 | if isinstance(content, str): 40 | return content 41 | return '\n'.join( 42 | content_part['text'] 43 | for content_part in content 44 | if content_part['type'] == 'text' 45 | ) 46 | 47 | @staticmethod 48 | def format_messages(messages: List[Dict[str, Any]]) -> str: 49 | return '\n'.join( 50 | f'{msg['role']}: {MessageFormatter.format_content(msg['content'])}' 51 | for msg in messages 52 | ) 53 | 54 | class ResponseGenerator: 55 | @staticmethod 56 | def generate_error(message: str, provider_id: str) -> str: 57 | error_response = { 58 | 'error': { 59 | 'message': message, 60 | 'provider_id': provider_id, 61 | 'type': 'invalid_response_error', 62 | 'code': 500 63 | } 64 | } 65 | return ResponseGenerator._serialize_json(error_response) 66 | 67 | @staticmethod 68 | def generate_chunk( 69 | content: str, 70 | model: str, 71 | system_fp: str, 72 | completion_id: str, 73 | provider_id: str 74 | ) -> str: 75 | chunk_response = { 76 | 'provider_id': provider_id, 77 | 'id': completion_id, 78 | 'object': 'chat.completion.chunk', 79 | 'created': int(time.time()), 80 | 'model': model, 81 | 'choices': [ 82 | { 83 | 'index': 0, 84 | 'delta': { 85 | 'role': 'assistant', 86 | 'content': content 87 | } 88 | } 89 | ], 90 | 'system_fingerprint': system_fp 91 | } 92 | return ResponseGenerator._serialize_json(chunk_response, False) 93 | 94 | @staticmethod 95 | def _serialize_json(obj: Dict[str, Any], indented: bool = True) -> str: 96 | if indented: 97 | return ujson.dumps( 98 | obj=obj, 99 | ensure_ascii=False, 100 | allow_nan=False, 101 | indent=4, 102 | separators=(', ', ': '), 103 | escape_forward_slashes=False 104 | ) 105 | 106 | return ujson.dumps( 107 | obj=obj, 108 | ensure_ascii=False, 109 | allow_nan=False, 110 | escape_forward_slashes=False 111 | ) 112 | 113 | class IDGenerator: 114 | COMPLETION_PREFIX = 'chatcmpl-AXb' 115 | FINGERPRINT_PREFIX = 'fp_' 116 | 117 | @staticmethod 118 | def generate_random_string(length: int, chars: str) -> str: 119 | return ''.join(random.choices(chars, k=length)) 120 | 121 | @classmethod 122 | def generate_completion_id(cls) -> str: 123 | return f'{cls.COMPLETION_PREFIX}{cls.generate_random_string(29, string.ascii_letters + string.digits)}' 124 | 125 | @classmethod 126 | def generate_fingerprint(cls) -> str: 127 | return f'{cls.FINGERPRINT_PREFIX}{cls.generate_random_string(10, string.hexdigits.lower())}' 128 | 129 | class WebhookManager: 130 | def __init__(self): 131 | self.config = WebhookConfig() 132 | 133 | def _create_embed_data( 134 | self, 135 | is_error: bool, 136 | model: str, 137 | pid: str, 138 | user_id: str, 139 | exception: Optional[str] = None 140 | ) -> Dict[str, Any]: 141 | return { 142 | 'title': 'Status Update', 143 | 'color': self.config.error_color if is_error else self.config.success_color, 144 | 'fields': [ 145 | { 146 | 'name': 'Status', 147 | 'value': 'Failed' if is_error else 'Success', 148 | 'inline': True 149 | }, 150 | { 151 | 'name': 'Model', 152 | 'value': model, 153 | 'inline': True 154 | }, 155 | { 156 | 'name': 'Error', 157 | 'value': exception if exception else 'No Error.', 158 | 'inline': True 159 | }, 160 | { 161 | 'name': 'PID', 162 | 'value': pid if pid else 'No PID.', 163 | 'inline': True 164 | }, 165 | { 166 | 'name': 'User', 167 | 'value': f'<@{user_id}>', 168 | 'inline': True 169 | } 170 | ], 171 | 'footer': { 172 | 'text': f'Timestamp: {time.strftime('%Y-%m-%d %H:%M:%S')}' 173 | } 174 | } 175 | 176 | def _create_payload( 177 | self, 178 | is_error: bool, 179 | embed_data: Dict[str, Any] 180 | ) -> Dict[str, Any]: 181 | payload = {'embeds': [embed_data]} 182 | 183 | if is_error: 184 | payload['content'] = f'{self.config.error_alert} <@{self.config.admin_id}>: WAKE THE FUCK UP' 185 | 186 | return payload 187 | 188 | @classmethod 189 | async def send_to_webhook( 190 | cls, 191 | request: Request, 192 | is_error: bool, 193 | model: str, 194 | pid: str, 195 | exception: Optional[str] = None 196 | ) -> None: 197 | instance = cls() 198 | 199 | embed_data = instance._create_embed_data( 200 | is_error=is_error, 201 | model=model, 202 | pid=pid, 203 | user_id=request.state.user['user_id'], 204 | exception=exception 205 | ) 206 | 207 | payload = instance._create_payload(is_error, embed_data) 208 | 209 | async with httpx.AsyncClient() as client: 210 | await client.post(settings.webhook_url, json=payload) -------------------------------------------------------------------------------- /api/src/responses.py: -------------------------------------------------------------------------------- 1 | import ujson 2 | from dataclasses import dataclass 3 | from fastapi.responses import StreamingResponse, Response 4 | from starlette.types import Send, Scope, Receive 5 | from typing import Union, Dict, Any, Tuple, AsyncGenerator, Optional 6 | 7 | @dataclass 8 | class ResponseConfig: 9 | charset: str = 'utf-8' 10 | content_type: bytes = b'content-type' 11 | json_content_type: bytes = b'application/json' 12 | success_status_range: range = range(200, 300) 13 | 14 | class StreamResponseHandler: 15 | def __init__(self, config: ResponseConfig = ResponseConfig()): 16 | self.config = config 17 | 18 | def _is_success_status(self, status_code: int) -> bool: 19 | return status_code in self.config.success_status_range 20 | 21 | def _encode_content(self, content: Union[str, bytes]) -> bytes: 22 | if isinstance(content, str): 23 | return content.encode(self.config.charset) 24 | return content 25 | 26 | def _update_headers_for_error( 27 | self, 28 | headers: list, 29 | status_code: int 30 | ) -> list: 31 | if not self._is_success_status(status_code): 32 | content_type_header = next( 33 | (h for h in headers if h[0].decode('latin-1').lower() == 'content-type'), 34 | None 35 | ) 36 | if content_type_header: 37 | headers.remove(content_type_header) 38 | headers.append(( 39 | self.config.content_type, 40 | self.config.json_content_type 41 | )) 42 | return headers 43 | 44 | async def _send_chunk( 45 | self, 46 | send: Send, 47 | content: Union[str, bytes], 48 | more_body: bool = True 49 | ) -> None: 50 | await send({ 51 | 'type': 'http.response.body', 52 | 'body': self._encode_content(content), 53 | 'more_body': more_body 54 | }) 55 | 56 | async def _send_response_start( 57 | self, 58 | send: Send, 59 | status_code: int, 60 | headers: list 61 | ) -> None: 62 | await send({ 63 | 'type': 'http.response.start', 64 | 'status': status_code, 65 | 'headers': headers, 66 | }) 67 | 68 | class StreamingResponseWithStatusCode(StreamingResponse): 69 | def __init__( 70 | self, 71 | content: AsyncGenerator[Tuple[str, int], None], 72 | status_code: int = 200, 73 | headers: Optional[Dict[str, str]] = None, 74 | media_type: Optional[str] = None, 75 | background: Optional[Any] = None, 76 | ): 77 | super().__init__( 78 | content=content, 79 | status_code=status_code, 80 | headers=headers, 81 | media_type=media_type, 82 | background=background 83 | ) 84 | self.handler = StreamResponseHandler() 85 | 86 | async def stream_response(self, send: Send) -> None: 87 | first_chunk_content, self.status_code = await self.body_iterator.__anext__() 88 | 89 | self.raw_headers = self.handler._update_headers_for_error( 90 | self.raw_headers, 91 | self.status_code 92 | ) 93 | 94 | await self.handler._send_response_start( 95 | send, 96 | self.status_code, 97 | self.raw_headers 98 | ) 99 | 100 | await self.handler._send_chunk(send, first_chunk_content) 101 | 102 | async for chunk_content, chunk_status in self.body_iterator: 103 | if not self.handler._is_success_status(chunk_status): 104 | self.status_code = chunk_status 105 | await self.handler._send_chunk(send, '', False) 106 | return 107 | 108 | await self.handler._send_chunk(send, chunk_content) 109 | 110 | await self.handler._send_chunk(send, '', False) 111 | 112 | class JSONResponse(Response): 113 | media_type = 'application/json' 114 | 115 | @staticmethod 116 | def _serialize_json(content: Union[list, dict]) -> str: 117 | return ujson.dumps( 118 | obj=content, 119 | ensure_ascii=False, 120 | allow_nan=False, 121 | indent=4, 122 | separators=(', ', ': '), 123 | escape_forward_slashes=False 124 | ) 125 | 126 | def render(self, content: Union[list, dict]) -> bytes: 127 | return self._serialize_json(content).encode('utf-8') 128 | 129 | async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: 130 | await super().__call__(scope, receive, send) -------------------------------------------------------------------------------- /api/src/tasks.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | import yaml 4 | from pathlib import Path 5 | from dataclasses import dataclass 6 | from typing import Dict, Any, Optional 7 | from motor.motor_asyncio import AsyncIOMotorClient, AsyncIOMotorCollection 8 | from .core import settings 9 | 10 | @dataclass 11 | class CreditsConfig: 12 | max_credits: int = 5000 13 | check_interval: int = 600 14 | daily_interval: int = 86400 15 | credits_file: str = 'credits.yml' 16 | 17 | class CreditsManager: 18 | def __init__( 19 | self, 20 | db_collection: AsyncIOMotorCollection, 21 | config: Optional[CreditsConfig] = None 22 | ): 23 | self.db = db_collection 24 | self.config = config or CreditsConfig() 25 | self.credits_tiers = self._load_credits_tiers() 26 | 27 | def _load_credits_tiers(self) -> Dict[int, int]: 28 | try: 29 | credits_path = Path(self.config.credits_file) 30 | if not credits_path.exists(): 31 | raise FileNotFoundError( 32 | f'Credits file not found: {self.config.credits_file}' 33 | ) 34 | 35 | with credits_path.open() as f: 36 | return yaml.safe_load(f) 37 | except Exception as e: 38 | raise RuntimeError(f'Failed to load credits configuration: {str(e)}') 39 | 40 | async def _get_users_needing_update(self) -> AsyncIOMotorCollection: 41 | current_time = time.time() 42 | return self.db.find({ 43 | 'credits': {'$lt': self.config.max_credits}, 44 | 'last_daily': { 45 | '$lte': current_time - self.config.daily_interval 46 | } 47 | }) 48 | 49 | async def _update_user_credits(self, user: Dict[str, Any]) -> None: 50 | try: 51 | credits_to_add = self.credits_tiers[user['premium_tier']] 52 | current_time = time.time() 53 | 54 | await self.db.update_one( 55 | {'user_id': user['user_id']}, 56 | { 57 | '$set': { 58 | 'credits': min( 59 | user['credits'] + credits_to_add, 60 | self.config.max_credits 61 | ), 62 | 'last_daily': current_time 63 | } 64 | } 65 | ) 66 | except Exception as e: 67 | print(f'Failed to update credits for user {user["user_id"]}: {str(e)}') 68 | 69 | async def process_credits_updates(self) -> None: 70 | try: 71 | async for user in await self._get_users_needing_update(): 72 | await self._update_user_credits(user) 73 | except Exception as e: 74 | print(f'Error processing credits updates: {str(e)}') 75 | 76 | async def start_credits_service(self) -> None: 77 | while True: 78 | try: 79 | await self.process_credits_updates() 80 | await asyncio.sleep(self.config.check_interval) 81 | except asyncio.CancelledError: 82 | break 83 | except Exception as e: 84 | print(f'Credits service error: {str(e)}') 85 | await asyncio.sleep(self.config.check_interval) 86 | 87 | class CreditsService: 88 | def __init__(self): 89 | self.db = AsyncIOMotorClient(settings.db_url)['db']['users'] 90 | self.config = CreditsConfig() 91 | self.manager = CreditsManager(self.db, self.config) 92 | self.task: Optional[asyncio.Task] = None 93 | 94 | async def start(self) -> None: 95 | if not self.task or self.task.done(): 96 | self.task = asyncio.create_task( 97 | self.manager.start_credits_service() 98 | ) 99 | 100 | async def stop(self) -> None: 101 | if self.task and not self.task.done(): 102 | self.task.cancel() -------------------------------------------------------------------------------- /api/src/utils.py: -------------------------------------------------------------------------------- 1 | import tiktoken 2 | from fastapi import Request 3 | from typing import Union, List, Optional 4 | from dataclasses import dataclass 5 | from .models import ChatRequest, Message, TextContentPart, ImageContentPart 6 | 7 | @dataclass 8 | class TokenizerConfig: 9 | encoding_name: str = 'o200k_base' 10 | non_text_token_count: int = 100 11 | 12 | class TokenCounter: 13 | def __init__(self, config: Optional[TokenizerConfig] = None): 14 | self.config = config or TokenizerConfig() 15 | self.encoding = tiktoken.get_encoding(self.config.encoding_name) 16 | 17 | def count_text_tokens(self, text: str) -> int: 18 | return len(self.encoding.encode(text)) 19 | 20 | def count_message_content_tokens( 21 | self, 22 | content: Union[str, List[Union[TextContentPart, ImageContentPart]]] 23 | ) -> int: 24 | if isinstance(content, str): 25 | return self.count_text_tokens(content) 26 | 27 | return sum( 28 | self.count_text_tokens(part.text) 29 | if part.type == 'text' 30 | else self.config.non_text_token_count 31 | for part in content 32 | ) 33 | 34 | def count_message_tokens(self, message: Union[Message, str]) -> int: 35 | if isinstance(message, str): 36 | return self.count_text_tokens(message) 37 | 38 | return self.count_message_content_tokens(message.content) 39 | 40 | def count_request_tokens(self, request: ChatRequest) -> int: 41 | return sum( 42 | self.count_message_tokens(msg) 43 | for msg in request.messages 44 | ) 45 | 46 | class APIKeyExtractor: 47 | def __init__(self, config: Optional[TokenizerConfig] = None): 48 | self.config = config or TokenizerConfig() 49 | 50 | def extract_api_key(self, request: Request) -> str: 51 | auth_header = request.headers.get('Authorization', '') 52 | 53 | if not auth_header or not auth_header.startswith('Bearer '): 54 | return 'none' 55 | 56 | return auth_header.replace('Bearer ', '', 1) 57 | 58 | def validate_api_key(self, api_key: str) -> bool: 59 | return ( 60 | api_key != 'none' and 61 | len(api_key.strip()) > 0 62 | ) 63 | 64 | class RequestProcessor: 65 | def __init__( 66 | self, 67 | token_counter: Optional[TokenCounter] = None, 68 | key_extractor: Optional[APIKeyExtractor] = None, 69 | config: Optional[TokenizerConfig] = None 70 | ): 71 | self.config = config or TokenizerConfig() 72 | self.token_counter = token_counter or TokenCounter(self.config) 73 | self.key_extractor = key_extractor or APIKeyExtractor(self.config) 74 | 75 | def count_tokens(self, input_data: Union[ChatRequest, str]) -> int: 76 | if isinstance(input_data, ChatRequest): 77 | return self.token_counter.count_request_tokens(input_data) 78 | return self.token_counter.count_message_tokens(input_data) 79 | 80 | def get_api_key(self, request: Request) -> str: 81 | return self.key_extractor.extract_api_key(request) 82 | 83 | request_processor = RequestProcessor() -------------------------------------------------------------------------------- /bot/.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | app.egg-info 3 | *.pyc 4 | .mypy_cache 5 | .coverage 6 | htmlcov 7 | .cache 8 | .venv -------------------------------------------------------------------------------- /bot/.env.example: -------------------------------------------------------------------------------- 1 | TOKEN= 2 | DB_URL=mongodb://root:password@db:27017 -------------------------------------------------------------------------------- /bot/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/astral-sh/uv:latest AS uv 2 | 3 | # Builder stage 4 | FROM python:3.11-alpine AS builder 5 | 6 | ENV UV_LINK_MODE=copy 7 | 8 | RUN --mount=from=uv,source=/uv,target=/opt/uv \ 9 | /opt/uv venv /opt/venv 10 | 11 | ENV VIRTUAL_ENV=/opt/venv \ 12 | PATH="/opt/venv/bin:$PATH" 13 | 14 | COPY pyproject.toml . 15 | 16 | RUN --mount=type=cache,target=/root/.cache/uv \ 17 | --mount=from=uv,source=/uv,target=/opt/uv \ 18 | /opt/uv pip install -r pyproject.toml 19 | 20 | # Final stage 21 | FROM python:3.11-alpine AS final 22 | 23 | WORKDIR /app 24 | 25 | COPY . /app 26 | 27 | COPY --from=builder /opt/venv /opt/venv 28 | 29 | RUN --mount=from=uv,source=/uv,target=/tmp/uv \ 30 | cp /tmp/uv /opt/uv 31 | 32 | ENV PYTHONUNBUFFERED=1 \ 33 | PYTHONDONTWRITEBYTECODE=1 \ 34 | VIRTUAL_ENV=/opt/venv \ 35 | PATH="/opt/venv/bin:$PATH" \ 36 | PATH="/opt/uv:$PATH" 37 | 38 | CMD ["/opt/venv/bin/python3", "-m", "src.__main__"] -------------------------------------------------------------------------------- /bot/README.md: -------------------------------------------------------------------------------- 1 | # Bot -------------------------------------------------------------------------------- /bot/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "bot" 3 | version = "1.0.0" 4 | requires-python = ">=3.11" 5 | description = "" 6 | dependencies = [ 7 | "discord-py>=2.4.0", 8 | "motor>=3.6.0", 9 | "pydantic>=2.9.2", 10 | "pydantic-settings>=2.5.2", 11 | "pyyaml>=6.0.2", 12 | ] 13 | -------------------------------------------------------------------------------- /bot/src/__main__.py: -------------------------------------------------------------------------------- 1 | from .config import settings 2 | from .main import DiscordBot 3 | 4 | bot = DiscordBot() 5 | bot.run(settings.token) -------------------------------------------------------------------------------- /bot/src/cogs/__init__.py: -------------------------------------------------------------------------------- 1 | from .key_cog import KeyCog, setup as key_setup 2 | from .switcher_cog import SwitcherCog, setup as switcher_setup 3 | 4 | __all__ = [ 5 | 'KeyCog', 6 | 'SwitcherCog', 7 | 'key_setup', 8 | 'switcher_setup' 9 | ] -------------------------------------------------------------------------------- /bot/src/cogs/key_cog.py: -------------------------------------------------------------------------------- 1 | from discord import Interaction, Member, app_commands 2 | from discord.ext import commands 3 | from typing import Optional 4 | from ..utils import Utils 5 | 6 | class KeyCog(commands.Cog): 7 | def __init__(self, bot: commands.Bot) -> None: 8 | self.bot = bot 9 | self.utils = Utils() 10 | 11 | group = app_commands.Group(name='key', description='a') 12 | 13 | @group.command(name='get') 14 | async def get(self, interaction: Interaction) -> None: 15 | await self.utils.retrieve_api_key(interaction) 16 | 17 | @group.command(name='lookup') 18 | @app_commands.describe(user='[OPTIONAL] The user to lookup.') 19 | async def lookup(self, interaction: Interaction, user: Optional[Member] = None) -> None: 20 | await self.utils.user_lookup(interaction, user) 21 | 22 | @group.command(name='reset-ip') 23 | async def reset_ip(self, interaction: Interaction) -> None: 24 | await self.utils.reset_user_ip(interaction) 25 | 26 | async def setup(bot: commands.Bot) -> None: 27 | await bot.add_cog(KeyCog(bot)) -------------------------------------------------------------------------------- /bot/src/cogs/switcher_cog.py: -------------------------------------------------------------------------------- 1 | import discord 2 | from discord import app_commands 3 | from discord.ext import commands 4 | from ..utils import Utils 5 | 6 | class SwitcherCog(commands.Cog): 7 | def __init__(self, bot: commands.Bot) -> None: 8 | self.bot = bot 9 | self.utils = Utils() 10 | 11 | @app_commands.command(name='switch') 12 | @app_commands.describe( 13 | user='The user to switch the property of.', 14 | property='The property to switch.', 15 | status='The new status of the property.' 16 | ) 17 | @app_commands.choices( 18 | property=[ 19 | app_commands.Choice(name='premium', value='premium_tier'), 20 | app_commands.Choice(name='banned', value='banned') 21 | ] 22 | ) 23 | async def switch( 24 | self, 25 | interaction: discord.Interaction, 26 | user: discord.Member, 27 | status: str, 28 | property: app_commands.Choice[str] 29 | ) -> None: 30 | await self.utils.modify_user_status( 31 | interaction, 32 | user, 33 | property.value, 34 | status 35 | ) 36 | 37 | async def setup(bot: commands.Bot) -> None: 38 | await bot.add_cog(SwitcherCog(bot)) -------------------------------------------------------------------------------- /bot/src/config.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings, SettingsConfigDict 2 | 3 | class Settings(BaseSettings): 4 | token: str 5 | db_url: str 6 | 7 | model_config = SettingsConfigDict( 8 | env_file='.env', 9 | env_ignore_empty=True, 10 | extra='ignore' 11 | ) 12 | 13 | settings = Settings() -------------------------------------------------------------------------------- /bot/src/db/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from .user_manager import UserManager 3 | 4 | __all__ = ['UserManager'] -------------------------------------------------------------------------------- /bot/src/db/exceptions.py: -------------------------------------------------------------------------------- 1 | class DatabaseError(Exception): 2 | def __init__(self, message: str): 3 | self.message = message 4 | super().__init__(self.message) -------------------------------------------------------------------------------- /bot/src/db/user_manager.py: -------------------------------------------------------------------------------- 1 | import time 2 | import secrets 3 | from typing import Dict, Any, Optional 4 | from motor.motor_asyncio import AsyncIOMotorClient 5 | from .exceptions import DatabaseError 6 | from ..config import settings 7 | 8 | class UserDatabase: 9 | def __init__(self): 10 | self.client = AsyncIOMotorClient(settings.db_url) 11 | self.collection = self.client['db']['users'] 12 | 13 | class UserManager: 14 | def __init__(self): 15 | self.db = UserDatabase() 16 | 17 | async def get_user( 18 | self, 19 | user_id: Optional[int] = None, 20 | key: Optional[str] = None 21 | ) -> Optional[Dict[str, Any]]: 22 | try: 23 | query = {'user_id': user_id} if user_id else {'key': key} 24 | return await self.db.collection.find_one(query) 25 | except Exception as e: 26 | raise DatabaseError(f'Failed to retrieve user: {str(e)}') 27 | 28 | async def update_user( 29 | self, 30 | user_id: str, 31 | new_data: Dict[str, Any], 32 | upsert: bool = True 33 | ) -> Optional[Dict[str, Any]]: 34 | try: 35 | update_data = {k: v for k, v in new_data.items() if k != '_id'} 36 | 37 | return await self.db.collection.find_one_and_update( 38 | filter={'user_id': user_id}, 39 | update={'$set': update_data}, 40 | upsert=upsert, 41 | return_document=True 42 | ) 43 | except Exception as e: 44 | raise DatabaseError(f'Failed to update user: {str(e)}') 45 | 46 | async def insert_user(self, id: int) -> Optional[Dict[str, Any]]: 47 | try: 48 | insert_data = { 49 | 'user_id': id, 50 | 'key': f'zu-{"".join(secrets.token_hex(16))}', 51 | 'premium_tier': 0, 52 | 'banned': False, 53 | 'credits': 42069, 54 | 'premium_expiry': 0, 55 | 'last_daily': time.time(), 56 | 'ip': None 57 | } 58 | await self.db.collection.insert_one(insert_data) 59 | return insert_data 60 | except Exception as e: 61 | raise DatabaseError(f'Failed to insert user: {str(e)}') 62 | 63 | async def delete_user(self, user_id: str) -> None: 64 | try: 65 | await self.db.collection.delete_one({'user_id': user_id}) 66 | except Exception as e: 67 | raise DatabaseError(f'Failed to delete user: {str(e)}') -------------------------------------------------------------------------------- /bot/src/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import discord 3 | from discord.ext import commands 4 | 5 | class DiscordBot(commands.Bot): 6 | def __init__(self) -> None: 7 | super().__init__( 8 | command_prefix=commands.when_mentioned_or('!'), 9 | intents=discord.Intents.default() 10 | ) 11 | 12 | async def load_cogs(self) -> None: 13 | for file in os.listdir(os.path.join(os.path.dirname(__file__), 'cogs')): 14 | if file.endswith('.py') and not file.startswith('_'): 15 | await self.load_extension(f'src.cogs.{file[:-3]}') 16 | 17 | async def setup_hook(self) -> None: 18 | await self.load_cogs() 19 | await self.tree.sync() 20 | print(f'Logged in as {self.user} (ID: {self.user.id})') -------------------------------------------------------------------------------- /bot/src/utils.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | import time 3 | import logging 4 | from typing import Optional, Dict, Any 5 | from discord import Interaction, Member, HTTPException, InteractionResponded, utils 6 | from .db import UserManager 7 | 8 | class Utils: 9 | def __init__(self): 10 | self._user_manager = UserManager() 11 | 12 | try: 13 | with open('credits.yml', 'r') as config_file: 14 | self._credits_config = yaml.safe_load(config_file) 15 | except FileNotFoundError: 16 | logging.error('Credits configuration file not found: credits.yml') 17 | self._credits_config = {} 18 | except yaml.YAMLError as e: 19 | logging.error(f'Error parsing credits configuration: {e}') 20 | self._credits_config = {} 21 | 22 | async def retrieve_api_key(self, interaction: Interaction) -> None: 23 | try: 24 | await interaction.response.defer(ephemeral=True) 25 | 26 | user = await self._user_manager.get_user('user_id', interaction.user.id) 27 | 28 | if user: 29 | await interaction.followup.send( 30 | f'Your existing API key is: {user["key"]}', 31 | ephemeral=True 32 | ) 33 | return 34 | 35 | new_user = await self._user_manager.insert_user(interaction.user.id) 36 | 37 | await interaction.followup.send( 38 | f'Your new API key is: {new_user["key"]}', 39 | ephemeral=True 40 | ) 41 | 42 | except (HTTPException, InteractionResponded) as e: 43 | logging.error(f'Error retrieving API key: {e}') 44 | await interaction.followup.send( 45 | 'An error occurred while processing your request.', 46 | ephemeral=True 47 | ) 48 | 49 | async def reset_user_ip(self, interaction: Interaction) -> None: 50 | try: 51 | await interaction.response.defer(ephemeral=True) 52 | 53 | user = await self._user_manager.get_user('user_id', interaction.user.id) 54 | 55 | if not user: 56 | await interaction.followup.send( 57 | 'You do not have an existing API key.', 58 | ephemeral=True 59 | ) 60 | return 61 | 62 | user['ip'] = None 63 | await self._user_manager.update_user('user_id', interaction.user.id, user) 64 | 65 | await interaction.followup.send( 66 | f'Successfully reset the IP for {interaction.user.mention}.', 67 | ephemeral=True 68 | ) 69 | 70 | except (HTTPException, InteractionResponded) as e: 71 | logging.error(f'Error resetting user IP: {e}') 72 | await interaction.followup.send( 73 | 'An error occurred while resetting your IP.', 74 | ephemeral=True 75 | ) 76 | 77 | async def user_lookup(self, interaction: Interaction, member: Optional[Member] = None) -> None: 78 | try: 79 | await interaction.response.defer(ephemeral=True) 80 | 81 | is_admin = utils.get(interaction.user.roles, name='bot-admin') is not None 82 | target_user_id = member.id if member and is_admin else interaction.user.id 83 | 84 | if member and not is_admin: 85 | await interaction.followup.send( 86 | 'You do not have permission to look up other users.', 87 | ephemeral=True 88 | ) 89 | return 90 | 91 | user = await self._user_manager.get_user('user_id', target_user_id) 92 | 93 | if not user: 94 | await interaction.followup.send( 95 | 'No API key found for this user.', 96 | ephemeral=True 97 | ) 98 | return 99 | 100 | if user.get('banned', False): 101 | await interaction.followup.send( 102 | 'This user\'s API key has been banned.', 103 | ephemeral=True 104 | ) 105 | return 106 | 107 | target_mention = member.mention if member else interaction.user.mention 108 | await interaction.followup.send( 109 | f'User data for {target_mention}: {user}', 110 | ephemeral=True 111 | ) 112 | 113 | except (HTTPException, InteractionResponded) as e: 114 | logging.error(f'Error during user lookup: {e}') 115 | await interaction.followup.send( 116 | 'An error occurred during user lookup.', 117 | ephemeral=True 118 | ) 119 | 120 | async def modify_user_status( 121 | self, 122 | interaction: Interaction, 123 | member: Member, 124 | property_name: str, 125 | new_status: str 126 | ) -> None: 127 | try: 128 | await interaction.response.defer(ephemeral=True) 129 | 130 | if not utils.get(interaction.user.roles, name='bot-admin'): 131 | await interaction.followup.send( 132 | 'You do not have permission to modify user status.', 133 | ephemeral=True 134 | ) 135 | return 136 | 137 | user = await self._user_manager.get_user('user_id', member.id) 138 | 139 | if not user: 140 | await interaction.followup.send( 141 | 'No API key found for this user.', 142 | ephemeral=True 143 | ) 144 | return 145 | 146 | if property_name == 'banned': 147 | await self._handle_ban_modification( 148 | interaction, member, user, new_status 149 | ) 150 | elif property_name == 'premium_tier': 151 | await self._handle_premium_modification( 152 | interaction, member, user, new_status 153 | ) 154 | else: 155 | await interaction.followup.send( 156 | f'Invalid property: {property_name}', 157 | ephemeral=True 158 | ) 159 | 160 | except (HTTPException, InteractionResponded) as e: 161 | logging.error(f'Error modifying user status: {e}') 162 | await interaction.followup.send( 163 | 'An error occurred while modifying user status.', 164 | ephemeral=True 165 | ) 166 | 167 | async def _handle_ban_modification( 168 | self, 169 | interaction: Interaction, 170 | member: Member, 171 | new_status: str 172 | ) -> None: 173 | banned_status = new_status.lower() == 'true' 174 | await self._user_manager.update_user( 175 | 'user_id', member.id, {'banned': banned_status} 176 | ) 177 | 178 | status_message = ( 179 | f'Successfully {"banned" if banned_status else "unbanned"} ' 180 | f'{member.mention}.' 181 | ) 182 | await interaction.followup.send(status_message, ephemeral=True) 183 | 184 | async def _handle_premium_modification( 185 | self, 186 | interaction: Interaction, 187 | member: Member, 188 | user: Dict[str, Any], 189 | new_status: str 190 | ) -> None: 191 | new_tier = int(new_status) 192 | user['premium_tier'] = new_tier 193 | 194 | if new_tier > 0: 195 | user['credits'] += self._credits_config.get(new_tier, 0) 196 | user['last_daily'] = time.time() 197 | 198 | await self._user_manager.update_user('user_id', member.id, user) 199 | 200 | status_message = ( 201 | f'Successfully {"gave" if new_tier > 0 else "removed"} ' 202 | f'premium to {member.mention}.' 203 | ) 204 | await interaction.followup.send(status_message, ephemeral=True) -------------------------------------------------------------------------------- /bot/uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.11" 3 | resolution-markers = [ 4 | "python_full_version < '3.13'", 5 | "python_full_version >= '3.13'", 6 | ] 7 | 8 | [[package]] 9 | name = "aiohappyeyeballs" 10 | version = "2.4.3" 11 | source = { registry = "https://pypi.org/simple" } 12 | sdist = { url = "https://files.pythonhosted.org/packages/bc/69/2f6d5a019bd02e920a3417689a89887b39ad1e350b562f9955693d900c40/aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586", size = 21809 } 13 | wheels = [ 14 | { url = "https://files.pythonhosted.org/packages/f7/d8/120cd0fe3e8530df0539e71ba9683eade12cae103dd7543e50d15f737917/aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572", size = 14742 }, 15 | ] 16 | 17 | [[package]] 18 | name = "aiohttp" 19 | version = "3.10.10" 20 | source = { registry = "https://pypi.org/simple" } 21 | dependencies = [ 22 | { name = "aiohappyeyeballs" }, 23 | { name = "aiosignal" }, 24 | { name = "attrs" }, 25 | { name = "frozenlist" }, 26 | { name = "multidict" }, 27 | { name = "yarl" }, 28 | ] 29 | sdist = { url = "https://files.pythonhosted.org/packages/17/7e/16e57e6cf20eb62481a2f9ce8674328407187950ccc602ad07c685279141/aiohttp-3.10.10.tar.gz", hash = "sha256:0631dd7c9f0822cc61c88586ca76d5b5ada26538097d0f1df510b082bad3411a", size = 7542993 } 30 | wheels = [ 31 | { url = "https://files.pythonhosted.org/packages/72/31/3c351d17596194e5a38ef169a4da76458952b2497b4b54645b9d483cbbb0/aiohttp-3.10.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c30a0eafc89d28e7f959281b58198a9fa5e99405f716c0289b7892ca345fe45f", size = 586501 }, 32 | { url = "https://files.pythonhosted.org/packages/a4/a8/a559d09eb08478cdead6b7ce05b0c4a133ba27fcdfa91e05d2e62867300d/aiohttp-3.10.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:258c5dd01afc10015866114e210fb7365f0d02d9d059c3c3415382ab633fcbcb", size = 398993 }, 33 | { url = "https://files.pythonhosted.org/packages/c5/47/7736d4174613feef61d25332c3bd1a4f8ff5591fbd7331988238a7299485/aiohttp-3.10.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:15ecd889a709b0080f02721255b3f80bb261c2293d3c748151274dfea93ac871", size = 390647 }, 34 | { url = "https://files.pythonhosted.org/packages/27/21/e9ba192a04b7160f5a8952c98a1de7cf8072ad150fa3abd454ead1ab1d7f/aiohttp-3.10.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3935f82f6f4a3820270842e90456ebad3af15810cf65932bd24da4463bc0a4c", size = 1306481 }, 35 | { url = "https://files.pythonhosted.org/packages/cf/50/f364c01c8d0def1dc34747b2470969e216f5a37c7ece00fe558810f37013/aiohttp-3.10.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:413251f6fcf552a33c981c4709a6bba37b12710982fec8e558ae944bfb2abd38", size = 1344652 }, 36 | { url = "https://files.pythonhosted.org/packages/1d/c2/74f608e984e9b585649e2e83883facad6fa3fc1d021de87b20cc67e8e5ae/aiohttp-3.10.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1720b4f14c78a3089562b8875b53e36b51c97c51adc53325a69b79b4b48ebcb", size = 1378498 }, 37 | { url = "https://files.pythonhosted.org/packages/9f/a7/05a48c7c0a7a80a5591b1203bf1b64ca2ed6a2050af918d09c05852dc42b/aiohttp-3.10.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:679abe5d3858b33c2cf74faec299fda60ea9de62916e8b67e625d65bf069a3b7", size = 1292718 }, 38 | { url = "https://files.pythonhosted.org/packages/7d/78/a925655018747e9790350180330032e27d6e0d7ed30bde545fae42f8c49c/aiohttp-3.10.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79019094f87c9fb44f8d769e41dbb664d6e8fcfd62f665ccce36762deaa0e911", size = 1251776 }, 39 | { url = "https://files.pythonhosted.org/packages/47/9d/85c6b69f702351d1236594745a4fdc042fc43f494c247a98dac17e004026/aiohttp-3.10.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe2fb38c2ed905a2582948e2de560675e9dfbee94c6d5ccdb1301c6d0a5bf092", size = 1271716 }, 40 | { url = "https://files.pythonhosted.org/packages/7f/a7/55fc805ff9b14af818903882ece08e2235b12b73b867b521b92994c52b14/aiohttp-3.10.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a3f00003de6eba42d6e94fabb4125600d6e484846dbf90ea8e48a800430cc142", size = 1266263 }, 41 | { url = "https://files.pythonhosted.org/packages/1f/ec/d2be2ca7b063e4f91519d550dbc9c1cb43040174a322470deed90b3d3333/aiohttp-3.10.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1bbb122c557a16fafc10354b9d99ebf2f2808a660d78202f10ba9d50786384b9", size = 1321617 }, 42 | { url = "https://files.pythonhosted.org/packages/c9/a3/b29f7920e1cd0a9a68a45dd3eb16140074d2efb1518d2e1f3e140357dc37/aiohttp-3.10.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:30ca7c3b94708a9d7ae76ff281b2f47d8eaf2579cd05971b5dc681db8caac6e1", size = 1339227 }, 43 | { url = "https://files.pythonhosted.org/packages/8a/81/34b67235c47e232d807b4bbc42ba9b927c7ce9476872372fddcfd1e41b3d/aiohttp-3.10.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df9270660711670e68803107d55c2b5949c2e0f2e4896da176e1ecfc068b974a", size = 1299068 }, 44 | { url = "https://files.pythonhosted.org/packages/04/1f/26a7fe11b6ad3184f214733428353c89ae9fe3e4f605a657f5245c5e720c/aiohttp-3.10.10-cp311-cp311-win32.whl", hash = "sha256:aafc8ee9b742ce75044ae9a4d3e60e3d918d15a4c2e08a6c3c3e38fa59b92d94", size = 362223 }, 45 | { url = "https://files.pythonhosted.org/packages/10/91/85dcd93f64011434359ce2666bece981f08d31bc49df33261e625b28595d/aiohttp-3.10.10-cp311-cp311-win_amd64.whl", hash = "sha256:362f641f9071e5f3ee6f8e7d37d5ed0d95aae656adf4ef578313ee585b585959", size = 381576 }, 46 | { url = "https://files.pythonhosted.org/packages/ae/99/4c5aefe5ad06a1baf206aed6598c7cdcbc7c044c46801cd0d1ecb758cae3/aiohttp-3.10.10-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9294bbb581f92770e6ed5c19559e1e99255e4ca604a22c5c6397b2f9dd3ee42c", size = 583536 }, 47 | { url = "https://files.pythonhosted.org/packages/a9/36/8b3bc49b49cb6d2da40ee61ff15dbcc44fd345a3e6ab5bb20844df929821/aiohttp-3.10.10-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a8fa23fe62c436ccf23ff930149c047f060c7126eae3ccea005f0483f27b2e28", size = 395693 }, 48 | { url = "https://files.pythonhosted.org/packages/e1/77/0aa8660dcf11fa65d61712dbb458c4989de220a844bd69778dff25f2d50b/aiohttp-3.10.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c6a5b8c7926ba5d8545c7dd22961a107526562da31a7a32fa2456baf040939f", size = 390898 }, 49 | { url = "https://files.pythonhosted.org/packages/38/d2/b833d95deb48c75db85bf6646de0a697e7fb5d87bd27cbade4f9746b48b1/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:007ec22fbc573e5eb2fb7dec4198ef8f6bf2fe4ce20020798b2eb5d0abda6138", size = 1312060 }, 50 | { url = "https://files.pythonhosted.org/packages/aa/5f/29fd5113165a0893de8efedf9b4737e0ba92dfcd791415a528f947d10299/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9627cc1a10c8c409b5822a92d57a77f383b554463d1884008e051c32ab1b3742", size = 1350553 }, 51 | { url = "https://files.pythonhosted.org/packages/ad/cc/f835f74b7d344428469200105236d44606cfa448be1e7c95ca52880d9bac/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50edbcad60d8f0e3eccc68da67f37268b5144ecc34d59f27a02f9611c1d4eec7", size = 1392646 }, 52 | { url = "https://files.pythonhosted.org/packages/bf/fe/1332409d845ca601893bbf2d76935e0b93d41686e5f333841c7d7a4a770d/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a45d85cf20b5e0d0aa5a8dca27cce8eddef3292bc29d72dcad1641f4ed50aa16", size = 1306310 }, 53 | { url = "https://files.pythonhosted.org/packages/e4/a1/25a7633a5a513278a9892e333501e2e69c83e50be4b57a62285fb7a008c3/aiohttp-3.10.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b00807e2605f16e1e198f33a53ce3c4523114059b0c09c337209ae55e3823a8", size = 1260255 }, 54 | { url = "https://files.pythonhosted.org/packages/f2/39/30eafe89e0e2a06c25e4762844c8214c0c0cd0fd9ffc3471694a7986f421/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f2d4324a98062be0525d16f768a03e0bbb3b9fe301ceee99611dc9a7953124e6", size = 1271141 }, 55 | { url = "https://files.pythonhosted.org/packages/5b/fc/33125df728b48391ef1fcb512dfb02072158cc10d041414fb79803463020/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:438cd072f75bb6612f2aca29f8bd7cdf6e35e8f160bc312e49fbecab77c99e3a", size = 1280244 }, 56 | { url = "https://files.pythonhosted.org/packages/3b/61/e42bf2c2934b5caa4e2ec0b5e5fd86989adb022b5ee60c2572a9d77cf6fe/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:baa42524a82f75303f714108fea528ccacf0386af429b69fff141ffef1c534f9", size = 1316805 }, 57 | { url = "https://files.pythonhosted.org/packages/18/32/f52a5e2ae9ad3bba10e026a63a7a23abfa37c7d97aeeb9004eaa98df3ce3/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a7d8d14fe962153fc681f6366bdec33d4356f98a3e3567782aac1b6e0e40109a", size = 1343930 }, 58 | { url = "https://files.pythonhosted.org/packages/05/be/6a403b464dcab3631fe8e27b0f1d906d9e45c5e92aca97ee007e5a895560/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1277cd707c465cd09572a774559a3cc7c7a28802eb3a2a9472588f062097205", size = 1306186 }, 59 | { url = "https://files.pythonhosted.org/packages/8e/fd/bb50fe781068a736a02bf5c7ad5f3ab53e39f1d1e63110da6d30f7605edc/aiohttp-3.10.10-cp312-cp312-win32.whl", hash = "sha256:59bb3c54aa420521dc4ce3cc2c3fe2ad82adf7b09403fa1f48ae45c0cbde6628", size = 359289 }, 60 | { url = "https://files.pythonhosted.org/packages/70/9e/5add7e240f77ef67c275c82cc1d08afbca57b77593118c1f6e920ae8ad3f/aiohttp-3.10.10-cp312-cp312-win_amd64.whl", hash = "sha256:0e1b370d8007c4ae31ee6db7f9a2fe801a42b146cec80a86766e7ad5c4a259cf", size = 379313 }, 61 | { url = "https://files.pythonhosted.org/packages/b1/eb/618b1b76c7fe8082a71c9d62e3fe84c5b9af6703078caa9ec57850a12080/aiohttp-3.10.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ad7593bb24b2ab09e65e8a1d385606f0f47c65b5a2ae6c551db67d6653e78c28", size = 576114 }, 62 | { url = "https://files.pythonhosted.org/packages/aa/37/3126995d7869f8b30d05381b81a2d4fb4ec6ad313db788e009bc6d39c211/aiohttp-3.10.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1eb89d3d29adaf533588f209768a9c02e44e4baf832b08118749c5fad191781d", size = 391901 }, 63 | { url = "https://files.pythonhosted.org/packages/3e/f2/8fdfc845be1f811c31ceb797968523813f8e1263ee3e9120d61253f6848f/aiohttp-3.10.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3fe407bf93533a6fa82dece0e74dbcaaf5d684e5a51862887f9eaebe6372cd79", size = 387418 }, 64 | { url = "https://files.pythonhosted.org/packages/60/d5/33d2061d36bf07e80286e04b7e0a4de37ce04b5ebfed72dba67659a05250/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aed5155f819873d23520919e16703fc8925e509abbb1a1491b0087d1cd969e", size = 1287073 }, 65 | { url = "https://files.pythonhosted.org/packages/00/52/affb55be16a4747740bd630b4c002dac6c5eac42f9bb64202fc3cf3f1930/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f05e9727ce409358baa615dbeb9b969db94324a79b5a5cea45d39bdb01d82e6", size = 1323612 }, 66 | { url = "https://files.pythonhosted.org/packages/94/f2/cddb69b975387daa2182a8442566971d6410b8a0179bb4540d81c97b1611/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dffb610a30d643983aeb185ce134f97f290f8935f0abccdd32c77bed9388b42", size = 1368406 }, 67 | { url = "https://files.pythonhosted.org/packages/c1/e4/afba7327da4d932da8c6e29aecaf855f9d52dace53ac15bfc8030a246f1b/aiohttp-3.10.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa6658732517ddabe22c9036479eabce6036655ba87a0224c612e1ae6af2087e", size = 1282761 }, 68 | { url = "https://files.pythonhosted.org/packages/9f/6b/364856faa0c9031ea76e24ef0f7fef79cddd9fa8e7dba9a1771c6acc56b5/aiohttp-3.10.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:741a46d58677d8c733175d7e5aa618d277cd9d880301a380fd296975a9cdd7bc", size = 1236518 }, 69 | { url = "https://files.pythonhosted.org/packages/46/af/c382846f8356fe64a7b5908bb9b477457aa23b71be7ed551013b7b7d4d87/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e00e3505cd80440f6c98c6d69269dcc2a119f86ad0a9fd70bccc59504bebd68a", size = 1250344 }, 70 | { url = "https://files.pythonhosted.org/packages/87/53/294f87fc086fd0772d0ab82497beb9df67f0f27a8b3dd5742a2656db2bc6/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ffe595f10566f8276b76dc3a11ae4bb7eba1aac8ddd75811736a15b0d5311414", size = 1248956 }, 71 | { url = "https://files.pythonhosted.org/packages/86/30/7d746717fe11bdfefb88bb6c09c5fc985d85c4632da8bb6018e273899254/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdfcf6443637c148c4e1a20c48c566aa694fa5e288d34b20fcdc58507882fed3", size = 1293379 }, 72 | { url = "https://files.pythonhosted.org/packages/48/b9/45d670a834458db67a24258e9139ba61fa3bd7d69b98ecf3650c22806f8f/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d183cf9c797a5291e8301790ed6d053480ed94070637bfaad914dd38b0981f67", size = 1320108 }, 73 | { url = "https://files.pythonhosted.org/packages/72/8c/804bb2e837a175635d2000a0659eafc15b2e9d92d3d81c8f69e141ecd0b0/aiohttp-3.10.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:77abf6665ae54000b98b3c742bc6ea1d1fb31c394bcabf8b5d2c1ac3ebfe7f3b", size = 1281546 }, 74 | { url = "https://files.pythonhosted.org/packages/89/c0/862e6a9de3d6eeb126cd9d9ea388243b70df9b871ce1a42b193b7a4a77fc/aiohttp-3.10.10-cp313-cp313-win32.whl", hash = "sha256:4470c73c12cd9109db8277287d11f9dd98f77fc54155fc71a7738a83ffcc8ea8", size = 357516 }, 75 | { url = "https://files.pythonhosted.org/packages/ae/63/3e1aee3e554263f3f1011cca50d78a4894ae16ce99bf78101ac3a2f0ef74/aiohttp-3.10.10-cp313-cp313-win_amd64.whl", hash = "sha256:486f7aabfa292719a2753c016cc3a8f8172965cabb3ea2e7f7436c7f5a22a151", size = 376785 }, 76 | ] 77 | 78 | [[package]] 79 | name = "aiosignal" 80 | version = "1.3.1" 81 | source = { registry = "https://pypi.org/simple" } 82 | dependencies = [ 83 | { name = "frozenlist" }, 84 | ] 85 | sdist = { url = "https://files.pythonhosted.org/packages/ae/67/0952ed97a9793b4958e5736f6d2b346b414a2cd63e82d05940032f45b32f/aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc", size = 19422 } 86 | wheels = [ 87 | { url = "https://files.pythonhosted.org/packages/76/ac/a7305707cb852b7e16ff80eaf5692309bde30e2b1100a1fcacdc8f731d97/aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17", size = 7617 }, 88 | ] 89 | 90 | [[package]] 91 | name = "annotated-types" 92 | version = "0.7.0" 93 | source = { registry = "https://pypi.org/simple" } 94 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 95 | wheels = [ 96 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 97 | ] 98 | 99 | [[package]] 100 | name = "attrs" 101 | version = "24.2.0" 102 | source = { registry = "https://pypi.org/simple" } 103 | sdist = { url = "https://files.pythonhosted.org/packages/fc/0f/aafca9af9315aee06a89ffde799a10a582fe8de76c563ee80bbcdc08b3fb/attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346", size = 792678 } 104 | wheels = [ 105 | { url = "https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2", size = 63001 }, 106 | ] 107 | 108 | [[package]] 109 | name = "bot" 110 | version = "1.0.0" 111 | source = { virtual = "." } 112 | dependencies = [ 113 | { name = "discord-py" }, 114 | { name = "motor" }, 115 | { name = "pydantic" }, 116 | { name = "pydantic-settings" }, 117 | { name = "pyyaml" }, 118 | ] 119 | 120 | [package.metadata] 121 | requires-dist = [ 122 | { name = "discord-py", specifier = ">=2.4.0" }, 123 | { name = "motor", specifier = ">=3.6.0" }, 124 | { name = "pydantic", specifier = ">=2.9.2" }, 125 | { name = "pydantic-settings", specifier = ">=2.5.2" }, 126 | { name = "pyyaml", specifier = ">=6.0.2" }, 127 | ] 128 | 129 | [[package]] 130 | name = "discord-py" 131 | version = "2.4.0" 132 | source = { registry = "https://pypi.org/simple" } 133 | dependencies = [ 134 | { name = "aiohttp" }, 135 | ] 136 | sdist = { url = "https://files.pythonhosted.org/packages/39/af/80cab4015722d3bee175509b7249a11d5adf77b5ff4c27f268558079d149/discord_py-2.4.0.tar.gz", hash = "sha256:d07cb2a223a185873a1d0ee78b9faa9597e45b3f6186df21a95cec1e9bcdc9a5", size = 1027707 } 137 | wheels = [ 138 | { url = "https://files.pythonhosted.org/packages/23/10/3c44e9331a5ec3bae8b2919d51f611a5b94e179563b1b89eb6423a8f43eb/discord.py-2.4.0-py3-none-any.whl", hash = "sha256:b8af6711c70f7e62160bfbecb55be699b5cb69d007426759ab8ab06b1bd77d1d", size = 1125988 }, 139 | ] 140 | 141 | [[package]] 142 | name = "dnspython" 143 | version = "2.7.0" 144 | source = { registry = "https://pypi.org/simple" } 145 | sdist = { url = "https://files.pythonhosted.org/packages/b5/4a/263763cb2ba3816dd94b08ad3a33d5fdae34ecb856678773cc40a3605829/dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1", size = 345197 } 146 | wheels = [ 147 | { url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632 }, 148 | ] 149 | 150 | [[package]] 151 | name = "frozenlist" 152 | version = "1.5.0" 153 | source = { registry = "https://pypi.org/simple" } 154 | sdist = { url = "https://files.pythonhosted.org/packages/8f/ed/0f4cec13a93c02c47ec32d81d11c0c1efbadf4a471e3f3ce7cad366cbbd3/frozenlist-1.5.0.tar.gz", hash = "sha256:81d5af29e61b9c8348e876d442253723928dce6433e0e76cd925cd83f1b4b817", size = 39930 } 155 | wheels = [ 156 | { url = "https://files.pythonhosted.org/packages/79/43/0bed28bf5eb1c9e4301003b74453b8e7aa85fb293b31dde352aac528dafc/frozenlist-1.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fd74520371c3c4175142d02a976aee0b4cb4a7cc912a60586ffd8d5929979b30", size = 94987 }, 157 | { url = "https://files.pythonhosted.org/packages/bb/bf/b74e38f09a246e8abbe1e90eb65787ed745ccab6eaa58b9c9308e052323d/frozenlist-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2f3f7a0fbc219fb4455264cae4d9f01ad41ae6ee8524500f381de64ffaa077d5", size = 54584 }, 158 | { url = "https://files.pythonhosted.org/packages/2c/31/ab01375682f14f7613a1ade30149f684c84f9b8823a4391ed950c8285656/frozenlist-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f47c9c9028f55a04ac254346e92977bf0f166c483c74b4232bee19a6697e4778", size = 52499 }, 159 | { url = "https://files.pythonhosted.org/packages/98/a8/d0ac0b9276e1404f58fec3ab6e90a4f76b778a49373ccaf6a563f100dfbc/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0996c66760924da6e88922756d99b47512a71cfd45215f3570bf1e0b694c206a", size = 276357 }, 160 | { url = "https://files.pythonhosted.org/packages/ad/c9/c7761084fa822f07dac38ac29f841d4587570dd211e2262544aa0b791d21/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2fe128eb4edeabe11896cb6af88fca5346059f6c8d807e3b910069f39157869", size = 287516 }, 161 | { url = "https://files.pythonhosted.org/packages/a1/ff/cd7479e703c39df7bdab431798cef89dc75010d8aa0ca2514c5b9321db27/frozenlist-1.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a8ea951bbb6cacd492e3948b8da8c502a3f814f5d20935aae74b5df2b19cf3d", size = 283131 }, 162 | { url = "https://files.pythonhosted.org/packages/59/a0/370941beb47d237eca4fbf27e4e91389fd68699e6f4b0ebcc95da463835b/frozenlist-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de537c11e4aa01d37db0d403b57bd6f0546e71a82347a97c6a9f0dcc532b3a45", size = 261320 }, 163 | { url = "https://files.pythonhosted.org/packages/b8/5f/c10123e8d64867bc9b4f2f510a32042a306ff5fcd7e2e09e5ae5100ee333/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", size = 274877 }, 164 | { url = "https://files.pythonhosted.org/packages/fa/79/38c505601ae29d4348f21706c5d89755ceded02a745016ba2f58bd5f1ea6/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cee6798eaf8b1416ef6909b06f7dc04b60755206bddc599f52232606e18179d3", size = 269592 }, 165 | { url = "https://files.pythonhosted.org/packages/19/e2/39f3a53191b8204ba9f0bb574b926b73dd2efba2a2b9d2d730517e8f7622/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f5f9da7f5dbc00a604fe74aa02ae7c98bcede8a3b8b9666f9f86fc13993bc71a", size = 265934 }, 166 | { url = "https://files.pythonhosted.org/packages/d5/c9/3075eb7f7f3a91f1a6b00284af4de0a65a9ae47084930916f5528144c9dd/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:90646abbc7a5d5c7c19461d2e3eeb76eb0b204919e6ece342feb6032c9325ae9", size = 283859 }, 167 | { url = "https://files.pythonhosted.org/packages/05/f5/549f44d314c29408b962fa2b0e69a1a67c59379fb143b92a0a065ffd1f0f/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bdac3c7d9b705d253b2ce370fde941836a5f8b3c5c2b8fd70940a3ea3af7f4f2", size = 287560 }, 168 | { url = "https://files.pythonhosted.org/packages/9d/f8/cb09b3c24a3eac02c4c07a9558e11e9e244fb02bf62c85ac2106d1eb0c0b/frozenlist-1.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03d33c2ddbc1816237a67f66336616416e2bbb6beb306e5f890f2eb22b959cdf", size = 277150 }, 169 | { url = "https://files.pythonhosted.org/packages/37/48/38c2db3f54d1501e692d6fe058f45b6ad1b358d82cd19436efab80cfc965/frozenlist-1.5.0-cp311-cp311-win32.whl", hash = "sha256:237f6b23ee0f44066219dae14c70ae38a63f0440ce6750f868ee08775073f942", size = 45244 }, 170 | { url = "https://files.pythonhosted.org/packages/ca/8c/2ddffeb8b60a4bce3b196c32fcc30d8830d4615e7b492ec2071da801b8ad/frozenlist-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:0cc974cc93d32c42e7b0f6cf242a6bd941c57c61b618e78b6c0a96cb72788c1d", size = 51634 }, 171 | { url = "https://files.pythonhosted.org/packages/79/73/fa6d1a96ab7fd6e6d1c3500700963eab46813847f01ef0ccbaa726181dd5/frozenlist-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:31115ba75889723431aa9a4e77d5f398f5cf976eea3bdf61749731f62d4a4a21", size = 94026 }, 172 | { url = "https://files.pythonhosted.org/packages/ab/04/ea8bf62c8868b8eada363f20ff1b647cf2e93377a7b284d36062d21d81d1/frozenlist-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7437601c4d89d070eac8323f121fcf25f88674627505334654fd027b091db09d", size = 54150 }, 173 | { url = "https://files.pythonhosted.org/packages/d0/9a/8e479b482a6f2070b26bda572c5e6889bb3ba48977e81beea35b5ae13ece/frozenlist-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7948140d9f8ece1745be806f2bfdf390127cf1a763b925c4a805c603df5e697e", size = 51927 }, 174 | { url = "https://files.pythonhosted.org/packages/e3/12/2aad87deb08a4e7ccfb33600871bbe8f0e08cb6d8224371387f3303654d7/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:feeb64bc9bcc6b45c6311c9e9b99406660a9c05ca8a5b30d14a78555088b0b3a", size = 282647 }, 175 | { url = "https://files.pythonhosted.org/packages/77/f2/07f06b05d8a427ea0060a9cef6e63405ea9e0d761846b95ef3fb3be57111/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:683173d371daad49cffb8309779e886e59c2f369430ad28fe715f66d08d4ab1a", size = 289052 }, 176 | { url = "https://files.pythonhosted.org/packages/bd/9f/8bf45a2f1cd4aa401acd271b077989c9267ae8463e7c8b1eb0d3f561b65e/frozenlist-1.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d57d8f702221405a9d9b40f9da8ac2e4a1a8b5285aac6100f3393675f0a85ee", size = 291719 }, 177 | { url = "https://files.pythonhosted.org/packages/41/d1/1f20fd05a6c42d3868709b7604c9f15538a29e4f734c694c6bcfc3d3b935/frozenlist-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c72000fbcc35b129cb09956836c7d7abf78ab5416595e4857d1cae8d6251a6", size = 267433 }, 178 | { url = "https://files.pythonhosted.org/packages/af/f2/64b73a9bb86f5a89fb55450e97cd5c1f84a862d4ff90d9fd1a73ab0f64a5/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", size = 283591 }, 179 | { url = "https://files.pythonhosted.org/packages/29/e2/ffbb1fae55a791fd6c2938dd9ea779509c977435ba3940b9f2e8dc9d5316/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d7f5a50342475962eb18b740f3beecc685a15b52c91f7d975257e13e029eca9", size = 273249 }, 180 | { url = "https://files.pythonhosted.org/packages/2e/6e/008136a30798bb63618a114b9321b5971172a5abddff44a100c7edc5ad4f/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:87f724d055eb4785d9be84e9ebf0f24e392ddfad00b3fe036e43f489fafc9039", size = 271075 }, 181 | { url = "https://files.pythonhosted.org/packages/ae/f0/4e71e54a026b06724cec9b6c54f0b13a4e9e298cc8db0f82ec70e151f5ce/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6e9080bb2fb195a046e5177f10d9d82b8a204c0736a97a153c2466127de87784", size = 285398 }, 182 | { url = "https://files.pythonhosted.org/packages/4d/36/70ec246851478b1c0b59f11ef8ade9c482ff447c1363c2bd5fad45098b12/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b93d7aaa36c966fa42efcaf716e6b3900438632a626fb09c049f6a2f09fc631", size = 294445 }, 183 | { url = "https://files.pythonhosted.org/packages/37/e0/47f87544055b3349b633a03c4d94b405956cf2437f4ab46d0928b74b7526/frozenlist-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:52ef692a4bc60a6dd57f507429636c2af8b6046db8b31b18dac02cbc8f507f7f", size = 280569 }, 184 | { url = "https://files.pythonhosted.org/packages/f9/7c/490133c160fb6b84ed374c266f42800e33b50c3bbab1652764e6e1fc498a/frozenlist-1.5.0-cp312-cp312-win32.whl", hash = "sha256:29d94c256679247b33a3dc96cce0f93cbc69c23bf75ff715919332fdbb6a32b8", size = 44721 }, 185 | { url = "https://files.pythonhosted.org/packages/b1/56/4e45136ffc6bdbfa68c29ca56ef53783ef4c2fd395f7cbf99a2624aa9aaa/frozenlist-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:8969190d709e7c48ea386db202d708eb94bdb29207a1f269bab1196ce0dcca1f", size = 51329 }, 186 | { url = "https://files.pythonhosted.org/packages/da/3b/915f0bca8a7ea04483622e84a9bd90033bab54bdf485479556c74fd5eaf5/frozenlist-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a1a048f9215c90973402e26c01d1cff8a209e1f1b53f72b95c13db61b00f953", size = 91538 }, 187 | { url = "https://files.pythonhosted.org/packages/c7/d1/a7c98aad7e44afe5306a2b068434a5830f1470675f0e715abb86eb15f15b/frozenlist-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dd47a5181ce5fcb463b5d9e17ecfdb02b678cca31280639255ce9d0e5aa67af0", size = 52849 }, 188 | { url = "https://files.pythonhosted.org/packages/3a/c8/76f23bf9ab15d5f760eb48701909645f686f9c64fbb8982674c241fbef14/frozenlist-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1431d60b36d15cda188ea222033eec8e0eab488f39a272461f2e6d9e1a8e63c2", size = 50583 }, 189 | { url = "https://files.pythonhosted.org/packages/1f/22/462a3dd093d11df623179d7754a3b3269de3b42de2808cddef50ee0f4f48/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6482a5851f5d72767fbd0e507e80737f9c8646ae7fd303def99bfe813f76cf7f", size = 265636 }, 190 | { url = "https://files.pythonhosted.org/packages/80/cf/e075e407fc2ae7328155a1cd7e22f932773c8073c1fc78016607d19cc3e5/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44c49271a937625619e862baacbd037a7ef86dd1ee215afc298a417ff3270608", size = 270214 }, 191 | { url = "https://files.pythonhosted.org/packages/a1/58/0642d061d5de779f39c50cbb00df49682832923f3d2ebfb0fedf02d05f7f/frozenlist-1.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:12f78f98c2f1c2429d42e6a485f433722b0061d5c0b0139efa64f396efb5886b", size = 273905 }, 192 | { url = "https://files.pythonhosted.org/packages/ab/66/3fe0f5f8f2add5b4ab7aa4e199f767fd3b55da26e3ca4ce2cc36698e50c4/frozenlist-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ce3aa154c452d2467487765e3adc730a8c153af77ad84096bc19ce19a2400840", size = 250542 }, 193 | { url = "https://files.pythonhosted.org/packages/f6/b8/260791bde9198c87a465224e0e2bb62c4e716f5d198fc3a1dacc4895dbd1/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", size = 267026 }, 194 | { url = "https://files.pythonhosted.org/packages/2e/a4/3d24f88c527f08f8d44ade24eaee83b2627793fa62fa07cbb7ff7a2f7d42/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45e0896250900b5aa25180f9aec243e84e92ac84bd4a74d9ad4138ef3f5c97de", size = 257690 }, 195 | { url = "https://files.pythonhosted.org/packages/de/9a/d311d660420b2beeff3459b6626f2ab4fb236d07afbdac034a4371fe696e/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:561eb1c9579d495fddb6da8959fd2a1fca2c6d060d4113f5844b433fc02f2641", size = 253893 }, 196 | { url = "https://files.pythonhosted.org/packages/c6/23/e491aadc25b56eabd0f18c53bb19f3cdc6de30b2129ee0bc39cd387cd560/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:df6e2f325bfee1f49f81aaac97d2aa757c7646534a06f8f577ce184afe2f0a9e", size = 267006 }, 197 | { url = "https://files.pythonhosted.org/packages/08/c4/ab918ce636a35fb974d13d666dcbe03969592aeca6c3ab3835acff01f79c/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:140228863501b44b809fb39ec56b5d4071f4d0aa6d216c19cbb08b8c5a7eadb9", size = 276157 }, 198 | { url = "https://files.pythonhosted.org/packages/c0/29/3b7a0bbbbe5a34833ba26f686aabfe982924adbdcafdc294a7a129c31688/frozenlist-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7707a25d6a77f5d27ea7dc7d1fc608aa0a478193823f88511ef5e6b8a48f9d03", size = 264642 }, 199 | { url = "https://files.pythonhosted.org/packages/ab/42/0595b3dbffc2e82d7fe658c12d5a5bafcd7516c6bf2d1d1feb5387caa9c1/frozenlist-1.5.0-cp313-cp313-win32.whl", hash = "sha256:31a9ac2b38ab9b5a8933b693db4939764ad3f299fcaa931a3e605bc3460e693c", size = 44914 }, 200 | { url = "https://files.pythonhosted.org/packages/17/c4/b7db1206a3fea44bf3b838ca61deb6f74424a8a5db1dd53ecb21da669be6/frozenlist-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:11aabdd62b8b9c4b84081a3c246506d1cddd2dd93ff0ad53ede5defec7886b28", size = 51167 }, 201 | { url = "https://files.pythonhosted.org/packages/c6/c8/a5be5b7550c10858fcf9b0ea054baccab474da77d37f1e828ce043a3a5d4/frozenlist-1.5.0-py3-none-any.whl", hash = "sha256:d994863bba198a4a518b467bb971c56e1db3f180a25c6cf7bb1949c267f748c3", size = 11901 }, 202 | ] 203 | 204 | [[package]] 205 | name = "idna" 206 | version = "3.10" 207 | source = { registry = "https://pypi.org/simple" } 208 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 209 | wheels = [ 210 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 211 | ] 212 | 213 | [[package]] 214 | name = "motor" 215 | version = "3.6.0" 216 | source = { registry = "https://pypi.org/simple" } 217 | dependencies = [ 218 | { name = "pymongo" }, 219 | ] 220 | sdist = { url = "https://files.pythonhosted.org/packages/6a/d1/06af0527fd02d49b203db70dba462e47275a3c1094f830fdaf090f0cb20c/motor-3.6.0.tar.gz", hash = "sha256:0ef7f520213e852bf0eac306adf631aabe849227d8aec900a2612512fb9c5b8d", size = 278447 } 221 | wheels = [ 222 | { url = "https://files.pythonhosted.org/packages/b4/c2/bba4dce0dc56e49d95c270c79c9330ed19e6b71a2a633aecf53e7e1f04c9/motor-3.6.0-py3-none-any.whl", hash = "sha256:9f07ed96f1754963d4386944e1b52d403a5350c687edc60da487d66f98dbf894", size = 74802 }, 223 | ] 224 | 225 | [[package]] 226 | name = "multidict" 227 | version = "6.1.0" 228 | source = { registry = "https://pypi.org/simple" } 229 | sdist = { url = "https://files.pythonhosted.org/packages/d6/be/504b89a5e9ca731cd47487e91c469064f8ae5af93b7259758dcfc2b9c848/multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", size = 64002 } 230 | wheels = [ 231 | { url = "https://files.pythonhosted.org/packages/93/13/df3505a46d0cd08428e4c8169a196131d1b0c4b515c3649829258843dde6/multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6", size = 48570 }, 232 | { url = "https://files.pythonhosted.org/packages/f0/e1/a215908bfae1343cdb72f805366592bdd60487b4232d039c437fe8f5013d/multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156", size = 29316 }, 233 | { url = "https://files.pythonhosted.org/packages/70/0f/6dc70ddf5d442702ed74f298d69977f904960b82368532c88e854b79f72b/multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb", size = 29640 }, 234 | { url = "https://files.pythonhosted.org/packages/d8/6d/9c87b73a13d1cdea30b321ef4b3824449866bd7f7127eceed066ccb9b9ff/multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b", size = 131067 }, 235 | { url = "https://files.pythonhosted.org/packages/cc/1e/1b34154fef373371fd6c65125b3d42ff5f56c7ccc6bfff91b9b3c60ae9e0/multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72", size = 138507 }, 236 | { url = "https://files.pythonhosted.org/packages/fb/e0/0bc6b2bac6e461822b5f575eae85da6aae76d0e2a79b6665d6206b8e2e48/multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304", size = 133905 }, 237 | { url = "https://files.pythonhosted.org/packages/ba/af/73d13b918071ff9b2205fcf773d316e0f8fefb4ec65354bbcf0b10908cc6/multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351", size = 129004 }, 238 | { url = "https://files.pythonhosted.org/packages/74/21/23960627b00ed39643302d81bcda44c9444ebcdc04ee5bedd0757513f259/multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb", size = 121308 }, 239 | { url = "https://files.pythonhosted.org/packages/8b/5c/cf282263ffce4a596ed0bb2aa1a1dddfe1996d6a62d08842a8d4b33dca13/multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3", size = 132608 }, 240 | { url = "https://files.pythonhosted.org/packages/d7/3e/97e778c041c72063f42b290888daff008d3ab1427f5b09b714f5a8eff294/multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399", size = 127029 }, 241 | { url = "https://files.pythonhosted.org/packages/47/ac/3efb7bfe2f3aefcf8d103e9a7162572f01936155ab2f7ebcc7c255a23212/multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423", size = 137594 }, 242 | { url = "https://files.pythonhosted.org/packages/42/9b/6c6e9e8dc4f915fc90a9b7798c44a30773dea2995fdcb619870e705afe2b/multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3", size = 134556 }, 243 | { url = "https://files.pythonhosted.org/packages/1d/10/8e881743b26aaf718379a14ac58572a240e8293a1c9d68e1418fb11c0f90/multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753", size = 130993 }, 244 | { url = "https://files.pythonhosted.org/packages/45/84/3eb91b4b557442802d058a7579e864b329968c8d0ea57d907e7023c677f2/multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80", size = 26405 }, 245 | { url = "https://files.pythonhosted.org/packages/9f/0b/ad879847ecbf6d27e90a6eabb7eff6b62c129eefe617ea45eae7c1f0aead/multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926", size = 28795 }, 246 | { url = "https://files.pythonhosted.org/packages/fd/16/92057c74ba3b96d5e211b553895cd6dc7cc4d1e43d9ab8fafc727681ef71/multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", size = 48713 }, 247 | { url = "https://files.pythonhosted.org/packages/94/3d/37d1b8893ae79716179540b89fc6a0ee56b4a65fcc0d63535c6f5d96f217/multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", size = 29516 }, 248 | { url = "https://files.pythonhosted.org/packages/a2/12/adb6b3200c363062f805275b4c1e656be2b3681aada66c80129932ff0bae/multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", size = 29557 }, 249 | { url = "https://files.pythonhosted.org/packages/47/e9/604bb05e6e5bce1e6a5cf80a474e0f072e80d8ac105f1b994a53e0b28c42/multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", size = 130170 }, 250 | { url = "https://files.pythonhosted.org/packages/7e/13/9efa50801785eccbf7086b3c83b71a4fb501a4d43549c2f2f80b8787d69f/multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", size = 134836 }, 251 | { url = "https://files.pythonhosted.org/packages/bf/0f/93808b765192780d117814a6dfcc2e75de6dcc610009ad408b8814dca3ba/multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", size = 133475 }, 252 | { url = "https://files.pythonhosted.org/packages/d3/c8/529101d7176fe7dfe1d99604e48d69c5dfdcadb4f06561f465c8ef12b4df/multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", size = 131049 }, 253 | { url = "https://files.pythonhosted.org/packages/ca/0c/fc85b439014d5a58063e19c3a158a889deec399d47b5269a0f3b6a2e28bc/multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", size = 120370 }, 254 | { url = "https://files.pythonhosted.org/packages/db/46/d4416eb20176492d2258fbd47b4abe729ff3b6e9c829ea4236f93c865089/multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", size = 125178 }, 255 | { url = "https://files.pythonhosted.org/packages/5b/46/73697ad7ec521df7de5531a32780bbfd908ded0643cbe457f981a701457c/multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", size = 119567 }, 256 | { url = "https://files.pythonhosted.org/packages/cd/ed/51f060e2cb0e7635329fa6ff930aa5cffa17f4c7f5c6c3ddc3500708e2f2/multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", size = 129822 }, 257 | { url = "https://files.pythonhosted.org/packages/df/9e/ee7d1954b1331da3eddea0c4e08d9142da5f14b1321c7301f5014f49d492/multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", size = 128656 }, 258 | { url = "https://files.pythonhosted.org/packages/77/00/8538f11e3356b5d95fa4b024aa566cde7a38aa7a5f08f4912b32a037c5dc/multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", size = 125360 }, 259 | { url = "https://files.pythonhosted.org/packages/be/05/5d334c1f2462d43fec2363cd00b1c44c93a78c3925d952e9a71caf662e96/multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", size = 26382 }, 260 | { url = "https://files.pythonhosted.org/packages/a3/bf/f332a13486b1ed0496d624bcc7e8357bb8053823e8cd4b9a18edc1d97e73/multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", size = 28529 }, 261 | { url = "https://files.pythonhosted.org/packages/22/67/1c7c0f39fe069aa4e5d794f323be24bf4d33d62d2a348acdb7991f8f30db/multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", size = 48771 }, 262 | { url = "https://files.pythonhosted.org/packages/3c/25/c186ee7b212bdf0df2519eacfb1981a017bda34392c67542c274651daf23/multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", size = 29533 }, 263 | { url = "https://files.pythonhosted.org/packages/67/5e/04575fd837e0958e324ca035b339cea174554f6f641d3fb2b4f2e7ff44a2/multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", size = 29595 }, 264 | { url = "https://files.pythonhosted.org/packages/d3/b2/e56388f86663810c07cfe4a3c3d87227f3811eeb2d08450b9e5d19d78876/multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", size = 130094 }, 265 | { url = "https://files.pythonhosted.org/packages/6c/ee/30ae9b4186a644d284543d55d491fbd4239b015d36b23fea43b4c94f7052/multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", size = 134876 }, 266 | { url = "https://files.pythonhosted.org/packages/84/c7/70461c13ba8ce3c779503c70ec9d0345ae84de04521c1f45a04d5f48943d/multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", size = 133500 }, 267 | { url = "https://files.pythonhosted.org/packages/4a/9f/002af221253f10f99959561123fae676148dd730e2daa2cd053846a58507/multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", size = 131099 }, 268 | { url = "https://files.pythonhosted.org/packages/82/42/d1c7a7301d52af79d88548a97e297f9d99c961ad76bbe6f67442bb77f097/multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", size = 120403 }, 269 | { url = "https://files.pythonhosted.org/packages/68/f3/471985c2c7ac707547553e8f37cff5158030d36bdec4414cb825fbaa5327/multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", size = 125348 }, 270 | { url = "https://files.pythonhosted.org/packages/67/2c/e6df05c77e0e433c214ec1d21ddd203d9a4770a1f2866a8ca40a545869a0/multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", size = 119673 }, 271 | { url = "https://files.pythonhosted.org/packages/c5/cd/bc8608fff06239c9fb333f9db7743a1b2eafe98c2666c9a196e867a3a0a4/multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", size = 129927 }, 272 | { url = "https://files.pythonhosted.org/packages/44/8e/281b69b7bc84fc963a44dc6e0bbcc7150e517b91df368a27834299a526ac/multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", size = 128711 }, 273 | { url = "https://files.pythonhosted.org/packages/12/a4/63e7cd38ed29dd9f1881d5119f272c898ca92536cdb53ffe0843197f6c85/multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", size = 125519 }, 274 | { url = "https://files.pythonhosted.org/packages/38/e0/4f5855037a72cd8a7a2f60a3952d9aa45feedb37ae7831642102604e8a37/multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", size = 26426 }, 275 | { url = "https://files.pythonhosted.org/packages/7e/a5/17ee3a4db1e310b7405f5d25834460073a8ccd86198ce044dfaf69eac073/multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", size = 28531 }, 276 | { url = "https://files.pythonhosted.org/packages/99/b7/b9e70fde2c0f0c9af4cc5277782a89b66d35948ea3369ec9f598358c3ac5/multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", size = 10051 }, 277 | ] 278 | 279 | [[package]] 280 | name = "propcache" 281 | version = "0.2.0" 282 | source = { registry = "https://pypi.org/simple" } 283 | sdist = { url = "https://files.pythonhosted.org/packages/a9/4d/5e5a60b78dbc1d464f8a7bbaeb30957257afdc8512cbb9dfd5659304f5cd/propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70", size = 40951 } 284 | wheels = [ 285 | { url = "https://files.pythonhosted.org/packages/e0/1c/71eec730e12aec6511e702ad0cd73c2872eccb7cad39de8ba3ba9de693ef/propcache-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354", size = 80811 }, 286 | { url = "https://files.pythonhosted.org/packages/89/c3/7e94009f9a4934c48a371632197406a8860b9f08e3f7f7d922ab69e57a41/propcache-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de", size = 46365 }, 287 | { url = "https://files.pythonhosted.org/packages/c0/1d/c700d16d1d6903aeab28372fe9999762f074b80b96a0ccc953175b858743/propcache-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87", size = 45602 }, 288 | { url = "https://files.pythonhosted.org/packages/2e/5e/4a3e96380805bf742712e39a4534689f4cddf5fa2d3a93f22e9fd8001b23/propcache-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016", size = 236161 }, 289 | { url = "https://files.pythonhosted.org/packages/a5/85/90132481183d1436dff6e29f4fa81b891afb6cb89a7306f32ac500a25932/propcache-0.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb", size = 244938 }, 290 | { url = "https://files.pythonhosted.org/packages/4a/89/c893533cb45c79c970834274e2d0f6d64383ec740be631b6a0a1d2b4ddc0/propcache-0.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2", size = 243576 }, 291 | { url = "https://files.pythonhosted.org/packages/8c/56/98c2054c8526331a05f205bf45cbb2cda4e58e56df70e76d6a509e5d6ec6/propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4", size = 236011 }, 292 | { url = "https://files.pythonhosted.org/packages/2d/0c/8b8b9f8a6e1abd869c0fa79b907228e7abb966919047d294ef5df0d136cf/propcache-0.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504", size = 224834 }, 293 | { url = "https://files.pythonhosted.org/packages/18/bb/397d05a7298b7711b90e13108db697732325cafdcd8484c894885c1bf109/propcache-0.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178", size = 224946 }, 294 | { url = "https://files.pythonhosted.org/packages/25/19/4fc08dac19297ac58135c03770b42377be211622fd0147f015f78d47cd31/propcache-0.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d", size = 217280 }, 295 | { url = "https://files.pythonhosted.org/packages/7e/76/c79276a43df2096ce2aba07ce47576832b1174c0c480fe6b04bd70120e59/propcache-0.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2", size = 220088 }, 296 | { url = "https://files.pythonhosted.org/packages/c3/9a/8a8cf428a91b1336b883f09c8b884e1734c87f724d74b917129a24fe2093/propcache-0.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db", size = 233008 }, 297 | { url = "https://files.pythonhosted.org/packages/25/7b/768a8969abd447d5f0f3333df85c6a5d94982a1bc9a89c53c154bf7a8b11/propcache-0.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b", size = 237719 }, 298 | { url = "https://files.pythonhosted.org/packages/ed/0d/e5d68ccc7976ef8b57d80613ac07bbaf0614d43f4750cf953f0168ef114f/propcache-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b", size = 227729 }, 299 | { url = "https://files.pythonhosted.org/packages/05/64/17eb2796e2d1c3d0c431dc5f40078d7282f4645af0bb4da9097fbb628c6c/propcache-0.2.0-cp311-cp311-win32.whl", hash = "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1", size = 40473 }, 300 | { url = "https://files.pythonhosted.org/packages/83/c5/e89fc428ccdc897ade08cd7605f174c69390147526627a7650fb883e0cd0/propcache-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71", size = 44921 }, 301 | { url = "https://files.pythonhosted.org/packages/7c/46/a41ca1097769fc548fc9216ec4c1471b772cc39720eb47ed7e38ef0006a9/propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2", size = 80800 }, 302 | { url = "https://files.pythonhosted.org/packages/75/4f/93df46aab9cc473498ff56be39b5f6ee1e33529223d7a4d8c0a6101a9ba2/propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7", size = 46443 }, 303 | { url = "https://files.pythonhosted.org/packages/0b/17/308acc6aee65d0f9a8375e36c4807ac6605d1f38074b1581bd4042b9fb37/propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8", size = 45676 }, 304 | { url = "https://files.pythonhosted.org/packages/65/44/626599d2854d6c1d4530b9a05e7ff2ee22b790358334b475ed7c89f7d625/propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793", size = 246191 }, 305 | { url = "https://files.pythonhosted.org/packages/f2/df/5d996d7cb18df076debae7d76ac3da085c0575a9f2be6b1f707fe227b54c/propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09", size = 251791 }, 306 | { url = "https://files.pythonhosted.org/packages/2e/6d/9f91e5dde8b1f662f6dd4dff36098ed22a1ef4e08e1316f05f4758f1576c/propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89", size = 253434 }, 307 | { url = "https://files.pythonhosted.org/packages/3c/e9/1b54b7e26f50b3e0497cd13d3483d781d284452c2c50dd2a615a92a087a3/propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e", size = 248150 }, 308 | { url = "https://files.pythonhosted.org/packages/a7/ef/a35bf191c8038fe3ce9a414b907371c81d102384eda5dbafe6f4dce0cf9b/propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9", size = 233568 }, 309 | { url = "https://files.pythonhosted.org/packages/97/d9/d00bb9277a9165a5e6d60f2142cd1a38a750045c9c12e47ae087f686d781/propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4", size = 229874 }, 310 | { url = "https://files.pythonhosted.org/packages/8e/78/c123cf22469bdc4b18efb78893e69c70a8b16de88e6160b69ca6bdd88b5d/propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c", size = 225857 }, 311 | { url = "https://files.pythonhosted.org/packages/31/1b/fd6b2f1f36d028820d35475be78859d8c89c8f091ad30e377ac49fd66359/propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887", size = 227604 }, 312 | { url = "https://files.pythonhosted.org/packages/99/36/b07be976edf77a07233ba712e53262937625af02154353171716894a86a6/propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57", size = 238430 }, 313 | { url = "https://files.pythonhosted.org/packages/0d/64/5822f496c9010e3966e934a011ac08cac8734561842bc7c1f65586e0683c/propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23", size = 244814 }, 314 | { url = "https://files.pythonhosted.org/packages/fd/bd/8657918a35d50b18a9e4d78a5df7b6c82a637a311ab20851eef4326305c1/propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348", size = 235922 }, 315 | { url = "https://files.pythonhosted.org/packages/a8/6f/ec0095e1647b4727db945213a9f395b1103c442ef65e54c62e92a72a3f75/propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5", size = 40177 }, 316 | { url = "https://files.pythonhosted.org/packages/20/a2/bd0896fdc4f4c1db46d9bc361c8c79a9bf08ccc08ba054a98e38e7ba1557/propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3", size = 44446 }, 317 | { url = "https://files.pythonhosted.org/packages/a8/a7/5f37b69197d4f558bfef5b4bceaff7c43cc9b51adf5bd75e9081d7ea80e4/propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7", size = 78120 }, 318 | { url = "https://files.pythonhosted.org/packages/c8/cd/48ab2b30a6b353ecb95a244915f85756d74f815862eb2ecc7a518d565b48/propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763", size = 45127 }, 319 | { url = "https://files.pythonhosted.org/packages/a5/ba/0a1ef94a3412aab057bd996ed5f0ac7458be5bf469e85c70fa9ceb43290b/propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d", size = 44419 }, 320 | { url = "https://files.pythonhosted.org/packages/b4/6c/ca70bee4f22fa99eacd04f4d2f1699be9d13538ccf22b3169a61c60a27fa/propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a", size = 229611 }, 321 | { url = "https://files.pythonhosted.org/packages/19/70/47b872a263e8511ca33718d96a10c17d3c853aefadeb86dc26e8421184b9/propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b", size = 234005 }, 322 | { url = "https://files.pythonhosted.org/packages/4f/be/3b0ab8c84a22e4a3224719099c1229ddfdd8a6a1558cf75cb55ee1e35c25/propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb", size = 237270 }, 323 | { url = "https://files.pythonhosted.org/packages/04/d8/f071bb000d4b8f851d312c3c75701e586b3f643fe14a2e3409b1b9ab3936/propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf", size = 231877 }, 324 | { url = "https://files.pythonhosted.org/packages/93/e7/57a035a1359e542bbb0a7df95aad6b9871ebee6dce2840cb157a415bd1f3/propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2", size = 217848 }, 325 | { url = "https://files.pythonhosted.org/packages/f0/93/d1dea40f112ec183398fb6c42fde340edd7bab202411c4aa1a8289f461b6/propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f", size = 216987 }, 326 | { url = "https://files.pythonhosted.org/packages/62/4c/877340871251145d3522c2b5d25c16a1690ad655fbab7bb9ece6b117e39f/propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136", size = 212451 }, 327 | { url = "https://files.pythonhosted.org/packages/7c/bb/a91b72efeeb42906ef58ccf0cdb87947b54d7475fee3c93425d732f16a61/propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325", size = 212879 }, 328 | { url = "https://files.pythonhosted.org/packages/9b/7f/ee7fea8faac57b3ec5d91ff47470c6c5d40d7f15d0b1fccac806348fa59e/propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44", size = 222288 }, 329 | { url = "https://files.pythonhosted.org/packages/ff/d7/acd67901c43d2e6b20a7a973d9d5fd543c6e277af29b1eb0e1f7bd7ca7d2/propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83", size = 228257 }, 330 | { url = "https://files.pythonhosted.org/packages/8d/6f/6272ecc7a8daad1d0754cfc6c8846076a8cb13f810005c79b15ce0ef0cf2/propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544", size = 221075 }, 331 | { url = "https://files.pythonhosted.org/packages/7c/bd/c7a6a719a6b3dd8b3aeadb3675b5783983529e4a3185946aa444d3e078f6/propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032", size = 39654 }, 332 | { url = "https://files.pythonhosted.org/packages/88/e7/0eef39eff84fa3e001b44de0bd41c7c0e3432e7648ffd3d64955910f002d/propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e", size = 43705 }, 333 | { url = "https://files.pythonhosted.org/packages/3d/b6/e6d98278f2d49b22b4d033c9f792eda783b9ab2094b041f013fc69bcde87/propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036", size = 11603 }, 334 | ] 335 | 336 | [[package]] 337 | name = "pydantic" 338 | version = "2.9.2" 339 | source = { registry = "https://pypi.org/simple" } 340 | dependencies = [ 341 | { name = "annotated-types" }, 342 | { name = "pydantic-core" }, 343 | { name = "typing-extensions" }, 344 | ] 345 | sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 } 346 | wheels = [ 347 | { url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 }, 348 | ] 349 | 350 | [[package]] 351 | name = "pydantic-core" 352 | version = "2.23.4" 353 | source = { registry = "https://pypi.org/simple" } 354 | dependencies = [ 355 | { name = "typing-extensions" }, 356 | ] 357 | sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 } 358 | wheels = [ 359 | { url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160 }, 360 | { url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777 }, 361 | { url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244 }, 362 | { url = "https://files.pythonhosted.org/packages/a9/8f/89c1405176903e567c5f99ec53387449e62f1121894aa9fc2c4fdc51a59b/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607", size = 1805307 }, 363 | { url = "https://files.pythonhosted.org/packages/d5/a5/1a194447d0da1ef492e3470680c66048fef56fc1f1a25cafbea4bc1d1c48/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", size = 2000663 }, 364 | { url = "https://files.pythonhosted.org/packages/13/a5/1df8541651de4455e7d587cf556201b4f7997191e110bca3b589218745a5/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", size = 2655941 }, 365 | { url = "https://files.pythonhosted.org/packages/44/31/a3899b5ce02c4316865e390107f145089876dff7e1dfc770a231d836aed8/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", size = 2052105 }, 366 | { url = "https://files.pythonhosted.org/packages/1b/aa/98e190f8745d5ec831f6d5449344c48c0627ac5fed4e5340a44b74878f8e/pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", size = 1919967 }, 367 | { url = "https://files.pythonhosted.org/packages/ae/35/b6e00b6abb2acfee3e8f85558c02a0822e9a8b2f2d812ea8b9079b118ba0/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", size = 1964291 }, 368 | { url = "https://files.pythonhosted.org/packages/13/46/7bee6d32b69191cd649bbbd2361af79c472d72cb29bb2024f0b6e350ba06/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", size = 2109666 }, 369 | { url = "https://files.pythonhosted.org/packages/39/ef/7b34f1b122a81b68ed0a7d0e564da9ccdc9a2924c8d6c6b5b11fa3a56970/pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", size = 1732940 }, 370 | { url = "https://files.pythonhosted.org/packages/2f/76/37b7e76c645843ff46c1d73e046207311ef298d3f7b2f7d8f6ac60113071/pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", size = 1916804 }, 371 | { url = "https://files.pythonhosted.org/packages/74/7b/8e315f80666194b354966ec84b7d567da77ad927ed6323db4006cf915f3f/pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", size = 1856459 }, 372 | { url = "https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", size = 1770007 }, 373 | { url = "https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", size = 1790245 }, 374 | { url = "https://files.pythonhosted.org/packages/80/33/9c24334e3af796ce80d2274940aae38dd4e5676298b4398eff103a79e02d/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", size = 1801260 }, 375 | { url = "https://files.pythonhosted.org/packages/a5/6f/e9567fd90104b79b101ca9d120219644d3314962caa7948dd8b965e9f83e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", size = 1996872 }, 376 | { url = "https://files.pythonhosted.org/packages/2d/ad/b5f0fe9e6cfee915dd144edbd10b6e9c9c9c9d7a56b69256d124b8ac682e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", size = 2661617 }, 377 | { url = "https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", size = 2071831 }, 378 | { url = "https://files.pythonhosted.org/packages/89/4d/3079d00c47f22c9a9a8220db088b309ad6e600a73d7a69473e3a8e5e3ea3/pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", size = 1917453 }, 379 | { url = "https://files.pythonhosted.org/packages/e9/88/9df5b7ce880a4703fcc2d76c8c2d8eb9f861f79d0c56f4b8f5f2607ccec8/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", size = 1968793 }, 380 | { url = "https://files.pythonhosted.org/packages/e3/b9/41f7efe80f6ce2ed3ee3c2dcfe10ab7adc1172f778cc9659509a79518c43/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", size = 2116872 }, 381 | { url = "https://files.pythonhosted.org/packages/63/08/b59b7a92e03dd25554b0436554bf23e7c29abae7cce4b1c459cd92746811/pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", size = 1738535 }, 382 | { url = "https://files.pythonhosted.org/packages/88/8d/479293e4d39ab409747926eec4329de5b7129beaedc3786eca070605d07f/pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", size = 1917992 }, 383 | { url = "https://files.pythonhosted.org/packages/ad/ef/16ee2df472bf0e419b6bc68c05bf0145c49247a1095e85cee1463c6a44a1/pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc", size = 1856143 }, 384 | { url = "https://files.pythonhosted.org/packages/da/fa/bc3dbb83605669a34a93308e297ab22be82dfb9dcf88c6cf4b4f264e0a42/pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd", size = 1770063 }, 385 | { url = "https://files.pythonhosted.org/packages/4e/48/e813f3bbd257a712303ebdf55c8dc46f9589ec74b384c9f652597df3288d/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05", size = 1790013 }, 386 | { url = "https://files.pythonhosted.org/packages/b4/e0/56eda3a37929a1d297fcab1966db8c339023bcca0b64c5a84896db3fcc5c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d", size = 1801077 }, 387 | { url = "https://files.pythonhosted.org/packages/04/be/5e49376769bfbf82486da6c5c1683b891809365c20d7c7e52792ce4c71f3/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510", size = 1996782 }, 388 | { url = "https://files.pythonhosted.org/packages/bc/24/e3ee6c04f1d58cc15f37bcc62f32c7478ff55142b7b3e6d42ea374ea427c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6", size = 2661375 }, 389 | { url = "https://files.pythonhosted.org/packages/c1/f8/11a9006de4e89d016b8de74ebb1db727dc100608bb1e6bbe9d56a3cbbcce/pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b", size = 2071635 }, 390 | { url = "https://files.pythonhosted.org/packages/7c/45/bdce5779b59f468bdf262a5bc9eecbae87f271c51aef628d8c073b4b4b4c/pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327", size = 1916994 }, 391 | { url = "https://files.pythonhosted.org/packages/d8/fa/c648308fe711ee1f88192cad6026ab4f925396d1293e8356de7e55be89b5/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6", size = 1968877 }, 392 | { url = "https://files.pythonhosted.org/packages/16/16/b805c74b35607d24d37103007f899abc4880923b04929547ae68d478b7f4/pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f", size = 2116814 }, 393 | { url = "https://files.pythonhosted.org/packages/d1/58/5305e723d9fcdf1c5a655e6a4cc2a07128bf644ff4b1d98daf7a9dbf57da/pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769", size = 1738360 }, 394 | { url = "https://files.pythonhosted.org/packages/a5/ae/e14b0ff8b3f48e02394d8acd911376b7b66e164535687ef7dc24ea03072f/pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5", size = 1919411 }, 395 | ] 396 | 397 | [[package]] 398 | name = "pydantic-settings" 399 | version = "2.6.1" 400 | source = { registry = "https://pypi.org/simple" } 401 | dependencies = [ 402 | { name = "pydantic" }, 403 | { name = "python-dotenv" }, 404 | ] 405 | sdist = { url = "https://files.pythonhosted.org/packages/b5/d4/9dfbe238f45ad8b168f5c96ee49a3df0598ce18a0795a983b419949ce65b/pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0", size = 75646 } 406 | wheels = [ 407 | { url = "https://files.pythonhosted.org/packages/5e/f9/ff95fd7d760af42f647ea87f9b8a383d891cdb5e5dbd4613edaeb094252a/pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", size = 28595 }, 408 | ] 409 | 410 | [[package]] 411 | name = "pymongo" 412 | version = "4.9.2" 413 | source = { registry = "https://pypi.org/simple" } 414 | dependencies = [ 415 | { name = "dnspython" }, 416 | ] 417 | sdist = { url = "https://files.pythonhosted.org/packages/fb/43/d5e8993bd43e6f9cbe985e8ae1398eb73309e88694ac2ea618eacbc9cea2/pymongo-4.9.2.tar.gz", hash = "sha256:3e63535946f5df7848307b9031aa921f82bb0cbe45f9b0c3296f2173f9283eb0", size = 1889366 } 418 | wheels = [ 419 | { url = "https://files.pythonhosted.org/packages/a8/b4/7af80304a0798526fac959e3de651b0747472c049c8b89a6c15fed2026f6/pymongo-4.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:99e40f44877b32bf4b3c46ceed2228f08c222cf7dec8a4366dd192a1429143fa", size = 887499 }, 420 | { url = "https://files.pythonhosted.org/packages/33/ee/5389229774f842bd92a123fd3ea4f2d72b474bde9315ff00e889fe104a0d/pymongo-4.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6f6834d575ed87edc7dfcab4501d961b6a423b3839edd29ecb1382eee7736777", size = 887755 }, 421 | { url = "https://files.pythonhosted.org/packages/d4/fd/3f0ae0fd3a7049ec67ab8f952020bc9fad841791d52d8c51405bd91b3c9b/pymongo-4.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3010018f5672e5b7e8d096dea9f1ea6545b05345ff0eb1754f6ee63785550773", size = 1647336 }, 422 | { url = "https://files.pythonhosted.org/packages/00/b7/0472d51778e9e22b2ffd5ae9a401888525c4872cb2073f1bff8d5ae9659b/pymongo-4.9.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69394ee9f0ce38ff71266bad01b7e045cd75e58500ebad5d72187cbabf2e652a", size = 1713193 }, 423 | { url = "https://files.pythonhosted.org/packages/8c/ac/aa41cb291107bb16bae286d7b9f2c868e393765830bc173609ae4dc9a3ae/pymongo-4.9.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87b18094100f21615d9db99c255dcd9e93e476f10fb03c1d3632cf4b82d201d2", size = 1681720 }, 424 | { url = "https://files.pythonhosted.org/packages/dc/70/ac12eb58bd46a7254daaa4d39e7c4109983ee2227dac44df6587954fe345/pymongo-4.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3039e093d28376d6a54bdaa963ca12230c8a53d7b19c8e6368e19bcfbd004176", size = 1652109 }, 425 | { url = "https://files.pythonhosted.org/packages/d3/20/38f71e0f1c7878b287305b2965cebe327fc5626ecca83ea52a272968cbe2/pymongo-4.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ab42d9ee93fe6b90020c42cba5bfb43a2b4660951225d137835efc21940da48", size = 1611503 }, 426 | { url = "https://files.pythonhosted.org/packages/9b/4c/d3b26e1040c9538b9c8aed005ec18af7515c6dd3091aabfbf6c30a3b3b1a/pymongo-4.9.2-cp311-cp311-win32.whl", hash = "sha256:a663ca60e187a248d370c58961e40f5463077d2b43831eb92120ea28a79ecf96", size = 855570 }, 427 | { url = "https://files.pythonhosted.org/packages/40/3d/7de1a4cf51bf2b10bb9f43ffa208acad0d64c18994ca8d83f490edef6834/pymongo-4.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:24e7b6887bbfefd05afed26a99a2c69459e2daa351a43a410de0d6c0ee3cce4e", size = 874715 }, 428 | { url = "https://files.pythonhosted.org/packages/a1/08/7d95aab0463dc5a2c460a0b4e50a45a743afbe20986f47f87a9a88f43c0c/pymongo-4.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8083bbe8cb10bb33dca4d93f8223dd8d848215250bb73867374650bac5fe69e1", size = 941617 }, 429 | { url = "https://files.pythonhosted.org/packages/bb/28/40613d8d97fc33bf2b9187446a6746925623aa04a9a27c9b058e97076f7a/pymongo-4.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a1b8c636bf557c7166e3799bbf1120806ca39e3f06615b141c88d9c9ceae4d8c", size = 941394 }, 430 | { url = "https://files.pythonhosted.org/packages/df/b2/7f1a0d75f538c0dcaa004ea69e28706fa3ca72d848e0a5a7dafd30939fff/pymongo-4.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8aac5dce28454f47576063fbad31ea9789bba67cab86c95788f97aafd810e65b", size = 1907396 }, 431 | { url = "https://files.pythonhosted.org/packages/ba/70/9304bae47a361a4b12adb5be714bad41478c0e5bc3d6cf403b328d6398a0/pymongo-4.9.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1d5e7123af1fddf15b2b53e58f20bf5242884e671bcc3860f5e954fe13aeddd", size = 1986029 }, 432 | { url = "https://files.pythonhosted.org/packages/ae/51/ac0378d001995c4a705da64a4a2b8e1732f95de5080b752d69f452930cc7/pymongo-4.9.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe97c847b56d61e533a7af0334193d6b28375b9189effce93129c7e4733794a9", size = 1949088 }, 433 | { url = "https://files.pythonhosted.org/packages/1a/30/e93dc808039dc29fc47acee64f128aa650aacae3e4b57b68e01ff1001cda/pymongo-4.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ad54433a996e2d1985a9cd8fc82538ca8747c95caae2daf453600cc8c317f9", size = 1910516 }, 434 | { url = "https://files.pythonhosted.org/packages/2b/34/895b9cad3bd5342d5ab51a853ed3a814840ce281d55c6928968e9f3f49f5/pymongo-4.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98b9cade40f5b13e04492a42ae215c3721099be1014ddfe0fbd23f27e4f62c0c", size = 1860499 }, 435 | { url = "https://files.pythonhosted.org/packages/24/7e/167818f324bf2122d45551680671a3c6406a345d3fcace4e737f57bda4e4/pymongo-4.9.2-cp312-cp312-win32.whl", hash = "sha256:dde6068ae7c62ea8ee2c5701f78c6a75618cada7e11f03893687df87709558de", size = 901282 }, 436 | { url = "https://files.pythonhosted.org/packages/12/6b/b7ffa7114177fc1c60ae529512b82629ff7e25d19be88e97f2d0ddd16717/pymongo-4.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:e1ab6cd7cd2d38ffc7ccdc79fdc166c7a91a63f844a96e3e6b2079c054391c68", size = 924925 }, 437 | { url = "https://files.pythonhosted.org/packages/5b/d6/b57ef5f376e2e171218a98b8c30dfd001aa5cac6338aa7f3ca76e6315667/pymongo-4.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1ad79d6a74f439a068caf9a1e2daeabc20bf895263435484bbd49e90fbea7809", size = 995233 }, 438 | { url = "https://files.pythonhosted.org/packages/32/80/4ec79e36e99f86a063d297a334883fb5115ad70e9af46142b8dc33f636fa/pymongo-4.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:877699e21703717507cbbea23e75b419f81a513b50b65531e1698df08b2d7094", size = 995025 }, 439 | { url = "https://files.pythonhosted.org/packages/c4/fd/8f5464321fdf165700f10aec93b07a75c3537be593291ac2f8c8f5f69bd0/pymongo-4.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc9322ce7cf116458a637ac10517b0c5926a8211202be6dbdc51dab4d4a9afc8", size = 2167429 }, 440 | { url = "https://files.pythonhosted.org/packages/da/42/0f749d805d17f5b17f48f2ee1aaf2a74e67939607b87b245e5ec9b4c1452/pymongo-4.9.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cca029f46acf475504eedb33c7839f030c4bc4f946dcba12d9a954cc48850b79", size = 2258834 }, 441 | { url = "https://files.pythonhosted.org/packages/b8/52/b0c1b8e9cbeae234dd1108a906f30b680755533b7229f9f645d7e7adad25/pymongo-4.9.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c8c861e77527eec5a4b7363c16030dd0374670b620b08a5300f97594bbf5a40", size = 2216412 }, 442 | { url = "https://files.pythonhosted.org/packages/4d/20/53395473a1023bb6a670b68fbfa937664c75b354c2444463075ff43523e2/pymongo-4.9.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fc70326ae71b3c7b8d6af82f46bb71dafdba3c8f335b29382ae9cf263ef3a5c", size = 2168891 }, 443 | { url = "https://files.pythonhosted.org/packages/01/b7/fa4030279d8a4a9c0a969a719b6b89da8a59795b5cdf129ef553fce6d1f2/pymongo-4.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba9d2f6df977fee24437f82f7412460b0628cd6b961c4235c9cff71577a5b61f", size = 2109380 }, 444 | { url = "https://files.pythonhosted.org/packages/f3/55/f252972a039fc6bfca748625c5080d6f88801eb61f118fe79cde47342d6a/pymongo-4.9.2-cp313-cp313-win32.whl", hash = "sha256:b3254769e708bc4aa634745c262081d13c841a80038eff3afd15631540a1d227", size = 946962 }, 445 | { url = "https://files.pythonhosted.org/packages/7b/36/88d8438699ba09b714dece00a4a7462330c1d316f5eaa28db450572236f6/pymongo-4.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:169b85728cc17800344ba17d736375f400ef47c9fbb4c42910c4b3e7c0247382", size = 975113 }, 446 | ] 447 | 448 | [[package]] 449 | name = "python-dotenv" 450 | version = "1.0.1" 451 | source = { registry = "https://pypi.org/simple" } 452 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } 453 | wheels = [ 454 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, 455 | ] 456 | 457 | [[package]] 458 | name = "pyyaml" 459 | version = "6.0.2" 460 | source = { registry = "https://pypi.org/simple" } 461 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } 462 | wheels = [ 463 | { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, 464 | { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, 465 | { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, 466 | { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, 467 | { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, 468 | { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, 469 | { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, 470 | { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, 471 | { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, 472 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, 473 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, 474 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, 475 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, 476 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, 477 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, 478 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, 479 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, 480 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, 481 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, 482 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, 483 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, 484 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, 485 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, 486 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, 487 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, 488 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, 489 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, 490 | ] 491 | 492 | [[package]] 493 | name = "typing-extensions" 494 | version = "4.12.2" 495 | source = { registry = "https://pypi.org/simple" } 496 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 497 | wheels = [ 498 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 499 | ] 500 | 501 | [[package]] 502 | name = "yarl" 503 | version = "1.17.1" 504 | source = { registry = "https://pypi.org/simple" } 505 | dependencies = [ 506 | { name = "idna" }, 507 | { name = "multidict" }, 508 | { name = "propcache" }, 509 | ] 510 | sdist = { url = "https://files.pythonhosted.org/packages/54/9c/9c0a9bfa683fc1be7fdcd9687635151544d992cccd48892dc5e0a5885a29/yarl-1.17.1.tar.gz", hash = "sha256:067a63fcfda82da6b198fa73079b1ca40b7c9b7994995b6ee38acda728b64d47", size = 178163 } 511 | wheels = [ 512 | { url = "https://files.pythonhosted.org/packages/ec/0f/ce6a2c8aab9946446fb27f1e28f0fd89ce84ae913ab18a92d18078a1c7ed/yarl-1.17.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cbad927ea8ed814622305d842c93412cb47bd39a496ed0f96bfd42b922b4a217", size = 140727 }, 513 | { url = "https://files.pythonhosted.org/packages/9d/df/204f7a502bdc3973cd9fc29e7dfad18ae48b3acafdaaf1ae07c0f41025aa/yarl-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fca4b4307ebe9c3ec77a084da3a9d1999d164693d16492ca2b64594340999988", size = 93560 }, 514 | { url = "https://files.pythonhosted.org/packages/a2/e1/f4d522ae0560c91a4ea31113a50f00f85083be885e1092fc6e74eb43cb1d/yarl-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff5c6771c7e3511a06555afa317879b7db8d640137ba55d6ab0d0c50425cab75", size = 91497 }, 515 | { url = "https://files.pythonhosted.org/packages/f1/82/783d97bf4a226f1a2e59b1966f2752244c2bf4dc89bc36f61d597b8e34e5/yarl-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b29beab10211a746f9846baa39275e80034e065460d99eb51e45c9a9495bcca", size = 339446 }, 516 | { url = "https://files.pythonhosted.org/packages/e5/ff/615600647048d81289c80907165de713fbc566d1e024789863a2f6563ba3/yarl-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a52a1ffdd824fb1835272e125385c32fd8b17fbdefeedcb4d543cc23b332d74", size = 354616 }, 517 | { url = "https://files.pythonhosted.org/packages/a5/04/bfb7adb452bd19dfe0c35354ffce8ebc3086e028e5f8270e409d17da5466/yarl-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58c8e9620eb82a189c6c40cb6b59b4e35b2ee68b1f2afa6597732a2b467d7e8f", size = 351801 }, 518 | { url = "https://files.pythonhosted.org/packages/10/e0/efe21edacdc4a638ce911f8cabf1c77cac3f60e9819ba7d891b9ceb6e1d4/yarl-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d216e5d9b8749563c7f2c6f7a0831057ec844c68b4c11cb10fc62d4fd373c26d", size = 343381 }, 519 | { url = "https://files.pythonhosted.org/packages/63/f9/7bc7e69857d6fc3920ecd173592f921d5701f4a0dd3f2ae293b386cfa3bf/yarl-1.17.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:881764d610e3269964fc4bb3c19bb6fce55422828e152b885609ec176b41cf11", size = 337093 }, 520 | { url = "https://files.pythonhosted.org/packages/93/52/99da61947466275ff17d7bc04b0ac31dfb7ec699bd8d8985dffc34c3a913/yarl-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8c79e9d7e3d8a32d4824250a9c6401194fb4c2ad9a0cec8f6a96e09a582c2cc0", size = 346619 }, 521 | { url = "https://files.pythonhosted.org/packages/91/8a/8aaad86a35a16e485ba0e5de0d2ae55bf8dd0c9f1cccac12be4c91366b1d/yarl-1.17.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:299f11b44d8d3a588234adbe01112126010bd96d9139c3ba7b3badd9829261c3", size = 344347 }, 522 | { url = "https://files.pythonhosted.org/packages/af/b6/97f29f626b4a1768ffc4b9b489533612cfcb8905c90f745aade7b2eaf75e/yarl-1.17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cc7d768260f4ba4ea01741c1b5fe3d3a6c70eb91c87f4c8761bbcce5181beafe", size = 350316 }, 523 | { url = "https://files.pythonhosted.org/packages/d7/98/8e0e8b812479569bdc34d66dd3e2471176ca33be4ff5c272a01333c4b269/yarl-1.17.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:de599af166970d6a61accde358ec9ded821234cbbc8c6413acfec06056b8e860", size = 361336 }, 524 | { url = "https://files.pythonhosted.org/packages/9e/d3/d1507efa0a85c25285f8eb51df9afa1ba1b6e446dda781d074d775b6a9af/yarl-1.17.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2b24ec55fad43e476905eceaf14f41f6478780b870eda5d08b4d6de9a60b65b4", size = 365350 }, 525 | { url = "https://files.pythonhosted.org/packages/22/ba/ee7f1830449c96bae6f33210b7d89e8aaf3079fbdaf78ac398e50a9da404/yarl-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9fb815155aac6bfa8d86184079652c9715c812d506b22cfa369196ef4e99d1b4", size = 357689 }, 526 | { url = "https://files.pythonhosted.org/packages/a0/85/321c563dc5afe1661108831b965c512d185c61785400f5606006507d2e18/yarl-1.17.1-cp311-cp311-win32.whl", hash = "sha256:7615058aabad54416ddac99ade09a5510cf77039a3b903e94e8922f25ed203d7", size = 83635 }, 527 | { url = "https://files.pythonhosted.org/packages/bc/da/543a32c00860588ff1235315b68f858cea30769099c32cd22b7bb266411b/yarl-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:14bc88baa44e1f84164a392827b5defb4fa8e56b93fecac3d15315e7c8e5d8b3", size = 90218 }, 528 | { url = "https://files.pythonhosted.org/packages/5d/af/e25615c7920396219b943b9ff8b34636ae3e1ad30777649371317d7f05f8/yarl-1.17.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:327828786da2006085a4d1feb2594de6f6d26f8af48b81eb1ae950c788d97f61", size = 141839 }, 529 | { url = "https://files.pythonhosted.org/packages/83/5e/363d9de3495c7c66592523f05d21576a811015579e0c87dd38c7b5788afd/yarl-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cc353841428d56b683a123a813e6a686e07026d6b1c5757970a877195f880c2d", size = 94125 }, 530 | { url = "https://files.pythonhosted.org/packages/e3/a2/b65447626227ebe36f18f63ac551790068bf42c69bb22dfa3ae986170728/yarl-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c73df5b6e8fabe2ddb74876fb82d9dd44cbace0ca12e8861ce9155ad3c886139", size = 92048 }, 531 | { url = "https://files.pythonhosted.org/packages/a1/f5/2ef86458446f85cde10582054fd5113495ef8ce8477da35aaaf26d2970ef/yarl-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bdff5e0995522706c53078f531fb586f56de9c4c81c243865dd5c66c132c3b5", size = 331472 }, 532 | { url = "https://files.pythonhosted.org/packages/f3/6b/1ba79758ba352cdf2ad4c20cab1b982dd369aa595bb0d7601fc89bf82bee/yarl-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06157fb3c58f2736a5e47c8fcbe1afc8b5de6fb28b14d25574af9e62150fcaac", size = 341260 }, 533 | { url = "https://files.pythonhosted.org/packages/2d/41/4e07c2afca3f9ed3da5b0e38d43d0280d9b624a3d5c478c425e5ce17775c/yarl-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1654ec814b18be1af2c857aa9000de7a601400bd4c9ca24629b18486c2e35463", size = 340882 }, 534 | { url = "https://files.pythonhosted.org/packages/c3/c0/cd8e94618983c1b811af082e1a7ad7764edb3a6af2bc6b468e0e686238ba/yarl-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6595c852ca544aaeeb32d357e62c9c780eac69dcd34e40cae7b55bc4fb1147", size = 336648 }, 535 | { url = "https://files.pythonhosted.org/packages/ac/fc/73ec4340d391ffbb8f34eb4c55429784ec9f5bd37973ce86d52d67135418/yarl-1.17.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:459e81c2fb920b5f5df744262d1498ec2c8081acdcfe18181da44c50f51312f7", size = 325019 }, 536 | { url = "https://files.pythonhosted.org/packages/57/48/da3ebf418fc239d0a156b3bdec6b17a5446f8d2dea752299c6e47b143a85/yarl-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e48cdb8226644e2fbd0bdb0a0f87906a3db07087f4de77a1b1b1ccfd9e93685", size = 342841 }, 537 | { url = "https://files.pythonhosted.org/packages/5d/79/107272745a470a8167924e353a5312eb52b5a9bb58e22686adc46c94f7ec/yarl-1.17.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d9b6b28a57feb51605d6ae5e61a9044a31742db557a3b851a74c13bc61de5172", size = 341433 }, 538 | { url = "https://files.pythonhosted.org/packages/30/9c/6459668b3b8dcc11cd061fc53e12737e740fb6b1575b49c84cbffb387b3a/yarl-1.17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e594b22688d5747b06e957f1ef822060cb5cb35b493066e33ceac0cf882188b7", size = 344927 }, 539 | { url = "https://files.pythonhosted.org/packages/c5/0b/93a17ed733aca8164fc3a01cb7d47b3f08854ce4f957cce67a6afdb388a0/yarl-1.17.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5f236cb5999ccd23a0ab1bd219cfe0ee3e1c1b65aaf6dd3320e972f7ec3a39da", size = 355732 }, 540 | { url = "https://files.pythonhosted.org/packages/9a/63/ead2ed6aec3c59397e135cadc66572330325a0c24cd353cd5c94f5e63463/yarl-1.17.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a2a64e62c7a0edd07c1c917b0586655f3362d2c2d37d474db1a509efb96fea1c", size = 362123 }, 541 | { url = "https://files.pythonhosted.org/packages/89/bf/f6b75b4c2fcf0e7bb56edc0ed74e33f37fac45dc40e5a52a3be66b02587a/yarl-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d0eea830b591dbc68e030c86a9569826145df485b2b4554874b07fea1275a199", size = 356355 }, 542 | { url = "https://files.pythonhosted.org/packages/45/1f/50a0257cd07eef65c8c65ad6a21f5fb230012d659e021aeb6ac8a7897bf6/yarl-1.17.1-cp312-cp312-win32.whl", hash = "sha256:46ddf6e0b975cd680eb83318aa1d321cb2bf8d288d50f1754526230fcf59ba96", size = 83279 }, 543 | { url = "https://files.pythonhosted.org/packages/bc/82/fafb2c1268d63d54ec08b3a254fbe51f4ef098211501df646026717abee3/yarl-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:117ed8b3732528a1e41af3aa6d4e08483c2f0f2e3d3d7dca7cf538b3516d93df", size = 89590 }, 544 | { url = "https://files.pythonhosted.org/packages/06/1e/5a93e3743c20eefbc68bd89334d9c9f04f3f2334380f7bbf5e950f29511b/yarl-1.17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5d1d42556b063d579cae59e37a38c61f4402b47d70c29f0ef15cee1acaa64488", size = 139974 }, 545 | { url = "https://files.pythonhosted.org/packages/a1/be/4e0f6919013c7c5eaea5c31811c551ccd599d2fc80aa3dd6962f1bbdcddd/yarl-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0167540094838ee9093ef6cc2c69d0074bbf84a432b4995835e8e5a0d984374", size = 93364 }, 546 | { url = "https://files.pythonhosted.org/packages/73/f0/650f994bc491d0cb85df8bb45392780b90eab1e175f103a5edc61445ff67/yarl-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2f0a6423295a0d282d00e8701fe763eeefba8037e984ad5de44aa349002562ac", size = 91177 }, 547 | { url = "https://files.pythonhosted.org/packages/f3/e8/9945ed555d14b43ede3ae8b1bd73e31068a694cad2b9d3cad0a28486c2eb/yarl-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b078134f48552c4d9527db2f7da0b5359abd49393cdf9794017baec7506170", size = 333086 }, 548 | { url = "https://files.pythonhosted.org/packages/a6/c0/7d167e48e14d26639ca066825af8da7df1d2fcdba827e3fd6341aaf22a3b/yarl-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d401f07261dc5aa36c2e4efc308548f6ae943bfff20fcadb0a07517a26b196d8", size = 343661 }, 549 | { url = "https://files.pythonhosted.org/packages/fa/81/80a266517531d4e3553aecd141800dbf48d02e23ebd52909e63598a80134/yarl-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5f1ac7359e17efe0b6e5fec21de34145caef22b260e978336f325d5c84e6938", size = 345196 }, 550 | { url = "https://files.pythonhosted.org/packages/b0/77/6adc482ba7f2dc6c0d9b3b492e7cd100edfac4cfc3849c7ffa26fd7beb1a/yarl-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f63d176a81555984e91f2c84c2a574a61cab7111cc907e176f0f01538e9ff6e", size = 338743 }, 551 | { url = "https://files.pythonhosted.org/packages/6d/cc/f0c4c0b92ff3ada517ffde2b127406c001504b225692216d969879ada89a/yarl-1.17.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e275792097c9f7e80741c36de3b61917aebecc08a67ae62899b074566ff8556", size = 326719 }, 552 | { url = "https://files.pythonhosted.org/packages/18/3b/7bfc80d3376b5fa162189993a87a5a6a58057f88315bd0ea00610055b57a/yarl-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:81713b70bea5c1386dc2f32a8f0dab4148a2928c7495c808c541ee0aae614d67", size = 345826 }, 553 | { url = "https://files.pythonhosted.org/packages/2e/66/cf0b0338107a5c370205c1a572432af08f36ca12ecce127f5b558398b4fd/yarl-1.17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:aa46dce75078fceaf7cecac5817422febb4355fbdda440db55206e3bd288cfb8", size = 340335 }, 554 | { url = "https://files.pythonhosted.org/packages/2f/52/b084b0eec0fd4d2490e1d33ace3320fad704c5f1f3deaa709f929d2d87fc/yarl-1.17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1ce36ded585f45b1e9bb36d0ae94765c6608b43bd2e7f5f88079f7a85c61a4d3", size = 345301 }, 555 | { url = "https://files.pythonhosted.org/packages/ef/38/9e2036d948efd3bafcdb4976cb212166fded76615f0dfc6c1492c4ce4784/yarl-1.17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2d374d70fdc36f5863b84e54775452f68639bc862918602d028f89310a034ab0", size = 354205 }, 556 | { url = "https://files.pythonhosted.org/packages/81/c1/13dfe1e70b86811733316221c696580725ceb1c46d4e4db852807e134310/yarl-1.17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2d9f0606baaec5dd54cb99667fcf85183a7477f3766fbddbe3f385e7fc253299", size = 360501 }, 557 | { url = "https://files.pythonhosted.org/packages/91/87/756e05c74cd8bf9e71537df4a2cae7e8211a9ebe0d2350a3e26949e1e41c/yarl-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0341e6d9a0c0e3cdc65857ef518bb05b410dbd70d749a0d33ac0f39e81a4258", size = 359452 }, 558 | { url = "https://files.pythonhosted.org/packages/06/b2/b2bb09c1e6d59e1c9b1b36a86caa473e22c3dbf26d1032c030e9bfb554dc/yarl-1.17.1-cp313-cp313-win32.whl", hash = "sha256:2e7ba4c9377e48fb7b20dedbd473cbcbc13e72e1826917c185157a137dac9df2", size = 308904 }, 559 | { url = "https://files.pythonhosted.org/packages/f3/27/f084d9a5668853c1f3b246620269b14ee871ef3c3cc4f3a1dd53645b68ec/yarl-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:949681f68e0e3c25377462be4b658500e85ca24323d9619fdc41f68d46a1ffda", size = 314637 }, 560 | { url = "https://files.pythonhosted.org/packages/52/ad/1fe7ff5f3e8869d4c5070f47b96bac2b4d15e67c100a8278d8e7876329fc/yarl-1.17.1-py3-none-any.whl", hash = "sha256:f1790a4b1e8e8e028c391175433b9c8122c39b46e1663228158e61e6f915bf06", size = 44352 }, 561 | ] 562 | -------------------------------------------------------------------------------- /credits.yml: -------------------------------------------------------------------------------- 1 | 0: 42069 2 | 1: 69420 -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo docker compose up -d --build --remove-orphans -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | api: 3 | build: 4 | dockerfile: Dockerfile 5 | context: ./api 6 | volumes: 7 | - ./credits.yml:/app/credits.yml 8 | 9 | bot: 10 | build: 11 | dockerfile: Dockerfile 12 | context: ./bot 13 | volumes: 14 | - ./credits.yml:/app/credits.yml 15 | 16 | db: 17 | image: mongo:latest 18 | volumes: 19 | - mongo-data:/data/db 20 | ports: 21 | - 27017:27017 22 | env_file: 23 | - ./secrets/db.env 24 | 25 | tunnel: 26 | image: cloudflare/cloudflared:latest 27 | command: tunnel --no-autoupdate run 28 | env_file: 29 | - ./secrets/cloudflared.env 30 | 31 | volumes: 32 | mongo-data: -------------------------------------------------------------------------------- /secrets/cloudflared.env: -------------------------------------------------------------------------------- 1 | TUNNEL_TOKEN= -------------------------------------------------------------------------------- /secrets/db.env: -------------------------------------------------------------------------------- 1 | MONGO_INITDB_DATABASE=db 2 | MONGO_INITDB_ROOT_USERNAME=root 3 | MONGO_INITDB_ROOT_PASSWORD=password --------------------------------------------------------------------------------