Skip to content

Commit

Permalink
feat(cli): Implement a standard for running CLI functions
Browse files Browse the repository at this point in the history
This commit also includes a new commandutil module that helps format CLI-like inputs for the command.
  • Loading branch information
chriswmackey authored and Chris Mackey committed Jul 8, 2024
1 parent 041986a commit 6a3ca6b
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 28 deletions.
109 changes: 90 additions & 19 deletions ladybug/cli/translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,26 +31,49 @@ def translate():
@click.option('--output-file', '-f', help='Optional .wea file path to output the Wea '
'string of the translation. By default this will be printed out to stdout',
type=click.File('w'), default='-')
def epw_to_wea(epw_file, analysis_period, timestep, output_file):
def epw_to_wea_cli(epw_file, analysis_period, timestep, output_file):
"""Translate an .epw file to a .wea file.
\b
Args:
epw_file: Path to an .epw file.
"""
try:
wea_obj = Wea.from_epw_file(epw_file, timestep)
analysis_period = _load_analysis_period_str(analysis_period)
if analysis_period is not None:
wea_obj = wea_obj.filter_by_analysis_period(analysis_period)
output_file.write(wea_obj.to_file_string())
epw_to_wea(epw_file, analysis_period, timestep, output_file)
except Exception as e:
_logger.exception('Wea translation failed.\n{}'.format(e))
sys.exit(1)
else:
sys.exit(0)


def epw_to_wea(epw_file, analysis_period=None, timestep=1, output_file=None):
"""Translate an .epw file to a .wea file.
Args:
epw_file: Path to an .epw file.
analysis_period: An AnalysisPeriod string to filter the datetimes in the
resulting Wea (eg. "6/21 to 9/21 between 8 and 16 @1"). If None,
the Wea will be annual.
timestep: An optional integer to set the number of time steps per hour.
Default is 1 for one value per hour. Note that this input will only
do a linear interpolation over the data in the EPW file.
output_file: Optional file to output the string of the Wea file contents.
If None, the string will simply be returned from this method.
"""
wea_obj = Wea.from_epw_file(epw_file, timestep)
analysis_period = _load_analysis_period_str(analysis_period)
if analysis_period is not None:
wea_obj = wea_obj.filter_by_analysis_period(analysis_period)
if output_file is None:
return wea_obj.to_file_string()
elif isinstance(output_file, str):
with open(output_file, 'w') as of:
of.write(wea_obj.to_file_string())
else:
output_file.write(wea_obj.to_file_string())


