sigima 0.0.1.dev0__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. sigima/__init__.py +142 -2
  2. sigima/client/__init__.py +105 -0
  3. sigima/client/base.py +780 -0
  4. sigima/client/remote.py +469 -0
  5. sigima/client/stub.py +814 -0
  6. sigima/client/utils.py +90 -0
  7. sigima/config.py +444 -0
  8. sigima/data/logo/Sigima.svg +135 -0
  9. sigima/data/tests/annotations.json +798 -0
  10. sigima/data/tests/curve_fitting/exponential_fit.txt +511 -0
  11. sigima/data/tests/curve_fitting/gaussian_fit.txt +100 -0
  12. sigima/data/tests/curve_fitting/piecewiseexponential_fit.txt +1022 -0
  13. sigima/data/tests/curve_fitting/polynomial_fit.txt +100 -0
  14. sigima/data/tests/curve_fitting/twohalfgaussian_fit.txt +1000 -0
  15. sigima/data/tests/curve_formats/bandwidth.txt +201 -0
  16. sigima/data/tests/curve_formats/boxcar.npy +0 -0
  17. sigima/data/tests/curve_formats/datetime.txt +1001 -0
  18. sigima/data/tests/curve_formats/dynamic_parameters.txt +4000 -0
  19. sigima/data/tests/curve_formats/fw1e2.txt +301 -0
  20. sigima/data/tests/curve_formats/fwhm.txt +319 -0
  21. sigima/data/tests/curve_formats/multiple_curves.csv +29 -0
  22. sigima/data/tests/curve_formats/noised_saw.mat +0 -0
  23. sigima/data/tests/curve_formats/oscilloscope.csv +111 -0
  24. sigima/data/tests/curve_formats/other/other2/recursive2.txt +5 -0
  25. sigima/data/tests/curve_formats/other/recursive1.txt +5 -0
  26. sigima/data/tests/curve_formats/paracetamol.npy +0 -0
  27. sigima/data/tests/curve_formats/paracetamol.txt +1010 -0
  28. sigima/data/tests/curve_formats/paracetamol_dx_dy.csv +1000 -0
  29. sigima/data/tests/curve_formats/paracetamol_dy.csv +1001 -0
  30. sigima/data/tests/curve_formats/pulse1.npy +0 -0
  31. sigima/data/tests/curve_formats/pulse2.npy +0 -0
  32. sigima/data/tests/curve_formats/simple.txt +5 -0
  33. sigima/data/tests/curve_formats/spectrum.mca +2139 -0
  34. sigima/data/tests/curve_formats/square2.npy +0 -0
  35. sigima/data/tests/curve_formats/step.npy +0 -0
  36. sigima/data/tests/fabry-perot1.jpg +0 -0
  37. sigima/data/tests/fabry-perot2.jpg +0 -0
  38. sigima/data/tests/flower.npy +0 -0
  39. sigima/data/tests/image_formats/NF 180338201.scor-data +11003 -0
  40. sigima/data/tests/image_formats/binary_image.npy +0 -0
  41. sigima/data/tests/image_formats/binary_image.png +0 -0
  42. sigima/data/tests/image_formats/centroid_test.npy +0 -0
  43. sigima/data/tests/image_formats/coordinated_text/complex_image.txt +10011 -0
  44. sigima/data/tests/image_formats/coordinated_text/complex_ref_image.txt +10010 -0
  45. sigima/data/tests/image_formats/coordinated_text/image.txt +15 -0
  46. sigima/data/tests/image_formats/coordinated_text/image2.txt +14 -0
  47. sigima/data/tests/image_formats/coordinated_text/image_no_unit_no_label.txt +14 -0
  48. sigima/data/tests/image_formats/coordinated_text/image_with_nan.txt +15 -0
  49. sigima/data/tests/image_formats/coordinated_text/image_with_unit.txt +14 -0
  50. sigima/data/tests/image_formats/fiber.csv +480 -0
  51. sigima/data/tests/image_formats/fiber.jpg +0 -0
  52. sigima/data/tests/image_formats/fiber.png +0 -0
  53. sigima/data/tests/image_formats/fiber.txt +480 -0
  54. sigima/data/tests/image_formats/gaussian_spot_with_noise.npy +0 -0
  55. sigima/data/tests/image_formats/mr-brain.dcm +0 -0
  56. sigima/data/tests/image_formats/noised_gaussian.mat +0 -0
  57. sigima/data/tests/image_formats/sif_reader/nd_lum_image_no_glue.sif +0 -0
  58. sigima/data/tests/image_formats/sif_reader/raman1.sif +0 -0
  59. sigima/data/tests/image_formats/tiling.txt +10 -0
  60. sigima/data/tests/image_formats/uint16.tiff +0 -0
  61. sigima/data/tests/image_formats/uint8.tiff +0 -0
  62. sigima/data/tests/laser_beam/TEM00_z_13.jpg +0 -0
  63. sigima/data/tests/laser_beam/TEM00_z_18.jpg +0 -0
  64. sigima/data/tests/laser_beam/TEM00_z_23.jpg +0 -0
  65. sigima/data/tests/laser_beam/TEM00_z_30.jpg +0 -0
  66. sigima/data/tests/laser_beam/TEM00_z_35.jpg +0 -0
  67. sigima/data/tests/laser_beam/TEM00_z_40.jpg +0 -0
  68. sigima/data/tests/laser_beam/TEM00_z_45.jpg +0 -0
  69. sigima/data/tests/laser_beam/TEM00_z_50.jpg +0 -0
  70. sigima/data/tests/laser_beam/TEM00_z_55.jpg +0 -0
  71. sigima/data/tests/laser_beam/TEM00_z_60.jpg +0 -0
  72. sigima/data/tests/laser_beam/TEM00_z_65.jpg +0 -0
  73. sigima/data/tests/laser_beam/TEM00_z_70.jpg +0 -0
  74. sigima/data/tests/laser_beam/TEM00_z_75.jpg +0 -0
  75. sigima/data/tests/laser_beam/TEM00_z_80.jpg +0 -0
  76. sigima/enums.py +195 -0
  77. sigima/io/__init__.py +123 -0
  78. sigima/io/base.py +311 -0
  79. sigima/io/common/__init__.py +5 -0
  80. sigima/io/common/basename.py +164 -0
  81. sigima/io/common/converters.py +189 -0
  82. sigima/io/common/objmeta.py +181 -0
  83. sigima/io/common/textreader.py +58 -0
  84. sigima/io/convenience.py +157 -0
  85. sigima/io/enums.py +17 -0
  86. sigima/io/ftlab.py +395 -0
  87. sigima/io/image/__init__.py +9 -0
  88. sigima/io/image/base.py +177 -0
  89. sigima/io/image/formats.py +1016 -0
  90. sigima/io/image/funcs.py +414 -0
  91. sigima/io/signal/__init__.py +9 -0
  92. sigima/io/signal/base.py +129 -0
  93. sigima/io/signal/formats.py +290 -0
  94. sigima/io/signal/funcs.py +723 -0
  95. sigima/objects/__init__.py +260 -0
  96. sigima/objects/base.py +937 -0
  97. sigima/objects/image/__init__.py +88 -0
  98. sigima/objects/image/creation.py +556 -0
  99. sigima/objects/image/object.py +524 -0
  100. sigima/objects/image/roi.py +904 -0
  101. sigima/objects/scalar/__init__.py +57 -0
  102. sigima/objects/scalar/common.py +215 -0
  103. sigima/objects/scalar/geometry.py +502 -0
  104. sigima/objects/scalar/table.py +784 -0
  105. sigima/objects/shape.py +290 -0
  106. sigima/objects/signal/__init__.py +133 -0
  107. sigima/objects/signal/constants.py +27 -0
  108. sigima/objects/signal/creation.py +1428 -0
  109. sigima/objects/signal/object.py +444 -0
  110. sigima/objects/signal/roi.py +274 -0
  111. sigima/params.py +405 -0
  112. sigima/proc/__init__.py +96 -0
  113. sigima/proc/base.py +381 -0
  114. sigima/proc/decorator.py +330 -0
  115. sigima/proc/image/__init__.py +513 -0
  116. sigima/proc/image/arithmetic.py +335 -0
  117. sigima/proc/image/base.py +260 -0
  118. sigima/proc/image/detection.py +519 -0
  119. sigima/proc/image/edges.py +329 -0
  120. sigima/proc/image/exposure.py +406 -0
  121. sigima/proc/image/extraction.py +458 -0
  122. sigima/proc/image/filtering.py +219 -0
  123. sigima/proc/image/fourier.py +147 -0
  124. sigima/proc/image/geometry.py +661 -0
  125. sigima/proc/image/mathops.py +340 -0
  126. sigima/proc/image/measurement.py +195 -0
  127. sigima/proc/image/morphology.py +155 -0
  128. sigima/proc/image/noise.py +107 -0
  129. sigima/proc/image/preprocessing.py +182 -0
  130. sigima/proc/image/restoration.py +235 -0
  131. sigima/proc/image/threshold.py +217 -0
  132. sigima/proc/image/transformations.py +393 -0
  133. sigima/proc/signal/__init__.py +376 -0
  134. sigima/proc/signal/analysis.py +206 -0
  135. sigima/proc/signal/arithmetic.py +551 -0
  136. sigima/proc/signal/base.py +262 -0
  137. sigima/proc/signal/extraction.py +60 -0
  138. sigima/proc/signal/features.py +310 -0
  139. sigima/proc/signal/filtering.py +484 -0
  140. sigima/proc/signal/fitting.py +276 -0
  141. sigima/proc/signal/fourier.py +259 -0
  142. sigima/proc/signal/mathops.py +420 -0
  143. sigima/proc/signal/processing.py +580 -0
  144. sigima/proc/signal/stability.py +175 -0
  145. sigima/proc/title_formatting.py +227 -0
  146. sigima/proc/validation.py +272 -0
  147. sigima/tests/__init__.py +7 -0
  148. sigima/tests/common/__init__.py +0 -0
  149. sigima/tests/common/arithmeticparam_unit_test.py +26 -0
  150. sigima/tests/common/basename_unit_test.py +126 -0
  151. sigima/tests/common/client_unit_test.py +412 -0
  152. sigima/tests/common/converters_unit_test.py +77 -0
  153. sigima/tests/common/decorator_unit_test.py +176 -0
  154. sigima/tests/common/examples_unit_test.py +104 -0
  155. sigima/tests/common/kernel_normalization_unit_test.py +242 -0
  156. sigima/tests/common/roi_basic_unit_test.py +73 -0
  157. sigima/tests/common/roi_geometry_unit_test.py +171 -0
  158. sigima/tests/common/scalar_builder_unit_test.py +142 -0
  159. sigima/tests/common/scalar_unit_test.py +991 -0
  160. sigima/tests/common/shape_unit_test.py +183 -0
  161. sigima/tests/common/stat_unit_test.py +138 -0
  162. sigima/tests/common/title_formatting_unit_test.py +338 -0
  163. sigima/tests/common/tools_coordinates_unit_test.py +60 -0
  164. sigima/tests/common/transformations_unit_test.py +178 -0
  165. sigima/tests/common/validation_unit_test.py +205 -0
  166. sigima/tests/conftest.py +129 -0
  167. sigima/tests/data.py +998 -0
  168. sigima/tests/env.py +280 -0
  169. sigima/tests/guiutils.py +163 -0
  170. sigima/tests/helpers.py +532 -0
  171. sigima/tests/image/__init__.py +28 -0
  172. sigima/tests/image/binning_unit_test.py +128 -0
  173. sigima/tests/image/blob_detection_unit_test.py +312 -0
  174. sigima/tests/image/centroid_unit_test.py +170 -0
  175. sigima/tests/image/check_2d_array_unit_test.py +63 -0
  176. sigima/tests/image/contour_unit_test.py +172 -0
  177. sigima/tests/image/convolution_unit_test.py +178 -0
  178. sigima/tests/image/datatype_unit_test.py +67 -0
  179. sigima/tests/image/edges_unit_test.py +155 -0
  180. sigima/tests/image/enclosingcircle_unit_test.py +88 -0
  181. sigima/tests/image/exposure_unit_test.py +223 -0
  182. sigima/tests/image/fft2d_unit_test.py +189 -0
  183. sigima/tests/image/filtering_unit_test.py +166 -0
  184. sigima/tests/image/geometry_unit_test.py +654 -0
  185. sigima/tests/image/hough_circle_unit_test.py +147 -0
  186. sigima/tests/image/imageobj_unit_test.py +737 -0
  187. sigima/tests/image/morphology_unit_test.py +71 -0
  188. sigima/tests/image/noise_unit_test.py +57 -0
  189. sigima/tests/image/offset_correction_unit_test.py +72 -0
  190. sigima/tests/image/operation_unit_test.py +518 -0
  191. sigima/tests/image/peak2d_limits_unit_test.py +41 -0
  192. sigima/tests/image/peak2d_unit_test.py +133 -0
  193. sigima/tests/image/profile_unit_test.py +159 -0
  194. sigima/tests/image/projections_unit_test.py +121 -0
  195. sigima/tests/image/restoration_unit_test.py +141 -0
  196. sigima/tests/image/roi2dparam_unit_test.py +53 -0
  197. sigima/tests/image/roi_advanced_unit_test.py +588 -0
  198. sigima/tests/image/roi_grid_unit_test.py +279 -0
  199. sigima/tests/image/spectrum2d_unit_test.py +40 -0
  200. sigima/tests/image/threshold_unit_test.py +91 -0
  201. sigima/tests/io/__init__.py +0 -0
  202. sigima/tests/io/addnewformat_unit_test.py +125 -0
  203. sigima/tests/io/convenience_funcs_unit_test.py +470 -0
  204. sigima/tests/io/coordinated_text_format_unit_test.py +495 -0
  205. sigima/tests/io/datetime_csv_unit_test.py +198 -0
  206. sigima/tests/io/imageio_formats_test.py +41 -0
  207. sigima/tests/io/ioregistry_unit_test.py +69 -0
  208. sigima/tests/io/objmeta_unit_test.py +87 -0
  209. sigima/tests/io/readobj_unit_test.py +130 -0
  210. sigima/tests/io/readwriteobj_unit_test.py +67 -0
  211. sigima/tests/signal/__init__.py +0 -0
  212. sigima/tests/signal/analysis_unit_test.py +135 -0
  213. sigima/tests/signal/check_1d_arrays_unit_test.py +169 -0
  214. sigima/tests/signal/convolution_unit_test.py +404 -0
  215. sigima/tests/signal/datetime_unit_test.py +176 -0
  216. sigima/tests/signal/fft1d_unit_test.py +303 -0
  217. sigima/tests/signal/filters_unit_test.py +403 -0
  218. sigima/tests/signal/fitting_unit_test.py +929 -0
  219. sigima/tests/signal/fwhm_unit_test.py +111 -0
  220. sigima/tests/signal/noise_unit_test.py +128 -0
  221. sigima/tests/signal/offset_correction_unit_test.py +34 -0
  222. sigima/tests/signal/operation_unit_test.py +489 -0
  223. sigima/tests/signal/peakdetection_unit_test.py +145 -0
  224. sigima/tests/signal/processing_unit_test.py +657 -0
  225. sigima/tests/signal/pulse/__init__.py +112 -0
  226. sigima/tests/signal/pulse/crossing_times_unit_test.py +123 -0
  227. sigima/tests/signal/pulse/plateau_detection_unit_test.py +102 -0
  228. sigima/tests/signal/pulse/pulse_unit_test.py +1824 -0
  229. sigima/tests/signal/roi_advanced_unit_test.py +392 -0
  230. sigima/tests/signal/signalobj_unit_test.py +603 -0
  231. sigima/tests/signal/stability_unit_test.py +431 -0
  232. sigima/tests/signal/uncertainty_unit_test.py +611 -0
  233. sigima/tests/vistools.py +1030 -0
  234. sigima/tools/__init__.py +59 -0
  235. sigima/tools/checks.py +290 -0
  236. sigima/tools/coordinates.py +308 -0
  237. sigima/tools/datatypes.py +26 -0
  238. sigima/tools/image/__init__.py +97 -0
  239. sigima/tools/image/detection.py +451 -0
  240. sigima/tools/image/exposure.py +77 -0
  241. sigima/tools/image/extraction.py +48 -0
  242. sigima/tools/image/fourier.py +260 -0
  243. sigima/tools/image/geometry.py +190 -0
  244. sigima/tools/image/preprocessing.py +165 -0
  245. sigima/tools/signal/__init__.py +86 -0
  246. sigima/tools/signal/dynamic.py +254 -0
  247. sigima/tools/signal/features.py +135 -0
  248. sigima/tools/signal/filtering.py +171 -0
  249. sigima/tools/signal/fitting.py +1171 -0
  250. sigima/tools/signal/fourier.py +466 -0
  251. sigima/tools/signal/interpolation.py +70 -0
  252. sigima/tools/signal/peakdetection.py +126 -0
  253. sigima/tools/signal/pulse.py +1626 -0
  254. sigima/tools/signal/scaling.py +50 -0
  255. sigima/tools/signal/stability.py +258 -0
  256. sigima/tools/signal/windowing.py +90 -0
  257. sigima/worker.py +79 -0
  258. sigima-1.0.0.dist-info/METADATA +233 -0
  259. sigima-1.0.0.dist-info/RECORD +262 -0
  260. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/licenses/LICENSE +29 -29
  261. sigima-0.0.1.dev0.dist-info/METADATA +0 -60
  262. sigima-0.0.1.dev0.dist-info/RECORD +0 -6
  263. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/WHEEL +0 -0
  264. {sigima-0.0.1.dev0.dist-info → sigima-1.0.0.dist-info}/top_level.txt +0 -0
