From 09e6b71aca5c8cc8f18ec9725368550911319792 Mon Sep 17 00:00:00 2001
From: Ali Sinan Saglam <asinansaglam@gmail.com>
Date: Mon, 20 Sep 2021 11:14:00 -0400
Subject: [PATCH 1/6] version tick and changelog update

---
 CHANGELOG.md             | 5 ++++-
 bionetgen/assets/VERSION | 2 +-
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c2717d0..b2fb5c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -134,4 +134,7 @@ Bugfix where the libroadrunner simulator object was not handled correctly.
 New info subcommand, major updates to test suite, some updates to error reporting. 
 
 ## 0.4.5
-Early development version of a new visualize subcommand that automatically runs a visualize action on a model and returns the resulting file. New require keyword that quits if the current version is not equal to or greater than the required one. 
\ No newline at end of file
+Early development version of a new visualize subcommand that automatically runs a visualize action on a model and returns the resulting file. New require keyword that quits if the current version is not equal to or greater than the required one. 
+
+## 0.4.6
+Minor bugfix for notebook template, numpy requirement removed for issue #11, fixes for issues #15, #16 and partially #21. 
\ No newline at end of file
diff --git a/bionetgen/assets/VERSION b/bionetgen/assets/VERSION
index 98da97b..7704fd4 100644
--- a/bionetgen/assets/VERSION
+++ b/bionetgen/assets/VERSION
@@ -1 +1 @@
-0 4 5 alpha 0
\ No newline at end of file
+0 4 6 alpha 0
\ No newline at end of file

From a48d3f788f9d3771dd205fba4f106987c99e9ce8 Mon Sep 17 00:00:00 2001
From: Ali Sinan Saglam <asinansaglam@gmail.com>
Date: Mon, 20 Sep 2021 13:26:37 -0400
Subject: [PATCH 2/6] first attempt to fix #24, will need cleanup

---
 bionetgen/modelapi/bngfile.py | 44 ++++++++++++++++++++++++++++-------
 1 file changed, 35 insertions(+), 9 deletions(-)

diff --git a/bionetgen/modelapi/bngfile.py b/bionetgen/modelapi/bngfile.py
index f091121..3f52006 100644
--- a/bionetgen/modelapi/bngfile.py
+++ b/bionetgen/modelapi/bngfile.py
@@ -1,8 +1,5 @@
 import bionetgen as bng
-import subprocess
-import os
-import xmltodict
-import sys
+import os, re
 
 from bionetgen.main import BioNetGen
 from .utils import find_BNG_path, run_command, ActionList
@@ -104,14 +101,43 @@ def strip_actions(self, model_path, folder) -> str:
         # open model and strip actions
         with open(model_path, "r", encoding="UTF-8") as mf:
             # read and strip actions
-            mlines = mf.readlines()
-            stripped_lines = filter(lambda x: self._not_action(x), mlines)
-            self.parsed_actions = list(
-                filter(lambda x: not self._not_action(x), mlines)
-            )
+            mstr = mf.read()
+            # this removes any new line escapes (\ \n) to continue
+            # to another line, so we can just remove the action lines
+            mstr = re.sub(r"\\\n", "", mstr)
+            mlines = mstr.split("\n")
+            stripped_lines = list(filter(lambda x: self._not_action(x), mlines))
+            # let's remove begin/end actions, rarely used but should be removed
+            remove_from = -1
+            remove_to = -1
+            for iline, line in enumerate(stripped_lines):
+                if re.match(r"\s*(begin)\s+(actions)\s*", line):
+                    remove_from = iline
+                elif re.match(r"\s*(end)\s+(actions)\s*", line):
+                    remove_to = iline
+            if remove_from > 0:
+                # we have a begin/end actions block
+                if remove_to < 0:
+                    raise RuntimeError(
+                        f'There is a "begin actions" statement at line {remove_from} without a matching "end actions" statement'
+                    )
+                stripped_lines = (
+                    stripped_lines[:remove_from] + stripped_lines[remove_to + 1 :]
+                )
+            if remove_to > 0:
+                if remove_from < 0:
+                    raise RuntimeError(
+                        f'There is an "end actions" statement at line {remove_to} without a matching "begin actions" statement'
+                    )
+            # remove spaces, actions don't allow them
+            self.parsed_actions = [
+                x.replace(" ", "")
+                for x in filter(lambda x: not self._not_action(x), mlines)
+            ]
         # TODO: read stripped lines and store the actions
         # open new file and write just the model
         stripped_model = os.path.join(folder, model_file)
+        stripped_lines = [x + "\n" for x in stripped_lines]
         with open(stripped_model, "w") as sf:
             sf.writelines(stripped_lines)
         return stripped_model

