mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-24 16:29:57 +00:00
Rename ifcapi to ifcedit, README and black
This commit is contained in:
@@ -0,0 +1,208 @@
|
|||||||
|
# ifcedit
|
||||||
|
|
||||||
|
A CLI wrapper that exposes all 350+ `ifcopenshell.api` mutation functions as
|
||||||
|
shell commands. Functions are auto-discovered at runtime via introspection --
|
||||||
|
no hardcoded list to maintain.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install ifcedit
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires `ifcopenshell`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
ifcedit <command> [options] [--format json|text]
|
||||||
|
```
|
||||||
|
|
||||||
|
Three subcommands: `list` to discover functions, `docs` to read their
|
||||||
|
documentation, and `run` to execute them.
|
||||||
|
|
||||||
|
## Subcommands
|
||||||
|
|
||||||
|
### list
|
||||||
|
|
||||||
|
Discover available API modules and their functions.
|
||||||
|
|
||||||
|
**List all modules:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ifcedit list
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"module": "root",
|
||||||
|
"description": "Functions for creating project-level entities",
|
||||||
|
"functions": ["create_entity", "remove_product", "copy_class"],
|
||||||
|
"count": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module": "spatial",
|
||||||
|
"description": "Functions for managing spatial relationships",
|
||||||
|
"functions": ["assign_container", "unassign_container"],
|
||||||
|
"count": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**List functions in a module:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ifcedit list root
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "create_entity",
|
||||||
|
"description": "Create an IFC entity with optional initial attributes",
|
||||||
|
"params": [
|
||||||
|
{"name": "ifc_class", "type": "str", "required": true},
|
||||||
|
{"name": "name", "type": "Optional[str]"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### docs
|
||||||
|
|
||||||
|
Show full documentation for a specific function, including parameter
|
||||||
|
descriptions from docstrings and return type.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ifcedit docs root.create_entity
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"module": "root",
|
||||||
|
"function": "create_entity",
|
||||||
|
"description": "Create an IFC entity with optional initial attributes",
|
||||||
|
"long_description": "This function creates a new entity instance...",
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"name": "ifc_class",
|
||||||
|
"type": "str",
|
||||||
|
"required": true,
|
||||||
|
"description": "The IFC class name (e.g. 'IfcWall', 'IfcProject')"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"type": "Optional[str]",
|
||||||
|
"description": "Optional name attribute"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"return_type": "ifcopenshell.entity_instance",
|
||||||
|
"return_description": "The newly created entity instance"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### run
|
||||||
|
|
||||||
|
Execute an API function against an IFC file. Parameters are passed as
|
||||||
|
`--key value` pairs after the function name.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ifcedit run model.ifc root.create_entity --ifc_class IfcWall --name "My Wall"
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"result": {"id": 42, "type": "IfcWall", "name": "My Wall"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
|
||||||
|
- `-o, --output <path>` -- write to a different file instead of overwriting the input
|
||||||
|
- `--dry-run` -- validate parameters without executing or saving
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Save to a new file
|
||||||
|
ifcedit run model.ifc root.create_entity -o out.ifc --ifc_class IfcWall
|
||||||
|
|
||||||
|
# Validate without executing
|
||||||
|
ifcedit run model.ifc root.create_entity --dry-run --ifc_class IfcWall
|
||||||
|
```
|
||||||
|
|
||||||
|
Dry-run output shows the resolved parameters:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"dry_run": true,
|
||||||
|
"module": "root",
|
||||||
|
"function": "create_entity",
|
||||||
|
"args": {"ifc_class": "IfcWall", "name": "My Wall"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parameter type coercion
|
||||||
|
|
||||||
|
CLI strings are automatically converted to the types expected by each API
|
||||||
|
function, using the function's type annotations:
|
||||||
|
|
||||||
|
| Type | CLI input | Python value |
|
||||||
|
|------|-----------|--------------|
|
||||||
|
| `str` | `"hello"` | `"hello"` |
|
||||||
|
| `int` | `"42"` or `"#42"` | `42` |
|
||||||
|
| `float` | `"3.14"` | `3.14` |
|
||||||
|
| `bool` | `"true"`, `"1"`, `"yes"` | `True` |
|
||||||
|
| `Optional[X]` | `"none"` | `None` |
|
||||||
|
| `entity_instance` | `"42"` or `"#42"` | resolved from model by step ID |
|
||||||
|
| `list[entity_instance]` | `"5,6,7"` or `"[5, 6, 7]"` | list of resolved entities |
|
||||||
|
| `dict` | `'{"key": "val"}'` | parsed JSON object |
|
||||||
|
| `Literal["A", "B"]` | `"A"` | validated against allowed values |
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a project
|
||||||
|
ifcedit run model.ifc root.create_entity --ifc_class IfcProject --name "My Project"
|
||||||
|
|
||||||
|
# Assign an element to a storey
|
||||||
|
ifcedit run model.ifc spatial.assign_container --products 10 --relating_structure 4
|
||||||
|
|
||||||
|
# Assign multiple elements at once
|
||||||
|
ifcedit run model.ifc aggregate.assign_object --products "5,6,7" --relating_object 1
|
||||||
|
|
||||||
|
# Add a property set
|
||||||
|
ifcedit run model.ifc pset.add_pset --product 10 --name "Pset_WallCommon"
|
||||||
|
|
||||||
|
# Edit properties
|
||||||
|
ifcedit run model.ifc pset.edit_pset --pset 15 \
|
||||||
|
--properties '{"IsExternal": true, "FireRating": "2HR"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
Errors are reported in the JSON response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": false,
|
||||||
|
"error": "Entity #999 not found in model"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit code is 0 on success, 1 on error.
|
||||||
|
|
||||||
|
## Relationship to ifcquery
|
||||||
|
|
||||||
|
`ifcedit` and `ifcquery` are complementary tools:
|
||||||
|
|
||||||
|
- **ifcquery** reads and inspects IFC models (summary, tree, info, select, relations, clash)
|
||||||
|
- **ifcedit** modifies IFC models by wrapping `ifcopenshell.api` functions
|
||||||
|
|
||||||
|
A typical workflow: inspect with `ifcquery`, look up the right API function
|
||||||
|
with `ifcedit docs`, then apply changes with `ifcedit run`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
LGPLv3+ -- see the IfcOpenShell project license.
|
||||||
@@ -1,19 +1,19 @@
|
|||||||
# IfcApi - CLI wrapper for ifcopenshell.api mutation functions
|
# IfcEdit - CLI wrapper for ifcopenshell.api mutation functions
|
||||||
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
||||||
#
|
#
|
||||||
# This file is part of IfcApi.
|
# This file is part of IfcEdit.
|
||||||
#
|
#
|
||||||
# IfcApi is free software: you can redistribute it and/or modify
|
# IfcEdit is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
# 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
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
# (at your option) any later version.
|
# (at your option) any later version.
|
||||||
#
|
#
|
||||||
# IfcApi is distributed in the hope that it will be useful,
|
# IfcEdit is distributed in the hope that it will be useful,
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU Lesser General Public License for more details.
|
# GNU Lesser General Public License for more details.
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Lesser General Public License
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
# along with IfcApi. If not, see <http://www.gnu.org/licenses/>.
|
# along with IfcEdit. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
__version__ = version = "0.0.0"
|
__version__ = version = "0.0.0"
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
# IfcApi - CLI wrapper for ifcopenshell.api mutation functions
|
# IfcEdit - CLI wrapper for ifcopenshell.api mutation functions
|
||||||
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
||||||
#
|
#
|
||||||
# This file is part of IfcApi.
|
# This file is part of IfcEdit.
|
||||||
#
|
#
|
||||||
# IfcApi is free software: you can redistribute it and/or modify
|
# IfcEdit is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
# 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
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
# (at your option) any later version.
|
# (at your option) any later version.
|
||||||
#
|
#
|
||||||
# IfcApi is distributed in the hope that it will be useful,
|
# IfcEdit is distributed in the hope that it will be useful,
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU Lesser General Public License for more details.
|
# GNU Lesser General Public License for more details.
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Lesser General Public License
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
# along with IfcApi. If not, see <http://www.gnu.org/licenses/>.
|
# along with IfcEdit. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -24,8 +24,8 @@ import sys
|
|||||||
|
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
|
|
||||||
from ifcapi.discover import function_docs, list_functions, list_modules
|
from ifcedit.discover import function_docs, list_functions, list_modules
|
||||||
from ifcapi.run import run_api
|
from ifcedit.run import run_api
|
||||||
|
|
||||||
|
|
||||||
def format_output(data, fmt: str) -> str:
|
def format_output(data, fmt: str) -> str:
|
||||||
@@ -138,7 +138,7 @@ def _parse_extra_args(extra: list[str]) -> dict[str, str]:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
prog="ifcapi",
|
prog="ifcedit",
|
||||||
description="CLI wrapper for ifcopenshell.api IFC model mutation functions",
|
description="CLI wrapper for ifcopenshell.api IFC model mutation functions",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
# IfcApi - CLI wrapper for ifcopenshell.api mutation functions
|
# IfcEdit - CLI wrapper for ifcopenshell.api mutation functions
|
||||||
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
||||||
#
|
#
|
||||||
# This file is part of IfcApi.
|
# This file is part of IfcEdit.
|
||||||
#
|
#
|
||||||
# IfcApi is free software: you can redistribute it and/or modify
|
# IfcEdit is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
# 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
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
# (at your option) any later version.
|
# (at your option) any later version.
|
||||||
#
|
#
|
||||||
# IfcApi is distributed in the hope that it will be useful,
|
# IfcEdit is distributed in the hope that it will be useful,
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU Lesser General Public License for more details.
|
# GNU Lesser General Public License for more details.
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Lesser General Public License
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
# along with IfcApi. If not, see <http://www.gnu.org/licenses/>.
|
# along with IfcEdit. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
# IfcApi - CLI wrapper for ifcopenshell.api mutation functions
|
# IfcEdit - CLI wrapper for ifcopenshell.api mutation functions
|
||||||
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
||||||
#
|
#
|
||||||
# This file is part of IfcApi.
|
# This file is part of IfcEdit.
|
||||||
#
|
#
|
||||||
# IfcApi is free software: you can redistribute it and/or modify
|
# IfcEdit is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
# 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
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
# (at your option) any later version.
|
# (at your option) any later version.
|
||||||
#
|
#
|
||||||
# IfcApi is distributed in the hope that it will be useful,
|
# IfcEdit is distributed in the hope that it will be useful,
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU Lesser General Public License for more details.
|
# GNU Lesser General Public License for more details.
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Lesser General Public License
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
# along with IfcApi. If not, see <http://www.gnu.org/licenses/>.
|
# along with IfcEdit. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -55,12 +55,14 @@ def list_modules() -> list[dict]:
|
|||||||
description = ""
|
description = ""
|
||||||
if mod.__doc__:
|
if mod.__doc__:
|
||||||
description = mod.__doc__.strip().split("\n")[0]
|
description = mod.__doc__.strip().split("\n")[0]
|
||||||
modules.append({
|
modules.append(
|
||||||
"module": child.name,
|
{
|
||||||
"description": description,
|
"module": child.name,
|
||||||
"functions": list(all_names),
|
"description": description,
|
||||||
"count": len(all_names),
|
"functions": list(all_names),
|
||||||
})
|
"count": len(all_names),
|
||||||
|
}
|
||||||
|
)
|
||||||
return modules
|
return modules
|
||||||
|
|
||||||
|
|
||||||
@@ -80,11 +82,13 @@ def list_functions(module: str) -> list[dict]:
|
|||||||
if fn.__doc__:
|
if fn.__doc__:
|
||||||
description = fn.__doc__.strip().split("\n")[0]
|
description = fn.__doc__.strip().split("\n")[0]
|
||||||
params = _extract_params(fn)
|
params = _extract_params(fn)
|
||||||
functions.append({
|
functions.append(
|
||||||
"name": name,
|
{
|
||||||
"description": description,
|
"name": name,
|
||||||
"params": params,
|
"description": description,
|
||||||
})
|
"params": params,
|
||||||
|
}
|
||||||
|
)
|
||||||
return functions
|
return functions
|
||||||
|
|
||||||
|
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
# IfcApi - CLI wrapper for ifcopenshell.api mutation functions
|
# IfcEdit - CLI wrapper for ifcopenshell.api mutation functions
|
||||||
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
# Copyright (C) 2025 Bruno Postle <bruno@postle.net>
|
||||||
#
|
#
|
||||||
# This file is part of IfcApi.
|
# This file is part of IfcEdit.
|
||||||
#
|
#
|
||||||
# IfcApi is free software: you can redistribute it and/or modify
|
# IfcEdit is free software: you can redistribute it and/or modify
|
||||||
# it under the terms of the GNU Lesser General Public License as published by
|
# 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
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
# (at your option) any later version.
|
# (at your option) any later version.
|
||||||
#
|
#
|
||||||
# IfcApi is distributed in the hope that it will be useful,
|
# IfcEdit is distributed in the hope that it will be useful,
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
# GNU Lesser General Public License for more details.
|
# GNU Lesser General Public License for more details.
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Lesser General Public License
|
# You should have received a copy of the GNU Lesser General Public License
|
||||||
# along with IfcApi. If not, see <http://www.gnu.org/licenses/>.
|
# along with IfcEdit. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ import typing
|
|||||||
|
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
|
|
||||||
from ifcapi.coerce import coerce_value
|
from ifcedit.coerce import coerce_value
|
||||||
|
|
||||||
|
|
||||||
def run_api(
|
def run_api(
|
||||||
@@ -3,7 +3,7 @@ requires = ["setuptools>=61.0"]
|
|||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "ifcapi"
|
name = "ifcedit"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
authors = [
|
authors = [
|
||||||
{ name="Bruno Postle", email="bruno@postle.net" },
|
{ name="Bruno Postle", email="bruno@postle.net" },
|
||||||
@@ -23,7 +23,7 @@ Documentation = "https://docs.ifcopenshell.org"
|
|||||||
Issues = "https://github.com/IfcOpenShell/IfcOpenShell/issues"
|
Issues = "https://github.com/IfcOpenShell/IfcOpenShell/issues"
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
include = ["ifcapi*"]
|
include = ["ifcedit*"]
|
||||||
exclude = ["test*"]
|
exclude = ["test*"]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
@@ -5,7 +5,7 @@ import pytest
|
|||||||
|
|
||||||
import ifcopenshell
|
import ifcopenshell
|
||||||
|
|
||||||
from ifcapi.coerce import coerce_value
|
from ifcedit.coerce import coerce_value
|
||||||
|
|
||||||
|
|
||||||
class TestStringCoercion:
|
class TestStringCoercion:
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from ifcapi.discover import function_docs, list_functions, list_modules
|
from ifcedit.discover import function_docs, list_functions, list_modules
|
||||||
|
|
||||||
|
|
||||||
class TestListModules:
|
class TestListModules:
|
||||||
@@ -5,10 +5,10 @@ import sys
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def run_ifcapi(*args):
|
def run_ifcedit(*args):
|
||||||
"""Run ifcapi as a subprocess and return (stdout, stderr, returncode)."""
|
"""Run ifcedit as a subprocess and return (stdout, stderr, returncode)."""
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[sys.executable, "-m", "ifcapi", *args],
|
[sys.executable, "-m", "ifcedit", *args],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
)
|
)
|
||||||
@@ -17,7 +17,7 @@ def run_ifcapi(*args):
|
|||||||
|
|
||||||
class TestListCommand:
|
class TestListCommand:
|
||||||
def test_list_all_modules(self):
|
def test_list_all_modules(self):
|
||||||
stdout, stderr, rc = run_ifcapi("list")
|
stdout, stderr, rc = run_ifcedit("list")
|
||||||
assert rc == 0
|
assert rc == 0
|
||||||
data = json.loads(stdout)
|
data = json.loads(stdout)
|
||||||
assert isinstance(data, list)
|
assert isinstance(data, list)
|
||||||
@@ -26,7 +26,7 @@ class TestListCommand:
|
|||||||
assert "spatial" in module_names
|
assert "spatial" in module_names
|
||||||
|
|
||||||
def test_list_module_functions(self):
|
def test_list_module_functions(self):
|
||||||
stdout, stderr, rc = run_ifcapi("list", "root")
|
stdout, stderr, rc = run_ifcedit("list", "root")
|
||||||
assert rc == 0
|
assert rc == 0
|
||||||
data = json.loads(stdout)
|
data = json.loads(stdout)
|
||||||
assert isinstance(data, list)
|
assert isinstance(data, list)
|
||||||
@@ -34,14 +34,14 @@ class TestListCommand:
|
|||||||
assert "create_entity" in names
|
assert "create_entity" in names
|
||||||
|
|
||||||
def test_list_text_format(self):
|
def test_list_text_format(self):
|
||||||
stdout, stderr, rc = run_ifcapi("--format", "text", "list")
|
stdout, stderr, rc = run_ifcedit("--format", "text", "list")
|
||||||
assert rc == 0
|
assert rc == 0
|
||||||
assert "root" in stdout
|
assert "root" in stdout
|
||||||
|
|
||||||
|
|
||||||
class TestDocsCommand:
|
class TestDocsCommand:
|
||||||
def test_docs_create_entity(self):
|
def test_docs_create_entity(self):
|
||||||
stdout, stderr, rc = run_ifcapi("docs", "root.create_entity")
|
stdout, stderr, rc = run_ifcedit("docs", "root.create_entity")
|
||||||
assert rc == 0
|
assert rc == 0
|
||||||
data = json.loads(stdout)
|
data = json.loads(stdout)
|
||||||
assert data["module"] == "root"
|
assert data["module"] == "root"
|
||||||
@@ -49,18 +49,18 @@ class TestDocsCommand:
|
|||||||
assert "params" in data
|
assert "params" in data
|
||||||
|
|
||||||
def test_docs_invalid_path(self):
|
def test_docs_invalid_path(self):
|
||||||
stdout, stderr, rc = run_ifcapi("docs", "invalid_path")
|
stdout, stderr, rc = run_ifcedit("docs", "invalid_path")
|
||||||
assert rc != 0
|
assert rc != 0
|
||||||
assert "module.function" in stderr
|
assert "module.function" in stderr
|
||||||
|
|
||||||
def test_docs_unknown_function(self):
|
def test_docs_unknown_function(self):
|
||||||
stdout, stderr, rc = run_ifcapi("docs", "root.nonexistent")
|
stdout, stderr, rc = run_ifcedit("docs", "root.nonexistent")
|
||||||
assert rc != 0
|
assert rc != 0
|
||||||
|
|
||||||
|
|
||||||
class TestRunCommand:
|
class TestRunCommand:
|
||||||
def test_create_entity(self, model_file):
|
def test_create_entity(self, model_file):
|
||||||
stdout, stderr, rc = run_ifcapi(
|
stdout, stderr, rc = run_ifcedit(
|
||||||
"run", model_file, "root.create_entity", "--ifc_class", "IfcWall", "--name", "CLIWall"
|
"run", model_file, "root.create_entity", "--ifc_class", "IfcWall", "--name", "CLIWall"
|
||||||
)
|
)
|
||||||
assert rc == 0, f"stderr: {stderr}"
|
assert rc == 0, f"stderr: {stderr}"
|
||||||
@@ -70,9 +70,7 @@ class TestRunCommand:
|
|||||||
assert data["result"]["name"] == "CLIWall"
|
assert data["result"]["name"] == "CLIWall"
|
||||||
|
|
||||||
def test_dry_run(self, model_file):
|
def test_dry_run(self, model_file):
|
||||||
stdout, stderr, rc = run_ifcapi(
|
stdout, stderr, rc = run_ifcedit("run", model_file, "root.create_entity", "--dry-run", "--ifc_class", "IfcWall")
|
||||||
"run", model_file, "root.create_entity", "--dry-run", "--ifc_class", "IfcWall"
|
|
||||||
)
|
|
||||||
assert rc == 0
|
assert rc == 0
|
||||||
data = json.loads(stdout)
|
data = json.loads(stdout)
|
||||||
assert data["ok"] is True
|
assert data["ok"] is True
|
||||||
@@ -80,9 +78,7 @@ class TestRunCommand:
|
|||||||
|
|
||||||
def test_output_to_different_file(self, model_file, tmp_path):
|
def test_output_to_different_file(self, model_file, tmp_path):
|
||||||
output = str(tmp_path / "output.ifc")
|
output = str(tmp_path / "output.ifc")
|
||||||
stdout, stderr, rc = run_ifcapi(
|
stdout, stderr, rc = run_ifcedit("run", model_file, "root.create_entity", "-o", output, "--ifc_class", "IfcSlab")
|
||||||
"run", model_file, "root.create_entity", "-o", output, "--ifc_class", "IfcSlab"
|
|
||||||
)
|
|
||||||
assert rc == 0, f"stderr: {stderr}"
|
assert rc == 0, f"stderr: {stderr}"
|
||||||
data = json.loads(stdout)
|
data = json.loads(stdout)
|
||||||
assert data["ok"] is True
|
assert data["ok"] is True
|
||||||
@@ -92,10 +88,10 @@ class TestRunCommand:
|
|||||||
assert os.path.exists(output)
|
assert os.path.exists(output)
|
||||||
|
|
||||||
def test_run_error_bad_function(self, model_file):
|
def test_run_error_bad_function(self, model_file):
|
||||||
stdout, stderr, rc = run_ifcapi("run", model_file, "root.nonexistent")
|
stdout, stderr, rc = run_ifcedit("run", model_file, "root.nonexistent")
|
||||||
assert rc != 0
|
assert rc != 0
|
||||||
|
|
||||||
def test_run_invalid_function_path(self, model_file):
|
def test_run_invalid_function_path(self, model_file):
|
||||||
stdout, stderr, rc = run_ifcapi("run", model_file, "invalid_path")
|
stdout, stderr, rc = run_ifcedit("run", model_file, "invalid_path")
|
||||||
assert rc != 0
|
assert rc != 0
|
||||||
assert "module.function" in stderr
|
assert "module.function" in stderr
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import ifcopenshell.api.pset
|
import ifcopenshell.api.pset
|
||||||
import ifcopenshell.api.root
|
import ifcopenshell.api.root
|
||||||
|
|
||||||
from ifcapi.run import run_api, serialize_result
|
from ifcedit.run import run_api, serialize_result
|
||||||
|
|
||||||
|
|
||||||
class TestRunApi:
|
class TestRunApi:
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
# ifcquery
|
||||||
|
|
||||||
|
A CLI tool for querying and inspecting IFC building models. All output is
|
||||||
|
structured JSON (or human-readable text), making it easy to pipe into other
|
||||||
|
tools or scripts.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install ifcquery
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires `ifcopenshell`. The `clash` subcommand additionally requires the
|
||||||
|
IfcOpenShell C++ geometry bindings (`ifcopenshell.geom`).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
ifcquery <ifc_file> <command> [options] [--format json|text]
|
||||||
|
```
|
||||||
|
|
||||||
|
The `--format` flag controls output. Default is `json`; use `text` for
|
||||||
|
indented human-readable output.
|
||||||
|
|
||||||
|
## Subcommands
|
||||||
|
|
||||||
|
### summary
|
||||||
|
|
||||||
|
Get a model overview: schema version, entity counts, and project info.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ifcquery model.ifc summary
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema": "IFC4",
|
||||||
|
"total_entities": 1847,
|
||||||
|
"project": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "Office Building",
|
||||||
|
"description": null
|
||||||
|
},
|
||||||
|
"types": {
|
||||||
|
"IfcWall": 42,
|
||||||
|
"IfcSlab": 12,
|
||||||
|
"IfcWindow": 36
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### tree
|
||||||
|
|
||||||
|
Display the spatial hierarchy from IfcProject down through sites, buildings,
|
||||||
|
storeys, and their contained elements.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ifcquery model.ifc tree
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"type": "IfcProject",
|
||||||
|
"name": "Office Building",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"type": "IfcSite",
|
||||||
|
"name": "Default Site",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "IfcBuilding",
|
||||||
|
"name": "Main Building",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "IfcBuildingStorey",
|
||||||
|
"name": "Ground Floor",
|
||||||
|
"elements": [
|
||||||
|
{"id": 10, "type": "IfcWall", "name": "Wall001"},
|
||||||
|
{"id": 11, "type": "IfcSlab", "name": "Floor001"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### info
|
||||||
|
|
||||||
|
Get detailed information about a specific element by step ID.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ifcquery model.ifc info 10
|
||||||
|
ifcquery model.ifc info '#10'
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns attributes, property sets, type relationship, material assignment,
|
||||||
|
spatial container, and placement matrix.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"type": "IfcWall",
|
||||||
|
"attributes": {
|
||||||
|
"Name": "Wall001",
|
||||||
|
"Description": null,
|
||||||
|
"ObjectType": "LOADBEARING"
|
||||||
|
},
|
||||||
|
"property_sets": {
|
||||||
|
"Pset_WallCommon": {
|
||||||
|
"IsExternal": true,
|
||||||
|
"FireRating": "2HR"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"element_type": {"id": 50, "type": "IfcWallType", "name": "Standard"},
|
||||||
|
"material": {"id": 60, "type": "IfcMaterial", "name": "Concrete"},
|
||||||
|
"container": {"id": 4, "type": "IfcBuildingStorey", "name": "Ground Floor"},
|
||||||
|
"placement": [
|
||||||
|
[1.0, 0.0, 0.0, 5.0],
|
||||||
|
[0.0, 1.0, 0.0, 0.0],
|
||||||
|
[0.0, 0.0, 1.0, 0.0],
|
||||||
|
[0.0, 0.0, 0.0, 1.0]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### select
|
||||||
|
|
||||||
|
Filter elements using the ifcopenshell selector syntax.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ifcquery model.ifc select 'IfcWall'
|
||||||
|
ifcquery model.ifc select 'IfcWall, IfcSlab'
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"id": 10, "type": "IfcWall", "name": "Wall001"},
|
||||||
|
{"id": 11, "type": "IfcWall", "name": "Wall002"},
|
||||||
|
{"id": 20, "type": "IfcSlab", "name": "Floor001"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Results are sorted by ID.
|
||||||
|
|
||||||
|
### relations
|
||||||
|
|
||||||
|
Show all relationships for an element, organized by category: hierarchy,
|
||||||
|
children, type relationships, groups, systems, material, and connections.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ifcquery model.ifc relations 10
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"type": "IfcWall",
|
||||||
|
"name": "Wall001",
|
||||||
|
"hierarchy": {
|
||||||
|
"parent": {"id": 4, "type": "IfcBuildingStorey", "name": "Ground Floor"},
|
||||||
|
"container": {"id": 4, "type": "IfcBuildingStorey", "name": "Ground Floor"}
|
||||||
|
},
|
||||||
|
"children": {
|
||||||
|
"openings": [{"id": 30, "type": "IfcOpeningElement", "name": "Opening01"}]
|
||||||
|
},
|
||||||
|
"type_relationship": {
|
||||||
|
"type_of": {"id": 50, "type": "IfcWallType", "name": "Standard"}
|
||||||
|
},
|
||||||
|
"material": {"id": 60, "type": "IfcMaterial", "name": "Concrete"}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Empty categories are omitted from output.
|
||||||
|
|
||||||
|
Use `--traverse up` to walk the spatial hierarchy from the element up to
|
||||||
|
IfcProject:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ifcquery model.ifc relations 10 --traverse up
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"id": 10, "type": "IfcWall", "name": "Wall001"},
|
||||||
|
{"id": 4, "type": "IfcBuildingStorey", "name": "Ground Floor"},
|
||||||
|
{"id": 3, "type": "IfcBuilding", "name": "Main Building"},
|
||||||
|
{"id": 2, "type": "IfcSite", "name": "Default Site"},
|
||||||
|
{"id": 1, "type": "IfcProject", "name": "Office Building"}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### clash
|
||||||
|
|
||||||
|
Check a single element for geometric intersections and clearance violations
|
||||||
|
against other elements.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ifcquery model.ifc clash 10
|
||||||
|
ifcquery model.ifc clash 10 --clearance 0.5
|
||||||
|
ifcquery model.ifc clash 10 --scope all --tolerance 0.001
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
- `--clearance <meters>` -- minimum clearance distance to check
|
||||||
|
- `--tolerance <meters>` -- intersection tolerance (default: 0.002)
|
||||||
|
- `--scope {storey,all}` -- check against same-storey elements or all elements (default: storey)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"element": {"id": 10, "type": "IfcWall", "name": "Wall001"},
|
||||||
|
"scope": "storey",
|
||||||
|
"pass": false,
|
||||||
|
"checks": {
|
||||||
|
"intersection": {
|
||||||
|
"pass": false,
|
||||||
|
"tolerance": 0.002,
|
||||||
|
"clashes": [
|
||||||
|
{
|
||||||
|
"element": {"id": 11, "type": "IfcWall", "name": "Wall002"},
|
||||||
|
"type": "intersection",
|
||||||
|
"distance": 0.0,
|
||||||
|
"p1": [2.5, 2.5, 1.5],
|
||||||
|
"p2": [2.5, 2.5, 1.5]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"clearance": {
|
||||||
|
"pass": true,
|
||||||
|
"clearance": 0.5,
|
||||||
|
"clashes": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires the IfcOpenShell C++ geometry bindings.
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
Errors are written to stderr. Exit code is 0 on success, 1 on error.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
LGPLv3+ -- see the IfcOpenShell project license.
|
||||||
@@ -62,9 +62,7 @@ def _get_scope_elements(
|
|||||||
return elements, "all"
|
return elements, "all"
|
||||||
|
|
||||||
|
|
||||||
def _build_tree(
|
def _build_tree(model: ifcopenshell.file, elements: set[ifcopenshell.entity_instance]) -> ifcopenshell.geom.tree | None:
|
||||||
model: ifcopenshell.file, elements: set[ifcopenshell.entity_instance]
|
|
||||||
) -> ifcopenshell.geom.tree | None:
|
|
||||||
"""Build geometry tree for given elements using iterator.
|
"""Build geometry tree for given elements using iterator.
|
||||||
|
|
||||||
Returns None if iterator fails to initialize (no geometry available).
|
Returns None if iterator fails to initialize (no geometry available).
|
||||||
@@ -72,9 +70,7 @@ def _build_tree(
|
|||||||
geom_settings = ifcopenshell.geom.settings()
|
geom_settings = ifcopenshell.geom.settings()
|
||||||
geom_settings.set("use-world-coords", True)
|
geom_settings.set("use-world-coords", True)
|
||||||
geom_tree = ifcopenshell.geom.tree()
|
geom_tree = ifcopenshell.geom.tree()
|
||||||
iterator = ifcopenshell.geom.iterator(
|
iterator = ifcopenshell.geom.iterator(geom_settings, model, multiprocessing.cpu_count(), include=list(elements))
|
||||||
geom_settings, model, multiprocessing.cpu_count(), include=list(elements)
|
|
||||||
)
|
|
||||||
if not iterator.initialize():
|
if not iterator.initialize():
|
||||||
return None
|
return None
|
||||||
while True:
|
while True:
|
||||||
@@ -84,9 +80,7 @@ def _build_tree(
|
|||||||
return geom_tree
|
return geom_tree
|
||||||
|
|
||||||
|
|
||||||
def _format_clash(
|
def _format_clash(clash_result, geom_tree: ifcopenshell.geom.tree, model: ifcopenshell.file) -> dict[str, Any]:
|
||||||
clash_result, geom_tree: ifcopenshell.geom.tree, model: ifcopenshell.file
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Format a single clash result to dict."""
|
"""Format a single clash result to dict."""
|
||||||
# clash result .a/.b are C++ wrapper entity_instances without .Name;
|
# clash result .a/.b are C++ wrapper entity_instances without .Name;
|
||||||
# look up the Python entity from the model by id for proper serialization
|
# look up the Python entity from the model by id for proper serialization
|
||||||
@@ -124,9 +118,7 @@ def clash(
|
|||||||
|
|
||||||
if not scope_elements:
|
if not scope_elements:
|
||||||
result["pass"] = True
|
result["pass"] = True
|
||||||
result["checks"] = {
|
result["checks"] = {"intersection": {"pass": True, "tolerance": tolerance, "clashes": []}}
|
||||||
"intersection": {"pass": True, "tolerance": tolerance, "clashes": []}
|
|
||||||
}
|
|
||||||
if clearance is not None:
|
if clearance is not None:
|
||||||
result["checks"]["clearance"] = {"pass": True, "clearance": clearance, "clashes": []}
|
result["checks"]["clearance"] = {"pass": True, "clearance": clearance, "clashes": []}
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -142,9 +142,7 @@ class TestNoClashes:
|
|||||||
# Create a model with a single wall
|
# Create a model with a single wall
|
||||||
f = ifcopenshell.api.project.create_file()
|
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_user = lambda ifc: (ifc.by_type("IfcPersonAndOrganization") or [None])[0]
|
||||||
ifcopenshell.api.owner.settings.get_application = lambda ifc: (
|
ifcopenshell.api.owner.settings.get_application = lambda ifc: (ifc.by_type("IfcApplication") or [None])[0]
|
||||||
ifc.by_type("IfcApplication") or [None]
|
|
||||||
)[0]
|
|
||||||
project = ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject", name="P")
|
project = ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject", name="P")
|
||||||
ifcopenshell.api.unit.assign_unit(f)
|
ifcopenshell.api.unit.assign_unit(f)
|
||||||
site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite", name="S")
|
site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite", name="S")
|
||||||
|
|||||||
Reference in New Issue
Block a user