diff --git a/halfapi/app.py b/halfapi/app.py index 35509bc..0088963 100644 --- a/halfapi/app.py +++ b/halfapi/app.py @@ -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)) ], diff --git a/halfapi/cli/cli.py b/halfapi/cli/cli.py index 1431f09..777d5b2 100644 --- a/halfapi/cli/cli.py +++ b/halfapi/cli/cli.py @@ -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 diff --git a/halfapi/cli/config.py b/halfapi/cli/config.py index adada7c..437aeeb 100644 --- a/halfapi/cli/config.py +++ b/halfapi/cli/config.py @@ -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()) diff --git a/halfapi/cli/domain.py b/halfapi/cli/domain.py index 5cf4cf2..fae9f66 100644 --- a/halfapi/cli/domain.py +++ b/halfapi/cli/domain.py @@ -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() diff --git a/halfapi/cli/run.py b/halfapi/cli/run.py index 718e77a..3a24150 100644 --- a/halfapi/cli/run.py +++ b/halfapi/cli/run.py @@ -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, diff --git a/halfapi/conf.py b/halfapi/conf.py index fac4bca..8c811f9 100644 --- a/halfapi/conf.py +++ b/halfapi/conf.py @@ -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()) diff --git a/halfapi/lib/domain.py b/halfapi/lib/domain.py index 4afa6af..dc7473c 100644 --- a/halfapi/lib/domain.py +++ b/halfapi/lib/domain.py @@ -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 diff --git a/halfapi/lib/domain_middleware.py b/halfapi/lib/domain_middleware.py new file mode 100644 index 0000000..6582c9a --- /dev/null +++ b/halfapi/lib/domain_middleware.py @@ -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 diff --git a/halfapi/lib/jwt_middleware.py b/halfapi/lib/jwt_middleware.py index 00129db..95176bc 100644 --- a/halfapi/lib/jwt_middleware.py +++ b/halfapi/lib/jwt_middleware.py @@ -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( diff --git a/halfapi/lib/query.py b/halfapi/lib/query.py index f2efcbc..eaa1d1d 100644 --- a/halfapi/lib/query.py +++ b/halfapi/lib/query.py @@ -10,7 +10,7 @@ def parse_query(q: str = ""): """ Returns the fitting Response object according to query parameters. - The parse_query function handles the following arguments in the query + The parse_query function handles the following arguments in the query string : format, limit, and offset It returns a callable function that returns the desired Response object. @@ -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 diff --git a/halfapi/lib/responses.py b/halfapi/lib/responses.py index f6610c0..5b533b3 100644 --- a/halfapi/lib/responses.py +++ b/halfapi/lib/responses.py @@ -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)) diff --git a/halfapi/lib/routes.py b/halfapi/lib/routes.py index 7f0fb32..04608a4 100644 --- a/halfapi/lib/routes.py +++ b/halfapi/lib/routes.py @@ -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) diff --git a/halfapi/lib/schemas.py b/halfapi/lib/schemas.py index 0fc03ae..95f9434 100644 --- a/halfapi/lib/schemas.py +++ b/halfapi/lib/schemas.py @@ -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):