Skip to content

Commit f4e78a7

Browse files
committed
Extend plugin system with PluginContext while maintaining backwards
compatibility
1 parent c787148 commit f4e78a7

6 files changed

Lines changed: 630 additions & 14 deletions

File tree

pygeoapi/plugin.py

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# =================================================================
22
#
33
# Authors: Tom Kralidis <tomkralidis@gmail.com>
4+
# Francesco Bartoli <xbartolone@gmail.com>
45
#
56
# Copyright (c) 2026 Tom Kralidis
7+
# Copyright (c) 2026 Francesco Bartoli
68
#
79
# Permission is hereby granted, free of charge, to any person
810
# obtaining a copy of this software and associated documentation
@@ -30,10 +32,57 @@
3032

3133
import importlib
3234
import logging
33-
from typing import Any
35+
from dataclasses import dataclass
36+
from typing import Any, Dict, List, Optional
3437

3538
LOGGER = logging.getLogger(__name__)
3639

40+
41+
@dataclass
42+
class PluginContext:
43+
"""
44+
Inject dependencies with a context object into plugins.
45+
46+
This allows passing runtime dependencies to plugins without
47+
relying on global state or complex config dictionaries.
48+
49+
Attributes:
50+
config: Original plugin configuration dictionary
51+
logger: Optional injected logger instance
52+
locales: Optional list of supported locale codes
53+
base_url: Optional API base URL for link generation
54+
55+
Example:
56+
>>> from pygeoapi.plugin import PluginContext, load_plugin
57+
>>> context = PluginContext(
58+
... config={'name': 'GeoJSON', 'type': 'feature', 'data': 'obs.geojson'},
59+
... logger=custom_logger,
60+
... base_url='https://api.example.com'
61+
... )
62+
>>> provider = load_plugin('provider', context.config, context=context)
63+
"""
64+
65+
config: Dict[str, Any]
66+
logger: Optional[Any] = None
67+
locales: Optional[List[str]] = None
68+
base_url: Optional[str] = None
69+
70+
def to_dict(self) -> Dict[str, Any]:
71+
"""
72+
Convert to plain dict format for backwards compatibility.
73+
74+
:returns: Dictionary with config and injected dependencies
75+
"""
76+
result = dict(self.config)
77+
if self.logger:
78+
result["_logger"] = self.logger
79+
if self.base_url:
80+
result["_base_url"] = self.base_url
81+
if self.locales:
82+
result["_locales"] = self.locales
83+
return result
84+
85+
3786
#: Loads provider plugins to be used by pygeoapi,\
3887
#: formatters and processes available
3988
PLUGINS = {
@@ -94,14 +143,32 @@
94143
}
95144

96145

97-
def load_plugin(plugin_type: str, plugin_def: dict) -> Any:
146+
def load_plugin(
147+
plugin_type: str, plugin_def: dict, context: Optional[PluginContext] = None
148+
) -> Any:
98149
"""
99-
loads plugin by name
150+
Loads plugin by name with optional dependency injection.
100151
101-
:param plugin_type: type of plugin (provider, formatter)
102-
:param plugin_def: plugin definition
152+
:param plugin_type: type of plugin (provider, formatter, process, etc.)
153+
:param plugin_def: plugin definition dictionary
154+
:param context: optional context with injected dependencies
103155
104156
:returns: plugin object
157+
158+
Example:
159+
# Plain mode (backwards compatible)
160+
>>> provider = load_plugin('provider', {
161+
... 'name': 'GeoJSON',
162+
... 'type': 'feature',
163+
... 'data': 'obs.geojson'
164+
... })
165+
166+
# Modern mode with dependencies
167+
>>> context = PluginContext(
168+
... config={'name': 'GeoJSON', 'type': 'feature', 'data': 'obs.geojson'},
169+
... logger=custom_logger
170+
... )
171+
>>> provider = load_plugin('provider', context.config, context=context)
105172
"""
106173

