Skip to content

Commit d78079e

Browse files
authored
fix: Allow modifying fdv2 data source options independent of main config (#403)
1 parent e99a27d commit d78079e

File tree

14 files changed

+414
-237
lines changed

14 files changed

+414
-237
lines changed

contract-tests/client_entity.py

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import logging
33
import sys
4+
from typing import Any, Callable
45
from urllib.parse import urlparse
56

67
import requests
@@ -51,11 +52,10 @@ def __init__(self, tag, config):
5152
for init_config in init_configs:
5253
polling = init_config.get('polling')
5354
if polling is not None:
54-
if polling.get("baseUri") is not None:
55-
opts["base_uri"] = polling["baseUri"]
56-
_set_optional_time_prop(polling, "pollIntervalMs", opts, "poll_interval")
57-
polling = polling_ds_builder()
58-
initializers.append(polling)
55+
polling_builder = polling_ds_builder()
56+
_set_optional_value(polling, "baseUri", polling_builder.base_uri)
57+
_set_optional_time(polling, "pollIntervalMs", polling_builder.poll_interval)
58+
initializers.append(polling_builder)
5959

6060
datasystem.initializers(initializers)
6161
sync_config = datasystem_config.get('synchronizers')
@@ -71,33 +71,34 @@ def __init__(self, tag, config):
7171
streaming = primary.get('streaming')
7272
if streaming is not None:
7373
primary_builder = streaming_ds_builder()
74-
if streaming.get("baseUri") is not None:
75-
opts["stream_uri"] = streaming["baseUri"]
76-
_set_optional_time_prop(streaming, "initialRetryDelayMs", opts, "initial_reconnect_delay")
77-
primary_builder = streaming_ds_builder()
74+
_set_optional_value(streaming, "baseUri", primary_builder.base_uri)
75+
_set_optional_time(streaming, "initialRetryDelayMs", primary_builder.initial_reconnect_delay)
7876
elif primary.get('polling') is not None:
7977
polling = primary.get('polling')
80-
if polling.get("baseUri") is not None:
81-
opts["base_uri"] = polling["baseUri"]
82-
_set_optional_time_prop(polling, "pollIntervalMs", opts, "poll_interval")
78+
8379
primary_builder = polling_ds_builder()
80+
_set_optional_value(polling, "baseUri", primary_builder.base_uri)
81+
_set_optional_time(polling, "pollIntervalMs", primary_builder.poll_interval)
82+
8483
fallback_builder = fdv1_fallback_ds_builder()
84+
_set_optional_value(polling, "baseUri", fallback_builder.base_uri)
85+
_set_optional_time(polling, "pollIntervalMs", fallback_builder.poll_interval)
8586

8687
if secondary is not None:
8788
streaming = secondary.get('streaming')
8889
if streaming is not None:
8990
secondary_builder = streaming_ds_builder()
90-
if streaming.get("baseUri") is not None:
91-
opts["stream_uri"] = streaming["baseUri"]
92-
_set_optional_time_prop(streaming, "initialRetryDelayMs", opts, "initial_reconnect_delay")
93-
secondary_builder = streaming_ds_builder()
91+
_set_optional_value(streaming, "baseUri", secondary_builder.base_uri)
92+
_set_optional_time(streaming, "initialRetryDelayMs", secondary_builder.initial_reconnect_delay)
9493
elif secondary.get('polling') is not None:
9594
polling = secondary.get('polling')
96-
if polling.get("baseUri") is not None:
97-
opts["base_uri"] = polling["baseUri"]
98-
_set_optional_time_prop(polling, "pollIntervalMs", opts, "poll_interval")
95+
9996
secondary_builder = polling_ds_builder()
97+
_set_optional_value(polling, "baseUri", secondary_builder.base_uri)
98+
_set_optional_time(polling, "pollIntervalMs", secondary_builder.poll_interval)
10099
fallback_builder = fdv1_fallback_ds_builder()
100+
_set_optional_value(polling, "baseUri", fallback_builder.base_uri)
101+
_set_optional_time(polling, "pollIntervalMs", fallback_builder.poll_interval)
101102

102103
if primary_builder is not None:
103104
datasystem.synchronizers(primary_builder, secondary_builder)
@@ -307,7 +308,16 @@ def close(self):
307308
def _set_optional_time_prop(params_in: dict, name_in: str, params_out: dict, name_out: str):
308309
if params_in.get(name_in) is not None:
309310
params_out[name_out] = params_in[name_in] / 1000.0
310-
return None
311+
312+
313+
def _set_optional_time(params_in: dict, name_in: str, func: Callable[[float], Any]):
314+
if params_in.get(name_in) is not None:
315+
func(params_in[name_in] / 1000.0)
316+
317+
318+
def _set_optional_value(params_in: dict, name_in: str, func: Callable[[Any], Any]):
319+
if params_in.get(name_in) is not None:
320+
func(params_in[name_in])
311321

312322

313323
def _create_persistent_store(persistent_store_config: dict):

ldclient/config.py

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from dataclasses import dataclass
88
from threading import Event
9-
from typing import Callable, List, Optional, Set, TypeVar
9+
from typing import Callable, List, Optional, Protocol, Set, TypeVar
1010

1111
from ldclient.feature_store import InMemoryFeatureStore
1212
from ldclient.hook import Hook
@@ -92,11 +92,11 @@ def stale_after(self) -> float:
9292

9393

9494
class HTTPConfig:
95-
"""Advanced HTTP configuration options for the SDK client.
95+
"""Advanced HTTP configuration options for the SDK client / data sources.
9696
9797
This class groups together HTTP/HTTPS-related configuration properties that rarely need to be changed.
9898
If you need to set these, construct an ``HTTPConfig`` instance and pass it as the ``http`` parameter when
99-
you construct the main :class:`Config` for the SDK client.
99+
you construct the main :class:`Config` for the SDK client, or to the appropriate data source builder method.
100100
"""
101101

102102
def __init__(
@@ -156,22 +156,35 @@ def disable_ssl_verification(self) -> bool:
156156
return self.__disable_ssl_verification
157157

158158

159-
T = TypeVar("T")
159+
T_co = TypeVar("T_co", covariant=True)
160160

161-
Builder = Callable[['Config'], T]
161+
162+
class DataSourceBuilder(Protocol[T_co]): # pylint: disable=too-few-public-methods
163+
"""
164+
Protocol for building data sources.
165+
"""
166+
167+
def build(self, config: 'Config') -> T_co:
168+
"""
169+
Builds the data source.
170+
171+
:param config: the SDK configuration
172+
:return: the built data source
173+
"""
174+
raise NotImplementedError
162175

163176

164177
@dataclass(frozen=True)
165178
class DataSystemConfig:
166179
"""Configuration for LaunchDarkly's data acquisition strategy."""
167180

168-
initializers: Optional[List[Builder[Initializer]]]
181+
initializers: Optional[List[DataSourceBuilder[Initializer]]]
169182
"""The initializers for the data system."""
170183

171-
primary_synchronizer: Optional[Builder[Synchronizer]]
184+
primary_synchronizer: Optional[DataSourceBuilder[Synchronizer]]
172185
"""The primary synchronizer for the data system."""
173186

174-
secondary_synchronizer: Optional[Builder[Synchronizer]] = None
187+
secondary_synchronizer: Optional[DataSourceBuilder[Synchronizer]] = None
175188
"""The secondary synchronizers for the data system."""
176189

177190
data_store_mode: DataStoreMode = DataStoreMode.READ_WRITE
@@ -180,7 +193,7 @@ class DataSystemConfig:
180193
data_store: Optional[FeatureStore] = None
181194
"""The (optional) persistent data store instance."""
182195

183-
fdv1_fallback_synchronizer: Optional[Builder[Synchronizer]] = None
196+
fdv1_fallback_synchronizer: Optional[DataSourceBuilder[Synchronizer]] = None
184197
"""An optional fallback synchronizer that will read from FDv1"""
185198

186199

@@ -441,7 +454,7 @@ def event_processor_class(self) -> Optional[Callable[['Config'], EventProcessor]
441454
return self.__event_processor_class
442455

443456
@property
444-
def feature_requester_class(self) -> Callable:
457+
def feature_requester_class(self) -> Optional[Callable]:
445458
return self.__feature_requester_class
446459

447460
@property
@@ -595,4 +608,4 @@ def _validate(self):
595608
log.warning("Missing or blank SDK key")
596609

597610

598-
__all__ = ['Config', 'BigSegmentsConfig', 'DataSystemConfig', 'HTTPConfig']
611+
__all__ = ['Config', 'BigSegmentsConfig', 'DataSourceBuilder', 'DataSystemConfig', 'HTTPConfig']

ldclient/datasystem.py

Lines changed: 35 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,16 @@
22
Configuration for LaunchDarkly's data acquisition strategy.
33
"""
44

5-
from typing import Callable, List, Optional, TypeVar
5+
from typing import List, Optional
66

7-
from ldclient.config import Config as LDConfig
8-
from ldclient.config import DataSystemConfig
7+
from ldclient.config import DataSourceBuilder, DataSystemConfig
98
from ldclient.impl.datasourcev2.polling import (
10-
PollingDataSource,
11-
PollingDataSourceBuilder,
12-
Urllib3FDv1PollingRequester,
13-
Urllib3PollingRequester
14-
)
15-
from ldclient.impl.datasourcev2.streaming import (
16-
StreamingDataSource,
17-
StreamingDataSourceBuilder
9+
FallbackToFDv1PollingDataSourceBuilder,
10+
PollingDataSourceBuilder
1811
)
12+
from ldclient.impl.datasourcev2.streaming import StreamingDataSourceBuilder
1913
from ldclient.impl.integrations.files.file_data_sourcev2 import (
20-
_FileDataSourceV2
14+
FileDataSourceV2Builder
2115
)
2216
from ldclient.interfaces import (
2317
DataStoreMode,
@@ -26,25 +20,21 @@
2620
Synchronizer
2721
)
2822

29-
T = TypeVar("T")
30-
31-
Builder = Callable[[LDConfig], T]
32-
3323

3424
class ConfigBuilder: # pylint: disable=too-few-public-methods
3525
"""
3626
Builder for the data system configuration.
3727
"""
3828

3929
def __init__(self) -> None:
40-
self._initializers: Optional[List[Builder[Initializer]]] = None
41-
self._primary_synchronizer: Optional[Builder[Synchronizer]] = None
42-
self._secondary_synchronizer: Optional[Builder[Synchronizer]] = None
43-
self._fdv1_fallback_synchronizer: Optional[Builder[Synchronizer]] = None
30+
self._initializers: Optional[List[DataSourceBuilder[Initializer]]] = None
31+
self._primary_synchronizer: Optional[DataSourceBuilder[Synchronizer]] = None
32+
self._secondary_synchronizer: Optional[DataSourceBuilder[Synchronizer]] = None
33+
self._fdv1_fallback_synchronizer: Optional[DataSourceBuilder[Synchronizer]] = None
4434
self._store_mode: DataStoreMode = DataStoreMode.READ_ONLY
4535
self._data_store: Optional[FeatureStore] = None
4636

47-
def initializers(self, initializers: Optional[List[Builder[Initializer]]]) -> "ConfigBuilder":
37+
def initializers(self, initializers: Optional[List[DataSourceBuilder[Initializer]]]) -> "ConfigBuilder":
4838
"""
4939
Sets the initializers for the data system.
5040
"""
@@ -53,8 +43,8 @@ def initializers(self, initializers: Optional[List[Builder[Initializer]]]) -> "C
5343

5444
def synchronizers(
5545
self,
56-
primary: Builder[Synchronizer],
57-
secondary: Optional[Builder[Synchronizer]] = None,
46+
primary: DataSourceBuilder[Synchronizer],
47+
secondary: Optional[DataSourceBuilder[Synchronizer]] = None,
5848
) -> "ConfigBuilder":
5949
"""
6050
Sets the synchronizers for the data system.
@@ -65,7 +55,7 @@ def synchronizers(
6555

6656
def fdv1_compatible_synchronizer(
6757
self,
68-
fallback: Builder[Synchronizer]
58+
fallback: DataSourceBuilder[Synchronizer]
6959
) -> "ConfigBuilder":
7060
"""
7161
Configures the SDK with a fallback synchronizer that is compatible with
@@ -99,40 +89,32 @@ def build(self) -> DataSystemConfig:
9989
)
10090

10191

102-
def polling_ds_builder() -> Builder[PollingDataSource]:
103-
def builder(config: LDConfig) -> PollingDataSource:
104-
requester = Urllib3PollingRequester(config)
105-
polling_ds = PollingDataSourceBuilder(config)
106-
polling_ds.requester(requester)
107-
108-
return polling_ds.build()
109-
110-
return builder
111-
112-
113-
def fdv1_fallback_ds_builder() -> Builder[PollingDataSource]:
114-
def builder(config: LDConfig) -> PollingDataSource:
115-
requester = Urllib3FDv1PollingRequester(config)
116-
polling_ds = PollingDataSourceBuilder(config)
117-
polling_ds.requester(requester)
118-
119-
return polling_ds.build()
120-
121-
return builder
92+
def polling_ds_builder() -> PollingDataSourceBuilder:
93+
"""
94+
Returns a builder for a polling data source.
95+
"""
96+
return PollingDataSourceBuilder()
12297

12398

124-
def streaming_ds_builder() -> Builder[StreamingDataSource]:
125-
def builder(config: LDConfig) -> StreamingDataSource:
126-
return StreamingDataSourceBuilder(config).build()
99+
def fdv1_fallback_ds_builder() -> FallbackToFDv1PollingDataSourceBuilder:
100+
"""
101+
Returns a builder for a Flag Delivery v1 compatible fallback polling data source.
102+
"""
103+
return FallbackToFDv1PollingDataSourceBuilder()
127104

128-
return builder
129105

106+
def streaming_ds_builder() -> StreamingDataSourceBuilder:
107+
"""
108+
Returns a builder for a streaming data source.
109+
"""
110+
return StreamingDataSourceBuilder()
130111

131-
def file_ds_builder(paths: List[str]) -> Builder[Initializer]:
132-
def builder(_: LDConfig) -> Initializer:
133-
return _FileDataSourceV2(paths)
134112

135-
return builder
113+
def file_ds_builder(paths: List[str]) -> FileDataSourceV2Builder:
114+
"""
115+
Returns a builder for a file-based data source.
116+
"""
117+
return FileDataSourceV2Builder(paths)
136118

137119

138120
def default() -> ConfigBuilder:
@@ -185,7 +167,7 @@ def polling() -> ConfigBuilder:
185167
streaming, but may be necessary in some network environments.
186168
"""
187169

188-
polling_builder: Builder[Synchronizer] = polling_ds_builder()
170+
polling_builder: DataSourceBuilder[Synchronizer] = polling_ds_builder()
189171
fallback = fdv1_fallback_ds_builder()
190172

191173
builder = ConfigBuilder()

0 commit comments

Comments
 (0)