Skip to content

Commit 1a95f97

Browse files
prepare 7.2.1 release (launchdarkly#160)
1 parent 2827558 commit 1a95f97

23 files changed

+584
-45
lines changed

.circleci/config.yml

+31-11
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,23 @@ workflows:
88
jobs:
99
- test-linux:
1010
name: Python 3.5
11-
docker-image: circleci/python:3.5-jessie
11+
docker-image: cimg/python:3.5
12+
skip-sse-contract-tests: true # the test service app has dependencies that aren't available in 3.5, which is EOL anyway
1213
- test-linux:
1314
name: Python 3.6
14-
docker-image: circleci/python:3.6-jessie
15+
docker-image: cimg/python:3.6
1516
- test-linux:
1617
name: Python 3.7
17-
docker-image: circleci/python:3.7-stretch
18+
docker-image: cimg/python:3.7
1819
- test-linux:
1920
name: Python 3.8
20-
docker-image: circleci/python:3.8-buster
21+
docker-image: cimg/python:3.8
2122
- test-linux:
2223
name: Python 3.9
23-
docker-image: circleci/python:3.9-rc-buster
24+
docker-image: cimg/python:3.9
25+
- test-linux:
26+
name: Python 3.10
27+
docker-image: cimg/python:3.10
2428
- test-windows:
2529
name: Windows Python 3
2630
py3: true
@@ -39,6 +43,9 @@ jobs:
3943
test-with-mypy:
4044
type: boolean
4145
default: true
46+
skip-sse-contract-tests:
47+
type: boolean
48+
default: false
4249
docker:
4350
- image: <<parameters.docker-image>>
4451
- image: redis
@@ -49,12 +56,10 @@ jobs:
4956
- run:
5057
name: install requirements
5158
command: |
52-
sudo pip install --upgrade pip;
53-
sudo pip install 'virtualenv~=16.0';
54-
sudo pip install -r test-requirements.txt;
55-
sudo pip install -r test-filesource-optional-requirements.txt;
56-
sudo pip install -r consul-requirements.txt;
57-
sudo python setup.py install;
59+
pip install -r test-requirements.txt;
60+
pip install -r test-filesource-optional-requirements.txt;
61+
pip install -r consul-requirements.txt;
62+
python setup.py install;
5863
pip freeze
5964
- when:
6065
condition: <<parameters.test-with-codeclimate>>
@@ -89,6 +94,21 @@ jobs:
8994
command: |
9095
export PATH="/home/circleci/.local/bin:$PATH"
9196
mypy --config-file mypy.ini ldclient testing
97+
98+
- unless:
99+
condition: <<parameters.skip-sse-contract-tests>>
100+
steps:
101+
- run:
102+
name: build SSE contract test service
103+
command: cd sse-contract-tests && make build-test-service
104+
- run:
105+
name: start SSE contract test service
106+
command: cd sse-contract-tests && make start-test-service
107+
background: true
108+
- run:
109+
name: run SSE contract tests
110+
command: cd sse-contract-tests && make run-contract-tests
111+
92112
- store_test_results:
93113
path: test-reports
94114
- store_artifacts:

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,4 @@ p2venv
6969
test-packaging-venv
7070

7171
.vscode/
72+
.python-version

.ldrelease/config.yml

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
version: 2
2+
13
repo:
24
public: python-server-sdk
35
private: python-server-sdk-private
@@ -8,15 +10,17 @@ publications:
810
- url: https://launchdarkly-python-sdk.readthedocs.io/en/latest/
911
description: documentation (readthedocs.io)
1012

11-
releasableBranches:
13+
branches:
1214
- name: master
1315
description: 7.x
1416
- name: 6.x
1517

16-
template:
17-
name: python
18-
env:
19-
LD_SKIP_DATABASE_TESTS: 1
18+
jobs:
19+
- docker: {}
20+
template:
21+
name: python
22+
env:
23+
LD_SKIP_DATABASE_TESTS: 1
2024

2125
sdk:
2226
displayName: "Python"

CHANGELOG.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,11 @@ Note that starting with this release, generated API documentation is available o
168168

169169
## [6.8.0] - 2019-01-31
170170
### Added:
171-
- It is now possible to use Consul as a persistent feature store, similar to the existing Redis and DynamoDB integrations. See `Consul` in `ldclient.integrations`, and the reference guide for ["Using a persistent feature store"](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store).
171+
- It is now possible to use Consul as a persistent feature store, similar to the existing Redis and DynamoDB integrations. See `Consul` in `ldclient.integrations`, and the reference guide for ["Storing data"](https://docs.launchdarkly.com/sdk/features/storing-data#python).
172172

173173
## [6.7.0] - 2019-01-15
174174
### Added:
175-
- It is now possible to use DynamoDB as a persistent feature store, similar to the existing Redis integration. See `DynamoDB` in `ldclient.integrations`, and the reference guide to ["Using a persistent feature store"](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store).
175+
- It is now possible to use DynamoDB as a persistent feature store, similar to the existing Redis integration. See `DynamoDB` in `ldclient.integrations`, and the reference guide to ["Storing data"](https://docs.launchdarkly.com/sdk/features/storing-data#python).
176176
- The new class `CacheConfig` (in `ldclient.feature_store`) encapsulates all the parameters that control local caching in database feature stores. This takes the place of the `expiration` and `capacity` parameters that are in the deprecated `RedisFeatureStore` constructor; it can be used with DynamoDB and any other database integrations in the future, and if more caching options are added to `CacheConfig` they will be automatically supported in all of the feature stores.
177177

178178
### Deprecated:
@@ -261,7 +261,7 @@ _This release was broken and has been removed._
261261
## [6.0.0] - 2018-05-10
262262

263263
### Changed:
264-
- To reduce the network bandwidth used for analytics events, feature request events are now sent as counters rather than individual events, and user details are now sent only at intervals rather than in each event. These behaviors can be modified through the LaunchDarkly UI and with the new configuration option `inline_users_in_events`. For more details, see [Analytics Data Stream Reference](https://docs.launchdarkly.com/v2.0/docs/analytics-data-stream-reference).
264+
- To reduce the network bandwidth used for analytics events, feature request events are now sent as counters rather than individual events, and user details are now sent only at intervals rather than in each event. These behaviors can be modified through the LaunchDarkly UI and with the new configuration option `inline_users_in_events`.
265265
- The analytics event processor now flushes events at a configurable interval defaulting to 5 seconds, like the other SDKs (previously it flushed if no events had been posted for 5 seconds, or if events exceeded a configurable number). This interval is set by the new `Config` property `flush_interval`.
266266

267267
### Removed:

CONTRIBUTING.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Contributing to the LaunchDarkly Server-side SDK for Python
22

3-
LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK.
3+
LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/sdk/concepts/contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK.
44

55
## Submitting bug reports and feature requests
66

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@
77

88
## LaunchDarkly overview
99

10-
[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today!
10+
[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today!
1111

1212
[![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly)
1313

1414
## Supported Python versions
1515

16-
This version of the LaunchDarkly SDK is compatible with Python 3.5 through 3.9. It is tested with the most recent patch releases of those versions. Python versions 2.7 to 3.4 are no longer supported.
16+
This version of the LaunchDarkly SDK is compatible with Python 3.5 through 3.10. It is tested with the most recent patch releases of those versions. Python versions 2.7 to 3.4 are no longer supported.
1717

1818
## Getting started
1919

20-
Refer to the [SDK reference guide](https://docs.launchdarkly.com/docs/python-sdk-reference) for instructions on getting started with using the SDK.
20+
Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/python) for instructions on getting started with using the SDK.
2121

2222
## Learn more
2323

@@ -40,7 +40,7 @@ We encourage pull requests and other contributions from the community. Check out
4040
* Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).
4141
* Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.
4242
* Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline.
43-
* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/docs) for a complete list.
43+
* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list.
4444
* Explore LaunchDarkly
4545
* [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information
4646
* [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides

docs/index.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ This is the API reference for the `LaunchDarkly <https://launchdarkly.com/>`_ SD
1010

1111
The latest version of the SDK can be found on `PyPI <https://pypi.org/project/launchdarkly-server-sdk/>`_, and the source code is on `GitHub <https://github.com/launchdarkly/python-server-sdk>`_.
1212

13-
For more information, see LaunchDarkly's `Quickstart <https://docs.launchdarkly.com/docs>`_ and `SDK Reference Guide <http://docs.launchdarkly.com/docs/python-sdk-reference>`_.
13+
For more information, see LaunchDarkly's `Quickstart <https://docs.launchdarkly.com/home>`_ and `SDK Reference Guide <https://docs.launchdarkly.com/sdk/server-side/python>`_.
1414

1515
.. toctree::
1616
:maxdepth: 2

ldclient/client.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,7 @@ def all_flags_state(self, user: dict, **kwargs) -> FeatureFlagsState:
331331
"""Returns an object that encapsulates the state of all feature flags for a given user,
332332
including the flag values and also metadata that can be used on the front end. See the
333333
JavaScript SDK Reference Guide on
334-
`Bootstrapping <https://docs.launchdarkly.com/docs/js-sdk-reference#section-bootstrapping>`_.
334+
`Bootstrapping <https://docs.launchdarkly.com/sdk/features/bootstrapping#javascript>`_.
335335
336336
This method does not send analytics events back to LaunchDarkly.
337337

ldclient/flags_state.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class FeatureFlagsState:
1212
calling the :func:`ldclient.client.LDClient.all_flags_state()` method. Serializing this
1313
object to JSON, using the :func:`to_json_dict` method or ``jsonpickle``, will produce the
1414
appropriate data structure for bootstrapping the LaunchDarkly JavaScript client. See the
15-
JavaScript SDK Reference Guide on `Bootstrapping <https://docs.launchdarkly.com/docs/js-sdk-reference#section-bootstrapping>`_.
15+
JavaScript SDK Reference Guide on `Bootstrapping <https://docs.launchdarkly.com/sdk/features/bootstrapping#javascript>`_.
1616
"""
1717
def __init__(self, valid: bool):
1818
self.__flag_values = {} # type: Dict[str, Any]

ldclient/impl/sse.py

+191
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import urllib3
2+
3+
from ldclient.config import HTTPConfig
4+
from ldclient.impl.http import HTTPFactory
5+
from ldclient.util import throw_if_unsuccessful_response
6+
7+
8+
class _BufferedLineReader:
9+
"""
10+
Helper class that encapsulates the logic for reading UTF-8 stream data as a series of text lines,
11+
each of which can be terminated by \n, \r, or \r\n.
12+
"""
13+
def lines_from(chunks):
14+
"""
15+
Takes an iterable series of encoded chunks (each of "bytes" type) and parses it into an iterable
16+
series of strings, each of which is one line of text. The line does not include the terminator.
17+
"""
18+
last_char_was_cr = False
19+
partial_line = None
20+
21+
for chunk in chunks:
22+
if len(chunk) == 0:
23+
continue
24+
25+
# bytes.splitlines() will correctly break lines at \n, \r, or \r\n, and is faster than
26+
# iterating through the characters in Python code. However, we have to adjust the results
27+
# in several ways as described below.
28+
lines = chunk.splitlines()
29+
if last_char_was_cr:
30+
last_char_was_cr = False
31+
if chunk[0] == 10:
32+
# If the last character we saw was \r, and then the first character in buf is \n, then
33+
# that's just a single \r\n terminator, so we should remove the extra blank line that
34+
# splitlines added for that first \n.
35+
lines.pop(0)
36+
if len(lines) == 0:
37+
continue # ran out of data, continue to get next chunk
38+
if partial_line is not None:
39+
# On our last time through the loop, we ended up with an unterminated line, so we should
40+
# treat our first parsed line here as a continuation of that.
41+
lines[0] = partial_line + lines[0]
42+
partial_line = None
43+
# Check whether the buffer really ended in a terminator. If it did not, then the last line in
44+
# lines is a partial line and should not be emitted yet.
45+
last_char = chunk[len(chunk)-1]
46+
if last_char == 13:
47+
last_char_was_cr = True # remember this in case the next chunk starts with \n
48+
elif last_char != 10:
49+
partial_line = lines.pop() # remove last element which is the partial line
50+
for line in lines:
51+
yield line.decode()
52+
53+
54+
class Event:
55+
"""
56+
An event received by SSEClient.
57+
"""
58+
def __init__(self, event='message', data='', last_event_id=None):
59+
self._event = event
60+
self._data = data
61+
self._id = last_event_id
62+
63+
@property
64+
def event(self):
65+
"""
66+
The event type, or "message" if not specified.
67+
"""
68+
return self._event
69+
70+
@property
71+
def data(self):
72+
"""
73+
The event data.
74+
"""
75+
return self._data
76+
77+
@property
78+
def last_event_id(self):
79+
"""
80+
The last non-empty "id" value received from this stream so far.
81+
"""
82+
return self._id
83+
84+
def dump(self):
85+
lines = []
86+
if self.id:
87+
lines.append('id: %s' % self.id)
88+
89+
# Only include an event line if it's not the default already.
90+
if self.event != 'message':
91+
lines.append('event: %s' % self.event)
92+
93+
lines.extend('data: %s' % d for d in self.data.split('\n'))
94+
return '\n'.join(lines) + '\n\n'
95+
96+
97+
class SSEClient:
98+
"""
99+
A simple Server-Sent Events client.
100+
101+
This implementation does not include automatic retrying of a dropped connection; the caller will do that.
102+
If a connection ends, the events iterator will simply end.
103+
"""
104+
def __init__(self, url, last_id=None, http_factory=None, **kwargs):
105+
self.url = url
106+
self.last_id = last_id
107+
self._chunk_size = 10000
108+
109+
if http_factory is None:
110+
http_factory = HTTPFactory({}, HTTPConfig())
111+
self._timeout = http_factory.timeout
112+
base_headers = http_factory.base_headers
113+
114+
self.http = http_factory.create_pool_manager(1, url)
115+
116+
# Any extra kwargs will be fed into the request call later.
117+
self.requests_kwargs = kwargs
118+
119+
# The SSE spec requires making requests with Cache-Control: nocache
120+
if 'headers' not in self.requests_kwargs:
121+
self.requests_kwargs['headers'] = {}
122+
123+
self.requests_kwargs['headers'].update(base_headers)
124+
125+
self.requests_kwargs['headers']['Cache-Control'] = 'no-cache'
126+
127+
# The 'Accept' header is not required, but explicit > implicit
128+
self.requests_kwargs['headers']['Accept'] = 'text/event-stream'
129+
130+
self._connect()
131+
132+
def _connect(self):
133+
if self.last_id:
134+
self.requests_kwargs['headers']['Last-Event-ID'] = self.last_id
135+
136+
# Use session if set. Otherwise fall back to requests module.
137+
self.resp = self.http.request(
138+
'GET',
139+
self.url,
140+
timeout=self._timeout,
141+
preload_content=False,
142+
retries=0, # caller is responsible for implementing appropriate retry semantics, e.g. backoff
143+
**self.requests_kwargs)
144+
145+
# Raw readlines doesn't work because we may be missing newline characters until the next chunk
146+
# For some reason, we also need to specify a chunk size because stream=True doesn't seem to guarantee
147+
# that we get the newlines in a timeline manner
148+
self.resp_file = self.resp.stream(amt=self._chunk_size)
149+
150+
# TODO: Ensure we're handling redirects. Might also stick the 'origin'
151+
# attribute on Events like the Javascript spec requires.
152+
throw_if_unsuccessful_response(self.resp)
153+
154+
@property
155+
def events(self):
156+
"""
157+
An iterable series of Event objects received from the stream.
158+
"""
159+
event_type = ""
160+
event_data = None
161+
for line in _BufferedLineReader.lines_from(self.resp_file):
162+
if line == "":
163+
if event_data is not None:
164+
yield Event("message" if event_type == "" else event_type, event_data, self.last_id)
165+
event_type = ""
166+
event_data = None
167+
continue
168+
colon_pos = line.find(':')
169+
if colon_pos < 0:
170+
continue # malformed line - ignore
171+
if colon_pos == 0:
172+
continue # comment - currently we're not surfacing these
173+
name = line[0:colon_pos]
174+
if colon_pos < (len(line) - 1) and line[colon_pos + 1] == ' ':
175+
colon_pos += 1
176+
value = line[colon_pos+1:]
177+
if name == 'event':
178+
event_type = value
179+
elif name == 'data':
180+
event_data = value if event_data is None else (event_data + "\n" + value)
181+
elif name == 'id':
182+
self.last_id = value
183+
elif name == 'retry':
184+
pass # auto-reconnect is not implemented in this simplified client
185+
# unknown field names are ignored in SSE
186+
187+
def __enter__(self):
188+
return self
189+
190+
def __exit__(self, type, value, traceback):
191+
self.close()

0 commit comments

Comments
 (0)