From 970f96048316d6ab6476d935a1cb6f2212a6b38a Mon Sep 17 00:00:00 2001 From: Andre Anjos <andre.dos.anjos@gmail.com> Date: Mon, 30 Apr 2018 15:42:10 +0200 Subject: [PATCH] More python fixes after gigantic move --- beat/editor/resources.py | 408 +++++++++++++++++++++++++++++ beat/editor/scripts/server.py | 479 ++++------------------------------ beat/editor/templates.py | 51 ---- beat/editor/utils.py | 179 +++++++++++++ conda/meta.yaml | 7 +- requirements.txt | 2 + setup.py | 2 +- 7 files changed, 641 insertions(+), 487 deletions(-) create mode 100644 beat/editor/resources.py delete mode 100644 beat/editor/templates.py create mode 100644 beat/editor/utils.py diff --git a/beat/editor/resources.py b/beat/editor/resources.py new file mode 100644 index 00000000..fc1d198a --- /dev/null +++ b/beat/editor/resources.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + + +'''Server resources (API endpoints)''' + + +import os +import glob +import shutil +import subprocess + +import simplejson + +from flask_restful import Resource + +import logging +logger = logging.getLogger(__name__) + +from . import utils + + +class Home(Resource): + """The base resource path with subroutes""" + + def get(self): + """Returns the available sub-routes""" + + return { + 'routes': [ + 'databases', + 'dataformats', + 'libraries', + 'algorithms', + 'toolchains', + 'experiments', + 'plotters', + 'plotterparameters', + 'settings', + 'layout', + 'environments', + ] + } + + +class Layout(Resource): + """Exposes toolchain layout functionality""" + + def post(self): + data = request.get_json() + if data != None and 'toolchain' in data: + return get_tc_layout(data['toolchain']) + else: + raise RuntimeError('Invalid post content for tc layout!') + + +class Environments(Resource): + """Exposes local environment info""" + + def get(self): + return get_environment_info() + + +class Settings(Resource): + """Exposes user settings""" + + def get(self): + """Returns the settings""" + return utils.get_user_conf() + + def put(self): + """Overwrites the settings""" + new_conf = request.get_json() + with open('./user_conf.json', 'w') as f: + try: + json.dump(new_conf, f) + except json.JSONDecodeError: + logger.critical('Invalid new_conf %s', new_conf) + raise SystemExit + return utils.get_user_conf() + + +class Templates(Resource): + """Endpoint for generating template files""" + def post(self): + data = request.get_json() + entity = data.pop('entity') + name = data.pop('name') + generate_python_template(entity, name, **data) + + +def read_json(path): + """Reads a JSON file and returns the parse JSON obj""" + with open(path, 'rt') as f: + return json.loads(f.read()) + + +def path_to_dict(path): + """Generates a dict of the given file/folder in the BEAT prefix""" + d = { + 'name': os.path.basename(path) + } + if os.path.isdir(path): + d['type'] = "directory" + d['children'] = [path_to_dict(os.path.join(path, x)) + for x in os.listdir(path)] + elif os.path.isfile(path): + d['type'] = "file" + fname, fext = os.path.splitext(path) + if fext == '.json': + d['json'] = read_json(path) + return d + + +VALID_ENTITIES = [ + 'dataformats', + 'databases', + 'libraries', + 'algorithms', + 'toolchains', + 'experiments', + 'plotters', + 'plotterparameters', + ] +"""List of valid BEAT object entitities""" + + +def get_tc_layout(tc_name): + """Returns the JSON returned by Graphviz's dot "-Tjson0" output for the given toolchain""" + + prefix = utils.get_prefix() + beat_ex_loc = '%s/bin/beat' % prefix + tc_dot_loc = '%s/toolchains/%s.dot' % (prefix, tc_name) + if not os.path.isfile(beat_ex_loc): + raise Exception('BEAT executable not at %s' % beat_ex_loc) + beat_cmd = 'cd %s && ./bin/beat tc draw %s' % (prefix, tc_name) + output = subprocess.call(beat_cmd, shell=True) + if not os.path.isfile(tc_dot_loc): + logger.critical('Running command "%s" got:', beat_cmd) + raise IOError('"%s" graphviz dot file not found at "%s"' % (tc_name, tc_dot_loc)) + + s = subprocess.check_output('dot %s -Tjson0' % tc_dot_loc, shell=True, encoding='utf-8') + + return s + + +def get_environment_info(): + json = simplejson.loads(''' + [ + { + "name": "Scientific Python 2.7", + "packages": { + "alabaster": "0.7.6", + "babel": "1.3" + }, + "queues": { + "Default": { + "memory_limit": 5983, + "nb_slots": 2, + "max_slots_per_user": 2, + "nb_cores_per_slot": 1, + "time_limit": 360 + } + }, + "accessibility": "public", + "languages": [ + "python" + ], + "version": "0.0.4", + "short_description": "Scientific Python 2.7" + }, + { + "name": "Scientific Python 2.7", + "packages": { + "alabaster": "0.7.10", + "Babel": "2.4.0" + }, + "queues": { + "Default": { + "memory_limit": 5983, + "nb_slots": 2, + "max_slots_per_user": 2, + "nb_cores_per_slot": 1, + "time_limit": 360 + } + }, + "accessibility": "public", + "languages": [ + "python" + ], + "version": "1.0.0", + "short_description": "Scientific Python 2.7" + } + ] + ''') + + return json +def assert_valid_entity(v): + """Asserts the passed value corresponds to a valid BEAT entity""" + + assert v in VALID_ENTITIES, '%s is not a valid BEAT entity ' \ + '(valid values are %s)' % (v, ', '.join(VALID_ENTITIES)) + + +def generate_file_tree(entity): + """Generates a file tree (of dicts) given a specific BEAT entity""" + + assert_valid_entity(entity) + resource_path = os.path.join(utils.get_prefix(), entity) + if not os.path.isdir(resource_path): + raise NotADirectoryError('Invalid resource path %s' % resource_path) + + return path_to_dict(resource_path) + + +def generate_json_entity(fto, parent_names): + """Generates info for a file in the BEAT path""" + if fto['type'] != 'file': + raise Exception('bad file tree obj') + + fname, fext = os.path.splitext(fto['name']) + + name_str = '' + for name in parent_names: + name_str += name + '/' + + name_str += fname + + return { + 'name': name_str, + 'contents': fto['json'] + } + + +def generate_entity_tree(entity): + """Generates the entire tree for an entity type from the prefix""" + + file_tree = generate_file_tree(entity) + entity_tree = {} + user_and_name = [ + 'dataformat', + 'library', + 'algorithm', + 'toolchain', + 'plotter', + 'plotterparameter', + ] + + if entity in user_and_name: + for user in file_tree['children']: + entity_tree[user['name']] = {} + for obj in user['children']: + entity_tree[user['name']][obj['name']] = list() + for f in obj['children']: + fname, fext = os.path.splitext(f['name']) + if fext != '.json': + continue + parent_names = [user['name'], obj['name']] + json_obj = generate_json_entity(f, parent_names) + entity_tree[user['name']][obj['name']].append(json_obj) + + elif entity == BeatEntity.DATABASE: + for obj in file_tree['children']: + entity_tree[obj['name']] = list() + for f in obj['children']: + fname, fext = os.path.splitext(f['name']) + if fext != '.json': + continue + parent_names = [obj['name']] + json_obj = generate_json_entity(f, parent_names) + entity_tree[obj['name']].append(json_obj) + + elif entity == BeatEntity.EXPERIMENT: + for user in file_tree['children']: + uname = user['name'] + entity_tree[uname] = {} + for tc_user in user['children']: + tcuname = tc_user['name'] + entity_tree[uname][tcuname] = {} + for tc_name in tc_user['children']: + tcname = tc_name['name'] + entity_tree[uname][tcuname][tcname] = {} + for tc_version in tc_name['children']: + tcv = tc_version['name'] + entity_tree[uname][tcuname][tcname][tcv] = list() + for exp_name in tc_version['children']: + fname, fext = os.path.splitext(exp_name['name']) + if fext != '.json': + continue + parent_names = [uname, tcuname, tcname, tcv] + json_obj = generate_json_entity( + exp_name, parent_names) + entity_tree[uname][tcuname][tcname][tcv].append( + json_obj) + + return entity_tree + + +def write_json(entity, obj, mode, copy_obj_name='', **kwargs): + """Writes JSON from a webapp request to the prefix using the specified + mode""" + + assert_valid_entity(entity) + resource_path = os.path.join(utils.get_prefix(), entityt) + name = obj['name'] + name_segs = name.split('/') + contents = obj['contents'] + stringified = simplejson.dumps(contents, indent=4, sort_keys=True) + + folder_path = os.path.join(resource_path, '/'.join(name_segs[:-1])) + file_subpath = os.path.join(resource_path, name) + file_path = file_subpath + '.json' + + if mode == WriteMode.UPDATE: + os.makedirs(folder_path, exist_ok=True) + with open(file_path, 'w') as f: + f.write(stringified) + elif mode == WriteMode.CREATE: + if not os.path.isfile(file_path): + os.makedirs(folder_path, exist_ok=True) + if copy_obj_name != '': + copy_obj_path = os.path.join(resource_path, copy_obj_name) + if os.path.isfile(copy_obj_path + '.json'): + files_to_copy = glob.glob('%s.*' % copy_obj_path) + copy_locations = ['%s%s' % (file_subpath, os.path.splitext(f)[1]) for f in files_to_copy] + for i in range(0, len(files_to_copy)): + if files_to_copy[i].endswith('.json'): continue + shutil.copy(files_to_copy[i], copy_locations[i]) + with open(file_path, 'w') as f: + f.write(stringified) + elif mode == WriteMode.DELETE: + if os.path.isfile(file_path): + files_to_delete = glob.glob('%s.*' % file_subpath) + for f in files_to_delete: + os.remove(f) + # taken from https://stackoverflow.com/a/23488980 + + def remove_empty_dirs(path): + """Remove directories now made empty by deleting an object from the prefix""" + for root, dirnames, filenames in os.walk(path, topdown=False): + for dirname in dirnames: + remove_empty_dirs( + os.path.realpath( + os.path.join(root, dirname) + ) + ) + if not os.listdir(path): + os.rmdir(path) + + remove_empty_dirs(folder_path) + else: + raise TypeError('Invalid WriteMode %s' % mode) + + +def gen_endpoint(entity): + """Generates an endpoint for the given BEAT entity + + Exposes actions to perform on the prefix + + """ + + class Endpoint(Resource): + """A class representing the template for an endpoint for a BEAT entity""" + + def refresh(self): + """Regenerates the entity tree""" + try: + return generate_entity_tree(entity) + except NotADirectoryError: + return {} + + def get(self): + """Returns the entity tree""" + return self.refresh() + + def post(self): + """Creates a new object""" + obj_list = request.get_json() + if not isinstance(obj_list, list): + obj_list = [obj_list] + for o in obj_list: + # two fields: + # - "obj" field (the object to create) + # - "copyObjName" field (the object that was copied, blank if + # not copied) + obj = o['obj'] + copy_obj_name = o['copiedObjName'] + write_json(entity, obj, 'create', copy_obj_name) + return self.refresh() + + def put(self): + """Updates an already-existing object""" + obj_list = request.get_json() + if not isinstance(obj_list, list): + obj_list = [obj_list] + for obj in obj_list: + write_json(entity, obj, 'update') + return self.refresh() + + def delete(self): + """Deletes an object""" + obj = request.get_json() + write_json(entity, obj, 'delete') + return self.refresh() + + Endpoint.__name__ = entity + + return Endpoint diff --git a/beat/editor/scripts/server.py b/beat/editor/scripts/server.py index b456ad9a..4b869ddf 100644 --- a/beat/editor/scripts/server.py +++ b/beat/editor/scripts/server.py @@ -1,454 +1,70 @@ -""" -A simple Flask server providing an API for the local BEAT prefix and user configuration. -Dependencies: - simplejson - flask - flask_restful - flask_cors -""" - -import os -import glob -from shutil import copy as copy_file -import subprocess -import json - -from enum import Enum -import simplejson - -from flask import Flask, request -from flask_restful import Resource, Api -from flask_cors import CORS - -from .. import templates - - -def get_user_conf(): - """Reads & returns the user configuration in a dict""" - - user_conf = {} - if os.path.isfile('./user_conf.json'): - with open('./user_conf.json') as f: - try: - user_conf = json.load(f) - except json.JSONDecodeError: - print('user_conf.json is invalid!') - raise SystemExit - else: - with open('./user_conf.json', 'w') as f: - user_conf = { - 'prefix': '~' - } - json.dump(user_conf, f) - - - if 'prefix' not in user_conf: - raise Exception('Invalid user_conf: Needs "prefix" key!') - return user_conf - - -def get_prefix(): - """Returns an absolute path to the user-defined BEAT prefix location""" - return os.path.expanduser(get_user_conf()['prefix']) - - -def get_tc_layout(tc_name): - """Returns the JSON returned by Graphviz's dot "-Tjson0" output for the given toolchain""" - prefix = get_prefix() - beat_ex_loc = '%s/bin/beat' % prefix - tc_dot_loc = '%s/toolchains/%s.dot' % (prefix, tc_name) - if not os.path.isfile(beat_ex_loc): - raise Exception('BEAT executable not at %s' % beat_ex_loc) - beat_cmd = 'cd %s && ./bin/beat tc draw %s' % (prefix, tc_name) - output = subprocess.call(beat_cmd, shell=True) - if not os.path.isfile(tc_dot_loc): - print('Running command "%s" got:' % beat_cmd) - print(output) - raise Exception('"%s" graphviz dot file not found at "%s"' % (tc_name, tc_dot_loc)) - - s = subprocess.check_output('dot %s -Tjson0' % tc_dot_loc, shell=True, encoding='utf-8') - - return s - -def get_environment_info(): - json = simplejson.loads(''' - [ - { - "name": "Scientific Python 2.7", - "packages": { - "alabaster": "0.7.6", - "babel": "1.3" - }, - "queues": { - "Default": { - "memory_limit": 5983, - "nb_slots": 2, - "max_slots_per_user": 2, - "nb_cores_per_slot": 1, - "time_limit": 360 - } - }, - "accessibility": "public", - "languages": [ - "python" - ], - "version": "0.0.4", - "short_description": "Scientific Python 2.7" - }, - { - "name": "Scientific Python 2.7", - "packages": { - "alabaster": "0.7.10", - "Babel": "2.4.0" - }, - "queues": { - "Default": { - "memory_limit": 5983, - "nb_slots": 2, - "max_slots_per_user": 2, - "nb_cores_per_slot": 1, - "time_limit": 360 - } - }, - "accessibility": "public", - "languages": [ - "python" - ], - "version": "1.0.0", - "short_description": "Scientific Python 2.7" - } - ] - ''') - - return json - - -class BeatEntity(Enum): - """List of BEAT entities to serve to the webapp""" - DATAFORMAT = 'dataformats' - DATABASE = 'databases' - LIBRARY = 'libraries' - ALGORITHM = 'algorithms' - TOOLCHAIN = 'toolchains' - EXPERIMENT = 'experiments' - PLOTTER = 'plotters' - PLOTTERPARAMETER = 'plotterparameters' - - -def read_json(path): - """Reads a JSON file and returns the parse JSON obj""" - with open(path, 'r') as f: - data = json.load(f) - return data - - -def path_to_dict(path): - """Generates a dict of the given file/folder in the BEAT prefix""" - d = { - 'name': os.path.basename(path) - } - if os.path.isdir(path): - d['type'] = "directory" - d['children'] = [path_to_dict(os.path.join(path, x)) - for x in os.listdir(path)] - elif os.path.isfile(path): - d['type'] = "file" - fname, fext = os.path.splitext(path) - if fext == '.json': - d['json'] = read_json(path) - return d - +#!/usr/bin/env python +# -*- coding: utf-8 -*- -def generate_file_tree(be): - """Generates a file tree (of dicts) given a specific BEAT entity""" - resource_path = os.path.join(get_prefix(), be.value) - if not os.path.isdir(resource_path): - raise NotADirectoryError('Invalid resource path %s' % resource_path) +"""Starts the BEAT editor - #print('generating entity tree for path %s' % resource_path) - return path_to_dict(resource_path) +Usage: %(prog)s [-v...] [--debug] + %(prog)s --help + %(prog)s --version -def generate_json_entity(fto, parent_names): - """Generates info for a file in the BEAT path""" - if fto['type'] != 'file': - raise Exception('bad file tree obj') +Options: - fname, fext = os.path.splitext(fto['name']) + -h, --help Shows this help message and exits + -V, --version Prints the version and exits + -v, --verbose Increases the output verbosity level. Using "-vv" allows the + program to output informational messages as it goes along. + -d, --debug Use the debug version of the javascript source to launch the + editor - name_str = '' - for name in parent_names: - name_str += name + '/' - name_str += fname +Examples: - return { - 'name': name_str, - 'contents': fto['json'] - } + Start the editor: + $ %(prog)s -vv -def generate_entity_tree(be): - """Generates the entire tree for an entity type from the prefix""" - file_tree = generate_file_tree(be) - entity_tree = {} - user_and_name = [ - BeatEntity.DATAFORMAT, - BeatEntity.LIBRARY, - BeatEntity.ALGORITHM, - BeatEntity.TOOLCHAIN, - BeatEntity.PLOTTER, - BeatEntity.PLOTTERPARAMETER, - ] + Start the editor in development mode: - if be in user_and_name: - for user in file_tree['children']: - entity_tree[user['name']] = {} - for obj in user['children']: - entity_tree[user['name']][obj['name']] = list() - for f in obj['children']: - fname, fext = os.path.splitext(f['name']) - if fext != '.json': - continue - parent_names = [user['name'], obj['name']] - json_obj = generate_json_entity(f, parent_names) - entity_tree[user['name']][obj['name']].append(json_obj) - - elif be == BeatEntity.DATABASE: - for obj in file_tree['children']: - entity_tree[obj['name']] = list() - for f in obj['children']: - fname, fext = os.path.splitext(f['name']) - if fext != '.json': - continue - parent_names = [obj['name']] - json_obj = generate_json_entity(f, parent_names) - entity_tree[obj['name']].append(json_obj) - - elif be == BeatEntity.EXPERIMENT: - for user in file_tree['children']: - uname = user['name'] - entity_tree[uname] = {} - for tc_user in user['children']: - tcuname = tc_user['name'] - entity_tree[uname][tcuname] = {} - for tc_name in tc_user['children']: - tcname = tc_name['name'] - entity_tree[uname][tcuname][tcname] = {} - for tc_version in tc_name['children']: - tcv = tc_version['name'] - entity_tree[uname][tcuname][tcname][tcv] = list() - for exp_name in tc_version['children']: - fname, fext = os.path.splitext(exp_name['name']) - if fext != '.json': - continue - parent_names = [uname, tcuname, tcname, tcv] - json_obj = generate_json_entity( - exp_name, parent_names) - entity_tree[uname][tcuname][tcname][tcv].append( - json_obj) - - return entity_tree - - -def generate_python_template(be, name, **kwargs): - """Generates a template for a certain beat entity type with the given named arguments""" - template_func = None - if be == BeatEntity.DATABASE: - template_func = templates.generate_database - elif be == BeatEntity.LIBRARY: - template_func = templates.generate_library - elif be == BeatEntity.ALGORITHM: - template_func = templates.generate_algorithm - else: - raise TypeError('Cannot create template for beat entity type "%s"' % be.value) - - - s = template_func(**kwargs) - - resource_path = os.path.join(get_prefix(), be.value) - file_path = os.path.join(resource_path, name) + '.py' - with open(file_path, 'w') as f: - f.write(s) - return s - - -class WriteMode(Enum): - """Write modes for requests from the web app""" - CREATE = 0 - UPDATE = 1 - DELETE = 2 - - -def write_json(be, obj, mode, copy_obj_name='', **kwargs): - """writes JSON from a webapp request to the prefix using the specified mode""" - resource_path = os.path.join(get_prefix(), be.value) - name = obj['name'] - name_segs = name.split('/') - contents = obj['contents'] - stringified = simplejson.dumps(contents, indent=4, sort_keys=True) - - folder_path = os.path.join(resource_path, '/'.join(name_segs[:-1])) - file_subpath = os.path.join(resource_path, name) - file_path = file_subpath + '.json' - - if mode == WriteMode.UPDATE: - os.makedirs(folder_path, exist_ok=True) - with open(file_path, 'w') as f: - f.write(stringified) - elif mode == WriteMode.CREATE: - if not os.path.isfile(file_path): - os.makedirs(folder_path, exist_ok=True) - if copy_obj_name != '': - copy_obj_path = os.path.join(resource_path, copy_obj_name) - if os.path.isfile(copy_obj_path + '.json'): - files_to_copy = glob.glob('%s.*' % copy_obj_path) - copy_locations = ['%s%s' % (file_subpath, os.path.splitext(f)[1]) for f in files_to_copy] - for i in range(0, len(files_to_copy)): - if files_to_copy[i].endswith('.json'): continue - copy_file(files_to_copy[i], copy_locations[i]) - with open(file_path, 'w') as f: - f.write(stringified) - elif mode == WriteMode.DELETE: - if os.path.isfile(file_path): - files_to_delete = glob.glob('%s.*' % file_subpath) - for f in files_to_delete: - os.remove(f) - # taken from https://stackoverflow.com/a/23488980 - - def remove_empty_dirs(path): - """Remove directories now made empty by deleting an object from the prefix""" - for root, dirnames, filenames in os.walk(path, topdown=False): - for dirname in dirnames: - remove_empty_dirs( - os.path.realpath( - os.path.join(root, dirname) - ) - ) - if not os.listdir(path): - os.rmdir(path) - - remove_empty_dirs(folder_path) - else: - raise TypeError('Invalid WriteMode %s' % mode) - - -class Home(Resource): - """The base resource path with subroutes""" - def get(self): - """Returns the available sub-routes""" - return { - 'routes': [ - 'databases', - 'dataformats', - 'libraries', - 'algorithms', - 'toolchains', - 'experiments', - 'plotters', - 'plotterparameters', - 'settings', - 'layout', - 'environments', - ] - } - -class Layout(Resource): - """Exposes toolchain layout functionality""" - - def post(self): - data = request.get_json() - if data != None and 'toolchain' in data: - return get_tc_layout(data['toolchain']) - else: - print(data) - raise Exception('Invalid post content for tc layout!') - -class Environments(Resource): - """Exposes local environment info""" - - def get(self): - return get_environment_info() - -class Settings(Resource): - """Exposes user settings""" - - def get(self): - """Returns the settings""" - return get_user_conf() - - def put(self): - """Overwrites the settings""" - new_conf = request.get_json() - with open('./user_conf.json', 'w') as f: - try: - json.dump(new_conf, f) - except json.JSONDecodeError: - print('Invalid new_conf %s' % new_conf) - raise SystemExit - return get_user_conf() + $ %(prog)s -vv --debug +""" -class Templates(Resource): - """Endpoint for generating template files""" - def post(self): - data = request.get_json() - entity = BeatEntity(data.pop('entity')) - name = data.pop('name') - generate_python_template(entity, name, **data) -def gen_beat_endpoint(entity): - """Generates an endpoint for the given BEAT entity, exposing actions to perform on the prefix""" - class BeatEndpoint(Resource): - """A class representing the template for an endpoint for a BEAT entity""" - be = entity +import os +import sys +import docopt - def refresh(self): - """Regenerates the entity tree""" - try: - return generate_entity_tree(self.be) - except NotADirectoryError: - return {} - def get(self): - """Returns the entity tree""" - return self.refresh() +def main(user_input=None): - def post(self): - """Creates a new object""" - obj_list = request.get_json() - if not isinstance(obj_list, list): - obj_list = [obj_list] - for o in obj_list: - # two fields: - # - "obj" field (the object to create) - # - "copyObjName" field (the object that was copied, blank if not copied) - obj = o['obj'] - copy_obj_name = o['copiedObjName'] - write_json(self.be, obj, WriteMode.CREATE, copy_obj_name) - return self.refresh() + if user_input is not None: + argv = user_input + else: + argv = sys.argv[1:] - def put(self): - """Updates an already-existing object""" - obj_list = request.get_json() - if not isinstance(obj_list, list): - obj_list = [obj_list] - for obj in obj_list: - write_json(self.be, obj, WriteMode.UPDATE) - return self.refresh() + import pkg_resources - def delete(self): - """Deletes an object""" - obj = request.get_json() - write_json(self.be, obj, WriteMode.DELETE) - return self.refresh() + completions = dict( + prog=os.path.basename(sys.argv[0]), + version=pkg_resources.require('beat.editor')[0].version + ) - BeatEndpoint.__name__ = entity.value + args = docopt.docopt( + __doc__ % completions, + argv=argv, + version=completions['version'], + ) - return BeatEndpoint + from ..utils import setup_logger + logger = setup_logger('beat.editor', args['--verbose']) -def main(): + from flask import Flask, request + from flask_restful import Api + from flask_cors import CORS + from ..resources import Home, Settings, Layout, Templates, Environments + from ..resources import VALID_ENTITIES, gen_endpoint app = Flask(__name__) api = Api(app) @@ -459,8 +75,7 @@ def main(): api.add_resource(Layout, '/layout') api.add_resource(Templates, '/templates') api.add_resource(Environments, '/environments') - for entity in BeatEntity: - val = entity.value - api.add_resource(gen_beat_endpoint(entity), '/' + val) + for entity in VALID_ENTITIES: + api.add_resource(gen_endpoint(entity), '/' + entity) - app.run(debug=True) + app.run(debug=args['--debug']) diff --git a/beat/editor/templates.py b/beat/editor/templates.py deleted file mode 100644 index 044afa94..00000000 --- a/beat/editor/templates.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - - -from jinja2 import Environment, PackageLoader - -ENV = Environment(loader=PackageLoader(__name__, 'templates')) -"""Jinja2 environment for loading our templates""" - - -def generate_database(views=None): - """Generates a BEAT database template - - Parameters: - - views (:py:class:`list`, Optional): A list of strings that represents the - views for the database - - - Returns: - - str: The rendered template as a string - - """ - - views = views or ['View'] - template = env.get_template('database.jinja2') - s = template.render(views=views) - return s - - -def generate_library(uses=None): - """Parameters: - - uses : A dict of other libraries that the library uses. - Keys are the value to reference the library, values are the library being referenced. - """ - - uses = uses or {} - template = env.get_template('library.jinja2') - s = template.render(uses=uses) - return s - -def generate_algorithm(has_parameters=False, uses={}): - """Parameters: - - has_parameters: Whether the algorithm has parameters or not (default: False) - - uses : A dict of libraries that the algorithm uses. - Keys are the value to reference the library, values are the library being referenced. - """ - template = env.get_template('algorithm.jinja2') - s = template.render(uses=uses, has_parameters=has_parameters) - return s diff --git a/beat/editor/utils.py b/beat/editor/utils.py new file mode 100644 index 00000000..2419bd1b --- /dev/null +++ b/beat/editor/utils.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import os +import sys + +import jinja2 + +import logging +logger = logging.getLogger(__name__) + + +ENV = jinja2.Environment(loader=jinja2.PackageLoader(__name__, 'templates')) +"""Jinja2 environment for loading our templates""" + + +def generate_database(views=None): + """Generates a valid BEAT database from our stored template + + + Parameters: + + views (:py:class:`list`, Optional): A list of strings that represents the + views for the database + + + Returns: + + str: The rendered template as a string + + """ + + views = views or ['View'] + template = env.get_template('database.jinja2') + return template.render(views=views) + + +def generate_library(uses=None): + """Generates a valid BEAT library from our stored template + + + Parameters: + + uses (:py:class:`dict`, Optional): A dict of other libraries that the + library uses. Keys are the value to reference the library, values are + the library being referenced. + + + Returns: + + str: The rendered template as a string + + """ + + uses = uses or {} + template = env.get_template('library.jinja2') + return template.render(uses=uses) + + +def generate_algorithm(has_parameters=False, uses=None): + """Generates a valid BEAT algorithm from our stored template + + + Parameters: + + has_parameters (:py:class:`bool`, Optional): Whether the algorithm has + parameters or not (default: False) + + uses (:py:class:`dict`, Optional): A dict of libraries that the algorithm + uses. Keys are the value to reference the library, values are the + library being referenced. + + + Returns: + + str: The rendered template as a string + + """ + + uses = uses or {} + template = env.get_template('algorithm.jinja2') + return template.render(uses=uses, has_parameters=has_parameters) + + +TEMPLATE_FUNCTION = dict( + database = generate_database, + library = generate_library, + algorithm = generate_algorithm, + ) +"""Functions for template instantiation within beat.editor""" + + +def generate_python_template(entity, name, **kwargs): + """Generates a template for a BEAT entity with the given named arguments + + + Parameters: + + entity (str): A valid BEAT entity (valid values are + """ + + s = TEMPLATE_FUNCTION[entity](**kwargs) + + resource_path = os.path.join(get_prefix(), entity) + file_path = os.path.join(resource_path, name) + '.py' + with open(file_path, 'w') as f: f.write(s) + + return s + + +def get_user_conf(): + """Reads & returns the user configuration in a dict""" + + user_conf = {} + if os.path.isfile('./user_conf.json'): + with open('./user_conf.json') as f: + try: + user_conf = json.load(f) + except json.JSONDecodeError: + logger.critical('user_conf.json is invalid!') + raise SystemExit + else: + with open('./user_conf.json', 'w') as f: + user_conf = { + 'prefix': '~' + } + json.dump(user_conf, f) + + + if 'prefix' not in user_conf: + raise Exception('Invalid user_conf: Needs "prefix" key!') + return user_conf + + +def get_prefix(): + """Returns an absolute path to the user-defined BEAT prefix location""" + return os.path.expanduser(get_user_conf()['prefix']) + + +def setup_logger(name, verbosity): + '''Sets up the logging of a script + + + Parameters: + + name (str): The name of the logger to setup + + verbosity (int): The verbosity level to operate with. A value of ``0`` + (zero) means only errors, ``1``, errors and warnings; ``2``, errors, + warnings and informational messages and, finally, ``3``, all types of + messages including debugging ones. + + ''' + + logger = logging.getLogger(name) + formatter = logging.Formatter("%(name)s@%(asctime)s -- %(levelname)s: " \ + "%(message)s") + + _warn_err = logging.StreamHandler(sys.stderr) + _warn_err.setFormatter(formatter) + _warn_err.setLevel(logging.WARNING) + + class _InfoFilter: + def filter(self, record): return record.levelno <= logging.INFO + _debug_info = logging.StreamHandler(sys.stdout) + _debug_info.setFormatter(formatter) + _debug_info.setLevel(logging.DEBUG) + _debug_info.addFilter(_InfoFilter()) + + logger.addHandler(_debug_info) + logger.addHandler(_warn_err) + + + logger.setLevel(logging.ERROR) + if verbosity == 1: logger.setLevel(logging.WARNING) + elif verbosity == 2: logger.setLevel(logging.INFO) + elif verbosity >= 3: logger.setLevel(logging.DEBUG) + + return logger diff --git a/conda/meta.yaml b/conda/meta.yaml index 1b1db4a4..efad8962 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -1,6 +1,5 @@ {% set name = 'beat.editor' %} {% set project_dir = environ.get('RECIPE_DIR') + '/..' %} -{% set nodejs = '8.9.3' %} package: name: {{ name }} @@ -8,7 +7,7 @@ package: build: entry_points: - - beatedit = beat.editor.scripts.server:main + - beateditor = beat.editor.scripts.server:main number: {{ environ.get('BOB_BUILD_NUMBER', 0) }} run_exports: - {{ pin_subpackage(name) }} @@ -32,7 +31,8 @@ requirements: - flask - flask-cors - flask-restful - - enum34 # [py<34] + - docopt + - beat.cmdline test: source_files: @@ -52,6 +52,7 @@ test: - {{ name }} commands: + - beateditor --help - nosetests --with-coverage --cover-package={{ name }} -sv {{ name }} - sphinx-build -aEW {{ project_dir }}/doc {{ project_dir }}/sphinx - sphinx-build -aEb doctest {{ project_dir }}/doc sphinx diff --git a/requirements.txt b/requirements.txt index b705bb93..7881ec20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,5 @@ jinja2 flask flask-cors flask-restful +docopt +beat.cmdline diff --git a/setup.py b/setup.py index 9eb52f76..3b9ccffd 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,7 @@ setup( install_requires=load_requirements('requirements.txt'), entry_points={ 'console_scripts': [ - 'beatedit = beat.editor.scripts.server:main', + 'beateditor = beat.editor.scripts.server:main', ], }, -- GitLab