pymmcore-plus 0.10.2__tar.gz → 0.11.0__tar.gz
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-0.10.2 → pymmcore_plus-0.11.0}/PKG-INFO +4 -3
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/README.md +1 -2
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/pyproject.toml +2 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/__init__.py +4 -1
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/_build.py +2 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/_cli.py +47 -12
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/_util.py +99 -9
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/__init__.py +2 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/_constants.py +109 -8
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/_mmcore_plus.py +67 -47
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/_engine.py +148 -98
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/_protocol.py +5 -3
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/_runner.py +16 -21
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/events/_protocol.py +10 -2
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/handlers/_5d_writer_base.py +25 -13
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/handlers/_img_sequence_writer.py +9 -5
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/handlers/_ome_tiff_writer.py +7 -3
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/handlers/_ome_zarr_writer.py +9 -4
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/handlers/_tensorstore_handler.py +19 -19
- pymmcore_plus-0.11.0/src/pymmcore_plus/metadata/__init__.py +36 -0
- pymmcore_plus-0.11.0/src/pymmcore_plus/metadata/functions.py +343 -0
- pymmcore_plus-0.11.0/src/pymmcore_plus/metadata/schema.py +471 -0
- pymmcore_plus-0.11.0/src/pymmcore_plus/metadata/serialize.py +116 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/model/_config_file.py +2 -4
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/model/_config_group.py +29 -3
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/model/_device.py +20 -1
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/model/_microscope.py +35 -1
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/model/_pixel_size_config.py +25 -3
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/conftest.py +5 -5
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/io/test_zarr_writers.py +26 -2
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/test_bench.py +14 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/test_cli.py +32 -4
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/test_core.py +17 -32
- pymmcore_plus-0.11.0/tests/test_metadata.py +122 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/test_model.py +11 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/test_sequencing.py +11 -2
- pymmcore_plus-0.10.2/src/pymmcore_plus/core/_state.py +0 -244
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/.gitignore +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/LICENSE +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/_logger.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/_adapter.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/_config.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/_config_group.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/_device.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/_metadata.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/_property.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/_sequencing.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/events/__init__.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/events/_device_signal_view.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/events/_norm_slot.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/events/_prop_event_mixin.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/events/_protocol.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/events/_psygnal.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/core/events/_qsignals.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/install.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/__init__.py +2 -2
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/_thread_relay.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/events/__init__.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/events/_psygnal.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/events/_qsignals.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/handlers/__init__.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/mda/handlers/_util.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/model/__init__.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/model/_core_device.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/model/_core_link.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/model/_property.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/py.typed +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/src/pymmcore_plus/seq_tester.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/__init__.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/io/test_image_sequence_writer.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/io/test_ome_tiff.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/local_config.cfg +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/test_adapter_class.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/test_config_group_class.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/test_device_class.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/test_events.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/test_mda.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/test_misc.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/test_pixel_config_class.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/test_property_class.py +0 -0
- {pymmcore_plus-0.10.2 → pymmcore_plus-0.11.0}/tests/test_thread_relay.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pymmcore-plus
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: pymmcore superset providing improved APIs, event handling, and a pure python acquisition engine
|
|
5
5
|
Project-URL: Source, https://github.com/pymmcore-plus/pymmcore-plus
|
|
6
6
|
Project-URL: Tracker, https://github.com/pymmcore-plus/pymmcore-plus/issues
|
|
@@ -47,6 +47,7 @@ Requires-Dist: ruff; extra == 'dev'
|
|
|
47
47
|
Requires-Dist: tensorstore-stubs; extra == 'dev'
|
|
48
48
|
Provides-Extra: docs
|
|
49
49
|
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
50
|
+
Requires-Dist: mkdocs-typer==0.0.3; extra == 'docs'
|
|
50
51
|
Requires-Dist: mkdocs>=1.4; extra == 'docs'
|
|
51
52
|
Requires-Dist: mkdocstrings-python==1.1.2; extra == 'docs'
|
|
52
53
|
Requires-Dist: mkdocstrings==0.22.0; extra == 'docs'
|
|
@@ -55,6 +56,7 @@ Requires-Dist: tifffile>=2021.6.14; extra == 'io'
|
|
|
55
56
|
Requires-Dist: zarr>=2.2; extra == 'io'
|
|
56
57
|
Provides-Extra: test
|
|
57
58
|
Requires-Dist: msgpack; extra == 'test'
|
|
59
|
+
Requires-Dist: msgspec; extra == 'test'
|
|
58
60
|
Requires-Dist: pytest-cov>=4; extra == 'test'
|
|
59
61
|
Requires-Dist: pytest-qt>=4; extra == 'test'
|
|
60
62
|
Requires-Dist: pytest>=7.3.2; extra == 'test'
|
|
@@ -143,8 +145,7 @@ provides a python API to control the C++ core directly, without the need for
|
|
|
143
145
|
Java in the loop. Each has its own advantages and disadvantages! With
|
|
144
146
|
pycro-manager you immediately get the entire existing micro-manager ecosystem
|
|
145
147
|
and GUI application. With pymmcore-plus you don't need to install Java, and you
|
|
146
|
-
have direct access to the memory buffers used by the C++ core
|
|
147
|
-
side of things is far less mature.
|
|
148
|
+
have direct access to the memory buffers used by the C++ core.
|
|
148
149
|
|
|
149
150
|
## Quickstart
|
|
150
151
|
|
|
@@ -75,8 +75,7 @@ provides a python API to control the C++ core directly, without the need for
|
|
|
75
75
|
Java in the loop. Each has its own advantages and disadvantages! With
|
|
76
76
|
pycro-manager you immediately get the entire existing micro-manager ecosystem
|
|
77
77
|
and GUI application. With pymmcore-plus you don't need to install Java, and you
|
|
78
|
-
have direct access to the memory buffers used by the C++ core
|
|
79
|
-
side of things is far less mature.
|
|
78
|
+
have direct access to the memory buffers used by the C++ core.
|
|
80
79
|
|
|
81
80
|
## Quickstart
|
|
82
81
|
|
|
@@ -54,6 +54,7 @@ dependencies = [
|
|
|
54
54
|
cli = ["typer >=0.4.2", "rich >=10.2.0"]
|
|
55
55
|
io = ["tifffile >=2021.6.14", "zarr >=2.2"]
|
|
56
56
|
test = [
|
|
57
|
+
"msgspec",
|
|
57
58
|
"msgpack",
|
|
58
59
|
"pytest-cov >=4",
|
|
59
60
|
"pytest-qt >=4",
|
|
@@ -71,6 +72,7 @@ docs = [
|
|
|
71
72
|
"mkdocs-material",
|
|
72
73
|
"mkdocstrings ==0.22.0",
|
|
73
74
|
"mkdocstrings-python ==1.1.2",
|
|
75
|
+
"mkdocs-typer ==0.0.3"
|
|
74
76
|
# "griffe @ git+https://github.com/tlambert03/griffe@recursion"
|
|
75
77
|
]
|
|
76
78
|
|
|
@@ -9,7 +9,7 @@ except PackageNotFoundError: # pragma: no cover
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
from ._logger import configure_logging
|
|
12
|
-
from ._util import find_micromanager
|
|
12
|
+
from ._util import find_micromanager, use_micromanager
|
|
13
13
|
from .core import (
|
|
14
14
|
CFGCommand,
|
|
15
15
|
CFGGroup,
|
|
@@ -26,6 +26,7 @@ from .core import (
|
|
|
26
26
|
FocusDirection,
|
|
27
27
|
Keyword,
|
|
28
28
|
Metadata,
|
|
29
|
+
PixelFormat,
|
|
29
30
|
PortType,
|
|
30
31
|
PropertyType,
|
|
31
32
|
)
|
|
@@ -55,6 +56,8 @@ __all__ = [
|
|
|
55
56
|
"Keyword",
|
|
56
57
|
"Metadata",
|
|
57
58
|
"PCoreSignaler",
|
|
59
|
+
"PixelFormat",
|
|
58
60
|
"PortType",
|
|
59
61
|
"PropertyType",
|
|
62
|
+
"use_micromanager",
|
|
60
63
|
]
|
|
@@ -18,11 +18,12 @@ except ImportError: # pragma: no cover
|
|
|
18
18
|
) from None
|
|
19
19
|
|
|
20
20
|
import pymmcore_plus
|
|
21
|
+
from pymmcore_plus._build import DEFAULT_PACKAGES, build
|
|
21
22
|
from pymmcore_plus._logger import configure_logging
|
|
22
23
|
from pymmcore_plus._util import USER_DATA_MM_PATH
|
|
23
24
|
from pymmcore_plus.install import PLATFORM
|
|
24
25
|
|
|
25
|
-
app = typer.Typer(no_args_is_help=True)
|
|
26
|
+
app = typer.Typer(name="mmcore", no_args_is_help=True)
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def _show_version_and_exit(value: bool) -> None:
|
|
@@ -47,7 +48,7 @@ def _main(
|
|
|
47
48
|
) -> None:
|
|
48
49
|
"""mmcore: pymmcore-plus command line (v{version}).
|
|
49
50
|
|
|
50
|
-
For additional help on a specific command: type
|
|
51
|
+
For additional help on a specific command: type `mmcore [command] --help`
|
|
51
52
|
"""
|
|
52
53
|
# fix for windows CI encoding and emoji printing
|
|
53
54
|
if getattr(sys.stdout, "encoding", None) != "utf-8":
|
|
@@ -55,10 +56,13 @@ def _main(
|
|
|
55
56
|
sys.stdout.reconfigure(encoding="utf-8") # type: ignore [attr-defined]
|
|
56
57
|
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
(_main.__doc__ or "").
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
if "mkdocs" in sys.argv[0]: # pragma: no cover
|
|
60
|
+
_main.__doc__ = (_main.__doc__ or "").replace(" (v{version})", "")
|
|
61
|
+
else:
|
|
62
|
+
_main.__doc__ = typer.style(
|
|
63
|
+
(_main.__doc__ or "").format(version=pymmcore_plus.__version__),
|
|
64
|
+
fg=typer.colors.BRIGHT_YELLOW,
|
|
65
|
+
)
|
|
62
66
|
|
|
63
67
|
|
|
64
68
|
@app.command()
|
|
@@ -91,7 +95,8 @@ def _list() -> None:
|
|
|
91
95
|
print(f":file_folder:[bold green] {parent}")
|
|
92
96
|
for item in items:
|
|
93
97
|
bullet = " [bold yellow]*" if first else " •"
|
|
94
|
-
|
|
98
|
+
using = " [bold blue](active)" if first else ""
|
|
99
|
+
print(f"{bullet} [cyan]{item}{using}")
|
|
95
100
|
first = False
|
|
96
101
|
else:
|
|
97
102
|
print(":x: [bold red]There are no pymmcore-plus Micro-Manager files.")
|
|
@@ -100,7 +105,11 @@ def _list() -> None:
|
|
|
100
105
|
|
|
101
106
|
@app.command()
|
|
102
107
|
def mmstudio() -> None: # pragma: no cover
|
|
103
|
-
"""Run the Java Micro-Manager GUI.
|
|
108
|
+
"""Run the Java Micro-Manager GUI.
|
|
109
|
+
|
|
110
|
+
This command will attempt to locate an execute an ImageJ application found in
|
|
111
|
+
the active Micro-Manager directory.
|
|
112
|
+
"""
|
|
104
113
|
mm = pymmcore_plus.find_micromanager()
|
|
105
114
|
app = (
|
|
106
115
|
next((x for x in Path(mm).glob("ImageJ*") if not str(x).endswith("cfg")), None)
|
|
@@ -137,7 +146,7 @@ def install(
|
|
|
137
146
|
show_default=False,
|
|
138
147
|
),
|
|
139
148
|
) -> None:
|
|
140
|
-
"""Install Micro-Manager Device adapters."""
|
|
149
|
+
"""Install Micro-Manager Device adapters from <https://download.micro-manager.org>."""
|
|
141
150
|
import pymmcore_plus.install
|
|
142
151
|
|
|
143
152
|
if plain_output:
|
|
@@ -286,7 +295,7 @@ def run(
|
|
|
286
295
|
@app.command()
|
|
287
296
|
def build_dev(
|
|
288
297
|
devices: Optional[List[str]] = typer.Argument(
|
|
289
|
-
None, help="Device adapters to build. Defaults to
|
|
298
|
+
None, help=f"Device adapters to build. Defaults to {DEFAULT_PACKAGES}"
|
|
290
299
|
),
|
|
291
300
|
dest: Path = typer.Option(
|
|
292
301
|
USER_DATA_MM_PATH,
|
|
@@ -305,9 +314,10 @@ def build_dev(
|
|
|
305
314
|
"If not specified, will prompt.",
|
|
306
315
|
),
|
|
307
316
|
) -> None: # pragma: no cover
|
|
308
|
-
"""Build
|
|
309
|
-
from pymmcore_plus._build import DEFAULT_PACKAGES, build
|
|
317
|
+
"""Build Micro-Manager device adapters from the git repo.
|
|
310
318
|
|
|
319
|
+
Currently only supports macos and linux.
|
|
320
|
+
"""
|
|
311
321
|
devices = DEFAULT_PACKAGES if not devices else devices
|
|
312
322
|
try:
|
|
313
323
|
build(dest, overwrite=overwrite, devices=devices)
|
|
@@ -374,6 +384,31 @@ def info() -> None:
|
|
|
374
384
|
typer.secho(f"{key:{length}}: {value}")
|
|
375
385
|
|
|
376
386
|
|
|
387
|
+
@app.command()
|
|
388
|
+
def use(
|
|
389
|
+
pattern: str = typer.Argument(
|
|
390
|
+
...,
|
|
391
|
+
help="Path to an existing directory, or pattern to match against installations "
|
|
392
|
+
"found by `mmcore list`",
|
|
393
|
+
),
|
|
394
|
+
) -> None:
|
|
395
|
+
"""Change the currently used Micro-manager version/path."""
|
|
396
|
+
from pymmcore_plus._util import use_micromanager
|
|
397
|
+
|
|
398
|
+
_pth = Path(pattern)
|
|
399
|
+
if _pth.exists():
|
|
400
|
+
if not _pth.is_dir():
|
|
401
|
+
raise typer.BadParameter("must be a directory")
|
|
402
|
+
result = use_micromanager(path=_pth)
|
|
403
|
+
else:
|
|
404
|
+
try:
|
|
405
|
+
result = use_micromanager(pattern=pattern)
|
|
406
|
+
except FileNotFoundError as e:
|
|
407
|
+
raise typer.BadParameter(str(e)) from None
|
|
408
|
+
|
|
409
|
+
typer.secho(f"using {result}", fg=typer.colors.BRIGHT_GREEN)
|
|
410
|
+
|
|
411
|
+
|
|
377
412
|
def _tail_file(file_path: Union[str, Path], interval: float = 0.1) -> None:
|
|
378
413
|
with open(file_path) as file:
|
|
379
414
|
# Move the file pointer to the end
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import datetime
|
|
3
4
|
import importlib
|
|
4
5
|
import os
|
|
5
6
|
import platform
|
|
7
|
+
import re
|
|
6
8
|
import sys
|
|
7
9
|
import warnings
|
|
8
10
|
from collections import defaultdict
|
|
@@ -16,6 +18,7 @@ from typing import TYPE_CHECKING, cast, overload
|
|
|
16
18
|
from platformdirs import user_data_dir
|
|
17
19
|
|
|
18
20
|
if TYPE_CHECKING:
|
|
21
|
+
from re import Pattern
|
|
19
22
|
from typing import Any, Callable, Iterator, Literal, TypeVar
|
|
20
23
|
|
|
21
24
|
QtConnectionType = Literal["AutoConnection", "DirectConnection", "QueuedConnection"]
|
|
@@ -40,6 +43,7 @@ __all__ = ["find_micromanager", "retry", "no_stdout", "signals_backend"]
|
|
|
40
43
|
APP_NAME = "pymmcore-plus"
|
|
41
44
|
USER_DATA_DIR = Path(user_data_dir(appname=APP_NAME))
|
|
42
45
|
USER_DATA_MM_PATH = USER_DATA_DIR / "mm"
|
|
46
|
+
CURRENT_MM_PATH = USER_DATA_MM_PATH / ".current_mm"
|
|
43
47
|
PYMMCORE_PLUS_PATH = Path(__file__).parent.parent
|
|
44
48
|
|
|
45
49
|
|
|
@@ -57,16 +61,17 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
|
57
61
|
In order, this will look for:
|
|
58
62
|
|
|
59
63
|
1. An environment variable named `MICROMANAGER_PATH`
|
|
60
|
-
2. A
|
|
64
|
+
2. A path stored in the `CURRENT_MM_PATH` file (set by `use_micromanager`).
|
|
65
|
+
3. A `Micro-Manager*` folder in the `pymmcore-plus` user data directory
|
|
61
66
|
(this is the default install location when running `mmcore install`)
|
|
62
67
|
|
|
63
68
|
- **Windows**: C:\Users\\[user]\AppData\Local\pymmcore-plus\pymmcore-plus
|
|
64
69
|
- **macOS**: ~/Library/Application Support/pymmcore-plus
|
|
65
70
|
- **Linux**: ~/.local/share/pymmcore-plus
|
|
66
71
|
|
|
67
|
-
|
|
72
|
+
4. A `Micro-Manager*` folder in the `pymmcore_plus` package directory (this is the
|
|
68
73
|
default install location when running `python -m pymmcore_plus.install`)
|
|
69
|
-
|
|
74
|
+
5. The default micro-manager install location:
|
|
70
75
|
|
|
71
76
|
- **Windows**: `C:/Program Files/`
|
|
72
77
|
- **macOS**: `/Applications`
|
|
@@ -88,14 +93,25 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
|
88
93
|
"""
|
|
89
94
|
from ._logger import logger
|
|
90
95
|
|
|
96
|
+
# we use a dict here to avoid duplicates
|
|
97
|
+
full_list: dict[str, None] = {}
|
|
98
|
+
|
|
91
99
|
# environment variable takes precedence
|
|
92
|
-
full_list: list[str] = []
|
|
93
100
|
env_path = os.getenv("MICROMANAGER_PATH")
|
|
94
101
|
if env_path and os.path.isdir(env_path):
|
|
95
102
|
if return_first:
|
|
96
103
|
logger.debug("using MM path from env var: %s", env_path)
|
|
97
104
|
return env_path
|
|
98
|
-
full_list
|
|
105
|
+
full_list[env_path] = None
|
|
106
|
+
|
|
107
|
+
# then check for a path in CURRENT_MM_PATH
|
|
108
|
+
if CURRENT_MM_PATH.exists():
|
|
109
|
+
path = CURRENT_MM_PATH.read_text().strip()
|
|
110
|
+
if os.path.isdir(path):
|
|
111
|
+
if return_first:
|
|
112
|
+
logger.debug("using MM path from current_mm: %s", path)
|
|
113
|
+
return path
|
|
114
|
+
full_list[path] = None
|
|
99
115
|
|
|
100
116
|
# then look in user_data_dir
|
|
101
117
|
_folders = (p for p in USER_DATA_MM_PATH.glob("Micro-Manager*") if p.is_dir())
|
|
@@ -104,7 +120,8 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
|
104
120
|
if return_first:
|
|
105
121
|
logger.debug("using MM path from user install: %s", user_install[0])
|
|
106
122
|
return str(user_install[0])
|
|
107
|
-
|
|
123
|
+
for x in user_install:
|
|
124
|
+
full_list[str(x)] = None
|
|
108
125
|
|
|
109
126
|
# then look for an installation in this folder (from `pymmcore_plus.install`)
|
|
110
127
|
sfx = "_win" if os.name == "nt" else "_mac"
|
|
@@ -115,7 +132,8 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
|
115
132
|
if return_first:
|
|
116
133
|
logger.debug("using MM path from local install: %s", local_install[0])
|
|
117
134
|
return str(local_install[0])
|
|
118
|
-
|
|
135
|
+
for x in local_install:
|
|
136
|
+
full_list[str(x)] = None
|
|
119
137
|
|
|
120
138
|
applications = {
|
|
121
139
|
"darwin": Path("/Applications/"),
|
|
@@ -137,8 +155,59 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
|
137
155
|
logger.debug("using MM path found in applications: %s", pth)
|
|
138
156
|
return str(pth)
|
|
139
157
|
if pth is not None:
|
|
140
|
-
full_list
|
|
141
|
-
return full_list
|
|
158
|
+
full_list[str(pth)] = None
|
|
159
|
+
return list(full_list)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _match_mm_pattern(pattern: str | Pattern[str]) -> Path | None:
|
|
163
|
+
"""Locate an existing Micro-Manager folder using a regex pattern."""
|
|
164
|
+
for _path in find_micromanager(return_first=False):
|
|
165
|
+
if not isinstance(pattern, re.Pattern):
|
|
166
|
+
pattern = str(pattern)
|
|
167
|
+
if re.search(pattern, _path) is not None:
|
|
168
|
+
return Path(_path)
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def use_micromanager(
|
|
173
|
+
path: str | Path | None = None, pattern: str | Pattern[str] | None = None
|
|
174
|
+
) -> Path | None:
|
|
175
|
+
"""Set the preferred Micro-Manager path.
|
|
176
|
+
|
|
177
|
+
This sets the preferred micromanager path, and persists across sessions.
|
|
178
|
+
This path takes precedence over everything *except* the `MICROMANAGER_PATH`
|
|
179
|
+
environment variable.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
path : str | Path | None
|
|
184
|
+
Path to an existing directory. This directory should contain micro-manager
|
|
185
|
+
device adapters. If `None`, the path will be determined using `pattern`.
|
|
186
|
+
pattern : str Pattern | | None
|
|
187
|
+
A regex pattern to match against the micromanager paths found by
|
|
188
|
+
`find_micromanager`. If no match is found, a `FileNotFoundError` will be raised.
|
|
189
|
+
"""
|
|
190
|
+
if path is None:
|
|
191
|
+
if pattern is None: # pragma: no cover
|
|
192
|
+
raise ValueError("One of 'path' or 'pattern' must be provided")
|
|
193
|
+
if (path := _match_mm_pattern(pattern)) is None:
|
|
194
|
+
options = "\n".join(find_micromanager(return_first=False))
|
|
195
|
+
raise FileNotFoundError(
|
|
196
|
+
f"No micromanager path found matching: {pattern!r}. Options:\n{options}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if not isinstance(path, Path): # pragma: no cover
|
|
200
|
+
path = Path(path)
|
|
201
|
+
|
|
202
|
+
path = path.expanduser().resolve()
|
|
203
|
+
if not path.is_dir(): # pragma: no cover
|
|
204
|
+
if not path.exists():
|
|
205
|
+
raise FileNotFoundError(f"Path not found: {path!r}")
|
|
206
|
+
raise NotADirectoryError(f"Not a directory: {path!r}")
|
|
207
|
+
|
|
208
|
+
USER_DATA_MM_PATH.mkdir(parents=True, exist_ok=True)
|
|
209
|
+
CURRENT_MM_PATH.write_text(str(path))
|
|
210
|
+
return path
|
|
142
211
|
|
|
143
212
|
|
|
144
213
|
def _imported_qt_modules() -> Iterator[str]:
|
|
@@ -521,3 +590,24 @@ def system_info() -> dict[str, str]:
|
|
|
521
590
|
info["qt"] = f"{API_NAME} {QT_VERSION}"
|
|
522
591
|
|
|
523
592
|
return info
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
if sys.version_info < (3, 11):
|
|
596
|
+
|
|
597
|
+
def _utcnow() -> datetime.datetime:
|
|
598
|
+
return datetime.datetime.utcnow()
|
|
599
|
+
else:
|
|
600
|
+
|
|
601
|
+
def _utcnow() -> datetime.datetime:
|
|
602
|
+
return datetime.datetime.now(datetime.UTC)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def timestamp() -> str:
|
|
606
|
+
"""Return the current timestamp, try using local timezone, in ISO format.
|
|
607
|
+
|
|
608
|
+
YYYY-MM-DD HH:MM:SS.mmmmmm+HH:MM
|
|
609
|
+
"""
|
|
610
|
+
now = _utcnow()
|
|
611
|
+
with suppress(Exception):
|
|
612
|
+
now = now.astimezone()
|
|
613
|
+
return now.isoformat()
|
|
@@ -15,6 +15,7 @@ __all__ = [
|
|
|
15
15
|
"FocusDirection",
|
|
16
16
|
"Keyword",
|
|
17
17
|
"Metadata",
|
|
18
|
+
"PixelFormat",
|
|
18
19
|
"PortType",
|
|
19
20
|
"PropertyType",
|
|
20
21
|
]
|
|
@@ -32,6 +33,7 @@ from ._constants import (
|
|
|
32
33
|
DeviceType,
|
|
33
34
|
FocusDirection,
|
|
34
35
|
Keyword,
|
|
36
|
+
PixelFormat,
|
|
35
37
|
PortType,
|
|
36
38
|
PropertyType,
|
|
37
39
|
)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from enum import Enum, IntEnum
|
|
4
|
+
from typing import Literal
|
|
4
5
|
|
|
5
6
|
import pymmcore
|
|
6
7
|
|
|
@@ -163,8 +164,8 @@ class PropertyType(IntEnum):
|
|
|
163
164
|
def to_json(self) -> str:
|
|
164
165
|
return {0: "null", 1: "string", 2: "number", 3: "integer"}[self]
|
|
165
166
|
|
|
166
|
-
def __repr__(self) -> str:
|
|
167
|
-
return getattr(self.to_python(), "__name__", "
|
|
167
|
+
def __repr__(self) -> Literal["undefined", "float", "int", "str"]:
|
|
168
|
+
return getattr(self.to_python(), "__name__", "undefined")
|
|
168
169
|
|
|
169
170
|
|
|
170
171
|
class ActionType(IntEnum):
|
|
@@ -185,13 +186,13 @@ class PortType(IntEnum):
|
|
|
185
186
|
|
|
186
187
|
|
|
187
188
|
class FocusDirection(IntEnum):
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
Unknown = pymmcore.FocusDirectionUnknown
|
|
190
|
+
TowardSample = pymmcore.FocusDirectionTowardSample
|
|
191
|
+
AwayFromSample = pymmcore.FocusDirectionAwayFromSample
|
|
191
192
|
# aliases
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
FocusDirectionUnknown = Unknown
|
|
194
|
+
FocusDirectionTowardSample = TowardSample
|
|
195
|
+
FocusDirectionAwayFromSample = AwayFromSample
|
|
195
196
|
|
|
196
197
|
|
|
197
198
|
class DeviceNotification(IntEnum):
|
|
@@ -222,6 +223,12 @@ class DeviceInitializationState(IntEnum):
|
|
|
222
223
|
|
|
223
224
|
|
|
224
225
|
class PixelType(str, Enum):
|
|
226
|
+
"""These are pixel types, as used in MMStudio and MMCoreJ wrapper.
|
|
227
|
+
|
|
228
|
+
They are only here for supporting the legacy (and probably to-be-deprecated)
|
|
229
|
+
taggedImages.
|
|
230
|
+
"""
|
|
231
|
+
|
|
225
232
|
UNKNOWN = ""
|
|
226
233
|
GRAY8 = "GRAY8"
|
|
227
234
|
GRAY16 = "GRAY16"
|
|
@@ -239,3 +246,97 @@ class PixelType(str, Enum):
|
|
|
239
246
|
depth, cls.UNKNOWN
|
|
240
247
|
)
|
|
241
248
|
return cls.GRAY32 if n_comp == 1 else cls.RGB32
|
|
249
|
+
|
|
250
|
+
def to_pixel_format(self) -> PixelFormat:
|
|
251
|
+
return {
|
|
252
|
+
self.GRAY8: PixelFormat.MONO8,
|
|
253
|
+
self.GRAY16: PixelFormat.MONO16,
|
|
254
|
+
self.GRAY32: PixelFormat.MONO32,
|
|
255
|
+
self.RGB32: PixelFormat.RGB8,
|
|
256
|
+
self.RGB64: PixelFormat.RGB16,
|
|
257
|
+
}[self]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class PixelFormat(str, Enum):
|
|
261
|
+
"""Subset of GeniCam Pixel Format names used by pymmcore-plus.
|
|
262
|
+
|
|
263
|
+
(This is similar to PixelType, but follows GeniCam standards.)
|
|
264
|
+
|
|
265
|
+
See <https://docs.baslerweb.com/pixel-format#unpacked-and-packed-pixel-formats>
|
|
266
|
+
for helpful clarifications. Note that **unpacked** pixel formats (like
|
|
267
|
+
Mono8, Mono12, Mono16) are always 8-bit aligned. Meaning Mono12 is actually
|
|
268
|
+
a 16-bit buffer.
|
|
269
|
+
|
|
270
|
+
Attributes
|
|
271
|
+
----------
|
|
272
|
+
MONO8 : str
|
|
273
|
+
8-bit (unpacked) monochrome pixel format.
|
|
274
|
+
MONO10 : str
|
|
275
|
+
10-bit (unpacked) monochrome pixel format. (16-bit buffer)
|
|
276
|
+
MONO12 : str
|
|
277
|
+
12-bit (unpacked) monochrome pixel format. (16-bit buffer)
|
|
278
|
+
MONO14 : str
|
|
279
|
+
14-bit (unpacked) monochrome pixel format. (16-bit buffer)
|
|
280
|
+
MONO16 : str
|
|
281
|
+
16-bit (unpacked) monochrome pixel format
|
|
282
|
+
MONO32 : str
|
|
283
|
+
32-bit (unpacked) monochrome pixel format
|
|
284
|
+
RGB8 : str
|
|
285
|
+
8-bit RGB pixel format. (24-bit buffer)
|
|
286
|
+
RGB10 : str
|
|
287
|
+
10-bit RGB pixel format. (48-bit buffer)
|
|
288
|
+
RGB12 : str
|
|
289
|
+
12-bit RGB pixel format. (48-bit buffer)
|
|
290
|
+
RGB14 : str
|
|
291
|
+
14-bit RGB pixel format. (48-bit buffer)
|
|
292
|
+
RGB16 : str
|
|
293
|
+
16-bit RGB pixel format. (48-bit buffer)
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
MONO8 = "Mono8"
|
|
297
|
+
MONO10 = "Mono10"
|
|
298
|
+
MONO12 = "Mono12"
|
|
299
|
+
MONO14 = "Mono14"
|
|
300
|
+
MONO16 = "Mono16"
|
|
301
|
+
MONO32 = "Mono32"
|
|
302
|
+
RGB8 = "RGB8"
|
|
303
|
+
RGB10 = "RGB10"
|
|
304
|
+
RGB12 = "RGB12"
|
|
305
|
+
RGB14 = "RGB14"
|
|
306
|
+
RGB16 = "RGB16"
|
|
307
|
+
|
|
308
|
+
@classmethod
|
|
309
|
+
def pick(cls, bit_depth: int, n_comp: int = 1) -> PixelFormat:
|
|
310
|
+
try:
|
|
311
|
+
return PIXEL_FORMATS[n_comp][bit_depth]
|
|
312
|
+
except KeyError as e:
|
|
313
|
+
raise NotImplementedError(
|
|
314
|
+
f"Unsupported Pixel Format {bit_depth=} {n_comp=}"
|
|
315
|
+
) from e
|
|
316
|
+
|
|
317
|
+
@classmethod
|
|
318
|
+
def for_current_camera(cls, core: pymmcore.CMMCore) -> PixelFormat:
|
|
319
|
+
n_comp = core.getNumberOfComponents()
|
|
320
|
+
if n_comp == 4:
|
|
321
|
+
n_comp = 3
|
|
322
|
+
return cls.pick(core.getImageBitDepth(), n_comp)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# map of {number of components: {bit depth: PixelFormat}}
|
|
326
|
+
PIXEL_FORMATS: dict[int, dict[int, PixelFormat]] = {
|
|
327
|
+
1: {
|
|
328
|
+
8: PixelFormat.MONO8,
|
|
329
|
+
10: PixelFormat.MONO10,
|
|
330
|
+
12: PixelFormat.MONO12,
|
|
331
|
+
14: PixelFormat.MONO14,
|
|
332
|
+
16: PixelFormat.MONO16,
|
|
333
|
+
32: PixelFormat.MONO32,
|
|
334
|
+
},
|
|
335
|
+
3: {
|
|
336
|
+
8: PixelFormat.RGB8,
|
|
337
|
+
10: PixelFormat.RGB10,
|
|
338
|
+
12: PixelFormat.RGB12,
|
|
339
|
+
14: PixelFormat.RGB14,
|
|
340
|
+
16: PixelFormat.RGB16,
|
|
341
|
+
},
|
|
342
|
+
}
|