pymmcore-plus 0.15.3__py3-none-any.whl → 0.16.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.
- pymmcore_plus/__init__.py +20 -1
- pymmcore_plus/_accumulator.py +23 -5
- pymmcore_plus/_cli.py +44 -26
- pymmcore_plus/_discovery.py +344 -0
- pymmcore_plus/_logger.py +1 -1
- pymmcore_plus/_util.py +9 -245
- pymmcore_plus/core/_device.py +20 -7
- pymmcore_plus/core/_mmcore_plus.py +16 -8
- pymmcore_plus/core/_property.py +34 -28
- pymmcore_plus/core/events/_device_signal_view.py +8 -1
- pymmcore_plus/core/events/_protocol.py +3 -0
- pymmcore_plus/core/events/_psygnal.py +1 -0
- pymmcore_plus/core/events/_qsignals.py +1 -0
- pymmcore_plus/experimental/unicore/_device_manager.py +20 -13
- pymmcore_plus/experimental/unicore/core/_unicore.py +4 -1
- pymmcore_plus/install.py +150 -18
- pymmcore_plus/mda/_engine.py +268 -73
- pymmcore_plus/mda/events/__init__.py +1 -1
- pymmcore_plus/metadata/_ome.py +499 -0
- {pymmcore_plus-0.15.3.dist-info → pymmcore_plus-0.16.0.dist-info}/METADATA +3 -2
- {pymmcore_plus-0.15.3.dist-info → pymmcore_plus-0.16.0.dist-info}/RECORD +24 -22
- {pymmcore_plus-0.15.3.dist-info → pymmcore_plus-0.16.0.dist-info}/WHEEL +0 -0
- {pymmcore_plus-0.15.3.dist-info → pymmcore_plus-0.16.0.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.15.3.dist-info → pymmcore_plus-0.16.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -23,7 +23,7 @@ logger = logging.getLogger(__name__)
|
|
|
23
23
|
class PyDeviceManager:
|
|
24
24
|
"""Manages loaded Python devices."""
|
|
25
25
|
|
|
26
|
-
__slots__ = ("_devices",)
|
|
26
|
+
__slots__ = ("_devices", "_executor")
|
|
27
27
|
|
|
28
28
|
def __init__(self) -> None:
|
|
29
29
|
self._devices: dict[str, Device] = {}
|
|
@@ -53,9 +53,8 @@ class PyDeviceManager:
|
|
|
53
53
|
|
|
54
54
|
# Initialize all devices in parallel
|
|
55
55
|
with ThreadPoolExecutor() as executor:
|
|
56
|
-
for
|
|
57
|
-
|
|
58
|
-
):
|
|
56
|
+
futures = [executor.submit(self.initialize, label) for label in labels]
|
|
57
|
+
for future in as_completed(futures):
|
|
59
58
|
future.result()
|
|
60
59
|
|
|
61
60
|
def wait_for(
|
|
@@ -75,17 +74,25 @@ class PyDeviceManager:
|
|
|
75
74
|
)
|
|
76
75
|
time.sleep(polling_interval)
|
|
77
76
|
|
|
78
|
-
def wait_for_device_type(
|
|
77
|
+
def wait_for_device_type(
|
|
78
|
+
self, dev_type: int, timeout_ms: float = 5000, *, parallel: bool = True
|
|
79
|
+
) -> None:
|
|
79
80
|
if not (labels := self.get_labels_of_type(dev_type)):
|
|
80
81
|
return # pragma: no cover
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
if not parallel:
|
|
83
|
+
for lbl in labels:
|
|
84
|
+
self.wait_for(lbl, timeout_ms)
|
|
85
|
+
else:
|
|
86
|
+
# Wait for all python devices of the given type in parallel
|
|
87
|
+
# it's critical that this be a list comprehension,
|
|
88
|
+
# not a generator expression, otherwise the executor may be shut down
|
|
89
|
+
# before any tasks are actually submitted
|
|
90
|
+
with ThreadPoolExecutor() as executor:
|
|
91
|
+
futures = [
|
|
92
|
+
executor.submit(self.wait_for, lbl, timeout_ms) for lbl in labels
|
|
93
|
+
]
|
|
94
|
+
for future in as_completed(futures):
|
|
95
|
+
future.result() # Raises any exceptions from wait_for_device
|
|
89
96
|
|
|
90
97
|
def get_initialization_state(self, label: str) -> DeviceInitializationState:
|
|
91
98
|
"""Return the initialization state of the device with the given label."""
|
|
@@ -130,7 +130,10 @@ class UniMMCore(CMMCorePlus):
|
|
|
130
130
|
|
|
131
131
|
def reset(self) -> None:
|
|
132
132
|
with suppress(TimeoutError):
|
|
133
|
-
self.
|
|
133
|
+
self._pydevices.wait_for_device_type(
|
|
134
|
+
DeviceType.AnyType, self.getTimeoutMs(), parallel=False
|
|
135
|
+
)
|
|
136
|
+
super().waitForDeviceType(DeviceType.AnyType)
|
|
134
137
|
self.unloadAllDevices()
|
|
135
138
|
self._pycore.reset_current()
|
|
136
139
|
super().reset()
|
pymmcore_plus/install.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import os
|
|
4
5
|
import re
|
|
5
6
|
import shutil
|
|
@@ -7,6 +8,7 @@ import ssl
|
|
|
7
8
|
import subprocess
|
|
8
9
|
import sys
|
|
9
10
|
import tempfile
|
|
11
|
+
import zipfile
|
|
10
12
|
from contextlib import contextmanager, nullcontext
|
|
11
13
|
from functools import cache
|
|
12
14
|
from pathlib import Path
|
|
@@ -16,6 +18,7 @@ from urllib.request import urlopen, urlretrieve
|
|
|
16
18
|
|
|
17
19
|
import typer
|
|
18
20
|
|
|
21
|
+
from pymmcore_plus import _pymmcore
|
|
19
22
|
from pymmcore_plus._util import USER_DATA_MM_PATH
|
|
20
23
|
|
|
21
24
|
if TYPE_CHECKING:
|
|
@@ -62,12 +65,14 @@ MACH = machine()
|
|
|
62
65
|
BASE_URL = "https://download.micro-manager.org"
|
|
63
66
|
plat = {"Darwin": "Mac", "Windows": "Windows", "Linux": "Linux"}.get(PLATFORM)
|
|
64
67
|
DOWNLOADS_URL = f"{BASE_URL}/nightly/2.0/{plat}/"
|
|
65
|
-
|
|
68
|
+
TEST_ADAPTERS_REPO = "micro-manager/mm-test-adapters"
|
|
69
|
+
PYMMCORE_DIV = _pymmcore.version_info.device_interface
|
|
66
70
|
|
|
67
71
|
# Dates of release for each interface version.
|
|
68
72
|
# generally running `mmcore install -r <some_date>` will bring in devices with
|
|
69
|
-
# the NEW interface.
|
|
73
|
+
# the NEW interface, introduced that day.
|
|
70
74
|
INTERFACES: dict[int, str] = {
|
|
75
|
+
74: "20250815",
|
|
71
76
|
73: "20250318",
|
|
72
77
|
72: "20250318",
|
|
73
78
|
71: "20221031",
|
|
@@ -172,11 +177,19 @@ def _mac_install(dmg: Path, dest: Path, log_msg: _MsgLogger) -> None:
|
|
|
172
177
|
|
|
173
178
|
@cache
|
|
174
179
|
def available_versions() -> dict[str, str]:
|
|
175
|
-
"""Return a map of version -> url available for download.
|
|
180
|
+
"""Return a map of version -> url available for download.
|
|
181
|
+
|
|
182
|
+
Returns a dict like:
|
|
183
|
+
{
|
|
184
|
+
'20220906': 'https://download.micro-manager.org/nightly/2.0/Mac/Micro-Manager-2.0.1-20220906.dmg',
|
|
185
|
+
'20220901': 'https://download.micro-manager.org/nightly/2.0/Mac/Micro-Manager-2.0.1-20220901.dmg',
|
|
186
|
+
'20220823': 'https://download.micro-manager.org/nightly/2.0/Mac/Micro-Manager-2.0.1-20220823.dmg',
|
|
187
|
+
}
|
|
188
|
+
"""
|
|
176
189
|
with urlopen(DOWNLOADS_URL) as resp:
|
|
177
190
|
html = resp.read().decode("utf-8")
|
|
178
191
|
|
|
179
|
-
all_links = re.findall(r
|
|
192
|
+
all_links = re.findall(r'href="([^"]+)"', html)
|
|
180
193
|
delim = "_" if PLATFORM == "Windows" else "-"
|
|
181
194
|
return {
|
|
182
195
|
ref.rsplit(delim, 1)[-1].split(".")[0]: BASE_URL + ref
|
|
@@ -185,6 +198,122 @@ def available_versions() -> dict[str, str]:
|
|
|
185
198
|
}
|
|
186
199
|
|
|
187
200
|
|
|
201
|
+
@cache
|
|
202
|
+
def _available_test_adapter_releases() -> dict[str, str]:
|
|
203
|
+
"""Get available releases from GitHub mm-test-adapters repository.
|
|
204
|
+
|
|
205
|
+
Returns a dict like:
|
|
206
|
+
{
|
|
207
|
+
'20250825': '74.20250825',
|
|
208
|
+
...
|
|
209
|
+
}
|
|
210
|
+
"""
|
|
211
|
+
github_api_url = f"https://api.github.com/repos/{TEST_ADAPTERS_REPO}/releases"
|
|
212
|
+
|
|
213
|
+
with urlopen(github_api_url) as resp:
|
|
214
|
+
releases = json.loads(resp.read().decode("utf-8"))
|
|
215
|
+
|
|
216
|
+
release_map = {}
|
|
217
|
+
for release in releases:
|
|
218
|
+
tag_name = release["tag_name"] # e.g., "74.20250825"
|
|
219
|
+
if "." in tag_name:
|
|
220
|
+
_, date_part = tag_name.split(".", 1) # Extract YYYYMMDD part
|
|
221
|
+
release_map[date_part] = tag_name
|
|
222
|
+
|
|
223
|
+
return release_map
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _get_platform_arch_string() -> str:
|
|
227
|
+
"""Get platform and architecture string for GitHub releases."""
|
|
228
|
+
if PLATFORM == "Darwin":
|
|
229
|
+
arch = "ARM64" if MACH == "arm64" else "X64"
|
|
230
|
+
return f"macOS-{arch}"
|
|
231
|
+
elif PLATFORM == "Windows":
|
|
232
|
+
return "Windows-X64"
|
|
233
|
+
elif PLATFORM == "Linux":
|
|
234
|
+
return "Linux-X64"
|
|
235
|
+
else: # pragma: no cover
|
|
236
|
+
raise ValueError(f"Unsupported platform: {PLATFORM}")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _test_dev_install(
|
|
240
|
+
dest: Path | str = USER_DATA_MM_PATH,
|
|
241
|
+
release: str = "latest",
|
|
242
|
+
) -> None:
|
|
243
|
+
"""Install just the test devices into dest.
|
|
244
|
+
|
|
245
|
+
From https://github.com/micro-manager/mm-test-adapters/releases/latest
|
|
246
|
+
(Releases on github are versioned as: DIV.YYYYMMDD)
|
|
247
|
+
|
|
248
|
+
If release is `latest-compatible`, it will install the latest compatible version
|
|
249
|
+
of the test devices for the current device interface version.
|
|
250
|
+
|
|
251
|
+
Parameters
|
|
252
|
+
----------
|
|
253
|
+
dest : Path | str, optional
|
|
254
|
+
Where to install Micro-Manager, by default will install to a pymmcore-plus
|
|
255
|
+
folder in the user's data directory: `appdirs.user_data_dir()`.
|
|
256
|
+
release : str, optional
|
|
257
|
+
Which release to install, by default "latest-compatible". Should be a date
|
|
258
|
+
in the form YYYYMMDD, "latest" to install the latest nightly release, or
|
|
259
|
+
"latest-compatible" to install the latest nightly release that is
|
|
260
|
+
compatible with the device interface version of the current pymmcore version.
|
|
261
|
+
"""
|
|
262
|
+
dest = Path(dest).expanduser().resolve()
|
|
263
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
264
|
+
|
|
265
|
+
# Get available GitHub releases
|
|
266
|
+
if not (github_releases := _available_test_adapter_releases()): # pragma: no cover
|
|
267
|
+
raise ValueError("No test device releases found on GitHub")
|
|
268
|
+
|
|
269
|
+
# Build download URL
|
|
270
|
+
filename = f"mm-test-adapters-{_get_platform_arch_string()}.zip"
|
|
271
|
+
base_url = f"https://github.com/{TEST_ADAPTERS_REPO}/releases"
|
|
272
|
+
if release == "latest-compatible":
|
|
273
|
+
# Find the latest release compatible with the current device interface
|
|
274
|
+
# this is easier with test devices because their version starts with the DIV
|
|
275
|
+
available = sorted(github_releases.values(), reverse=True)
|
|
276
|
+
for version in available:
|
|
277
|
+
if version.startswith(f"{PYMMCORE_DIV}."):
|
|
278
|
+
# Found a compatible version
|
|
279
|
+
download_url = f"{base_url}/download/{version}/{filename}"
|
|
280
|
+
tag_name = version
|
|
281
|
+
break
|
|
282
|
+
else: # pragma: no cover
|
|
283
|
+
raise ValueError(
|
|
284
|
+
f"No compatible releases found for device interface {PYMMCORE_DIV}. "
|
|
285
|
+
f"Found: {available}"
|
|
286
|
+
)
|
|
287
|
+
elif release == "latest":
|
|
288
|
+
download_url = f"{base_url}/latest/download/{filename}"
|
|
289
|
+
tag_name = sorted(github_releases.values(), reverse=True)[0]
|
|
290
|
+
else:
|
|
291
|
+
if release not in github_releases:
|
|
292
|
+
_available = ", ".join(sorted(github_releases.keys(), reverse=True))
|
|
293
|
+
raise ValueError(
|
|
294
|
+
f"Release {release!r} not found. Available releases: {_available}"
|
|
295
|
+
)
|
|
296
|
+
tag_name = github_releases[release]
|
|
297
|
+
download_url = f"{base_url}/download/{tag_name}/{filename}"
|
|
298
|
+
|
|
299
|
+
_dest = dest / f"Micro-Manager-{tag_name}"
|
|
300
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
301
|
+
# Download the zip file
|
|
302
|
+
zip_path = Path(tmpdir) / filename
|
|
303
|
+
_download_url(
|
|
304
|
+
url=download_url, output_path=zip_path, show_progress=progress is not None
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Extract the zip file
|
|
308
|
+
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
|
309
|
+
zip_ref.extractall(_dest)
|
|
310
|
+
|
|
311
|
+
# On macOS, remove quarantine attribute
|
|
312
|
+
if PLATFORM == "Darwin":
|
|
313
|
+
cmd = ["xattr", "-r", "-d", "com.apple.quarantine", str(_dest)]
|
|
314
|
+
subprocess.run(cmd, check=False) # Don't fail if xattr fails
|
|
315
|
+
|
|
316
|
+
|
|
188
317
|
def _download_url(url: str, output_path: Path, show_progress: bool = True) -> None:
|
|
189
318
|
"""Download `url` to `output_path` with a nice progress bar."""
|
|
190
319
|
ctx = ssl.create_default_context()
|
|
@@ -218,6 +347,7 @@ def install(
|
|
|
218
347
|
dest: Path | str = USER_DATA_MM_PATH,
|
|
219
348
|
release: str = "latest-compatible",
|
|
220
349
|
log_msg: _MsgLogger = _pretty_print,
|
|
350
|
+
test_adapters: bool = False,
|
|
221
351
|
) -> None:
|
|
222
352
|
"""Install Micro-Manager to `dest`.
|
|
223
353
|
|
|
@@ -235,30 +365,32 @@ def install(
|
|
|
235
365
|
Callback to log messages, must have signature:
|
|
236
366
|
`def logger(text: str, color: str = "", emoji: str = ""): ...`
|
|
237
367
|
May ignore color and emoji.
|
|
368
|
+
test_adapters : bool, optional
|
|
369
|
+
Whether to install test adapters, by default False.
|
|
238
370
|
"""
|
|
371
|
+
if test_adapters:
|
|
372
|
+
_test_dev_install(dest=dest, release=release)
|
|
373
|
+
return
|
|
374
|
+
|
|
239
375
|
if PLATFORM not in ("Darwin", "Windows") or (
|
|
240
376
|
PLATFORM == "Darwin" and MACH == "arm64"
|
|
241
|
-
):
|
|
377
|
+
):
|
|
242
378
|
log_msg(
|
|
243
|
-
f"Unsupported platform/architecture: {PLATFORM}/{MACH}"
|
|
379
|
+
f"Unsupported platform/architecture for nightly build: {PLATFORM}/{MACH}\n"
|
|
380
|
+
" (Downloading just test adapters) ...",
|
|
381
|
+
"bold magenta",
|
|
382
|
+
":exclamation:",
|
|
244
383
|
)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
"bold yellow",
|
|
248
|
-
":light_bulb:",
|
|
249
|
-
)
|
|
250
|
-
raise sys.exit(1)
|
|
384
|
+
_test_dev_install(dest=dest, release=release)
|
|
385
|
+
return
|
|
251
386
|
|
|
252
387
|
if release == "latest-compatible":
|
|
253
|
-
from pymmcore_plus import _pymmcore
|
|
254
|
-
|
|
255
|
-
div = _pymmcore.version_info.device_interface
|
|
256
388
|
# date when the device interface version FOLLOWING the version that this
|
|
257
389
|
# pymmcore supports was released.
|
|
258
|
-
next_div_date = INTERFACES.get(
|
|
390
|
+
next_div_date = INTERFACES.get(PYMMCORE_DIV + 1, None)
|
|
259
391
|
|
|
260
392
|
# if div is equal to the greatest known interface version, use latest
|
|
261
|
-
if
|
|
393
|
+
if PYMMCORE_DIV == max(INTERFACES.keys()) or next_div_date is None:
|
|
262
394
|
release = "latest"
|
|
263
395
|
else: # pragma: no cover
|
|
264
396
|
# otherwise, find the date of the release in available_versions() that
|
|
@@ -272,7 +404,7 @@ def install(
|
|
|
272
404
|
# fallback to latest if no compatible versions found
|
|
273
405
|
raise ValueError(
|
|
274
406
|
"Unable to find a compatible release for device interface"
|
|
275
|
-
f"{
|
|
407
|
+
f"{PYMMCORE_DIV} at {DOWNLOADS_URL} "
|
|
276
408
|
)
|
|
277
409
|
|
|
278
410
|
if release == "latest":
|