[0.5.9] release

Squashed commit of the following:

commit 7fe3e22f5e4108b5eb149abf8d608334debc49ca
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Sep 2 16:59:52 2021 +0200

    [0.5.9] release

commit c36c0fcc982388a5acf2f9f937fa8ab54a18f3de
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Sep 2 16:53:13 2021 +0200

    [conf] fix #19 et ajout du test (test_dummy_project_router/test_get_config)

    configuration du domaine accessible depuis :

    l'attribut config de l'argument "halfapi" pour les fonctions
    request.scope['config'] pour les fonctions async

commit cc235eee8c6f8f5d3606dda0f88156697eac296e
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Sep 2 14:59:17 2021 +0200

    [tests] don't import click two times

commit fa418478c76205bb407e536737d8e389b4bf391c
Author: Maxime Alves LIRMM <maxime.alves@lirmm.fr>
Date:   Thu Sep 2 14:57:06 2021 +0200

    [clean] remove unused variables, remove [] as default value in fct, raise from exc
This commit is contained in:
Maxime Alves LIRMM 2021-09-02 17:01:36 +02:00
parent 74b79120ba
commit 0470f9fa89
9 changed files with 56 additions and 31 deletions

View File

@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
__version__ = '0.5.8' __version__ = '0.5.9'
def version(): def version():
return f'HalfAPI version:{__version__}' return f'HalfAPI version:{__version__}'

View File

@ -111,16 +111,15 @@ def read_config():
The highest index in "filenames" are the highest priorty The highest index in "filenames" are the highest priorty
""" """
config.read(HALFAPI_CONFIG_FILES) config.read(HALFAPI_CONFIG_FILES)
return config_dict()
CONFIG = {} CONFIG = {}
IS_PROJECT = False IS_PROJECT = False
if is_project(): if is_project():
read_config()
IS_PROJECT = True IS_PROJECT = True
CONFIG = read_config()
PROJECT_NAME = config.get('project', 'name', fallback=PROJECT_NAME) PROJECT_NAME = config.get('project', 'name', fallback=PROJECT_NAME)
@ -154,11 +153,11 @@ if is_project():
'production': PRODUCTION, 'production': PRODUCTION,
'secret': SECRET, 'secret': SECRET,
'domains': DOMAINS, 'domains': DOMAINS,
'config': {} 'domain_config': {}
} }
for domain in DOMAINS: for domain in DOMAINS:
if domain not in config.sections(): if domain not in config.sections():
continue continue
CONFIG['config'][domain] = dict(config.items(domain)) CONFIG['domain_config'][domain] = dict(config.items(domain))

View File

