[refactor] update to avoid using global variables, and configuration are not stored in /etc/half_api anymore

Added domain_middleware
This commit is contained in:
Maxime Alves LIRMM 2020-10-07 09:50:46 +02:00
parent 781736c151
commit f5ebabbcd4
13 changed files with 184 additions and 93 deletions

View File

@ -6,9 +6,6 @@ runner.
It defines the following globals :
- routes (contains the Route objects for the application)
- d_acl (a dictionnary of the active acls for the current project)
- d_api (a dictionnary of the routes depending on the routers definition in
the project)
- application (the asgi application itself - a starlette object)
"""
@ -21,7 +18,9 @@ from starlette.middleware.authentication import AuthenticationMiddleware
# module libraries
from halfapi.conf import SECRET, PRODUCTION, DOMAINSDICT
from halfapi.conf import config, SECRET, PRODUCTION, DOMAINSDICT
from .lib.domain_middleware import DomainMiddleware
from halfapi.lib.jwt_middleware import JWTAuthenticationBackend
@ -44,20 +43,16 @@ routes += [
Route('/halfapi/acls', get_acls)
]
d_api = {}
d_acl = {}
for domain, m_domain in DOMAINSDICT.items():
for route in gen_starlette_routes(m_domain):
routes.append(route)
d_api[domain], d_acl[domain] = api_routes(m_domain)
for route in gen_starlette_routes(DOMAINSDICT()):
routes.append(route)
application = Starlette(
debug=not PRODUCTION,
routes=routes,
middleware=[
Middleware(DomainMiddleware, config=config),
Middleware(AuthenticationMiddleware,
backend=JWTAuthenticationBackend(secret_key=SECRET))
],

View File

@ -20,7 +20,7 @@ def cli(ctx, version):
"""
if version:
from halfapi import version
return click.echo(version())
click.echo(version())
if IS_PROJECT:
from . import config

View File

