pymmcore-plus 0.9.3__py3-none-any.whl → 0.13.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 (68) hide show
  1. pymmcore_plus/__init__.py +7 -4
  2. pymmcore_plus/_benchmark.py +203 -0
  3. pymmcore_plus/_build.py +6 -1
  4. pymmcore_plus/_cli.py +131 -31
  5. pymmcore_plus/_logger.py +19 -10
  6. pymmcore_plus/_pymmcore.py +12 -0
  7. pymmcore_plus/_util.py +139 -32
  8. pymmcore_plus/core/__init__.py +5 -0
  9. pymmcore_plus/core/_config.py +6 -4
  10. pymmcore_plus/core/_config_group.py +4 -3
  11. pymmcore_plus/core/_constants.py +135 -10
  12. pymmcore_plus/core/_device.py +4 -4
  13. pymmcore_plus/core/_metadata.py +3 -3
  14. pymmcore_plus/core/_mmcore_plus.py +254 -170
  15. pymmcore_plus/core/_property.py +6 -6
  16. pymmcore_plus/core/_sequencing.py +370 -233
  17. pymmcore_plus/core/events/__init__.py +6 -6
  18. pymmcore_plus/core/events/_device_signal_view.py +8 -6
  19. pymmcore_plus/core/events/_norm_slot.py +2 -4
  20. pymmcore_plus/core/events/_prop_event_mixin.py +7 -4
  21. pymmcore_plus/core/events/_protocol.py +5 -2
  22. pymmcore_plus/core/events/_psygnal.py +2 -2
  23. pymmcore_plus/experimental/__init__.py +0 -0
  24. pymmcore_plus/experimental/unicore/__init__.py +14 -0
  25. pymmcore_plus/experimental/unicore/_device_manager.py +173 -0
  26. pymmcore_plus/experimental/unicore/_proxy.py +127 -0
  27. pymmcore_plus/experimental/unicore/_unicore.py +703 -0
  28. pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
  29. pymmcore_plus/experimental/unicore/devices/_device.py +269 -0
  30. pymmcore_plus/experimental/unicore/devices/_properties.py +400 -0
  31. pymmcore_plus/experimental/unicore/devices/_stage.py +221 -0
  32. pymmcore_plus/install.py +16 -11
  33. pymmcore_plus/mda/__init__.py +1 -1
  34. pymmcore_plus/mda/_engine.py +320 -148
  35. pymmcore_plus/mda/_protocol.py +6 -4
  36. pymmcore_plus/mda/_runner.py +62 -51
  37. pymmcore_plus/mda/_thread_relay.py +5 -3
  38. pymmcore_plus/mda/events/__init__.py +2 -2
  39. pymmcore_plus/mda/events/_protocol.py +10 -2
  40. pymmcore_plus/mda/events/_psygnal.py +2 -2
  41. pymmcore_plus/mda/handlers/_5d_writer_base.py +106 -15
  42. pymmcore_plus/mda/handlers/__init__.py +7 -1
  43. pymmcore_plus/mda/handlers/_img_sequence_writer.py +11 -6
  44. pymmcore_plus/mda/handlers/_ome_tiff_writer.py +8 -4
  45. pymmcore_plus/mda/handlers/_ome_zarr_writer.py +82 -9
  46. pymmcore_plus/mda/handlers/_tensorstore_handler.py +374 -0
  47. pymmcore_plus/mda/handlers/_util.py +1 -1
  48. pymmcore_plus/metadata/__init__.py +36 -0
  49. pymmcore_plus/metadata/functions.py +353 -0
  50. pymmcore_plus/metadata/schema.py +472 -0
  51. pymmcore_plus/metadata/serialize.py +120 -0
  52. pymmcore_plus/mocks.py +51 -0
  53. pymmcore_plus/model/_config_file.py +5 -6
  54. pymmcore_plus/model/_config_group.py +29 -2
  55. pymmcore_plus/model/_core_device.py +12 -1
  56. pymmcore_plus/model/_core_link.py +2 -1
  57. pymmcore_plus/model/_device.py +39 -8
  58. pymmcore_plus/model/_microscope.py +39 -3
  59. pymmcore_plus/model/_pixel_size_config.py +27 -4
  60. pymmcore_plus/model/_property.py +13 -3
  61. pymmcore_plus/seq_tester.py +1 -1
  62. {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/METADATA +22 -12
  63. pymmcore_plus-0.13.0.dist-info/RECORD +71 -0
  64. {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/WHEEL +1 -1
  65. pymmcore_plus/core/_state.py +0 -244
  66. pymmcore_plus-0.9.3.dist-info/RECORD +0 -55
  67. {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/entry_points.txt +0 -0
  68. {pymmcore_plus-0.9.3.dist-info → pymmcore_plus-0.13.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,472 @@
1
+ from typing import Any, Literal, Optional, TypedDict, Union
2
+
3
+ import useq
4
+ from typing_extensions import NotRequired
5
+
6
+ __all__ = [
7
+ "ConfigGroup",
8
+ "ConfigPreset",
9
+ "DeviceInfo",
10
+ "FrameMetaV1",
11
+ "ImageInfo",
12
+ "PixelSizeConfigPreset",
13
+ "Position",
14
+ "PropertyInfo",
15
+ "PropertyValue",
16
+ "SummaryMetaV1",
17
+ "SystemInfo",
18
+ ]
19
+
20
+ AffineTuple = tuple[float, float, float, float, float, float]
21
+
22
+
23
+ class PropertyInfo(TypedDict):
24
+ """Information about a single device property.
25
+
26
+ Attributes
27
+ ----------
28
+ name : str
29
+ The name of the property.
30
+ value : str | None
31
+ The current value of the property, if any.
32
+ data_type : Literal["undefined", "float", "int", "str"]
33
+ The data type of the `value` field.
34
+ is_read_only : bool
35
+ Whether the property is read-only.
36
+ allowed_values : tuple[str, ...]
37
+ *Not Required*. The allowed values for the property, if any. Consumers should
38
+ not depend on this field being present.
39
+ is_pre_init : bool
40
+ *Not Required*. Whether the property is pre-init. If missing, assume `False`.
41
+ limits : tuple[float, float]
42
+ *Not Required*. The limits of the property, if any. If missing, the property
43
+ has no limits.
44
+ sequenceable : bool
45
+ *Not Required*. Whether the property is sequenceable. If missing, assume
46
+ `False`.
47
+ sequence_max_length : int
48
+ *Not Required*. The maximum length of a sequence for the property,
49
+ if applicable. Will be missing if the property is not sequenceable.
50
+ """
51
+
52
+ name: str
53
+ value: Optional[str]
54
+ data_type: Literal["undefined", "float", "int", "str"]
55
+ is_read_only: bool
56
+ allowed_values: NotRequired[tuple[str, ...]]
57
+ is_pre_init: NotRequired[bool]
58
+ limits: NotRequired[tuple[float, float]]
59
+ sequenceable: NotRequired[bool]
60
+ sequence_max_length: NotRequired[int]
61
+ # device_label: str
62
+
63
+
64
+ class DeviceInfo(TypedDict):
65
+ """Information about a specific device.
66
+
67
+ Attributes
68
+ ----------
69
+ label : str
70
+ The user-provided label of the device.
71
+ library : str
72
+ The name of the device adapter library (e.g. "DemoCamera" or "ASITiger").
73
+ name : str
74
+ The name of the device, as known to the adapter. (e.g. "DCam" or "XYStage")
75
+ type : str
76
+ The type of the device (e.g. "Camera", "XYStage", "State", etc...)
77
+ description : str
78
+ A description of the device, provided by the adapter.
79
+ properties : tuple[PropertyInfo, ...]
80
+ Information about the device's properties.
81
+ parent_label : str
82
+ *Not Required*. The label of the parent device, if any. This will be missing for
83
+ hub devices and other devices that are not peripherals.
84
+ labels : tuple[str, ...]
85
+ *Not Required*. The labels of the device, if it is a state device.
86
+ child_names : tuple[str, ...]
87
+ *Not Required*. The names of the child (peripheral) devices, if it is a hub
88
+ device.
89
+ is_continuous_focus_drive : bool
90
+ *Not Required*. Whether the device is a continuous focus drive. If missing,
91
+ assume `False`.
92
+ focus_direction : Literal["Unknown", "TowardSample", "AwayFromSample"]
93
+ *Not Required*. The direction of focus movement. Will be missing if device
94
+ is not a Stage device.
95
+ is_sequenceable : bool
96
+ *Not Required*. Whether the device is sequenceable. If missing, assume `False`.
97
+ This may be present for Cameras, SLMs, Stages, and XYStages. See also the
98
+ `is_sequenceable` property of each
99
+ [`PropertyInfo`][pymmcore_plus.metadata.schema.PropertyInfo] object.
100
+ """
101
+
102
+ label: str
103
+ library: str
104
+ name: str
105
+ type: str
106
+ description: str
107
+ properties: tuple[PropertyInfo, ...]
108
+
109
+ # hub devices and non-peripheral devices will have no parent_label
110
+ parent_label: NotRequired[str]
111
+ # state device only
112
+ labels: NotRequired[tuple[str, ...]]
113
+ # hub device only
114
+ child_names: NotRequired[tuple[str, ...]]
115
+ # stage/focus device only
116
+ is_continuous_focus_drive: NotRequired[bool]
117
+ focus_direction: NotRequired[Literal["Unknown", "TowardSample", "AwayFromSample"]]
118
+ # camera, slm, stage/focus, or XYStage devices only
119
+ is_sequenceable: NotRequired[bool]
120
+
121
+
122
+ class SystemInfo(TypedDict):
123
+ """General system information.
124
+
125
+ Attributes
126
+ ----------
127
+ pymmcore_version : str
128
+ The version of the PyMMCore library.
129
+ pymmcore_plus_version : str
130
+ The version of the PyMMCore Plus library.
131
+ mmcore_version : str
132
+ The version of the MMCore library. (e.g. `MMCore version 11.1.1`)
133
+ device_api_version : str
134
+ The version of the device API.
135
+ (e.g. `Device API version 71, Module API version 10`)
136
+ device_adapter_search_paths : tuple[str, ...]
137
+ The active search paths for device adapters. This may be useful to indicate
138
+ the nightly build of device adapters, or other information that isn't in the
139
+ version numbers.
140
+ system_configuration_file : str | None
141
+ The path of the last loaded system configuration file, if any.
142
+ primary_log_file : str
143
+ The path of the primary log file.
144
+ sequence_buffer_size_mb : int
145
+ The size of the circular buffer available for storing images during
146
+ hardware-triggered sequence acquisition.
147
+ continuous_focus_enabled : bool
148
+ Whether continuous focus is enabled.
149
+ continuous_focus_locked : bool
150
+ Whether continuous focus is currently locked.
151
+ auto_shutter : bool
152
+ Whether auto-shutter is currently active.
153
+ timeout_ms : int | None
154
+ *Not Required*. The current timeout in milliseconds for the system. The default
155
+ timeout is 5000 ms.
156
+ """
157
+
158
+ pymmcore_version: str
159
+ pymmcore_plus_version: str
160
+ mmcore_version: str
161
+ device_api_version: str
162
+ device_adapter_search_paths: tuple[str, ...]
163
+ system_configuration_file: Optional[str]
164
+ primary_log_file: str
165
+ sequence_buffer_size_mb: int
166
+ continuous_focus_enabled: bool
167
+ continuous_focus_locked: bool
168
+ auto_shutter: bool
169
+ timeout_ms: NotRequired[int]
170
+
171
+
172
+ class ImageInfo(TypedDict):
173
+ """Information about the image format for a camera device.
174
+
175
+ Attributes
176
+ ----------
177
+ camera_label : str
178
+ The label of the corresponding camera device.
179
+ plane_shape : tuple[int, ...]
180
+ The shape (height, width[, num_components]) of the numpy array that will be
181
+ returned for each snap of the camera. This will be length 2 for monochromatic
182
+ images, and length 3 for images with multiple components (e.g. RGB).
183
+ dtype : str
184
+ The numpy dtype of the image array (e.g. "uint8", "uint16", etc...)
185
+ height : int
186
+ The height of the image in pixels.
187
+ width : int
188
+ The width of the image in pixels.
189
+ pixel_format : Literal["Mono8", "Mono10", "Mono12", "Mono14", "Mono16", "Mono32", "RGB8", "RGB10", "RGB12", "RGB14", "RGB16"]
190
+ The GenICam pixel format of the camera. See
191
+ [PixelFormat][pymmcore_plus.PixelFormat] and
192
+ <https://docs.baslerweb.com/pixel-format#unpacked-and-packed-pixel-formats>
193
+ for more information.
194
+ pixel_size_config_name : str
195
+ The name of the currently active pixel size configuration.
196
+ pixel_size_um : float
197
+ The pixel size in microns.
198
+ magnification_factor : float
199
+ *Not Required*. The product of magnification of all loaded devices of type
200
+ MagnifierDevice. If no devices are found, or all have magnification=1, this
201
+ will not be present.
202
+ pixel_size_affine : tuple[float, float, float, float, float, float]
203
+ *Not Required*. Affine Transform to relate camera pixels with stage movement,
204
+ corrected for binning and known magnification devices. The affine transform
205
+ consists of the first two rows of a 3x3 matrix, the third row is always assumed
206
+ to be `(0, 0, 1)`. If missing, assume identity transform.
207
+ roi : tuple[int, int, int, int]
208
+ *Not Required*. The active subarray (ROI: region of interest) on the camera, in
209
+ the form `(x_offset, y_offset, width, height)`. If missing, the full chip
210
+ is being used.
211
+ multi_roi : tuple[list[int], list[int], list[int], list[int]]
212
+ *Not Required*. The active subarrays (ROIs: regions of interest) on the camera,
213
+ in the form `(x_offsets, y_offsets, widths, heights)`. If missing, the camera
214
+ does not support multiple ROIs or is not currently using them.
215
+ """ # noqa: E501
216
+
217
+ camera_label: str
218
+ plane_shape: tuple[int, ...]
219
+ dtype: str
220
+
221
+ height: int
222
+ width: int
223
+ pixel_format: Literal[
224
+ "Mono8",
225
+ "Mono10",
226
+ "Mono12",
227
+ "Mono14",
228
+ "Mono16",
229
+ "Mono32",
230
+ "RGB8",
231
+ "RGB10",
232
+ "RGB12",
233
+ "RGB14",
234
+ "RGB16",
235
+ ]
236
+
237
+ pixel_size_config_name: str
238
+ pixel_size_um: float
239
+ magnification_factor: NotRequired[float]
240
+ pixel_size_affine: NotRequired[AffineTuple]
241
+ roi: NotRequired[tuple[int, int, int, int]]
242
+ multi_roi: NotRequired[tuple[list[int], list[int], list[int], list[int]]]
243
+
244
+ # # this will be != 1 for things like multi-camera device,
245
+ # # or any "single" device adapter that manages multiple detectors, like PMTs, etc..
246
+ # num_camera_adapter_channels: NotRequired[int]
247
+
248
+
249
+ class StagePosition(TypedDict):
250
+ """Represents the position of a single stage device."""
251
+
252
+ device_label: str
253
+ position: Union[float, tuple[float, float]]
254
+
255
+
256
+ class Position(TypedDict):
257
+ """Represents a position in 3D space and focus.
258
+
259
+ Attributes
260
+ ----------
261
+ x : float
262
+ *Not Required*. The X coordinate of the "active" XY stage device.
263
+ May be missing if there is no current XY stage device.
264
+ y : float
265
+ *Not Required*. The Y coordinate of the "active" XY stage device.
266
+ May be missing if there is no current XY stage device.
267
+ z : float
268
+ *Not Required*. The coordinate of the "active" focus device.
269
+ May be missing if there is no current focus stage device.
270
+ all_stages : tuple[StagePosition, ...]
271
+ *Not Required*. The positions of *all* stage devices (both inactive and active
272
+ devices that are represented by `x`, `y`, and `z`). Inclusion of this field
273
+ is up to the implementer.
274
+ """
275
+
276
+ x: NotRequired[float]
277
+ y: NotRequired[float]
278
+ z: NotRequired[float]
279
+ all_stages: NotRequired[list[StagePosition]]
280
+
281
+
282
+ class PropertyValue(TypedDict):
283
+ """A single device property setting.
284
+
285
+ This represents a single device property setting, whether it be an "active" value,
286
+ or an intended value as a part of a configuration preset.
287
+
288
+ Attributes
289
+ ----------
290
+ dev : str
291
+ The label of the device.
292
+ prop : str
293
+ The name of the property.
294
+ val : Any
295
+ The value of the property.
296
+ """
297
+
298
+ dev: str
299
+ prop: str
300
+ val: Any
301
+
302
+
303
+ class ConfigPreset(TypedDict):
304
+ """A group of device property settings.
305
+
306
+ Attributes
307
+ ----------
308
+ name : str
309
+ The name of the preset.
310
+ settings : tuple[PropertyValue, ...]
311
+ A collection of device property settings that make up the preset.
312
+ """
313
+
314
+ name: str
315
+ settings: tuple[PropertyValue, ...]
316
+
317
+
318
+ class PixelSizeConfigPreset(ConfigPreset):
319
+ """A specialized group of device property settings for a pixel size preset.
320
+
321
+ Attributes
322
+ ----------
323
+ name : str
324
+ The name of the pixel size preset.
325
+ settings : tuple[PropertyValue, ...]
326
+ A collection of device property settings that make up the pixel size preset.
327
+ pixel_size_um : float
328
+ The pixel size in microns.
329
+ pixel_size_affine : tuple[float, float, float, float, float, float]
330
+ *Not Required*. Affine Transform to relate camera pixels with stage movement,
331
+ corrected for binning and known magnification devices. The affine transform
332
+ consists of the first two rows of a 3x3 matrix, the third row is always assumed
333
+ to be 0.0 0.0 1.0.
334
+ """
335
+
336
+ pixel_size_um: float
337
+ pixel_size_affine: NotRequired[AffineTuple]
338
+
339
+
340
+ class ConfigGroup(TypedDict):
341
+ """A group of configuration presets.
342
+
343
+ Attributes
344
+ ----------
345
+ name : str
346
+ The name of the config group.
347
+ presets : tuple[ConfigPreset, ...]
348
+ A collection of presets, each of which define a set of device property settings
349
+ that can be applied to the system.
350
+ """
351
+
352
+ name: str
353
+ presets: tuple[ConfigPreset, ...]
354
+
355
+
356
+ class SummaryMetaV1(TypedDict):
357
+ """Complete summary metadata for the system.
358
+
359
+ This is the structure of the summary metadata object that is emitted during the
360
+ [`sequenceStarted`][pymmcore_plus.mda.events.PMDASignaler.sequenceStarted] event of
361
+ an MDA run. It contains general information about the system and all of the
362
+ devices.
363
+
364
+ It may be generated outside of a running mda sequence as well using
365
+ [`pymmcore_plus.metadata.summary_metadata`][]
366
+
367
+ Attributes
368
+ ----------
369
+ format: Literal["summary-dict"]
370
+ The format of this summary metadata object.
371
+ version: Literal["1.0"]
372
+ The version of this summary metadata object.
373
+ datetime : str
374
+ The date and time when the summary metadata was generated. This is an ISO 8601
375
+ formatted string, including date, time and offset from UTC:
376
+ `YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM`
377
+ devices : tuple[DeviceInfo, ...]
378
+ Information about all loaded devices.
379
+ system_info : SystemInfo
380
+ General system information.
381
+ image_infos : tuple[ImageInfo, ...]
382
+ Information about the current image structure.
383
+ config_groups : tuple[ConfigGroup, ...]
384
+ Groups of device property settings.
385
+ pixel_size_configs : tuple[PixelSizeConfigPreset, ...]
386
+ Pixel size presets.
387
+ position : Position
388
+ Current position in 3D space.
389
+ mda_sequence : useq.MDASequence
390
+ *NotRequired*. The current MDA sequence.
391
+ extra: dict[str, Any]
392
+ *NotRequired*. Additional information, may be used to store arbitrary user info.
393
+ """
394
+
395
+ format: Literal["summary-dict"]
396
+ version: Literal["1.0"]
397
+ datetime: NotRequired[str]
398
+ devices: tuple[DeviceInfo, ...]
399
+ system_info: SystemInfo
400
+ image_infos: tuple[ImageInfo, ...]
401
+ config_groups: tuple[ConfigGroup, ...]
402
+ pixel_size_configs: tuple[PixelSizeConfigPreset, ...]
403
+ position: Position
404
+ mda_sequence: NotRequired[useq.MDASequence]
405
+ extra: NotRequired[dict[str, Any]]
406
+
407
+
408
+ class FrameMetaV1(TypedDict):
409
+ """Metadata for a single frame.
410
+
411
+ This is the structure of the summary metadata object that is emitted during the
412
+ [`frameReady`][pymmcore_plus.mda.events.PMDASignaler.frameReady] event of
413
+ an MDA run. It contains information about the frame that was just acquired. By
414
+ design, it is relatively lightweight and does not contain the full system state.
415
+ Values that are not expected to change during an MDA sequence should be looked up
416
+ in the summary metadata.
417
+
418
+ It may be generated outside of a running mda sequence as well using
419
+ [`pymmcore_plus.metadata.frame_metadata`][]
420
+
421
+ Attributes
422
+ ----------
423
+ format: Literal["frame-dict"]
424
+ The format of this frame metadata object.
425
+ version: Literal["1.0"]
426
+ The version of this frame metadata object.
427
+ pixel_size_um: float
428
+ The pixel size in microns.
429
+ camera_device: str
430
+ The label of the camera device used to acquire the image.
431
+ exposure_ms: float
432
+ The exposure time in milliseconds.
433
+ property_values: tuple[PropertyValue, ...]
434
+ Device property settings. This is not a comprehensive list of all device
435
+ properties, but only those that may have changed for this frame (such as
436
+ properties in the channel config or light path config).
437
+ runner_time_ms: float
438
+ Elapsed time in milliseconds since the beginning of the MDA sequence.
439
+ position: Position
440
+ *NotRequired*. The current stage position(s) in 3D space. This is often slow
441
+ to retrieve, so its inclusion is optional and left to the implementer.
442
+ mda_event: useq.MDAEvent
443
+ *NotRequired*. The MDA event object that commanded the acquisition of this
444
+ frame.
445
+ hardware_triggered: bool
446
+ *NotRequired*. Whether the frame was part of a hardware-triggered sequence.
447
+ If missing, assume `False`.
448
+ images_remaining_in_buffer: int
449
+ *NotRequired*. The number of images remaining to be popped from the image
450
+ buffer (only applicable for hardware-triggered sequences).
451
+ camera_metadata: dict[str, Any]
452
+ *NotRequired*. Additional metadata from the camera device. This is unstructured
453
+ and may contain any information that the camera device provides. Do not rely
454
+ on the presence of any particular keys.
455
+ extra: dict[str, Any]
456
+ *NotRequired*. Additional information, may be used to store arbitrary user info
457
+ or additional metadata.
458
+ """
459
+
460
+ format: Literal["frame-dict"]
461
+ version: Literal["1.0"]
462
+ pixel_size_um: float
463
+ camera_device: Optional[str]
464
+ exposure_ms: float
465
+ property_values: tuple[PropertyValue, ...]
466
+ runner_time_ms: float
467
+ position: NotRequired[Position]
468
+ mda_event: NotRequired[useq.MDAEvent]
469
+ hardware_triggered: NotRequired[bool]
470
+ images_remaining_in_buffer: NotRequired[int]
471
+ camera_metadata: NotRequired[dict[str, Any]]
472
+ extra: NotRequired[dict[str, Any]]
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from collections.abc import Mapping, Sequence
6
+ from contextlib import suppress
7
+ from datetime import timedelta
8
+ from enum import Enum
9
+ from types import MappingProxyType
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ import numpy as np
13
+
14
+ try:
15
+ # use msgspec if available
16
+ import msgspec
17
+ except ImportError:
18
+ msgspec = None
19
+
20
+
21
+ if TYPE_CHECKING:
22
+ import pydantic # noqa: F401
23
+
24
+
25
+ def encode_hook(obj: Any, raises: bool = True) -> Any:
26
+ """Hook to encode objects that are not JSON serializable."""
27
+ if not TYPE_CHECKING:
28
+ pydantic = sys.modules.get("pydantic")
29
+ if pydantic and isinstance(obj, pydantic.BaseModel):
30
+ try:
31
+ return obj.model_dump(mode="json", exclude_unset=True)
32
+ except (AttributeError, TypeError):
33
+ return to_builtins(obj.dict(exclude_unset=True))
34
+ if isinstance(obj, MappingProxyType):
35
+ return dict(obj)
36
+ if isinstance(obj, np.number):
37
+ return obj.item()
38
+ if isinstance(obj, timedelta):
39
+ return obj.total_seconds()
40
+ if isinstance(obj, Enum):
41
+ return obj.value
42
+ if raises:
43
+ raise NotImplementedError(f"Cannot serialize object of type {type(obj)}")
44
+ return obj
45
+
46
+
47
+ # def decode_hook(type: type, obj: Any) -> Any:
48
+ # """Hook to decode objects that are not JSON deserializable."""
49
+ # if not TYPE_CHECKING:
50
+ # pydantic = sys.modules.get("pydantic")
51
+ # if pydantic:
52
+ # with suppress(TypeError):
53
+ # if issubclass(type, pydantic.BaseModel):
54
+ # return type.model_validate(obj)
55
+ # raise NotImplementedError(f"Cannot deserialize object of type {type}")
56
+
57
+
58
+ def schema_hook(obj: type) -> dict[str, Any]:
59
+ """Hook to convert objects to schema."""
60
+ if not TYPE_CHECKING:
61
+ pydantic = sys.modules.get("pydantic")
62
+ if pydantic:
63
+ with suppress(TypeError):
64
+ if issubclass(obj, pydantic.BaseModel):
65
+ return obj.model_json_schema()
66
+ raise NotImplementedError(f"Cannot create schema for object of type {type(obj)}")
67
+
68
+
69
+ def msgspec_json_dumps(obj: Any, *, indent: int | None = None) -> bytes:
70
+ """Serialize object to bytes."""
71
+ encoded = msgspec.json.encode(obj, enc_hook=encode_hook)
72
+ if indent is not None:
73
+ encoded = msgspec.json.format(encoded, indent=indent)
74
+ return encoded # type: ignore [no-any-return]
75
+
76
+
77
+ def msgspec_json_loads(s: bytes | str) -> Any:
78
+ """Deserialize bytes to object."""
79
+ return msgspec.json.decode(s)
80
+
81
+
82
+ def msgspec_to_builtins(obj: Any) -> Any:
83
+ """Convert object to built-in types."""
84
+ return msgspec.to_builtins(obj, enc_hook=encode_hook)
85
+
86
+
87
+ def msgspec_to_schema(type: Any) -> Any:
88
+ """Generate JSON schema for a given type."""
89
+ if msgspec is None: # pragma: no cover
90
+ raise ImportError("msgspec is required for this function")
91
+ return msgspec.json.schema(type, schema_hook=schema_hook)
92
+
93
+
94
+ def std_json_dumps(obj: Any, *, indent: int | None = None) -> bytes:
95
+ """Serialize object to bytes."""
96
+ return json.dumps(obj, default=std_to_builtins, indent=indent).encode("utf-8")
97
+
98
+
99
+ def std_json_loads(s: bytes | str) -> Any:
100
+ """Deserialize bytes to object."""
101
+ return json.loads(s)
102
+
103
+
104
+ def std_to_builtins(obj: Any) -> Any:
105
+ """Convert object to built-in types."""
106
+ if isinstance(obj, Mapping):
107
+ return {k: std_to_builtins(v) for k, v in obj.items()}
108
+ if isinstance(obj, Sequence) and not isinstance(obj, str):
109
+ return [std_to_builtins(v) for v in obj]
110
+ return encode_hook(obj, raises=False)
111
+
112
+
113
+ if msgspec is None: # pragma: no cover
114
+ json_dumps = std_json_dumps
115
+ json_loads = std_json_loads
116
+ to_builtins = std_to_builtins
117
+ else:
118
+ json_dumps = msgspec_json_dumps
119
+ json_loads = msgspec_json_loads
120
+ to_builtins = msgspec_to_builtins
pymmcore_plus/mocks.py ADDED
@@ -0,0 +1,51 @@
1
+ from typing import Any
2
+ from unittest.mock import MagicMock
3
+
4
+
5
+ class MockSequenceableCore(MagicMock):
6
+ """Sequenceable mock for testing."""
7
+
8
+ def __init__(self, *args: Any, max_len: int = 100, **kwargs: Any) -> None:
9
+ super().__init__(*args, **kwargs)
10
+
11
+ from pymmcore_plus import CMMCorePlus
12
+
13
+ if isinstance(kwargs.get("wraps", None), CMMCorePlus):
14
+ self.isExposureSequenceable.return_value = True
15
+ self.getExposureSequenceMaxLength.return_value = max_len
16
+
17
+ self.isStageSequenceable.return_value = True
18
+ self.getStageSequenceMaxLength.return_value = max_len
19
+
20
+ self.isXYStageSequenceable.return_value = True
21
+ self.getXYStageSequenceMaxLength.return_value = max_len
22
+
23
+ self.getSLMSequenceMaxLength.return_value = max_len
24
+ self.getPropertySequenceMaxLength.return_value = max_len
25
+
26
+ self.isPropertySequenceable.side_effect = self._isPropertySequenceable
27
+
28
+ self.loadExposureSequence.return_value = None
29
+ self.loadStageSequence.return_value = None
30
+ self.loadXYStageSequence.return_value = None
31
+ # TODO: add support in pymmcore-nano
32
+ if hasattr(CMMCorePlus, "loadSLMSequence"):
33
+ self.loadSLMSequence.return_value = None
34
+
35
+ self.loadPropertySequence.return_value = None
36
+
37
+ self.startExposureSequence.return_value = None
38
+ self.stopExposureSequence.return_value = None
39
+
40
+ self.startStageSequence.return_value = None
41
+ self.stopStageSequence.return_value = None
42
+
43
+ self.startXYStageSequence.return_value = None
44
+ self.stopXYStageSequence.return_value = None
45
+
46
+ self.startPropertySequence.return_value = None
47
+ self.stopPropertySequence.return_value = None
48
+
49
+ def _isPropertySequenceable(self, dev: str, prop: str) -> bool:
50
+ # subclass to implement more interesting behavior
51
+ return True
@@ -2,11 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import datetime
6
5
  import warnings
7
- from typing import TYPE_CHECKING, Any, Callable, Iterable, Sequence
6
+ from typing import TYPE_CHECKING, Any, Callable
8
7
 
9
8
  from pymmcore_plus import CFGCommand, DeviceType, FocusDirection, Keyword
9
+ from pymmcore_plus._util import timestamp
10
10
 
11
11
  from ._config_group import ConfigGroup, ConfigPreset, Setting
12
12
  from ._device import Device
@@ -15,11 +15,12 @@ from ._pixel_size_config import DEFAULT_AFFINE, PixelSizePreset
15
15
 
16
16
  if TYPE_CHECKING:
17
17
  import io
18
+ from collections.abc import Iterable, Sequence
18
19
  from typing import TypeAlias
19
20
 
20
21
  Executor: TypeAlias = Callable[[Microscope, Sequence[str]], None]
21
22
 
22
- __all__ = ["load_from_string", "dump"]
23
+ __all__ = ["dump", "load_from_string"]
23
24
 
24
25
 
25
26
  def load_from_string(text: str, scope: Microscope | None = None) -> Microscope:
@@ -57,9 +58,7 @@ INIT = _serialize(CFGCommand.Property, Keyword.CoreDevice, Keyword.CoreInitializ
57
58
 
58
59
 
59
60
  def yield_date(scope: Microscope) -> Iterable[str]:
60
- now = datetime.datetime.now(datetime.timezone.utc)
61
- date = now.astimezone().strftime("%a %b %d %H:%M:%S %Z %Y")
62
- yield f"# Date: {date}\n"
61
+ yield f"# Date: {timestamp()}\n"
63
62
 
64
63
 
65
64
  def iter_devices(scope: Microscope) -> Iterable[str]: