Feature #19: a user can now create a new composition with fine-grained limits (#22)

Signed-off-by: Pedro Escaleira <pedroescaleira@hotmail.com>
diff --git a/src/conductor/conductor.py b/src/conductor/conductor.py
index 783730d..00339cc 100644
--- a/src/conductor/conductor.py
+++ b/src/conductor/conductor.py
@@ -72,7 +72,7 @@
         *composition["annotations"]
     ]
 
-    return { 'name': composition['name'], 'action': { 'exec': { 'kind': 'python:3', 'code':code }, 'annotations': annotations } }
+    return { 'name': composition['name'], 'action': { 'exec': { 'kind': 'python:3', 'code':code }, 'annotations': annotations, 'limits': composition['limits'] } }
 
 def openwhisk(options):
     ''' return enhanced openwhisk client capable of deploying compositions '''
diff --git a/src/pydeploy/__main__.py b/src/pydeploy/__main__.py
index 548358a..36414df 100644
--- a/src/pydeploy/__main__.py
+++ b/src/pydeploy/__main__.py
@@ -22,86 +22,147 @@
 import conductor
 import sys
 
-def keyValue(a):
-    parts = a.partition('=')
-    if parts[1] != '=':
-        raise 'Annotation syntax must be "KEY=VALUE"'
-    return { 'key': parts[0], 'value': parts[2] }
 
-def keyValueFromFile(a):
-    parts = a.partition('=')
-    if parts[1] != '=':
-        raise 'Annotation syntax must be "KEY=FILE"'
+def key_value_arg_verification(a: str):
+    parts = a.partition("=")
+    if parts[1] != "=":
+        raise Exception('Annotation syntax must be "KEY=VALUE"')
+    return parts
 
-    with open(parts[2], encoding='UTF-8') as f:
+
+def annotation_key_value(a: str):
+    parts = key_value_arg_verification(a)
+    return {"key": parts[0], "value": parts[2]}
+
+
+def annotation_key_value_file(a: str):
+    parts = key_value_arg_verification(a)
+    with open(parts[2], encoding="UTF-8") as f:
         value = json.load(f)
 
-    return { 'key': parts[0], 'value': value }
+    return {"key": parts[0], "value": value}
 
 
 def main():
-    parser = argparse.ArgumentParser(description='deploy composition', prog='pydeploy', usage='%(prog)s composition composition.json [flags]')
-    parser.add_argument('name', metavar='composition', type=str, help='composition name')
-    parser.add_argument('file', metavar='composition', type=str, help='composition')
-    parser.add_argument('--apihost', action='store', metavar='HOST', help='API HOST')
-    parser.add_argument('-i', '--insecure', action='store_true', help='bypass certificate checking')
-    parser.add_argument('-u', '--auth', metavar='KEY', help='authorization KEY')
-    parser.add_argument('-v', '--version', action='version', version='%(prog)s '+ composer.__version__)
-    parser.add_argument('-a', '--annotation', action='append', nargs=1, help='add KEY annotation with VALUE')
-    parser.add_argument('-A', '--annotation-file', action='append', nargs=1, help='add KEY annotation with FILE content')
-    parser.add_argument('-w', '--overwrite',  action='store_true', help='overwrite actions if already defined')
+    parser = argparse.ArgumentParser(
+        description="deploy composition",
+        prog="pydeploy",
+        usage="%(prog)s composition composition.json [flags]",
+    )
+    parser.add_argument(
+        "name", metavar="composition", type=str, help="composition name"
+    )
+    parser.add_argument("file", metavar="composition", type=str, help="composition")
+    parser.add_argument("--apihost", action="store", metavar="HOST", help="API HOST")
+    parser.add_argument(
+        "-i", "--insecure", action="store_true", help="bypass certificate checking"
+    )
+    parser.add_argument("-u", "--auth", metavar="KEY", help="authorization KEY")
+    parser.add_argument(
+        "-v", "--version", action="version", version="%(prog)s " + composer.__version__
+    )
+    parser.add_argument(
+        "-a",
+        "--annotation",
+        action="append",
+        nargs=1,
+        help="add KEY annotation with VALUE",
+    )
+    parser.add_argument(
+        "-A",
+        "--annotation-file",
+        action="append",
+        nargs=1,
+        help="add KEY annotation with FILE content",
+    )
+    parser.add_argument("-l", "--limits", nargs=1, help="define limits for this composition, providing a JSON dictionary")
+    parser.add_argument(
+        "-L",
+        "--limits-file",
+        nargs=1,
+        help="define limits for this composition, providing a JSON file",
+    )
+    parser.add_argument(
+        "-w",
+        "--overwrite",
+        action="store_true",
+        help="overwrite actions if already defined",
+    )
 
     args = parser.parse_args()
 
     try:
         filename = args.file
 