107174
name = plugin_def['name']
@@ -130,7 +197,23 @@ def load_plugin(plugin_type: str, plugin_def: dict) -> Any:
130197

131198
module = importlib.import_module(packagename)
132199
class_ = getattr(module, classname)
133-
plugin = class_(plugin_def)
200+
201+
# Support injected dependencies via PluginContext
202+
if context is not None:
203+
# Try context-aware constructor first
204+
try:
205+
plugin = class_(plugin_def, context=context)
206+
LOGGER.debug(f"{name} initialized with PluginContext")
207+
except TypeError as err:
208+
# Fallback: legacy constructor without context parameter
209+
LOGGER.debug(
210+
f"{name} does not support PluginContext, "
211+
f"using legacy init: {err}"
212+
)
213+
plugin = class_(plugin_def)
214+
else:
215+
# Plain mode: no more context provided
216+
plugin = class_(plugin_def)
134217

135218
return plugin
136219

pygeoapi/process/base.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
#
33
# Authors: Tom Kralidis <tomkralidis@gmail.com>
44
# Francesco Martinelli <francesco.martinelli@ingv.it>
5+
# Francesco Bartoli <xbartolone@gmail.com>
56
#
67
# Copyright (c) 2022 Tom Kralidis
78
# Copyright (c) 2024 Francesco Martinelli
9+
# Copyright (c) 2026 Francesco Bartoli
810
#
911
# Permission is hereby granted, free of charge, to any person
1012
# obtaining a copy of this software and associated documentation
@@ -29,23 +31,31 @@
2931
#
3032
# =================================================================
3133

34+
from __future__ import annotations
35+
3236
import logging
33-
from typing import Any, Tuple, Optional
37+
from typing import Any, Optional, Tuple, TYPE_CHECKING
3438

3539
from pygeoapi.error import GenericError
3640

41+
if TYPE_CHECKING:
42+
from pygeoapi.plugin import PluginContext
43+
3744
LOGGER = logging.getLogger(__name__)
3845

3946

4047
class BaseProcessor:
4148
"""generic Processor ABC. Processes are inherited from this class"""
4249

43-
def __init__(self, processor_def: dict, process_metadata: dict):
50+
def __init__(self, processor_def: dict, process_metadata: dict,
51+
context: Optional[PluginContext] = None,
52+
):
4453
"""
4554
Initialize object
4655
4756
:param processor_def: processor definition
4857
:param process_metadata: process metadata `dict`
58+
:param context: optional PluginContext with injected dependencies
4959
5060
:returns: pygeoapi.processor.base.BaseProvider
5161
"""
@@ -54,6 +64,18 @@ def __init__(self, processor_def: dict, process_metadata: dict):
5464
self.metadata = process_metadata
5565
self.supports_outputs = False
5666

