[cli][domain] Re-implementation of list_routes, using .lib.schemas, add DOMAINSDICT constant in configuration, set a lot of default configurations

This commit is contained in:
Maxime Alves LIRMM@home 2020-09-24 19:48:42 +02:00
parent 2ad0b3a14b
commit 6a81c61649
7 changed files with 131 additions and 75 deletions

View File

@ -11,33 +11,38 @@ from starlette.middleware.authentication import AuthenticationMiddleware
from typing import Any, Awaitable, Callable, MutableMapping from typing import Any, Awaitable, Callable, MutableMapping
# module libraries # module libraries
from halfapi.conf import HOST, PORT, DB_NAME, SECRET, PRODUCTION, DOMAINS from halfapi.conf import HOST, PORT, DB_NAME, SECRET, PRODUCTION, DOMAINS, DOMAINSDICT
from halfapi.lib.jwt_middleware import JWTAuthenticationBackend from halfapi.lib.jwt_middleware import JWTAuthenticationBackend
from halfapi.lib.responses import * from halfapi.lib.responses import *
from halfapi.lib.routes import gen_starlette_routes from halfapi.lib.routes import gen_starlette_routes
from halfapi.lib.schemas import sch_json
from starlette.schemas import SchemaGenerator
schemas = SchemaGenerator(
{"openapi": "3.0.0", "info": {"title": "HalfAPI", "version": "1.0"}}
)
"""
Base routes definition
Only debug or doc routes, that should not be available in production
"""
routes = [ routes = [
Route('/', lambda request, *args, **kwargs: PlainTextResponse('It Works!')), Route('/', lambda request, *args, **kwargs: ORJSONResponse('It Works!')),
Route('/user', lambda request, *args, **kwargs: Route('/user', lambda request, *args, **kwargs:
JSONResponse({'user':request.user.json}) ORJSONResponse({'user':request.user.json})
if type(request.user) != UnauthenticatedUser if type(request.user) != UnauthenticatedUser
else JSONResponse({'user':False})), else ORJSONResponse({'user':False})),
Route('/payload', lambda request, *args, **kwargs: Route('/payload', lambda request, *args, **kwargs:
JSONResponse({'payload':str(request.payload)})), ORJSONResponse({'payload':str(request.payload)})),
Route('/schema', lambda request, *args, **kwargs:
schemas.OpenAPIResponse(request=request)) Route('/schema', schema_json)
] if not PRODUCTION else [] ] if not PRODUCTION else []
for route in gen_starlette_routes(): for domain, m_domain in DOMAINSDICT.items():
routes.append(route) for route in gen_starlette_routes(m_dom):
routes.append(route)
application = Starlette( application = Starlette(
debug=not PRODUCTION, debug=not PRODUCTION,

View File

@ -7,13 +7,15 @@ import importlib
from .cli import cli from .cli import cli
from halfapi.conf import DOMAINS, BASE_DIR from halfapi.conf import DOMAINS, DOMAINSDICT, BASE_DIR
from halfapi.db import (
Domain, from halfapi.lib.schemas import schema_dict_dom
APIRouter, # from halfapi.db import (
APIRoute, # Domain,
AclFunction, # APIRouter,
Acl) # APIRoute,
# AclFunction,
# Acl)
logger = logging.getLogger('halfapi') logger = logging.getLogger('halfapi')
@ -28,15 +30,18 @@ def create_domain():
############### ###############
def list_routes(domain): def list_routes(domain):
click.echo(f'\nDomain : {domain}') click.echo(f'\nDomain : {domain}')
routers = APIRouter(domain=domain)
for router in routers.select(): m_dom = DOMAINSDICT[domain]
routes = APIRoute(domain=domain, router=router['name']) click.echo(schema_dict_dom(m_dom))
click.echo('# /{name}'.format(**router))
for route in routes.select(): # for router in routers.select():
route.pop('fct_name') # routes = APIRoute(domain=domain, router=router['name'])
acls = ', '.join([ acl['acl_fct_name'] for acl in Acl(**route).select() ]) # click.echo('# /{name}'.format(**router))
route['acls'] = acls # for route in routes.select():
click.echo('- [{http_verb}] {path} ({acls})'.format(**route)) # route.pop('fct_name')
# acls = ', '.join([ acl['acl_fct_name'] for acl in Acl(**route).select() ])
# route['acls'] = acls
# click.echo('- [{http_verb}] {path} ({acls})'.format(**route))
################# #################
# domain update # # domain update #
@ -244,10 +249,10 @@ def domain(domains, delete, update, create, read): #, domains, read, create, up
update (boolean): If set, update the database for the selected domains update (boolean): If set, update the database for the selected domains
""" """
raise NotImplementedError
if not domains: if not domains:
if create: if create:
raise NotImplementedError
return create_domain() return create_domain()
domains = DOMAINS domains = DOMAINS
@ -265,8 +270,10 @@ def domain(domains, delete, update, create, read): #, domains, read, create, up
for domain in domains: for domain in domains:
if update: if update:
raise NotImplementedError
update_db(domain) update_db(domain)
if delete: if delete:
raise NotImplementedError
delete_domain(domain) delete_domain(domain)
else: else:
list_routes(domain) list_routes(domain)

View File

@ -7,7 +7,6 @@ import click
import logging import logging
from halfapi import __version__ from halfapi import __version__
from halfapi.cli.lib.db import ProjectDB
from .cli import cli from .cli import cli
logger = logging.getLogger('halfapi') logger = logging.getLogger('halfapi')
@ -33,8 +32,9 @@ halfapi_version = {halfapi_version}
""" """
@click.argument('project') @click.argument('project')
@click.option('--venv', default=None)
@cli.command() @cli.command()
def init(project): def init(project, venv):
if not re.match('^[a-z0-9_]+$', project, re.I): if not re.match('^[a-z0-9_]+$', project, re.I):
click.echo('Project name must match "^[a-z0-9_]+$", retry.', err=True) click.echo('Project name must match "^[a-z0-9_]+$", retry.', err=True)
sys.exit(1) sys.exit(1)
@ -46,26 +46,3 @@ def init(project):
click.echo(f'create directory {project}') click.echo(f'create directory {project}')
os.mkdir(project) os.mkdir(project)
try:
pdb = ProjectDB(project)
pdb.init()
except Exception as e:
logger.warning(e)
logger.debug(os.environ.get('HALFORM_CONF_DIR'))
raise e
os.mkdir(os.path.join(project, '.halfapi'))
open(os.path.join(project, '.halfapi', 'domains'), 'w').write('[domains]\n')
config_file = os.path.join(project, '.halfapi', 'config')
with open(config_file, 'w') as f:
f.write(TMPL_HALFAPI_CONFIG.format(
name=project,
halfapi_version=__version__
))
click.echo(f'Insert this into the HALFAPI_CONF_DIR/{project} file')
click.echo(format_halfapi_etc(
project,
os.path.abspath(project)))

View File

@ -4,6 +4,15 @@ from os import environ
import sys import sys
from configparser import ConfigParser from configparser import ConfigParser
PROJECT_NAME = ''
DOMAINS = []
DOMAINSDICT = {}
PRODUCTION = False
BASE_DIR = None
HOST='127.0.0.1'
PORT='3000'
DB_NAME = None
IS_PROJECT = os.path.isfile('.halfapi/config') IS_PROJECT = os.path.isfile('.halfapi/config')
if IS_PROJECT: if IS_PROJECT:
@ -31,6 +40,14 @@ if IS_PROJECT:
if config.has_section('domains') \ if config.has_section('domains') \
else [] else []
try:
DOMAINSDICT = {
dom, importlib.import_module(dom)
for dom in DOMAINS
}
except ImportError as e:
logger.error('Could not load a domain', e)
CONF_DIR = environ.get('HALFAPI_CONF_DIR', '/etc/half_api') CONF_DIR = environ.get('HALFAPI_CONF_DIR', '/etc/half_api')
HALFAPI_CONF_FILE=os.path.join( HALFAPI_CONF_FILE=os.path.join(
@ -51,9 +68,9 @@ if IS_PROJECT:
SECRET = secret_file.read() SECRET = secret_file.read()
# 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: except FileNotFoundError as e:
print('There is no file like {}'.format(config.get('project', 'secret'))) logger.error('There is no file like {}'.format(config.get('project', 'secret')))
sys.exit(1) logger.debug(e)
PRODUCTION = config.getboolean('project', 'production') or False PRODUCTION = config.getboolean('project', 'production') or False
os.environ['HALFAPI_PROD'] = str(PRODUCTION) os.environ['HALFAPI_PROD'] = str(PRODUCTION)

View File

@ -22,15 +22,33 @@ from starlette.requests import Request
class DomainNotFoundError(Exception): class DomainNotFoundError(Exception):
pass pass
def route_decorator(fct: Callable, acls_mod, params: List[Dict]): def route_acl_decorator(fct: Callable, acls_mod, params: List[Dict]):
"""
Decorator for async functions that calls pre-conditions functions
and appends kwargs to the target function
Parameters:
fct (Callable):
The function to decorate
acls_mod (Module):
The module that contains the pre-condition functions (acls)
params List[Dict]:
A list of dicts that have an "acl" key that points to a function
Returns:
async function
"""
@wraps(fct) @wraps(fct)
async def caller(req: Request, *args, **kwargs): async def caller(req: Request, *args, **kwargs):
for param in params: for param in params:
if param['acl'](req, *args, **kwargs): if param['acl'](req, *args, **kwargs):
""" """
We the 'acl' and 'keys' kwargs values to let the We merge the 'acl' and 'keys' kwargs values to let the
decorated function know which ACL function answered decorated function know which ACL function answered
True, and which keys the request will return True, and other parameters that you'd need
""" """
return await fct( return await fct(
req, *args, req, *args,
@ -43,17 +61,33 @@ def route_decorator(fct: Callable, acls_mod, params: List[Dict]):
return caller return caller
def gen_starlette_routes(): ###
for domain in DOMAINS: # testing purpose only
domain_acl_mod = importlib.import_module( def acl_mock(fct):
f'{domain}.acl') return lambda r, *a, **kw: True
#
##
for route in gen_domain_routes(domain): def gen_starlette_routes(m_dom):
yield ( """
Route(route['path'], Yields the Route objects for HalfAPI app
route_decorator(
Parameters:
m_dom (module): the halfapi module
Returns:
Generator[Route]
"""
m_dom_acl = importlib.import_module(m_dom '.acl')
for route in gen_domain_routes(m_dom):
yield (
Route(route['path'],
route_acl_decorator(
route['fct'], route['fct'],
domain_acl_mod, m_dom_acl,
route['params'], route['params'],
), methods=[route['verb']]) ),
) methods=[route['verb']])
)

16
halfapi/lib/schemas.py Normal file
View File

@ -0,0 +1,16 @@
from .routes import gen_starlette_routes
from .responses import *
from starlette.schemas import SchemaGenerator
schemas = SchemaGenerator(
{"openapi": "3.0.0", "info": {"title": "HalfAPI", "version": "1.0"}}
)
async def schema_json(request, *args, **kwargs):
return ORJSONResponse(
schemas.get_schema(routes=request.app.routes))
def schema_dict_dom(m_domain):
return schemas.get_schema(routes=[
elt for elt in gen_starlette_routes(m_domain) ])