dirac-cwl 1.0.2__py3-none-any.whl
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.
- dirac_cwl/__init__.py +28 -0
- dirac_cwl/commands/__init__.py +5 -0
- dirac_cwl/commands/core.py +37 -0
- dirac_cwl/commands/download_config.py +22 -0
- dirac_cwl/commands/group_outputs.py +32 -0
- dirac_cwl/core/__init__.py +1 -0
- dirac_cwl/core/exceptions.py +5 -0
- dirac_cwl/core/utility.py +41 -0
- dirac_cwl/data_management_mocks/data_manager.py +99 -0
- dirac_cwl/data_management_mocks/file_catalog.py +132 -0
- dirac_cwl/data_management_mocks/sandbox.py +89 -0
- dirac_cwl/execution_hooks/__init__.py +40 -0
- dirac_cwl/execution_hooks/core.py +342 -0
- dirac_cwl/execution_hooks/plugins/__init__.py +16 -0
- dirac_cwl/execution_hooks/plugins/core.py +58 -0
- dirac_cwl/execution_hooks/registry.py +209 -0
- dirac_cwl/job/__init__.py +249 -0
- dirac_cwl/job/job_wrapper.py +375 -0
- dirac_cwl/job/job_wrapper_template.py +56 -0
- dirac_cwl/job/submission_clients.py +166 -0
- dirac_cwl/modules/crypto.py +96 -0
- dirac_cwl/modules/pi_gather.py +41 -0
- dirac_cwl/modules/pi_simulate.py +33 -0
- dirac_cwl/production/__init__.py +200 -0
- dirac_cwl/submission_models.py +157 -0
- dirac_cwl/transformation/__init__.py +203 -0
- dirac_cwl-1.0.2.dist-info/METADATA +285 -0
- dirac_cwl-1.0.2.dist-info/RECORD +32 -0
- dirac_cwl-1.0.2.dist-info/WHEEL +5 -0
- dirac_cwl-1.0.2.dist-info/entry_points.txt +8 -0
- dirac_cwl-1.0.2.dist-info/licenses/LICENSE +674 -0
- dirac_cwl-1.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Core metadata framework for DIRAC CWL integration.
|
|
2
|
+
|
|
3
|
+
This module provides the foundational classes and interfaces for the extensible
|
|
4
|
+
metadata plugin system in DIRAC/DIRACX.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import (
|
|
14
|
+
Any,
|
|
15
|
+
ClassVar,
|
|
16
|
+
Dict,
|
|
17
|
+
List,
|
|
18
|
+
Mapping,
|
|
19
|
+
Optional,
|
|
20
|
+
Self,
|
|
21
|
+
Sequence,
|
|
22
|
+
TypeVar,
|
|
23
|
+
Union,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from DIRAC.DataManagementSystem.Client.DataManager import ( # type: ignore[import-untyped]
|
|
27
|
+
DataManager,
|
|
28
|
+
)
|
|
29
|
+
from DIRACCommon.Core.Utilities.ReturnValues import ( # type: ignore[import-untyped]
|
|
30
|
+
returnSingleResult,
|
|
31
|
+
)
|
|
32
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
|
33
|
+
|
|
34
|
+
from dirac_cwl.commands import PostProcessCommand, PreProcessCommand
|
|
35
|
+
from dirac_cwl.data_management_mocks.data_manager import MockDataManager
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
# TypeVar for generic class methods
|
|
40
|
+
T = TypeVar("T", bound="SchedulingHint")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ExecutionHooksBasePlugin(BaseModel):
|
|
44
|
+
"""Base class for all runtime plugin models with execution hooks.
|
|
45
|
+
|
|
46
|
+
This class uses composition instead of inheritance for data catalog operations,
|
|
47
|
+
providing better separation of concerns and flexibility.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
model_config = ConfigDict(
|
|
51
|
+
extra="ignore",
|
|
52
|
+
validate_assignment=True,
|
|
53
|
+
arbitrary_types_allowed=True,
|
|
54
|
+
json_schema_extra={
|
|
55
|
+
"title": "DIRAC Metadata Model",
|
|
56
|
+
"description": "Base metadata model for DIRAC jobs",
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Class-level metadata for plugin discovery
|
|
61
|
+
vo: ClassVar[Optional[str]] = None
|
|
62
|
+
version: ClassVar[str] = "1.0.0"
|
|
63
|
+
description: ClassVar[str] = "Base metadata model"
|
|
64
|
+
|
|
65
|
+
output_paths: Dict[str, Any] = {}
|
|
66
|
+
output_sandbox: list[str] = []
|
|
67
|
+
output_se: list[str] = []
|
|
68
|
+
|
|
69
|
+
_datamanager: DataManager = PrivateAttr(default_factory=DataManager)
|
|
70
|
+
|
|
71
|
+
def __init__(self, **kwargs):
|
|
72
|
+
"""Initialize the execution hooks base plugin.
|
|
73
|
+
|
|
74
|
+
:param kwargs: Additional keyword arguments passed to the parent class.
|
|
75
|
+
"""
|
|
76
|
+
super().__init__(**kwargs)
|
|
77
|
+
if os.getenv("DIRAC_PROTO_LOCAL") == "1":
|
|
78
|
+
self._datamanager = MockDataManager()
|
|
79
|
+
|
|
80
|
+
_preprocess_commands: List[type[PreProcessCommand]] = PrivateAttr(default=[])
|
|
81
|
+
_postprocess_commands: List[type[PostProcessCommand]] = PrivateAttr(default=[])
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def preprocess_commands(self) -> List[type[PreProcessCommand]]:
|
|
85
|
+
"""Get the list of pre-processing commands."""
|
|
86
|
+
return self._preprocess_commands
|
|
87
|
+
|
|
88
|
+
@preprocess_commands.setter
|
|
89
|
+
def preprocess_commands(self, value: List[type[PreProcessCommand]]) -> None:
|
|
90
|
+
"""Set the list of pre-processing commands."""
|
|
91
|
+
self._preprocess_commands = value
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def postprocess_commands(self) -> List[type[PostProcessCommand]]:
|
|
95
|
+
"""Get the list of post-processing commands."""
|
|
96
|
+
return self._postprocess_commands
|
|
97
|
+
|
|
98
|
+
@postprocess_commands.setter
|
|
99
|
+
def postprocess_commands(self, value: List[type[PostProcessCommand]]) -> None:
|
|
100
|
+
"""Set the list of post-processing commands."""
|
|
101
|
+
self._postprocess_commands = value
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def name(cls) -> str:
|
|
105
|
+
"""Auto-derive hook plugin identifier from class name."""
|
|
106
|
+
return cls.__name__
|
|
107
|
+
|
|
108
|
+
def store_output(
|
|
109
|
+
self,
|
|
110
|
+
outputs: dict[str, str | Path | Sequence[str | Path]],
|
|
111
|
+
**kwargs: Any,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Store an output file or set of files via the appropriate storage interface.
|
|
114
|
+
|
|
115
|
+
:param dict[str, str | Path | Sequence[str | Path]] outputs:
|
|
116
|
+
Dictionary containing the path or list of paths to the source file(s) to be stored
|
|
117
|
+
for each cwl output.
|
|
118
|
+
:param Any **kwargs:
|
|
119
|
+
Additional keyword arguments for extensibility.
|
|
120
|
+
"""
|
|
121
|
+
for output_name, src_path in outputs.items():
|
|
122
|
+
logger.info("Storing output %s, with source %s", output_name, src_path)
|
|
123
|
+
|
|
124
|
+
if not src_path:
|
|
125
|
+
raise RuntimeError(f"src_path parameter required for filesystem storage of {output_name}")
|
|
126
|
+
|
|
127
|
+
lfn = self.output_paths.get(output_name, None)
|
|
128
|
+
|
|
129
|
+
if lfn:
|
|
130
|
+
if isinstance(src_path, str) or isinstance(src_path, Path):
|
|
131
|
+
src_path = [src_path]
|
|
132
|
+
for src in src_path:
|
|
133
|
+
file_lfn = Path(lfn) / Path(src).name
|
|
134
|
+
res = None
|
|
135
|
+
for se in self.output_se:
|
|
136
|
+
res = returnSingleResult(self._datamanager.putAndRegister(str(file_lfn), src, se))
|
|
137
|
+
if res["OK"]:
|
|
138
|
+
logger.info("Successfully saved file %s with LFN %s", src, file_lfn)
|
|
139
|
+
break
|
|
140
|
+
if res and not res["OK"]:
|
|
141
|
+
raise RuntimeError(f"Could not save file {src} with LFN {str(lfn)} : {res['Message']}")
|
|
142
|
+
|
|
143
|
+
def get_input_query(self, input_name: str, **kwargs: Any) -> Union[Path, List[Path], None]:
|
|
144
|
+
"""Generate LFN-based input query path.
|
|
145
|
+
|
|
146
|
+
Accepts and ignores extra kwargs for interface compatibility.
|
|
147
|
+
"""
|
|
148
|
+
# Build LFN: /query_root/vo/campaign/site/data_type/input_name
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def get_schema_info(cls) -> Dict[str, Any]:
|
|
153
|
+
"""Get schema information for this metadata model."""
|
|
154
|
+
return {
|
|
155
|
+
"hook_plugin": cls.name(),
|
|
156
|
+
"vo": cls.vo,
|
|
157
|
+
"version": cls.version,
|
|
158
|
+
"description": cls.description,
|
|
159
|
+
"schema": cls.model_json_schema(),
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Hint(ABC):
|
|
164
|
+
"""Base class for all DIRAC hints and requirements models."""
|
|
165
|
+
|
|
166
|
+
@classmethod
|
|
167
|
+
@abstractmethod
|
|
168
|
+
def from_cwl(cls, cwl_object: Any) -> "Hint":
|
|
169
|
+
"""Extract hint information from a CWL object."""
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class SchedulingHint(BaseModel, Hint):
|
|
174
|
+
"""Descriptor for job execution configuration."""
|
|
175
|
+
|
|
176
|
+
model_config = ConfigDict(extra="forbid", validate_assignment=True)
|
|
177
|
+
|
|
178
|
+
platform: Optional[str] = Field(default=None, description="Target platform (e.g., 'DIRAC', 'DIRACX')")
|
|
179
|
+
|
|
180
|
+
priority: int = Field(default=10, description="Job priority (higher values = higher priority)")
|
|
181
|
+
|
|
182
|
+
sites: Optional[List[str]] = Field(default=None, description="Candidate execution sites")
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def from_cwl(cls: type[T], cwl_object: Any) -> T:
|
|
186
|
+
"""Extract task descriptor from CWL hints."""
|
|
187
|
+
descriptor = cls()
|
|
188
|
+
|
|
189
|
+
hints = getattr(cwl_object, "hints", []) or []
|
|
190
|
+
for hint in hints:
|
|
191
|
+
if hint.get("class") == "dirac:Scheduling":
|
|
192
|
+
hint_data = {k: v for k, v in hint.items() if k != "class"}
|
|
193
|
+
descriptor = descriptor.model_copy(update=hint_data)
|
|
194
|
+
|
|
195
|
+
return descriptor
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class ExecutionHooksHint(BaseModel, Hint):
|
|
199
|
+
"""Descriptor for data management configuration in CWL hints.
|
|
200
|
+
|
|
201
|
+
This class represents the serializable data management configuration that
|
|
202
|
+
can be embedded in CWL hints and later instantiated into concrete
|
|
203
|
+
metadata models.
|
|
204
|
+
|
|
205
|
+
Enhanced with submission functionality for DIRAC CWL integration.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
model_config = ConfigDict(
|
|
209
|
+
extra="allow", # Allow vo-specific fields
|
|
210
|
+
validate_assignment=True,
|
|
211
|
+
json_schema_extra={
|
|
212
|
+
"title": "DIRAC Data Manager",
|
|
213
|
+
"description": "Data management configuration for DIRAC jobs",
|
|
214
|
+
},
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
hook_plugin: str = Field(
|
|
218
|
+
default="QueryBasedPlugin",
|
|
219
|
+
description="Registry key for the metadata implementation class",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Enhanced fields for submission functionality
|
|
223
|
+
configuration: Dict[str, Any] = Field(
|
|
224
|
+
default_factory=dict, description="Additional parameters for metadata plugins"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
output_paths: Dict[str, Any] = Field(default_factory=dict, description="LFNs for outputs on the Data Catalog")
|
|
228
|
+
|
|
229
|
+
output_sandbox: list[str] = Field(
|
|
230
|
+
default_factory=list,
|
|
231
|
+
description="List of the outputs stored in the output sandbox",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
output_se: list[str] = Field(
|
|
235
|
+
default_factory=lambda: ["SE-USER"],
|
|
236
|
+
description="List of Storage Elements that can be used to store the outputs",
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
def model_copy(
|
|
240
|
+
self,
|
|
241
|
+
update: Optional[Mapping[str, Any]] = None,
|
|
242
|
+
*,
|
|
243
|
+
deep: bool = False,
|
|
244
|
+
) -> Self:
|
|
245
|
+
"""Enhanced model copy with intelligent merging of dict fields (including configuration)."""
|
|
246
|
+
if update is None:
|
|
247
|
+
update = {}
|
|
248
|
+
else:
|
|
249
|
+
update = dict(update)
|
|
250
|
+
|
|
251
|
+
merged_update = {}
|
|
252
|
+
for key, value in update.items():
|
|
253
|
+
if hasattr(self, key) and isinstance(getattr(self, key), dict) and isinstance(value, dict):
|
|
254
|
+
existing_value = getattr(self, key).copy()
|
|
255
|
+
existing_value.update(value)
|
|
256
|
+
merged_update[key] = existing_value
|
|
257
|
+
else:
|
|
258
|
+
merged_update[key] = value
|
|
259
|
+
|
|
260
|
+
return super().model_copy(update=merged_update, deep=deep)
|
|
261
|
+
|
|
262
|
+
def to_runtime(self, submitted: Optional[Any] = None) -> "ExecutionHooksBasePlugin":
|
|
263
|
+
"""Build and instantiate the runtime metadata implementation.
|
|
264
|
+
|
|
265
|
+
The returned object is an instance of :class:`ExecutionHooksBasePlugin` created
|
|
266
|
+
by the metadata registry. The instantiation parameters are constructed
|
|
267
|
+
by merging, in order:
|
|
268
|
+
|
|
269
|
+
1. Input defaults declared on the CWL task (if ``submitted`` is provided).
|
|
270
|
+
2. The first set of CWL parameter overrides (``submitted.parameters``),
|
|
271
|
+
if present.
|
|
272
|
+
3. The descriptor's ``configuration``.
|
|
273
|
+
|
|
274
|
+
During merging, keys are normalized from dash-case to snake_case to
|
|
275
|
+
align with typical Python argument names used by runtime implementations.
|
|
276
|
+
|
|
277
|
+
:param submitted: Optional submission context used to resolve CWL input defaults
|
|
278
|
+
and parameter overrides.
|
|
279
|
+
:type submitted: JobSubmissionModel | None
|
|
280
|
+
:return: Runtime plugin implementation instantiated from the registry.
|
|
281
|
+
:rtype: ExecutionHooksBasePlugin
|
|
282
|
+
"""
|
|
283
|
+
# Import here to avoid circular imports
|
|
284
|
+
from .registry import get_registry
|
|
285
|
+
|
|
286
|
+
# Quick helper to convert dash-case to snake_case without importing utils
|
|
287
|
+
def _dash_to_snake(s: str) -> str:
|
|
288
|
+
return s.replace("-", "_")
|
|
289
|
+
|
|
290
|
+
if submitted is None:
|
|
291
|
+
descriptor = ExecutionHooksHint(
|
|
292
|
+
hook_plugin=self.hook_plugin,
|
|
293
|
+
output_paths=self.output_paths,
|
|
294
|
+
output_sandbox=self.output_sandbox,
|
|
295
|
+
output_se=self.output_se,
|
|
296
|
+
**self.configuration,
|
|
297
|
+
)
|
|
298
|
+
return get_registry().instantiate_plugin(descriptor)
|
|
299
|
+
|
|
300
|
+
# Build inputs from task defaults and parameter overrides
|
|
301
|
+
inputs: dict[str, Any] = {}
|
|
302
|
+
for inp in submitted.task.inputs:
|
|
303
|
+
input_name = inp.id.split("#")[-1].split("/")[-1]
|
|
304
|
+
input_value = getattr(inp, "default", None)
|
|
305
|
+
params_list = getattr(submitted, "parameters", None)
|
|
306
|
+
if params_list and params_list[0]:
|
|
307
|
+
input_value = params_list[0].cwl.get(input_name, input_value)
|
|
308
|
+
inputs[input_name] = input_value
|
|
309
|
+
|
|
310
|
+
# Merge with explicit configuration
|
|
311
|
+
if self.configuration:
|
|
312
|
+
inputs.update(self.configuration)
|
|
313
|
+
|
|
314
|
+
params = {_dash_to_snake(key): value for key, value in inputs.items()}
|
|
315
|
+
|
|
316
|
+
descriptor = ExecutionHooksHint(
|
|
317
|
+
hook_plugin=self.hook_plugin,
|
|
318
|
+
output_paths=self.output_paths,
|
|
319
|
+
output_sandbox=self.output_sandbox,
|
|
320
|
+
output_se=self.output_se,
|
|
321
|
+
**params,
|
|
322
|
+
)
|
|
323
|
+
return get_registry().instantiate_plugin(descriptor)
|
|
324
|
+
|
|
325
|
+
@classmethod
|
|
326
|
+
def from_cwl(cls, cwl_object: Any) -> Self:
|
|
327
|
+
"""Extract metadata descriptor from CWL object using Hint interface."""
|
|
328
|
+
descriptor = cls()
|
|
329
|
+
hints = getattr(cwl_object, "hints", []) or []
|
|
330
|
+
for hint in hints:
|
|
331
|
+
if hint.get("class") == "dirac:ExecutionHooks":
|
|
332
|
+
hint_data = {k: v for k, v in hint.items() if k != "class"}
|
|
333
|
+
descriptor = descriptor.model_copy(update=hint_data)
|
|
334
|
+
return descriptor
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class TransformationExecutionHooksHint(ExecutionHooksHint):
|
|
338
|
+
"""Extended data manager for transformations."""
|
|
339
|
+
|
|
340
|
+
group_size: Optional[Dict[str, int]] = Field(
|
|
341
|
+
default=None, description="Input grouping configuration for transformation jobs"
|
|
342
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Plugin package initialization.
|
|
2
|
+
|
|
3
|
+
This module ensures that core plugins are automatically registered
|
|
4
|
+
when the metadata system is imported.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .core import (
|
|
8
|
+
QueryBasedPlugin,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# Plugins will be auto-registered through the metaclass or discovery system
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
# Core plugins
|
|
15
|
+
"QueryBasedPlugin",
|
|
16
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Core DIRAC metadata models.
|
|
2
|
+
|
|
3
|
+
This module contains the standard metadata models provided by DIRAC core.
|
|
4
|
+
These serve as examples and provide basic functionality for common use cases.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, ClassVar, List, Optional, Union
|
|
12
|
+
|
|
13
|
+
from pydantic import Field
|
|
14
|
+
|
|
15
|
+
from ..core import (
|
|
16
|
+
ExecutionHooksBasePlugin,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class QueryBasedPlugin(ExecutionHooksBasePlugin):
|
|
23
|
+
"""Metadata plugin using LFN-based data catalog for structured data discovery.
|
|
24
|
+
|
|
25
|
+
This plugin demonstrates filesystem-based data organization using
|
|
26
|
+
Logical File Names (LFNs) with campaign, site, and data type parameters.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
description: ClassVar[str] = "LFN-based metadata for structured data discovery"
|
|
30
|
+
|
|
31
|
+
# LFN parameters
|
|
32
|
+
query_root: str = Field(default="/grid/data", description="Base path for LFN structure")
|
|
33
|
+
site: Optional[str] = Field(default=None, description="Site identifier for LFN path")
|
|
34
|
+
campaign: Optional[str] = Field(default=None, description="Campaign name for LFN path")
|
|
35
|
+
data_type: Optional[str] = Field(default=None, description="Data type classification")
|
|
36
|
+
|
|
37
|
+
def get_input_query(self, input_name: str, **kwargs: Any) -> Union[Path, List[Path], None]:
|
|
38
|
+
"""Generate LFN-based input query path.
|
|
39
|
+
|
|
40
|
+
Accepts and ignores extra kwargs for interface compatibility.
|
|
41
|
+
"""
|
|
42
|
+
# Build LFN: /query_root/vo/campaign/site/data_type/input_name
|
|
43
|
+
path_parts = []
|
|
44
|
+
|
|
45
|
+
if self.vo:
|
|
46
|
+
path_parts.append(self.vo)
|
|
47
|
+
|
|
48
|
+
if self.campaign:
|
|
49
|
+
path_parts.append(self.campaign)
|
|
50
|
+
if self.site:
|
|
51
|
+
path_parts.append(self.site)
|
|
52
|
+
if self.data_type:
|
|
53
|
+
path_parts.append(self.data_type)
|
|
54
|
+
|
|
55
|
+
if len(path_parts) > 0: # More than just VO
|
|
56
|
+
return Path(self.query_root) / Path(*path_parts) / Path(input_name)
|
|
57
|
+
|
|
58
|
+
return Path(self.query_root) / Path(input_name)
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Enhanced plugin registry for metadata models.
|
|
2
|
+
|
|
3
|
+
This module provides a sophisticated plugin discovery and registration system
|
|
4
|
+
for DIRAC metadata models, supporting virtual organization-specific extensions and
|
|
5
|
+
automatic discovery.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from importlib.metadata import entry_points
|
|
12
|
+
from typing import Any, Dict, List, Optional, Type
|
|
13
|
+
|
|
14
|
+
from .core import ExecutionHooksBasePlugin, ExecutionHooksHint
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ExecutionHooksPluginRegistry:
|
|
20
|
+
"""
|
|
21
|
+
Registry for execution hooks plugins.
|
|
22
|
+
|
|
23
|
+
This class manages the registration and retrieval of execution hooks plugins
|
|
24
|
+
for different steps in CWL workflows. Plugins are registered using
|
|
25
|
+
entry points and can be retrieved by name.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
"""Initialize the execution hooks registry."""
|
|
30
|
+
self._plugins: Dict[str, Type[ExecutionHooksBasePlugin]] = {}
|
|
31
|
+
self._vo_plugins: Dict[str, Dict[str, Type[ExecutionHooksBasePlugin]]] = {}
|
|
32
|
+
self._plugin_info: Dict[str, Dict[str, Any]] = {}
|
|
33
|
+
|
|
34
|
+
def register_plugin(self, plugin_class: Type[ExecutionHooksBasePlugin], override: bool = False) -> None:
|
|
35
|
+
"""Register a metadata plugin.
|
|
36
|
+
|
|
37
|
+
:param plugin_class: The metadata model class to register.
|
|
38
|
+
:type plugin_class: Type[ExecutionHooksBasePlugin]
|
|
39
|
+
:param override: Whether to override existing registrations, by default False.
|
|
40
|
+
:type override: bool
|
|
41
|
+
:raises ValueError: If plugin is already registered and override=False.
|
|
42
|
+
"""
|
|
43
|
+
if not issubclass(plugin_class, ExecutionHooksBasePlugin):
|
|
44
|
+
raise ValueError(f"Plugin {plugin_class} must inherit from ExecutionHooksBasePlugin")
|
|
45
|
+
|
|
46
|
+
plugin_key = plugin_class.name()
|
|
47
|
+
vo = plugin_class.vo
|
|
48
|
+
|
|
49
|
+
# Check for conflicts
|
|
50
|
+
if plugin_key in self._plugins and not override:
|
|
51
|
+
existing = self._plugins[plugin_key]
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"Plugin '{plugin_key}' already registered by {existing.__module__}.{existing.__name__}. "
|
|
54
|
+
f"Use override=True to replace."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Register globally
|
|
58
|
+
self._plugins[plugin_key] = plugin_class
|
|
59
|
+
self._plugin_info[plugin_key] = plugin_class.get_schema_info()
|
|
60
|
+
|
|
61
|
+
# Register by VO if specified
|
|
62
|
+
if vo:
|
|
63
|
+
if vo not in self._vo_plugins:
|
|
64
|
+
self._vo_plugins[vo] = {}
|
|
65
|
+
self._vo_plugins[vo][plugin_key] = plugin_class
|
|
66
|
+
|
|
67
|
+
vo_suffix = f" (VO: {vo})" if vo else ""
|
|
68
|
+
logger.info(
|
|
69
|
+
"Registered metadata plugin '%s' from %s.%s%s",
|
|
70
|
+
plugin_key,
|
|
71
|
+
plugin_class.__module__,
|
|
72
|
+
plugin_class.__name__,
|
|
73
|
+
vo_suffix,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def get_plugin(self, plugin_key: str, vo: Optional[str] = None) -> Optional[Type[ExecutionHooksBasePlugin]]:
|
|
77
|
+
"""Get a registered plugin.
|
|
78
|
+
|
|
79
|
+
:param plugin_key: The plugin identifier.
|
|
80
|
+
:type plugin_key: str
|
|
81
|
+
:param vo: Virtual Organization namespace to search first, by default None.
|
|
82
|
+
:type vo: Optional[str]
|
|
83
|
+
:return: The plugin class or None if not found.
|
|
84
|
+
:rtype: Optional[Type[ExecutionHooksBasePlugin]]
|
|
85
|
+
"""
|
|
86
|
+
# Try VO-specific first if specified
|
|
87
|
+
if vo and vo in self._vo_plugins:
|
|
88
|
+
if plugin_key in self._vo_plugins[vo]:
|
|
89
|
+
return self._vo_plugins[vo][plugin_key]
|
|
90
|
+
|
|
91
|
+
# Fall back to global registry
|
|
92
|
+
return self._plugins.get(plugin_key)
|
|
93
|
+
|
|
94
|
+
def instantiate_plugin(self, descriptor: ExecutionHooksHint, **kwargs: Any) -> ExecutionHooksBasePlugin:
|
|
95
|
+
"""Instantiate a metadata plugin from a descriptor.
|
|
96
|
+
|
|
97
|
+
:param descriptor: The data manager containing configuration.
|
|
98
|
+
:type descriptor: ExecutionHooksHint
|
|
99
|
+
:param kwargs: Additional parameters to pass to the plugin constructor.
|
|
100
|
+
:type kwargs: Any
|
|
101
|
+
:return: Instantiated metadata model.
|
|
102
|
+
:rtype: ExecutionHooksBasePlugin
|
|
103
|
+
:raises KeyError: If the requested plugin is not registered.
|
|
104
|
+
:raises ValueError: If plugin instantiation fails.
|
|
105
|
+
"""
|
|
106
|
+
plugin_class = self.get_plugin(descriptor.hook_plugin)
|
|
107
|
+
|
|
108
|
+
if plugin_class is None:
|
|
109
|
+
available = self.list_plugins()
|
|
110
|
+
raise KeyError(f"Unknown execution hooks plugin: '{descriptor.hook_plugin}'" f"Available: {available}")
|
|
111
|
+
|
|
112
|
+
# Extract plugin parameters from descriptor
|
|
113
|
+
plugin_params = descriptor.model_dump(
|
|
114
|
+
exclude={
|
|
115
|
+
"hook_plugin",
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
plugin_params.update(kwargs)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
return plugin_class(**plugin_params)
|
|
122
|
+
except Exception as e:
|
|
123
|
+
raise ValueError(f"Failed to instantiate plugin '{descriptor.hook_plugin}': {e}") from e
|
|
124
|
+
|
|
125
|
+
def list_plugins(self, vo: Optional[str] = None) -> List[str]:
|
|
126
|
+
"""List available plugins.
|
|
127
|
+
|
|
128
|
+
:param vo: Filter by Virtual Organization, by default None.
|
|
129
|
+
:type vo: Optional[str]
|
|
130
|
+
:return: List of available plugin keys.
|
|
131
|
+
:rtype: List[str]
|
|
132
|
+
"""
|
|
133
|
+
if vo and vo in self._vo_plugins:
|
|
134
|
+
return list(self._vo_plugins[vo].keys())
|
|
135
|
+
return list(self._plugins.keys())
|
|
136
|
+
|
|
137
|
+
def list_virtual_organizations(self) -> List[str]:
|
|
138
|
+
"""List available Virtual Organizations."""
|
|
139
|
+
return list(self._vo_plugins.keys())
|
|
140
|
+
|
|
141
|
+
def get_plugin_info(self, plugin_key: str) -> Optional[Dict[str, Any]]:
|
|
142
|
+
"""Get detailed information about a plugin."""
|
|
143
|
+
return self._plugin_info.get(plugin_key)
|
|
144
|
+
|
|
145
|
+
def discover_plugins(self) -> int:
|
|
146
|
+
"""Discover and register plugins from the entry points defined in the pyproject.toml.
|
|
147
|
+
|
|
148
|
+
:return: Number of plugins discovered and registered.
|
|
149
|
+
:rtype: int
|
|
150
|
+
"""
|
|
151
|
+
entrypoints = entry_points(group="dirac_cwl.execution_hooks")
|
|
152
|
+
discovered = 0
|
|
153
|
+
for hook_name in entrypoints.names:
|
|
154
|
+
try:
|
|
155
|
+
hook = entrypoints[hook_name].load()
|
|
156
|
+
if issubclass(hook, ExecutionHooksBasePlugin):
|
|
157
|
+
self.register_plugin(hook)
|
|
158
|
+
discovered += 1
|
|
159
|
+
else:
|
|
160
|
+
logger.warning(
|
|
161
|
+
"Tried to discover execution hook with name '%s' that does not inherit %s",
|
|
162
|
+
hook_name,
|
|
163
|
+
ExecutionHooksBasePlugin.__name__,
|
|
164
|
+
)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
logger.error("Failed to import plugin %s: %s", hook_name, e)
|
|
167
|
+
|
|
168
|
+
return discovered
|
|
169
|
+
|
|
170
|
+
def validate_descriptor(self, descriptor: ExecutionHooksHint) -> List[str]:
|
|
171
|
+
"""Validate a data manager against registered plugins.
|
|
172
|
+
|
|
173
|
+
:param descriptor: The data manager to validate.
|
|
174
|
+
:type descriptor: ExecutionHooksHint
|
|
175
|
+
:return: List of validation errors (empty if valid).
|
|
176
|
+
:rtype: List[str]
|
|
177
|
+
"""
|
|
178
|
+
errors = []
|
|
179
|
+
|
|
180
|
+
plugin_class = self.get_plugin(descriptor.hook_plugin)
|
|
181
|
+
|
|
182
|
+
if plugin_class is None:
|
|
183
|
+
available = self.list_plugins()
|
|
184
|
+
errors.append(f"Unknown metadata plugin: '{descriptor.hook_plugin}'. " f"Available: {available}")
|
|
185
|
+
return errors
|
|
186
|
+
|
|
187
|
+
# Validate descriptor against plugin schema
|
|
188
|
+
try:
|
|
189
|
+
plugin_params = descriptor.model_dump(exclude={"hook_plugin"})
|
|
190
|
+
plugin_class.model_validate(plugin_params)
|
|
191
|
+
except Exception as e:
|
|
192
|
+
errors.append(f"Plugin validation failed: {e}")
|
|
193
|
+
|
|
194
|
+
return errors
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# Global registry instance
|
|
198
|
+
_registry = ExecutionHooksPluginRegistry()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Public API
|
|
202
|
+
def get_registry() -> ExecutionHooksPluginRegistry:
|
|
203
|
+
"""Get the global execution hooks plugin registry."""
|
|
204
|
+
return _registry
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def discover_plugins() -> int:
|
|
208
|
+
"""Discover and register plugins from packages."""
|
|
209
|
+
return _registry.discover_plugins()
|