pymmcore-plus 0.10.2__py3-none-any.whl → 0.11.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 (33) hide show
  1. pymmcore_plus/__init__.py +4 -1
  2. pymmcore_plus/_build.py +2 -0
  3. pymmcore_plus/_cli.py +47 -12
  4. pymmcore_plus/_util.py +99 -9
  5. pymmcore_plus/core/__init__.py +2 -0
  6. pymmcore_plus/core/_constants.py +109 -8
  7. pymmcore_plus/core/_mmcore_plus.py +67 -47
  8. pymmcore_plus/mda/__init__.py +2 -2
  9. pymmcore_plus/mda/_engine.py +148 -98
  10. pymmcore_plus/mda/_protocol.py +5 -3
  11. pymmcore_plus/mda/_runner.py +16 -21
  12. pymmcore_plus/mda/events/_protocol.py +10 -2
  13. pymmcore_plus/mda/handlers/_5d_writer_base.py +25 -13
  14. pymmcore_plus/mda/handlers/_img_sequence_writer.py +9 -5
  15. pymmcore_plus/mda/handlers/_ome_tiff_writer.py +7 -3
  16. pymmcore_plus/mda/handlers/_ome_zarr_writer.py +9 -4
  17. pymmcore_plus/mda/handlers/_tensorstore_handler.py +19 -19
  18. pymmcore_plus/metadata/__init__.py +36 -0
  19. pymmcore_plus/metadata/functions.py +343 -0
  20. pymmcore_plus/metadata/schema.py +471 -0
  21. pymmcore_plus/metadata/serialize.py +116 -0
  22. pymmcore_plus/model/_config_file.py +2 -4
  23. pymmcore_plus/model/_config_group.py +29 -3
  24. pymmcore_plus/model/_device.py +20 -1
  25. pymmcore_plus/model/_microscope.py +35 -1
  26. pymmcore_plus/model/_pixel_size_config.py +25 -3
  27. {pymmcore_plus-0.10.2.dist-info → pymmcore_plus-0.11.0.dist-info}/METADATA +4 -3
  28. pymmcore_plus-0.11.0.dist-info/RECORD +59 -0
  29. {pymmcore_plus-0.10.2.dist-info → pymmcore_plus-0.11.0.dist-info}/WHEEL +1 -1
  30. pymmcore_plus/core/_state.py +0 -244
  31. pymmcore_plus-0.10.2.dist-info/RECORD +0 -56
  32. {pymmcore_plus-0.10.2.dist-info → pymmcore_plus-0.11.0.dist-info}/entry_points.txt +0 -0
  33. {pymmcore_plus-0.10.2.dist-info → pymmcore_plus-0.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,471 @@
1
+ from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union
2
+
3
+ import useq
4
+ from typing_extensions import NotRequired
5
+
6
+ __all__ = [
7
+ "FrameMetaV1",
8
+ "SummaryMetaV1",
9
+ "ConfigGroup",
10
+ "ConfigPreset",
11
+ "DeviceInfo",
12
+ "ImageInfo",
13
+ "PixelSizeConfigPreset",
14
+ "Position",
15
+ "PropertyInfo",
16
+ "PropertyValue",
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
+ position: Position
434
+ The current stage position(s) in 3D space.
435
+ property_values: tuple[PropertyValue, ...]
436
+ Device property settings. This is not a comprehensive list of all device
437
+ properties, but only those that may have changed for this frame (such as
438
+ properties in the channel config or light path config).
439
+ runner_time_ms: float
440
+ Elapsed time in milliseconds since the beginning of the MDA sequence.
441
+ mda_event: useq.MDAEvent
442
+ *NotRequired*. The MDA event object that commanded the acquisition of this
443
+ frame.
444
+ hardware_triggered: bool
445
+ *NotRequired*. Whether the frame was part of a hardware-triggered sequence.
446
+ If missing, assume `False`.
447
+ images_remaining_in_buffer: int
448
+ *NotRequired*. The number of images remaining to be popped from the image
449
+ buffer (only applicable for hardware-triggered sequences).
450
+ camera_metadata: dict[str, Any]
451
+ *NotRequired*. Additional metadata from the camera device. This is unstructured
452
+ and may contain any information that the camera device provides. Do not rely
453
+ on the presence of any particular keys.
454
+ extra: dict[str, Any]
455
+ *NotRequired*. Additional information, may be used to store arbitrary user info
456
+ or additional metadata.
457
+ """
458
+
459
+ format: Literal["frame-dict"]
460
+ version: Literal["1.0"]
461
+ pixel_size_um: float
462
+ camera_device: Optional[str]
463
+ exposure_ms: float
464
+ position: Position
465
+ property_values: Tuple[PropertyValue, ...]
466
+ runner_time_ms: float
467
+ mda_event: NotRequired[useq.MDAEvent]
468
+ hardware_triggered: NotRequired[bool]
469
+ images_remaining_in_buffer: NotRequired[int]
470
+ camera_metadata: NotRequired[Dict[str, Any]]
471
+ extra: NotRequired[Dict[str, Any]]
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from contextlib import suppress
6
+ from datetime import timedelta
7
+ from types import MappingProxyType
8
+ from typing import TYPE_CHECKING, Any, Mapping, Sequence
9
+
10
+ import numpy as np
11
+
12
+ try:
13
+ # use msgspec if available
14
+ import msgspec
15
+ except ImportError:
16
+ msgspec = None
17
+
18
+
19
+ if TYPE_CHECKING:
20
+ import pydantic # noqa: F401
21
+
22
+
23
+ def encode_hook(obj: Any, raises: bool = True) -> Any:
24
+ """Hook to encode objects that are not JSON serializable."""
25
+ if not TYPE_CHECKING:
26
+ pydantic = sys.modules.get("pydantic")
27
+ if pydantic and isinstance(obj, pydantic.BaseModel):
28
+ try:
29
+ return obj.model_dump(mode="json", exclude_unset=True)
30
+ except (AttributeError, TypeError):
31
+ return to_builtins(obj.dict(exclude_unset=True))
32
+ if isinstance(obj, MappingProxyType):
33
+ return dict(obj)
34
+ if isinstance(obj, np.number):
35
+ return obj.item()
36
+ if isinstance(obj, timedelta):
37
+ return obj.total_seconds()
38
+ if raises:
39
+ raise NotImplementedError(f"Cannot serialize object of type {type(obj)}")
40
+ return obj
41
+
42
+
43
+ # def decode_hook(type: type, obj: Any) -> Any:
44
+ # """Hook to decode objects that are not JSON deserializable."""
45
+ # if not TYPE_CHECKING:
46
+ # pydantic = sys.modules.get("pydantic")
47
+ # if pydantic:
48
+ # with suppress(TypeError):
49
+ # if issubclass(type, pydantic.BaseModel):
50
+ # return type.model_validate(obj)
51
+ # raise NotImplementedError(f"Cannot deserialize object of type {type}")
52
+
53
+
54
+ def schema_hook(obj: type) -> dict[str, Any]:
55
+ """Hook to convert objects to schema."""
56
+ if not TYPE_CHECKING:
57
+ pydantic = sys.modules.get("pydantic")
58
+ if pydantic:
59
+ with suppress(TypeError):
60
+ if issubclass(obj, pydantic.BaseModel):
61
+ return obj.model_json_schema()
62
+ raise NotImplementedError(f"Cannot create schema for object of type {type(obj)}")
63
+
64
+
65
+ def msgspec_json_dumps(obj: Any, *, indent: int | None = None) -> bytes:
66
+ """Serialize object to bytes."""
67
+ encoded = msgspec.json.encode(obj, enc_hook=encode_hook)
68
+ if indent is not None:
69
+ encoded = msgspec.json.format(encoded, indent=indent)
70
+ return encoded # type: ignore [no-any-return]
71
+
72
+
73
+ def msgspec_json_loads(s: bytes | str) -> Any:
74
+ """Deserialize bytes to object."""
75
+ return msgspec.json.decode(s)
76
+
77
+
78
+ def msgspec_to_builtins(obj: Any) -> Any:
79
+ """Convert object to built-in types."""
80
+ return msgspec.to_builtins(obj, enc_hook=encode_hook)
81
+
82
+
83
+ def msgspec_to_schema(type: Any) -> Any:
84
+ """Generate JSON schema for a given type."""
85
+ if msgspec is None: # pragma: no cover
86
+ raise ImportError("msgspec is required for this function")
87
+ return msgspec.json.schema(type, schema_hook=schema_hook)
88
+
89
+
90
+ def std_json_dumps(obj: Any, *, indent: int | None = None) -> bytes:
91
+ """Serialize object to bytes."""
92
+ return json.dumps(obj, default=std_to_builtins, indent=indent).encode("utf-8")
93
+
94
+
95
+ def std_json_loads(s: bytes | str) -> Any:
96
+ """Deserialize bytes to object."""
97
+ return json.loads(s)
98
+
99
+
100
+ def std_to_builtins(obj: Any) -> Any:
101
+ """Convert object to built-in types."""
102
+ if isinstance(obj, Mapping):
103
+ return {k: std_to_builtins(v) for k, v in obj.items()}
104
+ if isinstance(obj, Sequence) and not isinstance(obj, str):
105
+ return [std_to_builtins(v) for v in obj]
106
+ return encode_hook(obj, raises=False)
107
+
108
+
109
+ if msgspec is None: # pragma: no cover
110
+ json_dumps = std_json_dumps
111
+ json_loads = std_json_loads
112
+ to_builtins = std_to_builtins
113
+ else:
114
+ json_dumps = msgspec_json_dumps
115
+ json_loads = msgspec_json_loads
116
+ to_builtins = msgspec_to_builtins
@@ -2,11 +2,11 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import datetime
6
5
  import warnings
7
6
  from typing import TYPE_CHECKING, Any, Callable, Iterable, Sequence
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
@@ -57,9 +57,7 @@ INIT = _serialize(CFGCommand.Property, Keyword.CoreDevice, Keyword.CoreInitializ
57
57
 
58
58
 
59
59
  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"
60
+ yield f"# Date: {timestamp()}\n"
63
61
 
64
62
 
65
63
  def iter_devices(scope: Microscope) -> Iterable[str]:
@@ -1,12 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass, field
4
- from typing import TYPE_CHECKING, Container, MutableMapping, NamedTuple
4
+ from typing import TYPE_CHECKING, NamedTuple
5
5
 
6
6
  if TYPE_CHECKING:
7
- from typing import Final
7
+ from typing import Container, Final, MutableMapping
8
+
9
+ from typing_extensions import Self # py311
8
10
 
9
11
  from pymmcore_plus import CMMCorePlus
12
+ from pymmcore_plus.metadata.schema import ConfigGroup as ConfigGroupMeta
13
+ from pymmcore_plus.metadata.schema import ConfigPreset as ConfigPresetMeta
10
14
 
11
15
  from ._core_link import ErrCallback
12
16
 
@@ -37,6 +41,20 @@ class ConfigPreset:
37
41
  name: str
38
42
  settings: list[Setting] = field(default_factory=list)
39
43
 
44
+ @classmethod
45
+ def from_metadata(cls, meta: ConfigPresetMeta) -> Self:
46
+ return cls(
47
+ name=meta["name"],
48
+ settings=[
49
+ Setting(
50
+ device_name=d["dev"],
51
+ property_name=d["prop"],
52
+ property_value=d["val"],
53
+ )
54
+ for d in meta["settings"]
55
+ ],
56
+ )
57
+
40
58
 
41
59
  @dataclass
42
60
  class ConfigGroup:
@@ -46,7 +64,15 @@ class ConfigGroup:
46
64
  presets: MutableMapping[str, ConfigPreset] = field(default_factory=dict)
47
65
 
48
66
  @classmethod
49
- def create_from_core(cls, core: CMMCorePlus, name: str) -> ConfigGroup:
67
+ def from_metadata(cls, meta: ConfigGroupMeta) -> Self:
68
+ presets = {
69
+ preset["name"]: ConfigPreset.from_metadata(preset)
70
+ for preset in meta["presets"]
71
+ }
72
+ return cls(name=meta["name"], presets=presets)
73
+
74
+ @classmethod
75
+ def create_from_core(cls, core: CMMCorePlus, name: str) -> Self:
50
76
  obj = cls(name=name)
51
77
  obj.update_from_core(core)
52
78
  return obj
@@ -14,7 +14,12 @@ from ._property import Property
14
14
  if TYPE_CHECKING:
15
15
  from typing import Any, Callable, Container, Iterable
16
16
 
17
- from typing_extensions import TypeAlias # py310
17
+ from typing_extensions import (
18
+ Self, # py311
19
+ TypeAlias, # py310
20
+ )
21
+
22
+ from pymmcore_plus.metadata.schema import DeviceInfo
18
23
 
19
24
  from ._core_link import ErrCallback
20
25
  from ._microscope import Microscope
@@ -84,6 +89,20 @@ class Device(CoreObject):
84
89
  # from the same library that can be loaded into this hub.
85
90
  children: tuple[str, ...] = field(default_factory=tuple)
86
91
 
92
+ @classmethod
93
+ def from_metadata(cls, metadata: DeviceInfo) -> Self:
94
+ return cls(
95
+ name=metadata["label"],
96
+ library=metadata["library"],
97
+ adapter_name=metadata["name"],
98
+ description=metadata["description"],
99
+ device_type=DeviceType[metadata["type"]],
100
+ parent_label=metadata.get("parent_label") or "",
101
+ labels=tuple(metadata.get("labels", [])),
102
+ focus_direction=FocusDirection[metadata.get("focus_direction", "Unknown")],
103
+ children=tuple(metadata.get("child_names", [])),
104
+ )
105
+
87
106
  def __post_init__(self) -> None:
88
107
  if self.name == Keyword.CoreDevice or self.device_type == DeviceType.Core:
89
108
  raise ValueError(