diff --git a/README.md b/README.md index 94e275d..7d5b717 100644 --- a/README.md +++ b/README.md @@ -2,5 +2,3 @@ This package provides an API to write and execute scripts in a running ZEISS INSPECT instance. Please read the [ZEISS INSPECT API documentation](https://zeiss.github.io/IQS/) for details. - -The [ZEISS INSPECT API wheel](https://pypi.org/project/zeiss-inspect-api/) is hosted on PyPI. diff --git a/pyproject.toml b/pyproject.toml index 7580d29..f818e5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,23 @@ -[build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" - -[project] -name = "zeiss_inspect_api" -version = "2026.1.0.359" -authors = [ - { name="Carl Zeiss GOM Metrology GmbH", email="info.optical.metrology@zeiss.com" }, -] -description = "ZEISS INSPECT API" -readme = "README.md" -requires-python = ">=3.9" -dependencies = [ - "websocket-client" -] -classifiers = [ - "Programming Language :: Python :: 3", - "Operating System :: OS Independent", -] - -[project.urls] -"Homepage" = "https://github.com/ZEISS/zeiss-inspect-api-wheel" +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "zeiss_inspect_api" +version = "2026.3.0.342" +authors = [ + { name="Carl Zeiss GOM Metrology GmbH", email="info.optical.metrology@zeiss.com" }, +] +description = "ZEISS INSPECT API" +readme = "README.md" +requires-python = ">=3.9" +dependencies = [ + "websocket-client" +] +classifiers = [ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", +] + +[project.urls] +"Homepage" = "https://github.com/ZEISS/zeiss-inspect-api-wheel" diff --git a/src/gom/__api__.py b/src/gom/__api__.py index e29397a..48fcf90 100644 --- a/src/gom/__api__.py +++ b/src/gom/__api__.py @@ -382,13 +382,14 @@ def __instance_creator__(self, obj): # return GomApiInstance(obj['id']) - def encode(self, req): + def encode(self, req, caller): from gom.__network__ import EncoderContext, DecoderContext with EncoderContext() as context: string = json.dumps(req, cls=Encoder.CustomJSONEncoder, encoding_context=context) - string = gom.__common__.__connection__.request(Request.API, {'json': string}) + string = gom.__common__.__connection__.request( + Request.API, {'json': string, 'caller': caller if caller != None else ""}) with DecoderContext() as context: reply = json.loads(string, cls=Encoder.CustomJSONDecoder, decoding_context=context) @@ -399,7 +400,7 @@ def encode(self, req): return reply['result'] raise KeyError() - def call_function(self, module, function, args, kwargs): + def call_function(self, module, function, args, kwargs={}, caller=None): # # Script-to-script call shortlink. This way, various value conversions can be skipped and shared memory for @@ -414,16 +415,16 @@ def call_function(self, module, function, args, kwargs): 'params': args } - return self.encode(request) + return self.encode(request, caller) - def call_method(self, instance, method, args): + def call_method(self, instance, method, args, caller=None): request = { 'instance': instance, 'call': method, 'params': args } - return self.encode(request) + return self.encode(request, caller) __encoder__ = Encoder() @@ -437,12 +438,20 @@ def __call_function__(*args, **kwargs): ''' frame = inspect.currentframe().f_back + # Extract the original function caller, i.e., the script in which the api function was written + # The distinction to the executed script is important for some api functions in the context of shared environments + # There scripts from other apps can be imported and some api functions resolve the calls based on the app (e.g. settings) + parent = frame.f_back + caller = "" + if parent != None: + caller = parent.f_code.co_filename + module = inspect.getmodule(frame).__name__ prefix = 'gom.api.' if module.startswith(prefix): module = module[len(prefix):] - return __encoder__.call_function(module, frame.f_code.co_name, args, kwargs) + return __encoder__.call_function(module, frame.f_code.co_name, args, kwargs, caller) class Object: diff --git a/src/gom/__init__.py b/src/gom/__init__.py index 4263b0f..98eac3b 100644 --- a/src/gom/__init__.py +++ b/src/gom/__init__.py @@ -48,7 +48,6 @@ import gom.__api__ import gom.__config__ -import gom.__state__ import gom.__test__ import gom.__tools__ diff --git a/src/gom/__network__.py b/src/gom/__network__.py index 972d0e4..07d633b 100644 --- a/src/gom/__network__.py +++ b/src/gom/__network__.py @@ -27,7 +27,6 @@ # POSSIBILITY OF SUCH DAMAGE. # -import inspect import pickle import threading import traceback @@ -151,8 +150,8 @@ def request(self, command, params): ''' Send request and wait for incoming replies or calls - This function sends a request to the server and waits until a new message comes in. - That message can either be the reply to the request sent or a new client site function + This function sends a request to the server and waits until a new message comes in. + That message can either be the reply to the request sent or a new client site function call. The function does not return before the request has been answered. ''' if not isinstance(command, Request): diff --git a/src/gom/__tools__.py b/src/gom/__tools__.py index eadb2bb..7c1470f 100644 --- a/src/gom/__tools__.py +++ b/src/gom/__tools__.py @@ -71,15 +71,22 @@ def filter_exception_traceback(tb): # Traceback frames originating from the 'gom' system module are completely skipped. # The temporary directory information is removed from the traceback file paths. # + # entries at the start of the stacktrace are removed as well, these originate from the in-memory startup-script + # filtered = [] + at_stacktrace_start = True for frame, line in traceback.walk_tb(tb): if not gom.__config__.strip_tracebacks: filtered.append((frame, line)) + elif at_stacktrace_start and frame.f_code.co_filename == "": + continue elif not os.path.realpath(frame.f_code.co_filename).startswith(system_frame_prefix): if not '\\importlib\\' in frame.f_code.co_filename: if not 'frozen importlib' in frame.f_code.co_filename: - if str(frame.f_code.co_filename) != executed_file_prefix + '__gom_run_script__.py': - filtered.append((frame, line)) + filtered.append((frame, line)) + # Once we encounter anything but a to be filtered "" entry, + # we do not filter any other of this type from the middle of the stacktrace + at_stacktrace_start = False return ''.join(traceback.StackSummary.extract( filtered).format()).replace(executed_file_prefix, '') diff --git a/src/gom/__types__.py b/src/gom/__types__.py index 2bb2043..41db2b2 100644 --- a/src/gom/__types__.py +++ b/src/gom/__types__.py @@ -124,7 +124,7 @@ def type_getattribute(self, key): @staticmethod def type_setattr(self, key, value): - if key in self.__args__: + if key in self.__kwargs__: self.__kwargs__[key] = value gom.__common__.__connection__.request(Request.TYPE_SETATTR, {'type': type(self).__id__, diff --git a/src/gom/api/__init__.py b/src/gom/api/__init__.py deleted file mode 100644 index b412195..0000000 --- a/src/gom/api/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -# -# api/progress/__init__.py - gom.api infrastructure access classes -# -# (C) 2023 Carl Zeiss GOM Metrology GmbH -# -# Use of this source code and binary forms of it, without modification, is permitted provided that -# the following conditions are met: -# -# 1. Redistribution of this source code or binary forms of this with or without any modifications is -# not allowed without specific prior written permission by GOM. -# -# As this source code is provided as glue logic for connecting the Python interpreter to the commands of -# the GOM software any modification to this sources will not make sense and would affect a suitable functioning -# and therefore shall be avoided, so consequently the redistribution of this source with or without any -# modification in source or binary form is not permitted as it would lead to malfunctions of GOM Software. -# -# 2. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or -# promote products derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED -# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A -# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED -# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING -# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -# diff --git a/src/gom/api/__internal__progress/__init__.py b/src/gom/api/__internal__progress/__init__.py index 157b5c1..63400a7 100644 --- a/src/gom/api/__internal__progress/__init__.py +++ b/src/gom/api/__internal__progress/__init__.py @@ -1,75 +1,75 @@ -# -# API declarations for gom.api.__internal__progress -# -# @nodoc -# -# @brief Internal API for accessing the progress bar in the main window -# - -import gom -import gom.__api__ - -from typing import Any -from uuid import UUID - -class InternalProgressBar (gom.__api__.Object): - ''' - @nodoc - - @brief Class representing the internal ProgressBar - ''' - - def __init__ (self, instance_id): - super ().__init__ (instance_id) - - def _register_watcher(self) -> None: - ''' - @nodoc - - @brief Only for internal use! - ''' - return self.__call_method__('_register_watcher') - - def _deregister_watcher(self) -> None: - ''' - @nodoc - - @brief Only for internal use! - ''' - return self.__call_method__('_deregister_watcher') - - def _set_progress(self, progress:int) -> None: - ''' - @nodoc - - @brief Sets the progress in the main window progress bar - @version 1 - - @param progress in percent, given as an integer from 0 to 100 - @return nothing - ''' - return self.__call_method__('_set_progress', progress) - - def _set_message(self, message:str) -> None: - ''' - @nodoc - - @brief Sets a message in the main window progress bar - @version 1 - - @param message the message to display - @return nothing - ''' - return self.__call_method__('_set_message', message) - - -def _create_internal_progress_bar(passcode:str) -> None: - ''' - gom.api.__internal__progress.InternalProgressBar - - @nodoc - - @brief Only for internal use! - ''' - return gom.__api__.__call_function__(passcode) - +# +# API declarations for gom.api.__internal__progress +# +# @nodoc +# +# @brief Internal API for accessing the progress bar in the main window +# + +import gom +import gom.__api__ + +from typing import Any +from uuid import UUID + +class InternalProgressBar (gom.__api__.Object): + ''' + @nodoc + + @brief Class representing the internal ProgressBar + ''' + + def __init__ (self, instance_id): + super ().__init__ (instance_id) + + def _register_watcher(self) -> None: + ''' + @nodoc + + @brief Only for internal use! + ''' + return self.__call_method__('_register_watcher') + + def _deregister_watcher(self) -> None: + ''' + @nodoc + + @brief Only for internal use! + ''' + return self.__call_method__('_deregister_watcher') + + def _set_progress(self, progress:int) -> None: + ''' + @nodoc + + @brief Sets the progress in the main window progress bar + @version 1 + + @param progress in percent, given as an integer from 0 to 100 + @return nothing + ''' + return self.__call_method__('_set_progress', progress) + + def _set_message(self, message:str) -> None: + ''' + @nodoc + + @brief Sets a message in the main window progress bar + @version 1 + + @param message the message to display + @return nothing + ''' + return self.__call_method__('_set_message', message) + + +def _create_internal_progress_bar(passcode:str) -> None: + ''' + gom.api.__internal__progress.InternalProgressBar + + @nodoc + + @brief Only for internal use! + ''' + return gom.__api__.__call_function__(passcode) + diff --git a/src/gom/api/addons/__init__.py b/src/gom/api/addons/__init__.py index 2d9e6f7..e021a67 100644 --- a/src/gom/api/addons/__init__.py +++ b/src/gom/api/addons/__init__.py @@ -1,319 +1,319 @@ -# -# API declarations for gom.api.addons -# -# @brief API for accessing the add-ons currently installed in the running software instance -# -# This API enables access to the installed add-ons. Information about these add-ons can be -# queried, add-on files and resources can be read and if the calling instance is a member of -# one specific add-on, this specific add-on can be modified on-the-fly and during software -# update processes. -# - -import gom -import gom.__api__ - -from typing import Any -from uuid import UUID - -class AddOn (gom.__api__.Object): - ''' - @brief Class representing a single add-on - - This class represents a single add-on. Properties of that add-on can be queried from here. - ''' - - def __init__ (self, instance_id): - super ().__init__ (instance_id) - - def get_id(self) -> UUID: - ''' - @brief Return the unique id (uuid) of this add-on - @version 1 - - This function returns the uuid associated with this add-on. The id can be used to - uniquely address the add-on. - - @return Add-on uuid - ''' - return self.__call_method__('get_id') - - def get_name(self) -> str: - ''' - @brief Return the displayable name of the add-on - @version 1 - - This function returns the displayable name of the add-on. This is the human - readable name which is displayed in the add-on manager and the add-on store. - - @return Add-on name - ''' - return self.__call_method__('get_name') - - def get_file(self) -> str: - ''' - @brief Return the installed add-on file - @version 1 - - This function returns the installed ZIP file representing the add-on. The file might be - empty if the add-on has never been 'completed'. If the add-on is currently in edit mode, - instead the edit directory containing the unpacked add-on sources is returned. In any way, - this function returns the location the application uses, too, to access add-on content. - - @return Add-on file path (path to the add-ons installed ZIP file) or add-on edit directory if the add-on is currently in edit mode. - ''' - return self.__call_method__('get_file') - - def get_level(self) -> str: - ''' - @brief Return the level (system/shared/user) of the add-on - @version 1 - - This function returns the 'configuration level' of the add-on. This can be - * 'system' for pre installed add-on which are distributed together with the application - * 'shared' for add-ons in the public or shared folder configured in the application's preferences or - * 'user' for user level add-ons installed for the current user only. - - @return Level of the add-on - ''' - return self.__call_method__('get_level') - - def is_edited(self) -> bool: - ''' - @brief Return if the add-on is currently edited - @version 1 - - Usually, an add-on is simply a ZIP file which is included into the applications file system. When - an add-on is in edit mode, it will be temporarily unzipped and is then present on disk in a directory. - - @return 'true' if the add-on is currently in edit mode - ''' - return self.__call_method__('is_edited') - - def is_protected(self) -> bool: - ''' - @brief Return if the add-on is protected - @version 1 - - The content of a protected add-on is encrypted. It can be listed, but not read. Protection - includes both 'IP protection' (content cannot be read) and 'copy protection' (content cannot be - copied, as far as possible) - - @return Add-on protection state - ''' - return self.__call_method__('is_protected') - - def has_license(self) -> bool: - ''' - @brief Return if the necessary licenses to use this add-on are present - @version 1 - - This function returns if the necessary licenses to use the add-on are currently present. - Add-ons can either be free and commercial. Commercial add-ons require the presence of a - matching license via a license dongle or a license server. - ''' - return self.__call_method__('has_license') - - def get_tags(self) -> str: - ''' - @brief Return the list of tags with which the add-on has been tagged - @version 1 - - This function returns the list of tags in the addons `metainfo.json` file. - - @return List of tags - ''' - return self.__call_method__('get_tags') - - def get_file_list(self) -> list: - ''' - @brief Return the list of files contained in the add-on - @version 1 - - This function returns the list of files and directories in an add-on. These path names can - be used to read or write/modify add-on content. - - Please note that the list of files can only be obtained for add-ons which are currently not - in edit mode ! An add-on in edit mode is unzipped and the `get_file ()` function will return - the file system path to its directory in that case. That directory can then be browsed with - the standard file tools instead. - - #### Example - - ``` - for addon in gom.api.addons.get_installed_addons(): - # Edited add-ons are file system based and must be accessed via file system functions - if addon.is_edited(): - for root, dirs, files in os.walk(addon.get_file ()): - for file in files: - print(os.path.join(root, file)) - - # Finished add-ons can be accessed via this function - else: - for file in addon.get_file_list(): - print (file) - ``` - - @return List of files in that add-on (full path) - ''' - return self.__call_method__('get_file_list') - - def get_content_list(self) -> list: - ''' - @brief Return the list of contents contained in the add-on - @version 1 - - @return List of contents in that add-on (full path) - ''' - return self.__call_method__('get_content_list') - - def get_script_list(self) -> list: - ''' - @brief Return the list of scripts contained in the add-on - @version 1 - - @return List of scripts in that add-on (full path) - ''' - return self.__call_method__('get_script_list') - - def get_version(self) -> str: - ''' - @brief Return the version of the add-on - @version 1 - - @return Add-on version in string format - ''' - return self.__call_method__('get_version') - - def get_required_software_version(self) -> str: - ''' - @brief Return the minimum version of the ZEISS INSPECT software required to use this add-on - @version 1 - - By default, an add-on is compatible with the ZEISS INSPECT software version it was created in and - all following software version. This is the case because it can be assumed that this add-on is - tested with that specific software version, not with any prior version, leading to a minimum requirement. - On the other hand, the software version where an add-on then later will break because of incompatibilities - cannot be foreseen at add-on creation time. Thus, it is also assumed that a maintainer cares for an - add-on and updates it to the latest software version if necessary. There cannot be a "works until" entry - in the add-on itself, because this would require to modify already released version as soon as this specific - version which breaks the add-on becomes known. - - @return Addon version in string format - ''' - return self.__call_method__('get_required_software_version') - - def exists(self, path:str) -> bool: - ''' - @brief Check if the given file or directory exists in an add-on - @version 1 - - This function checks if the given file exists in the add-on - - @param path File path as retrieved by 'gom.api.addons.AddOn.get_file_list ()' - @return 'true' if a file or directory with that name exists in the add-on - ''' - return self.__call_method__('exists', path) - - def read(self, path:str) -> bytes: - ''' - @brief Read file from add-on - @version 1 - - This function reads the content of a file from the add-on. If the add-on is protected, - the file can still be read but will be AES encrypted. - - **Example:** Print all add-on 'metainfo.json' files - - ``` - import gom - import json - - for a in gom.api.addons.get_installed_addons (): - text = json.loads (a.read ('metainfo.json')) - print (json.dumps (text, indent=4)) - ``` - - @param path File path as retrieved by 'gom.api.addons.AddOn.get_file_list ()' - @return Content of that file as a byte array - ''' - return self.__call_method__('read', path) - - def write(self, path:str, data:bytes) -> None: - ''' - @brief Write data into add-on file - @version 1 - - This function writes data into a file into an add-ons file system. It can be used to update, - migrate or adapt the one add-on the API call originates from. Protected add-ons cannot be - modified at all. - - ```{important} - An add-on can modify only its own content ! Access to other add-ons is not permitted. Use this - function with care, as the result is permanent ! - ``` - - @param path File path as retrieved by 'gom.api.addons.AddOn.get_file_list ()' - @param data Data to be written into that file - ''' - return self.__call_method__('write', path, data) - - -def get_installed_addons() -> list[AddOn]: - ''' - @brief Return a list of the installed add-ons - @version 1 - - This function can be used to query information of the add-ons which are currently - installed in the running instance. - - **Example:** - - ``` - for a in gom.api.addons.get_installed_addons (): - print (a.get_id (), a.get_name ()) - ``` - - @return List of 'AddOn' objects. Each 'AddOn' object represents an add-on and can be used to query information about that specific add-on. - ''' - return gom.__api__.__call_function__() - -def get_current_addon() -> AddOn: - ''' - @brief Return the current add-on - @version 1 - - This function returns the add-on the caller is a member of - - **Example:** - - ``` - addon = gom.api.addons.get_current_addon () - print (addon.get_id ()) - > d04a082c-093e-4bb3-8714-8c36c7252fa0 - ``` - - @return Add-on the caller is a member of or `None` if there is no such add-on - ''' - return gom.__api__.__call_function__() - -def get_addon(id:UUID) -> AddOn: - ''' - @brief Return the add-on with the given id - @version 1 - - This function returns the add-on with the given id - - **Example:** - - ``` - addon = gom.api.addons.get_addon ('1127a8be-231f-44bf-b15e-56da4b510bf1') - print (addon.get_name ()) - > 'AddOn #1' - ``` - - @param id Id of the add-on to get - @return Add-on with the given id - @throws Exception if there is no add-on with that id - ''' - return gom.__api__.__call_function__(id) - +# +# API declarations for gom.api.addons +# +# @brief API for accessing the add-ons currently installed in the running software instance +# +# This API enables access to the installed add-ons. Information about these add-ons can be +# queried, add-on files and resources can be read and if the calling instance is a member of +# one specific add-on, this specific add-on can be modified on-the-fly and during software +# update processes. +# + +import gom +import gom.__api__ + +from typing import Any +from uuid import UUID + +class AddOn (gom.__api__.Object): + ''' + @brief Class representing a single add-on + + This class represents a single add-on. Properties of that add-on can be queried from here. + ''' + + def __init__ (self, instance_id): + super ().__init__ (instance_id) + + def get_id(self) -> UUID: + ''' + @brief Return the unique id (uuid) of this add-on + @version 1 + + This function returns the uuid associated with this add-on. The id can be used to + uniquely address the add-on. + + @return Add-on uuid + ''' + return self.__call_method__('get_id') + + def get_name(self) -> str: + ''' + @brief Return the displayable name of the add-on + @version 1 + + This function returns the displayable name of the add-on. This is the human + readable name which is displayed in the add-on manager and the add-on store. + + @return Add-on name + ''' + return self.__call_method__('get_name') + + def get_file(self) -> str: + ''' + @brief Return the installed add-on file + @version 1 + + This function returns the installed ZIP file representing the add-on. The file might be + empty if the add-on has never been 'completed'. If the add-on is currently in edit mode, + instead the edit directory containing the unpacked add-on sources is returned. In any way, + this function returns the location the application uses, too, to access add-on content. + + @return Add-on file path (path to the add-ons installed ZIP file) or add-on edit directory if the add-on is currently in edit mode. + ''' + return self.__call_method__('get_file') + + def get_level(self) -> str: + ''' + @brief Return the level (system/shared/user) of the add-on + @version 1 + + This function returns the 'configuration level' of the add-on. This can be + * 'system' for pre installed add-on which are distributed together with the application + * 'shared' for add-ons in the public or shared folder configured in the application's preferences or + * 'user' for user level add-ons installed for the current user only. + + @return Level of the add-on + ''' + return self.__call_method__('get_level') + + def is_edited(self) -> bool: + ''' + @brief Return if the add-on is currently edited + @version 1 + + Usually, an add-on is simply a ZIP file which is included into the applications file system. When + an add-on is in edit mode, it will be temporarily unzipped and is then present on disk in a directory. + + @return 'true' if the add-on is currently in edit mode + ''' + return self.__call_method__('is_edited') + + def is_protected(self) -> bool: + ''' + @brief Return if the add-on is protected + @version 1 + + The content of a protected add-on is encrypted. It can be listed, but not read. Protection + includes both 'IP protection' (content cannot be read) and 'copy protection' (content cannot be + copied, as far as possible) + + @return Add-on protection state + ''' + return self.__call_method__('is_protected') + + def has_license(self) -> bool: + ''' + @brief Return if the necessary licenses to use this add-on are present + @version 1 + + This function returns if the necessary licenses to use the add-on are currently present. + Add-ons can either be free and commercial. Commercial add-ons require the presence of a + matching license via a license dongle or a license server. + ''' + return self.__call_method__('has_license') + + def get_tags(self) -> str: + ''' + @brief Return the list of tags with which the add-on has been tagged + @version 1 + + This function returns the list of tags in the addons `metainfo.json` file. + + @return List of tags + ''' + return self.__call_method__('get_tags') + + def get_file_list(self) -> list: + ''' + @brief Return the list of files contained in the add-on + @version 1 + + This function returns the list of files and directories in an add-on. These path names can + be used to read or write/modify add-on content. + + Please note that the list of files can only be obtained for add-ons which are currently not + in edit mode ! An add-on in edit mode is unzipped and the `get_file ()` function will return + the file system path to its directory in that case. That directory can then be browsed with + the standard file tools instead. + + #### Example + + ``` + for addon in gom.api.addons.get_installed_addons(): + # Edited add-ons are file system based and must be accessed via file system functions + if addon.is_edited(): + for root, dirs, files in os.walk(addon.get_file ()): + for file in files: + print(os.path.join(root, file)) + + # Finished add-ons can be accessed via this function + else: + for file in addon.get_file_list(): + print (file) + ``` + + @return List of files in that add-on (full path) + ''' + return self.__call_method__('get_file_list') + + def get_content_list(self) -> list: + ''' + @brief Return the list of contents contained in the add-on + @version 1 + + @return List of contents in that add-on (full path) + ''' + return self.__call_method__('get_content_list') + + def get_script_list(self) -> list: + ''' + @brief Return the list of scripts contained in the add-on + @version 1 + + @return List of scripts in that add-on (full path) + ''' + return self.__call_method__('get_script_list') + + def get_version(self) -> str: + ''' + @brief Return the version of the add-on + @version 1 + + @return Add-on version in string format + ''' + return self.__call_method__('get_version') + + def get_required_software_version(self) -> str: + ''' + @brief Return the minimum version of the ZEISS INSPECT software required to use this add-on + @version 1 + + By default, an add-on is compatible with the ZEISS INSPECT software version it was created in and + all following software version. This is the case because it can be assumed that this add-on is + tested with that specific software version, not with any prior version, leading to a minimum requirement. + On the other hand, the software version where an add-on then later will break because of incompatibilities + cannot be foreseen at add-on creation time. Thus, it is also assumed that a maintainer cares for an + add-on and updates it to the latest software version if necessary. There cannot be a "works until" entry + in the add-on itself, because this would require to modify already released version as soon as this specific + version which breaks the add-on becomes known. + + @return Addon version in string format + ''' + return self.__call_method__('get_required_software_version') + + def exists(self, path:str) -> bool: + ''' + @brief Check if the given file or directory exists in an add-on + @version 1 + + This function checks if the given file exists in the add-on + + @param path File path as retrieved by 'gom.api.addons.AddOn.get_file_list ()' + @return 'true' if a file or directory with that name exists in the add-on + ''' + return self.__call_method__('exists', path) + + def read(self, path:str) -> bytes: + ''' + @brief Read file from add-on + @version 1 + + This function reads the content of a file from the add-on. If the add-on is protected, + the file can still be read but will be AES encrypted. + + **Example:** Print all add-on 'metainfo.json' files + + ``` + import gom + import json + + for a in gom.api.addons.get_installed_addons (): + text = json.loads (a.read ('metainfo.json')) + print (json.dumps (text, indent=4)) + ``` + + @param path File path as retrieved by 'gom.api.addons.AddOn.get_file_list ()' + @return Content of that file as a byte array + ''' + return self.__call_method__('read', path) + + def write(self, path:str, data:bytes) -> None: + ''' + @brief Write data into add-on file + @version 1 + + This function writes data into a file into an add-ons file system. It can be used to update, + migrate or adapt the one add-on the API call originates from. Protected add-ons cannot be + modified at all. + + ```{important} + An add-on can modify only its own content ! Access to other add-ons is not permitted. Use this + function with care, as the result is permanent ! + ``` + + @param path File path as retrieved by 'gom.api.addons.AddOn.get_file_list ()' + @param data Data to be written into that file + ''' + return self.__call_method__('write', path, data) + + +def get_installed_addons() -> list[AddOn]: + ''' + @brief Return a list of the installed add-ons + @version 1 + + This function can be used to query information of the add-ons which are currently + installed in the running instance. + + **Example:** + + ``` + for a in gom.api.addons.get_installed_addons (): + print (a.get_id (), a.get_name ()) + ``` + + @return List of 'AddOn' objects. Each 'AddOn' object represents an add-on and can be used to query information about that specific add-on. + ''' + return gom.__api__.__call_function__() + +def get_current_addon() -> AddOn: + ''' + @brief Return the current add-on + @version 1 + + This function returns the add-on the caller is a member of + + **Example:** + + ``` + addon = gom.api.addons.get_current_addon () + print (addon.get_id ()) + > d04a082c-093e-4bb3-8714-8c36c7252fa0 + ``` + + @return Add-on the caller is a member of or `None` if there is no such add-on + ''' + return gom.__api__.__call_function__() + +def get_addon(id:UUID) -> AddOn: + ''' + @brief Return the add-on with the given id + @version 1 + + This function returns the add-on with the given id + + **Example:** + + ``` + addon = gom.api.addons.get_addon ('1127a8be-231f-44bf-b15e-56da4b510bf1') + print (addon.get_name ()) + > 'AddOn #1' + ``` + + @param id Id of the add-on to get + @return Add-on with the given id + @throws Exception if there is no add-on with that id + ''' + return gom.__api__.__call_function__(id) + diff --git a/src/gom/api/dialog/__init__.py b/src/gom/api/dialog/__init__.py index 24a4929..18ffee4 100644 --- a/src/gom/api/dialog/__init__.py +++ b/src/gom/api/dialog/__init__.py @@ -1,64 +1,64 @@ -# -# API declarations for gom.api.dialog -# -# @brief API for handling dialogs -# -# This API is used to create and execute script based dialogs. The dialogs are defined in a -# JSON based description format and can be executed server side in the native UI style. -# - -import gom -import gom.__api__ - -from typing import Any -from uuid import UUID - -def create(context:Any, url:str) -> Any: - ''' - @brief Create modal dialog, but do not execute it yet - - This function creates a dialog. The dialog is passed in an abstract JSON description defining its layout. - The dialog is created but not executed yet. The dialog can be executed later by calling the 'gom.api.dialog.show' - function. The purpose of this function is to create a dialog in advance and allow the user setting it up before - - This function is part of the scripted contribution framework. It can be used in the scripts - 'dialog' functions to pop up user input dialogs, e.g. for creation commands. Passing of the - contributions script context is mandatory for the function to work. - - @param context Script execution context - @param url URL of the dialog definition (*.gdlg file) - @return Dialog handle which can be used to set up the dialog before executing it - ''' - return gom.__api__.__call_function__(context, url) - -def show(context:Any, dialog:Any) -> Any: - ''' - @brief Show previously created and configured dialog - - This function shows and executes previously created an configured dialog. The combination of - 'create' and 'show' in effect is the same as calling 'execute' directly. - - @param context Script execution context - @param dialog Handle of the previously created dialog - @return Dialog input field value map. The dictionary contains one entry per dialog widget with that widgets current value. - ''' - return gom.__api__.__call_function__(context, dialog) - -def execute(context:Any, url:str) -> Any: - ''' - @brief Create and execute a modal dialog - - This function creates and executes a dialog. The dialog is passed in an abstract JSON - description and will be executed modal. The script will pause until the dialog is either - confirmed or cancelled. - - This function is part of the scripted contribution framework. It can be used in the scripts - 'dialog' functions to pop up user input dialogs, e.g. for creation commands. Passing of the - contributions script context is mandatory for the function to work. - - @param context Script execution context - @param url URL of the dialog definition (*.gdlg file) - @return Dialog input field value map. The dictionary contains one entry per dialog widget with that widgets current value. - ''' - return gom.__api__.__call_function__(context, url) - +# +# API declarations for gom.api.dialog +# +# @brief API for handling dialogs +# +# This API is used to create and execute script based dialogs. The dialogs are defined in a +# JSON based description format and can be executed server side in the native UI style. +# + +import gom +import gom.__api__ + +from typing import Any +from uuid import UUID + +def create(context:Any, url:str) -> Any: + ''' + @brief Create modal dialog, but do not execute it yet + + This function creates a dialog. The dialog is passed in an abstract JSON description defining its layout. + The dialog is created but not executed yet. The dialog can be executed later by calling the 'gom.api.dialog.show' + function. The purpose of this function is to create a dialog in advance and allow the user setting it up before + + This function is part of the scripted contribution framework. It can be used in the scripts + 'dialog' functions to pop up user input dialogs, e.g. for creation commands. Passing of the + contributions script context is mandatory for the function to work. + + @param context Script execution context + @param url URL of the dialog definition (*.gdlg file) + @return Dialog handle which can be used to set up the dialog before executing it + ''' + return gom.__api__.__call_function__(context, url) + +def show(context:Any, dialog:Any) -> Any: + ''' + @brief Show previously created and configured dialog + + This function shows and executes previously created an configured dialog. The combination of + 'create' and 'show' in effect is the same as calling 'execute' directly. + + @param context Script execution context + @param dialog Handle of the previously created dialog + @return Dialog input field value map. The dictionary contains one entry per dialog widget with that widgets current value. + ''' + return gom.__api__.__call_function__(context, dialog) + +def execute(context:Any, url:str) -> Any: + ''' + @brief Create and execute a modal dialog + + This function creates and executes a dialog. The dialog is passed in an abstract JSON + description and will be executed modal. The script will pause until the dialog is either + confirmed or cancelled. + + This function is part of the scripted contribution framework. It can be used in the scripts + 'dialog' functions to pop up user input dialogs, e.g. for creation commands. Passing of the + contributions script context is mandatory for the function to work. + + @param context Script execution context + @param url URL of the dialog definition (*.gdlg file) + @return Dialog input field value map. The dictionary contains one entry per dialog widget with that widgets current value. + ''' + return gom.__api__.__call_function__(context, url) + diff --git a/src/gom/api/extensions/__init__.py b/src/gom/api/extensions/__init__.py index 78b6f5a..e0d0896 100644 --- a/src/gom/api/extensions/__init__.py +++ b/src/gom/api/extensions/__init__.py @@ -28,7 +28,7 @@ # ''' @brief API for script based functionality extensions - + This API enables the user to define various element classes which can be used to extend the functionality of ZEISS INSPECT. ''' @@ -40,19 +40,23 @@ from abc import ABC, abstractmethod from enum import Enum -from typing import final, Dict, Any +from typing import final, List, Dict, Any + +import traceback class ScriptedElement (ABC, gom.__common__.Contribution): ''' - This class is used to define a scripted element. A scripted element is a user defined - element type where configuration and computation are happening entirely in a Python script, - so user defined behavior and visualization can be implemented. + Base class for all scripted elements + + This class is the base class for all scripted element types . A scripted element is a user defined + element type where configuration and computation are happening entirely in a Python script, so user + defined behavior and visualization can be implemented. **Element id** - Every element must have a unique id. It is left to the implementor to avoid inter app conflicts here. The - id can be hierarchical like `company.topic.group.element_type`. The id may only contain lower case characters, + Every element must have a unique id. It is left to the implementer to avoid inter app conflicts here. The + id can be hierarchical like `company.topic.group.element_type`. The id may only contain lower case characters, grouping dots and underscores. **Element category** @@ -60,41 +64,6 @@ class ScriptedElement (ABC, gom.__common__.Contribution): The category of an element type is used to find the application side counterpart which cares for the functionality implementation. For example, `scriptedelement.actual` links that element type the application counterpart which cares for scripted actual elements and handles its creation, editing, administration, ... - - **Working with stages** - - Each scripted element must be computed for one or more stages. In the case of a preview or - for simple project setups, computation is usually done for a single stage only. In case of - a recalc, computation for many stages is usually required. To support both cases and keep it - simple for beginners, the scripted elements are using two computation functions: - - - `compute ()`: Computes the result for one single stage only. If nothing else is implemented, - this function will be called for each stage one by one and return the computed - value for that stage only. The stage for which the computation is performed is - passed via the function's script context, but does usually not matter as all input - values are already associated with that single stage. - - `compute_stages ()`: Computes the results for many (all) stages at once. The value parameters are - always vectors of the same size, one entry per stage. This is the case even if - there is just one stage in the project. The result is expected to be a result - vector of the same size as these stage vectors. The script context passed to that - function will contain a list of stages of equal size matching the value's stage - ordering. - - So for a project with stages, it is usually sufficient to just implement `compute ()`. For increased - performance or parallelization, `compute_stages ()` can then be implemented as a second step. - - **Stage indexing** - - Stages are represented by an integer index. No item reference or other resolvable types like - `gom.script.project[...].stages['Stage #1']` are used because it is assumed that reaching over stage borders into - other stages' data domain will lead to incorrect or missing dependencies. Instead, if vectorized data or data tensors - are fetched, the stage sorting within that object will match that stages vector in the context. In the best case, the - stage vector is just a consecutive range of numbers `(0, 1, 2, 3, ...)` which match the index in a staged tensor. - Nevertheless, the vector can be number entirely different depending on active/inactive stages, stage sorting, ... - - ```{caution} - Usually, it is *not* possible to access arbitrary stages of other elements due to recalc restrictions ! - ``` ''' class Event(str, Enum): @@ -108,34 +77,62 @@ class Event(str, Enum): DIALOG_CHANGED = "dialog::changed" DIALOG_CLOSED = "dialog::closed" - def __init__(self, id: str, category: str, description: str, element_type: str, callables={}, properties={}): + class Attribute(str, Enum): + ''' + Attributes used in the dialog definition + + The attributes are used to define the dialog widgets and their behavior. A selected set of these + attributes are listed here as a central reference and for unified constant value access. + ''' + + NAME = "name" + TOLERANCE = "tolerance" + VALUES = "values" + VISIBLE = "visible" + + class WidgetType(str, Enum): + ''' + (Selected) widget types used in the dialog definition + + The widget types are used to define the dialog widgets and their behavior. A selected set of these + widget types are listed here as a central reference and for unified constant value access. + ''' + + ELEMENTNAME = "input::elementname" + LABEL = "label" + LIST = "input::list" + SEPARATOR = "separator" + SPACER_HORIZONTAL = "spacer::horizontal" + SPACER_VERTICAL = "spacer::vertical" + TOLERANCE = "tolerances" + + def __init__(self, id: str, category: str, description: str, callables={}, properties={}): ''' Constructor @param id Unique contribution id, like `special_point` @param category Scripted element type id, like `scriptedelement.actual` @param description Human readable contribution description - @param element_type Type of the generated element (point, line, ...) @param category Contribution category ''' assert id, "Id must be set" assert category, "Category must be set" assert description, "Description must be set" - assert element_type, "Element type must be set" super().__init__(id=id, category=category, description=description, callables={ + 'add_log_message': self.add_log_message, + 'apply_dialog': self.apply_dialog, + 'compute_all': self.compute_all, 'dialog': self.dialog, 'event': self.event_handler, - 'compute': self.compute_stage, - 'compute_stages': self.compute_stages, + 'finish': self.finish, 'is_visible': self.is_visible } | callables, properties={ - 'element_type': element_type, 'icon': bytes() } | properties) @@ -150,7 +147,7 @@ def dialog(self, context, args): ``` { "version": 1, - "name": "Element name", + "name": "Element 1", "values: { "widget1": value1, "widget2": value2 @@ -161,10 +158,13 @@ def dialog(self, context, args): - `version`: Version of the dialog structure. This is used to allow for future changes in the dialog structure without breaking existing scripts - - `name`: Human readable name of the element which is created or edited + - `name`: Human readable name of the element which is created or edited. This entry is inserted automatically + from the dialog widget if two conditions are met: The name of the dialog widget is 'name' and its + type is 'Element name'. So in principle, the dialog must be setup to contain an 'Element name' widget + named 'name' in an appropriate layout location and the rest then happens automatically. - `values`: A map of widget names and their initial values. The widget names are the keys and the values are the initial or edited values for the widgets. This map is always present, but can be empty - for newly created elements. The element names are matching those in the user defined dialog, so + for newly created elements. The keys are matching the widget names in the user defined dialog, so the values can be set accordingly. As a default, use the function `initialize_dialog (args)` to setup all widgets from the args values. @@ -175,10 +175,24 @@ def dialog(self, context, args): def dialog (self, context, args): dlg = gom.api.dialog.create ('/dialogs/create_element.gdlg') self.initialize_dialog (dlg, args) - args = self.apply_dialog (args, gom.api.dialog.show (dlg)) + args = self.apply_dialog (dlg, gom.api.dialog.show (dlg)) return args ``` + The `dlg` object is a handle to the dialog which can be used to access the dialog widgets and their values. + For example, if a selection element with user defined filter function called `element` is member of the dialog, + the filter can be applied like this: + + ``` + def dialog (self, context, args): + dlg = gom.api.dialog.create ('/dialogs/create_element.gdlg') + dlg.element.filter = self.element_filter + ... + + def element_filter (self, element): + return element.type == 'curve' + ``` + For default dialogs, this can be shortened to a call to `show_dialog ()` which will handle the dialog creation, initialization and return the dialog values in the correct format in a single call: @@ -199,43 +213,167 @@ def show_dialog(self, context, args, url): and execution. It will create the dialog, initialize it with the given arguments and show it. The resulting values are then returned in the expected return format. + This function is a shortcut for the following code: + + ``` + dlg = gom.api.dialog.create(context, url) # Create dialog and return a handle to it + self.initialize_dialog(context, dlg, args) # Initialize the dialog with the given arguments + result = gom.api.dialog.show(context, dlg) # Show dialog and enter the dialog loop + return self.apply_dialog(dlg, result) # Apply the final dialog values + ``` + @param context Script context object containing execution related parameters. @param args Dialog execution arguments. This is a JSON like map structure, see above. @param url Dialog URL of the dialog to show ''' dlg = gom.api.dialog.create(context, url) self.initialize_dialog(context, dlg, args) - return self.apply_dialog(args, gom.api.dialog.show(context, dlg)) + return self.apply_dialog(dlg, gom.api.dialog.show(context, dlg)) - def apply_dialog(self, args, values): + def apply_dialog(self, dlg, result): ''' Apply dialog values to the dialog arguments. This function is used to read the dialog values back into the dialog arguments. See function `dialog ()` for a format description of the arguments. - ''' - if 'values' not in args: - args['values'] = {} - for widget in values: - args['values'][widget] = values[widget] + In its default implementation, the function performs the following tasks: + + - The dialog result contains values for all dialog widgets, including spacers, labels and other displaying only + widgets. These values are not directly relevant for the dialog arguments and are removed. + - The name of the created element must be treated in a dedicated way. So the dialog results are scanned for + an entry named `name` which originates from an element name widget. If this argument is present, it is assumed + that it configured the dialog name, is removed from the general dialog result and passed as a special `name` + result instead. + - The tolerance values are also treated in a dedicated way. If a dialog tolerance widget with the name `tolerance` + is present, its value is extracted and included in the final result. + + So the dialog `result` parameters can look like this: - return args + ``` + {'list': 'one', 'list_label': None, 'threshold': 1.0, 'threshold_label': None, 'name' :'Element 1', 'name_label': None} + ``` + + This will be modified into a format which can be recorded as a script element creation command parameter set: + + ``` + {'name': 'Element 1', 'values': {'list': 'one', 'threshold': 1.0}} + ``` + + This function can be overloaded if necessary and if the parameters must be adapted before being applied: + + ``` + def apply_dialog (self, dlg, result): + params = super ().apply_dialog (dlg, result) + # ... Adapt parameters... + return params + ``` + + For example, if a check should support tolerances, the dialogs tolerance widget value must be present in a parameter called + 'tolerance'. So the `apply_dialog()` function can be tailored like this for that purpose: + + ``` + def apply_dialog (self, dlg, result): + params = super ().apply_dialog (dlg, result) + + params['name'] = dlg.name.value # Read result directly from dialog object + params['tolerance'] = result['tolerance'] # Apply result from dialog result dictionary + + return params + ``` + + This will result in a dictionary with the parameters which are then used in the elements creation command. When recorded, this could look + like this: + + ``` + gom.script.scriptedelements.create_actual (name='Element 1', values={'mode': 23, 'threshold': 1.0}, tolerance={'lower': -0.5, 'upper': +0.5}) + ``` + + The `values` part will be directly forwarded to the elements custom `compute ()` function, which the `name` and `tolerance` parameters + are evaluated by the ZEISS INSPECT framework to apply the necessary settings automatically. + + @param dlg Dialog handle as created via the `gom.api.dialog.create ()` function + @param result Dialog result values as returned from the `gom.api.dialog.show ()` function. + @return Resulting dialog parameters + ''' + + # + # Extract element name explicitly + # + name = None + if hasattr(dlg, ScriptedElement.Attribute.NAME): + name_w = getattr(dlg, ScriptedElement.Attribute.NAME) + if name_w.widget_type == ScriptedElement.WidgetType.ELEMENTNAME: + name = name_w.value.strip() if name_w.value else '### wrong element name widget type ###' + + # + # Extract tolerance values explicitly + # + tolerance = None + + if hasattr(dlg, ScriptedElement.Attribute.TOLERANCE): + tolerance_w = getattr(dlg, ScriptedElement.Attribute.TOLERANCE) + if tolerance_w.widget_type == ScriptedElement.WidgetType.TOLERANCE: + tolerance = tolerance_w.value + + # + # Convert the dialog result into a dictionary of values. + # + ignore_widget_types = [ + ScriptedElement.WidgetType.LABEL, # Labels are not values + ScriptedElement.WidgetType.SEPARATOR, # Separators are not values + ScriptedElement.WidgetType.SPACER_HORIZONTAL, # Spacers are not values + ScriptedElement.WidgetType.SPACER_VERTICAL # Spacers are not values + ] + + values = {} + + for widget in result: + if hasattr(dlg, widget): + w = getattr(dlg, widget) + if w.widget_type == ScriptedElement.WidgetType.LIST: + # + # Workaround: The list widget is returning the current text which can be translated and is then + # different depending on which language is currently set - this is a design flaw. Instead, the + # selected index is required. Because the fundamental logic in the script dialog cannot be touched + # due to compatibility reasons, the conversation happens here. + # + values[widget] = w.index + elif w.name == ScriptedElement.Attribute.NAME and name is not None: + pass + elif w.name == ScriptedElement.Attribute.TOLERANCE and tolerance is not None: + pass + elif w.widget_type in ignore_widget_types: + pass + else: + values[widget] = result[widget] + + result = {ScriptedElement.Attribute.VALUES: values} + + if name is None: + result[ScriptedElement.Attribute.NAME] = '### element name not specified ###' + else: + result[ScriptedElement.Attribute.NAME] = name + + if tolerance is not None: + result[ScriptedElement.Attribute.TOLERANCE] = tolerance + + return result @final def event_handler(self, context, event_type, parameters): ''' Wrapper function for calls to `event ()`. This function is called from the application side - and will convert the event parameter accordingly + and will convert the event parameters accordingly ''' return self.event(context, ScriptedElement.Event(event_type), parameters) def event(self, context, event_type, parameters): ''' Contribution event handling function. This function is called when the contributions UI state changes. - The function can then react to that event and update the UI state accordingly. + The function can then react to that event and update the UI state accordingly. @param context Script context object containing execution related parameters. This includes the stage this computation call refers to. @param event_type Event type - @param args Event arguments + @param parameters Event arguments @return `True` if the event requires a recomputation of the elements preview. Upon return, the framework will then trigger a call to the `compute ()` function and use its result for a preview update. @@ -244,60 +382,26 @@ def event(self, context, event_type, parameters): return event_type == ScriptedElement.Event.DIALOG_INITIALIZED or event_type == ScriptedElement.Event.DIALOG_CHANGED - @abstractmethod - def compute(self, context, values): - ''' - This function is called for a single stage value is to be computed. The input values from the - associated dialog function are passed as `kwargs` parameters - one value as one specific - parameter named as the associated input widget. - - @param context Script context object containing execution related parameters. This includes - the stage this computation call refers to. - @param values Dialog widget values as a dictionary. The keys are the widget names as defined - in the dialog definition. - ''' - pass - - @abstractmethod - def compute_stage(self, context, values): + def finish(self, context, results_states_map): ''' - This function is called for a single stage value is to be computed. The input values from the - associated dialog function are passed as `kwargs` parameters - one value as one specific - parameter named as the associated input widget. + This function is called to compile diagram data. It can then later be collected by + scripted diagrams and displayed. - @param context Script context object containing execution related parameters. This includes - the stage this computation call refers to. - @param values Dialog widget values as a dictionary. The keys are the widget names as defined - in the dialog definition. - ''' - return self.compute(context, values) + The default option is to simply pass the results and states, + so this function must be overwritten to utilize other diagrams. - def compute_stages(self, context, values): + Example: + diagram_data = [] + self.add_diagram_data(diagram_data=diagram_data, diagram_id="SVGDiagram", + service_id="gom.api.endpoint.example.py", element_data=results_states_map["results"][0]) + results_states_map["diagram_data"] = diagram_data + @return results_states_map ''' - This function is called to compute multiple stages of the scripted element. The expected result is - a vector of the same length as the number of stages. - - The function is calling the `compute ()` function of the scripted element for each stage by default. - For a more efficient implementation, it can be overwritten and bulk compute many stages at once. - - @param context Script context object containing execution related parameters. This includes - the stage this computation call refers to. - @param values Dialog widget values as a dictionary. - ''' - - results = [] - states = [] - - for stage in context.stages: - context.stage = stage - try: - results.append(self.compute_stage(context, values)) - states.append(True) - except BaseException as e: - results.append(str(e)) - states.append(False) + return results_states_map - return {'results': results, 'states': states} + def compute_all(self, context, values): + results_states = self.compute_stages(context, values) + return self.finish(context, results_states) def is_visible(self, context): ''' @@ -323,8 +427,8 @@ def initialize_dialog(self, context, dlg, args) -> bool: ''' ok = True - if 'values' in args: - for widget, value in args['values'].items(): + if ScriptedElement.Attribute.VALUES in args and args[ScriptedElement.Attribute.VALUES] is not None: + for widget, value in args[ScriptedElement.Attribute.VALUES].items(): try: if hasattr(dlg, widget): getattr(dlg, widget).value = value @@ -333,35 +437,79 @@ def initialize_dialog(self, context, dlg, args) -> bool: gom.log.warning( f"Failed to set dialog widget '{widget}' to value '{value}' due to exception: {str(e)}") - return ok + if ScriptedElement.Attribute.NAME in args and args[ScriptedElement.Attribute.NAME]: + try: + if hasattr(dlg, ScriptedElement.Attribute.NAME): + name_w = getattr(dlg, ScriptedElement.Attribute.NAME) + if name_w.widget_type == ScriptedElement.WidgetType.ELEMENTNAME: + name_w.value = args[ScriptedElement.Attribute.NAME] + else: + gom.log.warning( + f"Element name parameter given, but dialog widget '{ScriptedElement.Attribute.NAME}' is not of type '{ScriptedElement.WidgetType.ELEMENTNAME}'.") + else: + gom.log.warning( + f"Element name parameter given, but dialog does not contain an appropriate widget for element name input.") + except Exception as e: + ok = False + gom.log.warning( + f"Failed to set element dialog widget '{ScriptedElement.Attribute.NAME}' to value '{args[ScriptedElement.Attribute.NAME]}' due to exception: {str(e)}") - def add_target_element_parameter(self, values: Dict[str, Any], element): - ''' - Adds an element as the target element to the parameters map in the - appropriate fields + if ScriptedElement.Attribute.TOLERANCE in args and args[ScriptedElement.Attribute.TOLERANCE]: + try: + if hasattr(dlg, ScriptedElement.Attribute.TOLERANCE): + tolerance_w = getattr(dlg, ScriptedElement.Attribute.TOLERANCE) + if tolerance_w.widget_type == ScriptedElement.WidgetType.TOLERANCE: + tolerance_w.value = args[ScriptedElement.Attribute.TOLERANCE] + else: + gom.log.warning( + f"Element tolerance parameter given, but dialog widget '{ScriptedElement.Attribute.TOLERANCE}' is not of type '{ScriptedElement.WidgetType.TOLERANCE}'.") + else: + gom.log.warning( + f"Element tolerance parameter given, but dialog does not contain an appropriate widget for element tolerance input.") + except Exception as e: + ok = False + gom.log.warning( + f"Failed to set element dialog widget '{ScriptedElement.Attribute.TOLERANCE}' to value '{args[ScriptedElement.Attribute.TOLERANCE]}' due to exception: {str(e)}") - @param values Values map - @param element Element to be added - ''' - values['target_element'] = element + return ok - def add_selected_element_parameter(self, values: Dict[str, Any]): - ''' - Adds the current selected element as the target element to the values map + def add_diagram_data(self, diagram_data: List, diagram_id: str, service_id: str, element_data: Dict[str, Any]): + diagram_data.append({ + "element_id": self.id, + "diagram_id": diagram_id, + "service_id": service_id, + "element_data": element_data + }) - @param values Values map + def add_log_message(self, context, level, message): ''' - elements = gom.api.selection.get_selected_elements() - if len(elements) > 0: - self.add_target_element_parameter(values, elements[0]) + Add a log message to the service log. The message will be logged with the given level and appear + in the service log file. It is used to forward errors from the C++ side to the Python side. + + @param context Script context object containing execution related parameters. + @param level Log level + @param message Log message to be added + ''' + if level.lower() == 'error': + gom.log.error(message) + elif level.lower() == 'warn': + gom.log.warning(message) + elif level.lower() == 'info': + gom.log.info(message) + elif level.lower() == 'fatal': + gom.log.critical(message) + elif level.lower() == 'debug': + gom.log.debug(message) + else: + gom.log.info(message) def check_value(self, values: Dict[str, Any], key: str, value_type: type): ''' Check a single value for expected properties - @param values: Dictionary of values - @param key: Key of the value to check - @param value_type: Type the value is expected to have + @param values Dictionary of values + @param key Key of the value to check + @param value_type Type the value is expected to have ''' if type(values) != dict: raise TypeError(f"Expected a dictionary of values, but got {values}") @@ -377,10 +525,10 @@ def check_list(self, values: Dict[str, Any], key: str, value_type: type, length: ''' Check tuple result for expected properties - @param values: Dictionary of values - @param key: Key of the value to check - @param value_type: Type each of the values is expected to have - @param length: Number of values expected in the tuple or 'None' if any length is allowed + @param values Dictionary of values + @param key Key of the value to check + @param value_type Type each of the values is expected to have + @param length Number of values expected in the tuple or 'None' if any length is allowed ''' if not key in values: raise TypeError(f"Missing '{key}' value") @@ -411,3 +559,131 @@ def check_target_element(self, values: Dict[str, Any]): Check if a base element (an element the scripted element is constructed upon) is present in the values map ''' self.check_value(values, 'target_element', gom.Item) + + +class ScriptedCalculationElement (ScriptedElement): + ''' + This class is used to define a scripted calculation element which calculated its own data. It is used as a + base class for scripted actual, nominals and checks. + + **Working with stages** + + Each scripted element must be computed for one or more stages. In the case of a preview or + for simple project setups, computation is usually done for a single stage only. In case of + a recalc, computation for many stages is usually required. To support both cases and keep it + simple for beginners, the scripted elements are using two computation functions: + + - `compute ()`: Computes the result for one single stage only. If nothing else is implemented, + this function will be called for each stage one by one and return the computed + value for that stage only. The stage for which the computation is performed is + passed via the function's script context, but does usually not matter as all input + values are already associated with that single stage. + - `compute_stages ()`: Computes the results for many (all) stages at once. The value parameters are + always vectors of the same size, one entry per stage. This is the case even if + there is just one stage in the project. The result is expected to be a result + vector of the same size as these stage vectors. The script context passed to that + function will contain a list of stages of equal size matching the value's stage + ordering. + + So for a project with stages, it is usually sufficient to just implement `compute ()`. For increased + performance or parallelization, `compute_stages ()` can then be implemented as a second step. + + **Stage indexing** + + Stages are represented by an integer index. No item reference or other resolvable types like + `gom.script.project[...].stages['Stage #1']` are used because it is assumed that reaching over stage borders into + other stages' data domain will lead to incorrect or missing dependencies. Instead, if vectorized data or data tensors + are fetched, the stage sorting within that object will match that stages vector in the context. In the best case, the + stage vector is just a consecutive range of numbers `(0, 1, 2, 3, ...)` which match the index in a staged tensor. + Nevertheless, the vector can be number entirely different depending on active/inactive stages, stage sorting, ... + + ```{caution} + Usually, it is *not* possible to access arbitrary stages of other elements due to recalc restrictions ! + ``` + ''' + + def __init__(self, id: str, category: str, description: str, element_type: str, callables={}, properties={}): + ''' + Constructor + + @param id Unique contribution id, like `special_point` + @param category Scripted element type id, like `scriptedelement.actual` + @param description Human readable contribution description + @param element_type Type of the generated element (point, line, ...) + @param category Contribution category + ''' + + assert element_type, "Element type must be set" + + super().__init__(id=id, + category=category, + description=description, + callables={ + 'compute': self.compute_stage, + 'compute_stages': self.compute_stages, + } | callables, + properties={ + 'element_type': element_type + } | properties) + + @abstractmethod + def compute(self, context, values): + ''' + This function is called for a single stage value is to be computed. The input values from the + associated dialog function are passed as `kwargs` parameters - one value as one specific + parameter named as the associated input widget. + + @param context Script context object containing execution related parameters. This includes + the stage this computation call refers to. + @param values Dialog widget values as a dictionary. The keys are the widget names as defined + in the dialog definition. + ''' + pass + + @abstractmethod + def compute_stage(self, context, values): + ''' + This function is called for a single stage value is to be computed. The input values from the + associated dialog function are passed as `kwargs` parameters - one value as one specific + parameter named as the associated input widget. + + @param context Script context object containing execution related parameters. This includes + the stage this computation call refers to. + @param values Dialog widget values as a dictionary. The keys are the widget names as defined + in the dialog definition. + ''' + return self.compute(context, values) + + def compute_stages(self, context, values): + ''' + This function is called to compute multiple stages of the scripted element. The expected result is + a vector of the same length as the number of stages. + + The function is calling the `compute ()` function of the scripted element for each stage by default. + For a more efficient implementation, it can be overwritten and bulk compute many stages at once. + + @param context Script context object containing execution related parameters. This includes + the stage this computation call refers to. + @param values Dialog widget values as a dictionary. + ''' + + results = [] + states = [] + + # + # Iterate over the stage indices. Each stage is computed separately per default. + # If set, the `context.stage` property determines from which stage the tokens etc. + # are queried. + # + for stage in context.stages: + context.stage = stage + try: + results.append(self.compute_stage(context, values)) + states.append(True) + except BaseException as e: + results.append((str(e), traceback.format_exc())) + states.append(False) + finally: + context.stage = None + + return {'results': results, 'states': states} diff --git a/src/gom/api/extensions/actuals/__init__.py b/src/gom/api/extensions/actuals/__init__.py index c5f22f6..4019ee9 100644 --- a/src/gom/api/extensions/actuals/__init__.py +++ b/src/gom/api/extensions/actuals/__init__.py @@ -35,10 +35,10 @@ import gom -from gom.api.extensions import ScriptedElement +from gom.api.extensions import ScriptedCalculationElement -class ScriptedActual (ScriptedElement): +class ScriptedActual (ScriptedCalculationElement): ''' This class is the base class for all scripted actuals ''' @@ -67,7 +67,8 @@ class Point (ScriptedActual): ``` { - "value": (x: float, y: float, z: float) // The point in 3D space. + "value": (x: float, y: float, z: float), // The point in 3D space. + "data": {...} // Optional element data, stored with the element } ``` ''' @@ -92,7 +93,8 @@ class Distance (ScriptedActual): ``` { "point1": (x: float, y: float, z: float), // First point of the distance - "point2": (x: float, y: float, z: float) // Second point of the distance + "point2": (x: float, y: float, z: float), // Second point of the distance + "data": {...} // Optional element data, stored with the element } ``` ''' @@ -119,7 +121,8 @@ class Circle (ScriptedActual): { "center" : (x: float, y: float, z: float), // Centerpoint of the circle "direction": (x: float, y: float, z: float), // Direction/normal of the circle - "radius" : r: float // Radius of the circle + "radius" : r: float, // Radius of the circle + "data": {...} // Optional element data, stored with the element } ``` ''' @@ -148,7 +151,8 @@ class Cone (ScriptedActual): "point1": (x: float, y: float, z: float), // First point of the cone (circle center) "radius1": r1: float, // Radius of the first circle "point2": (x: float, y: float, z: float), // Second point of the cone (circle center) - "radius2": r2: float // Radius of the second circle + "radius2": r2: float, // Radius of the second circle + "data": {...} // Optional element data, stored with the element } ``` ''' @@ -175,9 +179,10 @@ class Cylinder (ScriptedActual): ``` { - "point": (x: float, y: float, z: float), // Base point of the cylinder + "center": (x: float, y: float, z: float), // Center point of the cylinder "direction": (x: float, y: float, z: float), // Direction of the cylinder "radius": r: float, // Radius of the cylinder + "data": {...} // Optional element data, stored with the element } ``` ''' @@ -188,7 +193,7 @@ def __init__(self, id: str, description: str, help_id: str = None): def compute_stage(self, context, values): result = self.compute(context, values) - self.check_list(result, "point", float, 3) + self.check_list(result, "center", float, 3) self.check_list(result, "direction", float, 3) self.check_value(result, "radius", float) @@ -203,8 +208,9 @@ class Plane (ScriptedActual): ``` { - "normal": (x: float, y: float, z: float), // Normal of the plane - "distance": d: float // Distance of the plane + "normal": (x: float, y: float, z: float), // Normal direction of the plane + "point": (x: float, y: float, z: float), // One point of the plane + "data": {...} // Optional element data, stored with the element } ``` @@ -212,8 +218,10 @@ class Plane (ScriptedActual): ``` { - "target": plane: Plane, // Source plane point of this plane - "offset": offset: float // Offset relative to the source plane + "point1": (x: float, y: float, z: float), // Point 1 of the plane + "point2": (x: float, y: float, z: float), // Point 2 of the plane + "point3": (x: float, y: float, z: float), // Point 3 of the plane + "data": {...} // Optional element data, stored with the element } ``` @@ -221,7 +229,9 @@ class Plane (ScriptedActual): ``` { - "plane": Reference // Reference to another plane element of coordinate system + "plane": Reference, // Reference to another plane element + "data": {...} // Optional element data, stored with the element + } ``` ''' @@ -241,7 +251,8 @@ class ValueElement (ScriptedActual): ``` { - "value": v: float // Value of the element + "value": v: float, // Value of the element + "data": {...} // Optional element data, stored with the element } ``` ''' @@ -265,8 +276,9 @@ class Curve (ScriptedActual): ``` { - "plane": p: Plane // Plane of the curve (optional) - "curves": [Curve] // List of curves + "plane": p: Plane, // Plane of the curve (optional) + "curves": [Curve], // List of curves + "data": {...} // Optional element data, stored with the element } ``` @@ -274,7 +286,7 @@ class Curve (ScriptedActual): ``` { - "points": [(x: float, y: float, z: float), ...] // List of points + "points": [(x: float, y: float, z: float), ...] } ``` @@ -296,7 +308,8 @@ class SurfaceCurve (ScriptedActual): ``` { - "curves": [Curve] + "curves": [Curve], // Curve definition + "data": {...} // Optional element data, stored with the element } ``` @@ -328,7 +341,8 @@ class Section (ScriptedActual): "curves": [Curve], "plane": Plane, "cone": Cone, - "cylinder": Cylinder + "cylinder": Cylinder, + "data": {...} // Optional element data, stored with the element } ``` @@ -360,7 +374,8 @@ class PointCloud (ScriptedActual): ``` { "points": [(x: float, y: float, z: float), ...], // List of points - "normals": [(x: float, y: float, z: float), ...] // List of normals for each point + "normals": [(x: float, y: float, z: float), ...], // List of normals for each point + "data": {...} // Optional element data, stored with the element } ``` ''' @@ -389,7 +404,8 @@ class Surface (ScriptedActual): ``` { "vertices": [(x: float, y: float, z: float), ...], // List of vertices - "triangles": [(i1: int, i2: int, i3: int), ...] // List of triangles (vertices' indices) + "triangles": [(i1: int, i2: int, i3: int), ...], // List of triangles (vertices' indices) + "data": {...} // Optional element data, stored with the element } ``` ''' @@ -422,7 +438,8 @@ class Volume (ScriptedActual): ``` { 'voxel_data': data: np.array (shape=(x, y, z), dtype=np.float32), // Voxels of the volume - 'transformation': (x: float, y: float, z: float) // Transformation of the volume + 'transformation': (x: float, y: float, z: float), // Transformation of the volume + "data": {...} // Optional element data, stored with the element } ``` ''' diff --git a/src/gom/api/extensions/diagrams/__init__.py b/src/gom/api/extensions/diagrams/__init__.py new file mode 100644 index 0000000..41f298b --- /dev/null +++ b/src/gom/api/extensions/diagrams/__init__.py @@ -0,0 +1,507 @@ +# +# extensions/views/__init__.py - Scripted views definitions +# +# (C) 2025 Carl Zeiss GOM Metrology GmbH +# +# Use of this source code and binary forms of it, without modification, is permitted provided that +# the following conditions are met: +# +# 1. Redistribution of this source code or binary forms of this with or without any modifications is +# not allowed without specific prior written permission by GOM. +# +# As this source code is provided as glue logic for connecting the Python interpreter to the commands of +# the GOM software any modification to this sources will not make sense and would affect a suitable functioning +# and therefore shall be avoided, so consequently the redistribution of this source with or without any +# modification in source or binary form is not permitted as it would lead to malfunctions of GOM Software. +# +# 2. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or +# promote products derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED +# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +''' +@brief Scripted diagrams + +The classes in this module enable the user to define scripted diagrams. A scripted diagram implements an +interface to transform element data into data that can be rendered by a corresponding Javascript renderer +implementation in the diagram view. +''' + +import gom +import gom.__common__ + +from abc import ABC, abstractmethod +from enum import Enum +from typing import Dict, List, Tuple, Any, final + + +class ScriptedDiagram (ABC, gom.__common__.Contribution): + ''' + This class is used to defined a polisher for a scripted diagram that processes a collection of raw + element (diagram) data into a format used by a Javascript based renderer. + + Implement the `plot ()` function to receive and polish diagram data that is marked with the corresponding contribution id. + + Optionally implement the `partitions ()` function to partition the full set of diagram data into subsets + that are rendered separately using the `plot ()` function. + ''' + class Token(str, Enum): + ''' + Token identifiers for the ScriptedDiagram data format + ''' + # Plot result + PLOT = "plot" + SUBPLOT = "subplot" + INDICES = "indices" + + # Event result + CMD_SCRIPT = "cmd_script" + DATA_SCRIPT = "data_script" + + def __init__(self, id: str, description: str, diagram_type: str = "", properties: Dict[str, Any] = {}, + callables: Dict[str, Any] = {}): + ''' + Constructor + + @param id Unique contribution id, like `my.diagram.circles` + @param description Human readable contribution description + @param diagram_type Javascript renderer to use (leave empty to use renderer set by element) + @param properties Additional properties for this contribution (optional) + @param callables Addtitional callables for this contribution (optional) + ''' + + if not id: + raise ValueError('id must be set') + if not description: + raise ValueError('description must be set') + + super().__init__(id=id, + category='scripteddiagram', + description=description, + callables={ + 'plot_all': self.plot_all, + 'event': self.event, + 'error': self.error + } | callables, + properties={ + 'diagram_type': diagram_type, + # Determine whether the partitions() method has been overwritten + 'use_partitions': (type(self).partitions != ScriptedDiagram.partitions) + } | properties) + + @final + def check_and_filter_partitions(self, partitions_in: List[List[int]], cnt_elements: int) -> List[List[int]]: + ''' + Check a given set of partitions to ensure only valid partitions are included. + + @param partitions_in List of partitions to check + @param cnt_elements Count of elements in full data set + + For internal use only. + ''' + partitions = [] + if isinstance(partitions_in, List): + for partition in partitions_in: + if isinstance(partition, List): + if all(isinstance(x, int) and x >= 0 and x < cnt_elements for x in partition): + partitions.append(partition) + else: + gom.log.warning( + f"Expected only valid integer values [0,{cnt_elements}) in partition, instead got: {partition}") + else: + gom.log.warning(f"Expected a list of indices to define a partition, instead got: {partition}") + else: + gom.log.warning(f"Expected a list of partitions, instead got: {partitions_in}") + + return partitions + + @final + def plot_all(self, view: Dict[str, Any], element_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + ''' + Internal coordination function for the overall plot process. + + This functions calls the (potentially) user defined method 'partitions()' to determine + the partitions of this diagram and then calls 'plot()' for each to data partition to generate a diagram. + + @param view: Dictionary with view canvas data ('width' (int), 'height' (int), 'dpi' (float), 'font' (int)) used for the diagrams of all partitions + @param element_data List of dictionaries containing scripted element references and context data ('element' (object), 'data' (dict), 'type' (str)) + + @return List of dictionaries: each dictionary contains the keys 'plot' (the plot information for this partition) + and 'indices' (the indices of the elements used for this partition) + ''' + # Fetch partition information and ensure the format is as expected + partitions = self.check_and_filter_partitions(self.partitions(element_data), len(element_data)) + + # If something went terribly wrong, still plot one diagram with all the data + if len(partitions) == 0: + view.update({ScriptedDiagram.Token.SUBPLOT: 0}) + return [{ScriptedDiagram.Token.PLOT: self.plot(view, element_data), ScriptedDiagram.Token.INDICES: list(range(len(element_data)))}] + + plots = [] + for num, partition in enumerate(partitions): + data = [] + indices = set() + # Collect subset of data + for idx in partition: + # Avoid doubled indices + if idx in indices: + continue + indices.add(idx) + data.append(element_data[idx]) + + # Now plot this subset of data + view.update({ScriptedDiagram.Token.SUBPLOT: num}) + plots.append({ScriptedDiagram.Token.PLOT: self.sanitize_plot_data( + self.plot(view, data)), ScriptedDiagram.Token.INDICES: list(indices)}) + + return plots + + def sanitize_plot_data(self, plot_data: Any) -> Dict[str, Any]: + ''' + This function is used to sanitize the output of the user defined 'plot' function + ''' + return plot_data + + @abstractmethod + def plot(self, view: Dict[str, Any], element_data: List[Dict[str, Any]]) -> Dict[str, Any]: + ''' + This function is called to create a plot based on a set of element data. + + @param view Dictionary with view canvas data and subplot index ('width' (int), 'height' (int), 'dpi' (float), 'font' (int), 'subplot' (int)) + @param element_data List of dictionaries containing scripted element references and context data ('element' (object), 'data' (dict), 'type' (str)) + + @return Data that is passed to the corresponding Javascript diagram type for rendering + ''' + pass + + def event(self, element_name: str, element_uuid: str, event_data: Any) -> Dict[str, Any]: + ''' + This function is called upon interaction with the diagram (except hover) + The user can return a script to be executed when this function is called + + @param element_name String containing the element identification (name) + @param element_uuid String containing the element uuid for internal identification + @param event_data Contains current mouse coordinates and button presses + + @return Dictionary with finish_event(