[0.5.3]
Squashed commit of the following: commitac935db6d6
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr> Date: Thu Jun 17 18:52:49 2021 +0200 [tests] remove dummy-domain from dependencies commit4d50363c9b
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr> Date: Thu Jun 17 18:52:18 2021 +0200 [tests] update tests for 0.5.3 commit6181592692
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr> Date: Thu Jun 17 18:17:51 2021 +0200 [lib.*] Refactor libs commited7485a8a1
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr> Date: Thu Jun 17 18:15:10 2021 +0200 [app] Use HalfAPI class to be able to use custom configuration à commitfa1ca6bf9d
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr> Date: Wed Jun 16 15:34:25 2021 +0200 [wip] tests dummy_domain commit86e8dd3465
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr> Date: Tue Jun 15 18:12:13 2021 +0200 [0.5.3] ajout de la config actuelle dans les arguments des routes commitaa7ec62c7a
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr> Date: Tue Jun 15 11:16:23 2021 +0200 [lib.jwtMw] verify signature even if halfapi is in DEBUG mode commite208728d7e
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr> Date: Tue Jun 15 10:49:46 2021 +0200 [lib.acl] args_check doesn't check required/optional arguments if "args" is not specified in request, if the target function is not async commitaa4c309778
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr> Date: Tue Jun 15 09:45:37 2021 +0200 [lib.domain] SUBROUTER can be a path parameter if including ":" commit138420461d
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr> Date: Tue Jun 15 07:24:32 2021 +0200 [gitignore] *.swp commit0c1e2849ba
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr> Date: Tue Jun 15 07:24:14 2021 +0200 [tests] test get route with dummy projects commit7227e2d7f1
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr> Date: Mon Jun 14 17:18:47 2021 +0200 [lib.domain] handle modules without ROUTES attribute commit78c75cd60e
Author: Maxime Alves LIRMM@home <maxime.alves@lirmm.fr> Date: Mon Jun 14 16:34:58 2021 +0200 [tests] add dummy_project_router tests for path-based routers (without ROUTES variable)
This commit is contained in:
parent
eb68d06ac0
commit
81f6cf8b39
|
@ -138,3 +138,6 @@ cython_debug/
|
||||||
|
|
||||||
domains/
|
domains/
|
||||||
.vscode
|
.vscode
|
||||||
|
|
||||||
|
# Vim swap files
|
||||||
|
*.swp
|
||||||
|
|
1
Pipfile
1
Pipfile
|
@ -8,7 +8,6 @@ pytest = "*"
|
||||||
requests = "*"
|
requests = "*"
|
||||||
pytest-asyncio = "*"
|
pytest-asyncio = "*"
|
||||||
pylint = "*"
|
pylint = "*"
|
||||||
dummy-domain = {path = "./tests"}
|
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
click = ">=7.1,<8"
|
click = ">=7.1,<8"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
__version__ = '0.5.2'
|
__version__ = '0.5.3'
|
||||||
|
|
||||||
def version():
|
def version():
|
||||||
return f'HalfAPI version:{__version__}'
|
return f'HalfAPI version:{__version__}'
|
||||||
|
|
114
halfapi/app.py
114
halfapi/app.py
|
@ -23,7 +23,6 @@ from timing_asgi import TimingMiddleware
|
||||||
from timing_asgi.integrations import StarletteScopeToName
|
from timing_asgi.integrations import StarletteScopeToName
|
||||||
|
|
||||||
# module libraries
|
# module libraries
|
||||||
from halfapi.conf import config, SECRET, PRODUCTION, DOMAINSDICT
|
|
||||||
|
|
||||||
from .lib.domain_middleware import DomainMiddleware
|
from .lib.domain_middleware import DomainMiddleware
|
||||||
from .lib.timing import HTimingClient
|
from .lib.timing import HTimingClient
|
||||||
|
@ -34,61 +33,86 @@ from halfapi.lib.responses import (ORJSONResponse, UnauthorizedResponse,
|
||||||
NotFoundResponse, InternalServerErrorResponse, NotImplementedResponse)
|
NotFoundResponse, InternalServerErrorResponse, NotImplementedResponse)
|
||||||
|
|
||||||
from halfapi.lib.routes import gen_starlette_routes, debug_routes
|
from halfapi.lib.routes import gen_starlette_routes, debug_routes
|
||||||
from halfapi.lib.schemas import get_api_routes, schema_json, get_acls
|
from halfapi.lib.schemas import get_api_routes, get_api_domain_routes, schema_json, get_acls
|
||||||
|
|
||||||
logger = logging.getLogger('uvicorn.asgi')
|
logger = logging.getLogger('uvicorn.asgi')
|
||||||
|
|
||||||
routes = [ Route('/', get_api_routes) ]
|
|
||||||
|
class HalfAPI:
|
||||||
|
def __init__(self, config=None):
|
||||||
|
if config:
|
||||||
|
SECRET = config.get('SECRET')
|
||||||
|
PRODUCTION = config.get('PRODUCTION')
|
||||||
|
DOMAINS = config.get('DOMAINS', {})
|
||||||
|
CONFIG = config.get('CONFIG', {
|
||||||
|
'domains': DOMAINS
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
from halfapi.conf import CONFIG, SECRET, PRODUCTION, DOMAINS
|
||||||
|
|
||||||
|
|
||||||
routes += [
|
routes = [ Route('/', get_api_routes) ]
|
||||||
Route('/halfapi/schema', schema_json),
|
|
||||||
Route('/halfapi/acls', get_acls)
|
|
||||||
]
|
|
||||||
|
|
||||||
routes += Route('/halfapi/current_user', lambda request, *args, **kwargs:
|
|
||||||
ORJSONResponse({'user':request.user.json})
|
|
||||||
if SECRET and not isinstance(request.user, UnauthenticatedUser)
|
|
||||||
else ORJSONResponse({'user': None})),
|
|
||||||
|
|
||||||
|
|
||||||
if not PRODUCTION:
|
routes += [
|
||||||
for route in debug_routes():
|
Route('/halfapi/schema', schema_json),
|
||||||
routes.append( route )
|
Route('/halfapi/acls', get_acls)
|
||||||
|
]
|
||||||
|
|
||||||
|
routes += Route('/halfapi/current_user', lambda request, *args, **kwargs:
|
||||||
|
ORJSONResponse({'user':request.user.json})
|
||||||
|
if SECRET and not isinstance(request.user, UnauthenticatedUser)
|
||||||
|
else ORJSONResponse({'user': None})),
|
||||||
|
|
||||||
|
|
||||||
if DOMAINSDICT:
|
if not PRODUCTION:
|
||||||
for route in gen_starlette_routes(DOMAINSDICT()):
|
for route in debug_routes():
|
||||||
routes.append(route)
|
routes.append( route )
|
||||||
|
|
||||||
|
|
||||||
application = Starlette(
|
if DOMAINS:
|
||||||
debug=not PRODUCTION,
|
for route in gen_starlette_routes(DOMAINS):
|
||||||
routes=routes,
|
routes.append(route)
|
||||||
exception_handlers={
|
|
||||||
401: UnauthorizedResponse,
|
|
||||||
404: NotFoundResponse,
|
|
||||||
500: InternalServerErrorResponse,
|
|
||||||
501: NotImplementedResponse
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if DOMAINSDICT:
|
for domain in DOMAINS:
|
||||||
application.add_middleware(
|
routes.append(
|
||||||
DomainMiddleware,
|
Route(
|
||||||
config=config
|
f'/{domain}',
|
||||||
)
|
get_api_domain_routes(domain)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if SECRET:
|
|
||||||
application.add_middleware(
|
|
||||||
AuthenticationMiddleware,
|
|
||||||
backend=JWTAuthenticationBackend(secret_key=SECRET)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not PRODUCTION:
|
self.application = Starlette(
|
||||||
application.add_middleware(
|
debug=not PRODUCTION,
|
||||||
TimingMiddleware,
|
routes=routes,
|
||||||
client=HTimingClient(),
|
exception_handlers={
|
||||||
metric_namer=StarletteScopeToName(prefix="halfapi",
|
401: UnauthorizedResponse,
|
||||||
starlette_app=application)
|
404: NotFoundResponse,
|
||||||
)
|
500: InternalServerErrorResponse,
|
||||||
|
501: NotImplementedResponse
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.application.add_middleware(
|
||||||
|
DomainMiddleware,
|
||||||
|
config=CONFIG
|
||||||
|
)
|
||||||
|
|
||||||
|
if 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)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
application = HalfAPI().application
|
||||||
|
|
|
@ -47,13 +47,15 @@ logger = logging.getLogger('halfapi')
|
||||||
|
|
||||||
PROJECT_NAME = os.path.basename(os.getcwd())
|
PROJECT_NAME = os.path.basename(os.getcwd())
|
||||||
DOMAINSDICT = lambda: {}
|
DOMAINSDICT = lambda: {}
|
||||||
|
DOMAINS = {}
|
||||||
PRODUCTION = False
|
PRODUCTION = False
|
||||||
LOGLEVEL = 'info'
|
LOGLEVEL = 'info'
|
||||||
HOST = '127.0.0.1'
|
HOST = '127.0.0.1'
|
||||||
PORT = '3000'
|
PORT = '3000'
|
||||||
SECRET = ''
|
SECRET = ''
|
||||||
|
CONF_FILE = os.environ.get('HALFAPI_CONF_FILE', '.halfapi/config')
|
||||||
|
|
||||||
IS_PROJECT = os.path.isfile('.halfapi/config')
|
is_project = lambda: os.path.isfile(CONF_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,7 +79,7 @@ HALFAPI_ETC_FILE=os.path.join(
|
||||||
HALFAPI_DOT_FILE=os.path.join(
|
HALFAPI_DOT_FILE=os.path.join(
|
||||||
os.getcwd(), '.halfapi', 'config')
|
os.getcwd(), '.halfapi', 'config')
|
||||||
|
|
||||||
HALFAPI_CONFIG_FILES = [ HALFAPI_ETC_FILE, HALFAPI_DOT_FILE ]
|
HALFAPI_CONFIG_FILES = [ CONF_FILE, HALFAPI_DOT_FILE ]
|
||||||
|
|
||||||
def conf_files():
|
def conf_files():
|
||||||
return [
|
return [
|
||||||
|
@ -114,8 +116,11 @@ def read_config():
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if IS_PROJECT:
|
CONFIG = {}
|
||||||
read_config()
|
IS_PROJECT = False
|
||||||
|
if is_project():
|
||||||
|
IS_PROJECT = True
|
||||||
|
CONFIG = read_config()
|
||||||
|
|
||||||
PROJECT_NAME = config.get('project', 'name', fallback=PROJECT_NAME)
|
PROJECT_NAME = config.get('project', 'name', fallback=PROJECT_NAME)
|
||||||
|
|
||||||
|
@ -123,12 +128,14 @@ if IS_PROJECT:
|
||||||
raise Exception('Need a project name as argument')
|
raise Exception('Need a project name as argument')
|
||||||
|
|
||||||
DOMAINSDICT = lambda: d_domains(config)
|
DOMAINSDICT = lambda: d_domains(config)
|
||||||
|
DOMAINS = DOMAINSDICT()
|
||||||
HOST = config.get('project', 'host')
|
HOST = config.get('project', 'host')
|
||||||
PORT = config.getint('project', 'port')
|
PORT = config.getint('project', 'port')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(config.get('project', 'secret')) as secret_file:
|
with open(config.get('project', 'secret')) as secret_file:
|
||||||
SECRET = secret_file.read()
|
SECRET = secret_file.read().strip()
|
||||||
|
CONFIG['secret'] = SECRET.strip()
|
||||||
# Set the secret so we can use it in domains
|
# Set the secret so we can use it in domains
|
||||||
os.environ['HALFAPI_SECRET'] = SECRET
|
os.environ['HALFAPI_SECRET'] = SECRET
|
||||||
except FileNotFoundError as exc:
|
except FileNotFoundError as exc:
|
||||||
|
@ -141,3 +148,11 @@ if IS_PROJECT:
|
||||||
LOGLEVEL = config.get('project', 'loglevel').lower() or 'info'
|
LOGLEVEL = config.get('project', 'loglevel').lower() or 'info'
|
||||||
|
|
||||||
BASE_DIR = config.get('project', 'base_dir', fallback='.') #os.getcwd())
|
BASE_DIR = config.get('project', 'base_dir', fallback='.') #os.getcwd())
|
||||||
|
|
||||||
|
CONFIG = {
|
||||||
|
'project_name': PROJECT_NAME,
|
||||||
|
'production': PRODUCTION,
|
||||||
|
'secret': SECRET,
|
||||||
|
'domains': DOMAINS
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -66,26 +66,31 @@ def args_check(fct):
|
||||||
return ', '.join(array)
|
return ', '.join(array)
|
||||||
|
|
||||||
|
|
||||||
args_d = kwargs.get('args', {})
|
args_d = kwargs.get('args', None)
|
||||||
required = args_d.get('required', set())
|
if args_d is not None:
|
||||||
|
required = args_d.get('required', set())
|
||||||
|
|
||||||
missing = []
|
missing = []
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
for key in required:
|
for key in required:
|
||||||
data[key] = data_.pop(key, None)
|
data[key] = data_.pop(key, None)
|
||||||
if data[key] is None:
|
if data[key] is None:
|
||||||
missing.append(key)
|
missing.append(key)
|
||||||
|
|
||||||
if missing:
|
if missing:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
400,
|
400,
|
||||||
f"Missing value{plural(missing)} for: {comma_list(missing)}!")
|
f"Missing value{plural(missing)} for: {comma_list(missing)}!")
|
||||||
|
|
||||||
optional = args_d.get('optional', set())
|
optional = args_d.get('optional', set())
|
||||||
for key in optional:
|
for key in optional:
|
||||||
if key in data_:
|
if key in data_:
|
||||||
data[key] = data_[key]
|
data[key] = data_[key]
|
||||||
|
else:
|
||||||
|
""" Unsafe mode, without specified arguments
|
||||||
|
"""
|
||||||
|
data = data_
|
||||||
|
|
||||||
kwargs['data'] = data
|
kwargs['data'] = data
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
VERBS = ('GET', 'POST', 'PUT', 'PATCH', 'DELETE')
|
|
@ -3,15 +3,71 @@
|
||||||
lib/domain.py The domain-scoped utility functions
|
lib/domain.py The domain-scoped utility functions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import importlib
|
import importlib
|
||||||
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from types import ModuleType, FunctionType
|
from types import ModuleType, FunctionType
|
||||||
from typing import Generator, Dict, List
|
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")
|
logger = logging.getLogger("uvicorn.asgi")
|
||||||
|
|
||||||
VERBS = ('GET', 'POST', 'PUT', 'PATCH', 'DELETE')
|
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:
|
def get_fct_name(http_verb: str, path: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
@ -57,59 +113,53 @@ def get_fct_name(http_verb: str, path: str) -> str:
|
||||||
|
|
||||||
return '_'.join(fct_name)
|
return '_'.join(fct_name)
|
||||||
|
|
||||||
def gen_routes(route_params: Dict, path: List, m_router: ModuleType) -> Generator:
|
def gen_routes(m_router: ModuleType,
|
||||||
|
verb: str,
|
||||||
|
path: List[str],
|
||||||
|
params: List[Dict]) -> Tuple[FunctionType, Dict]:
|
||||||
"""
|
"""
|
||||||
Generates a tuple of the following form for a specific path:
|
Returns a tuple of the function associatied to the verb and path arguments,
|
||||||
|
and the dictionary of it's acls
|
||||||
"/path/to/route", {
|
|
||||||
"GET": {
|
|
||||||
"fct": endpoint_fct,
|
|
||||||
"params": [
|
|
||||||
{ "acl": acl_fct, [...] }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
[...]
|
|
||||||
}
|
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
- m_router (ModuleType): The module containing the function definition
|
||||||
|
|
||||||
- route_params (Dict): Contains the following keys :
|
- verb (str): The HTTP verb for the route (GET, POST, ...)
|
||||||
- one or more HTTP VERB (if none, route is not treated)
|
|
||||||
- one or zero FQTN (if none, fqtn is set to None)
|
|
||||||
|
|
||||||
- path (List): The route path, as a list (each item being a level of
|
- path (List): The route path, as a list (each item being a level of
|
||||||
deepness), from the lowest level (domain) to the highest
|
deepness), from the lowest level (domain) to the highest
|
||||||
|
|
||||||
- m_router (ModuleType): The parent router module
|
- params (Dict): The acl list of the following format :
|
||||||
|
[{'acl': Function, 'args': {'required': [], 'optional': []}}]
|
||||||
|
|
||||||
Yields:
|
|
||||||
|
|
||||||
(str, Dict): The path routes description
|
Returns:
|
||||||
|
|
||||||
|
(Function, Dict): The destination function and the acl dictionary
|
||||||
|
|
||||||
"""
|
"""
|
||||||
d_res = {'fqtn': route_params.get('FQTN')}
|
if len(params) == 0:
|
||||||
|
raise MissingAclError('[{}] {}'.format(verb, '/'.join(path)))
|
||||||
|
|
||||||
for verb in VERBS:
|
if len(path) == 0:
|
||||||
params = route_params.get(verb)
|
logger.error('Empty path for [{%s}]', verb)
|
||||||
if params is None:
|
raise PathError()
|
||||||
continue
|
|
||||||
if len(params) == 0:
|
|
||||||
logger.error('No ACL for route [{%s}] %s', verb, "/".join(path))
|
|
||||||
|
|
||||||
try:
|
fct_name = get_fct_name(verb, path[-1])
|
||||||
fct_name = get_fct_name(verb, path[-1])
|
if hasattr(m_router, fct_name):
|
||||||
fct = getattr(m_router, fct_name)
|
fct = getattr(m_router, fct_name)
|
||||||
logger.debug('%s defined in %s', fct.__name__, m_router.__name__)
|
else:
|
||||||
except AttributeError as exc:
|
raise UndefinedFunction('{}.{}'.format(m_router.__name__, fct_name or ''))
|
||||||
logger.error('%s is not defined in %s', fct_name, m_router.__name__)
|
|
||||||
continue
|
|
||||||
|
|
||||||
d_res[verb] = {'fct': fct, 'params': params}
|
|
||||||
|
|
||||||
yield f"/{'/'.join([ elt for elt in path if elt ])}", d_res
|
|
||||||
|
|
||||||
|
|
||||||
def gen_router_routes(m_router: ModuleType, path: List[str]) -> Generator:
|
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)
|
Recursive generatore that parses a router (or a subrouter)
|
||||||
and yields from gen_routes
|
and yields from gen_routes
|
||||||
|
@ -121,33 +171,43 @@ def gen_router_routes(m_router: ModuleType, path: List[str]) -> Generator:
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
|
|
||||||
(str, Dict): The path routes description from **gen_routes**
|
(str, str, Coroutine, List): A tuple containing the path, verb,
|
||||||
|
function and parameters of the route
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not hasattr(m_router, 'ROUTES'):
|
for subpath, params in read_router(m_router).items():
|
||||||
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)
|
path.append(subpath)
|
||||||
|
|
||||||
yield from gen_routes(route_params, path, m_router)
|
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)
|
||||||
|
|
||||||
subroutes = route_params.get('SUBROUTES', [])
|
|
||||||
for subroute in subroutes:
|
|
||||||
logger.debug('Processing subroute **%s** - %s', subroute, m_router.__name__)
|
|
||||||
path.append(subroute)
|
|
||||||
try:
|
try:
|
||||||
submod = importlib.import_module(f'.{subroute}', m_router.__name__)
|
yield from gen_router_routes(
|
||||||
|
importlib.import_module(f'.{subroute}', m_router.__name__),
|
||||||
|
path)
|
||||||
|
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
logger.error('Failed to import subroute **{%s}**', subroute)
|
logger.error('Failed to import subroute **{%s}**', subroute)
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
yield from gen_router_routes(submod, path)
|
|
||||||
|
|
||||||
path.pop()
|
path.pop()
|
||||||
|
|
||||||
path.pop()
|
path.pop()
|
||||||
|
@ -163,14 +223,14 @@ def d_domains(config) -> Dict[str, ModuleType]:
|
||||||
|
|
||||||
dict[str, ModuleType]
|
dict[str, ModuleType]
|
||||||
"""
|
"""
|
||||||
if not config.has_section('domains'):
|
if not 'domains' in config:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sys.path.append('.')
|
sys.path.append('.')
|
||||||
return {
|
return {
|
||||||
domain: importlib.import_module(''.join((domain, module)))
|
domain: importlib.import_module(''.join((domain, module)))
|
||||||
for domain, module in config.items('domains')
|
for domain, module in config['domains'].items()
|
||||||
}
|
}
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
logger.error('Could not load a domain : %s', exc)
|
logger.error('Could not load a domain : %s', exc)
|
||||||
|
|
|
@ -40,10 +40,17 @@ class DomainMiddleware(BaseHTTPMiddleware):
|
||||||
"""
|
"""
|
||||||
domain = scope['path'].split('/')[1]
|
domain = scope['path'].split('/')[1]
|
||||||
|
|
||||||
self.domains = d_domains(self.config)
|
self.domains = self.config.get('domains', {})
|
||||||
|
|
||||||
if domain in self.domains:
|
if len(domain) == 0:
|
||||||
|
for domain in self.domains:
|
||||||
|
self.api[domain], self.acl[domain] = api_routes(self.domains[domain])
|
||||||
|
elif domain in self.domains:
|
||||||
self.api[domain], self.acl[domain] = api_routes(self.domains[domain])
|
self.api[domain], self.acl[domain] = api_routes(self.domains[domain])
|
||||||
|
else:
|
||||||
|
logger.error('domain not in self.domains %s / %s',
|
||||||
|
scope['path'],
|
||||||
|
self.domains)
|
||||||
|
|
||||||
scope_ = scope.copy()
|
scope_ = scope.copy()
|
||||||
scope_['domains'] = self.domains
|
scope_['domains'] = self.domains
|
||||||
|
@ -57,8 +64,7 @@ class DomainMiddleware(BaseHTTPMiddleware):
|
||||||
current_domain = cur_path.split('/')[0]
|
current_domain = cur_path.split('/')[0]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config_section = self.config.items(current_domain)
|
scope_['config'] = self.config.copy()
|
||||||
scope_['config'] = dict(config_section)
|
|
||||||
except configparser.NoSectionError:
|
except configparser.NoSectionError:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'No specific configuration for domain **%s**', current_domain)
|
'No specific configuration for domain **%s**', current_domain)
|
||||||
|
|
|
@ -140,7 +140,7 @@ class JWTAuthenticationBackend(AuthenticationBackend):
|
||||||
key=self.secret_key,
|
key=self.secret_key,
|
||||||
algorithms=[self.algorithm],
|
algorithms=[self.algorithm],
|
||||||
options={
|
options={
|
||||||
'verify_signature': bool(PRODUCTION)
|
'verify_signature': True
|
||||||
})
|
})
|
||||||
|
|
||||||
if is_check_call:
|
if is_check_call:
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import os
|
||||||
|
from types import ModuleType
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from halfapi.lib.constants import VERBS
|
||||||
|
|
||||||
|
def read_router(m_router: ModuleType) -> Dict:
|
||||||
|
"""
|
||||||
|
Reads a module and returns a router dict
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not hasattr(m_router, 'ROUTES'):
|
||||||
|
routes = {'':{}}
|
||||||
|
acls = getattr(m_router, 'ACLS') if hasattr(m_router, 'ACLS') else None
|
||||||
|
|
||||||
|
if acls is not None:
|
||||||
|
for verb in VERBS:
|
||||||
|
if not hasattr(m_router, verb.lower()):
|
||||||
|
continue
|
||||||
|
|
||||||
|
""" There is a "verb" route in the router
|
||||||
|
"""
|
||||||
|
|
||||||
|
if verb.upper() not in acls:
|
||||||
|
continue
|
||||||
|
|
||||||
|
routes[''][verb.upper()] = []
|
||||||
|
routes[''][verb.upper()] = acls[verb.upper()].copy()
|
||||||
|
|
||||||
|
routes['']['SUBROUTES'] = []
|
||||||
|
if hasattr(m_router, '__path__'):
|
||||||
|
""" Module is a package
|
||||||
|
"""
|
||||||
|
m_path = getattr(m_router, '__path__')
|
||||||
|
if isinstance(m_path, list) and len(m_path) == 1:
|
||||||
|
routes['']['SUBROUTES'] = [
|
||||||
|
elt.name
|
||||||
|
for elt in os.scandir(m_path[0])
|
||||||
|
if elt.is_dir()
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
routes = getattr(m_router, 'ROUTES')
|
||||||
|
|
||||||
|
return routes
|
|
@ -16,7 +16,7 @@ Exception :
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import partial, wraps
|
from functools import partial, wraps
|
||||||
import logging
|
import logging
|
||||||
from typing import Callable, List, Dict, Generator
|
from typing import Callable, List, Dict, Generator, Tuple
|
||||||
from types import ModuleType, FunctionType
|
from types import ModuleType, FunctionType
|
||||||
|
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
@ -105,22 +105,19 @@ def gen_starlette_routes(d_domains: Dict[str, ModuleType]) -> Generator:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for domain_name, m_domain in d_domains.items():
|
for domain_name, m_domain in d_domains.items():
|
||||||
for path, d_route in gen_router_routes(m_domain, [domain_name]):
|
for path, verb, fct, params in gen_router_routes(m_domain, []):
|
||||||
for verb in VERBS:
|
yield (
|
||||||
if verb not in d_route.keys():
|
Route(f'/{domain_name}/{path}',
|
||||||
continue
|
route_acl_decorator(
|
||||||
|
fct,
|
||||||
yield (
|
params
|
||||||
Route(path,
|
),
|
||||||
route_acl_decorator(
|
methods=[verb])
|
||||||
d_route[verb]['fct'],
|
)
|
||||||
d_route[verb]['params']
|
|
||||||
),
|
|
||||||
methods=[verb])
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def api_routes(m_dom: ModuleType) -> Generator:
|
|
||||||
|
def api_routes(m_dom: ModuleType) -> Tuple[Dict, Dict]:
|
||||||
"""
|
"""
|
||||||
Yields the description objects for HalfAPI app routes
|
Yields the description objects for HalfAPI app routes
|
||||||
|
|
||||||
|
@ -128,7 +125,7 @@ def api_routes(m_dom: ModuleType) -> Generator:
|
||||||
m_dom (ModuleType): the halfapi module
|
m_dom (ModuleType): the halfapi module
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Generator(Dict)
|
(Dict, Dict)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
d_acls = {}
|
d_acls = {}
|
||||||
|
@ -137,6 +134,7 @@ def api_routes(m_dom: ModuleType) -> Generator:
|
||||||
l_params = []
|
l_params = []
|
||||||
|
|
||||||
for param in params:
|
for param in params:
|
||||||
|
|
||||||
if 'acl' not in param.keys() or not param['acl']:
|
if 'acl' not in param.keys() or not param['acl']:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -149,13 +147,10 @@ def api_routes(m_dom: ModuleType) -> Generator:
|
||||||
return l_params
|
return l_params
|
||||||
|
|
||||||
d_res = {}
|
d_res = {}
|
||||||
for path, d_route in gen_router_routes(m_dom, [m_dom.__name__]):
|
for path, verb, fct, params in gen_router_routes(m_dom, []):
|
||||||
d_res[path] = {'fqtn': d_route['fqtn'] }
|
if path not in d_res:
|
||||||
|
d_res[path] = {}
|
||||||
for verb in VERBS:
|
d_res[path][verb] = str_acl(params)
|
||||||
if verb not in d_route.keys():
|
|
||||||
continue
|
|
||||||
d_res[path][verb] = str_acl(d_route[verb]['params'])
|
|
||||||
|
|
||||||
return d_res, d_acls
|
return d_res, d_acls
|
||||||
|
|
||||||
|
@ -196,7 +191,7 @@ def debug_routes():
|
||||||
yield Route('/halfapi/log', debug_log)
|
yield Route('/halfapi/log', debug_log)
|
||||||
|
|
||||||
async def error_code(request: Request, *args, **kwargs):
|
async def error_code(request: Request, *args, **kwargs):
|
||||||
code = request.path_params.get('code')
|
code = request.path_params['code']
|
||||||
raise HTTPException(code)
|
raise HTTPException(code)
|
||||||
|
|
||||||
yield Route('/halfapi/error/{code:int}', error_code)
|
yield Route('/halfapi/error/{code:int}', error_code)
|
||||||
|
|
|
@ -10,13 +10,16 @@ Constant :
|
||||||
SCHEMAS (starlette.schemas.SchemaGenerator)
|
SCHEMAS (starlette.schemas.SchemaGenerator)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
from starlette.schemas import SchemaGenerator
|
from starlette.schemas import SchemaGenerator
|
||||||
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
from .routes import gen_starlette_routes, api_acls
|
from .routes import gen_starlette_routes, api_acls
|
||||||
from .responses import ORJSONResponse
|
from .responses import ORJSONResponse
|
||||||
|
|
||||||
|
logger = logging.getLogger('uvicorn.asgi')
|
||||||
SCHEMAS = SchemaGenerator(
|
SCHEMAS = SchemaGenerator(
|
||||||
{"openapi": "3.0.0", "info": {"title": "HalfAPI", "version": "1.0"}}
|
{"openapi": "3.0.0", "info": {"title": "HalfAPI", "version": "1.0"}}
|
||||||
)
|
)
|
||||||
|
@ -28,6 +31,20 @@ async def get_api_routes(request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
return ORJSONResponse(request.scope['api'])
|
return ORJSONResponse(request.scope['api'])
|
||||||
|
|
||||||
|
def get_api_domain_routes(domain):
|
||||||
|
async def wrapped(request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
description: Returns the current API routes description (HalfAPI 0.2.1)
|
||||||
|
as a JSON object
|
||||||
|
"""
|
||||||
|
if domain in request.scope['api']:
|
||||||
|
return ORJSONResponse(request.scope['api'][domain])
|
||||||
|
else:
|
||||||
|
raise HTTPException(404)
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def schema_json(request, *args, **kwargs):
|
async def schema_json(request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -11,6 +11,7 @@ from uuid import uuid1, uuid4, UUID
|
||||||
import click
|
import click
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
import jwt
|
import jwt
|
||||||
|
import sys
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
import pytest
|
import pytest
|
||||||
from starlette.applications import Starlette
|
from starlette.applications import Starlette
|
||||||
|
@ -212,8 +213,8 @@ def dummy_app():
|
||||||
backend=JWTAuthenticationBackend(secret_key='dummysecret')
|
backend=JWTAuthenticationBackend(secret_key='dummysecret')
|
||||||
)
|
)
|
||||||
return app
|
return app
|
||||||
@pytest.fixture
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
def dummy_debug_app():
|
def dummy_debug_app():
|
||||||
app = Starlette(debug=True)
|
app = Starlette(debug=True)
|
||||||
app.add_route('/',
|
app.add_route('/',
|
||||||
|
@ -228,3 +229,75 @@ def dummy_debug_app():
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_client(dummy_app):
|
def test_client(dummy_app):
|
||||||
return TestClient(dummy_app)
|
return TestClient(dummy_app)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def create_route():
|
||||||
|
def wrapped(domain_path, method, path):
|
||||||
|
stack = [domain_path, *path.split('/')[1:]]
|
||||||
|
for i in range(len(stack)):
|
||||||
|
if len(stack[i]) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
path = os.path.join(*stack[0:i+1])
|
||||||
|
if os.path.isdir(os.path.join(path)):
|
||||||
|
continue
|
||||||
|
os.mkdir(path)
|
||||||
|
init_path = os.path.join(*stack, '__init__.py')
|
||||||
|
with open(init_path, 'a+') as f:
|
||||||
|
f.write(f'\ndef {method}():\n raise NotImplementedError')
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def dummy_project():
|
||||||
|
sys.path.insert(0, './tests')
|
||||||
|
halfapi_config = tempfile.mktemp()
|
||||||
|
halfapi_secret = tempfile.mktemp()
|
||||||
|
domain = 'dummy_domain'
|
||||||
|
|
||||||
|
with open(halfapi_config, 'w') as f:
|
||||||
|
f.writelines([
|
||||||
|
'[project]\n',
|
||||||
|
'name = lirmm_api\n',
|
||||||
|
'halfapi_version = 0.5.0\n',
|
||||||
|
f'secret = {halfapi_secret}\n',
|
||||||
|
'port = 3050\n',
|
||||||
|
'loglevel = debug\n',
|
||||||
|
'[domains]\n',
|
||||||
|
f'{domain}= .routers'
|
||||||
|
])
|
||||||
|
|
||||||
|
with open(halfapi_secret, 'w') as f:
|
||||||
|
f.write('turlututu')
|
||||||
|
|
||||||
|
return (halfapi_config, 'dummy_domain', 'routers')
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def routers():
|
||||||
|
sys.path.insert(0, './tests')
|
||||||
|
|
||||||
|
from dummy_domain import routers
|
||||||
|
return routers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def application_debug():
|
||||||
|
from halfapi.app import HalfAPI
|
||||||
|
return HalfAPI({
|
||||||
|
'SECRET':'turlututu',
|
||||||
|
'PRODUCTION':False
|
||||||
|
}).application
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def application_domain(routers):
|
||||||
|
from halfapi.app import HalfAPI
|
||||||
|
return HalfAPI({
|
||||||
|
'SECRET':'turlututu',
|
||||||
|
'PRODUCTION':True,
|
||||||
|
'DOMAINS':{'dummy_domain':routers}
|
||||||
|
}).application
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
ROUTES={
|
|
||||||
'': {
|
|
||||||
'SUBROUTES': ['abc','act']
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
ROUTES={
|
|
||||||
'': {
|
|
||||||
'SUBROUTES': ['alphabet', 'pinnochio']
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
from halfapi.lib import acl
|
||||||
|
ACLS = {
|
||||||
|
'GET': [{'acl':acl.public}],
|
||||||
|
'POST': [{'acl':acl.public}],
|
||||||
|
'PATCH': [{'acl':acl.public}],
|
||||||
|
'PUT': [{'acl':acl.public}]
|
||||||
|
}
|
||||||
|
|
||||||
|
def get(test):
|
||||||
|
return str(test)
|
||||||
|
|
||||||
|
def post(test):
|
||||||
|
return str(test)
|
||||||
|
|
||||||
|
def patch(test):
|
||||||
|
return str(test)
|
||||||
|
|
||||||
|
def put(test):
|
||||||
|
return str(test)
|
|
@ -1,17 +1,8 @@
|
||||||
from starlette.responses import PlainTextResponse
|
from starlette.responses import PlainTextResponse
|
||||||
from dummy_domain import acl
|
from halfapi.lib import acl
|
||||||
|
|
||||||
ROUTES={
|
|
||||||
'': {
|
|
||||||
'GET': [{'acl':acl.public}]
|
|
||||||
},
|
|
||||||
'{test:uuid}': {
|
|
||||||
'GET': [{'acl':None}],
|
|
||||||
'POST': [{'acl':None}],
|
|
||||||
'PATCH': [{'acl':None}],
|
|
||||||
'PUT': [{'acl':None}]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
ACLS = {
|
||||||
|
'GET': [{'acl':acl.public}]
|
||||||
}
|
}
|
||||||
|
|
||||||
async def get(request, *args, **kwargs):
|
async def get(request, *args, **kwargs):
|
||||||
|
|
|
@ -1,2 +1,6 @@
|
||||||
ROUTES={
|
from halfapi.lib import acl
|
||||||
|
ACLS = {
|
||||||
|
'GET' : [{'acl':acl.public}]
|
||||||
}
|
}
|
||||||
|
def get():
|
||||||
|
raise NotImplementedError
|
||||||
|
|
|
@ -2,31 +2,21 @@
|
||||||
import pytest
|
import pytest
|
||||||
from starlette.authentication import UnauthenticatedUser
|
from starlette.authentication import UnauthenticatedUser
|
||||||
from starlette.testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
from halfapi.app import application
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
def test_get_api_routes():
|
|
||||||
c = TestClient(application)
|
|
||||||
r = c.get('/')
|
|
||||||
d_r = r.json()
|
|
||||||
assert isinstance(d_r, dict)
|
|
||||||
|
|
||||||
|
def test_current_user(project_runner, application_debug):
|
||||||
def test_current_user(project_runner):
|
c = TestClient(application_debug)
|
||||||
"""
|
|
||||||
Missing HALFAPI_SECRET to give current user route
|
|
||||||
"""
|
|
||||||
c = TestClient(application)
|
|
||||||
r = c.get('/halfapi/current_user')
|
r = c.get('/halfapi/current_user')
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
|
|
||||||
def test_log():
|
def test_log(application_debug):
|
||||||
c = TestClient(application)
|
c = TestClient(application_debug)
|
||||||
r = c.get('/halfapi/log')
|
r = c.get('/halfapi/log')
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
|
|
||||||
def test_error():
|
def test_error(application_debug):
|
||||||
c = TestClient(application)
|
c = TestClient(application_debug)
|
||||||
r = c.get('/halfapi/error/400')
|
r = c.get('/halfapi/error/400')
|
||||||
assert r.status_code == 400
|
assert r.status_code == 400
|
||||||
r = c.get('/halfapi/error/404')
|
r = c.get('/halfapi/error/404')
|
||||||
|
@ -34,8 +24,8 @@ def test_error():
|
||||||
r = c.get('/halfapi/error/500')
|
r = c.get('/halfapi/error/500')
|
||||||
assert r.status_code == 500
|
assert r.status_code == 500
|
||||||
|
|
||||||
def test_exception():
|
def test_exception(application_debug):
|
||||||
c = TestClient(application)
|
c = TestClient(application_debug)
|
||||||
try:
|
try:
|
||||||
r = c.get('/halfapi/exception')
|
r = c.get('/halfapi/exception')
|
||||||
assert r.status_code == 500
|
assert r.status_code == 500
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import importlib
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import pytest
|
||||||
|
from starlette.routing import Route
|
||||||
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
|
from halfapi.lib.domain import gen_router_routes
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_route(dummy_project, application_domain, routers):
|
||||||
|
c = TestClient(application_domain)
|
||||||
|
path = verb = params = None
|
||||||
|
for path, verb, _, params in gen_router_routes(routers, []):
|
||||||
|
if len(params):
|
||||||
|
route_path = '/dummy_domain/{}'.format(path)
|
||||||
|
try:
|
||||||
|
if verb.lower() == 'get':
|
||||||
|
r = c.get(route_path)
|
||||||
|
elif verb.lower() == 'post':
|
||||||
|
r = c.post(route_path)
|
||||||
|
elif verb.lower() == 'patch':
|
||||||
|
r = c.patch(route_path)
|
||||||
|
elif verb.lower() == 'put':
|
||||||
|
r = c.put(route_path)
|
||||||
|
elif verb.lower() == 'delete':
|
||||||
|
r = c.delete(route_path)
|
||||||
|
else:
|
||||||
|
raise Exception(verb)
|
||||||
|
try:
|
||||||
|
assert r.status_code in [200, 501]
|
||||||
|
except AssertionError as exc:
|
||||||
|
print('{} [{}] {}'.format(str(r.status_code), verb, route_path))
|
||||||
|
|
||||||
|
except NotImplementedError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not path:
|
||||||
|
raise Exception('No route generated')
|
|
@ -1,18 +1,36 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import importlib
|
import importlib
|
||||||
from halfapi.lib.domain import VERBS, gen_router_routes
|
from halfapi.lib.domain import VERBS, gen_routes, gen_router_routes, MissingAclError
|
||||||
|
|
||||||
|
from types import FunctionType
|
||||||
|
|
||||||
|
|
||||||
def test_gen_router_routes():
|
def test_gen_router_routes():
|
||||||
from .dummy_domain import routers
|
from .dummy_domain import routers
|
||||||
for path, d_route in gen_router_routes(routers, ['dummy_domain']):
|
for path, verb, fct, params in gen_router_routes(routers, ['dummy_domain']):
|
||||||
assert isinstance(path, str)
|
assert isinstance(path, str)
|
||||||
for verb in VERBS:
|
assert verb in VERBS
|
||||||
if verb not in d_route.keys():
|
assert len(params) > 0
|
||||||
continue
|
assert hasattr(fct, '__call__')
|
||||||
route = d_route[verb]
|
|
||||||
print(f'[{verb}] {path} {route["fct"]}')
|
def test_gen_routes():
|
||||||
assert len(route['params']) > 0
|
from .dummy_domain.routers.abc.alphabet import TEST_uuid
|
||||||
assert hasattr(route['fct'], '__call__')
|
try:
|
||||||
if 'fqtn' in route:
|
gen_routes(
|
||||||
assert isinstance(route['fqtn'], str)
|
TEST_uuid,
|
||||||
|
'get',
|
||||||
|
['abc', 'alphabet', 'TEST_uuid', ''],
|
||||||
|
[])
|
||||||
|
except MissingAclError:
|
||||||
|
assert True
|
||||||
|
|
||||||
|
fct, params = gen_routes(
|
||||||
|
TEST_uuid,
|
||||||
|
'get',
|
||||||
|
['abc', 'alphabet', 'TEST_uuid', ''],
|
||||||
|
TEST_uuid.ACLS['GET'])
|
||||||
|
|
||||||
|
assert isinstance(fct, FunctionType)
|
||||||
|
assert isinstance(params, list)
|
||||||
|
assert len(TEST_uuid.ACLS['GET']) == len(params)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import os
|
||||||
|
from halfapi.lib.router import read_router
|
||||||
|
|
||||||
|
def test_read_router_routers():
|
||||||
|
from .dummy_domain import routers
|
||||||
|
|
||||||
|
router_d = read_router(routers)
|
||||||
|
assert '' in router_d
|
||||||
|
assert 'SUBROUTES' in router_d['']
|
||||||
|
assert isinstance(router_d['']['SUBROUTES'], list)
|
||||||
|
|
||||||
|
for elt in os.scandir(routers.__path__[0]):
|
||||||
|
if elt.is_dir():
|
||||||
|
assert elt.name in router_d['']['SUBROUTES']
|
||||||
|
|
||||||
|
def test_read_router_abc():
|
||||||
|
from .dummy_domain.routers import abc
|
||||||
|
router_d = read_router(abc)
|
||||||
|
|
||||||
|
assert '' in router_d
|
||||||
|
assert 'SUBROUTES' in router_d['']
|
||||||
|
assert isinstance(router_d['']['SUBROUTES'], list)
|
||||||
|
|
||||||
|
def test_read_router_alphabet():
|
||||||
|
from .dummy_domain.routers.abc import alphabet
|
||||||
|
router_d = read_router(alphabet)
|
||||||
|
|
||||||
|
assert '' in router_d
|
||||||
|
assert 'SUBROUTES' in router_d['']
|
||||||
|
assert isinstance(router_d['']['SUBROUTES'], list)
|
||||||
|
|
||||||
|
def test_read_router_TEST():
|
||||||
|
from .dummy_domain.routers.abc.alphabet import TEST_uuid
|
||||||
|
router_d = read_router(TEST_uuid)
|
||||||
|
|
||||||
|
print(router_d)
|
||||||
|
assert '' in router_d
|
||||||
|
assert 'SUBROUTES' in router_d['']
|
||||||
|
assert isinstance(router_d['']['GET'], list)
|
||||||
|
assert isinstance(router_d['']['POST'], list)
|
||||||
|
assert isinstance(router_d['']['PATCH'], list)
|
||||||
|
assert isinstance(router_d['']['PUT'], list)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ from starlette.authentication import (
|
||||||
AuthenticationBackend, AuthenticationError, BaseUser, AuthCredentials,
|
AuthenticationBackend, AuthenticationError, BaseUser, AuthCredentials,
|
||||||
UnauthenticatedUser)
|
UnauthenticatedUser)
|
||||||
|
|
||||||
from halfapi.app import application
|
|
||||||
from halfapi.lib.schemas import schema_dict_dom
|
from halfapi.lib.schemas import schema_dict_dom
|
||||||
|
|
||||||
def test_schemas_dict_dom():
|
def test_schemas_dict_dom():
|
||||||
|
@ -15,8 +14,23 @@ def test_schemas_dict_dom():
|
||||||
assert isinstance(schema, dict)
|
assert isinstance(schema, dict)
|
||||||
|
|
||||||
|
|
||||||
def test_get_api_routes(project_runner):
|
def test_get_api_routes(project_runner, application_debug):
|
||||||
c = TestClient(application)
|
c = TestClient(application_debug)
|
||||||
r = c.get('/')
|
r = c.get('/')
|
||||||
d_r = r.json()
|
d_r = r.json()
|
||||||
assert isinstance(d_r, dict)
|
assert isinstance(d_r, dict)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_api_dummy_domain_routes(application_domain, routers):
|
||||||
|
c = TestClient(application_domain)
|
||||||
|
r = c.get('/dummy_domain')
|
||||||
|
assert r.status_code == 200
|
||||||
|
d_r = r.json()
|
||||||
|
assert isinstance(d_r, dict)
|
||||||
|
print(d_r)
|
||||||
|
assert 'abc/alphabet' in d_r
|
||||||
|
assert 'GET' in d_r['abc/alphabet']
|
||||||
|
assert len(d_r['abc/alphabet']['GET']) > 0
|
||||||
|
assert 'acl' in d_r['abc/alphabet']['GET'][0]
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue