Skip to content

Commit 5b0db31

Browse files
authored
Merge pull request #19 from ZEISS/apiwheel-update-main
Python API wheel update pull request (main)
2 parents 54b4e0f + 8d5ecac commit 5b0db31

File tree

9 files changed

+842
-61
lines changed

9 files changed

+842
-61
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22

33
This package provides an API to write and execute scripts in a running ZEISS INSPECT instance.
44
Please read the [ZEISS INSPECT API documentation](https://zeiss.github.io/IQS/) for details.
5+
6+
The [ZEISS INSPECT API wheel](https://pypi.org/project/zeiss-inspect-api/) is hosted on PyPI.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "zeiss_inspect_api"
7-
version = "2026.3.0.342"
7+
version = "2027.0.0.2328"
88
authors = [
99
{ name="Carl Zeiss GOM Metrology GmbH", email="info.optical.metrology@zeiss.com" },
1010
]

src/gom/__encoding__.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,16 @@ def supports_numpy():
8080

8181
@staticmethod
8282
def supports_shared_memory():
83+
'''
84+
Check if shared memory is supported on the current platform.
85+
'''
86+
87+
#
88+
# Shared memory is supported on Windows only currently, because the underlying implementation differs.
89+
# On Windows, the kernel keeps a reference count of the open handles to a segment while on POSIX the
90+
# memory is managed by the process itself. So ownership passing is harder on POSIX and would require
91+
# additional synchronization mechanisms.
92+
#
8393
return sys.platform.startswith('win')
8494

8595
@staticmethod
@@ -98,14 +108,23 @@ def read_from_shared_memory(key, shape, dtype, context):
98108

99109
byte_size = size * np.dtype(dtype).itemsize
100110
with mmap.mmap(-1, byte_size, tagname=key, access=mmap.ACCESS_READ) as m:
101-
result = np.frombuffer(bytes(m[:]), dtype=dtype, count=size).reshape(shape)
111+
result = np.frombuffer(bytes(m[:]), dtype=dtype, count=size).reshape(shape).copy()
102112

103113
context.add(key)
104114

105115
return result
106116

107117
@staticmethod
108118
def create_shared_memory_segment(data):
119+
'''
120+
@brief Create shared memory segment
121+
122+
This function creates a shared memory segment which can be used to pass large amounts of data
123+
between processes.
124+
125+
@param data Data to be stored in the shared memory segment
126+
@return Tuple of (segment, key) for the created shared memory segment
127+
'''
109128

110129
segment = None
111130
key = None
@@ -117,6 +136,8 @@ def create_shared_memory_segment(data):
117136
Encoder.shared_memory_id_counter += 1
118137

119138
segment = mmap.mmap(-1, len(b), tagname=key, access=mmap.ACCESS_WRITE)
139+
if segment.size() < len(b):
140+
raise RuntimeError(f'Cannot create shared memory segment of {len (b)} bytes')
120141

121142
segment.write(b)
122143
segment.flush()
@@ -309,11 +330,11 @@ def encodeValue(self, buffer, obj, context):
309330
raise RuntimeError(
310331
'\'{obj}\' has unsupported data type \'{type}\'and cannot be encoded'.format(obj=obj, type=obj.dtype))
311332

312-
if Encoder.supports_shared_memory():
333+
if Encoder.supports_shared_memory() and obj.nbytes > 10 * 1024:
313334
self.encodeBool(buffer, True)
314335
segment, key = Encoder.create_shared_memory_segment(obj)
315336

316-
context.add(segment)
337+
context.add(key, segment)
317338
self.encodeStr(buffer, key)
318339

319340
else:
@@ -669,7 +690,7 @@ def encode_traits(obj, context):
669690

670691
if Encoder.supports_shared_memory():
671692
segment, key = Encoder.create_shared_memory_segment(obj)
672-
context.add(segment)
693+
context.add(key, segment)
673694
return {JsonEncoder.TYPE_DEFINITION_KEY: JsonEncoder.TYPE_PACKAGE,
674695
'shape': list(obj.shape),
675696
'type': type,

src/gom/__init__.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,27 @@ def tr(text, id=None):
7676
translated = text
7777

7878
try:
79+
origin = ""
80+
try:
81+
#
82+
# Extract the caller information to find the true origin of the translation call
83+
# If it is an script from within an app, origin will be the qualified name of that script
84+
# Otherwise it will usually be some filepath (external script)
85+
#
86+
87+
#
88+
# sys._getframe is more performant than using the function overhead from the inspect module
89+
# however, it is considered an implementation detail of CPython and not guaranteed to exist, so we keep a fallback
90+
#
91+
if hasattr(sys, '_getframe'):
92+
origin = sys._getframe(1).f_code.co_filename
93+
else:
94+
origin = inspect.currentframe().f_back.f_code.co_filename
95+
except:
96+
pass
97+
7998
translated = gom.__common__.__connection__.request(Request.TRANSLATE, {
80-
'text': text, 'id': id if id else ''})['translation']
99+
'text': text, 'id': id if id else '', 'origin': origin})['translation']
81100
except:
82101
pass
83102

src/gom/__logging__.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
#
2+
# __logging__.py - Network related classes for connecting with the C++ application part
3+
#
4+
# (C) 2025 Carl Zeiss GOM Metrology GmbH
5+
#
6+
# Use of this source code and binary forms of it, without modification, is permitted provided that
7+
# the following conditions are met:
8+
#
9+
# 1. Redistribution of this source code or binary forms of this with or without any modifications is
10+
# not allowed without specific prior written permission by GOM.
11+
#
12+
# As this source code is provided as glue logic for connecting the Python interpreter to the commands of
13+
# the GOM software any modification to this sources will not make sense and would affect a suitable functioning
14+
# and therefore shall be avoided, so consequently the redistribution of this source with or without any
15+
# modification in source or binary form is not permitted as it would lead to malfunctions of GOM Software.
16+
#
17+
# 2. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or
18+
# promote products derived from this software without specific prior written permission.
19+
#
20+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
21+
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
22+
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
23+
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
24+
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
25+
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26+
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27+
# POSSIBILITY OF SUCH DAMAGE.
28+
#
29+
30+
import enum
31+
import inspect
32+
import logging
33+
import os
34+
import uuid
35+
36+
from datetime import datetime, timezone
37+
38+
39+
def get_default_log_dir():
40+
"""
41+
Determines and creates the default directory for log files based on the operating system and user privileges.
42+
43+
On Windows, it uses the %ProgramData%\gom\log directory (defaulting to C:\ProgramData\gom\log if the environment variable is missing).
44+
On POSIX systems, it uses /var/log/gom if running as root, or ~/.local/share/gom/log for non-root users.
45+
46+
Returns:
47+
str: The absolute path to the default log directory.
48+
"""
49+
if os.name == "nt":
50+
# On Windows, get %ProgramData% (typically C:\ProgramData)
51+
program_data = os.environ.get("ProgramData")
52+
if not program_data:
53+
# Fallback if environment variable is missing
54+
program_data = r"C:\ProgramData"
55+
log_dir = os.path.join(program_data, "gom", "log")
56+
else:
57+
# On POSIX, use /var/log/gom or ~/.local/share/gom/log if not root
58+
if os.geteuid() == 0:
59+
log_dir = "/var/log/gom"
60+
else:
61+
log_dir = os.path.expanduser("~/.local/share/gom/log")
62+
63+
os.makedirs(log_dir, exist_ok=True)
64+
65+
return log_dir
66+
67+
68+
class MillisecondFormatter(logging.Formatter):
69+
"""
70+
A custom logging.Formatter that formats log record timestamps with millisecond precision.
71+
72+
Overrides the formatTime method to allow formatting of timestamps with milliseconds.
73+
If a date format string (datefmt) is provided and contains '%f', it will be replaced
74+
with the milliseconds component (first three digits of microseconds) of the timestamp.
75+
76+
Args:
77+
record (logging.LogRecord): The log record whose creation time is to be formatted.
78+
datefmt (str, optional): A date format string. If provided and contains '%f', it will
79+
be replaced with milliseconds.
80+
81+
Returns:
82+
str: The formatted time string with millisecond precision.
83+
"""
84+
85+
def formatTime(self, record, datefmt=None):
86+
dt = datetime.fromtimestamp(record.created, tz=timezone.utc)
87+
if datefmt:
88+
s = dt.strftime(datefmt)
89+
# Replace %f with milliseconds (first 3 digits of microseconds)
90+
s = s.replace('%f', f"{dt.microsecond // 1000:03d}")
91+
return s
92+
return super().formatTime(record, datefmt)
93+
94+
95+
class ProtocolLogger:
96+
"""
97+
ProtocolLogger is a logging utility designed to record protocol-related events in a structured log file, matching the format and domain conventions of a corresponding C++ logging system.
98+
99+
Attributes:
100+
domain (str): The logging domain, set to 'scripting.core.protocol' to match the C++ domain.
101+
102+
Classes:
103+
EventType (enum.Enum): Enumeration of protocol event types, including CONNECTED, DISCONNECTED, REQUEST, RESPONSE, and ERROR.
104+
105+
Methods:
106+
__init__(log_dir=None):
107+
Initializes the ProtocolLogger instance.
108+
Determines the log directory (uses a default if not provided), constructs a log file name with a UTC timestamp and process ID, and sets up a file handler with a custom formatter for millisecond precision and ISO 8601 timestamps.
109+
110+
log(event_type, event_id, message):
111+
Logs a protocol event with the specified type, identifier, and message.
112+
Automatically includes the caller's filename and line number, formats the timestamp to match C++ conventions, and records the thread ID.
113+
Supports event_id as a UUID or string.
114+
"""
115+
116+
domain = 'scripting.core.protocol' # Must match the C++ domain
117+
118+
class EventType(enum.Enum):
119+
"""
120+
Enumeration of possible event types for logging purposes.
121+
122+
Attributes:
123+
CONNECTED (str): Indicates a successful connection event.
124+
DISCONNECTED (str): Indicates a disconnection event.
125+
REQUEST (str): Represents a request event.
126+
RESPONSE (str): Represents a response event.
127+
ERROR (str): Represents an error event.
128+
"""
129+
CONNECTED = "Connected"
130+
DISCONNECTED = "Disconnected"
131+
REQUEST = "Request"
132+
RESPONSE = "Response"
133+
ERROR = "Error"
134+
135+
def __init__(self, log_dir=None):
136+
137+
# Determine log directory
138+
if log_dir is None:
139+
log_dir = get_default_log_dir()
140+
141+
# Compute log file name with start timestamp and process id
142+
start_time = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H-%M-%S")
143+
pid = os.getpid()
144+
log_filename = f"python_{start_time}_{pid}.log"
145+
log_path = os.path.join(log_dir, log_filename)
146+
147+
self.logger = logging.getLogger(ProtocolLogger.domain)
148+
handler = logging.FileHandler(log_path, encoding="utf-8")
149+
formatter = MillisecondFormatter(
150+
'%(asctime)s TID%(thread)d INFO [%(name)s] %(event_type)s %(event_id)s %(message)s (%(filename)s:%(lineno)d)',
151+
"%Y-%m-%dT%H:%M:%S.%fZ"
152+
)
153+
handler.setFormatter(formatter)
154+
self.logger.handlers = []
155+
self.logger.addHandler(handler)
156+
self.logger.setLevel(logging.INFO)
157+
self.log_path = log_path
158+
159+
def log(self, event_type, event_id, message):
160+
161+
if isinstance(event_id, uuid.UUID):
162+
event_id = str(event_id)
163+
164+
# Get caller's filename and line number from the call stack
165+
frame = inspect.currentframe()
166+
outer_frames = inspect.getouterframes(frame)
167+
if len(outer_frames) > 1:
168+
caller_frame = outer_frames[1]
169+
filename = os.path.basename(caller_frame.filename)
170+
lineno = caller_frame.lineno
171+
else:
172+
filename = "unknown"
173+
lineno = 0
174+
extra = {
175+
'event_type': event_type.value if isinstance(event_type, enum.Enum) else str(event_type),
176+
'event_id': event_id
177+
}
178+
179+
# Patch asctime to match C++ format (ISO 8601 with ms, Z)
180+
record = self.logger.makeRecord(
181+
self.logger.name, logging.INFO, filename, lineno,
182+
message, (), None, None, extra)
183+
record.asctime = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
184+
try:
185+
import threading
186+
record.thread = threading.get_ident()
187+
except ImportError:
188+
record.thread = 1
189+
record.event_type = event_type
190+
record.event_id = event_id
191+
record.filename = filename
192+
record.lineno = lineno
193+
record.name = self.logger.name
194+
self.logger.handle(record)
195+
196+
197+
# Example usage:
198+
if __name__ == "__main__":
199+
logger = ProtocolLogger()
200+
logger.log(ProtocolLogger.EventType.REQUEST.value, uuid.uuid4(), "Register")
201+
print(f"Log written to: {logger.log_path}")

0 commit comments

Comments
 (0)