Skip to content

Commit

Permalink
fix(cli): Refactor validate CLI so command can be called as a function
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswmackey committed Aug 9, 2024
1 parent a7ab1a7 commit fdba6d2
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 61 deletions.
134 changes: 80 additions & 54 deletions honeybee/cli/validate.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""honeybee validation commands."""
import sys
import logging
import json
import json as py_json
import click

from honeybee.model import Model
Expand All @@ -16,7 +16,7 @@ def validate():


@validate.command('model')
@click.argument('model-json', type=click.Path(
@click.argument('model-file', type=click.Path(
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
@click.option(
'--check-all/--room-overlaps', ' /-ro', help='Flag to note whether the output '
Expand All @@ -37,8 +37,8 @@ def validate():
'--output-file', '-f', help='Optional file to output the full report '
'of the validation. By default it will be printed out to stdout',
type=click.File('w'), default='-')
def validate_model(model_json, check_all, plain_text, output_file):
"""Validate all properties of a Model file against the Honeybee schema.
def validate_model_cli(model_file, check_all, plain_text, output_file):
"""Validate all properties of a Model file against Honeybee schema.
This includes checking basic compliance with the 5 rules of honeybee geometry
as well as checks for all extension attributes. The 5 rules of honeybee geometry
Expand All @@ -58,88 +58,114 @@ def validate_model(model_json, check_all, plain_text, output_file):
\b
Args:
model_json: Full path to a Model JSON file.
model_file: Full path to a Model JSON file.
"""
try:
if plain_text:
# re-serialize the Model to make sure no errors are found
c_ver = folders.honeybee_core_version_str
s_ver = folders.honeybee_schema_version_str
click.echo(
'Validating Model using honeybee-core=={} and '
'honeybee-schema=={}'.format(c_ver, s_ver)
)
parsed_model = Model.from_hbjson(model_json)
click.echo('Re-serialization passed.')
# perform several other checks for geometry rules and others
if check_all:
report = parsed_model.check_all(raise_exception=False, detailed=False)
else:
report = parsed_model.check_room_volume_collisions(raise_exception=False)
click.echo('Model checks completed.')
# check the report and write the summary of errors
if report == '':
output_file.write('Congratulations! Your Model is valid!')
else:
error_msg = '\nYour Model is invalid for the following reasons:'
output_file.write('\n'.join([error_msg, report]))
else:
out_dict = {
'type': 'ValidationReport',
'app_name': 'Honeybee',
'app_version': folders.honeybee_core_version_str,
'schema_version': folders.honeybee_schema_version_str
}
try:
parsed_model = Model.from_hbjson(model_json)
out_dict['fatal_error'] = ''
if check_all:
errors = parsed_model.check_all(raise_exception=False, detailed=True)
else:
errors = parsed_model.check_room_volume_collisions(
raise_exception=False, detailed=True)
out_dict['errors'] = errors
out_dict['valid'] = True if len(out_dict['errors']) == 0 else False
except Exception as e:
out_dict['fatal_error'] = str(e)
out_dict['errors'] = []
out_dict['valid'] = False
output_file.write(json.dumps(out_dict, indent=4))
json = not plain_text
room_overlaps = not check_all
validate_model(model_file, room_overlaps, json, output_file)
except Exception as e:
_logger.exception('Model validation failed.\n{}'.format(e))
sys.exit(1)
else:
sys.exit(0)


def validate_model(model_file, room_overlaps=False, json=False, output_file=None,
check_all=True, plain_text=True):
"""Validate all properties of a Model file against the Honeybee schema.
This includes checking basic compliance with the 5 rules of honeybee geometry
as well as checks for all extension attributes.
Args:
model_file: Full path to a Honeybee Model file.
room_overlaps: Boolean to note whether the output validation report
should only validate the Room collisions (True) or all possible
issues with the model should be checked (False). (Default: False).
json: Boolean to note whether the output validation report should be
formatted as a JSON object instead of plain text.
"""
if not json:
# re-serialize the Model to make sure no errors are found
c_ver = folders.honeybee_core_version_str
s_ver = folders.honeybee_schema_version_str
ver_msg = 'Validating Model using honeybee-core=={} and ' \
'honeybee-schema=={}'.format(c_ver, s_ver)
print(ver_msg)
parsed_model = Model.from_file(model_file)
print('Re-serialization passed.')
# perform several other checks for geometry rules and others
if not room_overlaps:
report = parsed_model.check_all(raise_exception=False, detailed=False)
else:
report = parsed_model.check_room_volume_collisions(raise_exception=False)
print('Model checks completed.')
# check the report and write the summary of errors
if report == '':
full_msg = ver_msg + '\nCongratulations! Your Model is valid!'
else:
full_msg = ver_msg + \
'\nYour Model is invalid for the following reasons:\n' + report
if output_file is None:
return full_msg
else:
output_file.write(full_msg)
else:
out_dict = {
'type': 'ValidationReport',
'app_name': 'Honeybee',
'app_version': folders.honeybee_core_version_str,
'schema_version': folders.honeybee_schema_version_str
}
try:
parsed_model = Model.from_file(model_file)
out_dict['fatal_error'] = ''
if not room_overlaps:
errors = parsed_model.check_all(raise_exception=False, detailed=True)
else:
errors = parsed_model.check_room_volume_collisions(
raise_exception=False, detailed=True)
out_dict['errors'] = errors
out_dict['valid'] = True if len(out_dict['errors']) == 0 else False
except Exception as e:
out_dict['fatal_error'] = str(e)
out_dict['errors'] = []
out_dict['valid'] = False
if output_file is None:
return py_json.dumps(out_dict, indent=4)
else:
output_file.write(py_json.dumps(out_dict, indent=4))