From 82be1fe5181eff1b41a50cbb0038957edd612aee Mon Sep 17 00:00:00 2001
From: Ali Sinan Saglam <asinansaglam@gmail.com>
Date: Tue, 21 Sep 2021 11:09:46 -0400
Subject: [PATCH 3/6] First attempt at fixing #18

---
 bionetgen/modelapi/blocks.py  | 15 ++++++++++-----
 bionetgen/modelapi/bngfile.py | 11 ++++++-----
 bionetgen/modelapi/model.py   |  9 ++++++++-
 bionetgen/modelapi/utils.py   | 12 +++++++++++-
 4 files changed, 35 insertions(+), 12 deletions(-)

diff --git a/bionetgen/modelapi/blocks.py b/bionetgen/modelapi/blocks.py
index d42babb..823df1b 100644
--- a/bionetgen/modelapi/blocks.py
+++ b/bionetgen/modelapi/blocks.py
@@ -541,17 +541,22 @@ class ActionBlock(ModelBlock):
     def __init__(self) -> None:
         super().__init__()
         self.name = "actions"
-        AList = ActionList()
-        self._action_list = AList.possible_types
+        self.AList = ActionList()
+        self._action_list = self.AList.possible_types
         self.items = []
+        self.before_model = []
 
     def __setattr__(self, name, value) -> None:
         self.__dict__[name] = value
 
     def add_item(self, item_tpl) -> None:
         name, value = item_tpl
-        # set the line
-        self.items.append(value)
+        # check to see if it's a before model action
+        if self.AList.is_before_model(name):
+            self.before_model.append(value)
+        else:
+            # set the line
+            self.items.append(value)
 
     def __repr__(self) -> str:
         # overwrites what the class representation
@@ -579,7 +584,7 @@ def __iter__(self):
         return self.items.__iter__()
 
     def __contains__(self, key) -> bool:
-        return key in self.items
+        return (key in self.items) or (key in [x.name for x in self.items])
 
     def add_action(self, action_type, action_args) -> None:
         """
diff --git a/bionetgen/modelapi/bngfile.py b/bionetgen/modelapi/bngfile.py
index 3f52006..00b5af5 100644
--- a/bionetgen/modelapi/bngfile.py
+++ b/bionetgen/modelapi/bngfile.py
@@ -102,11 +102,17 @@ def strip_actions(self, model_path, folder) -> str:
         with open(model_path, "r", encoding="UTF-8") as mf:
             # read and strip actions
             mstr = mf.read()
+            # TODO: Clean this up _a lot_
             # this removes any new line escapes (\ \n) to continue
             # to another line, so we can just remove the action lines
             mstr = re.sub(r"\\\n", "", mstr)
             mlines = mstr.split("\n")
             stripped_lines = list(filter(lambda x: self._not_action(x), mlines))
+            # remove spaces, actions don't allow them
+            self.parsed_actions = [
+                x.replace(" ", "")
+                for x in filter(lambda x: not self._not_action(x), mlines)
+            ]
             # let's remove begin/end actions, rarely used but should be removed
             remove_from = -1
             remove_to = -1
@@ -129,11 +135,6 @@ def strip_actions(self, model_path, folder) -> str:
                     raise RuntimeError(
                         f'There is an "end actions" statement at line {remove_to} without a matching "begin actions" statement'
                     )
-            # remove spaces, actions don't allow them
-            self.parsed_actions = [
-                x.replace(" ", "")
-                for x in filter(lambda x: not self._not_action(x), mlines)
-            ]
         # TODO: read stripped lines and store the actions
         # open new file and write just the model
         stripped_model = os.path.join(folder, model_file)
diff --git a/bionetgen/modelapi/model.py b/bionetgen/modelapi/model.py
index 5db9ee9..1e9b3d1 100644
--- a/bionetgen/modelapi/model.py
+++ b/bionetgen/modelapi/model.py
@@ -99,7 +99,14 @@ def __str__(self):
         """
         write the model to str
         """
-        model_str = "begin model\n"
+        model_str = ""
+        # gotta check for "before model" type actions
+        if hasattr(self, "actions"):
+            ablock = getattr(self, "actions")
+            if len(ablock.before_model) > 0:
+                for baction in ablock.before_model:
+                    model_str += str(baction) + "\n"
+        model_str += "begin model\n"
         for block in self.block_order:
             # ensure we didn't get new items into a
             # previously inactive block, if we did
diff --git a/bionetgen/modelapi/utils.py b/bionetgen/modelapi/utils.py
index aa96537..55ff249 100644
--- a/bionetgen/modelapi/utils.py
+++ b/bionetgen/modelapi/utils.py
@@ -42,6 +42,12 @@ def __init__(self):
             "resetConcentrations",
             "resetParameters",
         ]
+        self.before_model = [
+            "setModelName",
+            "substanceUnits",
+            "version",
+            "setOption",
+        ]
         self.possible_types = (
             self.normal_types + self.no_setter_syntax + self.square_braces
         )
@@ -388,7 +394,11 @@ def __init__(self):
         self.arg_dict["saveConcentrations"] = []
         self.arg_dict["resetConcentrations"] = []
         self.arg_dict["resetParameters"] = []
-
+    
+    def is_before_model(self, action_name):
+        if action_name in self.before_model:
+            return True
+        return False
 
 def find_BNG_path(BNGPATH=None):
     """

