Skip to content

Commit 241f6f4

Browse files
feat: Add support for plugins. (#337)
Co-authored-by: Matthew M. Keeler <[email protected]>
1 parent a495562 commit 241f6f4

File tree

5 files changed

+652
-1
lines changed

5 files changed

+652
-1
lines changed

ldclient/client.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
DataStoreStatusProvider, DataStoreUpdateSink,
4141
FeatureStore, FlagTracker)
4242
from ldclient.migrations import OpTracker, Stage
43+
from ldclient.plugin import (ApplicationMetadata, EnvironmentMetadata,
44+
SdkMetadata)
45+
from ldclient.version import VERSION
4346
from ldclient.versioned_data_kind import FEATURES, SEGMENTS, VersionedDataKind
4447

4548
from .impl import AnyNum
@@ -223,8 +226,11 @@ def postfork(self, start_wait: float = 5):
223226
self.__start_up(start_wait)
224227

225228
def __start_up(self, start_wait: float):
229+
environment_metadata = self.__get_environment_metadata()
230+
plugin_hooks = self.__get_plugin_hooks(environment_metadata)
231+
226232
self.__hooks_lock = ReadWriteLock()
227-
self.__hooks = self._config.hooks # type: List[Hook]
233+
self.__hooks = self._config.hooks + plugin_hooks # type: List[Hook]
228234

229235
data_store_listeners = Listeners()
230236
store_sink = DataStoreUpdateSinkImpl(data_store_listeners)
@@ -256,6 +262,8 @@ def __start_up(self, start_wait: float):
256262

257263
diagnostic_accumulator = self._set_event_processor(self._config)
258264

265+
self.__register_plugins(environment_metadata)
266+
259267
update_processor_ready = threading.Event()
260268
self._update_processor = self._make_update_processor(self._config, self._store, update_processor_ready, diagnostic_accumulator)
261269
self._update_processor.start()
@@ -273,6 +281,43 @@ def __start_up(self, start_wait: float):
273281
else:
274282
log.warning("Initialization timeout exceeded for LaunchDarkly Client or an error occurred. " "Feature Flags may not yet be available.")
275283

284+
def __get_environment_metadata(self) -> EnvironmentMetadata:
285+
sdk_metadata = SdkMetadata(
286+
name="python-server-sdk",
287+
version=VERSION,
288+
wrapper_name=self._config.wrapper_name,
289+
wrapper_version=self._config.wrapper_version
290+
)
291+
292+
application_metadata = None
293+
if self._config.application:
294+
application_metadata = ApplicationMetadata(
295+
id=self._config.application.get('id'),
296+
version=self._config.application.get('version'),
297+
)
298+
299+
return EnvironmentMetadata(
300+
sdk=sdk_metadata,
301+
application=application_metadata,
302+
sdk_key=self._config.sdk_key
303+
)
304+
305+
def __get_plugin_hooks(self, environment_metadata: EnvironmentMetadata) -> List[Hook]:
306+
hooks = []
307+
for plugin in self._config.plugins:
308+
try:
309+
hooks.extend(plugin.get_hooks(environment_metadata))
310+
except Exception as e:
311+
log.error("Error getting hooks from plugin %s: %s", plugin.metadata.name, e)
312+
return hooks
313+
314+
def __register_plugins(self, environment_metadata: EnvironmentMetadata):
315+
for plugin in self._config.plugins:
316+
try:
317+
plugin.register(self, environment_metadata)
318+
except Exception as e:
319+
log.error("Error registering plugin %s: %s", plugin.metadata.name, e)
320+
276321
def _set_event_processor(self, config):
277322
if config.offline or not config.send_events:
278323
self._event_processor = NullEventProcessor()

ldclient/config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ldclient.impl.util import log, validate_application_info
1313
from ldclient.interfaces import (BigSegmentStore, DataSourceUpdateSink,
1414
EventProcessor, FeatureStore, UpdateProcessor)
15+
from ldclient.plugin import Plugin
1516