@ -8,8 +8,9 @@ import click
from .cli import cli
from ..conf import (
read_config,
PROJECT_NAME,
DOMAINS,
DOMAINSDICT,
HOST,
PORT,
PRODUCTION,
@ -26,12 +27,13 @@ base_dir = {BASE_DIR}
[domains]"""
for dom in DOMAINS:
for dom in DOMAINSDICT():
CONF_STR = '\n'.join((CONF_STR, dom))
@cli.command()
def config():
"""
Lists config parameters and their values
"""
click.echo(CONF_STR)
click.echo(read_config())

View File

@ -2,14 +2,15 @@
"""
cli/domain.py Defines the "halfapi domain" cli commands
"""
# builtins
import logging
import sys
import click
from .cli import cli
from ..conf import DOMAINS, DOMAINSDICT
from ..conf import config, write_config, DOMAINS, DOMAINSDICT
from ..lib.schemas import schema_dict_dom
@ -19,29 +20,46 @@ logger = logging.getLogger('halfapi')
#################
# domain create #
#################
def create_domain():
"""
TODO: Implement function to create (add) domain to a project through cli
"""
raise NotImplementedError
def create_domain(domain_name: str, module_path: str):
logger.info('Will add **%s** (%s) to current halfAPI project',
domain_name, module_path)
if domain_name in DOMAINSDICT():
logger.warning('Domain **%s** is already in project')
sys.exit(1)
if not config.has_section('domains'):
config.add_section('domains')
config.set('domains', domain_name, module_path)
write_config()
###############
# domain read #
###############
def list_routes(domain_name):
def list_routes(domain, m_dom):
"""
Echoes the list of the **domain_name** active routes
Echoes the list of the **m_dom** active routes
"""
click.echo(f'\nDomain : {domain}')
m_dom = DOMAINSDICT[domain_name]
for key, item in schema_dict_dom(m_dom).get('paths', {}).items():
for key, item in schema_dict_dom({domain: m_dom}).get('paths', {}).items():
methods = '|'.join(list(item.keys()))
click.echo(f'{key} : {methods}')
def list_api_routes():
"""
Echoes the list of all active domains.
"""
click.echo('# API Routes')
for domain, m_dom in DOMAINSDICT().items():
list_routes(domain, m_dom)
@click.option('--read',default=False, is_flag=True)
@click.option('--create',default=False, is_flag=True)
@click.option('--update',default=False, is_flag=True)
@ -63,7 +81,8 @@ def domain(domains, delete, update, create, read): #, domains, read, create, up
if not domains:
if create:
create_domain()
# TODO: Connect to the create_domain function
raise NotImplementedError
domains = DOMAINS
else:
@ -85,4 +104,4 @@ def domain(domains, delete, update, create, read): #, domains, read, create, up
raise NotImplementedError
if read:
list_routes(domain_name)
list_api_routes()

View File

@ -7,9 +7,9 @@ import click
import uvicorn
from .cli import cli
from .domain import list_routes
from .domain import list_api_routes
from ..conf import (PROJECT_NAME, HOST, PORT,
PRODUCTION, BASE_DIR, DOMAINS)
PRODUCTION, BASE_DIR, DOMAINSDICT)
@click.option('--host', default=None)
@click.option('--port', default=None)
@ -34,8 +34,7 @@ def run(host, port):
sys.path.insert(0, BASE_DIR)
for domain in DOMAINS:
list_routes(domain)
list_api_routes()
uvicorn.run('halfapi.app:application',
host=host,

View File

@ -41,26 +41,27 @@ import sys
from configparser import ConfigParser
import importlib
from .lib.domain import d_domains
logger = logging.getLogger('halfapi')
PROJECT_NAME = ''
PROJECT_NAME = os.path.basename(os.getcwd())
DOMAINS = []
DOMAINSDICT = {}
DOMAINSDICT = lambda: {}
PRODUCTION = False
BASE_DIR = None
HOST = '127.0.0.1'
PORT = '3000'
CONF_DIR = environ.get('HALFAPI_CONF_DIR', '/etc/half_api')
SECRET = ''
IS_PROJECT = os.path.isfile('.halfapi/config')
default_config = {
'project': {
'host': '127.0.0.1',
'port': '8000',
'secret': '',
'base_dir': '',
'production': 'no'
}
}
@ -68,36 +69,62 @@ default_config = {
config = ConfigParser(allow_no_value=True)
config.read_dict(default_config)
if IS_PROJECT:
config.read(filenames=['.halfapi/config'])
CONF_DIR = environ.get('HALFAPI_CONF_DIR', '/etc/half_api')
HALFAPI_ETC_FILE=os.path.join(
CONF_DIR, 'default.ini'
)
PROJECT_NAME = config.get('project', 'name')
HALFAPI_DOT_FILE=os.path.join(
os.getcwd(), '.halfapi', 'config')
HALFAPI_CONFIG_FILES = [ HALFAPI_ETC_FILE, HALFAPI_DOT_FILE ]
def conf_files():
return [
os.path.join(
CONF_DIR, 'default.ini'
),
os.path.join(
os.getcwd(), '.halfapi', 'config')]
def write_config():
"""
Writes the current config to the highest priority config file
"""
with open(conf_files()[-1], 'w') as halfapi_config:
config.write(halfapi_config)
def config_dict():
"""
The config object as a dict
"""
return {
section: {
config.items(section)
}
for section in config.sections()
}
def read_config():
"""
The highest index in "filenames" are the highest priorty
"""
config.read(HALFAPI_CONFIG_FILES)
return config_dict()
if IS_PROJECT:
read_config()
PROJECT_NAME = config.get('project', 'name', fallback=PROJECT_NAME)
if len(PROJECT_NAME) == 0:
raise Exception('Need a project name as argument')
DOMAINS = [domain for domain, _ in config.items('domains')] \
if config.has_section('domains') \
else []
try:
DOMAINSDICT = {
dom: importlib.import_module(dom)
for dom in DOMAINS
}
except ImportError as exc:
logger.error('Could not load a domain : %s', exc)
HALFAPI_CONF_FILE=os.path.join(
CONF_DIR,
PROJECT_NAME
)
if not os.path.isfile(HALFAPI_CONF_FILE):
print(f'Missing {HALFAPI_CONF_FILE}, exiting')
sys.exit(1)
config.read(filenames=[HALFAPI_CONF_FILE])
DOMAINSDICT = lambda: d_domains(config)
HOST = config.get('project', 'host')
PORT = config.getint('project', 'port')
@ -113,4 +140,4 @@ if IS_PROJECT:
PRODUCTION = config.getboolean('project', 'production') or False
os.environ['HALFAPI_PROD'] = str(PRODUCTION)
BASE_DIR = config.get('project', 'base_dir')
BASE_DIR = config.get('project', 'base_dir', fallback='.') #os.getcwd())

View File

@ -146,7 +146,7 @@ def gen_router_routes(m_router: ModuleType, path: List[str]):
def gen_domain_routes(domain: str):
def gen_domain_routes(domain: str, m_dom: ModuleType):
"""
Generator that calls gen_router_routes for a domain
@ -160,3 +160,26 @@ def gen_domain_routes(domain: str):
except ImportError:
logger.warning('Domain **%s** has no **routers** module', domain)
logger.debug('%s', m_dom)
def d_domains(config):
"""
Parameters:
config (ConfigParser): The .halfapi/config based configparser object
Returns:
dict[str, ModuleType]
"""
if not config.has_section('domains'):
return {}
try:
return {
domain: importlib.import_module(domain)
for domain, _ in config.items('domains')
}
except ImportError as exc:
logger.error('Could not load a domain : %s', exc)
raise exc

View File

@ -0,0 +1,29 @@
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.types import Scope, Send, Receive
from .routes import api_routes
from .domain import d_domains
class DomainMiddleware(BaseHTTPMiddleware):
def __init__(self, app, config):
super().__init__(app)
self.config = config
self.domains = d_domains(config)
self.api = {}
self.acl = {}
for domain, m_domain in self.domains.items():
self.api[domain], self.acl[domain] = api_routes(m_domain)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
scope_ = scope.copy()
scope_['domains'] = self.domains
scope_['api'] = self.api
scope_['acl'] = self.acl
request = Request(scope_, receive)
response = await self.call_next(request)
await response(scope_, receive, send)
return response

View File

@ -114,7 +114,7 @@ class JWTAuthenticationBackend(AuthenticationBackend):
raise AuthenticationError(str(exc))
except Exception as exc:
logger.error('Authentication error : %s', exc)
raise e
raise exc
return AuthCredentials(["authenticated"]), JWTUser(

View File

@ -78,6 +78,6 @@ def parse_query(q: str = ""):
if 'offset' in params and int(params['offset']) > 0:
obj.offset(int(params['offset']))
return [elt for elt in obj.select(*fields)]
return list(obj.select(*fields))
return select

View File

@ -53,4 +53,4 @@ class ORJSONResponse(JSONResponse):
class HJSONResponse(ORJSONResponse):
def render(self, content: typ.Generator):
return super().render([ elt for elt in content ])
return super().render(list(content))

View File

@ -52,31 +52,31 @@ def route_acl_decorator(fct: Callable, params: List[Dict]):
return caller
def gen_starlette_routes(m_dom: ModuleType) -> Generator:
def gen_starlette_routes(d_domains: Dict[str, ModuleType]) -> Generator:
"""
Yields the Route objects for HalfAPI app
Parameters:
m_dom (ModuleType): the halfapi module
d_domains (dict[str, ModuleType])
Returns:
Generator(Route)
"""
for domain_name, m_domain in d_domains.items():
for path, d_route in gen_domain_routes(domain_name, m_domain):
for verb in VERBS:
if verb not in d_route.keys():
continue
for path, d_route in gen_domain_routes(m_dom.__name__):
for verb in VERBS:
if verb not in d_route.keys():
continue
yield (
Route(path,
route_acl_decorator(
d_route[verb]['fct'],
d_route[verb]['params']
),
methods=[verb])
)
yield (
Route(path,
route_acl_decorator(
d_route[verb]['fct'],
d_route[verb]['params']
),
methods=[verb])
)
def api_routes(m_dom: ModuleType) -> Generator:
@ -108,7 +108,7 @@ def api_routes(m_dom: ModuleType) -> Generator:
return l_params
d_res = {}
for path, d_route in gen_domain_routes(m_dom.__name__):
for path, d_route in gen_domain_routes(m_dom.__name__, m_dom):
d_res[path] = {'fqtn': d_route['fqtn'] }
for verb in VERBS:
@ -120,9 +120,8 @@ def api_routes(m_dom: ModuleType) -> Generator:
def api_acls(request):
from .. import app # FIXME: Find a way to get d_acl without having to import
res = {}
for domain, d_domain_acl in app.d_acl.items():
for domain, d_domain_acl in request.scope['acl'].items():
res[domain] = {}
for acl_name, fct in d_domain_acl.items():
fct_result = fct(request)

View File

@ -39,9 +39,8 @@ async def get_api_routes(request, *args, **kwargs):
description: Returns the current API routes description (HalfAPI 0.2.1)
as a JSON object
"""
from .. import app
return ORJSONResponse(app.d_api)
assert 'api' in request.scope
return ORJSONResponse(request.scope['api'])
async def schema_json(request, *args, **kwargs):
@ -55,7 +54,7 @@ async def schema_json(request, *args, **kwargs):
SCHEMAS.get_schema(routes=request.app.routes))
def schema_dict_dom(m_domain: ModuleType) -> Dict:
def schema_dict_dom(d_domains) -> Dict:
"""
Returns the API schema of the *m_domain* domain as a python dictionnary
@ -69,9 +68,8 @@ def schema_dict_dom(m_domain: ModuleType) -> Dict:
Dict: A dictionnary containing the description of the API using the
| OpenAPI standard
"""
routes = [
elt for elt in gen_starlette_routes(m_domain) ]
return SCHEMAS.get_schema(routes=routes)
return SCHEMAS.get_schema(
routes=list(gen_starlette_routes(d_domains)))
async def get_acls(request, *args, **kwargs):