Initial check-in for notification - only structure
This commit is contained in:
parent
0a0c0d128d
commit
c0986b10bf
0
app/notification/Dockerfile
Normal file
0
app/notification/Dockerfile
Normal file
11
app/notification/backend/application/notification_hub.py
Normal file
11
app/notification/backend/application/notification_hub.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from app.notification.backend.business.notification_manager import (
|
||||||
|
NotificationManager,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationHub:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
self.notification_manager = NotificationManager()
|
||||||
|
return
|
||||||
10
app/notification/backend/business/notification_manager.py
Normal file
10
app/notification/backend/business/notification_manager.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from app.notification.backend.services.sms_service import SmsService
|
||||||
|
from app.notification.backend.services.in_app_notif_service import InAppNotifService
|
||||||
|
from app.notification.backend.services.email_service import EmailService
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationManager:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.sms_service = SmsService()
|
||||||
|
self.in_app_notif_service = InAppNotifService()
|
||||||
|
self.email_service = EmailService()
|
||||||
2
app/notification/backend/infra/email_handler.py
Normal file
2
app/notification/backend/infra/email_handler.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
class EmailHandler:
|
||||||
|
pass
|
||||||
2
app/notification/backend/infra/in_app_notif_handler.py
Normal file
2
app/notification/backend/infra/in_app_notif_handler.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
class InAppNotifHandler:
|
||||||
|
pass
|
||||||
2
app/notification/backend/infra/sms_handler.py
Normal file
2
app/notification/backend/infra/sms_handler.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
class SmsHandler:
|
||||||
|
pass
|
||||||
0
app/notification/backend/models/models.py
Normal file
0
app/notification/backend/models/models.py
Normal file
1
app/notification/backend/readme.md
Normal file
1
app/notification/backend/readme.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
You can put code for backend logic here.
|
||||||
2
app/notification/backend/services/email_service.py
Normal file
2
app/notification/backend/services/email_service.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
class EmailService:
|
||||||
|
pass
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
class InAppNotifService:
|
||||||
|
pass
|
||||||
2
app/notification/backend/services/sms_service.py
Normal file
2
app/notification/backend/services/sms_service.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
class SmsService:
|
||||||
|
pass
|
||||||
18
app/notification/common/config/app_settings.py
Normal file
18
app/notification/common/config/app_settings.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import os
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
|
class AppSettings(BaseSettings):
|
||||||
|
NAME: str = "notification"
|
||||||
|
|
||||||
|
AZURE_STORAGE_DOCUMENT_API_ENDPOINT: str = (
|
||||||
|
"https://freeleaps1document.blob.core.windows.net/"
|
||||||
|
)
|
||||||
|
AZURE_STORAGE_DOCUMENT_API_KEY: str = ""
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".myapp.env"
|
||||||
|
env_file_encoding = "utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
app_settings = AppSettings()
|
||||||
14
app/notification/common/config/log_settings.py
Normal file
14
app/notification/common/config/log_settings.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
## This would be the app specific log setting file
|
||||||
|
class LogSettings:
|
||||||
|
LOG_LEVEL: str = "DEBUG"
|
||||||
|
LOG_PATH_BASE: str = "./logs"
|
||||||
|
LOG_PATH: str = LOG_PATH_BASE + "/" + "app" + ".log"
|
||||||
|
LOG_RETENTION: str = "14 days"
|
||||||
|
LOG_ROTATION: str = "00:00" # mid night
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".log.env"
|
||||||
|
env_file_encoding = "utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
log_settings = LogSettings()
|
||||||
0
app/notification/requirements.txt
Normal file
0
app/notification/requirements.txt
Normal file
75
app/notification/webapi/bootstrap/application.py
Normal file
75
app/notification/webapi/bootstrap/application.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import logging
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.openapi.utils import get_openapi
|
||||||
|
|
||||||
|
from webapi.providers import common
|
||||||
|
from webapi.providers import logger
|
||||||
|
from webapi.providers import router
|
||||||
|
from webapi.providers import database
|
||||||
|
from webapi.providers import scheduler
|
||||||
|
from webapi.providers import exception_handler
|
||||||
|
from .freeleaps_app import FreeleapsApp
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> FastAPI:
|
||||||
|
logging.info("App initializing")
|
||||||
|
|
||||||
|
app = FreeleapsApp()
|
||||||
|
|
||||||
|
register(app, exception_handler)
|
||||||
|
register(app, database)
|
||||||
|
register(app, logger)
|
||||||
|
register(app, router)
|
||||||
|
register(app, scheduler)
|
||||||
|
register(app, common)
|
||||||
|
|
||||||
|
# Call the custom_openapi function to change the OpenAPI version
|
||||||
|
customize_openapi_security(app)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
# This function overrides the OpenAPI schema version to 3.0.0
|
||||||
|
def customize_openapi_security(app: FastAPI) -> None:
|
||||||
|
|
||||||
|
def custom_openapi():
|
||||||
|
if app.openapi_schema:
|
||||||
|
return app.openapi_schema
|
||||||
|
|
||||||
|
# Generate OpenAPI schema
|
||||||
|
openapi_schema = get_openapi(
|
||||||
|
title="FreeLeaps API",
|
||||||
|
version="3.1.0",
|
||||||
|
description="FreeLeaps API Documentation",
|
||||||
|
routes=app.routes,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure the components section exists in the OpenAPI schema
|
||||||
|
if "components" not in openapi_schema:
|
||||||
|
openapi_schema["components"] = {}
|
||||||
|
|
||||||
|
# Add security scheme to components
|
||||||
|
openapi_schema["components"]["securitySchemes"] = {
|
||||||
|
"bearerAuth": {
|
||||||
|
"type": "http",
|
||||||
|
"scheme": "bearer",
|
||||||
|
"bearerFormat": "JWT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add security requirement globally
|
||||||
|
openapi_schema["security"] = [{"bearerAuth": []}]
|
||||||
|
|
||||||
|
app.openapi_schema = openapi_schema
|
||||||
|
return app.openapi_schema
|
||||||
|
|
||||||
|
app.openapi = custom_openapi
|
||||||
|
|
||||||
|
|
||||||
|
def register(app, provider):
|
||||||
|
logging.info(provider.__name__ + " registering")
|
||||||
|
provider.register(app)
|
||||||
|
|
||||||
|
|
||||||
|
def boot(app, provider):
|
||||||
|
logging.info(provider.__name__ + " booting")
|
||||||
|
provider.boot(app)
|
||||||
6
app/notification/webapi/bootstrap/freeleaps_app.py
Normal file
6
app/notification/webapi/bootstrap/freeleaps_app.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
|
||||||
|
class FreeleapsApp(FastAPI):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
25
app/notification/webapi/config/site_settings.py
Normal file
25
app/notification/webapi/config/site_settings.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
|
class SiteSettings(BaseSettings):
|
||||||
|
NAME: str = "appname"
|
||||||
|
DEBUG: bool = True
|
||||||
|
|
||||||
|
ENV: str = "dev"
|
||||||
|
|
||||||
|
SERVER_HOST: str = "0.0.0.0"
|
||||||
|
SERVER_PORT: int = 8103
|
||||||
|
|
||||||
|
URL: str = "http://localhost"
|
||||||
|
TIME_ZONE: str = "UTC"
|
||||||
|
|
||||||
|
BASE_PATH: str = os.path.dirname(os.path.dirname((os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".devbase-webapi.env"
|
||||||
|
env_file_encoding = "utf-8"
|
||||||
|
|
||||||
|
|
||||||
|
site_settings = SiteSettings()
|
||||||
30
app/notification/webapi/main.py
Executable file
30
app/notification/webapi/main.py
Executable file
@ -0,0 +1,30 @@
|
|||||||
|
from webapi.bootstrap.application import create_app
|
||||||
|
from webapi.config.site_settings import site_settings
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from strawberry.fastapi import GraphQLRouter
|
||||||
|
from strawberry.fastapi.handlers import GraphQLTransportWSHandler, GraphQLWSHandler
|
||||||
|
import uvicorn
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
|
||||||
|
@app.get("/", status_code=301)
|
||||||
|
async def root():
|
||||||
|
"""
|
||||||
|
TODO: redirect client to /doc#
|
||||||
|
"""
|
||||||
|
return RedirectResponse("docs")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
uvicorn.run(app="main:app", host=site_settings.SERVER_HOST, port=site_settings.SERVER_PORT)
|
||||||
|
|
||||||
|
def get_context() -> Any:
|
||||||
|
# Define your context function. This is where you can set up authentication, database connections, etc.
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_root_value() -> Any:
|
||||||
|
# Define your root value function. This can be used to customize the root value for GraphQL operations.
|
||||||
|
return {}
|
||||||
31
app/notification/webapi/providers/common.py
Normal file
31
app/notification/webapi/providers/common.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from webapi.config.site_settings import site_settings
|
||||||
|
|
||||||
|
|
||||||
|
def register(app):
|
||||||
|
app.debug = site_settings.DEBUG
|
||||||
|
app.title = site_settings.NAME
|
||||||
|
|
||||||
|
add_global_middleware(app)
|
||||||
|
|
||||||
|
# This hook ensures that a connection is opened to handle any queries
|
||||||
|
# generated by the request.
|
||||||
|
@app.on_event("startup")
|
||||||
|
def startup():
|
||||||
|
pass
|
||||||
|
|
||||||
|
# This hook ensures that the connection is closed when we've finished
|
||||||
|
# processing the request.
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
def shutdown():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def add_global_middleware(app):
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
17
app/notification/webapi/providers/database.py
Normal file
17
app/notification/webapi/providers/database.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from webapi.config.site_settings import site_settings
|
||||||
|
from beanie import init_beanie
|
||||||
|
from motor.motor_asyncio import AsyncIOMotorClient
|
||||||
|
|
||||||
|
|
||||||
|
def register(app):
|
||||||
|
app.debug = site_settings.DEBUG
|
||||||
|
app.title = site_settings.NAME
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def start_database():
|
||||||
|
await initiate_database()
|
||||||
|
|
||||||
|
|
||||||
|
async def initiate_database():
|
||||||
|
#init your database here
|
||||||
|
pass
|
||||||
39
app/notification/webapi/providers/exception_handler.py
Normal file
39
app/notification/webapi/providers/exception_handler.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from fastapi import FastAPI, HTTPException
|
||||||
|
from fastapi.exceptions import RequestValidationError
|
||||||
|
from starlette.requests import Request
|
||||||
|
from starlette.responses import JSONResponse
|
||||||
|
from starlette.status import (
|
||||||
|
HTTP_400_BAD_REQUEST,
|
||||||
|
HTTP_401_UNAUTHORIZED,
|
||||||
|
HTTP_403_FORBIDDEN,
|
||||||
|
HTTP_404_NOT_FOUND,
|
||||||
|
HTTP_422_UNPROCESSABLE_ENTITY,
|
||||||
|
HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def custom_http_exception_handler(request: Request, exc: HTTPException):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content={"error": exc.detail},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=HTTP_400_BAD_REQUEST,
|
||||||
|
content={"error": str(exc)},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def exception_handler(request: Request, exc: Exception):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=HTTP_500_INTERNAL_SERVER_ERROR,
|
||||||
|
content={"error": str(exc)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register(app: FastAPI):
|
||||||
|
app.add_exception_handler(HTTPException, custom_http_exception_handler)
|
||||||
|
app.add_exception_handler(RequestValidationError, validation_exception_handler)
|
||||||
|
app.add_exception_handler(Exception, exception_handler)
|
||||||
60
app/notification/webapi/providers/logger.py
Normal file
60
app/notification/webapi/providers/logger.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
from loguru import logger
|
||||||
|
from common.config.log_settings import log_settings
|
||||||
|
|
||||||
|
|
||||||
|
def register(app=None):
|
||||||
|
level = log_settings.LOG_LEVEL
|
||||||
|
file_path = log_settings.LOG_PATH
|
||||||
|
retention = log_settings.LOG_RETENTION
|
||||||
|
rotation = log_settings.LOG_ROTATION
|
||||||
|
|
||||||
|
# intercept everything at the root logger
|
||||||
|
logging.root.handlers = [InterceptHandler()]
|
||||||
|
logging.root.setLevel(level)
|
||||||
|
|
||||||
|
# remove every other logger's handlers
|
||||||
|
# and propagate to root logger
|
||||||
|
for name in logging.root.manager.loggerDict.keys():
|
||||||
|
logging.getLogger(name).handlers = []
|
||||||
|
logging.getLogger(name).propagate = True
|
||||||
|
|
||||||
|
# configure loguru
|
||||||
|
logger.add(
|
||||||
|
sink=sys.stdout
|
||||||
|
)
|
||||||
|
logger.add(
|
||||||
|
sink=file_path,
|
||||||
|
level=level,
|
||||||
|
retention=retention,
|
||||||
|
rotation=rotation
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.disable("pika.adapters")
|
||||||
|
logger.disable("pika.connection")
|
||||||
|
logger.disable("pika.channel")
|
||||||
|
logger.disable("pika.callback")
|
||||||
|
logger.disable("pika.frame")
|
||||||
|
logger.disable("pika.spec")
|
||||||
|
logger.disable("aiormq.connection")
|
||||||
|
logger.disable("urllib3.connectionpool")
|
||||||
|
|
||||||
|
|
||||||
|
class InterceptHandler(logging.Handler):
|
||||||
|
def emit(self, record):
|
||||||
|
# Get corresponding Loguru level if it exists
|
||||||
|
try:
|
||||||
|
level = logger.level(record.levelname).name
|
||||||
|
except ValueError:
|
||||||
|
level = record.levelno
|
||||||
|
|
||||||
|
# Find caller from where originated the logged message
|
||||||
|
frame, depth = logging.currentframe(), 2
|
||||||
|
while frame.f_code.co_filename == logging.__file__:
|
||||||
|
frame = frame.f_back
|
||||||
|
depth += 1
|
||||||
|
|
||||||
|
logger.opt(depth=depth, exception=record.exc_info).log(
|
||||||
|
level, record.getMessage()
|
||||||
|
)
|
||||||
34
app/notification/webapi/providers/router.py
Normal file
34
app/notification/webapi/providers/router.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
from webapi.routes import api_router
|
||||||
|
|
||||||
|
from starlette import routing
|
||||||
|
|
||||||
|
|
||||||
|
def register(app):
|
||||||
|
app.include_router(
|
||||||
|
api_router,
|
||||||
|
prefix="/api",
|
||||||
|
tags=["api"],
|
||||||
|
dependencies=[],
|
||||||
|
responses={404: {"description": "no page found"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
if app.debug:
|
||||||
|
for route in app.routes:
|
||||||
|
if not isinstance(route, routing.WebSocketRoute):
|
||||||
|
print(
|
||||||
|
{
|
||||||
|
"path": route.path,
|
||||||
|
"endpoint": route.endpoint,
|
||||||
|
"name": route.name,
|
||||||
|
"methods": route.methods,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
{
|
||||||
|
"path": route.path,
|
||||||
|
"endpoint": route.endpoint,
|
||||||
|
"name": route.name,
|
||||||
|
"type": "web socket route",
|
||||||
|
}
|
||||||
|
)
|
||||||
8
app/notification/webapi/providers/scheduler.py
Normal file
8
app/notification/webapi/providers/scheduler.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
|
def register(app):
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def start_scheduler():
|
||||||
|
#create your scheduler here
|
||||||
|
pass
|
||||||
5
app/notification/webapi/routes/__init__.py
Normal file
5
app/notification/webapi/routes/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
api_router = APIRouter()
|
||||||
|
|
||||||
|
websocket_router = APIRouter()
|
||||||
15
app/notification/webapi/routes/api.py
Normal file
15
app/notification/webapi/routes/api.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from fastapi.routing import APIRoute
|
||||||
|
from starlette import routing
|
||||||
|
|
||||||
|
|
||||||
|
def post_process_router(app) -> None:
|
||||||
|
"""
|
||||||
|
Simplify operation IDs so that generated API clients have simpler function
|
||||||
|
names.
|
||||||
|
|
||||||
|
Should be called only after all routes have been added.
|
||||||
|
"""
|
||||||
|
for route in app.routes:
|
||||||
|
if isinstance(route, APIRoute):
|
||||||
|
if hasattr(route, "operation_id"):
|
||||||
|
route.operation_id = route.name # in this case, 'read_items'
|
||||||
@ -1 +1,2 @@
|
|||||||
export AZURE_STORAGE_DOCUMENT_API_KEY=xbiFtFeQ6v5dozgVM99fZ9huUomL7QcLu6s0y8zYHtIXZ8XdneKDMcg4liQr/9oNlVoRFcZhWjLY+ASt9cjICQ==
|
export AZURE_STORAGE_DOCUMENT_API_KEY=xbiFtFeQ6v5dozgVM99fZ9huUomL7QcLu6s0y8zYHtIXZ8XdneKDMcg4liQr/9oNlVoRFcZhWjLY+ASt9cjICQ==
|
||||||
|
export JWT_SECRET_KEY=ea84edf152976b2fcec12b78aa8e45bc26a5cf0ef61bf16f5c317ae33b3fd8b0
|
||||||
@ -1 +1,2 @@
|
|||||||
export AZURE_STORAGE_DOCUMENT_API_KEY=xbiFtFeQ6v5dozgVM99fZ9huUomL7QcLu6s0y8zYHtIXZ8XdneKDMcg4liQr/9oNlVoRFcZhWjLY+ASt9cjICQ==
|
export AZURE_STORAGE_DOCUMENT_API_KEY=xbiFtFeQ6v5dozgVM99fZ9huUomL7QcLu6s0y8zYHtIXZ8XdneKDMcg4liQr/9oNlVoRFcZhWjLY+ASt9cjICQ==
|
||||||
|
export JWT_SECRET_KEY=ea84edf152976b2fcec12b78aa8e45bc26a5cf0ef61bf16f5c317ae33b3fd8b0
|
||||||
5
sites/notification/.env
Normal file
5
sites/notification/.env
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export SENDGRID_API_KEY='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'
|
||||||
|
export EMAIL_FROM=freeleaps@freeleaps.com
|
||||||
|
export TWILIO_ACCOUNT_SID=ACf8c9283a6acda060258eadb29be58bc8
|
||||||
|
export TWILIO_AUTH_TOKEN=ef160748cc22c8b7195b49df4b8eca7e
|
||||||
|
export JWT_SECRET_KEY=ea84edf152976b2fcec12b78aa8e45bc26a5cf0ef61bf16f5c317ae33b3fd8b0
|
||||||
Loading…
Reference in New Issue
Block a user