285 lines
7.8 KiB
Python
285 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
lib/domain.py The domain-scoped utility functions
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
import importlib
|
|
import inspect
|
|
import logging
|
|
from types import ModuleType, FunctionType
|
|
from typing import Any, Callable, Coroutine, Generator
|
|
from typing import Dict, List, Tuple, Iterator
|
|
import inspect
|
|
|
|
from starlette.exceptions import HTTPException
|
|
|
|
from halfapi.lib import acl
|
|
from halfapi.lib.responses import ORJSONResponse
|
|
from halfapi.lib.router import read_router
|
|
from halfapi.lib.constants import VERBS
|
|
|
|
logger = logging.getLogger("uvicorn.asgi")
|
|
|
|
class MissingAclError(Exception):
|
|
pass
|
|
|
|
class PathError(Exception):
|
|
pass
|
|
|
|
class UnknownPathParameterType(Exception):
|
|
pass
|
|
|
|
class UndefinedRoute(Exception):
|
|
pass
|
|
|
|
class UndefinedFunction(Exception):
|
|
pass
|
|
|
|
def route_decorator(fct: FunctionType, ret_type: str = 'json') -> Coroutine:
|
|
""" Returns an async function that can be mounted on a router
|
|
"""
|
|
if ret_type == 'json':
|
|
@acl.args_check
|
|
async def wrapped(request, *args, **kwargs):
|
|
fct_args_spec = inspect.getfullargspec(fct).args
|
|
fct_args = request.path_params.copy()
|
|
|
|
if 'halfapi' in fct_args_spec:
|
|
fct_args['halfapi'] = {
|
|
'user': request.user if
|
|
'user' in request else None,
|
|
'config': request.scope['config']
|
|
}
|
|
|
|
|
|
if 'data' in fct_args_spec:
|
|
fct_args['data'] = kwargs.get('data')
|
|
|
|
try:
|
|
return ORJSONResponse(fct(**fct_args))
|
|
except NotImplementedError as exc:
|
|
raise HTTPException(501) from exc
|
|
|
|
else:
|
|
raise Exception('Return type not available')
|
|
|
|
return wrapped
|
|
|
|
|
|
def get_fct_name(http_verb: str, path: str) -> str:
|
|
"""
|
|
Returns the predictable name of the function for a route
|
|
|
|
Parameters:
|
|
- http_verb (str): The Route's HTTP method (GET, POST, ...)
|
|
- path (str): The functions path
|
|
|
|
Returns:
|
|
str: The *unique* function name for a route and it's verb
|
|
|
|
|
|
Examples:
|
|
|
|
>>> get_fct_name('get', '')
|
|
'get'
|
|
|
|
>>> get_fct_name('GET', '')
|
|
'get'
|
|
|
|
>>> get_fct_name('POST', 'foo')
|
|
'post_foo'
|
|
|
|
>>> get_fct_name('POST', 'bar')
|
|
'post_bar'
|
|
|
|
>>> get_fct_name('DEL', 'foo/{boo}')
|
|
'del_foo_BOO'
|
|
|
|
>>> get_fct_name('DEL', '{boo:zoo}/far')
|
|
'del_BOO_far'
|
|
"""
|
|
if path and path[0] == '/':
|
|
path = path[1:]
|
|
|
|
fct_name = [http_verb.lower()]
|
|
for elt in path.split('/'):
|
|
if elt and elt[0] == '{':
|
|
fct_name.append(elt[1:-1].split(':')[0].upper())
|
|
elif elt:
|
|
fct_name.append(elt)
|
|
|
|
return '_'.join(fct_name)
|
|
|
|
def gen_routes(m_router: ModuleType,
|
|
verb: str,
|
|
path: List[str],
|
|
params: List[Dict]) -> Tuple[FunctionType, Dict]:
|
|
"""
|
|
Returns a tuple of the function associatied to the verb and path arguments,
|
|
and the dictionary of it's acls
|
|
|
|
Parameters:
|
|
- m_router (ModuleType): The module containing the function definition
|
|
|
|
- verb (str): The HTTP verb for the route (GET, POST, ...)
|
|
|
|
- path (List): The route path, as a list (each item being a level of
|
|
deepness), from the lowest level (domain) to the highest
|
|
|
|
- params (Dict): The acl list of the following format :
|
|
[{'acl': Function, 'args': {'required': [], 'optional': []}}]
|
|
|
|
|
|
Returns:
|
|
|
|
(Function, Dict): The destination function and the acl dictionary
|
|
|
|
"""
|
|
if len(params) == 0:
|
|
raise MissingAclError('[{}] {}'.format(verb, '/'.join(path)))
|
|
|
|
if len(path) == 0:
|
|
logger.error('Empty path for [{%s}]', verb)
|
|
raise PathError()
|
|
|
|
fct_name = get_fct_name(verb, path[-1])
|
|
if hasattr(m_router, fct_name):
|
|
fct = getattr(m_router, fct_name)
|
|
else:
|
|
raise UndefinedFunction('{}.{}'.format(m_router.__name__, fct_name or ''))
|
|
|
|
|
|
if not inspect.iscoroutinefunction(fct):
|
|
return route_decorator(fct), params
|
|
else:
|
|
return fct, params
|
|
|
|
|
|
def gen_router_routes(m_router: ModuleType, path: List[str]) -> \
|
|
Iterator[Tuple[str, str, Coroutine, List]]:
|
|
"""
|
|
Recursive generatore that parses a router (or a subrouter)
|
|
and yields from gen_routes
|
|
|
|
Parameters:
|
|
|
|
- m_router (ModuleType): The currently treated router module
|
|
- path (List[str]): The current path stack
|
|
|
|
Yields:
|
|
|
|
(str, str, Coroutine, List): A tuple containing the path, verb,
|
|
function and parameters of the route
|
|
"""
|
|
|
|
for subpath, params in read_router(m_router).items():
|
|
path.append(subpath)
|
|
|
|
for verb in VERBS:
|
|
if verb not in params:
|
|
continue
|
|
yield ('/'.join(filter(lambda x: len(x) > 0, path)),
|
|
verb,
|
|
*gen_routes(m_router, verb, path, params[verb])
|
|
)
|
|
|
|
for subroute in params.get('SUBROUTES', []):
|
|
#logger.debug('Processing subroute **%s** - %s', subroute, m_router.__name__)
|
|
param_match = re.fullmatch('^([A-Z_]+)_([a-z]+)$', subroute)
|
|
if param_match is not None:
|
|
try:
|
|
path.append('{{{}:{}}}'.format(
|
|
param_match.groups()[0].lower(),
|
|
param_match.groups()[1]))
|
|
except AssertionError:
|
|
raise UnknownPathParameterType(subroute)
|
|
else:
|
|
path.append(subroute)
|
|
|
|
try:
|
|
yield from gen_router_routes(
|
|
importlib.import_module(f'.{subroute}', m_router.__name__),
|
|
path)
|
|
|
|
except ImportError as exc:
|
|
logger.error('Failed to import subroute **{%s}**', subroute)
|
|
raise exc
|
|
|
|
path.pop()
|
|
|
|
path.pop()
|
|
|
|
|
|
def d_domains(config) -> Dict[str, ModuleType]:
|
|
"""
|
|
Parameters:
|
|
|
|
config (ConfigParser): The .halfapi/config based configparser object
|
|
|
|
Returns:
|
|
|
|
dict[str, ModuleType]
|
|
"""
|
|
if not 'domains' in config:
|
|
return {}
|
|
|
|
try:
|
|
sys.path.append('.')
|
|
return {
|
|
domain: importlib.import_module(''.join((domain, module)))
|
|
for domain, module in config['domains'].items()
|
|
}
|
|
except ImportError as exc:
|
|
logger.error('Could not load a domain : %s', exc)
|
|
raise exc
|
|
|
|
def router_acls(route_params: Dict, path: List, m_router: ModuleType) -> Generator:
|
|
d_res = {'fqtn': route_params.get('FQTN')}
|
|
|
|
for verb in VERBS:
|
|
params = route_params.get(verb)
|
|
if params is None:
|
|
continue
|
|
if len(params) == 0:
|
|
logger.error('No ACL for route [{%s}] %s', verb, "/".join(path))
|
|
else:
|
|
for param in params:
|
|
acl = param.get('acl')
|
|
if not isinstance(acl, FunctionType):
|
|
continue
|
|
|
|
yield acl.__name__, acl
|
|
|
|
|
|
def domain_acls(m_router, path):
|
|
if not hasattr(m_router, 'ROUTES'):
|
|
logger.error('Missing *ROUTES* constant in *%s*', m_router.__name__)
|
|
raise Exception(f'No ROUTES constant for {m_router.__name__}')
|
|
|
|
|
|
routes = m_router.ROUTES
|
|
|
|
for subpath, route_params in routes.items():
|
|
path.append(subpath)
|
|
|
|
yield from router_acls(route_params, path, m_router)
|
|
|
|
subroutes = route_params.get('SUBROUTES', [])
|
|
for subroute in subroutes:
|
|
logger.debug('Processing subroute **%s** - %s', subroute, m_router.__name__)
|
|
path.append(subroute)
|
|
try:
|
|
submod = importlib.import_module(f'.{subroute}', m_router.__name__)
|
|
except ImportError as exc:
|
|
logger.error('Failed to import subroute **{%s}**', subroute)
|
|
raise exc
|
|
|
|
yield from domain_acls(submod, path)
|
|
|
|
path.pop()
|
|
|
|
path.pop()
|