PyIncucyte 0.3.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.
Files changed (43) hide show
  1. pyincucyte-0.3.0/LICENSE +28 -0
  2. pyincucyte-0.3.0/PKG-INFO +355 -0
  3. pyincucyte-0.3.0/PyIncucyte.egg-info/PKG-INFO +355 -0
  4. pyincucyte-0.3.0/PyIncucyte.egg-info/SOURCES.txt +41 -0
  5. pyincucyte-0.3.0/PyIncucyte.egg-info/dependency_links.txt +1 -0
  6. pyincucyte-0.3.0/PyIncucyte.egg-info/entry_points.txt +3 -0
  7. pyincucyte-0.3.0/PyIncucyte.egg-info/requires.txt +13 -0
  8. pyincucyte-0.3.0/PyIncucyte.egg-info/top_level.txt +1 -0
  9. pyincucyte-0.3.0/README.md +321 -0
  10. pyincucyte-0.3.0/pyincucyte/__init__.py +113 -0
  11. pyincucyte-0.3.0/pyincucyte/__main__.py +8 -0
  12. pyincucyte-0.3.0/pyincucyte/cache.py +169 -0
  13. pyincucyte-0.3.0/pyincucyte/channels.py +89 -0
  14. pyincucyte-0.3.0/pyincucyte/cli.py +665 -0
  15. pyincucyte-0.3.0/pyincucyte/client.py +611 -0
  16. pyincucyte-0.3.0/pyincucyte/compat.py +68 -0
  17. pyincucyte-0.3.0/pyincucyte/config.py +130 -0
  18. pyincucyte-0.3.0/pyincucyte/engine.py +1667 -0
  19. pyincucyte-0.3.0/pyincucyte/errors.py +68 -0
  20. pyincucyte-0.3.0/pyincucyte/gui/__init__.py +5 -0
  21. pyincucyte-0.3.0/pyincucyte/gui/__main__.py +8 -0
  22. pyincucyte-0.3.0/pyincucyte/gui/app.py +1447 -0
  23. pyincucyte-0.3.0/pyincucyte/gui/dialogs.py +285 -0
  24. pyincucyte-0.3.0/pyincucyte/gui/theme.py +398 -0
  25. pyincucyte-0.3.0/pyincucyte/gui/widgets.py +620 -0
  26. pyincucyte-0.3.0/pyincucyte/manifest.py +162 -0
  27. pyincucyte-0.3.0/pyincucyte/models.py +446 -0
  28. pyincucyte-0.3.0/pyincucyte/options.py +491 -0
  29. pyincucyte-0.3.0/pyincucyte/state.py +190 -0
  30. pyincucyte-0.3.0/pyincucyte/watch.py +177 -0
  31. pyincucyte-0.3.0/pyincucyte/wells.py +114 -0
  32. pyincucyte-0.3.0/pyproject.toml +67 -0
  33. pyincucyte-0.3.0/setup.cfg +4 -0
  34. pyincucyte-0.3.0/tests/test_cache.py +153 -0
  35. pyincucyte-0.3.0/tests/test_cli.py +157 -0
  36. pyincucyte-0.3.0/tests/test_client.py +265 -0
  37. pyincucyte-0.3.0/tests/test_frame_counts.py +213 -0
  38. pyincucyte-0.3.0/tests/test_gui_threading.py +63 -0
  39. pyincucyte-0.3.0/tests/test_options.py +145 -0
  40. pyincucyte-0.3.0/tests/test_packaging.py +140 -0
  41. pyincucyte-0.3.0/tests/test_stack_downloads.py +236 -0
  42. pyincucyte-0.3.0/tests/test_time_window.py +196 -0
  43. pyincucyte-0.3.0/tests/test_wells_and_state.py +143 -0
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Jamie Malcolm
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its contributors
16
+ may be used to endorse or promote products derived from this software
17
+ without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,355 @@
1
+ Metadata-Version: 2.4
2
+ Name: PyIncucyte
3
+ Version: 0.3.0
4
+ Summary: Download Incucyte live-cell imaging TIFFs from Python, a CLI, or a desktop app.
5
+ Author: Jamie Malcolm
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/Jay2owe/PyIncucyteGUI
8
+ Project-URL: Source, https://github.com/Jay2owe/PyIncucyteGUI
9
+ Project-URL: Issues, https://github.com/Jay2owe/PyIncucyteGUI/issues
10
+ Keywords: incucyte,pyincucyte,microscopy,live-cell imaging,imagej,tiff,automation
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Environment :: Win32 (MS Windows)
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: requests>=2.31
24
+ Requires-Dist: urllib3>=2.0
25
+ Requires-Dist: Pillow>=10.0
26
+ Requires-Dist: numpy>=1.24
27
+ Requires-Dist: tifffile>=2023.7.10
28
+ Requires-Dist: pythonnet>=3.0; sys_platform == "win32"
29
+ Provides-Extra: dev
30
+ Requires-Dist: build>=1.2; extra == "dev"
31
+ Requires-Dist: twine>=5.0; extra == "dev"
32
+ Requires-Dist: pytest>=8.0; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # PyIncucyte
36
+
37
+ Download images off an Incucyte live-cell imaging system — from a desktop app, a
38
+ command line, or a Python script. It replaces click-by-click downloading with a
39
+ tool that knows what it already has and fetches only what is new.
40
+
41
+ Three front ends, one engine:
42
+
43
+ | You want to | Use |
44
+ |---|---|
45
+ | Pick a plate and grab the images | the desktop app — `pyincucyte gui` |
46
+ | Script it, or run it on a schedule | the CLI — `pyincucyte download …` |
47
+ | Call it from an analysis pipeline | the API — `from pyincucyte import IncucyteClient` |
48
+
49
+ ## Requirements
50
+
51
+ - Python 3.10+
52
+ - A network route to the instrument (at Imperial the Incucyte is on the internal
53
+ network only — it is not reachable from outside)
54
+ - Windows with the Incucyte desktop client installed, **for the login step only**.
55
+ The password is hashed by the vendor's own .NET assembly before it leaves the
56
+ machine, and only that hash is stored. Once a machine has logged in, the saved
57
+ credentials work without it.
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pip install PyIncucyte # from PyPI
63
+ pip install -e . # from this checkout
64
+ ```
65
+
66
+ One name throughout: the distribution, the import name and the command are all
67
+ `pyincucyte`. The desktop app is a module inside it, so `pyincucyte gui` and
68
+ `pyincucyte download ...` are the same program.
69
+
70
+ Installing does not replace `py-incucyte-gui`, the name this was published
71
+ under before 0.3. Uninstall that one if it is still on the machine:
72
+ `pip uninstall py-incucyte-gui`.
73
+
74
+ ---
75
+
76
+ ## The desktop app
77
+
78
+ ```bash
79
+ pyincucyte gui # or: pyincucyte-gui, python -m pyincucyte.gui
80
+ ```
81
+
82
+ Sign in, pick a vessel, paint the wells you want, choose a layout, press
83
+ **Download**. Everything long-running happens on a worker thread, so the window
84
+ stays usable and **Cancel** in the status bar always works.
85
+
86
+ Worth knowing:
87
+
88
+ - **Preview** (Ctrl+P) counts exactly what would be downloaded and lists the
89
+ filenames, without fetching a single image.
90
+ - **Scanned only** keeps just the wells the instrument actually imaged in the
91
+ most recent scan — wells with no data are shown dimmed on the plate.
92
+ - **Copy CLI command** (Tools menu, and in the confirm dialog) turns whatever is
93
+ on screen into the equivalent `pyincucyte` command, ready to paste into a
94
+ pipeline script or a scheduled task.
95
+ - **Presets** (File menu) save the whole recipe as JSON. The same file works as
96
+ `pyincucyte download --preset my-run.json` and as `ExportOptions.load(...)`.
97
+ - The plate picker takes click, drag-to-paint, shift-click for a block, and a
98
+ click on a row letter or column number to flip a whole line.
99
+ - Light and dark themes follow Windows, and **View → Toggle dark mode** overrides.
100
+ - Settings, window size and per-vessel well selections are remembered. Settings
101
+ saved by earlier versions are migrated on first run.
102
+
103
+ ## The command line
104
+
105
+ ```bash
106
+ pyincucyte probe # is the instrument reachable?
107
+ pyincucyte login # saves credentials
108
+ pyincucyte vessels # what is on the device
109
+ pyincucyte plan -v 38 -o ./images --start-from first # dry run
110
+
111
+ pyincucyte download -v 38 -o ./images --start-from first
112
+ pyincucyte download -v 38 -o ./images --wells D2-D5 --channels phase,green \
113
+ --layout time_stack
114
+ pyincucyte watch -v 38 -o ./images -i 10 --start-from first
115
+ ```
116
+
117
+ Useful anywhere: `--json` for machine-readable output, `--preset FILE` to load a
118
+ saved recipe, `--save-preset FILE` to write one, `--dry-run`, `--quiet`.
119
+
120
+ `pyincucyte manifest ./images` summarises what a folder already contains.
121
+
122
+ ### Time windows
123
+
124
+ `--start-from` and `--end-at` both take a date, a date *and time*, or a relative
125
+ offset. Both ends are inclusive.
126
+
127
+ | Written as | Means |
128
+ |---|---|
129
+ | `first` | the experiment's own first scan, at its real time of day |
130
+ | `today` / `now` | midnight this morning / this instant |
131
+ | `2026-03-05` | start: 00:00 that day. End: the whole of that day |
132
+ | `2026-03-05 14:30` | exactly that minute (`T` instead of a space also works) |
133
+ | `-48h` (start only) | a rolling window: 48 hours before now |
134
+ | `+72h` (end only) | 72 hours after the *start* |
135
+ | `-50f` (start only) | the last 50 frames |
136
+ | `+100f` (end only) | the first 100 frames from the start |
137
+
138
+ So the first three days of an experiment that began at 14:30 is
139
+ `--start-from first --end-at +72h`, and it correctly excludes that morning's
140
+ scans. Time units are `s`, `m`, `h`, `d`, `w`, and the sign is required so an
141
+ offset can never be mistaken for a date.
142
+
143
+ **Frame counts** measure the same window in scan times rather than clock time —
144
+ `-50f`, `+100 frames`, `-24 scans` all work. Use them when the *length of the
145
+ stack* is what matters, which is usually the case for a video: `--start-from
146
+ -100f` gives a 100-frame stack whatever the scan interval was, where `-48h`
147
+ gives however many frames happened to fall in two days.
148
+
149
+ A frame is one scan time, so the count is unaffected by how many wells or
150
+ channels you select — five frames of two channels is five timepoints and ten
151
+ files. Count from one end or the other, not both.
152
+
153
+ PyIncucyte walks the days backwards for `-50f` and stops as soon as it has
154
+ enough, so asking for the last 50 frames of a three-month run costs a couple of
155
+ metadata calls rather than ninety.
156
+
157
+ The instrument only lists scans one calendar day at a time, so PyIncucyte sweeps
158
+ whole days and then trims to your exact window.
159
+
160
+ ### Layouts
161
+
162
+ `--layout` replaces the old `--hyperstack` / `--time-stack` flag pair, which
163
+ still works.
164
+
165
+ | Layout | Axes | One file per |
166
+ |---|---|---|
167
+ | `separate` (default) | `YX` | well, channel and scan time |
168
+ | `channel_stack` | `CYX` | well and scan time |
169
+ | `time_stack` | `TYX` | well and channel |
170
+ | `time_channel_stack` | `TCYX` | well |
171
+
172
+ ### Options
173
+
174
+ | Flag | Description |
175
+ |------|-------------|
176
+ | `-v`, `--vessel` | Vessel ID — repeat for several |
177
+ | `-o`, `--output` | Output folder |
178
+ | `-w`, `--wells` | Well filter (`A1`, `A1,B3`, `A1-D4`, `all`) |
179
+ | `--vessel-wells ID:WELLS` | Per-vessel well filter (`-f` is the old spelling) |
180
+ | `-c`, `--channels` | `phase`, `green`/`color1`, `red`/`color2`, `all` |
181
+ | `--layout` | See the table above |
182
+ | `-s`, `--start-from` | `first`, `today`, `now`, a date/time, `-48h`, or `-50f` |
183
+ | `--end-at` | `now` (default), a date/time, `+72h`, or `+100f` |
184
+ | `-d`, `--date` | Shorthand for a single day |
185
+ | `-t`, `--scan-time` | Only scan times containing this text |
186
+ | `--workers` | Parallel fetches (default 4) |
187
+ | `--green-lut` / `--no-green-lut` | Recolour Phase as green RGB (display only) |
188
+ | `--state-scope` | `auto` (default), `folder`, `global`, `none` |
189
+ | `--cache` | `auto` (default), `always`, `never` — cache source payloads |
190
+ | `--no-manifest` | Skip writing the manifest and CSV index |
191
+ | `-i`, `--interval` | `watch` only: poll interval in minutes |
192
+
193
+ ---
194
+
195
+ ## The Python API
196
+
197
+ This is the part an automated pipeline should use.
198
+
199
+ ```python
200
+ from pyincucyte import IncucyteClient
201
+
202
+ with IncucyteClient.from_saved() as incucyte:
203
+ plan = incucyte.plan(
204
+ vessel=38, output="./run-01",
205
+ wells="A1-D6", channels="phase,green",
206
+ layout="time_channel_stack",
207
+ start_from="first", end_at="+72h") # the first three days
208
+
209
+ print(plan.summary())
210
+ # 1 vessel - 24 wells - 118 scan times - One ImageJ TCYX stack per well
211
+ # 24 output files - 5,664 source images - ~15.8 GB
212
+
213
+ result = incucyte.download(plan, progress=print)
214
+
215
+ for image in result.files:
216
+ segment(image.path,
217
+ well=image.well, # "A1"
218
+ channels=image.channels, # ["Phase", "GFP"] in stack order
219
+ axes=image.axes, # "TCYX"
220
+ timepoints=image.scan_times)
221
+ ```
222
+
223
+ `incucyte.fetch(...)` plans and downloads in one call. `start_from` and `end_at`
224
+ also accept `date` and `datetime` objects directly, and `plan.window` reports the
225
+ `(start, end)` the plan actually used — it is recorded in the manifest too.
226
+
227
+ ### What you get back
228
+
229
+ `DownloadResult` carries `.files` (typed `OutputFile` records), `.paths`,
230
+ `.errors`, `.cancelled`, `.bytes_total`, `.duration_seconds` and `.summary()`.
231
+ Failures are collected, not raised — one unreadable well never aborts a plate.
232
+
233
+ Each `OutputFile` knows its vessel, well, row/column, site, channel display
234
+ names, device channel numbers, scan times, ImageJ axis order, elapsed-time label
235
+ and size.
236
+
237
+ ### The manifest
238
+
239
+ Every download writes `pyincucyte-manifest.json` and `pyincucyte-index.csv` into
240
+ the output folder. **Read these instead of globbing the folder and re-parsing
241
+ filenames** — they already say which well, channel and timepoint every file is.
242
+ Watch mode merges into the same manifest, so it stays a complete index of the
243
+ folder however many polls filled it.
244
+
245
+ ```python
246
+ import json, pandas as pd
247
+ manifest = json.load(open("run-01/pyincucyte-manifest.json"))
248
+ index = pd.read_csv("run-01/pyincucyte-index.csv")
249
+ ```
250
+
251
+ ### Watching, without blocking
252
+
253
+ ```python
254
+ watcher = incucyte.watch(options, on_result=lambda r: analyse(r.paths))
255
+ ... # your pipeline carries on
256
+ watcher.stop(wait=30)
257
+ ```
258
+
259
+ `Watcher` also works as a context manager, and `run_forever()` blocks if that is
260
+ what you want.
261
+
262
+ ### Presets shared with the GUI
263
+
264
+ ```python
265
+ from pyincucyte import ExportOptions
266
+
267
+ options = ExportOptions.load("nightly.json") # saved from the GUI
268
+ result = incucyte.fetch(options.replace(output="./tonight"))
269
+ print(options.cli_command()) # the equivalent CLI line
270
+ ```
271
+
272
+ ### Why watch mode stays cheap
273
+
274
+ A time stack has to contain every frame, so one new scan invalidates the whole
275
+ file. Rebuilt naively, every poll would re-download the entire experiment — and
276
+ the cost grows with each hour of the run. PyIncucyte keeps the source payloads
277
+ in `.pyincucyte-cache/` inside the output folder the first time it fetches them,
278
+ so a rebuild is a disk read and only genuinely new frames touch the instrument.
279
+
280
+ It is on automatically for the time layouts, which are the ones that rebuild.
281
+ `--cache always` / `--cache never` (or `cache_payloads=` in `ExportOptions`)
282
+ override that; deleting the folder only costs time. `result.cache.summary()`
283
+ reports the hit rate.
284
+
285
+ ### Resume state
286
+
287
+ By default the ledger of what has already been fetched lives in the output
288
+ folder (`.pyincucyte-state.json`), so parallel experiments never collide and
289
+ moving a folder moves its history with it. `state_scope="global"` restores the
290
+ old shared file; `"none"` disables resume entirely.
291
+
292
+ ### Errors
293
+
294
+ Everything derives from `IncucyteError`, so a pipeline can wrap a whole run in
295
+ one `except`: `DeviceUnreachableError`, `AuthenticationError`,
296
+ `NotLoggedInError`, `TokenExpiredError`, `ApiError`, `VesselNotFoundError`,
297
+ `EncryptionUnavailableError`, `ExportError`.
298
+
299
+ ---
300
+
301
+ ## Layout of this repository
302
+
303
+ ```
304
+ PyIncucyte/
305
+ pyincucyte/
306
+ client.py IncucyteClient - the object a pipeline imports
307
+ options.py ExportOptions - the recipe shared by GUI, CLI and API
308
+ models.py Vessel, ExportPlan, DownloadResult, OutputFile
309
+ manifest.py the JSON manifest and CSV index
310
+ state.py resume ledger, scoped to the output folder
311
+ cache.py source-payload cache, so rebuilt stacks do not re-download
312
+ watch.py Watcher - poll and download in a background thread
313
+ engine.py wire-level REST and ImageJ TIFF writing
314
+ cli.py command line
315
+ compat.py the import names retired in 0.3
316
+ gui/ desktop app: theme.py, widgets.py, dialogs.py, app.py
317
+ tests/ run with: python -m pytest
318
+ ```
319
+
320
+ Nothing lives outside the package any more. The loose `incucyte_downloader.py`
321
+ and `incucyte_gui.py` modules are gone, and so is the second `py_incucyte_gui`
322
+ package; importing `pyincucyte` registers all three as aliases, so this still
323
+ resolves - to the very same module object, so monkeypatching behaves as before:
324
+
325
+ ```python
326
+ import pyincucyte
327
+ from incucyte_downloader import download_scan_images
328
+ ```
329
+
330
+ ### Where settings live
331
+
332
+ - Windows: `%APPDATA%\PyIncucyte`
333
+ - macOS/Linux: `$XDG_CONFIG_HOME/pyincucyte` or `~/.config/pyincucyte`
334
+ - Override with `PYINCUCYTE_HOME`
335
+
336
+ A folder left over from before the rename (`PyIncucyteGUI` / `pyincucytegui`)
337
+ keeps being used if it holds settings, and `PYINCUCYTEGUI_HOME` still works, so
338
+ upgrading does not log anybody out.
339
+
340
+ A source checkout that already has a `.tmp/` folder keeps using it.
341
+ `PYINCUCYTE_CLIENT_DIR` overrides where the Incucyte client install is looked for.
342
+
343
+ ## Build and publish
344
+
345
+ ```bash
346
+ python -m pip install --upgrade build twine
347
+ python -m build
348
+ python -m twine check dist/*
349
+ python -m twine upload dist/*
350
+ ```
351
+
352
+ ## License
353
+
354
+ BSD 3-Clause. Copyright (c) 2026, Jamie Malcolm. Ownership is personal rather
355
+ than lab or institute owned.