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