sigima/client/stub.py ADDED
@@ -0,0 +1,814 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
2
+
3
+ """
4
+ Sigima Client Stub/Mock Server (Real Objects)
5
+ ---------------------------------------------
6
+
7
+ This module provides a stub XML-RPC server that emulates DataLab's XML-RPC interface
8
+ for testing purposes using real Sigima objects. The stub server allows tests to run
9
+ without requiring a real DataLab instance.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import threading
16
+ import uuid
17
+ from contextlib import contextmanager
18
+ from socketserver import ThreadingMixIn
19
+ from typing import TYPE_CHECKING
20
+ from xmlrpc.client import Binary
21
+ from xmlrpc.server import SimpleXMLRPCServer
22
+
23
+ from guidata.env import execenv
24
+
25
+ from sigima.client import utils
26
+ from sigima.objects import ImageObj, SignalObj, create_image, create_signal
27
+
28
+ if TYPE_CHECKING:
29
+ from collections.abc import Generator
30
+
31
+ # pylint: disable=invalid-name # Allows short reference names like x, y, ...
32
+ # pylint: disable=duplicate-code
33
+
34
+
35
+ class MockGroup:
36
+ """Mock group object."""
37
+
38
+ def __init__(self, title: str, uuid_str: str | None = None):
39
+ self.title = title
40
+ self.uuid = uuid_str or str(uuid.uuid4())
41
+ self.objects: list[str] = [] # List of object UUIDs
42
+
43
+
44
+ class ThreadingXMLRPCServer(ThreadingMixIn, SimpleXMLRPCServer):
45
+ """Threading XML-RPC server to handle multiple requests."""
46
+
47
+ daemon_threads = True
48
+ allow_reuse_address = True
49
+
50
+
51
+ class DataLabStubServer:
52
+ """Stub XML-RPC server emulating DataLab XML-RPC interface with real objects.
53
+
54
+ This server provides mock implementations of all DataLab XML-RPC methods
55
+ using real SignalObj and ImageObj instances for maximum compatibility.
56
+
57
+ Args:
58
+ port: Port to bind to. If 0, uses a random available port.
59
+ verbose: If True, print verbose debug information.
60
+ """
61
+
62
+ def __init__(self, port: int = 0, verbose: bool = True) -> None:
63
+ """Initialize the stub server.
64
+
65
+ Args:
66
+ port: Port to bind to. If 0, uses a random available port.
67
+ """
68
+ self.port = port
69
+ self.verbose = verbose
70
+ self.server: ThreadingXMLRPCServer | None = None
71
+ self.server_thread: threading.Thread | None = None
72
+
73
+ # Real Sigima object storage
74
+ self.signals: dict[str, SignalObj] = {} # uuid -> SignalObj
75
+ self.images: dict[str, ImageObj] = {} # uuid -> ImageObj
76
+ self.signal_groups: dict[str, MockGroup] = {} # uuid -> group
77
+ self.image_groups: dict[str, MockGroup] = {} # uuid -> group
78
+
79
+ # Current state
80
+ self.current_panel = "signal" # "signal", "image", or "macro"
81
+ self.selected_objects: list[str] = [] # list of UUIDs
82
+ self.selected_groups: list[str] = [] # list of group UUIDs
83
+ self.auto_refresh = True
84
+ self.show_titles = True
85
+
86
+ # Add default groups
87
+ self._add_default_group("signal")
88
+ self._add_default_group("image")
89
+
90
+ def _add_default_group(self, panel: str) -> None:
91
+ """Add default group for a panel."""
92
+ group = MockGroup("Group 1")
93
+ if panel == "signal":
94
+ self.signal_groups[group.uuid] = group
95
+ elif panel == "image":
96
+ self.image_groups[group.uuid] = group
97
+
98
+ def start(self) -> int:
99
+ """Start the XML-RPC server.
100
+
101
+ Returns:
102
+ Port number the server is listening on
103
+ """
104
+ self.server = ThreadingXMLRPCServer(
105
+ ("127.0.0.1", self.port), allow_none=True, logRequests=False
106
+ )
107
+
108
+ # Register all methods
109
+ self._register_functions()
110
+
111
+ self.port = self.server.server_address[1]
112
+
113
+ # Start server in a separate thread
114
+ self.server_thread = threading.Thread(
115
+ target=self.server.serve_forever, daemon=True
116
+ )
117
+ self.server_thread.start()
118
+
119
+ execenv.print(f"DataLab stub server started on port {self.port}")
120
+ return self.port
121
+
122
+ def stop(self) -> None:
123
+ """Stop the XML-RPC server."""
124
+ if self.server:
125
+ self.server.shutdown()
126
+ self.server.server_close()
127
+ if self.server_thread:
128
+ self.server_thread.join(timeout=1.0)
129
+ execenv.print("DataLab stub server stopped")
130
+
131
+ def _register_functions(self) -> None:
132
+ """Register all XML-RPC functions."""
133
+ # System introspection methods
134
+ self.server.register_introspection_functions()
135
+
136
+ # Basic server methods
137
+ self.server.register_function(self.get_version, "get_version")
138
+ self.server.register_function(self.close_application, "close_application")
139
+ self.server.register_function(self.raise_window, "raise_window")
140
+
141
+ # Panel management
142
+ self.server.register_function(self.get_current_panel, "get_current_panel")
143
+ self.server.register_function(self.set_current_panel, "set_current_panel")
144
+
145
+ # Application control
146
+ self.server.register_function(self.reset_all, "reset_all")
147
+ self.server.register_function(self.toggle_auto_refresh, "toggle_auto_refresh")
148
+ self.server.register_function(self.toggle_show_titles, "toggle_show_titles")
149
+
150
+ # File operations
151
+ self.server.register_function(self.save_to_h5_file, "save_to_h5_file")
152
+ self.server.register_function(self.open_h5_files, "open_h5_files")
153
+ self.server.register_function(self.import_h5_file, "import_h5_file")
154
+
155
+ # Object operations
156
+ self.server.register_function(self.add_signal, "add_signal")
157
+ self.server.register_function(self.add_image, "add_image")
158
+ self.server.register_function(self.get_object_titles, "get_object_titles")
159
+ self.server.register_function(self.get_object_uuids, "get_object_uuids")
160
+ self.server.register_function(self.get_object, "get_object")
161
+ self.server.register_function(self.get_object_shapes, "get_object_shapes")
162
+ self.server.register_function(self.delete_metadata, "delete_metadata")
163
+
164
+ # Selection operations
165
+ self.server.register_function(self.select_objects, "select_objects")
166
+ self.server.register_function(self.select_groups, "select_groups")
167
+ self.server.register_function(self.get_sel_object_uuids, "get_sel_object_uuids")
168
+ self.server.register_function(self.delete_object, "delete_object")
169
+ self.server.register_function(self.duplicate_object, "duplicate_object")
170
+ self.server.register_function(self.copy_metadata, "copy_metadata")
171
+
172
+ # Group operations
173
+ self.server.register_function(self.add_group, "add_group")
174
+ self.server.register_function(self.get_group_titles, "get_group_titles")
175
+ self.server.register_function(
176
+ self.get_group_titles_with_object_info, "get_group_titles_with_object_info"
177
+ )
178
+ self.server.register_function(self.move_up, "move_up")
179
+ self.server.register_function(self.move_down, "move_down")
180
+ self.server.register_function(self.delete_group, "delete_group")
181
+
182
+ # Calculation operations
183
+ self.server.register_function(self.calc, "calc")
184
+
185
+ # Annotation operations
186
+ self.server.register_function(
187
+ self.add_annotations_from_items, "add_annotations_from_items"
188
+ )
189
+ self.server.register_function(self.add_label_with_title, "add_label_with_title")
190
+
191
+ # Basic server methods
192
+ def get_version(self) -> str:
193
+ """Get DataLab version.
194
+
195
+ Returns a valid PEP 440 version string for testing purposes.
196
+ Since this is a stub server, we return "1.0.0" which is compliant
197
+ with both PEP 440 and the minimum version requirement.
198
+ """
199
+ return "1.0.0"
200
+
201
+ def close_application(self) -> None:
202
+ """Close DataLab application."""
203
+ # In stub mode, do nothing
204
+
205
+ def raise_window(self) -> None:
206
+ """Raise DataLab window."""
207
+ # In stub mode, do nothing
208
+
209
+ # Panel management
210
+ def get_current_panel(self) -> str:
211
+ """Get current panel name."""
212
+ return self.current_panel
213
+
214
+ def set_current_panel(self, panel: str) -> None:
215
+ """Set current panel."""
216
+ if panel in ("signal", "image", "macro"):
217
+ self.current_panel = panel
218
+
219
+ # Application control
220
+ def reset_all(self) -> None:
221
+ """Reset all data."""
222
+ self.signals.clear()
223
+ self.images.clear()
224
+ self.selected_objects.clear()
225
+ self.selected_groups.clear()
226
+
227
+ def toggle_auto_refresh(self, state: bool) -> None:
228
+ """Toggle auto refresh mode."""
229
+ self.auto_refresh = state
230
+ if self.verbose:
231
+ execenv.print(f"[STUB] Auto-refresh set to: {state}")
232
+
233
+ def toggle_show_titles(self, state: bool) -> None:
234
+ """Toggle show titles mode."""
235
+ self.show_titles = state
236
+ if self.verbose:
237
+ execenv.print(f"[STUB] Show titles set to: {state}")
238
+
239
+ # File operations
240
+ def save_to_h5_file(self, filename: str) -> None:
241
+ """Save to a DataLab HDF5 file."""
242
+ if self.verbose:
243
+ execenv.print(f"[STUB] Simulating H5 file save to: {filename}")
244
+ # In stub mode, just create a dummy text file to simulate the save operation
245
+ # This avoids HDF5 dependencies and potential test failures
246
+ try:
247
+ with open(filename, "w", encoding="utf-8") as f:
248
+ f.write("# DataLab stub file (for testing)\n")
249
+ f.write(f"# Signals: {len(self.signals)}\n")
250
+ f.write(f"# Images: {len(self.images)}\n")
251
+ f.write("# This is a dummy file created by the stub server\n")
252
+ if self.verbose:
253
+ execenv.print(
254
+ f"[STUB] Successfully created dummy file with {len(self.signals)} "
255
+ f"signals and {len(self.images)} images"
256
+ )
257
+ except Exception as exc: # pylint: disable=broad-except
258
+ if self.verbose:
259
+ execenv.print(f"[STUB] Failed to create dummy file: {exc}")
260
+ # Ignore errors in stub mode
261
+
262
+ # pylint: disable=unused-argument
263
+ def open_h5_files(
264
+ self,
265
+ h5files: list[str] | None = None,
266
+ import_all: bool | None = None,
267
+ reset_all: bool | None = None,
268
+ ) -> None:
269
+ """Open a DataLab HDF5 file or import from any other HDF5 file."""
270
+ if h5files is None:
271
+ return
272
+
273
+ if self.verbose:
274
+ execenv.print(f"[STUB] Simulating H5 file loading: {h5files}")
275
+
276
+ if reset_all:
277
+ if self.verbose:
278
+ execenv.print("[STUB] Resetting all data before loading")
279
+ self.reset_all()
280
+
281
+ # In stub mode, just simulate loading by creating dummy objects
282
+ # This avoids complex HDF5 file parsing and potential test failures
283
+ for _i, filename in enumerate(h5files):
284
+ # Create a dummy signal for each file
285
+ signal = create_signal(f"Loaded Signal from {os.path.basename(filename)}")
286
+ self.signals[str(uuid.uuid4())] = signal
287
+ if self.verbose:
288
+ execenv.print(f"[STUB] Created dummy signal: {signal.title}")
289
+
290
+ # Create a dummy image for each file
291
+ image = create_image(f"Loaded Image from {os.path.basename(filename)}")
292
+ self.images[str(uuid.uuid4())] = image
293
+ if self.verbose:
294
+ execenv.print(f"[STUB] Created dummy image: {image.title}")
295
+
296
+ def import_h5_file(self, filename: str, reset_all: bool | None = None) -> None:
297
+ """Open DataLab HDF5 browser to Import HDF5 file."""
298
+ self.open_h5_files([filename], import_all=True, reset_all=reset_all)
299
+
300
+ # Object operations
301
+ # pylint: disable=unused-argument
302
+ def add_signal(
303
+ self,
304
+ title: str,
305
+ xbinary: Binary,
306
+ ybinary: Binary,
307
+ xunit: str = "",
308
+ yunit: str = "",
309
+ xlabel: str = "",
310
+ ylabel: str = "",
311
+ group_id: str = "",
312
+ set_current: bool = True,
313
+ ) -> bool:
314
+ """Add signal data to DataLab."""
315
+ xdata = utils.rpcbinary_to_array(xbinary)
316
+ ydata = utils.rpcbinary_to_array(ybinary)
317
+
318
+ # Create real SignalObj using factory function
319
+ signal = create_signal(title, x=xdata, y=ydata)
320
+ signal.xunit = xunit or ""
321
+ signal.yunit = yunit or ""
322
+ signal.xlabel = xlabel or ""
323
+ signal.ylabel = ylabel or ""
324
+
325
+ # Store signal
326
+ obj_uuid = str(uuid.uuid4())
327
+ self.signals[obj_uuid] = signal
328
+
329
+ # Add to group if specified
330
+ if group_id and group_id in self.signal_groups:
331
+ self.signal_groups[group_id].objects.append(obj_uuid)
332
+
333
+ return True
334
+
335
+ # pylint: disable=unused-argument
336
+ def add_image(
337
+ self,
338
+ title: str,
339
+ zbinary: Binary,
340
+ xunit: str = "",
341
+ yunit: str = "",
342
+ zunit: str = "",
343
+ xlabel: str = "",
344
+ ylabel: str = "",
345
+ zlabel: str = "",
346
+ group_id: str = "",
347
+ set_current: bool = True,
348
+ ) -> bool:
349
+ """Add image data to DataLab."""
350
+ data = utils.rpcbinary_to_array(zbinary)
351
+
352
+ # Create real ImageObj using factory function
353
+ image = create_image(title, data=data)
354
+ image.xunit = xunit or ""
355
+ image.yunit = yunit or ""
356
+ image.zunit = zunit or ""
357
+ image.xlabel = xlabel or ""
358
+ image.ylabel = ylabel or ""
359
+ image.zlabel = zlabel or ""
360
+
361
+ # Store image
362
+ obj_uuid = str(uuid.uuid4())
363
+ self.images[obj_uuid] = image
364
+
365
+ # Add to group if specified
366
+ if group_id and group_id in self.image_groups:
367
+ self.image_groups[group_id].objects.append(obj_uuid)
368
+
369
+ return True
370
+
371
+ def add_object(
372
+ self, obj_data: list[str], group_id: str = "", set_current: bool = True
373
+ ) -> bool:
374
+ """Add object to stub server.
375
+
376
+ Args:
377
+ obj_serialized: Serialized signal or image object
378
+ group_id: group id in which to add the object. Defaults to ""
379
+ set_current: if True, set the added object as current
380
+ """
381
+ obj: SignalObj | ImageObj = utils.rpcjson_to_dataset(obj_data)
382
+ if self.verbose:
383
+ obj_str = "signal" if isinstance(obj, SignalObj) else "image"
384
+ obj_uuid = str(uuid.uuid4())
385
+ print(f"Added {obj_str} {obj.title} with UUID {obj_uuid}")
386
+ if isinstance(obj, SignalObj):
387
+ self.signals[obj_uuid] = obj
388
+ if group_id and group_id in self.signal_groups:
389
+ self.signal_groups[group_id].objects.append(obj_uuid)
390
+ else:
391
+ self.images[obj_uuid] = obj
392
+ if group_id and group_id in self.image_groups:
393
+ self.image_groups[group_id].objects.append(obj_uuid)
394
+
395
+ def load_from_files(self, filenames: list[str]) -> None:
396
+ """Load objects from files (stub implementation).
397
+
398
+ Args:
399
+ filenames: list of file names
400
+ """
401
+ if self.verbose:
402
+ print(
403
+ f"load_from_files called with {len(filenames)} files "
404
+ "(stub - not implemented)"
405
+ )
406
+
407
+ def load_from_directory(self, path: str) -> None:
408
+ """Load objects from directory (stub implementation).
409
+
410
+ Args:
411
+ path: directory path
412
+ """
413
+ if self.verbose:
414
+ print(
415
+ f"load_from_directory called with path: {path} (stub - not implemented)"
416
+ )
417
+
418
+ def get_object_titles(self, panel: str | None = None) -> list[str]:
419
+ """Get object titles for panel."""
420
+ panel = panel or self.current_panel
421
+ if panel == "signal":
422
+ return [signal.title for signal in self.signals.values()]
423
+ if panel == "image":
424
+ return [image.title for image in self.images.values()]
425
+ return []
426
+
427
+ # pylint: disable=unused-argument
428
+ def get_object_uuids(
429
+ self, panel: str | None = None, group: int | str | None = None
430
+ ) -> list[str]:
431
+ """Get object UUIDs for panel."""
432
+ panel = panel or self.current_panel
433
+ if panel == "signal":
434
+ return list(self.signals.keys())
435
+ if panel == "image":
436
+ return list(self.images.keys())
437
+ return []
438
+
439
+ def get_object(self, uuid_str: str, panel: str | None = None) -> list[str] | None:
440
+ """Get object by UUID, index, or title."""
441
+ panel = panel or self.current_panel
442
+
443
+ # Get the appropriate objects dictionary
444
+ if panel == "signal":
445
+ objects = self.signals
446
+ object_list = list(self.signals.keys())
447
+ elif panel == "image":
448
+ objects = self.images
449
+ object_list = list(self.images.keys())
450
+ else:
451
+ return None
452
+
453
+ # Try to resolve uuid_str as UUID first
454
+ if uuid_str in objects:
455
+ obj = objects[uuid_str]
456
+ else:
457
+ # Try to resolve as 1-based index
458
+ try:
459
+ index = int(uuid_str) - 1 # Convert to 0-based
460
+ if 0 <= index < len(object_list):
461
+ uuid_key = object_list[index]
462
+ obj = objects[uuid_key]
463
+ else:
464
+ return None
465
+ except (ValueError, TypeError):
466
+ # Try to find by title
467
+ obj = None
468
+ for object_instance in objects.values():
469
+ if object_instance.title == uuid_str:
470
+ obj = object_instance
471
+ break
472
+ if obj is None:
473
+ return None
474
+
475
+ # Use standard serialization with real objects
476
+ return utils.dataset_to_rpcjson(obj)
477
+
478
+ # pylint: disable=unused-argument
479
+ def get_object_shapes(
480
+ self, uuid_str: str, panel: str | None = None
481
+ ) -> list[dict] | None:
482
+ """Get object shapes."""
483
+ obj = self.signals.get(uuid_str) or self.images.get(uuid_str)
484
+ if obj is None:
485
+ return None
486
+ return [roi.to_dict() for roi in obj.roi]
487
+
488
+ def delete_metadata(self, uuid_str: str, key: str) -> bool:
489
+ """Delete metadata entry for object."""
490
+ obj = self.signals.get(uuid_str) or self.images.get(uuid_str)
491
+ if obj is None:
492
+ return False
493
+ if key in obj.metadata:
494
+ del obj.metadata[key]
495
+ return True
496
+ return False
497
+
498
+ # Selection operations
499
+ def select_objects(
500
+ self, selection: list[int | str], panel: str | None = None
501
+ ) -> None:
502
+ """Select objects by indices or UUIDs."""
503
+ panel = panel or self.current_panel
504
+ if panel == "signal":
505
+ uuids = list(self.signals.keys())
506
+ elif panel == "image":
507
+ uuids = list(self.images.keys())
508
+ else:
509
+ return
510
+
511
+ selected_uuids = []
512
+ for item in selection:
513
+ if isinstance(item, str):
514
+ # Item is a UUID
515
+ if item in uuids:
516
+ selected_uuids.append(item)
517
+ elif isinstance(item, int):
518
+ # Item is a 1-based index
519
+ index = item - 1 # Convert to 0-based
520
+ if 0 <= index < len(uuids):
521
+ selected_uuids.append(uuids[index])
522
+
523
+ self.selected_objects = selected_uuids
524
+
525
+ # pylint: disable=unused-argument
526
+ def get_sel_object_uuids(self, include_groups: bool = False) -> list[str]:
527
+ """Return selected objects uuids."""
528
+ return self.selected_objects.copy()
529
+
530
+ def select_groups(self, selection: list[int], panel: str | None = None) -> None:
531
+ """Select groups by indices."""
532
+ panel = panel or self.current_panel
533
+ if panel == "signal":
534
+ group_uuids = list(self.signal_groups.keys())
535
+ elif panel == "image":
536
+ group_uuids = list(self.image_groups.keys())
537
+ else:
538
+ return
539
+
540
+ self.selected_groups = [
541
+ group_uuids[i] for i in selection if 0 <= i < len(group_uuids)
542
+ ]
543
+
544
+ def delete_object(self, uuid_str: str) -> bool:
545
+ """Delete object by UUID."""
546
+ if uuid_str in self.signals:
547
+ del self.signals[uuid_str]
548
+ return True
549
+ if uuid_str in self.images:
550
+ del self.images[uuid_str]
551
+ return True
552
+ return False
553
+
554
+ def duplicate_object(self, uuid_str: str) -> str | None:
555
+ """Duplicate object and return new UUID."""
556
+ obj = self.signals.get(uuid_str) or self.images.get(uuid_str)
557
+ if obj is None:
558
+ return None
559
+
560
+ # Create a copy using serialization/deserialization
561
+ json_data = utils.dataset_to_rpcjson(obj)
562
+ new_obj: SignalObj | ImageObj = utils.rpcjson_to_dataset(json_data)
563
+ obj_uuid = str(uuid.uuid4())
564
+ new_obj.title = f"{obj.title} (copy)"
565
+
566
+ # Store the copy
567
+ if isinstance(obj, SignalObj):
568
+ self.signals[obj_uuid] = new_obj
569
+ else:
570
+ self.images[obj_uuid] = new_obj
571
+ return obj_uuid
572
+
573
+ def copy_metadata(self, src_uuid: str, dst_uuid: str) -> bool:
574
+ """Copy metadata from source to destination object."""
575
+ src_obj = self.signals.get(src_uuid) or self.images.get(src_uuid)
576
+ dst_obj = self.signals.get(dst_uuid) or self.images.get(dst_uuid)
577
+
578
+ if src_obj is None or dst_obj is None:
579
+ return False
580
+
581
+ dst_obj.metadata.update(src_obj.metadata)
582
+ return True
583
+
584
+ # Group operations
585
+ # pylint: disable=unused-argument
586
+ def add_group(
587
+ self, title: str, panel: str | None = None, select: bool = False
588
+ ) -> str:
589
+ """Add group and return UUID."""
590
+ panel = panel or self.current_panel
591
+ group = MockGroup(title)
592
+
593
+ if panel == "signal":
594
+ self.signal_groups[group.uuid] = group
595
+ elif panel == "image":
596
+ self.image_groups[group.uuid] = group
597
+
598
+ return group.uuid
599
+
600
+ def get_group_titles_with_object_info(
601
+ self,
602
+ ) -> tuple[list[str], list[list[str]], list[list[str]]]:
603
+ """Return groups titles and lists of inner objects uuids and titles."""
604
+ panel = self.current_panel
605
+ if panel == "signal":
606
+ groups = self.signal_groups
607
+ objects = self.signals
608
+ elif panel == "image":
609
+ groups = self.image_groups
610
+ objects = self.images
611
+ else:
612
+ return ([], [], [])
613
+
614
+ group_titles = []
615
+ group_uuids_lists = []
616
+ group_titles_lists = []
617
+
618
+ for group in groups.values():
619
+ group_titles.append(group.title)
620
+
621
+ # Get objects in this group
622
+ object_uuids = [uuid for uuid in group.objects if uuid in objects]
623
+ object_titles = [
624
+ objects[uuid].title for uuid in object_uuids if uuid in objects
625
+ ]
626
+
627
+ group_uuids_lists.append(object_uuids)
628
+ group_titles_lists.append(object_titles)
629
+
630
+ return (group_titles, group_uuids_lists, group_titles_lists)
631
+
632
+ def get_group_titles(self, panel: str | None = None) -> list[str]:
633
+ """Get group titles."""
634
+ panel = panel or self.current_panel
635
+ if panel == "signal":
636
+ return [group.title for group in self.signal_groups.values()]
637
+ if panel == "image":
638
+ return [group.title for group in self.image_groups.values()]
639
+ return []
640
+
641
+ def move_up(self, uuid_str: str) -> bool: # pylint: disable=unused-argument
642
+ """Move object up in list."""
643
+ # In stub mode, just return success
644
+ return True
645
+
646
+ def move_down(self, uuid_str: str) -> bool: # pylint: disable=unused-argument
647
+ """Move object down in list."""
648
+ # In stub mode, just return success
649
+ return True
650
+
651
+ def delete_group(self, group_id: str, panel: str | None = None) -> bool:
652
+ """Delete group."""
653
+ panel = panel or self.current_panel
654
+ if panel == "signal" and group_id in self.signal_groups:
655
+ del self.signal_groups[group_id]
656
+ return True
657
+ if panel == "image" and group_id in self.image_groups:
658
+ del self.image_groups[group_id]
659
+ return True
660
+ return False
661
+
662
+ # Macro operations (stub implementations)
663
+ def import_macro_from_file(self, filename: str) -> None:
664
+ """Import macro from file (stub implementation).
665
+
666
+ Args:
667
+ filename: Filename
668
+ """
669
+ if self.verbose:
670
+ print(f"import_macro_from_file called: {filename} (stub - not implemented)")
671
+
672
+ def run_macro(self, number_or_title: int | str | None = None) -> None:
673
+ """Run macro (stub implementation).
674
+
675
+ Args:
676
+ number_or_title: Number or title of the macro. Defaults to None.
677
+ """
678
+ if self.verbose:
679
+ print(f"run_macro called: {number_or_title} (stub - not implemented)")
680
+
681
+ def stop_macro(self, number_or_title: int | str | None = None) -> None:
682
+ """Stop macro (stub implementation).
683
+
684
+ Args:
685
+ number_or_title: Number or title of the macro. Defaults to None.
686
+ """
687
+ if self.verbose:
688
+ print(f"stop_macro called: {number_or_title} (stub - not implemented)")
689
+
690
+ # Calculation operations
691
+ def calc(self, name: str, param: list[str] | None = None) -> str | None:
692
+ """Execute calculation and return result object UUID."""
693
+ if self.verbose:
694
+ execenv.print(
695
+ f"[STUB] Simulating calculation '{name}' with params: {param}"
696
+ )
697
+
698
+ # In stub mode, just simulate by creating a dummy result object
699
+ if not self.selected_objects:
700
+ if self.verbose:
701
+ execenv.print("[STUB] No objects selected for calculation")
702
+ return None
703
+
704
+ src_uuid = self.selected_objects[0]
705
+ src_obj = self.signals.get(src_uuid) or self.images.get(src_uuid)
706
+
707
+ if src_obj is None:
708
+ if self.verbose:
709
+ execenv.print(f"[STUB] Source object {src_uuid} not found")
710
+ return None
711
+
712
+ # Create a dummy result object based on source type
713
+ if isinstance(src_obj, SignalObj):
714
+ result = create_signal(f"{name}({src_obj.title})")
715
+ obj_uuid = str(uuid.uuid4())
716
+ self.signals[obj_uuid] = result
717
+ if self.verbose:
718
+ execenv.print(f"[STUB] Created dummy signal result: {result.title}")
719
+ return obj_uuid
720
+ if isinstance(src_obj, ImageObj):
721
+ result = create_image(f"{name}({src_obj.title})")
722
+ obj_uuid = str(uuid.uuid4())
723
+ self.images[obj_uuid] = result
724
+ if self.verbose:
725
+ execenv.print(f"[STUB] Created dummy image result: {result.title}")
726
+ return obj_uuid
727
+
728
+ if self.verbose:
729
+ execenv.print("[STUB] Unsupported object type for calculation")
730
+ return None
731
+
732
+ # Annotation operations
733
+ # pylint: disable=unused-argument
734
+ def add_annotations_from_items(
735
+ self, items: list[str], refresh_plot: bool = True, panel: str | None = None
736
+ ) -> None:
737
+ """Add object annotations (annotation plot items)."""
738
+ # In stub mode, just acknowledge the annotations
739
+ # Real implementation would deserialize items and add to objects
740
+
741
+ # pylint: disable=unused-argument
742
+ def add_label_with_title(
743
+ self, title: str | None = None, panel: str | None = None
744
+ ) -> None:
745
+ """Add label with title."""
746
+ if self.verbose:
747
+ execenv.print(f"[STUB] Simulating add label with title: {title}")
748
+ # In stub mode, just acknowledge the label
749
+
750
+
751
+ @contextmanager
752
+ def datalab_stub_server(
753
+ port: int = 0, verbose: bool = True
754
+ ) -> Generator[int, None, None]:
755
+ """Context manager for DataLab stub server.
756
+
757
+ Args:
758
+ port: Port to bind to. If 0, uses a random available port.
759
+ verbose: If True, print verbose debug information.
760
+
761
+ Yields:
762
+ Port number the server is listening on
763
+ """
764
+ server = DataLabStubServer(port, verbose=verbose)
765
+ try:
766
+ actual_port = server.start()
767
+ yield actual_port
768
+ finally:
769
+ server.stop()
770
+
771
+
772
+ def patch_simpleremoteproxy_for_stub() -> DataLabStubServer:
773
+ """Patch SimpleRemoteProxy to connect to a stub server instead of real DataLab.
774
+
775
+ This utility function:
776
+ 1. Creates and starts a DataLabStubServer instance
777
+ 2. Patches SimpleRemoteProxy.__connect_to_server to use the stub server port
778
+ 3. Returns the stub server instance for later cleanup
779
+
780
+ Returns:
781
+ The running DataLabStubServer instance that needs to be stopped later
782
+
783
+ Example:
784
+ >>> stub_server = patch_simpleremoteproxy_for_stub()
785
+ >>> try:
786
+ ... # Your code using SimpleRemoteProxy here
787
+ ... proxy = SimpleRemoteProxy()
788
+ ... # proxy will connect to stub server automatically
789
+ ... finally:
790
+ ... stub_server.stop()
791
+ """
792
+ # pylint: disable=import-outside-toplevel
793
+ # Import directly from remote module to avoid circular dependency
794
+ from sigima.client.remote import SimpleRemoteProxy
795
+
796
+ # Store original method
797
+ # pylint: disable=protected-access
798
+ original_connect_to_server = SimpleRemoteProxy._SimpleRemoteProxy__connect_to_server
799
+
800
+ # Start stub server
801
+ stub_server_instance = DataLabStubServer(verbose=False)
802
+ stub_port = stub_server_instance.start()
803
+
804
+ # pylint: disable=unused-argument
805
+ def patched_connect_to_server(self, port=None):
806
+ """Patched connect that uses stub server port."""
807
+ # Always use the stub server port, ignore the requested port
808
+ original_connect_to_server(self, port=str(stub_port))
809
+
810
+ # Apply the patch
811
+ # pylint: disable=protected-access
812
+ SimpleRemoteProxy._SimpleRemoteProxy__connect_to_server = patched_connect_to_server
813
+
814
+ return stub_server_instance