[SEBA-497]

Change to using alpine-grpc-base
Fix issue with nested exceptions causing an error in structlog
Reformat and python3 fixes, v3.5 mock support
Record execution times in the loader

Change-Id: I6d7923818d57012fca32ce44668820de422206d6
diff --git a/src/tosca/__init__.py b/src/tosca/__init__.py
index d4e8062..cbdb48b 100644
--- a/src/tosca/__init__.py
+++ b/src/tosca/__init__.py
@@ -1,4 +1,3 @@
-
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,4 +12,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from __future__ import absolute_import
 
+from .generator import TOSCA_Generator
+
+__all__ = [
+    'TOSCA_Generator',
+]
diff --git a/src/tosca/default.py b/src/tosca/default.py
index 4de0975..02d7de4 100644
--- a/src/tosca/default.py
+++ b/src/tosca/default.py
@@ -1,4 +1,3 @@
-
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,8 +13,12 @@
 # limitations under the License.
 
 
+from __future__ import absolute_import
+
 import os
 
 TOSCA_DEFS_DIR = os.path.dirname(os.path.realpath(__file__)) + "/custom_types"
 TOSCA_RECIPES_DIR = os.path.dirname(os.path.realpath(__file__)) + "/"
-TOSCA_KEYS_DIR = os.path.abspath(os.path.dirname(os.path.realpath(__file__)) + "/../grpc_client")
\ No newline at end of file
+TOSCA_KEYS_DIR = os.path.abspath(
+    os.path.dirname(os.path.realpath(__file__)) + "/../grpc_client"
+)
diff --git a/src/tosca/generator.py b/src/tosca/generator.py
index 1c2dccb..fbaf4e5 100644
--- a/src/tosca/generator.py
+++ b/src/tosca/generator.py
@@ -1,4 +1,3 @@
-
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,24 +12,30 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from xosconfig import Config
-from multistructlog import create_logger
-log = create_logger(Config().get('logging'))
+from __future__ import absolute_import
 
 import os
-from default import TOSCA_DEFS_DIR, TOSCA_KEYS_DIR
-from xosgenx.generator import XOSProcessor, XOSProcessorArgs
+
 from xosapi.xos_grpc_client import Empty
+from xosgenx.generator import XOSProcessor, XOSProcessorArgs
+
+from multistructlog import create_logger
+from xosconfig import Config
+
+from .default import TOSCA_DEFS_DIR, TOSCA_KEYS_DIR
+
+log = create_logger(Config().get("logging"))
+
 
 current_dir = os.path.dirname(os.path.realpath(__file__))
 
-class TOSCA_Generator:
 
+class TOSCA_Generator:
     def clean(self, dir=TOSCA_DEFS_DIR):
         filesToRemove = [f for f in os.listdir(dir)]
         for f in filesToRemove:
-            if not f.startswith('.'):
-                os.remove(dir + '/' + f)
+            if not f.startswith("."):
+                os.remove(dir + "/" + f)
 
     def clean_keys(self, dir=TOSCA_KEYS_DIR):
         keys_fn = os.path.join(dir, "KEYS.py")
@@ -42,23 +47,27 @@
 
         try:
             xproto = client.utility.GetXproto(Empty())
-            args = XOSProcessorArgs(output = TOSCA_DEFS_DIR,
-                                    inputs = str(xproto.xproto),
-                                    target = os.path.join(current_dir, 'xtarget/tosca.xtarget'),
-                                    write_to_file = 'target')
+            args = XOSProcessorArgs(
+                output=TOSCA_DEFS_DIR,
+                inputs=str(xproto.xproto),
+                target=os.path.join(current_dir, "xtarget/tosca.xtarget"),
+                write_to_file="target",
+            )
             XOSProcessor.process(args)
             log.info("[XOS-TOSCA] Recipes generated in %s" % args.output)
-        except Exception as e:
+        except Exception:
             log.exception("[XOS-TOSCA] Failed to generate TOSCA")
 
         try:
             xproto = client.utility.GetXproto(Empty())
