Skip to content

Commit 86a0671

Browse files
authored
Merge branch 'main' into jb/sdk-1748/minor-changes
2 parents 9774633 + c73ad14 commit 86a0671

File tree

16 files changed

+566
-358
lines changed

16 files changed

+566
-358
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,16 @@ jobs:
6161
with:
6262
test_service_port: 9000
6363
token: ${{ secrets.GITHUB_TOKEN }}
64-
stop_service: 'false'
65-
enable_persistence_tests: 'true'
64+
stop_service: "false"
65+
enable_persistence_tests: "true"
6666

6767
- name: Run contract tests v3
6868
uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1
6969
with:
7070
test_service_port: 9000
7171
token: ${{ secrets.GITHUB_TOKEN }}
72-
version: v3.0.0-alpha.2
73-
enable_persistence_tests: 'true'
72+
version: v3.0.0-alpha.3
73+
enable_persistence_tests: "true"
7474

7575
windows:
7676
runs-on: windows-latest

contract-tests/client_entity.py

Lines changed: 35 additions & 43 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,56 +52,38 @@ 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)
61-
sync_config = datasystem_config.get('synchronizers')
62-
if sync_config is not None:
63-
primary = sync_config.get('primary')
64-
secondary = sync_config.get('secondary')
65-
66-
primary_builder = None
67-
secondary_builder = None
61+
sync_configs = datasystem_config.get('synchronizers')
62+
if sync_configs is not None:
63+
sync_builders = []
6864
fallback_builder = None
6965

70-
if primary is not None:
71-
streaming = primary.get('streaming')
66+
for sync_config in sync_configs:
67+
streaming = sync_config.get('streaming')
7268
if streaming is not None:
73-
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()
78-
elif primary.get('polling') is not None:
79-
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")
83-
primary_builder = polling_ds_builder()
84-
fallback_builder = fdv1_fallback_ds_builder()
69+
builder = streaming_ds_builder()
70+
_set_optional_value(streaming, "baseUri", builder.base_uri)
71+
_set_optional_time(streaming, "initialRetryDelayMs", builder.initial_reconnect_delay)
72+
sync_builders.append(builder)
73+
elif sync_config.get('polling') is not None:
74+
polling = sync_config.get('polling')
75+
76+
builder = polling_ds_builder()
77+
_set_optional_value(polling, "baseUri", builder.base_uri)
78+
_set_optional_time(polling, "pollIntervalMs", builder.poll_interval)
79+
sync_builders.append(builder)
8580

86-
if secondary is not None:
87-
streaming = secondary.get('streaming')
88-
if streaming is not None:
89-
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()
94-
elif secondary.get('polling') is not None:
95-
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")
99-
secondary_builder = polling_ds_builder()
10081
fallback_builder = fdv1_fallback_ds_builder()
82+
_set_optional_value(polling, "baseUri", fallback_builder.base_uri)
83+
_set_optional_time(polling, "pollIntervalMs", fallback_builder.poll_interval)
10184

102-
if primary_builder is not None:
103-
datasystem.synchronizers(primary_builder, secondary_builder)
85+
if sync_builders:
86+
datasystem.synchronizers(*sync_builders)
10487
if fallback_builder is not None:
10588
datasystem.fdv1_compatible_synchronizer(fallback_builder)
10689

@@ -307,7 +290,16 @@ def close(self):
307290
def _set_optional_time_prop(params_in: dict, name_in: str, params_out: dict, name_out: str):
308291
if params_in.get(name_in) is not None:
309292
params_out[name_out] = params_in[name_in] / 1000.0
310-
return None
293+
294+
295+
def _set_optional_time(params_in: dict, name_in: str, func: Callable[[float], Any]):
296+
if params_in.get(name_in) is not None:
297+
func(params_in[name_in] / 1000.0)
298+
299+
300+
def _set_optional_value(params_in: dict, name_in: str, func: Callable[[Any], Any]):
301+
if params_in.get(name_in) is not None:
302+
func(params_in[name_in])
311303

312304

313305
def _create_persistent_store(persistent_store_config: dict):

ldclient/config.py

Lines changed: 28 additions & 14 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,31 +156,45 @@ 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]]
172-
"""The primary synchronizer for the data system."""
173-
174-
secondary_synchronizer: Optional[Builder[Synchronizer]] = None
175-
"""The secondary synchronizers for the data system."""
184+
synchronizers: Optional[List[DataSourceBuilder[Synchronizer]]]
185+
"""
186+
The synchronizers for the data system, ordered by preference.
187+
The first synchronizer is the most preferred, with subsequent synchronizers
188+
serving as fallbacks in order of decreasing preference.
189+
"""
176190

177191
data_store_mode: DataStoreMode = DataStoreMode.READ_WRITE
178192
"""The data store mode specifies the mode in which the persistent store will operate, if present."""
179193

180194
data_store: Optional[FeatureStore] = None
181195
"""The (optional) persistent data store instance."""
182196

183-
fdv1_fallback_synchronizer: Optional[Builder[Synchronizer]] = None
197+
fdv1_fallback_synchronizer: Optional[DataSourceBuilder[Synchronizer]] = None
184198
"""An optional fallback synchronizer that will read from FDv1"""
185199

