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" ifcpatch -i input.ifc -o output.ifc -r ExtractElements -a "IfcWall"
cat output.ifc 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: Here is a minimal example of how to use IfcPatch as a library:
.. code-block:: python .. 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") 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 patcher = ExtractElements.Patcher(
ifcopenshell.open("input.ifc"),
python make.py query="IfcWall",
./dist/ifcpatch )
patcher.patch()
ifcpatch.write(patcher.get_output(), "output.ifc")
Patch recipes Patch recipes
------------- -------------
+25 -5
View File
@@ -45,7 +45,27 @@ class ArgumentsDict(TypedDict):
arguments: NotRequired[Sequence[Any]] 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 """Execute a patch recipe
The details of how the patch recipe is executed depends on the definition of 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: if "log" in args:
logging.basicConfig(filename=args["log"], filemode="a", level=logging.DEBUG) 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"): 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")) spec = importlib.util.spec_from_file_location(args["recipe"], os.path.join(recipe_dir, args["recipe"] + ".py"))
recipe = importlib.util.module_from_spec(spec) recipe = importlib.util.module_from_spec(spec)
@@ -103,17 +123,17 @@ def execute(args: ArgumentsDict) -> Union[ifcopenshell.file, str]:
else: else:
patcher = recipe.Patcher(args.get("file"), logger, arguments) patcher = recipe.Patcher(args.get("file"), logger, arguments)
patcher.patch() patcher.patch()
output = getattr(patcher, "file_patched", patcher.file) output = BasePatcher.get_output(patcher)
return output 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 """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 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. 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 :param filepath: A filepath to where the results of the patched model should
be written to. be written to.
:return: None :return: None
@@ -22,9 +22,11 @@ import ifcopenshell.api.owner.settings
import ifcopenshell.util.pset import ifcopenshell.util.pset
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.unit import ifcopenshell.util.unit
import ifcpatch
from logging import Logger from logging import Logger
import typing import typing
from typing import Union
LengthUnit = typing.Literal[ LengthUnit = typing.Literal[
"ATTOMETER", "ATTOMETER",
@@ -50,11 +52,11 @@ LengthUnit = typing.Literal[
] ]
class Patcher: class Patcher(ifcpatch.BasePatcher):
def __init__( def __init__(
self, self,
file: ifcopenshell.file, file: ifcopenshell.file,
logger: Logger, logger: Union[Logger, None] = None,
unit: LengthUnit = "METER", unit: LengthUnit = "METER",
): ):
"""Converts the length unit of a model to the specified unit """Converts the length unit of a model to the specified unit
@@ -74,8 +76,7 @@ class Patcher:
# Convert to feet # Convert to feet
model = ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["FOOT"]}) model = ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ConvertLengthUnit", "arguments": ["FOOT"]})
""" """
self.file = file super().__init__(file, logger)
self.logger = logger
self.unit = unit self.unit = unit
self.file_patched: ifcopenshell.file self.file_patched: ifcopenshell.file
@@ -21,15 +21,16 @@ import ifcopenshell.api
import ifcopenshell.api.project import ifcopenshell.api.project
import ifcopenshell.guid import ifcopenshell.guid
import ifcopenshell.util.selector import ifcopenshell.util.selector
import ifcpatch
from typing import Union from typing import Union
from logging import Logger from logging import Logger
class Patcher: class Patcher(ifcpatch.BasePatcher):
def __init__( def __init__(
self, self,
file: ifcopenshell.file, file: ifcopenshell.file,
logger: Logger, logger: Union[Logger, None] = None,
query: str = "IfcWall", query: str = "IfcWall",
assume_asset_uniqueness_by_name: bool = True, assume_asset_uniqueness_by_name: bool = True,
): ):
@@ -62,8 +63,7 @@ class Patcher:
# Extract all walls and slabs # Extract all walls and slabs
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ExtractElements", "arguments": ["IfcWall, IfcSlab"]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "ExtractElements", "arguments": ["IfcWall, IfcSlab"]})
""" """
self.file = file super().__init__(file, logger)
self.logger = logger
self.query = query self.query = query
self.assume_asset_uniqueness_by_name = assume_asset_uniqueness_by_name 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.schema
import ifcopenshell.util.shape import ifcopenshell.util.shape
import ifcopenshell.util.unit import ifcopenshell.util.unit
import ifcpatch
from pathlib import Path from pathlib import Path
from typing import Any, TYPE_CHECKING, Literal, Union from typing import Any, TYPE_CHECKING, Literal, Union
from typing_extensions import assert_never from typing_extensions import assert_never
@@ -61,11 +62,11 @@ else:
DEFAULT_DATABASE_NAME = "database" DEFAULT_DATABASE_NAME = "database"
class Patcher: class Patcher(ifcpatch.BasePatcher):
def __init__( def __init__(
self, self,
file: ifcopenshell.file, file: ifcopenshell.file,
logger: logging.Logger, logger: Union[logging.Logger, None] = None,
sql_type: SQLTypes = "SQLite", sql_type: SQLTypes = "SQLite",
host: str = "localhost", host: str = "localhost",
username: str = "root", username: str = "root",
@@ -116,7 +117,7 @@ class Patcher:
{"input": "input.ifc", "file": model, "recipe": "Ifc2Sql", "arguments": ["sqlite"]} {"input": "input.ifc", "file": model, "recipe": "Ifc2Sql", "arguments": ["sqlite"]}
) )
""" """
self.file = file super().__init__(file, logger)
self.logger = logger self.logger = logger
self.sql_type: Literal["sqlite", "mysql"] = sql_type.lower() self.sql_type: Literal["sqlite", "mysql"] = sql_type.lower()
self.host = host self.host = host
+5 -4
View File
@@ -18,15 +18,17 @@
import ifcopenshell import ifcopenshell
import ifcopenshell.util.schema import ifcopenshell.util.schema
import ifcpatch
import typing import typing
from typing import Union
from logging import Logger from logging import Logger
class Patcher: class Patcher(ifcpatch.BasePatcher):
def __init__( def __init__(
self, self,
file: ifcopenshell.file, file: ifcopenshell.file,
logger: Logger, logger: Union[Logger, None] = None,
schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4", schema: ifcopenshell.util.schema.IFC_SCHEMA = "IFC4",
): ):
"""Migrate from one IFC version to another """Migrate from one IFC version to another
@@ -43,8 +45,7 @@ class Patcher:
# Upgrade an IFC2X3 model to IFC4 # Upgrade an IFC2X3 model to IFC4
ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "Migrate", "arguments": ["IFC4"]}) ifcpatch.execute({"input": "input.ifc", "file": model, "recipe": "Migrate", "arguments": ["IFC4"]})
""" """
self.file = file super().__init__(file, logger)
self.logger = logger
self.schema = schema self.schema = schema
def patch(self): def patch(self):
+25
View File
@@ -16,6 +16,8 @@
# 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 IfcOpenShell. If not, see <http://www.gnu.org/licenses/>. # along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
import tempfile
import ifcopenshell.api.root
import ifcpatch import ifcpatch
from pathlib import Path from pathlib import Path
@@ -32,3 +34,26 @@ class Test:
expected_keys = ("class_", "description", "output", "inputs") expected_keys = ("class_", "description", "output", "inputs")
for key in expected_keys: for key in expected_keys:
assert key in docs 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()