halfapi/halfapi/halfapi.py

189 lines
5.9 KiB
Python

#!/usr/bin/env python3
"""
app.py is the file that is read when launching the application using an asgi
runner.
It defines the following globals :
- routes (contains the Route objects for the application)
- application (the asgi application itself - a starlette object)
"""
import logging
import time
import importlib
from datetime import datetime
# asgi framework
from starlette.applications import Starlette
from starlette.authentication import UnauthenticatedUser
from starlette.exceptions import HTTPException
from starlette.middleware import Middleware
from starlette.routing import Route, Mount
from starlette.requests import Request
from starlette.responses import Response, PlainTextResponse
from starlette.middleware.authentication import AuthenticationMiddleware
from timing_asgi import TimingMiddleware
from timing_asgi.integrations import StarletteScopeToName
# module libraries
from .lib.constants import API_SCHEMA_DICT
from .lib.domain_middleware import DomainMiddleware
from .lib.timing import HTimingClient
from .lib.domain import NoDomainsException
from .lib.jwt_middleware import JWTAuthenticationBackend
from .lib.responses import (ORJSONResponse, UnauthorizedResponse,
NotFoundResponse, InternalServerErrorResponse, NotImplementedResponse,
ServiceUnavailableResponse)
from .lib.domain import domain_schema_dict
from .lib.routes import gen_domain_routes, gen_schema_routes, JSONRoute
from .lib.schemas import schema_json, get_acls
from .logging import logger, config_logging
from halfapi import __version__
class HalfAPI:
def __init__(self, config,
routes_dict=None):
config_logging(logging.DEBUG)
SECRET = config.get('secret')
PRODUCTION = config.get('production', True)
CONFIG = config.get('config', {})
domain = config.get('domain')['name']
router = config.get('domain')['router']
if not (domain and router):
raise NoDomainsException()
self.PRODUCTION = PRODUCTION
self.CONFIG = CONFIG
self.SECRET = SECRET
self.__application = None
if domain and router:
m_domain = importlib.import_module(f'{domain}')
m_domain_router = importlib.import_module(f'{domain}.{router}')
m_domain_acl = importlib.import_module(f'{domain}.acl')
self.schema = { **API_SCHEMA_DICT }
self.schema['domain'] = {
'name': domain,
'version': getattr(m_domain, '__version__', ''),
'patch_release': getattr(m_domain, '__path_release__', ''),
'acls': tuple(getattr(m_domain_acl, 'ACLS', ()))
}
self.schema['paths'] = domain_schema_dict(m_domain_router)
routes = [ Route('/', JSONRoute(self.schema)) ]
""" HalfAPI routes (if not PRODUCTION, includes debug routes)
"""
routes.append(
Mount('/halfapi', routes=list(self.routes()))
)
if routes_dict:
# Mount the routes from the routes_dict argument - domain-less mode
logger.info('Domain-less mode : the given schema defines the activated routes')
for route in gen_schema_routes(routes_dict):
routes.append(route)
else:
for route in gen_domain_routes(m_domain_router):
routes.append(route)
self.__application = Starlette(
debug=not PRODUCTION,
routes=routes,
exception_handlers={
401: UnauthorizedResponse,
404: NotFoundResponse,
500: InternalServerErrorResponse,
501: NotImplementedResponse,
503: ServiceUnavailableResponse
}
)
self.__application.add_middleware(
DomainMiddleware,
domain=domain,
config=CONFIG
)
if SECRET:
self.SECRET = SECRET
self.__application.add_middleware(
AuthenticationMiddleware,
backend=JWTAuthenticationBackend(secret_key=SECRET)
)
if not PRODUCTION:
self.__application.add_middleware(
TimingMiddleware,
client=HTimingClient(),
metric_namer=StarletteScopeToName(prefix="halfapi",
starlette_app=self.__application)
)
@property
def version(self):
return __version__
async def version_async(self, request, *args, **kwargs):
return Response(self.version)
@property
def application(self):
return self.__application
def routes(self):
""" Halfapi default routes
"""
async def get_user(request, *args, **kwargs):
return ORJSONResponse({'user':request.user})
yield Route('/whoami', get_user)
yield Route('/schema', schema_json)
yield Route('/acls', get_acls)
yield Route('/version', self.version_async)
""" Halfapi debug routes definition
"""
if self.PRODUCTION:
return
""" Debug routes
"""
async def debug_log(request: Request, *args, **kwargs):
logger.debug('debuglog# %s', {datetime.now().isoformat()})
logger.info('debuglog# %s', {datetime.now().isoformat()})
logger.warning('debuglog# %s', {datetime.now().isoformat()})
logger.error('debuglog# %s', {datetime.now().isoformat()})
logger.critical('debuglog# %s', {datetime.now().isoformat()})
return Response('')
yield Route('/log', debug_log)
async def error_code(request: Request, *args, **kwargs):
code = request.path_params['code']
raise HTTPException(code)
yield Route('/error/{code:int}', error_code)
async def exception(request: Request, *args, **kwargs):
raise Exception('Test exception')
yield Route('/exception', exception)
@staticmethod
def api_schema(domain):
pass