@validate.command('room-volumes')
@click.argument('model-json', type=click.Path(
@click.argument('model-file', type=click.Path(
exists=True, file_okay=True, dir_okay=False, resolve_path=True))
@click.option(
'--output-file', '-f', help='Optional file to output the JSON strings of '
'ladybug_geometry LineSegment3Ds that represent naked and non-manifold edges. '
'By default it will be printed out to stdout', type=click.File('w'), default='-')
def validate_room_volumes(model_json, output_file):
def validate_room_volumes(model_file, output_file):
"""Get a list of all naked and non-manifold edges preventing closed room volumes.
This is helpful for visually identifying issues in geometry that are preventing
the room volume from reading as closed.
\b
Args:
model_json: Full path to a Model JSON file.
model_file: Full path to a Honeybee Model file.
"""
try:
# re-serialize the Model and collect all naked and non-manifold edges
parsed_model = Model.from_hbjson(model_json)
parsed_model = Model.from_file(model_file)
problem_edges = []
for room in parsed_model.rooms:
if not room.geometry.is_solid:
problem_edges.extend(room.geometry.naked_edges)
problem_edges.extend(room.geometry.non_manifold_edges)
# write the new model out to the file or stdout
prob_array = [lin.to_dict() for lin in problem_edges]
output_file.write(json.dumps(prob_array))
output_file.write(py_json.dumps(prob_array))
except Exception as e:
_logger.exception('Model room volume validation failed.\n{}'.format(e))
sys.exit(1)
Expand Down
14 changes: 7 additions & 7 deletions tests/cli_validate_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from click.testing import CliRunner
from honeybee.cli import viz, config
from honeybee.cli.validate import validate_model
from honeybee.cli.validate import validate_model_cli


def test_viz():
Expand All @@ -28,10 +28,10 @@ def test_validate_model():
incorrect_input_model = './tests/json/bad_geometry_model.hbjson'
if (sys.version_info >= (3, 7)):
runner = CliRunner()
result = runner.invoke(validate_model, [input_model])
result = runner.invoke(validate_model_cli, [input_model])
assert result.exit_code == 0
runner = CliRunner()
result = runner.invoke(validate_model, [incorrect_input_model])
result = runner.invoke(validate_model_cli, [incorrect_input_model])
outp = result.output
assert 'Your Model is invalid for the following reasons' in outp
assert 'is not coplanar or fully bounded by its parent Face' in outp
Expand All @@ -42,13 +42,13 @@ def test_validate_model_json():
incorrect_input_model = './tests/json/bad_geometry_model.hbjson'
if (sys.version_info >= (3, 7)):
runner = CliRunner()
result = runner.invoke(validate_model, [input_model, '--json'])
result = runner.invoke(validate_model_cli, [input_model, '--json'])
assert result.exit_code == 0
outp = result.output
valid_report = json.loads(outp)
assert valid_report['valid']
runner = CliRunner()
result = runner.invoke(validate_model, [incorrect_input_model, '--json'])
result = runner.invoke(validate_model_cli, [incorrect_input_model, '--json'])
outp = result.output
valid_report = json.loads(outp)
assert not valid_report['valid']
Expand All @@ -59,7 +59,7 @@ def test_validate_mismatched_adjacency():
incorrect_input_model = './tests/json/mismatched_area_adj.hbjson'
if (sys.version_info >= (3, 7)):
runner = CliRunner()
result = runner.invoke(validate_model, [incorrect_input_model, '--json'])
result = runner.invoke(validate_model_cli, [incorrect_input_model, '--json'])
outp = result.output
valid_report = json.loads(outp)
assert not valid_report['valid']
Expand All @@ -71,7 +71,7 @@ def test_colliding_room_volumes():
incorrect_input_model = './tests/json/colliding_room_volumes.hbjson'
if (sys.version_info >= (3, 7)):
runner = CliRunner()
result = runner.invoke(validate_model, [incorrect_input_model, '--json'])
result = runner.invoke(validate_model_cli, [incorrect_input_model, '--json'])
outp = result.output
valid_report = json.loads(outp)
assert not valid_report['valid']
Expand Down

0 comments on commit fdba6d2

Please sign in to comment.