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/>.
2022-05-09 15:35:52 +10:00
""" High level user-oriented IFC authoring capabilities """
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
2021-03-27 22:52:52 +11:00
import ifcopenshell . api
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
2021-05-19 14:45:36 +10:00
pre_listeners = { }
post_listeners = { }
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-05 16:26:41 +10:00
CACHED_USECASE_CLASSES = { }
CACHED_USECASES = { }
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
results = [ ]
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 :
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 . __init__ ) } \n See help(ifcopenshell.api. { usecase_path } ) for documentation. "
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`.
for loader , module_name , is_pkg in pkgutil . iter_modules ( __path__ , __name__ + " . " ) :
module = importlib . import_module ( module_name )
# Check if it's a direct child (only one level deep)
if module_name . count ( " . " ) == __name__ . count ( " . " ) + 1 :
2024-05-06 14:43:37 +10:00
for usecase_name in vars ( module ) :
usecase = getattr ( module , usecase_name )
if callable ( usecase ) :
usecase_path = f " { module_name . split ( ' . ' ) [ - 1 ] } . { usecase_name } "
setattr ( module , usecase_name , wrap_usecase ( usecase_path , usecase ) )