-        with open(filename, encoding='UTF-8') as f:
+        with open(filename, encoding="UTF-8") as f:
             composition = json.load(f)
 
-        if 'ast' not in composition:
-            raise 'Composition must have a field "ast" of type dictionary'
-        if 'composition' not in composition:
-            raise 'Composition must have a field "composition" of type dictionary'
-        if 'version' not in composition:
-            raise 'Composition must have a field "composition" of type dictionary'
-        if 'actions' in composition:
-            if not isinstance(composition['actions'], list):
-                raise 'Optional field "actions" must be an array'
+        if "ast" not in composition:
+            raise Exception('Composition must have a field "ast" of type dictionary')
+        if "composition" not in composition:
+            raise Exception(
+                'Composition must have a field "composition" of type dictionary'
+            )
+        if "version" not in composition:
+            raise Exception(
+                'Composition must have a field "composition" of type dictionary'
+            )
+        if "actions" in composition:
+            if not isinstance(composition["actions"], list):
+                raise Exception('Optional field "actions" must be an array')
 
-        composition['annotations'] = []
+        composition["annotations"] = []
+        composition["limits"] = {}
 
         if args.annotation is not None:
-            composition['annotations'].extend([keyValue(a[0]) for a in args.annotation])
+            composition["annotations"].extend(
+                [annotation_key_value(a[0]) for a in args.annotation]
+            )
 
         if args.annotation_file is not None:
-            composition['annotations'].extend([keyValueFromFile(a[0]) for a in args.annotation_file])
+            composition["annotations"].extend(
+                [annotation_key_value_file(a[0]) for a in args.annotation_file]
+            )
+
+        if args.limits is not None:
+            composition["limits"].update(
+                (lambda limits: json.loads(limits))(args.limits[0])
+            )
+
+        if args.limits_file is not None:
+            composition["limits"].update(
+                (lambda name: json.load(open(name, encoding="UTF-8")))(
+                    args.limits_file[0]
+                )
+            )
 
     except Exception as err:
+        raise err
         print(err)
-        sys.exit(422 - 256) # Unprocessable Entity
+        sys.exit(422 - 256)  # Unprocessable Entity
 
-
-    options = { 'ignore_certs': args.insecure }
+    options = {"ignore_certs": args.insecure}
     if args.apihost is not None:
-        options['apihost'] = args.apihost
+        options["apihost"] = args.apihost
     if args.auth is not None:
-        options['api_key'] = args.auth
+        options["api_key"] = args.auth
 
     try:
-        composition['name'] = composer.parse_action_name(args.name)
+        composition["name"] = composer.parse_action_name(args.name)
     except Exception as err:
         print(err)
-        sys.exit(400 - 256) # Bad Request
+        sys.exit(400 - 256)  # Bad Request
 
     try:
-        actions = conductor.openwhisk(options).compositions.deploy(composition, args.overwrite)
-        names = ' '.join([n['name'] for n in actions])
-        print('ok: created action'+ ('s' if len(names) > 1 else '') + '' + names)
+        actions = conductor.openwhisk(options).compositions.deploy(
+            composition, args.overwrite
+        )
+        names = " ".join([n["name"] for n in actions])
+        print("ok: created action" + ("s" if len(names) > 1 else "") + "" + names)
     except Exception as err:
         print(err.error)
         sys.exit(500 - 256)
 
-if __name__ == '__main__':
-    main()
 
+if __name__ == "__main__":
+    main()