Repository: urbanopt/modelica-builder (language: python)

Issue:
Add method for getting and setting parameters
Parameters fall under a different type_prefix. This PR adds the `get` and `update` method for a parameter.


Discussion / hints:

Fix patch:
diff --git a/.cspell.json b/.cspell.json
index a7915f0..f0eb92a 100644
--- a/.cspell.json
+++ b/.cspell.json
@@ -23,10 +23,12 @@
     "Modelica",
     "MODELICAPATH",
     "mofile",
+    "mpignore",
     "nllong",
     "openstudio",
     "Optimica",
     "oversizing",
+    "pathspec",
     "pygments",
     "redeclarations",
     "Reparse",
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 50c3b21..2f1d93b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,7 +9,7 @@ jobs:
       fail-fast: false
       matrix:
         os: [ubuntu-latest]
-        python-version: ["3.9", "3.11"]
+        python-version: ["3.9", "3.12"]
     runs-on: ${{ matrix.os }}
     steps:
       - uses: actions/checkout@v3
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index da038c2..a461241 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,7 +6,7 @@ exclude: |
 
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.4.0
+    rev: v4.5.0
     hooks:
       - id: trailing-whitespace
       - id: check-added-large-files
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 9c032b5..ce96840 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,6 +2,19 @@
 Changelog
 =========
 
+Version 0.5.0
+=============
+
+## What's Changed
+* Support py312 by @vtnate in https://github.com/urbanopt/modelica-builder/pull/73
+* Add method to scale loads in an MOS file by @nllong in https://github.com/urbanopt/modelica-builder/pull/74
+* Bump version of antlr runtime to 4.13.1 by @nllong in https://github.com/urbanopt/modelica-builder/pull/75
+* Add `.mpignore` to skip loading files in ModelicaProject by @nllong in https://github.com/urbanopt/modelica-builder/pull/77
+* Upgrade to MBLv10 by @vtnate in https://github.com/urbanopt/modelica-builder/pull/72
+* Update copyright dates by @nllong in https://github.com/urbanopt/modelica-builder/pull/80
+
+**Full Changelog**: https://github.com/urbanopt/modelica-builder/compare/v0.4.0...v0.5.0
+
 Version 0.4.0
 =============
 
diff --git a/LICENSE.txt b/LICENSE.txt
index 19341d8..12c7f47 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,29 +1,26 @@
-Copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
+Copyright (c) 2020, 2024, Alliance for Sustainable Energy, LLC.
 All rights reserved.
 
 BSD 3-Clause License
 
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
+Redistribution and use in source and binary forms, with or without modification, are permitted
+provided that the following conditions are met:
 
-1. Redistributions of source code must retain the above copyright notice, this
-   list of conditions and the following disclaimer.
+(1) Redistributions of source code must retain the above copyright notice, this list of conditions
+and the following disclaimer.
 
-2. Redistributions in binary form must reproduce the above copyright notice,
-   this list of conditions and the following disclaimer in the documentation
-   and/or other materials provided with the distribution.
+(2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions
+and the following disclaimer in the documentation and/or other materials provided with the
+distribution.
 
-3. Neither the name of the copyright holder nor the names of its
-   contributors may be used to endorse or promote products derived from
-   this software without specific prior written permission.
+(3) Neither the name of the copyright holder nor the names of its contributors may be used to endorse
+or promote products derived from this software without specific prior written permission.
 
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
+IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/docs/conf.py b/docs/conf.py
index a300db8..5ffd534 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -90,7 +90,7 @@
 
 # General information about the project.
 project = u'modelica-builder'
-copyright = u'2023, Nicholas Long'
+copyright = u'2024, Alliance for Sustainable Energy, LLC'
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
diff --git a/modelica_builder/builder.py b/modelica_builder/builder.py
index 9daa752..f31d953 100644
--- a/modelica_builder/builder.py
+++ b/modelica_builder/builder.py
@@ -1,10 +1,5 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
-
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 
 from modelica_builder import config
 from modelica_builder.edit import Edit
diff --git a/modelica_builder/config.py b/modelica_builder/config.py
index 10bc1bb..09a37ef 100644
--- a/modelica_builder/config.py
+++ b/modelica_builder/config.py
@@ -1,9 +1,5 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 
 # global config variables
 # If true, then each inserted component/annotation argument will be indented on a new line
diff --git a/modelica_builder/edit.py b/modelica_builder/edit.py
index a63a5ff..7d2be1f 100644
--- a/modelica_builder/edit.py
+++ b/modelica_builder/edit.py
@@ -1,10 +1,5 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
-
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 
 from modelica_builder import modelica_parser
 
diff --git a/modelica_builder/model.py b/modelica_builder/model.py
index eb4c663..236ca78 100644
--- a/modelica_builder/model.py
+++ b/modelica_builder/model.py
@@ -1,9 +1,5 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 
 import logging
 import os
diff --git a/modelica_builder/modelica_mos_file.py b/modelica_builder/modelica_mos_file.py
index e920d15..ade6288 100644
--- a/modelica_builder/modelica_mos_file.py
+++ b/modelica_builder/modelica_mos_file.py
@@ -1,27 +1,35 @@
 # :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
 # See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 
+import copy
+import logging
 import re
 from pathlib import Path
 from typing import Any, Union
 
+logger = logging.getLogger(__name__)
+
 
 class ModelicaMOS(object):
-    def __init__(self, filename: str, data: str = None):
+    def __init__(self, filename: str, header_data: str = None, data_definition: str = None):
         """Read in a .mos file if it exists into a data object
         The format is CSV with additional header info, so for
-        now read the data as a string.
+        now read the data as a string and then convert the timeseries
+        data as floats.
 
         Args:
             filename (str): Name of the file to import, typically a full path
+            header_data (str, optional): header information as a string. Defaults to None.
+            data_definition (str, optional): The type of data without the dimension, e.g., double tab1. Defaults to None.
 
         Raises:
             FileNotFoundError: File does not exist
             Exception: The file extension needs to be MOS
         """
-        # allow reading in the data directly for testing purposes, for the
+        # allow reading in the header_data directly for testing purposes, for the
         # most part.
-        if not data:
+        self.data = []
+        if not header_data:
             if Path(filename).exists():
                 self.filename = Path(filename)
             else:
@@ -30,9 +38,27 @@ def __init__(self, filename: str, data: str = None):
             if self.filename.suffix.lower() != ".mos":
                 raise Exception(f"{filename} is not a .mos file")
 
-            self.data = self.filename.read_text()
+            self.header_data = ""
+            self.data_definition = ""
+            # read the first n-rows until the data are not prepended with a # or is blank
+            for line in self.filename.open():
+                if line.startswith('#') or line.strip() == '':
+                    self.header_data += line
+                else:
+                    # if the line has a ; then it is data
+                    if ';' not in line:
+                        # strip out the dimensions, because we will put
+                        # those in based on the size of data later.
+                        self.data_definition = line.rstrip().split('(')[0]
+                    else:
+                        self.data.append(line.rstrip().split(';'))
+
+            # convert the data types to have first column be int and the remaining float
+            for row, column in enumerate(self.data):
+                self.data[row] = [int(column[0])] + [float(x) for x in column[1:]]
         else:
-            self.data = data
+            self.header_data = header_data
+            self.data_definition = "double tab1"
 
     def retrieve_header_variable_value(self, key: str, cast_type: type = str) -> Any:
         """Retrieve a value from a variable in the header
@@ -46,7 +72,7 @@ def retrieve_header_variable_value(self, key: str, cast_type: type = str) -> Any
         """
         # check if the peak water heating load is zero, otherwise just skip
         key_re = rf'#(\s?{key}\s?)=\s?(-?\b\d[\d,.]*\b)(.*\s)'
-        match = re.search(key_re, self.data)
+        match = re.search(key_re, self.header_data)
         if match:
             # the first group is the variable name, the second is the value
             value = match.group(2).strip()
@@ -77,23 +103,97 @@ def replace_header_variable_value(self, key: str, new_value: Any) -> bool:
         key_re = rf'#(\s?{key}\s?)=\s?(-?\b\d[\d,.]*\b)(.*\s)'
 
         # verify that group3 exists, otherwise the key doesn't exist
-        match = re.search(key_re, self.data)
+        match = re.search(key_re, self.header_data)
         if match:
             if match.group(3):
                 # if there are units, then put the unit back in
-                self.data = re.sub(key_re, '#' + r'\g<1>' + f"= {new_value}" + r'\g<3>', self.data)
+                self.header_data = re.sub(key_re, '#' + r'\g<1>' + f"= {new_value}" + r'\g<3>', self.header_data)
             else:
                 # no g3, therefore, just replace the var and value.
-                self.data = re.sub(key_re, '#' + r'\g<1>' + f"= {new_value}", self.data)
+                self.header_data = re.sub(key_re, '#' + r'\g<1>' + f"= {new_value}", self.header_data)
 
         # replace the value, this requires recreating the variable and value
-        self.data = re.sub(key_re, '#' + r'\g<1>' + f"= {new_value}" + r'\g<3>', self.data)
+        self.header_data = re.sub(key_re, '#' + r'\g<1>' + f"= {new_value}" + r'\g<3>', self.header_data)
+        return True
+
+    def scale_loads(self, scale_factors: list, load_type: Union[str, int, list[int]] = "all") -> bool:
+        """Scale the loads by a factor. The scale factors must look like the following:
+            [
+              {'start_time': Timestamp('2021-01-01 00:00:00'), 'end_time': Timestamp('2021-01-04 23:59:59'), 'scaling_factor': 0.72},
+              {'start_time': Timestamp('2021-05-01 00:00:00'), 'end_time': Timestamp('2021-01-10 23:59:59'), 'scaling_factor': 1.50},
+              ...
+            ]
+
+        This method can be used to scale the loads for a time period in an attempt to match
+        existing data from another source. For example, if we know the total loads per day, then
+        you can scale the loads to meet the total load.
+
+        Args:
+            scale_factors (list): A list of dictionaries with start, end, and value to scale.
+
+            load_type (str, optional): Type of load to scale. Defaults to "all", but can be "heating",
+                                       "cooling", "water_heating", or "all".
+
+        Returns:
+            bool: Always True
+        """
+        # skip if data is empty
+        if not self.data:
+            raise Exception("No data to scale")
+
+        # if passing a string, then convert to the index in the same
+        # format as the load_type version.
+        if isinstance(load_type, str):
+            # column name mappers
+            column_mapper = {
+                "cooling": [1],
+                "heating": [2],
+                "water_heating": [3],
+                "all": [1, 2, 3]
+            }
+        elif isinstance(load_type, int):
+            column_mapper = {
+                "custom": [load_type],
+            }
+            load_type = 'custom'
+        elif isinstance(load_type, list):
+            column_mapper = {
+                "custom": load_type,
+            }
+            load_type = 'custom'
+
+        # create a copy to operate on
+        scale_factor = copy.deepcopy(scale_factors)
+        # convert the start_time and end_time to seconds from the beginning of the year
+        first_time_stamp = scale_factor[0]["start_time"]
+        for scale_factor in scale_factors:
+            scale_factor["start_time_seconds"] = (scale_factor["start_time"] - first_time_stamp).total_seconds()
+            scale_factor["end_time_seconds"] = (scale_factor["end_time"] - first_time_stamp).total_seconds()
+
+        logger.debug(scale_factors)
+
+        # convert the scale_factor datetime to a seconds value
+        for column_index in column_mapper[load_type]:
+            # multiple each row's column by the scale factor
+            for row, column in enumerate(self.data):
+                # get the scaling factor based on the time (inclusive on both sides)
+                for scale_factor in scale_factors:
+                    if scale_factor["start_time_seconds"] <= column[0] <= scale_factor["end_time_seconds"]:
+                        column[column_index] = column[column_index] * scale_factor["scaling_factor"]
+                        self.data[row] = column
+
         return True
 
     def save(self):
         """Save the file back to disk"""
-        self.filename.write_text(self.data)
+        self.save_as(self.filename)
 
     def save_as(self, filename: Union[str, Path]):
         """Save the file to a new filename"""
-        Path(filename).write_text(self.data)
+        # reconstruct the file data
+        data_definition = self.data_definition + f"({len(self.data)},{len(self.data[0])})"
+        write_data = self.header_data + data_definition + '\n'
+        for row in self.data:
+            # join the row with a ; and convert each element to a string
+            write_data += ';'.join([str(x) for x in row]) + '\n'
+        Path(filename).write_text(write_data)
diff --git a/modelica_builder/modelica_parser/README.rst b/modelica_builder/modelica_parser/README.rst
index 111506c..f69ebfa 100644
--- a/modelica_builder/modelica_parser/README.rst
+++ b/modelica_builder/modelica_parser/README.rst
@@ -1,1 +1,4 @@
 These files are automatically generated using Antlr. Do not manipulate directly.
+
+However, the modelica.g4 file has been updated to support functionality that is not yet supported by the community-provided Modelica grammar. Make sure to conduct substantial
+testing if modifying the modelica.g4 file.
diff --git a/modelica_builder/modelica_parser/utils.py b/modelica_builder/modelica_parser/utils.py
index 9a11588..0a2505d 100644
--- a/modelica_builder/modelica_parser/utils.py
+++ b/modelica_builder/modelica_parser/utils.py
@@ -1,9 +1,6 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
+
 
 import sys
 
diff --git a/modelica_builder/modelica_project.py b/modelica_builder/modelica_project.py
index 67a59fd..88f799e 100644
--- a/modelica_builder/modelica_project.py
+++ b/modelica_builder/modelica_project.py
@@ -1,11 +1,9 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
+
 import logging
 import os
+import pathspec
 import time
 from pathlib import Path
 from typing import Union
@@ -86,9 +84,15 @@ def __init__(self, package_file):
         self.root_directory = Path(package_file).parent
         self.file_types = ['.mo', '.txt', '.mos', '.order', ]
         self.file_types_to_skip = ['.log', '.mat', ]
+
         # skip some specific files that software may create that are not needed to be
         # sent around with the modelica project.
-        self.file_names_to_skip = ['analysis_variables.csv', 'analysis_name.txt', '.DS_Store', ]
+        self.file_names_to_skip = ['.DS_Store', '.mpignore']
+
+        # Keep track of the files that have been ignored.
+        self.mpignore_files = []
+
+        # object to store all the files in the package
         self.file_data = {}
 
         self._load_data()
@@ -96,7 +100,25 @@ def __init__(self, package_file):
     def _load_data(self) -> None:
         """method to load all of the files into a data structure for processing"""
         # walk the tree and add in all the files
+
+        # Try to find a .mpignore file in the directory with the package.mo.
+        # Eventually we should support searching up the tree.
+        project_ignore_file = self.root_directory / '.mpignore'
+        if project_ignore_file.exists():
+            patterns = []
+            with open(project_ignore_file) as f:
+                patterns = f.read().splitlines()
+            spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, patterns)
+        else:
+            spec = None
+
         for file_path in self.root_directory.rglob('*'):
