mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-26 10:11:46 +00:00
ifcmcp: source tool descriptions from ifcquery/ifcedit instead of duplicating them
Alternative to #8955, for #8951 (23 of 25 ifcmcp tools reach MCP clients with an empty description because FastMCP reads each wrapper's own __doc__, and the server.py wrappers had none). #8955 fixes this by hand-writing a new docstring directly onto each server.py wrapper. Most of those wrappers are thin passthroughs to IfcSession methods in core.py, which already had short docstrings, which themselves mostly delegate to already-documented ifcquery/ifcedit functions -- so that fix tripled up content across three layers that can drift out of sync. This instead enriches the true source (the ifcquery/ifcedit library functions, useful independently of MCP) and has core.py's IfcSession methods copy __doc__ from their delegate via a small _use_doc() decorator, and server.py's tool registration pull description= from the matching IfcSession method. Methods that aren't pure passthroughs (session lifecycle, generic API/shape dispatch) keep their own hand-written docs. Keeps #8955's regression test. Generated with the assistance of an AI coding tool.
This commit is contained in:
committed by
Thomas Krijnen
parent
e077390e3d
commit
6f3acc84ee
@@ -94,9 +94,15 @@ def list_functions(module: str) -> list[dict]:
|
|||||||
|
|
||||||
|
|
||||||
def function_docs(module: str, function: str) -> dict:
|
def function_docs(module: str, function: str) -> dict:
|
||||||
"""Full documentation for a single API function.
|
"""Show the full documentation for one ifcopenshell.api function.
|
||||||
|
|
||||||
Returns a dict with: module, function, description, params (with types/defaults/descriptions), return_type
|
Returns the summary and long description, every parameter with its type,
|
||||||
|
default and description, and the return type. Read this before calling
|
||||||
|
``run_api()`` so that parameter names and value types are correct.
|
||||||
|
|
||||||
|
:param module: API module name, for example ``'root'``.
|
||||||
|
:param function: Function name within the module, for example
|
||||||
|
``'create_entity'``.
|
||||||
"""
|
"""
|
||||||
fn = _get_underlying_function(module, function)
|
fn = _get_underlying_function(module, function)
|
||||||
if fn is None:
|
if fn is None:
|
||||||
|
|||||||
@@ -14,10 +14,21 @@ def list_rules() -> list[dict[str, str]]:
|
|||||||
|
|
||||||
|
|
||||||
def run_quantify(model: ifcopenshell.file, rule: str, selector: str | None = None) -> dict[str, Any]:
|
def run_quantify(model: ifcopenshell.file, rule: str, selector: str | None = None) -> dict[str, Any]:
|
||||||
"""Run quantity take-off on the model using the named rule.
|
"""Compute base quantities for elements and write them into the model.
|
||||||
|
|
||||||
Modifies the model in-place by adding/updating IfcElementQuantity psets.
|
This is a write operation: it derives lengths, areas and volumes from
|
||||||
Returns a summary dict with ok, rule, and elements_quantified.
|
element geometry and adds or updates their ``IfcElementQuantity`` sets.
|
||||||
|
It does not report a schedule — see ``ifcquery.schedule()`` for the
|
||||||
|
construction programme and ``ifcquery.cost()`` for cost schedules. An
|
||||||
|
unrecognised ``rule`` is reported as an error listing the rules that are
|
||||||
|
available.
|
||||||
|
|
||||||
|
:param model: The in-memory IFC model. Modified in-place.
|
||||||
|
:param rule: Quantity take-off rule set, for example
|
||||||
|
``'IFC4QtoBaseQuantities'`` or ``'IFC4X3QtoBaseQuantities'``.
|
||||||
|
:param selector: ifcopenshell selector restricting which elements are
|
||||||
|
measured, e.g. ``'IfcWall'``. Omit to measure every ``IfcElement`` and
|
||||||
|
``IfcSpace``.
|
||||||
"""
|
"""
|
||||||
from ifc5d.qto import edit_qtos, quantify
|
from ifc5d.qto import edit_qtos, quantify
|
||||||
from ifc5d.qto import rules as rule_sets
|
from ifc5d.qto import rules as rule_sets
|
||||||
|
|||||||
+133
-45
@@ -35,6 +35,27 @@ from ifcquery import (
|
|||||||
from ifcquery import validate as validate_mod
|
from ifcquery import validate as validate_mod
|
||||||
|
|
||||||
|
|
||||||
|
def _use_doc(source: Callable, extra: str = "") -> Callable:
|
||||||
|
"""Decorator: copy `source`'s docstring onto the decorated method.
|
||||||
|
|
||||||
|
Keeps the query/edit logic in ``ifcquery``/``ifcedit`` as the single
|
||||||
|
source of truth for what a delegating ``IfcSession`` method does, rather
|
||||||
|
than maintaining a second prose description here. Only ``__doc__`` is
|
||||||
|
copied — unlike `functools.wraps`, this leaves the method's own signature
|
||||||
|
(and MCP tool schema derived from it) untouched.
|
||||||
|
|
||||||
|
:param extra: Optional session-specific note appended after `source`'s
|
||||||
|
docstring, for the handful of methods that translate an argument
|
||||||
|
(e.g. a JSON/MCP-friendly default) before delegating.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(fn: Callable) -> Callable:
|
||||||
|
fn.__doc__ = (source.__doc__ or "").rstrip() + extra
|
||||||
|
return fn
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def _jsonify(x: Any) -> Any:
|
def _jsonify(x: Any) -> Any:
|
||||||
"""Convert IfcOpenShell objects / iterables into JSON-safe primitives."""
|
"""Convert IfcOpenShell objects / iterables into JSON-safe primitives."""
|
||||||
if x is None or isinstance(x, (str, int, float, bool)):
|
if x is None or isinstance(x, (str, int, float, bool)):
|
||||||
@@ -231,20 +252,47 @@ class IfcSession:
|
|||||||
return self.model
|
return self.model
|
||||||
|
|
||||||
def ifc_new(self, schema: str = "IFC4") -> dict[str, Any]:
|
def ifc_new(self, schema: str = "IFC4") -> dict[str, Any]:
|
||||||
"""Create a new empty IFC model in memory."""
|
"""Create a new empty IFC model in memory.
|
||||||
|
|
||||||
|
Replaces the model currently held by the session, discarding any unsaved
|
||||||
|
edits. The new model has no file path of its own, so ``ifc_save`` must be
|
||||||
|
given an explicit path.
|
||||||
|
|
||||||
|
:param schema: IFC schema version — ``IFC2X3``, ``IFC4``, ``IFC4X1``,
|
||||||
|
``IFC4X2`` or ``IFC4X3`` — passed straight to ``ifcopenshell.file()``
|
||||||
|
(default ``IFC4``). ``IFC4X3_ADD2`` is also accepted and, like
|
||||||
|
``IFC4X3``, produces a model whose ``schema`` reports ``IFC4X3``.
|
||||||
|
"""
|
||||||
self.model = ifcopenshell.file(schema=schema)
|
self.model = ifcopenshell.file(schema=schema)
|
||||||
self.model_path = None
|
self.model_path = None
|
||||||
return {"ok": True, "schema": self.model.schema, "entities": sum(1 for _ in self.model)}
|
return {"ok": True, "schema": self.model.schema, "entities": sum(1 for _ in self.model)}
|
||||||
|
|
||||||
def ifc_load(self, path: str) -> str:
|
def ifc_load(self, path: str) -> str:
|
||||||
"""Open an IFC file into memory. Returns confirmation string."""
|
"""Open an IFC file from disk into the session.
|
||||||
|
|
||||||
|
Replaces the model currently held by the session, discarding any unsaved
|
||||||
|
edits, and remembers the path so a later ``ifc_save`` can overwrite it.
|
||||||
|
Call this before any query or edit method. Returns a confirmation string
|
||||||
|
naming the schema version and entity count.
|
||||||
|
|
||||||
|
:param path: Filesystem path of the IFC file to open.
|
||||||
|
"""
|
||||||
self.model = ifcopenshell.open(path)
|
self.model = ifcopenshell.open(path)
|
||||||
self.model_path = path
|
self.model_path = path
|
||||||
count = sum(1 for _ in self.model)
|
count = sum(1 for _ in self.model)
|
||||||
return f"Loaded {path}: schema {self.model.schema}, {count} entities"
|
return f"Loaded {path}: schema {self.model.schema}, {count} entities"
|
||||||
|
|
||||||
def ifc_save(self, path: str = "") -> str:
|
def ifc_save(self, path: str = "") -> str:
|
||||||
"""Write the in-memory model to disk. Empty path overwrites the original file."""
|
"""Write the in-memory model to disk.
|
||||||
|
|
||||||
|
Overwrites the target file without further confirmation. Edits made by
|
||||||
|
``ifc_edit``, ``ifc_shape`` and ``ifc_quantify`` exist only in memory
|
||||||
|
until this is called.
|
||||||
|
|
||||||
|
:param path: Destination path. Omit to overwrite the file the model was
|
||||||
|
loaded from; this fails for a model created by ``ifc_new``, which has
|
||||||
|
no original path.
|
||||||
|
"""
|
||||||
model = self._require_model()
|
model = self._require_model()
|
||||||
target = path if path else self.model_path
|
target = path if path else self.model_path
|
||||||
if not target:
|
if not target:
|
||||||
@@ -253,7 +301,11 @@ class IfcSession:
|
|||||||
return f"Saved to {target}"
|
return f"Saved to {target}"
|
||||||
|
|
||||||
def ifc_reset(self) -> dict[str, Any]:
|
def ifc_reset(self) -> dict[str, Any]:
|
||||||
"""Drop the in-memory model."""
|
"""Discard the in-memory model.
|
||||||
|
|
||||||
|
Drops the model and its file path, throwing away any edits not already
|
||||||
|
written with ``ifc_save``. Succeeds even when no model is loaded.
|
||||||
|
"""
|
||||||
self.model = None
|
self.model = None
|
||||||
self.model_path = None
|
self.model_path = None
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
@@ -261,39 +313,42 @@ class IfcSession:
|
|||||||
# -------------
|
# -------------
|
||||||
# Query tools
|
# Query tools
|
||||||
# -------------
|
# -------------
|
||||||
|
@_use_doc(summary.summary)
|
||||||
def ifc_summary(self) -> dict[str, Any]:
|
def ifc_summary(self) -> dict[str, Any]:
|
||||||
"""Model overview: schema, entity counts, project info."""
|
|
||||||
return summary.summary(self._require_model())
|
return summary.summary(self._require_model())
|
||||||
|
|
||||||
|
@_use_doc(tree.tree)
|
||||||
def ifc_tree(self) -> dict[str, Any] | list[dict[str, Any]]:
|
def ifc_tree(self) -> dict[str, Any] | list[dict[str, Any]]:
|
||||||
"""Full spatial hierarchy tree (Project -> Site -> Building -> Storeys -> Elements)."""
|
|
||||||
return tree.tree(self._require_model())
|
return tree.tree(self._require_model())
|
||||||
|
|
||||||
|
@_use_doc(info.info)
|
||||||
def ifc_info(self, element_id: int) -> dict[str, Any]:
|
def ifc_info(self, element_id: int) -> dict[str, Any]:
|
||||||
"""Deep inspection of an entity by step ID (attributes, psets, placement, type, material)."""
|
|
||||||
model = self._require_model()
|
model = self._require_model()
|
||||||
element = model.by_id(element_id)
|
element = model.by_id(element_id)
|
||||||
if element is None:
|
if element is None:
|
||||||
raise IfcSessionError(f"Element #{element_id} not found.")
|
raise IfcSessionError(f"Element #{element_id} not found.")
|
||||||
return info.info(model, element)
|
return info.info(model, element)
|
||||||
|
|
||||||
|
@_use_doc(select.select)
|
||||||
def ifc_select(self, query: str) -> list[dict[str, Any]]:
|
def ifc_select(self, query: str) -> list[dict[str, Any]]:
|
||||||
"""Filter elements using ifcopenshell selector syntax.
|
|
||||||
|
|
||||||
Examples: ``IfcWall``, ``IfcWall, IfcColumn``, ``! IfcWall``,
|
|
||||||
``IfcWall, Name = "My Wall"``, ``type = "Concrete Wall"``,
|
|
||||||
``material = "Concrete"``.
|
|
||||||
"""
|
|
||||||
return select.select(self._require_model(), query)
|
return select.select(self._require_model(), query)
|
||||||
|
|
||||||
|
@_use_doc(relations.relations)
|
||||||
def ifc_relations(self, element_id: int, traverse: str = "") -> dict[str, Any] | list[dict[str, Any]]:
|
def ifc_relations(self, element_id: int, traverse: str = "") -> dict[str, Any] | list[dict[str, Any]]:
|
||||||
"""Show relationships for an element. Set traverse='up' to walk hierarchy to IfcProject."""
|
|
||||||
model = self._require_model()
|
model = self._require_model()
|
||||||
element = model.by_id(element_id)
|
element = model.by_id(element_id)
|
||||||
if element is None:
|
if element is None:
|
||||||
raise IfcSessionError(f"Element #{element_id} not found.")
|
raise IfcSessionError(f"Element #{element_id} not found.")
|
||||||
return relations.relations(model, element, traverse=traverse if traverse else None)
|
return relations.relations(model, element, traverse=traverse if traverse else None)
|
||||||
|
|
||||||
|
@_use_doc(
|
||||||
|
clash_mod.clash,
|
||||||
|
extra=(
|
||||||
|
"\n\nNote: this method takes a plain ``clearance: float`` rather than\n"
|
||||||
|
'``clearance: float | None`` — ``0.0`` (the default) means "skip the\n'
|
||||||
|
'clearance check", matching ``None`` in ``ifcquery.clash.clash()``.'
|
||||||
|
),
|
||||||
|
)
|
||||||
def ifc_clash(
|
def ifc_clash(
|
||||||
self,
|
self,
|
||||||
element_id: int,
|
element_id: int,
|
||||||
@@ -301,7 +356,6 @@ class IfcSession:
|
|||||||
tolerance: float = 0.002,
|
tolerance: float = 0.002,
|
||||||
scope: str = "storey",
|
scope: str = "storey",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Check element for geometric clashes. clearance=0.0 means no clearance check."""
|
|
||||||
model = self._require_model()
|
model = self._require_model()
|
||||||
element = model.by_id(element_id)
|
element = model.by_id(element_id)
|
||||||
if element is None:
|
if element is None:
|
||||||
@@ -314,33 +368,53 @@ class IfcSession:
|
|||||||
scope=scope,
|
scope=scope,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@_use_doc(contexts_mod.contexts)
|
||||||
def ifc_contexts(self) -> list[dict[str, Any]]:
|
def ifc_contexts(self) -> list[dict[str, Any]]:
|
||||||
"""List all geometric representation contexts and subcontexts with their step IDs."""
|
|
||||||
return contexts_mod.contexts(self._require_model())
|
return contexts_mod.contexts(self._require_model())
|
||||||
|
|
||||||
|
@_use_doc(materials_mod.materials)
|
||||||
def ifc_materials(self) -> list[dict[str, Any]]:
|
def ifc_materials(self) -> list[dict[str, Any]]:
|
||||||
"""List all materials and material sets (layers, constituents, profiles)."""
|
|
||||||
return materials_mod.materials(self._require_model())
|
return materials_mod.materials(self._require_model())
|
||||||
|
|
||||||
# ------------------------
|
# ------------------------
|
||||||
# Edit discovery + execute
|
# Edit discovery + execute
|
||||||
# ------------------------
|
# ------------------------
|
||||||
def ifc_list(self, module: str = "") -> list[dict]:
|
def ifc_list(self, module: str = "") -> list[dict]:
|
||||||
"""List all API modules, or functions within a module. Empty module = all modules."""
|
"""Discover the ifcopenshell.api functions available for editing.
|
||||||
|
|
||||||
|
With no argument returns every API module with its description,
|
||||||
|
function names and function count. With a module name returns that
|
||||||
|
module's functions, each with a one-line description and its
|
||||||
|
parameters. This is the starting point for ``ifc_docs`` and
|
||||||
|
``ifc_edit``; it inspects the installed ifcopenshell package and works
|
||||||
|
without a model loaded.
|
||||||
|
|
||||||
|
:param module: API module name, for example ``'root'``, ``'geometry'``
|
||||||
|
or ``'pset'``. Omit to list all modules.
|
||||||
|
"""
|
||||||
return list_functions(module) if module else list_modules()
|
return list_functions(module) if module else list_modules()
|
||||||
|
|
||||||
|
@_use_doc(function_docs)
|
||||||
def ifc_docs(self, function_path: str) -> dict:
|
def ifc_docs(self, function_path: str) -> dict:
|
||||||
"""Show full documentation for an API function. Input format: 'module.function'."""
|
|
||||||
module, function = function_path.split(".", 1)
|
module, function = function_path.split(".", 1)
|
||||||
return function_docs(module, function)
|
return function_docs(module, function)
|
||||||
|
|
||||||
def ifc_edit(self, function_path: str, params: Any = "{}") -> dict:
|
def ifc_edit(self, function_path: str, params: Any = "{}") -> dict:
|
||||||
"""Execute an ifcopenshell.api mutation.
|
"""Run an ifcopenshell.api function to modify the model.
|
||||||
|
|
||||||
params may be:
|
This is the general-purpose edit method; use ``ifc_list`` and
|
||||||
- JSON string
|
``ifc_docs`` first to find the function and its parameters. Changes
|
||||||
- dict (from tool calling / JS)
|
are made to the in-memory model only, so ``ifc_save`` is needed to
|
||||||
- JsProxy (handled upstream in embedded.py)
|
persist them. Returns ``{"ok": True, "result": ...}``, or
|
||||||
|
``{"ok": False, "error": ...}`` when the function is unknown, a
|
||||||
|
parameter cannot be converted, or the call raises.
|
||||||
|
|
||||||
|
:param function_path: ``'module.function'``, for example
|
||||||
|
``'root.create_entity'``.
|
||||||
|
:param params: Keyword arguments as a JSON string, a dict (tool
|
||||||
|
calling) or a JsProxy (handled upstream in embedded.py). Pass
|
||||||
|
entity references as integer step IDs, and arguments typed as an
|
||||||
|
IFC file as a file path string.
|
||||||
"""
|
"""
|
||||||
model = self._require_model()
|
model = self._require_model()
|
||||||
module, function = function_path.split(".", 1)
|
module, function = function_path.split(".", 1)
|
||||||
@@ -359,28 +433,20 @@ class IfcSession:
|
|||||||
# ------------------------
|
# ------------------------
|
||||||
# Extended query + edit tools
|
# Extended query + edit tools
|
||||||
# ------------------------
|
# ------------------------
|
||||||
|
@_use_doc(validate_mod.validate)
|
||||||
def ifc_validate(self, express_rules: bool = False) -> dict[str, Any]:
|
def ifc_validate(self, express_rules: bool = False) -> dict[str, Any]:
|
||||||
"""Validate the loaded model. Returns {'valid': bool, 'issues': [...]}."""
|
|
||||||
return validate_mod.validate(self._require_model(), express_rules=express_rules)
|
return validate_mod.validate(self._require_model(), express_rules=express_rules)
|
||||||
|
|
||||||
|
@_use_doc(schedule.schedule)
|
||||||
def ifc_schedule(self, max_depth: int | None = None) -> list[dict[str, Any]]:
|
def ifc_schedule(self, max_depth: int | None = None) -> list[dict[str, Any]]:
|
||||||
"""List work schedules and nested tasks from the model.
|
|
||||||
|
|
||||||
max_depth limits subtask expansion (None = unlimited). At the cutoff,
|
|
||||||
subtasks is replaced with {"truncated": True, "count": N}.
|
|
||||||
"""
|
|
||||||
return schedule.schedule(self._require_model(), max_depth=max_depth)
|
return schedule.schedule(self._require_model(), max_depth=max_depth)
|
||||||
|
|
||||||
|
@_use_doc(cost_mod.cost)
|
||||||
def ifc_cost(self, max_depth: int | None = None) -> list[dict[str, Any]]:
|
def ifc_cost(self, max_depth: int | None = None) -> list[dict[str, Any]]:
|
||||||
"""List cost schedules and nested cost items from the model.
|
|
||||||
|
|
||||||
max_depth limits cost item expansion (None = unlimited). At the cutoff,
|
|
||||||
subitems is replaced with {"truncated": True, "count": N}.
|
|
||||||
"""
|
|
||||||
return cost_mod.cost(self._require_model(), max_depth=max_depth)
|
return cost_mod.cost(self._require_model(), max_depth=max_depth)
|
||||||
|
|
||||||
|
@_use_doc(schema.schema)
|
||||||
def ifc_schema(self, entity_type: str) -> dict[str, Any]:
|
def ifc_schema(self, entity_type: str) -> dict[str, Any]:
|
||||||
"""Return IFC class documentation for entity_type using the model's schema version."""
|
|
||||||
return schema.schema(self._require_model(), entity_type)
|
return schema.schema(self._require_model(), entity_type)
|
||||||
|
|
||||||
def ifc_plot(
|
def ifc_plot(
|
||||||
@@ -456,18 +522,43 @@ class IfcSession:
|
|||||||
# Shape builder tools
|
# Shape builder tools
|
||||||
# ------------------------
|
# ------------------------
|
||||||
def ifc_shape_list(self) -> list[dict]:
|
def ifc_shape_list(self) -> list[dict]:
|
||||||
"""List all ShapeBuilder geometry methods with one-line descriptions and parameter names."""
|
"""List the ShapeBuilder methods available for constructing geometry.
|
||||||
|
|
||||||
|
Returns every public ``ifcopenshell.util.shape_builder.ShapeBuilder``
|
||||||
|
method with a one-line description and its parameter names, read
|
||||||
|
directly from that class's own docstrings. Use it to find a method,
|
||||||
|
then ``ifc_shape_docs`` for the details and ``ifc_shape`` to call it.
|
||||||
|
Works without a model loaded.
|
||||||
|
"""
|
||||||
return _list_shape_methods()
|
return _list_shape_methods()
|
||||||
|
|
||||||
def ifc_shape_docs(self, method: str) -> dict:
|
def ifc_shape_docs(self, method: str) -> dict:
|
||||||
"""Full documentation for a ShapeBuilder method: params, types, return value."""
|
"""Show the full documentation for one ShapeBuilder method.
|
||||||
|
|
||||||
|
Returns the summary and long description, every parameter with its
|
||||||
|
type and default, and the return type — read directly from
|
||||||
|
``ShapeBuilder``'s own docstring. Read this before ``ifc_shape`` so
|
||||||
|
that argument names and value shapes are correct. Works without a
|
||||||
|
model loaded.
|
||||||
|
|
||||||
|
:param method: ShapeBuilder method name, for example ``'polyline'``,
|
||||||
|
``'rectangle'`` or ``'extrude'``.
|
||||||
|
"""
|
||||||
return _shape_method_docs(method)
|
return _shape_method_docs(method)
|
||||||
|
|
||||||
def ifc_shape(self, method: str, params: Any = "{}") -> dict:
|
def ifc_shape(self, method: str, params: Any = "{}") -> dict:
|
||||||
"""Call a ShapeBuilder method by name. Returns the created entity's step ID.
|
"""Call a ShapeBuilder method to build geometry in the model.
|
||||||
|
|
||||||
params is a JSON string of keyword arguments. Pass entity references as integer
|
The created entities are added to the in-memory model, so
|
||||||
step IDs; vectors as JSON arrays (e.g. [1.0, 0.0, 0.0]).
|
``ifc_save`` is needed to persist them. On success the result
|
||||||
|
identifies the created entity by step ID and type; an unknown method
|
||||||
|
or a failed call is reported as an error instead.
|
||||||
|
|
||||||
|
:param method: ShapeBuilder method name, as listed by
|
||||||
|
``ifc_shape_list``.
|
||||||
|
:param params: JSON string of keyword arguments. Pass entity
|
||||||
|
references as integer step IDs and vectors as JSON arrays, e.g.
|
||||||
|
``[1.0, 0.0, 0.0]``.
|
||||||
"""
|
"""
|
||||||
model = self._require_model()
|
model = self._require_model()
|
||||||
|
|
||||||
@@ -493,11 +584,8 @@ class IfcSession:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {"ok": False, "error": f"{type(e).__name__}: {e}"}
|
return {"ok": False, "error": f"{type(e).__name__}: {e}"}
|
||||||
|
|
||||||
|
@_use_doc(run_quantify, extra="\n\nCall ``ifc_save`` afterwards to persist the result.")
|
||||||
def ifc_quantify(self, rule: str, selector: str = "") -> dict[str, Any]:
|
def ifc_quantify(self, rule: str, selector: str = "") -> dict[str, Any]:
|
||||||
"""Run quantity take-off on the model using the named rule.
|
|
||||||
|
|
||||||
Modifies the model in-place; call ifc_save() after.
|
|
||||||
"""
|
|
||||||
model = self._require_model()
|
model = self._require_model()
|
||||||
return run_quantify(model, rule, selector=selector if selector else None)
|
return run_quantify(model, rule, selector=selector if selector else None)
|
||||||
|
|
||||||
|
|||||||
+29
-23
@@ -2,6 +2,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
import inspect
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from ifcmcp.core import IfcSession
|
from ifcmcp.core import IfcSession
|
||||||
@@ -23,6 +24,11 @@ def build_server() -> Any:
|
|||||||
|
|
||||||
session = IfcSession()
|
session = IfcSession()
|
||||||
|
|
||||||
|
def _tool(fn):
|
||||||
|
"""Register a tool, taking its MCP description from the identically-named
|
||||||
|
IfcSession method rather than duplicating it here."""
|
||||||
|
return server.tool(description=inspect.getdoc(getattr(IfcSession, fn.__name__)))(fn)
|
||||||
|
|
||||||
server = FastMCP(
|
server = FastMCP(
|
||||||
name="ifc-mcp",
|
name="ifc-mcp",
|
||||||
instructions=(
|
instructions=(
|
||||||
@@ -33,44 +39,44 @@ def build_server() -> Any:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# ---- Lifecycle ----
|
# ---- Lifecycle ----
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_new(schema: str = "IFC4") -> dict[str, Any]:
|
def ifc_new(schema: str = "IFC4") -> dict[str, Any]:
|
||||||
return session.ifc_new(schema=schema)
|
return session.ifc_new(schema=schema)
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_load(path: str) -> str:
|
def ifc_load(path: str) -> str:
|
||||||
return session.ifc_load(path)
|
return session.ifc_load(path)
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_save(path: str = "") -> str:
|
def ifc_save(path: str = "") -> str:
|
||||||
return session.ifc_save(path)
|
return session.ifc_save(path)
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_reset() -> dict[str, Any]:
|
def ifc_reset() -> dict[str, Any]:
|
||||||
return session.ifc_reset()
|
return session.ifc_reset()
|
||||||
|
|
||||||
# ---- Query ----
|
# ---- Query ----
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_summary() -> dict[str, Any]:
|
def ifc_summary() -> dict[str, Any]:
|
||||||
return session.ifc_summary()
|
return session.ifc_summary()
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_tree() -> dict[str, Any] | list[dict[str, Any]]:
|
def ifc_tree() -> dict[str, Any] | list[dict[str, Any]]:
|
||||||
return session.ifc_tree()
|
return session.ifc_tree()
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_info(element_id: int) -> dict[str, Any]:
|
def ifc_info(element_id: int) -> dict[str, Any]:
|
||||||
return session.ifc_info(element_id)
|
return session.ifc_info(element_id)
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_select(query: str) -> list[dict[str, Any]]:
|
def ifc_select(query: str) -> list[dict[str, Any]]:
|
||||||
return session.ifc_select(query)
|
return session.ifc_select(query)
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_relations(element_id: int, traverse: str = "") -> dict[str, Any] | list[dict[str, Any]]:
|
def ifc_relations(element_id: int, traverse: str = "") -> dict[str, Any] | list[dict[str, Any]]:
|
||||||
return session.ifc_relations(element_id, traverse=traverse)
|
return session.ifc_relations(element_id, traverse=traverse)
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_clash(
|
def ifc_clash(
|
||||||
element_id: int,
|
element_id: int,
|
||||||
clearance: float = 0.0,
|
clearance: float = 0.0,
|
||||||
@@ -84,58 +90,58 @@ def build_server() -> Any:
|
|||||||
scope=scope,
|
scope=scope,
|
||||||
)
|
)
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_contexts() -> list[dict[str, Any]]:
|
def ifc_contexts() -> list[dict[str, Any]]:
|
||||||
return session.ifc_contexts()
|
return session.ifc_contexts()
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_materials() -> list[dict[str, Any]]:
|
def ifc_materials() -> list[dict[str, Any]]:
|
||||||
return session.ifc_materials()
|
return session.ifc_materials()
|
||||||
|
|
||||||
# ---- Edit ----
|
# ---- Edit ----
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_list(module: str = "") -> list[dict]:
|
def ifc_list(module: str = "") -> list[dict]:
|
||||||
return session.ifc_list(module=module)
|
return session.ifc_list(module=module)
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_docs(function_path: str) -> dict:
|
def ifc_docs(function_path: str) -> dict:
|
||||||
return session.ifc_docs(function_path=function_path)
|
return session.ifc_docs(function_path=function_path)
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_edit(function_path: str, params: str = "{}") -> dict:
|
def ifc_edit(function_path: str, params: str = "{}") -> dict:
|
||||||
return session.ifc_edit(function_path=function_path, params=params)
|
return session.ifc_edit(function_path=function_path, params=params)
|
||||||
|
|
||||||
# ---- Extended query + edit ----
|
# ---- Extended query + edit ----
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_validate(express_rules: bool = False) -> dict[str, Any]:
|
def ifc_validate(express_rules: bool = False) -> dict[str, Any]:
|
||||||
return session.ifc_validate(express_rules=express_rules)
|
return session.ifc_validate(express_rules=express_rules)
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_schedule(max_depth: int | None = None) -> list[dict[str, Any]]:
|
def ifc_schedule(max_depth: int | None = None) -> list[dict[str, Any]]:
|
||||||
return session.ifc_schedule(max_depth=max_depth)
|
return session.ifc_schedule(max_depth=max_depth)
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_cost(max_depth: int | None = None) -> list[dict[str, Any]]:
|
def ifc_cost(max_depth: int | None = None) -> list[dict[str, Any]]:
|
||||||
return session.ifc_cost(max_depth=max_depth)
|
return session.ifc_cost(max_depth=max_depth)
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_schema(entity_type: str) -> dict[str, Any]:
|
def ifc_schema(entity_type: str) -> dict[str, Any]:
|
||||||
return session.ifc_schema(entity_type=entity_type)
|
return session.ifc_schema(entity_type=entity_type)
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_quantify(rule: str, selector: str = "") -> dict[str, Any]:
|
def ifc_quantify(rule: str, selector: str = "") -> dict[str, Any]:
|
||||||
return session.ifc_quantify(rule=rule, selector=selector)
|
return session.ifc_quantify(rule=rule, selector=selector)
|
||||||
|
|
||||||
# ---- Shape builder ----
|
# ---- Shape builder ----
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_shape_list() -> list[dict]:
|
def ifc_shape_list() -> list[dict]:
|
||||||
return session.ifc_shape_list()
|
return session.ifc_shape_list()
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_shape_docs(method: str) -> dict:
|
def ifc_shape_docs(method: str) -> dict:
|
||||||
return session.ifc_shape_docs(method=method)
|
return session.ifc_shape_docs(method=method)
|
||||||
|
|
||||||
@server.tool()
|
@_tool
|
||||||
def ifc_shape(method: str, params: str = "{}") -> dict:
|
def ifc_shape(method: str, params: str = "{}") -> dict:
|
||||||
return session.ifc_shape(method=method, params=params)
|
return session.ifc_shape(method=method, params=params)
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ class TestServerRegistration:
|
|||||||
for name in expected:
|
for name in expected:
|
||||||
assert name in tools, f"Tool {name} not registered"
|
assert name in tools, f"Tool {name} not registered"
|
||||||
|
|
||||||
|
def test_all_tools_have_descriptions(self):
|
||||||
|
server = build_server()
|
||||||
|
tools = server._tool_manager.list_tools()
|
||||||
|
missing = [t.name for t in tools if not (t.description or "").strip()]
|
||||||
|
assert not missing, f"Tools with no description: {missing}"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def tool_fns():
|
def tool_fns():
|
||||||
|
|||||||
@@ -102,13 +102,26 @@ def clash(
|
|||||||
tolerance: float = 0.002,
|
tolerance: float = 0.002,
|
||||||
scope: str = "storey",
|
scope: str = "storey",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Check element for geometric clashes against other elements.
|
"""Check one element for geometric clashes against other elements.
|
||||||
|
|
||||||
|
Reports hard intersections and, optionally, violations of a required
|
||||||
|
clearance. Returns the overall ``pass``, the ``scope`` actually used, a
|
||||||
|
``checks`` block in which each clash names the other ``element``, the
|
||||||
|
clash ``type``, the ``distance`` and the two closest points ``p1``/``p2``,
|
||||||
|
and a de-duplicated flat ``elements`` list of everything involved.
|
||||||
|
Geometry is computed for every element in scope, so this is slow on large
|
||||||
|
models; ``pass`` is ``None`` with an ``error`` when the element has no
|
||||||
|
usable geometry.
|
||||||
|
|
||||||
:param model: The IFC model.
|
:param model: The IFC model.
|
||||||
:param element: The element to check.
|
:param element: The element to check.
|
||||||
:param clearance: Minimum clearance distance; if provided, runs clearance check.
|
:param clearance: Minimum required clearance distance; when given, also
|
||||||
|
runs the clearance check alongside the intersection check.
|
||||||
:param tolerance: Intersection tolerance in meters (default 0.002).
|
:param tolerance: Intersection tolerance in meters (default 0.002).
|
||||||
:param scope: Which elements to check against: "storey" or "all".
|
:param scope: ``"storey"`` (default) checks only elements sharing the
|
||||||
|
same spatial container; ``"all"`` checks every ``IfcElement``.
|
||||||
|
``"storey"`` falls back to ``"all"`` when the element has no spatial
|
||||||
|
container.
|
||||||
:return: Dict with clash results suitable for JSON serialization.
|
:return: Dict with clash results suitable for JSON serialization.
|
||||||
"""
|
"""
|
||||||
result: dict[str, Any] = {"element": _ref(element)}
|
result: dict[str, Any] = {"element": _ref(element)}
|
||||||
|
|||||||
@@ -26,10 +26,19 @@ def _cost_item_to_dict(item: ifcopenshell.entity_instance, max_depth: int | None
|
|||||||
|
|
||||||
|
|
||||||
def cost(model: ifcopenshell.file, max_depth: int | None = None) -> list[dict[str, Any]]:
|
def cost(model: ifcopenshell.file, max_depth: int | None = None) -> list[dict[str, Any]]:
|
||||||
"""Return a list of IfcCostSchedule entries with nested cost item trees.
|
"""List the cost schedules: bills of quantities and their cost items.
|
||||||
|
|
||||||
max_depth limits how many levels of subitems are expanded (None = unlimited).
|
Covers ``IfcCostSchedule`` only — this is the money dimension of the
|
||||||
At the cutoff level, subitems is replaced with {"truncated": True, "count": N}.
|
model; see ``schedule()`` in this module for the construction programme.
|
||||||
|
Each cost item reports its cost ``values`` as ``formula`` label and
|
||||||
|
``category`` pairs, together with its nested ``subitems``. Returns an
|
||||||
|
empty list when the model has no cost schedules.
|
||||||
|
|
||||||
|
:param model: The in-memory IFC model.
|
||||||
|
:param max_depth: Levels of cost item nesting to expand, counting root
|
||||||
|
items as level 1. Past the cutoff ``subitems`` is replaced by a
|
||||||
|
``{"truncated": True, "count": N}`` marker giving the number of items
|
||||||
|
not expanded. ``None`` (default) expands to unlimited depth.
|
||||||
"""
|
"""
|
||||||
result = []
|
result = []
|
||||||
for cost_schedule in model.by_type("IfcCostSchedule"):
|
for cost_schedule in model.by_type("IfcCostSchedule"):
|
||||||
|
|||||||
@@ -188,7 +188,16 @@ def _material_to_dict(material: ifcopenshell.entity_instance | None) -> dict[str
|
|||||||
|
|
||||||
|
|
||||||
def info(model: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]:
|
def info(model: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]:
|
||||||
"""Return deep inspection data for an element."""
|
"""Inspect a single entity in depth.
|
||||||
|
|
||||||
|
Returns the entity's direct ``attributes`` plus, where present,
|
||||||
|
``property_sets``, ``element_type``, ``material``, ``container``,
|
||||||
|
``placement`` and ``geometry_summary``. Keys are omitted when the
|
||||||
|
information is unavailable.
|
||||||
|
|
||||||
|
:param model: The in-memory IFC model.
|
||||||
|
:param element: The entity to inspect.
|
||||||
|
"""
|
||||||
result: dict[str, Any] = {
|
result: dict[str, Any] = {
|
||||||
"id": element.id(),
|
"id": element.id(),
|
||||||
"type": element.is_a(),
|
"type": element.is_a(),
|
||||||
|
|||||||
@@ -22,7 +22,13 @@ import ifcopenshell
|
|||||||
|
|
||||||
|
|
||||||
def materials(model: ifcopenshell.file) -> list[dict]:
|
def materials(model: ifcopenshell.file) -> list[dict]:
|
||||||
"""Return all materials and material sets from the model.
|
"""List the materials and material sets defined in the model.
|
||||||
|
|
||||||
|
Returns a single list covering ``IfcMaterial`` (with its category),
|
||||||
|
``IfcMaterialLayerSet`` (each layer's name, thickness, material and
|
||||||
|
ventilation flag), ``IfcMaterialConstituentSet`` (constituent names,
|
||||||
|
materials and fractions) and ``IfcMaterialProfileSet`` (profile names and
|
||||||
|
materials). Every entry carries the step ID of the material entity.
|
||||||
|
|
||||||
:param model: The in-memory IFC model.
|
:param model: The in-memory IFC model.
|
||||||
:return: List of dicts covering IfcMaterial, IfcMaterialLayerSet,
|
:return: List of dicts covering IfcMaterial, IfcMaterialLayerSet,
|
||||||
|
|||||||
@@ -182,7 +182,22 @@ def _collect_elements(data: Any, seen: set[int], result: list[dict[str, Any]]) -
|
|||||||
def relations(
|
def relations(
|
||||||
model: ifcopenshell.file, element: ifcopenshell.entity_instance, traverse: str | None = None
|
model: ifcopenshell.file, element: ifcopenshell.entity_instance, traverse: str | None = None
|
||||||
) -> dict[str, Any] | list[dict[str, Any]]:
|
) -> dict[str, Any] | list[dict[str, Any]]:
|
||||||
"""Return relationships for an element, or hierarchy chain if traverse='up'."""
|
"""Show how an element relates to the rest of the model.
|
||||||
|
|
||||||
|
By default returns a dict whose optional blocks are ``hierarchy`` (parent,
|
||||||
|
container, aggregate, nest, filled void, voided element), ``children``
|
||||||
|
(contained, parts, components, openings), ``type_relationship``,
|
||||||
|
``groups``, ``systems``, ``zones``, ``material``, ``referenced_structures``
|
||||||
|
and ``connections`` (connected to/from, ports), plus a de-duplicated flat
|
||||||
|
``elements`` list of everything referenced. Blocks with nothing to report
|
||||||
|
are omitted.
|
||||||
|
|
||||||
|
:param model: The IFC model.
|
||||||
|
:param element: The element to examine.
|
||||||
|
:param traverse: Set to ``'up'`` to instead return the chain of ancestors
|
||||||
|
from the element to ``IfcProject`` as a flat list. Any other value
|
||||||
|
gives the default behaviour.
|
||||||
|
"""
|
||||||
if traverse == "up":
|
if traverse == "up":
|
||||||
return _traverse_up(element)
|
return _traverse_up(element)
|
||||||
result = _all_relations(model, element)
|
result = _all_relations(model, element)
|
||||||
|
|||||||
@@ -37,10 +37,20 @@ def _task_to_dict(task: ifcopenshell.entity_instance, max_depth: int | None, dep
|
|||||||
|
|
||||||
|
|
||||||
def schedule(model: ifcopenshell.file, max_depth: int | None = None) -> list[dict[str, Any]]:
|
def schedule(model: ifcopenshell.file, max_depth: int | None = None) -> list[dict[str, Any]]:
|
||||||
"""Return a list of IfcWorkSchedule entries with nested task trees.
|
"""List the construction programme: work schedules and their task trees.
|
||||||
|
|
||||||
max_depth limits how many levels of subtasks are expanded (None = unlimited).
|
Covers ``IfcWorkSchedule`` only — this is the time dimension of the
|
||||||
At the cutoff level, subtasks is replaced with {"truncated": True, "count": N}.
|
model; see ``cost()`` in this module for the money dimension. Each
|
||||||
|
schedule lists its tasks recursively, and each task carries its scheduled
|
||||||
|
``start`` and ``finish``, an ``is_milestone`` flag, the products it
|
||||||
|
``outputs`` and its ``subtasks``. Returns an empty list when the model has
|
||||||
|
no work schedules.
|
||||||
|
|
||||||
|
:param model: The in-memory IFC model.
|
||||||
|
:param max_depth: Levels of subtask nesting to expand, counting root tasks
|
||||||
|
as level 1. Past the cutoff ``subtasks`` is replaced by a
|
||||||
|
``{"truncated": True, "count": N}`` marker giving the number of tasks
|
||||||
|
not expanded. ``None`` (default) expands to unlimited depth.
|
||||||
"""
|
"""
|
||||||
result = []
|
result = []
|
||||||
for work_schedule in model.by_type("IfcWorkSchedule"):
|
for work_schedule in model.by_type("IfcWorkSchedule"):
|
||||||
|
|||||||
@@ -8,7 +8,15 @@ import ifcopenshell.util.doc
|
|||||||
|
|
||||||
|
|
||||||
def schema(model: ifcopenshell.file, entity_type: str) -> dict[str, Any]:
|
def schema(model: ifcopenshell.file, entity_type: str) -> dict[str, Any]:
|
||||||
"""Return IFC class documentation for entity_type from model's schema version."""
|
"""Look up the IFC documentation for an entity class.
|
||||||
|
|
||||||
|
Returns the class ``description``, its ``predefined_types``, per-attribute
|
||||||
|
documentation and a ``spec_url``, resolved against the model's schema
|
||||||
|
version. Returns an ``error`` key for an unknown class.
|
||||||
|
|
||||||
|
:param model: The in-memory IFC model, used only for its schema version.
|
||||||
|
:param entity_type: IFC class name, for example ``'IfcWall'``.
|
||||||
|
"""
|
||||||
schema_name = model.schema
|
schema_name = model.schema
|
||||||
try:
|
try:
|
||||||
doc = ifcopenshell.util.doc.get_entity_doc(schema_name, entity_type)
|
doc = ifcopenshell.util.doc.get_entity_doc(schema_name, entity_type)
|
||||||
|
|||||||
@@ -26,7 +26,15 @@ import ifcopenshell
|
|||||||
|
|
||||||
|
|
||||||
def summary(model: ifcopenshell.file) -> dict[str, Any]:
|
def summary(model: ifcopenshell.file) -> dict[str, Any]:
|
||||||
"""Return a model overview with schema, element counts, and project info."""
|
"""Summarise the model: schema, entity counts and project info.
|
||||||
|
|
||||||
|
Returns the ``schema`` version, ``total_entities``, and a ``project``
|
||||||
|
block with the id, name and description of the first ``IfcProject``
|
||||||
|
(omitted if the model has none). The count covers every entity in the
|
||||||
|
file, not just physical elements.
|
||||||
|
|
||||||
|
:param model: The in-memory IFC model.
|
||||||
|
"""
|
||||||
# Count elements by IFC type, sorted by count descending
|
# Count elements by IFC type, sorted by count descending
|
||||||
type_counter: Counter[str] = Counter()
|
type_counter: Counter[str] = Counter()
|
||||||
total = 0
|
total = 0
|
||||||
|
|||||||
@@ -59,7 +59,17 @@ def _build_spatial_node(element: ifcopenshell.entity_instance) -> dict[str, Any]
|
|||||||
|
|
||||||
|
|
||||||
def tree(model: ifcopenshell.file) -> dict[str, Any] | list[dict[str, Any]]:
|
def tree(model: ifcopenshell.file) -> dict[str, Any] | list[dict[str, Any]]:
|
||||||
"""Return the spatial hierarchy tree starting from IfcProject."""
|
"""Return the spatial hierarchy of the model as a nested tree.
|
||||||
|
|
||||||
|
Starts at ``IfcProject`` and descends through decomposition (site,
|
||||||
|
building, storeys) and containment (the elements placed in each storey).
|
||||||
|
Every node carries ``id``, ``type`` and ``name``; ``children`` holds
|
||||||
|
decomposed sub-spaces and ``elements`` holds contained elements, and
|
||||||
|
either key is omitted when empty. Returns a list when the file contains
|
||||||
|
several projects, or an ``error`` key when it contains none.
|
||||||
|
|
||||||
|
:param model: The in-memory IFC model.
|
||||||
|
"""
|
||||||
projects = model.by_type("IfcProject")
|
projects = model.by_type("IfcProject")
|
||||||
if not projects:
|
if not projects:
|
||||||
return {"error": "No IfcProject found in model"}
|
return {"error": "No IfcProject found in model"}
|
||||||
|
|||||||
@@ -8,7 +8,16 @@ import ifcopenshell.validate
|
|||||||
|
|
||||||
|
|
||||||
def validate(model: ifcopenshell.file, express_rules: bool = False) -> dict[str, Any]:
|
def validate(model: ifcopenshell.file, express_rules: bool = False) -> dict[str, Any]:
|
||||||
"""Validate the model and return a dict with 'valid' bool and 'issues' list."""
|
"""Validate the model against the IFC schema.
|
||||||
|
|
||||||
|
Returns ``valid`` together with a list of ``issues``, each carrying a
|
||||||
|
``level`` and a ``message``. Worth running after a batch of edits and
|
||||||
|
before writing the model back to disk.
|
||||||
|
|
||||||
|
:param model: The in-memory IFC model.
|
||||||
|
:param express_rules: Also evaluate the schema's EXPRESS rules. Catches
|
||||||
|
more problems but is considerably slower (default ``False``).
|
||||||
|
"""
|
||||||
logger = ifcopenshell.validate.json_logger()
|
logger = ifcopenshell.validate.json_logger()
|
||||||
ifcopenshell.validate.validate(model, logger, express_rules=express_rules)
|
ifcopenshell.validate.validate(model, logger, express_rules=express_rules)
|
||||||
issues = [{"level": s["level"], "message": s["message"]} for s in logger.statements]
|
issues = [{"level": s["level"], "message": s["message"]} for s in logger.statements]
|
||||||
|
|||||||
Reference in New Issue
Block a user