Compare commits

..

1 Commits

Author SHA1 Message Date
renovate[bot] 28a4302014 Update python Docker tag to v3.13 2025-02-17 23:53:04 +00:00
12 changed files with 481 additions and 845 deletions
+1 -3
View File
@@ -31,9 +31,7 @@ jobs:
- name: Extract functionInputSchema
id: extract_schema
run: |
python main.py generate_schema "${HOME}/${{ env.FUNCTION_SCHEMA_FILE_NAME }}"
echo "Checking if functionSchema.json exists after generation..."
ls -lah "${HOME}/${{ env.FUNCTION_SCHEMA_FILE_NAME }}"
python main.py generate_schema "${{ env.FUNCTION_SCHEMA_FILE_NAME }}"
# Step 5: Build and publish the Speckle function
- name: Speckle Automate Function - Build and Publish
+11 -10
View File
@@ -1,15 +1,16 @@
# Use the official Python 3.11 slim image as the base
FROM python:3.11-slim
# We use the official Python 3.11 image as our base image and will add our code to it. For more details, see https://hub.docker.com/_/python
FROM python:3.13-slim
# Set the working directory inside the container
# We install poetry to generate a list of dependencies which will be required by our application
RUN pip install poetry==1.8.4
# We set the working directory to be the /home/speckle directory; all of our files will be copied here.
WORKDIR /home/speckle
# Copy the application files to the working directory
# Copy all of our code and assets from the local directory into the /home/speckle directory of the container.
# We also ensure that the user 'speckle' owns these files, so it can access them
# This assumes that the Dockerfile is in the same directory as the rest of the code
COPY . /home/speckle
# Upgrade pip and install dependencies using requirements.txt
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r /home/speckle/requirements.txt
# Set the entrypoint for running the Speckle function
CMD ["python", "-u", "main.py", "run"]
# Using poetry, we generate a list of requirements, save them to requirements.txt, and then use pip to install them
RUN poetry export --format requirements.txt --output /home/speckle/requirements.txt && pip install --requirement /home/speckle/requirements.txt
+3 -3
View File
@@ -1,7 +1,7 @@
annotated-types==0.7.0
anyio==4.8.0
appdirs==1.4.4
attrs==25.1.0
attrs==23.2.0
backoff==2.2.1
black==25.1.0
certifi==2025.1.31
@@ -13,7 +13,7 @@ gql==3.5.0
graphql-core==3.2.6
h11==0.14.0
httpcore==1.0.7
httpx==0.28.1
httpx==0.25.2
idna==3.10
iniconfig==2.0.0
levenshtein==0.26.1
@@ -49,6 +49,6 @@ typing-extensions==4.12.2
tzdata==2025.1
ujson==5.10.0
urllib3==2.3.0
websockets==15.0
websockets==11.0.3
wrapt==1.17.2
yarl==1.18.3
+11 -21
View File
@@ -1,13 +1,12 @@
"""This is the main function that will be executed when the automation is triggered.
It will receive the inputs from the user, and the context of the run.
It will then apply the rules to the objects in the model, and report back the results.
"""
# This is the main function that will be executed when the automation is triggered.
# It will receive the inputs from the user, and the context of the run.
# It will then apply the rules to the objects in the model, and report back the results.
from pandas import DataFrame
from speckle_automate import AutomationContext
from specklepy.objects.base import Base
from src.helpers import flatten_base, speckle_print
from src.helpers import flatten_base
from src.inputs import FunctionInputs
from src.rule_processor import apply_rules_to_objects
from src.spreadsheet import read_rules_from_spreadsheet
@@ -39,25 +38,16 @@ def automate_function(
global VERSION
VERSION = getattr(version_root_object, "version", 2) # noqa: F841SION = getattr(version_root_object,"version", 2) # noqa: F841 # noqa: F841
# Read and group rules
grouped_rules, messages = read_rules_from_spreadsheet(function_inputs.spreadsheet_url)
# read the rules from the spreadsheet
rules: DataFrame = read_rules_from_spreadsheet(function_inputs.spreadsheet_url)
# Handle any validation messages
for message in messages:
speckle_print(message) # or log them appropriately
if (rules is None) or (len(rules) == 0):
automate_context.mark_run_exception("No rules defined")
if grouped_rules is None:
automate_context.mark_run_exception("Failed to process rules")
return
grouped_rules = rules.groupby("Rule Number")
# apply the rules to the objects
apply_rules_to_objects(
flat_list_of_objects,
grouped_rules,
automate_context,
minimum_severity=function_inputs.minimum_severity,
hide_skipped=function_inputs.hide_skipped,
)
apply_rules_to_objects(flat_list_of_objects, grouped_rules, automate_context)
# set the automation context view, to the original model / VERSION view
automate_context.set_context_view()
+16 -33
View File
@@ -5,20 +5,9 @@ from speckle_automate import AutomateBase
class PropertyMatchMode(Enum):
"""Controls how strictly parameter names must match."""
STRICT = "strict" # Exact parameter path match
FUZZY = "fuzzy" # Search all parameters ignoring hierarchy
MIXED = "mixed" # Exact match first, fuzzy fallback
class MinimumSeverity(str, Enum):
"""Enum for minimum severity level to report."""
INFO = "Info"
WARNING = "Warning"
ERROR = "Error"
STRICT = "strict" # Exact parameter path match
FUZZY = "fuzzy" # Search all parameters ignoring hierarchy
MIXED = "mixed" # Exact match first, fuzzy fallback
class FunctionInputs(AutomateBase):
"""These are function author defined values.
@@ -28,29 +17,23 @@ class FunctionInputs(AutomateBase):
https://docs.pydantic.dev/latest/usage/models/
"""
# In this exercise, we will move rules to an external source so not to hardcode them.
spreadsheet_url: str = Field(
title="Spreadsheet URL",
description="This is the URL of the spreadsheet to check. It should be a TSV format data source.",
)
minimum_severity: MinimumSeverity = Field(
default=MinimumSeverity.INFO,
title="Minimum Severity Level",
description="Only report test results with this severity level or higher. Info will show all results, Warning will show warnings and errors, Error will show only errors.",
property_match_mode: PropertyMatchMode = Field(
default=PropertyMatchMode.MIXED,
title="Property Match Mode",
description='Controls how strictly parameter names must match. ' +
'STRICT will only match exact parameter paths, ' +
'FUZZY will search all parameters ignoring hierarchy, ' +
'MIXED will exact match first, fuzzy fallback.'
)
hide_skipped: bool = Field(
default=False,
title="Hide Skipped Tests",
description="If enabled, tests that were skipped (no matching objects found) will not be reported.",
)
# property_match_mode: PropertyMatchMode = Field(
# default=PropertyMatchMode.MIXED,
# title="Property Match Mode",
# description='Controls how strictly parameter names must match. ' +
# 'STRICT will only match exact parameter paths, ' +
# 'FUZZY will search all parameters ignoring hierarchy, ' +
# 'MIXED will exact match first, fuzzy fallback.'
# )
+32 -138
View File
@@ -1,5 +1,3 @@
"""Module for processing rules against Speckle objects and updating the automate context with the results."""
from enum import Enum
from typing import Any
@@ -8,52 +6,11 @@ from pandas.core.groupby import DataFrameGroupBy
from speckle_automate import AutomationContext, ObjectResultLevel
from specklepy.objects.base import Base
from src.inputs import MinimumSeverity
from src.helpers import speckle_print
from src.predicates import PREDICATE_METHOD_MAP
from src.rules import PropertyRules
def validate_rule_structure(rule_group: pd.DataFrame) -> None:
"""Validates the structure and logic of a rule group.
Args:
rule_group: DataFrame containing the rule conditions
Raises:
ValueError: If rule structure is invalid
"""
if rule_group.empty:
return
# Validate Logic column exists
if "Logic" not in rule_group.columns:
raise ValueError("Rule must have a 'Logic' column")
# Get uppercase Logic values for case-insensitive comparison
logic_values = rule_group["Logic"].str.upper()
# Check if first condition is WHERE
if logic_values.iloc[0] != "WHERE":
raise ValueError(f"Rule {rule_group.iloc[0]['Rule Number']} must start with WHERE")
# Count CHECK conditions
check_count = sum(1 for value in logic_values if value == "CHECK")
if check_count > 1:
raise ValueError(f"Rule {rule_group.iloc[0]['Rule Number']} has multiple CHECK conditions")
# If CHECK exists, ensure it's the last condition
check_indices = logic_values[logic_values == "CHECK"].index
if check_count == 1 and check_indices[0] != rule_group.index[-1]:
raise ValueError(f"CHECK must be the last condition in rule {rule_group.iloc[0]['Rule Number']}")
# Validate Logic values
valid_values = {"WHERE", "AND", "CHECK"}
invalid_values = set(logic_values.unique()) - valid_values
if invalid_values:
raise ValueError(f"Invalid Logic values found: {invalid_values}")
def evaluate_condition(
speckle_object: Base, condition: pd.Series, rule_number: str | None = None, case_number: int | None = None
) -> bool:
@@ -85,52 +42,12 @@ def evaluate_condition(
method = getattr(PropertyRules, method_name, None)
if method:
return method(speckle_object, property_name, value)
check_answer = method(speckle_object, property_name, value)
return check_answer
return False
def get_filters_and_check(rule_group: pd.DataFrame) -> tuple[pd.DataFrame, pd.Series]:
"""Separates rule conditions into filters and final check.
Args:
rule_group: DataFrame containing rule conditions
Returns:
Tuple containing filter conditions and final check condition
"""
if rule_group.empty:
return pd.DataFrame(), pd.Series()
# Get uppercase Logic values for case-insensitive comparison
logic_values = rule_group["Logic"].str.upper()
# Look for explicit CHECK
check_conditions = rule_group[logic_values == "CHECK"]
has_explicit_check = not check_conditions.empty
if has_explicit_check:
# Use first CHECK condition as final check
final_check = check_conditions.iloc[0]
# All other conditions are filters
filters = rule_group[logic_values != "CHECK"]
else:
# Legacy behavior: use last AND as check if present
and_conditions = rule_group[logic_values == "AND"]
if not and_conditions.empty:
# Get the last AND as the check
final_check = and_conditions.iloc[-1]
# All conditions up to the last AND are filters
last_and_idx = and_conditions.index[-1]
filters = rule_group[rule_group.index < last_and_idx]
else:
# No AND conditions found, just use WHERE as filter
filters = rule_group
final_check = rule_group.iloc[0] # Default to first condition as check
return filters, final_check
def process_rule(
speckle_objects: list[Base], rule_group: pd.DataFrame
) -> tuple[list[Any], list[Any]] | tuple[list[Base], list[Base]]:
@@ -145,47 +62,36 @@ def process_rule(
Returns:
A tuple of lists containing objects that passed and failed the rule.
"""
if not speckle_objects or rule_group.empty:
# Extract the 'WHERE' condition and subsequent 'AND' conditions
filter_condition = rule_group.iloc[0]
subsequent_conditions = rule_group.iloc[1:]
# get the last row of the rule_group and get the Message and Report Severity
rule_info = rule_group.iloc[-1]
rule_number = rule_info["Rule Number"]
# Filter objects based on the 'WHERE' condition
filtered_objects = [
speckle_object for speckle_object in speckle_objects if evaluate_condition(speckle_object, filter_condition)
]
if not filtered_objects or len(list(filtered_objects)) == 0:
return [], []
try:
validate_rule_structure(rule_group)
except ValueError as e:
speckle_print(f"Rule validation error: {str(e)}")
return [], []
# Initialize lists for passed and failed objects
pass_objects, fail_objects = [], []
# Get filters and final check
filters, final_check = get_filters_and_check(rule_group)
# Start with all objects
filtered_objects = speckle_objects.copy()
rule_number = rule_group.iloc[0]["Rule Number"]
# Apply each filter condition sequentially
for index, (_, filter_condition) in enumerate(filters.iterrows()):
filtered_objects = [
obj
for obj in filtered_objects
if evaluate_condition(
speckle_object=obj, condition=filter_condition, rule_number=rule_number, case_number=index
# Evaluate each filtered object against the 'AND' conditions
for speckle_object in filtered_objects:
if all(
evaluate_condition(
speckle_object=speckle_object, condition=condition, rule_number=rule_number, case_number=index
)
]
# Early exit if no objects pass filters
if not filtered_objects:
return [], []
# For remaining objects, evaluate the final check
pass_objects = []
fail_objects = []
for obj in filtered_objects:
if evaluate_condition(
speckle_object=obj, condition=final_check, rule_number=rule_number, case_number=len(filters)
for index, condition in subsequent_conditions.iterrows()
):
pass_objects.append(obj)
pass_objects.append(speckle_object)
else:
fail_objects.append(obj)
fail_objects.append(speckle_object)
return pass_objects, fail_objects
@@ -194,8 +100,6 @@ def apply_rules_to_objects(
speckle_objects: list[Base],
grouped_rules: DataFrameGroupBy,
automate_context: AutomationContext,
minimum_severity: MinimumSeverity = MinimumSeverity.INFO,
hide_skipped: bool = False,
) -> dict[str, tuple[list[Base], list[Base]]]:
"""Applies defined rules to a list of objects and updates the automate context based on the results.
@@ -203,16 +107,14 @@ def apply_rules_to_objects(
speckle_objects (List[Base]): The list of objects to which rules are applied.
grouped_rules (pd.DataFrameGroupBy): The DataFrame containing rule definitions.
automate_context (Any): Context manager for attaching rule results.
minimum_severity: Minimum severity level to report
hide_skipped: Whether to hide skipped tests
"""
grouped_results = {}
rules_processed = 0
severity_levels = {MinimumSeverity.INFO: 0, MinimumSeverity.WARNING: 1, MinimumSeverity.ERROR: 2}
min_severity_level = severity_levels[minimum_severity]
for rule_id, rule_group in grouped_rules:
rule_id_str = str(rule_id) # Convert rule_id to string
rules_processed += 1
# Ensure rule_group has necessary columns
@@ -221,25 +123,17 @@ def apply_rules_to_objects(
pass_objects, fail_objects = process_rule(speckle_objects, rule_group)
# Get the severity level for this rule
rule_severity = get_severity(rule_group.iloc[-1])
rule_severity_level = severity_levels[MinimumSeverity(rule_severity.value)]
attach_results(pass_objects, rule_group.iloc[-1], rule_id_str, automate_context, True)
attach_results(fail_objects, rule_group.iloc[-1], rule_id_str, automate_context, False)
# For passing objects, only attach if we're showing all levels (INFO)
if minimum_severity == MinimumSeverity.INFO:
attach_results(pass_objects, rule_group.iloc[-1], rule_id_str, automate_context, True)
# For failing objects, attach if they meet minimum severity threshold
if rule_severity_level >= min_severity_level:
attach_results(fail_objects, rule_group.iloc[-1], rule_id_str, automate_context, False)
if len(pass_objects) == 0 and len(fail_objects) == 0 and not hide_skipped:
if len(pass_objects) == 0 and len(fail_objects) == 0:
automate_context.attach_info_to_objects(
category=f"Rule {rule_id_str} Skipped",
object_ids=["0"], # This is a hack to get a rule to report with no valid objects
message=f"No objects found for rule {rule_id_str}",
metadata={},
)
# pass
grouped_results[rule_id_str] = (pass_objects, fail_objects)
+18 -37
View File
@@ -1,5 +1,3 @@
"""A collection of rules for processing Speckle objects and their properties."""
import math
import re
from typing import Any
@@ -245,39 +243,27 @@ class PropertyRules:
@staticmethod
def is_parameter_value_greater_than(speckle_object: Base, parameter_name: str, threshold: str) -> bool:
"""Checks if parameter value is greater than threshold.
Note: From a UX perspective, if someone writes 'height greater than 2401',
they mean "flag an error if height <= 2401". So we flip the comparison.
"""
"""Checks if parameter value is greater than threshold."""
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
if parameter_value is None:
return False
if not isinstance(parameter_value, int | float):
raise ValueError(f"Parameter value must be a number, got {type(parameter_value)}")
return parameter_value <= PropertyRules.parse_number_from_string(threshold)
return parameter_value > PropertyRules.parse_number_from_string(threshold)
@staticmethod
def is_parameter_value_less_than(speckle_object: Base, parameter_name: str, threshold: str) -> bool:
"""Checks if parameter value is less than threshold.
Note: From a UX perspective, if someone writes 'height less than 2401',
they mean "flag an error if height >= 2401". So we flip the comparison.
"""
"""Checks if parameter value is less than threshold."""
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
if parameter_value is None:
return False
if not isinstance(parameter_value, int | float):
raise ValueError(f"Parameter value must be a number, got {type(parameter_value)}")
return parameter_value >= PropertyRules.parse_number_from_string(threshold)
return parameter_value < PropertyRules.parse_number_from_string(threshold)
@staticmethod
def is_parameter_value_in_range(speckle_object: Base, parameter_name: str, value_range: str) -> bool:
"""Checks if parameter value falls outside specified range.
Note: From a UX perspective, if someone writes 'height in range 2401,3000',
they mean "flag an error if height < 2401 or height > 3000".
"""
"""Checks if parameter value falls within range."""
min_value, max_value = value_range.split(",")
min_value = PropertyRules.parse_number_from_string(min_value)
max_value = PropertyRules.parse_number_from_string(max_value)
@@ -325,7 +311,7 @@ class PropertyRules:
return is_value_in_list(parameter_value, value_list)
@staticmethod
def check_boolean_value(value: Any, values_to_match: tuple[str, ...]) -> bool:
def _check_boolean_value(value: Any, values_to_match: tuple[str, ...]) -> bool:
"""Check if a value matches any target value in expected format."""
if isinstance(value, bool):
return value is (True if "true" in values_to_match else False)
@@ -339,13 +325,13 @@ class PropertyRules:
def is_parameter_value_true(speckle_object: Base, parameter_name: str) -> bool:
"""Check if parameter value represents true."""
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
return PropertyRules.check_boolean_value(parameter_value, ("yes", "true", "1"))
return PropertyRules._check_boolean_value(parameter_value, ("yes", "true", "1"))
@staticmethod
def is_parameter_value_false(speckle_object: Base, parameter_name: str) -> bool:
"""Check if parameter value represents false."""
parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
return PropertyRules.check_boolean_value(parameter_value, ("no", "false", "0"))
return PropertyRules._check_boolean_value(parameter_value, ("no", "false", "0"))
@staticmethod
def has_category(speckle_object: Base) -> bool:
@@ -364,7 +350,7 @@ class PropertyRules:
return PropertyRules.get_parameter_value(speckle_object, "category")
@staticmethod
def try_boolean_comparison(value1: Any, value2: Any, allow_yes_no: bool) -> tuple[bool, bool]:
def _try_boolean_comparison(value1: Any, value2: Any, allow_yes_no: bool) -> tuple[bool, bool]:
"""Attempts to compare two values as booleans."""
def strict_convert_boolean(value: Any) -> Any:
@@ -394,7 +380,7 @@ class PropertyRules:
return False, False
@staticmethod
def compare_values(
def _compare_values(
value1: Any,
value2: Any,
case_sensitive: bool = False,
@@ -415,20 +401,15 @@ class PropertyRules:
bool: True if values are considered equal, False otherwise
"""
# Try boolean comparison first
can_compare, result = PropertyRules.try_boolean_comparison(value1, value2, allow_yes_no_bools)
can_compare, result = PropertyRules._try_boolean_comparison(value1, value2, allow_yes_no_bools)
if can_compare:
return result
# Handle case where one value is a string that can be interpreted as a number
def safe_convert_to_number(val):
if isinstance(val, str):
val = val.strip() # Remove whitespace
if val.replace(".", "", 1).replace("-", "", 1).isdigit(): # Handle negative numbers
return float(val)
return val
value1 = safe_convert_to_number(value1)
value2 = safe_convert_to_number(value2)
if isinstance(value1, str) and value1.replace(".", "", 1).isdigit():
value1 = float(value1)
if isinstance(value2, str) and value2.replace(".", "", 1).isdigit():
value2 = float(value2)
# For strings: Allow case insensitivity if specified
if isinstance(value1, str) and isinstance(value2, str):
@@ -469,7 +450,7 @@ class PropertyRules:
if parameter_value is None:
return False
return PropertyRules.compare_values(
return PropertyRules._compare_values(
parameter_value, value_to_match, case_sensitive, tolerance, allow_yes_no_bools=True
)
@@ -497,7 +478,7 @@ class PropertyRules:
if parameter_value is None:
return True # Non-existent parameters are considered not equal
return not PropertyRules.compare_values(
return not PropertyRules._compare_values(
parameter_value, value_to_match, case_sensitive, tolerance, allow_yes_no_bools=True
)
@@ -519,6 +500,6 @@ class PropertyRules:
if parameter_value is None:
return False
return PropertyRules.compare_values(
return PropertyRules._compare_values(
parameter_value, value_to_match, case_sensitive=True, tolerance=0, allow_yes_no_bools=False, use_exact=True
)
+14 -119
View File
@@ -1,143 +1,38 @@
"""Module for reading and processing rules from a cloud hosted TSV file."""
import pandas as pd
from pandas import DataFrame
from pandas.core.groupby import DataFrameGroupBy
def process_rule_numbers(df: DataFrame) -> DataFrame:
"""Process rule numbers in a DataFrame.
1. Finding rule groups based on 'WHERE' logic
2. Using the first non-null rule number in each group
3. Handling duplicate rule numbers by incrementing
def read_rules_from_spreadsheet(url: str) -> DataFrame | None:
"""Reads a TSV file from a provided URL and returns a DataFrame.
Args:
df: DataFrame with columns including 'Rule Number' and 'Logic'
url (str): The URL to the TSV file.
Returns:
DataFrame with processed rule numbers
"""
# Create a copy to avoid modifying original
df = df.copy()
# Initialize tracking variables
current_rule_num = None
used_rule_nums = set()
processed_rule_nums = []
# Find indices where Logic is 'WHERE' to identify rule group starts
where_indices = df[df["Logic"].str.upper() == "WHERE"].index
# Process each group
for i in range(len(where_indices)):
start_idx = where_indices[i]
end_idx = where_indices[i + 1] if i + 1 < len(where_indices) else len(df)
# Get slice of rows for this group
group_slice = df.iloc[start_idx:end_idx]
# Try to get rule number from first row
group_rule_num = group_slice["Rule Number"].iloc[0]
if pd.isna(group_rule_num):
# If no rule number, generate next available
if current_rule_num is None:
current_rule_num = 1
else:
current_rule_num += 1
group_rule_num = current_rule_num
else:
# Convert to int if it's a float or string
group_rule_num = int(group_rule_num)
# Handle duplicate rule numbers
while group_rule_num in used_rule_nums:
group_rule_num += 1
# Update tracking
current_rule_num = group_rule_num
used_rule_nums.add(group_rule_num)
# Fill rule numbers for this group
processed_rule_nums.extend([group_rule_num] * len(group_slice))
# Update DataFrame with processed rule numbers
df["Rule Number"] = processed_rule_nums
return df
def validate_rule_numbers(df: DataFrame) -> list[str]:
"""Validate rule numbers and return any warnings or errors.
Args:
df: DataFrame with processed rule numbers
Returns:
List of warning/error messages
"""
messages = []
# Check for missing rule numbers
if df["Rule Number"].isna().any():
messages.append("Warning: Some rules are missing rule numbers")
# Check for non-integer rule numbers
non_int_mask = df["Rule Number"].apply(lambda x: not pd.isna(x) and not float(x).is_integer())
if non_int_mask.any():
messages.append("Warning: Some rule numbers are not integers")
# Check for duplicate rule numbers in WHERE rows
where_rules = df[df["Logic"].str.upper() == "WHERE"]["Rule Number"]
duplicates = where_rules[where_rules.duplicated()]
if not duplicates.empty:
messages.append(f"Warning: Duplicate rule numbers found: {list(duplicates)}")
return messages
def read_rules_from_spreadsheet(url: str) -> tuple[DataFrameGroupBy, list[str]] | tuple[None, list[str]]:
"""Reads a TSV file from a provided URL, processes rule numbers, and returns grouped rules.
Args:
url (str): The URL to the TSV file
Returns:
Tuple containing:
- DataFrameGroupBy object with rules grouped by rule number (or None if error)
- List of validation messages/warnings
DataFrame: Pandas DataFrame containing the TSV data.
"""
try:
# Read the TSV file
# Since the output is a TSV, we use `pd.read_csv` with `sep='\t'` to specify tab-separated values.
df = pd.read_csv(url, sep="\t")
# Convert mixed type columns
df = convert_mixed_columns(df)
# Process rule numbers
df = process_rule_numbers(df)
# Get validation messages
messages = validate_rule_numbers(df)
# Group by rule number
grouped_rules = df.groupby("Rule Number")
return grouped_rules, messages
# Convert columns to appropriate types based on their content.
return df
except Exception as e:
return None, [f"Failed to read the TSV from the URL: {str(e)}"]
print(f"Failed to read the TSV from the URL: {e}")
return None
def convert_mixed_columns(df: DataFrame) -> DataFrame:
def convert_mixed_columns(df):
"""Converts columns in a DataFrame to appropriate types based on their content.
Args:
df (DataFrame): The DataFrame whose columns are to be converted
df (DataFrame): The DataFrame whose columns are to be converted.
Returns:
DataFrame with columns converted to appropriate types
DataFrame: The DataFrame with columns converted to appropriate types.
"""
# df = df.apply(lambda c: c.astype(object) if any(str(x).replace(".", "", 1).isdigit() for x in c) else c.astype(str))
df = df.apply(lambda c: c.astype(object) if any(str(x).replace(".", "", 1).isdigit() for x in c) else c.astype(str))
return df
-86
View File
@@ -1,86 +0,0 @@
from typing import Any
import pytest
from src.rules import PropertyRules
class TestValueComparison:
"""Test suite for value comparison functionality."""
@pytest.mark.parametrize(
"value1, value2",
[
# Basic numeric strings
("1400", 1400.0),
("1400.0", 1400),
("1400.00", 1400),
# Whitespace handling
(" 1400 ", 1400.0),
(" 1400 ", 1400.0),
("\t1400\n", 1400.0),
# Negative numbers
("-1400", -1400.0),
(" -1400 ", -1400.0),
("-1400.0", -1400),
# Zero handling
("0", 0.0),
("-0", 0.0),
("0.0", 0),
# Simple integers
("1", 1),
("1.0", 1),
],
)
def test_numeric_string_comparison(self, value1: Any, value2: Any):
"""Test comparison of numeric strings with numbers."""
assert PropertyRules.compare_values(value1, value2)
# Test reverse comparison
assert PropertyRules.compare_values(value2, value1)
@pytest.mark.parametrize(
"value1, value2, expected",
[
("Yes", True, True),
("No", False, True),
("yes", True, True),
("no", False, True),
("YES", True, True),
("NO", False, True),
("true", True, True),
("false", False, True),
("True", True, True),
("False", False, True),
],
)
def test_boolean_string_comparison(self, value1: str, value2: bool, expected: bool):
"""Test comparison of boolean strings with booleans."""
assert PropertyRules.compare_values(value1, value2) == expected
# Test reverse comparison
assert PropertyRules.compare_values(value2, value1) == expected
@pytest.mark.parametrize(
"value1, value2, case_sensitive, expected",
[
("hello", "HELLO", False, True),
("hello", "HELLO", True, False),
("Hello", "hello", False, True),
("Hello", "Hello", True, True),
],
)
def test_string_comparison(self, value1: str, value2: str, case_sensitive: bool, expected: bool):
"""Test string comparison with case sensitivity options."""
assert PropertyRules.compare_values(value1, value2, case_sensitive=case_sensitive) == expected
@pytest.mark.parametrize(
"value1, value2, tolerance, expected",
[
(1.0001, 1.0, 1e-3, True),
(1.0001, 1.0, 1e-6, False),
(1.00000001, 1.0, 1e-6, True),
(-1.0001, -1.0, 1e-3, True),
],
)
def test_float_comparison_tolerance(self, value1: float, value2: float, tolerance: float, expected: bool):
"""Test float comparison with different tolerance levels."""
assert PropertyRules.compare_values(value1, value2, tolerance=tolerance) == expected
+21 -21
View File
@@ -8,34 +8,34 @@ from speckle_automate import (
)
from speckle_automate.fixtures import * # noqa: F401, F403
from inputs import MinimumSeverity
from src.function import automate_function
from src.helpers import speckle_print
from src.inputs import FunctionInputs
class TestFunction:
"""Test suite for the automate function."""
def test_function_run(test_automation_run_data: AutomationRunData, test_automation_token: str):
"""Run an integration test for the automate function.
def test_function_run(self, test_automation_run_data: AutomationRunData, test_automation_token: str):
"""Run an integration test for the automate function.
Args:
test_automation_run_data (AutomationRunData): The automation run data provided by sdk.
test_automation_token (str): The automation token.
Args:
test_automation_run_data (AutomationRunData): The automation run data provided by sdk.
test_automation_token (str): The automation token.
"""
speckle_print(str(test_automation_run_data))
speckle_print(str(test_automation_token))
"""
speckle_print(str(test_automation_run_data))
speckle_print(str(test_automation_token))
"""Run an integration test for the automate function."""
automation_context = AutomationContext.initialize(
test_automation_run_data, test_automation_token
)
default_url: str = (
"https://drive.google.com/uc?export=download&id=1hiPSw23eOaqd27QD_YsXvZg9PWm7_XBx"
)
"""Run an integration test for the automate function."""
automation_context = AutomationContext.initialize(test_automation_run_data, test_automation_token)
default_url: str = "https://drive.google.com/uc?export=download&id=1hiPSw23eOaqd27QD_YsXvZg9PWm7_XBx"
automate_sdk = run_function(
automation_context,
automate_function,
FunctionInputs(spreadsheet_url=default_url),
)
automate_sdk = run_function(
automation_context,
automate_function,
FunctionInputs(spreadsheet_url=default_url, minimum_severity=MinimumSeverity.WARNING, hide_skipped=True),
)
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
+354 -338
View File
@@ -1,5 +1,3 @@
"""Test suite for parameter handling functionality."""
import os
from typing import Any
@@ -17,372 +15,390 @@ from helpers import speckle_print
from src.rules import PropertyRules
class TestParameterHandling:
"""Test suite for parameter handling functionality."""
def load_test_objects(v2_wall: Any, v3_wall: Any) -> tuple[Base, Base]:
"""Load test objects from a Speckle server."""
client = SpeckleClient(host="https://app.speckle.systems", use_ssl=True)
@staticmethod
def load_test_objects(v2_wall: Any, v3_wall: Any) -> tuple[Base, Base]:
"""Load test objects from a Speckle server."""
client = SpeckleClient(host="https://app.speckle.systems", use_ssl=True)
load_dotenv(dotenv_path="../.env")
load_dotenv(dotenv_path="../.env")
client.authenticate_with_token(os.getenv("SPECKLE_TOKEN"))
client.authenticate_with_token(os.getenv("SPECKLE_TOKEN"))
transport = ServerTransport(client=client, stream_id=os.getenv("SPECKLE_PROJECT_ID"))
transport = ServerTransport(client=client, stream_id=os.getenv("SPECKLE_PROJECT_ID"))
speckle_print(v2_wall)
v2_obj = operations.receive("cdb18060dc48281909e94f0f1d8d3cc0", transport)
v3_obj = operations.receive("46f06fef727d64a0bbcbd7ced51e0cd2", transport)
speckle_print(v2_wall)
speckle_print(v3_wall)
v2_obj = operations.receive("cdb18060dc48281909e94f0f1d8d3cc0", transport)
v3_obj = operations.receive("46f06fef727d64a0bbcbd7ced51e0cd2", transport)
# return v2_wall, v3_wall
return v2_obj, v3_obj
# return v2_wall, v3_wall
return v2_obj, v3_obj
@pytest.fixture
def test_objects(self, v2_wall: Any, v3_wall: Any) -> tuple[Base, Base]:
"""Pytest fixture to provide test objects."""
return self.load_test_objects(v2_wall, v3_wall)
@pytest.fixture
def test_objects(v2_wall: Any, v3_wall: Any) -> tuple[Base, Base]:
"""Pytest fixture to provide test objects."""
return load_test_objects(v2_wall, v3_wall)
def test_deserialization_structure(self, test_objects):
"""Test that objects are properly deserialized with correct structure."""
v2_obj, v3_obj = test_objects
# Check base class type
for obj in [v2_obj, v3_obj]:
assert isinstance(obj, Base), f"Expected {obj} to be an instance of Base"
def test_deserialization_structure(test_objects):
"""Test that objects are properly deserialized with correct structure."""
v2_obj, v3_obj = test_objects
# Check v2 structure
assert hasattr(v2_obj, "parameters"), "v2_obj should have 'parameters' attribute"
assert v2_obj["parameters"] is not None, "v2_obj['parameters'] should not be None"
# Check base class type
for obj in [v2_obj, v3_obj]:
assert isinstance(obj, Base), f"Expected {obj} to be an instance of Base"
# Check v3 structure
assert hasattr(v3_obj, "properties"), "v3_obj should have 'properties' attribute"
assert v3_obj["properties"] is not None, "v3_obj['properties'] should not be None"
assert "Parameters" in v3_obj["properties"], "'Parameters' key should exist in v3_obj['properties']"
# Check v2 structure
assert hasattr(v2_obj, "parameters"), "v2_obj should have 'parameters' attribute"
assert v2_obj["parameters"] is not None, "v2_obj['parameters'] should not be None"
@pytest.mark.parametrize(
"param_name, expected_result",
[
("category", True), # Test parameters that should exist
("WALL_ATTR_WIDTH_PARAM", True), # Test nested parameters
("WALL_ATTR_WIDTH_PARAM.value", True),
("WALL_ATTR_WIDTH_PARAM.id", True),
("WALL_ATTR_WIDTH_PARAM.units", True),
("non_existent_param", False), # Test non-existent parameters
],
# Check v3 structure
assert hasattr(v3_obj, "properties"), "v3_obj should have 'properties' attribute"
assert v3_obj["properties"] is not None, "v3_obj['properties'] should not be None"
assert "Parameters" in v3_obj["properties"], "'Parameters' key should exist in v3_obj['properties']"
@pytest.mark.parametrize(
"param_name, expected_result",
[
("category", True), # Test parameters that should exist
("WALL_ATTR_WIDTH_PARAM", True), # Test nested parameters
("WALL_ATTR_WIDTH_PARAM.value", True),
("WALL_ATTR_WIDTH_PARAM.id", True),
("WALL_ATTR_WIDTH_PARAM.units", True),
("non_existent_param", False), # Test non-existent parameters
],
)
def test_v2_parameter_exists(test_objects, param_name, expected_result):
"""Test parameter existence checking in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.has_parameter(v2_obj, param_name) == expected_result
@pytest.mark.parametrize(
"param_name",
[
"WALL_ATTR_WIDTH_PARAM.id",
"WALL_ATTR_WIDTH_PARAM.value",
"WALL_ATTR_WIDTH_PARAM",
"WALL_ATTR_WIDTH_PARAM.units",
],
)
def test_v2_parameter_value_retrieval(test_objects, param_name):
"""Test parameter value retrieval in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.get_parameter_value(v2_obj, param_name)
@pytest.mark.parametrize(
"param_name, expected_result",
[
("category", True), # Test parameters that should exist
("Width", True), # Test nested parameters
("non_existent_param", False), # Test non-existent parameters
],
)
def test_v3_parameter_exists(test_objects, param_name, expected_result):
"""Test parameter existence checking in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.has_parameter(v3_obj, param_name) == expected_result
@pytest.mark.parametrize(
"param_name_1, param_name_2",
[
(
"properties.Parameters.Instance Parameters.Dimensions.Length.value",
"Instance Parameters.Dimensions.Length",
),
],
)
def test_v3_parameter_search_equivalence(test_objects, param_name_1, param_name_2):
"""Test parameter existence checking equivalence in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.get_parameter_value(v3_obj, param_name_1) == PropertyRules.get_parameter_value(
v3_obj, param_name_2
)
def test_v2_parameter_exists(self, test_objects, param_name, expected_result):
"""Test parameter existence checking in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.has_parameter(v2_obj, param_name) == expected_result
@pytest.mark.parametrize(
"param_name",
[
"WALL_ATTR_WIDTH_PARAM.id",
"WALL_ATTR_WIDTH_PARAM.value",
"WALL_ATTR_WIDTH_PARAM",
"WALL_ATTR_WIDTH_PARAM.units",
],
)
def test_v2_parameter_value_retrieval(self, test_objects, param_name):
"""Test parameter value retrieval in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.get_parameter_value(v2_obj, param_name)
@pytest.mark.parametrize(
"param_name, expected_result",
[
("category", True), # Test parameters that should exist
("Width", True), # Test nested parameters
("non_existent_param", False), # Test non-existent parameters
],
)
def test_v3_parameter_exists(self, test_objects, param_name, expected_result):
"""Test parameter existence checking in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.has_parameter(v3_obj, param_name) == expected_result
@pytest.mark.parametrize(
"obj_version, param_name, expected_value, default_value",
[
# Test direct parameters
("v2", "category", "Walls", None),
("v3", "category", "Walls", None),
# Test nested parameters - using both internal and friendly names
("v2", "WALL_ATTR_WIDTH_PARAM", 300, None),
("v3", "Construction.Width", 300, None),
# Test parameters with units
("v2", "CURVE_ELEM_LENGTH", 5300.000000000001, None),
("v3", "Instance Parameters.Dimensions.Length", 5300.000000000001, None),
# Test non-existent parameters with a default value
("v2", "parameters.non_existent", "default", "default"),
("v3", "properties.Parameters.non_existent", "default", "default"),
],
)
def test_parameter_value_retrieval(test_objects, obj_version, param_name, expected_value, default_value):
"""Test parameter value retrieval from both v2 and v3 objects."""
v2_obj, v3_obj = test_objects
obj = v2_obj if obj_version == "v2" else v3_obj
result = PropertyRules.get_parameter_value(obj, param_name, default_value=default_value)
assert result == expected_value
@pytest.mark.parametrize(
"param_name_1, param_name_2",
[
(
"properties.Parameters.Instance Parameters.Dimensions.Length.value",
"Instance Parameters.Dimensions.Length",
),
],
)
def test_v3_parameter_search_equivalence(self, test_objects, param_name_1, param_name_2):
"""Test parameter existence checking equivalence in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.get_parameter_value(v3_obj, param_name_1) == PropertyRules.get_parameter_value(
v3_obj, param_name_2
)
@pytest.mark.parametrize(
"obj_version, param_name, expected_value, default_value",
[
# Test direct parameters
("v2", "category", "Walls", None),
("v3", "category", "Walls", None),
# Test nested parameters - using both internal and friendly names
("v2", "WALL_ATTR_WIDTH_PARAM", 300, None),
("v3", "Construction.Width", 300, None),
# Test parameters with units
("v2", "CURVE_ELEM_LENGTH", 5300.000000000001, None),
("v3", "Instance Parameters.Dimensions.Length", 5300.000000000001, None),
# Test non-existent parameters with a default value
("v2", "parameters.non_existent", "default", "default"),
("v3", "properties.Parameters.non_existent", "default", "default"),
],
)
def test_parameter_value_retrieval(self, test_objects, obj_version, param_name, expected_value, default_value):
"""Test parameter value retrieval from both v2 and v3 objects."""
v2_obj, v3_obj = test_objects
obj = v2_obj if obj_version == "v2" else v3_obj
result = PropertyRules.get_parameter_value(obj, param_name, default_value=default_value)
assert result == expected_value
@pytest.mark.parametrize(
"param_name, expected_value, expected_result",
[
("category", "Walls", True), # Test exact match
("WALL_ATTR_WIDTH_PARAM", 300, True), # Test numeric match
("category", "Windows", False), # Test non-match
],
)
def test_v2_parameter_value_matching(test_objects, param_name, expected_value, expected_result):
"""Test parameter value matching in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.is_parameter_value(v2_obj, param_name, expected_value) == expected_result
@pytest.mark.parametrize(
"param_name, expected_value, expected_result",
[
("category", "Walls", True), # Test exact match
("WALL_ATTR_WIDTH_PARAM", 300, True), # Test numeric match
("category", "Windows", False), # Test non-match
],
)
def test_v2_parameter_value_matching(self, test_objects, param_name, expected_value, expected_result):
"""Test parameter value matching in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.is_parameter_value(v2_obj, param_name, expected_value) == expected_result
@pytest.mark.parametrize(
"param_name, expected_value, expected_result",
[
("category", "Walls", True), # Test exact match
("Width", 300, True), # Test numeric match
("category", "Windows", False), # Test non-match
],
)
def test_v3_parameter_value_matching(self, test_objects, param_name, expected_value, expected_result):
"""Test parameter value matching in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.is_parameter_value(v3_obj, param_name, expected_value) == expected_result
@pytest.mark.parametrize(
"param_name, expected_value, expected_result",
[
("category", "Walls", True), # Test exact match
("Width", 300, True), # Test numeric match
("category", "Windows", False), # Test non-match
],
)
def test_v3_parameter_value_matching(test_objects, param_name, expected_value, expected_result):
"""Test parameter value matching in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.is_parameter_value(v3_obj, param_name, expected_value) == expected_result
@pytest.mark.parametrize(
"comparison_func, param_name, value, expected_result",
[
(PropertyRules.is_parameter_value_greater_than, "WALL_ATTR_WIDTH_PARAM", "200", True), # Test greater than
(PropertyRules.is_parameter_value_less_than, "WALL_ATTR_WIDTH_PARAM", "400", True), # Test less than
(PropertyRules.is_parameter_value_in_range, "WALL_ATTR_WIDTH_PARAM", "200,400", True), # Test in range
],
)
def test_v2_parameter_numeric_comparisons(self, test_objects, comparison_func, param_name, value, expected_result):
"""Test numeric parameter comparisons in v2 objects."""
v2_obj, _ = test_objects
assert comparison_func(v2_obj, param_name, value) == expected_result
@pytest.mark.parametrize(
"comparison_func, param_name, value, expected_result",
[
(PropertyRules.is_parameter_value_greater_than, "Width", "200", True), # Test greater than
(PropertyRules.is_parameter_value_less_than, "Width", "400", True), # Test less than
(PropertyRules.is_parameter_value_in_range, "Width", "200,400", True), # Test in range
],
)
def test_v3_parameter_numeric_comparisons(self, test_objects, comparison_func, param_name, value, expected_result):
"""Test numeric parameter comparisons in v3 objects."""
_, v3_obj = test_objects
assert comparison_func(v3_obj, param_name, value) == expected_result
@pytest.mark.parametrize(
"comparison_func, param_name, value, expected_result",
[
(PropertyRules.is_parameter_value_greater_than, "WALL_ATTR_WIDTH_PARAM", "200", True), # Test greater than
(PropertyRules.is_parameter_value_less_than, "WALL_ATTR_WIDTH_PARAM", "400", True), # Test less than
(PropertyRules.is_parameter_value_in_range, "WALL_ATTR_WIDTH_PARAM", "200,400", True), # Test in range
],
)
def test_v2_parameter_numeric_comparisons(test_objects, comparison_func, param_name, value, expected_result):
"""Test numeric parameter comparisons in v2 objects."""
v2_obj, _ = test_objects
assert comparison_func(v2_obj, param_name, value) == expected_result
@pytest.mark.parametrize(
"param_name, pattern, fuzzy, expected_result",
[
("category", "^Walls$", False, True), # Test exact pattern matches
("category", "Walls", True, True), # Test fuzzy matches
("category", "Wall", False, True), # Test partial pattern matches
("category", "^Windows$", False, False), # Test non-matches
],
)
def test_v2_parameter_value_like(self, test_objects, param_name, pattern, fuzzy, expected_result):
"""Test pattern matching on parameter values in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.is_parameter_value_like(v2_obj, param_name, pattern, fuzzy=fuzzy) == expected_result
@pytest.mark.parametrize(
"param_name, pattern, fuzzy, expected_result",
[
("category", "^Walls$", False, True), # Test exact pattern matches
("category", "Walls", True, True), # Test fuzzy matches
("category", "Wall", False, True), # Test partial pattern matches
("category", "^Windows$", False, False), # Test non-matches
],
)
def test_v3_parameter_value_like(self, test_objects, param_name, pattern, fuzzy, expected_result):
"""Test pattern matching on parameter values in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.is_parameter_value_like(v3_obj, param_name, pattern, fuzzy=fuzzy) == expected_result
@pytest.mark.parametrize(
"comparison_func, param_name, value, expected_result",
[
(PropertyRules.is_parameter_value_greater_than, "Width", "200", True), # Test greater than
(PropertyRules.is_parameter_value_less_than, "Width", "400", True), # Test less than
(PropertyRules.is_parameter_value_in_range, "Width", "200,400", True), # Test in range
],
)
def test_v3_parameter_numeric_comparisons(test_objects, comparison_func, param_name, value, expected_result):
"""Test numeric parameter comparisons in v3 objects."""
_, v3_obj = test_objects
assert comparison_func(v3_obj, param_name, value) == expected_result
@pytest.mark.parametrize(
"param_name, valid_list, expected_result",
[
("category", ["Walls", "Windows", "Doors"], True), # Test value in list
("category", "Walls,Windows,Doors", True), # Test comma-separated string list
("category", ["Windows", "Doors"], False), # Test value not in list
],
)
def test_v2_parameter_lists(self, test_objects, param_name, valid_list, expected_result):
"""Test list-based parameter checks in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.is_parameter_value_in_list(v2_obj, param_name, valid_list) == expected_result
@pytest.mark.parametrize(
"param_name, valid_list, expected_result",
[
("category", ["Walls", "Windows", "Doors"], True), # Test value in list
("category", "Walls,Windows,Doors", True), # Test comma-separated string list
("category", ["Windows", "Doors"], False), # Test value not in list
],
)
def test_v3_parameter_lists(self, test_objects, param_name, valid_list, expected_result):
"""Test list-based parameter checks in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.is_parameter_value_in_list(v3_obj, param_name, valid_list) == expected_result
@pytest.mark.parametrize(
"param_name, pattern, fuzzy, expected_result",
[
("category", "^Walls$", False, True), # Test exact pattern matches
("category", "Walls", True, True), # Test fuzzy matches
("category", "Wall", False, True), # Test partial pattern matches
("category", "^Windows$", False, False), # Test non-matches
],
)
def test_v2_parameter_value_like(test_objects, param_name, pattern, fuzzy, expected_result):
"""Test pattern matching on parameter values in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.is_parameter_value_like(v2_obj, param_name, pattern, fuzzy=fuzzy) == expected_result
@pytest.mark.parametrize(
"param_name, expected_result",
[
("WALL_ATTR_ROOM_BOUNDING.value", True), # Test true values
("wall_top_is_attached", False), # Test false values
],
)
def test_v2_boolean_parameters(self, test_objects, param_name, expected_result):
"""Test boolean parameter checks in v2 objects."""
v2_obj, _ = test_objects
if expected_result:
assert PropertyRules.is_parameter_value_true(v2_obj, param_name)
else:
assert PropertyRules.is_parameter_value_false(v2_obj, param_name)
@pytest.mark.parametrize(
"param_name, expected_result",
[
("Room Bounding", True), # Test true values
("top is attached", False), # Test false values
("Top is Attached", False), # Case sensitivity test
],
)
def test_v3_boolean_parameters(self, test_objects, param_name, expected_result):
"""Test boolean parameter checks in v3 objects."""
_, v3_obj = test_objects
if expected_result:
assert PropertyRules.is_parameter_value_true(v3_obj, param_name)
else:
assert PropertyRules.is_parameter_value_false(v3_obj, param_name)
@pytest.mark.parametrize(
"param_name, pattern, fuzzy, expected_result",
[
("category", "^Walls$", False, True), # Test exact pattern matches
("category", "Walls", True, True), # Test fuzzy matches
("category", "Wall", False, True), # Test partial pattern matches
("category", "^Windows$", False, False), # Test non-matches
],
)
def test_v3_parameter_value_like(test_objects, param_name, pattern, fuzzy, expected_result):
"""Test pattern matching on parameter values in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.is_parameter_value_like(v3_obj, param_name, pattern, fuzzy=fuzzy) == expected_result
@pytest.mark.parametrize(
"param_name, expected_value, expected_result",
[
# Test numeric value comparisons
("WALL_ATTR_WIDTH_PARAM", 300, True),
("WALL_ATTR_WIDTH_PARAM.value", 300, True),
("baseLine.length", 5300.000000000002, True),
# Test string value comparisons
("STRUCTURAL_MATERIAL_PARAM.value", "Fc24", True),
("ee1f33e1-5506-4a64-b87b-7b98d30aea52.value", "W30", True),
# Test non-matches
("WALL_ATTR_WIDTH_PARAM", 301, False),
("nonexistent_param", "any_value", False),
],
)
def test_v2_parameter_value_comparisons(self, v2_wall, param_name, expected_value, expected_result):
"""Test value comparisons using v2 wall parameters."""
assert PropertyRules.is_equal_value(v2_wall, param_name, expected_value) == expected_result
@pytest.mark.parametrize(
"attribute, value, expected",
[
# Test numeric value comparisons
("Type Parameters.Structure.Fc24 (0).thickness", 300, True),
("location.length", 5300.000000000002, True),
("location.length", 5300, True),
# Test string value comparisons
("Type Parameters.Text.符号.value", "W30", True),
("Instance Parameters.Structural.Structural.value", "Yes", True),
# Test non-matches
("Type Parameters.Structure.Fc24 (0).thickness", 301, False),
("nonexistent_param", "any_value", False),
],
)
def test_v3_parameter_value_comparisons(self, v3_wall, attribute, value, expected):
"""Test value comparisons using v3 wall parameters."""
assert PropertyRules.is_equal_value(v3_wall, attribute, value) == expected
@pytest.mark.parametrize(
"param_name, valid_list, expected_result",
[
("category", ["Walls", "Windows", "Doors"], True), # Test value in list
("category", "Walls,Windows,Doors", True), # Test comma-separated string list
("category", ["Windows", "Doors"], False), # Test value not in list
],
)
def test_v2_parameter_lists(test_objects, param_name, valid_list, expected_result):
"""Test list-based parameter checks in v2 objects."""
v2_obj, _ = test_objects
assert PropertyRules.is_parameter_value_in_list(v2_obj, param_name, valid_list) == expected_result
@pytest.mark.parametrize(
"wall, attribute, value, expected",
[
# V2 wall tests
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 300, True),
("v2_wall", "type", "W30(Fc24)", True),
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 300.0001, False),
# V3 wall tests
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 300, True),
("v3_wall", "type", "W30(Fc24)", True),
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 300.0001, False),
("v3_wall", "location.length", 5300.000000000002, True),
("v3_wall", "location.length", 5300, False),
],
)
def test_identical_comparisons(self, request, wall, attribute, value, expected):
"""Test identical value comparisons on both wall versions."""
wall_instance = request.getfixturevalue(wall)
assert PropertyRules.is_identical_value(wall_instance, attribute, value) == expected
@pytest.mark.parametrize(
"wall, attribute, value",
[
# V2 wall tests
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 301),
("v2_wall", "STRUCTURAL_MATERIAL_PARAM.value", "Fc25"),
("v2_wall", "nonexistent_param", "any_value"),
# V3 wall tests
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 301),
("v3_wall", "Type Parameters.Text.符号.value", "W31"),
("v3_wall", "nonexistent_param", "any_value"),
],
)
def test_not_equal_comparisons(self, request, wall, attribute, value):
"""Test not equal comparisons on both wall versions."""
wall_instance = request.getfixturevalue(wall)
assert PropertyRules.is_not_equal_value(wall_instance, attribute, value)
@pytest.mark.parametrize(
"param_name, valid_list, expected_result",
[
("category", ["Walls", "Windows", "Doors"], True), # Test value in list
("category", "Walls,Windows,Doors", True), # Test comma-separated string list
("category", ["Windows", "Doors"], False), # Test value not in list
],
)
def test_v3_parameter_lists(test_objects, param_name, valid_list, expected_result):
"""Test list-based parameter checks in v3 objects."""
_, v3_obj = test_objects
assert PropertyRules.is_parameter_value_in_list(v3_obj, param_name, valid_list) == expected_result
@pytest.mark.parametrize(
"attribute, value, expected_equal, expected_identical",
[
# Test Yes/No conversion in equals (should convert)
("Instance Parameters.Structural.Structural.value", True, True, False), # Yes vs True
("Instance Parameters.Structural.Structural.value", "Yes", True, True), # Yes vs "Yes"
("Instance Parameters.Structural.Structural.value", "yes", True, False), # Yes vs "yes"
],
)
def test_boolean_conversions(self, v3_wall, attribute, value, expected_equal, expected_identical):
"""Test conversion of Yes/No strings to boolean values."""
assert PropertyRules.is_equal_value(v3_wall, attribute, value) == expected_equal
assert PropertyRules.is_identical_value(v3_wall, attribute, value) == expected_identical
@pytest.mark.parametrize(
"wall, attribute, expected_value",
[
# V2 wall tests
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", "300"),
("v2_wall", "baseLine.length", "5300.000000000002"),
# V3 wall tests
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", "300"),
("v3_wall", "location.length", "5300.000000000002"),
],
)
def test_numeric_string_handling(self, wall, attribute, expected_value, request):
"""Test handling of numeric strings in both wall versions."""
wall_instance = request.getfixturevalue(wall) # Retrieve fixture dynamically
assert PropertyRules.is_equal_value(wall_instance, attribute, expected_value)
@pytest.mark.parametrize(
"param_name, expected_result",
[
("WALL_ATTR_ROOM_BOUNDING.value", True), # Test true values
("wall_top_is_attached", False), # Test false values
],
)
def test_v2_boolean_parameters(test_objects, param_name, expected_result):
"""Test boolean parameter checks in v2 objects."""
v2_obj, _ = test_objects
if expected_result:
assert PropertyRules.is_parameter_value_true(v2_obj, param_name)
else:
assert PropertyRules.is_parameter_value_false(v2_obj, param_name)
@pytest.mark.parametrize(
"param_name, expected_result",
[
("Room Bounding", True), # Test true values
("top is attached", False), # Test false values
("Top is Attached", False), # Case sensitivity test
],
)
def test_v3_boolean_parameters(test_objects, param_name, expected_result):
"""Test boolean parameter checks in v3 objects."""
_, v3_obj = test_objects
if expected_result:
assert PropertyRules.is_parameter_value_true(v3_obj, param_name)
else:
assert PropertyRules.is_parameter_value_false(v3_obj, param_name)
@pytest.mark.parametrize(
"param_name, expected_value, expected_result",
[
# Test numeric value comparisons
("WALL_ATTR_WIDTH_PARAM", 300, True),
("WALL_ATTR_WIDTH_PARAM.value", 300, True),
("baseLine.length", 5300.000000000002, True),
# Test string value comparisons
("STRUCTURAL_MATERIAL_PARAM.value", "Fc24", True),
("ee1f33e1-5506-4a64-b87b-7b98d30aea52.value", "W30", True),
# Test non-matches
("WALL_ATTR_WIDTH_PARAM", 301, False),
("nonexistent_param", "any_value", False),
],
)
def test_v2_parameter_value_comparisons(v2_wall, param_name, expected_value, expected_result):
"""Test value comparisons using v2 wall parameters."""
assert PropertyRules.is_equal_value(v2_wall, param_name, expected_value) == expected_result
@pytest.mark.parametrize(
"attribute, value, expected",
[
# Test numeric value comparisons
("Type Parameters.Structure.Fc24 (0).thickness", 300, True),
("location.length", 5300.000000000002, True),
("location.length", 5300, True),
# Test string value comparisons
("Type Parameters.Text.符号.value", "W30", True),
("Instance Parameters.Structural.Structural.value", "Yes", True),
# Test non-matches
("Type Parameters.Structure.Fc24 (0).thickness", 301, False),
("nonexistent_param", "any_value", False),
],
)
def test_v3_parameter_value_comparisons(v3_wall, attribute, value, expected):
"""Test value comparisons using v3 wall parameters."""
assert PropertyRules.is_equal_value(v3_wall, attribute, value) == expected
@pytest.mark.parametrize(
"wall, attribute, value, expected",
[
# V2 wall tests
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 300, True),
("v2_wall", "type", "W30(Fc24)", True),
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 300.0001, False),
# V3 wall tests
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 300, True),
("v3_wall", "type", "W30(Fc24)", True),
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 300.0001, False),
("v3_wall", "location.length", 5300.000000000002, True),
("v3_wall", "location.length", 5300, False),
],
)
def test_identical_comparisons(request, wall, attribute, value, expected):
"""Test identical value comparisons on both wall versions."""
wall_instance = request.getfixturevalue(wall)
assert PropertyRules.is_identical_value(wall_instance, attribute, value) == expected
@pytest.mark.parametrize(
"wall, attribute, value",
[
# V2 wall tests
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", 301),
("v2_wall", "STRUCTURAL_MATERIAL_PARAM.value", "Fc25"),
("v2_wall", "nonexistent_param", "any_value"),
# V3 wall tests
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 301),
("v3_wall", "Type Parameters.Text.符号.value", "W31"),
("v3_wall", "nonexistent_param", "any_value"),
],
)
def test_not_equal_comparisons(request, wall, attribute, value):
"""Test not equal comparisons on both wall versions."""
wall_instance = request.getfixturevalue(wall)
assert PropertyRules.is_not_equal_value(wall_instance, attribute, value)
@pytest.mark.parametrize(
"attribute, value, expected_equal, expected_identical",
[
# Test Yes/No conversion in equals (should convert)
("Instance Parameters.Structural.Structural.value", True, True, False), # Yes vs True
("Instance Parameters.Structural.Structural.value", "Yes", True, True), # Yes vs "Yes"
("Instance Parameters.Structural.Structural.value", "yes", True, False), # Yes vs "yes"
],
)
def test_boolean_conversions(v3_wall, attribute, value, expected_equal, expected_identical):
"""Test conversion of Yes/No strings to boolean values."""
assert PropertyRules.is_equal_value(v3_wall, attribute, value) == expected_equal
assert PropertyRules.is_identical_value(v3_wall, attribute, value) == expected_identical
@pytest.mark.parametrize(
"wall, attribute, expected_value",
[
# V2 wall tests
("v2_wall", "WALL_ATTR_WIDTH_PARAM.value", "300"),
("v2_wall", "baseLine.length", "5300.000000000002"),
# V3 wall tests
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", "300"),
("v3_wall", "location.length", "5300.000000000002"),
],
)
def test_numeric_string_handling(wall, attribute, expected_value, request):
"""Test handling of numeric strings in both wall versions."""
wall_instance = request.getfixturevalue(wall) # Retrieve fixture dynamically
assert PropertyRules.is_equal_value(wall_instance, attribute, expected_value)
-36
View File
@@ -1,36 +0,0 @@
"""Tests for rule processing functionality."""
import pandas as pd
import pytest
@pytest.fixture
def explicit_check_rule():
"""Create a rule using explicit CHECK format."""
return pd.DataFrame(
{
"Rule Number": [1, 1, 1],
"Logic": ["WHERE", "AND", "CHECK"],
"Property Name": ["category", "width", "material"],
"Predicate": ["matches", "greater than", "matches"],
"Value": ["Walls", "200", "Concrete"],
"Message": ["Test message", "", ""],
"Severity": ["Error", "", ""],
}
)
@pytest.fixture
def legacy_rule():
"""Create a rule using legacy format (last AND is implicit check)."""
return pd.DataFrame(
{
"Rule Number": [1, 1, 1],
"Logic": ["WHERE", "AND", "AND"],
"Property Name": ["category", "width", "material"],
"Predicate": ["matches", "greater than", "matches"],
"Value": ["Walls", "200", "Concrete"],
"Message": ["Test message", "", ""],
"Severity": ["Error", "", ""],
}
)