pymmcore-plus 0.15.4__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/experimental/unicore/_device_manager.py +20 -13
- pymmcore_plus/experimental/unicore/core/_unicore.py +4 -1
- pymmcore_plus/install.py +149 -18
- pymmcore_plus/mda/_engine.py +268 -73
- pymmcore_plus/metadata/_ome.py +499 -0
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.16.0.dist-info}/METADATA +3 -2
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.16.0.dist-info}/RECORD +20 -18
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.16.0.dist-info}/WHEEL +0 -0
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.16.0.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.16.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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,11 +65,12 @@ 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] = {
|
|
71
75
|
74: "20250815",
|
|
72
76
|
73: "20250318",
|
|
@@ -173,11 +177,19 @@ def _mac_install(dmg: Path, dest: Path, log_msg: _MsgLogger) -> None:
|
|
|
173
177
|
|
|
174
178
|
@cache
|
|
175
179
|
def available_versions() -> dict[str, str]:
|
|
176
|
-
"""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
|
+
"""
|
|
177
189
|
with urlopen(DOWNLOADS_URL) as resp:
|
|
178
190
|
html = resp.read().decode("utf-8")
|
|
179
191
|
|
|
180
|
-
all_links = re.findall(r
|
|
192
|
+
all_links = re.findall(r'href="([^"]+)"', html)
|
|
181
193
|
delim = "_" if PLATFORM == "Windows" else "-"
|
|
182
194
|
return {
|
|
183
195
|
ref.rsplit(delim, 1)[-1].split(".")[0]: BASE_URL + ref
|
|
@@ -186,6 +198,122 @@ def available_versions() -> dict[str, str]:
|
|
|
186
198
|
}
|
|
187
199
|
|
|
188
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
|
+
|
|
189
317
|
def _download_url(url: str, output_path: Path, show_progress: bool = True) -> None:
|
|
190
318
|
"""Download `url` to `output_path` with a nice progress bar."""
|
|
191
319
|
ctx = ssl.create_default_context()
|
|
@@ -219,6 +347,7 @@ def install(
|
|
|
219
347
|
dest: Path | str = USER_DATA_MM_PATH,
|
|
220
348
|
release: str = "latest-compatible",
|
|
221
349
|
log_msg: _MsgLogger = _pretty_print,
|
|
350
|
+
test_adapters: bool = False,
|
|
222
351
|
) -> None:
|
|
223
352
|
"""Install Micro-Manager to `dest`.
|
|
224
353
|
|
|
@@ -236,30 +365,32 @@ def install(
|
|
|
236
365
|
Callback to log messages, must have signature:
|
|
237
366
|
`def logger(text: str, color: str = "", emoji: str = ""): ...`
|
|
238
367
|
May ignore color and emoji.
|
|
368
|
+
test_adapters : bool, optional
|
|
369
|
+
Whether to install test adapters, by default False.
|
|
239
370
|
"""
|
|
371
|
+
if test_adapters:
|
|
372
|
+
_test_dev_install(dest=dest, release=release)
|
|
373
|
+
return
|
|
374
|
+
|
|
240
375
|
if PLATFORM not in ("Darwin", "Windows") or (
|
|
241
376
|
PLATFORM == "Darwin" and MACH == "arm64"
|
|
242
|
-
):
|
|
377
|
+
):
|
|
243
378
|
log_msg(
|
|
244
|
-
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:",
|
|
245
383
|
)
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
"bold yellow",
|
|
249
|
-
":light_bulb:",
|
|
250
|
-
)
|
|
251
|
-
raise sys.exit(1)
|
|
384
|
+
_test_dev_install(dest=dest, release=release)
|
|
385
|
+
return
|
|
252
386
|
|
|
253
387
|
if release == "latest-compatible":
|
|
254
|
-
from pymmcore_plus import _pymmcore
|
|
255
|
-
|
|
256
|
-
div = _pymmcore.version_info.device_interface
|
|
257
388
|
# date when the device interface version FOLLOWING the version that this
|
|
258
389
|
# pymmcore supports was released.
|
|
259
|
-
next_div_date = INTERFACES.get(
|
|
390
|
+
next_div_date = INTERFACES.get(PYMMCORE_DIV + 1, None)
|
|
260
391
|
|
|
261
392
|
# if div is equal to the greatest known interface version, use latest
|
|
262
|
-
if
|
|
393
|
+
if PYMMCORE_DIV == max(INTERFACES.keys()) or next_div_date is None:
|
|
263
394
|
release = "latest"
|
|
264
395
|
else: # pragma: no cover
|
|
265
396
|
# otherwise, find the date of the release in available_versions() that
|
|
@@ -273,7 +404,7 @@ def install(
|
|
|
273
404
|
# fallback to latest if no compatible versions found
|
|
274
405
|
raise ValueError(
|
|
275
406
|
"Unable to find a compatible release for device interface"
|
|
276
|
-
f"{
|
|
407
|
+
f"{PYMMCORE_DIV} at {DOWNLOADS_URL} "
|
|
277
408
|
)
|
|
278
409
|
|
|
279
410
|
if release == "latest":
|