Merge remote-tracking branch 'origin/v0.8.0' into test-ci

This commit is contained in:
Thomas Krijnen
2025-10-27 06:54:10 +01:00
25 changed files with 352 additions and 107 deletions
+1
View File
@@ -114,6 +114,7 @@ jobs:
-DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \ -DPYTHON_INCLUDE_DIR:PATH=${{ env.pythonLocation }}/include/python3.11 \
-DPYTHON_LIBRARY:FILEPATH=${{ env.pythonLocation }}/lib/libpython3.11.so \ -DPYTHON_LIBRARY:FILEPATH=${{ env.pythonLocation }}/lib/libpython3.11.so \
-DCOLLADA_SUPPORT=Off \ -DCOLLADA_SUPPORT=Off \
-DUSE_MMAP=On \
"-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \ "-DSCHEMA_VERSIONS=2x3;4;4x3_add2" \
-DGLTF_SUPPORT=On \ -DGLTF_SUPPORT=On \
-DWITH_ROCKSDB=On \ -DWITH_ROCKSDB=On \
+2
View File
@@ -18,6 +18,8 @@
# # # #
############################################################################### ###############################################################################
# ruff: noqa: UP035
""" """
Example usage: Example usage:
# Build all targets by default. # Build all targets by default.
+5 -1
View File
@@ -487,7 +487,11 @@ def sync_references(
continue continue
if (obj := ifc.get_object(element)) and ifc.is_moved(obj): if (obj := ifc.get_object(element)) and ifc.is_moved(obj):
drawing_tool.sync_object_placement(obj) drawing_tool.sync_object_placement(obj)
reference_element = drawing_tool.get_assigned_product(element) if not (reference_element := drawing_tool.get_assigned_product(element)):
if obj := ifc.get_object(element):
drawing_tool.delete_object(obj)
ifc.run("root.remove_product", product=element)
continue
reference_obj = ifc.get_object(reference_element) reference_obj = ifc.get_object(reference_element)
if reference_element not in potential_reference_elements: if reference_element not in potential_reference_elements:
# It was auto created, so it makes sense to auto delete # It was auto created, so it makes sense to auto delete
+9 -5
View File
@@ -786,17 +786,21 @@ class Model(bonsai.core.tool.Model):
elif material.is_a("IfcMaterialLayerSet"): elif material.is_a("IfcMaterialLayerSet"):
axis = ifcopenshell.util.element.get_pset(element, "EPset_Parametric", "LayerSetDirection") axis = ifcopenshell.util.element.get_pset(element, "EPset_Parametric", "LayerSetDirection")
if axis is None: if axis is None:
if element.is_a() in [ if element.is_a() in (
"IfcSlabType", "IfcSlabType",
"IfcRoofType", "IfcRoofType",
"IfcRampType", "IfcRampType",
"IfcPlateType", "IfcPlateType",
"IfcCovering", "IfcSlab",
"IfcFurniture", "IfcRoof",
]: "IfcRamp",
"IfcPlate",
):
axis = "AXIS3" axis = "AXIS3"
else: elif element.is_a() in ("IfcWallType", "IfcWall"):
axis = "AXIS2" axis = "AXIS2"
else:
return
return f"LAYER{axis[-1]}" return f"LAYER{axis[-1]}"
elif material.is_a("IfcMaterialProfileSetUsage"): elif material.is_a("IfcMaterialProfileSetUsage"):
# TODO: remove after we support editing profile usages with IfcRevolvedAreaSolid. # TODO: remove after we support editing profile usages with IfcRevolvedAreaSolid.
+2 -1
View File
@@ -24,7 +24,8 @@ import importlib
import ifcopenshell.util.selector import ifcopenshell.util.selector
from pathlib import Path from pathlib import Path
from collections import defaultdict from collections import defaultdict
from typing import Literal, Union, Any, Callable, TYPE_CHECKING from typing import Literal, Union, Any, TYPE_CHECKING
from collections.abc import Callable
try: try:
from openpyxl import Workbook from openpyxl import Workbook
+4
View File
@@ -367,11 +367,13 @@ void IfcGeom::Iterator::create_element_(ifcopenshell::geometry::Converter* kerne
})); }));
if (!brep) { if (!brep) {
Logger::SetProduct(boost::none);
return; return;
} }
auto elem = process_based_on_settings(settings, brep); auto elem = process_based_on_settings(settings, brep);
if (!elem) { if (!elem) {
Logger::SetProduct(boost::none);
return; return;
} }
@@ -394,6 +396,8 @@ void IfcGeom::Iterator::create_element_(ifcopenshell::geometry::Converter* kerne
} }
} }
} }
Logger::SetProduct(boost::none);
} }
IfcGeom::Element* IfcGeom::Iterator::process_based_on_settings(ifcopenshell::geometry::Settings settings, IfcGeom::BRepElement* elem, IfcGeom::TriangulationElement* previous) IfcGeom::Element* IfcGeom::Iterator::process_based_on_settings(ifcopenshell::geometry::Settings settings, IfcGeom::BRepElement* elem, IfcGeom::TriangulationElement* previous)
+1
View File
@@ -43,6 +43,7 @@ Examples
Learning how to use the bSDD is best done by reading the official Swagger API docs. Learning how to use the bSDD is best done by reading the official Swagger API docs.
.. code-block:: python .. code-block:: python
from bsdd import Client,apply_ifc_classification_properties from bsdd import Client,apply_ifc_classification_properties
from pprint import pprint from pprint import pprint
@@ -17,4 +17,5 @@ capabilities of the C++ core are available in Python.
ifcopenshell-python/geometry_creation ifcopenshell-python/geometry_creation
ifcopenshell-python/geometry_tree ifcopenshell-python/geometry_tree
ifcopenshell-python/selector_syntax ifcopenshell-python/selector_syntax
ifcopenshell-python/developer_guide ifcopenshell-python/schema_querying
ifcopenshell-python/validation
@@ -1,15 +0,0 @@
Developer Guide
===============
The core module implements low-level functionality to read and write IFC data. This includes:
- Reading IFC data from different serialisations into Python objects
- Accessing direct and indirect attributes of IFC entities
- Creating IFC entities
- Generating GlobalIds
- Removing IFC entities and all references
- Modifying IFC direct attributes
- Checking IFC class inheritance
- Validating IFC data
TODO
@@ -0,0 +1,73 @@
Schema querying
===============
Schema declarations
-------------------
IfcOpenShell can query the IFC schema itself without instantiating or loading an IFC dataset.
.. code-block:: python
import ifcopenshell
ifc4 = ifcopenshell.schema_by_name("IFC4")
A schema definition is known as a declaration. You may loop through all declarations or retrieve a declaration by name. All declarations have a name.
.. code-block:: python
for declaration in ifc4.declarations():
print(declaration.name()) # 'IfcAbsorbedDoseMeasure', 'IfcAccelerationMeasure', 'IfcActionRequest', ...
ifcwall = ifc4.declaration_by_name("IfcWall")
You can check if an entity is abstract, and retrive both the supertype and subtypes of an entity:
.. code-block:: python
print(ifcwall.is_abstract()) # False
print(ifcwall.supertype()) # <entity IfcBuildingElement>
print(ifcwall.subtypes()) # (<entity IfcWallElementedCase>, <entity IfcWallStandardCase>)
You can retrieve only the direct attributes of an entity, or all the direct attributes including inherited attributes, or inverse attributes:
.. code-block:: python
print(ifcwall.attributes())
print(ifcwall.all_attributes())
print(ifcwall.all_inverse_attributes())
buildingSMART property set templates
------------------------------------
For each IFC schema version, buildingSMART publishes built in property and quantity set templates for standardised properties. These define property names, property sets, data types, and which IFC class they are applicable to. You can query these templates.
.. code-block:: python
import ifcopenshell.util.pset
templates = ifcopenshell.util.pset.PsetQto("IFC4")
To get just the names of applicable templates for an entity:
.. code-block:: python
# ['Pset_EnvironmentalImpactIndicators', 'Pset_EnvironmentalImpactValues', 'Pset_WallCommon', 'Qto_WallBaseQuantities', ...]
print(templates.get_applicable_names("IfcWall"))
They may also be retrieved as an ``IfcPropertySetTemplate`` entity:
.. code-block:: python
print(templates.get_applicable("IfcWall"))
A single template may be retrieved by name:
.. code-block:: python
templates.get_by_name('Pset_WallCommon')
You may add your own IFC files containing pset template definitions:
.. code-block:: python
my_pset_library = ifcopenshell.open('/path/to/library.ifc')
templates.templates.append(my_pset_library)
@@ -0,0 +1,78 @@
Validation
==========
SPF syntax validation
---------------------
IfcOpenShell can validate whether or not an IFC-SPF file contains correct SPF syntax.
.. code-block::
$ python -m ifcopenshell.simple_spf path/to/model.ifc
Valid
Here are some examples of failures:
.. code-block::
$ python -m ifcopenshell.simple_spf fixtures/fail_double_comma.ifc
On line 8 column 21:
Unexpected comma (',')
Expecting one of DBLQUOTE DOT HASH INT LPAR NONE QUOTE REAL STAR UPPER
00008 | #1=IFCPERSON($,$,'',,$,$,$,$);
^
$ python -m ifcopenshell.simple_spf fixtures/fail_double_semi.ifc
On line 27 column 66:
Unexpected semicolon (';')
Expecting one of ENDSEC HASH
00027 | #20=IFCPROJECT('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);;
^
$ python -m ifcopenshell.simple_spf fixtures/fail_duplicate_id.ifc
On line 27:
Duplicate instance name #19
00027 | #19=IFCPROJECT('2AyG2X0sb16Bjd4gQc07yZ',#5,'',$,$,$,$,(#11),#19);
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
$ python -m ifcopenshell.simple_spf fixtures/fail_no_header.ifc
On line 2 column 1:
Unexpected hex ('F')
Expecting HEADER
00002 | FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1');
^
The optional ``--json`` argument may be used to instead get results in JSON.
.. code-block::
$ python -m ifcopenshell.simple_spf test.ifc
{"type": "unexpected_token", "lineno": 8, "column": 48, "found_type": "semicolon", "found_value": ";", "expected": ["ENDSEC"], "line": "#1= IFCPERSON($,'Nicht definiert',$,$,$,$,$,$);;", "message": "On line 8 column 48:\nUnexpected semicolon (';')\nExpecting ENDSEC\n00008 | #1= IFCPERSON($,'Nicht definiert',$,$,$,$,$,$);;\n ^"}
IFC schema validation
---------------------
IfcOpenShell can validate models against the IFC schema itself. It checks against attributes, entity names, data types, cardinality, and where rules.
.. code-block:: console
$ python -m ifcopenshell.validate -h
usage: validate.py [-h] [--rules] [--json] [--fields] [--spf] files [files ...]
positional arguments:
files The IFC file to validate.
options:
-h, --help show this help message and exit
--rules Run express rules.
--json Output in JSON format.
--fields Output more detailed information about failed entities (only with --json).
--spf Output entities in SPF format (only with --json).
For example:
.. code-block:: bash
python -m ifcopenshell.validate /path/to/model.ifc --rules
+21 -21
View File
@@ -54,27 +54,27 @@ IfcOpenShell is a modular ecosystem of tools that work together, where each tool
.. csv-table:: .. csv-table::
:header: "Name", "Description" :header: "Name", "Description"
"**IfcOpenShell**", "The core library for C++ developers. The library includes the ability to parse schemas, tessellate and process implicit geometry." "`IfcOpenShell <https://docs.ifcopenshell.org/ifcopenshell.html>`_", "The core library for C++ developers. The library includes the ability to parse schemas, tessellate and process implicit geometry."
"**IfcOpenShell-Python**", "Python bindings to the core IfcOpenShell C++ system, as well as high level analysis and authoring functions." "`IfcOpenShell-Python <https://docs.ifcopenshell.org/ifcopenshell-python.html>`_", "Python bindings to the core IfcOpenShell C++ system, as well as high level analysis and authoring functions."
"**IfcConvert**", "A command-line application for converting IFC geometry into file formats such as OBJ, DAE, GLB, STP, IGS, XML, SVG, H5, and IFC itself." "`IfcConvert <https://docs.ifcopenshell.org/ifcconvert.html>`_", "A command-line application for converting IFC geometry into file formats such as OBJ, DAE, GLB, STP, IGS, XML, SVG, H5, and IFC itself."
"**Bonsai**", "A graphical add-on for Blender that lets you analyse, author, and modify IFC with Blender. Graphically create BIM models from scratch!" "`Bonsai <https://docs.ifcopenshell.org/bonsai.html>`_", "A graphical add-on for Blender that lets you analyse, author, and modify IFC with Blender. Graphically create BIM models from scratch!"
"**BCF**", "BIM Collaboration Format (BCF) is a standard to manage and exchange coordination topics between disciplines collaborating on a project by changing XML files or querying an API." "`BCF <https://docs.ifcopenshell.org/bcf.html>`_", "BIM Collaboration Format (BCF) is a standard to manage and exchange coordination topics between disciplines collaborating on a project by changing XML files or querying an API."
"**BIMServer-Plugin**", "A plugin to the open source BIMServer CDE to allow you to use IfcOpenShell to parse, view, and audit models." "`BIMServer-Plugin <https://docs.ifcopenshell.org/bimserver-plugin.html>`_", "A plugin to the open source BIMServer CDE to allow you to use IfcOpenShell to parse, view, and audit models."
"**BIMTester**", "A utility that allows you to write Gherkin-based tests for models." "`BIMTester <https://docs.ifcopenshell.org/bimtester.html>`_", "A utility that allows you to write Gherkin-based tests for models."
"**bSDD**", "A Python library to query the buildingSMART Data Dictionary API to search for standardised classifications and properties." "`bSDD <https://docs.ifcopenshell.org/bsdd.html>`_", "A Python library to query the buildingSMART Data Dictionary API to search for standardised classifications and properties."
"**Ifc2CA**", "Converts IFC models to FEM structural analytical models to be used in Code_Aster." "`Ifc2CA <https://docs.ifcopenshell.org/ifc2ca.html>`_", "Converts IFC models to FEM structural analytical models to be used in Code_Aster."
"**Ifc4D**", "A series of utilities for converting to and from various 4D software like MS Project, PowerProject, and Oracle P6." "`Ifc4D <https://docs.ifcopenshell.org/ifc4d.html>`_", "A series of utilities for converting to and from various 4D software like MS Project, PowerProject, and Oracle P6."
"**Ifc5D**", "A collection of utilities of manipulating cost-related data to and from formats, reports, and optimisation engines." "`Ifc5D <https://docs.ifcopenshell.org/ifc5d.html>`_", "A collection of utilities of manipulating cost-related data to and from formats, reports, and optimisation engines."
"**IfcCityJSON**", "A converter for CityJSON files and IFC. It currently only supports one-way conversion from CityJSON to IFC." "`IfcCityJSON <https://docs.ifcopenshell.org/ifccityjson.html>`_", "A converter for CityJSON files and IFC. It currently only supports one-way conversion from CityJSON to IFC."
"**IfcClash**", "A CLI utility and library that lets you perform clash detection on one or more IFC models. Clashes are defined in terms of clash sets with filters using the IFC query syntax." "`IfcClash <https://docs.ifcopenshell.org/ifcclash.html>`_", "A CLI utility and library that lets you perform clash detection on one or more IFC models. Clashes are defined in terms of clash sets with filters using the IFC query syntax."
"**IfcCSV**", "View and edit IFC data using spreadsheets or tabular datasets, such as CSV, ODS, XLSX, Pandas DataFrames, and regular Python lists." "`IfcCSV <https://docs.ifcopenshell.org/ifccsv.html>`_", "View and edit IFC data using spreadsheets or tabular datasets, such as CSV, ODS, XLSX, Pandas DataFrames, and regular Python lists."
"**IfcDiff**", "A CLI utility and library that lets you compare the changes between two IFC models." "`IfcDiff <https://docs.ifcopenshell.org/ifcdiff.html>`_", "A CLI utility and library that lets you compare the changes between two IFC models."
"**IfcFM**", "A highly standards-compliant tool (e.g. COBie 2.4, COBie 3.0, AOH-BSEM) to convert FM data in IFC databases to spreadsheets and other machine readable formats, such as ODS, XLSX, CSV, Pandas, XML, and JSON." "`IfcFM <https://docs.ifcopenshell.org/ifcfm.html>`_", "A highly standards-compliant tool (e.g. COBie 2.4, COBie 3.0, AOH-BSEM) to convert FM data in IFC databases to spreadsheets and other machine readable formats, such as ODS, XLSX, CSV, Pandas, XML, and JSON."
"**IfcMax**", "A 3ds Max importer plugin able to import the IFC file format." "`IfcMax <https://docs.ifcopenshell.org/ifcmax.html>`_", "A 3ds Max importer plugin able to import the IFC file format."
"**IfcPatch**", "A CLI utility and library that lets you run and distribute predetermined modifications on an IFC file, known as a patch recipe. Useful in deploying a data pipeline or batch-fixing external models." "`IfcPatch <https://docs.ifcopenshell.org/ifcpatch.html>`_", "A CLI utility and library that lets you run and distribute predetermined modifications on an IFC file, known as a patch recipe. Useful in deploying a data pipeline or batch-fixing external models."
"**IfcSverchok**", "A node based visual programming add-on for Blender to interact with IFC and Sverchok." "`IfcSverchok <https://docs.ifcopenshell.org/ifcsverchok.html>`_", "A node based visual programming add-on for Blender to interact with IFC and Sverchok."
"**IfcTester**", "Author and read Information Delivery Specification (IDS) files. You can validate IFC models against IDS and generate reports in multiple formats. It works from the command line, as a web app, or as a library." "`IfcTester <https://docs.ifcopenshell.org/ifctester.html>`_", "Author and read Information Delivery Specification (IDS) files. You can validate IFC models against IDS and generate reports in multiple formats. It works from the command line, as a web app, or as a library."
"**VoxelisationToolkit**", "Converts .ifc geometry into voxels, and lets you perform voxel based geometric analysis." "`VoxelisationToolkit <https://github.com/opensourceBIM/voxelization_toolkit>`_", "Converts .ifc geometry into voxels, and lets you perform voxel based geometric analysis."
.. note:: .. note::
+1 -1
View File
@@ -251,7 +251,7 @@ INVALID_SYNTAX = ifcopenshell_wrapper.file_open_status.INVALID_SYNTAX
try: try:
UNKNOWN = ifcopenshell_wrapper.file_open_status.UNKNOWN UNKNOWN = ifcopenshell_wrapper.file_open_status.UNKNOWN
except: except:
UNKNOWN = 5 # Workaround UNKNOWN = 5 # Workaround
import struct import struct
@@ -22,7 +22,7 @@ import os
import sys import sys
import operator import operator
from .. import ifcopenshell_wrapper from .. import open, ifcopenshell_wrapper
from ..file import file from ..file import file
from ..entity_instance import entity_instance from ..entity_instance import entity_instance
@@ -307,10 +307,7 @@ class iterator(ifcopenshell_wrapper.Iterator):
self.file = file self.file = file
file_or_filename = file_or_filename.wrapped_data file_or_filename = file_or_filename.wrapped_data
else: else:
# @todo? file_or_filename = self.file = open(file_or_filename)
self.file = None
# Makes sure people are able to use python's platform agnostic paths
file_or_filename = os.path.abspath(file_or_filename)
if include is not None and exclude is not None: if include is not None and exclude is not None:
raise ValueError("include and exclude cannot be specified simultaneously") raise ValueError("include and exclude cannot be specified simultaneously")
@@ -342,7 +339,9 @@ class iterator(ifcopenshell_wrapper.Iterator):
geometry_library, self.settings, file_or_filename, include_or_exclude, include is not None, num_threads geometry_library, self.settings, file_or_filename, include_or_exclude, include is not None, num_threads
) )
else: else:
ifcopenshell_wrapper.Iterator.__init__(self, geometry_library, settings, file_or_filename, num_threads) self.this = ifcopenshell_wrapper.construct_iterator(
geometry_library, self.settings, file_or_filename, num_threads
)
if has_occ: if has_occ:
@@ -71,6 +71,13 @@ FILTERED_CARTESIAN_QUOTIENT: Any
EXACT_PREDICATES: Any EXACT_PREDICATES: Any
EXACT_CONSTRUCTIONS: Any EXACT_CONSTRUCTIONS: Any
FT_AUTODETECT: Any
FT_IFCSPF: Any
FT_IFCXML: Any
FT_IFCZIP: Any
FT_ROCKSDB: Any
FT_UNKNOWN: Any
CURVES: Any CURVES: Any
SURFACES_AND_SOLIDS: Any SURFACES_AND_SOLIDS: Any
CURVES_SURFACES_AND_SOLIDS: Any CURVES_SURFACES_AND_SOLIDS: Any
@@ -96,12 +103,9 @@ class IfcSpfHeader:
""" """
def file(self, *args): ... def file(self, *args): ...
@property def file_description_py(self): ...
def file_description(self) -> ifcopenshell.entity_instance: ... def file_name_py(self): ...
@property def file_schema_py(self): ...
def file_name(self) -> ifcopenshell.entity_instance: ...
@property
def file_schema(self) -> ifcopenshell.entity_instance: ...
def read(self): ... def read(self): ...
def tryRead(self): ... def tryRead(self): ...
def write(self, out): ... def write(self, out): ...
@@ -145,6 +149,7 @@ class ConversionResult:
def Style(self): ... def Style(self): ...
def StylePtr(self): ... def StylePtr(self): ...
def append(self, trsf): ... def append(self, trsf): ...
def apply_transform(self, unit_scale): ...
def hasStyle(self): ... def hasStyle(self): ...
def prepend(self, trsf): ... def prepend(self, trsf): ...
def setStyle(self, newStyle): ... def setStyle(self, newStyle): ...
@@ -157,6 +162,7 @@ class ConversionResultShape:
def axis(self): ... def axis(self): ...
def bounding_box(self, *args): ... def bounding_box(self, *args): ...
def box(self): ... def box(self): ...
def concat(self, arg2): ...
def convex_decomposition(self): ... def convex_decomposition(self): ...
def convex_tag(self, b): ... def convex_tag(self, b): ...
def edges(self): ... def edges(self): ...
@@ -178,9 +184,11 @@ class ConversionResultShape:
def solid(self): ... def solid(self): ...
def solid_mt(self): ... def solid_mt(self): ...
def subtract(self, arg2): ... def subtract(self, arg2): ...
def surface_area_along_direction(self, tol, arg3, along_x, along_y, along_z): ...
def surface_genus(self): ... def surface_genus(self): ...
def vertices(self): ... def vertices(self): ...
def volume(self): ... def volume(self): ...
def wrap_in_compound(self): ...
class DoubleArray3: class DoubleArray3:
def back(self): ... def back(self): ...
@@ -284,11 +292,6 @@ class HdfSerializer(GeometrySerializer):
def write(self, *args): ... def write(self, *args): ...
def writeHeader(self): ... def writeHeader(self): ...
class HeaderEntity:
def getArgument(self, index): ...
def getArgumentCount(self): ...
def toString(self, upper): ...
class IfcBaseEntity(entity_instance): class IfcBaseEntity(entity_instance):
def declaration(self): ... def declaration(self): ...
def get(self, name): ... def get(self, name): ...
@@ -304,9 +307,17 @@ class IfcEntityInstanceData: ...
class IfcLateBoundEntity(IfcBaseEntity): class IfcLateBoundEntity(IfcBaseEntity):
def declaration(self): ... def declaration(self): ...
class InstanceReference: class InstanceStreamer:
file_offset: Any def bypassTypes(self, type_names): ...
v: Any def bypassed_instances(self): ...
coerce_attribute_count: bool
def hasSemicolon(self): ...
def inverses(self, *args): ...
def pushPage(self, page): ...
def readInstancePy(self, type_as_declaration_instance): ...
def references(self, *args): ...
def semicolonCount(self): ...
def status(self): ...
class Iterator: class Iterator:
initialization_outcome_: Any initialization_outcome_: Any
@@ -377,6 +388,18 @@ class Representation:
def settings(self): ... def settings(self): ...
class RocksDBPrefixIterator:
def key(self): ...
def next(self): ...
def valid(self): ...
def value(self): ...
class RocksDbSerializer:
def finalize(self): ...
def ready(self): ...
def setFile(self, arg2): ...
def writeHeader(self): ...
class Serialization(Representation): class Serialization(Representation):
@property @property
def brep_data(self): ... def brep_data(self): ...
@@ -829,6 +852,7 @@ class entity_instance:
... ...
def get_attribute_names(self): ... def get_attribute_names(self): ...
def get_attribute_value(self, index): ...
def get_inverse(self, a): ... def get_inverse(self, a): ...
def get_inverse_attribute_names(self): ... def get_inverse_attribute_names(self): ...
def id(self) -> int: ... def id(self) -> int: ...
@@ -851,6 +875,7 @@ class entity_instance:
def setArgumentAsLogical(self, i, v): ... def setArgumentAsLogical(self, i, v): ...
def setArgumentAsNull(self, i): ... def setArgumentAsNull(self, i): ...
def setArgumentAsString(self, i, a): ... def setArgumentAsString(self, i, a): ...
def set_attribute_value(self, *args): ...
def toString(self, arg2, upper): ... def toString(self, arg2, upper): ...
def to_string(self, valid_spf): ... def to_string(self, valid_spf): ...
def unset_attribute_value(self, i): ... def unset_attribute_value(self, i): ...
@@ -888,14 +913,10 @@ class face:
def print_impl(self, o, indent): ... def print_impl(self, o, indent): ...
class file: class file:
INSTANCE_ID: Any
INSTANCE_TYPE: Any
ATTRIBUTE_INDEX: Any
guid_map_: Any
stream: Any
def FreshId(self): ... def FreshId(self): ...
def add(self, entity: entity_instance, id: int) -> entity_instance: ... def add(self, entity: entity_instance, id: int) -> entity_instance: ...
def addEntities(self, entities): ... def addEntities(self, entities): ...
def add_type_ref(self, new_entity): ...
def batch(self) -> None: def batch(self) -> None:
"""Enable batch mode. """Enable batch mode.
@@ -919,10 +940,15 @@ class file:
... ...
def build_inverses(self): ... def build_inverses(self): ...
def build_inverses_(self, arg2): ...
def by_guid(self, guid: str) -> entity_instance: ... def by_guid(self, guid: str) -> entity_instance: ...
def by_id(self, id: int) -> entity_instance: ... def by_id(self, id: int) -> entity_instance: ...
def by_type(self, *args): ... def by_type(self, *args): ...
def by_type_excl_subtypes(self, *args): ... def by_type_excl_subtypes(self, *args): ...
def bypass_type(self, type_name): ...
calculate_unit_factors: bool
check_existance_before_adding: bool
def create(self, decl): ...
@staticmethod @staticmethod
def createTimestamp(): ... def createTimestamp(): ...
def entity_names(self) -> tuple[int, ...]: def entity_names(self) -> tuple[int, ...]:
@@ -961,24 +987,27 @@ class file:
def get_total_inverses(self, e: entity_instance) -> int: ... def get_total_inverses(self, e: entity_instance) -> int: ...
def good(self): ... def good(self): ...
@staticmethod def header(self, *args) -> IfcSpfHeader:
def guid_map(*args): ...
def header(self) -> IfcSpfHeader:
"""Internal IfcSpfHeader instance, always prefer ``ifcopenshell.file.header`` instead.""" """Internal IfcSpfHeader instance, always prefer ``ifcopenshell.file.header`` instead."""
def ifcroot_type(self) -> entity: ... def ifcroot_type(self) -> entity: ...
def internal_guid_map(self): ... def initialize(self, *args): ...
def load(self, entity_instance_name, entity, arg4, attribute_index): ... instantiate_typed_instances: bool
def key_value_store_iter(self, prefix): ...
def key_value_store_query(self, key): ...
def process_deletion_inverse(self, inst): ...
def recalculate_id_counter(self): ... def recalculate_id_counter(self): ...
def remove(self, entity: entity_instance) -> None: ... def remove(self, entity: entity_instance) -> None: ...
def remove_type_ref(self, new_entity): ...
def reset_identity_cache(self): ...
@property @property
def schema(self): ... def schema(self): ...
def storage_mode(self): ...
def to_string(self): ... def to_string(self): ...
@staticmethod @staticmethod
def traverse(instance: entity_instance, max_level: int) -> tuple[entity_instance, ...]: ... def traverse(instance: entity_instance, max_level: int) -> tuple[entity_instance, ...]: ...
@staticmethod @staticmethod
def traverse_breadth_first(instance: entity_instance, max_level: int) -> tuple[entity_instance, ...]: ... def traverse_breadth_first(instance: entity_instance, max_level: int) -> tuple[entity_instance, ...]: ...
def try_read_semicolon(self): ...
def types(self) -> tuple[str, ...]: def types(self) -> tuple[str, ...]:
"""Return a tuple of classes present in the file. """Return a tuple of classes present in the file.
@@ -986,16 +1015,15 @@ class file:
""" """
... ...
def types_begin(self): ...
def types_end(self): ...
def write(self, fn): ... def write(self, fn): ...
class file_open_status: class file_open_status:
SUCCESS: Any SUCCESS: int
READ_ERROR: Any READ_ERROR: int
NO_HEADER: Any NO_HEADER: int
UNSUPPORTED_SCHEMA: Any UNSUPPORTED_SCHEMA: int
INVALID_SYNTAX: Any INVALID_SYNTAX: int
UNKNOWN: int
def value(self): ... def value(self): ...
class fn_evaluator: class fn_evaluator:
@@ -1037,9 +1065,6 @@ class geometry_conversion_result:
products_2: Any products_2: Any
representation: Any representation: Any
class geometry_exception:
def what(self): ...
class gradient_function(function_item): class gradient_function(function_item):
def calc_hash(self): ... def calc_hash(self): ...
def clone_(self): ... def clone_(self): ...
@@ -1138,6 +1163,8 @@ class matrix4(item):
def is_identity(self): ... def is_identity(self): ...
def kind(self): ... def kind(self): ...
def translation_part(self): ... def translation_part(self): ...
def post_multiply_scale(self, s): ...
def pre_multiply_scale(self, s): ...
class named_type(parameter_type): class named_type(parameter_type):
def _is(self, *args): ... def _is(self, *args): ...
@@ -1516,13 +1543,11 @@ class sweep(geom_item):
class sweep_along_curve(sweep): class sweep_along_curve(sweep):
curve: Any curve: Any
surface: Any surface: Any
direction: Any
def calc_hash(self): ... def calc_hash(self): ...
def clone_(self): ... def clone_(self): ...
def kind(self): ... def kind(self): ...
class too_many_faces_exception(geometry_exception): ...
class topology_error: ...
class torus(surface): class torus(surface):
radius1: Any radius1: Any
radius2: Any radius2: Any
@@ -1568,6 +1593,8 @@ class type_declaration(declaration):
def as_type_declaration(self) -> type_declaration: ... def as_type_declaration(self) -> type_declaration: ...
def declared_type(self): ... def declared_type(self): ...
class uninitialized_tag: ...
def arrange_polygons(polygons): ... def arrange_polygons(polygons): ...
def clear_schemas(): ... def clear_schemas(): ...
def construct_iterator_with_include_exclude(geometry_library, settings, file, elems, include, num_threads): ... def construct_iterator_with_include_exclude(geometry_library, settings, file, elems, include, num_threads): ...
@@ -1581,6 +1608,7 @@ def flatten(deep): ...
def get_feature(x): ... def get_feature(x): ...
def get_info_cpp(v, include_identifier): ... def get_info_cpp(v, include_identifier): ...
def get_log(): ... def get_log(): ...
def guess_file_type(fn): ...
def helmert_curve_point(A0, A1, A2, s): ... def helmert_curve_point(A0, A1, A2, s): ...
def kind_to_string(k): ... def kind_to_string(k): ...
def less(arg1, arg2): ... def less(arg1, arg2): ...
@@ -1588,7 +1616,7 @@ def line_segments_to_polygons(s, eps, segments): ...
def map_shape(settings, instance): ... def map_shape(settings, instance): ...
def nary_union(sequence): ... def nary_union(sequence): ...
def new_IfcBaseClass(schema_identifier: str, name: str) -> entity_instance: ... def new_IfcBaseClass(schema_identifier: str, name: str) -> entity_instance: ...
def open(fn): ... def open(fn, readonly): ...
def parse_ifcxml(filename): ... def parse_ifcxml(filename): ...
def polygons_to_svg(*args): ... def polygons_to_svg(*args): ...
def read(data): ... def read(data): ...
@@ -1599,6 +1627,7 @@ def serialise(schema_name, shape_str, advanced): ...
def set_feature(x, v): ... def set_feature(x, v): ...
def set_log_format_json(): ... def set_log_format_json(): ...
def set_log_format_text(): ... def set_log_format_text(): ...
def stream_from_string(data): ...
def svg_to_line_segments(data, class_name): ... def svg_to_line_segments(data, class_name): ...
def svg_to_polygons(data, class_name): ... def svg_to_polygons(data, class_name): ...
def taxonomy_item_repr(i): ... def taxonomy_item_repr(i): ...
@@ -53,6 +53,8 @@ def get_brick_type(element: ifcopenshell.entity_instance) -> Union[str, None]:
if not result: if not result:
result = ifc4_to_brick_map.get(ifc_type_class, None) result = ifc4_to_brick_map.get(ifc_type_class, None)
if result: if result:
if result.startswith("http"):
return result
return f"https://brickschema.org/schema/Brick#{result}" return f"https://brickschema.org/schema/Brick#{result}"
# Generic fallback # Generic fallback
if element.is_a("IfcDistributionElement"): if element.is_a("IfcDistributionElement"):
@@ -474,7 +474,7 @@ def get_elements_by_pset(pset: ifcopenshell.entity_instance) -> set[ifcopenshell
"""Retrieve the elements (or element types) that are using the provided property set.""" """Retrieve the elements (or element types) that are using the provided property set."""
is_ifc2x3 = pset.file.schema == "IFC2X3" is_ifc2x3 = pset.file.schema == "IFC2X3"
elements = set() elements = set()
if pset.is_a("IfcPropertySet") or pset.is_a("IfcElementQuantity"): if pset.is_a("IfcPropertySet") or pset.is_a("IfcPreDefinedPropertySet") or pset.is_a("IfcElementQuantity"):
rels = pset.PropertyDefinitionOf if is_ifc2x3 else pset.DefinesOccurrence rels = pset.PropertyDefinitionOf if is_ifc2x3 else pset.DefinesOccurrence
for rel in rels: for rel in rels:
elements.update(rel.RelatedObjects) elements.update(rel.RelatedObjects)
@@ -27,10 +27,9 @@
"IfcFlowMeter.WATERMETER": "Water_Meter", "IfcFlowMeter.WATERMETER": "Water_Meter",
"IfcElectricMotor": "Motor", "IfcElectricMotor": "Motor",
"IfcSolarDevice.SOLARPANEL": "PV_Panel", "IfcSolarDevice.SOLARPANEL": "PV_Panel",
"IfcBuilding": "Building", "IfcBuilding": "https://w3id.org/rec#Building",
"IfcBuildingStorey": "Floor", "IfcBuildingStorey": "https://w3id.org/rec#Level",
"IfcSpace": "Space", "IfcSpace": "https://w3id.org/rec#Room",
"IfcSpatialZone": "Zone", "IfcSpatialZone": "https://w3id.org/rec#Zone",
"IfcSpatialZone.THERMAL": "HVAC_Zone", "IfcSpatialZone.THERMAL": "https://w3id.org/rec#HVACZone"
"IfcSpatialZone.LIGHTING": "Lighting_Zone"
} }
@@ -856,8 +856,10 @@ class FacetTransformer(lark.Transformer):
comparison, value = args comparison, value = args
def filter_function(element: ifcopenshell.entity_instance) -> bool: def filter_function(element: ifcopenshell.entity_instance) -> bool:
element_value = getattr(ifcopenshell.util.element.get_type(element), "Name", None) element_type = ifcopenshell.util.element.get_type(element)
return self.compare(element_value, comparison, value) return self.compare(getattr(element_type, "Name", None), comparison, value) or self.compare(
getattr(element_type, "GlobalId", None), comparison, value
)
self.add_default_elements() self.add_default_elements()
self.elements = set(filter(filter_function, self.elements)) self.elements = set(filter(filter_function, self.elements))
@@ -940,7 +942,7 @@ class FacetTransformer(lark.Transformer):
containers = self.get_container_tree(container) containers = self.get_container_tree(container)
result = False if containers else None result = False if containers else None
for container in containers: for container in containers:
if self.compare(container.Name, "=", value): if self.compare(container.Name, "=", value) or self.compare(container.GlobalId, "=", value):
result = True result = True
if result is not None: if result is not None:
return result if comparison == "=" else not result return result if comparison == "=" else not result
@@ -958,6 +960,8 @@ class FacetTransformer(lark.Transformer):
if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup: if rel.is_a("IfcRelAssignsToGroup") and rel.RelatingGroup:
if self.compare(rel.RelatingGroup.Name, "=", value): if self.compare(rel.RelatingGroup.Name, "=", value):
result = True result = True
elif self.compare(rel.RelatingGroup.GlobalId, "=", value):
result = True
return result if comparison == "=" else not result return result if comparison == "=" else not result
self.add_default_elements() self.add_default_elements()
@@ -969,32 +973,44 @@ class FacetTransformer(lark.Transformer):
parents = set() parents = set()
for rel in self.file.by_type("IfcRelAggregates"): for rel in self.file.by_type("IfcRelAggregates"):
parent = rel.RelatingObject parent = rel.RelatingObject
if parent and self.compare(parent.Name, comparison, value): if parent and (
self.compare(parent.Name, comparison, value) or self.compare(parent.GlobalId, comparison, value)
):
parents.add(parent) parents.add(parent)
for rel in self.file.by_type("IfcRelContainedInSpatialStructure"): for rel in self.file.by_type("IfcRelContainedInSpatialStructure"):
parent = rel.RelatingStructure parent = rel.RelatingStructure
if parent and self.compare(parent.Name, comparison, value): if parent and (
self.compare(parent.Name, comparison, value) or self.compare(parent.GlobalId, comparison, value)
):
parents.add(parent) parents.add(parent)
for rel in self.file.by_type("IfcRelNests"): for rel in self.file.by_type("IfcRelNests"):
parent = rel.RelatingObject parent = rel.RelatingObject
if parent and self.compare(parent.Name, comparison, value): if parent and (
self.compare(parent.Name, comparison, value) or self.compare(parent.GlobalId, comparison, value)
):
parents.add(parent) parents.add(parent)
for rel in self.file.by_type("IfcRelVoidsElement"): for rel in self.file.by_type("IfcRelVoidsElement"):
parent = rel.RelatingBuildingElement parent = rel.RelatingBuildingElement
if parent and self.compare(parent.Name, comparison, value): if parent and (
self.compare(parent.Name, comparison, value) or self.compare(parent.GlobalId, comparison, value)
):
parents.add(parent) parents.add(parent)
for rel in self.file.by_type("IfcRelVoidsElement"): for rel in self.file.by_type("IfcRelVoidsElement"):
parent = rel.RelatingBuildingElement parent = rel.RelatingBuildingElement
if parent and self.compare(parent.Name, comparison, value): if parent and (
self.compare(parent.Name, comparison, value) or self.compare(parent.GlobalId, comparison, value)
):
parents.add(parent) parents.add(parent)
for rel in self.file.by_type("IfcRelFillsElement"): for rel in self.file.by_type("IfcRelFillsElement"):
parent = rel.RelatingOpeningElement parent = rel.RelatingOpeningElement
if parent and self.compare(parent.Name, comparison, value): if parent and (
self.compare(parent.Name, comparison, value) or self.compare(parent.GlobalId, comparison, value)
):
parents.add(parent) parents.add(parent)
children: set[ifcopenshell.entity_instance] = set() children: set[ifcopenshell.entity_instance] = set()
@@ -1,3 +1,8 @@
import functools
import itertools
import multiprocessing
import operator
import os
import pytest import pytest
import test.bootstrap import test.bootstrap
import ifcopenshell import ifcopenshell
@@ -12,6 +17,8 @@ import ifcopenshell.util.shape
from ifcopenshell.util.shape_builder import ShapeBuilder from ifcopenshell.util.shape_builder import ShapeBuilder
from typing import get_args from typing import get_args
fn = os.path.join(os.path.dirname(__file__), "fixtures/ColumnPSetsOfSets.ifc")
class TestGeomSettings: class TestGeomSettings:
def test_settings(self): def test_settings(self):
@@ -185,6 +192,28 @@ class TestAssignObject:
assert len(set(vs)) == 12 assert len(set(vs)) == 12
def test_iterator():
# just test some permutations of invocation
settings = ifcopenshell.geom.settings()
file_or_filename = [fn, ifcopenshell.open(fn)]
with_or_without_threads = [[], [multiprocessing.cpu_count()]]
includes = [
{},
{"include": ["IfcColumn"]},
{"include": [file_or_filename[1].by_type("IfcColumn")[0]]},
]
for args in itertools.product(file_or_filename, with_or_without_threads, includes):
kwargs = functools.reduce(operator.or_, (a for a in args if isinstance(a, dict)))
pargs = []
for a in (_ for _ in args if not isinstance(_, dict)):
if isinstance(a, list):
pargs.extend(a)
else:
pargs.append(a)
iterator = ifcopenshell.geom.iterator(settings, *pargs, **kwargs)
assert iterator.initialize()
if __name__ == "__main__": if __name__ == "__main__":
import pytest import pytest
@@ -196,6 +196,7 @@ class TestFilterElements(test.bootstrap.IFC4):
assert subject.filter_elements(self.file, "IfcWall, type=Foo") == {element} assert subject.filter_elements(self.file, "IfcWall, type=Foo") == {element}
assert subject.filter_elements(self.file, 'IfcWall, type="Foo"') == {element} assert subject.filter_elements(self.file, 'IfcWall, type="Foo"') == {element}
assert subject.filter_elements(self.file, "IfcWall, type=/Fo.*/") == {element} assert subject.filter_elements(self.file, "IfcWall, type=/Fo.*/") == {element}
assert subject.filter_elements(self.file, f"IfcWall, type={element_type.GlobalId}") == {element}
def test_selecting_by_material(self): def test_selecting_by_material(self):
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
@@ -273,6 +274,7 @@ class TestFilterElements(test.bootstrap.IFC4):
assert subject.filter_elements(self.file, "IfcWall, location=G") == {element, element2} assert subject.filter_elements(self.file, "IfcWall, location=G") == {element, element2}
assert subject.filter_elements(self.file, "IfcWall, location=Building") == {element, element2} assert subject.filter_elements(self.file, "IfcWall, location=Building") == {element, element2}
assert subject.filter_elements(self.file, "IfcWall, location!=Space") == {element2} assert subject.filter_elements(self.file, "IfcWall, location!=Space") == {element2}
assert subject.filter_elements(self.file, f"IfcWall, location={space.GlobalId}") == {element}
def test_selecting_by_group(self): def test_selecting_by_group(self):
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
@@ -281,6 +283,7 @@ class TestFilterElements(test.bootstrap.IFC4):
ifcopenshell.api.group.assign_group(self.file, products=[element], group=group) ifcopenshell.api.group.assign_group(self.file, products=[element], group=group)
assert subject.filter_elements(self.file, "IfcWall, group=Foo") == {element} assert subject.filter_elements(self.file, "IfcWall, group=Foo") == {element}
assert subject.filter_elements(self.file, "IfcWall, group!=Foo") == {element2} assert subject.filter_elements(self.file, "IfcWall, group!=Foo") == {element2}
assert subject.filter_elements(self.file, f"IfcWall, group={group.GlobalId}") == {element}
def test_selecting_by_parent(self): def test_selecting_by_parent(self):
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall", name="Element1") element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall", name="Element1")
@@ -300,6 +303,7 @@ class TestFilterElements(test.bootstrap.IFC4):
assert subject.filter_elements(self.file, "IfcWall, parent=Space") == {element} assert subject.filter_elements(self.file, "IfcWall, parent=Space") == {element}
assert subject.filter_elements(self.file, "IfcWall, parent=G") == {element, element2, element3} assert subject.filter_elements(self.file, "IfcWall, parent=G") == {element, element2, element3}
assert subject.filter_elements(self.file, "IfcWall, parent=Element2") == {element3} assert subject.filter_elements(self.file, "IfcWall, parent=Element2") == {element3}
assert subject.filter_elements(self.file, "IfcWall, parent=Space") == {element}
def test_selecting_multiple_filter_groups(self): def test_selecting_multiple_filter_groups(self):
element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall") element = ifcopenshell.api.root.create_entity(self.file, ifc_class="IfcWall")
+5
View File
@@ -628,6 +628,7 @@ struct ShapeRTTI : public boost::static_visitor<PyObject*>
$result = boost::apply_visitor(ShapeRTTI(), (boost::variant<IfcGeom::Element*, IfcGeom::Representation::Representation*, IfcGeom::Transformation*>) $1); $result = boost::apply_visitor(ShapeRTTI(), (boost::variant<IfcGeom::Element*, IfcGeom::Representation::Representation*, IfcGeom::Transformation*>) $1);
} }
%newobject construct_iterator;
%newobject construct_iterator_with_include_exclude; %newobject construct_iterator_with_include_exclude;
%newobject construct_iterator_with_include_exclude_globalid; %newobject construct_iterator_with_include_exclude_globalid;
%newobject construct_iterator_with_include_exclude_id; %newobject construct_iterator_with_include_exclude_id;
@@ -635,6 +636,10 @@ struct ShapeRTTI : public boost::static_visitor<PyObject*>
// I couldn't get the vector<string> typemap to be applied when %extending Iterator constructor. // I couldn't get the vector<string> typemap to be applied when %extending Iterator constructor.
// anyway it does not matter as SWIG generates C code without actual constructors // anyway it does not matter as SWIG generates C code without actual constructors
%inline %{ %inline %{
IfcGeom::Iterator* construct_iterator(const std::string& geometry_library, ifcopenshell::geometry::Settings settings, IfcParse::IfcFile* file, int num_threads) {
return new IfcGeom::Iterator(ifcopenshell::geometry::kernels::construct(file, geometry_library, settings), settings, file, num_threads);
}
IfcGeom::Iterator* construct_iterator_with_include_exclude(const std::string& geometry_library, ifcopenshell::geometry::Settings settings, IfcParse::IfcFile* file, std::vector<std::string> elems, bool include, int num_threads) { IfcGeom::Iterator* construct_iterator_with_include_exclude(const std::string& geometry_library, ifcopenshell::geometry::Settings settings, IfcParse::IfcFile* file, std::vector<std::string> elems, bool include, int num_threads) {
std::set<std::string> elems_set(elems.begin(), elems.end()); std::set<std::string> elems_set(elems.begin(), elems.end());
IfcGeom::entity_filter ef{ include, false, elems_set }; IfcGeom::entity_filter ef{ include, false, elems_set };
+4
View File
@@ -33,6 +33,10 @@ private:
%ignore IfcParse::IfcFile::types_end; %ignore IfcParse::IfcFile::types_end;
%ignore IfcParse::IfcFile::internal_guid_map; %ignore IfcParse::IfcFile::internal_guid_map;
%ignore IfcParse::IfcFile::storage_; %ignore IfcParse::IfcFile::storage_;
%ignore IfcParse::IfcFile::byguid_;
%ignore IfcParse::IfcFile::byid_;
%ignore IfcParse::IfcFile::byref_excl_;
%ignore IfcParse::IfcFile::types_to_bypass_loading_;
%ignore IfcParse::InstanceStreamer::InstanceStreamer(const IfcParse::schema_definition* schema, IfcParse::IfcSpfLexer* lexer); %ignore IfcParse::InstanceStreamer::InstanceStreamer(const IfcParse::schema_definition* schema, IfcParse::IfcSpfLexer* lexer);
+4
View File
@@ -93,6 +93,10 @@
%ignore curve_to_face_upgrade_impl; %ignore curve_to_face_upgrade_impl;
%ignore loop_to_function_item_upgrade_impl; %ignore loop_to_function_item_upgrade_impl;
%ignore IfcGeom::geometry_exception;
%ignore IfcGeom::too_many_faces_exception;
%ignore ifcopenshell::geometry::taxonomy::topology_error;
// settings, can this done more generally? // settings, can this done more generally?
// GeometrySerializer.h // GeometrySerializer.h
%ignore UseElementNames; %ignore UseElementNames;
+1 -1
View File
@@ -3,7 +3,7 @@ from starlette.background import BackgroundTask
from starlette.responses import StreamingResponse from starlette.responses import StreamingResponse
from fastapi.routing import APIRoute from fastapi.routing import APIRoute
from starlette.types import Message from starlette.types import Message
from typing import Callable from collections.abc import Callable
import logging import logging
import httpx import httpx