Skip to content

Commit b9108c9

Browse files
authored
Load Contexts in Analyser via entry points (#763)
* Install SXT dependencies as part of instrument server Dockerfile * Renamed 'SPAModularContext' to 'SPAContext' * Apply Context classes to the Analyser via entry points instead of direct imports to break dependency chains
1 parent cd967b5 commit b9108c9

6 files changed

Lines changed: 91 additions & 53 deletions

File tree

Dockerfiles/murfey-instrument-server

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ RUN apt-get update && \
3232
pip \
3333
build \
3434
importlib-metadata && \
35-
/venv/bin/python -m pip install /python-murfey
35+
/venv/bin/python -m pip install /python-murfey[sxt]
3636

3737

3838
# Transfer completed Murfey build to base image

pyproject.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ GitHub = "https://github.com/DiamondLightSource/python-murfey"
9696
"murfey.transfer" = "murfey.cli.transfer:run"
9797
[project.entry-points."murfey.config.extraction"]
9898
"murfey_machine" = "murfey.util.config:get_extended_machine_config"
99+
[project.entry-points."murfey.contexts"]
100+
AtlasContext = "murfey.client.contexts.atlas:AtlasContext"
101+
CLEMContext = "murfey.client.contexts.clem:CLEMContext"
102+
FIBContext = "murfey.client.contexts.fib:FIBContext"
103+
SPAContext = "murfey.client.contexts.spa:SPAContext"
104+
SPAMetadataContext = "murfey.client.contexts.spa_metadata:SPAMetadataContext"
105+
SXTContext = "murfey.client.contexts.sxt:SXTContext"
106+
TomographyContext = "murfey.client.contexts.tomo:TomographyContext"
107+
TomographyMetadataContext = "murfey.client.contexts.tomo_metadata:TomographyMetadataContext"
99108
[project.entry-points."murfey.workflows"]
100109
"atlas_update" = "murfey.workflows.register_atlas_update:run"
101110
"clem.align_and_merge" = "murfey.workflows.clem.align_and_merge:run"

src/murfey/client/analyser.py

Lines changed: 74 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,15 @@
88

99
from __future__ import annotations
1010

11+
import functools
1112
import logging
1213
import queue
1314
import threading
15+
from importlib.metadata import entry_points
1416
from pathlib import Path
1517
from typing import Type
1618

1719
from murfey.client.context import Context
18-
from murfey.client.contexts.atlas import AtlasContext
19-
from murfey.client.contexts.clem import CLEMContext
20-
from murfey.client.contexts.fib import FIBContext
21-
from murfey.client.contexts.spa import SPAModularContext
22-
from murfey.client.contexts.spa_metadata import SPAMetadataContext
23-
from murfey.client.contexts.sxt import SXTContext
24-
from murfey.client.contexts.tomo import TomographyContext
25-
from murfey.client.contexts.tomo_metadata import TomographyMetadataContext
2620
from murfey.client.destinations import find_longest_data_directory
2721
from murfey.client.instance_environment import MurfeyInstanceEnvironment
2822
from murfey.client.rsync import RSyncerUpdate, TransferResult
@@ -33,6 +27,23 @@
3327
logger = logging.getLogger("murfey.client.analyser")
3428

3529

30+
# Load the Context entry points as a list upon initialisation
31+
context_eps = list(entry_points(group="murfey.contexts"))
32+
33+
34+
@functools.lru_cache(maxsize=1)
35+
def _get_context(name: str):
36+
"""
37+
Load the desired context from the configured list of entry points.
38+
Returns None if the entry point is not found
39+
"""
40+
if context := [ep for ep in context_eps if ep.name == name]:
41+
return context[0]
42+
else:
43+
logger.warning(f"Could not find entry point for {name!r}")
44+
return None
45+
46+
3647
class Analyser(Observer):
3748
def __init__(
3849
self,
@@ -145,7 +156,9 @@ def _find_context(self, file_path: Path) -> bool:
145156
)
146157
)
147158
):
148-
self._context = CLEMContext(
159+
if (context := _get_context("CLEMContext")) is None:
160+
return False
161+
self._context = context.load()(
149162
"leica",
150163
self._basepath,
151164
self._murfey_config,
@@ -166,7 +179,9 @@ def _find_context(self, file_path: Path) -> bool:
166179
and "Sites" in file_path.parts
167180
)
168181
):
169-
self._context = FIBContext(
182+
if (context := _get_context("FIBContext")) is None:
183+
return False
184+
self._context = context.load()(
170185
"autotem",
171186
self._basepath,
172187
self._murfey_config,
@@ -183,7 +198,9 @@ def _find_context(self, file_path: Path) -> bool:
183198
all(path in file_path.parts for path in ("LayersData", "Layer"))
184199
)
185200
):
186-
self._context = FIBContext(
201+
if (context := _get_context("FIBContext")) is None:
202+
return False
203+
self._context = context.load()(
187204
"maps",
188205
self._basepath,
189206
self._murfey_config,
@@ -196,7 +213,9 @@ def _find_context(self, file_path: Path) -> bool:
196213
# Image metadata stored in "features.json" file
197214
file_path.name == "features.json" or ()
198215
):
199-
self._context = FIBContext(
216+
if (context := _get_context("FIBContext")) is None:
217+
return False
218+
self._context = context.load()(
200219
"meteor",
201220
self._basepath,
202221
self._murfey_config,
@@ -208,7 +227,9 @@ def _find_context(self, file_path: Path) -> bool:
208227
# SXT workflow checks
209228
# -----------------------------------------------------------------------------
210229
if file_path.suffix in (".txrm", ".xrm"):
211-
self._context = SXTContext(
230+
if (context := _get_context("SXTContext")) is None:
231+
return False
232+
self._context = context.load()(
212233
"zeiss",
213234
self._basepath,
214235
self._murfey_config,
@@ -220,7 +241,9 @@ def _find_context(self, file_path: Path) -> bool:
220241
# Tomography and SPA workflow checks
221242
# -----------------------------------------------------------------------------
222243
if "atlas" in file_path.parts:
223-
self._context = AtlasContext(
244+
if (context := _get_context("AtlasContext")) is None:
245+
return False
246+
self._context = context.load()(
224247
"serialem" if self._serialem else "epu",
225248
self._basepath,
226249
self._murfey_config,
@@ -229,7 +252,9 @@ def _find_context(self, file_path: Path) -> bool:
229252
return True
230253

231254
if "Metadata" in file_path.parts or file_path.name == "EpuSession.dm":
232-
self._context = SPAMetadataContext(
255+
if (context := _get_context("SPAMetadataContext")) is None:
256+
return False
257+
self._context = context.load()(
233258
"epu",
234259
self._basepath,
235260
self._murfey_config,
@@ -242,7 +267,9 @@ def _find_context(self, file_path: Path) -> bool:
242267
or "Thumbnails" in file_path.parts
243268
or file_path.name == "Session.dm"
244269
):
245-
self._context = TomographyMetadataContext(
270+
if (context := _get_context("TomographyMetadataContext")) is None:
271+
return False
272+
self._context = context.load()(
246273
"tomo",
247274
self._basepath,
248275
self._murfey_config,
@@ -263,7 +290,9 @@ def _find_context(self, file_path: Path) -> bool:
263290
]:
264291
if not self._context:
265292
logger.info("Acquisition software: EPU")
266-
self._context = SPAModularContext(
293+
if (context := _get_context("SPAContext")) is None:
294+
return False
295+
self._context = context.load()(
267296
"epu",
268297
self._basepath,
269298
self._murfey_config,
@@ -282,7 +311,9 @@ def _find_context(self, file_path: Path) -> bool:
282311
):
283312
if not self._context:
284313
logger.info("Acquisition software: tomo")
285-
self._context = TomographyContext(
314+
if (context := _get_context("TomographyContext")) is None:
315+
return False
316+
self._context = context.load()(
286317
"tomo",
287318
self._basepath,
288319
self._murfey_config,
@@ -322,7 +353,9 @@ def _analyse(self):
322353
or transferred_file.name == "EpuSession.dm"
323354
and not self._context
324355
):
325-
self._context = SPAMetadataContext(
356+
if not (context := _get_context("SPAMetadataContext")):
357+
continue
358+
self._context = context.load()(
326359
"epu",
327360
self._basepath,
328361
self._murfey_config,
@@ -334,7 +367,9 @@ def _analyse(self):
334367
or transferred_file.name == "Session.dm"
335368
and not self._context
336369
):
337-
self._context = TomographyMetadataContext(
370+
if not (context := _get_context("TomographyMetadataContext")):
371+
continue
372+
self._context = context.load()(
338373
"tomo",
339374
self._basepath,
340375
self._murfey_config,
@@ -364,12 +399,10 @@ def _analyse(self):
364399
elif transferred_file.suffix == ".mdoc":
365400
mdoc_for_reading = transferred_file
366401
if not self._context:
367-
valid_extension = self._find_extension(transferred_file)
368-
if not valid_extension:
402+
if not self._find_extension(transferred_file):
369403
logger.error(f"No extension found for {transferred_file}")
370404
continue
371-
found = self._find_context(transferred_file)
372-
if not found:
405+
if not self._find_context(transferred_file):
373406
logger.debug(
374407
f"Couldn't find context for {str(transferred_file)!r}"
375408
)
@@ -386,7 +419,7 @@ def _analyse(self):
386419
)
387420
except Exception as e:
388421
logger.error(f"Exception encountered: {e}")
389-
if not isinstance(self._context, AtlasContext):
422+
if "AtlasContext" not in str(self._context):
390423
if not dc_metadata:
391424
try:
392425
dc_metadata = self._context.gather_metadata(
@@ -417,31 +450,27 @@ def _analyse(self):
417450
)
418451
self.notify(dc_metadata)
419452

420-
# If a file with a CLEM context is identified, immediately post it
421-
elif isinstance(self._context, CLEMContext):
453+
# Contexts that can be immediately posted without additional work
454+
elif "CLEMContext" in str(self._context):
422455
logger.debug(
423-
f"File {transferred_file.name!r} will be processed as part of CLEM workflow"
456+
f"File {transferred_file.name!r} is part of CLEM workflow"
424457
)
425458
self.post_transfer(transferred_file)
426-
427-
elif isinstance(self._context, FIBContext):
459+
elif "FIBContext" in str(self._context):
428460
logger.debug(
429-
f"File {transferred_file.name!r} will be processed as part of the FIB workflow"
461+
f"File {transferred_file.name!r} is part of the FIB workflow"
430462
)
431463
self.post_transfer(transferred_file)
432-
433-
elif isinstance(self._context, SXTContext):
464+
elif "SXTContext" in str(self._context):
434465
logger.debug(f"File {transferred_file.name!r} is an SXT file")
435466
self.post_transfer(transferred_file)
436-
437-
elif isinstance(self._context, AtlasContext):
467+
elif "AtlasContext" in str(self._context):
438468
logger.debug(f"File {transferred_file.name!r} is part of the atlas")
439469
self.post_transfer(transferred_file)
440470

441471
# Handle files with tomography and SPA context differently
442472
elif not self._extension or self._unseen_xml:
443-
valid_extension = self._find_extension(transferred_file)
444-
if not valid_extension:
473+
if not self._find_extension(transferred_file):
445474
logger.error(f"No extension found for {transferred_file}")
446475
continue
447476
if self._extension:
@@ -480,14 +509,14 @@ def _analyse(self):
480509
self._context._acquisition_software
481510
)
482511
self.notify(dc_metadata)
483-
elif isinstance(
484-
self._context,
485-
(
486-
SPAModularContext,
487-
SPAMetadataContext,
488-
TomographyContext,
489-
TomographyMetadataContext,
490-
),
512+
elif any(
513+
context in str(self._context)
514+
for context in (
515+
"SPAContext",
516+
"SPAMetadataContext",
517+
"TomographyContext",
518+
"TomographyMetadataContext",
519+
)
491520
):
492521
context = str(self._context).split(" ")[0].split(".")[-1]
493522
logger.debug(

src/murfey/client/contexts/spa.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def _get_xml_list_index(key: str, xml_list: list) -> int:
5656
raise ValueError(f"Key not found in XML list: {key}")
5757

5858

59-
class SPAModularContext(Context):
59+
class SPAContext(Context):
6060
user_params = [
6161
ProcessingParameter(
6262
"dose_per_frame",

src/murfey/client/multigrid_control.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from murfey.client.analyser import Analyser
1313
from murfey.client.context import ensure_dcg_exists
14-
from murfey.client.contexts.spa import SPAModularContext
14+
from murfey.client.contexts.spa import SPAContext
1515
from murfey.client.contexts.tomo import TomographyContext
1616
from murfey.client.destinations import determine_default_destination
1717
from murfey.client.instance_environment import MurfeyInstanceEnvironment
@@ -560,7 +560,7 @@ def _start_dc(self, metadata_json):
560560
)
561561
log.info("Tomography processing flushed")
562562

563-
elif isinstance(context, SPAModularContext):
563+
elif isinstance(context, SPAContext):
564564
if self._environment.visit in source.parts:
565565
metadata_source = source
566566
else:

tests/client/test_analyser.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from murfey.client.contexts.atlas import AtlasContext
77
from murfey.client.contexts.clem import CLEMContext
88
from murfey.client.contexts.fib import FIBContext
9-
from murfey.client.contexts.spa import SPAModularContext
9+
from murfey.client.contexts.spa import SPAContext
1010
from murfey.client.contexts.spa_metadata import SPAMetadataContext
1111
from murfey.client.contexts.sxt import SXTContext
1212
from murfey.client.contexts.tomo import TomographyContext
@@ -28,8 +28,8 @@
2828
["visit/Batch/BatchPositionsList.xml", TomographyMetadataContext],
2929
["visit/Thumbnails/file.mrc", TomographyMetadataContext],
3030
# SPA
31-
["visit/FoilHole_01234_fractions.tiff", SPAModularContext],
32-
["visit/FoilHole_01234_EER.eer", SPAModularContext],
31+
["visit/FoilHole_01234_fractions.tiff", SPAContext],
32+
["visit/FoilHole_01234_EER.eer", SPAContext],
3333
# SPA metadata
3434
["atlas/atlas.mrc", AtlasContext],
3535
["visit/EpuSession.dm", SPAMetadataContext],
@@ -116,7 +116,7 @@ def test_find_context(file_and_context, tmp_path):
116116
# Checks for the specific workflow contexts
117117
if isinstance(analyser._context, TomographyContext):
118118
assert analyser.parameters_model == ProcessingParametersTomo
119-
if isinstance(analyser._context, SPAModularContext):
119+
if isinstance(analyser._context, SPAContext):
120120
assert analyser.parameters_model == ProcessingParametersSPA
121121

122122

0 commit comments

Comments
 (0)