From 65a15a70a63e57e1abd6fd711a42dc81443cf773 Mon Sep 17 00:00:00 2001
From: Ali Sinan Saglam <asinansaglam@gmail.com>
Date: Tue, 21 Sep 2021 11:10:01 -0400
Subject: [PATCH 4/6] ran black on previous fix

---
 bionetgen/modelapi/utils.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/bionetgen/modelapi/utils.py b/bionetgen/modelapi/utils.py
index 55ff249..edae48c 100644
--- a/bionetgen/modelapi/utils.py
+++ b/bionetgen/modelapi/utils.py
@@ -394,12 +394,13 @@ def __init__(self):
         self.arg_dict["saveConcentrations"] = []
         self.arg_dict["resetConcentrations"] = []
         self.arg_dict["resetParameters"] = []
-    
+
     def is_before_model(self, action_name):
         if action_name in self.before_model:
             return True
         return False
 
+
 def find_BNG_path(BNGPATH=None):
     """
     A simple function finds the path to BNG2.pl from

From 872fc1afdb461391386ef4940d8a0d53ad7a5d0b Mon Sep 17 00:00:00 2001
From: Ali Sinan Saglam <asinansaglam@gmail.com>
Date: Tue, 21 Sep 2021 11:22:05 -0400
Subject: [PATCH 5/6] fixing action test case for the new action scheme

---
 tests/test_bionetgen.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/test_bionetgen.py b/tests/test_bionetgen.py
index da08287..7f06fa9 100644
--- a/tests/test_bionetgen.py
+++ b/tests/test_bionetgen.py
@@ -85,7 +85,7 @@ def test_action_loading():
     # tests a BNGL file containing all BNG actions
     all_action_model = os.path.join(*[tfold, "models", "actions", "all_actions.bngl"])
     m1 = bng.bngmodel(all_action_model)
-    assert len(m1.actions) == 29
+    assert len(m1.actions) + len(m1.actions.before_model) == 29
 
     no_action_model = os.path.join(*[tfold, "models", "actions", "no_actions.bngl"])
     m2 = bng.bngmodel(no_action_model)

From f34e29a0c0dac869fa7e43f64e3cacabb8d5f070 Mon Sep 17 00:00:00 2001
From: Ali Sinan Saglam <asinansaglam@gmail.com>
Date: Tue, 21 Sep 2021 13:14:55 -0400
Subject: [PATCH 6/6] change to allow for "<" in observable blocks to be read
 by standard XML readers

---
 bionetgen/modelapi/bngparser.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/bionetgen/modelapi/bngparser.py b/bionetgen/modelapi/bngparser.py
index 8c266f2..f52ea00 100644
--- a/bionetgen/modelapi/bngparser.py
+++ b/bionetgen/modelapi/bngparser.py
@@ -62,14 +62,18 @@ def _parse_model_bngpl(self, model_obj) -> None:
             with TemporaryFile("w+") as xml_file:
                 if self.bngfile.generate_xml(xml_file):
                     # TODO: Add verbosity option to the library
-                    # print("Parsing")
-                    self.parse_xml(xml_file.read(), model_obj)
+                    xmlstr = xml_file.read()
+                    # < is not a valid XML character, we need to replace it
+                    xmlstr = xmlstr.replace('relation="<', 'relation="&lt;')
+                    self.parse_xml(xmlstr, model_obj)
                     model_obj.reset_compilation_tags()
                 else:
                     raise ValueError("XML file couldn't be generated")
         elif model_file.endswith(".xml"):
             with open(model_file, "r") as f:
                 xml_str = f.read()
+                # < is not a valid XML character, we need to replace it
+                xmlstr = xml_str.replace('relation="<', 'relation="&lt;')
                 self.parse_xml(xml_str, model_obj)
             model_obj.reset_compilation_tags()
         else: