-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fleshed out unit tests to support the given infant state
- Loading branch information
1 parent
30ecec3
commit 2a1d09e
Showing
24 changed files
with
649 additions
and
78 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,27 +1,37 @@ | ||
# EpJSON Transition | ||
# EpJSON Transition | ||
|
||
[![GitHub release](https://img.shields.io/github/release/myoldmopar/epjsontransition.svg?style=for-the-badge)](https://github.com/myoldmopar/epjsontransition/releases/latest) | ||
|
||
[![Documentation Status](https://readthedocs.org/projects/epjson-transition/badge/?version=latest&style=for-the-badge)](https://epjson-transition.readthedocs.io/en/latest/?badge=latest) | ||
This tool provides a library, along with a command line interface, fully packaged binaries, and | ||
PyPi distributed wheels, for doing conversion of EnergyPlus EpJSON files. While EpJSON files may not change | ||
significantly between versions, there are still changes required sometimes. At a minimum the version number. | ||
|
||
[![Unit Tests](https://img.shields.io/github/workflow/status/Myoldmopar/EpJSONTransition/Test?label=Unit%20Tests&style=for-the-badge)](https://github.com/Myoldmopar/EpJSONTransition/actions?query=workflow%3A%22Test%22) | ||
[![Coverage Status](https://img.shields.io/coveralls/github/Myoldmopar/EpJSONTransition?label=Coverage&style=for-the-badge)](https://coveralls.io/github/Myoldmopar/EpJSONTransition?branch=main) | ||
[![PEP8 Enforcement](https://img.shields.io/github/workflow/status/Myoldmopar/EpJSONTransition/Flake8?label=Flake8&style=for-the-badge)](https://github.com/Myoldmopar/EpJSONTransition/actions?query=workflow%3AFlake8) | ||
## Documentation | ||
|
||
[![Releases](https://img.shields.io/github/workflow/status/Myoldmopar/EpJSONTransition/PyInstallerRelease?label=PyInstaller%20Release&style=for-the-badge)](https://github.com/Myoldmopar/EpJSONTransition/actions?query=workflow%3APyInstallerRelease) | ||
[![Releases](https://img.shields.io/github/workflow/status/Myoldmopar/EpJSONTransition/PyInstallerRelease?label=PyPI%20Release&style=for-the-badge)](https://github.com/Myoldmopar/EpJSONTransition/actions?query=workflow%3APyPIRelease) | ||
[![Releases](https://img.shields.io/pypi/wheel/EnergyPlus-EpJSON-Transition-Tool?label=PyPi%20Wheel&style=for-the-badge)](https://pypi.org/project/EnergyPlus-EpJSON-Transition-Tool/) | ||
[![Documentation Status](https://readthedocs.org/projects/epjson-transition/badge/?version=latest&style=for-the-badge)](https://epjson-transition.readthedocs.io/en/latest/?badge=latest) | ||
|
||
Prototype tool for doing EpJSON transition | ||
Documentation is built by Sphinx and automatically generated and hosted on ReadTheDocs. | ||
|
||
## Testing | ||
|
||
[![Unit Tests](https://img.shields.io/github/workflow/status/Myoldmopar/EpJSONTransition/Test?label=Unit%20Tests&style=for-the-badge)](https://github.com/Myoldmopar/EpJSONTransition/actions?query=workflow%3A%22Test%22) | ||
[![Coverage Status](https://img.shields.io/coveralls/github/Myoldmopar/EpJSONTransition?label=Coverage&style=for-the-badge)](https://coveralls.io/github/Myoldmopar/EpJSONTransition?branch=main) | ||
[![PEP8 Enforcement](https://img.shields.io/github/workflow/status/Myoldmopar/EpJSONTransition/Flake8?label=Flake8&style=for-the-badge)](https://github.com/Myoldmopar/EpJSONTransition/actions?query=workflow%3AFlake8) | ||
|
||
The project is tested using standard Python unit testing practices. | ||
Each commit is automatically tested with Github Actions on Windows, Mac, Ubuntu 18.04 and Ubuntu 20.04. | ||
The code coverage across platforms is collected on Coveralls. | ||
When a tag is created in the GitHub Repo, Github Actions builds downloadable packages. | ||
|
||
## Development | ||
|
||
To run the unit test suite, make sure to have nose and coverage installed via: `pip install nose coverage`. | ||
Then execute `setup.py nosetests`. | ||
Then execute `coverage run setup.py nosetests`. | ||
Unit test results will appear in the console, and coverage results will be in a `htmlcov` directory. | ||
|
||
## Releases | ||
|
||
[![Releases](https://img.shields.io/github/workflow/status/Myoldmopar/EpJSONTransition/PyInstallerRelease?label=PyInstaller%20Release&style=for-the-badge)](https://github.com/Myoldmopar/EpJSONTransition/actions?query=workflow%3APyInstallerRelease) | ||
[![Releases](https://img.shields.io/github/workflow/status/Myoldmopar/EpJSONTransition/PyInstallerRelease?label=PyPI%20Release&style=for-the-badge)](https://github.com/Myoldmopar/EpJSONTransition/actions?query=workflow%3APyPIRelease) | ||
[![Releases](https://img.shields.io/pypi/wheel/EnergyPlus-EpJSON-Transition-Tool?label=PyPi%20Wheel&style=for-the-badge)](https://pypi.org/project/EnergyPlus-EpJSON-Transition-Tool/) | ||
|
||
Releases will be made periodically at meaningful milestones. | ||
Each release tag results in pyinstaller-based prebuilt binaries available on the release asset page, and an updated PyPi wheel. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,66 @@ | ||
from json import loads | ||
from json.decoder import JSONDecodeError | ||
from json import dumps, loads | ||
from pathlib import Path | ||
|
||
from epjson_transition.exceptions import MissingEpJSONVersion | ||
from epjson_transition.version import EpJSONVersion | ||
from epjson_transition.exceptions import MissingEpJSONVersion, MalformedEpJSONFile | ||
from epjson_transition.logger import SimpleLogger | ||
from epjson_transition.version import EpJSONVersion, KnownVersions | ||
from epjson_transition.rules.common import OutputVariable, Version | ||
from epjson_transition.rules.Transition95to96 import EnergyPlus9596 | ||
|
||
|
||
class EpJSONFile: | ||
def __init__(self, path_to_file: Path): | ||
self.path = path_to_file | ||
self.file_contents = self.path.read_text(encoding='utf-8', errors='ignore') | ||
self.data = loads(self.file_contents) | ||
if 'Version' not in self.data: | ||
def __init__(self, path_to_input_file: Path, path_to_output_file: Path, logger: SimpleLogger): | ||
""" | ||
:param path_to_input_file: pathlib.Path instance, existence already verified | ||
:param path_to_output_file: | ||
:param logger: | ||
""" | ||
# allow a read_text call to raise whatever exceptions it wants | ||
self.file_contents = path_to_input_file.read_text(encoding='utf-8', errors='ignore') | ||
logger.print(f"EpJSON contents loaded; file length = {len(self.file_contents)} characters") | ||
try: | ||
self.original_data = loads(self.file_contents) | ||
except JSONDecodeError: | ||
raise MalformedEpJSONFile("Could not read in EpJSON file, are contents actually JSON?") | ||
logger.print(f"EpJSON contents interpreted into dictionary; contains {len(self.original_data.keys())} keys") | ||
if 'Version' not in self.original_data: | ||
raise MissingEpJSONVersion() | ||
self.version = EpJSONVersion(self.data['Version']) | ||
self.original_version = EpJSONVersion(self.original_data['Version']) | ||
original_version_key = self.original_version.known_version | ||
self.output_file_path = path_to_output_file | ||
logger.print( | ||
f"EpJSON version found, sanitized version string = {KnownVersions.VersionStrings[original_version_key]}" | ||
) | ||
logger.print("File interpretation complete") | ||
|
||
def transform(self, logger: SimpleLogger): | ||
logger.print("EpJSON Content Transition Beginning") | ||
transition_instance = EnergyPlus9596() # TODO: Remove and make the IF block exhaustive or fatal | ||
if self.original_version.known_version == KnownVersions.Version95: | ||
transition_instance = EnergyPlus9596() | ||
else: | ||
... | ||
|
||
# initialize the transitioned data dictionary | ||
transitioned_data = self.original_data | ||
|
||
logger.print("First Transition the Version Object") | ||
logger.add_prefix() | ||
transitioned_data = Version.transform(transitioned_data, logger) | ||
logger.remove_prefix() | ||
|
||
logger.print("Next Transitioning Any Output Variables and Related Objects and Fields") | ||
logger.add_prefix() | ||
transitioned_data = OutputVariable(transition_instance.variable_map()).transform(transitioned_data, logger) | ||
logger.remove_prefix() | ||
|
||
logger.print("Now Calling Main Transition Routine for this Version") | ||
logger.add_prefix() | ||
transitioned_data = transition_instance.transform(transitioned_data, logger) | ||
logger.remove_prefix() | ||
logger.print(f"Writing Updated Output File at {self.output_file_path} (pretend)") | ||
with self.output_file_path.open('w') as f: | ||
f.write(dumps(transitioned_data, indent=2)) | ||
logger.print("File Transformation Complete") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from datetime import datetime | ||
from typing import Union | ||
|
||
|
||
class SimpleLogger: | ||
def __init__(self, console=True, log_file_path: Union[str, None] = None): | ||
self.console = console | ||
self.prefix = "" | ||
if log_file_path is None: | ||
self.output_file = None | ||
else: | ||
self.output_file = open(log_file_path, 'w') | ||
|
||
def close(self): | ||
if self.output_file: | ||
self.output_file.close() | ||
|
||
def add_prefix(self): | ||
self.prefix += ' ' | ||
|
||
def remove_prefix(self): | ||
if len(self.prefix) > 0: | ||
self.prefix = self.prefix[:-1] | ||
|
||
def print(self, message): | ||
line = f"{datetime.now()} : {self.prefix}{message}" | ||
if self.console: | ||
print(line) # pragma: no cover - not muddying up the console output during testing | ||
if self.output_file: | ||
print(line, file=self.output_file) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
from pathlib import Path | ||
import sys | ||
|
||
from epjson_transition.exceptions import EpJSONTransitionError | ||
from epjson_transition.logger import SimpleLogger | ||
from epjson_transition.transition import EpJSONTransition | ||
|
||
|
||
def main() -> int: | ||
"""Entry point for command line packaging""" | ||
argc = len(sys.argv) | ||
if argc == 4: | ||
input_file_path = Path(sys.argv[1]) | ||
output_file_path = Path(sys.argv[2]) | ||
logger = SimpleLogger(console=True, log_file_path=sys.argv[3]) | ||
else: | ||
print("Invalid command line execution: expecting 3 arguments") | ||
print(" Argument 1: the path to the EpJSON file to be transitioned.") | ||
print(" Argument 2: the path to the output file to be written.") | ||
print(" Argument 3: optional path to an error file to log progress") | ||
return 1 | ||
try: | ||
EpJSONTransition(input_file_path, output_file_path).transform(logger) | ||
except EpJSONTransitionError: | ||
print("Caught EpJSON transition error during execution") | ||
return 1 | ||
# except Exception as e: | ||
# print("Caught unknown exception during execution, message: " + str(e)) | ||
# return 1 | ||
return 0 | ||
|
||
|
||
if __name__ == "__main__": # pragma: no cover - Covering main() directly, not this script entry | ||
"""Entry point for direct script usage""" | ||
sys.exit(main()) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
from typing import Dict | ||
|
||
from epjson_transition.logger import SimpleLogger | ||
from epjson_transition.rules.base import EpJSONTransitionRuleBase | ||
|
||
|
||
class EnergyPlus9596(EpJSONTransitionRuleBase): | ||
|
||
def message(self): | ||
return "Converting EpJSON 9.5 to 9.6" | ||
|
||
def old_version(self) -> float: | ||
return 9.5 | ||
|
||
def new_version(self) -> float: | ||
return 9.6 | ||
|
||
def transform(self, file_contents: Dict, logger: SimpleLogger) -> Dict: | ||
logger.print("Transitioning File Now") | ||
file_contents["Foo"] = "Bar" | ||
return file_contents | ||
|
||
def variable_map(self): | ||
return {'Site Outdoor Air Drybulb Temperature': 'Fake Outdoor DB Temp'} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,44 @@ | ||
from epjson_transition.logger import SimpleLogger | ||
|
||
|
||
class EpJSONTransitionRuleBase: | ||
def __init__(self): | ||
... | ||
""" | ||
An abstract base class for declaring transition rules from any one single EnergyPlus version to the next. | ||
""" | ||
|
||
def message(self): | ||
""" | ||
Returns a descriptive message | ||
:return: | ||
""" | ||
raise NotImplementedError("Must override message() method in EpJSONTransitionRuleBase derived classes") | ||
|
||
def old_version(self) -> float: | ||
""" | ||
:return: | ||
""" | ||
raise NotImplementedError("Must override old_version() method in EpJSONTransitionRuleBase derived classes") | ||
|
||
def new_version(self) -> float: | ||
""" | ||
:return: | ||
""" | ||
raise NotImplementedError("Must override new_version() method in EpJSONTransitionRuleBase derived classes") | ||
|
||
def transform(self, file_contents, logger: SimpleLogger): | ||
""" | ||
:param logger: | ||
:param file_contents: | ||
:return: | ||
""" | ||
raise NotImplementedError("Must override transform() method in EpJSONTransitionRuleBase derived classes") | ||
|
||
def variable_map(self): | ||
""" | ||
def transform(self, file_contents): | ||
raise NotImplementedError("Need to override transform() method in EpJSONTransitionRuleBase derived classes") | ||
:return: | ||
""" | ||
raise NotImplementedError("Must override variable_map() method in EpJSONTransitionRuleBase derived classes") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
# Contains class(es) that can be reused by each version's transition class | ||
# These can contain things that are generally common to every version, though they could change occasionally | ||
|
||
from typing import Dict | ||
|
||
from epjson_transition.logger import SimpleLogger | ||
from epjson_transition.version import KnownVersions | ||
|
||
|
||
class OutputVariable: | ||
def __init__(self, output_variable_changes_this_version: Dict): | ||
self.output_variable_map = output_variable_changes_this_version | ||
|
||
def _upper_case_variable_map(self): | ||
new_variable_map = {} | ||
for k, v in self.output_variable_map.items(): | ||
new_variable_map[k.upper()] = v | ||
return new_variable_map | ||
|
||
def transform(self, file_contents: Dict, logger: SimpleLogger) -> Dict: | ||
logger.print("Processing Output Variable Changes") | ||
if 'Output:Variable' not in file_contents: | ||
return file_contents | ||
output_variables = file_contents['Output:Variable'] | ||
upper_case_variable_map = self._upper_case_variable_map() | ||
modified_content = file_contents | ||
for name, ov in output_variables.items(): | ||
ov_original = ov['variable_name'] | ||
ov_original_upper = ov_original.upper() | ||
if ov_original_upper in upper_case_variable_map: | ||
ov_new = upper_case_variable_map[ov_original.upper()] | ||
logger.print(f"Found output variable to replace, going from {ov_original} to {ov_new}") | ||
modified_content['Output:Variable'][name]['variable_name'] = ov_new | ||
return modified_content | ||
|
||
|
||
class Version: | ||
|
||
@staticmethod | ||
def transform(file_contents: Dict, logger: SimpleLogger) -> Dict: | ||
logger.print("Processing Version Number Change") | ||
epjson_version_object = file_contents['Version'] | ||
sub_object = epjson_version_object["Version 1"] | ||
version_id = sub_object["version_identifier"] | ||
original_version = KnownVersions.version_enum_from_string(version_id) | ||
# TODO: Validate original version | ||
new_version = KnownVersions.NextVersion[original_version] | ||
new_version_string = KnownVersions.VersionStrings[new_version] | ||
file_contents['Version']['Version 1']['version_identifier'] = new_version_string | ||
return file_contents |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,30 @@ | ||
from pathlib import Path | ||
|
||
from epjson_transition.exceptions import MissingEpJSONFile | ||
from epjson_transition.file import EpJSONFile | ||
from epjson_transition.logger import SimpleLogger | ||
|
||
|
||
class EpJSONTransition: | ||
# main class for transitioning an EpJSON file | ||
def __init__(self, file_object: Path): | ||
if not file_object.exists(): | ||
... # fatal | ||
self.file = EpJSONFile(file_object) | ||
self.original_version = self.file.version.known_version | ||
|
||
def __init__(self, input_file_object: Path, output_file_object: Path): | ||
if not input_file_object.exists(): | ||
raise MissingEpJSONFile("Could not locate EpJSON file, expected at " + str(input_file_object)) | ||
self.input_file_object = input_file_object | ||
output_file_object.unlink(missing_ok=True) | ||
self.output_file_object = output_file_object | ||
|
||
def transform(self, logger: SimpleLogger): | ||
logger.print("Transition Process Starting") | ||
logger.print(f"Input File Path: {self.input_file_object}") | ||
logger.print(f"Output File Path: {self.output_file_object}") | ||
logger.print("Reading File Contents to Determine Version") | ||
logger.add_prefix() | ||
file = EpJSONFile(self.input_file_object, self.output_file_object, logger) | ||
logger.remove_prefix() | ||
logger.print("Calling Transition Routine") | ||
logger.add_prefix() | ||
file.transform(logger) | ||
logger.remove_prefix() | ||
logger.print("Transition Process Completed") | ||
logger.close() |
Oops, something went wrong.