+            # if there is a spec, check if the file is in the spec and ignore if matches
+            if spec:
+                if spec.match_file(str(file_path.relative_to(self.root_directory))):
+                    self.mpignore_files.append(file_path)
+                    continue
+
             if file_path.suffix in self.file_types_to_skip and file_path.is_file():
                 # skip files that have the file_types_to_skip suffix
                 continue
@@ -113,16 +135,19 @@ def _load_data(self) -> None:
                 # this is a directory, add in an empty ModelicaFileObject
                 # to keep track of the directory.
                 #
-                # however, we ignore if there is a tmp directory or the parent dir is
-                # tmp. Maybe we need to support more than 2 levels here.
+                # We ignore if there is a tmp directory and display only the
+                # first time the tmp shows up in the tree walking.
                 if 'tmp' in file_path.parts:
-                    _log.warning(f"Found a tmp directory, skipping {file_path}")
+                    if file_path.parts[-1] == 'tmp':
+                        _log.warning(f"Found a tmp directory, skipping {file_path}")
+
+                    # always continue
                     continue
 
                 rel_path = file_path.relative_to(self.root_directory)
                 self.file_data[str(rel_path)] = ModelicaFileObject(file_path)
             else:
-                print(f"Unknown file {file_path}")
+                print(f"Unknown file {file_path}, these can be ignored by creating a .mpignore file in the project root")
 
         # now sort the file_data by the keys
         self.file_data = {key: self.file_data[key] for key in sorted(self.file_data)}
diff --git a/modelica_builder/package_parser.py b/modelica_builder/package_parser.py
index 6aa4e3c..ddf371d 100644
--- a/modelica_builder/package_parser.py
+++ b/modelica_builder/package_parser.py
@@ -77,26 +77,34 @@ def parse_within_statement(self) -> Optional[List[str]]:
         return self.within
 
     @classmethod
-    def new_from_template(cls, path: Union[str, Path], name: str, order: list[str], within: Union[str, None] = None) -> "PackageParser":
-        """Create new package data based on the package.mo template. If within is not specified, then it is
+    def new_from_template(cls,
+                          path: Union[str, Path],
+                          name: str,
+                          order: list[str],
+                          mbl_version: Union[str, None] = None,
+                          within: Union[str, None] = None
+                          ) -> "PackageParser":
+        """Create new package data based on the package.mo template. If 'within' is not specified, then it is
         assumed that this is a top level package and will load from the package_base template.
 
         Args:
             path (str): the path where the resulting package file and order will be saved to.
             name (str): the name of the model
             order (list[str]): ordered list of which models will be loaded (saved to package.order)
