From ddca54a33d97f398dd5281a452cb0cecb4f30778 Mon Sep 17 00:00:00 2001 From: Anas WS Date: Tue, 19 Mar 2024 18:40:03 +0530 Subject: [PATCH 1/3] feat - flag feature added --- .config.celery | 2 +- .env.example | 4 +- .flake8 | 2 +- .pre-commit-config.yaml | 2 +- README.Docker.md | 2 +- alembic/README | 2 +- alembic/env.py | 16 ++++---- app/app.py | 24 +++++++----- app/celery_tasks/tasks.py | 2 + app/config/base.py | 31 ++++++++++------ app/config/celery_config.py | 4 +- app/config/celery_utils.py | 2 + app/config/redis_config.py | 5 ++- app/constants/jwt_utils.py | 8 +++- app/constants/messages/users.py | 2 + app/daos/users.py | 7 ++-- app/exceptions.py | 13 ++++++- app/middlewares/cache_middleware.py | 15 ++++---- app/middlewares/rate_limiter_middleware.py | 5 ++- app/routes/__init__.py | 6 ++- app/routes/cache_router/__init__.py | 2 + app/routes/cache_router/cache_samples.py | 5 ++- app/routes/celery_router/__init__.py | 2 + app/routes/celery_router/celery_samples.py | 5 ++- app/routes/home/__init__.py | 2 + app/routes/home/home.py | 6 +-- app/routes/users/__init__.py | 2 + app/schemas/users/users_request.py | 15 +++++--- app/schemas/users/users_response.py | 5 ++- app/sessions/db.py | 12 +++--- app/tests/test_daos_home.py | 20 +++++----- app/tests/test_daos_users.py | 8 ++-- app/utils/exception_handler.py | 2 +- app/utils/redis_utils.py | 2 + app/utils/slack_notification_utils.py | 6 ++- app/wrappers/cache_wrappers.py | 43 ++++++++++++---------- compose.yaml | 2 +- dependencies.py | 2 + pytest.ini | 2 +- scripts/local_server.sh | 2 +- scripts/update-ecs.sh | 2 +- 41 files changed, 187 insertions(+), 114 deletions(-) diff --git a/.config.celery b/.config.celery index 811e258..b98f480 100644 --- a/.config.celery +++ b/.config.celery @@ -1,4 +1,4 @@ RESULT_EXPIRES = 200 RESULT_PERSISTENT = True WORKER_SEND_TASK_EVENT = False -WORKER_PREFETCH_MULTIPLIER = 1 \ No newline at end of file +WORKER_PREFETCH_MULTIPLIER = 1 diff --git a/.env.example b/.env.example index 6c2b988..9a74af1 100644 --- a/.env.example +++ b/.env.example @@ -3,9 +3,9 @@ DB_PASSWORD= DB_HOSTNAME= DB_PORT= DB_NAME= -OPENAI_API_KEY_GPT4= -OPENAI_API_KEY_WEDNESDAY= SECRET_KEY= +CACHE_ENABLED= +SENTRY_ENABLED= REDIS_URL= SENTRY_DSN= SLACK_WEBHOOK_URL= diff --git a/.flake8 b/.flake8 index e5144d4..bc9d1df 100644 --- a/.flake8 +++ b/.flake8 @@ -1,7 +1,7 @@ [flake8] ignore = E501, W503 max-line-length = 120 -exclude = +exclude = .git, __pycache__, venv/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7bc7b5a..7995cb9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,7 +15,7 @@ repos: rev: v3.12.0 hooks: - id: reorder-python-imports - exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/) + exclude: ^(pre_commit/resources/|testing/resources/python3_hooks_repo/alembic/) args: [--py39-plus, --add-import, 'from __future__ import annotations'] - repo: https://github.com/asottile/add-trailing-comma rev: v3.1.0 diff --git a/README.Docker.md b/README.Docker.md index 6dae561..cf82a2f 100644 --- a/README.Docker.md +++ b/README.Docker.md @@ -19,4 +19,4 @@ Consult Docker's [getting started](https://docs.docker.com/go/get-started-sharin docs for more detail on building and pushing. ### References -* [Docker's Python guide](https://docs.docker.com/language/python/) \ No newline at end of file +* [Docker's Python guide](https://docs.docker.com/language/python/) diff --git a/alembic/README b/alembic/README index 98e4f9c..2500aa1 100644 --- a/alembic/README +++ b/alembic/README @@ -1 +1 @@ -Generic single-database configuration. \ No newline at end of file +Generic single-database configuration. diff --git a/alembic/env.py b/alembic/env.py index cdb247a..0325721 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -1,23 +1,25 @@ +from __future__ import annotations + import json import os from logging.config import fileConfig -from app.config.base import db_settings from dotenv import load_dotenv from sqlalchemy import engine_from_config from sqlalchemy import pool -from alembic import context +from alembic import context # type: ignore +from app.config.base import settings load_dotenv() print("==" * 50, "\n\n\n", "OS ENVIRONMENT", os.environ, "\n\n\n", "==" * 50) -HOST = db_settings.DB_HOSTNAME -PORT = db_settings.DB_PORT -DBNAME = db_settings.DB_NAME -USERNAME = db_settings.DB_USERNAME -PASSWORD = db_settings.DB_PASSWORD +HOST = settings.DB_HOSTNAME +PORT = settings.DB_PORT +DBNAME = settings.DB_NAME +USERNAME = settings.DB_USERNAME +PASSWORD = settings.DB_PASSWORD if "PYTHON_FASTAPI_TEMPLATE_CLUSTER_SECRET" in os.environ: print("Connecting to database on RDS..\n") diff --git a/app/app.py b/app/app.py index 6c0157f..5628933 100644 --- a/app/app.py +++ b/app/app.py @@ -1,22 +1,27 @@ -import sentry_sdk +from __future__ import annotations +import sentry_sdk from fastapi import FastAPI +from fastapi.exceptions import HTTPException +from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi_pagination import add_pagination -from fastapi.exceptions import HTTPException, RequestValidationError from sentry_sdk.integrations.asgi import SentryAsgiMiddleware -from app.config.base import cached_endpoints, settings +from app.config.base import cached_endpoints +from app.config.base import settings from app.config.celery_utils import create_celery from app.middlewares.cache_middleware import CacheMiddleware from app.middlewares.rate_limiter_middleware import RateLimitMiddleware from app.middlewares.request_id_injection import RequestIdInjection from app.routes import api_router -from app.utils.exception_handler import exception_handler, validation_exception_handler, http_exception_handler +from app.utils.exception_handler import exception_handler +from app.utils.exception_handler import http_exception_handler +from app.utils.exception_handler import validation_exception_handler # Sentry Initialization -if settings.SENTRY_DSN: +if settings.SENTRY_ENABLED: sentry_sdk.init( dsn=settings.SENTRY_DSN, traces_sample_rate=1.0, # Sample rate of 100% @@ -44,10 +49,11 @@ app.add_middleware(RequestIdInjection) app.add_middleware(CacheMiddleware, cached_endpoints=cached_endpoints.CACHED_ENDPOINTS) -try: - app.add_middleware(SentryAsgiMiddleware) -except Exception as e: - print(f"Error while adding Sentry Middleware: {e}") +if settings.SENTRY_ENABLED: + try: + app.add_middleware(SentryAsgiMiddleware) + except Exception as e: + print(f"Error while adding Sentry Middleware: {e}") # Include the routers app.include_router(api_router, prefix="/api") diff --git a/app/celery_tasks/tasks.py b/app/celery_tasks/tasks.py index 4ca50d9..7ba2adf 100644 --- a/app/celery_tasks/tasks.py +++ b/app/celery_tasks/tasks.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import time from celery import shared_task diff --git a/app/config/base.py b/app/config/base.py index 45b083d..20ce3c6 100644 --- a/app/config/base.py +++ b/app/config/base.py @@ -3,16 +3,6 @@ from pydantic import BaseSettings -class CelerySettings(BaseSettings): - RESULT_EXPIRES: int - RESULT_PERSISTENT: bool - WORKER_SEND_TASK_EVENT: bool - WORKER_PREFETCH_MULTIPLIER: int - - class Config: - env_file = ".config.celery" - - class DBSettings(BaseSettings): DB_HOSTNAME: str DB_PORT: str @@ -24,7 +14,15 @@ class Config: env_file = ".env" -class Settings(BaseSettings): +class FlagFeatureSettings(BaseSettings): + CACHE_ENABLED: bool + SENTRY_ENABLED: bool = False + + class Config: + env_file = ".env" + + +class Settings(FlagFeatureSettings, DBSettings): SECRET_KEY: str REDIS_URL: str SENTRY_DSN: str | None @@ -40,7 +38,16 @@ class CachedEndpoints(BaseSettings): CACHED_ENDPOINTS: list = ["/cache-sample/"] -db_settings = DBSettings() +class CelerySettings(BaseSettings): + RESULT_EXPIRES: int + RESULT_PERSISTENT: bool + WORKER_SEND_TASK_EVENT: bool + WORKER_PREFETCH_MULTIPLIER: int + + class Config: + env_file = ".config.celery" + + settings = Settings() celery_settings = CelerySettings() cached_endpoints = CachedEndpoints() diff --git a/app/config/celery_config.py b/app/config/celery_config.py index 1da7a0c..e1a446a 100644 --- a/app/config/celery_config.py +++ b/app/config/celery_config.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from functools import lru_cache from app.config.base import settings @@ -16,7 +18,7 @@ class BaseConfig: CELERY_TASK_ROUTES = (route_task,) -@lru_cache() +@lru_cache def get_settings(): config_cls = BaseConfig return config_cls() diff --git a/app/config/celery_utils.py b/app/config/celery_utils.py index a8caab9..3d88db4 100644 --- a/app/config/celery_utils.py +++ b/app/config/celery_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from celery import current_app as current_celery_app from celery.result import AsyncResult diff --git a/app/config/redis_config.py b/app/config/redis_config.py index 656611e..1e64665 100644 --- a/app/config/redis_config.py +++ b/app/config/redis_config.py @@ -1,6 +1,9 @@ -from .base import settings +from __future__ import annotations + from redis import asyncio +from .base import settings + async def get_redis_pool(): return asyncio.from_url(settings.REDIS_URL, encoding="utf-8", decode_responses=True) diff --git a/app/constants/jwt_utils.py b/app/constants/jwt_utils.py index 50a4869..4b11736 100644 --- a/app/constants/jwt_utils.py +++ b/app/constants/jwt_utils.py @@ -1,6 +1,10 @@ -from fastapi import HTTPException -import jwt +from __future__ import annotations + import datetime + +import jwt +from fastapi import HTTPException + from app.config.base import settings diff --git a/app/constants/messages/users.py b/app/constants/messages/users.py index c18945c..dd1eb46 100644 --- a/app/constants/messages/users.py +++ b/app/constants/messages/users.py @@ -1,3 +1,5 @@ +from __future__ import annotations + user_messages = { "CREATED_SUCCESSFULLY": "User registered successfully.", "EMAIL_ALREADY_EXIST": "Email already registered.", diff --git a/app/daos/users.py b/app/daos/users.py index 7297dae..a2af82a 100644 --- a/app/daos/users.py +++ b/app/daos/users.py @@ -19,14 +19,13 @@ from app.schemas.users.users_request import Login from app.utils.user_utils import check_existing_field from app.utils.user_utils import response_formatter -from app.wrappers.cache_wrappers import create_cache -from app.wrappers.cache_wrappers import retrieve_cache +from app.wrappers.cache_wrappers import CacheUtils async def get_user(user_id: int, db_session: Session): try: cache_key = f"user_{user_id}" - cached_user, _ = await retrieve_cache(cache_key) + cached_user, _ = await CacheUtils.retrieve_cache(cache_key) if cached_user: return json.loads(cached_user) # Check if the user already exists in the database @@ -47,7 +46,7 @@ async def get_user(user_id: int, db_session: Session): if not user: raise NoUserFoundException(messages["NO_USER_FOUND_FOR_ID"]) - await create_cache(json.dumps(user._asdict(), default=str), cache_key, 60) + await CacheUtils.create_cache(json.dumps(user._asdict(), default=str), cache_key, 60) return user._asdict() except Exception as e: # Return a user-friendly error message to the client diff --git a/app/exceptions.py b/app/exceptions.py index 984528f..23766f9 100644 --- a/app/exceptions.py +++ b/app/exceptions.py @@ -1,24 +1,33 @@ +from __future__ import annotations + + class ExternalServiceException(Exception): pass class NoUserFoundException(Exception): pass + + class EmailAlreadyExistException(Exception): pass + class MobileAlreadyExistException(Exception): pass + class InvalidCredentialsException(Exception): pass - + + class CentryTestException(Exception): pass + class DatabaseConnectionException(Exception): pass class RedisUrlNotFoundException(Exception): - pass \ No newline at end of file + pass diff --git a/app/middlewares/cache_middleware.py b/app/middlewares/cache_middleware.py index 1bf8d3c..bf61785 100644 --- a/app/middlewares/cache_middleware.py +++ b/app/middlewares/cache_middleware.py @@ -9,15 +9,14 @@ from starlette.responses import StreamingResponse from app.config.base import settings -from app.wrappers.cache_wrappers import create_cache -from app.wrappers.cache_wrappers import retrieve_cache +from app.wrappers.cache_wrappers import CacheUtils class CacheMiddleware(BaseHTTPMiddleware): def __init__( - self, - app, - cached_endpoints: list[str], + self, + app, + cached_endpoints: list[str], ): super().__init__(app) self.cached_endpoints = cached_endpoints @@ -30,7 +29,7 @@ def matches_any_path(self, path_url): async def handle_max_age(self, max_age, response_body, key): if max_age: - await create_cache(response_body[0].decode(), key, max_age) + await CacheUtils.create_cache(response_body[0].decode(), key, max_age) async def dispatch(self, request: Request, call_next) -> Response: path_url = request.url.path @@ -45,7 +44,7 @@ async def dispatch(self, request: Request, call_next) -> Response: if request_type != "GET": return await call_next(request) - stored_cache, expire = await retrieve_cache(key) + stored_cache, expire = await CacheUtils.retrieve_cache(key) res = stored_cache and cache_control != "no-cache" if res: @@ -64,5 +63,5 @@ async def dispatch(self, request: Request, call_next) -> Response: max_age = int(max_age_match.group(1)) await self.handle_max_age(max_age, response_body, key) elif matches: - await create_cache(response_body[0].decode(), key, max_age) + await CacheUtils.create_cache(response_body[0].decode(), key, max_age) return response diff --git a/app/middlewares/rate_limiter_middleware.py b/app/middlewares/rate_limiter_middleware.py index e9c1085..a49d329 100644 --- a/app/middlewares/rate_limiter_middleware.py +++ b/app/middlewares/rate_limiter_middleware.py @@ -1,8 +1,11 @@ -from app.config.redis_config import get_redis_pool +from __future__ import annotations + from fastapi.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request +from app.config.redis_config import get_redis_pool + MAX_REQUESTS = 10 TIME_WINDOW = 60 diff --git a/app/routes/__init__.py b/app/routes/__init__.py index a3cb482..dab0049 100644 --- a/app/routes/__init__.py +++ b/app/routes/__init__.py @@ -1,9 +1,11 @@ +from __future__ import annotations + from fastapi import APIRouter +from .cache_router import cache_sample_router +from .celery_router import celery_sample_router from .home import home_router from .users import user_router -from .celery_router import celery_sample_router -from .cache_router import cache_sample_router api_router = APIRouter() api_router.include_router(user_router, prefix="/user") diff --git a/app/routes/cache_router/__init__.py b/app/routes/cache_router/__init__.py index 8de5f9d..b76c982 100644 --- a/app/routes/cache_router/__init__.py +++ b/app/routes/cache_router/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .cache_samples import cache_sample_router __all__ = ["cache_sample_router"] diff --git a/app/routes/cache_router/cache_samples.py b/app/routes/cache_router/cache_samples.py index a739b15..fee4149 100644 --- a/app/routes/cache_router/cache_samples.py +++ b/app/routes/cache_router/cache_samples.py @@ -1,7 +1,10 @@ +from __future__ import annotations + import random from fastapi import APIRouter from fastapi.security import HTTPBearer + from app.middlewares.request_id_injection import request_id_contextvar cache_sample_router = APIRouter() @@ -11,5 +14,5 @@ @cache_sample_router.get("/get-cache", tags=["Cache-Sample"]) def get_cache(): print("Request ID:", request_id_contextvar.get()) - response = random.randint(100, 1000) #NOSONAR + response = random.randint(100, 1000) # NOSONAR return {"random value is": response} diff --git a/app/routes/celery_router/__init__.py b/app/routes/celery_router/__init__.py index 2cf150d..d1c52da 100644 --- a/app/routes/celery_router/__init__.py +++ b/app/routes/celery_router/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .celery_samples import celery_sample_router __all__ = ["celery_sample_router"] diff --git a/app/routes/celery_router/celery_samples.py b/app/routes/celery_router/celery_samples.py index 125f648..5fa811f 100644 --- a/app/routes/celery_router/celery_samples.py +++ b/app/routes/celery_router/celery_samples.py @@ -1,6 +1,9 @@ +from __future__ import annotations + from fastapi import APIRouter -from app.celery_tasks.tasks import add from fastapi.security import HTTPBearer + +from app.celery_tasks.tasks import add from app.middlewares.request_id_injection import request_id_contextvar celery_sample_router = APIRouter() diff --git a/app/routes/home/__init__.py b/app/routes/home/__init__.py index 67b69e4..4040075 100644 --- a/app/routes/home/__init__.py +++ b/app/routes/home/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .home import home_router __all__ = ["home_router"] diff --git a/app/routes/home/home.py b/app/routes/home/home.py index 5dc4cbc..675a41f 100644 --- a/app/routes/home/home.py +++ b/app/routes/home/home.py @@ -35,9 +35,9 @@ async def external_service_endpoint(): @home_router.get("/sentry-test", tags=["Home"]) def sentry_endpoint(): - if not settings.SENTRY_DSN: - raise HTTPException(status_code=503, detail="Sentry DSN not found") - raise CentryTestException("Centry Test") + if not settings.SENTRY_ENABLED: + raise HTTPException(status_code=503, detail="Sentry is not enabled") + raise CentryTestException("Sentry Test") @home_router.get("/{path:path}", include_in_schema=False) diff --git a/app/routes/users/__init__.py b/app/routes/users/__init__.py index 38b3745..e3ddde6 100644 --- a/app/routes/users/__init__.py +++ b/app/routes/users/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .users import user_router __all__ = ["user_router"] diff --git a/app/schemas/users/users_request.py b/app/schemas/users/users_request.py index 9477d9e..2d5230b 100644 --- a/app/schemas/users/users_request.py +++ b/app/schemas/users/users_request.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import re +from email_validator import validate_email from pydantic import BaseModel from pydantic import validator -from email_validator import validate_email class CreateUser(BaseModel): @@ -37,11 +39,12 @@ def validate_password_strength(cls, password): or not any(char.islower() for char in password) or not any(char.isdigit() for char in password) or not re.search( - r'[!@#$%^&*(),.?":{}|<>]', password + r'[!@#$%^&*(),.?":{}|<>]', + password, ) # The regular expression [!@#$%^&*(),.?":{}|<>] matches any of these special characters. ): raise ValueError( - "Password must be strong: at least 8 characters, containing at least one uppercase letter, one lowercase letter, one digit, and one special character." + "Password must be strong: at least 8 characters, containing at least one uppercase letter, one lowercase letter, one digit, and one special character.", ) return password @@ -51,8 +54,8 @@ class Config: "name": "Anas Nadeem", "email": "anas@gmail.com", "mobile": "1234567890", - "password": "Test@123", #NOSONAR - } + "password": "Test@123", # NOSONAR + }, } @@ -67,4 +70,4 @@ def validate_email(cls, email): return email class Config: - schema_extra = {"example": {"email": "anas@gmail.com", "password": "Test@123"}} #NOSONAR + schema_extra = {"example": {"email": "anas@gmail.com", "password": "Test@123"}} # NOSONAR diff --git a/app/schemas/users/users_response.py b/app/schemas/users/users_response.py index af8a179..c2a09cf 100644 --- a/app/schemas/users/users_response.py +++ b/app/schemas/users/users_response.py @@ -1,4 +1,7 @@ -from pydantic import BaseModel, Field +from __future__ import annotations + +from pydantic import BaseModel +from pydantic import Field class UserOutResponse(BaseModel): diff --git a/app/sessions/db.py b/app/sessions/db.py index e9ca8b4..1bcb76f 100644 --- a/app/sessions/db.py +++ b/app/sessions/db.py @@ -13,17 +13,17 @@ from sqlalchemy.orm import sessionmaker from sqlalchemy.pool import StaticPool -from app.config.base import db_settings +from app.config.base import settings from app.exceptions import DatabaseConnectionException load_dotenv() # Set the default values for connecting locally -HOST = db_settings.DB_HOSTNAME -PORT = db_settings.DB_PORT -DBNAME = db_settings.DB_NAME -USERNAME = db_settings.DB_USERNAME -PASSWORD = db_settings.DB_PASSWORD +HOST = settings.DB_HOSTNAME +PORT = settings.DB_PORT +DBNAME = settings.DB_NAME +USERNAME = settings.DB_USERNAME +PASSWORD = settings.DB_PASSWORD if "pytest" in sys.modules: SQLALCHEMY_DATABASE_URL = "sqlite://" diff --git a/app/tests/test_daos_home.py b/app/tests/test_daos_home.py index 65c4063..212b7e5 100644 --- a/app/tests/test_daos_home.py +++ b/app/tests/test_daos_home.py @@ -1,14 +1,15 @@ -import asyncio -import random +from __future__ import annotations + import unittest -from unittest.mock import MagicMock, patch +from unittest.mock import patch from app.daos.home import external_service_call from app.exceptions import ExternalServiceException + class TestExternalServiceCall(unittest.TestCase): - @patch('app.daos.home.random') - @patch('app.daos.home.asyncio.sleep') + @patch("app.daos.home.random") + @patch("app.daos.home.asyncio.sleep") async def test_external_service_call_success(self, mock_sleep, mock_random): # Mocking the random delay mock_random.uniform.return_value = 0.5 # Mocking a fixed delay for simplicity @@ -20,8 +21,8 @@ async def test_external_service_call_success(self, mock_sleep, mock_random): mock_sleep.assert_called_once_with(0.5) # Check if sleep is called with the correct delay self.assertEqual(result, "Success from external service") - @patch('your_module.random') - @patch('your_module.asyncio.sleep') + @patch("app.daos.home.random") + @patch("app.daos.home.asyncio.sleep") async def test_external_service_call_failure(self, mock_sleep, mock_random): # Mocking the random delay mock_random.uniform.return_value = 0.5 # Mocking a fixed delay for simplicity @@ -35,7 +36,6 @@ async def test_external_service_call_failure(self, mock_sleep, mock_random): # Assertions mock_sleep.assert_called_once_with(0.5) # Check if sleep is called with the correct delay -if __name__ == '__main__': - unittest.main() - +if __name__ == "__main__": + unittest.main() diff --git a/app/tests/test_daos_users.py b/app/tests/test_daos_users.py index 5a8ad5c..86280e0 100644 --- a/app/tests/test_daos_users.py +++ b/app/tests/test_daos_users.py @@ -25,17 +25,17 @@ @pytest.fixture def mock_create_cache(): - with patch("app.wrappers.cache_wrappers.create_cache") as mock_create_cache: + with patch("app.wrappers.cache_wrappers.CacheUtils.create_cache") as mock_create_cache: yield mock_create_cache @pytest.fixture def mock_retrieve_cache(): - with patch("app.wrappers.cache_wrappers.retrieve_cache") as mock_retrieve_cache: + with patch("app.wrappers.cache_wrappers.CacheUtils.retrieve_cache") as mock_retrieve_cache: yield mock_retrieve_cache -@patch("app.wrappers.cache_wrappers.create_cache") +@patch("app.wrappers.cache_wrappers.CacheUtils.create_cache") @pytest.mark.asyncio @freeze_time(datetime(2024, 3, 15, 17, 20, 37, 495390).strftime("%Y-%m-%d %H:%M:%S.%f")) async def test_get_user(self, mock_create_cache): @@ -142,7 +142,7 @@ def test_list_users_exception(self, mock_paginate): class TestGetUser(unittest.IsolatedAsyncioTestCase): - @patch("app.daos.users.retrieve_cache") + @patch("app.wrappers.cache_wrappers.CacheUtils.retrieve_cache") async def test_get_user_no_cache_no_user_found(self, mock_retrieve_cache): # Mocking retrieve_cache to return no cached user mock_retrieve_cache.return_value = None, None diff --git a/app/utils/exception_handler.py b/app/utils/exception_handler.py index 6e1062a..d6d0f5b 100644 --- a/app/utils/exception_handler.py +++ b/app/utils/exception_handler.py @@ -19,7 +19,7 @@ async def validation_exception_handler(exc: RequestValidationError): ) -async def http_exception_handler(request:Request, exc: HTTPException): +async def http_exception_handler(request: Request, exc: HTTPException): print(request) return JSONResponse(status_code=exc.status_code, content={"success": False, "message": exc.detail}) diff --git a/app/utils/redis_utils.py b/app/utils/redis_utils.py index 5d1a5e9..78d89a7 100644 --- a/app/utils/redis_utils.py +++ b/app/utils/redis_utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from app.config.redis_config import get_redis_pool diff --git a/app/utils/slack_notification_utils.py b/app/utils/slack_notification_utils.py index b83c1af..929903f 100644 --- a/app/utils/slack_notification_utils.py +++ b/app/utils/slack_notification_utils.py @@ -1,7 +1,9 @@ -import os -import requests +from __future__ import annotations + import json +import os +import requests from dotenv import load_dotenv from fastapi import HTTPException diff --git a/app/wrappers/cache_wrappers.py b/app/wrappers/cache_wrappers.py index b96ec16..08f6c32 100644 --- a/app/wrappers/cache_wrappers.py +++ b/app/wrappers/cache_wrappers.py @@ -2,26 +2,31 @@ from app.config.base import settings from app.config.redis_config import get_redis_pool -from app.exceptions import RedisUrlNotFoundException -if not settings.REDIS_URL: - raise RedisUrlNotFoundException("Failed To get Redis URL") - -async def create_cache(resp, key: str, ex: int = 60): - redis = await get_redis_pool() - await redis.set(key, resp, ex=ex) - - -async def retrieve_cache(key: str): - redis = await get_redis_pool() - data = await redis.get(key) - if not data: +class CacheUtils: + CACHE_ENABLED = settings.CACHE_ENABLED + + @classmethod + async def create_cache(cls, resp, key: str, ex: int = 60): + if cls.CACHE_ENABLED: + redis = await get_redis_pool() + await redis.set(key, resp, ex=ex) + + @classmethod + async def retrieve_cache(cls, key: str): + if cls.CACHE_ENABLED: + redis = await get_redis_pool() + data = await redis.get(key) + if not data: + return None, None + expire = await redis.ttl(key) + return data, expire return None, None - expire = await redis.ttl(key) - return data, expire - -async def invalidate_cache(key: str): - redis = await get_redis_pool() - await redis.delete(key) + @classmethod + async def invalidate_cache(cls, key: str): + if cls.CACHE_ENABLED: + redis = await get_redis_pool() + await redis.delete(key) + return None diff --git a/compose.yaml b/compose.yaml index cab1f85..bade2f7 100644 --- a/compose.yaml +++ b/compose.yaml @@ -108,4 +108,4 @@ networks: volumes: db: - pmm-data: \ No newline at end of file + pmm-data: diff --git a/dependencies.py b/dependencies.py index cabb797..2a9e2e2 100644 --- a/dependencies.py +++ b/dependencies.py @@ -1,4 +1,6 @@ # app/dependencies.py +from __future__ import annotations + import pybreaker # Global Circuit Breaker diff --git a/pytest.ini b/pytest.ini index 007e4a9..152f8e5 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] markers = - asyncio: marks tests as async (deselect with '-m "not asyncio"') \ No newline at end of file + asyncio: marks tests as async (deselect with '-m "not asyncio"') diff --git a/scripts/local_server.sh b/scripts/local_server.sh index 35a8ffb..3576a81 100755 --- a/scripts/local_server.sh +++ b/scripts/local_server.sh @@ -1,2 +1,2 @@ # Run migrations and start server -alembic upgrade head && uvicorn app.app:app --host 0.0.0.0 --port 8000 \ No newline at end of file +alembic upgrade head && uvicorn app.app:app --host 0.0.0.0 --port 8000 diff --git a/scripts/update-ecs.sh b/scripts/update-ecs.sh index 46769f8..56ca889 100755 --- a/scripts/update-ecs.sh +++ b/scripts/update-ecs.sh @@ -1 +1 @@ -copilot deploy --name "python-fastapi-template-$1-svc" -e $1 \ No newline at end of file +copilot deploy --name "python-fastapi-template-$1-svc" -e $1 From b574574aafade1fa901f0b08c75f237367991012 Mon Sep 17 00:00:00 2001 From: Anas WS Date: Tue, 19 Mar 2024 18:48:37 +0530 Subject: [PATCH 2/3] slack flag added --- .env.example | 5 +++-- app/config/base.py | 1 + app/utils/slack_notification_utils.py | 9 +++++---- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 9a74af1..297b586 100644 --- a/.env.example +++ b/.env.example @@ -4,9 +4,10 @@ DB_HOSTNAME= DB_PORT= DB_NAME= SECRET_KEY= -CACHE_ENABLED= -SENTRY_ENABLED= REDIS_URL= SENTRY_DSN= SLACK_WEBHOOK_URL= DB_ROOT_PASSWORD= //this is applicable for .env.local file only +CACHE_ENABLED= +SENTRY_ENABLED= +SLACK_ENABLED= diff --git a/app/config/base.py b/app/config/base.py index 20ce3c6..825a694 100644 --- a/app/config/base.py +++ b/app/config/base.py @@ -17,6 +17,7 @@ class Config: class FlagFeatureSettings(BaseSettings): CACHE_ENABLED: bool SENTRY_ENABLED: bool = False + SLACK_ENABLED: bool = False class Config: env_file = ".env" diff --git a/app/utils/slack_notification_utils.py b/app/utils/slack_notification_utils.py index 929903f..e27329e 100644 --- a/app/utils/slack_notification_utils.py +++ b/app/utils/slack_notification_utils.py @@ -1,13 +1,11 @@ from __future__ import annotations import json -import os import requests -from dotenv import load_dotenv from fastapi import HTTPException -load_dotenv() +from app.config.base import settings def send_slack_message(payload): @@ -18,7 +16,10 @@ def send_slack_message(payload): Returns: HTTP response code, i.e., """ - webhook_url = os.environ.get("SLACK_WEBHOOK_URL") # Use get to avoid KeyError + if not settings.SLACK_ENABLEDs: + return + + webhook_url = settings.SLACK_WEBHOOK_URL if not webhook_url: raise HTTPException(status_code=400, detail="Slack URL not configured.") From 65cf4b792ea68be6888a59609276017845649365 Mon Sep 17 00:00:00 2001 From: Anas WS Date: Tue, 19 Mar 2024 19:05:59 +0530 Subject: [PATCH 3/3] readme added --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 114ad40..5665815 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ This repository provides a template for creating and deploying a FastAPI project - Linting using flake8 - Formatting using black - Code quality analysis using SonarQube +- Feature flagging added - User can enabled/disabled ### Getting Started