1617
GET_LATEST_FEATURES_PATH = '/sdk/latest-flags'
1718
STREAM_FLAGS_PATH = '/flags'
@@ -180,6 +181,7 @@ def __init__(
180181
big_segments: Optional[BigSegmentsConfig] = None,
181182
application: Optional[dict] = None,
182183
hooks: Optional[List[Hook]] = None,
184+
plugins: Optional[List[Plugin]] = None,
183185
enable_event_compression: bool = False,
184186
omit_anonymous_contexts: bool = False,
185187
payload_filter_key: Optional[str] = None,
@@ -249,6 +251,7 @@ def __init__(
249251
:class:`HTTPConfig`.
250252
:param application: Optional properties for setting application metadata. See :py:attr:`~application`
251253
:param hooks: Hooks provide entrypoints which allow for observation of SDK functions.
254+
:param plugins: A list of plugins to be used with the SDK. Plugin support is currently experimental and subject to change.
252255
:param enable_event_compression: Whether or not to enable GZIP compression for outgoing events.
253256
:param omit_anonymous_contexts: Sets whether anonymous contexts should be omitted from index and identify events.
254257
:param payload_filter_key: The payload filter is used to selectively limited the flags and segments delivered in the data source payload.
@@ -285,6 +288,7 @@ def __init__(
285288
self.__big_segments = BigSegmentsConfig() if not big_segments else big_segments
286289
self.__application = validate_application_info(application or {}, log)
287290
self.__hooks = [hook for hook in hooks if isinstance(hook, Hook)] if hooks else []
291+
self.__plugins = [plugin for plugin in plugins if isinstance(plugin, Plugin)] if plugins else []
288292
self.__enable_event_compression = enable_event_compression
289293
self.__omit_anonymous_contexts = omit_anonymous_contexts
290294
self.__payload_filter_key = payload_filter_key
@@ -477,6 +481,16 @@ def hooks(self) -> List[Hook]:
477481
"""
478482
return self.__hooks
479483

484+
@property
485+
def plugins(self) -> List[Plugin]:
486+
"""
487+
Initial set of plugins for the client.
488+
489+
LaunchDarkly provides plugin packages, and most applications will
490+
not need to implement their own plugins.
491+
"""
492+
return self.__plugins
493+
480494
@property
481495
def enable_event_compression(self) -> bool:
482496
return self.__enable_event_compression

ldclient/plugin.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
from __future__ import annotations
2+
3+
from abc import ABCMeta, abstractmethod
4+
from dataclasses import dataclass
5+
from typing import TYPE_CHECKING, List, Optional
6+
7+
from ldclient.context import Context
8+
from ldclient.evaluation import EvaluationDetail, FeatureFlagsState
9+
from ldclient.hook import Hook
10+
from ldclient.impl import AnyNum
11+
from ldclient.impl.evaluator import error_reason
12+
from ldclient.interfaces import (BigSegmentStoreStatusProvider,
13+
DataSourceStatusProvider,
14+
DataStoreStatusProvider, FlagTracker)
15+
16+
if TYPE_CHECKING:
17+
from ldclient.client import LDClient
18+
19+
20+
@dataclass
21+
class SdkMetadata:
22+
"""
23+
Metadata about the SDK.
24+
"""
25+
name: str #: The id of the SDK (e.g., "python-server-sdk")
26+
version: str #: The version of the SDK
27+
wrapper_name: Optional[str] = None #: The wrapper name if this SDK is a wrapper
28+
wrapper_version: Optional[str] = None #: The wrapper version if this SDK is a wrapper
29+
30+
31+
@dataclass
32+
class ApplicationMetadata:
33+
"""
34+
Metadata about the application using the SDK.
35+
"""
36+
id: Optional[str] = None #: The id of the application
37+
version: Optional[str] = None #: The version of the application
38+
39+
40+
@dataclass
41+
class EnvironmentMetadata:
42+
"""
43+
Metadata about the environment in which the SDK is running.
44+
"""
45+
sdk: SdkMetadata #: Information about the SDK
46+
sdk_key: Optional[str] = None #: The SDK key used to initialize the SDK
47+
application: Optional[ApplicationMetadata] = None #: Information about the application
48+
49+
50+
@dataclass
51+
class PluginMetadata:
52+
"""
53+
Metadata about a plugin implementation.
54+
"""
55+
name: str #: A name representing the plugin instance
56+
57+
58+
class Plugin:
59+
"""
60+
Abstract base class for extending SDK functionality via plugins.
61+
62+
All provided plugin implementations **MUST** inherit from this class.
63+
64+
This class includes default implementations for optional methods. This
65+
allows LaunchDarkly to expand the list of plugin methods without breaking
66+
customer integrations.
67+
68+
Plugins provide an interface which allows for initialization, access to
69+
credentials, and hook registration in a single interface.
70+
"""
71+
72+
__metaclass__ = ABCMeta
73+
74+
@property
75+
@abstractmethod
76+
def metadata(self) -> PluginMetadata:
77+
"""
78+
Get metadata about the plugin implementation.
79+
80+
:return: Metadata containing information about the plugin
81+
"""
82+
return PluginMetadata(name='UNDEFINED')
83+
84+
@abstractmethod
85+
def register(self, client: LDClient, metadata: EnvironmentMetadata) -> None:
86+
"""
87+
Register the plugin with the SDK client.
88+
89+
This method is called during SDK initialization to allow the plugin
90+
to set up any necessary integrations, register hooks, or perform
91+
other initialization tasks.
92+
93+
:param client: The LDClient instance
94+
:param metadata: Metadata about the environment in which the SDK is running
95+
"""
96+
pass
97+
98+
@abstractmethod
99+
def get_hooks(self, metadata: EnvironmentMetadata) -> List[Hook]:
100+
"""
101+
Get a list of hooks that this plugin provides.
102+
103+
This method is called before register() to collect all hooks from
104+
plugins. The hooks returned will be added to the SDK's hook configuration.
105+
106+
:param metadata: Metadata about the environment in which the SDK is running
107+
:return: A list of hooks to be registered with the SDK
108+
"""
109+
return []

0 commit comments

Comments
 (0)