Files
Jonathon Broughton f077320638 demo ready
2023-11-13 12:22:09 +00:00

216 lines
7.6 KiB
Python

"""
This module contains the business logic for a Speckle Automate function.
It demonstrates how to define input models, traverse and process data,
and generate reports based on user-specified criteria.
"""
from enum import Enum
from pydantic import Field
from speckle_automate import (
AutomateBase,
AutomationContext,
execute_automate_function,
)
from Rules.checks import BaseObjectRules
from Rules.traversal import get_data_traversal_rules
from Utilities.helpers import process_parameters, get_type_and_family, create_object_info
from Utilities.report import generate_report
class ThresholdMode(Enum):
"""
ThresholdMode: Defines different modes for reporting thresholds.
"""
ERROR = "ERROR"
WARN = "WARN"
INFO = "INFO"
class Format(Enum):
"""
Format: Enum for defining report formats.
"""
PDF = "PDF"
HTML = "HTML"
JSON = "JSON"
def create_one_of_enum(enum_cls):
"""
Helper function to create a JSON schema from an Enum class.
This is used for generating user input forms in the UI.
"""
return [{"const": item.value, "title": item.name} for item in enum_cls]
class FunctionInputs(AutomateBase):
"""
FunctionInputs: Defines user inputs for the automation function.
The structure is based on Pydantic models for data validation.
Please use the pydantic model schema to define your inputs:
https://docs.pydantic.dev/latest/usage/models/
"""
ids_xml_file: str = Field(
"https://example.com/project_standards/ids.xml",
title="IDS XML File",
description="URL or content of the IDS XML file defining project standards. e.g. https://example.com/project_standards/ids.xml",
json_schema_extra={
"readOnly": True,
"label": "https://example.com/project_standards/ids.xml",
},
)
bsdd_sheets: str = Field(
"https://example.com/project_standards/bsdd.json",
title="bsDD Sheet Identifier(s)",
description="Identifier or URL for the bsDD sheet relevant to the project. e.g. https://example.com/project_standards/bsdd.json",
json_schema_extra={
"readOnly": True,
"label": "https://example.com/project_standards/bsdd.json",
},
)
single_category: str = Field(
default="Windows",
title="Single Category for Demo",
description="For demonstration purposes only this is a single category. e.g. Windows.",
)
single_property: str = Field(
default="OmniClass Number",
title="Single Property for Demo",
description="For demonstration purposes only this is a single property. e.g. OmniClass Number.",
)
single_rule: str = Field(
default="23.30.20",
title="Rule for Demo",
description="For demonstration purposes only this is a single value for that property. e.g. Prefixed 23.30.20. ",
)
report_format: Format = Field(
default=Format.PDF,
title="Report Format",
description="Preferred format for the compliance report. e.g. PDF, HTML, JSON.",
json_schema_extra={
"oneOf": create_one_of_enum(Format),
},
)
threshold_mode: ThresholdMode = Field(
default=ThresholdMode.ERROR,
title="Reporting Threshold",
description="Set the threshold mode for reporting results: ERROR, WARN, or INFO.",
json_schema_extra={
"oneOf": create_one_of_enum(ThresholdMode),
},
)
def automate_function(
automate_context: AutomationContext,
function_inputs: FunctionInputs,
) -> None:
"""
The core logic of the Speckle Automate function.
Processes Speckle data and generates a report based on user inputs.
Args:
automate_context: Context object with data and methods for the run.
function_inputs: User-defined input values.
"""
# the context provides a convenient way, to receive the triggering version
version_root_object = automate_context.receive_version()
# Traverse the received Speckle data.
speckle_data = get_data_traversal_rules()
traversal_contexts_collection = speckle_data.traverse(version_root_object)
# Assuming each object has properties: name, type, and id
assessed_objects = {"missing": [], "invalid": [], "passing": []}
# Main loop for checking parameters
for context in traversal_contexts_collection:
current_object = context.current
is_category = BaseObjectRules.is_category(function_inputs.single_category)
if is_category(current_object) and hasattr(current_object, "parameters"):
assessment = process_parameters(current_object, function_inputs)
if assessment:
type_, family = get_type_and_family(current_object)
object_info = create_object_info(current_object, type_, family)
assessed_objects[assessment].append(object_info)
# Attach errors or info to objects based on their parameter evaluation state
for state, objects in assessed_objects.items():
ids = [obj["id"] for obj in objects if "id" in obj and obj["id"]]
if not ids:
continue
# Construct a detailed message for each object
detailed_messages = [
f"{obj['name']} (Type: {obj['type']}, ID: {obj['id']})"
for obj in objects
if "id" in obj and obj["id"]
]
# Combine messages into a single string
combined_message = (
f"Found {len(objects)} objects with {state} parameters: "
+ "; ".join(detailed_messages)
)
if state in ["missing", "invalid"]:
automate_context.attach_error_to_objects(
category=state.capitalize(), object_ids=ids, message=combined_message
)
else: # 'valid'
automate_context.attach_info_to_objects(
category=state.capitalize(), object_ids=ids, message=combined_message
)
# Generate and attach the report
report_format = (
function_inputs.report_format.value
) # Accessing the value of the Enum
report_file = generate_report(
assessed_objects,
report_format,
function_inputs.single_category,
function_inputs.single_property,
function_inputs.single_rule,
)
automate_context.store_file_result(report_file)
print("Report file: ", report_file)
# Determine overall automation success or failure
if assessed_objects["missing"] or assessed_objects["invalid"]:
total_objects = len(assessed_objects["missing"]) + len(assessed_objects["invalid"]) + len(
assessed_objects["passing"])
pass_rate = len(assessed_objects["passing"]) / total_objects * 100
invalid_rate = len(assessed_objects["invalid"]) / total_objects * 100
missing_rate = len(assessed_objects["missing"]) / total_objects * 100
success_rating_message = f"Pass rate: {pass_rate:.2f}%, Invalid rate: {invalid_rate:.2f}%, Missing rate: {missing_rate:.2f}%"
print(success_rating_message)
automate_context.mark_run_failed("Automation failed due to parameter issues. " + success_rating_message)
else:
automate_context.mark_run_success("All parameters are valid.")
# make sure to call the function with the executor
if __name__ == "__main__":
# NOTE: always pass in the automate function by its reference, do not invoke it!
# pass in the function reference with the inputs schema to the executor
execute_automate_function(automate_function, FunctionInputs)
# if the function has no arguments, the executor can handle it like so
# execute_automate_function(automate_function_without_inputs)