Skip to content

Commit

Permalink
prepare 7.2.1 release (#160)
Browse files Browse the repository at this point in the history
  • Loading branch information
LaunchDarklyReleaseBot authored Dec 4, 2021
1 parent 2827558 commit 1a95f97
Show file tree
Hide file tree
Showing 23 changed files with 584 additions and 45 deletions.
42 changes: 31 additions & 11 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,23 @@ workflows:
jobs:
- test-linux:
name: Python 3.5
docker-image: circleci/python:3.5-jessie
docker-image: cimg/python:3.5
skip-sse-contract-tests: true # the test service app has dependencies that aren't available in 3.5, which is EOL anyway
- test-linux:
name: Python 3.6
docker-image: circleci/python:3.6-jessie
docker-image: cimg/python:3.6
- test-linux:
name: Python 3.7
docker-image: circleci/python:3.7-stretch
docker-image: cimg/python:3.7
- test-linux:
name: Python 3.8
docker-image: circleci/python:3.8-buster
docker-image: cimg/python:3.8
- test-linux:
name: Python 3.9
docker-image: circleci/python:3.9-rc-buster
docker-image: cimg/python:3.9
- test-linux:
name: Python 3.10
docker-image: cimg/python:3.10
- test-windows:
name: Windows Python 3
py3: true
Expand All @@ -39,6 +43,9 @@ jobs:
test-with-mypy:
type: boolean
default: true
skip-sse-contract-tests:
type: boolean
default: false
docker:
- image: <<parameters.docker-image>>
- image: redis
Expand All @@ -49,12 +56,10 @@ jobs:
- run:
name: install requirements
command: |
sudo pip install --upgrade pip;
sudo pip install 'virtualenv~=16.0';
sudo pip install -r test-requirements.txt;
sudo pip install -r test-filesource-optional-requirements.txt;
sudo pip install -r consul-requirements.txt;
sudo python setup.py install;
pip install -r test-requirements.txt;
pip install -r test-filesource-optional-requirements.txt;
pip install -r consul-requirements.txt;
python setup.py install;
pip freeze
- when:
condition: <<parameters.test-with-codeclimate>>
Expand Down Expand Up @@ -89,6 +94,21 @@ jobs:
command: |
export PATH="/home/circleci/.local/bin:$PATH"
mypy --config-file mypy.ini ldclient testing
- unless:
condition: <<parameters.skip-sse-contract-tests>>
steps:
- run:
name: build SSE contract test service
command: cd sse-contract-tests && make build-test-service
- run:
name: start SSE contract test service
command: cd sse-contract-tests && make start-test-service
background: true
- run:
name: run SSE contract tests
command: cd sse-contract-tests && make run-contract-tests

- store_test_results:
path: test-reports
- store_artifacts:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,4 @@ p2venv
test-packaging-venv

.vscode/
.python-version
14 changes: 9 additions & 5 deletions .ldrelease/config.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
version: 2

repo:
public: python-server-sdk
private: python-server-sdk-private
Expand All @@ -8,15 +10,17 @@ publications:
- url: https://launchdarkly-python-sdk.readthedocs.io/en/latest/
description: documentation (readthedocs.io)

releasableBranches:
branches:
- name: master
description: 7.x
- name: 6.x

template:
name: python
env:
LD_SKIP_DATABASE_TESTS: 1
jobs:
- docker: {}
template:
name: python
env:
LD_SKIP_DATABASE_TESTS: 1

sdk:
displayName: "Python"
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,11 @@ Note that starting with this release, generated API documentation is available o

## [6.8.0] - 2019-01-31
### Added:
- 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).
- 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).

## [6.7.0] - 2019-01-15
### Added:
- 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).
- 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).
- 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.

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

### Changed:
- 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).
- 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`.
- 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`.

### Removed:
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Contributing to the LaunchDarkly Server-side SDK for Python

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.
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.

## Submitting bug reports and feature requests

Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@

## LaunchDarkly overview