-            within (str, optional): name where this package is within.. Defaults to None.
+            mbl_version (str, optional): the version of the model buildings library (only used in package_base.mot)
+            within (str, optional): name where this package is within. Defaults to None.
+
 
         Returns:
             PackageParser: object of the package parser
         """
         klass = PackageParser(path)
-        if within:
+        if not mbl_version:
             template = klass.template_env.get_template("package.mot")
         else:
             template = klass.template_env.get_template("package_base.mot")
 
-        klass.package_data = template.render(within=within, name=name, order=order)
+        klass.package_data = template.render(within=within, name=name, order=order, mbl_version=mbl_version)
         klass.order_data = "\n".join(order)
         klass.package_name = name
         klass.parse_within_statement()
diff --git a/modelica_builder/selector.py b/modelica_builder/selector.py
index 948fa2c..bfce3f6 100644
--- a/modelica_builder/selector.py
+++ b/modelica_builder/selector.py
@@ -1,9 +1,5 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 
 
 import re
diff --git a/modelica_builder/templates/package_base.mot b/modelica_builder/templates/package_base.mot
index b967bfa..e0bf2b8 100644
--- a/modelica_builder/templates/package_base.mot
+++ b/modelica_builder/templates/package_base.mot
@@ -2,5 +2,5 @@ package {{name}}
   extends Modelica.Icons.Package;
 
   annotation (uses(
-    Modelica(version="4.0.0"), Buildings(version="9.1.0")), version="1");
+    Modelica(version="4.0.0"), Buildings(version="{{mbl_version}}")), version="1");
 end {{name}};
diff --git a/modelica_builder/transformation.py b/modelica_builder/transformation.py
index 78fc743..8b65790 100644
--- a/modelica_builder/transformation.py
+++ b/modelica_builder/transformation.py
@@ -1,10 +1,5 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
-
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 
 import logging
 from antlr4.xpath import XPath
diff --git a/modelica_builder/transformer.py b/modelica_builder/transformer.py
index 66e3c10..8f6a48e 100644
--- a/modelica_builder/transformer.py
+++ b/modelica_builder/transformer.py
@@ -1,9 +1,5 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 
 
 from modelica_builder.edit import Edit
diff --git a/poetry.lock b/poetry.lock
index 6412822..5fdad3b 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
 
 [[package]]
 name = "alabaster"
@@ -13,13 +13,13 @@ files = [
 
 [[package]]
 name = "antlr4-python3-runtime"
-version = "4.13.0"
-description = "ANTLR 4.13.0 runtime for Python 3"
+version = "4.13.1"
+description = "ANTLR 4.13.1 runtime for Python 3"
 optional = false
 python-versions = "*"
 files = [
-    {file = "antlr4-python3-runtime-4.13.0.tar.gz", hash = "sha256:0d5454928ae40c8a6b653caa35046cd8492c8743b5fbc22ff4009099d074c7ae"},
-    {file = "antlr4_python3_runtime-4.13.0-py3-none-any.whl", hash = "sha256:53e6e208cf4a1ad53fb8b1b4467b756375a4f827331e290618aedcf481cb1d5c"},
+    {file = "antlr4-python3-runtime-4.13.1.tar.gz", hash = "sha256:3cd282f5ea7cfb841537fe01f143350fdb1c0b1ce7981443a2fa8513fddb6d1a"},
+    {file = "antlr4_python3_runtime-4.13.1-py3-none-any.whl", hash = "sha256:78ec57aad12c97ac039ca27403ad61cb98aaec8a3f9bb8144f889aa0fa28b943"},
 ]
 
 [[package]]
@@ -39,13 +39,13 @@ tomli = {version = "*", markers = "python_version < \"3.11\""}
 
 [[package]]
 name = "babel"
-version = "2.13.0"
+version = "2.14.0"
 description = "Internationalization utilities"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "Babel-2.13.0-py3-none-any.whl", hash = "sha256:fbfcae1575ff78e26c7449136f1abbefc3c13ce542eeb13d43d50d8b047216ec"},
-    {file = "Babel-2.13.0.tar.gz", hash = "sha256:04c3e2d28d2b7681644508f836be388ae49e0cfe91465095340395b60d00f210"},
+    {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"},
+    {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"},
 ]
 
 [package.extras]
@@ -53,13 +53,13 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
 
 [[package]]
 name = "certifi"
-version = "2023.7.22"
+version = "2023.11.17"
 description = "Python package for providing Mozilla's CA Bundle."
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"},
-    {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
+    {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"},
+    {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"},
 ]
 
 [[package]]
@@ -75,101 +75,101 @@ files = [
 
 [[package]]
 name = "charset-normalizer"
-version = "3.3.0"
+version = "3.3.2"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
 optional = false
 python-versions = ">=3.7.0"
 files = [
-    {file = "charset-normalizer-3.3.0.tar.gz", hash = "sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-win32.whl", hash = "sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d"},
-    {file = "charset_normalizer-3.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-win32.whl", hash = "sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786"},
-    {file = "charset_normalizer-3.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-win32.whl", hash = "sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df"},
-    {file = "charset_normalizer-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-win32.whl", hash = "sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c"},
-    {file = "charset_normalizer-3.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-win32.whl", hash = "sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e"},
-    {file = "charset_normalizer-3.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-win32.whl", hash = "sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a"},
-    {file = "charset_normalizer-3.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884"},
-    {file = "charset_normalizer-3.3.0-py3-none-any.whl", hash = "sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2"},
+    {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
+    {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
+    {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
+    {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
+    {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
+    {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
+    {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
+    {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
 ]
 
 [[package]]
@@ -269,13 +269,13 @@ yaml = ["PyYAML (>=3.10)"]
 
 [[package]]
 name = "distlib"
-version = "0.3.7"
+version = "0.3.8"
 description = "Distribution utilities"
 optional = false
 python-versions = "*"
 files = [
-    {file = "distlib-0.3.7-py2.py3-none-any.whl", hash = "sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057"},
-    {file = "distlib-0.3.7.tar.gz", hash = "sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8"},
+    {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
+    {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
 ]
 
 [[package]]
@@ -301,13 +301,13 @@ files = [
 
 [[package]]
 name = "exceptiongroup"
-version = "1.1.3"
+version = "1.2.0"
 description = "Backport of PEP 654 (exception groups)"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"},
-    {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"},
+    {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
+    {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
 ]
 
 [package.extras]
@@ -315,29 +315,29 @@ test = ["pytest (>=6)"]
 
 [[package]]
 name = "filelock"
-version = "3.12.4"
+version = "3.13.1"
 description = "A platform independent file lock."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "filelock-3.12.4-py3-none-any.whl", hash = "sha256:08c21d87ded6e2b9da6728c3dff51baf1dcecf973b768ef35bcbc3447edb9ad4"},
-    {file = "filelock-3.12.4.tar.gz", hash = "sha256:2e6f249f1f3654291606e046b09f1fd5eac39b360664c27f5aad072012f8bcbd"},
+    {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"},
+    {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"},
 ]
 
 [package.extras]
-docs = ["furo (>=2023.7.26)", "sphinx (>=7.1.2)", "sphinx-autodoc-typehints (>=1.24)"]
-testing = ["covdefaults (>=2.3)", "coverage (>=7.3)", "diff-cover (>=7.7)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-timeout (>=2.1)"]
-typing = ["typing-extensions (>=4.7.1)"]
+docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
+typing = ["typing-extensions (>=4.8)"]
 
 [[package]]
 name = "identify"
-version = "2.5.30"
+version = "2.5.33"
 description = "File identification library for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "identify-2.5.30-py2.py3-none-any.whl", hash = "sha256:afe67f26ae29bab007ec21b03d4114f41316ab9dd15aa8736a167481e108da54"},
-    {file = "identify-2.5.30.tar.gz", hash = "sha256:f302a4256a15c849b91cfcdcec052a8ce914634b2f77ae87dad29cd749f2d88d"},
+    {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"},
+    {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"},
 ]
 
 [package.extras]
@@ -345,13 +345,13 @@ license = ["ukkonen"]
 
 [[package]]
 name = "idna"
-version = "3.4"
+version = "3.6"
 description = "Internationalized Domain Names in Applications (IDNA)"
 optional = false
 python-versions = ">=3.5"
 files = [
-    {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
-    {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
+    {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"},
+    {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"},
 ]
 
 [[package]]
@@ -367,20 +367,20 @@ files = [
 
 [[package]]
 name = "importlib-metadata"
-version = "6.8.0"
+version = "7.0.1"
 description = "Read metadata from Python packages"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "importlib_metadata-6.8.0-py3-none-any.whl", hash = "sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb"},
-    {file = "importlib_metadata-6.8.0.tar.gz", hash = "sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743"},
+    {file = "importlib_metadata-7.0.1-py3-none-any.whl", hash = "sha256:4805911c3a4ec7c3966410053e9ec6a1fecd629117df5adee56dfc9432a1081e"},
+    {file = "importlib_metadata-7.0.1.tar.gz", hash = "sha256:f238736bb06590ae52ac1fab06a3a9ef1d8dce2b7a35b5ab329371d6c8f5d2cc"},
 ]
 
 [package.dependencies]
 zipp = ">=0.5"
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
 perf = ["ipython"]
 testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
 
@@ -563,15 +563,26 @@ files = [
     {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
 ]
 
+[[package]]
+name = "pathspec"
+version = "0.11.2"
+description = "Utility library for gitignore style pattern matching of file paths."
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
+    {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
+]
+
 [[package]]
 name = "platformdirs"
-version = "3.11.0"
+version = "4.1.0"
 description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"},
-    {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"},
+    {file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"},
+    {file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"},
 ]
 
 [package.extras]
@@ -624,38 +635,39 @@ files = [
 
 [[package]]
 name = "pycodestyle"
-version = "2.11.0"
+version = "2.11.1"
 description = "Python style guide checker"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pycodestyle-2.11.0-py2.py3-none-any.whl", hash = "sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8"},
-    {file = "pycodestyle-2.11.0.tar.gz", hash = "sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0"},
+    {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"},
+    {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"},
 ]
 
 [[package]]
 name = "pygments"
-version = "2.16.1"
+version = "2.17.2"
 description = "Pygments is a syntax highlighting package written in Python."
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"},
-    {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"},
+    {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
+    {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
 ]
 
 [package.extras]
 plugins = ["importlib-metadata"]
+windows-terminal = ["colorama (>=0.4.6)"]
 
 [[package]]
 name = "pytest"
-version = "7.4.2"
+version = "7.4.4"
 description = "pytest: simple powerful testing with Python"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "pytest-7.4.2-py3-none-any.whl", hash = "sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002"},
-    {file = "pytest-7.4.2.tar.gz", hash = "sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"},
+    {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
+    {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
 ]
 
 [package.dependencies]
@@ -779,17 +791,17 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
 name = "setuptools"
-version = "68.2.2"
+version = "69.0.3"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"},
-    {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"},
+    {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"},
+    {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"},
 ]
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
+docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
 testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
 testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
 
@@ -1030,47 +1042,46 @@ files = [
 
 [[package]]
 name = "typing-extensions"
-version = "4.8.0"
+version = "4.9.0"
 description = "Backported and Experimental Type Hints for Python 3.8+"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"},
-    {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"},
+    {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
+    {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
 ]
 
 [[package]]
 name = "urllib3"
-version = "2.0.6"
+version = "2.1.0"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "urllib3-2.0.6-py3-none-any.whl", hash = "sha256:7a7c7003b000adf9e7ca2a377c9688bbc54ed41b985789ed576570342a375cd2"},
-    {file = "urllib3-2.0.6.tar.gz", hash = "sha256:b19e1a85d206b56d7df1d5e683df4a7725252a964e3993648dd0fb5a1c157564"},
+    {file = "urllib3-2.1.0-py3-none-any.whl", hash = "sha256:55901e917a5896a349ff771be919f8bd99aff50b79fe58fec595eb37bbc56bb3"},
+    {file = "urllib3-2.1.0.tar.gz", hash = "sha256:df7aa8afb0148fa78488e7899b2c59b5f4ffcfa82e6c54ccb9dd37c1d7b52d54"},
 ]
 
 [package.extras]
 brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
-secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17.1.0)", "urllib3-secure-extra"]
 socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
 zstd = ["zstandard (>=0.18.0)"]
 
 [[package]]
 name = "virtualenv"
-version = "20.24.5"
+version = "20.25.0"
 description = "Virtual Python Environment builder"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "virtualenv-20.24.5-py3-none-any.whl", hash = "sha256:b80039f280f4919c77b30f1c23294ae357c4c8701042086e3fc005963e4e537b"},
-    {file = "virtualenv-20.24.5.tar.gz", hash = "sha256:e8361967f6da6fbdf1426483bfe9fca8287c242ac0bc30429905721cefbff752"},
+    {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"},
+    {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"},
 ]
 
 [package.dependencies]
 distlib = ">=0.3.7,<1"
 filelock = ">=3.12.2,<4"
-platformdirs = ">=3.9.1,<4"
+platformdirs = ">=3.9.1,<5"
 
 [package.extras]
 docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
@@ -1093,5 +1104,5 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
 
 [metadata]
 lock-version = "2.0"
-python-versions = ">=3.9, <3.12"
-content-hash = "92b504d9c474e747e0d9471a733f94af9b4ad9a30a4c86258403f334ffaf4088"
+python-versions = ">=3.9, <3.13"
+content-hash = "a7f3ad1e8596d6e75f349f86d5e6576977f84121f7f30941b2e8a44a5441b606"
diff --git a/pyproject.toml b/pyproject.toml
index ab79c12..a3a86dc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,8 +1,8 @@
 [tool.poetry]
 name = "modelica-builder"
-version = "0.4.0"
+version = "0.5.0"
 description = "Modelica builder enables programmatic parsing and modification of Modelica files."
-authors = ["Nicholas Long <nicholas.long@nrel.gov>"]
+authors = ["Nicholas Long <nicholas.long@nrel.gov>", "Nathan Moore <nathan.moore@nrel.gov>"]
 license = "BSD-3-Clause"
 
 readme = "README.rst"
@@ -22,13 +22,15 @@ classifiers = [
     "Programming Language :: Python :: 3.9",
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
+    "Programming Language :: Python :: 3.12",
 ]
 
 [tool.poetry.dependencies]
 # Urbanopt SDK requires Python 3.10 as of UOv0.9.0
-python = ">=3.9, <3.12"
-antlr4-python3-runtime = "4.13"
+python = ">=3.9, <3.13"
+antlr4-python3-runtime = "4.13.1"
 jinja2 = "3.1.2"
+pathspec = "^0.11.2"
 
 [tool.poetry.dev-dependencies]
 autopep8 = "~2.0"
@@ -49,5 +51,9 @@ toml = "~0.10"
 requires = ["poetry-core>=1.0.0"]
 build-backend = "poetry.core.masonry.api"
 
+[tool.pytest.ini_options]
+log_cli = true
+log_cli_level = "DEBUG"
+
 [tool.isort]
 multi_line_output = 3
diff --git a/setup.py b/setup.py
index 088a405..0c50453 100755
--- a/setup.py
+++ b/setup.py
@@ -1,91 +1,12 @@
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 # -*- coding: utf-8 -*-
+
 """
 Setup file for modelica_builder.
 Use setup.cfg to configure your project.
 """
-from pkg_resources import VersionConflict, require
 from setuptools import setup
 
-import distutils.cmd
-import distutils.log
-import glob
-import os
-import re
-import sys
-
-PYTHON_REGEX = re.compile(r'^""".\*{100}.*:copyright.*\*{100}."""$', re.MULTILINE | re.DOTALL)
-PYTHON_LICENSE = '''"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""'''
-
-EXCLUDE_FILES = ["__init__.py", "modelicaLexer.py", "modelicaListener.py", "modelicaParser.py"]
-PATHS = [
-    {"glob": "modelica_builder/**/*.py", "license": PYTHON_LICENSE, "REGEX": PYTHON_REGEX, },
-    {"glob": "tests/**/*.py", "license": PYTHON_LICENSE, "REGEX": PYTHON_REGEX},
-    # single files
-    # { "glob": 'bin/resources/**/file.py', "license": PYTHON_LICENSE, "REGEX": PYTHON_REGEX },
-]
-
-
-class UpdateLicenses(distutils.cmd.Command):
-    """Custom command for updating the license doc strings."""
-
-    description = "Update the license/copyright headers"
-
-    user_options = []
-
-    def initialize_options(self):
-        pass
-
-    def finalize_options(self):
-        pass
-
-    def check_and_update_license(self, filename):
-        """
-        check if the license exists in the file, and if it does, then make sure it is up-to-date with
-        the license defined in this file.
-
-        :param filename: str, path of the file to update
-        :return: None
-        """
-        s = open(filename, "r").read()
-        if PYTHON_REGEX.search(s):
-            print("License already exists, updating")
-            content = re.sub(PYTHON_REGEX, PYTHON_LICENSE, s)
-            with open(filename, "w") as f:
-                f.write(content)
-                f.close()
-        else:
-            print("Adding license")
-            with open(filename, "r+") as f:
-                content = f.read()
-                f.seek(0, 0)
-                f.write(PYTHON_LICENSE.rstrip("\r\n") + "\n\n\n" + content)
-                f.close()
-
-    def run(self):
-        for p in PATHS:
-            gl = glob.glob(p["glob"], recursive=True)
-            for g in gl:
-                print(os.path.basename(g))
-                if os.path.basename(g) in EXCLUDE_FILES:
-                    print(f"Skipping file {g}")
-                else:
-                    print(f"Checking license in file {g}")
-                    self.check_and_update_license(g)
-
-
-try:
-    require('setuptools>=38.3')
-except VersionConflict:
-    print("Error: version of setuptools is too old (<38.3)!")
-    sys.exit(1)
-
-
 if __name__ == "__main__":
-    setup(
-        cmdclass={"update_licenses": UpdateLicenses},
-    )
+    setup()


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 9cbb5eb..a890b59 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,9 +1,5 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 
 
 # -*- coding: utf-8 -*-
diff --git a/tests/data/B2_no_water_load.mos b/tests/data/B2_no_water_load.mos
index 213648c..49d4413 100644
--- a/tests/data/B2_no_water_load.mos
+++ b/tests/data/B2_no_water_load.mos
@@ -15,11 +15,11 @@
 #Peak space heating load = 38986.3 Watts
 #Peak water heating load = 0 Watts
 double tab1(8760,4)
-3600;0;0;0.0
-7200;0;0;0.0
-10800;0;0;0.0
-14400;0;0;0.0
-18000;0;16240.4;0.0
-21600;0;13281.1;0.0
-25200;0;9459.4;0.0
-28800;-1793.3;0;0.0
+3600;-100;200;0.0
+7200;-100;200;0.0
+10800;-100;200;0.0
+14400;-100;200;0.0
+18000;-100;200;0.0
+21600;-100;200;0.0
+25200;-100;200;0.0
+28800;-100;200;0.0
diff --git a/tests/data/packages/teaser_single/Districts/unneeded.csv b/tests/data/packages/teaser_single/Districts/unneeded.csv
new file mode 100644
index 0000000..9f54893
--- /dev/null
+++ b/tests/data/packages/teaser_single/Districts/unneeded.csv
@@ -0,0 +1,1 @@
+# another file to make sure gets filtered out
diff --git a/tests/data/packages/teaser_single/package.mo b/tests/data/packages/teaser_single/package.mo
index b994622..719cdfd 100644
--- a/tests/data/packages/teaser_single/package.mo
+++ b/tests/data/packages/teaser_single/package.mo
@@ -2,5 +2,5 @@ package teaser_single
   extends Modelica.Icons.Package;
 
   annotation (uses(
-    Modelica(version="4.0.0"), Buildings(version="9.1.0")), version="1");
+    Modelica(version="4.0.0"), Buildings(version="10.0.0")), version="1");
 end teaser_single;
diff --git a/tests/data/packages/teaser_single/results.csv b/tests/data/packages/teaser_single/results.csv
new file mode 100644
index 0000000..304f58f
--- /dev/null
+++ b/tests/data/packages/teaser_single/results.csv
@@ -0,0 +1,1 @@
+# file to ensure pathspec filters this out
diff --git a/tests/test_benchmarks.py b/tests/test_benchmarks.py
index b755918..0bf7af2 100644
--- a/tests/test_benchmarks.py
+++ b/tests/test_benchmarks.py
@@ -1,9 +1,5 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 
 
 # -*- coding: utf-8 -*-
diff --git a/tests/test_builder.py b/tests/test_builder.py
index 6d7c5c8..35ac202 100644
--- a/tests/test_builder.py
+++ b/tests/test_builder.py
@@ -1,9 +1,5 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 
 
 # -*- coding: utf-8 -*-
diff --git a/tests/test_edit.py b/tests/test_edit.py
index 6457fec..d573ccb 100644
--- a/tests/test_edit.py
+++ b/tests/test_edit.py
@@ -1,10 +1,5 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
-
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 
 # -*- coding: utf-8 -*-
 
diff --git a/tests/test_model.py b/tests/test_model.py
index 65f8c6c..61832bd 100644
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -1,9 +1,6 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
+
 
 import os
 import tempfile
diff --git a/tests/test_modelica_mos.py b/tests/test_modelica_mos.py
index f1e0a42..fcbdd90 100644
--- a/tests/test_modelica_mos.py
+++ b/tests/test_modelica_mos.py
@@ -1,10 +1,14 @@
 # :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
 # See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 
+import logging
 import os
 import unittest
+from datetime import datetime
 from pathlib import Path
 
+_log = logging.getLogger(__name__)
+
 from modelica_builder.modelica_mos_file import ModelicaMOS
 
 
@@ -21,7 +25,7 @@ def test_variable_retrieval_basic(self):
 #Var2 = 1 Watts
 #Var3 = 2
 """
