Skip to content

Commit 96adf51

Browse files
LaunchDarklyReleaseBoteli-darklycharukiewiczLaunchDarklyReleaseBotkeelerm84
authored
prepare 9.0.0 release (#227)
## [9.0.0] - 2023-10-17 The latest version of this SDK supports the ability to manage migrations or modernizations, using migration flags. You might use this functionality if you are optimizing queries, upgrading to new tech stacks, migrating from one database to another, or other similar technology changes. Migration flags are part of LaunchDarkly's Early Access Program. This feature is available to all LaunchDarkly customers but may undergo additional changes before it is finalized. For detailed information about this version, refer to the list below. For information on how to upgrade from the previous version, read the [migration guide](https://docs.launchdarkly.com/sdk/server-side/python/migration-8-to-9). ### Added: - A new `Migrator` type which provides an out-of-the-box configurable migration framework. - For more advanced use cases, added new `migration_variation` and `track_migration_op` methods on `LDClient`. ### Changed: - Raised `pyyaml` dependency to `>=5.3`. ### Removed: - Python 3.7 support was removed. - The legacy user format for contexts is no longer supported. To learn more, read the [Contexts documentation](https://docs.launchdarkly.com/guides/flags/intro-contexts). - Methods which originally took a `Context` or a `dict` now only accept a `Context`. - Previously deprecated config options `user_cache_size`, `user_cache_time`, `user_keys_capacity`, `user_keys_flush_interval`, and `private_attribute_names` have been removed. - Previously deprecated test data flag builder method `variation_for_all_users` has been removed. --------- Co-authored-by: Eli Bishop <eli@launchdarkly.com> Co-authored-by: charukiewicz <charukiewicz@protonmail.com> Co-authored-by: LaunchDarklyReleaseBot <launchdarklyreleasebot@launchdarkly.com> Co-authored-by: Christian Charukiewicz <christian@foxhound.systems> Co-authored-by: Matthew M. Keeler <keelerm84@gmail.com> Co-authored-by: Matthew M. Keeler <mkeeler@launchdarkly.com> Co-authored-by: Ember Stevens <ember.stevens@launchdarkly.com> Co-authored-by: Ember Stevens <79482775+ember-stevens@users.noreply.github.com> Co-authored-by: LaunchDarklyCI <dev@launchdarkly.com> Co-authored-by: Ben Woskow <bwoskow@launchdarkly.com> Co-authored-by: Gavin Whelan <gwhelan@launchdarkly.com> Co-authored-by: Elliot <35050275+Apache-HB@users.noreply.github.com> Co-authored-by: Gabor Angeli <gabor@squareup.com> Co-authored-by: Elliot <apachehaisley@gmail.com> Co-authored-by: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Co-authored-by: LaunchDarklyCI <LaunchDarklyCI@users.noreply.github.com> Co-authored-by: hroederld <hroeder@launchdarkly.com> Co-authored-by: Robert J. Neal <rneal@launchdarkly.com> Co-authored-by: Robert J. Neal <robertjneal@users.noreply.github.com> Co-authored-by: Louis Chan <lchan@launchdarkly.com> Co-authored-by: prpnmac <95777763+prpnmac@users.noreply.github.com> Co-authored-by: Louis Chan <91093020+louis-launchdarkly@users.noreply.github.com> Co-authored-by: Daniel Fritz <dfritz@indigoag.com>
1 parent ff8060b commit 96adf51

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2653
-436
lines changed

.circleci/config.yml

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,11 @@ workflows:
77
test:
88
jobs:
99
- test-linux:
10-
name: Python 3.7
11-
docker-image: cimg/python:3.7
10+
name: Python 3.8
11+
docker-image: cimg/python:3.8
1212
test-build-docs: true
1313
skip-sse-contract-tests: true
1414
skip-contract-tests: true
15-
- test-linux:
16-
name: Python 3.8
17-
docker-image: cimg/python:3.8
1815
- test-linux:
1916
name: Python 3.9
2017
docker-image: cimg/python:3.9
@@ -24,6 +21,9 @@ workflows:
2421
- test-linux:
2522
name: Python 3.11
2623
docker-image: cimg/python:3.11
24+
- test-linux:
25+
name: Python 3.12
26+
docker-image: cimg/python:3.12
2727
- test-windows:
2828
name: Windows Python 3
2929
py3: true
@@ -60,6 +60,7 @@ jobs:
6060
name: install requirements
6161
command: |
6262
pip install --upgrade pip
63+
pip install setuptools
6364
pip install -r test-requirements.txt;
6465
pip install -r test-filesource-optional-requirements.txt;
6566
pip install -r consul-requirements.txt;
@@ -136,15 +137,7 @@ jobs:
136137
- checkout
137138
- run:
138139
name: install Python 3
139-
command: |
140-
choco install pyenv-win --force
141-
refreshenv
142-
pyenv install 3.11.0b3
143-
pyenv global 3.11.0b3
144-
[System.Environment]::SetEnvironmentVariable('PYENV',$env:USERPROFILE + "\.pyenv\pyenv-win\","User")
145-
[System.Environment]::SetEnvironmentVariable('PYENV_ROOT',$env:USERPROFILE + "\.pyenv\pyenv-win\","User")
146-
[System.Environment]::SetEnvironmentVariable('PYENV_HOME',$env:USERPROFILE + "\.pyenv\pyenv-win\","User")
147-
[System.Environment]::SetEnvironmentVariable('path', $env:USERPROFILE + "\.pyenv\pyenv-win\bin;" + $env:USERPROFILE + "\.pyenv\pyenv-win\shims;" + [System.Environment]::GetEnvironmentVariable('path', "User"),"User")
140+
command: choco install python --no-progress
148141
- run: python --version
149142
- run:
150143
name: set up DynamoDB
@@ -182,6 +175,7 @@ jobs:
182175
name: install requirements
183176
command: |
184177
python --version
178+
pip install setuptools
185179
pip install -r test-requirements.txt
186180
pip install -r consul-requirements.txt
187181
python setup.py install

.readthedocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
version: 2
22

33
python:
4-
version: 3.7
4+
version: 3.8
55
install:
66
- requirements: docs/requirements.txt
77
- requirements: requirements.txt

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
## Supported Python versions
1515

16-
This version of the LaunchDarkly SDK is compatible with Python 3.7 through 3.11. It is tested with the most recent patch releases of those versions. Python versions 2.7 to 3.6 are no longer supported.
16+
This version of the LaunchDarkly SDK is compatible with Python 3.8 through 3.12. It is tested with the most recent patch releases of those versions. Python versions 2.7 to 3.6 are no longer supported.
1717

1818
## Getting started
1919

contract-tests/client_entity.py

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import os
44
import sys
5+
import requests
56
from typing import Optional
67

78
from big_segment_store_fixture import BigSegmentStoreFixture
@@ -10,6 +11,7 @@
1011

1112
# Import ldclient from parent directory
1213
sys.path.insert(1, os.path.join(sys.path[0], '..'))
14+
from ldclient import Context, MigratorBuilder, ExecutionOrder, MigratorFn, Operation, Stage
1315
from ldclient import *
1416

1517

@@ -39,7 +41,7 @@ def __init__(self, tag, config):
3941
opts["events_max_pending"] = events["capacity"]
4042
opts["diagnostic_opt_out"] = not events.get("enableDiagnostics", False)
4143
opts["all_attributes_private"] = events.get("allAttributesPrivate", False)
42-
opts["private_attribute_names"] = events.get("globalPrivateAttributes", {})
44+
opts["private_attributes"] = events.get("globalPrivateAttributes", {})
4345
_set_optional_time_prop(events, "flushIntervalMs", opts, "flush_interval")
4446
else:
4547
opts["send_events"] = False
@@ -55,7 +57,7 @@ def __init__(self, tag, config):
5557
_set_optional_time_prop(big_params, "statusPollIntervalMs", big_config, "status_poll_interval")
5658
_set_optional_time_prop(big_params, "staleAfterMs", big_config, "stale_after")
5759
opts["big_segments"] = BigSegmentsConfig(**big_config)
58-
60+
5961
start_wait = config.get("startWaitTimeMs") or 5000
6062
config = Config(**opts)
6163

@@ -68,12 +70,12 @@ def evaluate(self, params: dict) -> dict:
6870
response = {}
6971

7072
if params.get("detail", False):
71-
detail = self.client.variation_detail(params["flagKey"], params["context"], params["defaultValue"])
73+
detail = self.client.variation_detail(params["flagKey"], Context.from_dict(params["context"]), params["defaultValue"])
7274
response["value"] = detail.value
7375
response["variationIndex"] = detail.variation_index
7476
response["reason"] = detail.reason
7577
else:
76-
response["value"] = self.client.variation(params["flagKey"], params["context"], params["defaultValue"])
78+
response["value"] = self.client.variation(params["flagKey"], Context.from_dict(params["context"]), params["defaultValue"])
7779

7880
return response
7981

@@ -83,30 +85,30 @@ def evaluate_all(self, params: dict):
8385
opts["with_reasons"] = params.get("withReasons", False)
8486
opts["details_only_for_tracked_flags"] = params.get("detailsOnlyForTrackedFlags", False)
8587

86-
state = self.client.all_flags_state(params["context"], **opts)
88+
state = self.client.all_flags_state(Context.from_dict(params["context"]), **opts)
8789

8890
return {"state": state.to_json_dict()}
8991

9092
def track(self, params: dict):
91-
self.client.track(params["eventKey"], params["context"], params["data"], params.get("metricValue", None))
93+
self.client.track(params["eventKey"], Context.from_dict(params["context"]), params["data"], params.get("metricValue", None))
9294

9395
def identify(self, params: dict):
94-
self.client.identify(params["context"])
96+
self.client.identify(Context.from_dict(params["context"]))
9597

9698
def flush(self):
9799
self.client.flush()
98100

99101
def secure_mode_hash(self, params: dict) -> dict:
100-
return {"result": self.client.secure_mode_hash(params["context"])}
101-
102+
return {"result": self.client.secure_mode_hash(Context.from_dict(params["context"]))}
103+
102104
def context_build(self, params: dict) -> dict:
103105
if params.get("multi"):
104106
b = Context.multi_builder()
105107
for c in params.get("multi"):
106108
b.add(self._context_build_single(c))
107109
return self._context_response(b.build())
108110
return self._context_response(self._context_build_single(params["single"]))
109-
111+
110112
def _context_build_single(self, params: dict) -> Context:
111113
b = Context.builder(params["key"])
112114
if "kind" in params:
@@ -122,31 +124,76 @@ def _context_build_single(self, params: dict) -> Context:
122124
for attr in params.get("private"):
123125
b.private(attr)
124126
return b.build()
125-
127+
126128
def context_convert(self, params: dict) -> dict:
127129
input = params["input"]
128130
try:
129131
props = json.loads(input)
130132
return self._context_response(Context.from_dict(props))
131133
except Exception as e:
132134
return {"error": str(e)}
133-
135+
134136
def _context_response(self, c: Context) -> dict:
135137
if c.valid:
136138
return {"output": c.to_json_string()}
137139
return {"error": c.error}
138-
140+
139141
def get_big_segment_store_status(self) -> dict:
140142
status = self.client.big_segment_store_status_provider.status
141143
return {
142144
"available": status.available,
143145
"stale": status.stale
144146
}
145147

148+
def migration_variation(self, params: dict) -> dict:
149+
stage, _ = self.client.migration_variation(params["key"], Context.from_dict(params["context"]), Stage.from_str(params["defaultStage"]))
150+
151+
return {'result': stage.value}
152+
153+
def migration_operation(self, params: dict) -> dict:
154+
builder = MigratorBuilder(self.client)
155+
156+
if params["readExecutionOrder"] == "concurrent":
157+
params["readExecutionOrder"] = "parallel"
158+
159+
builder.read_execution_order(ExecutionOrder.from_str(params["readExecutionOrder"]))
160+
builder.track_latency(params["trackLatency"])
161+
builder.track_errors(params["trackErrors"])
162+
163+
def callback(endpoint) -> MigratorFn:
164+
def fn(payload) -> Result:
165+
response = requests.post(endpoint, data=payload)
166+
167+
if response.status_code == 200:
168+
return Result.success(response.text)
169+
170+
return Result.error(f"Request failed with status code {response.status_code}")
171+
172+
return fn
173+
174+
if params["trackConsistency"]:
175+
builder.read(callback(params["oldEndpoint"]), callback(params["newEndpoint"]), lambda lhs, rhs: lhs == rhs)
176+
else:
177+
builder.read(callback(params["oldEndpoint"]), callback(params["newEndpoint"]))
178+
179+
builder.write(callback(params["oldEndpoint"]), callback(params["newEndpoint"]))
180+
migrator = builder.build()
181+
182+
if isinstance(migrator, str):
183+
return {"result": migrator}
184+
185+
if params["operation"] == Operation.READ.value:
186+
result = migrator.read(params["key"], Context.from_dict(params["context"]), Stage.from_str(params["defaultStage"]), params["payload"])
187+
return {"result": result.value if result.is_success() else result.error}
188+
189+
result = migrator.write(params["key"], Context.from_dict(params["context"]), Stage.from_str(params["defaultStage"]), params["payload"])
190+
return {"result": result.authoritative.value if result.authoritative.is_success() else result.authoritative.error}
191+
146192
def close(self):
147193
self.client.close()
148194
self.log.info('Test ended')
149195

196+
150197
def _set_optional_time_prop(params_in: dict, name_in: str, params_out: dict, name_out: str):
151198
if params_in.get(name_in) is not None:
152199
params_out[name_out] = params_in[name_in] / 1000.0

contract-tests/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
Flask==2.3.2
2+
requests>=2.31.0
23
urllib3>=1.22.0,<3

contract-tests/service.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ def status():
6868
'context-type',
6969
'secure-mode-hash',
7070
'tags',
71+
'migrations',
72+
'event-sampling'
7173
]
7274
}
7375
return (json.dumps(body), 200, {'Content-type': 'application/json'})
@@ -130,6 +132,10 @@ def post_client_command(id):
130132
response = client.context_convert(sub_params)
131133
elif command == "getBigSegmentStoreStatus":
132134
response = client.get_big_segment_store_status()
135+
elif command == "migrationVariation":
136+
response = client.migration_variation(sub_params)
137+
elif command == "migrationOperation":
138+
response = client.migration_operation(sub_params)
133139
else:
134140
return ('', 400)
135141

contract-tests/setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pycodestyle]
2+
ignore = E501

docs/api-main.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,10 @@ ldclient.evaluation module
2626
.. automodule:: ldclient.evaluation
2727
:members:
2828
:special-members: __init__
29+
30+
ldclient.migrations module
31+
--------------------------
32+
33+
.. automodule:: ldclient.migrations
34+
:members:
35+
:special-members: __init__

ldclient/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
"""
44

55
from ldclient.impl.rwlock import ReadWriteLock as _ReadWriteLock
6-
from ldclient.impl.util import log
6+
from ldclient.impl.util import log, Result
77
from ldclient.version import VERSION
88
from .client import *
99
from .context import *
10+
from .migrations import *
1011

1112
__version__ = VERSION
1213

@@ -104,9 +105,11 @@ def _reset_client():
104105
'ContextBuilder',
105106
'ContextMultiBuilder',
106107
'LDClient',
108+
'Result',
107109
'client',
108110
'context',
109111
'evaluation',
110112
'integrations',
111-
'interfaces'
113+
'interfaces',
114+
'migrations'
112115
]

0 commit comments

Comments
 (0)