0
# Hooks and Plugin System
1
2
Event-driven plugin architecture enabling extensibility through Actions, Filters, and Contexts. The hooks system provides lifecycle management and data transformation capabilities, allowing plugins to customize every aspect of Tutor's behavior from configuration to deployment.
3
4
## Capabilities
5
6
### Action Hooks
7
8
Event callbacks that execute at specific lifecycle points without modifying data flow.
9
10
```python { .api }
11
class Action[T]:
12
"""
13
Action hook for event callbacks at specific lifecycle points.
14
"""
15
def add(self, priority: int = None) -> Callable:
16
"""
17
Decorator to add callback function with optional priority.
18
19
Args:
20
priority (int, optional): Execution priority (lower numbers run first)
21
22
Returns:
23
Callable: Decorator function
24
"""
25
26
def do(self, *args, **kwargs) -> None:
27
"""
28
Execute all registered callbacks with provided arguments.
29
30
Args:
31
*args: Positional arguments passed to callbacks
32
**kwargs: Keyword arguments passed to callbacks
33
"""
34
35
def do_from_context(self, context: Context, *args, **kwargs) -> None:
36
"""
37
Execute callbacks from specific context only.
38
39
Args:
40
context (Context): Context to execute callbacks from
41
*args: Positional arguments passed to callbacks
42
**kwargs: Keyword arguments passed to callbacks
43
"""
44
45
def clear(self, context: Context = None) -> None:
46
"""
47
Clear callbacks from specific context or all contexts.
48
49
Args:
50
context (Context, optional): Context to clear, or None for all
51
"""
52
```
53
54
### Filter Hooks
55
56
Data transformation chains that modify values as they pass through the system.
57
58
```python { .api }
59
class Filter[T1, T2]:
60
"""
61
Filter hook for data transformation chains.
62
"""
63
def add(self, priority: int = None) -> Callable:
64
"""
65
Decorator to add filter callback with optional priority.
66
67
Args:
68
priority (int, optional): Execution priority (lower numbers run first)
69
70
Returns:
71
Callable: Decorator function
72
"""
73
74
def apply(self, value: T1, *args, **kwargs) -> T2:
75
"""
76
Apply filter chain to transform the input value.
77
78
Args:
79
value (T1): Input value to transform
80
*args: Additional arguments passed to filters
81
**kwargs: Additional keyword arguments passed to filters
82
83
Returns:
84
T2: Transformed value after applying all filters
85
"""
86
87
def add_item(self, item: Any) -> None:
88
"""
89
Add single item to list-type filters.
90
91
Args:
92
item (Any): Item to add to the list
93
"""
94
95
def add_items(self, *items: Any) -> None:
96
"""
97
Add multiple items to list-type filters.
98
99
Args:
100
*items (Any): Items to add to the list
101
"""
102
```
103
104
### Context System
105
106
Isolated execution contexts for organizing hooks by scope and lifecycle.
107
108
```python { .api }
109
class Context:
110
"""
111
Context for isolating hooks by scope and lifecycle.
112
"""
113
def __init__(self, name: str):
114
"""
115
Create a new context.
116
117
Args:
118
name (str): Context name identifier
119
"""
120
121
# Context instances
122
class Contexts:
123
APP: Dict[str, Context] # Per-application contexts
124
PLUGINS: Context # Plugin-specific context
125
PLUGINS_V0_YAML: Context # YAML v0 plugin context
126
PLUGINS_V0_ENTRYPOINT: Context # Entrypoint plugin context
127
```
128
129
### Core Actions Catalog
130
131
Predefined actions for key lifecycle events in Tutor.
132
133
```python { .api }
134
# Core lifecycle actions
135
COMPOSE_PROJECT_STARTED: Action[str, Config, str] # Triggered when compose project starts
136
CONFIG_INTERACTIVE: Action[Config] # After interactive configuration questions
137
CONFIG_LOADED: Action[Config] # After configuration loading (read-only)
138
CORE_READY: Action[] # Core system ready for plugin discovery
139
DO_JOB: Action[str, Any] # Before job task execution
140
PLUGIN_LOADED: Action[str] # Single plugin loaded
141
PLUGINS_LOADED: Action[] # All plugins loaded
142
PLUGIN_UNLOADED: Action[str, str, Config] # Plugin unloaded
143
PROJECT_ROOT_READY: Action[str] # Project root directory ready
144
```
145
146
### Core Filters Catalog
147
148
Predefined filters for data transformation throughout Tutor.
149
150
```python { .api }
151
# Application and CLI filters
152
APP_PUBLIC_HOSTS: Filter[list[str], list[str]] # Public application hostnames
153
CLI_COMMANDS: Filter[list[click.Command], list[click.Command]] # CLI command list
154
CLI_DO_COMMANDS: Filter[list[Callable], list[Callable]] # "do" subcommands
155
CLI_DO_INIT_TASKS: Filter[list[tuple[str, str]], list[tuple[str, str]]] # Initialization tasks
156
157
# Configuration filters
158
CONFIG_DEFAULTS: Filter[list[tuple[str, Any]], list[tuple[str, Any]]] # Default configuration
159
CONFIG_OVERRIDES: Filter[list[tuple[str, Any]], list[tuple[str, Any]]] # Configuration overrides
160
CONFIG_UNIQUE: Filter[list[tuple[str, Any]], list[tuple[str, Any]]] # Unique configuration values
161
CONFIG_USER: Filter[list[tuple[str, Any]], list[tuple[str, Any]]] # User configuration
162
163
# Environment and template filters
164
ENV_PATCHES: Filter[list[tuple[str, str]], list[tuple[str, str]]] # Template patches
165
ENV_PATTERNS_IGNORE: Filter[list[str], list[str]] # Ignored template patterns
166
ENV_PATTERNS_INCLUDE: Filter[list[str], list[str]] # Included template patterns
167
ENV_TEMPLATE_FILTERS: Filter[list[tuple[str, Callable]], list[tuple[str, Callable]]] # Jinja2 filters
168
ENV_TEMPLATE_ROOTS: Filter[list[str], list[str]] # Template root directories
169
ENV_TEMPLATE_TARGETS: Filter[list[tuple[str, str]], list[tuple[str, str]]] # Template targets
170
ENV_TEMPLATE_VARIABLES: Filter[list[tuple[str, Any]], list[tuple[str, Any]]] # Template variables
171
172
# Docker and image filters
173
DOCKER_BUILD_COMMAND: Filter[list[str], list[str]] # Docker build command modification
174
IMAGES_BUILD: Filter[list[tuple], list[tuple]] # Images to build
175
IMAGES_BUILD_REQUIRED: Filter[list[str], list[str]] # Required images for build
176
IMAGES_BUILD_MOUNTS: Filter[list[tuple[str, str]], list[tuple[str, str]]] # Build-time mounts
177
IMAGES_PULL: Filter[list[tuple[str, str]], list[tuple[str, str]]] # Images to pull
178
IMAGES_PUSH: Filter[list[tuple[str, str]], list[tuple[str, str]]] # Images to push
179
180
# Plugin and mount filters
181
COMPOSE_MOUNTS: Filter[list[tuple[str, str]], list[tuple[str, str]]] # Compose bind mounts
182
MOUNTED_DIRECTORIES: Filter[list[tuple[str, str]], list[tuple[str, str]]] # Auto-mounted directories
183
PLUGIN_INDEXES: Filter[list[str], list[str]] # Plugin index URLs
184
PLUGINS_INFO: Filter[list[tuple[str, str]], list[tuple[str, str]]] # Plugin information
185
PLUGINS_INSTALLED: Filter[list[str], list[str]] # Installed plugins list
186
PLUGINS_LOADED: Filter[list[str], list[str]] # Loaded plugins list
187
```
188
189
### Plugin Management
190
191
Functions for installing, enabling, and managing plugins.
192
193
```python { .api }
194
def is_installed(name: str) -> bool:
195
"""
196
Check if a plugin is installed.
197
198
Args:
199
name (str): Plugin name
200
201
Returns:
202
bool: True if plugin is installed
203
"""
204
205
def iter_installed() -> Iterator[str]:
206
"""
207
Iterate over installed plugin names.
208
209
Returns:
210
Iterator[str]: Iterator of installed plugin names
211
"""
212
213
def iter_info() -> Iterator[tuple[str, Optional[str]]]:
214
"""
215
Iterate over plugin information (name, description).
216
217
Returns:
218
Iterator[tuple[str, Optional[str]]]: Iterator of (name, description) tuples
219
"""
220
221
def is_loaded(name: str) -> bool:
222
"""
223
Check if a plugin is currently loaded.
224
225
Args:
226
name (str): Plugin name
227
228
Returns:
229
bool: True if plugin is loaded
230
"""
231
232
def load_all(names: Iterable[str]) -> None:
233
"""
234
Load multiple plugins.
235
236
Args:
237
names (Iterable[str]): Plugin names to load
238
"""
239
240
def load(name: str) -> None:
241
"""
242
Load a single plugin.
243
244
Args:
245
name (str): Plugin name to load
246
"""
247
248
def iter_loaded() -> Iterator[str]:
249
"""
250
Iterate over loaded plugin names.
251
252
Returns:
253
Iterator[str]: Iterator of loaded plugin names
254
"""
255
256
def iter_patches(name: str) -> Iterator[str]:
257
"""
258
Get template patches provided by a plugin.
259
260
Args:
261
name (str): Plugin name
262
263
Returns:
264
Iterator[str]: Iterator of patch names
265
"""
266
267
def unload(plugin: str) -> None:
268
"""
269
Unload a plugin.
270
271
Args:
272
plugin (str): Plugin name to unload
273
"""
274
```
275
276
### Hook Utilities
277
278
Utility functions for hook management and caching.
279
280
```python { .api }
281
def clear_all(context: Context = None) -> None:
282
"""
283
Clear all hooks from specific context or all contexts.
284
285
Args:
286
context (Context, optional): Context to clear, or None for all
287
"""
288
289
def lru_cache(func: Callable) -> Callable:
290
"""
291
LRU cache decorator that clears when plugins change.
292
293
Args:
294
func (Callable): Function to cache
295
296
Returns:
297
Callable: Cached function
298
"""
299
300
# Priority constants
301
class priorities:
302
HIGH = 10
303
DEFAULT = 50
304
LOW = 90
305
```
306
307
## Usage Examples
308
309
### Creating Action Hooks
310
311
```python
312
from tutor import hooks
313
314
# Define custom action
315
MY_CUSTOM_ACTION = hooks.Actions.create("my-custom-action")
316
317
# Add callback to action
318
@MY_CUSTOM_ACTION.add()
319
def my_callback(arg1: str, arg2: int) -> None:
320
print(f"Action called with {arg1} and {arg2}")
321
322
# Trigger the action
323
MY_CUSTOM_ACTION.do("hello", 42)
324
```
325
326
### Creating Filter Hooks
327
328
```python
329
from tutor import hooks
330
331
# Define custom filter
332
MY_CUSTOM_FILTER = hooks.Filters.create("my-custom-filter")
333
334
# Add filter callback
335
@MY_CUSTOM_FILTER.add()
336
def my_filter(value: str, context: str) -> str:
337
return f"{context}: {value.upper()}"
338
339
# Apply the filter
340
result = MY_CUSTOM_FILTER.apply("hello world", "debug")
341
# Result: "debug: HELLO WORLD"
342
```
343
344
### Plugin Development
345
346
```python
347
from tutor import hooks
348
from tutor.plugins import base
349
350
class MyPlugin(base.BasePlugin):
351
def __init__(self):
352
super().__init__("my-plugin", "My Custom Plugin")
353
354
def configure(self) -> None:
355
# Add configuration defaults
356
hooks.Filters.CONFIG_DEFAULTS.add_items([
357
("MY_PLUGIN_SETTING", "default_value"),
358
("MY_PLUGIN_ENABLED", True),
359
])
360
361
# Add template patches
362
hooks.Filters.ENV_PATCHES.add_items([
363
("docker-compose.yml", "my-plugin/docker-compose.patch"),
364
("lms.yml", "my-plugin/lms.patch"),
365
])
366
367
# Add CLI commands
368
@hooks.Filters.CLI_COMMANDS.add()
369
def add_my_command():
370
return [self.create_command()]
371
372
# React to lifecycle events
373
@hooks.Actions.PLUGINS_LOADED.add()
374
def on_plugins_loaded():
375
print("All plugins loaded, initializing my plugin")
376
```
377
378
### Working with Built-in Hooks
379
380
```python
381
from tutor import hooks
382
383
# Add custom CLI command
384
@hooks.Filters.CLI_COMMANDS.add()
385
def add_custom_command():
386
import click
387
388
@click.command()
389
def my_command():
390
"""My custom command."""
391
click.echo("Custom command executed!")
392
393
return [my_command]
394
395
# Add configuration default
396
hooks.Filters.CONFIG_DEFAULTS.add_items([
397
("CUSTOM_SETTING", "my_value")
398
])
399
400
# React to configuration loading
401
@hooks.Actions.CONFIG_LOADED.add()
402
def on_config_loaded(config):
403
print(f"Configuration loaded with {len(config)} settings")
404
```