diff --git a/.gitguardian.yml b/.gitguardian.yml new file mode 100644 index 0000000..e8a7243 --- /dev/null +++ b/.gitguardian.yml @@ -0,0 +1,5 @@ +incident: + ignore: + - name: "Ignore example secrets" + match: "src/core/config.py" + reason: "Example credentials that are not used in production." diff --git a/src/controller/api/endpoints/customer.py b/src/controller/api/endpoints/customer.py index 240821f..f0e66e0 100644 --- a/src/controller/api/endpoints/customer.py +++ b/src/controller/api/endpoints/customer.py @@ -52,6 +52,10 @@ async def get_customers( request: Request, http_request_info: CommonDeps, db_connection: Annotated[Session, Depends(get_db_session)], + street: Annotated[str | None, Query(description="Filter customer by street")] = None, + city: Annotated[str | None, Query(description="Filter customer by city")] = None, + country: Annotated[str | None, Query(description="Filter customer by country")] = None, + postalCode: Annotated[str | None, Query(description="Filter customer by postal code")] = None, limit: Annotated[ int, Query( @@ -74,10 +78,6 @@ async def get_customers( le=100, ), ] = 0, - street: Annotated[str | None, Query(description="Filter customer by street")] = None, - city: Annotated[str | None, Query(description="Filter customer by city")] = None, - country: Annotated[str | None, Query(description="Filter customer by country")] = None, - postalCode: Annotated[str | None, Query(description="Filter customer by postal code")] = None, ) -> JSONResponse: """List of customers.""" logger.info("Entering...") @@ -154,7 +154,7 @@ async def post_customer( @router.put( - "/v1/customers/{customer_id}", + "/v1/customers/{customerId}", responses={ 204: {"description": "No Content."}, 400: {"model": ErrorMessage, "description": "Bad Request."}, @@ -174,29 +174,29 @@ async def post_customer( response_model_by_alias=True, ) async def put_customers_customer_id( - customer_id: Annotated[UUID4, Path(description="Id of a specific customer.")], + customerId: Annotated[UUID4, Path(description="Id of a specific customer.")], http_request_info: CommonDeps, db_connection: Annotated[Session, Depends(get_db_session)], post_customers_request: Annotated[CustomerUpdate, Body()], ) -> Response: """Update of the information of a customer with the matching Id.""" logger.info("Entering...") - logger.debug("Updating customer with id %s", customer_id) + logger.debug("Updating customer with id %s", customerId) try: - CustomerApplicationService.put_customers(db_connection, customer_id, post_customers_request) - logger.debug("Customer with id %s updated", customer_id) + CustomerApplicationService.put_customers(db_connection, customerId, post_customers_request) + logger.debug("Customer with id %s updated", customerId) except ElementNotFoundError as error: - logger.error("Customer with id %s not found", customer_id) # noqa: TRY400 + logger.error("Customer with id %s not found", customerId) # noqa: TRY400 raise HTTP404NotFoundError from error except Exception as error: - logger.exception("Error updating customer with id %s", customer_id) + logger.exception("Error updating customer with id %s", customerId) raise HTTP500InternalServerError from error logger.info("Exiting...") return Response(status_code=status.HTTP_204_NO_CONTENT, headers=http_request_info) @router.delete( - "/v1/customers/{customer_id}", + "/v1/customers/{customerId}", responses={ 204: {"description": "No Content."}, 400: {"model": ErrorMessage, "description": "Bad Request."}, @@ -215,28 +215,28 @@ async def put_customers_customer_id( response_model=None, ) async def delete_customer_id( - customer_id: Annotated[UUID4, Path(description="Id of a specific customer.")], + customerId: Annotated[UUID4, Path(description="Id of a specific customer.")], http_request_info: CommonDeps, db_connection: Annotated[Session, Depends(get_db_session)], ) -> Response: """Delete the information of the customer with the matching Id.""" logger.info("Entering...") - logger.debug("Deleting customer with id %s", customer_id) + logger.debug("Deleting customer with id %s", customerId) try: - CustomerApplicationService.delete_customer(db_connection, customer_id) - logger.debug("Customer with id %s deleted", customer_id) + CustomerApplicationService.delete_customer(db_connection, customerId) + logger.debug("Customer with id %s deleted", customerId) except ElementNotFoundError as error: - logger.error("Customer with id %s not found", customer_id) # noqa: TRY400 + logger.error("Customer with id %s not found", customerId) # noqa: TRY400 raise HTTP404NotFoundError from error except Exception as error: - logger.exception("Error deleting customer with id %s", customer_id) + logger.exception("Error deleting customer with id %s", customerId) raise HTTP500InternalServerError from error logger.info("Exiting...") return Response(status_code=status.HTTP_204_NO_CONTENT, headers=http_request_info) @router.get( - "/v1/customers/{customer_id}", + "/v1/customers/{customerId}", responses={ 200: {"model": CustomerDetailResponse, "description": "OK."}, 401: {"model": ErrorMessage, "description": "Unauthorized."}, @@ -256,21 +256,21 @@ async def delete_customer_id( response_model=CustomerDetailResponse, ) async def get_customer_id( + customerId: Annotated[UUID4, Path(description="Id of a specific customer.")], http_request_info: CommonDeps, db_connection: Annotated[Session, Depends(get_db_session)], - customer_id: Annotated[UUID4, Path(description="Id of a specific customer.")], ) -> JSONResponse: """Retrieve the information of the customer with the matching code.""" logger.info("Entering...") - logger.debug("Getting customer with id %s", customer_id) + logger.debug("Getting customer with id %s", customerId) try: - api_data = CustomerApplicationService.get_customer_id(db_connection, customer_id) - logger.debug("Customer with id %s retrieved", customer_id) + api_data = CustomerApplicationService.get_customer_id(db_connection, customerId) + logger.debug("Customer with id %s retrieved", customerId) except ElementNotFoundError as error: - logger.error("Customer with id %s not found", customer_id) # noqa: TRY400 + logger.error("Customer with id %s not found", customerId) # noqa: TRY400 raise HTTP404NotFoundError from error except Exception as error: - logger.exception("Error getting customer with id %s", customer_id) + logger.exception("Error getting customer with id %s", customerId) raise HTTP500InternalServerError from error logger.info("Exiting...") return JSONResponse( @@ -279,7 +279,7 @@ async def get_customer_id( @router.post( - "/v1/customers/{customer_id}/addresses", + "/v1/customers/{customerId}/addresses", responses={ 201: {"description": "Created."}, 400: {"model": ErrorMessage, "description": "Bad Request."}, @@ -297,18 +297,18 @@ async def get_customer_id( response_model_by_alias=True, ) async def post_address( + customerId: Annotated[UUID4, Path(description="Id of a specific customer.")], + post_address_request: Annotated[AddressBase, Body()], request: Request, http_request_info: CommonDeps, - customer_id: Annotated[UUID4, Path(description="Id of a specific customer.")], db_connection: Annotated[Session, Depends(get_db_session)], - post_address_request: Annotated[AddressBase, Body()], ) -> Response: """Add a new address into the list.""" logger.info("Entering...") try: address_id = CustomerApplicationService.post_address( db_connection, - customer_id, + customerId, post_address_request, ) logger.debug("Address created") @@ -317,14 +317,14 @@ async def post_address( raise HTTP500InternalServerError from error url = request.url headers = http_request_info | { - "location": f"{url.scheme}://{url.netloc}/customers/{customer_id}/addresses/{address_id}", + "location": f"{url.scheme}://{url.netloc}/customers/{customerId}/addresses/{address_id}", } logger.info("Exiting...") return Response(status_code=status.HTTP_201_CREATED, headers=headers) @router.put( - "/v1/customers/{customer_id}/addresses/{address_id}", + "/v1/customers/{customerId}/addresses/{addressId}", responses={ 204: {"description": "No Content."}, 400: {"model": ErrorMessage, "description": "Bad Request."}, @@ -344,35 +344,35 @@ async def post_address( response_model_by_alias=True, ) async def put_addresses_customer_id( - customer_id: Annotated[UUID4, Path(description="Id of a specific customer.")], - address_id: Annotated[UUID4, Path(description="Id of a specific address.")], + customerId: Annotated[UUID4, Path(description="Id of a specific customer.")], + addressId: Annotated[UUID4, Path(description="Id of a specific address.")], http_request_info: CommonDeps, db_connection: Annotated[Session, Depends(get_db_session)], post_address_request: Annotated[AddressBase, Body()], ) -> Response: """Update of the information of a customer with the matching Id.""" logger.info("Entering...") - logger.debug("Updating address with id %s", address_id) + logger.debug("Updating address with id %s", addressId) try: CustomerApplicationService.put_address( db_connection, - customer_id, - address_id, + customerId, + addressId, post_address_request, ) - logger.debug("Address with id %s updated", address_id) + logger.debug("Address with id %s updated", addressId) except ElementNotFoundError as error: - logger.error("Address with id %s not found", address_id) # noqa: TRY400 + logger.error("Address with id %s not found", addressId) # noqa: TRY400 raise HTTP404NotFoundError from error except Exception as error: - logger.exception("Error updating address with id %s", address_id) + logger.exception("Error updating address with id %s", addressId) raise HTTP500InternalServerError from error logger.info("Exiting...") return Response(status_code=status.HTTP_204_NO_CONTENT, headers=http_request_info) @router.delete( - "/v1/customers/{customer_id}/addresses/{address_id}", + "/v1/customers/{customerId}/addresses/{addressId}", responses={ 204: {"description": "No Content."}, 400: {"model": ErrorMessage, "description": "Bad Request."}, @@ -391,19 +391,19 @@ async def put_addresses_customer_id( response_model=None, ) async def delete_address_id( - customer_id: Annotated[UUID4, Path(description="Id of a specific customer.")], - address_id: Annotated[UUID4, Path(description="Id of a specific address.")], + customerId: Annotated[UUID4, Path(description="Id of a specific customer.")], + addressId: Annotated[UUID4, Path(description="Id of a specific address.")], http_request_info: CommonDeps, db_connection: Annotated[Session, Depends(get_db_session)], ) -> Response: """Delete the information of the customer with the matching Id.""" logger.info("Entering...") - logger.debug("Deleting address with id %s", address_id) + logger.debug("Deleting address with id %s", addressId) try: - CustomerApplicationService.delete_address(db_connection, customer_id, address_id) - logger.debug("Address with id %s deleted", address_id) + CustomerApplicationService.delete_address(db_connection, customerId, addressId) + logger.debug("Address with id %s deleted", addressId) except Exception as error: - logger.exception("Error deleting address with id %s", address_id) + logger.exception("Error deleting address with id %s", addressId) raise HTTP500InternalServerError from error logger.info("Exiting...") return Response(status_code=status.HTTP_204_NO_CONTENT, headers=http_request_info) diff --git a/src/core/config.py b/src/core/config.py index dd3f58a..3730f06 100644 --- a/src/core/config.py +++ b/src/core/config.py @@ -35,7 +35,6 @@ class Settings(BaseSettings): """Represents the configuration settings for the application.""" # CORE SETTINGS - ## Could be improved by using a secret manager like AWS Secrets Manager or Hashicorp Vault SECRET_KEY: str = "HDx09iYK97MzUqezQ8InThpcEBk791oi" ENVIRONMENT: Literal["DEV", "PYTEST", "PREPROD", "PROD"] = "DEV" ## BACKEND_CORS_ORIGINS and ALLOWED_HOSTS are a JSON-formatted list of origins @@ -45,13 +44,23 @@ class Settings(BaseSettings): APP_LOG_FILE_PATH: str = "logs/app.log" # POSTGRESQL DATABASE - POSTGRES_SERVER: str = "db" - POSTGRES_USER: str = "postgres" - POSTGRES_PASSWORD: str = "postgres" - POSTGRES_PORT: int = 5432 - POSTGRES_DB: str = "app-db" + POSTGRES_SERVER: str = "db" # The name of the service in the docker-compose file + POSTGRES_USER: str = "postgres" # The default username for the PostgreSQL database + POSTGRES_PASSWORD: str = "postgres" # The default password for the PostgreSQL database + POSTGRES_PORT: int = 5432 # The default port for the PostgreSQL database + POSTGRES_DB: str = "app-db" # The default database name for the PostgreSQL database SQLALCHEMY_DATABASE_URI: PostgresDsn | None = None + # CONNECTION POOL SETTINGS + # The size of the pool to be maintained, defaults to 5 + POOL_SIZE: int = 10 + # Controls the number of connections that can be created after the pool reached its size + MAX_OVERFLOW: int = 20 + # Number of seconds to wait before giving up on getting a connection from the pool + POOL_TIMEOUT: int = 30 + # Number of seconds after which a connection is recycled (preventing stale connections) + POOL_RECYCLE: int = 1800 + @field_validator("SQLALCHEMY_DATABASE_URI", mode="before") @classmethod def assemble_db_connection( diff --git a/src/repository/session.py b/src/repository/session.py index 05e6853..d5b580e 100644 --- a/src/repository/session.py +++ b/src/repository/session.py @@ -5,13 +5,22 @@ from sqlalchemy import create_engine from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.pool import QueuePool from src.core.config import settings logger = logging.getLogger(__name__) -# Create a new engine -engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI), pool_pre_ping=True) +# Create a new engine with connection pooling +engine = create_engine( + str(settings.SQLALCHEMY_DATABASE_URI), + pool_pre_ping=True, + poolclass=QueuePool, + pool_size=settings.POOL_SIZE, + max_overflow=settings.MAX_OVERFLOW, + pool_timeout=settings.POOL_TIMEOUT, + pool_recycle=settings.POOL_RECYCLE, +) # Create a session factory session_local = sessionmaker(autocommit=False, autoflush=False, bind=engine)