186200

@@ -441,7 +455,7 @@ def event_processor_class(self) -> Optional[Callable[['Config'], EventProcessor]
441455
return self.__event_processor_class
442456

443457
@property
444-
def feature_requester_class(self) -> Callable:
458+
def feature_requester_class(self) -> Optional[Callable]:
445459
return self.__feature_requester_class
446460

447461
@property
@@ -595,4 +609,4 @@ def _validate(self):
595609
log.warning("Missing or blank SDK key")
596610

597611

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

ldclient/datasystem.py

Lines changed: 46 additions & 60 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,20 @@
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._synchronizers: List[DataSourceBuilder[Synchronizer]] = []
32+
self._fdv1_fallback_synchronizer: Optional[DataSourceBuilder[Synchronizer]] = None
4433
self._store_mode: DataStoreMode = DataStoreMode.READ_ONLY
4534
self._data_store: Optional[FeatureStore] = None
4635

47-
def initializers(self, initializers: Optional[List[Builder[Initializer]]]) -> "ConfigBuilder":
36+
def initializers(self, initializers: Optional[List[DataSourceBuilder[Initializer]]]) -> "ConfigBuilder":
4837
"""
4938
Sets the initializers for the data system.
5039
"""
@@ -53,19 +42,28 @@ def initializers(self, initializers: Optional[List[Builder[Initializer]]]) -> "C
5342

5443
def synchronizers(
5544
self,
56-
primary: Builder[Synchronizer],
57-
secondary: Optional[Builder[Synchronizer]] = None,
45+
*sync_builders: DataSourceBuilder[Synchronizer]
5846
) -> "ConfigBuilder":
5947
"""
6048
Sets the synchronizers for the data system.
49+
50+
Accepts one or more synchronizer builders, ordered by preference.
51+
The first synchronizer is the most preferred, with subsequent
52+
synchronizers serving as fallbacks in order of decreasing preference.
53+
54+
Examples:
55+
builder.synchronizers(streaming_builder)
56+
builder.synchronizers(streaming_builder, polling_builder)
57+
builder.synchronizers(sync1, sync2, sync3)
6158
"""
62-
self._primary_synchronizer = primary
63-
self._secondary_synchronizer = secondary
59+
if len(sync_builders) == 0:
60+
raise ValueError("At least one synchronizer must be provided")
61+
self._synchronizers = list(sync_builders)
6462
return self
6563

6664
def fdv1_compatible_synchronizer(
6765
self,
68-
fallback: Builder[Synchronizer]
66+
fallback: DataSourceBuilder[Synchronizer]
6967
) -> "ConfigBuilder":
7068
"""
7169
Configures the SDK with a fallback synchronizer that is compatible with
@@ -86,53 +84,41 @@ def build(self) -> DataSystemConfig:
8684
"""
8785
Builds the data system configuration.
8886
"""
89-
if self._secondary_synchronizer is not None and self._primary_synchronizer is None:
90-
raise ValueError("Primary synchronizer must be set if secondary is set")
91-
9287
return DataSystemConfig(
9388
initializers=self._initializers,
94-
primary_synchronizer=self._primary_synchronizer,
95-
secondary_synchronizer=self._secondary_synchronizer,
89+
synchronizers=self._synchronizers if len(self._synchronizers) > 0 else None,
9690
fdv1_fallback_synchronizer=self._fdv1_fallback_synchronizer,
9791
data_store_mode=self._store_mode,
9892
data_store=self._data_store,
9993
)
10094

10195

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
96+
def polling_ds_builder() -> PollingDataSourceBuilder:
97+
"""
98+
Returns a builder for a polling data source.
99+
"""
100+
return PollingDataSourceBuilder()
122101

123102

124-
def streaming_ds_builder() -> Builder[StreamingDataSource]:
125-
def builder(config: LDConfig) -> StreamingDataSource:
126-
return StreamingDataSourceBuilder(config).build()
103+
def fdv1_fallback_ds_builder() -> FallbackToFDv1PollingDataSourceBuilder:
104+
"""
105+
Returns a builder for a Flag Delivery v1 compatible fallback polling data source.
106+
"""
107+
return FallbackToFDv1PollingDataSourceBuilder()
127108

128-
return builder
129109

110+
def streaming_ds_builder() -> StreamingDataSourceBuilder:
111+
"""
112+
Returns a builder for a streaming data source.
113+
"""
114+
return StreamingDataSourceBuilder()
130115

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

135-
return builder
117+
def file_ds_builder(paths: List[str]) -> FileDataSourceV2Builder:
118+
"""
119+
Returns a builder for a file-based data source.
120+
"""
121+
return FileDataSourceV2Builder(paths)
136122

137123

138124
def default() -> ConfigBuilder:
@@ -185,7 +171,7 @@ def polling() -> ConfigBuilder:
185171
streaming, but may be necessary in some network environments.
186172
"""
187173

188-
polling_builder: Builder[Synchronizer] = polling_ds_builder()
174+
polling_builder: DataSourceBuilder[Synchronizer] = polling_ds_builder()
189175
fallback = fdv1_fallback_ds_builder()
190176

191177
builder = ConfigBuilder()

0 commit comments

Comments
 (0)