-        file = ModelicaMOS('not_a_real_file.mos', data=data)
+        file = ModelicaMOS('not_a_real_file.mos', header_data=data)
         self.assertEqual(file.retrieve_header_variable_value('Var1', cast_type=int), 0)
         self.assertEqual(file.retrieve_header_variable_value('Var2'), '1')
         self.assertEqual(file.retrieve_header_variable_value('Var3'), '2')
@@ -34,7 +38,7 @@ def test_variable_retrieval_floats(self):
 # Var3 = 1,245 Watts with space
 # Var 4 = 1,245 Watts
 """
-        file = ModelicaMOS('not_a_real_file.mos', data=data)
+        file = ModelicaMOS('not_a_real_file.mos', header_data=data)
         self.assertEqual(file.retrieve_header_variable_value('Var1', cast_type=float), 1245.25)
         self.assertEqual(file.retrieve_header_variable_value('Var2', cast_type=float), 1245)
         self.assertEqual(file.retrieve_header_variable_value('Var3', cast_type=float), 1245)
@@ -45,14 +49,14 @@ def test_variable_retrieval_negatives(self):
         data = """
 #Var1 = -1,245.25 Watts
 """
-        file = ModelicaMOS('not_a_real_file.mos', data=data)
+        file = ModelicaMOS('not_a_real_file.mos', header_data=data)
         self.assertEqual(file.retrieve_header_variable_value('Var1', cast_type=float), -1245.25)
 
     def test_variable_retrieval_not_found(self):
         data = """
 #Var1 = 1234 Watts
 """
-        file = ModelicaMOS('not_a_real_file.mos', data=data)
+        file = ModelicaMOS('not_a_real_file.mos', header_data=data)
         self.assertEqual(file.retrieve_header_variable_value('Var2', cast_type=float), None)
 
     def test_replace_variable_value(self):
@@ -62,7 +66,7 @@ def test_replace_variable_value(self):
 #Var3=2
 #Var4 = -3
 """
