2022-01-19 12:18:33 +11:00
# IfcOpenShell - IFC toolkit and geometry engine
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
#
# This file is part of IfcOpenShell.
#
# IfcOpenShell is free software: you can redistribute it and/or modify
# 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
# (at your option) any later version.
#
# IfcOpenShell is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with IfcOpenShell. If not, see <http://www.gnu.org/licenses/>.
2024-05-07 17:32:47 +10:00
""" High level IFC authoring and editing functions
Authoring, editing, and deleting IFC data requires a detailed understanding of
the rules of the IFC schema. This API module provides simple to use authoring
functions that hide this complexity from you. Things like managing differences
between IFC versions, tracking owernship changes, or cleaning up after orphaned
relationships are all handled automatically.
"""
2022-05-09 15:35:52 +10:00
2021-05-31 14:18:24 +10:00
import json
import numpy
2024-05-05 16:26:41 +10:00
import pkgutil
2024-05-06 14:43:37 +10:00
import inspect
2021-03-27 22:52:52 +11:00
import importlib
2021-05-19 14:45:36 +10:00
import ifcopenshell
2024-04-08 15:05:37 +05:00
from typing import Callable , Any , Optional
2024-04-08 15:47:31 +05:00
from functools import partial
2021-03-27 22:52:52 +11:00
2024-05-07 17:32:47 +10:00
pre_listeners : dict [ str , dict ] = { }
post_listeners : dict [ str , dict ] = { }
2021-05-19 14:45:36 +10:00
2024-04-09 16:06:21 +05:00
def batching_argument_deprecation (
usecase_path : str , settings : dict , prev_argument : str , new_argument : str , replace_usecase : Optional [ str ] = None
) - > tuple [ str , dict ] :
if replace_usecase is not None :
print ( f " WARNING. ` { usecase_path } ` api method is deprecated and should be replaced with ` { replace_usecase } `. " )
2024-04-08 15:47:31 +05:00
if prev_argument in settings :
print (
f " WARNING. ` { prev_argument } ` argument is deprecated for API method "
f ' " { usecase_path } " and should be replaced with ` { new_argument } `. '
)
settings = settings | { new_argument : [ settings [ prev_argument ] ] }
settings . pop ( prev_argument )
2024-04-09 16:06:21 +05:00
return ( replace_usecase or usecase_path , settings )
2024-04-08 15:47:31 +05:00
ARGUMENTS_DEPRECATION = {
" spatial.assign_container " : partial (
batching_argument_deprecation , prev_argument = " product " , new_argument = " products "
) ,
" spatial.unassign_container " : partial (
batching_argument_deprecation , prev_argument = " product " , new_argument = " products "
) ,
" group.unassign_group " : partial ( batching_argument_deprecation , prev_argument = " product " , new_argument = " products " ) ,
2024-04-08 16:23:48 +05:00
" aggregate.assign_object " : partial ( batching_argument_deprecation , prev_argument = " product " , new_argument = " products " ) ,
2024-04-09 16:06:21 +05:00
" aggregate.unassign_object " : partial (
batching_argument_deprecation , prev_argument = " product " , new_argument = " products "
) ,
2024-04-08 15:47:31 +05:00
" layer.assign_layer " : partial ( batching_argument_deprecation , prev_argument = " item " , new_argument = " items " ) ,
" layer.unassign_layer " : partial ( batching_argument_deprecation , prev_argument = " item " , new_argument = " items " ) ,
2024-04-09 16:06:21 +05:00
" spatial.remove_container " : partial (
batching_argument_deprecation ,
prev_argument = " product " ,
new_argument = " products " ,
replace_usecase = " spatial.unassign_container " ,
) ,
2024-04-09 16:41:34 +05:00
" nest.assign_object " : partial (
batching_argument_deprecation , prev_argument = " related_object " , new_argument = " related_objects "
) ,
2024-04-09 17:12:25 +05:00
" nest.unassign_object " : partial (
batching_argument_deprecation , prev_argument = " related_object " , new_argument = " related_objects "
) ,
2024-04-10 17:18:37 +05:00
" type.assign_type " : partial (
batching_argument_deprecation , prev_argument = " related_object " , new_argument = " related_objects "
) ,
2024-04-11 15:52:33 +05:00
" type.unassign_type " : partial (
batching_argument_deprecation , prev_argument = " related_object " , new_argument = " related_objects "
) ,
2024-04-12 16:15:14 +05:00
" system.assign_system " : partial ( batching_argument_deprecation , prev_argument = " product " , new_argument = " products " ) ,
" system.unassign_system " : partial ( batching_argument_deprecation , prev_argument = " product " , new_argument = " products " ) ,
" material.assign_material " : partial (
2024-04-11 16:59:39 +05:00
batching_argument_deprecation , prev_argument = " product " , new_argument = " products "
) ,
2024-04-12 16:15:14 +05:00
" material.unassign_material " : partial (
2024-04-12 11:21:35 +05:00
batching_argument_deprecation , prev_argument = " product " , new_argument = " products "
) ,
2024-04-15 12:06:39 +05:00
" classification.add_reference " : partial (
batching_argument_deprecation , prev_argument = " product " , new_argument = " products "
) ,
2024-04-15 12:20:19 +05:00
" classification.remove_reference " : partial (
batching_argument_deprecation , prev_argument = " product " , new_argument = " products "
) ,
2024-04-15 16:00:40 +05:00
" library.assign_reference " : partial (
batching_argument_deprecation , prev_argument = " product " , new_argument = " products "
) ,
2024-04-15 16:28:43 +05:00
" library.unassign_reference " : partial (
batching_argument_deprecation , prev_argument = " product " , new_argument = " products "
) ,
2024-04-15 16:32:54 +05:00
" document.assign_document " : partial (
batching_argument_deprecation , prev_argument = " product " , new_argument = " products "
) ,
2024-04-15 16:54:54 +05:00
" document.unassign_document " : partial (
batching_argument_deprecation , prev_argument = " product " , new_argument = " products "
) ,
2024-04-16 10:53:54 +05:00
" spatial.reference_structure " : partial (
batching_argument_deprecation , prev_argument = " product " , new_argument = " products "
) ,
2024-04-16 10:59:50 +05:00
" spatial.dereference_structure " : partial (
batching_argument_deprecation , prev_argument = " product " , new_argument = " products "
) ,
2024-04-16 11:24:45 +05:00
" constraint.assign_constraint " : partial (
batching_argument_deprecation , prev_argument = " product " , new_argument = " products "
) ,
2024-04-16 11:52:42 +05:00
" constraint.unassign_constraint " : partial (
batching_argument_deprecation , prev_argument = " product " , new_argument = " products "
) ,
2024-05-03 14:37:59 +05:00
" project.assign_declaration " : partial (
batching_argument_deprecation , prev_argument = " definition " , new_argument = " definitions "
) ,
2024-05-03 14:58:32 +05:00
" project.unassign_declaration " : partial (
batching_argument_deprecation , prev_argument = " definition " , new_argument = " definitions "
) ,
2024-04-08 15:47:31 +05:00
}
2024-05-07 17:32:47 +10:00
CACHED_USECASE_CLASSES : dict [ str , Callable ] = { }
CACHED_USECASES : dict [ str , Callable ] = { }
2024-04-09 16:18:11 +05:00
2024-04-09 16:41:34 +05:00
2024-04-08 15:05:37 +05:00
def run (
usecase_path : str ,
ifc_file : Optional [ ifcopenshell . file ] = None ,
2024-05-05 16:26:41 +10:00
should_run_listeners : bool = True ,
2024-04-08 15:05:37 +05:00
* * settings : Any ,
) - > Any :
2024-05-05 16:26:41 +10:00
usecase_function = CACHED_USECASES . get ( usecase_path )
if not usecase_function :
importlib . import_module ( f " ifcopenshell.api. { usecase_path } " )
module , usecase = usecase_path . split ( " . " )
usecase_function = getattr ( getattr ( ifcopenshell . api , module ) , usecase )
CACHED_USECASES [ usecase_path ] = usecase_function
if ifc_file :
return usecase_function ( ifc_file , should_run_listeners = should_run_listeners , * * settings )
return usecase_function ( should_run_listeners = should_run_listeners , * * settings )
2021-05-19 14:45:36 +10:00
if should_run_listeners :
2021-06-05 19:46:41 +10:00
for listener in pre_listeners . get ( usecase_path , { } ) . values ( ) :
2021-06-04 17:30:37 +10:00
listener ( usecase_path , ifc_file , settings )
2021-05-29 01:06:38 +02:00
2024-04-08 15:47:31 +05:00
# see #4531
if usecase_path in ARGUMENTS_DEPRECATION :
2024-04-09 16:06:21 +05:00
usecase_path , settings = ARGUMENTS_DEPRECATION [ usecase_path ] ( usecase_path , settings )
2024-04-08 15:47:31 +05:00
2024-03-11 11:10:09 +05:00
# TODO: settings serialization for client-server systems
# def serialise_entity_instance(entity):
# return {"cast_type": "entity_instance", "value": entity.id(), "Name": getattr(entity, "Name", None)}
# vcs_settings = settings.copy()
# for key, value in settings.items():
# if isinstance(value, ifcopenshell.entity_instance):
# vcs_settings[key] = serialise_entity_instance(value)
# elif isinstance(value, numpy.ndarray):
# vcs_settings[key] = {"cast_type": "ndarray", "value": value.tolist()}
# elif isinstance(value, list) and value and isinstance(value[0], ifcopenshell.entity_instance):
# vcs_settings[key] = [serialise_entity_instance(i) for i in value]
2021-05-31 14:18:24 +10:00
if " add_representation " in usecase_path :
pass
2021-06-05 19:46:41 +10:00
# print(usecase_path, "{ ... settings too complex right now ... }")
2021-05-31 14:18:24 +10:00
elif " owner. " in usecase_path :
pass
else :
pass
# print(vcs_settings)
# try:
2021-06-05 19:46:41 +10:00
# print(usecase_path, json.dumps(vcs_settings))
2021-05-31 14:18:24 +10:00
# except:
2021-06-05 19:46:41 +10:00
# print(usecase_path, vcs_settings)
2021-05-31 14:18:24 +10:00
2024-04-09 16:18:11 +05:00
usecase_class = CACHED_USECASE_CLASSES . get ( usecase_path )
if usecase_class is None :
importlib . import_module ( f " ifcopenshell.api. { usecase_path } " )
module , usecase = usecase_path . split ( " . " )
usecase_class = getattr ( getattr ( getattr ( ifcopenshell . api , module ) , usecase ) , " Usecase " )
CACHED_USECASE_CLASSES [ usecase_path ] = usecase_class
2021-05-19 14:45:36 +10:00
2021-03-27 22:52:52 +11:00
if ifc_file :
2021-05-19 14:45:36 +10:00
result = usecase_class ( ifc_file , * * settings ) . execute ( )
else :
result = usecase_class ( * * settings ) . execute ( )
if should_run_listeners :
2021-06-05 19:46:41 +10:00
for listener in post_listeners . get ( usecase_path , { } ) . values ( ) :
2021-06-04 17:30:37 +10:00
listener ( usecase_path , ifc_file , settings )
2021-05-29 01:06:38 +02:00
2021-05-19 14:45:36 +10:00
return result
2024-03-13 15:21:26 +05:00
def add_pre_listener ( usecase_path : str , name : str , callback : Callable [ [ str , ifcopenshell . file , dict ] , None ] ) - > None :
2021-05-31 14:18:24 +10:00
""" Add a pre listener
2021-06-05 19:46:41 +10:00
2021-05-29 14:03:00 +02:00
:param usecase_path: string, ifcopenshell api use case path
2021-06-05 19:46:41 +10:00
:param name: string, name of listener
2024-03-13 15:21:26 +05:00
:param callback: callback function with 3 arguments: `usecase_path`, `ifc_file`, `settings`
2021-05-29 14:03:00 +02:00
"""
2021-06-05 19:46:41 +10:00
pre_listeners . setdefault ( usecase_path , { } ) [ name ] = callback
2021-05-29 01:06:38 +02:00
2021-05-19 14:45:36 +10:00
2024-03-13 15:21:26 +05:00
def add_post_listener ( usecase_path : str , name : str , callback : Callable [ [ str , ifcopenshell . file , dict ] , None ] ) - > None :
2021-05-31 14:18:24 +10:00
""" Add a post listener
2021-06-05 19:46:41 +10:00
2021-05-29 14:03:00 +02:00
:param usecase_path: string, ifcopenshell api use case path
2021-06-05 19:46:41 +10:00
:param name: string, name of listener
2024-03-13 15:21:26 +05:00
:param callback: callback function with 3 arguments: `usecase_path`, `ifc_file`, `settings`
2021-05-29 14:03:00 +02:00
"""
2021-06-05 19:46:41 +10:00
post_listeners . setdefault ( usecase_path , { } ) [ name ] = callback
2021-05-29 01:06:38 +02:00
2024-03-13 15:21:26 +05:00
def remove_pre_listener ( usecase_path : str , name : str , callback : Callable [ [ str , ifcopenshell . file , dict ] , None ] ) - > None :
2021-05-31 14:18:24 +10:00
""" Remove a pre listener
2021-06-05 19:46:41 +10:00
:param usecase_path: string, ifcopenshell api use case path
:param name: string, name of listener
2024-03-13 15:21:26 +05:00
:param callback: callback function with 3 arguments: `usecase_path`, `ifc_file`, `settings`
2021-05-29 14:03:00 +02:00
"""
2021-06-05 19:46:41 +10:00
pre_listeners . get ( usecase_path , { } ) . pop ( name , None )
2024-03-13 15:21:26 +05:00
def remove_post_listener (
usecase_path : str , name : str , callback : Callable [ [ str , ifcopenshell . file , dict ] , None ]
) - > None :
2021-05-31 14:18:24 +10:00
""" Remove a post listener
2021-06-05 19:46:41 +10:00
:param usecase_path: string, ifcopenshell api use case path
:param name: string, name of listener
2024-03-13 15:21:26 +05:00
:param callback: callback function with 3 arguments: `usecase_path`, `ifc_file`, `settings`
2021-05-29 14:03:00 +02:00
"""
2021-06-05 19:46:41 +10:00
post_listeners . get ( usecase_path , { } ) . pop ( name , None )
2021-05-29 14:59:02 +10:00
2021-05-28 02:07:56 +02:00
def remove_all_listeners ( ) :
pre_listeners . clear ( )
post_listeners . clear ( )
2021-07-18 12:57:38 +10:00
def extract_docs ( module , usecase ) :
import typing
import collections
inputs = collections . OrderedDict ( )
function_init = getattr ( getattr ( ifcopenshell . api , module ) , usecase ) . Usecase . __init__
function_execute = getattr ( getattr ( ifcopenshell . api , module ) , usecase ) . Usecase . execute
node_data = { " module " : module , " usecase " : usecase }
signature = inspect . signature ( function_init )
for name , parameter in signature . parameters . items ( ) :
if name == " self " :
continue
inputs [ name ] = { " name " : name }
2021-07-25 18:42:28 +10:00
if isinstance ( parameter . default , ( str , float , int , bool ) ) :
2021-07-18 12:57:38 +10:00
inputs [ name ] [ " default " ] = parameter . default
type_hints = typing . get_type_hints ( function_init )
for name , socket_data in inputs . items ( ) :
type_hint = type_hints [ name ]
if isinstance ( type_hint , typing . _UnionGenericAlias ) :
inputs [ name ] [ " type " ] = [ t . __name__ for t in typing . get_args ( type_hint ) ]
else :
inputs [ name ] [ " type " ] = type_hint . __name__
description = " "
for i , line in enumerate ( function_init . __doc__ . split ( " \n " ) ) :
line = line . strip ( )
if i == 0 :
node_data [ " name " ] = line
elif line . startswith ( " :return: " ) :
node_data [ " output " ] = { " name " : line . split ( " : " ) [ 2 ] . strip ( ) , " description " : line . split ( " : " ) [ 3 ] . strip ( ) }
elif line . startswith ( " :param " ) :
param_name = line . split ( " : " ) [ 1 ] . strip ( ) . replace ( " param " , " " )
inputs [ param_name ] [ " description " ] = line . split ( " : " ) [ 2 ] . strip ( )
elif i > = 2 :
description + = line
if " output " in node_data :
node_data [ " output " ] [ " type " ] = typing . get_type_hints ( function_execute ) [ " return " ] . __name__
node_data [ " description " ] = description . strip ( )
node_data [ " inputs " ] = inputs
return node_data
2024-05-05 16:26:41 +10:00
2024-05-06 14:43:37 +10:00
def wrap_usecase ( usecase_path , usecase ) :
""" Wraps an API function in pre/post listeners. """
2024-05-05 16:26:41 +10:00
2024-05-06 14:43:37 +10:00
def wrapper ( * args , should_run_listeners : bool = True , * * settings ) :
ifc_file = args [ 0 ] if args else None
if should_run_listeners :
for listener in pre_listeners . get ( usecase_path , { } ) . values ( ) :
listener ( usecase_path , ifc_file , settings )
2024-05-05 16:26:41 +10:00
2024-05-06 14:43:37 +10:00
try :
result = usecase ( * args , * * settings )
except TypeError as e :
2024-05-07 17:32:47 +10:00
msg = f " Incorrect function arguments provided for { usecase_path } \n { str ( e ) } . You specified args { args } and settings { settings } \n \n Correct signature is { inspect . signature ( usecase ) } \n See help(ifcopenshell.api. { usecase_path } ) for documentation. "
2024-05-06 14:43:37 +10:00
raise TypeError ( msg ) from e
2024-05-05 16:26:41 +10:00
2024-05-06 14:43:37 +10:00
if should_run_listeners :
for listener in post_listeners . get ( usecase_path , { } ) . values ( ) :
listener ( usecase_path , ifc_file , settings )
2024-05-05 16:26:41 +10:00
2024-05-06 14:43:37 +10:00
return result
2024-05-05 16:26:41 +10:00
2024-05-06 14:43:37 +10:00
wrapper . __signature__ = inspect . signature ( usecase )
wrapper . __doc__ = usecase . __doc__
wrapper . __name__ = usecase_path
return wrapper
2024-05-05 16:26:41 +10:00
# Expose all submodules. This means that the user can just type `import ifcopenshell.api`.
2024-05-06 18:38:58 +10:00
import ifcopenshell . api . aggregate as aggregate
import ifcopenshell . api . attribute as attribute
import ifcopenshell . api . boundary as boundary
import ifcopenshell . api . classification as classification
import ifcopenshell . api . constraint as constraint
import ifcopenshell . api . context as context
import ifcopenshell . api . control as control
import ifcopenshell . api . cost as cost
import ifcopenshell . api . document as document
import ifcopenshell . api . drawing as drawing
import ifcopenshell . api . geometry as geometry
import ifcopenshell . api . georeference as georeference
import ifcopenshell . api . grid as grid
import ifcopenshell . api . group as group
import ifcopenshell . api . layer as layer
import ifcopenshell . api . library as library
import ifcopenshell . api . material as material
import ifcopenshell . api . nest as nest
import ifcopenshell . api . owner as owner
import ifcopenshell . api . profile as profile
import ifcopenshell . api . project as project
import ifcopenshell . api . pset as pset
import ifcopenshell . api . pset_template as pset_template
import ifcopenshell . api . resource as resource
import ifcopenshell . api . root as root
import ifcopenshell . api . sequence as sequence
import ifcopenshell . api . spatial as spatial
import ifcopenshell . api . structural as structural
import ifcopenshell . api . style as style
import ifcopenshell . api . system as system
import ifcopenshell . api . type as type # Whoohoo!
import ifcopenshell . api . unit as unit
import ifcopenshell . api . void as void
# Wrap all submodule usecases with listeners.
# This for loop also conveniently ensures that the above imports are comprehensive.
2024-05-05 16:26:41 +10:00
for loader , module_name , is_pkg in pkgutil . iter_modules ( __path__ , __name__ + " . " ) :
# Check if it's a direct child (only one level deep)
if module_name . count ( " . " ) == __name__ . count ( " . " ) + 1 :
2024-05-06 18:38:58 +10:00
module_name = module_name . split ( " . " ) [ - 1 ]
module = globals ( ) [ module_name ]
2024-05-06 14:43:37 +10:00
for usecase_name in vars ( module ) :
usecase = getattr ( module , usecase_name )
if callable ( usecase ) :
2024-05-06 18:38:58 +10:00
usecase_path = f " { module_name } . { usecase_name } "
2024-05-06 14:43:37 +10:00
setattr ( module , usecase_name , wrap_usecase ( usecase_path , usecase ) )