mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-23 10:12:44 +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