-        file = ModelicaMOS('not_a_real_file.mos', data=data)
+        file = ModelicaMOS('not_a_real_file.mos', header_data=data)
         self.assertEqual(file.retrieve_header_variable_value('Var1', cast_type=float), 0)
         file.replace_header_variable_value('Var1', 1234)
         file.replace_header_variable_value('Var2', 2345)
@@ -76,7 +80,6 @@ def test_replace_variable_value(self):
         self.assertEqual(file.retrieve_header_variable_value('Var4', cast_type=float), -1234)
 
     def test_read_write_with_replace(self):
-
         file = ModelicaMOS(self.data_dir / 'B2_no_water_load.mos')
         self.assertEqual(file.retrieve_header_variable_value('Peak water heating load', cast_type=float), 0)
         file.replace_header_variable_value('Peak water heating load', 5124)
@@ -92,3 +95,95 @@ def test_read_write_with_replace(self):
 
         # just call the save method for 'crash' check
         new_file.save()
+
+    def test_scaling_loads(self):
+        scaling_factors = [
+            #  y  m  d  h  m  s
+            {'start_time': datetime(2021, 1, 1, 0, 0, 0), 'end_time': datetime(2021,  1,  1,  3,  0,  0), 'scaling_factor': 0.25},  # noqa
+            {'start_time': datetime(2021, 1, 1, 4, 0, 0), 'end_time': datetime(2021, 12, 31, 23, 59, 59), 'scaling_factor': 1}
+        ]
+        file = ModelicaMOS(self.data_dir / 'B2_no_water_load.mos')
+        file.scale_loads(scaling_factors)
+
+        # save to a new file to compare
+        file.save_as(self.output_dir / 'B2_no_water_load_scaled.mos')
+        # read in the new file and check the value
+        self.assertTrue((self.output_dir / 'B2_no_water_load_scaled.mos').exists())
+        new_mos = ModelicaMOS(self.output_dir / 'B2_no_water_load_scaled.mos')
+
+        # to find these values, you must manually look at the original file and then the scaling factor and make
+        # sure that the values match.
+        _log.debug(new_mos.data)
+        self.assertEqual(new_mos.data[0][0], 3600)
+        self.assertEqual(new_mos.data[0][1], -25)
+        self.assertEqual(new_mos.data[0][2], 50)
+
+        # second >= 14400 should not be scaled, which is in row 3 (0-based)
+        self.assertEqual(new_mos.data[3][0], 14400)
+        self.assertEqual(new_mos.data[3][1], -100)
+        self.assertEqual(new_mos.data[3][2], 200)
+
+    def test_scaling_loads_single_named_column(self):
+        scaling_factors = [
+            #  y  m  d  h  m  s
+            {'start_time': datetime(2021, 1, 1, 0, 0, 0), 'end_time': datetime(2021,  1,  1,  3,  0,  0), 'scaling_factor': 0.25},  # noqa
+            {'start_time': datetime(2021, 1, 1, 4, 0, 0), 'end_time': datetime(2021, 12, 31, 23, 59, 59), 'scaling_factor': 1}
+        ]
+        file = ModelicaMOS(self.data_dir / 'B2_no_water_load.mos')
+        file.scale_loads(scaling_factors, 'heating')
+
+        # save to a new file to compare
+        file.save_as(self.output_dir / 'B2_no_water_load_scaled_single_named_column.mos')
+        # read in the new file and check the value
+        self.assertTrue((self.output_dir / 'B2_no_water_load_scaled_single_named_column.mos').exists())
+        new_mos = ModelicaMOS(self.output_dir / 'B2_no_water_load_scaled_single_named_column.mos')
+
+        # to find these values, you must manually look at the original file and then the scaling factor and make
+        # sure that the values match.
+        _log.debug(new_mos.data)
+        self.assertEqual(new_mos.data[0][0], 3600)
+        self.assertEqual(new_mos.data[0][1], -100)
+        self.assertEqual(new_mos.data[0][2], 50)
+
+        # second >= 14400 should not be scaled, which is in row 3 (0-based)
+        self.assertEqual(new_mos.data[3][0], 14400)
+        self.assertEqual(new_mos.data[3][1], -100)
+        self.assertEqual(new_mos.data[3][2], 200)
+
+    def test_scaling_loads_column_index(self):
+        """Test scaling a load by a column index"""
+        scaling_factors = [
+            #  y  m  d  h  m  s
+            {'start_time': datetime(2021, 1, 1, 0, 0, 0), 'end_time': datetime(2021,  1,  1,  3,  0,  0), 'scaling_factor': 0.25},  # noqa
+            {'start_time': datetime(2021, 1, 1, 4, 0, 0), 'end_time': datetime(2021, 12, 31, 23, 59, 59), 'scaling_factor': 1}
+        ]
+        file = ModelicaMOS(self.data_dir / 'B2_no_water_load.mos')
+        file.scale_loads(scaling_factors, 1)
+
+        # save to a new file to compare
+        file.save_as(self.output_dir / 'B2_no_water_load_scaled_column_index.mos')
+        # read in the new file and check the value
+        self.assertTrue((self.output_dir / 'B2_no_water_load_scaled_column_index.mos').exists())
+        new_mos = ModelicaMOS(self.output_dir / 'B2_no_water_load_scaled_column_index.mos')
+
+        # to find these values, you must manually look at the original file and then the scaling factor and make
+        # sure that the values match.
+        _log.debug(new_mos.data)
+        self.assertEqual(new_mos.data[0][0], 3600)
+        self.assertEqual(new_mos.data[0][1], -25)
+        self.assertEqual(new_mos.data[0][2], 200)
+
+        # second >= 14400 should not be scaled, which is in row 3 (0-based)
+        self.assertEqual(new_mos.data[3][0], 14400)
+        self.assertEqual(new_mos.data[3][1], -100)
+        self.assertEqual(new_mos.data[3][2], 200)
+
+    def test_scaling_load_no_data_error(self):
+        # test with decimal separators
+        data = """
+#Var1 = -1,245.25 Watts
+"""
+        file = ModelicaMOS('not_a_real_file.mos', header_data=data)
+        with self.assertRaises(Exception) as ctx:
+            file.scale_loads([])
+        self.assertIn("No data to scale", str(ctx.exception))
diff --git a/tests/test_modelica_project.py b/tests/test_modelica_project.py
index 57805ec..c8e9aac 100644
--- a/tests/test_modelica_project.py
+++ b/tests/test_modelica_project.py
@@ -1,9 +1,6 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
+
 import os
 import shutil
 import unittest
