mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Add ifcquery CLI tool for IFC model interrogation
Provides four subcommands for querying IFC models with JSON output: - summary: schema, entity counts, project info - tree: spatial hierarchy (Project > Site > Building > Storey > elements) - info: deep element inspection (attributes, psets, type, material, container) - select: filter elements using selector syntax
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# IfcQuery - IFC model interrogation CLI
|
||||
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
||||
#
|
||||
# This file is part of IfcQuery.
|
||||
#
|
||||
# IfcQuery is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcQuery is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcQuery. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
__version__ = version = "0.0.0"
|
||||
@@ -0,0 +1,123 @@
|
||||
# IfcQuery - IFC model interrogation CLI
|
||||
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
||||
#
|
||||
# This file is part of IfcQuery.
|
||||
#
|
||||
# IfcQuery is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcQuery is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcQuery. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
import ifcopenshell
|
||||
|
||||
from ifcquery import info, select, summary, tree
|
||||
|
||||
|
||||
def parse_element_id(raw: str) -> int:
|
||||
"""Parse an element ID from '#123' or '123' format."""
|
||||
raw = raw.strip().lstrip("#")
|
||||
return int(raw)
|
||||
|
||||
|
||||
def format_output(data, fmt: str) -> str:
|
||||
if fmt == "json":
|
||||
return json.dumps(data, indent=2, ensure_ascii=False)
|
||||
elif fmt == "text":
|
||||
return _format_text(data)
|
||||
return json.dumps(data, indent=2, ensure_ascii=False)
|
||||
|
||||
|
||||
def _format_text(data, indent: int = 0) -> str:
|
||||
prefix = " " * indent
|
||||
lines = []
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
if isinstance(value, (dict, list)):
|
||||
lines.append(f"{prefix}{key}:")
|
||||
lines.append(_format_text(value, indent + 1))
|
||||
else:
|
||||
lines.append(f"{prefix}{key}: {value}")
|
||||
elif isinstance(data, list):
|
||||
for item in data:
|
||||
if isinstance(item, dict):
|
||||
lines.append(_format_text(item, indent))
|
||||
lines.append("")
|
||||
else:
|
||||
lines.append(f"{prefix}- {item}")
|
||||
else:
|
||||
lines.append(f"{prefix}{data}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="ifcquery",
|
||||
description="Query and inspect IFC building models",
|
||||
)
|
||||
parser.add_argument("ifc_file", help="Path to the IFC file")
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
choices=["json", "text"],
|
||||
default="json",
|
||||
dest="output_format",
|
||||
help="Output format (default: json)",
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
subparsers.add_parser("summary", help="Model overview: schema, element counts, project info")
|
||||
|
||||
subparsers.add_parser("tree", help="Spatial hierarchy tree")
|
||||
|
||||
info_parser = subparsers.add_parser("info", help="Deep inspection of a specific element")
|
||||
info_parser.add_argument("element_id", help="Element step ID (e.g. 123 or #123)")
|
||||
|
||||
select_parser = subparsers.add_parser("select", help="Filter elements using selector syntax")
|
||||
select_parser.add_argument("query", help="Selector query string")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
model = ifcopenshell.open(args.ifc_file)
|
||||
except Exception as e:
|
||||
print(f"Error: Could not open IFC file: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.command == "summary":
|
||||
result = summary.summary(model)
|
||||
elif args.command == "tree":
|
||||
result = tree.tree(model)
|
||||
elif args.command == "info":
|
||||
try:
|
||||
element_id = parse_element_id(args.element_id)
|
||||
except ValueError:
|
||||
print(f"Error: Invalid element ID: {args.element_id}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
try:
|
||||
element = model.by_id(element_id)
|
||||
except RuntimeError:
|
||||
print(f"Error: Element #{element_id} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
result = info.info(model, element)
|
||||
elif args.command == "select":
|
||||
result = select.select(model, args.query)
|
||||
|
||||
print(format_output(result, args.output_format))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,117 @@
|
||||
# IfcQuery - IFC model interrogation CLI
|
||||
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
||||
#
|
||||
# This file is part of IfcQuery.
|
||||
#
|
||||
# IfcQuery is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcQuery is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcQuery. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
import ifcopenshell.util.placement
|
||||
|
||||
|
||||
def _serialize_attribute(value: Any) -> Any:
|
||||
"""Convert an IFC attribute value to a JSON-serializable form."""
|
||||
if isinstance(value, ifcopenshell.entity_instance):
|
||||
return {"id": value.id(), "type": value.is_a()}
|
||||
if isinstance(value, tuple):
|
||||
return [_serialize_attribute(v) for v in value]
|
||||
return value
|
||||
|
||||
|
||||
def _material_to_dict(material: ifcopenshell.entity_instance | None) -> dict[str, Any] | None:
|
||||
"""Convert a material entity to a summary dict."""
|
||||
if material is None:
|
||||
return None
|
||||
result: dict[str, Any] = {
|
||||
"id": material.id(),
|
||||
"type": material.is_a(),
|
||||
}
|
||||
if hasattr(material, "Name"):
|
||||
result["name"] = material.Name
|
||||
return result
|
||||
|
||||
|
||||
def info(model: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]:
|
||||
"""Return deep inspection data for an element."""
|
||||
result: dict[str, Any] = {
|
||||
"id": element.id(),
|
||||
"type": element.is_a(),
|
||||
}
|
||||
|
||||
# Direct attributes via get_info() which returns a dict of all attributes
|
||||
element_info = element.get_info()
|
||||
attrs = {}
|
||||
for key, value in element_info.items():
|
||||
if key in ("id", "type"):
|
||||
continue
|
||||
attrs[key] = _serialize_attribute(value)
|
||||
result["attributes"] = attrs
|
||||
|
||||
# Property sets and quantity sets
|
||||
try:
|
||||
psets = ifcopenshell.util.element.get_psets(element)
|
||||
if psets:
|
||||
result["property_sets"] = psets
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Element type
|
||||
try:
|
||||
element_type = ifcopenshell.util.element.get_type(element)
|
||||
if element_type:
|
||||
type_info: dict[str, Any] = {
|
||||
"id": element_type.id(),
|
||||
"type": element_type.is_a(),
|
||||
}
|
||||
if hasattr(element_type, "Name"):
|
||||
type_info["name"] = element_type.Name
|
||||
result["element_type"] = type_info
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Material
|
||||
try:
|
||||
material = ifcopenshell.util.element.get_material(element)
|
||||
mat_dict = _material_to_dict(material)
|
||||
if mat_dict:
|
||||
result["material"] = mat_dict
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Spatial container
|
||||
try:
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if container:
|
||||
result["container"] = {
|
||||
"id": container.id(),
|
||||
"type": container.is_a(),
|
||||
"name": container.Name if hasattr(container, "Name") else None,
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Placement (as 4x4 matrix)
|
||||
try:
|
||||
if hasattr(element, "ObjectPlacement") and element.ObjectPlacement:
|
||||
matrix = ifcopenshell.util.placement.get_local_placement(element.ObjectPlacement)
|
||||
result["placement"] = matrix.tolist()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,39 @@
|
||||
# IfcQuery - IFC model interrogation CLI
|
||||
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
||||
#
|
||||
# This file is part of IfcQuery.
|
||||
#
|
||||
# IfcQuery is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcQuery is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcQuery. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.selector
|
||||
|
||||
|
||||
def select(model: ifcopenshell.file, query: str) -> list[dict[str, Any]]:
|
||||
"""Filter elements using selector syntax and return matching element summaries."""
|
||||
elements = ifcopenshell.util.selector.filter_elements(model, query)
|
||||
results = []
|
||||
for element in sorted(elements, key=lambda e: e.id()):
|
||||
entry: dict[str, Any] = {
|
||||
"id": element.id(),
|
||||
"type": element.is_a(),
|
||||
}
|
||||
if hasattr(element, "Name"):
|
||||
entry["name"] = element.Name
|
||||
results.append(entry)
|
||||
return results
|
||||
@@ -0,0 +1,51 @@
|
||||
# IfcQuery - IFC model interrogation CLI
|
||||
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
||||
#
|
||||
# This file is part of IfcQuery.
|
||||
#
|
||||
# IfcQuery is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcQuery is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcQuery. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from typing import Any
|
||||
|
||||
import ifcopenshell
|
||||
|
||||
|
||||
def summary(model: ifcopenshell.file) -> dict[str, Any]:
|
||||
"""Return a model overview with schema, element counts, and project info."""
|
||||
# Count elements by IFC type, sorted by count descending
|
||||
type_counter: Counter[str] = Counter()
|
||||
total = 0
|
||||
for entity in model:
|
||||
type_counter[entity.is_a()] += 1
|
||||
total += 1
|
||||
|
||||
result: dict[str, Any] = {
|
||||
"schema": model.schema,
|
||||
"total_entities": total,
|
||||
}
|
||||
|
||||
projects = model.by_type("IfcProject")
|
||||
if projects:
|
||||
project = projects[0]
|
||||
result["project"] = {
|
||||
"id": project.id(),
|
||||
"name": project.Name,
|
||||
"description": project.Description,
|
||||
}
|
||||
|
||||
result["types"] = dict(type_counter.most_common())
|
||||
return result
|
||||
@@ -0,0 +1,67 @@
|
||||
# IfcQuery - IFC model interrogation CLI
|
||||
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
||||
#
|
||||
# This file is part of IfcQuery.
|
||||
#
|
||||
# IfcQuery is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# IfcQuery is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with IfcQuery. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
def _element_summary(element: ifcopenshell.entity_instance) -> dict[str, Any]:
|
||||
"""Return a minimal summary dict for an element."""
|
||||
return {
|
||||
"id": element.id(),
|
||||
"type": element.is_a(),
|
||||
"name": element.Name if hasattr(element, "Name") else None,
|
||||
}
|
||||
|
||||
|
||||
def _build_spatial_node(element: ifcopenshell.entity_instance) -> dict[str, Any]:
|
||||
"""Recursively build a spatial tree node."""
|
||||
node = _element_summary(element)
|
||||
|
||||
# Get aggregated children (Site in Project, Building in Site, Storey in Building, etc.)
|
||||
aggregates = []
|
||||
for rel in getattr(element, "IsDecomposedBy", []):
|
||||
for child in rel.RelatedObjects:
|
||||
aggregates.append(_build_spatial_node(child))
|
||||
|
||||
# Get contained elements (walls, slabs, etc. in a storey/space)
|
||||
contained = []
|
||||
for rel in getattr(element, "ContainsElements", []):
|
||||
for child in rel.RelatedElements:
|
||||
contained.append(_element_summary(child))
|
||||
|
||||
if aggregates:
|
||||
node["children"] = aggregates
|
||||
if contained:
|
||||
node["elements"] = contained
|
||||
|
||||
return node
|
||||
|
||||
|
||||
def tree(model: ifcopenshell.file) -> dict[str, Any] | list[dict[str, Any]]:
|
||||
"""Return the spatial hierarchy tree starting from IfcProject."""
|
||||
projects = model.by_type("IfcProject")
|
||||
if not projects:
|
||||
return {"error": "No IfcProject found in model"}
|
||||
if len(projects) == 1:
|
||||
return _build_spatial_node(projects[0])
|
||||
return [_build_spatial_node(p) for p in projects]
|
||||
@@ -0,0 +1,30 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "ifcquery"
|
||||
version = "0.0.0"
|
||||
authors = [
|
||||
{ name="Bruno Postle", email="bruno@postle.net" },
|
||||
]
|
||||
description = "CLI tool for querying and inspecting IFC building models"
|
||||
readme = "README.md"
|
||||
keywords = ["IFC", "BIM", "Query"]
|
||||
classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)",
|
||||
]
|
||||
dependencies = ["ifcopenshell"]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "http://ifcopenshell.org"
|
||||
Documentation = "https://docs.ifcopenshell.org"
|
||||
Issues = "https://github.com/IfcOpenShell/IfcOpenShell/issues"
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["ifcquery*"]
|
||||
exclude = ["test*"]
|
||||
|
||||
[tool.ruff]
|
||||
extend = "../../pyproject.toml"
|
||||
@@ -0,0 +1,36 @@
|
||||
import pytest
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.aggregate
|
||||
import ifcopenshell.api.owner.settings
|
||||
import ifcopenshell.api.project
|
||||
import ifcopenshell.api.root
|
||||
import ifcopenshell.api.spatial
|
||||
import ifcopenshell.api.unit
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def model():
|
||||
"""Create an IFC4 model with a spatial hierarchy and a wall."""
|
||||
f = ifcopenshell.api.project.create_file()
|
||||
ifcopenshell.api.owner.settings.get_user = lambda ifc: (ifc.by_type("IfcPersonAndOrganization") or [None])[0]
|
||||
ifcopenshell.api.owner.settings.get_application = lambda ifc: (ifc.by_type("IfcApplication") or [None])[0]
|
||||
|
||||
project = ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject", name="TestProject")
|
||||
ifcopenshell.api.unit.assign_unit(f)
|
||||
|
||||
site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite", name="TestSite")
|
||||
building = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuilding", name="TestBuilding")
|
||||
storey = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingStorey", name="Ground Floor")
|
||||
|
||||
ifcopenshell.api.aggregate.assign_object(f, products=[site], relating_object=project)
|
||||
ifcopenshell.api.aggregate.assign_object(f, products=[building], relating_object=site)
|
||||
ifcopenshell.api.aggregate.assign_object(f, products=[storey], relating_object=building)
|
||||
|
||||
wall = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wall001")
|
||||
ifcopenshell.api.spatial.assign_container(f, products=[wall], relating_structure=storey)
|
||||
|
||||
slab = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSlab", name="Slab001")
|
||||
ifcopenshell.api.spatial.assign_container(f, products=[slab], relating_structure=storey)
|
||||
|
||||
return f
|
||||
@@ -0,0 +1,31 @@
|
||||
from ifcquery.info import info
|
||||
|
||||
|
||||
class TestInfo:
|
||||
def test_basic_attributes(self, model):
|
||||
wall = model.by_type("IfcWall")[0]
|
||||
result = info(model, wall)
|
||||
assert result["id"] == wall.id()
|
||||
assert result["type"] == "IfcWall"
|
||||
assert result["attributes"]["Name"] == "Wall001"
|
||||
|
||||
def test_container(self, model):
|
||||
wall = model.by_type("IfcWall")[0]
|
||||
result = info(model, wall)
|
||||
assert result["container"]["type"] == "IfcBuildingStorey"
|
||||
assert result["container"]["name"] == "Ground Floor"
|
||||
|
||||
def test_project_info(self, model):
|
||||
project = model.by_type("IfcProject")[0]
|
||||
result = info(model, project)
|
||||
assert result["type"] == "IfcProject"
|
||||
assert result["attributes"]["Name"] == "TestProject"
|
||||
|
||||
def test_all_attributes_serializable(self, model):
|
||||
"""All attribute values should be JSON-serializable (no entity instances)."""
|
||||
import json
|
||||
|
||||
wall = model.by_type("IfcWall")[0]
|
||||
result = info(model, wall)
|
||||
# Should not raise
|
||||
json.dumps(result)
|
||||
@@ -0,0 +1,84 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.project
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ifc_path(model):
|
||||
"""Write the model fixture to a temp file and return its path."""
|
||||
with tempfile.NamedTemporaryFile(suffix=".ifc", delete=False) as f:
|
||||
model.write(f.name)
|
||||
yield f.name
|
||||
os.unlink(f.name)
|
||||
|
||||
|
||||
def run_ifcquery(*args):
|
||||
"""Run ifcquery as a subprocess and return (returncode, stdout, stderr)."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-m", "ifcquery", *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.returncode, result.stdout, result.stderr
|
||||
|
||||
|
||||
class TestCLI:
|
||||
def test_summary_json(self, ifc_path):
|
||||
rc, stdout, stderr = run_ifcquery(ifc_path, "summary")
|
||||
assert rc == 0
|
||||
data = json.loads(stdout)
|
||||
assert data["schema"] == "IFC4"
|
||||
assert "types" in data
|
||||
|
||||
def test_tree_json(self, ifc_path):
|
||||
rc, stdout, stderr = run_ifcquery(ifc_path, "tree")
|
||||
assert rc == 0
|
||||
data = json.loads(stdout)
|
||||
assert data["type"] == "IfcProject"
|
||||
|
||||
def test_info_json(self, ifc_path, model):
|
||||
wall = model.by_type("IfcWall")[0]
|
||||
rc, stdout, stderr = run_ifcquery(ifc_path, "info", str(wall.id()))
|
||||
assert rc == 0
|
||||
data = json.loads(stdout)
|
||||
assert data["type"] == "IfcWall"
|
||||
|
||||
def test_info_hash_id(self, ifc_path, model):
|
||||
wall = model.by_type("IfcWall")[0]
|
||||
rc, stdout, stderr = run_ifcquery(ifc_path, "info", f"#{wall.id()}")
|
||||
assert rc == 0
|
||||
data = json.loads(stdout)
|
||||
assert data["type"] == "IfcWall"
|
||||
|
||||
def test_select_json(self, ifc_path):
|
||||
rc, stdout, stderr = run_ifcquery(ifc_path, "select", "IfcWall")
|
||||
assert rc == 0
|
||||
data = json.loads(stdout)
|
||||
assert len(data) == 1
|
||||
assert data[0]["type"] == "IfcWall"
|
||||
|
||||
def test_text_format(self, ifc_path):
|
||||
rc, stdout, stderr = run_ifcquery(ifc_path, "--format", "text", "summary")
|
||||
assert rc == 0
|
||||
assert "schema:" in stdout
|
||||
|
||||
def test_bad_file(self):
|
||||
rc, stdout, stderr = run_ifcquery("/nonexistent.ifc", "summary")
|
||||
assert rc != 0
|
||||
assert "Error" in stderr
|
||||
|
||||
def test_bad_element_id(self, ifc_path):
|
||||
rc, stdout, stderr = run_ifcquery(ifc_path, "info", "999999")
|
||||
assert rc != 0
|
||||
assert "Error" in stderr
|
||||
|
||||
def test_no_command(self, ifc_path):
|
||||
rc, stdout, stderr = run_ifcquery(ifc_path)
|
||||
assert rc != 0
|
||||
@@ -0,0 +1,31 @@
|
||||
from ifcquery.select import select
|
||||
|
||||
|
||||
class TestSelect:
|
||||
def test_select_by_type(self, model):
|
||||
result = select(model, "IfcWall")
|
||||
assert len(result) == 1
|
||||
assert result[0]["type"] == "IfcWall"
|
||||
assert result[0]["name"] == "Wall001"
|
||||
|
||||
def test_select_multiple_types(self, model):
|
||||
result = select(model, "IfcWall, IfcSlab")
|
||||
assert len(result) == 2
|
||||
types = {r["type"] for r in result}
|
||||
assert types == {"IfcWall", "IfcSlab"}
|
||||
|
||||
def test_select_no_match(self, model):
|
||||
result = select(model, "IfcDoor")
|
||||
assert result == []
|
||||
|
||||
def test_results_sorted_by_id(self, model):
|
||||
result = select(model, "IfcWall, IfcSlab")
|
||||
ids = [r["id"] for r in result]
|
||||
assert ids == sorted(ids)
|
||||
|
||||
def test_result_has_id_type_name(self, model):
|
||||
result = select(model, "IfcWall")
|
||||
entry = result[0]
|
||||
assert "id" in entry
|
||||
assert "type" in entry
|
||||
assert "name" in entry
|
||||
@@ -0,0 +1,33 @@
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.project
|
||||
|
||||
from ifcquery.summary import summary
|
||||
|
||||
|
||||
class TestSummary:
|
||||
def test_schema(self, model):
|
||||
result = summary(model)
|
||||
assert result["schema"] == "IFC4"
|
||||
|
||||
def test_total_entities(self, model):
|
||||
result = summary(model)
|
||||
assert result["total_entities"] == len(list(model))
|
||||
assert result["total_entities"] > 0
|
||||
|
||||
def test_project_info(self, model):
|
||||
result = summary(model)
|
||||
assert result["project"]["name"] == "TestProject"
|
||||
|
||||
def test_type_counts(self, model):
|
||||
result = summary(model)
|
||||
types = result["types"]
|
||||
assert "IfcWall" in types
|
||||
assert types["IfcWall"] == 1
|
||||
assert "IfcSlab" in types
|
||||
assert types["IfcSlab"] == 1
|
||||
|
||||
def test_empty_model(self):
|
||||
f = ifcopenshell.api.project.create_file()
|
||||
result = summary(f)
|
||||
assert result["schema"] == "IFC4"
|
||||
assert "project" not in result
|
||||
@@ -0,0 +1,36 @@
|
||||
from ifcquery.tree import tree
|
||||
|
||||
|
||||
class TestTree:
|
||||
def test_root_is_project(self, model):
|
||||
result = tree(model)
|
||||
assert result["type"] == "IfcProject"
|
||||
assert result["name"] == "TestProject"
|
||||
|
||||
def test_spatial_hierarchy(self, model):
|
||||
result = tree(model)
|
||||
# Project > Site > Building > Storey
|
||||
site = result["children"][0]
|
||||
assert site["type"] == "IfcSite"
|
||||
assert site["name"] == "TestSite"
|
||||
|
||||
building = site["children"][0]
|
||||
assert building["type"] == "IfcBuilding"
|
||||
assert building["name"] == "TestBuilding"
|
||||
|
||||
storey = building["children"][0]
|
||||
assert storey["type"] == "IfcBuildingStorey"
|
||||
assert storey["name"] == "Ground Floor"
|
||||
|
||||
def test_contained_elements(self, model):
|
||||
result = tree(model)
|
||||
storey = result["children"][0]["children"][0]["children"][0]
|
||||
elements = storey["elements"]
|
||||
element_types = {e["type"] for e in elements}
|
||||
assert "IfcWall" in element_types
|
||||
assert "IfcSlab" in element_types
|
||||
|
||||
def test_element_ids_present(self, model):
|
||||
result = tree(model)
|
||||
assert "id" in result
|
||||
assert isinstance(result["id"], int)
|
||||
Reference in New Issue
Block a user