diff --git a/halfapi/app.py b/halfapi/app.py index c457504..e5f36f4 100644 --- a/halfapi/app.py +++ b/halfapi/app.py @@ -11,33 +11,38 @@ from starlette.middleware.authentication import AuthenticationMiddleware from typing import Any, Awaitable, Callable, MutableMapping # 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.responses import * from halfapi.lib.routes import gen_starlette_routes - -from starlette.schemas import SchemaGenerator -schemas = SchemaGenerator( - {"openapi": "3.0.0", "info": {"title": "HalfAPI", "version": "1.0"}} -) +from halfapi.lib.schemas import sch_json +""" +Base routes definition + +Only debug or doc routes, that should not be available in production +""" routes = [ - Route('/', lambda request, *args, **kwargs: PlainTextResponse('It Works!')), + Route('/', lambda request, *args, **kwargs: ORJSONResponse('It Works!')), + Route('/user', lambda request, *args, **kwargs: - JSONResponse({'user':request.user.json}) + ORJSONResponse({'user':request.user.json}) if type(request.user) != UnauthenticatedUser - else JSONResponse({'user':False})), + else ORJSONResponse({'user':False})), + Route('/payload', lambda request, *args, **kwargs: - JSONResponse({'payload':str(request.payload)})), - Route('/schema', lambda request, *args, **kwargs: - schemas.OpenAPIResponse(request=request)) + ORJSONResponse({'payload':str(request.payload)})), + + Route('/schema', schema_json) ] if not PRODUCTION else [] -for route in gen_starlette_routes(): - routes.append(route) +for domain, m_domain in DOMAINSDICT.items(): + for route in gen_starlette_routes(m_dom): + routes.append(route) + application = Starlette( debug=not PRODUCTION, diff --git a/halfapi/cli/domain.py b/halfapi/cli/domain.py index ed2b5f7..4391f16 100644 --- a/halfapi/cli/domain.py +++ b/halfapi/cli/domain.py @@ -7,13 +7,15 @@ import importlib from .cli import cli -from halfapi.conf import DOMAINS, BASE_DIR -from halfapi.db import ( - Domain, - APIRouter, - APIRoute, - AclFunction, - Acl) +from halfapi.conf import DOMAINS, DOMAINSDICT, BASE_DIR + +from halfapi.lib.schemas import schema_dict_dom +# from halfapi.db import ( +# Domain, +# APIRouter, +# APIRoute, +# AclFunction, +# Acl) logger = logging.getLogger('halfapi') @@ -28,15 +30,18 @@ def create_domain(): ############### def list_routes(domain): click.echo(f'\nDomain : {domain}') - routers = APIRouter(domain=domain) - for router in routers.select(): - routes = APIRoute(domain=domain, router=router['name']) - click.echo('# /{name}'.format(**router)) - for route in routes.select(): - 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)) + + m_dom = DOMAINSDICT[domain] + click.echo(schema_dict_dom(m_dom)) + + # for router in routers.select(): + # routes = APIRoute(domain=domain, router=router['name']) + # click.echo('# /{name}'.format(**router)) + # for route in routes.select(): + # 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 # @@ -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 """ - raise NotImplementedError if not domains: if create: + raise NotImplementedError return create_domain() domains = DOMAINS @@ -265,8 +270,10 @@ def domain(domains, delete, update, create, read): #, domains, read, create, up for domain in domains: if update: + raise NotImplementedError update_db(domain) if delete: + raise NotImplementedError delete_domain(domain) else: list_routes(domain) diff --git a/halfapi/cli/init.py b/halfapi/cli/init.py index 48f47b0..b1092c0 100644 --- a/halfapi/cli/init.py +++ b/halfapi/cli/init.py @@ -7,7 +7,6 @@ import click import logging from halfapi import __version__ -from halfapi.cli.lib.db import ProjectDB from .cli import cli logger = logging.getLogger('halfapi') @@ -33,8 +32,9 @@ halfapi_version = {halfapi_version} """ @click.argument('project') +@click.option('--venv', default=None) @cli.command() -def init(project): +def init(project, venv): if not re.match('^[a-z0-9_]+$', project, re.I): click.echo('Project name must match "^[a-z0-9_]+$", retry.', err=True) sys.exit(1) @@ -46,26 +46,3 @@ def init(project): click.echo(f'create directory {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))) - diff --git a/halfapi/conf.py b/halfapi/conf.py index 4623536..03a0e52 100644 --- a/halfapi/conf.py +++ b/halfapi/conf.py @@ -4,6 +4,15 @@ from os import environ import sys 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') if IS_PROJECT: @@ -31,6 +40,14 @@ if IS_PROJECT: if config.has_section('domains') \ 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') HALFAPI_CONF_FILE=os.path.join( @@ -51,9 +68,9 @@ if IS_PROJECT: SECRET = secret_file.read() # Set the secret so we can use it in domains os.environ['HALFAPI_SECRET'] = SECRET - except FileNotFoundError: - print('There is no file like {}'.format(config.get('project', 'secret'))) - sys.exit(1) + except FileNotFoundError as e: + logger.error('There is no file like {}'.format(config.get('project', 'secret'))) + logger.debug(e) PRODUCTION = config.getboolean('project', 'production') or False os.environ['HALFAPI_PROD'] = str(PRODUCTION) diff --git a/halfapi/lib/domain.py b/halfapi/lib/domain.py index 03a5442..18b416c 100644 --- a/halfapi/lib/domain.py +++ b/halfapi/lib/domain.py @@ -87,7 +87,7 @@ def gen_router_routes(m_router, path=[]): for route in gen_routes(route_params, path, m_router): yield route - + subroutes = route_params.get('SUBROUTES', []) for subroute in subroutes: path.append(subroute) diff --git a/halfapi/lib/routes.py b/halfapi/lib/routes.py index 7354f8c..e1837d0 100644 --- a/halfapi/lib/routes.py +++ b/halfapi/lib/routes.py @@ -22,15 +22,33 @@ from starlette.requests import Request class DomainNotFoundError(Exception): 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) async def caller(req: Request, *args, **kwargs): for param in params: if param['acl'](req, *args, **kwargs): """ - We the 'acl' and 'keys' kwargs values to let the - decorated function know which ACL function answered - True, and which keys the request will return + We merge the 'acl' and 'keys' kwargs values to let the + decorated function know which ACL function answered + True, and other parameters that you'd need """ return await fct( req, *args, @@ -43,17 +61,33 @@ def route_decorator(fct: Callable, acls_mod, params: List[Dict]): return caller -def gen_starlette_routes(): - for domain in DOMAINS: - domain_acl_mod = importlib.import_module( - f'{domain}.acl') +### +# testing purpose only +def acl_mock(fct): + return lambda r, *a, **kw: True +# +## - for route in gen_domain_routes(domain): - yield ( - Route(route['path'], - route_decorator( +def gen_starlette_routes(m_dom): + """ + Yields the Route objects for HalfAPI app + + 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'], - domain_acl_mod, + m_dom_acl, route['params'], - ), methods=[route['verb']]) - ) + ), + methods=[route['verb']]) + ) diff --git a/halfapi/lib/schemas.py b/halfapi/lib/schemas.py new file mode 100644 index 0000000..b2a4dc0 --- /dev/null +++ b/halfapi/lib/schemas.py @@ -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) ])