Skip to content

Commit db883f5

Browse files
LaunchDarklyReleaseBoteli-darklyLaunchDarklyCIapache-hbgangeli
authored
prepare 8.1.0 release (#193)
* implement our own retry logic & logging for event posts, don't use urllib3.Retry (#133) * remove support for indirect/patch and indirect/put * remove unused logic for individual flag/segment poll for indirect/patch * Ehaisley/84082/remove python2 (#136) * remove all references to six and remove queue fallback imports * remove NullHandler logger backwards compat * update circleci config to remove python 2.7 tests * remove ordereddict backwards compat * update setup.py to no longer list python 2.7 as compatible * no longer inherit from object for python 2 backwards compat * update readme and manifest to reflect python 2.7 removal * remove unicode type compatibility * remove 2.7 support from circleci * Allow authenticating with proxy This commit allows for authenticating with a proxy configured with the `http_proxy` environment variable. Authentication requires passing a header, and is not parsed by urllib3 from the proxy_url. * reimplement proxy tests for DRY and add test of proxy auth params * doc comment on auth params in proxy URL * add type hints to some of the public facing api. update some docs * Revert "add type hints to some of the public facing api." This reverts commit c35fa61. * Ehaisley/ch86857/type hints (#138) * add typehints to the public API * validate typehints in the public api and tests with mypy * remove all current deprecations (#139) * remove all currently deprecated classes, methods, arguments, and tests * also update semver usage to remove calls to deprecated functions and classes * remove global set_sdk_key, make SDK key required in Config (#140) * Removed the guides link * Pinning mypy and running it against different python versions (#141) * fix time zone mishandling that could make event debugging not work (#142) * fix 6.x build (#143) * fix time zone mishandling that could make event debugging not work (6.x) (#144) * prepare 6.13.3 release (#154) * Releasing version 6.13.3 * [ch99756] Add alias events (#145) * add support for experiment rollouts * fix unit test * address PR comments * use Releaser v2 config * Use newer docker images (#147) * Updates docs URLs * Add support for 3.10 (#150) * started work on FlagBuilder in as part of test data source implementation * finished FlagBuilder implementation and added FlagRuleBuilder implementation * added initial TestData interface and updated tests to not rely on test data internals * started data source implementation * changed FlagBuilder to public class; changed FlagBuilder attributes to be initialized in __init__ and eliminated use of try ... except: pass for handling empty attributes * (big segments 1) add public config/interface types * added implementation of test data source * docstring * formatting * ensure property doesn't return None * (big segments 2) implement evaluation, refactor eval logic & modules * linting * (big segments 3) implement big segment status tracking, wire up components * typing fixes * typing fixes * implement SSE contract tests * fix CI * fix CI again * fix CI * disable SSE tests in Python 3.5 * make test service port configurable * better SSE implementation that fixes linefeed and multi-byte char issues * fix constructor parameters in test service * comment * test improvements * rm obsolete default config logic * (big segments 4) implement big segment stores in Redis+DynamoDB, refactor db tests (#158) * converted ldclient.integrations module from file to directory; started moving public classes out of ldclient.impl.integrations.test_data* and instead into ldclient.integrations.test_data*; started adding TestData documentation * removed setup/teardown functions leftover from test scaffold * added TestData, FlagBuilder, and FlagRuleBuilder documentation; minor adjustments to implementation details * removed warning supression from TestData tests * fix big segments user hash algorithm to use SHA256 * update mypy version * updates to tests and related bug fixes * always cache Big Segment query result even if it's None * fix test assertion * lint * fix big segment ref format * fix big segments cache TTL being set to wrong value * fixed structure of fallthrough variation in result of FlagBuilder.build() * moved __test__ attribute into TestData class definition to prevent mypy from complaining about a missing class attribute * minor doc comment fix * Apply suggestions related to Sphinx docstring formatting from code review Co-authored-by: Eli Bishop <eli@launchdarkly.com> * fixed errors in the implementation of FlagBuilder's fallthrough_variation and off_variation when passing boolean variation values; updated tests to assert the expected behavior * added missing value_for_all_users() method to FlagBuilder class * Fix operator parsing errors (#169) * identify should not emit event if user key is empty (#164) * secondary should be treated as built-in attribute (#168) * URIs should have trailing slashes trimmed (#165) * all_flags_state should always include flag version (#166) * output event should not include a null prereqOf key (#167) * Account for traffic allocation on all flags (#171) * Add SDK contract tests (#170) * misc fixes to test data docs + add type hints * more type hints * remove some methods from the public test_data API * can't use "x|y" shortcut in typehints in older Pythons; use Union * fix misc type mistakes because I forgot to run the linter * update CONTRIBUTING.md and provide make targets * fixed a bug with flag rule clause builder internals; added unit test to verify rule evaluation * added ready argument to _TestDataSource class and indicated ready upon start to avoid delays in TestData initialization * Update contract tests to latest flask version (#176) Our contract tests depend on flask v1, which in turn depends on Jinja 2. Both of these are terribly dated and no longer supported. Jinja depends on markupsafe. markupsafe recently updated its code to no longer provide soft_unicode which in turn broke Jinja. Updating to the latest flask keeps all transitive dependencies better aligned and addresses this mismatch. * Adds link to Relay Proxy docs * Handle explicit None values in test payload (#179) The test harness may send explicit None values which should be treated the same as if the value was omitted entirely. * Fix "unhandled response" error in test harness (#180) When we return a `('', 204)` response from the flask handler, [Werkzeug intentionally removes the 'Content-Type' header][1], which causes the response to be created as a chunked response. The test harness is likely seeing a 204 response and isn't trying to read anything more from the stream. But since we are re-using connections, the next time it reads from the stream, it sees the `0\r\n\r\n` chunk and outputs an error: > 2022/04/20 14:23:39 Unsolicited response received on idle HTTP channel starting with "0\r\n\r\n"; err=<nil> Changing this response to 202 causes Werkzeug to return an empty response and silences the error. [1]: https://github.com/pallets/werkzeug/blob/560dd5f320bff318175f209595d42f5a80045417/src/werkzeug/wrappers/response.py#L540 * Exclude booleans when getting bucketable value (#181) When calculating a bucket, we get the bucketable value from the specified bucket by attribute. If this value is a string or an int, we can use it. Otherwise, we return None. Python considers a bool an instance of an int, which isn't what we want. So we need to add an explicit exclusion for this. * master -> main (#182) * Loosen restriction on expiringdict (#183) Originally this was pinned to a max version to deal with the incompatibility of Python 3.3 and the `typing` package. See [this PR][1]. Now that we now only support >=3.5, we can safely relax this restriction again. [1]: launchdarkly/python-server-sdk-private#120 * Fix mypy type checking (#184) A [customer requested][original-pr] that we start including a py.typed file in our repository. This would enable mypy to take advantage of our typehints. Unfortunately, this didn't completely solve the customers issue. A [second pr][second-pr] was opened to address the missing step of including the py.typed file in the `Manifest.in` file. However, this change alone is not sufficient. According to the [documentation][include_package_data], you must also include the `include_package_data=True` directive so that files specified in the `Manifest.in` file are included in distribution. [original-pr]: #166 [second-pr]: #172 [include_package_data]: https://setuptools.pypa.io/en/latest/userguide/datafiles.html#include-package-data * Add support for extra Redis connection parameters (#185) * Include wheel artifact when publishing package (#186) * remove warn-level logging done for every Big Segments query * skip tests that use a self-signed TLS cert in Python 3.7 * (U2C 1) drop EOL Python versions (#189) * drop EOL Python versions * misc cleanup, show Python version in CI * add Python 3.11 CI job * add Python 3.11 to package metadata * (U2C 2) remove alias event functionality (#187) * (U2C 3) remove inline users in events (#188) * (U2C 4) remove deprecated things (#192) * remove warn-level logging done for every Big Segments query (#190) * remove warn-level logging done for every Big Segments query * skip tests that use a self-signed TLS cert in Python 3.7 * implement context model * fix exports * specify exports * add copy constructor * minimal changes for SDK methods & evaluator to accept Context * update tests, add subscript method * lint * in type hints, must use Dict[A, B] rather than dict[A, B] for Python <3.9 * support context kind in clauses + enable v2 contract tests * misc fixes * misc fixes * support contextTargets * support contextKind in rollouts/experiments * support includedContexts/excludedContexts in segment * comment copyedit Co-authored-by: Matthew M. Keeler <mkeeler@launchdarkly.com> * comment fixes * rm unused Co-authored-by: Matthew M. Keeler <mkeeler@launchdarkly.com> * fix create_multi to support flattening * lint * use custom classes for flag/segment data model * use store adapter for safety * misc cleanup * misc fixes for persistent stores * more database store fixes * support attribute reference lookups in evaluations * pass logger from client * context kind logic for big segments + enable big segment contract tests * formatting fixes + test cleanup * prerequisite cycle detection * segment recursion * define custom classes for event data * add module init file * linting * fix prereq stack logic * (U2C 17) U2C changes for events, not including private attributes (#205) * private attribute redaction * move a lot of code out of top-level modules * TestData changes for contexts * general doc comment fixes for 8.0 * U2C configuration updates * update release metadata * store flag/segment target lists as sets * fix type hint * preprocess clause values for time/regex/semver operators * fix type checking for matches operator * Add application info support (#214) * Add application info support (#214) (#215) * Upgrade pip to fix failing CI build (#216) The CI build was failing because pip had an outdated list of available wheels for installation. Since it couldn't find a match, it was trying to build a package from source, which requires the rust compiler, which in turn isn't present on some of the docker images. By updating pip we get the updated list of available wheels, thereby allowing us to bypass source building and the need for the rust compiler entirely. * prepare 7.6.0 release (#192) * comment * add end-to-end unit tests for proxy config * indents * add 3.8 build * image name * fail on SyntaxWarning * typo * command syntax * pin expiringdict dependency for Python 3.3 compatibility * add Windows CircleCI job * periods are no longer valid in CircleCI job names * syntax fix * install Python in Windows * set path * move command * turn off debug logging * Py3 in Windows * config param * rm redundant step * choco switch * refactor Linux jobs using CircleCI 2.1 features * set log level before anything else * rm Azure config * use yaml.safe_load() to avoid code execution vulnerability in file data source * Initial work on wrapper_name, wrapper_version, diagnostic config options and start of diagnostic config event creation. * Python 2 compat changes. * More event generation code and starting to integrate tracking diagnostic values. * Add minimum diagnostic recording interval. Fix diagnostic.py to be importable. Add more diagnostic event fields. * don't let user fall outside of last bucket in rollout * fixing conditional logic * Add docstrings for diagnostic configuration options. * fix off-by-1 error * avoid redundant dict lookups * add unit tests for basic bucketing logic and edge case * Stream init tracking. Feeding of accumulator object through SDK. Various fixes. * Track events in last batch. * Fix sdk version field, some stylistic improvements. * Last of diagnostic configuration object fields. * Fill out rest of platform fields. * Cleanup and failed stream initialization tracking. * Add diagnostic config option test. * Add tests for diagnostics.py * Testing rest of diagnostic fields. * Test that streaming update processor records successful and unsuccessful connection attempts in the diagnostic accumulator when available. * Improvements to testability of event processor. * Rest of event processor tests. * Remove janky reflection. * Test change to filesource optional test requirements. * [ch61092] Add event payload ID on event requests. * normalize data store type and OS name in diagnostic events * gitignore * copyedit to diagnostic event config property comment * fix spurious error after sending diagnostic event * make verify_ssl=False turn off certificate verification too (#129) * add more TLS config options and collect HTTP/HTTPS config options in a class (#130) * make stream retry/backoff/jitter behavior consistent with other SDKs + improve testing (#131) * streams shouldn't use the same read timeout as the rest of the SDK (#132) * implement our own retry logic & logging for event posts, don't use urllib3.Retry (#133) * remove support for indirect/patch and indirect/put * remove unused logic for individual flag/segment poll for indirect/patch * Ehaisley/84082/remove python2 (#136) * remove all references to six and remove queue fallback imports * remove NullHandler logger backwards compat * update circleci config to remove python 2.7 tests * remove ordereddict backwards compat * update setup.py to no longer list python 2.7 as compatible * no longer inherit from object for python 2 backwards compat * update readme and manifest to reflect python 2.7 removal * remove unicode type compatibility * remove 2.7 support from circleci * Allow authenticating with proxy This commit allows for authenticating with a proxy configured with the `http_proxy` environment variable. Authentication requires passing a header, and is not parsed by urllib3 from the proxy_url. * reimplement proxy tests for DRY and add test of proxy auth params * doc comment on auth params in proxy URL * add type hints to some of the public facing api. update some docs * Revert "add type hints to some of the public facing api." This reverts commit c35fa61. * Ehaisley/ch86857/type hints (#138) * add typehints to the public API * validate typehints in the public api and tests with mypy * remove all current deprecations (#139) * remove all currently deprecated classes, methods, arguments, and tests * also update semver usage to remove calls to deprecated functions and classes * remove global set_sdk_key, make SDK key required in Config (#140) * Removed the guides link * Pinning mypy and running it against different python versions (#141) * fix time zone mishandling that could make event debugging not work (#142) * fix 6.x build (#143) * fix time zone mishandling that could make event debugging not work (6.x) (#144) * prepare 6.13.3 release (#154) * Releasing version 6.13.3 * [ch99756] Add alias events (#145) * add support for experiment rollouts * fix unit test * address PR comments * use Releaser v2 config * Use newer docker images (#147) * Updates docs URLs * Add support for 3.10 (#150) * started work on FlagBuilder in as part of test data source implementation * finished FlagBuilder implementation and added FlagRuleBuilder implementation * added initial TestData interface and updated tests to not rely on test data internals * started data source implementation * changed FlagBuilder to public class; changed FlagBuilder attributes to be initialized in __init__ and eliminated use of try ... except: pass for handling empty attributes * (big segments 1) add public config/interface types * added implementation of test data source * docstring * formatting * ensure property doesn't return None * (big segments 2) implement evaluation, refactor eval logic & modules * linting * (big segments 3) implement big segment status tracking, wire up components * typing fixes * typing fixes * implement SSE contract tests * fix CI * fix CI again * fix CI * disable SSE tests in Python 3.5 * make test service port configurable * better SSE implementation that fixes linefeed and multi-byte char issues * fix constructor parameters in test service * comment * test improvements * rm obsolete default config logic * (big segments 4) implement big segment stores in Redis+DynamoDB, refactor db tests (#158) * converted ldclient.integrations module from file to directory; started moving public classes out of ldclient.impl.integrations.test_data* and instead into ldclient.integrations.test_data*; started adding TestData documentation * removed setup/teardown functions leftover from test scaffold * added TestData, FlagBuilder, and FlagRuleBuilder documentation; minor adjustments to implementation details * removed warning supression from TestData tests * fix big segments user hash algorithm to use SHA256 * update mypy version * updates to tests and related bug fixes * always cache Big Segment query result even if it's None * fix test assertion * lint * fix big segment ref format * fix big segments cache TTL being set to wrong value * fixed structure of fallthrough variation in result of FlagBuilder.build() * moved __test__ attribute into TestData class definition to prevent mypy from complaining about a missing class attribute * minor doc comment fix * Apply suggestions related to Sphinx docstring formatting from code review Co-authored-by: Eli Bishop <eli@launchdarkly.com> * fixed errors in the implementation of FlagBuilder's fallthrough_variation and off_variation when passing boolean variation values; updated tests to assert the expected behavior * added missing value_for_all_users() method to FlagBuilder class * Fix operator parsing errors (#169) * identify should not emit event if user key is empty (#164) * secondary should be treated as built-in attribute (#168) * URIs should have trailing slashes trimmed (#165) * all_flags_state should always include flag version (#166) * output event should not include a null prereqOf key (#167) * Account for traffic allocation on all flags (#171) * Add SDK contract tests (#170) * misc fixes to test data docs + add type hints * more type hints * remove some methods from the public test_data API * can't use "x|y" shortcut in typehints in older Pythons; use Union * fix misc type mistakes because I forgot to run the linter * update CONTRIBUTING.md and provide make targets * fixed a bug with flag rule clause builder internals; added unit test to verify rule evaluation * added ready argument to _TestDataSource class and indicated ready upon start to avoid delays in TestData initialization * Update contract tests to latest flask version (#176) Our contract tests depend on flask v1, which in turn depends on Jinja 2. Both of these are terribly dated and no longer supported. Jinja depends on markupsafe. markupsafe recently updated its code to no longer provide soft_unicode which in turn broke Jinja. Updating to the latest flask keeps all transitive dependencies better aligned and addresses this mismatch. * Adds link to Relay Proxy docs * Handle explicit None values in test payload (#179) The test harness may send explicit None values which should be treated the same as if the value was omitted entirely. * Fix "unhandled response" error in test harness (#180) When we return a `('', 204)` response from the flask handler, [Werkzeug intentionally removes the 'Content-Type' header][1], which causes the response to be created as a chunked response. The test harness is likely seeing a 204 response and isn't trying to read anything more from the stream. But since we are re-using connections, the next time it reads from the stream, it sees the `0\r\n\r\n` chunk and outputs an error: > 2022/04/20 14:23:39 Unsolicited response received on idle HTTP channel starting with "0\r\n\r\n"; err=<nil> Changing this response to 202 causes Werkzeug to return an empty response and silences the error. [1]: https://github.com/pallets/werkzeug/blob/560dd5f320bff318175f209595d42f5a80045417/src/werkzeug/wrappers/response.py#L540 * Exclude booleans when getting bucketable value (#181) When calculating a bucket, we get the bucketable value from the specified bucket by attribute. If this value is a string or an int, we can use it. Otherwise, we return None. Python considers a bool an instance of an int, which isn't what we want. So we need to add an explicit exclusion for this. * master -> main (#182) * Loosen restriction on expiringdict (#183) Originally this was pinned to a max version to deal with the incompatibility of Python 3.3 and the `typing` package. See [this PR][1]. Now that we now only support >=3.5, we can safely relax this restriction again. [1]: launchdarkly/python-server-sdk-private#120 * Fix mypy type checking (#184) A [customer requested][original-pr] that we start including a py.typed file in our repository. This would enable mypy to take advantage of our typehints. Unfortunately, this didn't completely solve the customers issue. A [second pr][second-pr] was opened to address the missing step of including the py.typed file in the `Manifest.in` file. However, this change alone is not sufficient. According to the [documentation][include_package_data], you must also include the `include_package_data=True` directive so that files specified in the `Manifest.in` file are included in distribution. [original-pr]: #166 [second-pr]: #172 [include_package_data]: https://setuptools.pypa.io/en/latest/userguide/datafiles.html#include-package-data * Add support for extra Redis connection parameters (#185) * Include wheel artifact when publishing package (#186) * skip tests that use a self-signed TLS cert in Python 3.7 * remove warn-level logging done for every Big Segments query (#190) * remove warn-level logging done for every Big Segments query * skip tests that use a self-signed TLS cert in Python 3.7 * update release metadata * Add application info support (#214) * Upgrade pip to fix failing CI build (#216) The CI build was failing because pip had an outdated list of available wheels for installation. Since it couldn't find a match, it was trying to build a package from source, which requires the rust compiler, which in turn isn't present on some of the docker images. By updating pip we get the updated list of available wheels, thereby allowing us to bypass source building and the need for the rust compiler entirely. --------- Co-authored-by: Eli Bishop <eli@launchdarkly.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: Ember Stevens <ember.stevens@launchdarkly.com> Co-authored-by: ember-stevens <79482775+ember-stevens@users.noreply.github.com> Co-authored-by: Matthew M. Keeler <keelerm84@gmail.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 <mkeeler@launchdarkly.com> * Releasing version 7.6.0 --------- Co-authored-by: Eli Bishop <eli@launchdarkly.com> Co-authored-by: LaunchDarklyCI <dev@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: Ember Stevens <ember.stevens@launchdarkly.com> Co-authored-by: ember-stevens <79482775+ember-stevens@users.noreply.github.com> Co-authored-by: Matthew M. Keeler <keelerm84@gmail.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 <mkeeler@launchdarkly.com> Co-authored-by: Ben Woskow <bwoskow@launchdarkly.com> Co-authored-by: Gavin Whelan <gwhelan@launchdarkly.com>
1 parent 8512f4f commit db883f5

File tree

10 files changed

+137
-10
lines changed

10 files changed

+137
-10
lines changed

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ jobs:
5656
- run:
5757
name: install requirements
5858
command: |
59+
pip install --upgrade pip
5960
pip install -r test-requirements.txt;
6061
pip install -r test-filesource-optional-requirements.txt;
6162
pip install -r consul-requirements.txt;

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
All notable changes to the LaunchDarkly Python SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
44

5+
## [7.6.0] - 2023-01-31
6+
### Added:
7+
- Introduced support for an `application` config property which sets application metadata that may be used in LaunchDarkly analytics or other product features. . This does not affect feature flag evaluations.
8+
59
## [8.0.0] - 2022-12-30
610
The latest version of this SDK supports LaunchDarkly's new custom contexts feature. Contexts are an evolution of a previously-existing concept, "users." Contexts let you create targeting rules for feature flags based on a variety of different information, including attributes pertaining to users, organizations, devices, and more. You can even combine contexts to create "multi-contexts."
711

contract-tests/client_entity.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ def __init__(self, tag, config):
1818
self.log = logging.getLogger(tag)
1919
opts = {"sdk_key": config["credential"]}
2020

21+
tags = config.get('tags', {})
22+
if tags:
23+
opts['application'] = {
24+
'id': tags.get('applicationId', ''),
25+
'version': tags.get('applicationVersion', ''),
26+
}
27+
2128
if config.get("streaming") is not None:
2229
streaming = config["streaming"]
2330
if streaming.get("baseUri") is not None:

contract-tests/service.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def status():
6767
'big-segments',
6868
'context-type',
6969
'secure-mode-hash',
70+
'tags',
7071
]
7172
}
7273
return (json.dumps(body), 200, {'Content-type': 'application/json'})
@@ -131,7 +132,7 @@ def post_client_command(id):
131132
response = client.get_big_segment_store_status()
132133
else:
133134
return ('', 400)
134-
135+
135136
if response is None:
136137
return ('', 201)
137138
return (json.dumps(response), 200)