@ -3,16 +3,14 @@
lib/domain.py The domain-scoped utility functions lib/domain.py The domain-scoped utility functions
""" """
import os
import re import re
import sys import sys
import importlib import importlib
import inspect import inspect
import logging import logging
from types import ModuleType, FunctionType from types import ModuleType, FunctionType
from typing import Any, Callable, Coroutine, Generator from typing import Coroutine, Generator
from typing import Dict, List, Tuple, Iterator from typing import Dict, List, Tuple, Iterator
import inspect
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
@ -51,7 +49,9 @@ def route_decorator(fct: FunctionType, ret_type: str = 'json') -> Coroutine:
fct_args['halfapi'] = { fct_args['halfapi'] = {
'user': request.user if 'user': request.user if
'user' in request else None, 'user' in request else None,
'config': request.scope['config'] 'config': request.scope['config'],
'domain': request.scope['domain'],
} }
@ -65,7 +65,7 @@ def route_decorator(fct: FunctionType, ret_type: str = 'json') -> Coroutine:
except Exception as exc: except Exception as exc:
# TODO: Write tests # TODO: Write tests
if not isinstance(exc, HTTPException): if not isinstance(exc, HTTPException):
raise HTTPException(500) raise HTTPException(500) from exc
raise exc raise exc
@ -200,8 +200,8 @@ def gen_router_routes(m_router: ModuleType, path: List[str]) -> \
path.append('{{{}:{}}}'.format( path.append('{{{}:{}}}'.format(
param_match.groups()[0].lower(), param_match.groups()[0].lower(),
param_match.groups()[1])) param_match.groups()[1]))
except AssertionError: except AssertionError as exc:
raise UnknownPathParameterType(subroute) raise UnknownPathParameterType(subroute) from exc
else: else:
path.append(subroute) path.append(subroute)
@ -243,8 +243,6 @@ def d_domains(config) -> Dict[str, ModuleType]:
raise exc raise exc
def router_acls(route_params: Dict, path: List, m_router: ModuleType) -> Generator: def router_acls(route_params: Dict, path: List, m_router: ModuleType) -> Generator:
d_res = {'fqtn': route_params.get('FQTN')}
for verb in VERBS: for verb in VERBS:
params = route_params.get(verb) params = route_params.get(verb)
if params is None: if params is None:
@ -253,11 +251,11 @@ def router_acls(route_params: Dict, path: List, m_router: ModuleType) -> Generat
logger.error('No ACL for route [{%s}] %s', verb, "/".join(path)) logger.error('No ACL for route [{%s}] %s', verb, "/".join(path))
else: else:
for param in params: for param in params:
acl = param.get('acl') fct_acl = param.get('acl')
if not isinstance(acl, FunctionType): if not isinstance(fct_acl, FunctionType):
continue continue
yield acl.__name__, acl yield fct_acl.__name__, fct_acl
def domain_acls(m_router, path): def domain_acls(m_router, path):

View File

@ -1,7 +1,6 @@
""" """
DomainMiddleware DomainMiddleware
""" """
import configparser
import logging import logging
from starlette.datastructures import URL from starlette.datastructures import URL
@ -9,10 +8,6 @@ from starlette.middleware.base import (BaseHTTPMiddleware,
RequestResponseEndpoint) RequestResponseEndpoint)
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import Response from starlette.responses import Response
from starlette.types import Scope, Send, Receive
from .routes import api_routes
from .domain import d_domains
logger = logging.getLogger('uvicorn.asgi') logger = logging.getLogger('uvicorn.asgi')
@ -38,6 +33,15 @@ class DomainMiddleware(BaseHTTPMiddleware):
Call of the route fonction (decorated or not) Call of the route fonction (decorated or not)
""" """
l_path = URL(scope=request.scope).path.split('/')
cur_domain = l_path[0]
if len(cur_domain) == 0 and len(l_path) > 1:
cur_domain = l_path[1]
request.scope['domain'] = cur_domain
request.scope['config'] = self.config['domain_config'][cur_domain] \
if cur_domain in self.config.get('domain_config', {}) else {}
response = await call_next(request) response = await call_next(request)
if 'acl_pass' in request.scope: if 'acl_pass' in request.scope:
@ -54,5 +58,6 @@ class DomainMiddleware(BaseHTTPMiddleware):
response.headers['x-args-optional'] = \ response.headers['x-args-optional'] = \
','.join(request.scope['args']['optional']) ','.join(request.scope['args']['optional'])
response.headers['x-domain'] = cur_domain
return response return response

View File