@@ -66,3 +63,24 @@ def test_project_save_as(self):
 
         # verify that the package.mo file was saved
         self.assertTrue((self.output_dir / 'test_package_1' / 'package.mo').exists())
+
+    def test_mpignore_file(self):
+        """Verify that the mpignore file is loaded correctly"""
+        package_file = self.data_dir / 'teaser_single' / 'package.mo'
+
+        mpignore_file = package_file.parent / '.mpignore'
+        # delete the mpignore file if it exists
+        if mpignore_file.exists():
+            mpignore_file.unlink()
+        # write an mpignore file into the 'teaser_single' directory
+        with open(mpignore_file, 'w') as f:
+            f.write('*.csv\n')
+
+        project = ModelicaProject(package_file)
+
+        # there should be two ignored files
+        # ignored = [f.file_path.name for f in project.mpignore_files]
+        self.assertEqual(len(project.mpignore_files), 2)
+        ignored = [project.root_directory / Path('results.csv'),
+                   project.root_directory / Path('Districts/unneeded.csv')]
+        self.assertListEqual(project.mpignore_files, ignored)
diff --git a/tests/test_package_parser.py b/tests/test_package_parser.py
index 4a27602..92eb627 100644
--- a/tests/test_package_parser.py
+++ b/tests/test_package_parser.py
@@ -3,6 +3,7 @@
 
 import os
 import unittest