-            args = XOSProcessorArgs(output = TOSCA_KEYS_DIR,
-                                    inputs = str(xproto.xproto),
-                                    target = os.path.join(current_dir, 'xtarget/tosca_keys.xtarget'),
-                                    write_to_file = 'single',
-                                    dest_file = 'KEYS.py')
+            args = XOSProcessorArgs(
+                output=TOSCA_KEYS_DIR,
+                inputs=str(xproto.xproto),
+                target=os.path.join(current_dir, "xtarget/tosca_keys.xtarget"),
+                write_to_file="single",
+                dest_file="KEYS.py",
+            )
             XOSProcessor.process(args)
             log.info("[XOS-TOSCA] TOSCA Keys generated in %s" % args.output)
-        except Exception as e:
+        except Exception:
             log.exception("[XOS-TOSCA] Failed to generate TOSCA Keys")
diff --git a/src/tosca/parser.py b/src/tosca/parser.py
index 3f44c50..6643ac8 100644
--- a/src/tosca/parser.py
+++ b/src/tosca/parser.py
@@ -1,4 +1,3 @@
-
 # Copyright 2017-present Open Networking Foundation
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
@@ -13,23 +12,24 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from __future__ import absolute_import
 
+import json
 import os
 from tempfile import NamedTemporaryFile
-from xosconfig import Config
-from multistructlog import create_logger
-log = create_logger(Config().get('logging'))
 
-from toscaparser.tosca_template import ToscaTemplate, ValidationError
-from default import TOSCA_RECIPES_DIR
-from grpc_client.resources import RESOURCES
-from grpc_client.models_accessor import GRPCModelsAccessor
 from grpc._channel import _Rendezvous
-import json
-import traceback
+from grpc_client.models_accessor import GRPCModelsAccessor, GRPCModelsException
+from multistructlog import create_logger
+from toscaparser.tosca_template import ToscaTemplate, ValidationError
+from xosconfig import Config
+
+from .default import TOSCA_RECIPES_DIR
+
+log = create_logger(Config().get("logging"))
+
 
 class TOSCA_Parser:
-
     def compute_dependencies(self, template, models_by_name):
         """
         NOTE this method is augmenting self.template, isn't there a more explicit way to achieve it?
@@ -38,18 +38,18 @@
             nodetemplate.dependencies = []
             nodetemplate.dependencies_names = []
             for reqs in nodetemplate.requirements:
-                for (k,v) in reqs.items():
+                for (k, v) in reqs.items():
                     name = v["node"]
-                    if (name in models_by_name):
+                    if name in models_by_name:
                         nodetemplate.dependencies.append(models_by_name[name])
                         nodetemplate.dependencies_names.append(name)
 
                     # go another level deep, as our requirements can have requirements...
                     # NOTE do we still need to go deep?
-                    for sd_req in v.get("requirements",[]):
+                    for sd_req in v.get("requirements", []):
                         for (sd_req_k, sd_req_v) in sd_req.items():
                             name = sd_req_v["node"]
-                            if (name in models_by_name):
+                            if name in models_by_name:
                                 nodetemplate.dependencies.append(models_by_name[name])
                                 nodetemplate.dependencies_names.append(name)
 
@@ -63,8 +63,8 @@
             values = values | set(v.dependencies_names)
 
         all_nodes = list(keys | values)
-        steps = all_nodes
 
+        steps = all_nodes
 
         # Final order
         order = []
@@ -72,26 +72,26 @@
         # DFS stack, not using recursion
         stack = []
 
-        # Unmarked set
-        unmarked = all_nodes
+        # Unmarked set, alpha sorted to provide determinism not present in dicts
+        unmarked = sorted(all_nodes)
 
         # visiting = [] - skip, don't expect 1000s of nodes, |E|/|V| is small
 
         while unmarked:
             stack.insert(0, unmarked[0])  # push first unmarked
 
-            while (stack):
+            while stack:
                 n = stack[0]
                 add = True
                 try:
                     for m in g[n].dependencies_names:
-                        if (m in unmarked):
+                        if m in unmarked:
                             add = False
                             stack.insert(0, m)
                 except KeyError:
                     pass
-                if (add):
-                    if (n in steps and n not in order):
+                if add:
+                    if n in steps and n not in order:
                         order.append(n)
                     item = stack.pop(0)
                     try:
@@ -99,33 +99,39 @@
                     except ValueError:
                         pass
 
-        noorder = list(set(steps) - set(order))
+        # remove ordred items from steps, then alpha sort unordered items (determinism)
+        noorder = sorted(list(set(steps) - set(order)))
         return order + noorder
 
     @staticmethod
     def populate_model(model, data):
-        for k,v in data.iteritems():
+        for key in sorted(data):
             # NOTE must-exists is a TOSCA implementation choice, remove it before saving the model
-            if k != "must-exist":
+            if key != "must-exist":
                 try:
-                    setattr(model, k, v)
-                except TypeError, e:
-                    raise Exception('Failed to set %s on field %s for class %s, Exception was: "%s"' % (v, k, model.model_name, e))
+                    setattr(model, key, data[key])
+                except TypeError as e:
+                    raise Exception(
+                        'Failed to set %s on field %s for class %s, Exception was: "%s"'
+                        % (data[key], key, model.model_name, e)
+                    )
         return model
 
     @staticmethod
     def _translate_exception(msg):
         readable = []
         for line in msg.splitlines():
-            if line.strip().startswith('MissingRequiredFieldError') or \
-                    line.strip().startswith('UnknownFieldError') or \
-                    line.strip().startswith('ImportError') or \
-                    line.strip().startswith('InvalidTypeError') or \
-                    line.strip().startswith('TypeMismatchError'):
+            if (
+                line.strip().startswith("MissingRequiredFieldError")
+                or line.strip().startswith("UnknownFieldError")
+                or line.strip().startswith("ImportError")
+                or line.strip().startswith("InvalidTypeError")
+                or line.strip().startswith("TypeMismatchError")
+            ):
                 readable.append(line)
 
         if len(readable) > 0:
-            return '\n'.join(readable) + '\n'
+            return "\n".join(readable) + "\n"
         else:
             return msg
 
@@ -147,24 +153,24 @@
     @staticmethod
     def populate_dependencies(model, requirements, saved_models):
         for dep in requirements:
-            class_name = dep.keys()[0]
-            related_model = saved_models[dep[class_name]['node']]
+            class_name = list(dep.keys())[0]
+            related_model = saved_models[dep[class_name]["node"]]
             setattr(model, "%s_id" % class_name, related_model.id)
         return model
 
     @staticmethod
     def add_dependencies(data, requirements, saved_models):
         for dep in requirements:
-            class_name = dep.keys()[0]
-            related_model = saved_models[dep[class_name]['node']]
+            class_name = list(dep.keys())[0]
+            related_model = saved_models[dep[class_name]["node"]]
             data["%s_id" % class_name] = related_model.id
         return data
 
     def __init__(self, recipe, username, password, **kwargs):
 
         self.delete = False
-        if 'delete' in kwargs:
-            self.delete = True
+        if "delete" in kwargs:
+            self.delete = kwargs["delete"]
 
         # store username/password combination to read resources
         self.username = username
@@ -187,7 +193,9 @@
 
         try:
             # [] save the recipe to a tmp file
-            with NamedTemporaryFile(delete=False, suffix=".yaml", dir=TOSCA_RECIPES_DIR) as recipe_file:
+            with NamedTemporaryFile(
+                mode="w", delete=False, suffix=".yaml", dir=TOSCA_RECIPES_DIR
+            ) as recipe_file:
                 try:
                     recipe_file.write(self.recipe)
                     recipe_file.close()
@@ -203,49 +211,65 @@
             # [] compute requirements
             self.compute_dependencies(self.template, self.templates_by_model_name)
             # [] topsort requirements
-            self.ordered_models_name = self.topsort_dependencies(self.templates_by_model_name)
+            self.ordered_models_name = self.topsort_dependencies(
+                self.templates_by_model_name
+            )
             # [] topsort templates
-            self.ordered_models_template = self.get_ordered_models_template(self.ordered_models_name, self.templates_by_model_name)
+            self.ordered_models_template = self.get_ordered_models_template(
+                self.ordered_models_name, self.templates_by_model_name
+            )
 
             for recipe in self.ordered_models_template:
                 try:
                     # get properties from tosca
-                    if not 'properties' in recipe.templates[recipe.name]:
+                    if "properties" not in recipe.templates[recipe.name]:
                         data = {}
                     else:
-                        data = recipe.templates[recipe.name]['properties']
-                        if data == None:
+                        data = recipe.templates[recipe.name]["properties"]
+                        if data is None:
                             data = {}
                     # [] get model by class name
                     class_name = recipe.type.replace("tosca.nodes.", "")
 
                     # augemnt data with relations
-                    data = self.add_dependencies(data, recipe.requirements, self.saved_model_by_name)
+                    data = self.add_dependencies(
+                        data, recipe.requirements, self.saved_model_by_name
+                    )
 
-                    model = GRPCModelsAccessor.get_model_from_classname(class_name, data, self.username, self.password)
+                    model = GRPCModelsAccessor.get_model_from_classname(
+                        class_name, data, self.username, self.password
+                    )
                     # [] populate model with data
                     model = self.populate_model(model, data)
                     # [] check if the model has requirements
                     # [] if it has populate them
-                    model = self.populate_dependencies(model, recipe.requirements, self.saved_model_by_name)
+                    model = self.populate_dependencies(
+                        model, recipe.requirements, self.saved_model_by_name
+                    )
                     # [] save, update or delete
 
                     reference_only = False
-                    if 'must-exist' in data:
+                    if "must-exist" in data:
                         reference_only = True
 
                     if self.delete and not model.is_new and not reference_only:
-                        log.info("[XOS-Tosca] Deleting model %s[%s]" % (class_name, model.id))
+                        log.info(
+                            "[XOS-Tosca] Deleting model %s[%s]" % (class_name, model.id)
+                        )
                         model.delete()
                     elif not self.delete:
-                        log.info("[XOS-Tosca] Saving model %s[%s]" % (class_name, model.id))
+                        log.info(
+                            "[XOS-Tosca] Saving model %s[%s]" % (class_name, model.id)
+                        )
                         model.save()
 
-
                     self.saved_model_by_name[recipe.name] = model
-                except Exception, e:
-                    log.exception("[XOS-TOSCA] Failed to save model: %s [%s]" % (class_name, recipe.name))
-                    raise e
+
+                except GRPCModelsException as e:
+                    raise Exception(
+                        "[XOS-TOSCA] Failed to save or delete model %s [%s]: %s"
+                        % (class_name, recipe.name, str(e))
+                    )
 
         except ValidationError as e:
             if e.message:
@@ -254,17 +278,15 @@
                 exception_msg = TOSCA_Parser._translate_exception(str(e))
             raise Exception(exception_msg)
 
-        except _Rendezvous, e:
+        except _Rendezvous as e:
             try:
                 details = json.loads(e._state.details)
                 exception_msg = details["error"]
                 if "specific_error" in details:
-                    exception_msg = "%s: %s" % (exception_msg, details["specific_error"])
+                    exception_msg = "%s: %s" % (
+                        exception_msg,
+                        details["specific_error"],
+                    )
             except Exception:
                 exception_msg = e._state.details
             raise Exception(exception_msg)
-        except Exception, e:
-            log.exception(e)
-            raise Exception(e)
-
-
diff --git a/src/tosca/xtarget/tosca_keys.xtarget b/src/tosca/xtarget/tosca_keys.xtarget
index 9569061..6b73cb3 100644
--- a/src/tosca/xtarget/tosca_keys.xtarget
+++ b/src/tosca/xtarget/tosca_keys.xtarget
@@ -2,4 +2,5 @@
 {%- for m in proto.messages %}
     '{{ m.name }}': {{ xproto_fields_to_tosca_keys(m.fields + xproto_base_fields(m, proto.message_table), m) }},
 {%- endfor %}
-}
\ No newline at end of file
+}
+