67+
# Dependencies support
68+
self._context = context
69+
if context and context.logger:
70+
self._logger = context.logger
71+
else:
72+
self._logger = LOGGER # Global fallback
73+
74+
@property
75+
def logger(self):
76+
"""Get logger (injected or global)"""
77+
return self._logger
78+
5779
def set_job_id(self, job_id: str) -> None:
5880
"""
5981
Set the job_id within the processor

pygeoapi/process/manager/base.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,22 @@
3131
#
3232
# =================================================================
3333

34+
from __future__ import annotations
35+
3436
import collections
3537
import json
3638
import logging
39+
import uuid
3740
from multiprocessing import dummy
3841
from pathlib import Path
39-
from typing import Any, Dict, Tuple, Optional, OrderedDict
40-
import uuid
42+
from typing import Any, Dict, Optional, OrderedDict, Tuple, TYPE_CHECKING
4143

4244
import requests
4345

4446
from pygeoapi.plugin import load_plugin
47+
48+
if TYPE_CHECKING:
49+
from pygeoapi.plugin import PluginContext
4550
from pygeoapi.process.base import (
4651
BaseProcessor,
4752
JobNotFoundError,
@@ -64,11 +69,12 @@ class BaseManager:
6469
"""generic Manager ABC"""
6570
processes: OrderedDict[str, Dict]
6671

67-
def __init__(self, manager_def: dict):
72+
def __init__(self, manager_def: dict, context: Optional[PluginContext] = None):
6873
"""
6974
Initialize object
7075
7176
:param manager_def: manager definition
77+
:param context: optional PluginContext with injected dependencies
7278
7379
:returns: `pygeoapi.process.manager.base.BaseManager`
7480
"""
@@ -91,6 +97,18 @@ def __init__(self, manager_def: dict):
9197
for id_, process_conf in manager_def.get('processes', {}).items():
9298
self.processes[id_] = dict(process_conf)
9399

100+
# Dependencies support
101+
self._context = context
102+
if context and context.logger:
103+
self._logger = context.logger
104+
else:
105+
self._logger = LOGGER # Global fallback
106+
107+
@property
108+
def logger(self):
109+
"""Get logger (injected or global)"""
110+
return self._logger
111+
94112
def get_processor(self, process_id: str) -> BaseProcessor:
95113
"""Instantiate a processor.
96114
@@ -426,7 +444,7 @@ def execute_process(
426444
# do we support sync?
427445
process_supports_sync = (
428446
ProcessExecutionMode.sync_execute.value in job_control_options
429-
)
447+
)
430448
if not process_supports_sync:
431449
LOGGER.debug('Asynchronous execution')
432450
handler = self._execute_handler_async
@@ -489,7 +507,7 @@ def _send_in_progress_notification(self, subscriber: Optional[Subscriber]):
489507
)
490508

491509
def _send_success_notification(
492-
self, subscriber: Optional[Subscriber], outputs: Any
510+
self, subscriber: Optional[Subscriber], outputs: Any
493511
):
494512
if subscriber:
495513
response = requests.post(subscriber.success_uri, json=outputs)

pygeoapi/provider/base.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,20 @@
2727
#
2828
# =================================================================
2929

30+
from __future__ import annotations
31+
3032
import json
3133
import logging
3234
from enum import Enum
3335
from http import HTTPStatus
36+
from typing import TYPE_CHECKING, Optional
3437

3538
from pygeoapi.crs import DEFAULT_STORAGE_CRS, get_crs
3639
from pygeoapi.error import GenericError
3740

41+
if TYPE_CHECKING:
42+
from pygeoapi.plugin import PluginContext
43+
3844
LOGGER = logging.getLogger(__name__)
3945

4046

@@ -48,11 +54,12 @@ class SchemaType(Enum):
4854
class BaseProvider:
4955
"""generic Provider ABC"""
5056

51-
def __init__(self, provider_def):
57+
def __init__(self, provider_def, context: Optional[PluginContext] = None):
5258
"""
5359
Initialize object
5460
5561
:param provider_def: provider definition
62+
:param context: optional PluginContext with injected dependencies
5663
5764
:returns: pygeoapi.provider.base.BaseProvider
5865
"""
@@ -91,6 +98,28 @@ def __init__(self, provider_def):
9198
self.crs = None
9299
self.num_bands = None
93100

101+
# Dependencies support
102+
self._context = context
103+
if context and context.logger:
104+
self._logger = context.logger
105+
else:
106+
self._logger = LOGGER # Global fallback
107+
108+
if context and context.locales:
109+
self._locales = context.locales
110+
else:
111+
self._locales = []
112+
113+
@property
114+
def logger(self):
115+
"""Get logger (injected or global)"""
116+
return self._logger
117+
118+
@property
119+
def locales(self):
120+
"""Get supported locales (injected or global)"""
121+
return self._locales
122+
94123
def get_fields(self):
95124
"""
96125
Get provider field information (names, types)

0 commit comments

Comments
 (0)