Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5fb871d76f | |||
| af8352933b |
@@ -0,0 +1,30 @@
|
|||||||
|
# Use the official Python 3.13 slim image as the base
|
||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
# Change to UK mirror for better reliability (robust for missing files)
|
||||||
|
RUN find /etc/apt/ -name '*.list' -exec sed -i 's|http://deb.debian.org|http://ftp.uk.debian.org|g' {} + || true
|
||||||
|
|
||||||
|
# Force apt to use IPv4 to avoid CDN/network issues
|
||||||
|
RUN echo 'Acquire::ForceIPv4 "true";' > /etc/apt/apt.conf.d/99force-ipv4
|
||||||
|
|
||||||
|
# Install system dependencies
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /home/speckle
|
||||||
|
|
||||||
|
# Create a non-root user
|
||||||
|
RUN useradd -ms /bin/bash vscode
|
||||||
|
USER vscode
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONPATH=/home/speckle
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
COPY requirements.txt requirements-dev.txt pyproject.toml ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt && \
|
||||||
|
pip install --no-cache-dir -r requirements-dev.txt && \
|
||||||
|
echo 'export PATH=$PATH:$HOME/.local/bin' >> ~/.bashrc
|
||||||
@@ -1,43 +1,49 @@
|
|||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
|
||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/python
|
|
||||||
{
|
{
|
||||||
"name": "Python 3",
|
"name": "Model Checker - An Automate Function",
|
||||||
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
"dockerFile": "Dockerfile",
|
||||||
"image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye",
|
"context": "..",
|
||||||
"features": {
|
"workspaceFolder": "/home/speckle",
|
||||||
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
|
"runArgs": [
|
||||||
},
|
"--network",
|
||||||
|
"host"
|
||||||
"remoteEnv": {
|
],
|
||||||
"SPECKLE_TOKEN": "foobar"
|
"mounts": [
|
||||||
},
|
"source=${localWorkspaceFolder},target=/home/speckle,type=bind,consistency=cached"
|
||||||
"containerEnv": {
|
],
|
||||||
"SPECKLE_TOKEN": "asdfasdf"
|
|
||||||
},
|
|
||||||
|
|
||||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
|
||||||
// "features": {},
|
|
||||||
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
|
||||||
// "forwardPorts": [],
|
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
|
||||||
"postCreateCommand": "cp .env.example .env && POETRY_VIRTUALENVS_IN_PROJECT=true poetry install --no-root",
|
|
||||||
|
|
||||||
// Configure tool-specific properties.
|
|
||||||
"customizations": {
|
"customizations": {
|
||||||
"vscode": {
|
"vscode": {
|
||||||
// Add the IDs of extensions you want installed when the container is created.
|
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"ms-python.vscode-pylance",
|
|
||||||
"ms-python.python",
|
"ms-python.python",
|
||||||
|
"ms-python.vscode-pylance",
|
||||||
"ms-python.black-formatter",
|
"ms-python.black-formatter",
|
||||||
"streetsidesoftware.code-spell-checker",
|
"ms-python.isort",
|
||||||
"mikestead.dotenv"
|
"ms-python.flake8",
|
||||||
]
|
"littlefoxteam.vscode-python-test-adapter",
|
||||||
|
"ms-azuretools.vscode-docker",
|
||||||
|
"charliermarsh.ruff"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"python.defaultInterpreterPath": "/usr/local/bin/python",
|
||||||
|
"python.linting.enabled": true,
|
||||||
|
"python.linting.flake8Enabled": true,
|
||||||
|
"python.formatting.provider": "black",
|
||||||
|
"python.testing.pytestEnabled": true,
|
||||||
|
"python.testing.unittestEnabled": false,
|
||||||
|
"python.testing.nosetestsEnabled": false,
|
||||||
|
"python.testing.pytestArgs": [
|
||||||
|
"tests"
|
||||||
|
],
|
||||||
|
"python.testing.autoTestDiscoverOnSaveEnabled": true,
|
||||||
|
"python.testing.cwd": "${workspaceFolder}",
|
||||||
|
"[python]": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"postCreateCommand": "sh -c \"mkdir -p ~/.pip && echo '[global]\nprefer-ipv4 = true' > ~/.pip/pip.conf\"",
|
||||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
"postStartCommand": "echo 'Container started successfully!'"
|
||||||
// "remoteUser": "root"
|
}
|
||||||
}
|
|
||||||
Vendored
+24
-2
@@ -5,5 +5,27 @@
|
|||||||
"stringcase",
|
"stringcase",
|
||||||
"typer"
|
"typer"
|
||||||
],
|
],
|
||||||
"python.defaultInterpreterPath": ".venv/bin/python"
|
"python.defaultInterpreterPath": ".venv/bin/python",
|
||||||
}
|
"python.testing.pytestEnabled": true,
|
||||||
|
"python.testing.unittestEnabled": false,
|
||||||
|
"python.testing.nosetestsEnabled": false,
|
||||||
|
"python.testing.pytestArgs": [
|
||||||
|
"tests"
|
||||||
|
],
|
||||||
|
"python.testing.autoTestDiscoverOnSaveEnabled": true,
|
||||||
|
"python.testing.cwd": "${workspaceFolder}",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.rulers": [
|
||||||
|
79
|
||||||
|
],
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "explicit"
|
||||||
|
},
|
||||||
|
"[python]": {
|
||||||
|
"editor.defaultFormatter": "charliermarsh.ruff",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports.ruff": "explicit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
# Use the official Python 3.11 slim image as the base
|
# Use the official Python 3.13 slim image as the base
|
||||||
FROM python:3.11-slim
|
FROM python:3.13-slim
|
||||||
|
|
||||||
# Set the working directory inside the container
|
# Set the working directory inside the container
|
||||||
WORKDIR /home/speckle
|
WORKDIR /home/speckle
|
||||||
@@ -9,7 +9,7 @@ COPY . /home/speckle
|
|||||||
|
|
||||||
# Upgrade pip and install dependencies using requirements.txt
|
# Upgrade pip and install dependencies using requirements.txt
|
||||||
RUN pip install --no-cache-dir --upgrade pip && \
|
RUN pip install --no-cache-dir --upgrade pip && \
|
||||||
pip install --no-cache-dir -r /home/speckle/requirements.txt
|
pip install --no-cache-dir -r /home/speckle/requirements.txt
|
||||||
|
|
||||||
# Set the entrypoint for running the Speckle function
|
# Set the entrypoint for running the Speckle function
|
||||||
CMD ["python", "-u", "main.py", "run"]
|
CMD ["python", "-u", "main.py", "run"]
|
||||||
|
|||||||
+42
-21
@@ -1,36 +1,57 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "speckle-automate-checker"
|
name = "speckle-automate-checker"
|
||||||
version = "0.1.0"
|
version = "3.0.0"
|
||||||
description = "Allows for QAQC property checking with Speckle"
|
description = "Allows for QAQC property checking with Speckle"
|
||||||
authors = ["Jonathon Broughton <jonathon@speckle.systems>"]
|
authors = [{ name = "Jonathon Broughton", email = "jonathon@speckle.systems" }]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"more-itertools>=10.6.0",
|
"more-itertools>=10.6.0",
|
||||||
"pandas>=2.2.3",
|
"pandas>=2.2.3",
|
||||||
"pydantic==2.10.6",
|
"pydantic==2.10.6",
|
||||||
"python-dotenv>=1.0.1",
|
"python-dotenv>=1.0.1",
|
||||||
"python-levenshtein>=0.26.1",
|
"python-levenshtein>=0.26.1",
|
||||||
"specklepy>=2.21.3",
|
"specklepy>=3.0.0",
|
||||||
"pytest-assertcount>=1.0.0",
|
"pydantic-settings>=2.7.1",
|
||||||
"black>=25.1.0",
|
|
||||||
"mypy>=1.15.0",
|
|
||||||
"pydantic-settings>=2.7.1",
|
|
||||||
"pytest>=8.3.4",
|
|
||||||
"ruff>=0.9.6",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = [
|
||||||
|
"mypy>=1.15.0",
|
||||||
|
"pytest>=8.3.4",
|
||||||
|
"pytest-assertcount>=1.0.0",
|
||||||
|
"ruff==0.11.12",
|
||||||
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
select = [
|
select = [
|
||||||
"E", # pycodestyle
|
"E", # pycodestyle
|
||||||
"F", # pyflakes
|
"F", # pyflakes
|
||||||
"UP", # pyupgrade
|
"UP", # pyupgrade
|
||||||
"D", # pydocstyle
|
"D", # pydocstyle
|
||||||
"I", # isort
|
"I", # isort
|
||||||
]
|
]
|
||||||
line-length = 120
|
ignore = ["F401", "F403", "E501"]
|
||||||
ignore = ["F401", "F403"]
|
exclude = [".venv", "**/*.yml"]
|
||||||
|
line-length = 79
|
||||||
|
|
||||||
[tool.ruff.pydocstyle]
|
[tool.ruff.pydocstyle]
|
||||||
convention = "google"
|
convention = "google"
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
quote-style = "double"
|
||||||
|
indent-style = "space"
|
||||||
|
line-ending = "auto"
|
||||||
|
docstring-code-format = true
|
||||||
|
docstring-code-line-length = 79
|
||||||
|
|
||||||
|
[tool.ruff.isort]
|
||||||
|
known-first-party = ["src"]
|
||||||
|
|
||||||
|
|
||||||
|
[tool.ruff.lint.pydocstyle]
|
||||||
|
convention = "google"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
py-modules = []
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
argcomplete==3.6.2
|
||||||
|
click==8.1.8
|
||||||
|
colorama==0.4.6
|
||||||
|
coverage==7.8.2
|
||||||
|
flake8==7.2.0
|
||||||
|
iniconfig==2.1.0
|
||||||
|
isort==6.0.1
|
||||||
|
mccabe==0.7.0
|
||||||
|
mypy_extensions==1.1.0
|
||||||
|
packaging==24.2
|
||||||
|
pathspec==0.12.1
|
||||||
|
pipx==1.7.1
|
||||||
|
platformdirs==4.3.7
|
||||||
|
pluggy==1.6.0
|
||||||
|
pycodestyle==2.13.0
|
||||||
|
pyflakes==3.3.2
|
||||||
|
Pygments==2.19.1
|
||||||
|
pytest>=8.3.4
|
||||||
|
pytest-assertcount>=1.0.0
|
||||||
|
pytest-cov==6.1.1
|
||||||
|
ruff==0.11.12
|
||||||
|
userpath==1.9.2
|
||||||
|
mypy>=1.15.0
|
||||||
+6
-53
@@ -1,54 +1,7 @@
|
|||||||
annotated-types==0.7.0
|
more-itertools>=10.6.0
|
||||||
anyio==4.8.0
|
pandas>=2.2.3
|
||||||
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==2.10.6
|
||||||
pydantic-core==2.27.2
|
python-dotenv>=1.0.1
|
||||||
pydantic-settings==2.7.1
|
python-levenshtein>=0.26.1
|
||||||
pytest==8.3.4
|
specklepy>=3.0.0
|
||||||
pytest-assertcount==1.0.0
|
pydantic-settings>=2.7.1
|
||||||
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,22 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Store the current Python environment
|
||||||
|
CURRENT_ENV=$(pip freeze)
|
||||||
|
|
||||||
|
# Remove dev dependencies
|
||||||
|
pip uninstall -y pytest pytest-cov isort flake8 ruff
|
||||||
|
|
||||||
|
# Generate production requirements
|
||||||
|
pip freeze > requirements.txt
|
||||||
|
|
||||||
|
# Reinstall dev dependencies
|
||||||
|
pip install pytest pytest-cov isort flake8 ruff
|
||||||
|
|
||||||
|
# Generate dev requirements
|
||||||
|
pip freeze > requirements-dev.txt
|
||||||
|
|
||||||
|
# Restore the original environment
|
||||||
|
pip uninstall -y pytest pytest-cov isort flake8 ruff
|
||||||
|
echo "$CURRENT_ENV" | pip install -r /dev/stdin
|
||||||
|
|
||||||
|
echo "Requirements files have been updated successfully!"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"files.autoSave": "onFocusChange",
|
||||||
|
"editor.defaultFormatter": null,
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-2
@@ -82,7 +82,9 @@ def automate_function(
|
|||||||
# The rules are defined in an external spreadsheet (TSV format)
|
# The rules are defined in an external spreadsheet (TSV format)
|
||||||
# This allows non-technical users to define and modify rules
|
# This allows non-technical users to define and modify rules
|
||||||
# without changing the code
|
# without changing the code
|
||||||
grouped_rules, messages = read_rules_from_spreadsheet(function_inputs.spreadsheet_url)
|
grouped_rules, messages = read_rules_from_spreadsheet(
|
||||||
|
function_inputs.spreadsheet_url
|
||||||
|
)
|
||||||
|
|
||||||
# Handle any validation messages from rule processing
|
# Handle any validation messages from rule processing
|
||||||
for message in messages:
|
for message in messages:
|
||||||
@@ -119,5 +121,6 @@ def automate_function(
|
|||||||
# Mark the run as successful and provide a summary message
|
# Mark the run as successful and provide a summary message
|
||||||
# This message will be displayed to the user in the Speckle UI
|
# This message will be displayed to the user in the Speckle UI
|
||||||
automate_context.mark_run_success(
|
automate_context.mark_run_success(
|
||||||
f"Successfully applied {len(grouped_rules)} rules to {len(flat_list_of_objects)} version {VERSION} objects."
|
f"Successfully applied {len(grouped_rules)} rules to "
|
||||||
|
f"{len(flat_list_of_objects)} version {VERSION} objects."
|
||||||
)
|
)
|
||||||
|
|||||||
+57
-26
@@ -4,7 +4,7 @@ from collections.abc import Generator, Iterable
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from specklepy.objects import Base
|
from specklepy.objects import Base
|
||||||
from specklepy.objects.other import Instance, Transform
|
from specklepy.objects.proxies import InstanceProxy as Instance
|
||||||
|
|
||||||
|
|
||||||
def speckle_print(log_string: str = "banana") -> None:
|
def speckle_print(log_string: str = "banana") -> None:
|
||||||
@@ -27,7 +27,8 @@ def get_item(obj: Base | dict[str, Any], key, default=None):
|
|||||||
return obj.get(key, default)
|
return obj.get(key, default)
|
||||||
elif hasattr(obj, key): # If it's an object with the attribute
|
elif hasattr(obj, key): # If it's an object with the attribute
|
||||||
return getattr(obj, key, default)
|
return getattr(obj, key, default)
|
||||||
return default # Return default if it's neither a dict nor an object with the attribute
|
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:
|
def has_item(obj: Base | dict[str, Any], key: str) -> bool:
|
||||||
@@ -39,7 +40,9 @@ def has_item(obj: Base | dict[str, Any], key: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def flatten_base_thorough(base: Base, parent_type: str = None) -> Iterable[Base]:
|
def flatten_base_thorough(
|
||||||
|
base: Base, parent_type: str | None = None
|
||||||
|
) -> Iterable[Base]:
|
||||||
"""Take a base and flatten it to an iterable of bases.
|
"""Take a base and flatten it to an iterable of bases.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -69,7 +72,9 @@ def flatten_base_thorough(base: Base, parent_type: str = None) -> Iterable[Base]
|
|||||||
print(category)
|
print(category)
|
||||||
if category.startswith("@"):
|
if category.startswith("@"):
|
||||||
category_object: Base = getattr(base, category)[0]
|
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:
|
except KeyError:
|
||||||
pass
|
pass
|
||||||
@@ -80,52 +85,78 @@ def flatten_base_thorough(base: Base, parent_type: str = None) -> Iterable[Base]
|
|||||||
def extract_base_and_transform(
|
def extract_base_and_transform(
|
||||||
base: Base,
|
base: Base,
|
||||||
inherited_instance_id: str | None = None,
|
inherited_instance_id: str | None = None,
|
||||||
transform_list: list[Transform] | None = None,
|
transform_list: list[list[float]] | None = None,
|
||||||
) -> Generator[
|
) -> Generator[
|
||||||
Base | str | list[Transform] | None | tuple[Base, Any | None, list[Transform] | None | list[Any]], Any | None, None
|
Base
|
||||||
|
| str
|
||||||
|
| list[list[float]]
|
||||||
|
| None
|
||||||
|
| tuple[Base, Any | None, list[list[float]] | None | list[Any]],
|
||||||
|
Any | None,
|
||||||
]:
|
]:
|
||||||
"""Traverses Speckle object hierarchies to yield `Base` objects and their transformations.
|
"""Traverses Speckle object hierarchies to yield `Base`s and transformas.
|
||||||
|
|
||||||
Tailored to Speckle's AEC data structures, it covers the newer hierarchical structures
|
Tailored to Speckle's AEC data structures, it covers the newer
|
||||||
with Collections and also with patterns found in older Revit specific data.
|
hierarchical structures with Collections and also with patterns found in
|
||||||
|
older Revit specific data.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
- base (Base): The starting point `Base` object for traversal.
|
- base (Base): The starting point `Base` object for traversal.
|
||||||
- inherited_instance_id (str, optional): The inherited identifier for `Base` objects without a unique ID.
|
- inherited_instance_id (str, optional): The inherited identifier for
|
||||||
- transform_list (List[Transform], optional): Accumulated list of transformations from parent to child objects.
|
`Base` objects without a unique ID.
|
||||||
|
- transform_list (List[List[float]], optional): Accumulated list of
|
||||||
|
transformations from parent to child objects.
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
- tuple: A `Base` object, its identifier, and a list of applicable `Transform` objects or None.
|
- tuple: A `Base` object, its identifier, and a list of applicable
|
||||||
|
transformations or None.
|
||||||
|
|
||||||
The id of the `Base` object is either the inherited identifier for a definition from an instance
|
The id of the `Base` object is either the inherited identifier for a
|
||||||
or the one defined in the object.
|
definition from an instance or the one defined in the object.
|
||||||
"""
|
"""
|
||||||
# Derive the identifier for the current `Base` object, defaulting to an inherited one if needed.
|
# Derive the identifier for the current `Base` object, defaulting to an
|
||||||
|
# inherited one if needed.
|
||||||
current_id = getattr(base, "id", inherited_instance_id)
|
current_id = getattr(base, "id", inherited_instance_id)
|
||||||
transform_list = transform_list or []
|
transform_list = transform_list or []
|
||||||
|
|
||||||
if isinstance(base, Instance):
|
if isinstance(base, Instance):
|
||||||
# Append transformation data and dive into the definition of `Instance` objects.
|
# Append transformation data and dive into the definition of `Instance`
|
||||||
|
# objects.
|
||||||
if base.transform:
|
if base.transform:
|
||||||
transform_list.append(base.transform)
|
transform_list.append(base.transform)
|
||||||
if base.definition:
|
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:
|
else:
|
||||||
# Initial yield for the current `Base` object.
|
# Initial yield for the current `Base` object.
|
||||||
yield base, current_id, transform_list
|
yield base, current_id, transform_list
|
||||||
|
|
||||||
# Process 'elements' and '@elements', typical containers for `Base` objects in AEC models.
|
# Process 'elements' and '@elements', typical containers for `Base`
|
||||||
elements_attr = getattr(base, "elements", []) or getattr(base, "@elements", [])
|
# objects in AEC models.
|
||||||
|
elements_attr = getattr(base, "elements", []) or getattr(
|
||||||
|
base, "@elements", []
|
||||||
|
)
|
||||||
for element in elements_attr:
|
for element in elements_attr:
|
||||||
if isinstance(element, Base):
|
if isinstance(element, Base):
|
||||||
# Recurse into each `Base` object within 'elements' or '@elements'.
|
# Recurse into each `Base` object within 'elements' or
|
||||||
yield from extract_base_and_transform(element, current_id, transform_list.copy())
|
# '@elements'.
|
||||||
|
yield from extract_base_and_transform(
|
||||||
|
element, current_id, transform_list.copy()
|
||||||
|
)
|
||||||
|
|
||||||
# Recursively process '@'-prefixed properties that are Base objects with 'elements'.
|
# Recursively process '@'-prefixed properties that are Base objects
|
||||||
# This is a common pattern in older Speckle data models, such as those used for Revit commits.
|
# with 'elements'.
|
||||||
|
# This is a common pattern in older Speckle data models, such as those
|
||||||
|
# used for Revit commits.
|
||||||
for attr_name in dir(base):
|
for attr_name in dir(base):
|
||||||
if attr_name.startswith("@"):
|
if attr_name.startswith("@"):
|
||||||
attr_value = getattr(base, attr_name)
|
attr_value = getattr(base, attr_name)
|
||||||
# If the attribute is a Base object containing 'elements', recurse into it.
|
# If the attribute is a Base object containing 'elements',
|
||||||
if isinstance(attr_value, Base) and hasattr(attr_value, "elements"):
|
# recurse into it.
|
||||||
yield from extract_base_and_transform(attr_value, current_id, transform_list.copy())
|
if isinstance(attr_value, Base) and hasattr(
|
||||||
|
attr_value, "elements"
|
||||||
|
):
|
||||||
|
yield from extract_base_and_transform(
|
||||||
|
attr_value, current_id, transform_list.copy()
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
"""This file contains the inputs for the function.
|
||||||
|
|
||||||
|
It is used to define the inputs for the function and to validate them.
|
||||||
|
"""
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
|
|||||||
+4
-2
@@ -1,4 +1,4 @@
|
|||||||
"""Configuration module defining mappings between spreadsheet predicates and rule methods."""
|
"""Defines mappings between spreadsheet predicates and rule methods."""
|
||||||
|
|
||||||
from src.rules import PropertyRules
|
from src.rules import PropertyRules
|
||||||
|
|
||||||
@@ -16,5 +16,7 @@ PREDICATE_METHOD_MAP = {
|
|||||||
"is like": PropertyRules.is_parameter_value_like.__name__,
|
"is like": PropertyRules.is_parameter_value_like.__name__,
|
||||||
"identical to": PropertyRules.is_identical_value.__name__,
|
"identical to": PropertyRules.is_identical_value.__name__,
|
||||||
"contains": PropertyRules.is_parameter_value_containing.__name__,
|
"contains": PropertyRules.is_parameter_value_containing.__name__,
|
||||||
"does not contain": PropertyRules.is_parameter_value_not_containing.__name__,
|
"does not contain": (
|
||||||
|
PropertyRules.is_parameter_value_not_containing.__name__
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
+93
-31
@@ -53,17 +53,23 @@ def validate_rule_structure(rule_group: pd.DataFrame) -> None:
|
|||||||
|
|
||||||
# Check if first condition is WHERE
|
# Check if first condition is WHERE
|
||||||
if logic_values.iloc[0] != "WHERE":
|
if logic_values.iloc[0] != "WHERE":
|
||||||
raise ValueError(f"Rule {rule_group.iloc[0]['Rule Number']} must start with WHERE")
|
raise ValueError(
|
||||||
|
f"Rule {rule_group.iloc[0]['Rule Number']} must start with WHERE"
|
||||||
|
)
|
||||||
|
|
||||||
# Count CHECK conditions
|
# Count CHECK conditions
|
||||||
check_count = sum(1 for value in logic_values if value == "CHECK")
|
check_count = sum(1 for value in logic_values if value == "CHECK")
|
||||||
if check_count > 1:
|
if check_count > 1:
|
||||||
raise ValueError(f"Rule {rule_group.iloc[0]['Rule Number']} has multiple CHECK conditions")
|
raise ValueError(
|
||||||
|
f"Rule {rule_group.iloc[0]['Rule Number']} has multiple CHECK conditions"
|
||||||
|
)
|
||||||
|
|
||||||
# If CHECK exists, ensure it's the last condition
|
# If CHECK exists, ensure it's the last condition
|
||||||
check_indices = logic_values[logic_values == "CHECK"].index
|
check_indices = logic_values[logic_values == "CHECK"].index
|
||||||
if check_count == 1 and check_indices[0] != rule_group.index[-1]:
|
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']}")
|
raise ValueError(
|
||||||
|
f"CHECK must be the last condition in rule {rule_group.iloc[0]['Rule Number']}"
|
||||||
|
)
|
||||||
|
|
||||||
# Validate Logic values
|
# Validate Logic values
|
||||||
valid_values = {"WHERE", "AND", "CHECK"}
|
valid_values = {"WHERE", "AND", "CHECK"}
|
||||||
@@ -73,7 +79,10 @@ def validate_rule_structure(rule_group: pd.DataFrame) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def evaluate_condition(
|
def evaluate_condition(
|
||||||
speckle_object: Base, condition: pd.Series, rule_number: str | None = None, case_number: int | None = None
|
speckle_object: Base,
|
||||||
|
condition: pd.Series,
|
||||||
|
rule_number: str | None = None,
|
||||||
|
case_number: int | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Evaluates a single condition against a Speckle object.
|
"""Evaluates a single condition against a Speckle object.
|
||||||
|
|
||||||
@@ -87,7 +96,8 @@ def evaluate_condition(
|
|||||||
speckle_object: The Speckle object to evaluate against
|
speckle_object: The Speckle object to evaluate against
|
||||||
condition: A pandas Series containing the condition details
|
condition: A pandas Series containing the condition details
|
||||||
- 'Property Name': The name of the property to check
|
- 'Property Name': The name of the property to check
|
||||||
- 'Predicate': The comparison operation (like 'equals', 'greater than')
|
- 'Predicate': The comparison operation (like 'equals',
|
||||||
|
'greater than')
|
||||||
- 'Value': The value to compare against
|
- 'Value': The value to compare against
|
||||||
rule_number: For tracking, the rule number being evaluated
|
rule_number: For tracking, the rule number being evaluated
|
||||||
case_number: For tracking, the condition number within the rule
|
case_number: For tracking, the condition number within the rule
|
||||||
@@ -95,7 +105,9 @@ def evaluate_condition(
|
|||||||
Returns:
|
Returns:
|
||||||
True if the condition is met, False otherwise
|
True if the condition is met, False otherwise
|
||||||
"""
|
"""
|
||||||
property_name = condition.get("Property Name", condition.get("Property Path"))
|
property_name = condition.get(
|
||||||
|
"Property Name", condition.get("Property Path")
|
||||||
|
)
|
||||||
predicate_key = condition["Predicate"]
|
predicate_key = condition["Predicate"]
|
||||||
value = condition["Value"]
|
value = condition["Value"]
|
||||||
|
|
||||||
@@ -116,7 +128,9 @@ def evaluate_condition(
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_filters_and_check(rule_group: pd.DataFrame) -> tuple[pd.DataFrame, pd.Series]:
|
def get_filters_and_check(
|
||||||
|
rule_group: pd.DataFrame,
|
||||||
|
) -> tuple[pd.DataFrame, pd.Series]:
|
||||||
"""Separates rule conditions into filtering conditions and the final check condition.
|
"""Separates rule conditions into filtering conditions and the final check condition.
|
||||||
|
|
||||||
This function handles two rule formats:
|
This function handles two rule formats:
|
||||||
@@ -158,7 +172,9 @@ def get_filters_and_check(rule_group: pd.DataFrame) -> tuple[pd.DataFrame, pd.Se
|
|||||||
else:
|
else:
|
||||||
# No AND conditions found, just use WHERE as filter
|
# No AND conditions found, just use WHERE as filter
|
||||||
filters = rule_group
|
filters = rule_group
|
||||||
final_check = rule_group.iloc[0] # Default to first condition as check
|
final_check = rule_group.iloc[
|
||||||
|
0
|
||||||
|
] # Default to first condition as check
|
||||||
|
|
||||||
return filters, final_check
|
return filters, final_check
|
||||||
|
|
||||||
@@ -204,7 +220,10 @@ def process_rule(
|
|||||||
obj
|
obj
|
||||||
for obj in filtered_objects
|
for obj in filtered_objects
|
||||||
if evaluate_condition(
|
if evaluate_condition(
|
||||||
speckle_object=obj, condition=filter_condition, rule_number=rule_number, case_number=index
|
speckle_object=obj,
|
||||||
|
condition=filter_condition,
|
||||||
|
rule_number=rule_number,
|
||||||
|
case_number=index,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -219,7 +238,10 @@ def process_rule(
|
|||||||
|
|
||||||
for obj in filtered_objects:
|
for obj in filtered_objects:
|
||||||
if evaluate_condition(
|
if evaluate_condition(
|
||||||
speckle_object=obj, condition=final_check, rule_number=rule_number, case_number=len(filters)
|
speckle_object=obj,
|
||||||
|
condition=final_check,
|
||||||
|
rule_number=rule_number,
|
||||||
|
case_number=len(filters),
|
||||||
):
|
):
|
||||||
pass_objects.append(obj)
|
pass_objects.append(obj)
|
||||||
else:
|
else:
|
||||||
@@ -235,7 +257,7 @@ def apply_rules_to_objects(
|
|||||||
minimum_severity: MinimumSeverity = MinimumSeverity.INFO,
|
minimum_severity: MinimumSeverity = MinimumSeverity.INFO,
|
||||||
hide_skipped: bool = False,
|
hide_skipped: bool = False,
|
||||||
) -> dict[str, tuple[list[Base], list[Base]]]:
|
) -> dict[str, tuple[list[Base], list[Base]]]:
|
||||||
"""Applies defined rules to a list of objects and updates the automate context with the results.
|
"""Applies rules to objects and updates the automate context results.
|
||||||
|
|
||||||
This is the main orchestration function that:
|
This is the main orchestration function that:
|
||||||
1. Processes each rule group against all objects
|
1. Processes each rule group against all objects
|
||||||
@@ -255,7 +277,11 @@ def apply_rules_to_objects(
|
|||||||
"""
|
"""
|
||||||
grouped_results = {}
|
grouped_results = {}
|
||||||
rules_processed = 0
|
rules_processed = 0
|
||||||
severity_levels = {MinimumSeverity.INFO: 0, MinimumSeverity.WARNING: 1, MinimumSeverity.ERROR: 2}
|
severity_levels = {
|
||||||
|
MinimumSeverity.INFO: 0,
|
||||||
|
MinimumSeverity.WARNING: 1,
|
||||||
|
MinimumSeverity.ERROR: 2,
|
||||||
|
}
|
||||||
min_severity_level = severity_levels[minimum_severity]
|
min_severity_level = severity_levels[minimum_severity]
|
||||||
|
|
||||||
for rule_id, rule_group in grouped_rules:
|
for rule_id, rule_group in grouped_rules:
|
||||||
@@ -264,15 +290,19 @@ def apply_rules_to_objects(
|
|||||||
|
|
||||||
# Ensure rule_group has necessary columns
|
# Ensure rule_group has necessary columns
|
||||||
if "Message" not in rule_group.columns or (
|
if "Message" not in rule_group.columns or (
|
||||||
"Report Severity" not in rule_group.columns and "Severity" not in rule_group.columns
|
"Report Severity" not in rule_group.columns
|
||||||
|
and "Severity" not in rule_group.columns
|
||||||
):
|
):
|
||||||
continue # Or raise an exception if these columns are mandatory
|
continue # Or raise an exception if these columns are mandatory
|
||||||
|
|
||||||
# Get the severity level for this rule
|
# Get the severity level for this rule
|
||||||
rule_severity = get_severity(rule_group.iloc[-1])
|
rule_severity = get_severity(rule_group.iloc[-1])
|
||||||
rule_severity_level = severity_levels[MinimumSeverity(rule_severity.value)]
|
rule_severity_level = severity_levels[
|
||||||
|
MinimumSeverity(rule_severity.value)
|
||||||
|
]
|
||||||
|
|
||||||
# Check if the rule severity level meets the minimum severity level - no point in processing lower severity rules
|
# Check if the rule severity level meets the minimum severity level
|
||||||
|
# no point in processing lower severity rules
|
||||||
if rule_severity_level < min_severity_level:
|
if rule_severity_level < min_severity_level:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -280,16 +310,35 @@ def apply_rules_to_objects(
|
|||||||
|
|
||||||
# For passing objects, only attach if we're showing all levels (INFO)
|
# For passing objects, only attach if we're showing all levels (INFO)
|
||||||
if minimum_severity == MinimumSeverity.INFO:
|
if minimum_severity == MinimumSeverity.INFO:
|
||||||
attach_results(pass_objects, rule_group.iloc[-1], rule_id_str, automate_context, True)
|
attach_results(
|
||||||
|
pass_objects,
|
||||||
|
rule_group.iloc[-1],
|
||||||
|
rule_id_str,
|
||||||
|
automate_context,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
# For failing objects, attach if they meet minimum severity threshold
|
# For failing objects, attach if they meet minimum severity threshold
|
||||||
if len(fail_objects) and rule_severity_level >= min_severity_level:
|
if len(fail_objects) and rule_severity_level >= min_severity_level:
|
||||||
attach_results(fail_objects, rule_group.iloc[-1], rule_id_str, automate_context, False)
|
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
|
||||||
|
):
|
||||||
|
speckle_print(f"Rule {rule_id_str} Skipped")
|
||||||
|
|
||||||
if len(pass_objects) == 0 and len(fail_objects) == 0 and not hide_skipped:
|
|
||||||
automate_context.attach_info_to_objects(
|
automate_context.attach_info_to_objects(
|
||||||
category=f"Rule {rule_id_str} Skipped",
|
category=f"Rule {rule_id_str} Skipped",
|
||||||
object_ids=["0"], # This is a hack to get a rule to report with no valid objects
|
affected_objects=[Base()],
|
||||||
|
# This is a hack to get a rule to report with no valid objects
|
||||||
message=f"No objects found for rule {rule_id_str}",
|
message=f"No objects found for rule {rule_id_str}",
|
||||||
metadata={},
|
metadata={},
|
||||||
)
|
)
|
||||||
@@ -315,7 +364,7 @@ class SeverityLevel(Enum):
|
|||||||
|
|
||||||
|
|
||||||
def get_severity(rule_info: pd.Series) -> SeverityLevel:
|
def get_severity(rule_info: pd.Series) -> SeverityLevel:
|
||||||
"""Convert a string severity level from the spreadsheet to the corresponding SeverityLevel enum.
|
"""Convert a string severity to the corresponding SeverityLevel enum.
|
||||||
|
|
||||||
This function normalizes user input with robust handling for:
|
This function normalizes user input with robust handling for:
|
||||||
- Case insensitivity (e.g., "info", "WARNING" → "Info", "Warning")
|
- Case insensitivity (e.g., "info", "WARNING" → "Info", "Warning")
|
||||||
@@ -324,18 +373,24 @@ def get_severity(rule_info: pd.Series) -> SeverityLevel:
|
|||||||
- Default fallback to ERROR for invalid input
|
- Default fallback to ERROR for invalid input
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
rule_info: Series containing rule information with 'Report Severity' key
|
rule_info: Series containing rule information with 'Report Severity'
|
||||||
|
key
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Appropriate SeverityLevel enum value
|
Appropriate SeverityLevel enum value
|
||||||
"""
|
"""
|
||||||
severity = rule_info.get("Report Severity") or rule_info.get("Severity") # Extract severity from input data
|
severity = rule_info.get("Report Severity") or rule_info.get(
|
||||||
|
"Severity"
|
||||||
|
) # Extract severity from input data
|
||||||
|
|
||||||
# If severity is None or not a string (e.g., numeric input), default to ERROR
|
# If severity is None or not a string (e.g., numeric input),
|
||||||
|
# default to ERROR
|
||||||
if not isinstance(severity, str):
|
if not isinstance(severity, str):
|
||||||
return SeverityLevel.ERROR
|
return SeverityLevel.ERROR
|
||||||
|
|
||||||
severity = severity.strip().upper() # Remove leading/trailing spaces & normalize case
|
severity = (
|
||||||
|
severity.strip().upper()
|
||||||
|
) # Remove leading/trailing spaces & normalize case
|
||||||
|
|
||||||
# Define a mapping for shorthand or alternate spellings
|
# Define a mapping for shorthand or alternate spellings
|
||||||
alias_map = {
|
alias_map = {
|
||||||
@@ -345,7 +400,8 @@ def get_severity(rule_info: pd.Series) -> SeverityLevel:
|
|||||||
# Replace shorthand values if applicable
|
# Replace shorthand values if applicable
|
||||||
severity = alias_map.get(severity, severity)
|
severity = alias_map.get(severity, severity)
|
||||||
|
|
||||||
# Attempt to match with an existing SeverityLevel enum value (case-insensitive)
|
# Attempt to match with an existing SeverityLevel enum value
|
||||||
|
# (case-insensitive)
|
||||||
return next(
|
return next(
|
||||||
(level for level in SeverityLevel if level.value.upper() == severity),
|
(level for level in SeverityLevel if level.value.upper() == severity),
|
||||||
SeverityLevel.ERROR, # Default to ERROR if no match is found
|
SeverityLevel.ERROR, # Default to ERROR if no match is found
|
||||||
@@ -353,7 +409,10 @@ def get_severity(rule_info: pd.Series) -> SeverityLevel:
|
|||||||
|
|
||||||
|
|
||||||
def get_metadata(
|
def get_metadata(
|
||||||
rule_id: str, rule_info: pd.Series, passed: bool, speckle_objects: list[Base]
|
rule_id: str,
|
||||||
|
rule_info: pd.Series,
|
||||||
|
passed: bool,
|
||||||
|
speckle_objects: list[Base],
|
||||||
) -> dict[str, str | int | Any]:
|
) -> dict[str, str | int | Any]:
|
||||||
"""Generates structured metadata for rule results.
|
"""Generates structured metadata for rule results.
|
||||||
|
|
||||||
@@ -369,7 +428,8 @@ def get_metadata(
|
|||||||
speckle_objects: List of Speckle objects affected
|
speckle_objects: List of Speckle objects affected
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dictionary containing metadata if valid JSON serializable, empty dict otherwise
|
Dictionary containing metadata if valid JSON serializable,
|
||||||
|
empty dict otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
metadata = {
|
metadata = {
|
||||||
@@ -399,7 +459,8 @@ def attach_results(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Attaches rule results to objects in the Speckle Automate context.
|
"""Attaches rule results to objects in the Speckle Automate context.
|
||||||
|
|
||||||
This function is the interface to the Speckle platform for reporting results:
|
This function is the interface to the Speckle platform for reporting
|
||||||
|
results:
|
||||||
- For failing objects, attaches results with appropriate severity levels
|
- For failing objects, attaches results with appropriate severity levels
|
||||||
- For passing objects, attaches informational results
|
- For passing objects, attaches informational results
|
||||||
- Includes structured metadata for consistent reporting
|
- Includes structured metadata for consistent reporting
|
||||||
@@ -429,7 +490,7 @@ def attach_results(
|
|||||||
)
|
)
|
||||||
context.attach_result_to_objects(
|
context.attach_result_to_objects(
|
||||||
category=f"Rule {rule_id}",
|
category=f"Rule {rule_id}",
|
||||||
object_ids=[speckle_object.id for speckle_object in speckle_objects],
|
affected_objects=speckle_objects,
|
||||||
message=message,
|
message=message,
|
||||||
level=severity,
|
level=severity,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
@@ -437,7 +498,7 @@ def attach_results(
|
|||||||
else:
|
else:
|
||||||
context.attach_info_to_objects(
|
context.attach_info_to_objects(
|
||||||
category=f"Rule {rule_id}",
|
category=f"Rule {rule_id}",
|
||||||
object_ids=[speckle_object.id for speckle_object in speckle_objects],
|
affected_objects=speckle_objects,
|
||||||
message=message,
|
message=message,
|
||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
)
|
)
|
||||||
@@ -456,7 +517,8 @@ def format_message(rule_info):
|
|||||||
"""
|
"""
|
||||||
message = (
|
message = (
|
||||||
str(rule_info["Message"])
|
str(rule_info["Message"])
|
||||||
if rule_info["Message"] is not None and not pd.isna(rule_info["Message"])
|
if rule_info["Message"] is not None
|
||||||
|
and not pd.isna(rule_info["Message"])
|
||||||
else "No Message"
|
else "No Message"
|
||||||
)
|
)
|
||||||
return message
|
return message
|
||||||
|
|||||||
+41
-78
@@ -2,67 +2,6 @@ import pytest
|
|||||||
from specklepy.objects.base import Base
|
from specklepy.objects.base import Base
|
||||||
|
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# Create parameters structure
|
|
||||||
wall.parameters = Base()
|
|
||||||
|
|
||||||
# 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"
|
|
||||||
|
|
||||||
# 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
|
@pytest.fixture
|
||||||
def v3_wall():
|
def v3_wall():
|
||||||
"""Creates a v3-style Speckle wall object."""
|
"""Creates a v3-style Speckle wall object."""
|
||||||
@@ -79,25 +18,47 @@ def v3_wall():
|
|||||||
|
|
||||||
# Create location geometry
|
# Create location geometry
|
||||||
wall.location = Base()
|
wall.location = Base()
|
||||||
|
wall.location.id = "9c76b8de34382c9052965ee463f8374b"
|
||||||
wall.location.start = Base()
|
wall.location.start = Base()
|
||||||
wall.location.start.x = 22400.000000000007
|
wall.location.start.x = 22400.000000000007
|
||||||
wall.location.start.y = 15199.999999999998
|
wall.location.start.y = 15199.999999999998
|
||||||
wall.location.start.z = 0
|
wall.location.start.z = 0
|
||||||
|
wall.location.start.id = "d0c4fdb2e11cc825e7f05f9dc88a0be1"
|
||||||
|
wall.location.start.units = "mm"
|
||||||
|
wall.location.start.speckle_type = "Objects.Geometry.Point"
|
||||||
wall.location.end = Base()
|
wall.location.end = Base()
|
||||||
wall.location.end.x = 22400.000000000015
|
wall.location.end.x = 22400.000000000015
|
||||||
wall.location.end.y = 20500
|
wall.location.end.y = 20500
|
||||||
wall.location.end.z = 0
|
wall.location.end.z = 0
|
||||||
|
wall.location.end.id = "3455575bfd8939f264d295b61e74156f"
|
||||||
|
wall.location.end.units = "mm"
|
||||||
|
wall.location.end.speckle_type = "Objects.Geometry.Point"
|
||||||
wall.location.units = "mm"
|
wall.location.units = "mm"
|
||||||
|
wall.location.domain = Base()
|
||||||
|
wall.location.domain.id = "3b97feaad2dbcc2d894c9cec024a9bf2"
|
||||||
|
wall.location.domain.end = 17.388451443569522
|
||||||
|
wall.location.domain.start = -3.552713668866051e-14
|
||||||
|
wall.location.domain.speckle_type = "Objects.Primitive.Interval"
|
||||||
wall.location.length = 5300.000000000002
|
wall.location.length = 5300.000000000002
|
||||||
|
wall.location.speckle_type = "Objects.Geometry.Line"
|
||||||
|
|
||||||
# Create nested properties structure
|
# Create level references
|
||||||
|
wall.level = Base()
|
||||||
|
wall.level.name = "1FL"
|
||||||
|
wall.level.units = "mm"
|
||||||
|
wall.level.elevation = 0
|
||||||
|
|
||||||
|
wall.topLevel = Base()
|
||||||
|
wall.topLevel.name = "1FL"
|
||||||
|
wall.topLevel.units = "mm"
|
||||||
|
wall.topLevel.elevation = 0
|
||||||
|
|
||||||
|
# Create properties structure
|
||||||
wall.properties = Base()
|
wall.properties = Base()
|
||||||
wall.properties.Parameters = Base()
|
wall.properties.Parameters = Base()
|
||||||
|
|
||||||
# Type Parameters
|
|
||||||
wall.properties.Parameters["Type Parameters"] = Base()
|
wall.properties.Parameters["Type Parameters"] = Base()
|
||||||
|
|
||||||
# Add Text section with GUID parameter
|
# Add Text section
|
||||||
wall.properties.Parameters["Type Parameters"].Text = Base()
|
wall.properties.Parameters["Type Parameters"].Text = Base()
|
||||||
wall.properties.Parameters["Type Parameters"].Text["符号"] = {
|
wall.properties.Parameters["Type Parameters"].Text["符号"] = {
|
||||||
"name": "符号",
|
"name": "符号",
|
||||||
@@ -105,6 +66,7 @@ def v3_wall():
|
|||||||
"internalDefinitionName": "ee1f33e1-5506-4a64-b87b-7b98d30aea52",
|
"internalDefinitionName": "ee1f33e1-5506-4a64-b87b-7b98d30aea52",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Add Structure section
|
||||||
wall.properties.Parameters["Type Parameters"].Structure = Base()
|
wall.properties.Parameters["Type Parameters"].Structure = Base()
|
||||||
wall.properties.Parameters["Type Parameters"].Structure["Fc24 (0)"] = {
|
wall.properties.Parameters["Type Parameters"].Structure["Fc24 (0)"] = {
|
||||||
"units": "mm",
|
"units": "mm",
|
||||||
@@ -113,20 +75,21 @@ def v3_wall():
|
|||||||
"thickness": 300,
|
"thickness": 300,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Instance Parameters
|
# Add Construction section
|
||||||
|
wall.properties.Parameters["Type Parameters"].Construction = Base()
|
||||||
|
wall.properties.Parameters["Type Parameters"].Construction.Width = {
|
||||||
|
"name": "Width",
|
||||||
|
"units": "Millimeters",
|
||||||
|
"value": 300,
|
||||||
|
"internalDefinitionName": "WALL_ATTR_WIDTH_PARAM",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add Instance Parameters
|
||||||
wall.properties.Parameters["Instance Parameters"] = Base()
|
wall.properties.Parameters["Instance Parameters"] = Base()
|
||||||
wall.properties.Parameters["Instance Parameters"].Structural = Base()
|
wall.properties.Parameters["Instance Parameters"].Structural = Base()
|
||||||
wall.properties.Parameters["Instance Parameters"].Structural.Structural = {"name": "Structural", "value": "Yes"}
|
wall.properties.Parameters["Instance Parameters"].Structural.Structural = {
|
||||||
|
"name": "Structural",
|
||||||
# Create basic level references
|
"value": "Yes",
|
||||||
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
|
return wall
|
||||||
|
|||||||
+16
-8
@@ -8,20 +8,24 @@ from speckle_automate import (
|
|||||||
)
|
)
|
||||||
from speckle_automate.fixtures import * # noqa: F401, F403
|
from speckle_automate.fixtures import * # noqa: F401, F403
|
||||||
|
|
||||||
from inputs import MinimumSeverity
|
|
||||||
from src.function import automate_function
|
from src.function import automate_function
|
||||||
from src.helpers import speckle_print
|
from src.helpers import speckle_print
|
||||||
from src.inputs import FunctionInputs
|
from src.inputs import FunctionInputs, MinimumSeverity
|
||||||
|
|
||||||
|
|
||||||
class TestFunction:
|
class TestFunction:
|
||||||
"""Test suite for the automate function."""
|
"""Test suite for the automate function."""
|
||||||
|
|
||||||
def test_function_run(self, test_automation_run_data: AutomationRunData, test_automation_token: str):
|
def test_function_run(
|
||||||
|
self,
|
||||||
|
test_automation_run_data: AutomationRunData,
|
||||||
|
test_automation_token: str,
|
||||||
|
):
|
||||||
"""Run an integration test for the automate function.
|
"""Run an integration test for the automate function.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
test_automation_run_data (AutomationRunData): The automation run data provided by sdk.
|
test_automation_run_data (AutomationRunData): The automation run
|
||||||
|
data provided by sdk.
|
||||||
test_automation_token (str): The automation token.
|
test_automation_token (str): The automation token.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@@ -29,15 +33,19 @@ class TestFunction:
|
|||||||
speckle_print(str(test_automation_token))
|
speckle_print(str(test_automation_token))
|
||||||
|
|
||||||
"""Run an integration test for the automate function."""
|
"""Run an integration test for the automate function."""
|
||||||
automation_context = AutomationContext.initialize(test_automation_run_data, test_automation_token)
|
automation_context = AutomationContext.initialize(
|
||||||
default_url: str = (
|
test_automation_run_data, test_automation_token
|
||||||
"https://speckle-model-checker-cedxvz7lzq-ew.a.run.app/r/6hdycwPELyTIT7Ueedh0UsWdJlTBefwSjDlcnd8LXGg/tsv"
|
|
||||||
)
|
)
|
||||||
|
default_url: str = "https://model-checker.speckle.systems/r/7YhnQyQNP_Ydv97QCwHbj7BWHrNkG022bez_jVkxbYs/tsv"
|
||||||
|
|
||||||
automate_sdk = run_function(
|
automate_sdk = run_function(
|
||||||
automation_context,
|
automation_context,
|
||||||
automate_function,
|
automate_function,
|
||||||
FunctionInputs(spreadsheet_url=default_url, minimum_severity=MinimumSeverity.INFO, hide_skipped=True),
|
FunctionInputs(
|
||||||
|
spreadsheet_url=default_url,
|
||||||
|
minimum_severity=MinimumSeverity.INFO,
|
||||||
|
hide_skipped=True,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
|
assert automate_sdk.run_status == AutomationStatus.SUCCEEDED
|
||||||
|
|||||||
+376
-269
@@ -1,95 +1,55 @@
|
|||||||
"""Test suite for parameter handling functionality."""
|
"""Test suite for parameter handling functionality."""
|
||||||
|
|
||||||
import os
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import pytest
|
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.objects.base import Base
|
||||||
from specklepy.transports.server import ServerTransport
|
|
||||||
|
|
||||||
from helpers import speckle_print
|
|
||||||
from src.rules import PropertyRules
|
from src.rules import PropertyRules
|
||||||
|
|
||||||
|
|
||||||
class TestParameterHandling:
|
class TestParameterHandling:
|
||||||
"""Test suite for parameter handling functionality."""
|
"""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
|
@pytest.fixture
|
||||||
def test_objects(self, v2_wall: Any, v3_wall: Any) -> tuple[Base, Base]:
|
def test_objects(self) -> Base:
|
||||||
"""Pytest fixture to provide test objects."""
|
"""Pytest fixture to provide test objects."""
|
||||||
return self.load_test_objects(v2_wall, v3_wall)
|
# Create a mock Base object with the required structure
|
||||||
|
v3_obj = Base()
|
||||||
|
v3_obj.properties = {
|
||||||
|
"Parameters": {
|
||||||
|
"category": "Walls",
|
||||||
|
"Width": 300,
|
||||||
|
"Construction": {"Width": 300},
|
||||||
|
"Instance Parameters": {
|
||||||
|
"Dimensions": {"Length": 5300.000000000001},
|
||||||
|
"Structural": {"Structural": {"value": "Yes"}},
|
||||||
|
"Room Bounding": {"value": "Yes"},
|
||||||
|
"top is attached": {"value": "No"},
|
||||||
|
},
|
||||||
|
"Type Parameters": {
|
||||||
|
"Structure": {"Fc24 (0)": {"thickness": 300}},
|
||||||
|
"Text": {"符号": {"value": "W30"}},
|
||||||
|
},
|
||||||
|
"Type": "W30(Fc24)",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v3_obj.speckle_type = "Revit"
|
||||||
|
return v3_obj
|
||||||
|
|
||||||
def test_deserialization_structure(self, test_objects):
|
def test_deserialization_structure(self, test_objects):
|
||||||
"""Test that objects are properly deserialized with correct structure."""
|
"""Test that objects are properly deserialized with correct structure."""
|
||||||
v2_obj, v3_obj = test_objects
|
v3_obj = test_objects
|
||||||
|
|
||||||
# Check base class type
|
# Check base class type
|
||||||
for obj in [v2_obj, v3_obj]:
|
assert isinstance(v3_obj, Base), f"Expected {v3_obj} to be an instance of Base"
|
||||||
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
|
# Check v3 structure
|
||||||
assert hasattr(v3_obj, "properties"), "v3_obj should have 'properties' attribute"
|
assert hasattr(v3_obj, "properties"), (
|
||||||
assert v3_obj["properties"] is not None, "v3_obj['properties'] should not be None"
|
"v3_obj should have 'properties' attribute"
|
||||||
assert "Parameters" in v3_obj["properties"], "'Parameters' key should exist in v3_obj['properties']"
|
)
|
||||||
|
assert v3_obj.properties is not None, "v3_obj.properties should not be None"
|
||||||
@pytest.mark.parametrize(
|
assert "Parameters" in v3_obj.properties, (
|
||||||
"param_name, expected_result",
|
"'Parameters' key should exist in v3_obj.properties"
|
||||||
[
|
)
|
||||||
("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(
|
@pytest.mark.parametrize(
|
||||||
"param_name, expected_result",
|
"param_name, expected_result",
|
||||||
@@ -101,62 +61,72 @@ class TestParameterHandling:
|
|||||||
)
|
)
|
||||||
def test_v3_parameter_exists(self, test_objects, param_name, expected_result):
|
def test_v3_parameter_exists(self, test_objects, param_name, expected_result):
|
||||||
"""Test parameter existence checking in v3 objects."""
|
"""Test parameter existence checking in v3 objects."""
|
||||||
_, v3_obj = test_objects
|
v3_obj = test_objects
|
||||||
assert PropertyRules.has_parameter(v3_obj, param_name) == expected_result
|
assert PropertyRules.has_parameter(v3_obj, param_name) == expected_result
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"param_name_1, param_name_2",
|
"param_name_1, param_name_2",
|
||||||
[
|
[
|
||||||
|
# Test direct value access
|
||||||
(
|
(
|
||||||
"properties.Parameters.Instance Parameters.Dimensions.Length.value",
|
"location.length",
|
||||||
"Instance Parameters.Dimensions.Length",
|
"location.length",
|
||||||
|
),
|
||||||
|
# Test .value key access
|
||||||
|
(
|
||||||
|
"Type Parameters.Text.符号",
|
||||||
|
"Type Parameters.Text.符号.value",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_v3_parameter_search_equivalence(self, test_objects, param_name_1, param_name_2):
|
def test_v3_parameter_search_equivalence(
|
||||||
|
self,
|
||||||
|
v3_wall,
|
||||||
|
param_name_1,
|
||||||
|
param_name_2,
|
||||||
|
):
|
||||||
"""Test parameter existence checking equivalence in v3 objects."""
|
"""Test parameter existence checking equivalence in v3 objects."""
|
||||||
_, v3_obj = test_objects
|
assert PropertyRules.get_parameter_value(
|
||||||
assert PropertyRules.get_parameter_value(v3_obj, param_name_1) == PropertyRules.get_parameter_value(
|
v3_wall, param_name_1
|
||||||
v3_obj, param_name_2
|
) == PropertyRules.get_parameter_value(v3_wall, param_name_2)
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"obj_version, param_name, expected_value, default_value",
|
"param_name, expected_value, default_value",
|
||||||
[
|
[
|
||||||
# Test direct parameters
|
# Test direct parameters
|
||||||
("v2", "category", "Walls", None),
|
("category", "Walls", None),
|
||||||
("v3", "category", "Walls", None),
|
|
||||||
# Test nested parameters - using both internal and friendly names
|
# Test nested parameters - using both internal and friendly names
|
||||||
("v2", "WALL_ATTR_WIDTH_PARAM", 300, None),
|
("Construction.Width", 300, None),
|
||||||
("v3", "Construction.Width", 300, None),
|
|
||||||
# Test parameters with units
|
# Test parameters with units
|
||||||
("v2", "CURVE_ELEM_LENGTH", 5300.000000000001, None),
|
(
|
||||||
("v3", "Instance Parameters.Dimensions.Length", 5300.000000000001, None),
|
"Instance Parameters.Dimensions.Length",
|
||||||
|
5300.000000000001,
|
||||||
|
None,
|
||||||
|
),
|
||||||
# Test non-existent parameters with a default value
|
# Test non-existent parameters with a default value
|
||||||
("v2", "parameters.non_existent", "default", "default"),
|
(
|
||||||
("v3", "properties.Parameters.non_existent", "default", "default"),
|
"properties.Parameters.non_existent",
|
||||||
|
"default",
|
||||||
|
"default",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_parameter_value_retrieval(self, test_objects, obj_version, param_name, expected_value, default_value):
|
def test_parameter_value_retrieval(
|
||||||
"""Test parameter value retrieval from both v2 and v3 objects."""
|
self,
|
||||||
v2_obj, v3_obj = test_objects
|
test_objects,
|
||||||
obj = v2_obj if obj_version == "v2" else v3_obj
|
param_name,
|
||||||
result = PropertyRules.get_parameter_value(obj, param_name, default_value=default_value)
|
expected_value,
|
||||||
|
default_value,
|
||||||
|
):
|
||||||
|
"""Test parameter value retrieval from v3 objects."""
|
||||||
|
v3_obj = test_objects
|
||||||
|
result = PropertyRules.get_parameter_value(
|
||||||
|
v3_obj,
|
||||||
|
param_name,
|
||||||
|
default_value=default_value,
|
||||||
|
)
|
||||||
assert result == expected_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(
|
@pytest.mark.parametrize(
|
||||||
"param_name, expected_value, expected_result",
|
"param_name, expected_value, expected_result",
|
||||||
[
|
[
|
||||||
@@ -165,35 +135,52 @@ class TestParameterHandling:
|
|||||||
("category", "Windows", False), # Test non-match
|
("category", "Windows", False), # Test non-match
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_v3_parameter_value_matching(self, test_objects, param_name, expected_value, expected_result):
|
def test_v3_parameter_value_matching(
|
||||||
|
self,
|
||||||
|
test_objects,
|
||||||
|
param_name,
|
||||||
|
expected_value,
|
||||||
|
expected_result,
|
||||||
|
):
|
||||||
"""Test parameter value matching in v3 objects."""
|
"""Test parameter value matching in v3 objects."""
|
||||||
_, v3_obj = test_objects
|
v3_obj = test_objects
|
||||||
assert PropertyRules.is_parameter_value(v3_obj, param_name, expected_value) == expected_result
|
assert (
|
||||||
|
PropertyRules.is_parameter_value(
|
||||||
|
v3_obj,
|
||||||
|
param_name,
|
||||||
|
expected_value,
|
||||||
|
)
|
||||||
|
== expected_result
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"comparison_func, param_name, value, expected_result",
|
"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_greater_than,
|
||||||
(PropertyRules.is_parameter_value_in_range, "WALL_ATTR_WIDTH_PARAM", "200,400", True), # Test in range
|
"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_v2_parameter_numeric_comparisons(self, test_objects, comparison_func, param_name, value, expected_result):
|
def test_v3_parameter_numeric_comparisons(
|
||||||
"""Test numeric parameter comparisons in v2 objects."""
|
self, test_objects, comparison_func, param_name, value, expected_result
|
||||||
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."""
|
"""Test numeric parameter comparisons in v3 objects."""
|
||||||
_, v3_obj = test_objects
|
v3_obj = test_objects
|
||||||
assert comparison_func(v3_obj, param_name, value) == expected_result
|
assert comparison_func(v3_obj, param_name, value) == expected_result
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
@@ -205,65 +192,55 @@ class TestParameterHandling:
|
|||||||
("category", "^Windows$", False, False), # Test non-matches
|
("category", "^Windows$", False, False), # Test non-matches
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_v2_parameter_value_like(self, test_objects, param_name, pattern, fuzzy, expected_result):
|
def test_v3_parameter_value_like(
|
||||||
"""Test pattern matching on parameter values in v2 objects."""
|
self, test_objects, param_name, pattern, fuzzy, expected_result
|
||||||
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."""
|
"""Test pattern matching on parameter values in v3 objects."""
|
||||||
_, v3_obj = test_objects
|
v3_obj = test_objects
|
||||||
assert PropertyRules.is_parameter_value_like(v3_obj, param_name, pattern, fuzzy=fuzzy) == expected_result
|
assert (
|
||||||
|
PropertyRules.is_parameter_value_like(
|
||||||
|
v3_obj, param_name, pattern, fuzzy=fuzzy
|
||||||
|
)
|
||||||
|
== expected_result
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"param_name, valid_list, expected_result",
|
"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",
|
||||||
("category", ["Windows", "Doors"], False), # Test value not in list
|
["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):
|
def test_v3_parameter_lists(
|
||||||
"""Test list-based parameter checks in v2 objects."""
|
self,
|
||||||
v2_obj, _ = test_objects
|
test_objects,
|
||||||
assert PropertyRules.is_parameter_value_in_list(v2_obj, param_name, valid_list) == expected_result
|
param_name,
|
||||||
|
valid_list,
|
||||||
@pytest.mark.parametrize(
|
expected_result,
|
||||||
"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."""
|
"""Test list-based parameter checks in v3 objects."""
|
||||||
_, v3_obj = test_objects
|
v3_obj = test_objects
|
||||||
assert PropertyRules.is_parameter_value_in_list(v3_obj, param_name, valid_list) == expected_result
|
assert (
|
||||||
|
PropertyRules.is_parameter_value_in_list(
|
||||||
@pytest.mark.parametrize(
|
v3_obj,
|
||||||
"param_name, expected_result",
|
param_name,
|
||||||
[
|
valid_list,
|
||||||
("WALL_ATTR_ROOM_BOUNDING.value", True), # Test true values
|
)
|
||||||
("wall_top_is_attached", False), # Test false values
|
== expected_result
|
||||||
],
|
)
|
||||||
)
|
|
||||||
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(
|
@pytest.mark.parametrize(
|
||||||
"param_name, expected_result",
|
"param_name, expected_result",
|
||||||
@@ -273,145 +250,275 @@ class TestParameterHandling:
|
|||||||
("Top is Attached", False), # Case sensitivity test
|
("Top is Attached", False), # Case sensitivity test
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_v3_boolean_parameters(self, test_objects, param_name, expected_result):
|
def test_v3_boolean_parameters(
|
||||||
|
self,
|
||||||
|
test_objects,
|
||||||
|
param_name,
|
||||||
|
expected_result,
|
||||||
|
):
|
||||||
"""Test boolean parameter checks in v3 objects."""
|
"""Test boolean parameter checks in v3 objects."""
|
||||||
_, v3_obj = test_objects
|
v3_obj = test_objects
|
||||||
if expected_result:
|
if expected_result:
|
||||||
assert PropertyRules.is_parameter_value_true(v3_obj, param_name)
|
assert PropertyRules.is_parameter_value_true(v3_obj, param_name)
|
||||||
else:
|
else:
|
||||||
assert PropertyRules.is_parameter_value_false(v3_obj, param_name)
|
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(
|
@pytest.mark.parametrize(
|
||||||
"attribute, value, expected",
|
"attribute, value, expected",
|
||||||
[
|
[
|
||||||
# Test numeric value comparisons
|
# Test numeric value comparisons
|
||||||
("Type Parameters.Structure.Fc24 (0).thickness", 300, True),
|
(
|
||||||
("location.length", 5300.000000000002, True),
|
"Type Parameters.Structure.Fc24 (0).thickness",
|
||||||
("location.length", 5300, True),
|
300,
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Instance Parameters.Dimensions.Length",
|
||||||
|
5300.000000000002,
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Instance Parameters.Dimensions.Length",
|
||||||
|
5300,
|
||||||
|
True,
|
||||||
|
),
|
||||||
# Test string value comparisons
|
# Test string value comparisons
|
||||||
("Type Parameters.Text.符号.value", "W30", True),
|
(
|
||||||
("Instance Parameters.Structural.Structural.value", "Yes", True),
|
"Type Parameters.Text.符号.value",
|
||||||
|
"W30",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Instance Parameters.Structural.Structural.value",
|
||||||
|
"Yes",
|
||||||
|
True,
|
||||||
|
),
|
||||||
# Test non-matches
|
# Test non-matches
|
||||||
("Type Parameters.Structure.Fc24 (0).thickness", 301, False),
|
(
|
||||||
("nonexistent_param", "any_value", False),
|
"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):
|
def test_v3_parameter_value_comparisons(
|
||||||
|
self,
|
||||||
|
test_objects,
|
||||||
|
attribute,
|
||||||
|
value,
|
||||||
|
expected,
|
||||||
|
):
|
||||||
"""Test value comparisons using v3 wall parameters."""
|
"""Test value comparisons using v3 wall parameters."""
|
||||||
assert PropertyRules.is_equal_value(v3_wall, attribute, value) == expected
|
assert PropertyRules.is_equal_value(test_objects, attribute, value) == expected
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"wall, attribute, value, expected",
|
"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 tests
|
||||||
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 300, True),
|
(
|
||||||
|
"v3_wall",
|
||||||
|
"Type Parameters.Structure.Fc24 (0).thickness",
|
||||||
|
300,
|
||||||
|
True,
|
||||||
|
),
|
||||||
("v3_wall", "type", "W30(Fc24)", 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",
|
||||||
("v3_wall", "location.length", 5300, False),
|
"Type Parameters.Structure.Fc24 (0).thickness",
|
||||||
|
300.0001,
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"v3_wall",
|
||||||
|
"location.length",
|
||||||
|
5300.000000000002,
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"v3_wall",
|
||||||
|
"location.length",
|
||||||
|
5300,
|
||||||
|
False,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_identical_comparisons(self, request, wall, attribute, value, expected):
|
def test_identical_comparisons(
|
||||||
"""Test identical value comparisons on both wall versions."""
|
self,
|
||||||
wall_instance = request.getfixturevalue(wall)
|
test_objects,
|
||||||
assert PropertyRules.is_identical_value(wall_instance, attribute, value) == expected
|
wall,
|
||||||
|
attribute,
|
||||||
|
value,
|
||||||
|
expected,
|
||||||
|
):
|
||||||
|
"""Test identical value comparisons on v3 wall."""
|
||||||
|
if attribute == "type":
|
||||||
|
# Use case-insensitive comparison for type parameter
|
||||||
|
assert (
|
||||||
|
PropertyRules.is_equal_value(
|
||||||
|
test_objects,
|
||||||
|
attribute,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
== expected
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Use strict comparison for other parameters
|
||||||
|
assert (
|
||||||
|
PropertyRules.is_identical_value(
|
||||||
|
test_objects,
|
||||||
|
attribute,
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
== expected
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"wall, attribute, value",
|
"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 tests
|
||||||
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", 301),
|
(
|
||||||
("v3_wall", "Type Parameters.Text.符号.value", "W31"),
|
"v3_wall",
|
||||||
("v3_wall", "nonexistent_param", "any_value"),
|
"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):
|
def test_not_equal_comparisons(
|
||||||
"""Test not equal comparisons on both wall versions."""
|
self,
|
||||||
wall_instance = request.getfixturevalue(wall)
|
test_objects,
|
||||||
assert PropertyRules.is_not_equal_value(wall_instance, attribute, value)
|
wall,
|
||||||
|
attribute,
|
||||||
|
value,
|
||||||
|
):
|
||||||
|
"""Test not equal comparisons on v3 wall."""
|
||||||
|
assert PropertyRules.is_not_equal_value(test_objects, attribute, value)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"attribute, value, expected_equal, expected_identical",
|
"attribute, value, expected_equal, expected_identical",
|
||||||
[
|
[
|
||||||
# Test Yes/No conversion in equals (should convert)
|
# 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",
|
||||||
("Instance Parameters.Structural.Structural.value", "yes", True, False), # Yes vs "yes"
|
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):
|
def test_boolean_conversions(
|
||||||
|
self,
|
||||||
|
test_objects,
|
||||||
|
attribute,
|
||||||
|
value,
|
||||||
|
expected_equal,
|
||||||
|
expected_identical,
|
||||||
|
):
|
||||||
"""Test conversion of Yes/No strings to boolean values."""
|
"""Test conversion of Yes/No strings to boolean values."""
|
||||||
assert PropertyRules.is_equal_value(v3_wall, attribute, value) == expected_equal
|
assert (
|
||||||
assert PropertyRules.is_identical_value(v3_wall, attribute, value) == expected_identical
|
PropertyRules.is_equal_value(test_objects, attribute, value)
|
||||||
|
== expected_equal
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
PropertyRules.is_identical_value(test_objects, attribute, value)
|
||||||
|
== expected_identical
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"wall, attribute, expected_value",
|
"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 tests
|
||||||
("v3_wall", "Type Parameters.Structure.Fc24 (0).thickness", "300"),
|
(
|
||||||
("v3_wall", "location.length", "5300.000000000002"),
|
"v3_wall",
|
||||||
|
"Type Parameters.Structure.Fc24 (0).thickness",
|
||||||
|
"300",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"v3_wall",
|
||||||
|
"Instance Parameters.Dimensions.Length",
|
||||||
|
"5300.000000000002",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_numeric_string_handling(self, wall, attribute, expected_value, request):
|
def test_numeric_string_handling(
|
||||||
"""Test handling of numeric strings in both wall versions."""
|
self,
|
||||||
wall_instance = request.getfixturevalue(wall) # Retrieve fixture dynamically
|
test_objects,
|
||||||
assert PropertyRules.is_equal_value(wall_instance, attribute, expected_value)
|
wall,
|
||||||
|
attribute,
|
||||||
|
expected_value,
|
||||||
|
):
|
||||||
|
"""Test handling of numeric strings in v3 wall."""
|
||||||
|
assert PropertyRules.is_equal_value(
|
||||||
|
test_objects,
|
||||||
|
attribute,
|
||||||
|
expected_value,
|
||||||
|
)
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"param_name, substring, expected_result",
|
"param_name, substring, expected_result",
|
||||||
[
|
[
|
||||||
("speckle_type", "Revit", True), # Test basic substring match
|
(
|
||||||
("speckle_type", "revit", True), # Test case-insensitive
|
"speckle_type",
|
||||||
("speckle_type", "NotPresent", False), # Test no match
|
"Revit",
|
||||||
("speckle_type", "", True), # Test empty string
|
True,
|
||||||
("non_existent", "anything", False), # Test non-existent parameter
|
), # Should pass as it does not contain Revit
|
||||||
|
(
|
||||||
|
"speckle_type",
|
||||||
|
"NotPresent",
|
||||||
|
True,
|
||||||
|
), # Should pass as it doesn't contain
|
||||||
|
(
|
||||||
|
"speckle_type",
|
||||||
|
"",
|
||||||
|
False,
|
||||||
|
), # Should fail as empty string is contained in any string
|
||||||
|
(
|
||||||
|
"non_existent",
|
||||||
|
"anything",
|
||||||
|
True,
|
||||||
|
), # Should pass as non-existent can't contain
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_parameter_value_contains(self, test_objects, param_name, substring, expected_result):
|
def test_parameter_value_not_contains(
|
||||||
"""Test substring matching on parameter values."""
|
self,
|
||||||
v2_obj, _ = test_objects
|
test_objects,
|
||||||
assert PropertyRules.is_parameter_value_containing(v2_obj, param_name, substring) == expected_result
|
param_name,
|
||||||
|
substring,
|
||||||
@pytest.mark.parametrize(
|
expected_result,
|
||||||
"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."""
|
"""Test negative substring matching on parameter values."""
|
||||||
v2_obj, _ = test_objects
|
v3_obj = test_objects
|
||||||
assert PropertyRules.is_parameter_value_not_containing(v2_obj, param_name, substring) == expected_result
|
assert (
|
||||||
|
PropertyRules.is_parameter_value_not_containing(
|
||||||
|
v3_obj,
|
||||||
|
param_name,
|
||||||
|
substring,
|
||||||
|
)
|
||||||
|
== expected_result
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user