mesofield 0.3.2b0__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.
- docs/_static/custom.css +40 -0
- docs/_static/favicon.png +0 -0
- docs/_static/logo.png +0 -0
- docs/api/index.md +70 -0
- docs/conf.py +200 -0
- docs/developer_guide.md +303 -0
- docs/index.md +25 -0
- docs/tutorial.md +4 -0
- docs/user_guide.md +172 -0
- examples/teensy_pulse_generator.py +320 -0
- experiments/pipeline_demo/experiment.json +24 -0
- experiments/pipeline_demo/hardware.yaml +23 -0
- experiments/pipeline_demo/procedure.py +50 -0
- experiments/two_cam_demo/experiment.json +24 -0
- experiments/two_cam_demo/hardware.yaml +58 -0
- experiments/two_cam_demo/load_dataset.py +213 -0
- experiments/two_cam_demo/procedure.py +87 -0
- external/video-codecs/openh264-1.8.0-win64.dll +0 -0
- mesofield/__init__.py +45 -0
- mesofield/__main__.py +11 -0
- mesofield/_version.py +24 -0
- mesofield/base.py +750 -0
- mesofield/cli/__init__.py +57 -0
- mesofield/cli/_richhelp.py +100 -0
- mesofield/cli/acquire.py +254 -0
- mesofield/cli/datakit.py +165 -0
- mesofield/cli/process.py +376 -0
- mesofield/cli/rig.py +108 -0
- mesofield/cli/tools.py +347 -0
- mesofield/config.py +751 -0
- mesofield/data/__init__.py +23 -0
- mesofield/data/batch.py +633 -0
- mesofield/data/manager.py +388 -0
- mesofield/data/writer.py +289 -0
- mesofield/datakit/__init__.py +44 -0
- mesofield/datakit/__main__.py +35 -0
- mesofield/datakit/_utils/_logger.py +5 -0
- mesofield/datakit/_version.py +141 -0
- mesofield/datakit/config.py +50 -0
- mesofield/datakit/core.py +783 -0
- mesofield/datakit/datamodel.py +200 -0
- mesofield/datakit/discover.py +124 -0
- mesofield/datakit/explore.py +651 -0
- mesofield/datakit/notebooks/pupil_dlc.ipynb +2445 -0
- mesofield/datakit/profile.py +535 -0
- mesofield/datakit/shell.py +83 -0
- mesofield/datakit/sources/__init__.py +65 -0
- mesofield/datakit/sources/analysis/mesomap.py +194 -0
- mesofield/datakit/sources/analysis/mesoscope.py +77 -0
- mesofield/datakit/sources/analysis/pupil.py +246 -0
- mesofield/datakit/sources/behavior/__init__.py +0 -0
- mesofield/datakit/sources/behavior/dataqueue.py +281 -0
- mesofield/datakit/sources/behavior/psychopy.py +364 -0
- mesofield/datakit/sources/behavior/treadmill.py +323 -0
- mesofield/datakit/sources/behavior/wheel.py +277 -0
- mesofield/datakit/sources/camera/mesoscope.py +32 -0
- mesofield/datakit/sources/camera/metadata_json.py +130 -0
- mesofield/datakit/sources/camera/pupil.py +28 -0
- mesofield/datakit/sources/camera/suite2p.py +547 -0
- mesofield/datakit/sources/register.py +204 -0
- mesofield/datakit/sources/session/config.py +130 -0
- mesofield/datakit/sources/session/notes.py +63 -0
- mesofield/datakit/sources/session/timestamps.py +58 -0
- mesofield/datakit/timeline.py +306 -0
- mesofield/devices/__init__.py +42 -0
- mesofield/devices/base.py +498 -0
- mesofield/devices/base_camera.py +295 -0
- mesofield/devices/cameras.py +740 -0
- mesofield/devices/daq.py +151 -0
- mesofield/devices/encoder.py +384 -0
- mesofield/devices/mocks.py +275 -0
- mesofield/devices/psychopy_device.py +455 -0
- mesofield/devices/subprocesses/__init__.py +0 -0
- mesofield/devices/subprocesses/psychopy.py +133 -0
- mesofield/devices/treadmill.py +318 -0
- mesofield/engines.py +380 -0
- mesofield/gui/Mesofield_icon.png +0 -0
- mesofield/gui/__init__.py +76 -0
- mesofield/gui/config_wizard.py +724 -0
- mesofield/gui/controller.py +535 -0
- mesofield/gui/dynamic_controller.py +78 -0
- mesofield/gui/maingui.py +427 -0
- mesofield/gui/mdagui.py +285 -0
- mesofield/gui/qt_device_adapter.py +109 -0
- mesofield/gui/speedplotter.py +152 -0
- mesofield/gui/theme.py +445 -0
- mesofield/gui/tiff_viewer.py +1050 -0
- mesofield/gui/viewer.py +691 -0
- mesofield/hardware.py +549 -0
- mesofield/playback.py +1298 -0
- mesofield/processing/__init__.py +12 -0
- mesofield/processing/runner.py +237 -0
- mesofield/processors/__init__.py +13 -0
- mesofield/processors/base.py +287 -0
- mesofield/processors/frame_mean.py +19 -0
- mesofield/protocols.py +378 -0
- mesofield/scaffold/__init__.py +34 -0
- mesofield/scaffold/experiment.py +400 -0
- mesofield/scaffold/rigs.py +121 -0
- mesofield/signals.py +85 -0
- mesofield/utils/__init__.py +0 -0
- mesofield/utils/_logger.py +156 -0
- mesofield/utils/retrofit.py +309 -0
- mesofield/utils/utils.py +217 -0
- mesofield-0.3.2b0.dist-info/METADATA +178 -0
- mesofield-0.3.2b0.dist-info/RECORD +111 -0
- mesofield-0.3.2b0.dist-info/WHEEL +5 -0
- mesofield-0.3.2b0.dist-info/entry_points.txt +2 -0
- mesofield-0.3.2b0.dist-info/licenses/LICENSE +21 -0
- mesofield-0.3.2b0.dist-info/top_level.txt +6 -0
- scripts/bench_frame_processor.py +103 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Build a pandas pickle from this experiment's session outputs.
|
|
2
|
+
|
|
3
|
+
Reads each `manifest.json` produced by `procedure.py`, ingests each
|
|
4
|
+
declared producer (`wheel` CSV, `meso` / `pupil` OME-TIFF + frame-metadata
|
|
5
|
+
sidecars), and writes a single `processed/dataset.pkl` you can load with
|
|
6
|
+
``pd.read_pickle(...)``.
|
|
7
|
+
|
|
8
|
+
The dataset has the shape databench consumes:
|
|
9
|
+
|
|
10
|
+
Subject Session Task | Source Signal
|
|
11
|
+
-----------------------------------------------------
|
|
12
|
+
DEMO 01 freeview | wheel timestamp, payload
|
|
13
|
+
| meso frame_index, TimeReceivedByCore
|
|
14
|
+
| pupil frame_index, TimeReceivedByCore
|
|
15
|
+
|
|
16
|
+
The mock producers don't write columns that match datakit's real
|
|
17
|
+
SOURCE_REGISTRY parsers (the wheel parser expects ``Clicks, Time,
|
|
18
|
+
Speed``; the mesoscope parser globs ``*_mesoscope.ome.tiff…``). We
|
|
19
|
+
therefore use the AcquisitionManifest as the source of truth and do a
|
|
20
|
+
manifest-driven ingest -- which is the pattern lab programmers will
|
|
21
|
+
write against their own producers.
|
|
22
|
+
|
|
23
|
+
The script does, however, import freely from ``mesofield.datakit`` and
|
|
24
|
+
``mesokit_schema``: the manifest models, the loader's
|
|
25
|
+
``ExperimentStore`` (only for the final pickle write convention),
|
|
26
|
+
``hash_file`` for provenance, etc. The point is the datakit module is
|
|
27
|
+
the toolkit; the parsers shipped with it are one option among many.
|
|
28
|
+
|
|
29
|
+
Run::
|
|
30
|
+
|
|
31
|
+
python load_dataset.py # uses ./data
|
|
32
|
+
python load_dataset.py --root ./ # explicit
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import argparse
|
|
38
|
+
import json
|
|
39
|
+
from datetime import datetime, timezone
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Iterable
|
|
42
|
+
|
|
43
|
+
import numpy as np
|
|
44
|
+
import pandas as pd
|
|
45
|
+
|
|
46
|
+
# mesokit-schema: the typed manifest contract.
|
|
47
|
+
from mesokit_schema import (
|
|
48
|
+
AcquisitionManifest,
|
|
49
|
+
DatasetManifest,
|
|
50
|
+
ProducerEntry,
|
|
51
|
+
SourceVersion,
|
|
52
|
+
TimeBasis,
|
|
53
|
+
)
|
|
54
|
+
from mesokit_schema.dataset import hash_file
|
|
55
|
+
|
|
56
|
+
# mesofield.datakit: the ingest toolkit. We don't use SOURCE_REGISTRY's
|
|
57
|
+
# real-hardware parsers here (mock filenames don't match their globs),
|
|
58
|
+
# but we lean on the datakit module's package surface for everything else.
|
|
59
|
+
import mesofield.datakit # noqa: F401 — ensures datakit's logger is wired
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def discover_sessions(root: Path) -> list[Path]:
|
|
63
|
+
"""Find every `data/sub-*/ses-*/manifest.json` under `root`."""
|
|
64
|
+
data_root = root / "data"
|
|
65
|
+
if not data_root.exists():
|
|
66
|
+
return []
|
|
67
|
+
return sorted(data_root.glob("sub-*/ses-*/manifest.json"))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def load_producer_table(session_dir: Path, producer: ProducerEntry) -> pd.DataFrame:
|
|
71
|
+
"""Read one producer's output (or its sidecar) into a DataFrame."""
|
|
72
|
+
if producer.file_type.endswith("csv"):
|
|
73
|
+
return pd.read_csv(session_dir / producer.output_path)
|
|
74
|
+
|
|
75
|
+
if producer.file_type == "ome.tiff":
|
|
76
|
+
# The pixel data lives in the TIFF (inspect with ImageJ); the
|
|
77
|
+
# time-axis metadata we merge into the dataset comes from the
|
|
78
|
+
# frame-metadata sidecar.
|
|
79
|
+
if not producer.metadata_path:
|
|
80
|
+
raise RuntimeError(
|
|
81
|
+
f"Camera producer {producer.device_id!r} has no metadata_path"
|
|
82
|
+
)
|
|
83
|
+
sidecar = json.loads((session_dir / producer.metadata_path).read_text())
|
|
84
|
+
return pd.DataFrame(sidecar.get("p0", []))
|
|
85
|
+
|
|
86
|
+
raise RuntimeError(
|
|
87
|
+
f"load_dataset.py: unknown file_type {producer.file_type!r} for "
|
|
88
|
+
f"producer {producer.device_id!r}; extend this loader for new types."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def build_session_table(session_dir: Path, manifest: AcquisitionManifest) -> pd.DataFrame:
|
|
93
|
+
"""Multi-source DataFrame for a single session.
|
|
94
|
+
|
|
95
|
+
Anchored to the mesoscope (primary) frame count; shorter sources
|
|
96
|
+
are NaN-padded, longer ones truncated. Trades temporal precision
|
|
97
|
+
for an inspectable, regular table.
|
|
98
|
+
"""
|
|
99
|
+
primary = next(
|
|
100
|
+
(p for p in manifest.producers if p.data_type == "frames" and p.bids_type == "func"),
|
|
101
|
+
manifest.producers[0],
|
|
102
|
+
)
|
|
103
|
+
primary_df = load_producer_table(session_dir, primary)
|
|
104
|
+
anchor_rows = len(primary_df)
|
|
105
|
+
|
|
106
|
+
columns: dict[tuple[str, str], list] = {}
|
|
107
|
+
|
|
108
|
+
def _add(source: str, df: pd.DataFrame) -> None:
|
|
109
|
+
for col in df.columns:
|
|
110
|
+
values = df[col].iloc[:anchor_rows].tolist()
|
|
111
|
+
if len(values) < anchor_rows:
|
|
112
|
+
values = values + [None] * (anchor_rows - len(values))
|
|
113
|
+
columns[(source, col)] = values
|
|
114
|
+
|
|
115
|
+
for producer in manifest.producers:
|
|
116
|
+
df = load_producer_table(session_dir, producer)
|
|
117
|
+
# Use device_id as the source key so two cameras (meso / pupil)
|
|
118
|
+
# appear as distinct sources even though both share data_type=frames.
|
|
119
|
+
_add(producer.device_id, df)
|
|
120
|
+
|
|
121
|
+
table = pd.DataFrame(columns)
|
|
122
|
+
table.columns = pd.MultiIndex.from_tuples(table.columns, names=["Source", "Signal"])
|
|
123
|
+
|
|
124
|
+
session = manifest.session
|
|
125
|
+
task = session.task or "default"
|
|
126
|
+
table.index = pd.MultiIndex.from_tuples(
|
|
127
|
+
[(session.subject, session.session, task)] * len(table),
|
|
128
|
+
names=["Subject", "Session", "Task"],
|
|
129
|
+
)
|
|
130
|
+
return table
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def build_dataset(root: Path) -> tuple[Path, Path, pd.DataFrame]:
|
|
134
|
+
"""Ingest every session under `root/data/`. Returns (pickle, manifest, df)."""
|
|
135
|
+
manifest_paths = discover_sessions(root)
|
|
136
|
+
if not manifest_paths:
|
|
137
|
+
raise FileNotFoundError(
|
|
138
|
+
f"No AcquisitionManifests found under {root / 'data'}. "
|
|
139
|
+
f"Run `python procedure.py` first."
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
tables: list[pd.DataFrame] = []
|
|
143
|
+
acq_hashes: list[str] = []
|
|
144
|
+
for mpath in manifest_paths:
|
|
145
|
+
acq = AcquisitionManifest.read(mpath)
|
|
146
|
+
if not acq.acquisition_complete:
|
|
147
|
+
print(f"[skip] {mpath} reports acquisition_complete=False")
|
|
148
|
+
continue
|
|
149
|
+
tables.append(build_session_table(mpath.parent, acq))
|
|
150
|
+
acq_hashes.append(acq.content_hash())
|
|
151
|
+
print(f"[ok] {acq.session.subject}/{acq.session.session}: "
|
|
152
|
+
f"{len(tables[-1])} rows × {len(tables[-1].columns)} cols")
|
|
153
|
+
|
|
154
|
+
if not tables:
|
|
155
|
+
raise RuntimeError("No complete sessions to ingest.")
|
|
156
|
+
|
|
157
|
+
dataset = pd.concat(tables, axis=0).sort_index()
|
|
158
|
+
out_dir = root / "processed"
|
|
159
|
+
out_dir.mkdir(exist_ok=True)
|
|
160
|
+
pkl_path = out_dir / "dataset.pkl"
|
|
161
|
+
dataset.to_pickle(pkl_path)
|
|
162
|
+
|
|
163
|
+
ds_manifest = DatasetManifest(
|
|
164
|
+
datakit_version="manifest-driven-ingest",
|
|
165
|
+
built_at=datetime.now(timezone.utc),
|
|
166
|
+
upstream_acquisition_hash=acq_hashes[0] if len(acq_hashes) == 1 else None,
|
|
167
|
+
data_file="dataset.pkl",
|
|
168
|
+
data_content_hash=hash_file(pkl_path),
|
|
169
|
+
time_basis=TimeBasis(
|
|
170
|
+
clock_source="derived",
|
|
171
|
+
description="Anchored on mesoscope frame count per session.",
|
|
172
|
+
),
|
|
173
|
+
source_versions=[
|
|
174
|
+
SourceVersion(
|
|
175
|
+
tag="multi-source",
|
|
176
|
+
version="0.1.0",
|
|
177
|
+
parser_class="load_dataset.build_session_table",
|
|
178
|
+
)
|
|
179
|
+
],
|
|
180
|
+
columns=[(s, c) for (s, c) in dataset.columns.tolist()],
|
|
181
|
+
)
|
|
182
|
+
ds_manifest_path = out_dir / "dataset_manifest.json"
|
|
183
|
+
ds_manifest.write(ds_manifest_path)
|
|
184
|
+
|
|
185
|
+
return pkl_path, ds_manifest_path, dataset
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def main() -> int:
|
|
189
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
190
|
+
parser.add_argument(
|
|
191
|
+
"--root", type=Path, default=Path(__file__).resolve().parent,
|
|
192
|
+
help="Experiment root containing data/ (default: this file's dir)",
|
|
193
|
+
)
|
|
194
|
+
args = parser.parse_args()
|
|
195
|
+
|
|
196
|
+
pkl, ds_manifest_path, df = build_dataset(args.root.resolve())
|
|
197
|
+
print()
|
|
198
|
+
print(f"Wrote {pkl}")
|
|
199
|
+
print(f"Wrote {ds_manifest_path}")
|
|
200
|
+
print()
|
|
201
|
+
print("--- first 3 rows ---")
|
|
202
|
+
print(df.head(3))
|
|
203
|
+
print()
|
|
204
|
+
print(f"--- columns ({len(df.columns)} total) ---")
|
|
205
|
+
for col in df.columns:
|
|
206
|
+
print(f" {col}")
|
|
207
|
+
print()
|
|
208
|
+
print("Load in Python: pd.read_pickle({})".format(repr(str(pkl))))
|
|
209
|
+
return 0
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
if __name__ == "__main__":
|
|
213
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Two-camera + encoder demo: 10-second headless acquisition.
|
|
2
|
+
|
|
3
|
+
Subclasses :class:`mesofield.base.Procedure` with the standard duration-
|
|
4
|
+
timer pattern so the run terminates cleanly without a primary device's
|
|
5
|
+
hardware finished-signal. The AcquisitionManifest is written
|
|
6
|
+
automatically by the base Procedure's cleanup hook.
|
|
7
|
+
|
|
8
|
+
Run:
|
|
9
|
+
|
|
10
|
+
python procedure.py # headless (no Qt)
|
|
11
|
+
mesofield launch experiment.json # GUI
|
|
12
|
+
|
|
13
|
+
The headless path is what `mesofield init` scaffolds; cf. TUTORIAL.md.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import threading
|
|
19
|
+
|
|
20
|
+
from mesofield import DeviceRegistry
|
|
21
|
+
from mesofield.base import Procedure
|
|
22
|
+
from mesofield.devices.mocks import MockFrameProducer
|
|
23
|
+
from mesofield.devices.mocks import MockEncoderDevice
|
|
24
|
+
from mesofield.processors import FrameMean
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
DeviceRegistry._registry.setdefault("mock_wheel", MockEncoderDevice)
|
|
28
|
+
DeviceRegistry._registry.setdefault("mock_camera", MockFrameProducer)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TwoCamDemoProcedure(Procedure):
|
|
32
|
+
"""Mock 2-camera + encoder procedure with duration-gated cleanup."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, *args, **kwargs):
|
|
35
|
+
super().__init__(*args, **kwargs)
|
|
36
|
+
# Worked example of the procedure-authored processor API:
|
|
37
|
+
# construct, assign, done. Auto-attached, auto-registered with the
|
|
38
|
+
# DataManager, and auto-plotted in the GUI when `plot=True`.
|
|
39
|
+
self.frame_mean = FrameMean(
|
|
40
|
+
camera=self.hardware.primary,
|
|
41
|
+
plot=True,
|
|
42
|
+
label="Frame Mean",
|
|
43
|
+
value_label="Mean intensity",
|
|
44
|
+
y_range=(0, 255),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def on_started(self) -> None:
|
|
48
|
+
duration = self.config.get("duration")
|
|
49
|
+
if duration:
|
|
50
|
+
self.logger.info(f"Duration cap armed: {duration}s")
|
|
51
|
+
self._duration_timer = threading.Timer(float(duration), self.cleanup)
|
|
52
|
+
self._duration_timer.daemon = True
|
|
53
|
+
self._duration_timer.start()
|
|
54
|
+
|
|
55
|
+
def on_finished(self) -> None:
|
|
56
|
+
super().on_finished()
|
|
57
|
+
timer = getattr(self, "_duration_timer", None)
|
|
58
|
+
if timer is not None:
|
|
59
|
+
timer.cancel()
|
|
60
|
+
self._duration_timer = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main() -> int:
|
|
64
|
+
import sys
|
|
65
|
+
from pathlib import Path
|
|
66
|
+
|
|
67
|
+
cfg = Path(__file__).parent / "experiment.json"
|
|
68
|
+
proc = TwoCamDemoProcedure(str(cfg))
|
|
69
|
+
finished = proc.run_until_finished(timeout=30.0)
|
|
70
|
+
if not finished:
|
|
71
|
+
print("Procedure did not finish in time.", file=sys.stderr)
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
session_dir = (
|
|
75
|
+
Path(proc.data_dir)
|
|
76
|
+
/ f"sub-{proc.config.subject}"
|
|
77
|
+
/ f"ses-{proc.config.session}"
|
|
78
|
+
)
|
|
79
|
+
print(f"\nAcquisition complete.")
|
|
80
|
+
print(f" Session dir: {session_dir}")
|
|
81
|
+
print(f" Manifest: {session_dir / 'manifest.json'}")
|
|
82
|
+
print(f"\nNext: python load_dataset.py")
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
raise SystemExit(main())
|
|
Binary file
|
mesofield/__init__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Mesofield top-level package.
|
|
2
|
+
|
|
3
|
+
This module exposes the :class:`DeviceRegistry` decorator used by hardware
|
|
4
|
+
adapters to declare themselves under a YAML ``type:`` key. The remaining
|
|
5
|
+
subpackages are not auto-imported; pull them in explicitly:
|
|
6
|
+
|
|
7
|
+
.. code-block:: python
|
|
8
|
+
|
|
9
|
+
from mesofield.base import Procedure
|
|
10
|
+
from mesofield.config import ExperimentConfig
|
|
11
|
+
from mesofield.hardware import HardwareManager
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Type, Callable, Dict, Optional, Any, TypeVar
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
18
|
+
class DeviceRegistry:
|
|
19
|
+
"""Registry mapping YAML ``type:`` strings to device classes.
|
|
20
|
+
|
|
21
|
+
Hardware adapters register themselves via the
|
|
22
|
+
:meth:`~DeviceRegistry.register` decorator; the
|
|
23
|
+
:class:`~mesofield.hardware.HardwareManager` looks them up by string
|
|
24
|
+
when materialising devices from a YAML file.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
_registry: Dict[str, Type[Any]] = {}
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def register(cls, device_type: str) -> Callable[[Type[T]], Type[T]]:
|
|
31
|
+
"""Register a device class for a specific device type.
|
|
32
|
+
|
|
33
|
+
The decorator also stamps ``registry_key`` onto the class so any
|
|
34
|
+
device instance can report its YAML ``type:`` for hardware export.
|
|
35
|
+
"""
|
|
36
|
+
def decorator(device_class: Type[T]) -> Type[T]:
|
|
37
|
+
cls._registry[device_type] = device_class
|
|
38
|
+
device_class.registry_key = device_type
|
|
39
|
+
return device_class
|
|
40
|
+
return decorator
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def get_class(cls, device_type: str) -> Optional[Type[Any]]:
|
|
44
|
+
"""Get the device class for a specific device type."""
|
|
45
|
+
return cls._registry.get(device_type)
|
mesofield/__main__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""``python -m mesofield`` — entry point for the Mesofield CLI.
|
|
2
|
+
|
|
3
|
+
The CLI itself lives in :mod:`mesofield.cli`; this module just exposes the
|
|
4
|
+
root group so both ``python -m mesofield`` and the ``mesofield`` console
|
|
5
|
+
script resolve to the same place.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from mesofield.cli import cli
|
|
9
|
+
|
|
10
|
+
if __name__ == "__main__":
|
|
11
|
+
cli()
|
mesofield/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.3.2b0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 2, 'b0')
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|