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

1"""Instrument profile modules for customizing extraction behavior. 

2 

3This package contains instrument-specific profiles that customize metadata 

4extraction without modifying core extractor code. Profiles are automatically 

5discovered and registered during plugin initialization. 

6 

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()` 

12 

13Profile modules are loaded automatically - just add a new .py file to this 

14directory and it will be discovered during plugin initialization. 

15 

16Examples 

17-------- 

18Creating a new instrument profile (in profiles/my_instrument.py): 

19 

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""" 

33 

34from __future__ import annotations 

35 

36import importlib 

37import importlib.util 

38import logging 

39import pkgutil 

40from pathlib import Path 

41 

42_logger = logging.getLogger(__name__) 

43 

44__all__ = [ 

45 "register_all_profiles", 

46] 

47 

48 

49def register_all_profiles() -> None: 

50 """ 

51 Auto-discover and register all instrument profiles. 

52 

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) 

56 

57 Each profile module should register itself by calling 

58 get_profile_registry().register() at module level. 

59 

60 This function is called automatically during extractor plugin discovery. 

61 

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...") 

69 

70 # Load built-in profiles 

71 package_path = Path(__file__).parent 

72 profile_count = _load_profiles_from_directory(package_path, __name__) 

73 

74 # Load local profiles if configured 

75 try: 

76 from nexusLIMS import config # noqa: PLC0415 

77 

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 ) 

90 

91 _logger.info("Loaded %d total instrument profile modules", profile_count) 

92 

93 

94def _load_profiles_from_directory(directory: Path, module_prefix: str | None) -> int: 

95 """ 

96 Load all profile modules from a directory. 

97 

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). 

105 

106 Returns 

107 ------- 

108 int 

109 Number of profiles successfully loaded 

110 

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 

119 

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 

126 

127 try: 

128 # Create a unique module name for this local profile 

129 module_name = f"nexuslims_local_profile_{profile_file.stem}" 

130 

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 

139 

140 module = importlib.util.module_from_spec(spec) 

141 spec.loader.exec_module(module) 

142 

143 profile_count += 1 

144 _logger.debug("Loaded local profile: %s", profile_file.name) 

145 

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 

162 

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) 

168 

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 ) 

176 

177 return profile_count