[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:
parent
781736c151
commit
f5ebabbcd4
@ -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))
|
||||
],
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
29
halfapi/lib/domain_middleware.py
Normal file
29
halfapi/lib/domain_middleware.py
Normal 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
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user