[wip][testfail] update config monodomain - schema gives acls
This commit is contained in:
parent
ec26438340
commit
7e1cc21b8c
|
@ -59,6 +59,12 @@ that is available in the python path.
|
||||||
|
|
||||||
Run the project by using the `halfapi run` command.
|
Run the project by using the `halfapi run` command.
|
||||||
|
|
||||||
|
You can try the dummy_domain with the following command.
|
||||||
|
|
||||||
|
```
|
||||||
|
python -m halfapi routes --export --noheader dummy_domain.routers | python -m halfapi run -
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## API Testing
|
## API Testing
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
__version__ = '0.5.13'
|
__version__ = '0.6.0'
|
||||||
|
|
||||||
def version():
|
def version():
|
||||||
return f'HalfAPI version:{__version__}'
|
return f'HalfAPI version:{__version__}'
|
||||||
|
|
|
@ -5,4 +5,5 @@ from .logging import logger
|
||||||
logger.info('CONFIG: %s', CONFIG)
|
logger.info('CONFIG: %s', CONFIG)
|
||||||
logger.info('SCHEMA: %s', SCHEMA)
|
logger.info('SCHEMA: %s', SCHEMA)
|
||||||
|
|
||||||
application = HalfAPI(CONFIG, SCHEMA or None).application
|
application = HalfAPI(
|
||||||
|
CONFIG, SCHEMA or None).application
|
||||||
|
|
|
@ -9,10 +9,6 @@ import click
|
||||||
from .cli import cli
|
from .cli import cli
|
||||||
from ..conf import CONFIG
|
from ..conf import CONFIG
|
||||||
|
|
||||||
DOMAINS_STR='\n'.join(
|
|
||||||
[ ' = '.join((key, val.__name__)) for key, val in CONFIG['domains'].items() ]
|
|
||||||
)
|
|
||||||
|
|
||||||
CONF_STR="""
|
CONF_STR="""
|
||||||
[project]
|
[project]
|
||||||
name = {project_name}
|
name = {project_name}
|
||||||
|
@ -20,8 +16,11 @@ host = {host}
|
||||||
port = {port}
|
port = {port}
|
||||||
production = {production}
|
production = {production}
|
||||||
|
|
||||||
[domains]
|
[domain]
|
||||||
""".format(**CONFIG) + DOMAINS_STR
|
name = {domain_name}
|
||||||
|
router = {router}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
def config():
|
def config():
|
||||||
|
|
|
@ -28,10 +28,11 @@ def create_domain(domain_name: str, module_path: str):
|
||||||
# logger.warning('Domain **%s** is already in project', domain_name)
|
# logger.warning('Domain **%s** is already in project', domain_name)
|
||||||
# sys.exit(1)
|
# sys.exit(1)
|
||||||
|
|
||||||
if not config.has_section('domains'):
|
if not config.has_section('domain'):
|
||||||
config.add_section('domains')
|
config.add_section('domain')
|
||||||
|
|
||||||
config.set('domains', domain_name, module_path)
|
config.set('domain', 'name', domain_name)
|
||||||
|
config.set('domain', 'router', module_path)
|
||||||
write_config()
|
write_config()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ TMPL_HALFAPI_CONFIG = """[project]
|
||||||
name = {name}
|
name = {name}
|
||||||
halfapi_version = {halfapi_version}
|
halfapi_version = {halfapi_version}
|
||||||
|
|
||||||
[domains]
|
[domain]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@click.argument('project')
|
@click.argument('project')
|
||||||
|
|
|
@ -13,18 +13,19 @@ from .cli import cli
|
||||||
|
|
||||||
from ..logging import logger
|
from ..logging import logger
|
||||||
|
|
||||||
from ..lib.domain import gen_router_routes
|
from ..lib.domain import domain_schema_dict
|
||||||
from ..lib.constants import DOMAIN_SCHEMA
|
from ..lib.constants import DOMAIN_SCHEMA
|
||||||
from ..lib.routes import api_routes
|
# from ..lib.routes import api_routes
|
||||||
from ..lib.schemas import schema_to_csv
|
from ..lib.schemas import schema_to_csv # get_api_routes
|
||||||
|
|
||||||
@click.argument('module', required=True)
|
@click.argument('module', required=True)
|
||||||
@click.option('--export', default=False, is_flag=True)
|
@click.option('--export', default=False, is_flag=True)
|
||||||
@click.option('--validate', default=False, is_flag=True)
|
@click.option('--validate', default=False, is_flag=True)
|
||||||
@click.option('--check', default=False, is_flag=True)
|
@click.option('--check', default=False, is_flag=True)
|
||||||
@click.option('--noheader', default=False, is_flag=True)
|
@click.option('--noheader', default=False, is_flag=True)
|
||||||
|
@click.option('--schema', default=False, is_flag=True)
|
||||||
@cli.command()
|
@cli.command()
|
||||||
def routes(module, export=False, validate=False, check=False, noheader=False):
|
def routes(module, export=False, validate=False, check=False, noheader=False, schema=False):
|
||||||
"""
|
"""
|
||||||
The "halfapi routes" command
|
The "halfapi routes" command
|
||||||
"""
|
"""
|
||||||
|
@ -37,9 +38,14 @@ def routes(module, export=False, validate=False, check=False, noheader=False):
|
||||||
click.echo(schema_to_csv(module, header=not noheader))
|
click.echo(schema_to_csv(module, header=not noheader))
|
||||||
|
|
||||||
if validate:
|
if validate:
|
||||||
routes_d = api_routes(mod)
|
routes_d = domain_schema_dict(mod)
|
||||||
try:
|
try:
|
||||||
DOMAIN_SCHEMA.validate(routes_d[0])
|
DOMAIN_SCHEMA.validate(routes_d[0])
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
pprint(routes_d[0])
|
pprint(routes_d[0])
|
||||||
raise exc from exc
|
raise exc from exc
|
||||||
|
|
||||||
|
"""
|
||||||
|
if schema:
|
||||||
|
click.echo(get_api_routes(uu
|
||||||
|
"""
|
||||||
|
|
|
@ -10,7 +10,7 @@ import uvicorn
|
||||||
from .cli import cli
|
from .cli import cli
|
||||||
from .domain import list_api_routes
|
from .domain import list_api_routes
|
||||||
from ..conf import (PROJECT_NAME, HOST, PORT, SCHEMA,
|
from ..conf import (PROJECT_NAME, HOST, PORT, SCHEMA,
|
||||||
PRODUCTION, LOGLEVEL, DOMAINSDICT, CONFIG)
|
PRODUCTION, LOGLEVEL, DOMAINSDICT, CONFIG, DOMAIN, ROUTER)
|
||||||
from ..logging import logger
|
from ..logging import logger
|
||||||
from ..lib.schemas import schema_csv_dict
|
from ..lib.schemas import schema_csv_dict
|
||||||
|
|
||||||
|
@ -20,11 +20,13 @@ from ..lib.schemas import schema_csv_dict
|
||||||
@click.option('--secret', default=False)
|
@click.option('--secret', default=False)
|
||||||
@click.option('--production', default=True)
|
@click.option('--production', default=True)
|
||||||
@click.option('--loglevel', default=LOGLEVEL)
|
@click.option('--loglevel', default=LOGLEVEL)
|
||||||
@click.option('--prefix', default='')
|
@click.option('--prefix', default='/')
|
||||||
@click.option('--check', default=True)
|
@click.option('--check', default=True)
|
||||||
@click.argument('schema', type=click.File('r'), required=False)
|
@click.argument('schema', type=click.File('r'), required=False)
|
||||||
|
@click.argument('router', required=False)
|
||||||
|
@click.argument('domain', required=False)
|
||||||
@cli.command()
|
@cli.command()
|
||||||
def run(host, port, reload, secret, production, loglevel, prefix, check, schema):
|
def run(host, port, reload, secret, production, loglevel, prefix, check, schema, router, domain):
|
||||||
"""
|
"""
|
||||||
The "halfapi run" command
|
The "halfapi run" command
|
||||||
"""
|
"""
|
||||||
|
@ -50,9 +52,12 @@ def run(host, port, reload, secret, production, loglevel, prefix, check, schema)
|
||||||
|
|
||||||
sys.path.insert(0, os.getcwd())
|
sys.path.insert(0, os.getcwd())
|
||||||
|
|
||||||
|
CONFIG.get('domain')['name'] = domain
|
||||||
|
CONFIG.get('domain')['router'] = router
|
||||||
|
|
||||||
if schema:
|
if schema:
|
||||||
# Populate the SCHEMA global with the data from the given file
|
# Populate the SCHEMA global with the data from the given file
|
||||||
for key, val in schema_csv_dict(schema).items():
|
for key, val in schema_csv_dict(schema, prefix).items():
|
||||||
SCHEMA[key] = val
|
SCHEMA[key] = val
|
||||||
|
|
||||||
# list_api_routes()
|
# list_api_routes()
|
||||||
|
|
|
@ -53,6 +53,9 @@ HOST = '127.0.0.1'
|
||||||
PORT = '3000'
|
PORT = '3000'
|
||||||
SECRET = ''
|
SECRET = ''
|
||||||
CONF_FILE = os.environ.get('HALFAPI_CONF_FILE', '.halfapi/config')
|
CONF_FILE = os.environ.get('HALFAPI_CONF_FILE', '.halfapi/config')
|
||||||
|
|
||||||
|
DOMAIN = None
|
||||||
|
ROUTER = None
|
||||||
SCHEMA = {}
|
SCHEMA = {}
|
||||||
|
|
||||||
config = ConfigParser(allow_no_value=True)
|
config = ConfigParser(allow_no_value=True)
|
||||||
|
@ -140,7 +143,10 @@ CONFIG = {
|
||||||
'secret': SECRET,
|
'secret': SECRET,
|
||||||
'host': HOST,
|
'host': HOST,
|
||||||
'port': PORT,
|
'port': PORT,
|
||||||
'domains': DOMAINS,
|
'domain': {
|
||||||
|
'name': None,
|
||||||
|
'router': None
|
||||||
|
},
|
||||||
'domain_config': {}
|
'domain_config': {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,61 +29,61 @@ from timing_asgi.integrations import StarletteScopeToName
|
||||||
|
|
||||||
# module libraries
|
# module libraries
|
||||||
|
|
||||||
|
from .lib.constants import API_SCHEMA_DICT
|
||||||
from .lib.domain_middleware import DomainMiddleware
|
from .lib.domain_middleware import DomainMiddleware
|
||||||
from .lib.timing import HTimingClient
|
from .lib.timing import HTimingClient
|
||||||
from .lib.domain import NoDomainsException
|
from .lib.domain import NoDomainsException
|
||||||
|
from .lib.jwt_middleware import JWTAuthenticationBackend
|
||||||
from halfapi.lib.jwt_middleware import JWTAuthenticationBackend
|
from .lib.responses import (ORJSONResponse, UnauthorizedResponse,
|
||||||
|
|
||||||
from halfapi.lib.responses import (ORJSONResponse, UnauthorizedResponse,
|
|
||||||
NotFoundResponse, InternalServerErrorResponse, NotImplementedResponse,
|
NotFoundResponse, InternalServerErrorResponse, NotImplementedResponse,
|
||||||
ServiceUnavailableResponse)
|
ServiceUnavailableResponse)
|
||||||
|
from .lib.domain import domain_schema_dict
|
||||||
from halfapi.lib.routes import gen_domain_routes, gen_schema_routes, JSONRoute
|
from .lib.routes import gen_domain_routes, gen_schema_routes, JSONRoute
|
||||||
from halfapi.lib.schemas import schema_json, get_acls
|
from .lib.schemas import schema_json, get_acls
|
||||||
from halfapi.logging import logger, config_logging
|
from .logging import logger, config_logging
|
||||||
from halfapi import __version__
|
from halfapi import __version__
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HalfAPI:
|
class HalfAPI:
|
||||||
def __init__(self, config, routes_dict=None):
|
def __init__(self, config,
|
||||||
|
routes_dict=None):
|
||||||
config_logging(logging.DEBUG)
|
config_logging(logging.DEBUG)
|
||||||
|
|
||||||
SECRET = config.get('secret')
|
SECRET = config.get('secret')
|
||||||
PRODUCTION = config.get('production', True)
|
PRODUCTION = config.get('production', True)
|
||||||
DOMAINS = config.get('domains', {})
|
CONFIG = config.get('config', {})
|
||||||
CONFIG = config.get('config', {
|
|
||||||
'domains': DOMAINS
|
|
||||||
})
|
|
||||||
|
|
||||||
if not (DOMAINS or routes_dict):
|
domain = config.get('domain')['name']
|
||||||
|
router = config.get('domain')['router']
|
||||||
|
|
||||||
|
if not (domain and router):
|
||||||
raise NoDomainsException()
|
raise NoDomainsException()
|
||||||
|
|
||||||
self.PRODUCTION = PRODUCTION
|
self.PRODUCTION = PRODUCTION
|
||||||
self.CONFIG = CONFIG
|
self.CONFIG = CONFIG
|
||||||
self.DOMAINS = DOMAINS
|
|
||||||
self.SECRET = SECRET
|
self.SECRET = SECRET
|
||||||
|
|
||||||
self.__application = None
|
self.__application = None
|
||||||
|
|
||||||
""" The base route contains the route schema
|
if domain and router:
|
||||||
"""
|
m_domain = importlib.import_module(f'{domain}')
|
||||||
if routes_dict:
|
m_domain_router = importlib.import_module(f'{domain}.{router}')
|
||||||
any_route = routes_dict[
|
m_domain_acl = importlib.import_module(f'{domain}.acl')
|
||||||
list(routes_dict.keys())[0]
|
|
||||||
]
|
|
||||||
domain, router = any_route[
|
|
||||||
list(any_route.keys())[0]
|
|
||||||
]['module'].__name__.split('.')[0:2]
|
|
||||||
|
|
||||||
DOMAINS = {}
|
self.schema = { **API_SCHEMA_DICT }
|
||||||
DOMAINS[domain] = importlib.import_module(f'{domain}.{router}')
|
|
||||||
|
|
||||||
if DOMAINS:
|
self.schema['domain'] = {
|
||||||
self.api_routes = {}
|
'name': domain,
|
||||||
|
'version': getattr(m_domain, '__version__', ''),
|
||||||
|
'patch_release': getattr(m_domain, '__path_release__', ''),
|
||||||
|
'acls': tuple(getattr(m_domain_acl, 'ACLS', ()))
|
||||||
|
}
|
||||||
|
|
||||||
routes = [ Route('/', JSONRoute(self.api_routes)) ]
|
self.schema['paths'] = domain_schema_dict(m_domain_router)
|
||||||
|
|
||||||
|
|
||||||
|
routes = [ Route('/', JSONRoute(self.schema)) ]
|
||||||
|
|
||||||
""" HalfAPI routes (if not PRODUCTION, includes debug routes)
|
""" HalfAPI routes (if not PRODUCTION, includes debug routes)
|
||||||
"""
|
"""
|
||||||
|
@ -96,20 +96,9 @@ class HalfAPI:
|
||||||
logger.info('Domain-less mode : the given schema defines the activated routes')
|
logger.info('Domain-less mode : the given schema defines the activated routes')
|
||||||
for route in gen_schema_routes(routes_dict):
|
for route in gen_schema_routes(routes_dict):
|
||||||
routes.append(route)
|
routes.append(route)
|
||||||
elif DOMAINS:
|
else:
|
||||||
# Mount the domain routes
|
for route in gen_domain_routes(m_domain_router):
|
||||||
logger.info('Domains mode : the list of domains is retrieves from the configuration file')
|
routes.append(route)
|
||||||
for domain, m_domain in DOMAINS.items():
|
|
||||||
if domain not in self.api_routes.keys():
|
|
||||||
raise Exception(f'The domain does not have a schema: {domain}')
|
|
||||||
routes.append(
|
|
||||||
Route(f'/{domain}', JSONRoute(self.api_routes[domain]))
|
|
||||||
)
|
|
||||||
routes.append(
|
|
||||||
Mount(f'/{domain}', routes=gen_domain_routes(m_domain))
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
self.__application = Starlette(
|
self.__application = Starlette(
|
||||||
debug=not PRODUCTION,
|
debug=not PRODUCTION,
|
||||||
|
@ -125,6 +114,7 @@ class HalfAPI:
|
||||||
|
|
||||||
self.__application.add_middleware(
|
self.__application.add_middleware(
|
||||||
DomainMiddleware,
|
DomainMiddleware,
|
||||||
|
domain=domain,
|
||||||
config=CONFIG
|
config=CONFIG
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -192,3 +182,7 @@ class HalfAPI:
|
||||||
raise Exception('Test exception')
|
raise Exception('Test exception')
|
||||||
|
|
||||||
yield Route('/exception', exception)
|
yield Route('/exception', exception)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def api_schema(domain):
|
||||||
|
pass
|
||||||
|
|
|
@ -1,27 +1,50 @@
|
||||||
|
import re
|
||||||
from schema import Schema, Optional
|
from schema import Schema, Optional
|
||||||
|
from .. import __version__
|
||||||
|
|
||||||
VERBS = ('GET', 'POST', 'PUT', 'PATCH', 'DELETE')
|
VERBS = ('GET', 'POST', 'PUT', 'PATCH', 'DELETE')
|
||||||
|
|
||||||
ACLS_SCHEMA = Schema([{
|
ACLS_SCHEMA = Schema([{
|
||||||
'acl': lambda fct: isinstance(fct(), bool),
|
'acl': str,
|
||||||
Optional('args'): {
|
Optional('args'): {
|
||||||
Optional('required'): { str },
|
Optional('required'): [ str ],
|
||||||
Optional('optional'): { str }
|
Optional('optional'): [ str ]
|
||||||
},
|
},
|
||||||
Optional('out'): { str }
|
Optional('out'): [ str ]
|
||||||
}])
|
}])
|
||||||
|
|
||||||
|
is_callable_dotted_notation = lambda x: re.match(
|
||||||
|
r'^(([a-zA-Z_])+\.?)*:[a-zA-Z_]+$', 'ab_c.TEST:get')
|
||||||
|
|
||||||
ROUTE_SCHEMA = Schema({
|
ROUTE_SCHEMA = Schema({
|
||||||
str: {
|
str: { # path
|
||||||
|
str: { # method
|
||||||
|
'callable': is_callable_dotted_notation,
|
||||||
'docs': lambda n: True, # Should validate an openAPI spec
|
'docs': lambda n: True, # Should validate an openAPI spec
|
||||||
'acls': ACLS_SCHEMA
|
'acls': ACLS_SCHEMA
|
||||||
}
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
DOMAIN_SCHEMA = Schema({
|
DOMAIN_SCHEMA = Schema({
|
||||||
str: ROUTE_SCHEMA
|
'name': str,
|
||||||
|
Optional('version'): str,
|
||||||
|
Optional('patch_release'): str,
|
||||||
|
Optional('acls'): [
|
||||||
|
[str, str, int]
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
API_SCHEMA_DICT = {
|
||||||
|
'openapi': '3.0.0',
|
||||||
|
'info': {
|
||||||
|
'title': 'HalfAPI',
|
||||||
|
'version': __version__
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
API_SCHEMA = Schema({
|
API_SCHEMA = Schema({
|
||||||
str: DOMAIN_SCHEMA # key: domain name, value: result of lib.routes.api_routes(domain_module)
|
**API_SCHEMA_DICT,
|
||||||
|
'domain': DOMAIN_SCHEMA,
|
||||||
|
'paths': ROUTE_SCHEMA
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,6 +12,7 @@ from functools import wraps
|
||||||
from types import ModuleType, FunctionType
|
from types import ModuleType, FunctionType
|
||||||
from typing import Coroutine, Generator
|
from typing import Coroutine, Generator
|
||||||
from typing import Dict, List, Tuple, Iterator
|
from typing import Dict, List, Tuple, Iterator
|
||||||
|
import yaml
|
||||||
|
|
||||||
from starlette.exceptions import HTTPException
|
from starlette.exceptions import HTTPException
|
||||||
|
|
||||||
|
@ -229,6 +230,60 @@ def gen_router_routes(m_router: ModuleType, path: List[str]) -> \
|
||||||
path.pop()
|
path.pop()
|
||||||
|
|
||||||
|
|
||||||
|
def domain_schema_dict(m_router: ModuleType) -> Dict:
|
||||||
|
""" gen_router_routes return values as a dict
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
m_router (ModuleType): The domain routers' module
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
Dict: Schema of dict is halfapi.lib.constants.DOMAIN_SCHEMA
|
||||||
|
"""
|
||||||
|
d_res = {}
|
||||||
|
|
||||||
|
for path, verb, m_router, fct, parameters in gen_router_routes(m_router, []):
|
||||||
|
if path not in d_res:
|
||||||
|
d_res[path] = {}
|
||||||
|
|
||||||
|
if verb not in d_res[path]:
|
||||||
|
d_res[path][verb] = {}
|
||||||
|
|
||||||
|
d_res[path][verb]['callable'] = f'{m_router.__name__}:{fct.__name__}'
|
||||||
|
d_res[path][verb]['docs'] = yaml.safe_load(fct.__doc__)
|
||||||
|
d_res[path][verb]['acls'] = list(map(lambda elt: { **elt, 'acl': elt['acl'].__name__ },
|
||||||
|
parameters))
|
||||||
|
|
||||||
|
return d_res
|
||||||
|
|
||||||
|
def domain_schema_list(m_router: ModuleType) -> List:
|
||||||
|
""" Schema as list, one row by route/acl
|
||||||
|
Parameters:
|
||||||
|
|
||||||
|
m_router (ModuleType): The domain routers' module
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
List[Tuple]: (path, verb, callable, doc, acls)
|
||||||
|
"""
|
||||||
|
res = []
|
||||||
|
|
||||||
|
for path, verb, m_router, fct, parameters in gen_router_routes(m_router, []):
|
||||||
|
for params in parameters:
|
||||||
|
res.append((
|
||||||
|
path,
|
||||||
|
verb,
|
||||||
|
f'{m_router.__name__}:{fct.__name__}',
|
||||||
|
params.get('acl').__name__,
|
||||||
|
params.get('args', {}).get('required', []),
|
||||||
|
params.get('args', {}).get('optional', []),
|
||||||
|
params.get('out', [])
|
||||||
|
))
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def d_domains(config) -> Dict[str, ModuleType]:
|
def d_domains(config) -> Dict[str, ModuleType]:
|
||||||
"""
|
"""
|
||||||
Parameters:
|
Parameters:
|
||||||
|
|
|
@ -13,15 +13,15 @@ class DomainMiddleware(BaseHTTPMiddleware):
|
||||||
"""
|
"""
|
||||||
DomainMiddleware adds the api routes and acls to the following scope keys :
|
DomainMiddleware adds the api routes and acls to the following scope keys :
|
||||||
|
|
||||||
- domains
|
|
||||||
- api
|
- api
|
||||||
- acl
|
- acl
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, app, config):
|
def __init__(self, app, domain, config):
|
||||||
|
logger.info('DomainMiddleware %s %s', domain, config)
|
||||||
super().__init__(app)
|
super().__init__(app)
|
||||||
|
self.domain = domain
|
||||||
self.config = config
|
self.config = config
|
||||||
self.domains = {}
|
|
||||||
self.request = None
|
self.request = None
|
||||||
|
|
||||||
|
|
||||||
|
@ -31,14 +31,9 @@ class DomainMiddleware(BaseHTTPMiddleware):
|
||||||
Call of the route fonction (decorated or not)
|
Call of the route fonction (decorated or not)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
l_path = URL(scope=request.scope).path.split('/')
|
request.scope['domain'] = self.domain
|
||||||
cur_domain = l_path[0]
|
request.scope['config'] = self.config['domain_config'][self.domain] \
|
||||||
if len(cur_domain) == 0 and len(l_path) > 1:
|
if self.domain in self.config.get('domain_config', {}) else {}
|
||||||
cur_domain = l_path[1]
|
|
||||||
|
|
||||||
request.scope['domain'] = cur_domain
|
|
||||||
request.scope['config'] = self.config['domain_config'][cur_domain] \
|
|
||||||
if cur_domain in self.config.get('domain_config', {}) else {}
|
|
||||||
|
|
||||||
response = await call_next(request)
|
response = await call_next(request)
|
||||||
|
|
||||||
|
@ -56,6 +51,6 @@ class DomainMiddleware(BaseHTTPMiddleware):
|
||||||
response.headers['x-args-optional'] = \
|
response.headers['x-args-optional'] = \
|
||||||
','.join(request.scope['args']['optional'])
|
','.join(request.scope['args']['optional'])
|
||||||
|
|
||||||
response.headers['x-domain'] = cur_domain
|
response.headers['x-domain'] = self.domain
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
|
@ -17,7 +17,7 @@ from types import ModuleType
|
||||||
from starlette.schemas import SchemaGenerator
|
from starlette.schemas import SchemaGenerator
|
||||||
|
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
from .domain import gen_router_routes
|
from .domain import gen_router_routes, domain_schema_list
|
||||||
from ..logging import logger
|
from ..logging import logger
|
||||||
from .routes import gen_starlette_routes, api_routes, api_acls
|
from .routes import gen_starlette_routes, api_routes, api_acls
|
||||||
from .responses import ORJSONResponse
|
from .responses import ORJSONResponse
|
||||||
|
@ -67,7 +67,7 @@ def schema_to_csv(module_name, header=True) -> str:
|
||||||
lines should be unique in the result string;
|
lines should be unique in the result string;
|
||||||
"""
|
"""
|
||||||
# retrieve module
|
# retrieve module
|
||||||
mod = importlib.import_module(module_name)
|
m_router = importlib.import_module(module_name)
|
||||||
lines = []
|
lines = []
|
||||||
if header:
|
if header:
|
||||||
lines.append([
|
lines.append([
|
||||||
|
@ -79,35 +79,16 @@ def schema_to_csv(module_name, header=True) -> str:
|
||||||
'out'
|
'out'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
for line in domain_schema_list(m_router):
|
||||||
for path, verb, m_router, fct, parameters in gen_router_routes(mod, []):
|
lines.append([
|
||||||
""" Call route generator (.lib.domain)
|
line[0],
|
||||||
"""
|
line[1],
|
||||||
|
line[2],
|
||||||
for param in parameters:
|
line[3],
|
||||||
""" Each parameters row represents rules for a specific ACL
|
','.join(line[4]),
|
||||||
"""
|
','.join(line[5]),
|
||||||
fields = (
|
','.join(line[6])
|
||||||
f'/{path}',
|
])
|
||||||
verb,
|
|
||||||
f'{m_router.__name__}:{fct.__name__}',
|
|
||||||
param['acl'].__name__,
|
|
||||||
','.join((param.get('args', {}).get('required', set()))),
|
|
||||||
','.join((param.get('args', {}).get('optional', set()))),
|
|
||||||
','.join((param.get('out', set())))
|
|
||||||
)
|
|
||||||
|
|
||||||
if fields[0:4] in map(lambda elt: elt[0:4], lines):
|
|
||||||
raise Exception(
|
|
||||||
'Already defined acl for this route \
|
|
||||||
(path: {}, verb: {}, acl: {})'.format(
|
|
||||||
path,
|
|
||||||
verb,
|
|
||||||
param['acl'].__name__
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
lines.append(fields)
|
|
||||||
|
|
||||||
return '\n'.join(
|
return '\n'.join(
|
||||||
[ ';'.join(fields) for fields in lines ]
|
[ ';'.join(fields) for fields in lines ]
|
||||||
|
@ -115,7 +96,7 @@ def schema_to_csv(module_name, header=True) -> str:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def schema_csv_dict(csv: List[str]) -> Dict:
|
def schema_csv_dict(csv: List[str], prefix='/') -> Dict:
|
||||||
package = None
|
package = None
|
||||||
schema_d = {}
|
schema_d = {}
|
||||||
|
|
||||||
|
@ -125,8 +106,13 @@ def schema_csv_dict(csv: List[str]) -> Dict:
|
||||||
|
|
||||||
|
|
||||||
for line in csv:
|
for line in csv:
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
path, verb, router, acl_fct_name, args_req, args_opt, out = line.strip().split(';')
|
path, verb, router, acl_fct_name, args_req, args_opt, out = line.strip().split(';')
|
||||||
logger.info('schema_csv_dict %s %s %s', path, args_req, args_opt)
|
logger.info('schema_csv_dict %s %s %s', path, args_req, args_opt)
|
||||||
|
path = f'{prefix}{path}'
|
||||||
|
|
||||||
if path not in schema_d:
|
if path not in schema_d:
|
||||||
schema_d[path] = {}
|
schema_d[path] = {}
|
||||||
|
|
||||||
|
|
|
@ -7,4 +7,4 @@ def test_config(cli_runner):
|
||||||
cp = ConfigParser()
|
cp = ConfigParser()
|
||||||
cp.read_string(result.output)
|
cp.read_string(result.output)
|
||||||
assert cp.has_section('project')
|
assert cp.has_section('project')
|
||||||
assert cp.has_section('domains')
|
assert cp.has_section('domain')
|
||||||
|
|
|
@ -197,7 +197,7 @@ def project_runner(runner, halfapicli, halfapi_conf_dir):
|
||||||
###
|
###
|
||||||
# add dummy domain
|
# add dummy domain
|
||||||
###
|
###
|
||||||
create_domain('tests', '.dummy_domain.routers')
|
create_domain('dummy_domain', '.routers')
|
||||||
###
|
###
|
||||||
|
|
||||||
yield halfapicli
|
yield halfapicli
|
||||||
|
@ -264,9 +264,10 @@ def dummy_project():
|
||||||
f'secret = {halfapi_secret}\n',
|
f'secret = {halfapi_secret}\n',
|
||||||
'port = 3050\n',
|
'port = 3050\n',
|
||||||
'loglevel = debug\n',
|
'loglevel = debug\n',
|
||||||
'[domains]\n',
|
'[domain]\n',
|
||||||
f'{domain} = .routers',
|
f'name = {domain}\n',
|
||||||
f'[{domain}]',
|
'router = routers\n',
|
||||||
|
f'[{domain}]\n',
|
||||||
'test = True'
|
'test = True'
|
||||||
])
|
])
|
||||||
|
|
||||||
|
@ -288,11 +289,11 @@ def application_debug(routers):
|
||||||
halfAPI = HalfAPI({
|
halfAPI = HalfAPI({
|
||||||
'secret':'turlututu',
|
'secret':'turlututu',
|
||||||
'production':False,
|
'production':False,
|
||||||
'domains': {
|
'domain': {
|
||||||
'dummy_domain': routers
|
'name': 'dummy_domain',
|
||||||
|
'router': 'routers'
|
||||||
},
|
},
|
||||||
'config':{
|
'config':{
|
||||||
'domains': {'dummy_domain':routers},
|
|
||||||
'domain_config': {'dummy_domain': {'test': True}}
|
'domain_config': {'dummy_domain': {'test': True}}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -310,9 +311,11 @@ def application_domain(routers):
|
||||||
return HalfAPI({
|
return HalfAPI({
|
||||||
'secret':'turlututu',
|
'secret':'turlututu',
|
||||||
'production':True,
|
'production':True,
|
||||||
'domains':{'dummy_domain':routers},
|
'domain': {
|
||||||
|
'name': 'dummy_domain',
|
||||||
|
'router': 'routers'
|
||||||
|
},
|
||||||
'config':{
|
'config':{
|
||||||
'domains': {'dummy_domain':routers},
|
|
||||||
'domain_config': {'dummy_domain': {'test': True}}
|
'domain_config': {'dummy_domain': {'test': True}}
|
||||||
}
|
}
|
||||||
}).application
|
}).application
|
||||||
|
|
|
@ -2,10 +2,20 @@ from halfapi.lib import acl
|
||||||
from halfapi.lib.acl import public
|
from halfapi.lib.acl import public
|
||||||
from random import randint
|
from random import randint
|
||||||
|
|
||||||
|
|
||||||
def random():
|
def random():
|
||||||
|
""" Random access ACL
|
||||||
|
"""
|
||||||
return randint(0,1) == 1
|
return randint(0,1) == 1
|
||||||
|
|
||||||
def denied():
|
def denied():
|
||||||
|
""" Access denied
|
||||||
|
"""
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
ACLS = (
|
||||||
|
('public', public.__doc__, 999),
|
||||||
|
('random', random.__doc__, 10),
|
||||||
|
('denied', denied.__doc__, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,8 @@ def test_halfapi_dummy_domain():
|
||||||
with patch('starlette.applications.Starlette') as mock:
|
with patch('starlette.applications.Starlette') as mock:
|
||||||
mock.return_value = MagicMock()
|
mock.return_value = MagicMock()
|
||||||
halfapi = HalfAPI({
|
halfapi = HalfAPI({
|
||||||
'domains': {
|
'domain': {
|
||||||
'dummy_domain': '.routers'
|
'name': 'dummy_domain',
|
||||||
|
'router': 'routers'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -82,7 +82,7 @@ class TestCli():
|
||||||
assert cp.has_option('project', 'name')
|
assert cp.has_option('project', 'name')
|
||||||
assert cp.get('project', 'name') == PROJNAME
|
assert cp.get('project', 'name') == PROJNAME
|
||||||
assert cp.get('project', 'halfapi_version') == __version__
|
assert cp.get('project', 'halfapi_version') == __version__
|
||||||
assert cp.has_section('domains')
|
assert cp.has_section('domain')
|
||||||
except AssertionError as exc:
|
except AssertionError as exc:
|
||||||
subprocess.run(['tree', '-a', os.getcwd()])
|
subprocess.run(['tree', '-a', os.getcwd()])
|
||||||
raise exc
|
raise exc
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
from halfapi.halfapi import HalfAPI
|
from halfapi.halfapi import HalfAPI
|
||||||
|
|
||||||
|
halfapi_arg = { 'domain': { 'name': 'dummy_domain', 'router': 'routers' } }
|
||||||
def test_conf_production_default():
|
def test_conf_production_default():
|
||||||
halfapi = HalfAPI({
|
halfapi = HalfAPI({
|
||||||
'domains': {'test': True}
|
**halfapi_arg
|
||||||
})
|
})
|
||||||
assert halfapi.PRODUCTION is True
|
assert halfapi.PRODUCTION is True
|
||||||
|
|
||||||
def test_conf_production_true():
|
def test_conf_production_true():
|
||||||
halfapi = HalfAPI({
|
halfapi = HalfAPI({
|
||||||
|
**halfapi_arg,
|
||||||
'production': True,
|
'production': True,
|
||||||
'domains': {'test': True}
|
|
||||||
})
|
})
|
||||||
assert halfapi.PRODUCTION is True
|
assert halfapi.PRODUCTION is True
|
||||||
|
|
||||||
def test_conf_production_false():
|
def test_conf_production_false():
|
||||||
halfapi = HalfAPI({
|
halfapi = HalfAPI({
|
||||||
|
**halfapi_arg,
|
||||||
'production': False,
|
'production': False,
|
||||||
'domains': {'test': True}
|
|
||||||
})
|
})
|
||||||
assert halfapi.PRODUCTION is False
|
assert halfapi.PRODUCTION is False
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
import importlib
|
||||||
|
import functools
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
from unittest import TestCase
|
||||||
|
from click.testing import CliRunner
|
||||||
|
from halfapi.cli.cli import cli
|
||||||
|
from pprint import pprint
|
||||||
|
|
||||||
|
class TestDomain(TestCase):
|
||||||
|
DOMAIN = 'dummy_domain'
|
||||||
|
ROUTERS = 'routers'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def router_module(self):
|
||||||
|
return '.'.join((self.DOMAIN, self.ROUTERS))
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
class_ = CliRunner
|
||||||
|
def invoke_wrapper(f):
|
||||||
|
"""Augment CliRunner.invoke to emit its output to stdout.
|
||||||
|
|
||||||
|
This enables pytest to show the output in its logs on test
|
||||||
|
failures.
|
||||||
|
|
||||||
|
"""
|
||||||
|
@functools.wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
echo = kwargs.pop('echo', False)
|
||||||
|
result = f(*args, **kwargs)
|
||||||
|
|
||||||
|
if echo is True:
|
||||||
|
sys.stdout.write(result.output)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
class_.invoke = invoke_wrapper(class_.invoke)
|
||||||
|
self.runner = class_()
|
||||||
|
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_routes(self):
|
||||||
|
result = self.runner.invoke(cli, '--version')
|
||||||
|
self.assertEqual(result.exit_code, 0)
|
||||||
|
result = self.runner.invoke(cli, ['routes', '--export', self.router_module])
|
||||||
|
self.assertEqual(result.exit_code, 0)
|
||||||
|
print(result.stdout)
|
||||||
|
# result_d = json.loads(result.stdout)
|
||||||
|
# self.assertTrue()
|
|
@ -4,6 +4,7 @@ import importlib
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import pytest
|
import pytest
|
||||||
|
from pprint import pprint
|
||||||
from starlette.routing import Route
|
from starlette.routing import Route
|
||||||
from starlette.testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
|
|
||||||
|
@ -11,8 +12,11 @@ from halfapi.lib.domain import gen_router_routes
|
||||||
|
|
||||||
def test_get_config_route(dummy_project, application_domain, routers):
|
def test_get_config_route(dummy_project, application_domain, routers):
|
||||||
c = TestClient(application_domain)
|
c = TestClient(application_domain)
|
||||||
r = c.get('/dummy_domain/config')
|
r = c.get('/config')
|
||||||
|
assert r.status_code == 200
|
||||||
|
pprint(r.json())
|
||||||
assert 'test' in r.json()
|
assert 'test' in r.json()
|
||||||
|
|
||||||
def test_get_route(dummy_project, application_domain, routers):
|
def test_get_route(dummy_project, application_domain, routers):
|
||||||
c = TestClient(application_domain)
|
c = TestClient(application_domain)
|
||||||
path = verb = params = None
|
path = verb = params = None
|
||||||
|
@ -27,7 +31,7 @@ def test_get_route(dummy_project, application_domain, routers):
|
||||||
|
|
||||||
for route_def in []:#dummy_domain_routes:
|
for route_def in []:#dummy_domain_routes:
|
||||||
path, verb = route_def[0], route_def[1]
|
path, verb = route_def[0], route_def[1]
|
||||||
route_path = '/dummy_domain/{}'.format(path)
|
route_path = '/{}'.format(path)
|
||||||
print(route_path)
|
print(route_path)
|
||||||
try:
|
try:
|
||||||
if verb.lower() == 'get':
|
if verb.lower() == 'get':
|
||||||
|
@ -62,7 +66,7 @@ def test_get_route(dummy_project, application_domain, routers):
|
||||||
for route_def in dummy_domain_path_routes:
|
for route_def in dummy_domain_path_routes:
|
||||||
path, verb = route_def[0], route_def[1]
|
path, verb = route_def[0], route_def[1]
|
||||||
path = path.format(test=str(test_uuid))
|
path = path.format(test=str(test_uuid))
|
||||||
route_path = f'/dummy_domain/{path}'
|
route_path = f'/{path}'
|
||||||
if verb.lower() == 'get':
|
if verb.lower() == 'get':
|
||||||
r = c.get(f'{route_path}')
|
r = c.get(f'{route_path}')
|
||||||
|
|
||||||
|
@ -73,14 +77,14 @@ def test_delete_route(dummy_project, application_domain, routers):
|
||||||
c = TestClient(application_domain)
|
c = TestClient(application_domain)
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
arg = str(uuid4())
|
arg = str(uuid4())
|
||||||
r = c.delete(f'/dummy_domain/abc/alphabet/{arg}')
|
r = c.delete(f'/abc/alphabet/{arg}')
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert isinstance(r.json(), str)
|
assert isinstance(r.json(), str)
|
||||||
|
|
||||||
def test_arguments_route(dummy_project, application_domain, routers):
|
def test_arguments_route(dummy_project, application_domain, routers):
|
||||||
c = TestClient(application_domain)
|
c = TestClient(application_domain)
|
||||||
|
|
||||||
path = '/dummy_domain/arguments'
|
path = '/arguments'
|
||||||
r = c.get(path)
|
r = c.get(path)
|
||||||
assert r.status_code == 400
|
assert r.status_code == 400
|
||||||
r = c.get(path, params={'foo':True})
|
r = c.get(path, params={'foo':True})
|
||||||
|
@ -90,7 +94,7 @@ def test_arguments_route(dummy_project, application_domain, routers):
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
for key, val in arg.items():
|
for key, val in arg.items():
|
||||||
assert r.json()[key] == str(val)
|
assert r.json()[key] == str(val)
|
||||||
path = '/dummy_domain/async/arguments'
|
path = '/async/arguments'
|
||||||
r = c.get(path)
|
r = c.get(path)
|
||||||
assert r.status_code == 400
|
assert r.status_code == 400
|
||||||
r = c.get(path, params={'foo':True})
|
r = c.get(path, params={'foo':True})
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import importlib
|
import importlib
|
||||||
from halfapi.lib.domain import VERBS, gen_routes, gen_router_routes, MissingAclError
|
from halfapi.lib.domain import VERBS, gen_routes, gen_router_routes, \
|
||||||
|
MissingAclError, domain_schema_dict, domain_schema_list
|
||||||
|
|
||||||
from types import FunctionType
|
from types import FunctionType
|
||||||
|
|
||||||
|
@ -36,3 +37,15 @@ def test_gen_routes():
|
||||||
assert isinstance(params, list)
|
assert isinstance(params, list)
|
||||||
assert len(TEST_uuid.ACLS['GET']) == len(params)
|
assert len(TEST_uuid.ACLS['GET']) == len(params)
|
||||||
|
|
||||||
|
def test_domain_schema_dict():
|
||||||
|
from .dummy_domain import routers
|
||||||
|
d_res = domain_schema_dict(routers)
|
||||||
|
|
||||||
|
assert isinstance(d_res, dict)
|
||||||
|
|
||||||
|
def test_domain_schema_list():
|
||||||
|
from .dummy_domain import routers
|
||||||
|
res = domain_schema_list(routers)
|
||||||
|
|
||||||
|
assert isinstance(res, list)
|
||||||
|
assert len(res) > 0
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from starlette.routing import Route
|
from starlette.routing import Route
|
||||||
from halfapi.lib.routes import gen_starlette_routes, api_routes, gen_router_routes
|
from halfapi.lib.routes import gen_starlette_routes, gen_router_routes
|
||||||
|
|
||||||
def test_gen_starlette_routes():
|
def test_gen_starlette_routes():
|
||||||
from .dummy_domain import routers
|
from .dummy_domain import routers
|
||||||
|
@ -8,6 +8,9 @@ def test_gen_starlette_routes():
|
||||||
|
|
||||||
assert isinstance(route, Route)
|
assert isinstance(route, Route)
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
@pytest.mark.skip
|
||||||
def test_api_routes():
|
def test_api_routes():
|
||||||
from . import dummy_domain
|
from . import dummy_domain
|
||||||
d_res, d_acls = api_routes(dummy_domain)
|
d_res, d_acls = api_routes(dummy_domain)
|
||||||
|
|
|
@ -6,7 +6,7 @@ from starlette.authentication import (
|
||||||
UnauthenticatedUser)
|
UnauthenticatedUser)
|
||||||
|
|
||||||
from halfapi.lib.schemas import schema_dict_dom, schema_to_csv, schema_csv_dict
|
from halfapi.lib.schemas import schema_dict_dom, schema_to_csv, schema_csv_dict
|
||||||
from halfapi.lib.constants import DOMAIN_SCHEMA
|
from halfapi.lib.constants import DOMAIN_SCHEMA, API_SCHEMA
|
||||||
|
|
||||||
from halfapi import __version__
|
from halfapi import __version__
|
||||||
|
|
||||||
|
@ -22,20 +22,11 @@ def test_get_api_schema(project_runner, application_debug):
|
||||||
assert isinstance(c, TestClient)
|
assert isinstance(c, TestClient)
|
||||||
d_r = r.json()
|
d_r = r.json()
|
||||||
assert isinstance(d_r, dict)
|
assert isinstance(d_r, dict)
|
||||||
|
pprint(d_r)
|
||||||
def test_get_schema_route(project_runner, application_debug):
|
assert API_SCHEMA.validate(d_r)
|
||||||
c = TestClient(application_debug)
|
|
||||||
assert isinstance(c, TestClient)
|
|
||||||
r = c.get('/halfapi/schema')
|
|
||||||
d_r = r.json()
|
|
||||||
assert isinstance(d_r, dict)
|
|
||||||
assert 'openapi' in d_r.keys()
|
|
||||||
assert 'info' in d_r.keys()
|
|
||||||
assert d_r['info']['title'] == 'HalfAPI'
|
|
||||||
assert d_r['info']['version'] == __version__
|
|
||||||
assert 'paths' in d_r.keys()
|
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
def test_get_api_dummy_domain_routes(application_domain, routers):
|
def test_get_api_dummy_domain_routes(application_domain, routers):
|
||||||
c = TestClient(application_domain)
|
c = TestClient(application_domain)
|
||||||
r = c.get('/dummy_domain')
|
r = c.get('/dummy_domain')
|
||||||
|
@ -46,6 +37,7 @@ def test_get_api_dummy_domain_routes(application_domain, routers):
|
||||||
assert 'GET' in d_r['abc/alphabet']
|
assert 'GET' in d_r['abc/alphabet']
|
||||||
assert len(d_r['abc/alphabet']['GET']) > 0
|
assert len(d_r['abc/alphabet']['GET']) > 0
|
||||||
assert 'acls' in d_r['abc/alphabet']['GET']
|
assert 'acls' in d_r['abc/alphabet']['GET']
|
||||||
|
"""
|
||||||
|
|
||||||
def test_schema_to_csv():
|
def test_schema_to_csv():
|
||||||
csv = schema_to_csv('dummy_domain.routers', False)
|
csv = schema_to_csv('dummy_domain.routers', False)
|
||||||
|
@ -58,3 +50,4 @@ def test_schema_csv_dict():
|
||||||
schema_d = schema_csv_dict(csv.split('\n'))
|
schema_d = schema_csv_dict(csv.split('\n'))
|
||||||
assert isinstance(schema_d, dict)
|
assert isinstance(schema_d, dict)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue