ifcpatch - a static way to use ifcpatch

This commit is contained in:
Andrej
2025-06-02 12:29:20 +05:00
parent b393c120f1
commit c51e36dbca
7 changed files with 93 additions and 28 deletions
+25 -8
View File
@@ -57,6 +57,19 @@ example, we'll extract out all `IfcWall` elements.
ifcpatch -i input.ifc -o output.ifc -r ExtractElements -a "IfcWall"
cat output.ifc
You can also alias it to a command:
.. code-block:: bash
alias ifcpatch='python -m ifcpatch'
Alternatively, you can package it as an executable.
.. code-block:: bash
python make.py
./dist/ifcpatch
Here is a minimal example of how to use IfcPatch as a library:
.. code-block:: python
@@ -71,18 +84,22 @@ Here is a minimal example of how to use IfcPatch as a library:
})
ifcpatch.write(output, "output.ifc")
You can also alias it to a command:
.. code-block:: bash
Alternatively, there is a less dynamic way to use IfcPatch
that allows seeing available arguments, their descriptions, types, default values, etc.
alias ifcpatch='python -m ifcpatch'
..code-block:: python
Alternatively, you can package it as an executable.
import ifcopenshell
import ifcpatch
from ifcpatch.recipes import ExtractElements
.. code-block:: bash
python make.py
./dist/ifcpatch
patcher = ExtractElements.Patcher(
ifcopenshell.open("input.ifc"),
query="IfcWall",
)
patcher.patch()
ifcpatch.write(patcher.get_output(), "output.ifc")
Patch recipes
-------------
+25 -5
View File
@@ -45,7 +45,27 @@ class ArgumentsDict(TypedDict):
arguments: NotRequired[Sequence[Any]]
def execute(args: ArgumentsDict) -> Union[ifcopenshell.file, str]:
class BasePatcher:
def __init__(self, file: ifcopenshell.file, logger: Union[logging.Logger, None]):
self.file = file
self.logger = ensure_logger(logger)
def patch(self) -> None:
raise NotImplementedError
def get_output(self) -> Union[ifcopenshell.file, str, None]:
if hasattr(self, "file_patched"):
return self.file_patched # pyright: ignore[reportAttributeAccessIssue]
return self.file
def ensure_logger(logger: Union[logging.Logger, None] = None) -> logging.Logger:
if logger is not None:
return logger
return logging.getLogger("IFCPatch")
def execute(args: ArgumentsDict) -> Union[ifcopenshell.file, str, None]:
"""Execute a patch recipe
The details of how the patch recipe is executed depends on the definition of
@@ -88,7 +108,7 @@ def execute(args: ArgumentsDict) -> Union[ifcopenshell.file, str]:
"""
if "log" in args:
logging.basicConfig(filename=args["log"], filemode="a", level=logging.DEBUG)
logger = logging.getLogger("IFCPatch")
logger = ensure_logger()
if recipe_dir := os.environ.get("IFCPATCH_RECIPE_DIR"):
spec = importlib.util.spec_from_file_location(args["recipe"], os.path.join(recipe_dir, args["recipe"] + ".py"))
recipe = importlib.util.module_from_spec(spec)
@@ -103,17 +123,17 @@ def execute(args: ArgumentsDict) -> Union[ifcopenshell.file, str]:
else:
patcher = recipe.Patcher(args.get("file"), logger, arguments)
patcher.patch()
output = getattr(patcher, "file_patched", patcher.file)
output = BasePatcher.get_output(patcher)
return output
def write(output: Union[ifcopenshell.file, str], filepath: Union[Path, str]) -> None:
def write(output: Union[ifcopenshell.file, str, None], filepath: Union[Path, str]) -> None:
"""Write the output of an IFC patch to a file
Typically a patch output would be a patched IFC model file object, or as a
string. This function lets you agnostically write that output to a filepath.
:param output: The results from ifcpatch.execute()
:param output: The results from ``ifcpatch.execute()`` / ``Patcher.get_output()``
:param filepath: A filepath to where the results of the patched model should
be written to.
:return: None
@@ -22,9 +22,11 @@ import ifcopenshell.api.owner.settings
import ifcopenshell.util.pset
import ifcopenshell.util.element
import ifcopenshell.util.unit
import ifcpatch
from logging import Logger
import typing
from typing import Union
LengthUnit = typing.Literal[
"ATTOMETER",
@@ -50,11 +52,11 @@ LengthUnit = typing.Literal[
]
class Patcher:
class Patcher(ifcpatch.BasePatcher):
def __init__(
self,
file: ifcopenshell.file,
logger: Logger,
logger: Union[Logger, None] = None,
unit: LengthUnit = "METER",
):
"""Converts the length unit of a model to the specified unit
@@ -74,8 +76,7 @@ class Patcher:
# Convert to feet
model = ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["FOOT"]})
"""
self.file = file
self.logger = logger
super().__init__(file, logger)
self.unit = unit
self.file_patched: ifcopenshell.file
@@ -21,15 +21,16 @@ import ifcopenshell.api
import ifcopenshell.api.project
import ifcopenshell.guid
import ifcopenshell.util.selector
import ifcpatch
from typing import Union
from logging import Logger
class Patcher:
class Patcher(ifcpatch.BasePatcher):
def __init__(
self,
file: ifcopenshell.file,
logger: Logger,
logger: Union[Logger, None] = None,
query: str = "IfcWall",
assume_asset_uniqueness_by_name: bool = True,
):
@@ -62,8 +63,7 @@ class Patcher:
# Extract all walls and slabs
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ExtractElements", "arguments": ["IfcWall, IfcSlab"]})
"""
self.file = file
self.logger = logger
super().__init__(file, logger)
self.query = query
self.assume_asset_uniqueness_by_name = assume_asset_uniqueness_by_name
+4 -3
View File
@@ -36,6 +36,7 @@ import ifcopenshell.util.placement
import ifcopenshell.util.schema
import ifcopenshell.util.shape
import ifcopenshell.util.unit
import ifcpatch
from pathlib import Path
from typing import Any, TYPE_CHECKING, Literal, Union
from typing_extensions import assert_never
@@ -61,11 +62,11 @@ else:
DEFAULT_DATABASE_NAME = "database"
class Patcher:
class Patcher(ifcpatch.BasePatcher):
def __init__(
self,
file: ifcopenshell.file,
logger: logging.Logger,
logger: Union[logging.Logger, None] = None,
sql_type: SQLTypes = "SQLite",
host: str = "localhost",
username: str = "root",
@@ -116,7 +117,7 @@ class Patcher:
{"input": "input.ifc", "file": model, "recipe": "Ifc2Sql", "arguments": ["sqlite"]}
)
"""
self.file = file
super().__init__(file, logger)
self.logger = logger
self.sql_type: Literal["sqlite", "mysql"] = sql_type.lower()
self.host = host
+5 -4
View File
@@ -18,15 +18,17 @@
import ifcopenshell
import ifcopenshell.util.schema
import ifcpatch
import typing
from typing import Union
from logging import Logger
class Patcher:
class Patcher(ifcpatch.BasePatcher):
def __init__(
self,
file: ifcopenshell.file,
logger: Logger,
logger: Union[Logger, None] = None,
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4",
):
"""Migrate from one IFC version to another
@@ -43,8 +45,7 @@ class Patcher:
# Upgrade an IFC2X3 model to IFC4
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "Migrate", "arguments": ["IFC4"]})
"""
self.file = file
self.logger = logger
super().__init__(file, logger)
self.schema = schema
def patch(self):
+25
View File
@@ -16,6 +16,8 @@
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import tempfile
import ifcopenshell.api.root
import ifcpatch
from pathlib import Path
@@ -32,3 +34,26 @@ class Test:
expected_keys = ("class_", "description", "output", "inputs")
for key in expected_keys:
assert key in docs
def test_static_ifcpatch_execution(self):
from ifcpatch.recipes import ExtractElements
ifc_file = ifcopenshell.file()
project = ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcProject")
wall = ifcopenshell.api.root.create_entity(ifc_file, ifc_class="IfcWall")
patcher = ExtractElements.Patcher(ifc_file, query="IfcWall")
patcher.patch()
output = patcher.get_output()
assert isinstance(output, ifcopenshell.file)
assert output.by_type("IfcProject")[0].GlobalId == project.GlobalId
assert output.by_type("IfcWall")[0].GlobalId == wall.GlobalId
output_path = Path(tempfile.mktemp())
try:
assert not output_path.exists()
ifcpatch.write(patcher.get_output(), output_path)
assert output_path.exists()
finally:
output_path.unlink()