Repository: rorodata/firefly

Issue:
Make firefly support a config file
It should be possible to specify the specs in an yaml config file instead of passing all command-line arguments. Plan for more options for each function, like output-content-type, input-content-type, endpoint etc.

It would be better to have a version field to the config file so that we can handle old versions if we change the format in future.

Discussion:
anandology: The argument values are not constants, so they should be in lowercase.

On 3:33PM, Sat, Jun 17, 2017 Nabarun Pal <notifications@github.com> wrote:

> *@palnabarun* commented on this pull request.
> ------------------------------
>
> In firefly/main.py
> <https://github.com/rorodata/firefly/pull/15#discussion_r122564373>:
>
> >
>  def main():
>      # ensure current directory is added to sys.path
>      if "" not in sys.path:
>          sys.path.insert(0, "")
>
>      args = parse_args()
> -    functions = load_functions(args.functions)
> +    print(len(args.functions), args.CONFIG_FILE)
> +    if (len(args.functions) > 0 and args.CONFIG_FILE) or (len(args.functions) == 0 and not args.CONFIG_FILE):
> +        raise FireflyError("Invalid arguments provided. Please specify either a config file or a list of functions.")
>
> I thought about that. But the current one seemed more verbose to me. Both
> are okay with me.
>
> —
> You are receiving this because you commented.
>
>
> Reply to this email directly, view it on GitHub
> <https://github.com/rorodata/firefly/pull/15#discussion_r122564373>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/AAAdkUVfpndTH0tzinN7_Zo2Z9XKCmLfks5sE5H0gaJpZM4N8vrJ>
> .
>


Patch:
diff --git a/firefly/main.py b/firefly/main.py
index dd4a4fb..8db6caf 100644
--- a/firefly/main.py
+++ b/firefly/main.py
@@ -1,30 +1,47 @@
+import os
 import sys
 import argparse
 import importlib
+import yaml
 from .app import Firefly
 from .server import FireflyServer
+from .validator import ValidationError, FireflyError
 
 def parse_args():
     p = argparse.ArgumentParser()
     p.add_argument("-b", "--bind", dest="ADDRESS", default="127.0.0.1:8000")
-    p.add_argument("functions", nargs='+', help="functions to serve")
+    p.add_argument("-c", "--config", dest="config_file", default=None)
+    p.add_argument("functions", nargs='*', help="functions to serve")
     return p.parse_args()
 
-def load_function(function_spec):
+def load_function(function_spec, path=None, name=None):
     if "." not in function_spec:
         raise Exception("Invalid function, please specify it as module.function")
 
     mod_name, func_name = function_spec.rsplit(".", 1)
     mod = importlib.import_module(mod_name)
     func = getattr(mod, func_name)
-    return (func_name, func)
+    path = path or "/"+func_name
+    name = name or func_name
+    return (path, name, func)
 
 def load_functions(function_specs):
     return [load_function(function_spec) for function_spec in function_specs]
 
+def parse_config_file(config_file):
+    if not os.path.exists(config_file):
+        raise FireflyError("Specified config file does not exist.")
+    with open(config_file) as f:
+        config_dict = yaml.safe_load(f)
+    return config_dict
+
+def parse_config_data(config_dict):
+    return [(load_function(f["function"], path=f["path"], name=name, ))
+            for name, f in config_dict["functions"].items()]
+
 def add_routes(app, functions):
-    for name, function in functions:
-        app.add_route('/'+name, function, name)
+    for path, name, function in functions:
+        app.add_route(path, function, name)
 
 def main():
     # ensure current directory is added to sys.path
@@ -32,7 +49,14 @@ def main():
         sys.path.insert(0, "")
 
     args = parse_args()
-    functions = load_functions(args.functions)
+
+    if (args.functions and args.CONFIG_FILE) or (not args.functions and not args.CONFIG_FILE):
+        raise FireflyError("Invalid arguments provided. Please specify either a config file or a list of functions.")
+
+    if len(args.functions):
+        functions = load_functions(args.functions)
+    elif args.CONFIG_FILE:
+        functions = parse_config_data(parse_config_file(args.CONFIG_FILE))
 
     app = Firefly()
     add_routes(app, functions)
diff --git a/firefly/validator.py b/firefly/validator.py
index b1a2eaf..bb3a477 100644
--- a/firefly/validator.py
+++ b/firefly/validator.py
@@ -8,6 +8,9 @@ else:
 class ValidationError(Exception):
     pass
 
+class FireflyError(Exception):
+    pass
+
 def validate_args(function, kwargs):
     function_signature = signature(function)
     try:
diff --git a/requirements.txt b/requirements.txt
index 8ec0719..28de9ef 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,5 @@
 gunicorn==19.7.1
 WebOb==1.7.2
 requests==2.18.1
+PyYAML==3.12
 funcsigs==1.0.2 ; python_version < '3'


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
index 7ecfbf2..723def1 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -3,6 +3,7 @@ from firefly.main import load_function
 
 def test_load_functions():
     os.path.exists2 = os.path.exists
-    name, func = load_function("os.path.exists2")
+    path, name, func = load_function("os.path.exists2")
+    assert path == "/exists2"
     assert name == "exists2"
     assert func == os.path.exists