Coverage for nexusLIMS/extractors/plugins/profiles/__init__.py: 100%
49 statements
« prev ^ index » next coverage.py v7.11.3, created at 2026-03-24 05:23 +0000
« prev ^ index » next coverage.py v7.11.3, created at 2026-03-24 05:23 +0000
1"""Instrument profile modules for customizing extraction behavior.
3This package contains instrument-specific profiles that customize metadata
4extraction without modifying core extractor code. Profiles are automatically
5discovered and registered during plugin initialization.
7Each profile module should:
81. Import InstrumentProfile and get_profile_registry
92. Define parser/transformation functions
103. Create an InstrumentProfile instance
114. Register it via `get_profile_registry().register()`
13Profile modules are loaded automatically - just add a new .py file to this
14directory and it will be discovered during plugin initialization.
16Examples
17--------
18Creating a new instrument profile (in profiles/my_instrument.py):
20>>> from nexusLIMS.extractors.base import InstrumentProfile
21>>> from nexusLIMS.extractors.profiles import get_profile_registry
22>>>
23>>> def custom_parser(metadata: dict, context) -> dict:
24... # Custom parsing logic
25... return metadata
26>>>
27>>> my_profile = InstrumentProfile(
28... instrument_id="My-Instrument-12345",
29... parsers={"custom": custom_parser},
30... )
31>>> get_profile_registry().register(my_profile)
32"""
34from __future__ import annotations
36import importlib
37import importlib.util
38import logging
39import pkgutil
40from pathlib import Path
42_logger = logging.getLogger(__name__)
44__all__ = [
45 "register_all_profiles",
46]
49def register_all_profiles() -> None:
50 """
51 Auto-discover and register all instrument profiles.
53 Loads profiles from two sources:
54 1. Built-in profiles (nexusLIMS/extractors/plugins/profiles/)
55 2. Local profiles (from NX_LOCAL_PROFILES_PATH env var, if set)
57 Each profile module should register itself by calling
58 get_profile_registry().register() at module level.
60 This function is called automatically during extractor plugin discovery.
62 Examples
63 --------
64 >>> from nexusLIMS.extractors.plugins.profiles import register_all_profiles
65 >>> register_all_profiles()
66 >>> # All built-in and local profiles are now registered
67 """
68 _logger.info("Discovering instrument profiles...")
70 # Load built-in profiles
71 package_path = Path(__file__).parent
72 profile_count = _load_profiles_from_directory(package_path, __name__)
74 # Load local profiles if configured
75 try:
76 from nexusLIMS import config # noqa: PLC0415
78 if config.settings.NX_LOCAL_PROFILES_PATH:
79 local_path_obj = config.settings.NX_LOCAL_PROFILES_PATH
80 _logger.info("Loading local profiles from: %s", local_path_obj)
81 local_count = _load_profiles_from_directory(
82 local_path_obj, module_prefix=None
83 )
84 profile_count += local_count
85 except Exception:
86 _logger.debug(
87 "NexusLIMS config unavailable; skipping local profile loading "
88 "(built-in profiles still active)"
89 )
91 _logger.info("Loaded %d total instrument profile modules", profile_count)
94def _load_profiles_from_directory(directory: Path, module_prefix: str | None) -> int:
95 """
96 Load all profile modules from a directory.
98 Parameters
99 ----------
100 directory
101 Directory containing profile modules
102 module_prefix
103 Module name prefix for package-based imports (built-in profiles).
104 If None, profiles are loaded as standalone files (local profiles).
106 Returns
107 -------
108 int
109 Number of profiles successfully loaded
111 Notes
112 -----
113 Built-in profiles are loaded using Python's standard import system
114 (pkgutil.walk_packages), while local profiles are loaded directly
115 from files using importlib.util. This allows local profiles to exist
116 outside the package structure without needing to be installed.
117 """
118 profile_count = 0
120 if module_prefix is None:
121 # Load local profiles as standalone Python files
122 for profile_file in directory.glob("*.py"):
123 # Skip private modules
124 if profile_file.name.startswith("_"):
125 continue
127 try:
128 # Create a unique module name for this local profile
129 module_name = f"nexuslims_local_profile_{profile_file.stem}"
131 # Load the profile file as a module
132 spec = importlib.util.spec_from_file_location(module_name, profile_file)
133 if spec is None or spec.loader is None:
134 _logger.warning(
135 "Failed to create module spec for local profile: %s",
136 profile_file,
137 )
138 continue
140 module = importlib.util.module_from_spec(spec)
141 spec.loader.exec_module(module)
143 profile_count += 1
144 _logger.debug("Loaded local profile: %s", profile_file.name)
146 except Exception as e:
147 _logger.warning(
148 "Failed to load local profile '%s': %s",
149 profile_file,
150 e,
151 exc_info=True,
152 )
153 else:
154 # Load built-in profiles as package modules
155 for _finder, module_name, _ispkg in pkgutil.walk_packages(
156 [str(directory)],
157 prefix=f"{module_prefix}.",
158 ):
159 # Skip __pycache__ and this __init__ module
160 if "__pycache__" in module_name or module_name == module_prefix:
161 continue
163 try:
164 # Import the module - this triggers profile registration
165 importlib.import_module(module_name)
166 profile_count += 1
167 _logger.debug("Loaded built-in profile module: %s", module_name)
169 except Exception as e:
170 _logger.warning(
171 "Failed to load built-in profile module '%s': %s",
172 module_name,
173 e,
174 exc_info=True,
175 )
177 return profile_count