From 4ea0d73634a308ac32e905990302fee50eaded8b Mon Sep 17 00:00:00 2001 From: Guillermo Valdes Date: Mon, 13 May 2024 16:15:32 -0600 Subject: [PATCH 1/3] Ya se recibe el archivo, falta guardarlo --- README.md | 45 +++++++++++----- carina/v4/exh_exhortos/crud.py | 10 ++-- carina/v4/exh_exhortos/paths.py | 10 ++-- carina/v4/exh_exhortos_archivos/crud.py | 9 ++-- carina/v4/exh_exhortos_archivos/paths.py | 65 ++++++++++++++++++++++-- 5 files changed, 111 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index c15454f..3e132ea 100644 --- a/README.md +++ b/README.md @@ -55,32 +55,53 @@ Cuando NO se encuentra un registro el **status code** es **200** pero el **succe Cuando la ruta NO existe, simplemente ocurre un **status code** con error **404**. -## Configure Poetry +## Instalación -Por defecto, con **poetry** el entorno se guarda en un directorio en `~/.cache/pypoetry/virtualenvs` +Crear el entorno virtual -Modifique para que el entorno se guarde en el mismo directorio que el proyecto +```bash +python3.11 -m venv .venv +``` + +Ingresar al entorno virtual ```bash -poetry config --list -poetry config virtualenvs.in-project true +source venv/bin/activate +``` + +Actualizar el gestor de paquetes **pip** + +```bash +pip install --upgrade pip +``` + +Instalar el paquete **wheel** para compilar las dependencias + +```bash +pip install wheel ``` -Verifique que este en True +Instalar **poetry** en el entorno virtual si no lo tiene desde el sistema operativo + +```bash +pip install poetry +``` + +Verificar que la configuracion `virtualenvs.in-project` sea True ```bash poetry config virtualenvs.in-project ``` -## Instalacion +Si es falso, configurar **poetry** para que use el entorno virtual dentro del proyecto -Instale el entorno virtual con **Python 3.11** y los paquetes necesarios +```bash +poetry config virtualenvs.in-project true +``` + +Instalar los paquetes por medio de **poetry** ```bash -python3.11 -m venv .venv -source .venv/bin/activate -pip install --upgrade pip -pip install wheel poetry install ``` diff --git a/carina/v4/exh_exhortos/crud.py b/carina/v4/exh_exhortos/crud.py index 9f12737..69d5837 100644 --- a/carina/v4/exh_exhortos/crud.py +++ b/carina/v4/exh_exhortos/crud.py @@ -8,7 +8,7 @@ from sqlalchemy.orm import Session -from lib.exceptions import MyIsDeletedError, MyNotExistsError +from lib.exceptions import MyIsDeletedError, MyNotExistsError, MyNotValidParamError from ...core.estados.models import Estado from ...core.exh_exhortos.models import ExhExhorto from ...core.exh_exhortos_archivos.models import ExhExhortoArchivo @@ -26,9 +26,13 @@ def get_exh_exhortos(database: Session) -> Any: return consulta.filter_by(estatus="A").order_by(ExhExhorto.id) -def get_exh_exhorto(database: Session, exh_exhorto_id: int) -> ExhExhorto: +def get_exh_exhorto(database: Session, exhorto_origen_id: str) -> ExhExhorto: """Consultar un exhorto por su id""" - exh_exhorto = database.query(ExhExhorto).get(exh_exhorto_id) + try: + uuid.UUID(exhorto_origen_id) + except ValueError as error: + raise MyNotValidParamError("No es un UUID válido") from error + exh_exhorto = database.query(ExhExhorto).filter_by(exhorto_origen_id=exhorto_origen_id).first() if exh_exhorto is None: raise MyNotExistsError("No existe ese exhorto") if exh_exhorto.estatus != "A": diff --git a/carina/v4/exh_exhortos/paths.py b/carina/v4/exh_exhortos/paths.py index 7c7b896..585f954 100644 --- a/carina/v4/exh_exhortos/paths.py +++ b/carina/v4/exh_exhortos/paths.py @@ -43,17 +43,19 @@ async def paginado_exh_exhortos( return paginate(resultados) -@exh_exhortos.get("/{exh_exhorto_id}", response_model=OneExhExhortoOut) +@exh_exhortos.get("/{exhorto_origen_id}", response_model=OneExhExhortoOut) async def detalle_exh_exhorto( current_user: Annotated[UsuarioInDB, Depends(get_current_active_user)], database: Annotated[Session, Depends(get_db)], - exh_exhorto_id: int, + exhorto_origen_id: str, ): """Detalle de una exhorto a partir de su id""" if current_user.permissions.get("EXH EXHORTOS", 0) < Permiso.VER: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden") + + # Consultar el exhorto try: - exh_exhorto = get_exh_exhorto(database, exh_exhorto_id) + exh_exhorto = get_exh_exhorto(database, exhorto_origen_id) except MyAnyError as error: return OneExhExhortoOut(success=False, errors=[str(error)]) @@ -86,7 +88,7 @@ async def detalle_exh_exhorto( ) exh_exhorto.archivos = archivos - # Entregar un exhorto + # Entregar return OneExhExhortoOut(success=True, data=exh_exhorto) diff --git a/carina/v4/exh_exhortos_archivos/crud.py b/carina/v4/exh_exhortos_archivos/crud.py index 4c36e9f..d18bccc 100644 --- a/carina/v4/exh_exhortos_archivos/crud.py +++ b/carina/v4/exh_exhortos_archivos/crud.py @@ -12,14 +12,11 @@ from ...core.exh_exhortos_archivos.models import ExhExhortoArchivo -def get_exh_exhortos_archivos( - database: Session, - exh_exhorto_id: int = None, -) -> Any: +def get_exh_exhortos_archivos(database: Session, exhorto_origen_id: str = None) -> Any: """Consultar los archivos activos""" consulta = database.query(ExhExhortoArchivo) - if exh_exhorto_id is not None: - exh_exhorto = get_exh_exhorto(database, exh_exhorto_id) + if exhorto_origen_id is not None: + exh_exhorto = get_exh_exhorto(database, exhorto_origen_id) consulta = consulta.filter_by(exh_exhorto_id=exh_exhorto.id) return consulta.filter_by(estatus="A").order_by(ExhExhortoArchivo.id) diff --git a/carina/v4/exh_exhortos_archivos/paths.py b/carina/v4/exh_exhortos_archivos/paths.py index 479b5dd..0d9670a 100644 --- a/carina/v4/exh_exhortos_archivos/paths.py +++ b/carina/v4/exh_exhortos_archivos/paths.py @@ -2,11 +2,13 @@ Exh Exhortos Archivos v4, rutas (paths) """ +from datetime import datetime from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, status, UploadFile from fastapi_pagination.ext.sqlalchemy import paginate +from carina.v4.exh_exhortos.crud import get_exh_exhorto from lib.database import Session, get_db from lib.exceptions import MyAnyError from lib.fastapi_pagination_custom_page import CustomPage @@ -14,7 +16,15 @@ from ...core.permisos.models import Permiso from ..usuarios.authentications import UsuarioInDB, get_current_active_user from .crud import get_exh_exhortos_archivos, get_exh_exhorto_archivo -from .schemas import ExhExhortoArchivoOut, OneExhExhortoArchivoOut, ExhExhortoArchivoFileIn +from .schemas import ( + ExhExhortoArchivoFileDataAcuseOut, + ExhExhortoArchivoFileDataArchivoOut, + ExhExhortoArchivoFileDataOut, + ExhExhortoArchivoFileOut, + ExhExhortoArchivoOut, + OneExhExhortoArchivoOut, + ExhExhortoArchivoFileIn, +) exh_exhortos_archivos = APIRouter(prefix="/v4/exh_exhortos_archivos", tags=["exhortos"]) @@ -57,7 +67,56 @@ async def upload_exh_exhorto_archivo( exhortoOrigenId: str, archivo: UploadFile, ): - """Entregar un archivo""" + """Recibir un archivo""" if current_user.permissions.get("EXH EXHORTOS ARCHIVOS", 0) < Permiso.CREAR: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden") - return {"message": archivo.filename} + + # Consultar y validar el exhorto a partir del exhortoOrigenId + try: + exh_exhorto = get_exh_exhorto(database, exhortoOrigenId) + except MyAnyError as error: + return ExhExhortoArchivoFileOut(success=False, errors=[str(error)]) + + # Consultar los archivos del exhorto + exh_exhortos_archivos = get_exh_exhortos_archivos(database, exhortoOrigenId).all() + + # Buscar el archivo del exhorto a partir del nombre del archivo + se_encontro = False + for exh_exhorto_archivo in exh_exhortos_archivos: + if exh_exhorto_archivo.nombre_archivo == archivo.filename: + se_encontro = True + break + + # Si NO se encontró el archivo, entonces entregar un error + if not se_encontro: + return ExhExhortoArchivoFileOut(success=False, errors=["No se encontró el archivo"]) + + # Validar la integridad del archivo con los hashes + + # Almacenar el archivo en Google Storage + + # Definir los datos del archivo para la respuesta + archivo = ExhExhortoArchivoFileDataArchivoOut( + nombreArchivo=archivo.filename, + tamano=1024, + ) + + # Definir los datos del acuse para la respuesta + acuse = ExhExhortoArchivoFileDataAcuseOut( + exhortoOrigenId="", + folioSeguimiento="", + fechaHoraRecepcion=datetime.now(), + municipioAreaRecibeId=1, + areaRecibeId="", + areaRecibeNombre="", + urlInfo="", + ) + + # Juntar los datos para la respuesta + data = ExhExhortoArchivoFileDataOut( + archivo=archivo, + acuse=acuse, + ) + + # Entregar la respuesta + return ExhExhortoArchivoFileOut(success=True, data=data) From 902a7f5a8fe97d63da2cbf5a1c8f5d99d0e5e98e Mon Sep 17 00:00:00 2001 From: Guillermo Valdes Date: Mon, 13 May 2024 16:34:45 -0600 Subject: [PATCH 2/3] Avance en upload --- README.md | 5 + carina/v4/exh_exhortos_archivos/paths.py | 12 +- config/settings.py | 4 + lib/google_cloud_storage.py | 193 +++++++++++++++++++++++ 4 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 lib/google_cloud_storage.py diff --git a/README.md b/README.md index 3e132ea..54bca50 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,9 @@ DB_NAME=pjecz_plataforma_web DB_USER=adminpjeczplataformaweb DB_PASS=XXXXXXXXXXXXXXXX +# Google Cloud Storage +CLOUD_STORAGE_DEPOSITO=pjecz-desarrollo + # Origins ORIGINS=http://localhost:3000 @@ -145,11 +148,13 @@ then echo "-- Variables de entorno" export $(grep -v '^#' .env | xargs) # source .env && export $(sed '/^#/d' .env | cut -d= -f1) + echo " CLOUD_STORAGE_DEPOSITO: ${CLOUD_STORAGE_DEPOSITO}" echo " DB_HOST: ${DB_HOST}" echo " DB_PORT: ${DB_PORT}" echo " DB_NAME: ${DB_NAME}" echo " DB_USER: ${DB_USER}" echo " DB_PASS: ${DB_PASS}" + echo " GOOGLE_APPLICATION_CREDENTIALS: ${GOOGLE_APPLICATION_CREDENTIALS}" echo " ORIGINS: ${ORIGINS}" echo " SALT: ${SALT}" echo diff --git a/carina/v4/exh_exhortos_archivos/paths.py b/carina/v4/exh_exhortos_archivos/paths.py index 0d9670a..8842717 100644 --- a/carina/v4/exh_exhortos_archivos/paths.py +++ b/carina/v4/exh_exhortos_archivos/paths.py @@ -8,12 +8,14 @@ from fastapi import APIRouter, Depends, HTTPException, status, UploadFile from fastapi_pagination.ext.sqlalchemy import paginate -from carina.v4.exh_exhortos.crud import get_exh_exhorto +from config.settings import get_settings from lib.database import Session, get_db from lib.exceptions import MyAnyError from lib.fastapi_pagination_custom_page import CustomPage +from lib.google_cloud_storage import get_blob_name_from_url, get_media_type_from_filename, get_file_from_gcs, upload_file_to_gcs from ...core.permisos.models import Permiso +from ..exh_exhortos.crud import get_exh_exhorto from ..usuarios.authentications import UsuarioInDB, get_current_active_user from .crud import get_exh_exhortos_archivos, get_exh_exhorto_archivo from .schemas import ( @@ -23,7 +25,6 @@ ExhExhortoArchivoFileOut, ExhExhortoArchivoOut, OneExhExhortoArchivoOut, - ExhExhortoArchivoFileIn, ) exh_exhortos_archivos = APIRouter(prefix="/v4/exh_exhortos_archivos", tags=["exhortos"]) @@ -94,6 +95,13 @@ async def upload_exh_exhorto_archivo( # Validar la integridad del archivo con los hashes # Almacenar el archivo en Google Storage + settings = get_settings() + upload_file_to_gcs( + bucket_name=settings.cloud_storage_deposito, + blob_name=f"exh_exhortos_archivos/YYYY/MM/DD/{archivo.filename}", + content_type="application/pdf", + data=archivo.file, + ) # Definir los datos del archivo para la respuesta archivo = ExhExhortoArchivoFileDataArchivoOut( diff --git a/config/settings.py b/config/settings.py index 10797ab..45812e7 100644 --- a/config/settings.py +++ b/config/settings.py @@ -9,6 +9,7 @@ Para desarrollo debe crear un archivo .env en la raíz del proyecto con las siguientes variables: +- CLOUD_STORAGE_DEPOSITO - DB_HOST - DB_PORT - DB_NAME @@ -21,6 +22,7 @@ https://console.cloud.google.com/security/secret-manager y cree como secretos las siguientes variable de entorno +- pjecz_carina_api_key_cloud_storage_deposito - pjecz_carina_api_key_db_host - pjecz_carina_api_key_db_port - pjecz_carina_api_key_db_name @@ -34,6 +36,7 @@ - PROJECT_ID: justicia-digital-gob-mx - SERVICE_PREFIX: pjecz_carina_api_key """ + import os from functools import lru_cache @@ -68,6 +71,7 @@ def get_secret(secret_id: str) -> str: class Settings(BaseSettings): """Settings""" + cloud_storage_deposito: str = get_secret("cloud_storage_deposito") db_host: str = get_secret("db_host") db_port: int = get_secret("db_port") db_name: str = get_secret("db_name") diff --git a/lib/google_cloud_storage.py b/lib/google_cloud_storage.py new file mode 100644 index 0000000..e026cb2 --- /dev/null +++ b/lib/google_cloud_storage.py @@ -0,0 +1,193 @@ +""" +Google Cloud Storage + +Functions to get and upload files from Google Cloud Storage + +For develpment you need the environment variable GOOGLE_APPLICATION_CREDENTIALS + +""" + +from pathlib import Path +from urllib.parse import unquote, urlparse + +from google.cloud import storage +from google.cloud.exceptions import NotFound + +from lib.exceptions import ( + MyBucketNotFoundError, + MyFileNotAllowedError, + MyFileNotFoundError, + MyNotValidParamError, + MyUploadError, +) + +EXTENSIONS_MEDIA_TYPES = {"pdf": "application/pdf"} + + +def get_media_type_from_filename(filename: str) -> str: + """ + Get media type from filename + + :param filename: Name of file + :return: Media type + """ + + # Get extension + extension = Path(filename).suffix[1:].lower() + + # Get media type + try: + media_type = EXTENSIONS_MEDIA_TYPES[extension] + except KeyError as error: + raise MyFileNotAllowedError("File not allowed") from error + + # Return media type + return media_type + + +def get_blob_name_from_url(url: str) -> str: + """ + Get blob name from URL + + :param url: URL of the file + :return: Blob name + """ + + # Parse URL + parsed_url = urlparse(url) + + # Get blob name + try: + blob_name_complete = parsed_url.path[1:] # Extract the path and remove the first slash + blob_name = "/".join( + blob_name_complete.split("/")[1:] + ) # Remove the first directory from the path, because it is the bucket name + except IndexError as error: + raise MyNotValidParamError("Not valid URL") from error + + # Returno blob name unquoted + return unquote(blob_name) + + +def check_file_exists_from_gcs( + bucket_name: str, + blob_name: str, +) -> bool: + """ + Check if file exists in Google Cloud Storage + + :param bucket_name: Name of the bucket + :param blob_name: Path to the file + :return: True if file exists + """ + + # Get bucket + storage_client = storage.Client() + try: + bucket = storage_client.get_bucket(bucket_name) + except NotFound as error: + raise MyBucketNotFoundError("Bucket not found") from error + + # Get file + blob = bucket.get_blob(blob_name) + if blob is None: + return False + + # Return True if file exists + return True + + +def get_public_url_from_gcs( + bucket_name: str, + blob_name: str, +) -> str: + """ + Get public URL from Google Cloud Storage + + :param bucket_name: Name of the bucket + :param blob_name: Path to the file + :return: Public URL + """ + + # Get bucket + storage_client = storage.Client() + try: + bucket = storage_client.get_bucket(bucket_name) + except NotFound as error: + raise MyBucketNotFoundError("Bucket not found") from error + + # Get file + blob = bucket.get_blob(blob_name) + if blob is None: + raise MyFileNotFoundError("File not found") + + # Return public URL + return blob.public_url + + +def get_file_from_gcs( + bucket_name: str, + blob_name: str, +) -> bytes: + """ + Get file from Google Cloud Storage + + :param bucket_name: Name of the bucket + :param blob_name: Path to the file + :return: File content + """ + + # Get bucket + storage_client = storage.Client() + try: + bucket = storage_client.get_bucket(bucket_name) + except NotFound as error: + raise MyBucketNotFoundError("Bucket not found") from error + + # Get file + blob = bucket.get_blob(blob_name) + if blob is None: + raise MyFileNotFoundError("File not found") + + # Return file content + return blob.download_as_string() + + +def upload_file_to_gcs( + bucket_name: str, + blob_name: str, + content_type: str, + data: bytes, +) -> str: + """ + Upload file to Google Cloud Storage + + :param bucket_name: Name of the bucket + :param blob_name: Path to the file + :param content_type: Content type of the file + :param data: File content + :return: Public URL + """ + + # Check content type + if content_type not in EXTENSIONS_MEDIA_TYPES.values(): + raise MyFileNotAllowedError("File not allowed") + + # Get bucket + storage_client = storage.Client() + try: + bucket = storage_client.get_bucket(bucket_name) + except NotFound as error: + raise MyBucketNotFoundError("Bucket not found") from error + + # Create blob + blob = bucket.blob(blob_name) + + # Upload file + try: + blob.upload_from_string(data, content_type=content_type) + except Exception as error: + raise MyUploadError("Error uploading file") from error + + # Return public URL + return blob.public_url From 859140f285cf117b4db714f88a57781979fc03ea Mon Sep 17 00:00:00 2001 From: Guillermo Valdes Date: Tue, 14 May 2024 09:39:18 -0600 Subject: [PATCH 3/3] Inicia Google Cloud Storage --- pyproject.toml | 3 ++- requirements.txt | 51 ++++++++++++++++++++++++------------------------ 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 720b904..3ef22e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ version = "0.1.0" description = "API con autentificación para enviar y recibir exhortos." authors = ["Guillermo Valdes "] license = "AGPL3" +packages = [{include = "carina"}] [tool.poetry.dependencies] python = "^3.11" @@ -20,12 +21,12 @@ psycopg2-binary = "^2.9.9" pydantic = "^2.5.3" pydantic-settings = "^2.1.0" python-dotenv = "^1.0.0" +python-multipart = "^0.0.9" pytz = "^2023.3.post1" sqlalchemy = "^2.0.25" sqlalchemy-utils = "^0.41.1" unidecode = "^1.3.8" uvicorn = "^0.25.0" -python-multipart = "^0.0.9" [tool.poetry.group.dev.dependencies] diff --git a/requirements.txt b/requirements.txt index 6895482..21dd00e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,53 +1,54 @@ annotated-types==0.6.0 ; python_version >= "3.11" and python_version < "4.0" -anyio==4.2.0 ; python_version >= "3.11" and python_version < "4.0" -cachetools==5.3.2 ; python_version >= "3.11" and python_version < "4.0" -certifi==2023.11.17 ; python_version >= "3.11" and python_version < "4.0" +anyio==4.3.0 ; python_version >= "3.11" and python_version < "4.0" +cachetools==5.3.3 ; python_version >= "3.11" and python_version < "4.0" +certifi==2024.2.2 ; python_version >= "3.11" and python_version < "4.0" cffi==1.16.0 ; python_version >= "3.11" and python_version < "4.0" charset-normalizer==3.3.2 ; python_version >= "3.11" and python_version < "4.0" click==8.1.7 ; python_version >= "3.11" and python_version < "4.0" colorama==0.4.6 ; python_version >= "3.11" and python_version < "4.0" and platform_system == "Windows" cryptography==41.0.7 ; python_version >= "3.11" and python_version < "4.0" -fastapi-pagination==0.12.14 ; python_version >= "3.11" and python_version < "4.0" -fastapi==0.109.0 ; python_version >= "3.11" and python_version < "4.0" -fastapi[sqlalchemy]==0.109.0 ; python_version >= "3.11" and python_version < "4.0" -google-api-core==2.15.0 ; python_version >= "3.11" and python_version < "4.0" -google-api-core[grpc]==2.15.0 ; python_version >= "3.11" and python_version < "4.0" -google-auth==2.26.2 ; python_version >= "3.11" and python_version < "4.0" +fastapi-pagination==0.12.19 ; python_version >= "3.11" and python_version < "4.0" +fastapi==0.109.2 ; python_version >= "3.11" and python_version < "4.0" +fastapi[sqlalchemy]==0.109.2 ; python_version >= "3.11" and python_version < "4.0" +google-api-core==2.17.1 ; python_version >= "3.11" and python_version < "4.0" +google-api-core[grpc]==2.17.1 ; python_version >= "3.11" and python_version < "4.0" +google-auth==2.28.1 ; python_version >= "3.11" and python_version < "4.0" google-cloud-core==2.4.1 ; python_version >= "3.11" and python_version < "4.0" -google-cloud-secret-manager==2.17.0 ; python_version >= "3.11" and python_version < "4.0" -google-cloud-storage==2.14.0 ; python_version >= "3.11" and python_version < "4.0" +google-cloud-secret-manager==2.18.3 ; python_version >= "3.11" and python_version < "4.0" +google-cloud-storage==2.15.0 ; python_version >= "3.11" and python_version < "4.0" google-cloud==0.34.0 ; python_version >= "3.11" and python_version < "4.0" google-crc32c==1.5.0 ; python_version >= "3.11" and python_version < "4.0" google-resumable-media==2.7.0 ; python_version >= "3.11" and python_version < "4.0" googleapis-common-protos==1.62.0 ; python_version >= "3.11" and python_version < "4.0" googleapis-common-protos[grpc]==1.62.0 ; python_version >= "3.11" and python_version < "4.0" -greenlet==3.0.3 ; python_version >= "3.11" and python_version < "4.0" and (platform_machine == "win32" or platform_machine == "WIN32" or platform_machine == "AMD64" or platform_machine == "amd64" or platform_machine == "x86_64" or platform_machine == "ppc64le" or platform_machine == "aarch64") +greenlet==3.0.3 ; python_version >= "3.11" and python_version < "4.0" and (platform_machine == "aarch64" or platform_machine == "ppc64le" or platform_machine == "x86_64" or platform_machine == "amd64" or platform_machine == "AMD64" or platform_machine == "win32" or platform_machine == "WIN32") grpc-google-iam-v1==0.13.0 ; python_version >= "3.11" and python_version < "4.0" -grpcio-status==1.60.0 ; python_version >= "3.11" and python_version < "4.0" -grpcio==1.60.0 ; python_version >= "3.11" and python_version < "4.0" +grpcio-status==1.62.0 ; python_version >= "3.11" and python_version < "4.0" +grpcio==1.62.0 ; python_version >= "3.11" and python_version < "4.0" gunicorn==21.2.0 ; python_version >= "3.11" and python_version < "4.0" h11==0.14.0 ; python_version >= "3.11" and python_version < "4.0" hashids==1.3.1 ; python_version >= "3.11" and python_version < "4.0" idna==3.6 ; python_version >= "3.11" and python_version < "4.0" packaging==23.2 ; python_version >= "3.11" and python_version < "4.0" proto-plus==1.23.0 ; python_version >= "3.11" and python_version < "4.0" -protobuf==4.25.2 ; python_version >= "3.11" and python_version < "4.0" +protobuf==4.25.3 ; python_version >= "3.11" and python_version < "4.0" psycopg2-binary==2.9.9 ; python_version >= "3.11" and python_version < "4.0" pyasn1-modules==0.3.0 ; python_version >= "3.11" and python_version < "4.0" pyasn1==0.5.1 ; python_version >= "3.11" and python_version < "4.0" pycparser==2.21 ; python_version >= "3.11" and python_version < "4.0" -pydantic-core==2.14.6 ; python_version >= "3.11" and python_version < "4.0" -pydantic-settings==2.1.0 ; python_version >= "3.11" and python_version < "4.0" -pydantic==2.5.3 ; python_version >= "3.11" and python_version < "4.0" -python-dotenv==1.0.0 ; python_version >= "3.11" and python_version < "4.0" -pytz==2023.3.post1 ; python_version >= "3.11" and python_version < "4.0" +pydantic-core==2.16.3 ; python_version >= "3.11" and python_version < "4.0" +pydantic-settings==2.2.1 ; python_version >= "3.11" and python_version < "4.0" +pydantic==2.6.3 ; python_version >= "3.11" and python_version < "4.0" +python-dotenv==1.0.1 ; python_version >= "3.11" and python_version < "4.0" +python-multipart==0.0.9 ; python_version >= "3.11" and python_version < "4.0" +pytz==2023.4 ; python_version >= "3.11" and python_version < "4.0" requests==2.31.0 ; python_version >= "3.11" and python_version < "4.0" rsa==4.9 ; python_version >= "3.11" and python_version < "4" -sniffio==1.3.0 ; python_version >= "3.11" and python_version < "4.0" +sniffio==1.3.1 ; python_version >= "3.11" and python_version < "4.0" sqlalchemy-utils==0.41.1 ; python_version >= "3.11" and python_version < "4.0" -sqlalchemy==2.0.25 ; python_version >= "3.11" and python_version < "4.0" -starlette==0.35.1 ; python_version >= "3.11" and python_version < "4.0" -typing-extensions==4.9.0 ; python_version >= "3.11" and python_version < "4.0" +sqlalchemy==2.0.28 ; python_version >= "3.11" and python_version < "4.0" +starlette==0.36.3 ; python_version >= "3.11" and python_version < "4.0" +typing-extensions==4.10.0 ; python_version >= "3.11" and python_version < "4.0" unidecode==1.3.8 ; python_version >= "3.11" and python_version < "4.0" -urllib3==2.1.0 ; python_version >= "3.11" and python_version < "4.0" +urllib3==2.2.1 ; python_version >= "3.11" and python_version < "4.0" uvicorn==0.25.0 ; python_version >= "3.11" and python_version < "4.0"