├── .env.example ├── .gitignore ├── Dockerfile ├── Makefile ├── README.md ├── app ├── application │ ├── __init__.py │ └── api │ │ ├── __init__.py │ │ ├── main.py │ │ ├── messages │ │ ├── __init__.py │ │ ├── handlers.py │ │ └── schemas.py │ │ └── schemas.py ├── domain │ ├── __init__.py │ ├── entities │ │ ├── __init__.py │ │ ├── base.py │ │ └── messages.py │ ├── events │ │ ├── __init__.py │ │ ├── base.py │ │ └── messages.py │ ├── exceptions │ │ ├── __init__.py │ │ ├── base.py │ │ └── message.py │ └── values │ │ ├── __init__.py │ │ ├── base.py │ │ └── messages.py ├── infra │ ├── __init__.py │ └── repositories │ │ ├── __init__.py │ │ └── messages │ │ ├── __init__.py │ │ ├── base.py │ │ ├── converters.py │ │ ├── memory.py │ │ └── mongo.py ├── logic │ ├── __init__.py │ ├── commands │ │ ├── __init__.py │ │ ├── base.py │ │ └── messages.py │ ├── events │ │ ├── __init__.py │ │ └── base.py │ ├── exceptions │ │ ├── __init__.py │ │ ├── base.py │ │ ├── mediator.py │ │ └── messages.py │ ├── init.py │ └── mediator.py ├── settings │ ├── __init__.py │ └── config.py └── tests │ ├── __init__.py │ ├── application │ ├── __init__.py │ └── api │ │ ├── __init__.py │ │ ├── conftest.py │ │ └── test_messages.py │ ├── conftest.py │ ├── domain │ ├── __init__.py │ └── values │ │ ├── __init__.py │ │ └── test_messages.py │ ├── fixtures.py │ └── logic │ ├── __init__.py │ ├── conftest.py │ └── test_messages.py ├── docker_compose ├── app.yaml └── storages.yaml ├── poetry.lock └── pyproject.toml /.env.example: -------------------------------------------------------------------------------- 1 | API_PORT=8000 2 | PYTHONPATH=app 3 | MONGO_DB_CONNECTION_URI=mongodb://mongodb:27017 4 | MONGO_DB_ADMIN_USERNAME=admin 5 | MONGO_DB_ADMIN_PASSWORD=admin 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .vscode/ 3 | .env 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12.1-slim-bullseye 2 | 3 | ENV PYTHONDONTWRITEBYTECODE 1 4 | ENV PYTHONUNBUFFERED 1 5 | 6 | WORKDIR /app 7 | 8 | RUN apt update -y && \ 9 | apt install -y python3-dev \ 10 | gcc \ 11 | musl-dev 12 | 13 | ADD pyproject.toml /app 14 | 15 | RUN pip install --upgrade pip 16 | RUN pip install poetry 17 | 18 | RUN poetry config virtualenvs.create false 19 | RUN poetry install --no-root --no-interaction --no-ansi 20 | 21 | COPY /app/* /app/ 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DC = docker compose 2 | EXEC = docker exec -it 3 | LOGS = docker logs 4 | ENV = --env-file .env 5 | APP_FILE = docker_compose/app.yaml 6 | STORAGES_FILE = docker_compose/storages.yaml 7 | APP_CONTAINER = main-app 8 | 9 | .PHONY: app 10 | app: 11 | ${DC} -f ${APP_FILE} ${ENV} up --build -d 12 | 13 | .PHONY: storages 14 | storages: 15 | ${DC} -f ${STORAGES_FILE} ${ENV} up --build -d 16 | 17 | .PHONY: all 18 | all: 19 | ${DC} -f ${STORAGES_FILE} -f ${APP_FILE} ${ENV} up --build -d 20 | 21 | .PHONY: app-down 22 | app-down: 23 | ${DC} -f ${APP_FILE} down 24 | 25 | .PHONY: storages-down 26 | storages-down: 27 | ${DC} -f ${STORAGES_FILE} down 28 | 29 | .PHONY: app-shell 30 | app-shell: 31 | ${EXEC} ${APP_CONTAINER} bash 32 | 33 | .PHONY: app-logs 34 | app-logs: 35 | ${LOGS} ${APP_CONTAINER} -f 36 | 37 | .PHONY: test 38 | test: 39 | ${EXEC} ${APP_CONTAINER} pytest 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FastAPI + Kafka DDD chat Application w\ MongoDB 2 | 3 | This is a basic template for Django projects configured to use Docker Compose, Makefile, and PostgreSQL. 4 | 5 | ## Requirements 6 | 7 | - [Docker](https://www.docker.com/get-started) 8 | - [Docker Compose](https://docs.docker.com/compose/install/) 9 | - [GNU Make](https://www.gnu.org/software/make/) 10 | 11 | ## How to Use 12 | 13 | 1. **Clone the repository:** 14 | 15 | ```bash 16 | git clone https://github.com/your_username/your_repository.git 17 | cd your_repository 18 | 19 | 2. Install all required packages in `Requirements` section. 20 | 21 | 22 | ### Implemented Commands 23 | 24 | * `make app` - up application and database/infrastructure 25 | * `make app-logs` - follow the logs in app container 26 | * `make app-down` - down application and all infrastructure 27 | * `make app-shell` - go to contenerized interactive shell (bash) 28 | 29 | ### Most Used Django Specific Commands 30 | 31 | * `make migrations` - make migrations to models 32 | * `make migrate` - apply all made migrations 33 | * `make collectstatic` - collect static 34 | * `make superuser` - create admin user 35 | * `make test` - test application with pytest 36 | -------------------------------------------------------------------------------- /app/application/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/application/__init__.py -------------------------------------------------------------------------------- /app/application/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/application/api/__init__.py -------------------------------------------------------------------------------- /app/application/api/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | 3 | from application.api.messages.handlers import router as message_router 4 | 5 | 6 | def create_app() -> FastAPI: 7 | app = FastAPI( 8 | title='Simple Kafka Chat', 9 | docs_url='/api/docs', 10 | description='A simple kafka + ddd example.', 11 | debug=True, 12 | ) 13 | app.include_router(message_router, prefix='/chat') 14 | 15 | return app 16 | -------------------------------------------------------------------------------- /app/application/api/messages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/application/api/messages/__init__.py -------------------------------------------------------------------------------- /app/application/api/messages/handlers.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, status 2 | from fastapi.exceptions import HTTPException 3 | from fastapi.routing import APIRouter 4 | 5 | from application.api.messages.schemas import CreateChatRequestSchema, CreateChatResponseSchema 6 | from application.api.schemas import ErrorSchema 7 | from domain.exceptions.base import ApplicationException 8 | from logic.commands.messages import CreateChatCommand 9 | from logic.init import init_container 10 | from logic.mediator import Mediator 11 | 12 | 13 | router = APIRouter(tags=['Chat']) 14 | 15 | 16 | @router.post( 17 | '/', 18 | # response_model=CreateChatResponseSchema, 19 | status_code=status.HTTP_201_CREATED, 20 | description='Эндпоинт создаёт новый чат, если чат с таким названием существует, то возвращается 400 ошибка', 21 | responses={ 22 | status.HTTP_201_CREATED: {'model': CreateChatResponseSchema}, 23 | status.HTTP_400_BAD_REQUEST: {'model': ErrorSchema}, 24 | }, 25 | ) 26 | async def create_chat_handler(schema: CreateChatRequestSchema, container=Depends(init_container)): 27 | ''' Создать новый чат. ''' 28 | mediator: Mediator = container.resolve(Mediator) 29 | 30 | try: 31 | chat, *_ = await mediator.handle_command(CreateChatCommand(title=schema.title)) 32 | except ApplicationException as exception: 33 | raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail={'error': exception.message}) 34 | 35 | return CreateChatResponseSchema.from_entity(chat) 36 | -------------------------------------------------------------------------------- /app/application/api/messages/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | from domain.entities.messages import Chat 4 | 5 | 6 | class CreateChatRequestSchema(BaseModel): 7 | title: str 8 | 9 | 10 | class CreateChatResponseSchema(BaseModel): 11 | oid: str 12 | title: str 13 | 14 | @classmethod 15 | def from_entity(cls, chat: Chat) -> 'CreateChatResponseSchema': 16 | return CreateChatResponseSchema( 17 | oid=chat.oid, 18 | title=chat.title.as_generic_type(), 19 | ) 20 | -------------------------------------------------------------------------------- /app/application/api/schemas.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel 2 | 3 | 4 | class ErrorSchema(BaseModel): 5 | error: str 6 | -------------------------------------------------------------------------------- /app/domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/domain/__init__.py -------------------------------------------------------------------------------- /app/domain/entities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/domain/entities/__init__.py -------------------------------------------------------------------------------- /app/domain/entities/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from copy import copy 3 | from dataclasses import dataclass, field 4 | from datetime import datetime 5 | from uuid import uuid4 6 | 7 | from domain.events.base import BaseEvent 8 | 9 | 10 | @dataclass 11 | class BaseEntity(ABC): 12 | oid: str = field( 13 | default_factory=lambda: str(uuid4()), 14 | kw_only=True, 15 | ) 16 | _events: list[BaseEvent] = field( 17 | default_factory=list, 18 | kw_only=True, 19 | ) 20 | created_at: datetime = field( 21 | default_factory=datetime.now, 22 | kw_only=True, 23 | ) 24 | 25 | def __hash__(self) -> int: 26 | return hash(self.oid) 27 | 28 | def __eq__(self, __value: 'BaseEntity') -> bool: 29 | return self.oid == __value.oid 30 | 31 | def register_event(self, event: BaseEvent) -> None: 32 | self._events.append(event) 33 | 34 | def pull_events(self) -> list[BaseEvent]: 35 | registered_events = copy(self._events) 36 | self._events.clear() 37 | 38 | return registered_events 39 | -------------------------------------------------------------------------------- /app/domain/entities/messages.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from datetime import datetime 3 | 4 | from domain.entities.base import BaseEntity 5 | from domain.events.messages import NewChatCreated, NewMessageReceivedEvent 6 | from domain.values.messages import Text, Title 7 | 8 | 9 | @dataclass(eq=False) 10 | class Message(BaseEntity): 11 | text: Text 12 | 13 | 14 | @dataclass(eq=False) 15 | class Chat(BaseEntity): 16 | title: Title 17 | messages: set[Message] = field( 18 | default_factory=set, 19 | kw_only=True, 20 | ) 21 | 22 | @classmethod 23 | def create_chat(cls, title: Title) -> 'Chat': 24 | new_chat = cls(title=title) 25 | new_chat.register_event(NewChatCreated(chat_oid=new_chat.oid, chat_title=new_chat.title.as_generic_type())) 26 | 27 | return new_chat 28 | 29 | def add_message(self, message: Message): 30 | self.messages.add(message) 31 | self.register_event(NewMessageReceivedEvent( 32 | message_text=message.text.as_generic_type(), 33 | chat_oid=self.oid, 34 | message_oid=message.oid, 35 | )) 36 | -------------------------------------------------------------------------------- /app/domain/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/domain/events/__init__.py -------------------------------------------------------------------------------- /app/domain/events/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass, field 3 | from uuid import UUID, uuid4 4 | 5 | 6 | @dataclass 7 | class BaseEvent(ABC): 8 | event_id: UUID = field(default_factory=uuid4, kw_only=True) 9 | -------------------------------------------------------------------------------- /app/domain/events/messages.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from domain.events.base import BaseEvent 4 | 5 | 6 | @dataclass 7 | class NewMessageReceivedEvent(BaseEvent): 8 | message_text: str 9 | message_oid: str 10 | chat_oid: str 11 | 12 | 13 | @dataclass 14 | class NewChatCreated(BaseEvent): 15 | chat_oid: str 16 | chat_title: str 17 | -------------------------------------------------------------------------------- /app/domain/exceptions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/domain/exceptions/__init__.py -------------------------------------------------------------------------------- /app/domain/exceptions/base.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | 4 | @dataclass(eq=False) 5 | class ApplicationException(Exception): 6 | @property 7 | def message(self): 8 | return 'Произошла ошибка приложения,' 9 | -------------------------------------------------------------------------------- /app/domain/exceptions/message.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from domain.exceptions.base import ApplicationException 4 | 5 | 6 | @dataclass(eq=False) 7 | class TitleTooLongException(ApplicationException): 8 | text: str 9 | 10 | @property 11 | def message(self): 12 | return f'Слишком длинный текст сообщения "{self.text[:255]}..."' 13 | 14 | 15 | @dataclass(eq=False) 16 | class EmptyTextException(ApplicationException): 17 | @property 18 | def message(self): 19 | return 'Текст не может быть пустым' 20 | -------------------------------------------------------------------------------- /app/domain/values/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/domain/values/__init__.py -------------------------------------------------------------------------------- /app/domain/values/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from dataclasses import dataclass 3 | from typing import Any, Generic, TypeVar 4 | 5 | 6 | VT = TypeVar('VT', bound=Any) 7 | 8 | 9 | @dataclass(frozen=True) 10 | class BaseValueObject(ABC, Generic[VT]): 11 | value: VT 12 | 13 | def __post_init__(self): 14 | self.validate() 15 | 16 | @abstractmethod 17 | def validate(self): 18 | ... 19 | 20 | @abstractmethod 21 | def as_generic_type(self) -> VT: 22 | ... 23 | -------------------------------------------------------------------------------- /app/domain/values/messages.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from domain.values.base import BaseValueObject 4 | from domain.exceptions.message import EmptyTextException, TitleTooLongException 5 | 6 | 7 | @dataclass(frozen=True) 8 | class Text(BaseValueObject): 9 | value: str 10 | 11 | def validate(self): 12 | if not self.value: 13 | raise EmptyTextException() 14 | 15 | def as_generic_type(self) -> str: 16 | return str(self.value) 17 | 18 | 19 | @dataclass(frozen=True) 20 | class Title(BaseValueObject): 21 | def validate(self): 22 | if not self.value: 23 | raise EmptyTextException() 24 | 25 | if len(self.value) > 255: 26 | raise TitleTooLongException(self.value) 27 | 28 | def as_generic_type(self): 29 | return str(self.value) 30 | -------------------------------------------------------------------------------- /app/infra/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/infra/__init__.py -------------------------------------------------------------------------------- /app/infra/repositories/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/infra/repositories/__init__.py -------------------------------------------------------------------------------- /app/infra/repositories/messages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/infra/repositories/messages/__init__.py -------------------------------------------------------------------------------- /app/infra/repositories/messages/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from dataclasses import dataclass 3 | 4 | from domain.entities.messages import Chat 5 | 6 | 7 | @dataclass 8 | class BaseChatRepository(ABC): 9 | @abstractmethod 10 | async def check_chat_exists_by_title(self, title: str) -> bool: 11 | ... 12 | 13 | @abstractmethod 14 | async def add_chat(self, chat: Chat) -> None: 15 | ... 16 | -------------------------------------------------------------------------------- /app/infra/repositories/messages/converters.py: -------------------------------------------------------------------------------- 1 | from domain.entities.messages import Chat, Message 2 | 3 | 4 | def convert_message_to_document(message: Message) -> dict: 5 | return { 6 | 'oid': message.oid, 7 | 'text': message.text.as_generic_type(), 8 | } 9 | 10 | 11 | def convert_chat_entity_to_document(chat: Chat) -> dict: 12 | return { 13 | 'oid': chat.oid, 14 | 'title': chat.title.as_generic_type(), 15 | 'created_at': chat.created_at, 16 | 'messages': [convert_message_to_document(message) for message in chat.messages], 17 | } 18 | -------------------------------------------------------------------------------- /app/infra/repositories/messages/memory.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass, field 3 | 4 | from domain.entities.messages import Chat 5 | from infra.repositories.messages.base import BaseChatRepository 6 | 7 | 8 | 9 | @dataclass 10 | class MemoryChatRepository(BaseChatRepository): 11 | _saved_chats: list[Chat] = field(default_factory=list, kw_only=True) 12 | 13 | async def check_chat_exists_by_title(self, title: str) -> bool: 14 | try: 15 | return bool(next( 16 | chat for chat in self._saved_chats if chat.title.as_generic_type() == title 17 | )) 18 | except StopIteration: 19 | return False 20 | 21 | async def add_chat(self, chat: Chat) -> None: 22 | self._saved_chats.append(chat) 23 | -------------------------------------------------------------------------------- /app/infra/repositories/messages/mongo.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from motor.core import AgnosticClient 4 | 5 | from domain.entities.messages import Chat 6 | from infra.repositories.messages.base import BaseChatRepository 7 | from infra.repositories.messages.converters import convert_chat_entity_to_document 8 | 9 | 10 | @dataclass 11 | class MongoDBChatRepository(BaseChatRepository): 12 | mongo_db_client: AgnosticClient 13 | mongo_db_db_name: str 14 | mongo_db_collection_name: str 15 | 16 | def _get_chat_collection(self): 17 | return self.mongo_db_client[self.mongo_db_db_name][self.mongo_db_collection_name] 18 | 19 | async def check_chat_exists_by_title(self, title: str) -> bool: 20 | collection = self._get_chat_collection() 21 | 22 | return bool(await collection.find_one(filter={'title': title})) 23 | 24 | async def add_chat(self, chat: Chat) -> None: 25 | collection = self._get_chat_collection() 26 | await collection.insert_one(convert_chat_entity_to_document(chat)) 27 | -------------------------------------------------------------------------------- /app/logic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/logic/__init__.py -------------------------------------------------------------------------------- /app/logic/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/logic/commands/__init__.py -------------------------------------------------------------------------------- /app/logic/commands/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | from dataclasses import dataclass 3 | from typing import Any, Generic, TypeVar 4 | 5 | 6 | @dataclass(frozen=True) 7 | class BaseCommand(ABC): 8 | ... 9 | 10 | 11 | CT = TypeVar('CT', bound=BaseCommand) 12 | CR = TypeVar('CR', bound=Any) 13 | 14 | 15 | @dataclass(frozen=True) 16 | class CommandHandler(ABC, Generic[CT, CR]): 17 | @abstractmethod 18 | async def handle(self, command: CT) -> CR: 19 | ... 20 | -------------------------------------------------------------------------------- /app/logic/commands/messages.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from domain.entities.messages import Chat 4 | from domain.values.messages import Title 5 | from infra.repositories.messages.base import BaseChatRepository 6 | from logic.commands.base import BaseCommand, CommandHandler 7 | from logic.exceptions.messages import ChatWithThatTitleAlreadyExistsException 8 | 9 | 10 | @dataclass(frozen=True) 11 | class CreateChatCommand(BaseCommand): 12 | title: str 13 | 14 | 15 | @dataclass(frozen=True) 16 | class CreateChatCommandHandler(CommandHandler[CreateChatCommand, Chat]): 17 | chat_repository: BaseChatRepository 18 | 19 | async def handle(self, command: CreateChatCommand) -> Chat: 20 | if await self.chat_repository.check_chat_exists_by_title(command.title): 21 | raise ChatWithThatTitleAlreadyExistsException(command.title) 22 | 23 | title = Title(value=command.title) 24 | 25 | new_chat = Chat.create_chat(title=title) 26 | # TODO: считать ивенты 27 | await self.chat_repository.add_chat(new_chat) 28 | 29 | return new_chat 30 | -------------------------------------------------------------------------------- /app/logic/events/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/logic/events/__init__.py -------------------------------------------------------------------------------- /app/logic/events/base.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | from dataclasses import dataclass 3 | from typing import Any, Generic, TypeVar 4 | 5 | from domain.events.base import BaseEvent 6 | 7 | 8 | ET = TypeVar('ET', bound=BaseEvent) 9 | ER = TypeVar('ER', bound=Any) 10 | 11 | 12 | @dataclass 13 | class EventHandler(ABC, Generic[ET, ER]): 14 | def handle(self, event: ET) -> ER: 15 | ... 16 | -------------------------------------------------------------------------------- /app/logic/exceptions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/logic/exceptions/__init__.py -------------------------------------------------------------------------------- /app/logic/exceptions/base.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from domain.exceptions.base import ApplicationException 4 | 5 | 6 | @dataclass(eq=False) 7 | class LogicException(ApplicationException): 8 | @property 9 | def message(self): 10 | return 'В обработки запроса возникла ошибка' 11 | -------------------------------------------------------------------------------- /app/logic/exceptions/mediator.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from logic.exceptions.base import LogicException 4 | 5 | 6 | @dataclass(eq=False) 7 | class EventHandlersNotRegisteredException(LogicException): 8 | event_type: type 9 | 10 | @property 11 | def message(self): 12 | return f'Не удалось найти обработчики для события: {self.event_type}' 13 | 14 | 15 | @dataclass(eq=False) 16 | class CommandHandlersNotRegisteredException(LogicException): 17 | command_type: type 18 | 19 | @property 20 | def message(self): 21 | return f'Не удалось найти обработчики для команды: {self.command_type}' 22 | -------------------------------------------------------------------------------- /app/logic/exceptions/messages.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from logic.exceptions.base import LogicException 4 | 5 | 6 | @dataclass(eq=False) 7 | class ChatWithThatTitleAlreadyExistsException(LogicException): 8 | title: str 9 | 10 | @property 11 | def message(self): 12 | return f'Чат с таким названием "{self.title}" уже существует.' 13 | -------------------------------------------------------------------------------- /app/logic/init.py: -------------------------------------------------------------------------------- 1 | from functools import lru_cache 2 | from punq import Container, Scope 3 | 4 | from motor.motor_asyncio import AsyncIOMotorClient 5 | 6 | from infra.repositories.messages.base import BaseChatRepository 7 | from infra.repositories.messages.mongo import MongoDBChatRepository 8 | from logic.commands.messages import CreateChatCommand, CreateChatCommandHandler 9 | from logic.mediator import Mediator 10 | from settings.config import Config 11 | 12 | 13 | @lru_cache(1) 14 | def init_container(): 15 | return _init_container() 16 | 17 | 18 | def _init_container() -> Container: 19 | container = Container() 20 | 21 | container.register(CreateChatCommandHandler) 22 | container.register(Config, instance=Config(), scope=Scope.singleton) 23 | 24 | def init_mediator(): 25 | mediator = Mediator() 26 | mediator.register_command( 27 | CreateChatCommand, 28 | [container.resolve(CreateChatCommandHandler)], 29 | ) 30 | 31 | return mediator 32 | 33 | def init_chat_mongodb_repository(): 34 | config: Config = container.resolve(Config) 35 | client = AsyncIOMotorClient(config.mongodb_connection_uri, serverSelectionTimeoutMS=3000) 36 | return MongoDBChatRepository( 37 | mongo_db_client=client, 38 | mongo_db_db_name=config.mongodb_chat_database, 39 | mongo_db_collection_name=config.mongodb_chat_collection, 40 | ) 41 | 42 | container.register(BaseChatRepository, factory=init_chat_mongodb_repository, scope=Scope.singleton) 43 | container.register(Mediator, factory=init_mediator) 44 | 45 | return container 46 | -------------------------------------------------------------------------------- /app/logic/mediator.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from collections.abc import Iterable 3 | from dataclasses import dataclass, field 4 | 5 | from domain.events.base import BaseEvent 6 | from logic.commands.base import CR, CT, BaseCommand, CommandHandler 7 | from logic.events.base import ER, ET, EventHandler 8 | from logic.exceptions.mediator import CommandHandlersNotRegisteredException, EventHandlersNotRegisteredException 9 | 10 | 11 | @dataclass(eq=False) 12 | class Mediator: 13 | events_map: dict[ET, EventHandler] = field( 14 | default_factory=lambda: defaultdict(list), 15 | kw_only=True, 16 | ) 17 | commands_map: dict[CT, CommandHandler] = field( 18 | default_factory=lambda: defaultdict(list), 19 | kw_only=True, 20 | ) 21 | 22 | def register_event(self, event: ET, event_handlers: Iterable[EventHandler[ET, ER]]): 23 | self.events_map[event].append(event_handlers) 24 | 25 | def register_command(self, command: CT, command_handlers: Iterable[CommandHandler[CT, CR]]): 26 | self.events_map[command].extend(command_handlers) 27 | 28 | async def publish(self, events: Iterable[BaseEvent]) -> Iterable[ER]: 29 | event_type = events.__class__ 30 | handlers = self.events_map.get(event_type) 31 | 32 | if not handlers: 33 | raise EventHandlersNotRegisteredException(event_type) 34 | 35 | result = [] 36 | 37 | for event in events: 38 | result.extend([await handler.handle(event) for handler in handlers]) 39 | 40 | return result 41 | 42 | async def handle_command(self, command: BaseCommand) -> Iterable[CR]: 43 | command_type = command.__class__ 44 | handlers = self.events_map.get(command_type) 45 | 46 | if not handlers: 47 | raise CommandHandlersNotRegisteredException(command_type) 48 | 49 | return [await handler.handle(command) for handler in handlers] 50 | -------------------------------------------------------------------------------- /app/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/settings/__init__.py -------------------------------------------------------------------------------- /app/settings/config.py: -------------------------------------------------------------------------------- 1 | from pydantic import Field 2 | from pydantic_settings import BaseSettings 3 | 4 | 5 | class Config(BaseSettings): 6 | mongodb_connection_uri: str = Field(alias='MONGO_DB_CONNECTION_URI') 7 | mongodb_chat_database: str = Field(default='chat', alias='MONGODB_CHAT_DATABASE') 8 | mongodb_chat_collection: str = Field(default='chat', alias='MONGODB_CHAT_COLLECTION') 9 | -------------------------------------------------------------------------------- /app/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/tests/__init__.py -------------------------------------------------------------------------------- /app/tests/application/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/tests/application/__init__.py -------------------------------------------------------------------------------- /app/tests/application/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/tests/application/api/__init__.py -------------------------------------------------------------------------------- /app/tests/application/api/conftest.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI 2 | import pytest 3 | 4 | from fastapi.testclient import TestClient 5 | 6 | from application.api.main import create_app 7 | from logic.init import init_container 8 | from tests.fixtures import init_dummy_container 9 | 10 | 11 | @pytest.fixture 12 | def app() -> FastAPI: 13 | app = create_app() 14 | app.dependency_overrides[init_container] = init_dummy_container 15 | 16 | return app 17 | 18 | 19 | @pytest.fixture 20 | def client(app: FastAPI) -> TestClient: 21 | return TestClient(app=app) 22 | -------------------------------------------------------------------------------- /app/tests/application/api/test_messages.py: -------------------------------------------------------------------------------- 1 | from faker import Faker 2 | 3 | from httpx import Response 4 | 5 | from fastapi import FastAPI, status 6 | from fastapi.testclient import TestClient 7 | 8 | import pytest 9 | 10 | 11 | @pytest.mark.asyncio 12 | async def test_create_chat_success( 13 | app: FastAPI, 14 | client: TestClient, 15 | faker: Faker, 16 | ): 17 | url = app.url_path_for('create_chat_handler') 18 | title = faker.text()[:100] 19 | response: Response = client.post(url=url, json={'title': title}) 20 | 21 | assert response.is_success 22 | json_data = response.json() 23 | 24 | assert json_data['title'] == title 25 | 26 | 27 | @pytest.mark.asyncio 28 | async def test_create_chat_fail_text_too_long( 29 | app: FastAPI, 30 | client: TestClient, 31 | faker: Faker, 32 | ): 33 | url = app.url_path_for('create_chat_handler') 34 | title = faker.text(max_nb_chars=500) 35 | response: Response = client.post(url=url, json={'title': title}) 36 | 37 | assert response.status_code == status.HTTP_400_BAD_REQUEST, response.json() 38 | json_data = response.json() 39 | 40 | assert json_data['detail']['error'] 41 | 42 | 43 | @pytest.mark.asyncio 44 | async def test_create_chat_fail_text_empty( 45 | app: FastAPI, 46 | client: TestClient, 47 | ): 48 | url = app.url_path_for('create_chat_handler') 49 | response: Response = client.post(url=url, json={'title': ''}) 50 | 51 | assert response.status_code == status.HTTP_400_BAD_REQUEST, response.json() 52 | json_data = response.json() 53 | 54 | assert json_data['detail']['error'] 55 | -------------------------------------------------------------------------------- /app/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from punq import Container 4 | 5 | from infra.repositories.messages.base import BaseChatRepository 6 | from logic.mediator import Mediator 7 | from tests.fixtures import init_dummy_container 8 | 9 | 10 | @fixture(scope='function') 11 | def container() -> Container: 12 | return init_dummy_container() 13 | 14 | 15 | @fixture() 16 | def mediator(container: Container) -> Mediator: 17 | return container.resolve(Mediator) 18 | 19 | 20 | @fixture() 21 | def chat_repository(container: Container) -> BaseChatRepository: 22 | return container.resolve(BaseChatRepository) 23 | -------------------------------------------------------------------------------- /app/tests/domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/tests/domain/__init__.py -------------------------------------------------------------------------------- /app/tests/domain/values/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/tests/domain/values/__init__.py -------------------------------------------------------------------------------- /app/tests/domain/values/test_messages.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import pytest 3 | 4 | from domain.entities.messages import Chat, Message 5 | from domain.events.messages import NewMessageReceivedEvent 6 | from domain.exceptions.message import TitleTooLongException 7 | from domain.values.messages import Text, Title 8 | 9 | 10 | def test_create_message_success_short_text(): 11 | text = Text('hello world') 12 | message = Message(text=text) 13 | 14 | assert message.text == text 15 | assert message.created_at.date() == datetime.today().date() 16 | 17 | 18 | def test_create_message_success_long_text(): 19 | text = Text('a' * 400) 20 | message = Message(text=text) 21 | 22 | assert message.text == text 23 | assert message.created_at.date() == datetime.today().date() 24 | 25 | 26 | def test_create_chat_success(): 27 | title = Title('title') 28 | chat = Chat(title=title) 29 | 30 | assert chat.title == title 31 | assert not chat.messages 32 | assert chat.created_at.date() == datetime.today().date() 33 | 34 | 35 | def test_create_chat_title_too_long(): 36 | with pytest.raises(TitleTooLongException): 37 | Title('title' * 200) 38 | 39 | 40 | def test_add_chat_to_message(): 41 | text = Text('hello world') 42 | message = Message(text=text) 43 | 44 | title = Title('title') 45 | chat = Chat(title=title) 46 | 47 | chat.add_message(message) 48 | 49 | assert message in chat.messages 50 | 51 | 52 | def test_new_message_events(): 53 | text = Text('hello world') 54 | message = Message(text=text) 55 | 56 | title = Title('title') 57 | chat = Chat(title=title) 58 | 59 | chat.add_message(message) 60 | events = chat.pull_events() 61 | pulled_events = chat.pull_events() 62 | 63 | assert not pulled_events, pulled_events 64 | assert len(events) == 1, events 65 | 66 | new_event = events[0] 67 | 68 | assert isinstance(new_event, NewMessageReceivedEvent), new_event 69 | assert new_event.message_oid == message.oid 70 | assert new_event.message_text == message.text.as_generic_type() 71 | assert new_event.chat_oid == chat.oid 72 | -------------------------------------------------------------------------------- /app/tests/fixtures.py: -------------------------------------------------------------------------------- 1 | from punq import Container, Scope 2 | 3 | from infra.repositories.messages.base import BaseChatRepository 4 | from infra.repositories.messages.memory import MemoryChatRepository 5 | from logic.init import _init_container 6 | 7 | 8 | def init_dummy_container() -> Container: 9 | container = _init_container() 10 | container.register(BaseChatRepository, MemoryChatRepository, scope=Scope.singleton) 11 | 12 | return container 13 | -------------------------------------------------------------------------------- /app/tests/logic/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greedWizard/ddd-fastapi-kafka-chat/b37e9eba0085f70641021e0e4df1fbc2b28da0f0/app/tests/logic/__init__.py -------------------------------------------------------------------------------- /app/tests/logic/conftest.py: -------------------------------------------------------------------------------- 1 | from tests.logic.conftest import * # noqa 2 | -------------------------------------------------------------------------------- /app/tests/logic/test_messages.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from faker import Faker 4 | 5 | from domain.entities.messages import Chat 6 | from domain.values.messages import Title 7 | from infra.repositories.messages.base import BaseChatRepository 8 | from logic.commands.messages import CreateChatCommand 9 | from logic.exceptions.messages import ChatWithThatTitleAlreadyExistsException 10 | from logic.mediator import Mediator 11 | 12 | 13 | @pytest.mark.asyncio 14 | async def test_create_chat_command_success( 15 | chat_repository: BaseChatRepository, 16 | mediator: Mediator, 17 | faker: Faker, 18 | ): 19 | # TODO: Закинуть фейкер для генерации рандомных текстов 20 | chat: Chat 21 | chat, *_ = await mediator.handle_command(CreateChatCommand(title=faker.text())) 22 | 23 | assert await chat_repository.check_chat_exists_by_title(title=chat.title.as_generic_type()) 24 | 25 | 26 | @pytest.mark.asyncio 27 | async def test_create_chat_command_title_already_exists( 28 | chat_repository: BaseChatRepository, 29 | mediator: Mediator, 30 | faker: Faker, 31 | ): 32 | # TODO: Закинуть фейкер для генерации рандомных текстов 33 | title_text = faker.text() 34 | chat = Chat(title=Title(title_text)) 35 | await chat_repository.add_chat(chat) 36 | 37 | assert chat in chat_repository._saved_chats 38 | 39 | with pytest.raises(ChatWithThatTitleAlreadyExistsException): 40 | await mediator.handle_command(CreateChatCommand(title=title_text)) 41 | 42 | assert len(chat_repository._saved_chats) == 1 43 | -------------------------------------------------------------------------------- /docker_compose/app.yaml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | main-app: 5 | build: 6 | context: .. 7 | dockerfile: Dockerfile 8 | container_name: main-app 9 | ports: 10 | - "${API_PORT}:8000" 11 | command: "uvicorn --factory application.api.main:create_app --reload --host 0.0.0.0 --port 8000" 12 | env_file: 13 | - ../.env 14 | volumes: 15 | - ../app/:/app/ 16 | networks: 17 | - backend 18 | 19 | networks: 20 | backend: 21 | driver: bridge 22 | -------------------------------------------------------------------------------- /docker_compose/storages.yaml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | mongodb: 5 | container_name: chat-mongodb 6 | image: mongo:6-jammy 7 | ports: 8 | - '27017:27017' 9 | volumes: 10 | - dbdata6:/data/db 11 | networks: 12 | - backend 13 | 14 | mongo-express: 15 | image: mongo-express 16 | container_name: mongo-express 17 | restart: always 18 | ports: 19 | - "28081:8081" 20 | environment: 21 | ME_CONFIG_MONGODB_SERVER: mongodb 22 | ME_CONFIG_BASICAUTH_USERNAME: ${MONGO_DB_ADMIN_USERNAME} 23 | ME_CONFIG_BASICAUTH_PASSWORD: ${MONGO_DB_ADMIN_PASSWORD} 24 | ME_CONFIG_MONGODB_URL: mongodb://mongodb:27017/ 25 | depends_on: 26 | - mongodb 27 | networks: 28 | - backend 29 | 30 | volumes: 31 | dbdata6: 32 | 33 | networks: 34 | backend: 35 | driver: bridge 36 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "annotated-types" 5 | version = "0.6.0" 6 | description = "Reusable constraint types to use with typing.Annotated" 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, 11 | {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, 12 | ] 13 | 14 | [[package]] 15 | name = "anyio" 16 | version = "4.3.0" 17 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 18 | optional = false 19 | python-versions = ">=3.8" 20 | files = [ 21 | {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, 22 | {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, 23 | ] 24 | 25 | [package.dependencies] 26 | idna = ">=2.8" 27 | sniffio = ">=1.1" 28 | 29 | [package.extras] 30 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 31 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 32 | trio = ["trio (>=0.23)"] 33 | 34 | [[package]] 35 | name = "asttokens" 36 | version = "2.4.1" 37 | description = "Annotate AST trees with source code positions" 38 | optional = false 39 | python-versions = "*" 40 | files = [ 41 | {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, 42 | {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, 43 | ] 44 | 45 | [package.dependencies] 46 | six = ">=1.12.0" 47 | 48 | [package.extras] 49 | astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] 50 | test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] 51 | 52 | [[package]] 53 | name = "certifi" 54 | version = "2024.2.2" 55 | description = "Python package for providing Mozilla's CA Bundle." 56 | optional = false 57 | python-versions = ">=3.6" 58 | files = [ 59 | {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, 60 | {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, 61 | ] 62 | 63 | [[package]] 64 | name = "click" 65 | version = "8.1.7" 66 | description = "Composable command line interface toolkit" 67 | optional = false 68 | python-versions = ">=3.7" 69 | files = [ 70 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 71 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 72 | ] 73 | 74 | [package.dependencies] 75 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 76 | 77 | [[package]] 78 | name = "colorama" 79 | version = "0.4.6" 80 | description = "Cross-platform colored terminal text." 81 | optional = false 82 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 83 | files = [ 84 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 85 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 86 | ] 87 | 88 | [[package]] 89 | name = "decorator" 90 | version = "5.1.1" 91 | description = "Decorators for Humans" 92 | optional = false 93 | python-versions = ">=3.5" 94 | files = [ 95 | {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, 96 | {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, 97 | ] 98 | 99 | [[package]] 100 | name = "dnspython" 101 | version = "2.6.1" 102 | description = "DNS toolkit" 103 | optional = false 104 | python-versions = ">=3.8" 105 | files = [ 106 | {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, 107 | {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, 108 | ] 109 | 110 | [package.extras] 111 | dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] 112 | dnssec = ["cryptography (>=41)"] 113 | doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] 114 | doq = ["aioquic (>=0.9.25)"] 115 | idna = ["idna (>=3.6)"] 116 | trio = ["trio (>=0.23)"] 117 | wmi = ["wmi (>=1.5.1)"] 118 | 119 | [[package]] 120 | name = "executing" 121 | version = "2.0.1" 122 | description = "Get the currently executing AST node of a frame, and other information" 123 | optional = false 124 | python-versions = ">=3.5" 125 | files = [ 126 | {file = "executing-2.0.1-py2.py3-none-any.whl", hash = "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc"}, 127 | {file = "executing-2.0.1.tar.gz", hash = "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147"}, 128 | ] 129 | 130 | [package.extras] 131 | tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] 132 | 133 | [[package]] 134 | name = "faker" 135 | version = "24.3.0" 136 | description = "Faker is a Python package that generates fake data for you." 137 | optional = false 138 | python-versions = ">=3.8" 139 | files = [ 140 | {file = "Faker-24.3.0-py3-none-any.whl", hash = "sha256:9978025e765ba79f8bf6154c9630a9c2b7f9c9b0f175d4ad5e04b19a82a8d8d6"}, 141 | {file = "Faker-24.3.0.tar.gz", hash = "sha256:5fb5aa9749d09971e04a41281ae3ceda9414f683d4810a694f8a8eebb8f9edec"}, 142 | ] 143 | 144 | [package.dependencies] 145 | python-dateutil = ">=2.4" 146 | 147 | [[package]] 148 | name = "fastapi" 149 | version = "0.110.0" 150 | description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" 151 | optional = false 152 | python-versions = ">=3.8" 153 | files = [ 154 | {file = "fastapi-0.110.0-py3-none-any.whl", hash = "sha256:87a1f6fb632a218222c5984be540055346a8f5d8a68e8f6fb647b1dc9934de4b"}, 155 | {file = "fastapi-0.110.0.tar.gz", hash = "sha256:266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3"}, 156 | ] 157 | 158 | [package.dependencies] 159 | pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" 160 | starlette = ">=0.36.3,<0.37.0" 161 | typing-extensions = ">=4.8.0" 162 | 163 | [package.extras] 164 | all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] 165 | 166 | [[package]] 167 | name = "h11" 168 | version = "0.14.0" 169 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 170 | optional = false 171 | python-versions = ">=3.7" 172 | files = [ 173 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 174 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 175 | ] 176 | 177 | [[package]] 178 | name = "httpcore" 179 | version = "1.0.4" 180 | description = "A minimal low-level HTTP client." 181 | optional = false 182 | python-versions = ">=3.8" 183 | files = [ 184 | {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, 185 | {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, 186 | ] 187 | 188 | [package.dependencies] 189 | certifi = "*" 190 | h11 = ">=0.13,<0.15" 191 | 192 | [package.extras] 193 | asyncio = ["anyio (>=4.0,<5.0)"] 194 | http2 = ["h2 (>=3,<5)"] 195 | socks = ["socksio (==1.*)"] 196 | trio = ["trio (>=0.22.0,<0.25.0)"] 197 | 198 | [[package]] 199 | name = "httpx" 200 | version = "0.27.0" 201 | description = "The next generation HTTP client." 202 | optional = false 203 | python-versions = ">=3.8" 204 | files = [ 205 | {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, 206 | {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, 207 | ] 208 | 209 | [package.dependencies] 210 | anyio = "*" 211 | certifi = "*" 212 | httpcore = "==1.*" 213 | idna = "*" 214 | sniffio = "*" 215 | 216 | [package.extras] 217 | brotli = ["brotli", "brotlicffi"] 218 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 219 | http2 = ["h2 (>=3,<5)"] 220 | socks = ["socksio (==1.*)"] 221 | 222 | [[package]] 223 | name = "idna" 224 | version = "3.6" 225 | description = "Internationalized Domain Names in Applications (IDNA)" 226 | optional = false 227 | python-versions = ">=3.5" 228 | files = [ 229 | {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, 230 | {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, 231 | ] 232 | 233 | [[package]] 234 | name = "iniconfig" 235 | version = "2.0.0" 236 | description = "brain-dead simple config-ini parsing" 237 | optional = false 238 | python-versions = ">=3.7" 239 | files = [ 240 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 241 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 242 | ] 243 | 244 | [[package]] 245 | name = "ipython" 246 | version = "8.22.2" 247 | description = "IPython: Productive Interactive Computing" 248 | optional = false 249 | python-versions = ">=3.10" 250 | files = [ 251 | {file = "ipython-8.22.2-py3-none-any.whl", hash = "sha256:3c86f284c8f3d8f2b6c662f885c4889a91df7cd52056fd02b7d8d6195d7f56e9"}, 252 | {file = "ipython-8.22.2.tar.gz", hash = "sha256:2dcaad9049f9056f1fef63514f176c7d41f930daa78d05b82a176202818f2c14"}, 253 | ] 254 | 255 | [package.dependencies] 256 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 257 | decorator = "*" 258 | jedi = ">=0.16" 259 | matplotlib-inline = "*" 260 | pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} 261 | prompt-toolkit = ">=3.0.41,<3.1.0" 262 | pygments = ">=2.4.0" 263 | stack-data = "*" 264 | traitlets = ">=5.13.0" 265 | 266 | [package.extras] 267 | all = ["ipython[black,doc,kernel,nbconvert,nbformat,notebook,parallel,qtconsole,terminal]", "ipython[test,test-extra]"] 268 | black = ["black"] 269 | doc = ["docrepr", "exceptiongroup", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "stack-data", "typing-extensions"] 270 | kernel = ["ipykernel"] 271 | nbconvert = ["nbconvert"] 272 | nbformat = ["nbformat"] 273 | notebook = ["ipywidgets", "notebook"] 274 | parallel = ["ipyparallel"] 275 | qtconsole = ["qtconsole"] 276 | test = ["pickleshare", "pytest (<8)", "pytest-asyncio (<0.22)", "testpath"] 277 | test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] 278 | 279 | [[package]] 280 | name = "jedi" 281 | version = "0.19.1" 282 | description = "An autocompletion tool for Python that can be used for text editors." 283 | optional = false 284 | python-versions = ">=3.6" 285 | files = [ 286 | {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, 287 | {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, 288 | ] 289 | 290 | [package.dependencies] 291 | parso = ">=0.8.3,<0.9.0" 292 | 293 | [package.extras] 294 | docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] 295 | qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] 296 | testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] 297 | 298 | [[package]] 299 | name = "matplotlib-inline" 300 | version = "0.1.6" 301 | description = "Inline Matplotlib backend for Jupyter" 302 | optional = false 303 | python-versions = ">=3.5" 304 | files = [ 305 | {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, 306 | {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, 307 | ] 308 | 309 | [package.dependencies] 310 | traitlets = "*" 311 | 312 | [[package]] 313 | name = "motor" 314 | version = "3.3.2" 315 | description = "Non-blocking MongoDB driver for Tornado or asyncio" 316 | optional = false 317 | python-versions = ">=3.7" 318 | files = [ 319 | {file = "motor-3.3.2-py3-none-any.whl", hash = "sha256:6fe7e6f0c4f430b9e030b9d22549b732f7c2226af3ab71ecc309e4a1b7d19953"}, 320 | {file = "motor-3.3.2.tar.gz", hash = "sha256:d2fc38de15f1c8058f389c1a44a4d4105c0405c48c061cd492a654496f7bc26a"}, 321 | ] 322 | 323 | [package.dependencies] 324 | pymongo = ">=4.5,<5" 325 | 326 | [package.extras] 327 | aws = ["pymongo[aws] (>=4.5,<5)"] 328 | encryption = ["pymongo[encryption] (>=4.5,<5)"] 329 | gssapi = ["pymongo[gssapi] (>=4.5,<5)"] 330 | ocsp = ["pymongo[ocsp] (>=4.5,<5)"] 331 | snappy = ["pymongo[snappy] (>=4.5,<5)"] 332 | srv = ["pymongo[srv] (>=4.5,<5)"] 333 | test = ["aiohttp (<3.8.6)", "mockupdb", "motor[encryption]", "pytest (>=7)", "tornado (>=5)"] 334 | zstd = ["pymongo[zstd] (>=4.5,<5)"] 335 | 336 | [[package]] 337 | name = "packaging" 338 | version = "24.0" 339 | description = "Core utilities for Python packages" 340 | optional = false 341 | python-versions = ">=3.7" 342 | files = [ 343 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 344 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 345 | ] 346 | 347 | [[package]] 348 | name = "parso" 349 | version = "0.8.3" 350 | description = "A Python Parser" 351 | optional = false 352 | python-versions = ">=3.6" 353 | files = [ 354 | {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, 355 | {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, 356 | ] 357 | 358 | [package.extras] 359 | qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] 360 | testing = ["docopt", "pytest (<6.0.0)"] 361 | 362 | [[package]] 363 | name = "pexpect" 364 | version = "4.9.0" 365 | description = "Pexpect allows easy control of interactive console applications." 366 | optional = false 367 | python-versions = "*" 368 | files = [ 369 | {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, 370 | {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, 371 | ] 372 | 373 | [package.dependencies] 374 | ptyprocess = ">=0.5" 375 | 376 | [[package]] 377 | name = "pluggy" 378 | version = "1.4.0" 379 | description = "plugin and hook calling mechanisms for python" 380 | optional = false 381 | python-versions = ">=3.8" 382 | files = [ 383 | {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, 384 | {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, 385 | ] 386 | 387 | [package.extras] 388 | dev = ["pre-commit", "tox"] 389 | testing = ["pytest", "pytest-benchmark"] 390 | 391 | [[package]] 392 | name = "prompt-toolkit" 393 | version = "3.0.43" 394 | description = "Library for building powerful interactive command lines in Python" 395 | optional = false 396 | python-versions = ">=3.7.0" 397 | files = [ 398 | {file = "prompt_toolkit-3.0.43-py3-none-any.whl", hash = "sha256:a11a29cb3bf0a28a387fe5122cdb649816a957cd9261dcedf8c9f1fef33eacf6"}, 399 | {file = "prompt_toolkit-3.0.43.tar.gz", hash = "sha256:3527b7af26106cbc65a040bcc84839a3566ec1b051bb0bfe953631e704b0ff7d"}, 400 | ] 401 | 402 | [package.dependencies] 403 | wcwidth = "*" 404 | 405 | [[package]] 406 | name = "ptyprocess" 407 | version = "0.7.0" 408 | description = "Run a subprocess in a pseudo terminal" 409 | optional = false 410 | python-versions = "*" 411 | files = [ 412 | {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, 413 | {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, 414 | ] 415 | 416 | [[package]] 417 | name = "punq" 418 | version = "0.7.0" 419 | description = "An IOC Container for Python 3.8+" 420 | optional = false 421 | python-versions = ">=3.8.1,<4.0" 422 | files = [ 423 | {file = "punq-0.7.0-py3-none-any.whl", hash = "sha256:7e0aca446c36a73674ec3fc078ec5e90e7a172ee349537d075ff20d73c3b1810"}, 424 | {file = "punq-0.7.0.tar.gz", hash = "sha256:bb7a6cc75a2e7d51b861b0e11f4830a12617b3ee33dbced9ce2be6a98ba39d63"}, 425 | ] 426 | 427 | [[package]] 428 | name = "pure-eval" 429 | version = "0.2.2" 430 | description = "Safely evaluate AST nodes without side effects" 431 | optional = false 432 | python-versions = "*" 433 | files = [ 434 | {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, 435 | {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, 436 | ] 437 | 438 | [package.extras] 439 | tests = ["pytest"] 440 | 441 | [[package]] 442 | name = "pydantic" 443 | version = "2.6.4" 444 | description = "Data validation using Python type hints" 445 | optional = false 446 | python-versions = ">=3.8" 447 | files = [ 448 | {file = "pydantic-2.6.4-py3-none-any.whl", hash = "sha256:cc46fce86607580867bdc3361ad462bab9c222ef042d3da86f2fb333e1d916c5"}, 449 | {file = "pydantic-2.6.4.tar.gz", hash = "sha256:b1704e0847db01817624a6b86766967f552dd9dbf3afba4004409f908dcc84e6"}, 450 | ] 451 | 452 | [package.dependencies] 453 | annotated-types = ">=0.4.0" 454 | pydantic-core = "2.16.3" 455 | typing-extensions = ">=4.6.1" 456 | 457 | [package.extras] 458 | email = ["email-validator (>=2.0.0)"] 459 | 460 | [[package]] 461 | name = "pydantic-core" 462 | version = "2.16.3" 463 | description = "" 464 | optional = false 465 | python-versions = ">=3.8" 466 | files = [ 467 | {file = "pydantic_core-2.16.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:75b81e678d1c1ede0785c7f46690621e4c6e63ccd9192af1f0bd9d504bbb6bf4"}, 468 | {file = "pydantic_core-2.16.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9c865a7ee6f93783bd5d781af5a4c43dadc37053a5b42f7d18dc019f8c9d2bd1"}, 469 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:162e498303d2b1c036b957a1278fa0899d02b2842f1ff901b6395104c5554a45"}, 470 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2f583bd01bbfbff4eaee0868e6fc607efdfcc2b03c1c766b06a707abbc856187"}, 471 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b926dd38db1519ed3043a4de50214e0d600d404099c3392f098a7f9d75029ff8"}, 472 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:716b542728d4c742353448765aa7cdaa519a7b82f9564130e2b3f6766018c9ec"}, 473 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc4ad7f7ee1a13d9cb49d8198cd7d7e3aa93e425f371a68235f784e99741561f"}, 474 | {file = "pydantic_core-2.16.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bd87f48924f360e5d1c5f770d6155ce0e7d83f7b4e10c2f9ec001c73cf475c99"}, 475 | {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0df446663464884297c793874573549229f9eca73b59360878f382a0fc085979"}, 476 | {file = "pydantic_core-2.16.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4df8a199d9f6afc5ae9a65f8f95ee52cae389a8c6b20163762bde0426275b7db"}, 477 | {file = "pydantic_core-2.16.3-cp310-none-win32.whl", hash = "sha256:456855f57b413f077dff513a5a28ed838dbbb15082ba00f80750377eed23d132"}, 478 | {file = "pydantic_core-2.16.3-cp310-none-win_amd64.whl", hash = "sha256:732da3243e1b8d3eab8c6ae23ae6a58548849d2e4a4e03a1924c8ddf71a387cb"}, 479 | {file = "pydantic_core-2.16.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:519ae0312616026bf4cedc0fe459e982734f3ca82ee8c7246c19b650b60a5ee4"}, 480 | {file = "pydantic_core-2.16.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b3992a322a5617ded0a9f23fd06dbc1e4bd7cf39bc4ccf344b10f80af58beacd"}, 481 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d62da299c6ecb04df729e4b5c52dc0d53f4f8430b4492b93aa8de1f541c4aac"}, 482 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2acca2be4bb2f2147ada8cac612f8a98fc09f41c89f87add7256ad27332c2fda"}, 483 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b662180108c55dfbf1280d865b2d116633d436cfc0bba82323554873967b340"}, 484 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7c6ed0dc9d8e65f24f5824291550139fe6f37fac03788d4580da0d33bc00c97"}, 485 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1bb0827f56654b4437955555dc3aeeebeddc47c2d7ed575477f082622c49e"}, 486 | {file = "pydantic_core-2.16.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e56f8186d6210ac7ece503193ec84104da7ceb98f68ce18c07282fcc2452e76f"}, 487 | {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:936e5db01dd49476fa8f4383c259b8b1303d5dd5fb34c97de194560698cc2c5e"}, 488 | {file = "pydantic_core-2.16.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33809aebac276089b78db106ee692bdc9044710e26f24a9a2eaa35a0f9fa70ba"}, 489 | {file = "pydantic_core-2.16.3-cp311-none-win32.whl", hash = "sha256:ded1c35f15c9dea16ead9bffcde9bb5c7c031bff076355dc58dcb1cb436c4721"}, 490 | {file = "pydantic_core-2.16.3-cp311-none-win_amd64.whl", hash = "sha256:d89ca19cdd0dd5f31606a9329e309d4fcbb3df860960acec32630297d61820df"}, 491 | {file = "pydantic_core-2.16.3-cp311-none-win_arm64.whl", hash = "sha256:6162f8d2dc27ba21027f261e4fa26f8bcb3cf9784b7f9499466a311ac284b5b9"}, 492 | {file = "pydantic_core-2.16.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f56ae86b60ea987ae8bcd6654a887238fd53d1384f9b222ac457070b7ac4cff"}, 493 | {file = "pydantic_core-2.16.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9bd22a2a639e26171068f8ebb5400ce2c1bc7d17959f60a3b753ae13c632975"}, 494 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4204e773b4b408062960e65468d5346bdfe139247ee5f1ca2a378983e11388a2"}, 495 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f651dd19363c632f4abe3480a7c87a9773be27cfe1341aef06e8759599454120"}, 496 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aaf09e615a0bf98d406657e0008e4a8701b11481840be7d31755dc9f97c44053"}, 497 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e47755d8152c1ab5b55928ab422a76e2e7b22b5ed8e90a7d584268dd49e9c6b"}, 498 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:500960cb3a0543a724a81ba859da816e8cf01b0e6aaeedf2c3775d12ee49cade"}, 499 | {file = "pydantic_core-2.16.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf6204fe865da605285c34cf1172879d0314ff267b1c35ff59de7154f35fdc2e"}, 500 | {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d33dd21f572545649f90c38c227cc8631268ba25c460b5569abebdd0ec5974ca"}, 501 | {file = "pydantic_core-2.16.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:49d5d58abd4b83fb8ce763be7794d09b2f50f10aa65c0f0c1696c677edeb7cbf"}, 502 | {file = "pydantic_core-2.16.3-cp312-none-win32.whl", hash = "sha256:f53aace168a2a10582e570b7736cc5bef12cae9cf21775e3eafac597e8551fbe"}, 503 | {file = "pydantic_core-2.16.3-cp312-none-win_amd64.whl", hash = "sha256:0d32576b1de5a30d9a97f300cc6a3f4694c428d956adbc7e6e2f9cad279e45ed"}, 504 | {file = "pydantic_core-2.16.3-cp312-none-win_arm64.whl", hash = "sha256:ec08be75bb268473677edb83ba71e7e74b43c008e4a7b1907c6d57e940bf34b6"}, 505 | {file = "pydantic_core-2.16.3-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:b1f6f5938d63c6139860f044e2538baeee6f0b251a1816e7adb6cbce106a1f01"}, 506 | {file = "pydantic_core-2.16.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2a1ef6a36fdbf71538142ed604ad19b82f67b05749512e47f247a6ddd06afdc7"}, 507 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:704d35ecc7e9c31d48926150afada60401c55efa3b46cd1ded5a01bdffaf1d48"}, 508 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d937653a696465677ed583124b94a4b2d79f5e30b2c46115a68e482c6a591c8a"}, 509 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c9803edf8e29bd825f43481f19c37f50d2b01899448273b3a7758441b512acf8"}, 510 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:72282ad4892a9fb2da25defeac8c2e84352c108705c972db82ab121d15f14e6d"}, 511 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f752826b5b8361193df55afcdf8ca6a57d0232653494ba473630a83ba50d8c9"}, 512 | {file = "pydantic_core-2.16.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4384a8f68ddb31a0b0c3deae88765f5868a1b9148939c3f4121233314ad5532c"}, 513 | {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a4b2bf78342c40b3dc830880106f54328928ff03e357935ad26c7128bbd66ce8"}, 514 | {file = "pydantic_core-2.16.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:13dcc4802961b5f843a9385fc821a0b0135e8c07fc3d9949fd49627c1a5e6ae5"}, 515 | {file = "pydantic_core-2.16.3-cp38-none-win32.whl", hash = "sha256:e3e70c94a0c3841e6aa831edab1619ad5c511199be94d0c11ba75fe06efe107a"}, 516 | {file = "pydantic_core-2.16.3-cp38-none-win_amd64.whl", hash = "sha256:ecdf6bf5f578615f2e985a5e1f6572e23aa632c4bd1dc67f8f406d445ac115ed"}, 517 | {file = "pydantic_core-2.16.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bda1ee3e08252b8d41fa5537413ffdddd58fa73107171a126d3b9ff001b9b820"}, 518 | {file = "pydantic_core-2.16.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:21b888c973e4f26b7a96491c0965a8a312e13be108022ee510248fe379a5fa23"}, 519 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be0ec334369316fa73448cc8c982c01e5d2a81c95969d58b8f6e272884df0074"}, 520 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b5b6079cc452a7c53dd378c6f881ac528246b3ac9aae0f8eef98498a75657805"}, 521 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ee8d5f878dccb6d499ba4d30d757111847b6849ae07acdd1205fffa1fc1253c"}, 522 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7233d65d9d651242a68801159763d09e9ec96e8a158dbf118dc090cd77a104c9"}, 523 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6119dc90483a5cb50a1306adb8d52c66e447da88ea44f323e0ae1a5fcb14256"}, 524 | {file = "pydantic_core-2.16.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:578114bc803a4c1ff9946d977c221e4376620a46cf78da267d946397dc9514a8"}, 525 | {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d8f99b147ff3fcf6b3cc60cb0c39ea443884d5559a30b1481e92495f2310ff2b"}, 526 | {file = "pydantic_core-2.16.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4ac6b4ce1e7283d715c4b729d8f9dab9627586dafce81d9eaa009dd7f25dd972"}, 527 | {file = "pydantic_core-2.16.3-cp39-none-win32.whl", hash = "sha256:e7774b570e61cb998490c5235740d475413a1f6de823169b4cf94e2fe9e9f6b2"}, 528 | {file = "pydantic_core-2.16.3-cp39-none-win_amd64.whl", hash = "sha256:9091632a25b8b87b9a605ec0e61f241c456e9248bfdcf7abdf344fdb169c81cf"}, 529 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:36fa178aacbc277bc6b62a2c3da95226520da4f4e9e206fdf076484363895d2c"}, 530 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:dcca5d2bf65c6fb591fff92da03f94cd4f315972f97c21975398bd4bd046854a"}, 531 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a72fb9963cba4cd5793854fd12f4cfee731e86df140f59ff52a49b3552db241"}, 532 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60cc1a081f80a2105a59385b92d82278b15d80ebb3adb200542ae165cd7d183"}, 533 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cbcc558401de90a746d02ef330c528f2e668c83350f045833543cd57ecead1ad"}, 534 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:fee427241c2d9fb7192b658190f9f5fd6dfe41e02f3c1489d2ec1e6a5ab1e04a"}, 535 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f4cb85f693044e0f71f394ff76c98ddc1bc0953e48c061725e540396d5c8a2e1"}, 536 | {file = "pydantic_core-2.16.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:b29eeb887aa931c2fcef5aa515d9d176d25006794610c264ddc114c053bf96fe"}, 537 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a425479ee40ff021f8216c9d07a6a3b54b31c8267c6e17aa88b70d7ebd0e5e5b"}, 538 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5c5cbc703168d1b7a838668998308018a2718c2130595e8e190220238addc96f"}, 539 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b6add4c0b39a513d323d3b93bc173dac663c27b99860dd5bf491b240d26137"}, 540 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f76ee558751746d6a38f89d60b6228fa174e5172d143886af0f85aa306fd89"}, 541 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:00ee1c97b5364b84cb0bd82e9bbf645d5e2871fb8c58059d158412fee2d33d8a"}, 542 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:287073c66748f624be4cef893ef9174e3eb88fe0b8a78dc22e88eca4bc357ca6"}, 543 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed25e1835c00a332cb10c683cd39da96a719ab1dfc08427d476bce41b92531fc"}, 544 | {file = "pydantic_core-2.16.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:86b3d0033580bd6bbe07590152007275bd7af95f98eaa5bd36f3da219dcd93da"}, 545 | {file = "pydantic_core-2.16.3.tar.gz", hash = "sha256:1cac689f80a3abab2d3c0048b29eea5751114054f032a941a32de4c852c59cad"}, 546 | ] 547 | 548 | [package.dependencies] 549 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 550 | 551 | [[package]] 552 | name = "pydantic-settings" 553 | version = "2.2.1" 554 | description = "Settings management using Pydantic" 555 | optional = false 556 | python-versions = ">=3.8" 557 | files = [ 558 | {file = "pydantic_settings-2.2.1-py3-none-any.whl", hash = "sha256:0235391d26db4d2190cb9b31051c4b46882d28a51533f97440867f012d4da091"}, 559 | {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, 560 | ] 561 | 562 | [package.dependencies] 563 | pydantic = ">=2.3.0" 564 | python-dotenv = ">=0.21.0" 565 | 566 | [package.extras] 567 | toml = ["tomli (>=2.0.1)"] 568 | yaml = ["pyyaml (>=6.0.1)"] 569 | 570 | [[package]] 571 | name = "pygments" 572 | version = "2.17.2" 573 | description = "Pygments is a syntax highlighting package written in Python." 574 | optional = false 575 | python-versions = ">=3.7" 576 | files = [ 577 | {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, 578 | {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, 579 | ] 580 | 581 | [package.extras] 582 | plugins = ["importlib-metadata"] 583 | windows-terminal = ["colorama (>=0.4.6)"] 584 | 585 | [[package]] 586 | name = "pymongo" 587 | version = "4.6.2" 588 | description = "Python driver for MongoDB " 589 | optional = false 590 | python-versions = ">=3.7" 591 | files = [ 592 | {file = "pymongo-4.6.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7640d176ee5b0afec76a1bda3684995cb731b2af7fcfd7c7ef8dc271c5d689af"}, 593 | {file = "pymongo-4.6.2-cp310-cp310-manylinux1_i686.whl", hash = "sha256:4e2129ec8f72806751b621470ac5d26aaa18fae4194796621508fa0e6068278a"}, 594 | {file = "pymongo-4.6.2-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:c43205e85cbcbdf03cff62ad8f50426dd9d20134a915cfb626d805bab89a1844"}, 595 | {file = "pymongo-4.6.2-cp310-cp310-manylinux2014_i686.whl", hash = "sha256:91ddf95cedca12f115fbc5f442b841e81197d85aa3cc30b82aee3635a5208af2"}, 596 | {file = "pymongo-4.6.2-cp310-cp310-manylinux2014_ppc64le.whl", hash = "sha256:0fbdbf2fba1b4f5f1522e9f11e21c306e095b59a83340a69e908f8ed9b450070"}, 597 | {file = "pymongo-4.6.2-cp310-cp310-manylinux2014_s390x.whl", hash = "sha256:097791d5a8d44e2444e0c8c4d6e14570ac11e22bcb833808885a5db081c3dc2a"}, 598 | {file = "pymongo-4.6.2-cp310-cp310-manylinux2014_x86_64.whl", hash = "sha256:e0b208ebec3b47ee78a5c836e2e885e8c1e10f8ffd101aaec3d63997a4bdcd04"}, 599 | {file = "pymongo-4.6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1849fd6f1917b4dc5dbf744b2f18e41e0538d08dd8e9ba9efa811c5149d665a3"}, 600 | {file = "pymongo-4.6.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa0bbbfbd1f8ebbd5facaa10f9f333b20027b240af012748555148943616fdf3"}, 601 | {file = "pymongo-4.6.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4522ad69a4ab0e1b46a8367d62ad3865b8cd54cf77518c157631dac1fdc97584"}, 602 | {file = "pymongo-4.6.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397949a9cc85e4a1452f80b7f7f2175d557237177120954eff00bf79553e89d3"}, 603 | {file = "pymongo-4.6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9d511db310f43222bc58d811037b176b4b88dc2b4617478c5ef01fea404f8601"}, 604 | {file = "pymongo-4.6.2-cp310-cp310-win32.whl", hash = "sha256:991e406db5da4d89fb220a94d8caaf974ffe14ce6b095957bae9273c609784a0"}, 605 | {file = "pymongo-4.6.2-cp310-cp310-win_amd64.whl", hash = "sha256:94637941fe343000f728e28d3fe04f1f52aec6376b67b85583026ff8dab2a0e0"}, 606 | {file = "pymongo-4.6.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:84593447a5c5fe7a59ba86b72c2c89d813fbac71c07757acdf162fbfd5d005b9"}, 607 | {file = "pymongo-4.6.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9aebddb2ec2128d5fc2fe3aee6319afef8697e0374f8a1fcca3449d6f625e7b4"}, 608 | {file = "pymongo-4.6.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f706c1a644ed33eaea91df0a8fb687ce572b53eeb4ff9b89270cb0247e5d0e1"}, 609 | {file = "pymongo-4.6.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18c422e6b08fa370ed9d8670c67e78d01f50d6517cec4522aa8627014dfa38b6"}, 610 | {file = "pymongo-4.6.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d002ae456a15b1d790a78bb84f87af21af1cb716a63efb2c446ab6bcbbc48ca"}, 611 | {file = "pymongo-4.6.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f86ba0c781b497a3c9c886765d7b6402a0e3ae079dd517365044c89cd7abb06"}, 612 | {file = "pymongo-4.6.2-cp311-cp311-win32.whl", hash = "sha256:ac20dd0c7b42555837c86f5ea46505f35af20a08b9cf5770cd1834288d8bd1b4"}, 613 | {file = "pymongo-4.6.2-cp311-cp311-win_amd64.whl", hash = "sha256:e78af59fd0eb262c2a5f7c7d7e3b95e8596a75480d31087ca5f02f2d4c6acd19"}, 614 | {file = "pymongo-4.6.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:6125f73503407792c8b3f80165f8ab88a4e448d7d9234c762681a4d0b446fcb4"}, 615 | {file = "pymongo-4.6.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba052446a14bd714ec83ca4e77d0d97904f33cd046d7bb60712a6be25eb31dbb"}, 616 | {file = "pymongo-4.6.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b65433c90e07dc252b4a55dfd885ca0df94b1cf77c5b8709953ec1983aadc03"}, 617 | {file = "pymongo-4.6.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2160d9c8cd20ce1f76a893f0daf7c0d38af093f36f1b5c9f3dcf3e08f7142814"}, 618 | {file = "pymongo-4.6.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f251f287e6d42daa3654b686ce1fcb6d74bf13b3907c3ae25954978c70f2cd4"}, 619 | {file = "pymongo-4.6.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d7d227a60b00925dd3aeae4675575af89c661a8e89a1f7d1677e57eba4a3693c"}, 620 | {file = "pymongo-4.6.2-cp312-cp312-win32.whl", hash = "sha256:311794ef3ccae374aaef95792c36b0e5c06e8d5cf04a1bdb1b2bf14619ac881f"}, 621 | {file = "pymongo-4.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:f673b64a0884edcc56073bda0b363428dc1bf4eb1b5e7d0b689f7ec6173edad6"}, 622 | {file = "pymongo-4.6.2-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:fe010154dfa9e428bd2fb3e9325eff2216ab20a69ccbd6b5cac6785ca2989161"}, 623 | {file = "pymongo-4.6.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:1f5f4cd2969197e25b67e24d5b8aa2452d381861d2791d06c493eaa0b9c9fcfe"}, 624 | {file = "pymongo-4.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c9519c9d341983f3a1bd19628fecb1d72a48d8666cf344549879f2e63f54463b"}, 625 | {file = "pymongo-4.6.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c68bf4a399e37798f1b5aa4f6c02886188ef465f4ac0b305a607b7579413e366"}, 626 | {file = "pymongo-4.6.2-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:a509db602462eb736666989739215b4b7d8f4bb8ac31d0bffd4be9eae96c63ef"}, 627 | {file = "pymongo-4.6.2-cp37-cp37m-manylinux2014_ppc64le.whl", hash = "sha256:362a5adf6f3f938a8ff220a4c4aaa93e84ef932a409abecd837c617d17a5990f"}, 628 | {file = "pymongo-4.6.2-cp37-cp37m-manylinux2014_s390x.whl", hash = "sha256:ee30a9d4c27a88042d0636aca0275788af09cc237ae365cd6ebb34524bddb9cc"}, 629 | {file = "pymongo-4.6.2-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:477914e13501bb1d4608339ee5bb618be056d2d0e7267727623516cfa902e652"}, 630 | {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd343ca44982d480f1e39372c48e8e263fc6f32e9af2be456298f146a3db715"}, 631 | {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3797e0a628534e07a36544d2bfa69e251a578c6d013e975e9e3ed2ac41f2d95"}, 632 | {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97d81d357e1a2a248b3494d52ebc8bf15d223ee89d59ee63becc434e07438a24"}, 633 | {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed694c0d1977cb54281cb808bc2b247c17fb64b678a6352d3b77eb678ebe1bd9"}, 634 | {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ceaaff4b812ae368cf9774989dea81b9bbb71e5bed666feca6a9f3087c03e49"}, 635 | {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7dd63f7c2b3727541f7f37d0fb78d9942eb12a866180fbeb898714420aad74e2"}, 636 | {file = "pymongo-4.6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e571434633f99a81e081738721bb38e697345281ed2f79c2f290f809ba3fbb2f"}, 637 | {file = "pymongo-4.6.2-cp37-cp37m-win32.whl", hash = "sha256:3e9f6e2f3da0a6af854a3e959a6962b5f8b43bbb8113cd0bff0421c5059b3106"}, 638 | {file = "pymongo-4.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:3a5280f496297537301e78bde250c96fadf4945e7b2c397d8bb8921861dd236d"}, 639 | {file = "pymongo-4.6.2-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:5f6bcd2d012d82d25191a911a239fd05a8a72e8c5a7d81d056c0f3520cad14d1"}, 640 | {file = "pymongo-4.6.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:4fa30494601a6271a8b416554bd7cde7b2a848230f0ec03e3f08d84565b4bf8c"}, 641 | {file = "pymongo-4.6.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:bea62f03a50f363265a7a651b4e2a4429b4f138c1864b2d83d4bf6f9851994be"}, 642 | {file = "pymongo-4.6.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:b2d445f1cf147331947cc35ec10342f898329f29dd1947a3f8aeaf7e0e6878d1"}, 643 | {file = "pymongo-4.6.2-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:5db133d6ec7a4f7fc7e2bd098e4df23d7ad949f7be47b27b515c9fb9301c61e4"}, 644 | {file = "pymongo-4.6.2-cp38-cp38-manylinux2014_ppc64le.whl", hash = "sha256:9eec7140cf7513aa770ea51505d312000c7416626a828de24318fdcc9ac3214c"}, 645 | {file = "pymongo-4.6.2-cp38-cp38-manylinux2014_s390x.whl", hash = "sha256:5379ca6fd325387a34cda440aec2bd031b5ef0b0aa2e23b4981945cff1dab84c"}, 646 | {file = "pymongo-4.6.2-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:579508536113dbd4c56e4738955a18847e8a6c41bf3c0b4ab18b51d81a6b7be8"}, 647 | {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3bae553ca39ed52db099d76acd5e8566096064dc7614c34c9359bb239ec4081"}, 648 | {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0257e0eebb50f242ca28a92ef195889a6ad03dcdde5bf1c7ab9f38b7e810801"}, 649 | {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbafe3a1df21eeadb003c38fc02c1abf567648b6477ec50c4a3c042dca205371"}, 650 | {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaecfafb407feb6f562c7f2f5b91f22bfacba6dd739116b1912788cff7124c4a"}, 651 | {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e942945e9112075a84d2e2d6e0d0c98833cdcdfe48eb8952b917f996025c7ffa"}, 652 | {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2f7b98f8d2cf3eeebde738d080ae9b4276d7250912d9751046a9ac1efc9b1ce2"}, 653 | {file = "pymongo-4.6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8110b78fc4b37dced85081d56795ecbee6a7937966e918e05e33a3900e8ea07d"}, 654 | {file = "pymongo-4.6.2-cp38-cp38-win32.whl", hash = "sha256:df813f0c2c02281720ccce225edf39dc37855bf72cdfde6f789a1d1cf32ffb4b"}, 655 | {file = "pymongo-4.6.2-cp38-cp38-win_amd64.whl", hash = "sha256:64ec3e2dcab9af61bdbfcb1dd863c70d1b0c220b8e8ac11df8b57f80ee0402b3"}, 656 | {file = "pymongo-4.6.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bff601fbfcecd2166d9a2b70777c2985cb9689e2befb3278d91f7f93a0456cae"}, 657 | {file = "pymongo-4.6.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:f1febca6f79e91feafc572906871805bd9c271b6a2d98a8bb5499b6ace0befed"}, 658 | {file = "pymongo-4.6.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d788cb5cc947d78934be26eef1623c78cec3729dc93a30c23f049b361aa6d835"}, 659 | {file = "pymongo-4.6.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5c2f258489de12a65b81e1b803a531ee8cf633fa416ae84de65cd5f82d2ceb37"}, 660 | {file = "pymongo-4.6.2-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:fb24abcd50501b25d33a074c1790a1389b6460d2509e4b240d03fd2e5c79f463"}, 661 | {file = "pymongo-4.6.2-cp39-cp39-manylinux2014_ppc64le.whl", hash = "sha256:4d982c6db1da7cf3018183891883660ad085de97f21490d314385373f775915b"}, 662 | {file = "pymongo-4.6.2-cp39-cp39-manylinux2014_s390x.whl", hash = "sha256:b2dd8c874927a27995f64a3b44c890e8a944c98dec1ba79eab50e07f1e3f801b"}, 663 | {file = "pymongo-4.6.2-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:4993593de44c741d1e9f230f221fe623179f500765f9855936e4ff6f33571bad"}, 664 | {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:658f6c028edaeb02761ebcaca8d44d519c22594b2a51dcbc9bd2432aa93319e3"}, 665 | {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68109c13176749fbbbbbdb94dd4a58dcc604db6ea43ee300b2602154aebdd55f"}, 666 | {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:707d28a822b918acf941cff590affaddb42a5d640614d71367c8956623a80cbc"}, 667 | {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f251db26c239aec2a4d57fbe869e0a27b7f6b5384ec6bf54aeb4a6a5e7408234"}, 668 | {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57c05f2e310701fc17ae358caafd99b1830014e316f0242d13ab6c01db0ab1c2"}, 669 | {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b575fbe6396bbf21e4d0e5fd2e3cdb656dc90c930b6c5532192e9a89814f72d"}, 670 | {file = "pymongo-4.6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ca5877754f3fa6e4fe5aacf5c404575f04c2d9efc8d22ed39576ed9098d555c8"}, 671 | {file = "pymongo-4.6.2-cp39-cp39-win32.whl", hash = "sha256:8caa73fb19070008e851a589b744aaa38edd1366e2487284c61158c77fdf72af"}, 672 | {file = "pymongo-4.6.2-cp39-cp39-win_amd64.whl", hash = "sha256:3e03c732cb64b96849310e1d8688fb70d75e2571385485bf2f1e7ad1d309fa53"}, 673 | {file = "pymongo-4.6.2.tar.gz", hash = "sha256:ab7d01ac832a1663dad592ccbd92bb0f0775bc8f98a1923c5e1a7d7fead495af"}, 674 | ] 675 | 676 | [package.dependencies] 677 | dnspython = ">=1.16.0,<3.0.0" 678 | 679 | [package.extras] 680 | aws = ["pymongo-auth-aws (<2.0.0)"] 681 | encryption = ["certifi", "pymongo[aws]", "pymongocrypt (>=1.6.0,<2.0.0)"] 682 | gssapi = ["pykerberos", "winkerberos (>=0.5.0)"] 683 | ocsp = ["certifi", "cryptography (>=2.5)", "pyopenssl (>=17.2.0)", "requests (<3.0.0)", "service-identity (>=18.1.0)"] 684 | snappy = ["python-snappy"] 685 | test = ["pytest (>=7)"] 686 | zstd = ["zstandard"] 687 | 688 | [[package]] 689 | name = "pytest" 690 | version = "8.1.1" 691 | description = "pytest: simple powerful testing with Python" 692 | optional = false 693 | python-versions = ">=3.8" 694 | files = [ 695 | {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, 696 | {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, 697 | ] 698 | 699 | [package.dependencies] 700 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 701 | iniconfig = "*" 702 | packaging = "*" 703 | pluggy = ">=1.4,<2.0" 704 | 705 | [package.extras] 706 | testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 707 | 708 | [[package]] 709 | name = "pytest-asyncio" 710 | version = "0.23.6" 711 | description = "Pytest support for asyncio" 712 | optional = false 713 | python-versions = ">=3.8" 714 | files = [ 715 | {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"}, 716 | {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"}, 717 | ] 718 | 719 | [package.dependencies] 720 | pytest = ">=7.0.0,<9" 721 | 722 | [package.extras] 723 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] 724 | testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] 725 | 726 | [[package]] 727 | name = "python-dateutil" 728 | version = "2.9.0.post0" 729 | description = "Extensions to the standard Python datetime module" 730 | optional = false 731 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 732 | files = [ 733 | {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, 734 | {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, 735 | ] 736 | 737 | [package.dependencies] 738 | six = ">=1.5" 739 | 740 | [[package]] 741 | name = "python-dotenv" 742 | version = "1.0.1" 743 | description = "Read key-value pairs from a .env file and set them as environment variables" 744 | optional = false 745 | python-versions = ">=3.8" 746 | files = [ 747 | {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, 748 | {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, 749 | ] 750 | 751 | [package.extras] 752 | cli = ["click (>=5.0)"] 753 | 754 | [[package]] 755 | name = "six" 756 | version = "1.16.0" 757 | description = "Python 2 and 3 compatibility utilities" 758 | optional = false 759 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 760 | files = [ 761 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 762 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 763 | ] 764 | 765 | [[package]] 766 | name = "sniffio" 767 | version = "1.3.1" 768 | description = "Sniff out which async library your code is running under" 769 | optional = false 770 | python-versions = ">=3.7" 771 | files = [ 772 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 773 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 774 | ] 775 | 776 | [[package]] 777 | name = "stack-data" 778 | version = "0.6.3" 779 | description = "Extract data from python stack frames and tracebacks for informative displays" 780 | optional = false 781 | python-versions = "*" 782 | files = [ 783 | {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, 784 | {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, 785 | ] 786 | 787 | [package.dependencies] 788 | asttokens = ">=2.1.0" 789 | executing = ">=1.2.0" 790 | pure-eval = "*" 791 | 792 | [package.extras] 793 | tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] 794 | 795 | [[package]] 796 | name = "starlette" 797 | version = "0.36.3" 798 | description = "The little ASGI library that shines." 799 | optional = false 800 | python-versions = ">=3.8" 801 | files = [ 802 | {file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"}, 803 | {file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"}, 804 | ] 805 | 806 | [package.dependencies] 807 | anyio = ">=3.4.0,<5" 808 | 809 | [package.extras] 810 | full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] 811 | 812 | [[package]] 813 | name = "traitlets" 814 | version = "5.14.2" 815 | description = "Traitlets Python configuration system" 816 | optional = false 817 | python-versions = ">=3.8" 818 | files = [ 819 | {file = "traitlets-5.14.2-py3-none-any.whl", hash = "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80"}, 820 | {file = "traitlets-5.14.2.tar.gz", hash = "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9"}, 821 | ] 822 | 823 | [package.extras] 824 | docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] 825 | test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.1)", "pytest-mock", "pytest-mypy-testing"] 826 | 827 | [[package]] 828 | name = "typing-extensions" 829 | version = "4.10.0" 830 | description = "Backported and Experimental Type Hints for Python 3.8+" 831 | optional = false 832 | python-versions = ">=3.8" 833 | files = [ 834 | {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, 835 | {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, 836 | ] 837 | 838 | [[package]] 839 | name = "uvicorn" 840 | version = "0.28.0" 841 | description = "The lightning-fast ASGI server." 842 | optional = false 843 | python-versions = ">=3.8" 844 | files = [ 845 | {file = "uvicorn-0.28.0-py3-none-any.whl", hash = "sha256:6623abbbe6176204a4226e67607b4d52cc60ff62cda0ff177613645cefa2ece1"}, 846 | {file = "uvicorn-0.28.0.tar.gz", hash = "sha256:cab4473b5d1eaeb5a0f6375ac4bc85007ffc75c3cc1768816d9e5d589857b067"}, 847 | ] 848 | 849 | [package.dependencies] 850 | click = ">=7.0" 851 | h11 = ">=0.8" 852 | 853 | [package.extras] 854 | standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] 855 | 856 | [[package]] 857 | name = "wcwidth" 858 | version = "0.2.13" 859 | description = "Measures the displayed width of unicode strings in a terminal" 860 | optional = false 861 | python-versions = "*" 862 | files = [ 863 | {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, 864 | {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, 865 | ] 866 | 867 | [metadata] 868 | lock-version = "2.0" 869 | python-versions = "^3.11" 870 | content-hash = "8efa3c2eeef44e2641c67fbf536667fdbfc8fbdcc25d6bba8c728ecd3c8c1643" 871 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "fsatapi-example" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["vyacheslav "] 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.11" 10 | fastapi = {extras = ["httpx"], version = "^0.110.0"} 11 | motor = "^3.3.2" 12 | uvicorn = {extras = ["all"], version = "^0.28.0"} 13 | ipython = "^8.22.2" 14 | pytest = "^8.1.1" 15 | pytest-asyncio = "^0.23.6" 16 | faker = "^24.3.0" 17 | punq = "^0.7.0" 18 | httpx = "^0.27.0" 19 | pydantic-settings = "^2.2.1" 20 | 21 | 22 | [build-system] 23 | requires = ["poetry-core"] 24 | build-backend = "poetry.core.masonry.api" 25 | --------------------------------------------------------------------------------