template for service rather than hub
Previous template structure had direct handling of auth. This structure is designed to get auth from the hub instead.
This commit is contained in:
parent
ed4a3fe0b8
commit
ea9803536a
27 changed files with 223 additions and 117 deletions
|
|
@ -7,11 +7,8 @@ requests
|
||||||
itsdangerous
|
itsdangerous
|
||||||
starlette
|
starlette
|
||||||
pydantic-settings
|
pydantic-settings
|
||||||
authlib
|
|
||||||
joserfc
|
joserfc
|
||||||
httpx
|
httpx
|
||||||
types-authlib
|
|
||||||
python-jose
|
|
||||||
pytest
|
pytest
|
||||||
uvloop; sys_platform != 'win32'
|
uvloop; sys_platform != 'win32'
|
||||||
sqlalchemy
|
sqlalchemy
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Configurations for the _____ module
|
Configurations for the <this> module
|
||||||
|
|
||||||
Exports:
|
Exports:
|
||||||
"""
|
"""
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Constants and error codes for the _____ module
|
Constants for the <this> module
|
||||||
|
|
||||||
Exports:
|
Exports:
|
||||||
"""
|
"""
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""
|
"""
|
||||||
Router dependencies for the _____ module
|
Dependencies related to the <this> module
|
||||||
|
|
||||||
Exports:
|
Exports:
|
||||||
|
- <dep_name>: <return_type>: <description>
|
||||||
"""
|
"""
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
"""
|
"""
|
||||||
Module specific exceptions for the _____ module
|
Exceptions related to the <this> modules
|
||||||
|
|
||||||
Exports:
|
Exceptions:
|
||||||
|
- <ExceptionName>: Details e.g. optional params
|
||||||
"""
|
"""
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
"""
|
"""
|
||||||
Database models for the _____ module
|
Database models for the <this> module
|
||||||
|
|
||||||
Exports:
|
Models:
|
||||||
|
- <ModelName>:
|
||||||
|
- <normal_columns[FK][PK]>
|
||||||
|
- <orm_relationships>
|
||||||
|
- <calculated_properties>
|
||||||
"""
|
"""
|
||||||
|
|
@ -1,15 +1,31 @@
|
||||||
"""
|
"""
|
||||||
Router endpoints for the _____ module
|
Router endpoints for the <this> module
|
||||||
|
|
||||||
Endpoints:
|
Exports:
|
||||||
- /timer/start
|
- router: fastapi.APIRouter
|
||||||
- /timer/stop
|
|
||||||
|
### Router Guidelines ###
|
||||||
|
- Add responses to decorators
|
||||||
|
- Add status_codes to decorators
|
||||||
|
- All endpoints should either return a response object or 204
|
||||||
|
- Ensure response_model is declared in the decorator
|
||||||
|
- All query and path params should have validation and descriptions
|
||||||
|
- All endpoints should have a docstring (this is used in place of a description)
|
||||||
|
- All endpoints should have a summary
|
||||||
|
- All modules should have metadata in main.py
|
||||||
|
- All exceptions should have a custom definition in exceptions.py
|
||||||
|
- Dependencies should be used for db model get and validation where possible
|
||||||
|
- Verify module level docstring is still accurate after updates
|
||||||
"""
|
"""
|
||||||
import threading
|
import threading
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Request, HTTPException
|
from fastapi import APIRouter, Request, HTTPException
|
||||||
|
from fastapi.params import Query
|
||||||
|
|
||||||
from src.utils import create_timer
|
from src.utils import create_timer, generate_resource_name
|
||||||
|
from src.auth.dependencies import header_dependency
|
||||||
|
from src.dependencies import http_client_dependency
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
tags=[""],
|
tags=[""],
|
||||||
|
|
@ -52,3 +68,28 @@ async def stop_timer(request: Request, ident: str):
|
||||||
timers.pop(idx)
|
timers.pop(idx)
|
||||||
|
|
||||||
return {"timer_ident": ident, "status": "stopped"}
|
return {"timer_ident": ident, "status": "stopped"}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/hub/access")
|
||||||
|
async def test_hub_access(headers: header_dependency, client: http_client_dependency, org_name: Annotated[str, Query()]):
|
||||||
|
resource_name = "example_resource"
|
||||||
|
action = "read"
|
||||||
|
rn = generate_resource_name(resource=resource_name, org=org_name)
|
||||||
|
|
||||||
|
hub_response = await client.post(
|
||||||
|
f"http://localhost:8001/api/v1/iam/can_act_on_resource",
|
||||||
|
headers=headers,
|
||||||
|
params={"action": action},
|
||||||
|
json=rn.model_dump()
|
||||||
|
)
|
||||||
|
|
||||||
|
response = {
|
||||||
|
"resource_name": rn,
|
||||||
|
"action": action,
|
||||||
|
"response": {
|
||||||
|
"status": hub_response.status_code,
|
||||||
|
"json": hub_response.json()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
"""
|
"""
|
||||||
Pydantic models for the _____ module
|
Pydantic models for the <this> module
|
||||||
|
|
||||||
Exports:
|
Models follow the nomenclature of:
|
||||||
|
- Sub-models: "<Resource><Opt:>Schema"
|
||||||
|
- Mixins: "<Attribute>Mixin"
|
||||||
|
- Models: "<Module><Method><Resource><Opt:Resource><Direction>" ie ""
|
||||||
"""
|
"""
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Module specific business logic for the _____ module
|
Module specific business logic for the <this> module
|
||||||
|
|
||||||
Exports:
|
Exports:
|
||||||
"""
|
"""
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
"""
|
"""
|
||||||
Non-business logic reusable functions and classes for the _____ module
|
Non-business logic reusable functions and classes for the <this> module
|
||||||
|
|
||||||
Exports:
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -7,12 +7,15 @@ from src.auth.router import router as auth_router
|
||||||
from src._module_template.router import router as template_router
|
from src._module_template.router import router as template_router
|
||||||
|
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter(
|
||||||
|
prefix="/api/v1"
|
||||||
|
)
|
||||||
|
|
||||||
api_router.include_router(auth_router)
|
api_router.include_router(auth_router)
|
||||||
api_router.include_router(template_router)
|
api_router.include_router(template_router)
|
||||||
|
|
||||||
|
|
||||||
@api_router.get("/healthcheck", include_in_schema=False)
|
@api_router.get("/healthcheck", include_in_schema=False)
|
||||||
def healthcheck():
|
def healthcheck():
|
||||||
"""Simple health check endpoint."""
|
"""Simple health check endpoint."""
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
"""
|
"""
|
||||||
Configurations for auth module
|
Configurations for the auth module
|
||||||
|
|
||||||
Exports:
|
Exports:
|
||||||
- auth_settings: Configs for auth loaded from environment variables
|
- auth_settings: Contains OIDC & hub access information
|
||||||
"""
|
"""
|
||||||
from src.config import CustomBaseSettings
|
from src.config import CustomBaseSettings
|
||||||
|
|
||||||
|
|
||||||
class AuthConfig(CustomBaseSettings):
|
class AuthConfig(CustomBaseSettings):
|
||||||
OIDC_CONFIG: str = ""
|
OIDC_CONFIG: str = ""
|
||||||
OIDC_ISSUER: str = ""
|
|
||||||
OIDC_AUDIENCE: str = ""
|
|
||||||
CLIENT_ID: str = ""
|
CLIENT_ID: str = ""
|
||||||
|
|
||||||
|
HUB_ACCESS_KEY: str = ""
|
||||||
|
|
||||||
|
|
||||||
auth_settings = AuthConfig()
|
auth_settings = AuthConfig()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
"""
|
"""
|
||||||
Constants and error codes for auth module
|
Constants for the auth module
|
||||||
|
|
||||||
Exports:
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -1,5 +1,26 @@
|
||||||
"""
|
"""
|
||||||
Router dependencies for auth module
|
Auth dependencies
|
||||||
|
|
||||||
Exports:
|
Exports:
|
||||||
"""
|
"""
|
||||||
|
from typing import Annotated, Any
|
||||||
|
|
||||||
|
from fastapi import Depends
|
||||||
|
from fastapi.security import OpenIdConnect
|
||||||
|
|
||||||
|
from src.auth.config import auth_settings
|
||||||
|
|
||||||
|
|
||||||
|
oidc = OpenIdConnect(openIdConnectUrl=auth_settings.OIDC_CONFIG)
|
||||||
|
oidc_dependency = Annotated[str, Depends(oidc)]
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_access_headers(oidc_auth_string: oidc_dependency) -> dict[str, Any]:
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {oidc_auth_string}",
|
||||||
|
"X-API-Key": auth_settings.HUB_ACCESS_KEY,
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
header_dependency = Annotated[dict[str, Any], Depends(generate_access_headers)]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,18 @@
|
||||||
"""
|
"""
|
||||||
Module specific exceptions for auth module
|
Module specific exceptions for the auth module
|
||||||
|
|
||||||
Exports:
|
Exceptions:
|
||||||
"""
|
- UnauthorizedException: Takes an optional message string
|
||||||
|
"""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthorizedException(HTTPException):
|
||||||
|
def __init__(self, message: Optional[str] = None) -> None:
|
||||||
|
detail = "Not authorized" if not message else message
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=detail,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
"""
|
"""
|
||||||
Database models for auth module
|
Database models for the auth module
|
||||||
|
|
||||||
Exports:
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -1,20 +1,11 @@
|
||||||
"""
|
"""
|
||||||
Router endpoints for auth module
|
Router endpoints for the auth module
|
||||||
Contains oauth registration
|
|
||||||
|
|
||||||
Endpoints:
|
Exports:
|
||||||
- /auth/me/claims: Test endpoint returning user claims
|
- router: fastapi.APIRouter
|
||||||
"""
|
"""
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from src.auth.service import claims_dependency
|
|
||||||
|
|
||||||
router = APIRouter(
|
router = APIRouter(
|
||||||
tags=["auth"],
|
tags=["auth"],
|
||||||
prefix="/auth",
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me/claims")
|
|
||||||
async def current_user_claims(user: claims_dependency):
|
|
||||||
return user
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
"""
|
"""
|
||||||
Pydantic models for auth module
|
Pydantic models for the auth module
|
||||||
|
|
||||||
Exports:
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -1,50 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Module specific business logic for auth module
|
Module specific business logic for the auth module
|
||||||
|
|
||||||
Exports:
|
Exports:
|
||||||
- claims_dependency
|
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from typing import Annotated, Any
|
|
||||||
from joserfc import jwt
|
|
||||||
from urllib.request import urlopen
|
|
||||||
|
|
||||||
from fastapi import Depends
|
|
||||||
from fastapi.security import OpenIdConnect
|
|
||||||
from joserfc.jwk import KeySet
|
|
||||||
|
|
||||||
from src.auth.config import auth_settings
|
|
||||||
|
|
||||||
|
|
||||||
oidc = OpenIdConnect(openIdConnectUrl=auth_settings.OIDC_CONFIG)
|
|
||||||
oidc_dependency = Annotated[str, Depends(oidc)]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user(oidc_auth_string: oidc_dependency) -> dict[str, Any]:
|
|
||||||
config_url = urlopen(auth_settings.OIDC_CONFIG)
|
|
||||||
config = json.loads(config_url.read())
|
|
||||||
jwks_uri = config["jwks_uri"]
|
|
||||||
key_response = requests.get(jwks_uri)
|
|
||||||
jwk_keys = KeySet.import_key_set(key_response.json())
|
|
||||||
|
|
||||||
claims_options = {
|
|
||||||
"exp": {"essential": True},
|
|
||||||
"aud": {"essential": True, "value": "account"},
|
|
||||||
"iss": {"essential": True, "value": auth_settings.OIDC_ISSUER},
|
|
||||||
}
|
|
||||||
|
|
||||||
token = jwt.decode(
|
|
||||||
oidc_auth_string.replace("Bearer ", ""),
|
|
||||||
jwk_keys
|
|
||||||
)
|
|
||||||
|
|
||||||
claims_requests = jwt.JWTClaimsRegistry(**claims_options)
|
|
||||||
|
|
||||||
claims_requests.validate(token.claims)
|
|
||||||
|
|
||||||
return token.claims
|
|
||||||
|
|
||||||
|
|
||||||
claims_dependency = Annotated[dict[str, Any], Depends(get_current_user)]
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
"""
|
"""
|
||||||
Non-business logic reusable functions and classes for auth module
|
Non-business logic reusable functions and classes for the auth module
|
||||||
|
|
||||||
Exports:
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -2,8 +2,9 @@
|
||||||
Global configurations
|
Global configurations
|
||||||
|
|
||||||
Exports:
|
Exports:
|
||||||
- settings: Global configs loaded from environment variables
|
|
||||||
- CustomBaseSettings - Base class to be used by all modules for loading configs
|
- CustomBaseSettings - Base class to be used by all modules for loading configs
|
||||||
|
- settings: Global configurations object
|
||||||
|
- app_configs: Dict generated from configs, used in app initialisation
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
@ -24,6 +25,9 @@ class Config(CustomBaseSettings):
|
||||||
APP_VERSION: str = "0.1"
|
APP_VERSION: str = "0.1"
|
||||||
ENVIRONMENT: Environment = Environment.PRODUCTION
|
ENVIRONMENT: Environment = Environment.PRODUCTION
|
||||||
SECRET_KEY: SecretStr = ""
|
SECRET_KEY: SecretStr = ""
|
||||||
|
DISABLE_AUTH: bool = False
|
||||||
|
SERVICE_NAME: str = "template_app"
|
||||||
|
HUB_ADDRESS: str = "http://localhost:8000"
|
||||||
|
|
||||||
CORS_ORIGINS: list[str] = ["*"]
|
CORS_ORIGINS: list[str] = ["*"]
|
||||||
CORS_ORIGINS_REGEX: str | None = None
|
CORS_ORIGINS_REGEX: str | None = None
|
||||||
|
|
@ -46,6 +50,9 @@ _QUOTED_DATABASE_PASSWORD = parse.quote_plus(str(_DATABASE_CREDENTIAL_PASSWORD))
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URI = SecretStr(f"postgresql+psycopg://{_DATABASE_CREDENTIAL_USER}:{_QUOTED_DATABASE_PASSWORD}@{DATABASE_HOSTNAME}:{DATABASE_PORT}/{DATABASE_NAME}")
|
SQLALCHEMY_DATABASE_URI = SecretStr(f"postgresql+psycopg://{_DATABASE_CREDENTIAL_USER}:{_QUOTED_DATABASE_PASSWORD}@{DATABASE_HOSTNAME}:{DATABASE_PORT}/{DATABASE_NAME}")
|
||||||
|
|
||||||
|
if settings.ENVIRONMENT == Environment.TESTING:
|
||||||
|
SQLALCHEMY_DATABASE_URI = SecretStr("sqlite:///:memory:")
|
||||||
|
|
||||||
app_configs: dict[str, Any] = {"title": "App API"}
|
app_configs: dict[str, Any] = {"title": "App API"}
|
||||||
if settings.ENVIRONMENT.is_deployed:
|
if settings.ENVIRONMENT.is_deployed:
|
||||||
app_configs["root_path"] = f"/v{settings.APP_VERSION}"
|
app_configs["root_path"] = f"/v{settings.APP_VERSION}"
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,24 @@ Exports:
|
||||||
- Base (sqlalchemy base model)
|
- Base (sqlalchemy base model)
|
||||||
"""
|
"""
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine, StaticPool
|
||||||
from sqlalchemy.orm import declarative_base, sessionmaker, Session
|
from sqlalchemy.orm import DeclarativeBase, sessionmaker, Session
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
|
||||||
from src.config import SQLALCHEMY_DATABASE_URI
|
from src.constants import Environment
|
||||||
|
from src.config import SQLALCHEMY_DATABASE_URI, settings as global_settings
|
||||||
|
|
||||||
|
if global_settings.ENVIRONMENT == Environment.TESTING:
|
||||||
|
connect_args = {"check_same_thread": False}
|
||||||
|
engine = create_engine(SQLALCHEMY_DATABASE_URI.get_secret_value(), connect_args=connect_args, poolclass=StaticPool)
|
||||||
|
else:
|
||||||
|
engine = create_engine(SQLALCHEMY_DATABASE_URI.get_secret_value())
|
||||||
|
|
||||||
engine = create_engine(SQLALCHEMY_DATABASE_URI.get_secret_value())
|
|
||||||
|
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
|
|
@ -29,4 +36,5 @@ def get_db():
|
||||||
|
|
||||||
|
|
||||||
db_dependency = Annotated[Session, Depends(get_db)]
|
db_dependency = Annotated[Session, Depends(get_db)]
|
||||||
Base = declarative_base()
|
class Base(DeclarativeBase):
|
||||||
|
pass
|
||||||
|
|
|
||||||
15
src/dependencies.py
Normal file
15
src/dependencies.py
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
"""
|
||||||
|
Global dependencies
|
||||||
|
|
||||||
|
Exports:
|
||||||
|
"""
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from typing import Annotated
|
||||||
|
from fastapi import Depends
|
||||||
|
|
||||||
|
async def create_http_client():
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
yield client
|
||||||
|
|
||||||
|
http_client_dependency = Annotated[httpx.AsyncClient, Depends(create_http_client)]
|
||||||
|
|
@ -1,3 +1,28 @@
|
||||||
"""
|
"""
|
||||||
Global exceptions
|
Global exceptions
|
||||||
"""
|
|
||||||
|
Exports:
|
||||||
|
- UnprocessableContentException
|
||||||
|
- ConflictException
|
||||||
|
"""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import HTTPException, status
|
||||||
|
|
||||||
|
|
||||||
|
class UnprocessableContentException(HTTPException):
|
||||||
|
def __init__(self, message: Optional[str] = None) -> None:
|
||||||
|
detail = "Unprocessable content" if not message else message
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
detail=detail,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ConflictException(HTTPException):
|
||||||
|
def __init__(self, message: Optional[str] = None) -> None:
|
||||||
|
detail = "Conflict" if not message else message
|
||||||
|
super().__init__(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail=detail,
|
||||||
|
)
|
||||||
|
|
|
||||||
12
src/main.py
12
src/main.py
|
|
@ -12,6 +12,7 @@ from fastapi import FastAPI
|
||||||
from starlette.middleware.sessions import SessionMiddleware
|
from starlette.middleware.sessions import SessionMiddleware
|
||||||
from starlette.middleware.cors import CORSMiddleware
|
from starlette.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
from src.constants import Environment
|
||||||
from src.config import settings, app_configs
|
from src.config import settings, app_configs
|
||||||
from src.api import api_router
|
from src.api import api_router
|
||||||
|
|
||||||
|
|
@ -31,9 +32,10 @@ async def lifespan(_application: FastAPI) -> AsyncGenerator:
|
||||||
|
|
||||||
|
|
||||||
if settings.ENVIRONMENT.is_deployed:
|
if settings.ENVIRONMENT.is_deployed:
|
||||||
# Do this only on prod
|
# Just a precaution, should be False anyway
|
||||||
pass
|
settings.DISABLE_AUTH = False
|
||||||
|
|
||||||
|
tags_metadata = []
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
swagger_ui_init_oauth={
|
swagger_ui_init_oauth={
|
||||||
|
|
@ -41,6 +43,7 @@ app = FastAPI(
|
||||||
"usePkceWithAuthorizationCodeGrant": True,
|
"usePkceWithAuthorizationCodeGrant": True,
|
||||||
"scopes": "openid profile email",
|
"scopes": "openid profile email",
|
||||||
},
|
},
|
||||||
|
openapi_tags=tags_metadata,
|
||||||
**app_configs
|
**app_configs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -61,6 +64,11 @@ app.add_middleware(
|
||||||
allow_headers=settings.CORS_HEADERS,
|
allow_headers=settings.CORS_HEADERS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if settings.DISABLE_AUTH and (settings.ENVIRONMENT == Environment.LOCAL):
|
||||||
|
# app.dependency_overrides[] = get_dev_user
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
app.include_router(api_router)
|
app.include_router(api_router)
|
||||||
|
|
||||||
app.state["timers"] = []
|
app.state["timers"] = []
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
"""
|
"""
|
||||||
Global Pydantic models
|
Global Pydantic schemas
|
||||||
|
|
||||||
Exports:
|
Exports:
|
||||||
- CustomBaseModel: Pydantic BaseModel with extra parameters
|
- CustomBaseModel: Schema used for all other Pydantic models
|
||||||
|
- ResourceName
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class CustomBaseModel(BaseModel):
|
class CustomBaseModel(BaseModel):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceName(CustomBaseModel):
|
||||||
|
service: str
|
||||||
|
organisation: str
|
||||||
|
resource: str
|
||||||
|
instance: Optional[str] = None
|
||||||
|
|
|
||||||
16
src/utils.py
16
src/utils.py
|
|
@ -4,7 +4,10 @@ Global non-business-logic reusable functions and classes
|
||||||
Exports:
|
Exports:
|
||||||
"""
|
"""
|
||||||
import threading
|
import threading
|
||||||
from typing import Callable
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
from src.schemas import ResourceName
|
||||||
|
from src.config import settings
|
||||||
|
|
||||||
|
|
||||||
def create_timer(func: Callable, interval: int, stop_event: threading.Event):
|
def create_timer(func: Callable, interval: int, stop_event: threading.Event):
|
||||||
|
|
@ -13,3 +16,14 @@ def create_timer(func: Callable, interval: int, stop_event: threading.Event):
|
||||||
func()
|
func()
|
||||||
|
|
||||||
return threading.Thread(target=target, daemon=True)
|
return threading.Thread(target=target, daemon=True)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_resource_name(org: str, resource: str, instance: Optional[str] = None) -> ResourceName:
|
||||||
|
rn = ResourceName(
|
||||||
|
service=settings.SERVICE_NAME,
|
||||||
|
organisation=org,
|
||||||
|
resource=resource,
|
||||||
|
instance=instance,
|
||||||
|
)
|
||||||
|
|
||||||
|
return rn
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue