Compare commits

...

3 Commits

Author SHA1 Message Date
Maxime Alves LIRMM@home
6b6a2d47e0 [cli] halfapi route update, creates the router tree 2021-10-06 01:00:52 +02:00
Maxime Alves LIRMM@home
f0152fd0a8 [apidb] implementation of halfapi route add/list 2021-10-06 01:00:52 +02:00
Maxime Alves LIRMM@home
6503601c60 [cli] add "halfapi init" for hop projects, that creates an "halfapi" schema in the db
Read halfapi/sql/api.sql to see the structure
2021-10-06 01:00:52 +02:00
7 changed files with 275 additions and 4 deletions

View File

@ -10,12 +10,10 @@ It defines the following globals :
""" """
import logging import logging
import time
# asgi framework # asgi framework
from starlette.applications import Starlette from starlette.applications import Starlette
from starlette.authentication import UnauthenticatedUser from starlette.authentication import UnauthenticatedUser
from starlette.middleware import Middleware
from starlette.routing import Route from starlette.routing import Route
from starlette.responses import Response, PlainTextResponse from starlette.responses import Response, PlainTextResponse
from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.authentication import AuthenticationMiddleware

View File

@ -31,6 +31,8 @@ if IS_PROJECT:
from . import config from . import config
from . import domain from . import domain
from . import run from . import run
elif IS_HOP_PROJECT:
from . import init_hop
from . import route
else: else:
from . import init from . import init

36
halfapi/cli/init_hop.py Normal file
View File

@ -0,0 +1,36 @@
import os
import subprocess
from configparser import ConfigParser
from half_orm.model import Model
import click
from .cli import cli
@cli.command()
def init():
"""
The "halfapi init" command for hop projects
"""
hop_conf_path = os.path.join('.hop', 'config')
config = ConfigParser()
config.read([ hop_conf_path ])
assert os.path.isdir(config.get('halfORM', 'package_name'))
model = Model(config.get('halfORM', 'package_name'))
import halfapi
halfapi_path = list(halfapi.__path__)[0]
sql_path = os.path.join(halfapi_path, 'sql', 'api.sql')
with open(sql_path, 'r') as sql_file:
for query in ''.join(sql_file.readlines()).split(';'):
if len(query.strip()) == 0:
continue
model.execute_query(query.strip())
subprocess.run(['hop', 'update', '-f'])
click.echo('halfapi schema has been initialized')
click.echo('use halfapi route command to create your first route')
click.echo('example : halfapi route add')

173
halfapi/cli/route.py Normal file
View File

@ -0,0 +1,173 @@
import os
import sys
from configparser import ConfigParser
import importlib
import click
from half_orm.model import Model
from .cli import cli
from halfapi.lib.domain import VERBS
def get_package_name():
hop_conf_path = os.path.join('.hop', 'config')
config = ConfigParser()
config.read([ hop_conf_path ])
assert os.path.isdir(config.get('halfORM', 'package_name'))
return config.get('halfORM', 'package_name')
def get_package_module(name):
package_name = get_package_name()
if sys.path[0] != '.':
sys.path.insert(0, '.')
module = importlib.import_module(
'.'.join((
package_name,
'halfapi',
name)))
if not module:
raise Exception('Could not import {}. Please hop update -f'.format(
'.'.join((
config.get('halfORM', 'package_name'),
'halfapi',
name))))
return module
@cli.group()
def route():
pass
def endpoint_create(verb, endpoint, endpoint_type):
Endpoint_mod = get_package_module('endpoint')
Endpoint = Endpoint_mod.Endpoint
EndpointTypeDoesNotExist = Endpoint_mod.EndpointTypeDoesNotExist
try:
click.echo('Endpoint creation')
new_endpoint = Endpoint.create(
verb=verb, endpoint=endpoint, endpoint_type=endpoint_type
)
return Endpoint(**new_endpoint).path
except EndpointTypeDoesNotExist:
create_endpoint_type = click.prompt(
'The endpoint type {} does not exist. Do you want to create it?'.format(endpoint_type),
default='n',
type=click.Choice(['y', 'N'], case_sensitive=False)
)
if create_endpoint_type.lower() == 'n':
click.echo('Aborting...')
sys.exit(0)
EndpointType_mod = get_package_module('endpoint_type')
EndpointType = EndpointType_mod.EndpointType
EndpointType.create(endpoint_type)
return endpoint_create(verb, endpoint, endpoint_type)
@click.option('--type', prompt=True, type=str, default='JSON')
@click.option('--endpoint', prompt=True, type=str)
@click.option('--verb', prompt=True, type=click.Choice(VERBS, case_sensitive=False))
@route.command()
def add(verb, endpoint, type):
"""
The "halfapi route add" command for hop projects
"""
click.echo('About to create a new route : [{}] {} -> {}'.format(verb, endpoint, type))
new_endpoint = endpoint_create(verb, endpoint, type)
click.echo(f'Created endpoint {new_endpoint}')
@route.command()
def list():
"""
The "halfapi route list" command for hop projects
"""
Endpoint_mod = get_package_module('endpoint')
Endpoint = Endpoint_mod.Endpoint
click.echo('Current routes :')
for endpoint in Endpoint().select():
elt = Endpoint(**endpoint)
click.echo(f'{elt.method}: {elt.path}')
@click.option('--target', default='./Lib/api', type=str)
@route.command()
def update(target):
"""
The "halfapi route update" command for hop projects
Creates the router tree under <target>, and add missing methods
for endpoints, that raise NotImplementedError
"""
from time import sleep
package = get_package_name()
target_path = os.path.join(os.path.abspath('.'), package, target)
if not os.path.isdir(target_path):
raise Exception('Missing target path {}'.format(target_path))
click.echo('Will create router tree in {}'.format(target_path))
proceed = click.prompt(
'Proceed? [Y/n]',
default='y',
type=click.Choice(['Y', 'n'], case_sensitive=False)
)
if proceed.lower() == 'n':
sys.exit()
Endpoint_mod = get_package_module('endpoint')
Endpoint = Endpoint_mod.Endpoint
missing_methods = {}
for endpoint in Endpoint().select():
elt = Endpoint(**endpoint)
path = elt.path
stack = [target_path]
for segment in path.split('/'):
stack.append(segment)
if os.path.isdir(os.path.join(*stack)):
continue
print(f'Create {os.path.join(*stack)}')
os.mkdir(os.path.join(*stack))
sleep(.1)
endpoint_mod_path = '.'.join([package, *target.split('/')[1:], *path.split('/')[1:]])
try:
endpoint_mod = importlib.import_module(endpoint_mod_path)
if not hasattr(endpoint_mod, str(elt.method)):
if endpoint_mod.__path__[0] not in missing_methods:
missing_methods[endpoint_mod.__path__[0]] = []
missing_methods[endpoint_mod.__path__[0]].append(str(elt.method))
except Exception as exc:
print(f'Could not import {endpoint_mod_path}, may be a bug')
print(exc)
endpoint_mod_path = endpoint_mod_path.replace('.', '/')
if endpoint_mod_path not in missing_methods:
missing_methods[endpoint_mod_path] = []
missing_methods[endpoint_mod_path].append(str(elt.method))
pass
for path, methods in missing_methods.items():
with open(os.path.join(path, '__init__.py'), 'a+') as f:
for method in methods:
f.write('\n'.join((
f'def {method}():',
' raise NotImplementedError\n')))