+from pathlib import Path
 
 from modelica_builder.package_parser import PackageParser
 
@@ -16,7 +17,10 @@ def setUp(self):
 
     def test_new_from_template(self):
         package = PackageParser.new_from_template(
-            self.output_dir, 'new_model_name', ["model_a", "model_b"], within="SomeWithin"
+            self.output_dir,
+            'new_model_name',
+            ["model_a", "model_b"],
+            within="SomeWithin",
         )
         package.save()
 
@@ -33,9 +37,29 @@ def test_new_from_template(self):
         with open(os.path.join(self.output_dir, 'package.order')) as f:
             self.assertTrue('model_a\nmodel_b' in f.read(), 'Incorrect package order')
 
+    def test_mbl_version(self):
+        package = PackageParser.new_from_template(
+            self.output_dir,
+            'new_model_name',
+            ["model_a", "model_b"],
+            mbl_version="2.0.0"
+        )
+        package.save()
+
+        self.assertTrue((Path(self.output_dir) / 'package.mo').exists())
+        self.assertTrue((Path(self.output_dir) / 'package.order').exists())
+
+        # check for strings in files
+        with open(Path(self.output_dir) / 'package.mo') as f:
+            file_data = f.read()
+            self.assertTrue('Buildings(version="2.0.0"' in file_data, 'Incorrect mbl_version')
+
     def test_round_trip(self):
         package = PackageParser.new_from_template(
-            self.output_dir, 'another_model', ["model_x", "model_y"], within="DifferentWithin"
+            self.output_dir,
+            'another_model',
+            ["model_x", "model_y"],
+            within="DifferentWithin",
         )
         package.save()
 
@@ -45,7 +69,10 @@ def test_round_trip(self):
 
     def test_rename_package_model(self):
         package = PackageParser.new_from_template(
-            self.output_dir, 'rename_model', ["model_1", "model_2"], within="RenameWithin"
+            self.output_dir,
+            'rename_model',
+            ["model_1", "model_2"],
+            within="RenameWithin",
         )
         package.save()
 
@@ -56,7 +83,10 @@ def test_rename_package_model(self):
 
     def test_rename_model_in_order(self):
         package = PackageParser.new_from_template(
-            self.output_dir, 'rename_model', ["model_1", "model_2"], within="RenameWithin"
+            self.output_dir,
+            'rename_model',
+            ["model_1", "model_2"],
+            within="RenameWithin",
         )
         package.save()
 
@@ -66,7 +96,10 @@ def test_rename_model_in_order(self):
 
     def test_add_model(self):
         package = PackageParser.new_from_template(
-            self.output_dir, 'so_many_models', ["model_beta", "model_gamma"], within="SoMany"
+            self.output_dir,
+            'so_many_models',
+            ["model_beta", "model_gamma"],
+            within="SoMany",
         )
         package.save()
 
@@ -77,7 +110,10 @@ def test_add_model(self):
 
     def test_within_clause(self):
         package = PackageParser.new_from_template(
-            self.output_dir, 'within_clause', ["model_a", "model_b"], within="SomeWithin"
+            self.output_dir,
+            'within_clause',
+            ["model_a", "model_b"],
+            within="SomeWithin",
         )
         package.save()
 
diff --git a/tests/test_selector.py b/tests/test_selector.py
index 9a1a56f..3fd6664 100644
--- a/tests/test_selector.py
+++ b/tests/test_selector.py
@@ -1,10 +1,5 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
-
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 
 # -*- coding: utf-8 -*-
 
diff --git a/tests/tests.py b/tests/tests.py
index dde4af4..ecbcfe2 100644
--- a/tests/tests.py
+++ b/tests/tests.py
@@ -1,9 +1,5 @@
-"""
-****************************************************************************************************
-:copyright (c) 2020-2023, Alliance for Sustainable Energy, LLC.
-All rights reserved.
-****************************************************************************************************
-"""
+# :copyright (c) URBANopt, Alliance for Sustainable Energy, LLC, and other contributors.
+# See also https://github.com/urbanopt/geojson-modelica-translator/blob/develop/LICENSE.md
 
 
 import difflib