diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..85e7c1d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea/ diff --git a/REST-API-cliente-mqtt-consultas/Dockerfile b/REST-API-cliente-mqtt-consultas/Dockerfile new file mode 100644 index 0000000..444b633 --- /dev/null +++ b/REST-API-cliente-mqtt-consultas/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.11-slim +WORKDIR / + +COPY . . +COPY requirements.txt requirements.txt + +RUN pip install -r requirements.txt; apt-get update && apt-get install -y tzdata + +EXPOSE 8000 \ No newline at end of file diff --git a/REST-API-cliente-mqtt-consultas/__pycache__/main.cpython-311.pyc b/REST-API-cliente-mqtt-consultas/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..9ddd7ad Binary files /dev/null and b/REST-API-cliente-mqtt-consultas/__pycache__/main.cpython-311.pyc differ diff --git a/REST-API-cliente-mqtt-consultas/docker-compose.yaml b/REST-API-cliente-mqtt-consultas/docker-compose.yaml new file mode 100644 index 0000000..d654296 --- /dev/null +++ b/REST-API-cliente-mqtt-consultas/docker-compose.yaml @@ -0,0 +1,44 @@ +services: + mongodb: + image: mongo:latest + restart: always + ports: + - "27017:27017" + environment: + MONGO_INITDB_ROOT_USERNAME: admin + MONGO_INITDB_ROOT_PASSWORD: secret + TZ: America/Sao_Paulo + volumes: + - mongo_data:/data/db + + broker: + image: eclipse-mosquitto:latest + command: mosquitto -c /mosquitto/config/mosquitto.conf -v + volumes: + - ./mosquitto/config/mosquitto.conf:/mosquitto/config/mosquitto.conf + + publisher: + image: python:latest + volumes: + - ./:/usr/src/app + working_dir: /usr/src/app + + command: bash -c "pip install -r requirements.txt; python publisher.py" + + web: + build: + dockerfile: Dockerfile + ports: + - "8000:8000" + environment: + - DB_URI=mongodb://admin:secret@mongodb:27017 + - DB_NAME=meu_banco + - COLLECTION_NAME=minha_colecao + command: uvicorn main:app --host 0.0.0.0 --port 8000 + depends_on: + - mongodb + - broker + + +volumes: + mongo_data: \ No newline at end of file diff --git a/REST-API-cliente-mqtt-consultas/main.py b/REST-API-cliente-mqtt-consultas/main.py new file mode 100644 index 0000000..bc703b2 --- /dev/null +++ b/REST-API-cliente-mqtt-consultas/main.py @@ -0,0 +1,155 @@ +from repositories.MongoEnvironmentalDataRepository import MongoEnvironmentalDataRepository +from models.EnvironmentalData import EnvironmentalData +from datetime import datetime, time +import time as delay +from bson.json_util import dumps +import threading +import json +import sys +import os +from dotenv import load_dotenv +from contextlib import asynccontextmanager +import pytz +from fastapi import FastAPI, HTTPException +import paho.mqtt.client as mqtt + +import logging + +# Configura o logging global +logging.basicConfig(level=logging.INFO) + +load_dotenv() + +uri = os.getenv("DB_URI") +db_name = os.getenv("DB_NAME") +collection_name = os.getenv("COLLECTION_NAME") +mouuid_ets = "D4:36:39:DB:25:34" +mouuid_reunioes = "D4:36:39:DB:34:68" +#taskkill /f /im python.exe + +host = "broker" +port = 1883 +topic = "mhub/MHUB_SALAS/service_topic/HMSoft" +cliente = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + +def on_connect(client, userdata, flags, reason_code, properties): + if reason_code.is_failure: + logging.info(f"Failed to connect: {reason_code}. loop_forever() will retry connection") + else: + logging.info("Connected to broker") + client.subscribe("mhub/MHUB_SALAS/service_topic/HMSoft") # subscribing mqtt topic + +def on_disconnect(client, userdata, flags, reason_code, properties): + if reason_code != 0: + logging.info(f"Unexpected MQTT Broker disconnection! Reason code: {reason_code}") + logging.info(f"Trying to reconnect") + +def on_log(client, userdata, level, buf): + logging.info(f"{buf}----SYSTEM MESSAGE----") + +def on_message(client, userdata, message: mqtt.MQTTMessage): + msg = message.payload.decode() + data = json.loads(msg) # Transformando pra dicionario + service_values = data.get("serviceValue", []) + local = "ets" if data.get("mouuid") == mouuid_ets else "reunioes" + env_data = EnvironmentalData( + local=local, + timestamp=datetime.now(pytz.utc), + temperatura=service_values[0], + umidade=service_values[1], + gas=service_values[2] + ) + repository.insert_one(env_data) + +cliente.on_connect = on_connect +cliente.on_log = on_log +cliente.on_disconnect = on_disconnect +cliente.on_message = on_message + +app = FastAPI() +repository = MongoEnvironmentalDataRepository(uri, db_name, collection_name) + +def start_mqtt(): + while True: + try: + cliente.connect(host) + break + except Exception: + logging.info("Failed to connect to broker") + logging.info("Trying again") + delay.sleep(5) + cliente.loop_forever() # Isso bloqueia, por isso usamos em uma thread + +@app.on_event("startup") +def startup_event(): + logging.info("Iniciando MQTT em thread separada...") + threading.Thread(target=start_mqtt, daemon=True).start() + +@app.get("/") +async def root(): + return "Server está funcionando" + +@app.get("/instante") +async def get_medicao_instante(local:str, data: datetime = datetime.now()) -> str: + if data.tzinfo is None: # Data sem fuso horario + sao_paulo = pytz.timezone("America/Sao_Paulo") # Assume que a data recebida é UTC-3 + data = sao_paulo.localize(data).astimezone(pytz.utc) # Converte pra UTC + else: + data = data.astimezone(pytz.utc) + + env_data_dict = repository.get_by_instante(local, data) + if env_data_dict == dict(): + raise HTTPException(status_code=404, detail="Dados não encontrados") + print(env_data_dict) + timestamp = env_data_dict["timestamp"] + + env_data_dict["timestamp"] = timestamp.astimezone( # Convertendo o dado de volta pra UTC-3 + pytz.timezone("America/Sao_Paulo") + ).isoformat() + + env_data_dict.pop("_id") # Manda o dado sem id + return dumps(env_data_dict) + +@app.get("/recente") +async def get_mais_recente(local: str) -> str: + env_data_dict = repository.get_mais_recente(local) + if not env_data_dict: + raise HTTPException(status_code=404, detail="Dados não encontrados") + + timestamp = env_data_dict["timestamp"] + env_data_dict["timestamp"] = timestamp.astimezone( + pytz.timezone("America/Sao_Paulo") + ).isoformat() + + env_data_dict.pop("_id") + return dumps(env_data_dict) + +@app.get("/intervalo") +async def get_medicao_intervalo(local:str, inicio: datetime = datetime.combine(datetime.now(), time.min), + fim: datetime = datetime.combine(datetime.now(), time.max)) -> str: + if inicio.tzinfo is None: + sao_paulo = pytz.timezone("America/Sao_Paulo") + inicio = sao_paulo.localize(inicio).astimezone(pytz.utc) + else: + inicio = inicio.astimezone(pytz.utc) + + if fim.tzinfo is None: + sao_paulo = pytz.timezone("America/Sao_Paulo") + fim = sao_paulo.localize(fim).astimezone(pytz.utc) + else: + fim = fim.astimezone(pytz.utc) + + env_data_list = repository.get_by_intervalo(local, inicio, fim) + if env_data_list == list(): + raise HTTPException(status_code=404, detail="Dados não encontrados") + + for env_data in env_data_list: + timestamp = env_data["timestamp"] + env_data["timestamp"] = timestamp.astimezone( # Convertendo de volta pra UTC-3 + pytz.timezone("America/Sao_Paulo") + ).isoformat() + + for env_data in env_data_list: # Removendo IDs + env_data.pop("_id") + + return dumps(env_data_list) diff --git a/SensorData.py b/REST-API-cliente-mqtt-consultas/models/EnvironmentalData.py similarity index 76% rename from SensorData.py rename to REST-API-cliente-mqtt-consultas/models/EnvironmentalData.py index 18e4e1f..e4e8fc7 100644 --- a/SensorData.py +++ b/REST-API-cliente-mqtt-consultas/models/EnvironmentalData.py @@ -1,12 +1,11 @@ -from pydantic import BaseModel -from datetime import datetime - - -class EnvironmentalData(BaseModel): - timestamp: datetime = datetime.now() - local: str | None = None - temperatura: float | None = None - umidade: float | None = None - gas: bool | None = None - luminosidade: float | None = None +from pydantic import BaseModel +from datetime import datetime + +class EnvironmentalData(BaseModel): + local: str + timestamp: datetime + temperatura: float | None = None + umidade: float | None = None + gas: bool | None = None + luminosidade: float | None = None ruido: float | None = None \ No newline at end of file diff --git a/REST-API-cliente-mqtt-consultas/models/__init__.py b/REST-API-cliente-mqtt-consultas/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/REST-API-cliente-mqtt-consultas/models/__pycache__/EnvironmentalData.cpython-311.pyc b/REST-API-cliente-mqtt-consultas/models/__pycache__/EnvironmentalData.cpython-311.pyc new file mode 100644 index 0000000..c43567d Binary files /dev/null and b/REST-API-cliente-mqtt-consultas/models/__pycache__/EnvironmentalData.cpython-311.pyc differ diff --git a/REST-API-cliente-mqtt-consultas/models/__pycache__/EnvironmentalData.cpython-313.pyc b/REST-API-cliente-mqtt-consultas/models/__pycache__/EnvironmentalData.cpython-313.pyc new file mode 100644 index 0000000..9e68d7e Binary files /dev/null and b/REST-API-cliente-mqtt-consultas/models/__pycache__/EnvironmentalData.cpython-313.pyc differ diff --git a/REST-API-cliente-mqtt-consultas/models/__pycache__/__init__.cpython-311.pyc b/REST-API-cliente-mqtt-consultas/models/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..1644fd1 Binary files /dev/null and b/REST-API-cliente-mqtt-consultas/models/__pycache__/__init__.cpython-311.pyc differ diff --git a/REST-API-cliente-mqtt-consultas/models/__pycache__/__init__.cpython-313.pyc b/REST-API-cliente-mqtt-consultas/models/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..19be0f2 Binary files /dev/null and b/REST-API-cliente-mqtt-consultas/models/__pycache__/__init__.cpython-313.pyc differ diff --git a/REST-API-cliente-mqtt-consultas/mosquitto/config/mosquitto.conf b/REST-API-cliente-mqtt-consultas/mosquitto/config/mosquitto.conf new file mode 100644 index 0000000..99cf9c1 --- /dev/null +++ b/REST-API-cliente-mqtt-consultas/mosquitto/config/mosquitto.conf @@ -0,0 +1,2 @@ +listener 1883 0.0.0.0 +allow_anonymous true \ No newline at end of file diff --git a/REST-API-cliente-mqtt-consultas/publisher.py b/REST-API-cliente-mqtt-consultas/publisher.py new file mode 100644 index 0000000..e301c66 --- /dev/null +++ b/REST-API-cliente-mqtt-consultas/publisher.py @@ -0,0 +1,92 @@ +import json +import random +import logging +import paho.mqtt.client as mqtt +import time +broker = "broker" +topic = "mhub/MHUB_SALAS/service_topic/HMSoft" +logging.basicConfig(level=logging.INFO) + +def on_connect(client, userdata, flags, reason_code, properties): + if reason_code.is_failure: + logging.info(f"Failed to connect: {reason_code}. loop_forever() will retry connection") + else: + logging.info("Connected to broker") + +def publish(): + try: + while True: + msg = {"className": "br.ufma.lsdi.cddl.message.Message", + "delivered": False, + "deliveredFailed": False, + "measurementTime": 1753982313077, + "qocEvaluated": False, + "serviceByteArray": [-84, -19, 0, 5, 117, 114, 0, 19, 91, 76, 106, 97, 118, 97, 46, 108, 97, 110, + 103, 46, 79, 98, 106, 101, 99, 116, 59, -112, -50, 88, -97, 16, 115, 41, 108, 2, + 0, 0, 120, 112, 0, 0, 0, 5, 115, 114, 0, 16, 106, 97, 118, 97, 46, 108, 97, 110, + 103, 46, 68, 111, 117, 98, 108, 101, -128, -77, -62, 74, 41, 107, -5, 4, 2, 0, + 1, 68, 0, 5, 118, 97, 108, 117, 101, 120, 114, 0, 16, 106, 97, 118, 97, 46, 108, + 97, 110, 103, 46, 78, 117, 109, 98, 101, 114, -122, -84, -107, 29, 11, -108, + -32, -117, 2, 0, 0, 120, 112, 64, 55, -77, 51, 51, 51, 51, 51, 115, 113, 0, 126, + 0, 2, 64, 75, -128, 0, 0, 0, 0, 0, 115, 113, 0, 126, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 0, 115, 113, 0, 126, 0, 2, 64, 98, -64, 0, 0, 0, 0, 0, 115, 113, 0, 126, 0, 2, + -65, -16, 0, 0, 0, 0, 0, 0], + "serviceName": "HMSoft", + "topic": "mhub/MHUB_SALAS/service_topic/HMSoft", + "uuid": "15766b43-4e30-48ca-9e1a-c2cb0810a9fb", + "dup": False, + "messageId": 0, "mutable": True, "payload": [], "qos": 1, "retained": False} + + ets_mouuid = "D4:36:39:DB:25:34" + reunioes_mouuid = "D4:36:39:DB:34:68" + msg["mouuid"] = random.choice((ets_mouuid, reunioes_mouuid)) + msg["serviceValue"] = [random.uniform(25, 30), random.uniform(50, 60), random.choice((0, 1)), + random.uniform(100, 150)] + cliente.publish(topic, json.dumps(msg), qos=1) + time.sleep(10) + finally: + cliente.loop_stop() + cliente.disconnect() + +def on_disconnect(client, userdata, flags, reason_code, properties): + if reason_code != 0: + logging.info(f"Unexpected MQTT Broker disconnection! Reason code: {reason_code}") + logging.info(f"Trying to reconnect") + # Implement reconnection logic here + '''interval = 1 + while True: + try: + cliente.reconnect() + break + except Exception: + print("Reconnect failed") + print(f"Trying again in {interval} seconds") + time.sleep(interval) + interval *= 2''' + + else: + logging.info("Disconnected from MQTT Broker gracefully.") + +def on_publish(client, userdata, mid, reason_code, properties): + logging.info(f"message {mid} sent successfully") + +def on_log(client, userdata, level, buf): + logging.info(f"{buf}----SYSTEM MESSAGE----") + +cliente = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) +cliente.on_connect = on_connect +cliente.on_disconnect = on_disconnect +cliente.on_publish = on_publish +cliente.on_log = on_log +#cliente.reconnect_delay_set(min_delay=5, max_delay=5) + +while True: + try: + cliente.connect(broker) + break + except Exception: + logging.info("Failed to connect to broker") + time.sleep(5) + +cliente.loop_start() +publish() \ No newline at end of file diff --git a/REST-API-cliente-mqtt-consultas/repositories/EnvironmentalDataRepository.py b/REST-API-cliente-mqtt-consultas/repositories/EnvironmentalDataRepository.py new file mode 100644 index 0000000..d984c24 --- /dev/null +++ b/REST-API-cliente-mqtt-consultas/repositories/EnvironmentalDataRepository.py @@ -0,0 +1,22 @@ +from abc import ABC, abstractmethod +from datetime import datetime +from models.EnvironmentalData import EnvironmentalData + +# Classe abstrata de persistência e recuperação de dados do ambiente +class EnvironmentalDataRepository(ABC): + + @abstractmethod + def insert_one(self, data: EnvironmentalData) -> any: + pass + + @abstractmethod + def get_by_intervalo(self, local, inicio: datetime, fim: datetime) -> list[EnvironmentalData]: + pass + + @abstractmethod + def get_mais_recente(self, local) -> EnvironmentalData: + pass + + @abstractmethod + def get_by_instante(self,local, data: datetime) -> EnvironmentalData: + pass diff --git a/REST-API-cliente-mqtt-consultas/repositories/MongoEnvironmentalDataRepository.py b/REST-API-cliente-mqtt-consultas/repositories/MongoEnvironmentalDataRepository.py new file mode 100644 index 0000000..4aa0460 --- /dev/null +++ b/REST-API-cliente-mqtt-consultas/repositories/MongoEnvironmentalDataRepository.py @@ -0,0 +1,38 @@ +from repositories.EnvironmentalDataRepository import EnvironmentalDataRepository +from models.EnvironmentalData import EnvironmentalData +from datetime import datetime +from pymongo import MongoClient + +class MongoEnvironmentalDataRepository(EnvironmentalDataRepository): + def __init__(self, uri: str, db_name: str, collection_name: str) -> None: + self.client = MongoClient(uri, tz_aware=True) + self.db = self.client[db_name] + self.collection = self.db.get_collection(collection_name) + + def insert_one(self, data: EnvironmentalData) -> str: + resp = self.collection.insert_one(data.model_dump(exclude_none=True)) + return str(resp.inserted_id) + + def get_by_intervalo(self, local: str, inicio: datetime, fim: datetime) -> list[dict]: + filtro = { + "timestamp": { + "$gte": inicio, + "$lte": fim + }, + "local": local + } + cursor = self.collection.find(filtro) + documentos = cursor.to_list() + return documentos if documentos else list() + + def get_by_instante(self, local: str, data: datetime) -> dict: + anterior = self.collection.find_one( + {"timestamp": {"$lte": data}, "local":local}, + + sort=[("timestamp", -1)] + ) + return anterior if anterior else dict() + + def get_mais_recente(self, local: str) -> dict: + doc = self.collection.find_one({"local":local}, sort=[("timestamp", -1)]) + return doc if doc else dict() diff --git a/REST-API-cliente-mqtt-consultas/repositories/__init__.py b/REST-API-cliente-mqtt-consultas/repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/REST-API-cliente-mqtt-consultas/repositories/__pycache__/EnvironmentalDataRepository.cpython-311.pyc b/REST-API-cliente-mqtt-consultas/repositories/__pycache__/EnvironmentalDataRepository.cpython-311.pyc new file mode 100644 index 0000000..b402a11 Binary files /dev/null and b/REST-API-cliente-mqtt-consultas/repositories/__pycache__/EnvironmentalDataRepository.cpython-311.pyc differ diff --git a/REST-API-cliente-mqtt-consultas/repositories/__pycache__/EnvironmentalDataRepository.cpython-313.pyc b/REST-API-cliente-mqtt-consultas/repositories/__pycache__/EnvironmentalDataRepository.cpython-313.pyc new file mode 100644 index 0000000..c47e55b Binary files /dev/null and b/REST-API-cliente-mqtt-consultas/repositories/__pycache__/EnvironmentalDataRepository.cpython-313.pyc differ diff --git a/REST-API-cliente-mqtt-consultas/repositories/__pycache__/MongoEnvironmentalDataRepository.cpython-311.pyc b/REST-API-cliente-mqtt-consultas/repositories/__pycache__/MongoEnvironmentalDataRepository.cpython-311.pyc new file mode 100644 index 0000000..33789d7 Binary files /dev/null and b/REST-API-cliente-mqtt-consultas/repositories/__pycache__/MongoEnvironmentalDataRepository.cpython-311.pyc differ diff --git a/REST-API-cliente-mqtt-consultas/repositories/__pycache__/MongoEnvironmentalDataRepository.cpython-313.pyc b/REST-API-cliente-mqtt-consultas/repositories/__pycache__/MongoEnvironmentalDataRepository.cpython-313.pyc new file mode 100644 index 0000000..ead8ef2 Binary files /dev/null and b/REST-API-cliente-mqtt-consultas/repositories/__pycache__/MongoEnvironmentalDataRepository.cpython-313.pyc differ diff --git a/REST-API-cliente-mqtt-consultas/repositories/__pycache__/__init__.cpython-311.pyc b/REST-API-cliente-mqtt-consultas/repositories/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..37ee7f3 Binary files /dev/null and b/REST-API-cliente-mqtt-consultas/repositories/__pycache__/__init__.cpython-311.pyc differ diff --git a/REST-API-cliente-mqtt-consultas/repositories/__pycache__/__init__.cpython-313.pyc b/REST-API-cliente-mqtt-consultas/repositories/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..0280438 Binary files /dev/null and b/REST-API-cliente-mqtt-consultas/repositories/__pycache__/__init__.cpython-313.pyc differ diff --git a/REST-API-cliente-mqtt-consultas/requirements.txt b/REST-API-cliente-mqtt-consultas/requirements.txt new file mode 100644 index 0000000..f823caa Binary files /dev/null and b/REST-API-cliente-mqtt-consultas/requirements.txt differ diff --git a/db.py b/db.py deleted file mode 100644 index dce4cfe..0000000 --- a/db.py +++ /dev/null @@ -1,7 +0,0 @@ -from pymongo.mongo_client import MongoClient -from pymongo.server_api import ServerApi -import os - -uri = os.getenv("MONGODB_URI") -# Create a new client and connect to the server -client = MongoClient(uri, server_api=ServerApi('1')) \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index 3f0c264..0000000 --- a/main.py +++ /dev/null @@ -1,92 +0,0 @@ -from fastapi import HTTPException - -from fastapi import FastAPI - -from db import uri, client -from datetime import datetime -from SensorData import EnvironmentalData -from bson.json_util import dumps -import json - -db = client["sensor_data"] -collection = db.get_collection("monitoramento_ambiental") -app = FastAPI() -from datetime import timedelta -#taskkill /f /im python.exe - -def query_media_dia(local, inicio, fim): - # Retorna um documento que tem a média da temperatura, umidade, luminosidade e ruído daquele dia específico - return [ - {"$match": { - "local": local, - "timestamp": {"$gte": inicio, "$lte": fim} - }}, - { - "$group": { - "_id": None, - "temp_avg": { - "$avg": "$temperatura" - }, - "umi_avg": { - "$avg": "$umidade" - }, - "lumi_avg": { - "$avg": "$luminosidade" - }, - "ruido_avg": { - "$avg": "$ruido" - } - } - } - ] - - - -def query_document_at_datetime(local: str, data: datetime): - INTERVALO = 2 - margem = timedelta(seconds=INTERVALO) - inicio = data - margem - fim = data + margem - - print(margem) - print(inicio) - print(fim) - # Retorna um documento daquele local que esteja dentro do intervalo inicio-fim - return { - "local": local, - "timestamp": { - "$gte": inicio, - "$lte": fim - } - } - - -@app.get("/media") -async def get_medicao_media_dia(local: str, data: datetime): - inicio = datetime.combine(data.date(), datetime.min.time()) - fim = datetime.combine(data.date(), datetime.max.time()) - result = json.loads(dumps(collection.aggregate(query_media_dia(local, inicio, fim)))) - - if result: - return result[0] - else: - raise HTTPException(status_code=404, detail="Medição não encontrada nessa data") - -@app.get("/instante") -async def get_medicao_instante(local: str, data: datetime): - result = collection.find_one(query_document_at_datetime(local, data)) - if result is None: - raise HTTPException(status_code=404, detail="Medição não encontrada nesse instante") - return json.loads(dumps(result)) - -@app.post("/novo") -async def insert(s: EnvironmentalData): - try: - resp = collection.insert_one(s.dict(exclude_none=True)) - return str(resp.inserted_id) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/teste") -async def teste(): - print(uri) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e2078d3..0000000 --- a/requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -annotated-types==0.7.0 -anyio==4.9.0 -click==8.2.0 -colorama==0.4.6 -dnspython==2.7.0 -fastapi==0.115.12 -h11==0.16.0 -idna==3.10 -pydantic==2.11.4 -pydantic_core==2.33.2 -pymongo==4.13.0 -sniffio==1.3.1 -starlette==0.46.2 -typing-inspection==0.4.0 -typing_extensions==4.13.2 -uvicorn==0.34.2