View File

@ -60,6 +60,14 @@ is_project = lambda: os.path.isfile(CONF_FILE)
ENDPOINT_TYPES = [
'JSON'
]
config = ConfigParser(allow_no_value=True) config = ConfigParser(allow_no_value=True)
CONF_DIR = environ.get('HALFAPI_CONF_DIR', '/etc/half_api') CONF_DIR = environ.get('HALFAPI_CONF_DIR', '/etc/half_api')

50
halfapi/sql/api.sql Normal file
View File

@ -0,0 +1,50 @@
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE SCHEMA IF NOT EXISTS halfapi;
DROP TABLE IF EXISTS halfapi.endpoint;
DROP TABLE IF EXISTS halfapi.parameter;
DROP TABLE IF EXISTS halfapi.segment;
DROP TABLE IF EXISTS halfapi.base_table;
CREATE TABLE halfapi.segment (
id uuid DEFAULT public.gen_random_uuid(),
fqtn text,
PRIMARY KEY (id),
name text,
parent uuid DEFAULT NULL,
UNIQUE(name, parent)
);
ALTER TABLE halfapi.segment ADD CONSTRAINT
segment_parent_fkey FOREIGN KEY (parent) REFERENCES halfapi.segment(id);
DROP TABLE IF EXISTS halfapi.type;
CREATE TABLE halfapi.type (
name text,
PRIMARY KEY (name)
);
CREATE TABLE halfapi.parameter (
type text REFERENCES halfapi.type(name)
) INHERITS (halfapi.segment);
DROP TABLE IF EXISTS halfapi.endpoint_type;
CREATE TABLE halfapi.endpoint_type (
name text,
PRIMARY KEY (name)
);
DROP TYPE IF EXISTS method;
CREATE TYPE method AS ENUM ('get', 'post', 'patch', 'put', 'delete');
CREATE TABLE halfapi.endpoint (
method method,
type text,
segment uuid NOT NULL,
PRIMARY KEY (method, segment)
);
ALTER TABLE halfapi.endpoint ADD CONSTRAINT
endpoint_segment_id FOREIGN KEY (segment) REFERENCES halfapi.segment(id);
ALTER TABLE halfapi.endpoint ADD CONSTRAINT
endpoint_type_name FOREIGN KEY (type) REFERENCES halfapi.endpoint_type(name);

View File

@ -49,7 +49,8 @@ setup(
"uvicorn>=0.13,<1", "uvicorn>=0.13,<1",
"orjson>=3.4.7,<4", "orjson>=3.4.7,<4",
"pyyaml>=5.3.1,<6", "pyyaml>=5.3.1,<6",
"timing-asgi>=0.2.1,<1" "timing-asgi>=0.2.1,<1",
"half_orm>=0.5.0"
], ],
classifiers=[ classifiers=[
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
@ -67,6 +68,9 @@ setup(
"pylint" "pylint"
] ]
}, },
package_data={
'halfapi': ['sql/*.sql']
},
entry_points={ entry_points={
"console_scripts":[ "console_scripts":[
"halfapi=halfapi.cli.cli:cli" "halfapi=halfapi.cli.cli:cli"