@translate.command('epw-to-ddy')
@click.argument('epw-file', type=click.Path(
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
Expand All @@ -61,10 +84,10 @@ def epw_to_wea(epw_file, analysis_period, timestep, output_file):
@click.option('--output-file', '-f', help='Optional .wea file path to output the Wea '
'string of the translation. By default this will be printed out to stdout',
type=click.File('w'), default='-')
def epw_to_ddy(epw_file, percentile, output_file):
def epw_to_ddy_cli(epw_file, percentile, output_file):
"""Get a DDY file with a heating + cooling design day from this EPW.
This method will first check if there is a heating or cooling design day
This command will first check if there is a heating or cooling design day
that meets the input percentile within the EPW itself. If None is
found, the heating and cooling design days will be derived from analysis
of the annual data within the EPW, which is usually less accurate.
Expand All @@ -74,16 +97,41 @@ def epw_to_ddy(epw_file, percentile, output_file):
epw_file: Path to an .epw file.
"""
try:
epw_obj = EPW(epw_file)
ddy_obj = DDY(epw_obj.location, epw_obj.best_available_design_days(percentile))
output_file.write(ddy_obj.to_file_string())
epw_to_ddy(epw_file, percentile, output_file)
except Exception as e:
_logger.exception('DDY translation failed.\n{}'.format(e))
sys.exit(1)
else:
sys.exit(0)


def epw_to_ddy(epw_file, percentile=0.4, output_file=None):
"""Get a DDY file with a heating + cooling design day from this EPW.
This function will first check if there is a heating or cooling design day
that meets the input percentile within the EPW itself. If None is
found, the heating and cooling design days will be derived from analysis
of the annual data within the EPW, which is usually less accurate.
Args:
epw_file: Path to an .epw file.
percentile: A number between 0 and 50 for the percentile difference from
the most extreme conditions within the EPW to be used for the design
day. Typical values are 0.4 and 1.0. (Default: 0.4).
output_file: Optional file to output the string of the DDY file contents.
If None, the string will simply be returned from this method.
"""
epw_obj = EPW(epw_file)
ddy_obj = DDY(epw_obj.location, epw_obj.best_available_design_days(percentile))
if output_file is None:
return ddy_obj.to_file_string()
elif isinstance(output_file, str):
with open(output_file, 'w') as of:
of.write(ddy_obj.to_file_string())
else:
output_file.write(ddy_obj.to_file_string())


@translate.command('wea-to-constant')
@click.argument('wea-file', type=click.Path(
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
Expand All @@ -93,7 +141,7 @@ def epw_to_ddy(epw_file, percentile, output_file):
@click.option('--output-file', '-f', help='Optional .wea file path to output the Wea '
'string of the translation. By default this will be printed out to stdout',
type=click.File('w'), default='-')
def wea_to_constant(wea_file, value, output_file):
def wea_to_constant_cli(wea_file, value, output_file):
"""Convert a Wea or an EPW file to have a constant value for each datetime.
This is useful in workflows where hourly irradiance values are inconsequential
Expand All @@ -105,15 +153,38 @@ def wea_to_constant(wea_file, value, output_file):
wea_file: Full path to .wea file. This can also be an .epw file.
"""
try:
with open(wea_file) as inf:
first_word = inf.read(5)
is_wea = True if first_word == 'place' else False
if not is_wea:
_wea_file = os.path.join(os.path.dirname(wea_file), 'epw_to_wea.wea')
wea_file = Wea.from_epw_file(wea_file).write(_wea_file)
output_file.write(Wea.to_constant_value(wea_file, value))
wea_to_constant(wea_file, value, output_file)
except Exception as e:
_logger.exception('Wea translation failed.\n{}'.format(e))
sys.exit(1)
else:
sys.exit(0)


def wea_to_constant(wea_file, value=1000, output_file=None):
"""Convert a Wea or an EPW file to have a constant value for each datetime.
This is useful in workflows where hourly irradiance values are inconsequential
to the analysis and one is only using the Wea as a format to pass location
and datetime information (eg. for direct sun hours).
Args:
wea_file: Full path to .wea file. This can also be an .epw file.
value: The direct and diffuse irradiance value that will be written in
for all datetimes of the Wea. (Default: 1000).
output_file: Optional file to output the string of the Wea file contents.
If None, the string will simply be returned from this method.
"""
with open(wea_file) as inf:
first_word = inf.read(5)
is_wea = True if first_word == 'place' else False
if not is_wea:
_wea_file = os.path.join(os.path.dirname(wea_file), 'epw_to_wea.wea')
wea_file = Wea.from_epw_file(wea_file).write(_wea_file)
if output_file is None:
return Wea.to_constant_value(wea_file, value)
elif isinstance(output_file, str):
with open(output_file, 'w') as of:
of.write(Wea.to_constant_value(wea_file, value))
else:
output_file.write(Wea.to_constant_value(wea_file, value))
31 changes: 31 additions & 0 deletions ladybug/commandutil.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# coding=utf-8
"""Utility functions for running the functions of CLI commands outside of the CLI."""


def run_command_function(command_function, arguments=None, options=None):
"""Run a function used within the Ladybug Tools CLI.
Args:
command_function: The CLI function to be executed.
arguments: A list of required arguments for the command_function. None can
be used if the command function has no required arguments. (Default: None).
options: A dictionary of options to be passed to the function. The keys
of this dictionary should be the full names of the options or flags
in the CLI and formatted with dashes (eg. --output-file). The values
of the dictionary should be the values to be passed for each of
the options. For the case of flag arguments, the value should simply
be an empty string. None can be used here to indicate that all default
values for options should be used. (Default: None).
"""
# process the arguments and options
args = () if arguments is None else arguments
kwargs = {}
if options is not None:
for key, val in options.items():
clean_key = key[2:] if key.startswith('--') else key
clean_key = clean_key.replace('-', '_')
clean_val = True if val == '' else val
kwargs[clean_key] = clean_val

# run the command using arguments and keyword arguments
return command_function(*args, **kwargs)
45 changes: 36 additions & 9 deletions tests/cli_translate_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,54 +2,81 @@
from click.testing import CliRunner
import os

from ladybug.cli.translate import epw_to_wea, epw_to_ddy, wea_to_constant
from ladybug.cli.translate import epw_to_wea, epw_to_wea_cli, \
epw_to_ddy_cli, epw_to_ddy, wea_to_constant_cli, wea_to_constant
from ladybug.commandutil import run_command_function
from ladybug.analysisperiod import AnalysisPeriod




def test_epw_to_wea():
runner = CliRunner()
input_epw = './tests/assets/epw/chicago.epw'

result = runner.invoke(epw_to_wea, [input_epw])
result = runner.invoke(epw_to_wea_cli, [input_epw])
assert result.exit_code == 0

output_wea = './tests/assets/wea/test.wea'
result = runner.invoke(epw_to_wea, [input_epw, '--output-file', output_wea])
result = runner.invoke(epw_to_wea_cli, [input_epw, '--output-file', output_wea])
assert result.exit_code == 0
assert os.path.isfile(output_wea)
os.remove(output_wea)

wea_str = run_command_function(epw_to_wea, [input_epw])
assert isinstance(wea_str, str)
assert len(wea_str) > 100000

run_command_function(
epw_to_wea, [input_epw], {'--output-file': output_wea})
assert os.path.isfile(output_wea)
os.remove(output_wea)


def test_epw_to_wea_analysis_period():
runner = CliRunner()
input_epw = './tests/assets/epw/chicago.epw'

a_per = AnalysisPeriod(6, 21, 8, 9, 21, 17)
result = runner.invoke(epw_to_wea, [input_epw, '--analysis-period', str(a_per)])
result = runner.invoke(epw_to_wea_cli, [input_epw, '--analysis-period', str(a_per)])
assert result.exit_code == 0

wea_str = run_command_function(
epw_to_wea, [input_epw], {'--analysis-period': str(a_per)})
assert isinstance(wea_str, str)
assert 10000 < len(wea_str) < 100000


def test_epw_to_ddy():
runner = CliRunner()
input_epw = './tests/assets/epw/chicago.epw'

result = runner.invoke(epw_to_ddy, [input_epw])
result = runner.invoke(epw_to_ddy_cli, [input_epw])
assert result.exit_code == 0

output_ddy = './tests/assets/ddy/test.ddy'
result = runner.invoke(epw_to_ddy, [input_epw, '--output-file', output_ddy])
result = runner.invoke(epw_to_ddy_cli, [input_epw, '--output-file', output_ddy])
assert result.exit_code == 0
assert os.path.isfile(output_ddy)
os.remove(output_ddy)

ddy_str = run_command_function(epw_to_ddy, [input_epw])
assert isinstance(ddy_str, str)
assert len(ddy_str) > 1000

run_command_function(
epw_to_ddy, [input_epw], {'--output-file': output_ddy})
assert os.path.isfile(output_ddy)
os.remove(output_ddy)


def test_wea_to_constant():
runner = CliRunner()
input_wea = './tests/assets/wea/chicago.wea'

result = runner.invoke(wea_to_constant, [input_wea, '-v', '500'])
result = runner.invoke(wea_to_constant_cli, [input_wea, '-v', '500'])
assert result.exit_code == 0
lines = result.output.split('\n')
assert '500' in lines[10]

wea_str = run_command_function(wea_to_constant, [input_wea], {'--value': '500'})
assert isinstance(wea_str, str)
assert len(wea_str) > 1000

0 comments on commit 6a3ca6b

Please sign in to comment.