88
99from __future__ import annotations
1010
11+ import functools
1112import logging
1213import queue
1314import threading
15+ from importlib .metadata import entry_points
1416from pathlib import Path
1517from typing import Type
1618
1719from 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
2620from murfey .client .destinations import find_longest_data_directory
2721from murfey .client .instance_environment import MurfeyInstanceEnvironment
2822from murfey .client .rsync import RSyncerUpdate , TransferResult
3327logger = 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+
3647class 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 (
0 commit comments