diff --git a/CHANGES.txt b/CHANGES.txt index 0845c52e..2e3d313d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,7 @@ +10.6.1 (Jun 23, 2026) +- Updated splitChanges parsing to handle responses without the 'rbs' field. +- Fixed fallback treatment not being applied when a feature flag has an unsupported matcher type. + 10.6.0 (Jan 28, 2026) - Fixed non-blocking error when fetching feature flags from redis. - Added the ability to listen to different events triggered by the SDK. Read more in our docs. diff --git a/LICENSE.txt b/LICENSE similarity index 98% rename from LICENSE.txt rename to LICENSE index 954507dd..d6456956 100644 --- a/LICENSE.txt +++ b/LICENSE @@ -1,12 +1,18 @@ -Apache License + + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + 1. Definitions. + "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, @@ -14,19 +20,24 @@ Apache License direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications @@ -34,6 +45,7 @@ Apache License of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally @@ -47,15 +59,18 @@ Apache License Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable @@ -71,19 +86,24 @@ Apache License or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained @@ -100,12 +120,14 @@ Apache License or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of @@ -113,10 +135,12 @@ Apache License Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, @@ -126,6 +150,7 @@ Apache License PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly @@ -137,6 +162,7 @@ Apache License work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, @@ -147,8 +173,11 @@ Apache License defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + END OF TERMS AND CONDITIONS + APPENDIX: How to apply the Apache License to your work. + To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include @@ -157,11 +186,15 @@ Apache License file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2026 Harness Corporation + + Copyright [yyyy] [name of copyright owner] + Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. diff --git a/NOTICE.txt b/NOTICE similarity index 53% rename from NOTICE.txt rename to NOTICE index 7d7d845e..440c9e31 100644 --- a/NOTICE.txt +++ b/NOTICE @@ -1,5 +1,5 @@ -Harness Feature Management JavaScript SDK Copyright 2024-2026 Harness Inc. +Harness Feature Management Python SDK Copyright 2024-2026 Harness Inc. This product includes software developed at Harness Inc. (https://harness.io/). -This product includes software originally developed by Split Software, Inc. (https://www.split.io/). Copyright 2015-2024 Split Software, Inc. +This product includes software originally developed by Split Software, Inc. (https://www.split.io/). Copyright 2016-2024 Split Software, Inc. diff --git a/doc/source/introduction.rst b/doc/source/introduction.rst index a6df7a71..db989102 100644 --- a/doc/source/introduction.rst +++ b/doc/source/introduction.rst @@ -6,7 +6,7 @@ This project provides Python programs access to the `Split.io Installation and Requirements ----------------------------- -``splitio_client`` supports Python 3 (3.3 or later). Stable versions can be installed from `PyPI `_ using pip: :: +``splitio_client`` supports Python 3 (3.7 or later). Stable versions can be installed from `PyPI `_ using pip: :: pip install splitio_client @@ -14,95 +14,141 @@ and development versions are installed directly from the `Github >> from splitio import get_factory - >>> factory = get_factory('some_api_key') + >>> factory = get_factory('YOUR_SDK_KEY') + >>> factory.block_until_ready(5) >>> client = factory.client() >>> client.get_treatment('some_user', 'some_feature') 'SOME_TREATMENT' +For asyncio environments: :: + + >>> from splitio import get_factory_async + >>> factory = await get_factory_async('YOUR_SDK_KEY') + >>> await factory.block_until_ready(5) + >>> client = factory.client() + >>> await client.get_treatment('some_user', 'some_feature') + 'SOME_TREATMENT' + Bucketing key ------------- -In advanced mode the key can be set as two different parts, one of them just to match the condition and the other one to calculate the treatment bucket :: +In advanced mode the key can be set as two different parts, one of them just to match the condition and the other one to calculate the treatment bucket: :: >>> from splitio import get_factory, Key >>> user = 'some_user_or_anonymous' >>> bucketing_key = 'some_random_string' >>> split_key = Key(user, bucketing_key) - >>> factory = get_factory('API_KEY') + >>> factory = get_factory('YOUR_SDK_KEY') + >>> factory.block_until_ready(5) >>> client = factory.client() >>> client.get_treatment(split_key, 'some_feature') Manager API ----------- -Manager API is very useful to get a representation (view) of cached splits: :: +Manager API is useful to get a representation (view) of cached feature flags: :: >>> from splitio import get_factory - >>> factory = get_factory('API_KEY') + >>> factory = get_factory('YOUR_SDK_KEY') + >>> factory.block_until_ready(5) >>> manager = factory.manager() Available methods: -**splits():** Returns a list of SplitView instance :: +**splits():** Returns a list of SplitView instances: :: + >>> manager.splits() -**split(name):** Returns a SplitView instance :: - >>> manager.split('some_test_name') +**split(name):** Returns a SplitView instance: :: + + >>> manager.split('some_feature_flag') + +**split_names():** Returns a list of feature flag names (String): :: -**split_names():** Returns a list of Split names (String) :: >>> manager.split_names() +Client API +---------- + +The client provides the following methods for evaluating feature flags: + +**get_treatment(key, feature_flag_name, attributes=None, evaluation_options=None):** Returns a treatment string for a single flag. + +**get_treatment_with_config(key, feature_flag_name, attributes=None, evaluation_options=None):** Returns a treatment string and configuration for a single flag. + +**get_treatments(key, feature_flag_names, attributes=None, evaluation_options=None):** Returns treatments for multiple flags. + +**get_treatments_with_config(key, feature_flag_names, attributes=None, evaluation_options=None):** Returns treatments and configurations for multiple flags. + +**get_treatments_by_flag_set(key, flag_set, attributes=None, evaluation_options=None):** Returns treatments for all flags in a flag set. + +**get_treatments_by_flag_sets(key, flag_sets, attributes=None, evaluation_options=None):** Returns treatments for all flags in multiple flag sets. + +**get_treatments_with_config_by_flag_set(key, flag_set, attributes=None, evaluation_options=None):** Returns treatments and configs for all flags in a flag set. + +**get_treatments_with_config_by_flag_sets(key, flag_sets, attributes=None, evaluation_options=None):** Returns treatments and configs for all flags in multiple flag sets. + +**track(key, traffic_type, event_type, value=None, properties=None):** Tracks custom events. + +**destroy():** Gracefully shuts down the client and flushes pending data. + Client configuration -------------------- -It's possible to control certain aspects of the client behaviour by supplying a ``config`` dictionary. For instance, the following snippets shows you how to set the segment update interval to 10 seconds: :: +It's possible to control certain aspects of the client behaviour by supplying a ``config`` dictionary. For instance, the following snippet shows you how to set the segment update interval to 10 seconds: :: >>> from splitio import get_factory >>> config = {'segmentsRefreshRate': 10} - >>> factory = get_factory('some_api_key', config=config) + >>> factory = get_factory('YOUR_SDK_KEY', config=config) + >>> factory.block_until_ready(5) >>> client = factory.client() All the possible configuration options are: -+------------------------+------+--------------------------------------------------------+---------+ -| Key | Type | Description | Default | -+========================+======+========================================================+=========+ -| connectionTimeout | int | The timeout for HTTP connections in milliseconds. | 1500 | -+------------------------+------+--------------------------------------------------------+---------+ -| readTimeout | int | The read timeout for HTTP connections in milliseconds. | 1500 | -+------------------------+------+--------------------------------------------------------+---------+ -| featuresRefreshRate | int | The features (splits) update refresh period in | 5 | -| | | seconds. | | -+------------------------+------+--------------------------------------------------------+---------+ -| segmentsRefreshRate | int | The segments update refresh period in seconds. | 60 | -+------------------------+------+--------------------------------------------------------+---------+ -| metricsRefreshRate | int | The metrics report period in seconds | 60 | -+------------------------+------+--------------------------------------------------------+---------+ -| impressionsRefreshRate | int | The impressions report period in seconds | 60 | -+------------------------+------+--------------------------------------------------------+---------+ -| ready | int | How long to wait (in milliseconds) for the features | | -| | | and segments information to be available. If the | | -| | | timeout is exceeded, a ``TimeoutException`` will be | | -| | | raised. If value is 0, the constructor will return | | -| | | immediately but not all the information might be | | -| | | available right away. | | -+------------------------+------+--------------------------------------------------------+---------+ ++----------------------------+------+--------------------------------------------------------+-----------+ +| Key | Type | Description | Default | ++============================+======+========================================================+===========+ +| connectionTimeout | int | The timeout for HTTP connections in milliseconds. | 1500 | ++----------------------------+------+--------------------------------------------------------+-----------+ +| featuresRefreshRate | int | The feature flags update refresh period in seconds. | 30 | ++----------------------------+------+--------------------------------------------------------+-----------+ +| segmentsRefreshRate | int | The segments update refresh period in seconds. | 30 | ++----------------------------+------+--------------------------------------------------------+-----------+ +| impressionsRefreshRate | int | The impressions report period in seconds. | 300 | ++----------------------------+------+--------------------------------------------------------+-----------+ +| eventsPushRate | int | The events report period in seconds. | 10 | ++----------------------------+------+--------------------------------------------------------+-----------+ +| impressionsMode | str | Impressions tracking mode: OPTIMIZED, DEBUG, or NONE. | OPTIMIZED | ++----------------------------+------+--------------------------------------------------------+-----------+ +| streamingEnabled | bool | Enable streaming updates via SSE. | True | ++----------------------------+------+--------------------------------------------------------+-----------+ +| labelsEnabled | bool | Whether to send rule labels with impressions. | True | ++----------------------------+------+--------------------------------------------------------+-----------+ +| IPAddressesEnabled | bool | Send machine name and IP in headers. | True | ++----------------------------+------+--------------------------------------------------------+-----------+ +| flagSetsFilter | list | Only sync feature flags belonging to these flag sets. | None | ++----------------------------+------+--------------------------------------------------------+-----------+ .. _localhost_environment: + The localhost environment ------------------------- -During development it is possible to create a 'localhost client' to avoid hitting the -Split.io API SDK. The configuration is taken from a ``.split`` file in the user's *HOME* -directory. The ``.split`` file has the following format: :: +During development it is possible to create a 'localhost client' to avoid hitting the Split.io API. The configuration is taken from a ``.split`` file in the user's *HOME* directory or from a JSON file. The ``.split`` file has the following format: :: file: (comment | split_line)+ comment : '#' string*\n @@ -120,6 +166,7 @@ Whenever a treatment is requested for the feature ``feature_0``, ``treatment_0`` >>> from splitio import get_factory >>> factory = get_factory('localhost') + >>> factory.block_until_ready(5) >>> client = factory.client() >>> client.get_treatment('some_user', 'feature_0') 'treatment_0' @@ -128,11 +175,11 @@ Whenever a treatment is requested for the feature ``feature_0``, ``treatment_0`` >>> client.get_treatment('yet_another_user', 'feature_1') 'treatment_1' >>> client.get_treatment('some_user', 'non_existent_feature') - 'CONTROL' + 'control' -Notice that an API key is not necessary for the localhost environment, and the ``CONTROL`` is returned for non existent features. +Notice that an SDK key is not necessary for the localhost environment, and ``control`` is returned for non-existent features. -It is possible to specify a different splits file using the ``split_definition_file_name`` argument: :: +JSON files are also supported in localhost mode by setting the ``splitFile`` config option to a ``.json`` file path: :: >>> from splitio import get_factory >>> factory = get_factory('localhost', split_definition_file_name='/path/to/splits/file') diff --git a/splitio/api/client.py b/splitio/api/client.py index c9032e0e..559d1405 100644 --- a/splitio/api/client.py +++ b/splitio/api/client.py @@ -177,7 +177,7 @@ def __init__(self, timeout=None, sdk_url=None, events_url=None, auth_url=None, t :type telemetry_url: str """ HttpClientBase.__init__(self, timeout, sdk_url, events_url, auth_url, telemetry_url) - + def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: disable=too-many-arguments """ Issue a get request. @@ -209,9 +209,9 @@ def get(self, server, path, sdk_key, query=None, extra_headers=None): # pylint: except requests.exceptions.ChunkedEncodingError as exc: _LOGGER.error("IncompleteRead exception detected: %s", exc) - return HttpResponse(400, "", {}) - - except Exception as exc: # pylint: disable=broad-except + return HttpResponse(400, "", {}) + + except Exception as exc: # pylint: disable=broad-except raise HttpClientException(_EXC_MSG.format(source='request')) from exc def post(self, server, path, sdk_key, body, query=None, extra_headers=None): # pylint: disable=too-many-arguments @@ -306,8 +306,8 @@ async def get(self, server, path, apikey, query=None, extra_headers=None): # py except aiohttp.ClientPayloadError as exc: _LOGGER.error("ContentLengthError exception detected: %s", exc) - return HttpResponse(400, "", {}) - + return HttpResponse(400, "", {}) + except aiohttp.ClientError as exc: # pylint: disable=broad-except raise HttpClientException(_EXC_MSG.format(source='aiohttp')) from exc diff --git a/splitio/api/splits.py b/splitio/api/splits.py index 771100fc..a86ec019 100644 --- a/splitio/api/splits.py +++ b/splitio/api/splits.py @@ -104,24 +104,27 @@ def fetch_splits(self, change_number, rbs_change_number, fetch_options): if 200 <= response.status_code < 300: if self._spec_version == _SPEC_1_1: return util.convert_to_new_spec(json.loads(response.body)) - + self.clear_storage = self._last_proxy_check_timestamp != 0 self._last_proxy_check_timestamp = 0 - return json.loads(response.body) + parsed = json.loads(response.body) + if 'rbs' not in parsed: + parsed['rbs'] = {"d": [], "s": -1, "t": -1} + return parsed else: if response.status_code == 414: _LOGGER.error('Error fetching feature flags; the amount of flag sets provided are too big, causing uri length error.') - + if self._client.is_sdk_endpoint_overridden() and response.status_code == 400 and self._spec_version == SPEC_VERSION: _LOGGER.warning('Detected proxy response error, changing spec version from %s to %s and re-fetching.', self._spec_version, _SPEC_1_1) self._spec_version = _SPEC_1_1 - self._last_proxy_check_timestamp = utctime_ms() + self._last_proxy_check_timestamp = utctime_ms() return self.fetch_splits(change_number, None, FetchOptions(fetch_options.cache_control_headers, fetch_options.change_number, None, fetch_options.sets, self._spec_version)) - + raise APIException(response.body, response.status_code) - + except HttpClientException as exc: _LOGGER.error('Error fetching feature flags because an exception was raised by the HTTPClient') _LOGGER.debug('Error: ', exc_info=True) @@ -178,24 +181,27 @@ async def fetch_splits(self, change_number, rbs_change_number, fetch_options): if 200 <= response.status_code < 300: if self._spec_version == _SPEC_1_1: return util.convert_to_new_spec(json.loads(response.body)) - + self.clear_storage = self._last_proxy_check_timestamp != 0 self._last_proxy_check_timestamp = 0 - return json.loads(response.body) + parsed = json.loads(response.body) + if 'rbs' not in parsed: + parsed['rbs'] = {"d": [], "s": -1, "t": -1} + return parsed else: if response.status_code == 414: _LOGGER.error('Error fetching feature flags; the amount of flag sets provided are too big, causing uri length error.') - + if self._client.is_sdk_endpoint_overridden() and response.status_code == 400 and self._spec_version == SPEC_VERSION: _LOGGER.warning('Detected proxy response error, changing spec version from %s to %s and re-fetching.', self._spec_version, _SPEC_1_1) self._spec_version = _SPEC_1_1 - self._last_proxy_check_timestamp = utctime_ms() + self._last_proxy_check_timestamp = utctime_ms() return await self.fetch_splits(change_number, None, FetchOptions(fetch_options.cache_control_headers, fetch_options.change_number, None, fetch_options.sets, self._spec_version)) raise APIException(response.body, response.status_code) - + except HttpClientException as exc: _LOGGER.error('Error fetching feature flags because an exception was raised by the HTTPClient') _LOGGER.debug('Error: ', exc_info=True) diff --git a/splitio/client/config.py b/splitio/client/config.py index 25b1bc31..8310e5db 100644 --- a/splitio/client/config.py +++ b/splitio/client/config.py @@ -170,36 +170,36 @@ def sanitize(sdk_key, config): ' Defaulting to `none` mode.') processed["httpAuthenticateScheme"] = authenticate_scheme - processed = _sanitize_fallback_config(config, processed) - + processed = _sanitize_fallback_config(config, processed) + if config.get("redisErrors") is not None: _LOGGER.warning('Parameter `redisErrors` is deprecated as it is no longer supported in redis lib.' \ ' Will ignore this value.') - + processed["redisErrors"] = None return processed def _sanitize_fallback_config(config, processed): if config.get('fallbackTreatments') is None: return processed - + if not isinstance(config['fallbackTreatments'], FallbackTreatmentsConfiguration): _LOGGER.warning('Config: fallbackTreatments parameter should be of `FallbackTreatmentsConfiguration` class.') processed['fallbackTreatments'] = None - return processed - + return processed + sanitized_global_fallback_treatment = config['fallbackTreatments'].global_fallback_treatment if config['fallbackTreatments'].global_fallback_treatment is not None and not validate_fallback_treatment(config['fallbackTreatments'].global_fallback_treatment): _LOGGER.warning('Config: global fallbacktreatment parameter is discarded.') sanitized_global_fallback_treatment = None - + sanitized_flag_fallback_treatments = {} if config['fallbackTreatments'].by_flag_fallback_treatment is not None: for feature_name in config['fallbackTreatments'].by_flag_fallback_treatment.keys(): if not validate_regex_name(feature_name) or not validate_fallback_treatment(config['fallbackTreatments'].by_flag_fallback_treatment[feature_name]): _LOGGER.warning('Config: fallback treatment parameter for feature flag %s is discarded.', feature_name) continue - + sanitized_flag_fallback_treatments[feature_name] = config['fallbackTreatments'].by_flag_fallback_treatment[feature_name] processed['fallbackTreatments'] = FallbackTreatmentsConfiguration(sanitized_global_fallback_treatment, sanitized_flag_fallback_treatments) diff --git a/splitio/engine/evaluator.py b/splitio/engine/evaluator.py index b47db5c5..908cfadc 100644 --- a/splitio/engine/evaluator.py +++ b/splitio/engine/evaluator.py @@ -64,7 +64,13 @@ def eval_with_context(self, key, bucketing, feature_name, attrs, ctx): else: label, _treatment = self._check_prerequisites(feature, bucketing, key, attrs, ctx, label, _treatment) label, _treatment = self._get_treatment(feature, bucketing, key, attrs, ctx, label, _treatment) - config = feature.get_configurations_for(_treatment) + if _treatment == CONTROL: + fallback_treatment = self._fallback_treatment_calculator.resolve(feature_name, label) + label = fallback_treatment.label + _treatment = fallback_treatment.treatment + config = fallback_treatment.config + else: + config = feature.get_configurations_for(_treatment) return { 'treatment': _treatment,