@ -24,7 +24,7 @@ from starlette.routing import Route
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import Response, PlainTextResponse from starlette.responses import Response, PlainTextResponse
from halfapi.lib.domain import gen_router_routes, VERBS, domain_acls from halfapi.lib.domain import gen_router_routes, domain_acls
from ..conf import DOMAINSDICT from ..conf import DOMAINSDICT
@ -34,7 +34,7 @@ class DomainNotFoundError(Exception):
""" Exception when a domain is not importable """ Exception when a domain is not importable
""" """
def route_acl_decorator(fct: Callable = None, params: List[Dict] = []): def route_acl_decorator(fct: Callable = None, params: List[Dict] = None):
""" """
Decorator for async functions that calls pre-conditions functions Decorator for async functions that calls pre-conditions functions
and appends kwargs to the target function and appends kwargs to the target function
@ -51,6 +51,9 @@ def route_acl_decorator(fct: Callable = None, params: List[Dict] = []):
async function async function
""" """
if not params:
params = []
if not fct: if not fct:
return partial(route_acl_decorator, params=params) return partial(route_acl_decorator, params=params)
@ -147,7 +150,7 @@ def api_routes(m_dom: ModuleType) -> Tuple[Dict, Dict]:
return l_params return l_params
d_res = {} d_res = {}
for path, verb, fct, params in gen_router_routes(m_dom, []): for path, verb, _, params in gen_router_routes(m_dom, []):
if path not in d_res: if path not in d_res:
d_res[path] = {} d_res[path] = {}
d_res[path][verb] = str_acl(params) d_res[path][verb] = str_acl(params)

View File

@ -98,7 +98,7 @@ async def schema_json(request, *args, **kwargs):
SCHEMAS.get_schema(routes=request.app.routes)) SCHEMAS.get_schema(routes=request.app.routes))
def schema_dict_dom(d_domains) -> Dict: def schema_dict_dom(d_domains: Dict[str, ModuleType]) -> Dict:
""" """
Returns the API schema of the *m_domain* domain as a python dictionnary Returns the API schema of the *m_domain* domain as a python dictionnary

View File

@ -8,7 +8,6 @@ import importlib
import tempfile import tempfile
from typing import Dict, Tuple from typing import Dict, Tuple
from uuid import uuid1, uuid4, UUID from uuid import uuid1, uuid4, UUID
import click
from click.testing import CliRunner from click.testing import CliRunner
import jwt import jwt
import sys import sys
@ -70,7 +69,7 @@ def runner():
@pytest.fixture @pytest.fixture
def cli_runner(): def cli_runner():
"""Yield a click.testing.CliRunner to invoke the CLI.""" """Yield a click.testing.CliRunner to invoke the CLI."""
class_ = click.testing.CliRunner class_ = CliRunner
def invoke_wrapper(f): def invoke_wrapper(f):
"""Augment CliRunner.invoke to emit its output to stdout. """Augment CliRunner.invoke to emit its output to stdout.
@ -266,7 +265,9 @@ def dummy_project():
'port = 3050\n', 'port = 3050\n',
'loglevel = debug\n', 'loglevel = debug\n',
'[domains]\n', '[domains]\n',
f'{domain}= .routers' f'{domain} = .routers',
f'[{domain}]',
'test = True'
]) ])
with open(halfapi_secret, 'w') as f: with open(halfapi_secret, 'w') as f:
@ -297,7 +298,11 @@ def application_domain(routers):
return HalfAPI({ return HalfAPI({
'SECRET':'turlututu', 'SECRET':'turlututu',
'PRODUCTION':True, 'PRODUCTION':True,
'DOMAINS':{'dummy_domain':routers} 'DOMAINS':{'dummy_domain':routers},
'CONFIG':{
'domains': {'dummy_domain':routers},
'domain_config': {'dummy_domain': {'test': True}}
}
}).application }).application

View File

@ -0,0 +1,11 @@
from halfapi.lib import acl
import logging
logger = logging.getLogger('uvicorn.asgi')
ACLS = {
'GET' : [{'acl':acl.public}]
}
def get(halfapi):
logger.error('%s', halfapi)
return halfapi['config']

View File

@ -9,6 +9,10 @@ from starlette.testclient import TestClient
from halfapi.lib.domain import gen_router_routes from halfapi.lib.domain import gen_router_routes
def test_get_config_route(dummy_project, application_domain, routers):
c = TestClient(application_domain)
r = c.get('/dummy_domain/config')
assert 'test' in r.json()
def test_get_route(dummy_project, application_domain, routers): def test_get_route(dummy_project, application_domain, routers):
c = TestClient(application_domain) c = TestClient(application_domain)