Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72478b010f | |||
| 38d2073dbb | |||
| 091a272185 | |||
| 0e95f3998a | |||
| 05a5383060 | |||
| f3c56a48b5 | |||
| a704aded80 | |||
| 90c5051fc6 | |||
| ec6bdf3485 | |||
| ceaa75d40a | |||
| 0566f7d890 | |||
| b431662031 | |||
| e520d9bc91 | |||
| b6dcfe57df | |||
| ba8443ce92 | |||
| 0bab18d2f2 | |||
| dffb7ea7ba | |||
| 4420fd31f4 | |||
| 168a1f517a | |||
| e49bf225ec | |||
| f3987fced9 | |||
| 1ae3372f42 | |||
| b071380a4f | |||
| 460b21772a | |||
| bb40f185b5 | |||
| ee12143504 | |||
| 8582444e56 | |||
| f2e06f165e |
+20
-11
@@ -11,26 +11,35 @@ jobs:
|
||||
FUNCTION_SCHEMA_FILE_NAME: functionSchema.json
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: actions/setup-python@v5
|
||||
# Step 1: Checkout the repository
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
# Step 2: Set up Python
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
- name: Install poetry
|
||||
|
||||
# Step 3: Install dependencies using pip
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
pip install poetry==1.8.4 &&
|
||||
poetry config virtualenvs.create false &&
|
||||
poetry config virtualenvs.in-project false &&
|
||||
poetry config installer.parallel true
|
||||
- name: Restore dependencies
|
||||
run: poetry install --no-root
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
# Step 4: Generate the schema
|
||||
- name: Extract functionInputSchema
|
||||
id: extract_schema
|
||||
run: |
|
||||
python main.py generate_schema ${HOME}/${{ env.FUNCTION_SCHEMA_FILE_NAME }}
|
||||
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 }}"
|
||||
|
||||
# Step 5: Build and publish the Speckle function
|
||||
- name: Speckle Automate Function - Build and Publish
|
||||
uses: specklesystems/speckle-automate-github-composite-action@0.9.0
|
||||
with:
|
||||
speckle_automate_url: ${{ env.SPECKLE_AUTOMATE_URL || vars.SPECKLE_AUTOMATE_URL || 'https://automate.speckle.dev' }}
|
||||
speckle_automate_url: ${{ env.SPECKLE_AUTOMATE_URL || 'https://automate.speckle.dev' }}
|
||||
speckle_token: ${{ secrets.SPECKLE_FUNCTION_TOKEN }}
|
||||
speckle_function_id: ${{ secrets.SPECKLE_FUNCTION_ID }}
|
||||
speckle_function_input_schema_file_path: ${{ env.FUNCTION_SCHEMA_FILE_NAME }}
|
||||
|
||||
Generated
+9
-2
@@ -1,12 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="jdk" jdkName="Python 3.11.4 WSL (UbuntuDev): (/home/jsdbroughton/.virtualenvs/speckle_automate-mesh_density_checker/bin/python)" jdkType="Python SDK" />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="uv (Checker)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="PyDocumentationSettings">
|
||||
<option name="format" value="GOOGLE" />
|
||||
<option name="myDocStringFormat" value="Google" />
|
||||
</component>
|
||||
<component name="TestRunnerService">
|
||||
<option name="PROJECT_TEST_RUNNER" value="py.test" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+2
-2
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.11.4 WSL (UbuntuDev): (/home/jsdbroughton/.virtualenvs/speckle_automate-mesh_density_checker/bin/python)" />
|
||||
<option name="sdkName" value="WSL Checker" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11.4 WSL (UbuntuDev): (/home/jsdbroughton/.virtualenvs/speckle_automate-mesh_density_checker/bin/python)" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="uv (Checker)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
Generated
+9
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RuffConfigService">
|
||||
<option name="enableLsp" value="false" />
|
||||
<option name="runRuffOnSave" value="true" />
|
||||
<option name="useRuffFormat" value="true" />
|
||||
<option name="useRuffServer" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+14
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="WebResourcesPaths">
|
||||
<contentEntries>
|
||||
<entry url="file://$PROJECT_DIR$">
|
||||
<entryData>
|
||||
<resourceRoots>
|
||||
<path value="file://$PROJECT_DIR$/test_data" />
|
||||
</resourceRoots>
|
||||
</entryData>
|
||||
</entry>
|
||||
</contentEntries>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1 @@
|
||||
3.13
|
||||
+3
-1
@@ -3,11 +3,13 @@
|
||||
## Setup
|
||||
|
||||
1. Install dependencies:
|
||||
|
||||
```bash
|
||||
poetry shell && poetry install
|
||||
```
|
||||
|
||||
2. Configure `.env`:
|
||||
|
||||
```
|
||||
SPECKLE_TOKEN=your_speckle_token
|
||||
SPECKLE_SERVER_URL=app.speckle.systems
|
||||
@@ -32,7 +34,7 @@ poetry run pytest
|
||||
## Extending Rules
|
||||
|
||||
1. Add new predicate to `input_predicate_mapping` in `rules.py`
|
||||
2. Create corresponding method in `RevitRules` class
|
||||
2. Create corresponding method in `PropertyRules` class
|
||||
3. Update tests
|
||||
|
||||
## Building
|
||||
|
||||
+10
-11
@@ -1,16 +1,15 @@
|
||||
# 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
|
||||
# Use the official Python 3.11 slim image as the base
|
||||
FROM python:3.11-slim
|
||||
|
||||
# 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.
|
||||
# Set the working directory inside the container
|
||||
WORKDIR /home/speckle
|
||||
|
||||
# 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 the application files to the working directory
|
||||
COPY . /home/speckle
|
||||
|
||||
# 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
|
||||
# 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"]
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"speckleToken": "YOUR SPEKCLE TOKEN",
|
||||
"functionInputs": {
|
||||
"whisperMessage": "you are doing something weird",
|
||||
"forbiddenSpeckleType": "wall"
|
||||
},
|
||||
"automationRunData": {
|
||||
"project_id": "project id",
|
||||
"speckle_server_url": "https://latest.speckle.systems",
|
||||
"automation_id": "automation id",
|
||||
"automation_run_id": "automation run id",
|
||||
"function_run_id": "function run id",
|
||||
"triggers": [
|
||||
{
|
||||
"payload": { "modelId": "model id", "versionId": "version id" },
|
||||
"triggerType": "versionCreation"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
"""Helper module for a simple speckle object tree flattening."""
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
from specklepy.objects import Base
|
||||
|
||||
|
||||
def flatten_base(base: Base) -> Iterable[Base]:
|
||||
"""Flatten a base object into an iterable of bases.
|
||||
|
||||
This function recursively traverses the `elements` or `@elements` attribute of the
|
||||
base object, yielding each nested base object.
|
||||
|
||||
Args:
|
||||
base (Base): The base object to flatten.
|
||||
|
||||
Yields:
|
||||
Base: Each nested base object in the hierarchy.
|
||||
"""
|
||||
# Attempt to get the elements attribute, fallback to @elements if necessary
|
||||
elements = getattr(base, "elements", getattr(base, "@elements", None))
|
||||
|
||||
if elements is not None:
|
||||
for element in elements:
|
||||
yield from flatten_base(element)
|
||||
|
||||
yield base
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/bin/bash
|
||||
echo "Running pre-commit hook..."
|
||||
|
||||
# Ensure dependencies are installed with uv
|
||||
uv pip install --requirement requirements.txt
|
||||
|
||||
# Export dependencies with uv (and overwrite requirements.txt)
|
||||
uv pip freeze > requirements.txt
|
||||
|
||||
# Add generated requirements.txt to git
|
||||
git add requirements.txt
|
||||
|
||||
echo "Pre-commit hook completed successfully!"
|
||||
Generated
-1797
File diff suppressed because it is too large
Load Diff
+24
-27
@@ -1,39 +1,36 @@
|
||||
[tool.poetry]
|
||||
name = "speckle-automate-py"
|
||||
[project]
|
||||
name = "speckle-automate-checker"
|
||||
version = "0.1.0"
|
||||
description = "Allows for QAQC property checking with Speckle"
|
||||
authors = ["Jonathon Broughton <jonathon@speckle.systems>"]
|
||||
readme = "README.md"
|
||||
package-mode = false
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"more-itertools>=10.6.0",
|
||||
"pandas>=2.2.3",
|
||||
"pydantic==2.10.6",
|
||||
"python-dotenv>=1.0.1",
|
||||
"python-levenshtein>=0.26.1",
|
||||
"specklepy>=2.21.3",
|
||||
"pytest-assertcount>=1.0.0",
|
||||
"black>=25.1.0",
|
||||
"mypy>=1.15.0",
|
||||
"pydantic-settings>=2.7.1",
|
||||
"pytest>=8.3.4",
|
||||
"ruff>=0.9.6",
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
more-itertools = "^10.6.0"
|
||||
pandas = "^2.2.3"
|
||||
python = "^3.11"
|
||||
python-dotenv = "^1.0.1"
|
||||
python-levenshtein = "^0.26.1"
|
||||
specklepy = "^2.21.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = "^25.0.0"
|
||||
mypy = "^1.3.0"
|
||||
pydantic-settings = "^2.3.0"
|
||||
pytest = "^8.0.0"
|
||||
ruff = "^0.9.5"
|
||||
# specklepy = { path = "../specklepy", develop = true }
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.ruff]
|
||||
select = [
|
||||
"E", # pycodestyle
|
||||
"F", # pyflakes
|
||||
"UP", # pyupgrade
|
||||
"D", # pydocstyle
|
||||
"I", # isort
|
||||
"E", # pycodestyle
|
||||
"F", # pyflakes
|
||||
"UP", # pyupgrade
|
||||
"D", # pydocstyle
|
||||
"I", # isort
|
||||
]
|
||||
line-length = 120
|
||||
ignore = ["F401", "F403"]
|
||||
|
||||
[tool.ruff.pydocstyle]
|
||||
convention = "google"
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
annotated-types==0.7.0
|
||||
anyio==4.8.0
|
||||
appdirs==1.4.4
|
||||
attrs==23.2.0
|
||||
backoff==2.2.1
|
||||
black==25.1.0
|
||||
certifi==2025.1.31
|
||||
charset-normalizer==3.4.1
|
||||
click==8.1.8
|
||||
colorama==0.4.6
|
||||
deprecated==1.2.18
|
||||
gql==3.5.0
|
||||
graphql-core==3.2.6
|
||||
h11==0.14.0
|
||||
httpcore==1.0.7
|
||||
httpx==0.25.2
|
||||
idna==3.10
|
||||
iniconfig==2.0.0
|
||||
levenshtein==0.26.1
|
||||
more-itertools==10.6.0
|
||||
multidict==6.1.0
|
||||
mypy==1.15.0
|
||||
mypy-extensions==1.0.0
|
||||
numpy==2.2.3
|
||||
packaging==24.2
|
||||
pandas==2.2.3
|
||||
pathspec==0.12.1
|
||||
platformdirs==4.3.6
|
||||
pluggy==1.5.0
|
||||
propcache==0.2.1
|
||||
pydantic==2.10.6
|
||||
pydantic-core==2.27.2
|
||||
pydantic-settings==2.8.1
|
||||
pytest==8.3.4
|
||||
pytest-assertcount==1.0.0
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.0.1
|
||||
python-levenshtein==0.26.1
|
||||
pytz==2025.1
|
||||
rapidfuzz==3.12.1
|
||||
requests==2.32.3
|
||||
requests-toolbelt==1.0.0
|
||||
ruff==0.9.6
|
||||
six==1.17.0
|
||||
sniffio==1.3.1
|
||||
specklepy==2.21.3
|
||||
stringcase==1.2.0
|
||||
typing-extensions==4.12.2
|
||||
tzdata==2025.1
|
||||
ujson==5.10.0
|
||||
urllib3==2.3.0
|
||||
websockets==11.0.3
|
||||
wrapt==1.17.2
|
||||
yarl==1.18.3
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Copy the pre-commit hook to the .git/hooks/ directory
|
||||
cp hooks/pre-commit .git/hooks/pre-commit
|
||||
|
||||
# Ensure the hook is executable
|
||||
chmod +x .git/hooks/pre-commit
|
||||
|
||||
echo "Git hooks have been set up!"
|
||||
@@ -0,0 +1,31 @@
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
from src.rules import PropertyRules
|
||||
|
||||
|
||||
def filter_objects_by_category(speckle_objects: list[Base], category_input: str) -> tuple[list[Base], list[Base]]:
|
||||
"""Filters objects by category value and test.
|
||||
|
||||
This function takes a list of Speckle objects, filters out the objects
|
||||
with a matching category value and satisfies the test, and returns
|
||||
both the matching and non-matching objects.
|
||||
|
||||
Args:
|
||||
speckle_objects (List[Base]): The list of Speckle objects to filter.
|
||||
category_input (str): The category value to match against.
|
||||
|
||||
Returns:
|
||||
Tuple[List[Base], List[Base]]: A tuple containing two lists:
|
||||
- The first list contains objects with matching category and test.
|
||||
- The second list contains objects without matching category or test.
|
||||
"""
|
||||
matching_objects = []
|
||||
non_matching_objects = []
|
||||
|
||||
for obj in speckle_objects:
|
||||
if PropertyRules.is_category(obj, category_input):
|
||||
matching_objects.append(obj)
|
||||
else:
|
||||
non_matching_objects.append(obj)
|
||||
|
||||
return matching_objects, non_matching_objects
|
||||
+38
-16
@@ -1,21 +1,25 @@
|
||||
"""This module contains the function's business logic.
|
||||
"""This is the main function that will be executed when the automation is triggered.
|
||||
|
||||
Use the automation_context module to wrap your function in an Automate context helper.
|
||||
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 speckle_automate import AutomationContext, AutomateBase
|
||||
from speckle_automate import AutomationContext
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
from src.rules import apply_rules_to_objects
|
||||
from src.helpers import flatten_base, speckle_print
|
||||
from src.inputs import FunctionInputs
|
||||
from src.helpers import flatten_base
|
||||
from src.rule_processor import apply_rules_to_objects
|
||||
from src.spreadsheet import read_rules_from_spreadsheet
|
||||
|
||||
VERSION: int = 2
|
||||
|
||||
|
||||
def automate_function(
|
||||
automate_context: AutomationContext,
|
||||
function_inputs: FunctionInputs,
|
||||
automate_context: AutomationContext,
|
||||
function_inputs: FunctionInputs,
|
||||
) -> None:
|
||||
"""This version of the function will add a check for the new provide inputs.
|
||||
"""This VERSION of the function will add a check for the new provide inputs.
|
||||
|
||||
Args:
|
||||
automate_context: A context helper object, that carries relevant information
|
||||
@@ -24,23 +28,41 @@ def automate_function(
|
||||
It also has convenience methods attach result data to the Speckle model.
|
||||
function_inputs: An instance object matching the defined schema.
|
||||
"""
|
||||
|
||||
# the context provides a convenient way, to receive the triggering version
|
||||
version_root_object = automate_context.receive_version()
|
||||
# the context provides a convenient way, to receive the triggering VERSION
|
||||
version_root_object: Base = automate_context.receive_version()
|
||||
|
||||
# We can continue to work with a flattened list of objects.
|
||||
flat_list_of_objects = list(flatten_base(version_root_object))
|
||||
|
||||
# read the rules from the spreadsheet
|
||||
rules = read_rules_from_spreadsheet(function_inputs.spreadsheet_url)
|
||||
# If it is a next_gen model, we can get the VERSION from the root object
|
||||
# This function's rules don't make use of this check, but it is here for reference if you want to.
|
||||
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)
|
||||
|
||||
# Handle any validation messages
|
||||
for message in messages:
|
||||
speckle_print(message) # or log them appropriately
|
||||
|
||||
if grouped_rules is None:
|
||||
automate_context.mark_run_exception("Failed to process rules")
|
||||
return
|
||||
|
||||
# apply the rules to the objects
|
||||
apply_rules_to_objects(flat_list_of_objects, rules, automate_context)
|
||||
apply_rules_to_objects(
|
||||
flat_list_of_objects,
|
||||
grouped_rules,
|
||||
automate_context,
|
||||
minimum_severity=function_inputs.minimum_severity,
|
||||
hide_skipped=function_inputs.hide_skipped,
|
||||
)
|
||||
|
||||
# set the automation context view, to the original model / version view
|
||||
# set the automation context view, to the original model / VERSION view
|
||||
automate_context.set_context_view()
|
||||
|
||||
# report success
|
||||
automate_context.mark_run_success(
|
||||
f"Successfully applied rules to {len(flat_list_of_objects)} objects."
|
||||
f"Successfully applied {len(grouped_rules)} rules to {len(flat_list_of_objects)} version {VERSION} objects."
|
||||
)
|
||||
|
||||
+32
-20
@@ -1,14 +1,14 @@
|
||||
"""Helper module for a speckle object tree flattening."""
|
||||
|
||||
from collections.abc import Iterable
|
||||
from typing import Optional, Tuple, List
|
||||
from collections.abc import Generator, Iterable
|
||||
from typing import Any
|
||||
|
||||
from specklepy.objects import Base
|
||||
from specklepy.objects.other import Instance, Transform
|
||||
|
||||
|
||||
def speckle_print(log_string: str = "banana") -> None:
|
||||
|
||||
"""Print a string to the console with a green color."""
|
||||
print("\033[92m" + str(log_string) + "\033[0m")
|
||||
|
||||
|
||||
@@ -21,6 +21,24 @@ def flatten_base(base: Base) -> Iterable[Base]:
|
||||
yield base
|
||||
|
||||
|
||||
def get_item(obj: Base | dict[str, Any], key, default=None):
|
||||
"""Get an item from a dictionary or an object with a default value."""
|
||||
if isinstance(obj, dict): # If it's a dictionary
|
||||
return obj.get(key, default)
|
||||
elif hasattr(obj, key): # If it's an object with the attribute
|
||||
return getattr(obj, key, default)
|
||||
return default # Return default if it's neither a dict nor an object with the attribute
|
||||
|
||||
|
||||
def has_item(obj: Base | dict[str, Any], key: str) -> bool:
|
||||
"""Check if an object has a key or an attribute."""
|
||||
if isinstance(obj, dict):
|
||||
return key in obj
|
||||
elif hasattr(obj, key):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def flatten_base_thorough(base: Base, parent_type: str = None) -> Iterable[Base]:
|
||||
"""Take a base and flatten it to an iterable of bases.
|
||||
|
||||
@@ -51,9 +69,7 @@ def flatten_base_thorough(base: Base, parent_type: str = None) -> Iterable[Base]
|
||||
print(category)
|
||||
if category.startswith("@"):
|
||||
category_object: Base = getattr(base, category)[0]
|
||||
yield from flatten_base_thorough(
|
||||
category_object, category_object.speckle_type
|
||||
)
|
||||
yield from flatten_base_thorough(category_object, category_object.speckle_type)
|
||||
|
||||
except KeyError:
|
||||
pass
|
||||
@@ -63,11 +79,13 @@ def flatten_base_thorough(base: Base, parent_type: str = None) -> Iterable[Base]
|
||||
|
||||
def extract_base_and_transform(
|
||||
base: Base,
|
||||
inherited_instance_id: Optional[str] = None,
|
||||
transform_list: Optional[List[Transform]] = None,
|
||||
) -> Tuple[Base, str, Optional[List[Transform]]]:
|
||||
"""
|
||||
Traverses Speckle object hierarchies to yield `Base` objects and their transformations.
|
||||
inherited_instance_id: str | None = None,
|
||||
transform_list: list[Transform] | None = None,
|
||||
) -> Generator[
|
||||
Base | str | list[Transform] | None | tuple[Base, Any | None, list[Transform] | None | list[Any]], Any | None, None
|
||||
]:
|
||||
"""Traverses Speckle object hierarchies to yield `Base` objects and their transformations.
|
||||
|
||||
Tailored to Speckle's AEC data structures, it covers the newer hierarchical structures
|
||||
with Collections and also with patterns found in older Revit specific data.
|
||||
|
||||
@@ -91,9 +109,7 @@ def extract_base_and_transform(
|
||||
if base.transform:
|
||||
transform_list.append(base.transform)
|
||||
if base.definition:
|
||||
yield from extract_base_and_transform(
|
||||
base.definition, current_id, transform_list.copy()
|
||||
)
|
||||
yield from extract_base_and_transform(base.definition, current_id, transform_list.copy())
|
||||
else:
|
||||
# Initial yield for the current `Base` object.
|
||||
yield base, current_id, transform_list
|
||||
@@ -103,9 +119,7 @@ def extract_base_and_transform(
|
||||
for element in elements_attr:
|
||||
if isinstance(element, Base):
|
||||
# Recurse into each `Base` object within 'elements' or '@elements'.
|
||||
yield from extract_base_and_transform(
|
||||
element, current_id, transform_list.copy()
|
||||
)
|
||||
yield from extract_base_and_transform(element, current_id, transform_list.copy())
|
||||
|
||||
# Recursively process '@'-prefixed properties that are Base objects with 'elements'.
|
||||
# This is a common pattern in older Speckle data models, such as those used for Revit commits.
|
||||
@@ -114,6 +128,4 @@ def extract_base_and_transform(
|
||||
attr_value = getattr(base, attr_name)
|
||||
# If the attribute is a Base object containing 'elements', recurse into it.
|
||||
if isinstance(attr_value, Base) and hasattr(attr_value, "elements"):
|
||||
yield from extract_base_and_transform(
|
||||
attr_value, current_id, transform_list.copy()
|
||||
)
|
||||
yield from extract_base_and_transform(attr_value, current_id, transform_list.copy())
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import Field
|
||||
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"
|
||||
|
||||
|
||||
class FunctionInputs(AutomateBase):
|
||||
"""These are function author defined values.
|
||||
|
||||
@@ -15,3 +33,24 @@ class FunctionInputs(AutomateBase):
|
||||
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.",
|
||||
)
|
||||
|
||||
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.'
|
||||
# )
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Configuration module defining mappings between spreadsheet predicates and rule methods."""
|
||||
|
||||
from src.rules import PropertyRules
|
||||
|
||||
# Mapping of input predicates to the corresponding methods in PropertyRules
|
||||
PREDICATE_METHOD_MAP = {
|
||||
"exists": PropertyRules.has_parameter.__name__,
|
||||
"greater than": PropertyRules.is_parameter_value_greater_than.__name__,
|
||||
"less than": PropertyRules.is_parameter_value_less_than.__name__,
|
||||
"in range": PropertyRules.is_parameter_value_in_range.__name__,
|
||||
"in list": PropertyRules.is_parameter_value_in_list.__name__,
|
||||
"equal to": PropertyRules.is_equal_value.__name__,
|
||||
"not equal to": PropertyRules.is_not_equal_value.__name__,
|
||||
"is true": PropertyRules.is_parameter_value_true.__name__,
|
||||
"is false": PropertyRules.is_parameter_value_false.__name__,
|
||||
"is like": PropertyRules.is_parameter_value_like.__name__,
|
||||
"identical to": PropertyRules.is_identical_value.__name__,
|
||||
"contains": PropertyRules.is_parameter_value_containing.__name__,
|
||||
"does not contain": PropertyRules.is_parameter_value_not_containing.__name__,
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
"""Module for processing rules against Speckle objects and updating the automate context with the results."""
|
||||
|
||||
import json
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
from pandas.core.groupby import DataFrameGroupBy
|
||||
from speckle_automate import AutomationContext, ObjectResultLevel
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
from src.helpers import speckle_print
|
||||
from src.inputs import MinimumSeverity
|
||||
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:
|
||||
"""Given a Speckle object and a condition, evaluates the condition and returns a boolean value.
|
||||
|
||||
A condition is a pandas Series object with the following keys:
|
||||
- 'Property Name': The name of the property to evaluate.
|
||||
- 'Predicate': The predicate to use for evaluation.
|
||||
- 'Value': The value to compare against.
|
||||
|
||||
Args:
|
||||
rule_number (string): For information the rule number.
|
||||
case_number (int): For information the rule clause number.
|
||||
speckle_object (Base): The Speckle object to evaluate.
|
||||
condition (pd.Series): The condition to evaluate.
|
||||
|
||||
Returns:
|
||||
bool: The result of the evaluation. True if the condition is met, False otherwise.
|
||||
"""
|
||||
property_name = condition["Property Name"]
|
||||
predicate_key = condition["Predicate"]
|
||||
value = condition["Value"]
|
||||
|
||||
_ = rule_number
|
||||
_ = case_number
|
||||
|
||||
if predicate_key in PREDICATE_METHOD_MAP:
|
||||
method_name = PREDICATE_METHOD_MAP[predicate_key]
|
||||
method = getattr(PropertyRules, method_name, None)
|
||||
|
||||
if method:
|
||||
return method(speckle_object, property_name, value)
|
||||
|
||||
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]]:
|
||||
"""Processes a set of rules against Speckle objects, returning those that pass and fail.
|
||||
|
||||
The first rule is used as a filter ('WHERE'), and subsequent rules as conditions ('AND').
|
||||
|
||||
Args:
|
||||
speckle_objects: List of Speckle objects to be processed.
|
||||
rule_group: DataFrame defining the filter and conditions.
|
||||
|
||||
Returns:
|
||||
A tuple of lists containing objects that passed and failed the rule.
|
||||
"""
|
||||
if not speckle_objects or rule_group.empty:
|
||||
return [], []
|
||||
|
||||
try:
|
||||
validate_rule_structure(rule_group)
|
||||
except ValueError as e:
|
||||
speckle_print(f"Rule validation error: {str(e)}")
|
||||
return [], []
|
||||
|
||||
# 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
|
||||
)
|
||||
]
|
||||
|
||||
# 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)
|
||||
):
|
||||
pass_objects.append(obj)
|
||||
else:
|
||||
fail_objects.append(obj)
|
||||
|
||||
return pass_objects, fail_objects
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Args:
|
||||
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
|
||||
if "Message" not in rule_group.columns or "Report Severity" not in rule_group.columns:
|
||||
continue # Or raise an exception if these columns are mandatory
|
||||
|
||||
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)]
|
||||
|
||||
# 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:
|
||||
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={},
|
||||
)
|
||||
|
||||
grouped_results[rule_id_str] = (pass_objects, fail_objects)
|
||||
|
||||
# return pass_objects, fail_objects for each rule
|
||||
return grouped_results
|
||||
|
||||
|
||||
class SeverityLevel(Enum):
|
||||
"""Enum for severity levels."""
|
||||
|
||||
INFO = "Info"
|
||||
WARNING = "Warning"
|
||||
ERROR = "Error"
|
||||
|
||||
|
||||
def get_severity(rule_info: pd.Series) -> SeverityLevel:
|
||||
"""Convert a string severity level to the corresponding SeverityLevel enum.
|
||||
|
||||
This function normalizes input strings (because processing user entered dead is hard), handling:
|
||||
- Case insensitivity (e.g., "info", "WARNING" → "Info", "Warning")
|
||||
- Shorthand mappings (e.g., "WARN" → "Warning")
|
||||
- Stripping whitespace
|
||||
- Defaults to SeverityLevel.ERROR if the input is invalid
|
||||
"""
|
||||
severity = rule_info.get("Report Severity") # Extract severity from input data
|
||||
|
||||
# If severity is None or not a string (e.g., numeric input), default to ERROR
|
||||
if not isinstance(severity, str):
|
||||
return SeverityLevel.ERROR
|
||||
|
||||
severity = severity.strip().upper() # Remove leading/trailing spaces & normalize case
|
||||
|
||||
# Define a mapping for shorthand or alternate spellings
|
||||
alias_map = {
|
||||
"WARN": "WARNING", # Treat "WARN" as "WARNING"
|
||||
}
|
||||
|
||||
# Replace shorthand values if applicable
|
||||
severity = alias_map.get(severity, severity)
|
||||
|
||||
# Attempt to match with an existing SeverityLevel enum value (case-insensitive)
|
||||
return next(
|
||||
(level for level in SeverityLevel if level.value.upper() == severity),
|
||||
SeverityLevel.ERROR, # Default to ERROR if no match is found
|
||||
)
|
||||
|
||||
|
||||
def get_metadata(
|
||||
rule_id: str, rule_info: pd.Series, passed: bool, speckle_objects: list[Base]
|
||||
) -> dict[str, str | int | Any]:
|
||||
"""Function that generates metadata with severity validation and ensures JSON serializability.
|
||||
|
||||
Reasoning is that non-valid metadata fails inside the Automate context. So let's ensure it's valid.
|
||||
|
||||
Args:
|
||||
rule_id: Identifier for the rule
|
||||
rule_info: Series containing rule information
|
||||
passed: Boolean indicating if the rule passed
|
||||
speckle_objects: List of Speckle objects
|
||||
|
||||
Returns:
|
||||
Dictionary containing metadata if valid JSON serializable, empty dict otherwise
|
||||
"""
|
||||
try:
|
||||
metadata = {
|
||||
"rule_id": rule_id,
|
||||
"status": "PASS" if passed else "FAIL",
|
||||
"severity": get_severity(rule_info).value,
|
||||
"rule_message": format_message(rule_info),
|
||||
"object_count": len(speckle_objects),
|
||||
}
|
||||
|
||||
# Validate JSON serializability
|
||||
json.dumps(metadata)
|
||||
return metadata
|
||||
|
||||
except (TypeError, ValueError, json.JSONDecodeError) as e:
|
||||
# Log the error for debugging purposes
|
||||
print(f"Error creating metadata: {str(e)}")
|
||||
return {}
|
||||
|
||||
|
||||
def attach_results(
|
||||
speckle_objects: list[Base],
|
||||
rule_info: pd.Series,
|
||||
rule_id: str,
|
||||
context: AutomationContext,
|
||||
passed: bool,
|
||||
) -> None:
|
||||
"""Attaches the results of a rule to the objects in the context.
|
||||
|
||||
Args:
|
||||
speckle_objects (List[Base]): The list of objects to which the rule was applied.
|
||||
rule_info (pd.Series): The information about the rule.
|
||||
rule_id (str): The ID of the rule.
|
||||
context (AutomationContext): The context manager for attaching results.
|
||||
passed (bool): Whether the rule passed or failed.
|
||||
"""
|
||||
if not speckle_objects:
|
||||
return
|
||||
|
||||
# Create structured metadata for onward data analysis uses
|
||||
|
||||
metadata = get_metadata(rule_id, rule_info, passed, speckle_objects)
|
||||
message = format_message(rule_info)
|
||||
|
||||
if not passed:
|
||||
speckle_print(rule_info["Report Severity"])
|
||||
|
||||
severity = (
|
||||
ObjectResultLevel.WARNING
|
||||
if rule_info["Report Severity"].capitalize() in ["Warning", "Warn"]
|
||||
else ObjectResultLevel.ERROR
|
||||
)
|
||||
context.attach_result_to_objects(
|
||||
category=f"Rule {rule_id}",
|
||||
object_ids=[speckle_object.id for speckle_object in speckle_objects],
|
||||
message=message,
|
||||
level=severity,
|
||||
metadata=metadata,
|
||||
)
|
||||
else:
|
||||
context.attach_info_to_objects(
|
||||
category=f"Rule {rule_id}",
|
||||
object_ids=[speckle_object.id for speckle_object in speckle_objects],
|
||||
message=message,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
|
||||
def format_message(rule_info):
|
||||
"""Format the message for the rule."""
|
||||
message = (
|
||||
str(rule_info["Message"])
|
||||
if rule_info["Message"] is not None and not pd.isna(rule_info["Message"])
|
||||
else "No Message"
|
||||
)
|
||||
return message
|
||||
+456
-630
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,756 @@
|
||||
# import re
|
||||
# from typing import Any
|
||||
#
|
||||
# from Levenshtein import ratio
|
||||
# from specklepy.objects.base import Base
|
||||
#
|
||||
# from src.helpers import get_item, has_item, speckle_print
|
||||
# from src.inputs import PropertyMatchMode
|
||||
|
||||
# We're going to define a set of rules that will allow us to filter and
|
||||
# process parameters in our Speckle objects. These rules will be encapsulated
|
||||
# in a class called `ParameterRules`.
|
||||
|
||||
|
||||
# class Rules:
|
||||
# """A collection of rules for processing properties in Speckle objects.
|
||||
#
|
||||
# Simple rules can be straightforwardly implemented as static methods that
|
||||
# return boolean value to be used either as a filter or a condition.
|
||||
# These can then be abstracted into returning lambda functions that we can
|
||||
# use in our main processing logic. By encapsulating these rules, we can easily
|
||||
# extend or modify them in the future.
|
||||
# """
|
||||
#
|
||||
# @staticmethod
|
||||
# def try_get_display_value(
|
||||
# speckle_object: Base,
|
||||
# ) -> list[Base] | None:
|
||||
# """Try fetching the display value from a Speckle object.
|
||||
#
|
||||
# This method encapsulates the logic for attempting to retrieve the display value from a
|
||||
# Speckle object. It returns a list containing the display values if found,
|
||||
# otherwise it returns None.
|
||||
#
|
||||
# Args:
|
||||
# speckle_object (Base): The Speckle object to extract the display value from.
|
||||
#
|
||||
# Returns:
|
||||
# Optional[List[Base]]: A list containing the display values.
|
||||
# If no display value is found, returns None.
|
||||
# """
|
||||
# # Attempt to get the display value from the speckle_object
|
||||
# raw_display_value = getattr(speckle_object, "displayValue", None) or getattr(
|
||||
# speckle_object, "@displayValue", None
|
||||
# )
|
||||
#
|
||||
# # If no display value found, return None
|
||||
# if raw_display_value is None:
|
||||
# return None
|
||||
#
|
||||
# # If display value found, filter out non-Base objects
|
||||
# display_values = [value for value in raw_display_value if isinstance(value, Base)]
|
||||
#
|
||||
# # If no valid display values found, return None
|
||||
# if not display_values:
|
||||
# return None
|
||||
#
|
||||
# return display_values
|
||||
#
|
||||
# @staticmethod
|
||||
# def is_displayable_object(speckle_object: Base) -> bool:
|
||||
# """Determines if a given Speckle object is displayable.
|
||||
#
|
||||
# This method encapsulates the logic for determining if a Speckle object is displayable.
|
||||
# It checks if the speckle_object has a display value and returns True if it does, otherwise it returns False.
|
||||
#
|
||||
# Args:
|
||||
# speckle_object (Base): The Speckle object to check.
|
||||
#
|
||||
# Returns:
|
||||
# bool: True if the object has a display value, False otherwise.
|
||||
# """
|
||||
# # Check for direct displayable state using try_get_display_value
|
||||
# display_values = Rules.try_get_display_value(speckle_object)
|
||||
# if display_values and getattr(speckle_object, "id", None) is not None:
|
||||
# return True
|
||||
#
|
||||
# # Check for displayable state via definition, using try_get_display_value on the definition object
|
||||
# definition = getattr(speckle_object, "definition", None)
|
||||
# if definition:
|
||||
# definition_display_values = Rules.try_get_display_value(definition)
|
||||
# if definition_display_values and getattr(definition, "id", None) is not None:
|
||||
# return True
|
||||
#
|
||||
# return False
|
||||
#
|
||||
# @staticmethod
|
||||
# def get_displayable_objects(flat_list_of_objects: list[Base]) -> list[Base]:
|
||||
# """Filters a list of Speckle objects to only include displayable objects.
|
||||
#
|
||||
# This function takes a list of Speckle objects and filters out the objects that are displayable.
|
||||
# It returns a list containing only the displayable objects.
|
||||
#
|
||||
# Args:
|
||||
# flat_list_of_objects (List[Base]): The list of Speckle objects to filter.
|
||||
# """
|
||||
# return [
|
||||
# speckle_object
|
||||
# for speckle_object in flat_list_of_objects
|
||||
# if Rules.is_displayable_object(speckle_object) and getattr(speckle_object, "id", None)
|
||||
# ]
|
||||
#
|
||||
#
|
||||
# class PropertyRules:
|
||||
# """A collection of rules for processing Revit parameters in Speckle objects."""
|
||||
#
|
||||
# @staticmethod
|
||||
# def has_parameter(speckle_object: Base, parameter_name: str, *_args, **_kwargs) -> bool:
|
||||
# """Checks if the speckle_object has a parameter with the given name."""
|
||||
# found, _ = ParameterSearch.lookup_parameter(speckle_object, parameter_name)
|
||||
# return found
|
||||
#
|
||||
# @staticmethod
|
||||
# def get_parameter_value(
|
||||
# speckle_object: Base,
|
||||
# parameter_name: str,
|
||||
# match_mode: PropertyMatchMode = PropertyMatchMode.MIXED,
|
||||
# default_value: Any = None,
|
||||
# ) -> Any:
|
||||
# """Gets the value of a parameter if it exists."""
|
||||
# found, value = ParameterSearch.lookup_parameter(speckle_object, parameter_name, match_mode)
|
||||
# return value if found else default_value
|
||||
#
|
||||
# @staticmethod
|
||||
# def is_v3(speckle_object: Base) -> bool:
|
||||
# """Determines if a Speckle object uses v3 parameter structure.
|
||||
#
|
||||
# Args:
|
||||
# speckle_object (Base): The Speckle object to check
|
||||
#
|
||||
# Returns:
|
||||
# bool: True if object uses v3 structure, False otherwise
|
||||
# """
|
||||
# properties = get_item(speckle_object, "properties")
|
||||
# return bool(properties and has_item(properties, "Parameters"))
|
||||
#
|
||||
# # @staticmethod
|
||||
# # def has_parameter(speckle_object: Base, parameter_name: str, *_args, **_kwargs) -> bool:
|
||||
# # """Checks if the speckle_object has a Revit parameter with the given name.
|
||||
# #
|
||||
# # First checks direct properties, then determines if it's a v2 or v3 object structure
|
||||
# # and searches in the appropriate parameter hierarchy.
|
||||
# #
|
||||
# # Args:
|
||||
# # speckle_object (Base): The Speckle object to check.
|
||||
# # parameter_name (str): The name of the parameter to check for.
|
||||
# # *_args: Extra positional arguments which are ignored.
|
||||
# # **_kwargs: Extra keyword arguments which are ignored.
|
||||
# #
|
||||
# # Returns:
|
||||
# # bool: True if the object has the parameter, False otherwise.
|
||||
# # """
|
||||
# # # Check direct property first regardless of version
|
||||
# # if has_item(speckle_object, parameter_name):
|
||||
# # return True
|
||||
# #
|
||||
# # if PropertyRules.is_v3(speckle_object):
|
||||
# # properties = get_item(speckle_object, "properties")
|
||||
# # parameters = get_item(properties, "Parameters")
|
||||
# # if parameters:
|
||||
# #
|
||||
# # def search_v3_params(params: dict, search_name: str) -> bool:
|
||||
# # for key, value in params.items():
|
||||
# # if isinstance(value, dict):
|
||||
# # # Check direct name match
|
||||
# # if key.lower() == search_name.lower():
|
||||
# # return True
|
||||
# # # Check nested parameters
|
||||
# # if search_v3_params(value, search_name):
|
||||
# # return True
|
||||
# # return False
|
||||
# #
|
||||
# # return search_v3_params(parameters, parameter_name)
|
||||
# # else:
|
||||
# # # Handle v2 structure
|
||||
# # parameters = get_item(speckle_object, "parameters")
|
||||
# # if not parameters:
|
||||
# # return False
|
||||
# #
|
||||
# # # Check direct parameter name match
|
||||
# # if has_item(parameters, parameter_name):
|
||||
# # return True
|
||||
# #
|
||||
# # # Check nested parameters with name property
|
||||
# # def check_nested_name(value: Any) -> bool:
|
||||
# # if isinstance(value, dict):
|
||||
# # return get_item(value, "name") == parameter_name
|
||||
# # return get_item(value, "name") == parameter_name if hasattr(value, "name") else False
|
||||
# #
|
||||
# # return any(check_nested_name(param_value) for param_value in parameters.values() if param_value is not None)
|
||||
# #
|
||||
# # return False
|
||||
# #
|
||||
# # @staticmethod
|
||||
# # def get_parameter_value(
|
||||
# # speckle_object: Base,
|
||||
# # parameter_name: str,
|
||||
# # match_mode: PropertyMatchMode = PropertyMatchMode.MIXED,
|
||||
# # default_value: Any = None,
|
||||
# # ) -> Any | None:
|
||||
# # """Retrieves the value of the specified parameter from the speckle_object.
|
||||
# #
|
||||
# # First checks direct properties, then determines if it's a v2 or v3 object structure
|
||||
# # and retrieves from the appropriate parameter hierarchy.
|
||||
# #
|
||||
# # Args:
|
||||
# # speckle_object (Base): The Speckle object to retrieve the parameter value from.
|
||||
# # parameter_name (str): The name of the parameter to retrieve the value for.
|
||||
# # match_mode (PropertyMatchMode): The matching mode to use for parameter lookup
|
||||
# # default_value: The default value to return if parameter not found.
|
||||
# #
|
||||
# # Returns:
|
||||
# # The value of the parameter if found, else default_value.
|
||||
# # """
|
||||
# # # Check direct property first regardless of version
|
||||
# # if has_item(speckle_object, parameter_name):
|
||||
# # value = get_item(speckle_object, parameter_name)
|
||||
# # return value if value is not None else default_value
|
||||
# #
|
||||
# # if PropertyRules.is_v3(speckle_object):
|
||||
# # return PropertyRules.get_v3_parameter(speckle_object, parameter_name, match_mode, default_value)
|
||||
# # else:
|
||||
# # return PropertyRules.get_v2_parameter(speckle_object, parameter_name, match_mode, default_value)
|
||||
#
|
||||
# # @staticmethod
|
||||
# # def get_v2_parameter(obj: Base, name: str, mode: PropertyMatchMode, default: Any) -> Any:
|
||||
# # """Get parameter value from v2 Speckle object structure.
|
||||
# #
|
||||
# # Args:
|
||||
# # obj: Speckle object to get parameter from
|
||||
# # name: Parameter name to retrieve
|
||||
# # mode: Match mode for parameter lookup
|
||||
# # default: Default value if parameter not found
|
||||
# #
|
||||
# # Returns:
|
||||
# # Parameter value if found, else default
|
||||
# # """
|
||||
# # parameters = get_item(obj, "parameters")
|
||||
# # if not parameters:
|
||||
# # return default
|
||||
# #
|
||||
# # if mode == PropertyMatchMode.STRICT:
|
||||
# # return PropertyRules.strict_parameter_lookup(name, parameters, default)
|
||||
# #
|
||||
# # def search_params(param_dict: dict, search_name: str, fuzzy: bool) -> Any:
|
||||
# # for key, value in param_dict.items():
|
||||
# # key_match = (key.lower() == search_name.lower()) or (fuzzy and search_name.lower() in key.lower())
|
||||
# # if key_match:
|
||||
# # # Handle both direct values and nested parameter objects
|
||||
# # return get_item(value, "value", value)
|
||||
# # return None
|
||||
# #
|
||||
# # result = search_params(parameters, name, mode == PropertyMatchMode.FUZZY)
|
||||
# # return result if result is not None else default
|
||||
# #
|
||||
# # @staticmethod
|
||||
# # def get_v3_parameter(obj: Base, name: str, mode: PropertyMatchMode, default: Any) -> Any:
|
||||
# # """Get parameter value from v3 Speckle object structure.
|
||||
# #
|
||||
# # Args:
|
||||
# # obj: Speckle object to get parameter from
|
||||
# # name: Parameter name to retrieve
|
||||
# # mode: Match mode for parameter lookup
|
||||
# # default: Default value if parameter not found
|
||||
# #
|
||||
# # Returns:
|
||||
# # Parameter value if found, else default
|
||||
# # """
|
||||
# # properties = get_item(obj, "properties")
|
||||
# # if not properties or not has_item(properties, "Parameters"):
|
||||
# # return default
|
||||
# #
|
||||
# # parameters = get_item(properties, "Parameters")
|
||||
# # if not parameters:
|
||||
# # return default
|
||||
# #
|
||||
# # if mode == PropertyMatchMode.STRICT:
|
||||
# # return PropertyRules.strict_parameter_lookup(name, parameters, default)
|
||||
# #
|
||||
# # def search_nested(data: dict, search_name: str, fuzzy: bool) -> Any:
|
||||
# # for nested_key, value in data.items():
|
||||
# # if isinstance(value, dict):
|
||||
# # key_match = (nested_key.lower() == search_name.lower()) or (
|
||||
# # fuzzy and search_name.lower() in nested_key.lower()
|
||||
# # )
|
||||
# #
|
||||
# # if key_match and has_item(value, "value"):
|
||||
# # return get_item(value, "value")
|
||||
# #
|
||||
# # nested_result = search_nested(value, search_name, fuzzy)
|
||||
# # if nested_result is not None:
|
||||
# # return nested_result
|
||||
# # return None
|
||||
# #
|
||||
# # result = search_nested(parameters, name, mode == PropertyMatchMode.FUZZY)
|
||||
# # return result if result is not None else default
|
||||
# #
|
||||
# # @staticmethod
|
||||
# # def strict_parameter_lookup(name: str, parameters: dict, default: Any) -> Any:
|
||||
# # """Perform strict parameter lookup following exact path.
|
||||
# #
|
||||
# # Args:
|
||||
# # name: Parameter path (dot separated)
|
||||
# # parameters: Parameters dictionary
|
||||
# # default: Default value if not found
|
||||
# #
|
||||
# # Returns:
|
||||
# # Parameter value if found, else default
|
||||
# # """
|
||||
# # path_parts = name.split(".")
|
||||
# # current = parameters
|
||||
# #
|
||||
# # for part in path_parts:
|
||||
# # if not current or not isinstance(current, dict):
|
||||
# # return default
|
||||
# #
|
||||
# # # Find exact case-insensitive match
|
||||
# # key = next((k for k in current.keys() if k.lower() == part.lower()), None)
|
||||
# # if not key:
|
||||
# # return default
|
||||
# #
|
||||
# # current = get_item(current, key)
|
||||
# #
|
||||
# # # Handle both direct values and parameter objects
|
||||
# # if isinstance(current, dict):
|
||||
# # return get_item(current, "value", current)
|
||||
# # return current
|
||||
#
|
||||
# @staticmethod
|
||||
# def is_parameter_value(speckle_object: Base, parameter_name: str, value_to_match: Any) -> bool:
|
||||
# """Checks if the value of the specified parameter matches the given value.
|
||||
#
|
||||
# Args:
|
||||
# speckle_object (Base): The Speckle object to check.
|
||||
# parameter_name (str): The name of the parameter to check.
|
||||
# value_to_match (Any): The value to match against.
|
||||
#
|
||||
# Returns:
|
||||
# bool: True if the parameter value matches the given value, False otherwise.
|
||||
# """
|
||||
# parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
|
||||
# return parameter_value == value_to_match
|
||||
#
|
||||
# @staticmethod
|
||||
# def is_parameter_value_like(
|
||||
# speckle_object: Base,
|
||||
# parameter_name: str,
|
||||
# pattern: str,
|
||||
# fuzzy: bool = False,
|
||||
# threshold: float = 0.8,
|
||||
# ) -> bool:
|
||||
# """Checks if the value of the specified parameter matches the given pattern.
|
||||
#
|
||||
# Args:
|
||||
# speckle_object (Base): The Speckle object to check.
|
||||
# parameter_name (str): The name of the parameter to check.
|
||||
# pattern (str): The pattern to match against.
|
||||
# fuzzy (bool): If True, performs fuzzy matching using Levenshtein distance.
|
||||
# If False (default), performs exact pattern matching using regular expressions.
|
||||
# threshold (float): The similarity threshold for fuzzy matching (default: 0.8).
|
||||
# Only applicable when fuzzy=True.
|
||||
#
|
||||
# Returns:
|
||||
# bool: True if the parameter value matches the pattern (exact or fuzzy), False otherwise.
|
||||
# """
|
||||
# parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
|
||||
# if parameter_value is None:
|
||||
# return False
|
||||
#
|
||||
# if fuzzy:
|
||||
# similarity = ratio(str(parameter_value), pattern)
|
||||
# return similarity >= threshold
|
||||
# else:
|
||||
# return bool(re.match(pattern, str(parameter_value)))
|
||||
#
|
||||
# @staticmethod
|
||||
# def parse_number_from_string(input_string: str):
|
||||
# """Attempts to parse an integer or float from a given string.
|
||||
#
|
||||
# Args:
|
||||
# input_string (str): The string containing the number to be parsed.
|
||||
#
|
||||
# Returns:
|
||||
# int or float: The parsed number, or raises ValueError if parsing is not possible.
|
||||
# """
|
||||
# try:
|
||||
# # First try to convert it to an integer
|
||||
# return int(input_string)
|
||||
# except ValueError:
|
||||
# # If it fails to convert to an integer, try to convert to a float
|
||||
# try:
|
||||
# return float(input_string)
|
||||
# except ValueError:
|
||||
# # Raise an error if neither conversion is possible
|
||||
# raise ValueError("Input string is not a valid integer or float")
|
||||
#
|
||||
# @staticmethod
|
||||
# def is_parameter_value_greater_than(speckle_object: Base, parameter_name: str, threshold: str) -> bool:
|
||||
# """Checks if the value of the specified parameter is greater than the given threshold.
|
||||
#
|
||||
# Args:
|
||||
# speckle_object (Base): The Speckle object to check.
|
||||
# parameter_name (str): The name of the parameter to check.
|
||||
# threshold (Union[int, float]): The threshold value to compare against.
|
||||
#
|
||||
# Returns:
|
||||
# bool: True if the parameter value is greater than the threshold, False otherwise.
|
||||
# """
|
||||
# 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)
|
||||
#
|
||||
# @staticmethod
|
||||
# def is_parameter_value_less_than(speckle_object: Base, parameter_name: str, threshold: str) -> bool:
|
||||
# """Checks if the value of the specified parameter is less than the given threshold.
|
||||
#
|
||||
# Args:
|
||||
# speckle_object (Base): The Speckle object to check.
|
||||
# parameter_name (str): The name of the parameter to check.
|
||||
# threshold (Union[int, float]): The threshold value to compare against.
|
||||
#
|
||||
# Returns:
|
||||
# bool: True if the parameter value is less than the threshold, False otherwise.
|
||||
# """
|
||||
# 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)
|
||||
#
|
||||
# @staticmethod
|
||||
# def is_parameter_value_in_range(speckle_object: Base, parameter_name: str, value_range: str) -> bool:
|
||||
# """Checks if the value of the specified parameter falls within the given range.
|
||||
#
|
||||
# Args:
|
||||
# speckle_object (Base): The Speckle object to check.
|
||||
# parameter_name (str): The name of the parameter to check.
|
||||
# value_range (str): The range to check against, in the format "min_value, max_value".
|
||||
#
|
||||
# Returns:
|
||||
# bool: True if the parameter value falls within the range (inclusive), False otherwise.
|
||||
# """
|
||||
# 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)
|
||||
#
|
||||
# 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 min_value <= parameter_value <= max_value
|
||||
#
|
||||
# @staticmethod
|
||||
# def is_parameter_value_in_range_expanded(
|
||||
# speckle_object: Base,
|
||||
# parameter_name: str,
|
||||
# min_value: int | float,
|
||||
# max_value: int | float,
|
||||
# inclusive: bool = True,
|
||||
# ) -> bool:
|
||||
# """Checks if the value of the specified parameter falls within the given range.
|
||||
#
|
||||
# Args:
|
||||
# speckle_object (Base): The Speckle object to check.
|
||||
# parameter_name (str): The name of the parameter to check.
|
||||
# min_value (Union[int, float]): The minimum value of the range.
|
||||
# max_value (Union[int, float]): The maximum value of the range.
|
||||
# inclusive (bool): If True (default), the range is inclusive (min <= value <= max).
|
||||
# If False, the range is exclusive (min < value < max).
|
||||
#
|
||||
# Returns:
|
||||
# bool: True if the parameter value falls within the range (inclusive), False otherwise.
|
||||
# """
|
||||
# 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 min_value <= parameter_value <= max_value if inclusive else min_value < parameter_value < max_value
|
||||
#
|
||||
# @staticmethod
|
||||
# def is_parameter_value_in_list(speckle_object: Base, parameter_name: str, value_list: list[Any] | str) -> bool:
|
||||
# """Checks if the value of the specified parameter is present in the given list of values.
|
||||
#
|
||||
# Args:
|
||||
# speckle_object (Base): The Speckle object to check.
|
||||
# parameter_name (str): The name of the parameter to check.
|
||||
# value_list (List[Any]): The list of values to check against.
|
||||
#
|
||||
# Returns:
|
||||
# bool: True if the parameter value is found in the list, False otherwise.
|
||||
# """
|
||||
# parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
|
||||
#
|
||||
# if isinstance(value_list, str):
|
||||
# value_list = [value.strip() for value in value_list.split(",")]
|
||||
#
|
||||
# # parameter_value is effectively Any type, so to find its value in the value_list
|
||||
# def is_value_in_list(value: Any, my_list: Any) -> bool:
|
||||
# # Ensure that my_list is actually a list
|
||||
# if isinstance(my_list, list):
|
||||
# return value in my_list or str(value) in my_list
|
||||
# else:
|
||||
# speckle_print(f"Expected a list, got {type(my_list)} instead.")
|
||||
# return False
|
||||
#
|
||||
# return is_value_in_list(parameter_value, value_list)
|
||||
#
|
||||
# @staticmethod
|
||||
# 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)
|
||||
#
|
||||
# if isinstance(value, str):
|
||||
# return value.lower() in values_to_match
|
||||
#
|
||||
# return False
|
||||
#
|
||||
# @staticmethod
|
||||
# def is_parameter_value_true(speckle_object: Base, parameter_name: str) -> bool:
|
||||
# """Check if parameter value represents true (boolean True, 'yes', 'true', '1')."""
|
||||
# parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
|
||||
# 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 (boolean False, 'no', 'false', '0')."""
|
||||
# parameter_value = PropertyRules.get_parameter_value(speckle_object, parameter_name)
|
||||
# return PropertyRules._check_boolean_value(parameter_value, ("no", "false", "0"))
|
||||
#
|
||||
# @staticmethod
|
||||
# def has_category(speckle_object: Base) -> bool:
|
||||
# """Checks if the speckle_object has a 'category' parameter.
|
||||
#
|
||||
# This method checks if the speckle_object has a 'category' parameter.
|
||||
# If the 'category' parameter exists, it returns True; otherwise, it returns False.
|
||||
#
|
||||
# Args:
|
||||
# speckle_object (Base): The Speckle object to check.
|
||||
#
|
||||
# Returns:
|
||||
# bool: True if the object has the 'category' parameter, False otherwise.
|
||||
# """
|
||||
# return PropertyRules.has_parameter(speckle_object, "category")
|
||||
#
|
||||
# @staticmethod
|
||||
# def is_category(speckle_object: Base, category_input: str) -> bool:
|
||||
# """Checks if the value of the 'category' property matches the given input.
|
||||
#
|
||||
# This method checks if the 'category' property of the speckle_object
|
||||
# matches the given category_input. If they match, it returns True;
|
||||
# otherwise, it returns False.
|
||||
#
|
||||
# Args:
|
||||
# speckle_object (Base): The Speckle object to check.
|
||||
# category_input (str): The category value to compare against.
|
||||
#
|
||||
# Returns:
|
||||
# bool: True if the 'category' property matches the input, False otherwise.
|
||||
# """
|
||||
# category_value = PropertyRules.get_parameter_value(speckle_object, "category")
|
||||
# return category_value == category_input
|
||||
#
|
||||
# @staticmethod
|
||||
# def get_category_value(speckle_object: Base) -> str:
|
||||
# """Retrieves the value of the 'category' parameter from the speckle_object.
|
||||
#
|
||||
# This method retrieves the value of the 'category' parameter from the speckle_object.
|
||||
# If the 'category' parameter exists and its value is not None, it returns the value.
|
||||
# If the 'category' parameter does not exist or its value is None, it returns an empty string.
|
||||
#
|
||||
# Args:
|
||||
# speckle_object (Base): The Speckle object to retrieve the 'category' parameter value from.
|
||||
#
|
||||
# Returns:
|
||||
# str: The value of the 'category' parameter if it exists and is not None, or an empty string otherwise.
|
||||
# """
|
||||
# return PropertyRules.get_parameter_value(speckle_object, "category")
|
||||
#
|
||||
#
|
||||
# class ParameterSearch:
|
||||
# """Unified parameter search functionality for Speckle objects."""
|
||||
#
|
||||
# @staticmethod
|
||||
# def convert_revit_boolean(value: Any) -> Any:
|
||||
# """Convert Revit-style Yes/No strings to boolean values.
|
||||
#
|
||||
# Args:
|
||||
# value: The value to potentially convert
|
||||
#
|
||||
# Returns:
|
||||
# bool if value is a Revit boolean string, original value otherwise
|
||||
# """
|
||||
# if isinstance(value, str):
|
||||
# if value.lower() == "yes":
|
||||
# return True
|
||||
# if value.lower() == "no":
|
||||
# return False
|
||||
# return value
|
||||
#
|
||||
# @staticmethod
|
||||
# def search_parameters(
|
||||
# params: dict, search_name: str, mode: PropertyMatchMode = PropertyMatchMode.STRICT
|
||||
# ) -> tuple[bool, Any]:
|
||||
# """Search for parameters using consistent matching logic.
|
||||
#
|
||||
# Supports flexible property chain matching that can find paths like "Instance Parameters.Dimensions.Length"
|
||||
# within longer chains like "properties.Parameters.Instance Parameters.Dimensions.Length.value".
|
||||
# Uses STRICT matching by default for more predictable results.
|
||||
#
|
||||
# Args:
|
||||
# params: Parameter dictionary to search
|
||||
# search_name: Name of parameter to find, can be dot-separated chain
|
||||
# mode: Matching mode to use (STRICT by default, or FUZZY/MIXED for looser matching)
|
||||
#
|
||||
# Returns:
|
||||
# Tuple of (value_found: bool, value: Any)
|
||||
# """
|
||||
#
|
||||
# def matches_name(match_key: str, target: str, match_mode: PropertyMatchMode) -> bool:
|
||||
# if match_mode == PropertyMatchMode.STRICT:
|
||||
# return match_key.lower() == target.lower()
|
||||
# elif match_mode == PropertyMatchMode.FUZZY:
|
||||
# return target.lower() in match_key.lower()
|
||||
# else: # MIXED mode
|
||||
# return match_key.lower() == target.lower() or target.lower() in match_key.lower()
|
||||
#
|
||||
# def try_get_value(obj: Any) -> Any:
|
||||
# """Extract value from parameter object or return as is.
|
||||
#
|
||||
# Handles both dict and Base objects, checking for 'value' property in both cases.
|
||||
# Returns the 'value' if found, otherwise returns the original object.
|
||||
# """
|
||||
# # Handle dictionary objects
|
||||
# if isinstance(obj, dict):
|
||||
# return obj.get("value", obj)
|
||||
#
|
||||
# # Handle Base objects
|
||||
# if isinstance(obj, Base):
|
||||
# return getattr(obj, "value", obj)
|
||||
#
|
||||
# # For all other types, return as is
|
||||
# return obj
|
||||
#
|
||||
# # First try property chain lookup
|
||||
# if "." in search_name:
|
||||
# search_parts = search_name.split(".")
|
||||
#
|
||||
# def try_match_path(current: dict, remaining_search_parts: list[str], depth: int = 0) -> tuple[bool, Any]:
|
||||
# if not isinstance(current, dict):
|
||||
# return False, None
|
||||
#
|
||||
# if not remaining_search_parts: # We've matched all parts
|
||||
# return True, try_get_value(current)
|
||||
#
|
||||
# current_search = remaining_search_parts[0]
|
||||
#
|
||||
# # Try each key at current level
|
||||
# for key, item_value in current.items():
|
||||
# if matches_name(key, current_search, mode):
|
||||
# # Found a match for current part, recurse with rest
|
||||
# match_found, result = try_match_path(item_value, remaining_search_parts[1:], depth + 1)
|
||||
# if match_found:
|
||||
# return True, result
|
||||
#
|
||||
# # If no match found and value is a dict, try searching deeper
|
||||
# if isinstance(item_value, dict):
|
||||
# match_found, result = try_match_path(item_value, remaining_search_parts, depth)
|
||||
# if match_found:
|
||||
# return True, result
|
||||
#
|
||||
# return False, None
|
||||
#
|
||||
# try:
|
||||
# found, value = try_match_path(params, search_parts)
|
||||
# if found:
|
||||
# return True, value
|
||||
# except Exception:
|
||||
# pass # Fall through to recursive search if chain lookup fails
|
||||
#
|
||||
# # Recursive search through nested dictionaries
|
||||
# def recursive_search(data: dict | Base, target: str) -> tuple[bool, Any]:
|
||||
# if not isinstance(data, dict | Base):
|
||||
# return False, None
|
||||
#
|
||||
# # Handle both dict and Base objects for iteration
|
||||
# if isinstance(data, dict):
|
||||
# items = data.items()
|
||||
# else:
|
||||
# items = [(k, getattr(data, k)) for k in dir(data) if not k.startswith("_")]
|
||||
#
|
||||
# # First check current level
|
||||
# for key, item_value in items:
|
||||
# if matches_name(key, target, mode):
|
||||
# return True, try_get_value(item_value)
|
||||
#
|
||||
# # Then check nested levels
|
||||
# for _, item_value in items:
|
||||
# if isinstance(item_value, dict | Base):
|
||||
# item_found, result = recursive_search(item_value, target)
|
||||
# if item_found:
|
||||
# return True, result
|
||||
#
|
||||
# return False, None
|
||||
#
|
||||
# return recursive_search(params, search_name.split(".")[-1] if "." in search_name else search_name)
|
||||
#
|
||||
# @staticmethod
|
||||
# def lookup_parameter(
|
||||
# obj: Base, param_name: str, mode: PropertyMatchMode = PropertyMatchMode.MIXED
|
||||
# ) -> tuple[bool, Any]:
|
||||
# """Unified parameter lookup for both checking existence and getting values.
|
||||
#
|
||||
# Args:
|
||||
# obj: Speckle object to search
|
||||
# param_name: Parameter name to find
|
||||
# mode: Matching mode to use
|
||||
#
|
||||
# Returns:
|
||||
# Tuple of (found: bool, value: Any)
|
||||
# """
|
||||
# # Check direct property first
|
||||
# if has_item(obj, param_name):
|
||||
# value = get_item(obj, param_name)
|
||||
# # Check if the direct property has a value field
|
||||
# if isinstance(value, dict) and "value" in value:
|
||||
# return True, value["value"]
|
||||
# return True, value
|
||||
#
|
||||
# # Handle v3 structure
|
||||
# if PropertyRules.is_v3(obj):
|
||||
# properties = get_item(obj, "properties")
|
||||
# if not properties or not has_item(properties, "Parameters"):
|
||||
# return False, None
|
||||
#
|
||||
# parameters = get_item(properties, "Parameters")
|
||||
# if not parameters:
|
||||
# return False, None
|
||||
#
|
||||
# return ParameterSearch.search_parameters(parameters, param_name, mode)
|
||||
#
|
||||
# # Handle v2 structure
|
||||
# parameters = get_item(obj, "parameters")
|
||||
# if not parameters:
|
||||
# return False, None
|
||||
#
|
||||
# return ParameterSearch.search_parameters(parameters, param_name, mode)
|
||||
+136
-8
@@ -1,18 +1,146 @@
|
||||
"""Module for reading and processing rules from a cloud hosted TSV file."""
|
||||
|
||||
import traceback
|
||||
|
||||
import pandas as pd
|
||||
from pandas import DataFrame
|
||||
from pandas.core.groupby import DataFrameGroupBy
|
||||
|
||||
|
||||
def read_rules_from_spreadsheet(url):
|
||||
"""Reads a TSV file from a provided URL and returns a DataFrame.
|
||||
def process_rule_numbers(df: DataFrame) -> DataFrame:
|
||||
"""Process rule numbers in a DataFrame while preserving original rule identifiers.
|
||||
|
||||
Makes no assumptions about rule number format - preserves them exactly as provided.
|
||||
Only generates new numbers (as integers) when no rule number exists.
|
||||
|
||||
Args:
|
||||
url (str): The URL to the TSV file.
|
||||
df: DataFrame with columns including 'Rule Number' and 'Logic'
|
||||
|
||||
Returns:
|
||||
DataFrame: Pandas DataFrame containing the TSV data.
|
||||
DataFrame with processed rule numbers
|
||||
"""
|
||||
# Create a copy to avoid modifying original
|
||||
df = df.copy()
|
||||
|
||||
# Initialize tracking variables
|
||||
used_rule_nums = set()
|
||||
processed_rule_nums = []
|
||||
next_auto_num = 1 # For generating missing rule numbers only
|
||||
|
||||
# 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 number
|
||||
while str(next_auto_num) in used_rule_nums:
|
||||
next_auto_num += 1
|
||||
group_rule_num = str(next_auto_num)
|
||||
next_auto_num += 1
|
||||
else:
|
||||
# Keep the original rule number exactly as is
|
||||
group_rule_num = str(group_rule_num)
|
||||
|
||||
# Update tracking
|
||||
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
|
||||
"""
|
||||
try:
|
||||
# Since the output is a TSV, we use `pd.read_csv` with `sep='\t'` to specify tab-separated values.
|
||||
return pd.read_csv(url, sep="\t")
|
||||
# Read the TSV file
|
||||
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
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed to read the TSV from the URL: {e}")
|
||||
return None
|
||||
traceback.print_exc()
|
||||
return None, [f"Failed to read the TSV from the URL: {str(e)}:{e.with_traceback(None)}"]
|
||||
|
||||
|
||||
def convert_mixed_columns(df: DataFrame) -> DataFrame:
|
||||
"""Converts columns in a DataFrame to appropriate types based on their content.
|
||||
|
||||
null or empty strings are converted to empty strings instead of NaN.
|
||||
|
||||
Args:
|
||||
df (DataFrame): The DataFrame whose columns are to be converted
|
||||
|
||||
Returns:
|
||||
DataFrame with columns converted to appropriate types
|
||||
"""
|
||||
df = df.apply(
|
||||
lambda c: c
|
||||
if c.dropna().apply(lambda x: str(x).replace(".", "", 1).isdigit()).any()
|
||||
else c.map(lambda x: "" if pd.isna(x) else str(x))
|
||||
)
|
||||
|
||||
return df
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,696 @@
|
||||
[
|
||||
{
|
||||
"id": "46f06fef727d64a0bbcbd7ced51e0cd2",
|
||||
"name": "Walls - W30(Fc24)",
|
||||
"type": "W30(Fc24)",
|
||||
"level": {
|
||||
"name": "1FL",
|
||||
"units": "mm",
|
||||
"elevation": 0
|
||||
},
|
||||
"units": "mm",
|
||||
"family": "Basic Wall",
|
||||
"flipped": false,
|
||||
"category": "Walls",
|
||||
"elements": [],
|
||||
"location": {
|
||||
"id": "9c76b8de34382c9052965ee463f8374b",
|
||||
"end": {
|
||||
"x": 22400.000000000015,
|
||||
"y": 20500,
|
||||
"z": 0,
|
||||
"id": "3455575bfd8939f264d295b61e74156f",
|
||||
"units": "mm",
|
||||
"speckle_type": "Objects.Geometry.Point",
|
||||
"applicationId": null
|
||||
},
|
||||
"bbox": null,
|
||||
"start": {
|
||||
"x": 22400.000000000007,
|
||||
"y": 15199.999999999998,
|
||||
"z": 0,
|
||||
"id": "d0c4fdb2e11cc825e7f05f9dc88a0be1",
|
||||
"units": "mm",
|
||||
"speckle_type": "Objects.Geometry.Point",
|
||||
"applicationId": null
|
||||
},
|
||||
"units": "mm",
|
||||
"domain": {
|
||||
"id": "3b97feaad2dbcc2d894c9cec024a9bf2",
|
||||
"end": 17.388451443569522,
|
||||
"start": -3.552713668866051e-14,
|
||||
"speckle_type": "Objects.Primitive.Interval",
|
||||
"applicationId": null
|
||||
},
|
||||
"length": 5300.000000000002,
|
||||
"speckle_type": "Objects.Geometry.Line",
|
||||
"applicationId": null
|
||||
},
|
||||
"topLevel": {
|
||||
"name": "1FL",
|
||||
"units": "mm",
|
||||
"elevation": 0
|
||||
},
|
||||
"__closure": {
|
||||
"0ad4db1fe261a5b640ad9f315a46a6fc": 100,
|
||||
"0d5518a7a3e63fe345e198ad5d6acc4e": 100,
|
||||
"ec2040af4bd9c8619f9029a43df61a2e": 100
|
||||
},
|
||||
"elementId": "4479852",
|
||||
"worksetId": "0",
|
||||
"properties": {
|
||||
"Parameters": {
|
||||
"Type Parameters": {
|
||||
"Text": {
|
||||
"符号": {
|
||||
"name": "符号",
|
||||
"value": "W30",
|
||||
"internalDefinitionName": "ee1f33e1-5506-4a64-b87b-7b98d30aea52"
|
||||
}
|
||||
},
|
||||
"Other": {
|
||||
"Family Name": {
|
||||
"name": "Family Name",
|
||||
"value": "Basic Wall",
|
||||
"internalDefinitionName": "SYMBOL_FAMILY_NAME_PARAM"
|
||||
},
|
||||
"横筋ピッチ": {
|
||||
"name": "横筋ピッチ",
|
||||
"units": "General",
|
||||
"value": 0,
|
||||
"internalDefinitionName": "cd19c040-0788-4424-8a67-7127427e311f"
|
||||
},
|
||||
"縦筋ピッチ": {
|
||||
"name": "縦筋ピッチ",
|
||||
"units": "General",
|
||||
"value": 0,
|
||||
"internalDefinitionName": "6796d143-ef09-4441-9ecc-3411fe837a65"
|
||||
}
|
||||
},
|
||||
"Rebar Set": {
|
||||
"横筋 主筋径1": {
|
||||
"name": "横筋 主筋径1",
|
||||
"value": "D13",
|
||||
"internalDefinitionName": "横筋 主筋径1"
|
||||
},
|
||||
"横筋 主筋径2": {
|
||||
"name": "横筋 主筋径2",
|
||||
"value": "D16",
|
||||
"internalDefinitionName": "横筋 主筋径2"
|
||||
},
|
||||
"縦筋 主筋径1": {
|
||||
"name": "縦筋 主筋径1",
|
||||
"value": "D13",
|
||||
"internalDefinitionName": "縦筋 主筋径1"
|
||||
},
|
||||
"縦筋 主筋径2": {
|
||||
"name": "縦筋 主筋径2",
|
||||
"value": "D16",
|
||||
"internalDefinitionName": "縦筋 主筋径2"
|
||||
},
|
||||
"横筋 主筋ピッチ": {
|
||||
"name": "横筋 主筋ピッチ",
|
||||
"value": "200",
|
||||
"internalDefinitionName": "横筋 主筋ピッチ"
|
||||
},
|
||||
"縦筋 主筋ピッチ": {
|
||||
"name": "縦筋 主筋ピッチ",
|
||||
"value": "200",
|
||||
"internalDefinitionName": "縦筋 主筋ピッチ"
|
||||
},
|
||||
"開口補強筋 斜筋 本数": {
|
||||
"name": "開口補強筋 斜筋 本数",
|
||||
"value": "―",
|
||||
"internalDefinitionName": "開口補強筋 斜筋 本数"
|
||||
},
|
||||
"開口補強筋 横筋 本数": {
|
||||
"name": "開口補強筋 横筋 本数",
|
||||
"value": "―",
|
||||
"internalDefinitionName": "開口補強筋 横筋 本数"
|
||||
},
|
||||
"開口補強筋 縦筋 本数": {
|
||||
"name": "開口補強筋 縦筋 本数",
|
||||
"value": "―",
|
||||
"internalDefinitionName": "開口補強筋 縦筋 本数"
|
||||
}
|
||||
},
|
||||
"Structure": {
|
||||
"Fc24 (0)": {
|
||||
"units": "mm",
|
||||
"function": "Structure",
|
||||
"material": "Fc24",
|
||||
"thickness": 300
|
||||
}
|
||||
},
|
||||
"Construction": {
|
||||
"Width": {
|
||||
"name": "Width",
|
||||
"units": "Millimeters",
|
||||
"value": 300,
|
||||
"internalDefinitionName": "WALL_ATTR_WIDTH_PARAM"
|
||||
},
|
||||
"Function": {
|
||||
"name": "Function",
|
||||
"value": "Interior",
|
||||
"internalDefinitionName": "FUNCTION_PARAM"
|
||||
},
|
||||
"Wrapping at Ends": {
|
||||
"name": "Wrapping at Ends",
|
||||
"value": "None",
|
||||
"internalDefinitionName": "WRAPPING_AT_ENDS_PARAM"
|
||||
},
|
||||
"Wrapping at Inserts": {
|
||||
"name": "Wrapping at Inserts",
|
||||
"value": "Do not wrap",
|
||||
"internalDefinitionName": "WRAPPING_AT_INSERTS_PARAM"
|
||||
}
|
||||
},
|
||||
"Identity Data": {
|
||||
"Cost": {
|
||||
"name": "Cost",
|
||||
"units": "Currency",
|
||||
"value": 0,
|
||||
"internalDefinitionName": "ALL_MODEL_COST"
|
||||
},
|
||||
"Type Name": {
|
||||
"name": "Type Name",
|
||||
"value": "W30(Fc24)",
|
||||
"internalDefinitionName": "SYMBOL_NAME_PARAM"
|
||||
}
|
||||
},
|
||||
"IFC Parameters": {
|
||||
"Type IfcGUID": {
|
||||
"name": "Type IfcGUID",
|
||||
"value": "0oqavz9$zDZxycDyfZdYh0",
|
||||
"internalDefinitionName": "IFC_TYPE_GUID"
|
||||
},
|
||||
"Export Type to IFC": {
|
||||
"name": "Export Type to IFC",
|
||||
"value": "Default",
|
||||
"internalDefinitionName": "IFC_EXPORT_ELEMENT_TYPE"
|
||||
}
|
||||
},
|
||||
"Analytical Properties": {
|
||||
"Roughness": {
|
||||
"name": "Roughness",
|
||||
"value": 1,
|
||||
"internalDefinitionName": "ANALYTICAL_ROUGHNESS"
|
||||
},
|
||||
"Absorptance": {
|
||||
"name": "Absorptance",
|
||||
"units": "General",
|
||||
"value": 0.1,
|
||||
"internalDefinitionName": "ANALYTICAL_ABSORPTANCE"
|
||||
},
|
||||
"Thermal Mass": {
|
||||
"name": "Thermal Mass",
|
||||
"units": "Kilojoules per square meter Kelvin",
|
||||
"value": 0,
|
||||
"internalDefinitionName": "ANALYTICAL_THERMAL_MASS"
|
||||
},
|
||||
"Thermal Resistance (R)": {
|
||||
"name": "Thermal Resistance (R)",
|
||||
"units": "Square meter kelvins per watt",
|
||||
"value": 0,
|
||||
"internalDefinitionName": "ANALYTICAL_THERMAL_RESISTANCE"
|
||||
},
|
||||
"Heat Transfer Coefficient (U)": {
|
||||
"name": "Heat Transfer Coefficient (U)",
|
||||
"units": "Watts per square meter kelvin",
|
||||
"value": 0,
|
||||
"internalDefinitionName": "ANALYTICAL_HEAT_TRANSFER_COEFFICIENT"
|
||||
}
|
||||
},
|
||||
"Materials and Finishes": {
|
||||
"Structural Material": {
|
||||
"name": "Structural Material",
|
||||
"value": "Fc24",
|
||||
"internalDefinitionName": "STRUCTURAL_MATERIAL_PARAM"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Instance Parameters": {
|
||||
"Other": {
|
||||
"Type": {
|
||||
"name": "Type",
|
||||
"value": "W30(Fc24)",
|
||||
"internalDefinitionName": "ELEM_TYPE_PARAM"
|
||||
},
|
||||
"Family": {
|
||||
"name": "Family",
|
||||
"value": "Basic Wall",
|
||||
"internalDefinitionName": "ELEM_FAMILY_PARAM"
|
||||
},
|
||||
"Type Id": {
|
||||
"name": "Type Id",
|
||||
"value": "Basic Wall W30(Fc24)",
|
||||
"internalDefinitionName": "SYMBOL_ID_PARAM"
|
||||
},
|
||||
"Family and Type": {
|
||||
"name": "Family and Type",
|
||||
"value": "Basic Wall W30(Fc24)",
|
||||
"internalDefinitionName": "ELEM_FAMILY_AND_TYPE_PARAM"
|
||||
}
|
||||
},
|
||||
"Phasing": {
|
||||
"Phase Created": {
|
||||
"name": "Phase Created",
|
||||
"value": "フェーズ1",
|
||||
"internalDefinitionName": "PHASE_CREATED"
|
||||
}
|
||||
},
|
||||
"Dimensions": {
|
||||
"Area": {
|
||||
"name": "Area",
|
||||
"units": "Square meters",
|
||||
"value": 7.630000000000015,
|
||||
"internalDefinitionName": "HOST_AREA_COMPUTED"
|
||||
},
|
||||
"Length": {
|
||||
"name": "Length",
|
||||
"units": "Millimeters",
|
||||
"value": 5300.000000000001,
|
||||
"internalDefinitionName": "CURVE_ELEM_LENGTH"
|
||||
},
|
||||
"Volume": {
|
||||
"name": "Volume",
|
||||
"units": "Cubic meters",
|
||||
"value": 2.2890000000000135,
|
||||
"internalDefinitionName": "HOST_VOLUME_COMPUTED"
|
||||
}
|
||||
},
|
||||
"Structural": {
|
||||
"Structural": {
|
||||
"name": "Structural",
|
||||
"value": "Yes",
|
||||
"internalDefinitionName": "WALL_STRUCTURAL_SIGNIFICANT"
|
||||
},
|
||||
"Structural Usage": {
|
||||
"name": "Structural Usage",
|
||||
"value": "Bearing",
|
||||
"internalDefinitionName": "WALL_STRUCTURAL_USAGE_PARAM"
|
||||
},
|
||||
"Rebar Cover - Other Faces": {
|
||||
"name": "Rebar Cover - Other Faces",
|
||||
"value": "Rebar Cover Settings 内壁(フレーム、柱、および耐力壁)",
|
||||
"internalDefinitionName": "CLEAR_COVER_OTHER"
|
||||
},
|
||||
"Rebar Cover - Exterior Face": {
|
||||
"name": "Rebar Cover - Exterior Face",
|
||||
"value": "Rebar Cover Settings 内壁(フレーム、柱、および耐力壁)",
|
||||
"internalDefinitionName": "CLEAR_COVER_EXTERIOR"
|
||||
},
|
||||
"Rebar Cover - Interior Face": {
|
||||
"name": "Rebar Cover - Interior Face",
|
||||
"value": "Rebar Cover Settings 内壁(フレーム、柱、および耐力壁)",
|
||||
"internalDefinitionName": "CLEAR_COVER_INTERIOR"
|
||||
},
|
||||
"構造スリット 下端": {
|
||||
"name": "構造スリット 下端",
|
||||
"value": "No",
|
||||
"internalDefinitionName": "0560d1bb-007b-45e4-b8c3-87f9c6a3d73c"
|
||||
},
|
||||
"構造スリット 始端": {
|
||||
"name": "構造スリット 始端",
|
||||
"value": "No",
|
||||
"internalDefinitionName": "60795a90-8c91-4e11-90a0-b77b819c2403"
|
||||
},
|
||||
"構造スリット 終端": {
|
||||
"name": "構造スリット 終端",
|
||||
"value": "No",
|
||||
"internalDefinitionName": "e3223f14-6158-4d4f-8697-0de2bcc7f737"
|
||||
}
|
||||
},
|
||||
"Constraints": {
|
||||
"Top Offset": {
|
||||
"name": "Top Offset",
|
||||
"units": "Millimeters",
|
||||
"value": -600,
|
||||
"internalDefinitionName": "WALL_TOP_OFFSET"
|
||||
},
|
||||
"Base Offset": {
|
||||
"name": "Base Offset",
|
||||
"units": "Millimeters",
|
||||
"value": -2000,
|
||||
"internalDefinitionName": "WALL_BASE_OFFSET"
|
||||
},
|
||||
"Location Line": {
|
||||
"name": "Location Line",
|
||||
"value": "Core Centerline",
|
||||
"internalDefinitionName": "WALL_KEY_REF_PARAM"
|
||||
},
|
||||
"Room Bounding": {
|
||||
"name": "Room Bounding",
|
||||
"value": "Yes",
|
||||
"internalDefinitionName": "WALL_ATTR_ROOM_BOUNDING"
|
||||
},
|
||||
"Top Constraint": {
|
||||
"name": "Top Constraint",
|
||||
"value": "1FL",
|
||||
"internalDefinitionName": "WALL_HEIGHT_TYPE"
|
||||
},
|
||||
"Base Constraint": {
|
||||
"name": "Base Constraint",
|
||||
"value": "1FL",
|
||||
"internalDefinitionName": "WALL_BASE_CONSTRAINT"
|
||||
},
|
||||
"Related to Mass": {
|
||||
"name": "Related to Mass",
|
||||
"value": "No",
|
||||
"internalDefinitionName": "RELATED_TO_MASS"
|
||||
},
|
||||
"Top is Attached": {
|
||||
"name": "Top is Attached",
|
||||
"value": "No",
|
||||
"internalDefinitionName": "WALL_TOP_IS_ATTACHED"
|
||||
},
|
||||
"Base is Attached": {
|
||||
"name": "Base is Attached",
|
||||
"value": "No",
|
||||
"internalDefinitionName": "WALL_BOTTOM_IS_ATTACHED"
|
||||
},
|
||||
"Unconnected Height": {
|
||||
"name": "Unconnected Height",
|
||||
"units": "Millimeters",
|
||||
"value": 1400,
|
||||
"internalDefinitionName": "WALL_USER_HEIGHT_PARAM"
|
||||
},
|
||||
"Top Extension Distance": {
|
||||
"name": "Top Extension Distance",
|
||||
"units": "Millimeters",
|
||||
"value": 0,
|
||||
"internalDefinitionName": "WALL_TOP_EXTENSION_DIST_PARAM"
|
||||
},
|
||||
"Base Extension Distance": {
|
||||
"name": "Base Extension Distance",
|
||||
"units": "Millimeters",
|
||||
"value": 0,
|
||||
"internalDefinitionName": "WALL_BOTTOM_EXTENSION_DIST_PARAM"
|
||||
}
|
||||
},
|
||||
"Identity Data": {
|
||||
"Has Association": {
|
||||
"name": "Has Association",
|
||||
"value": "Yes",
|
||||
"internalDefinitionName": "ANALYTICAL_ELEMENT_HAS_ASSOCIATION"
|
||||
},
|
||||
"SPECKLE_Classification": {
|
||||
"name": "SPECKLE_Classification",
|
||||
"value": "Wall",
|
||||
"internalDefinitionName": "SPECKLE_Classification"
|
||||
}
|
||||
},
|
||||
"IFC Parameters": {
|
||||
"IfcGUID": {
|
||||
"name": "IfcGUID",
|
||||
"value": "0oqavz9$zDZxycDyfZdYsW",
|
||||
"internalDefinitionName": "IFC_GUID"
|
||||
},
|
||||
"Export to IFC": {
|
||||
"name": "Export to IFC",
|
||||
"value": "By Type",
|
||||
"internalDefinitionName": "IFC_EXPORT_ELEMENT"
|
||||
}
|
||||
},
|
||||
"Cross-Section Definition": {
|
||||
"Cross-Section": {
|
||||
"name": "Cross-Section",
|
||||
"value": "Vertical",
|
||||
"internalDefinitionName": "WALL_CROSS_SECTION"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Material Quantities": {
|
||||
"Fc24": {
|
||||
"area": 7630000.000000016,
|
||||
"units": "mm",
|
||||
"volume": 2289000000.0000134,
|
||||
"materialName": "Fc24",
|
||||
"materialClass": "コンクリート",
|
||||
"materialCategory": "コンクリート"
|
||||
}
|
||||
}
|
||||
},
|
||||
"worksetName": "ワークセット1",
|
||||
"displayValue": [
|
||||
{
|
||||
"__closure": null,
|
||||
"referencedId": "ec2040af4bd9c8619f9029a43df61a2e",
|
||||
"speckle_type": "reference"
|
||||
}
|
||||
],
|
||||
"isStructural": true,
|
||||
"speckle_type": "Objects.Data.DataObject:Objects.Data.RevitObject",
|
||||
"applicationId": "32d24e7d-27ff-4d8f-bf26-37ca63da76cc-00445b6c",
|
||||
"builtInCategory": "OST_Walls",
|
||||
"totalChildrenCount": null
|
||||
},
|
||||
{
|
||||
"id": "0ad4db1fe261a5b640ad9f315a46a6fc",
|
||||
"data": [
|
||||
3,
|
||||
5,
|
||||
0,
|
||||
1,
|
||||
3,
|
||||
5,
|
||||
1,
|
||||
4,
|
||||
3,
|
||||
4,
|
||||
2,
|
||||
3,
|
||||
3,
|
||||
2,
|
||||
4,
|
||||
1,
|
||||
3,
|
||||
11,
|
||||
12,
|
||||
9,
|
||||
3,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
3,
|
||||
12,
|
||||
13,
|
||||
9,
|
||||
3,
|
||||
13,
|
||||
6,
|
||||
8,
|
||||
3,
|
||||
7,
|
||||
8,
|
||||
6,
|
||||
3,
|
||||
13,
|
||||
8,
|
||||
9,
|
||||
3,
|
||||
15,
|
||||
16,
|
||||
18,
|
||||
3,
|
||||
14,
|
||||
15,
|
||||
19,
|
||||
3,
|
||||
17,
|
||||
18,
|
||||
16,
|
||||
3,
|
||||
19,
|
||||
20,
|
||||
14,
|
||||
3,
|
||||
18,
|
||||
19,
|
||||
15,
|
||||
3,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
3,
|
||||
23,
|
||||
24,
|
||||
21,
|
||||
3,
|
||||
26,
|
||||
27,
|
||||
29,
|
||||
3,
|
||||
25,
|
||||
26,
|
||||
30,
|
||||
3,
|
||||
28,
|
||||
29,
|
||||
27,
|
||||
3,
|
||||
30,
|
||||
31,
|
||||
25,
|
||||
3,
|
||||
29,
|
||||
30,
|
||||
26,
|
||||
3,
|
||||
32,
|
||||
33,
|
||||
34,
|
||||
3,
|
||||
34,
|
||||
35,
|
||||
32
|
||||
],
|
||||
"speckle_type": "Speckle.Core.Models.DataChunk",
|
||||
"applicationId": null
|
||||
},
|
||||
{
|
||||
"id": "0d5518a7a3e63fe345e198ad5d6acc4e",
|
||||
"data": [
|
||||
22550.00031738281,
|
||||
15049.999969482422,
|
||||
-2000.000015258789,
|
||||
22550.00031738281,
|
||||
20274.999645996093,
|
||||
-2000.000015258789,
|
||||
22550.00031738281,
|
||||
20499.999865722657,
|
||||
-2000.000015258789,
|
||||
22550.00031738281,
|
||||
20499.999865722657,
|
||||
-600.0000045776368,
|
||||
22550.00031738281,
|
||||
20274.999645996093,
|
||||
-600.0000045776368,
|
||||
22550.00031738281,
|
||||
15049.999969482422,
|
||||
-600.0000045776368,
|
||||
22250.000024414065,
|
||||
15049.999969482422,
|
||||
-2000.000015258789,
|
||||
22250.000024414065,
|
||||
15049.999969482422,
|
||||
-600.0000045776368,
|
||||
22250.000024414065,
|
||||
15350.000262451173,
|
||||
-600.0000045776368,
|
||||
22250.000024414065,
|
||||
20274.999645996093,
|
||||
-600.0000045776368,
|
||||
22250.000024414065,
|
||||
20499.999865722657,
|
||||
-600.0000045776368,
|
||||
22250.000024414065,
|
||||
20499.999865722657,
|
||||
-2000.000015258789,
|
||||
22250.000024414065,
|
||||
20274.999645996093,
|
||||
-2000.000015258789,
|
||||
22250.000024414065,
|
||||
15350.000262451173,
|
||||
-2000.000015258789,
|
||||
22550.00031738281,
|
||||
20499.999865722657,
|
||||
-2000.000015258789,
|
||||
22550.00031738281,
|
||||
20274.999645996093,
|
||||
-2000.000015258789,
|
||||
22550.00031738281,
|
||||
15049.999969482422,
|
||||
-2000.000015258789,
|
||||
22250.000024414065,
|
||||
15049.999969482422,
|
||||
-2000.000015258789,
|
||||
22250.000024414065,
|
||||
15350.000262451173,
|
||||
-2000.000015258789,
|
||||
22250.000024414065,
|
||||
20274.999645996093,
|
||||
-2000.000015258789,
|
||||
22250.000024414065,
|
||||
20499.999865722657,
|
||||
-2000.000015258789,
|
||||
22550.00031738281,
|
||||
20499.999865722657,
|
||||
-2000.000015258789,
|
||||
22250.000024414065,
|
||||
20499.999865722657,
|
||||
-2000.000015258789,
|
||||
22250.000024414065,
|
||||
20499.999865722657,
|
||||
-600.0000045776368,
|
||||
22550.00031738281,
|
||||
20499.999865722657,
|
||||
-600.0000045776368,
|
||||
22550.00031738281,
|
||||
15049.999969482422,
|
||||
-600.0000045776368,
|
||||
22550.00031738281,
|
||||
20274.999645996093,
|
||||
-600.0000045776368,
|
||||
22550.00031738281,
|
||||
20499.999865722657,
|
||||
-600.0000045776368,
|
||||
22250.000024414065,
|
||||
20499.999865722657,
|
||||
-600.0000045776368,
|
||||
22250.000024414065,
|
||||
20274.999645996093,
|
||||
-600.0000045776368,
|
||||
22250.000024414065,
|
||||
15350.000262451173,
|
||||
-600.0000045776368,
|
||||
22250.000024414065,
|
||||
15049.999969482422,
|
||||
-600.0000045776368,
|
||||
22550.00031738281,
|
||||
15049.999969482422,
|
||||
-600.0000045776368,
|
||||
22250.000024414065,
|
||||
15049.999969482422,
|
||||
-600.0000045776368,
|
||||
22250.000024414065,
|
||||
15049.999969482422,
|
||||
-2000.000015258789,
|
||||
22550.00031738281,
|
||||
15049.999969482422,
|
||||
-2000.000015258789
|
||||
],
|
||||
"speckle_type": "Speckle.Core.Models.DataChunk",
|
||||
"applicationId": null
|
||||
},
|
||||
{
|
||||
"id": "ec2040af4bd9c8619f9029a43df61a2e",
|
||||
"area": 0,
|
||||
"bbox": null,
|
||||
"faces": [
|
||||
{
|
||||
"__closure": null,
|
||||
"referencedId": "0ad4db1fe261a5b640ad9f315a46a6fc",
|
||||
"speckle_type": "reference"
|
||||
}
|
||||
],
|
||||
"units": "mm",
|
||||
"colors": [],
|
||||
"volume": 0,
|
||||
"vertices": [
|
||||
{
|
||||
"__closure": null,
|
||||
"referencedId": "0d5518a7a3e63fe345e198ad5d6acc4e",
|
||||
"speckle_type": "reference"
|
||||
}
|
||||
],
|
||||
"__closure": {
|
||||
"0ad4db1fe261a5b640ad9f315a46a6fc": 100,
|
||||
"0d5518a7a3e63fe345e198ad5d6acc4e": 100
|
||||
},
|
||||
"speckle_type": "Objects.Geometry.Mesh",
|
||||
"applicationId": "d2c33253-d0ed-4682-9633-e083daaa87db",
|
||||
"textureCoordinates": []
|
||||
}
|
||||
]
|
||||
+126
-18
@@ -1,24 +1,132 @@
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
import pytest
|
||||
from specklepy.objects.base import Base
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
load_dotenv(dotenv_path=".env")
|
||||
@pytest.fixture
|
||||
def v2_wall():
|
||||
"""Creates a v2-style Speckle wall object."""
|
||||
wall = Base()
|
||||
wall.id = "cdb18060dc48281909e94f0f1d8d3cc0"
|
||||
wall.type = "W30(Fc24)"
|
||||
wall.units = "mm"
|
||||
wall.family = "Basic Wall"
|
||||
wall.height = 1400
|
||||
wall.flipped = False
|
||||
wall.category = "Walls"
|
||||
wall.elementId = "4479852"
|
||||
wall.worksetId = "0"
|
||||
wall.structural = True
|
||||
wall.baseOffset = -2000
|
||||
wall.topOffset = -600
|
||||
|
||||
token_var = "SPECKLE_TOKEN"
|
||||
server_var = "SPECKLE_SERVER_URL"
|
||||
token = os.getenv(token_var)
|
||||
server = os.getenv(server_var)
|
||||
# Create base line geometry
|
||||
wall.baseLine = Base()
|
||||
wall.baseLine.start = Base()
|
||||
wall.baseLine.start.x = 22400.000000000007
|
||||
wall.baseLine.start.y = 15199.999999999998
|
||||
wall.baseLine.start.z = -2000.0000000000002
|
||||
wall.baseLine.end = Base()
|
||||
wall.baseLine.end.x = 22400.000000000015
|
||||
wall.baseLine.end.y = 20500
|
||||
wall.baseLine.end.z = -2000.0000000000002
|
||||
wall.baseLine.units = "mm"
|
||||
wall.baseLine.length = 5300.000000000002
|
||||
|
||||
if not token:
|
||||
raise ValueError(f"Cannot run tests without a {token_var} environment variable")
|
||||
# Create parameters structure
|
||||
wall.parameters = Base()
|
||||
|
||||
if not server:
|
||||
raise ValueError(
|
||||
f"Cannot run tests without a {server_var} environment variable"
|
||||
)
|
||||
# Standard parameter
|
||||
wall.parameters["WALL_ATTR_WIDTH_PARAM"] = Base()
|
||||
wall.parameters["WALL_ATTR_WIDTH_PARAM"].name = "Width"
|
||||
wall.parameters["WALL_ATTR_WIDTH_PARAM"].value = 300
|
||||
wall.parameters["WALL_ATTR_WIDTH_PARAM"].units = "mm"
|
||||
|
||||
# Set the token as an attribute on the config object
|
||||
config.SPECKLE_TOKEN = token
|
||||
config.SPECKLE_SERVER_URL = server
|
||||
# Parameter with GUID key
|
||||
wall.parameters["ee1f33e1-5506-4a64-b87b-7b98d30aea52"] = Base()
|
||||
wall.parameters["ee1f33e1-5506-4a64-b87b-7b98d30aea52"].name = "符号"
|
||||
wall.parameters["ee1f33e1-5506-4a64-b87b-7b98d30aea52"].value = "W30"
|
||||
wall.parameters["ee1f33e1-5506-4a64-b87b-7b98d30aea52"].isShared = True
|
||||
wall.parameters[
|
||||
"ee1f33e1-5506-4a64-b87b-7b98d30aea52"
|
||||
].internalDefinitionName = "ee1f33e1-5506-4a64-b87b-7b98d30aea52"
|
||||
|
||||
wall.parameters["STRUCTURAL_MATERIAL_PARAM"] = Base()
|
||||
wall.parameters["STRUCTURAL_MATERIAL_PARAM"].name = "Structural Material"
|
||||
wall.parameters["STRUCTURAL_MATERIAL_PARAM"].value = "Fc24"
|
||||
|
||||
# Create basic level reference
|
||||
wall.level = Base()
|
||||
wall.level.name = "1FL"
|
||||
wall.level.elevation = 0
|
||||
wall.level.units = "mm"
|
||||
|
||||
return wall
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def v3_wall():
|
||||
"""Creates a v3-style Speckle wall object."""
|
||||
wall = Base()
|
||||
wall.id = "46f06fef727d64a0bbcbd7ced51e0cd2"
|
||||
wall.name = "Walls - W30(Fc24)"
|
||||
wall.type = "W30(Fc24)"
|
||||
wall.units = "mm"
|
||||
wall.family = "Basic Wall"
|
||||
wall.flipped = False
|
||||
wall.category = "Walls"
|
||||
wall.elementId = "4479852"
|
||||
wall.worksetId = "0"
|
||||
|
||||
# Create location geometry
|
||||
wall.location = Base()
|
||||
wall.location.start = Base()
|
||||
wall.location.start.x = 22400.000000000007
|
||||
wall.location.start.y = 15199.999999999998
|
||||
wall.location.start.z = 0
|
||||
wall.location.end = Base()
|
||||
wall.location.end.x = 22400.000000000015
|
||||
wall.location.end.y = 20500
|
||||
wall.location.end.z = 0
|
||||
wall.location.units = "mm"
|
||||
wall.location.length = 5300.000000000002
|
||||
|
||||
# Create nested properties structure
|
||||
wall.properties = Base()
|
||||
wall.properties.Parameters = Base()
|
||||
|
||||
# Type Parameters
|
||||
wall.properties.Parameters["Type Parameters"] = Base()
|
||||
|
||||
# Add Text section with GUID parameter
|
||||
wall.properties.Parameters["Type Parameters"].Text = Base()
|
||||
wall.properties.Parameters["Type Parameters"].Text["符号"] = {
|
||||
"name": "符号",
|
||||
"value": "W30",
|
||||
"internalDefinitionName": "ee1f33e1-5506-4a64-b87b-7b98d30aea52",
|
||||
}
|
||||
|
||||
wall.properties.Parameters["Type Parameters"].Structure = Base()
|
||||
wall.properties.Parameters["Type Parameters"].Structure["Fc24 (0)"] = {
|
||||
"units": "mm",
|
||||
"function": "Structure",
|
||||
"material": "Fc24",
|
||||
"thickness": 300,
|
||||
}
|
||||
|
||||
# Instance Parameters
|
||||
wall.properties.Parameters["Instance Parameters"] = Base()
|
||||
wall.properties.Parameters["Instance Parameters"].Structural = Base()
|
||||
wall.properties.Parameters["Instance Parameters"].Structural.Structural = {"name": "Structural", "value": "Yes"}
|
||||
|
||||
# Create basic level references
|
||||
wall.level = Base()
|
||||
wall.level.name = "1FL"
|
||||
wall.level.elevation = 0
|
||||
wall.level.units = "mm"
|
||||
|
||||
wall.topLevel = Base()
|
||||
wall.topLevel.name = "1FL"
|
||||
wall.topLevel.elevation = 0
|
||||
wall.topLevel.units = "mm"
|
||||
|
||||
return wall
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
"""Run integration tests with a speckle server."""
|
||||
|
||||
from speckle_automate import (
|
||||
AutomationContext,
|
||||
AutomationRunData,
|
||||
AutomationStatus,
|
||||
run_function
|
||||
)
|
||||
|
||||
from src.inputs import FunctionInputs
|
||||
from src.function import automate_function
|
||||
|
||||
|
||||
def test_function_run(test_automation_run_data: AutomationRunData, test_automation_token: str):
|
||||
"""Run an integration test for the automate function."""
|
||||
automation_context = AutomationContext.initialize(
|
||||
test_automation_run_data, test_automation_token
|
||||
)
|
||||
default_url: str = (
|
||||
"https://docs.google.com/spreadsheets/d/e/2PACX-1vSFmjLfqxPKXJHg-wEs1cp_nJEJJhESGVTLCvWLG_"
|
||||
"IgIuRZ4CmMDCSceOYFvuo8IqcmT4sj9qPiLfCx/pub?gid=0&single=true&output=tsv"
|
||||
)
|
||||
|
||||
automate_sdk = run_function(
|
||||
automation_context,
|
||||
automate_function,
|
||||
FunctionInputs(spreadsheet_url=default_url),
|
||||
)
|
||||
|
||||
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
|
||||
|
||||
# cli command to run just this test with pytest: pytest tests/local_test_exercise2.py::test_function_run
|
||||
@@ -0,0 +1,86 @@
|
||||
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
|
||||
+30
-20
@@ -1,31 +1,41 @@
|
||||
"""Run integration tests with a speckle server."""
|
||||
|
||||
from pydantic import SecretStr
|
||||
|
||||
from speckle_automate import (
|
||||
AutomationContext,
|
||||
AutomationRunData,
|
||||
AutomationStatus,
|
||||
run_function
|
||||
run_function,
|
||||
)
|
||||
from speckle_automate.fixtures import * # noqa: F401, F403
|
||||
|
||||
from main import FunctionInputs, automate_function
|
||||
|
||||
from speckle_automate.fixtures import *
|
||||
from inputs import MinimumSeverity
|
||||
from src.function import automate_function
|
||||
from src.helpers import speckle_print
|
||||
from src.inputs import FunctionInputs
|
||||
|
||||
|
||||
def test_function_run(test_automation_run_data: AutomationRunData, test_automation_token: str):
|
||||
"""Run an integration test for the automate function."""
|
||||
automation_context = AutomationContext.initialize(
|
||||
test_automation_run_data, test_automation_token
|
||||
)
|
||||
automate_sdk = run_function(
|
||||
automation_context,
|
||||
automate_function,
|
||||
FunctionInputs(
|
||||
forbidden_speckle_type="None",
|
||||
whisper_message=SecretStr("testing automatically"),
|
||||
),
|
||||
)
|
||||
class TestFunction:
|
||||
"""Test suite for the automate function."""
|
||||
|
||||
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
|
||||
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.
|
||||
|
||||
"""
|
||||
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"
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,417 @@
|
||||
"""Test suite for parameter handling functionality."""
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from dotenv import load_dotenv
|
||||
from speckle_automate import AutomationContext, AutomationRunData # noqa: F401, F403
|
||||
|
||||
# from speckle_automate.fixtures import * # noqa: F401, F403
|
||||
from specklepy.api.client import SpeckleClient
|
||||
from specklepy.core.api import operations
|
||||
from specklepy.objects.base import Base
|
||||
from specklepy.transports.server import ServerTransport
|
||||
|
||||
from helpers import speckle_print
|
||||
from src.rules import PropertyRules
|
||||
|
||||
|
||||
class TestParameterHandling:
|
||||
"""Test suite for parameter handling functionality."""
|
||||
|
||||
@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")
|
||||
|
||||
client.authenticate_with_token(os.getenv("SPECKLE_TOKEN"))
|
||||
|
||||
transport = ServerTransport(client=client, stream_id=os.getenv("SPECKLE_PROJECT_ID"))
|
||||
|
||||
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
|
||||
|
||||
@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)
|
||||
|
||||
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"
|
||||
|
||||
# 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 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(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(
|
||||
"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(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(
|
||||
"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(
|
||||
"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(
|
||||
"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, 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, 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(
|
||||
"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(
|
||||
"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, substring, expected_result",
|
||||
[
|
||||
("speckle_type", "Revit", True), # Test basic substring match
|
||||
("speckle_type", "revit", True), # Test case-insensitive
|
||||
("speckle_type", "NotPresent", False), # Test no match
|
||||
("speckle_type", "", True), # Test empty string
|
||||
("non_existent", "anything", False), # Test non-existent parameter
|
||||
],
|
||||
)
|
||||
def test_parameter_value_contains(self, test_objects, param_name, substring, expected_result):
|
||||
"""Test substring matching on parameter values."""
|
||||
v2_obj, _ = test_objects
|
||||
assert PropertyRules.is_parameter_value_containing(v2_obj, param_name, substring) == expected_result
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param_name, substring, expected_result",
|
||||
[
|
||||
("speckle_type", "Revit", False), # Should fail as it does contain Revit
|
||||
("speckle_type", "NotPresent", True), # Should pass as it doesn't contain
|
||||
("speckle_type", "", False), # Should fail as empty string is contained
|
||||
("non_existent", "anything", True), # Should pass as non-existent can't contain
|
||||
],
|
||||
)
|
||||
def test_parameter_value_not_contains(self, test_objects, param_name, substring, expected_result):
|
||||
"""Test negative substring matching on parameter values."""
|
||||
v2_obj, _ = test_objects
|
||||
assert PropertyRules.is_parameter_value_not_containing(v2_obj, param_name, substring) == expected_result
|
||||
@@ -0,0 +1,36 @@
|
||||
"""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", "", ""],
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,33 @@
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from src.rule_processor import SeverityLevel, get_severity
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input_severity, expected_enum",
|
||||
[
|
||||
("INFO", SeverityLevel.INFO),
|
||||
("info", SeverityLevel.INFO),
|
||||
("Info", SeverityLevel.INFO),
|
||||
("WARNING", SeverityLevel.WARNING),
|
||||
("warning", SeverityLevel.WARNING),
|
||||
("Warning", SeverityLevel.WARNING),
|
||||
("ERROR", SeverityLevel.ERROR),
|
||||
("error", SeverityLevel.ERROR),
|
||||
("Error", SeverityLevel.ERROR),
|
||||
("WARN", SeverityLevel.WARNING),
|
||||
("warn", SeverityLevel.WARNING),
|
||||
("Critical", SeverityLevel.ERROR), # Invalid → Defaults to ERROR
|
||||
("Severe", SeverityLevel.ERROR), # Invalid → Defaults to ERROR
|
||||
("", SeverityLevel.ERROR), # Empty string → Defaults to ERROR
|
||||
(None, SeverityLevel.ERROR), # None → Defaults to ERROR
|
||||
(1.0, SeverityLevel.ERROR), # None → Defaults to ERROR
|
||||
],
|
||||
)
|
||||
def test_severity_conversion(input_severity, expected_enum):
|
||||
"""Test various user inputs for severity and check expected outputs."""
|
||||
rule_info = pd.Series({"Report Severity": input_severity})
|
||||
severity = get_severity(rule_info)
|
||||
|
||||
assert severity == expected_enum, f"Failed for input: {input_severity}"
|
||||
Reference in New Issue
Block a user