Skip to content

Commit

Permalink
Merge pull request #37 from wednesday-solutions/feat/flag-feature
Browse files Browse the repository at this point in the history
feat - flag feature added
  • Loading branch information
himanshu-wedensday authored Mar 19, 2024
2 parents 91ae8a5 + 65cf4b7 commit 13c35c9
Show file tree
Hide file tree
Showing 42 changed files with 194 additions and 117 deletions.
2 changes: 1 addition & 1 deletion .config.celery
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
RESULT_EXPIRES = 200
RESULT_PERSISTENT = True
WORKER_SEND_TASK_EVENT = False
WORKER_PREFETCH_MULTIPLIER = 1
WORKER_PREFETCH_MULTIPLIER = 1
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ DB_PASSWORD=
DB_HOSTNAME=
DB_PORT=
DB_NAME=
OPENAI_API_KEY_GPT4=
OPENAI_API_KEY_WEDNESDAY=
SECRET_KEY=
REDIS_URL=
SENTRY_DSN=
SLACK_WEBHOOK_URL=
DB_ROOT_PASSWORD= //this is applicable for .env.local file only
CACHE_ENABLED=
SENTRY_ENABLED=
SLACK_ENABLED=
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[flake8]
ignore = E501, W503
max-line-length = 120
exclude =
exclude =
.git,
__pycache__,
venv/
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.Docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
* [Docker's Python guide](https://docs.docker.com/language/python/)
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion alembic/README
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Generic single-database configuration.
Generic single-database configuration.
16 changes: 9 additions & 7 deletions alembic/env.py
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
24 changes: 15 additions & 9 deletions app/app.py
Original file line number Diff line number Diff line change
@@ -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%
Expand Down Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions app/celery_tasks/tasks.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import time

from celery import shared_task
Expand Down
32 changes: 20 additions & 12 deletions app/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,7 +14,16 @@ class Config:
env_file = ".env"


class Settings(BaseSettings):
class FlagFeatureSettings(BaseSettings):
CACHE_ENABLED: bool
SENTRY_ENABLED: bool = False
SLACK_ENABLED: bool = False

class Config:
env_file = ".env"


class Settings(FlagFeatureSettings, DBSettings):
SECRET_KEY: str
REDIS_URL: str
SENTRY_DSN: str | None
Expand All @@ -40,7 +39,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()
4 changes: 3 additions & 1 deletion app/config/celery_config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from functools import lru_cache

from app.config.base import settings
Expand All @@ -16,7 +18,7 @@ class BaseConfig:
CELERY_TASK_ROUTES = (route_task,)


@lru_cache()
@lru_cache
def get_settings():
config_cls = BaseConfig
return config_cls()
Expand Down
2 changes: 2 additions & 0 deletions app/config/celery_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from celery import current_app as current_celery_app
from celery.result import AsyncResult

Expand Down
5 changes: 4 additions & 1 deletion app/config/redis_config.py
Original file line number Diff line number Diff line change
@@ -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)
8 changes: 6 additions & 2 deletions app/constants/jwt_utils.py
Original file line number Diff line number Diff line change
@@ -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


Expand Down
2 changes: 2 additions & 0 deletions app/constants/messages/users.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

user_messages = {
"CREATED_SUCCESSFULLY": "User registered successfully.",
"EMAIL_ALREADY_EXIST": "Email already registered.",
Expand Down
7 changes: 3 additions & 4 deletions app/daos/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
13 changes: 11 additions & 2 deletions app/exceptions.py
Original file line number Diff line number Diff line change
@@ -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
pass
15 changes: 7 additions & 8 deletions app/middlewares/cache_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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
5 changes: 4 additions & 1 deletion app/middlewares/rate_limiter_middleware.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
6 changes: 4 additions & 2 deletions app/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -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")
Expand Down
2 changes: 2 additions & 0 deletions app/routes/cache_router/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from .cache_samples import cache_sample_router

__all__ = ["cache_sample_router"]
Loading

1 comment on commit 13c35c9

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage

Coverage Report
FileStmtsMissCoverMissing
app
   app.py372824%12–67
app/config
   __init__.py3167%6
   base.py37295%53–54
   celery_config.py17170%1–27
   celery_utils.py21210%1–30
   redis_config.py5260%8–9
app/constants
   jwt_utils.py161131%11–23
app/daos
   home.py10550%11–18
   users.py706211%12–133
app/middlewares
   cache_middleware.py51510%1–67
   rate_limiter_middleware.py25250%1–35
   request_id_injection.py17170%1–25
app/models
   __init__.py330%1–5
   users.py27270%1–38
app/routes
   __init__.py12120%1–15
app/routes/cache_router
   __init__.py330%1–5
   cache_samples.py12120%1–18
app/routes/celery_router
   __init__.py330%1–5
   celery_samples.py12120%1–17
app/routes/home
   __init__.py330%1–5
   home.py33330%1–45
app/routes/users
   __init__.py330%1–5
   users.py38380%1–57
app/schemas/users
   users_request.py42420%1–73
   users_response.py10100%1–14
app/sessions
   db.py53530%1–82
app/tests
   test_basic.py201525%10–34
   test_daos_home.py231057%15–22, 28–37, 41
   test_daos_users.py1109514%19–208
app/utils
   exception_handler.py19190%1–36
   redis_utils.py440%1–7
   slack_notification_utils.py14140%1–32
   user_utils.py25250%1–36
app/wrappers
   cache_wrappers.py26260%1–32
TOTAL82170414% 

Tests Skipped Failures Errors Time
2 0 💤 0 ❌ 2 🔥 0.838s ⏱️

Please sign in to comment.