Skip to content

Commit

Permalink
Fleshed out unit tests to support the given infant state
Browse files Browse the repository at this point in the history
  • Loading branch information
Myoldmopar committed Aug 2, 2021
1 parent 30ecec3 commit 2a1d09e
Show file tree
Hide file tree
Showing 24 changed files with 649 additions and 78 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/flake8.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ jobs:
with:
python-version: 3.8
- run: pip install -r requirements.txt
- run: flake8 epjson_transition
- run: flake8 epjson_transition/ tests/
34 changes: 22 additions & 12 deletions README.md
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.
19 changes: 16 additions & 3 deletions epjson_transition/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
# TODO: Add MalformedEpJSON to catch bad version objects, or whatever
class EpJSONTransitionError(Exception):
"""
Catch-all for EpJSON exceptions
"""
...

class BadEpJSONVersion(Exception):

class MissingEpJSONFile(EpJSONTransitionError):
...


class MalformedEpJSONFile(EpJSONTransitionError):
...


class BadEpJSONVersion(EpJSONTransitionError):
def __init__(self, entered_version: str):
super().__init__()
self.found_version = entered_version
Expand All @@ -9,7 +22,7 @@ def to_string(self) -> str:
return f"Found an invalid version in the EpJSON file: \"{self.found_version}\""


class MissingEpJSONVersion(Exception):
class MissingEpJSONVersion(EpJSONTransitionError):
@staticmethod
def to_string() -> str:
return "Version missing from EpJSON file, root object must have a \"Version\" key"
69 changes: 60 additions & 9 deletions epjson_transition/file.py
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")
30 changes: 30 additions & 0 deletions epjson_transition/logger.py
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)
35 changes: 35 additions & 0 deletions epjson_transition/main.py
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 removed epjson_transition/rules/95to96.py
Empty file.
24 changes: 24 additions & 0 deletions epjson_transition/rules/Transition95to96.py
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'}
46 changes: 42 additions & 4 deletions epjson_transition/rules/base.py
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")
50 changes: 50 additions & 0 deletions epjson_transition/rules/common.py
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
30 changes: 24 additions & 6 deletions epjson_transition/transition.py
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()
Loading

0 comments on commit 2a1d09e

Please sign in to comment.