ldclient/config.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Optional, Callable, List, Set
88

99
from ldclient.feature_store import InMemoryFeatureStore
10-
from ldclient.impl.util import log
10+
from ldclient.impl.util import log, validate_application_info
1111
from ldclient.interfaces import BigSegmentStore, EventProcessor, FeatureStore, UpdateProcessor
1212

1313
GET_LATEST_FEATURES_PATH = '/sdk/latest-flags'
@@ -60,15 +60,15 @@ def __init__(self,
6060
self.__status_poll_interval = status_poll_interval
6161
self.__stale_after = stale_after
6262
pass
63-
63+
6464
@property
6565
def store(self) -> Optional[BigSegmentStore]:
6666
return self.__store
67-
67+
6868
@property
6969
def context_cache_size(self) -> int:
7070
return self.__context_cache_size
71-
71+
7272
@property
7373
def context_cache_time(self) -> float:
7474
return self.__context_cache_time
@@ -77,7 +77,7 @@ def context_cache_time(self) -> float:
7777
def user_cache_size(self) -> int:
7878
"""Deprecated alias for :attr:`context_cache_size`."""
7979
return self.context_cache_size
80-
80+
8181
@property
8282
def user_cache_time(self) -> float:
8383
"""Deprecated alias for :attr:`context_cache_time`."""
@@ -86,7 +86,7 @@ def user_cache_time(self) -> float:
8686
@property
8787
def status_poll_interval(self) -> float:
8888
return self.__status_poll_interval
89-
89+
9090
@property
9191
def stale_after(self) -> float:
9292
return self.__stale_after
@@ -169,7 +169,7 @@ def __init__(self,
169169
initial_reconnect_delay: float=1,
170170
defaults: dict={},
171171
send_events: Optional[bool]=None,
172-
update_processor_class: Optional[Callable[[str, 'Config', FeatureStore], UpdateProcessor]]=None,
172+
update_processor_class: Optional[Callable[[str, 'Config', FeatureStore], UpdateProcessor]]=None,
173173
poll_interval: float=30,
174174
use_ldd: bool=False,
175175
feature_store: Optional[FeatureStore]=None,
@@ -188,7 +188,8 @@ def __init__(self,
188188
wrapper_name: Optional[str]=None,
189189
wrapper_version: Optional[str]=None,
190190
http: HTTPConfig=HTTPConfig(),
191-
big_segments: Optional[BigSegmentsConfig]=None):
191+
big_segments: Optional[BigSegmentsConfig]=None,
192+
application: Optional[dict]=None):
192193
"""
193194
:param sdk_key: The SDK key for your LaunchDarkly account. This is always required.
194195
:param base_uri: The base URL for the LaunchDarkly server. Most users should use the default
@@ -256,6 +257,7 @@ def __init__(self,
256257
servers.
257258
:param http: Optional properties for customizing the client's HTTP/HTTPS behavior. See
258259
:class:`HTTPConfig`.
260+
:param application: Optional properties for setting application metadata. See :py:attr:`~application`
259261
"""
260262
self.__sdk_key = sdk_key
261263

@@ -287,6 +289,7 @@ def __init__(self,
287289
self.__wrapper_version = wrapper_version
288290
self.__http = http
289291
self.__big_segments = BigSegmentsConfig() if not big_segments else big_segments
292+
self.__application = validate_application_info(application or {}, log)
290293

291294
def copy_with_new_sdk_key(self, new_sdk_key: str) -> 'Config':
292295
"""Returns a new ``Config`` instance that is the same as this one, except for having a different SDK key.
@@ -459,9 +462,21 @@ def http(self) -> HTTPConfig:
459462
def big_segments(self) -> BigSegmentsConfig:
460463
return self.__big_segments
461464

465+
@property
466+
def application(self) -> dict:
467+
"""
468+
An object that allows configuration of application metadata.
469+
470+
Application metadata may be used in LaunchDarkly analytics or other
471+
product features, but does not affect feature flag evaluations.
472+
473+
If you want to set non-default values for any of these fields, provide
474+
the appropriately configured dict to the {Config} object.
475+
"""
476+
return self.__application
477+
462478
def _validate(self):
463479
if self.offline is False and self.sdk_key is None or self.sdk_key == '':
464480
log.warning("Missing or blank sdk_key.")
465481

466-
467482
__all__ = ['Config', 'BigSegmentsConfig', 'HTTPConfig']

ldclient/impl/http.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,34 @@
33
from os import environ
44
import urllib3
55

6+
def _application_header_value(application: dict) -> str:
7+
parts = []
8+
id = application.get('id', '')
9+
version = application.get('version', '')
10+
11+
if id:
12+
parts.append("application-id/%s" % id)
13+
14+
if version:
15+
parts.append("application-version/%s" % version)
16+
17+
return " ".join(parts)
18+
19+
620
def _base_headers(config):
721
headers = {'Authorization': config.sdk_key or '',
822
'User-Agent': 'PythonClient/' + VERSION}
23+
24+
app_value = _application_header_value(config.application)
25+
if app_value:
26+
headers['X-LaunchDarkly-Tags'] = app_value
27+
928
if isinstance(config.wrapper_name, str) and config.wrapper_name != "":
1029
wrapper_version = ""
1130
if isinstance(config.wrapper_version, str) and config.wrapper_version != "":
1231
wrapper_version = "/" + config.wrapper_version
1332
headers.update({'X-LaunchDarkly-Wrapper': config.wrapper_name + wrapper_version})
33+
1434
return headers
1535

1636
def _http_factory(config):

ldclient/impl/util.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import logging
2+
import re
23
import sys
34
import time
45

6+
from typing import Any
57
from ldclient.impl.http import _base_headers
68

79

@@ -24,6 +26,26 @@ def current_time_millis() -> int:
2426

2527
_retryable_statuses = [400, 408, 429]
2628

29+
def validate_application_info(application: dict, logger: logging.Logger) -> dict:
30+
return {
31+
"id": validate_application_value(application.get("id", ""), "id", logger),
32+
"version": validate_application_value(application.get("version", ""), "version", logger),
33+
}
34+
35+
def validate_application_value(value: Any, name: str, logger: logging.Logger) -> str:
36+
if not isinstance(value, str):
37+
return ""
38+
39+
if len(value) > 64:
40+
logger.warning('Value of application[%s] was longer than 64 characters and was discarded' % name)
41+
return ""
42+
43+
if re.search(r"[^a-zA-Z0-9._-]", value):
44+
logger.warning('Value of application[%s] contained invalid characters and was discarded' % name)
45+
return ""
46+
47+
return value
48+
2749
def _headers(config):
2850
base_headers = _base_headers(config)
2951
base_headers.update({'Content-Type': "application/json"})

testing/impl/datasource/test_feature_requester.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def test_get_all_data_sends_headers():
3232
assert req.headers['Authorization'] == 'sdk-key'
3333
assert req.headers['User-Agent'] == 'PythonClient/' + VERSION
3434
assert req.headers.get('X-LaunchDarkly-Wrapper') is None
35+
assert req.headers.get('X-LaunchDarkly-Tags') is None
3536

3637
def test_get_all_data_sends_wrapper_header():
3738
with start_server() as server:
@@ -59,6 +60,19 @@ def test_get_all_data_sends_wrapper_header_without_version():
5960
req = server.require_request()
6061
assert req.headers.get('X-LaunchDarkly-Wrapper') == 'Flask'
6162

63+
def test_get_all_data_sends_tags_header():
64+
with start_server() as server:
65+
config = Config(sdk_key = 'sdk-key', base_uri = server.uri,
66+
application = {"id": "my-id", "version": "my-version"})
67+
fr = FeatureRequesterImpl(config)
68+
69+
resp_data = { 'flags': {}, 'segments': {} }
70+
server.for_path('/sdk/latest-all', JsonResponse(resp_data))
71+
72+
fr.get_all_data()
73+
req = server.require_request()
74+
assert req.headers.get('X-LaunchDarkly-Tags') == 'application-id/my-id application-version/my-version'
75+
6276
def test_get_all_data_can_use_cached_data():
6377
with start_server() as server:
6478
config = Config(sdk_key = 'sdk-key', base_uri = server.uri)

testing/impl/datasource/test_streaming.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def test_request_properties():
4040
assert req.headers.get('Authorization') == 'sdk-key'
4141
assert req.headers.get('User-Agent') == 'PythonClient/' + VERSION
4242
assert req.headers.get('X-LaunchDarkly-Wrapper') is None
43+
assert req.headers.get('X-LaunchDarkly-Tags') is None
4344

4445
def test_sends_wrapper_header():
4546
store = InMemoryFeatureStore()
@@ -71,6 +72,21 @@ def test_sends_wrapper_header_without_version():
7172
req = server.await_request()
7273
assert req.headers.get('X-LaunchDarkly-Wrapper') == 'Flask'
7374

75+
def test_sends_tag_header():
76+
store = InMemoryFeatureStore()
77+
ready = Event()
78+
79+
with start_server() as server:
80+
with stream_content(make_put_event()) as stream:
81+
config = Config(sdk_key = 'sdk-key', stream_uri = server.uri,
82+
application = {"id": "my-id", "version": "my-version"})
83+
server.for_path('/all', stream)
84+
85+
with StreamingUpdateProcessor(config, store, ready, None) as sp:
86+
sp.start()
87+
req = server.await_request()
88+
assert req.headers.get('X-LaunchDarkly-Tags') == 'application-id/my-id application-version/my-version'
89+
7490
def test_receives_put_event():
7591
store = InMemoryFeatureStore()
7692
ready = Event()

testing/test_config.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from ldclient.config import Config
2+
import pytest
23

34

45
def test_copy_config():
@@ -40,3 +41,29 @@ def test_trims_trailing_slashes_on_uris():
4041
assert config.base_uri == "https://launchdarkly.com"
4142
assert config.events_uri == "https://docs.launchdarkly.com/bulk"
4243
assert config.stream_base_uri == "https://blog.launchdarkly.com"
44+
45+
def application_can_be_set_and_read():
46+
application = {"id": "my-id", "version": "abcdef"}
47+
config = Config(sdk_key = "SDK_KEY", application = application)
48+
assert config.application == {"id": "my-id", "version": "abcdef"}
49+
50+
def application_can_handle_non_string_values():
51+
application = {"id": 1, "version": 2}
52+
config = Config(sdk_key = "SDK_KEY", application = application)
53+
assert config.application == {"id": "1", "version": "2"}
54+
55+
def application_will_ignore_invalid_keys():
56+
application = {"invalid": 1, "key": 2}
57+
config = Config(sdk_key = "SDK_KEY", application = application)
58+
assert config.application == {"id": "", "version": ""}
59+
60+
@pytest.fixture(params = [
61+
" ",
62+
"@",
63+
":",
64+
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-a"
65+
])
66+
def application_will_drop_invalid_values(value):
67+
application = {"id": value, "version": value}
68+
config = Config(sdk_key = "SDK_KEY", application = application)
69+
assert config.application == {"id": "", "version": ""}

0 commit comments

Comments
 (0)