[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!
[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!

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

## Supported Python versions

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.
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.

## Getting started

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

## Learn more

Expand All @@ -40,7 +40,7 @@ We encourage pull requests and other contributions from the community. Check out
* 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?).
* 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.
* 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.
* 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.
* 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.
* Explore LaunchDarkly
* [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information
* [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides
Expand Down
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This is the API reference for the `LaunchDarkly <https://launchdarkly.com/>`_ SD

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>`_.

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

.. toctree::
:maxdepth: 2
Expand Down
2 changes: 1 addition & 1 deletion ldclient/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ def all_flags_state(self, user: dict, **kwargs) -> FeatureFlagsState:
"""Returns an object that encapsulates the state of all feature flags for a given user,
including the flag values and also metadata that can be used on the front end. See the
JavaScript SDK Reference Guide on
`Bootstrapping <https://docs.launchdarkly.com/docs/js-sdk-reference#section-bootstrapping>`_.
`Bootstrapping <https://docs.launchdarkly.com/sdk/features/bootstrapping#javascript>`_.
This method does not send analytics events back to LaunchDarkly.
Expand Down
2 changes: 1 addition & 1 deletion ldclient/flags_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class FeatureFlagsState:
calling the :func:`ldclient.client.LDClient.all_flags_state()` method. Serializing this
object to JSON, using the :func:`to_json_dict` method or ``jsonpickle``, will produce the
appropriate data structure for bootstrapping the LaunchDarkly JavaScript client. See the
JavaScript SDK Reference Guide on `Bootstrapping <https://docs.launchdarkly.com/docs/js-sdk-reference#section-bootstrapping>`_.
JavaScript SDK Reference Guide on `Bootstrapping <https://docs.launchdarkly.com/sdk/features/bootstrapping#javascript>`_.
"""
def __init__(self, valid: bool):
self.__flag_values = {} # type: Dict[str, Any]
Expand Down
191 changes: 191 additions & 0 deletions ldclient/impl/sse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import urllib3

from ldclient.config import HTTPConfig
from ldclient.impl.http import HTTPFactory
from ldclient.util import throw_if_unsuccessful_response


class _BufferedLineReader:
"""
Helper class that encapsulates the logic for reading UTF-8 stream data as a series of text lines,
each of which can be terminated by \n, \r, or \r\n.
"""
def lines_from(chunks):
"""
Takes an iterable series of encoded chunks (each of "bytes" type) and parses it into an iterable
series of strings, each of which is one line of text. The line does not include the terminator.
"""
last_char_was_cr = False
partial_line = None

for chunk in chunks:
if len(chunk) == 0:
continue

# bytes.splitlines() will correctly break lines at \n, \r, or \r\n, and is faster than
# iterating through the characters in Python code. However, we have to adjust the results
# in several ways as described below.
lines = chunk.splitlines()
if last_char_was_cr:
last_char_was_cr = False
if chunk[0] == 10:
# If the last character we saw was \r, and then the first character in buf is \n, then
# that's just a single \r\n terminator, so we should remove the extra blank line that
# splitlines added for that first \n.
lines.pop(0)
if len(lines) == 0:
continue # ran out of data, continue to get next chunk
if partial_line is not None:
# On our last time through the loop, we ended up with an unterminated line, so we should
# treat our first parsed line here as a continuation of that.
lines[0] = partial_line + lines[0]
partial_line = None
# Check whether the buffer really ended in a terminator. If it did not, then the last line in
# lines is a partial line and should not be emitted yet.
last_char = chunk[len(chunk)-1]
if last_char == 13:
last_char_was_cr = True # remember this in case the next chunk starts with \n
elif last_char != 10:
partial_line = lines.pop() # remove last element which is the partial line
for line in lines:
yield line.decode()


class Event:
"""
An event received by SSEClient.
"""
def __init__(self, event='message', data='', last_event_id=None):
self._event = event
self._data = data
self._id = last_event_id

@property
def event(self):
"""
The event type, or "message" if not specified.
"""
return self._event

@property
def data(self):
"""
The event data.
"""
return self._data

@property
def last_event_id(self):
"""
The last non-empty "id" value received from this stream so far.
"""
return self._id

def dump(self):
lines = []
if self.id:
lines.append('id: %s' % self.id)

# Only include an event line if it's not the default already.
if self.event != 'message':
lines.append('event: %s' % self.event)

lines.extend('data: %s' % d for d in self.data.split('\n'))
return '\n'.join(lines) + '\n\n'


class SSEClient:
"""
A simple Server-Sent Events client.
This implementation does not include automatic retrying of a dropped connection; the caller will do that.
If a connection ends, the events iterator will simply end.
"""
def __init__(self, url, last_id=None, http_factory=None, **kwargs):
self.url = url
self.last_id = last_id
self._chunk_size = 10000

if http_factory is None:
http_factory = HTTPFactory({}, HTTPConfig())
self._timeout = http_factory.timeout
base_headers = http_factory.base_headers

self.http = http_factory.create_pool_manager(1, url)

# Any extra kwargs will be fed into the request call later.
self.requests_kwargs = kwargs

# The SSE spec requires making requests with Cache-Control: nocache
if 'headers' not in self.requests_kwargs:
self.requests_kwargs['headers'] = {}

self.requests_kwargs['headers'].update(base_headers)

self.requests_kwargs['headers']['Cache-Control'] = 'no-cache'

# The 'Accept' header is not required, but explicit > implicit
self.requests_kwargs['headers']['Accept'] = 'text/event-stream'

self._connect()

def _connect(self):
if self.last_id:
self.requests_kwargs['headers']['Last-Event-ID'] = self.last_id

# Use session if set. Otherwise fall back to requests module.
self.resp = self.http.request(
'GET',
self.url,
timeout=self._timeout,
preload_content=False,
retries=0, # caller is responsible for implementing appropriate retry semantics, e.g. backoff
**self.requests_kwargs)

# Raw readlines doesn't work because we may be missing newline characters until the next chunk
# For some reason, we also need to specify a chunk size because stream=True doesn't seem to guarantee
# that we get the newlines in a timeline manner
self.resp_file = self.resp.stream(amt=self._chunk_size)

# TODO: Ensure we're handling redirects. Might also stick the 'origin'
# attribute on Events like the Javascript spec requires.
throw_if_unsuccessful_response(self.resp)

@property
def events(self):
"""
An iterable series of Event objects received from the stream.
"""
event_type = ""
event_data = None
for line in _BufferedLineReader.lines_from(self.resp_file):
if line == "":
if event_data is not None:
yield Event("message" if event_type == "" else event_type, event_data, self.last_id)
event_type = ""
event_data = None
continue
colon_pos = line.find(':')
if colon_pos < 0:
continue # malformed line - ignore
if colon_pos == 0:
continue # comment - currently we're not surfacing these
name = line[0:colon_pos]
if colon_pos < (len(line) - 1) and line[colon_pos + 1] == ' ':
colon_pos += 1
value = line[colon_pos+1:]
if name == 'event':
event_type = value
elif name == 'data':
event_data = value if event_data is None else (event_data + "\n" + value)
elif name == 'id':
self.last_id = value
elif name == 'retry':
pass # auto-reconnect is not implemented in this simplified client
# unknown field names are ignored in SSE

def __enter__(self):
return self

def __exit__(self, type, value, traceback):
self.close()
Loading

0 comments on commit 1a95f97

Please sign in to comment.