├── .github ├── CODEOWNERS └── workflows │ ├── publish.yml │ ├── python-ci.yml │ └── stale.yml ├── .gitignore ├── .sonarcloud.properties ├── CHANGELOG.md ├── CONTRIBUTING.md ├── DEPLOY.md ├── LICENSE ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── configcatclient ├── __init__.py ├── config.py ├── configcache.py ├── configcatclient.py ├── configcatoptions.py ├── configentry.py ├── configfetcher.py ├── configservice.py ├── datagovernance.py ├── evaluationcontext.py ├── evaluationdetails.py ├── evaluationlogbuilder.py ├── interfaces.py ├── localdictionarydatasource.py ├── localfiledatasource.py ├── logger.py ├── overridedatasource.py ├── pollingmode.py ├── py.typed ├── refreshresult.py ├── rolloutevaluator.py ├── user.py ├── utils.py └── version.py ├── configcatclienttests ├── __init__.py ├── data │ ├── comparison_attribute_conversion.json │ ├── comparison_attribute_trimming.json │ ├── comparison_value_trimming.json │ ├── evaluation │ │ ├── 1_targeting_rule.json │ │ ├── 1_targeting_rule │ │ │ ├── 1_rule_matching_targeted_attribute.txt │ │ │ ├── 1_rule_no_targeted_attribute.txt │ │ │ ├── 1_rule_no_user.txt │ │ │ └── 1_rule_not_matching_targeted_attribute.txt │ │ ├── 2_targeting_rules.json │ │ ├── 2_targeting_rules │ │ │ ├── 2_rules_matching_targeted_attribute.txt │ │ │ ├── 2_rules_no_targeted_attribute.txt │ │ │ ├── 2_rules_no_user.txt │ │ │ └── 2_rules_not_matching_targeted_attribute.txt │ │ ├── and_rules.json │ │ ├── and_rules │ │ │ ├── and_rules_no_user.txt │ │ │ └── and_rules_user.txt │ │ ├── comparators.json │ │ ├── comparators │ │ │ └── allinone.txt │ │ ├── epoch_date_validation.json │ │ ├── epoch_date_validation │ │ │ └── date_error.txt │ │ ├── list_truncation.json │ │ ├── list_truncation │ │ │ ├── list_truncation.txt │ │ │ └── test_list_truncation.json │ │ ├── number_validation.json │ │ ├── number_validation │ │ │ └── number_error.txt │ │ ├── options_after_targeting_rule.json │ │ ├── options_after_targeting_rule │ │ │ ├── options_after_targeting_rule_matching_targeted_attribute.txt │ │ │ ├── options_after_targeting_rule_no_targeted_attribute.txt │ │ │ ├── options_after_targeting_rule_no_user.txt │ │ │ └── options_after_targeting_rule_not_matching_targeted_attribute.txt │ │ ├── options_based_on_custom_attr.json │ │ ├── options_based_on_custom_attr │ │ │ ├── matching_options_custom_attribute.txt │ │ │ ├── no_options_custom_attribute.txt │ │ │ └── options_custom_attribute_no_user.txt │ │ ├── options_based_on_user_id.json │ │ ├── options_based_on_user_id │ │ │ ├── options_user_attribute_no_user.txt │ │ │ └── options_user_attribute_user.txt │ │ ├── options_within_targeting_rule.json │ │ ├── options_within_targeting_rule │ │ │ ├── options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt │ │ │ ├── options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt │ │ │ ├── options_within_targeting_rule_no_targeted_attribute.txt │ │ │ ├── options_within_targeting_rule_no_user.txt │ │ │ └── options_within_targeting_rule_not_matching_targeted_attribute.txt │ │ ├── prerequisite_flag.json │ │ ├── prerequisite_flag │ │ │ ├── prerequisite_flag.txt │ │ │ ├── prerequisite_flag_multilevel.txt │ │ │ ├── prerequisite_flag_no_user_needed_by_both.txt │ │ │ ├── prerequisite_flag_no_user_needed_by_dep.txt │ │ │ └── prerequisite_flag_no_user_needed_by_prereq.txt │ │ ├── segment.json │ │ ├── segment │ │ │ ├── segment_matching.txt │ │ │ ├── segment_no_matching.txt │ │ │ ├── segment_no_targeted_attribute.txt │ │ │ ├── segment_no_user.txt │ │ │ └── segment_no_user_multi_conditions.txt │ │ ├── semver_validation.json │ │ ├── semver_validation │ │ │ ├── semver_error.txt │ │ │ └── semver_relations_error.txt │ │ ├── simple_value.json │ │ └── simple_value │ │ │ ├── double_setting.txt │ │ │ ├── int_setting.txt │ │ │ ├── off_flag.txt │ │ │ ├── on_flag.txt │ │ │ └── text_setting.txt │ ├── test-simple.json │ ├── test.json │ ├── test_circulardependency_v6.json │ ├── test_override_flagdependency_v6.json │ ├── test_override_segments_v6.json │ ├── testmatrix.csv │ ├── testmatrix_and_or.csv │ ├── testmatrix_comparators_v6.csv │ ├── testmatrix_number.csv │ ├── testmatrix_prerequisite_flag.csv │ ├── testmatrix_segments.csv │ ├── testmatrix_segments_old.csv │ ├── testmatrix_semantic.csv │ ├── testmatrix_semantic_2.csv │ ├── testmatrix_sensitive.csv │ ├── testmatrix_unicode.csv │ └── testmatrix_variationId.csv ├── mocks.py ├── test_autopollingcachepolicy.py ├── test_concurrency.py ├── test_config.py ├── test_configcache.py ├── test_configcatclient.py ├── test_configfetcher.py ├── test_datagovernance.py ├── test_evaluationlog.py ├── test_hooks.py ├── test_integration_configcatclient.py ├── test_lazyloadingcachepolicy.py ├── test_manualpollingcachepolicy.py ├── test_override.py ├── test_rollout.py ├── test_specialcharacter.py ├── test_user.py ├── test_utils.py └── test_variation_id.py ├── media └── readme02-3.png ├── requirements.txt ├── samples ├── consolesample │ ├── README.md │ ├── __init__.py │ ├── consolesample.py │ └── consolesample2.py └── webappsample │ ├── README.md │ ├── manage.py │ ├── requirements.txt │ ├── templates │ └── index.html │ ├── webapp │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── migrations │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ ├── urls.py │ └── views.py │ └── webappsample │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── setup.cfg ├── setup.py └── tox.ini /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @configcat/developers 2 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Python SDK Publish 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] 7 | 8 | workflow_dispatch: 9 | 10 | jobs: 11 | publish: 12 | runs-on: ubuntu-latest 13 | if: startsWith(github.ref, 'refs/tags') 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: '3.x' 21 | 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install setuptools wheel twine 26 | 27 | - name: Build and publish 28 | env: 29 | TWINE_USERNAME: __token__ 30 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 31 | run: | 32 | python setup.py sdist bdist_wheel 33 | twine upload dist/* 34 | -------------------------------------------------------------------------------- /.github/workflows/python-ci.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' 6 | push: 7 | branches: [ master ] 8 | paths-ignore: 9 | - '**.md' 10 | pull_request: 11 | branches: [ master ] 12 | 13 | workflow_dispatch: 14 | 15 | jobs: 16 | test: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | matrix: 20 | python-version: ["3.5", "3.6", "3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] 21 | os: [ windows-latest, macos-latest, ubuntu-latest ] 22 | exclude: # Python < v3.8 does not support Apple Silicon ARM64. 23 | - python-version: "3.5" 24 | os: macos-latest 25 | - python-version: "3.6" 26 | os: macos-latest 27 | - python-version: "3.7" 28 | os: macos-latest 29 | include: # So run those legacy versions on Intel CPUs. 30 | - python-version: "3.5" 31 | os: macos-13 32 | - python-version: "3.6" 33 | os: macos-13 34 | - python-version: "3.7" 35 | os: macos-13 36 | 37 | steps: 38 | - uses: actions/checkout@v4 39 | 40 | - name: Run tests in Docker for legacy Python 41 | if: matrix.os == 'ubuntu-latest' && contains(fromJSON('["3.5","3.6","3.7"]'), matrix.python-version) 42 | run: | 43 | docker run --rm -v ${{ github.workspace }}:/app -w /app python:${{ matrix.python-version }} bash -c " 44 | pip install --upgrade pip && 45 | pip install pytest pytest-cov parameterized mock flake8 && 46 | pip install -r requirements.txt && 47 | flake8 configcatclient --count --show-source --statistics && 48 | pytest configcatclienttests 49 | " 50 | 51 | - name: Set up Python ${{ matrix.python-version }} 52 | if: ${{ !(matrix.os == 'ubuntu-latest' && contains(fromJSON('["3.5","3.6","3.7"]'), matrix.python-version)) }} 53 | uses: actions/setup-python@v5 54 | with: 55 | python-version: ${{ matrix.python-version }} 56 | env: 57 | # Needed on Ubuntu for Python 3.5 build. 58 | PIP_TRUSTED_HOST: "pypi.python.org pypi.org files.pythonhosted.org" 59 | 60 | - name: Install dependencies 61 | if: ${{ !(matrix.os == 'ubuntu-latest' && contains(fromJSON('["3.5","3.6","3.7"]'), matrix.python-version)) }} 62 | run: | 63 | python -m pip install --upgrade pip 64 | pip install pytest pytest-cov parameterized mock flake8 65 | pip install -r requirements.txt 66 | 67 | - name: Lint with flake8 68 | if: ${{ !(matrix.os == 'ubuntu-latest' && contains(fromJSON('["3.5","3.6","3.7"]'), matrix.python-version)) }} 69 | run: | 70 | # Statical analysis 71 | flake8 configcatclient --count --show-source --statistics 72 | 73 | - name: Test 74 | if: ${{ !(matrix.os == 'ubuntu-latest' && contains(fromJSON('["3.5","3.6","3.7"]'), matrix.python-version)) }} 75 | run: pytest configcatclienttests 76 | 77 | 78 | coverage: 79 | needs: [ test ] 80 | runs-on: ubuntu-latest 81 | steps: 82 | - uses: actions/checkout@v4 83 | 84 | - name: Set up Python 85 | uses: actions/setup-python@v5 86 | with: 87 | python-version: "3.11" 88 | 89 | - name: Install dependencies 90 | run: | 91 | python -m pip install --upgrade pip 92 | pip install pytest pytest-cov parameterized mock flake8 93 | pip install -r requirements.txt 94 | 95 | - name: Lint with flake8 96 | run: | 97 | # Statical analysis 98 | flake8 configcatclient --count --show-source --statistics 99 | 100 | - name: Run coverage 101 | run: pytest --cov=configcatclient configcatclienttests 102 | 103 | - name: Upload coverage report 104 | uses: codecov/codecov-action@v3 105 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues 2 | 3 | on: 4 | schedule: 5 | - cron: '0 1 * * *' 6 | 7 | workflow_dispatch: 8 | 9 | jobs: 10 | stale: 11 | uses: configcat/.github/.github/workflows/stale.yml@master 12 | secrets: inherit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | .idea/* 104 | .pytest_cache/* 105 | .DS_Store 106 | samples/webappsample/db.sqlite3 107 | -------------------------------------------------------------------------------- /.sonarcloud.properties: -------------------------------------------------------------------------------- 1 | sonar.exclusions=samples/** -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Please check the [Github Releases](https://github.com/configcat/python-sdk/releases) page for the changelog of the ConfigCat Python SDK. 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the ConfigCat SDK for Python 2 | 3 | ConfigCat SDK is an open source project. Feedback and contribution are welcome. Contributions are made to this repo via Issues and Pull Requests. 4 | 5 | ## Submitting bug reports and feature requests 6 | 7 | The ConfigCat SDK team monitors the [issue tracker](https://github.com/configcat/python-sdk/issues) in the SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The team will respond to all newly filed issues. 8 | 9 | ## Submitting pull requests 10 | 11 | We encourage pull requests and other contributions from the community. 12 | - Before submitting pull requests, ensure that all temporary or unintended code is removed. 13 | - Be accompanied by a complete Pull Request template (loaded automatically when a PR is created). 14 | - Add unit or integration tests for fixed or changed functionality. 15 | 16 | When you submit a pull request or otherwise seek to include your change in the repository, you waive all your intellectual property rights, including your copyright and patent claims for the submission. For more details please read the [contribution agreement](https://github.com/configcat/legal/blob/main/contribution-agreement.md). 17 | 18 | In general, we follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr) 19 | 20 | 1. Fork the repository to your own Github account 21 | 2. Clone the project to your machine 22 | 3. Create a branch locally with a succinct but descriptive name 23 | 4. Commit changes to the branch 24 | 5. Following any formatting and testing guidelines specific to this repo 25 | 6. Push changes to your fork 26 | 7. Open a PR in our repository and follow the PR template so that we can efficiently review the changes. 27 | 28 | ## Build instructions 29 | 30 | It's advisable to create a virtual development environment within the project directory: 31 | 32 | ```bash 33 | python -m venv venv 34 | source venv/bin/activate 35 | ``` 36 | 37 | To install requirements: 38 | 39 | ```bash 40 | pip install -r requirements.txt 41 | pip install pytest mock 42 | ``` 43 | 44 | ## Running tests 45 | 46 | ```bash 47 | pytest configcatclienttests 48 | ``` 49 | 50 | ## Running tests against all supported Python versions and linters 51 | 52 | There is a [tox](https://tox.wiki/) configuration file allowing to test against all supported Python versions 53 | as well as linting all files in isolated environments. 54 | 55 | Just run: 56 | 57 | ```bash 58 | # Test against all supported Python versions and lint 59 | tox 60 | # Test against a given Python version 61 | tox -e py310 62 | # Lint 63 | tox -e lint 64 | ``` 65 | -------------------------------------------------------------------------------- /DEPLOY.md: -------------------------------------------------------------------------------- 1 | # Steps to deploy 2 | ## Preparation 3 | 1. Run tests 4 | ```bash 5 | pytest configcatclienttests 6 | ``` 7 | 2. Increase the version in `setup.py`. 8 | 3. Increase the version in `configcatclient/version.py`. 9 | 4. Commit & Push 10 | ## Publish 11 | Use the **same version** for the git tag as in `configcatclient/version.py` and `setup.py`. 12 | - Via git tag 13 | 1. Create a new version tag. 14 | ```bash 15 | git tag v[MAJOR].[MINOR].[PATCH] 16 | ``` 17 | > Example: `git tag v2.5.5` 18 | 2. Push the tag. 19 | ```bash 20 | git push origin --tags 21 | ``` 22 | - Via Github release 23 | 24 | Create a new [Github release](https://github.com/configcat/python-sdk/releases) with a new version tag and release notes. 25 | 26 | ## Python Package 27 | Make sure the new version is available on [PyPI](https://pypi.org/project/configcat-client/). 28 | 29 | ## Update samples 30 | Update and test sample apps with the new SDK version. 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ConfigCat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ConfigCat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | include LICENSE.txt 3 | include README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ConfigCat SDK for Python 2 | https://configcat.com 3 | ConfigCat SDK for Python provides easy integration for your application to ConfigCat. 4 | 5 | ConfigCat is a feature flag and configuration management service that lets you separate releases from deployments. You can turn your features ON/OFF using ConfigCat Dashboard even after they are deployed. ConfigCat lets you target specific groups of users based on region, email or any other custom user attribute. 6 | 7 | ConfigCat is a hosted feature flag service. Manage feature toggles across frontend, backend, mobile, desktop apps. Alternative to LaunchDarkly. Management app + feature flag SDKs. 8 | 9 | [![Python CI](https://github.com/configcat/python-sdk/actions/workflows/python-ci.yml/badge.svg?branch=master)](https://github.com/configcat/python-sdk/actions/workflows/python-ci.yml) 10 | [![codecov](https://codecov.io/gh/ConfigCat/python-sdk/branch/master/graph/badge.svg)](https://codecov.io/gh/ConfigCat/python-sdk) 11 | [![PyPI](https://img.shields.io/pypi/v/configcat-client.svg)](https://pypi.python.org/pypi/configcat-client) 12 | [![PyPI](https://img.shields.io/pypi/pyversions/configcat-client.svg)](https://pypi.python.org/pypi/configcat-client) 13 | [![Known Vulnerabilities](https://snyk.io/test/github/configcat/python-sdk/badge.svg?targetFile=requirements.txt)](https://snyk.io/test/github/configcat/python-sdk?targetFile=requirements.txt) 14 | [![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=configcat_python-sdk&metric=vulnerabilities)](https://sonarcloud.io/dashboard?id=configcat_python-sdk) 15 | ![License](https://img.shields.io/github/license/configcat/python-sdk.svg) 16 | 17 | [![SonarCloud](https://sonarcloud.io/images/project_badges/sonarcloud-orange.svg)](https://sonarcloud.io/dashboard?id=configcat_python-sdk) 18 | 19 | ## Getting started 20 | 21 | ### 1. Install the package with `pip` 22 | 23 | ```bash 24 | pip install configcat-client 25 | ``` 26 | 27 | ### 2. Import `configcatclient` to your application 28 | 29 | ```python 30 | import configcatclient 31 | ``` 32 | 33 | ### 3. Go to the ConfigCat Dashboard to get your *SDK Key*: 34 | ![SDK-KEY](https://raw.githubusercontent.com/ConfigCat/python-sdk/master/media/readme02-3.png "SDK-KEY") 35 | 36 | ### 4. Create a *ConfigCat* client instance: 37 | 38 | ```python 39 | configcat_client = configcatclient.get('#YOUR-SDK-KEY#') 40 | ``` 41 | 42 | > We strongly recommend you to use the *ConfigCat Client* as a Singleton object in your application. The `configcatclient.get()` static factory method constructs singleton client instances for your SDK keys. 43 | 44 | ### 5. Get your setting value 45 | ```python 46 | isMyAwesomeFeatureEnabled = configcat_client.get_value('isMyAwesomeFeatureEnabled', False) 47 | if isMyAwesomeFeatureEnabled: 48 | do_the_new_thing() 49 | else: 50 | do_the_old_thing() 51 | ``` 52 | 53 | ### 6. Stop *ConfigCat* client on application exit 54 | 55 | ```python 56 | configcat_client.close() 57 | ``` 58 | 59 | ## Getting user specific setting values with Targeting 60 | Using this feature, you will be able to get different setting values for different users in your application by passing a `User Object` to the `get_value()` function. 61 | 62 | Read more about [Targeting here](https://configcat.com/docs/advanced/targeting/). 63 | ```python 64 | from configcatclient.user import User 65 | 66 | user = User('#USER-IDENTIFIER#') 67 | 68 | isMyAwesomeFeatureEnabled = configcat_client.get_value('isMyAwesomeFeatureEnabled', False, user) 69 | if isMyAwesomeFeatureEnabled: 70 | do_the_new_thing() 71 | else: 72 | do_the_old_thing() 73 | ``` 74 | 75 | ## Sample/Demo apps 76 | * [Sample Console App](https://github.com/configcat/python-sdk/tree/master/samples/consolesample) 77 | * [Sample Django Web App](https://github.com/configcat/python-sdk/tree/master/samples/webappsample) 78 | 79 | ## Polling Modes 80 | The ConfigCat SDK supports 3 different polling mechanisms to acquire the setting values from ConfigCat. After latest setting values are downloaded, they are stored in the internal cache then all requests are served from there. Read more about Polling Modes and how to use them at [ConfigCat Docs](https://configcat.com/docs/sdk-reference/python/). 81 | 82 | ## Need help? 83 | https://configcat.com/support 84 | 85 | ## Contributing 86 | Contributions are welcome. For more info please read the [Contribution Guideline](CONTRIBUTING.md). 87 | 88 | ## About ConfigCat 89 | - [Official ConfigCat SDKs for other platforms](https://github.com/configcat) 90 | - [Documentation](https://configcat.com/docs) 91 | - [Blog](https://configcat.com/blog) 92 | -------------------------------------------------------------------------------- /configcatclient/__init__.py: -------------------------------------------------------------------------------- 1 | from .configcatclient import ConfigCatClient 2 | from .interfaces import ConfigCatClientException # noqa: F401 3 | from .datagovernance import DataGovernance # noqa: F401 4 | from .configcatoptions import ConfigCatOptions # noqa: F401 5 | from .pollingmode import PollingMode # noqa: F401 6 | 7 | 8 | def get(sdk_key, options=None): 9 | """ 10 | Creates a new or gets an already existing `ConfigCatClient` for the given `sdk_key`. 11 | 12 | :param sdk_key: ConfigCat SDK Key to access your configuration. 13 | :param options: Configuration `ConfigCatOptions` for `ConfigCatClient`. 14 | :return: the `ConfigCatClient` instance. 15 | """ 16 | return ConfigCatClient.get(sdk_key=sdk_key, options=options) 17 | 18 | 19 | def close_all(): 20 | """ 21 | Closes all ConfigCatClient instances. 22 | """ 23 | ConfigCatClient.close_all() 24 | -------------------------------------------------------------------------------- /configcatclient/configcache.py: -------------------------------------------------------------------------------- 1 | from .interfaces import ConfigCache 2 | 3 | 4 | class NullConfigCache(ConfigCache): 5 | 6 | def __init__(self): 7 | self._value = {} 8 | 9 | def get(self, key): 10 | return None 11 | 12 | def set(self, key, value): 13 | pass # do nothing 14 | 15 | 16 | class InMemoryConfigCache(ConfigCache): 17 | 18 | def __init__(self): 19 | self._value = {} 20 | 21 | def get(self, key): 22 | return self._value.get(key) 23 | 24 | def set(self, key, value): 25 | self._value[key] = value 26 | -------------------------------------------------------------------------------- /configcatclient/configcatoptions.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from .datagovernance import DataGovernance 3 | from .pollingmode import PollingMode 4 | 5 | 6 | class Hooks(object): 7 | """ 8 | Events fired by [ConfigCatClient]. 9 | """ 10 | 11 | def __init__(self, on_client_ready=None, on_config_changed=None, 12 | on_flag_evaluated=None, on_error=None): 13 | self._on_client_ready_callbacks = [on_client_ready] if on_client_ready else [] 14 | self._on_config_changed_callbacks = [on_config_changed] if on_config_changed else [] 15 | self._on_flag_evaluated_callbacks = [on_flag_evaluated] if on_flag_evaluated else [] 16 | self._on_error_callbacks = [on_error] if on_error else [] 17 | 18 | def add_on_client_ready(self, callback): 19 | self._on_client_ready_callbacks.append(callback) 20 | 21 | def add_on_config_changed(self, callback): 22 | self._on_config_changed_callbacks.append(callback) 23 | 24 | def add_on_flag_evaluated(self, callback): 25 | self._on_flag_evaluated_callbacks.append(callback) 26 | 27 | def add_on_error(self, callback): 28 | self._on_error_callbacks.append(callback) 29 | 30 | def invoke_on_client_ready(self): 31 | for callback in self._on_client_ready_callbacks: 32 | try: 33 | callback() 34 | except Exception as e: 35 | error = 'Exception occurred during invoke_on_client_ready callback: ' + str(e) 36 | self.invoke_on_error(error) 37 | logging.error(error) 38 | 39 | def invoke_on_config_changed(self, config): 40 | for callback in self._on_config_changed_callbacks: 41 | try: 42 | callback(config) 43 | except Exception as e: 44 | error = 'Exception occurred during invoke_on_config_changed callback: ' + str(e) 45 | self.invoke_on_error(error) 46 | logging.error(error) 47 | 48 | def invoke_on_flag_evaluated(self, evaluation_details): 49 | for callback in self._on_flag_evaluated_callbacks: 50 | try: 51 | callback(evaluation_details) 52 | except Exception as e: 53 | error = 'Exception occurred during invoke_on_flag_evaluated callback: ' + str(e) 54 | self.invoke_on_error(error) 55 | logging.error(error) 56 | 57 | def invoke_on_error(self, error): 58 | for callback in self._on_error_callbacks: 59 | try: 60 | callback(error) 61 | except Exception as e: 62 | logging.error('Exception occurred during invoke_on_error callback: ' + str(e)) 63 | 64 | def clear(self): 65 | self._on_client_ready_callbacks[:] = [] 66 | self._on_config_changed_callbacks[:] = [] 67 | self._on_flag_evaluated_callbacks[:] = [] 68 | self._on_error_callbacks[:] = [] 69 | 70 | 71 | class ConfigCatOptions(object): 72 | """ 73 | Configuration options for ConfigCatClient. 74 | """ 75 | 76 | def __init__(self, 77 | base_url=None, 78 | polling_mode=PollingMode.auto_poll(), 79 | config_cache=None, 80 | proxies=None, 81 | proxy_auth=None, 82 | connect_timeout_seconds=10, 83 | read_timeout_seconds=30, 84 | flag_overrides=None, 85 | data_governance=DataGovernance.Global, 86 | default_user=None, 87 | hooks=None, 88 | offline=False): 89 | # The base ConfigCat CDN url. 90 | self.base_url = base_url 91 | 92 | # The polling mode. 93 | self.polling_mode = polling_mode 94 | 95 | # The cache implementation used to cache the downloaded config.json. 96 | self.config_cache = config_cache 97 | 98 | # Proxy addresses. e.g. { "https": "your_proxy_ip:your_proxy_port" } 99 | self.proxies = proxies 100 | 101 | # Proxy authentication. e.g. HTTPProxyAuth('username', 'password') 102 | self.proxy_auth = proxy_auth 103 | 104 | # The number of seconds to wait for the server to make the initial connection 105 | self.connect_timeout_seconds = connect_timeout_seconds 106 | 107 | # The number of seconds to wait for the server to respond before giving up. 108 | self.read_timeout_seconds = read_timeout_seconds 109 | 110 | # Feature flag and setting overrides. 111 | self.flag_overrides = flag_overrides 112 | 113 | # Default: `DataGovernance.Global`. Set this parameter to be in sync with the 114 | # Data Governance preference on the [Dashboard](https://app.configcat.com/organization/data-governance). 115 | # (Only Organization Admins have access) 116 | self.data_governance = data_governance 117 | 118 | # The default user, used as fallback when there's no user parameter is passed to the getValue() method. 119 | self.default_user = default_user 120 | 121 | # Hooks for events sent by ConfigCatClient. 122 | self.hooks = hooks 123 | 124 | # Indicates whether the SDK should be initialized in offline mode or not. 125 | self.offline = offline 126 | -------------------------------------------------------------------------------- /configcatclient/configentry.py: -------------------------------------------------------------------------------- 1 | import json 2 | from math import floor 3 | 4 | from . import utils 5 | from .config import fixup_config_salt_and_segments 6 | 7 | 8 | class ConfigEntry(object): 9 | def __init__(self, config=None, etag='', config_json_string='{}', fetch_time=utils.distant_past): 10 | self.config = config if config is not None else {} 11 | self.etag = etag 12 | self.config_json_string = config_json_string 13 | self.fetch_time = fetch_time 14 | 15 | def is_empty(self): 16 | return self == ConfigEntry.empty 17 | 18 | def serialize(self): 19 | return '{:.0f}\n{}\n{}'.format(floor(self.fetch_time * 1000), self.etag, self.config_json_string) 20 | 21 | @classmethod 22 | def create_from_string(cls, string): 23 | if not string: 24 | return ConfigEntry.empty 25 | 26 | fetch_time_index = string.find('\n') 27 | etag_index = string.find('\n', fetch_time_index + 1) 28 | if fetch_time_index < 0 or etag_index < 0: 29 | raise ValueError('Number of values is fewer than expected.') 30 | 31 | try: 32 | fetch_time = float(string[0:fetch_time_index]) 33 | except ValueError: 34 | raise ValueError('Invalid fetch time: {}'.format(string[0:fetch_time_index])) 35 | 36 | etag = string[fetch_time_index + 1:etag_index] 37 | if not etag: 38 | raise ValueError('Empty eTag value') 39 | try: 40 | config_json = string[etag_index + 1:] 41 | config = json.loads(config_json) 42 | fixup_config_salt_and_segments(config) 43 | except ValueError as e: 44 | raise ValueError('Invalid config JSON: {}. {}'.format(config_json, str(e))) 45 | 46 | return ConfigEntry(config=config, etag=etag, config_json_string=config_json, fetch_time=fetch_time / 1000.0) 47 | 48 | 49 | ConfigEntry.empty = ConfigEntry(etag='empty') 50 | -------------------------------------------------------------------------------- /configcatclient/configservice.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from threading import Thread, Event, Lock 3 | 4 | from . import utils 5 | from .config import FEATURE_FLAGS, CONFIG_FILE_NAME, SERIALIZATION_FORMAT_VERSION 6 | from .configentry import ConfigEntry 7 | from .pollingmode import AutoPollingMode, LazyLoadingMode 8 | from .refreshresult import RefreshResult 9 | 10 | 11 | class ConfigService(object): 12 | def __init__(self, sdk_key, polling_mode, hooks, config_fetcher, log, config_cache, is_offline): 13 | self._cached_entry = ConfigEntry.empty 14 | self._cached_entry_string = '' 15 | self._polling_mode = polling_mode 16 | self.log = log 17 | self._config_cache = config_cache 18 | self._hooks = hooks 19 | self._cache_key = ConfigService._get_cache_key(sdk_key) 20 | self._config_fetcher = config_fetcher 21 | self._is_offline = is_offline 22 | self._response_future = None 23 | self._initialized = Event() 24 | self._lock = Lock() 25 | self._ongoing_fetch = False 26 | self._fetch_finished = Event() 27 | self._start_time = utils.get_utc_now() 28 | 29 | if isinstance(self._polling_mode, AutoPollingMode) and not is_offline: 30 | self._start_poll() 31 | else: 32 | self._set_initialized() 33 | 34 | def get_config(self): 35 | threshold = utils.distant_past 36 | prefer_cached = self._initialized.is_set() 37 | if isinstance(self._polling_mode, LazyLoadingMode): 38 | threshold = utils.get_utc_now_seconds_since_epoch() - self._polling_mode.cache_refresh_interval_seconds 39 | prefer_cached = False 40 | elif isinstance(self._polling_mode, AutoPollingMode) and not self._initialized.is_set(): 41 | elapsed_time = (utils.get_utc_now() - self._start_time).total_seconds() 42 | threshold = utils.get_utc_now_seconds_since_epoch() - self._polling_mode.poll_interval_seconds 43 | if elapsed_time < self._polling_mode.max_init_wait_time_seconds: 44 | self._initialized.wait(self._polling_mode.max_init_wait_time_seconds - elapsed_time) 45 | 46 | # Max wait time expired without result, notify subscribers with the cached config. 47 | if not self._initialized.is_set(): 48 | self._set_initialized() 49 | return (self._cached_entry.config, self._cached_entry.fetch_time) \ 50 | if not self._cached_entry.is_empty() \ 51 | else (None, utils.distant_past) 52 | 53 | # If we are initialized, we prefer the cached results 54 | entry, _ = self._fetch_if_older(threshold, prefer_cached=prefer_cached) 55 | return (entry.config, entry.fetch_time) \ 56 | if not entry.is_empty() \ 57 | else (None, utils.distant_past) 58 | 59 | def refresh(self): 60 | """ 61 | :return: RefreshResult object 62 | """ 63 | if self.is_offline(): 64 | offline_warning = 'Client is in offline mode, it cannot initiate HTTP calls.' 65 | self.log.warning(offline_warning, event_id=3200) 66 | return RefreshResult(is_success=False, error=offline_warning) 67 | 68 | _, error = self._fetch_if_older(utils.distant_future) 69 | return RefreshResult(is_success=error is None, error=error) 70 | 71 | def set_online(self): 72 | with self._lock: 73 | if not self._is_offline: 74 | return 75 | 76 | self._is_offline = False 77 | if isinstance(self._polling_mode, AutoPollingMode): 78 | self._start_poll() 79 | 80 | self.log.info('Switched to %s mode.', 'ONLINE', event_id=5200) 81 | 82 | def set_offline(self): 83 | with self._lock: 84 | if self._is_offline: 85 | return 86 | 87 | self._is_offline = True 88 | if isinstance(self._polling_mode, AutoPollingMode): 89 | self._stopped.set() 90 | self._thread.join() 91 | 92 | self.log.info('Switched to %s mode.', 'OFFLINE', event_id=5200) 93 | 94 | def is_offline(self): 95 | return self._is_offline # atomic operation in python (lock is not needed) 96 | 97 | def close(self): 98 | if isinstance(self._polling_mode, AutoPollingMode): 99 | self._stopped.set() 100 | 101 | def _fetch_if_older(self, threshold, prefer_cached=False): 102 | """ 103 | :return: Returns the ConfigEntry object and error message in case of any error. 104 | """ 105 | 106 | with self._lock: 107 | # Sync up with the cache and use it when it's not expired. 108 | from_cache = self._read_cache() 109 | if not from_cache.is_empty() and from_cache.etag != self._cached_entry.etag: 110 | self._cached_entry = from_cache 111 | self._hooks.invoke_on_config_changed(from_cache.config.get(FEATURE_FLAGS)) 112 | 113 | # Cache isn't expired 114 | if self._cached_entry.fetch_time > threshold: 115 | self._set_initialized() 116 | return self._cached_entry, None 117 | 118 | # If we are in offline mode or the caller prefers cached values, do not initiate fetch. 119 | if self._is_offline or prefer_cached: 120 | return self._cached_entry, None 121 | 122 | # No fetch is running, initiate a new one. 123 | # Ensure only one fetch request is running at a time. 124 | # If there's an ongoing fetch running, we will wait for the ongoing fetch. 125 | if self._ongoing_fetch: 126 | self._fetch_finished.wait() 127 | else: 128 | self._ongoing_fetch = True 129 | self._fetch_finished.clear() 130 | response = self._config_fetcher.get_configuration(self._cached_entry.etag) 131 | 132 | with self._lock: 133 | if response.is_fetched(): 134 | self._cached_entry = response.entry 135 | self._write_cache(response.entry) 136 | self._hooks.invoke_on_config_changed(response.entry.config.get(FEATURE_FLAGS)) 137 | elif (response.is_not_modified() or not response.is_transient_error) and \ 138 | not self._cached_entry.is_empty(): 139 | self._cached_entry.fetch_time = utils.get_utc_now_seconds_since_epoch() 140 | self._write_cache(self._cached_entry) 141 | 142 | self._set_initialized() 143 | 144 | self._ongoing_fetch = False 145 | self._fetch_finished.set() 146 | 147 | return self._cached_entry, None 148 | 149 | def _start_poll(self): 150 | self._started = Event() 151 | self._thread = Thread(target=self._run, args=[]) 152 | self._thread.daemon = True # daemon thread terminates its execution when the main thread terminates 153 | self._thread.start() 154 | self._started.wait() 155 | 156 | def _run(self): 157 | self._stopped = Event() 158 | self._started.set() 159 | while True: 160 | self._fetch_if_older(utils.get_utc_now_seconds_since_epoch() - self._polling_mode.poll_interval_seconds) 161 | self._stopped.wait(timeout=self._polling_mode.poll_interval_seconds) 162 | if self._stopped.is_set(): 163 | break 164 | 165 | def _set_initialized(self): 166 | if not self._initialized.is_set(): 167 | self._initialized.set() 168 | self._hooks.invoke_on_client_ready() 169 | 170 | @staticmethod 171 | def _get_cache_key(sdk_key): 172 | return hashlib.sha1( 173 | (sdk_key + '_' + CONFIG_FILE_NAME + '.json' + '_' + SERIALIZATION_FORMAT_VERSION).encode('utf-8')).hexdigest() 174 | 175 | def _read_cache(self): 176 | try: 177 | json_string = self._config_cache.get(self._cache_key) 178 | if not json_string or json_string == self._cached_entry_string: 179 | return ConfigEntry.empty 180 | 181 | self._cached_entry_string = json_string 182 | return ConfigEntry.create_from_string(json_string) 183 | except Exception: 184 | self.log.exception('Error occurred while reading the cache.', event_id=2200) 185 | return ConfigEntry.empty 186 | 187 | def _write_cache(self, config_entry): 188 | try: 189 | self._config_cache.set(self._cache_key, config_entry.serialize()) 190 | except Exception: 191 | self.log.exception('Error occurred while writing the cache.', event_id=2201) 192 | -------------------------------------------------------------------------------- /configcatclient/datagovernance.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | 3 | 4 | class DataGovernance(IntEnum): 5 | """ 6 | Control the location of the config.json files containing your feature flags 7 | and settings within the ConfigCat CDN. \n 8 | Global: Select this if your feature flags are published to all global CDN nodes. \n 9 | EuOnly: Select this if your feature flags are published to CDN nodes only in the EU. 10 | """ 11 | Global = 0 12 | EuOnly = 1 13 | -------------------------------------------------------------------------------- /configcatclient/evaluationcontext.py: -------------------------------------------------------------------------------- 1 | class EvaluationContext(object): 2 | def __init__(self, 3 | key, 4 | setting_type, 5 | user, 6 | visited_keys=None, 7 | is_missing_user_object_logged=False, 8 | is_missing_user_object_attribute_logged=False): 9 | self.key = key 10 | self.setting_type = setting_type 11 | self.user = user 12 | self.visited_keys = visited_keys 13 | self.is_missing_user_object_logged = is_missing_user_object_logged 14 | self.is_missing_user_object_attribute_logged = is_missing_user_object_attribute_logged 15 | -------------------------------------------------------------------------------- /configcatclient/evaluationdetails.py: -------------------------------------------------------------------------------- 1 | class EvaluationDetails(object): 2 | def __init__(self, 3 | key, 4 | value, 5 | variation_id=None, 6 | fetch_time=None, 7 | user=None, 8 | is_default_value=False, 9 | error=None, 10 | matched_targeting_rule=None, 11 | matched_percentage_option=None): 12 | # Key of the feature flag or setting. 13 | self.key = key 14 | 15 | # Evaluated value of the feature flag or setting. 16 | self.value = value 17 | 18 | # Variation ID of the feature flag or setting (if available). 19 | self.variation_id = variation_id 20 | 21 | # Time of last successful config download. 22 | self.fetch_time = fetch_time 23 | 24 | # The User Object used for the evaluation (if available). 25 | self.user = user 26 | 27 | # Indicates whether the default value passed to the setting evaluation methods like ConfigCatClient.get_value, 28 | # ConfigCatClient.get_value_details, etc. is used as the result of the evaluation. 29 | self.is_default_value = is_default_value 30 | 31 | # Error message in case evaluation failed. 32 | self.error = error 33 | 34 | # The targeting rule (if any) that matched during the evaluation and was used to return the evaluated value. 35 | self.matched_targeting_rule = matched_targeting_rule 36 | 37 | # The percentage option (if any) that was used to select the evaluated value. 38 | self.matched_percentage_option = matched_percentage_option 39 | 40 | @staticmethod 41 | def from_error(key, value, error, variation_id=None): 42 | return EvaluationDetails(key=key, value=value, variation_id=variation_id, is_default_value=True, error=error) 43 | -------------------------------------------------------------------------------- /configcatclient/evaluationlogbuilder.py: -------------------------------------------------------------------------------- 1 | from .config import Comparator 2 | from .utils import get_date_time 3 | 4 | 5 | class EvaluationLogBuilder(object): 6 | def __init__(self): 7 | self.indent_level = 0 8 | self.text = '' 9 | 10 | @staticmethod 11 | def trunc_comparison_value_if_needed(comparator, comparison_value): 12 | if comparator in [Comparator.IS_ONE_OF_HASHED, 13 | Comparator.IS_NOT_ONE_OF_HASHED, 14 | Comparator.EQUALS_HASHED, 15 | Comparator.NOT_EQUALS_HASHED, 16 | Comparator.STARTS_WITH_ANY_OF_HASHED, 17 | Comparator.NOT_STARTS_WITH_ANY_OF_HASHED, 18 | Comparator.ENDS_WITH_ANY_OF_HASHED, 19 | Comparator.NOT_ENDS_WITH_ANY_OF_HASHED, 20 | Comparator.ARRAY_CONTAINS_ANY_OF_HASHED, 21 | Comparator.ARRAY_NOT_CONTAINS_ANY_OF_HASHED]: 22 | if isinstance(comparison_value, list): 23 | length = len(comparison_value) 24 | if length > 1: 25 | return '[<{} hashed values>]'.format(length) 26 | return '[<{} hashed value>]'.format(length) 27 | 28 | return "''" 29 | 30 | if isinstance(comparison_value, list): 31 | length_limit = 10 32 | length = len(comparison_value) 33 | if length > length_limit: 34 | remaining = length - length_limit 35 | if remaining == 1: 36 | more_text = "<1 more value>" 37 | else: 38 | more_text = "<{} more values>".format(remaining) 39 | 40 | return str(comparison_value[:length_limit])[:-1] + ', ... ' + more_text + ']' 41 | 42 | return str(comparison_value) 43 | 44 | if comparator in [Comparator.BEFORE_DATETIME, Comparator.AFTER_DATETIME]: 45 | time = get_date_time(comparison_value) 46 | return "'%s' (%sZ UTC)" % (str(comparison_value), time.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3]) 47 | 48 | return "'%s'" % str(comparison_value) 49 | 50 | def increase_indent(self): 51 | self.indent_level += 1 52 | return self 53 | 54 | def decrease_indent(self): 55 | self.indent_level = max(0, self.indent_level - 1) 56 | return self 57 | 58 | def append(self, text): 59 | self.text += text 60 | return self 61 | 62 | def new_line(self, text=None): 63 | self.text += '\n' + ' ' * self.indent_level 64 | if text: 65 | self.text += text 66 | return self 67 | 68 | def __str__(self): 69 | return self.text 70 | -------------------------------------------------------------------------------- /configcatclient/interfaces.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class ConfigCache(object): 5 | """ 6 | Config cache interface 7 | """ 8 | __metaclass__ = ABCMeta 9 | 10 | @abstractmethod 11 | def get(self, key): 12 | """ 13 | :return: the config json object from the cache 14 | """ 15 | 16 | @abstractmethod 17 | def set(self, key, value): 18 | """ 19 | Sets the config json cache. 20 | """ 21 | 22 | 23 | class ConfigCatClientException(Exception): 24 | """ 25 | Generic ConfigCatClientException 26 | """ 27 | -------------------------------------------------------------------------------- /configcatclient/localdictionarydatasource.py: -------------------------------------------------------------------------------- 1 | from .config import VALUE, FEATURE_FLAGS, BOOL_VALUE, STRING_VALUE, INT_VALUE, DOUBLE_VALUE, SettingType, SETTING_TYPE, \ 2 | UNSUPPORTED_VALUE 3 | from .overridedatasource import OverrideDataSource, FlagOverrides 4 | 5 | 6 | class LocalDictionaryFlagOverrides(FlagOverrides): 7 | def __init__(self, source, override_behaviour): 8 | self.source = source 9 | self.override_behaviour = override_behaviour 10 | 11 | def create_data_source(self, log): 12 | return LocalDictionaryDataSource(self.source, self.override_behaviour, log) 13 | 14 | 15 | class LocalDictionaryDataSource(OverrideDataSource): 16 | def __init__(self, source, override_behaviour, log): 17 | OverrideDataSource.__init__(self, override_behaviour=override_behaviour) 18 | self.log = log 19 | self._config = {} 20 | for key, value in source.items(): 21 | if isinstance(value, bool): 22 | value_type = BOOL_VALUE 23 | elif isinstance(value, str): 24 | value_type = STRING_VALUE 25 | elif isinstance(value, int): 26 | value_type = INT_VALUE 27 | elif isinstance(value, float): 28 | value_type = DOUBLE_VALUE 29 | else: 30 | value_type = UNSUPPORTED_VALUE 31 | 32 | if FEATURE_FLAGS not in self._config: 33 | self._config[FEATURE_FLAGS] = {} 34 | 35 | self._config[FEATURE_FLAGS][key] = {VALUE: {value_type: value}} 36 | setting_type = SettingType.from_type(type(value)) 37 | if setting_type is not None: 38 | self._config[FEATURE_FLAGS][key][SETTING_TYPE] = int(setting_type) 39 | 40 | def get_overrides(self): 41 | return self._config 42 | -------------------------------------------------------------------------------- /configcatclient/localfiledatasource.py: -------------------------------------------------------------------------------- 1 | from .config import fixup_config_salt_and_segments, VALUE, FEATURE_FLAGS, BOOL_VALUE, STRING_VALUE, \ 2 | INT_VALUE, DOUBLE_VALUE, SettingType, SETTING_TYPE, UNSUPPORTED_VALUE 3 | from .overridedatasource import OverrideDataSource, FlagOverrides 4 | import json 5 | import os 6 | 7 | 8 | class LocalFileFlagOverrides(FlagOverrides): 9 | def __init__(self, file_path, override_behaviour): 10 | self.file_path = file_path 11 | self.override_behaviour = override_behaviour 12 | 13 | def create_data_source(self, log): 14 | return LocalFileDataSource(self.file_path, self.override_behaviour, log) 15 | 16 | 17 | def open_file(file_path, mode='r'): 18 | return open(file_path, mode, encoding='utf-8') 19 | 20 | 21 | class LocalFileDataSource(OverrideDataSource): 22 | def __init__(self, file_path, override_behaviour, log): 23 | OverrideDataSource.__init__(self, override_behaviour=override_behaviour) 24 | self.log = log 25 | if not os.path.exists(file_path): 26 | self.log.error('Cannot find the local config file \'%s\'. ' 27 | 'This is a path that your application provided to the ConfigCat SDK ' 28 | 'by passing it to the constructor of the `LocalFileFlagOverrides` class. ' 29 | 'Read more: https://configcat.com/docs/sdk-reference/python/#json-file', 30 | file_path, event_id=1300) 31 | 32 | self._file_path = file_path 33 | self._config = None 34 | self._cached_file_stamp = 0 35 | 36 | def get_overrides(self): 37 | self._reload_file_content() 38 | return self._config 39 | 40 | def _reload_file_content(self): # noqa: C901 41 | try: 42 | stamp = os.stat(self._file_path).st_mtime 43 | if stamp != self._cached_file_stamp: 44 | self._cached_file_stamp = stamp 45 | with open_file(self._file_path) as file: 46 | data = json.load(file) 47 | 48 | if 'flags' in data: 49 | self._config = {FEATURE_FLAGS: {}} 50 | source = data['flags'] 51 | for key, value in source.items(): 52 | if isinstance(value, bool): 53 | value_type = BOOL_VALUE 54 | elif isinstance(value, str): 55 | value_type = STRING_VALUE 56 | elif isinstance(value, int): 57 | value_type = INT_VALUE 58 | elif isinstance(value, float): 59 | value_type = DOUBLE_VALUE 60 | else: 61 | value_type = UNSUPPORTED_VALUE 62 | 63 | self._config[FEATURE_FLAGS][key] = {VALUE: {value_type: value}} 64 | setting_type = SettingType.from_type(type(value)) 65 | if setting_type is not None: 66 | self._config[FEATURE_FLAGS][key][SETTING_TYPE] = int(setting_type) 67 | else: 68 | fixup_config_salt_and_segments(data) 69 | self._config = data 70 | except OSError: 71 | self.log.exception('Failed to read the local config file \'%s\'.', self._file_path, event_id=1302) 72 | except ValueError: 73 | self.log.exception('Failed to decode JSON from the local config file \'%s\'.', self._file_path, event_id=2302) 74 | -------------------------------------------------------------------------------- /configcatclient/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | 5 | class Logger(logging.LoggerAdapter): 6 | def __init__(self, name, hooks): 7 | super(Logger, self).__init__(logging.getLogger(name), {}) 8 | self._hooks = hooks 9 | 10 | def process(self, msg, kwargs): 11 | # Remove event_id from kwargs (as it's not a built-in argument expected by the logging framework) 12 | # and put it in the extra dict so users can access it without parsing. 13 | event_id = kwargs.pop('event_id', 0) 14 | extra = kwargs.setdefault('extra', {}) 15 | extra['event_id'] = event_id 16 | 17 | # Include the event_id in the message. 18 | return "[" + str(event_id) + "] " + msg, kwargs 19 | 20 | def error(self, msg, *args, **kwargs): 21 | self._hooks.invoke_on_error(Logger.format(msg, args)) 22 | super(Logger, self).error(msg, *args, **kwargs) 23 | 24 | def exception(self, msg, *args, **kwargs): 25 | self._hooks.invoke_on_error(Logger.format(msg, args, sys.exc_info()[1])) 26 | super(Logger, self).exception(msg, *args, **kwargs) 27 | 28 | @staticmethod 29 | def format(msg, args, exc=None): 30 | msg = msg % args if len(args) > 0 else msg 31 | return msg if exc is None else msg + '\n' + str(exc) 32 | -------------------------------------------------------------------------------- /configcatclient/overridedatasource.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | from enum import IntEnum 3 | 4 | 5 | class OverrideBehaviour(IntEnum): 6 | # When evaluating values, the SDK will not use feature flags & settings from the ConfigCat CDN, but it will use 7 | # all feature flags & settings that are loaded from local-override sources. 8 | LocalOnly = 0, 9 | 10 | # When evaluating values, the SDK will use all feature flags & settings that are downloaded from the ConfigCat CDN, 11 | # plus all feature flags & settings that are loaded from local-override sources. If a feature flag or a setting is 12 | # defined both in the fetched and the local-override source then the local-override version will take precedence. 13 | LocalOverRemote = 1, 14 | 15 | # When evaluating values, the SDK will use all feature flags & settings that are downloaded from the ConfigCat CDN, 16 | # plus all feature flags & settings that are loaded from local-override sources. If a feature flag or a setting is 17 | # defined both in the fetched and the local-override source then the fetched version will take precedence. 18 | RemoteOverLocal = 2 19 | 20 | 21 | class FlagOverrides(object): 22 | __metaclass__ = ABCMeta 23 | 24 | @abstractmethod 25 | def create_data_source(self, log): 26 | """ 27 | :return: the created OverrideDataSource 28 | """ 29 | 30 | 31 | class OverrideDataSource(object): 32 | __metaclass__ = ABCMeta 33 | 34 | def __init__(self, override_behaviour): 35 | self._override_behaviour = override_behaviour 36 | 37 | def get_behaviour(self): 38 | return self._override_behaviour 39 | 40 | @abstractmethod 41 | def get_overrides(self): 42 | """ 43 | :return: the override dictionary 44 | """ 45 | -------------------------------------------------------------------------------- /configcatclient/pollingmode.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class PollingMode(object): 5 | """ 6 | Describes a polling mode. 7 | """ 8 | __metaclass__ = ABCMeta 9 | 10 | @abstractmethod 11 | def identifier(self): 12 | """ 13 | :return: the identifier of polling mode. Used for analytical purposes in HTTP User-Agent headers. 14 | """ 15 | 16 | @staticmethod 17 | def auto_poll(poll_interval_seconds=60, max_init_wait_time_seconds=5): 18 | """ 19 | Creates a configured auto polling configuration. 20 | 21 | :param poll_interval_seconds: 22 | sets at least how often this policy should fetch the latest configuration and refresh the cache. 23 | :param max_init_wait_time_seconds: 24 | sets the maximum waiting time between initialization and the first config acquisition in seconds. 25 | """ 26 | 27 | if poll_interval_seconds < 1: 28 | poll_interval_seconds = 1 29 | 30 | if max_init_wait_time_seconds < 0: 31 | max_init_wait_time_seconds = 0 32 | 33 | return AutoPollingMode(poll_interval_seconds=poll_interval_seconds, 34 | max_init_wait_time_seconds=max_init_wait_time_seconds) 35 | 36 | @staticmethod 37 | def lazy_load(cache_refresh_interval_seconds=60): 38 | """ 39 | Creates a configured lazy loading polling configuration. 40 | 41 | :param cache_refresh_interval_seconds: 42 | sets how long the cache will store its value before fetching the latest from the network again. 43 | """ 44 | 45 | if cache_refresh_interval_seconds < 1: 46 | cache_refresh_interval_seconds = 1 47 | 48 | return LazyLoadingMode(cache_refresh_interval_seconds=cache_refresh_interval_seconds) 49 | 50 | @staticmethod 51 | def manual_poll(): 52 | """ 53 | Creates a configured manual polling configuration. 54 | """ 55 | return ManualPollingMode() 56 | 57 | 58 | class AutoPollingMode(PollingMode): 59 | def __init__(self, poll_interval_seconds, max_init_wait_time_seconds): 60 | self.poll_interval_seconds = poll_interval_seconds 61 | self.max_init_wait_time_seconds = max_init_wait_time_seconds 62 | 63 | def identifier(self): 64 | return "a" 65 | 66 | 67 | class LazyLoadingMode(PollingMode): 68 | def __init__(self, cache_refresh_interval_seconds): 69 | self.cache_refresh_interval_seconds = cache_refresh_interval_seconds 70 | 71 | def identifier(self): 72 | return "l" 73 | 74 | 75 | class ManualPollingMode(PollingMode): 76 | def identifier(self): 77 | return "m" 78 | -------------------------------------------------------------------------------- /configcatclient/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/python-sdk/95e1f9f3a5bee4868299b25d8e974de7421ac8f5/configcatclient/py.typed -------------------------------------------------------------------------------- /configcatclient/refreshresult.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | RefreshResult = namedtuple('RefreshResult', 'is_success error') 4 | -------------------------------------------------------------------------------- /configcatclient/user.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | __PREDEFINED__ = ['Identifier', 'Email', 'Country'] 4 | 5 | from collections import OrderedDict 6 | from datetime import datetime 7 | 8 | 9 | class User(object): 10 | """ 11 | User Object. Contains user attributes which are used for evaluating targeting rules and percentage options. 12 | """ 13 | 14 | def __init__(self, identifier, email=None, country=None, custom=None): 15 | """ 16 | Initialize a User object. 17 | 18 | Args: 19 | identifier: The unique identifier of the user or session (e.g. email address, primary key, session ID, etc.) 20 | email: Email address of the user. 21 | country: Country of the user. 22 | custom: Custom attributes of the user for advanced targeting rule definitions (e.g. role, subscription type, etc.) 23 | 24 | All comparators support string values as User Object attribute (in some cases they need to be provided in a 25 | specific format though, see below), but some of them also support other types of values. It depends on the 26 | comparator how the values will be handled. The following rules apply: 27 | 28 | Text-based comparators (EQUALS, IS_ONE_OF, etc.) 29 | * accept string values, 30 | * all other values are automatically converted to string 31 | (a warning will be logged but evaluation will continue as normal). 32 | 33 | SemVer-based comparators (IS_ONE_OF_SEMVER, LESS_THAN_SEMVER, GREATER_THAN_SEMVER, etc.) 34 | * accept string values containing a properly formatted, valid semver value, 35 | * all other values are considered invalid 36 | (a warning will be logged and the currently evaluated targeting rule will be skipped). 37 | 38 | Number-based comparators (EQUALS_NUMBER, LESS_THAN_NUMBER, GREATER_THAN_OR_EQUAL_NUMBER, etc.) 39 | * accept float values and all other numeric values which can safely be converted to float, 40 | * accept string values containing a properly formatted, valid float value, 41 | * all other values are considered invalid 42 | (a warning will be logged and the currently evaluated targeting rule will be skipped). 43 | 44 | Date time-based comparators (BEFORE_DATETIME / AFTER_DATETIME) 45 | * accept datetime values, which are automatically converted to a second-based Unix timestamp 46 | (datetime values with naive timezone are considered to be in UTC), 47 | * accept float values representing a second-based Unix timestamp 48 | and all other numeric values which can safely be converted to float, 49 | * accept string values containing a properly formatted, valid float value, 50 | * all other values are considered invalid 51 | (a warning will be logged and the currently evaluated targeting rule will be skipped). 52 | 53 | String array-based comparators (ARRAY_CONTAINS_ANY_OF / ARRAY_NOT_CONTAINS_ANY_OF) 54 | * accept arrays of strings, 55 | * accept string values containing a valid JSON string which can be deserialized to an array of strings, 56 | * all other values are considered invalid 57 | (a warning will be logged and the currently evaluated targeting rule will be skipped). 58 | """ 59 | self.__identifier = identifier if identifier is not None else '' 60 | self.__data = {'Identifier': identifier, 'Email': email, 'Country': country} 61 | self.__custom = custom 62 | 63 | def get_identifier(self): 64 | return self.__identifier 65 | 66 | def get_attribute(self, attribute): 67 | attribute = str(attribute) 68 | if attribute in __PREDEFINED__: 69 | return self.__data[attribute] 70 | 71 | return self.__custom.get(attribute) if self.__custom else None 72 | 73 | def __str__(self): 74 | def serializer(obj): 75 | if isinstance(obj, datetime): 76 | return obj.isoformat() 77 | 78 | raise TypeError("Type not serializable") 79 | 80 | dump = OrderedDict([ 81 | ('Identifier', self.__identifier), 82 | ('Email', self.__data.get('Email')), 83 | ('Country', self.__data.get('Country')) 84 | ]) 85 | if self.__custom: 86 | dump.update(self.__custom) 87 | 88 | filtered_dump = OrderedDict([(k, v) for k, v in dump.items() if v is not None]) 89 | return json.dumps(filtered_dump, ensure_ascii=False, separators=(',', ':'), default=serializer) 90 | -------------------------------------------------------------------------------- /configcatclient/utils.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import inspect 3 | from qualname import qualname 4 | from datetime import datetime 5 | from datetime import timezone 6 | 7 | epoch_time = datetime(1970, 1, 1, tzinfo=timezone.utc) 8 | distant_future = sys.float_info.max 9 | distant_past = 0 10 | 11 | 12 | def get_class_from_method(method): 13 | method_class = sys.modules.get(method.__module__) 14 | if method_class is None: 15 | return None 16 | for name in qualname(method).split('.')[:-1]: 17 | method_class = getattr(method_class, name) 18 | if not inspect.isclass(method_class): 19 | return None 20 | return method_class 21 | 22 | 23 | def get_class_from_stack_frame(frame): 24 | args, _, _, value_dict = inspect.getargvalues(frame) 25 | # we check the first parameter for the frame function is 26 | # named 'self' or 'cls' 27 | if len(args): 28 | if args[0] == 'self': 29 | # in that case, 'self' will be referenced in value_dict 30 | instance = value_dict.get(args[0], None) 31 | if instance: 32 | # return its class 33 | return getattr(instance, '__class__', None) 34 | if args[0] == 'cls': 35 | # return the class 36 | return value_dict.get(args[0], None) 37 | 38 | # return None otherwise 39 | return None 40 | 41 | 42 | def method_is_called_from(method, level=1): 43 | """ 44 | Checks if the current method is being called from a certain method. 45 | """ 46 | stack_info = inspect.stack()[level + 1] 47 | frame = stack_info[0] 48 | calling_method_name = frame.f_code.co_name 49 | expected_method_name = method.__name__ 50 | if calling_method_name != expected_method_name: 51 | return False 52 | 53 | calling_class = get_class_from_stack_frame(frame) 54 | expected_class = get_class_from_method(method) 55 | if calling_class == expected_class: 56 | return True 57 | return False 58 | 59 | 60 | def get_utc_now(): 61 | return datetime.now(timezone.utc) 62 | 63 | 64 | def get_seconds_since_epoch(date_time): 65 | # if there is no timezone info, assume UTC 66 | if date_time.tzinfo is None: 67 | date_time = date_time.replace(tzinfo=timezone.utc) 68 | 69 | return (date_time - epoch_time).total_seconds() 70 | 71 | 72 | def get_date_time(seconds_since_epoch): 73 | return datetime.fromtimestamp(seconds_since_epoch, timezone.utc) 74 | 75 | 76 | def get_utc_now_seconds_since_epoch(): 77 | return get_seconds_since_epoch(get_utc_now()) 78 | 79 | 80 | def is_string_list(value): 81 | # Check if the value is a list 82 | if not isinstance(value, list): 83 | return False 84 | 85 | # Check if all items in the list are strings 86 | for item in value: 87 | if not isinstance(item, str): 88 | return False 89 | 90 | return True 91 | 92 | 93 | def encode_utf8(value): 94 | return value.encode('utf-8') 95 | -------------------------------------------------------------------------------- /configcatclient/version.py: -------------------------------------------------------------------------------- 1 | CONFIGCATCLIENT_VERSION = "10.0.0" 2 | -------------------------------------------------------------------------------- /configcatclienttests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/python-sdk/95e1f9f3a5bee4868299b25d8e974de7421ac8f5/configcatclienttests/__init__.py -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/1_targeting_rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 4 | "tests": [ 5 | { 6 | "key": "stringContainsDogDefaultCat", 7 | "defaultValue": "default", 8 | "returnValue": "Cat", 9 | "expectedLog": "1_rule_no_user.txt" 10 | }, 11 | { 12 | "key": "stringContainsDogDefaultCat", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": "Cat", 18 | "expectedLog": "1_rule_no_targeted_attribute.txt" 19 | }, 20 | { 21 | "key": "stringContainsDogDefaultCat", 22 | "defaultValue": "default", 23 | "user": { 24 | "Identifier": "12345", 25 | "Email": "joe@example.com" 26 | }, 27 | "returnValue": "Cat", 28 | "expectedLog": "1_rule_not_matching_targeted_attribute.txt" 29 | }, 30 | { 31 | "key": "stringContainsDogDefaultCat", 32 | "defaultValue": "default", 33 | "user": { 34 | "Identifier": "12345", 35 | "Email": "joe@configcat.com" 36 | }, 37 | "returnValue": "Dog", 38 | "expectedLog": "1_rule_matching_targeted_attribute.txt" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/1_targeting_rule/1_rule_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => MATCH, applying rule 4 | Returning 'Dog'. 5 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/1_targeting_rule/1_rule_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'stringContainsDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'Cat'. 7 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/1_targeting_rule/1_rule_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringContainsDogDefaultCat' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'Cat'. 7 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/1_targeting_rule/1_rule_not_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringContainsDogDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN 'Dog' => no match 4 | Returning 'Cat'. 5 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/2_targeting_rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 4 | "tests": [ 5 | { 6 | "key": "stringIsInDogDefaultCat", 7 | "defaultValue": "default", 8 | "returnValue": "Cat", 9 | "expectedLog": "2_rules_no_user.txt" 10 | }, 11 | { 12 | "key": "stringIsInDogDefaultCat", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": "Cat", 18 | "expectedLog": "2_rules_no_targeted_attribute.txt" 19 | }, 20 | { 21 | "key": "stringIsInDogDefaultCat", 22 | "defaultValue": "default", 23 | "user": { 24 | "Identifier": "12345", 25 | "Custom1": "user" 26 | }, 27 | "returnValue": "Cat", 28 | "expectedLog": "2_rules_not_matching_targeted_attribute.txt" 29 | }, 30 | { 31 | "key": "stringIsInDogDefaultCat", 32 | "defaultValue": "default", 33 | "user": { 34 | "Identifier": "12345", 35 | "Custom1": "admin" 36 | }, 37 | "returnValue": "Dog", 38 | "expectedLog": "2_rules_matching_targeted_attribute.txt" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/2_targeting_rules/2_rules_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"admin"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => MATCH, applying rule 7 | Returning 'Dog'. 8 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/2_targeting_rules/2_rules_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | WARNING [3003] Cannot evaluate condition (User.Custom1 IS ONE OF ['admin']) for setting 'stringIsInDogDefaultCat' (the User.Custom1 attribute is missing). You should set the User.Custom1 attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 3 | INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345"}' 4 | Evaluating targeting rules and applying the first match if any: 5 | - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, the User.Custom1 attribute is missing 8 | The current targeting rule is ignored and the evaluation continues with the next rule. 9 | Returning 'Cat'. 10 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/2_targeting_rules/2_rules_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringIsInDogDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringIsInDogDefaultCat' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => cannot evaluate, User Object is missing 7 | The current targeting rule is ignored and the evaluation continues with the next rule. 8 | Returning 'Cat'. 9 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/2_targeting_rules/2_rules_not_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com']) for setting 'stringIsInDogDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringIsInDogDefaultCat' for User '{"Identifier":"12345","Custom1":"user"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email IS ONE OF ['a@configcat.com', 'b@configcat.com'] THEN 'Dog' => cannot evaluate, the User.Email attribute is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | - IF User.Custom1 IS ONE OF ['admin'] THEN 'Dog' => no match 7 | Returning 'Cat'. 8 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/and_rules.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", 4 | "tests": [ 5 | { 6 | "key": "emailAnd", 7 | "defaultValue": "default", 8 | "returnValue": "Cat", 9 | "expectedLog": "and_rules_no_user.txt" 10 | }, 11 | { 12 | "key": "emailAnd", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345", 16 | "Email": "jane@configcat.com" 17 | }, 18 | "returnValue": "Cat", 19 | "expectedLog": "and_rules_user.txt" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/and_rules/and_rules_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'emailAnd' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'emailAnd' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 5 | THEN 'Dog' => cannot evaluate, User Object is missing 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | Returning 'Cat'. 8 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/and_rules/and_rules_user.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'emailAnd' for User '{"Identifier":"12345","Email":"jane@configcat.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true 4 | AND User.Email CONTAINS ANY OF ['@'] => true 5 | AND User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 6 | THEN 'Dog' => no match 7 | Returning 'Cat'. 8 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/comparators.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", 4 | "tests": [ 5 | { 6 | "key": "allinone", 7 | "defaultValue": "", 8 | "user": { 9 | "Identifier": "12345", 10 | "Email": "joe@example.com", 11 | "Country": "[\"USA\"]", 12 | "Version": "1.0.0", 13 | "Number": "1.0", 14 | "Date": "1693497500" 15 | }, 16 | "returnValue": "default", 17 | "expectedLog": "allinone.txt" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/comparators/allinone.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'allinone' for User '{"Identifier":"12345","Email":"joe@example.com","Country":"[\"USA\"]","Version":"1.0.0","Number":"1.0","Date":"1693497500"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email EQUALS '' => true 4 | AND User.Email NOT EQUALS '' => false, skipping the remaining AND conditions 5 | THEN '1h' => no match 6 | - IF User.Email EQUALS 'joe@example.com' => true 7 | AND User.Email NOT EQUALS 'joe@example.com' => false, skipping the remaining AND conditions 8 | THEN '1c' => no match 9 | - IF User.Email IS ONE OF [<1 hashed value>] => true 10 | AND User.Email IS NOT ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions 11 | THEN '2h' => no match 12 | - IF User.Email IS ONE OF ['joe@example.com'] => true 13 | AND User.Email IS NOT ONE OF ['joe@example.com'] => false, skipping the remaining AND conditions 14 | THEN '2c' => no match 15 | - IF User.Email STARTS WITH ANY OF [<1 hashed value>] => true 16 | AND User.Email NOT STARTS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 17 | THEN '3h' => no match 18 | - IF User.Email STARTS WITH ANY OF ['joe@'] => true 19 | AND User.Email NOT STARTS WITH ANY OF ['joe@'] => false, skipping the remaining AND conditions 20 | THEN '3c' => no match 21 | - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => true 22 | AND User.Email NOT ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 23 | THEN '4h' => no match 24 | - IF User.Email ENDS WITH ANY OF ['@example.com'] => true 25 | AND User.Email NOT ENDS WITH ANY OF ['@example.com'] => false, skipping the remaining AND conditions 26 | THEN '4c' => no match 27 | - IF User.Email CONTAINS ANY OF ['e@e'] => true 28 | AND User.Email NOT CONTAINS ANY OF ['e@e'] => false, skipping the remaining AND conditions 29 | THEN '5' => no match 30 | - IF User.Version IS ONE OF ['1.0.0'] => true 31 | AND User.Version IS NOT ONE OF ['1.0.0'] => false, skipping the remaining AND conditions 32 | THEN '6' => no match 33 | - IF User.Version < '1.0.1' => true 34 | AND User.Version >= '1.0.1' => false, skipping the remaining AND conditions 35 | THEN '7' => no match 36 | - IF User.Version > '0.9.9' => true 37 | AND User.Version <= '0.9.9' => false, skipping the remaining AND conditions 38 | THEN '8' => no match 39 | - IF User.Number = '1' => true 40 | AND User.Number != '1' => false, skipping the remaining AND conditions 41 | THEN '9' => no match 42 | - IF User.Number < '1.1' => true 43 | AND User.Number >= '1.1' => false, skipping the remaining AND conditions 44 | THEN '10' => no match 45 | - IF User.Number > '0.9' => true 46 | AND User.Number <= '0.9' => false, skipping the remaining AND conditions 47 | THEN '11' => no match 48 | - IF User.Date BEFORE '1693497600' (2023-08-31T16:00:00.000Z UTC) => true 49 | AND User.Date AFTER '1693497600' (2023-08-31T16:00:00.000Z UTC) => false, skipping the remaining AND conditions 50 | THEN '12' => no match 51 | - IF User.Country ARRAY CONTAINS ANY OF [<1 hashed value>] => true 52 | AND User.Country ARRAY NOT CONTAINS ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 53 | THEN '13h' => no match 54 | - IF User.Country ARRAY CONTAINS ANY OF ['USA'] => true 55 | AND User.Country ARRAY NOT CONTAINS ANY OF ['USA'] => false, skipping the remaining AND conditions 56 | THEN '13c' => no match 57 | Returning 'default'. 58 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/epoch_date_validation.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9a6b-4947-84e2-91529248278a/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/OfQqcTjfFUGBwMKqtyEOrQ", 4 | "tests": [ 5 | { 6 | "key": "boolTrueIn202304", 7 | "defaultValue": true, 8 | "returnValue": false, 9 | "expectedLog": "date_error.txt", 10 | "user": { 11 | "Identifier": "12345", 12 | "Custom1": "2023.04.10" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/epoch_date_validation/date_error.txt: -------------------------------------------------------------------------------- 1 | WARNING [3004] Cannot evaluate condition (User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC)) for setting 'boolTrueIn202304' ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 2 | INFO [5000] Evaluating 'boolTrueIn202304' for User '{"Identifier":"12345","Custom1":"2023.04.10"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Custom1 AFTER '1680307200' (2023-04-01T00:00:00.000Z UTC) => false, skipping the remaining AND conditions 5 | THEN 'True' => cannot evaluate, the User.Custom1 attribute is invalid ('2023.04.10' is not a valid Unix timestamp (number of seconds elapsed since Unix epoch)) 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | Returning 'False'. 8 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/list_truncation.json: -------------------------------------------------------------------------------- 1 | { 2 | "jsonOverride": "test_list_truncation.json", 3 | "tests": [ 4 | { 5 | "key": "booleanKey1", 6 | "defaultValue": false, 7 | "user": { 8 | "Identifier": "12" 9 | }, 10 | "returnValue": true, 11 | "expectedLog": "list_truncation.txt" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/list_truncation/list_truncation.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'booleanKey1' for User '{"Identifier":"12"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'] => true 4 | AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <1 more value>] => true 5 | AND User.Identifier CONTAINS ANY OF ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', ... <2 more values>] => true 6 | THEN 'True' => MATCH, applying rule 7 | Returning 'True'. 8 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/list_truncation/test_list_truncation.json: -------------------------------------------------------------------------------- 1 | { 2 | "p": { 3 | "u": "https://cdn-global.configcat.com", 4 | "r": 0, 5 | "s": "test-salt" 6 | }, 7 | "f": { 8 | "booleanKey1": { 9 | "t": 0, 10 | "v": { 11 | "b": false 12 | }, 13 | "r": [ 14 | { 15 | "c": [ 16 | { 17 | "u": { 18 | "a": "Identifier", 19 | "c": 2, 20 | "l": [ 21 | "1", 22 | "2", 23 | "3", 24 | "4", 25 | "5", 26 | "6", 27 | "7", 28 | "8", 29 | "9", 30 | "10" 31 | ] 32 | } 33 | }, 34 | { 35 | "u": { 36 | "a": "Identifier", 37 | "c": 2, 38 | "l": [ 39 | "1", 40 | "2", 41 | "3", 42 | "4", 43 | "5", 44 | "6", 45 | "7", 46 | "8", 47 | "9", 48 | "10", 49 | "11" 50 | ] 51 | } 52 | }, 53 | { 54 | "u": { 55 | "a": "Identifier", 56 | "c": 2, 57 | "l": [ 58 | "1", 59 | "2", 60 | "3", 61 | "4", 62 | "5", 63 | "6", 64 | "7", 65 | "8", 66 | "9", 67 | "10", 68 | "11", 69 | "12" 70 | ] 71 | } 72 | } 73 | ], 74 | "s": { 75 | "v": { 76 | "b": true 77 | } 78 | } 79 | } 80 | ] 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/number_validation.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d747f0-5986-c2ef-eef3-ec778e32e10a/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/uGyK3q9_ckmdxRyI7vjwCw", 4 | "tests": [ 5 | { 6 | "key": "number", 7 | "defaultValue": "default", 8 | "returnValue": "Default", 9 | "expectedLog": "number_error.txt", 10 | "user": { 11 | "Identifier": "12345", 12 | "Custom1": "not_a_number" 13 | } 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/number_validation/number_error.txt: -------------------------------------------------------------------------------- 1 | WARNING [3004] Cannot evaluate condition (User.Custom1 != '5') for setting 'number' ('not_a_number' is not a valid decimal number). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 2 | INFO [5000] Evaluating 'number' for User '{"Identifier":"12345","Custom1":"not_a_number"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Custom1 != '5' THEN '<>5' => cannot evaluate, the User.Custom1 attribute is invalid ('not_a_number' is not a valid decimal number) 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'Default'. 7 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_after_targeting_rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 4 | "tests": [ 5 | { 6 | "key": "integer25One25Two25Three25FourAdvancedRules", 7 | "defaultValue": 42, 8 | "returnValue": -1, 9 | "expectedLog": "options_after_targeting_rule_no_user.txt" 10 | }, 11 | { 12 | "key": "integer25One25Two25Three25FourAdvancedRules", 13 | "defaultValue": 42, 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": 2, 18 | "expectedLog": "options_after_targeting_rule_no_targeted_attribute.txt" 19 | }, 20 | { 21 | "key": "integer25One25Two25Three25FourAdvancedRules", 22 | "defaultValue": 42, 23 | "user": { 24 | "Identifier": "12345", 25 | "Email": "joe@example.com" 26 | }, 27 | "returnValue": 2, 28 | "expectedLog": "options_after_targeting_rule_not_matching_targeted_attribute.txt" 29 | }, 30 | { 31 | "key": "integer25One25Two25Three25FourAdvancedRules", 32 | "defaultValue": 42, 33 | "user": { 34 | "Identifier": "12345", 35 | "Email": "joe@configcat.com" 36 | }, 37 | "returnValue": 5, 38 | "expectedLog": "options_after_targeting_rule_matching_targeted_attribute.txt" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => MATCH, applying rule 4 | Returning '5'. 5 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'integer25One25Two25Three25FourAdvancedRules' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, the User.Email attribute is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Evaluating % options based on the User.Identifier attribute: 7 | - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) 8 | - Hash value 25 selects % option 2 (25%), '2'. 9 | Returning '2'. 10 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'integer25One25Two25Three25FourAdvancedRules' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Skipping % options because the User Object is missing. 7 | Returning '-1'. 8 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_after_targeting_rule/options_after_targeting_rule_not_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'integer25One25Two25Three25FourAdvancedRules' for User '{"Identifier":"12345","Email":"joe@example.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN '5' => no match 4 | Evaluating % options based on the User.Identifier attribute: 5 | - Computing hash in the [0..99] range from User.Identifier => 25 (this value is sticky and consistent across all SDKs) 6 | - Hash value 25 selects % option 2 (25%), '2'. 7 | Returning '2'. 8 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_based_on_custom_attr.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", 4 | "tests": [ 5 | { 6 | "key": "string75Cat0Dog25Falcon0HorseCustomAttr", 7 | "defaultValue": "default", 8 | "returnValue": "Chicken", 9 | "expectedLog": "options_custom_attribute_no_user.txt" 10 | }, 11 | { 12 | "key": "string75Cat0Dog25Falcon0HorseCustomAttr", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": "Chicken", 18 | "expectedLog": "no_options_custom_attribute.txt" 19 | }, 20 | { 21 | "key": "string75Cat0Dog25Falcon0HorseCustomAttr", 22 | "defaultValue": "default", 23 | "user": { 24 | "Identifier": "12345", 25 | "Country": "US" 26 | }, 27 | "returnValue": "Cat", 28 | "expectedLog": "matching_options_custom_attribute.txt" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_based_on_custom_attr/matching_options_custom_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345","Country":"US"}' 2 | Evaluating % options based on the User.Country attribute: 3 | - Computing hash in the [0..99] range from User.Country => 70 (this value is sticky and consistent across all SDKs) 4 | - Hash value 70 selects % option 1 (75%), 'Cat'. 5 | Returning 'Cat'. 6 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_based_on_custom_attr/no_options_custom_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (the User.Country attribute is missing). You should set the User.Country attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' for User '{"Identifier":"12345"}' 3 | Skipping % options because the User.Country attribute is missing. 4 | Returning 'Chicken'. 5 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_based_on_custom_attr/options_custom_attribute_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'string75Cat0Dog25Falcon0HorseCustomAttr' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'string75Cat0Dog25Falcon0HorseCustomAttr' 3 | Skipping % options because the User Object is missing. 4 | Returning 'Chicken'. 5 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_based_on_user_id.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 4 | "tests": [ 5 | { 6 | "key": "string75Cat0Dog25Falcon0Horse", 7 | "defaultValue": "default", 8 | "returnValue": "Chicken", 9 | "expectedLog": "options_user_attribute_no_user.txt" 10 | }, 11 | { 12 | "key": "string75Cat0Dog25Falcon0Horse", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": "Cat", 18 | "expectedLog": "options_user_attribute_user.txt" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_based_on_user_id/options_user_attribute_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'string75Cat0Dog25Falcon0Horse' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' 3 | Skipping % options because the User Object is missing. 4 | Returning 'Chicken'. 5 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_based_on_user_id/options_user_attribute_user.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'string75Cat0Dog25Falcon0Horse' for User '{"Identifier":"12345"}' 2 | Evaluating % options based on the User.Identifier attribute: 3 | - Computing hash in the [0..99] range from User.Identifier => 21 (this value is sticky and consistent across all SDKs) 4 | - Hash value 21 selects % option 1 (75%), 'Cat'. 5 | Returning 'Cat'. 6 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_within_targeting_rule.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9e4e-4f59-86b2-5da50924b6ca/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/P4e3fAz_1ky2-Zg2e4cbkw", 4 | "tests": [ 5 | { 6 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 7 | "defaultValue": "default", 8 | "returnValue": "Cat", 9 | "expectedLog": "options_within_targeting_rule_no_user.txt" 10 | }, 11 | { 12 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 13 | "defaultValue": "default", 14 | "user": { 15 | "Identifier": "12345" 16 | }, 17 | "returnValue": "Cat", 18 | "expectedLog": "options_within_targeting_rule_no_targeted_attribute.txt" 19 | }, 20 | { 21 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 22 | "defaultValue": "default", 23 | "user": { 24 | "Identifier": "12345", 25 | "Email": "joe@example.com" 26 | }, 27 | "returnValue": "Cat", 28 | "expectedLog": "options_within_targeting_rule_not_matching_targeted_attribute.txt" 29 | }, 30 | { 31 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 32 | "defaultValue": "default", 33 | "user": { 34 | "Identifier": "12345", 35 | "Email": "joe@configcat.com" 36 | }, 37 | "returnValue": "Cat", 38 | "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt" 39 | }, 40 | { 41 | "key": "stringContainsString75Cat0Dog25Falcon0HorseDefaultCat", 42 | "defaultValue": "default", 43 | "user": { 44 | "Identifier": "12345", 45 | "Email": "joe@configcat.com", 46 | "Country": "US" 47 | }, 48 | "returnValue": "Cat", 49 | "expectedLog": "options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt" 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (the User.Country attribute is missing). You should set the User.Country attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule 5 | Skipping % options because the User.Country attribute is missing. 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | Returning 'Cat'. 8 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@configcat.com","Country":"US"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => MATCH, applying rule 4 | Evaluating % options based on the User.Country attribute: 5 | - Computing hash in the [0..99] range from User.Country => 63 (this value is sticky and consistent across all SDKs) 6 | - Hash value 63 selects % option 1 (75%), 'Cat'. 7 | Returning 'Cat'. 8 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate condition (User.Email CONTAINS ANY OF ['@configcat.com']) for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, the User.Email attribute is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'Cat'. 7 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'Cat'. 7 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/options_within_targeting_rule/options_within_targeting_rule_not_matching_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringContainsString75Cat0Dog25Falcon0HorseDefaultCat' for User '{"Identifier":"12345","Email":"joe@example.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User.Email CONTAINS ANY OF ['@configcat.com'] THEN % options => no match 4 | Returning 'Cat'. 5 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/prerequisite_flag.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08dbc325-7f69-4fd4-8af4-cf9f24ec8ac9/08dbc325-9d5e-4988-891c-fd4a45790bd1/08dbc325-9ebd-4587-8171-88f76a3004cb", 3 | "sdkKey": "configcat-sdk-1/JcPbCGl_1E-K9M-fJOyKyQ/ByMO9yZNn02kXcm72lnY1A", 4 | "tests": [ 5 | { 6 | "key": "dependentFeatureWithUserCondition", 7 | "defaultValue": "default", 8 | "returnValue": "Chicken", 9 | "expectedLog": "prerequisite_flag_no_user_needed_by_dep.txt" 10 | }, 11 | { 12 | "key": "dependentFeature", 13 | "defaultValue": "default", 14 | "returnValue": "Chicken", 15 | "expectedLog": "prerequisite_flag_no_user_needed_by_prereq.txt" 16 | }, 17 | { 18 | "key": "dependentFeatureWithUserCondition2", 19 | "defaultValue": "default", 20 | "returnValue": "Frog", 21 | "expectedLog": "prerequisite_flag_no_user_needed_by_both.txt" 22 | }, 23 | { 24 | "key": "dependentFeature", 25 | "defaultValue": "default", 26 | "user": { 27 | "Identifier": "12345", 28 | "Email": "kate@configcat.com", 29 | "Country": "USA" 30 | }, 31 | "returnValue": "Horse", 32 | "expectedLog": "prerequisite_flag.txt" 33 | }, 34 | { 35 | "key": "dependentFeatureMultipleLevels", 36 | "defaultValue": "default", 37 | "returnValue": "Dog", 38 | "expectedLog": "prerequisite_flag_multilevel.txt" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'dependentFeature' for User '{"Identifier":"12345","Email":"kate@configcat.com","Country":"USA"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF Flag 'mainFeature' EQUALS 'target' 4 | ( 5 | Evaluating prerequisite flag 'mainFeature': 6 | Evaluating targeting rules and applying the first match if any: 7 | - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 8 | THEN 'private' => no match 9 | - IF User.Country IS ONE OF [<1 hashed value>] => true 10 | AND User IS NOT IN SEGMENT 'Beta Users' 11 | ( 12 | Evaluating segment 'Beta Users': 13 | - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions 14 | Segment evaluation result: User IS NOT IN SEGMENT. 15 | Condition (User IS NOT IN SEGMENT 'Beta Users') evaluates to true. 16 | ) => true 17 | AND User IS NOT IN SEGMENT 'Developers' 18 | ( 19 | Evaluating segment 'Developers': 20 | - IF User.Email IS ONE OF [<2 hashed values>] => false, skipping the remaining AND conditions 21 | Segment evaluation result: User IS NOT IN SEGMENT. 22 | Condition (User IS NOT IN SEGMENT 'Developers') evaluates to true. 23 | ) => true 24 | THEN 'target' => MATCH, applying rule 25 | Prerequisite flag evaluation result: 'target'. 26 | Condition (Flag 'mainFeature' EQUALS 'target') evaluates to true. 27 | ) 28 | THEN % options => MATCH, applying rule 29 | Evaluating % options based on the User.Identifier attribute: 30 | - Computing hash in the [0..99] range from User.Identifier => 78 (this value is sticky and consistent across all SDKs) 31 | - Hash value 78 selects % option 4 (25%), 'Horse'. 32 | Returning 'Horse'. 33 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag_multilevel.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'dependentFeatureMultipleLevels' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF Flag 'intermediateFeature' EQUALS 'True' 4 | ( 5 | Evaluating prerequisite flag 'intermediateFeature': 6 | Evaluating targeting rules and applying the first match if any: 7 | - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'True' 8 | ( 9 | Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': 10 | Prerequisite flag evaluation result: 'True'. 11 | Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'True') evaluates to true. 12 | ) => true 13 | AND Flag 'mainFeatureWithoutUserCondition' EQUALS 'True' 14 | ( 15 | Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': 16 | Prerequisite flag evaluation result: 'True'. 17 | Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'True') evaluates to true. 18 | ) => true 19 | THEN 'True' => MATCH, applying rule 20 | Prerequisite flag evaluation result: 'True'. 21 | Condition (Flag 'intermediateFeature' EQUALS 'True') evaluates to true. 22 | ) 23 | THEN 'Dog' => MATCH, applying rule 24 | Returning 'Dog'. 25 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_both.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'dependentFeatureWithUserCondition2' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 3 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 4 | INFO [5000] Evaluating 'dependentFeatureWithUserCondition2' 5 | Evaluating targeting rules and applying the first match if any: 6 | - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing 7 | The current targeting rule is ignored and the evaluation continues with the next rule. 8 | - IF Flag 'mainFeature' EQUALS 'public' 9 | ( 10 | Evaluating prerequisite flag 'mainFeature': 11 | Evaluating targeting rules and applying the first match if any: 12 | - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 13 | THEN 'private' => cannot evaluate, User Object is missing 14 | The current targeting rule is ignored and the evaluation continues with the next rule. 15 | - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions 16 | THEN 'target' => cannot evaluate, User Object is missing 17 | The current targeting rule is ignored and the evaluation continues with the next rule. 18 | Prerequisite flag evaluation result: 'public'. 19 | Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true. 20 | ) 21 | THEN % options => MATCH, applying rule 22 | Skipping % options because the User Object is missing. 23 | The current targeting rule is ignored and the evaluation continues with the next rule. 24 | - IF Flag 'mainFeature' EQUALS 'public' 25 | ( 26 | Evaluating prerequisite flag 'mainFeature': 27 | Evaluating targeting rules and applying the first match if any: 28 | - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 29 | THEN 'private' => cannot evaluate, User Object is missing 30 | The current targeting rule is ignored and the evaluation continues with the next rule. 31 | - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions 32 | THEN 'target' => cannot evaluate, User Object is missing 33 | The current targeting rule is ignored and the evaluation continues with the next rule. 34 | Prerequisite flag evaluation result: 'public'. 35 | Condition (Flag 'mainFeature' EQUALS 'public') evaluates to true. 36 | ) 37 | THEN 'Frog' => MATCH, applying rule 38 | Returning 'Frog'. 39 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_dep.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'dependentFeatureWithUserCondition' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'dependentFeatureWithUserCondition' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User.Email IS ONE OF [<2 hashed values>] THEN 'Dog' => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | - IF Flag 'mainFeatureWithoutUserCondition' EQUALS 'True' 7 | ( 8 | Evaluating prerequisite flag 'mainFeatureWithoutUserCondition': 9 | Prerequisite flag evaluation result: 'True'. 10 | Condition (Flag 'mainFeatureWithoutUserCondition' EQUALS 'True') evaluates to true. 11 | ) 12 | THEN % options => MATCH, applying rule 13 | Skipping % options because the User Object is missing. 14 | The current targeting rule is ignored and the evaluation continues with the next rule. 15 | Returning 'Chicken'. 16 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/prerequisite_flag/prerequisite_flag_no_user_needed_by_prereq.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'mainFeature' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'dependentFeature' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF Flag 'mainFeature' EQUALS 'target' 5 | ( 6 | Evaluating prerequisite flag 'mainFeature': 7 | Evaluating targeting rules and applying the first match if any: 8 | - IF User.Email ENDS WITH ANY OF [<1 hashed value>] => false, skipping the remaining AND conditions 9 | THEN 'private' => cannot evaluate, User Object is missing 10 | The current targeting rule is ignored and the evaluation continues with the next rule. 11 | - IF User.Country IS ONE OF [<1 hashed value>] => false, skipping the remaining AND conditions 12 | THEN 'target' => cannot evaluate, User Object is missing 13 | The current targeting rule is ignored and the evaluation continues with the next rule. 14 | Prerequisite flag evaluation result: 'public'. 15 | Condition (Flag 'mainFeature' EQUALS 'target') evaluates to false. 16 | ) 17 | THEN % options => no match 18 | Returning 'Chicken'. 19 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/segment.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/v2/e7a75611-4256-49a5-9320-ce158755e3ba/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08dbd6ca-a85f-4ed0-888a-2da18def92b5/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/y_ZB7o-Xb0Swxth-ZlMSeA", 4 | "tests": [ 5 | { 6 | "key": "featureWithSegmentTargeting", 7 | "defaultValue": false, 8 | "returnValue": false, 9 | "expectedLog": "segment_no_user.txt" 10 | }, 11 | { 12 | "key": "featureWithSegmentTargetingMultipleConditions", 13 | "defaultValue": false, 14 | "returnValue": false, 15 | "expectedLog": "segment_no_user_multi_conditions.txt" 16 | }, 17 | { 18 | "key": "featureWithNegatedSegmentTargetingCleartext", 19 | "defaultValue": false, 20 | "user": { 21 | "Identifier": "12345" 22 | }, 23 | "returnValue": false, 24 | "expectedLog": "segment_no_targeted_attribute.txt" 25 | }, 26 | { 27 | "key": "featureWithSegmentTargeting", 28 | "defaultValue": false, 29 | "user": { 30 | "Identifier": "12345", 31 | "Email": "jane@example.com" 32 | }, 33 | "returnValue": true, 34 | "expectedLog": "segment_matching.txt" 35 | }, 36 | { 37 | "key": "featureWithNegatedSegmentTargeting", 38 | "defaultValue": false, 39 | "user": { 40 | "Identifier": "12345", 41 | "Email": "jane@example.com" 42 | }, 43 | "returnValue": false, 44 | "expectedLog": "segment_no_matching.txt" 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/segment/segment_matching.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'featureWithSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User IS IN SEGMENT 'Beta users' 4 | ( 5 | Evaluating segment 'Beta users': 6 | - IF User.Email IS ONE OF [<2 hashed values>] => true 7 | Segment evaluation result: User IS IN SEGMENT. 8 | Condition (User IS IN SEGMENT 'Beta users') evaluates to true. 9 | ) 10 | THEN 'True' => MATCH, applying rule 11 | Returning 'True'. 12 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/segment/segment_no_matching.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'featureWithNegatedSegmentTargeting' for User '{"Identifier":"12345","Email":"jane@example.com"}' 2 | Evaluating targeting rules and applying the first match if any: 3 | - IF User IS NOT IN SEGMENT 'Beta users' 4 | ( 5 | Evaluating segment 'Beta users': 6 | - IF User.Email IS ONE OF [<2 hashed values>] => true 7 | Segment evaluation result: User IS IN SEGMENT. 8 | Condition (User IS NOT IN SEGMENT 'Beta users') evaluates to false. 9 | ) 10 | THEN 'True' => no match 11 | Returning 'False'. 12 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/segment/segment_no_targeted_attribute.txt: -------------------------------------------------------------------------------- 1 | WARNING [3003] Cannot evaluate condition (User.Email IS ONE OF ['jane@example.com', 'john@example.com']) for setting 'featureWithNegatedSegmentTargetingCleartext' (the User.Email attribute is missing). You should set the User.Email attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'featureWithNegatedSegmentTargetingCleartext' for User '{"Identifier":"12345"}' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User IS NOT IN SEGMENT 'Beta users (cleartext)' 5 | ( 6 | Evaluating segment 'Beta users (cleartext)': 7 | - IF User.Email IS ONE OF ['jane@example.com', 'john@example.com'] => false, skipping the remaining AND conditions 8 | Segment evaluation result: cannot evaluate, the User.Email attribute is missing. 9 | Condition (User IS NOT IN SEGMENT 'Beta users (cleartext)') failed to evaluate. 10 | ) 11 | THEN 'True' => cannot evaluate, the User.Email attribute is missing 12 | The current targeting rule is ignored and the evaluation continues with the next rule. 13 | Returning 'False'. 14 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/segment/segment_no_user.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargeting' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'featureWithSegmentTargeting' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User IS IN SEGMENT 'Beta users' THEN 'True' => cannot evaluate, User Object is missing 5 | The current targeting rule is ignored and the evaluation continues with the next rule. 6 | Returning 'False'. 7 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/segment/segment_no_user_multi_conditions.txt: -------------------------------------------------------------------------------- 1 | WARNING [3001] Cannot evaluate targeting rules and % options for setting 'featureWithSegmentTargetingMultipleConditions' (User Object is missing). You should pass a User Object to the evaluation methods like `get_value()` in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/ 2 | INFO [5000] Evaluating 'featureWithSegmentTargetingMultipleConditions' 3 | Evaluating targeting rules and applying the first match if any: 4 | - IF User IS IN SEGMENT 'Beta users (cleartext)' => false, skipping the remaining AND conditions 5 | THEN 'True' => cannot evaluate, User Object is missing 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | Returning 'False'. 8 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/semver_validation.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d745f1-f315-7daf-d163-5541d3786e6f/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/BAr3KgLTP0ObzKnBTo5nhA", 4 | "tests": [ 5 | { 6 | "key": "isNotOneOf", 7 | "defaultValue": "default", 8 | "returnValue": "Default", 9 | "expectedLog": "semver_error.txt", 10 | "user": { 11 | "Identifier": "12345", 12 | "Custom1": "wrong_semver" 13 | } 14 | }, 15 | { 16 | "key": "relations", 17 | "defaultValue": "default", 18 | "returnValue": "Default", 19 | "expectedLog": "semver_relations_error.txt", 20 | "user": { 21 | "Identifier": "12345", 22 | "Custom1": "wrong_semver" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/semver_validation/semver_error.txt: -------------------------------------------------------------------------------- 1 | WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', '']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 2 | WARNING [3004] Cannot evaluate condition (User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1']) for setting 'isNotOneOf' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 3 | INFO [5000] Evaluating 'isNotOneOf' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' 4 | Evaluating targeting rules and applying the first match if any: 5 | - IF User.Custom1 IS NOT ONE OF ['1.0.0', '1.0.1', '2.0.0', '2.0.1', '2.0.2', ''] THEN 'Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, )' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 6 | The current targeting rule is ignored and the evaluation continues with the next rule. 7 | - IF User.Custom1 IS NOT ONE OF ['1.0.0', '3.0.1'] THEN 'Is not one of (1.0.0, 3.0.1)' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 8 | The current targeting rule is ignored and the evaluation continues with the next rule. 9 | Returning 'Default'. 10 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/semver_validation/semver_relations_error.txt: -------------------------------------------------------------------------------- 1 | WARNING [3004] Cannot evaluate condition (User.Custom1 < '1.0.0,') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 2 | WARNING [3004] Cannot evaluate condition (User.Custom1 < '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 3 | WARNING [3004] Cannot evaluate condition (User.Custom1 <= '1.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 4 | WARNING [3004] Cannot evaluate condition (User.Custom1 > '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 5 | WARNING [3004] Cannot evaluate condition (User.Custom1 >= '2.0.0') for setting 'relations' ('wrong_semver' is not a valid semantic version). Please check the User.Custom1 attribute and make sure that its value corresponds to the comparison operator. 6 | INFO [5000] Evaluating 'relations' for User '{"Identifier":"12345","Custom1":"wrong_semver"}' 7 | Evaluating targeting rules and applying the first match if any: 8 | - IF User.Custom1 < '1.0.0,' THEN '<1.0.0,' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 9 | The current targeting rule is ignored and the evaluation continues with the next rule. 10 | - IF User.Custom1 < '1.0.0' THEN '< 1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 11 | The current targeting rule is ignored and the evaluation continues with the next rule. 12 | - IF User.Custom1 <= '1.0.0' THEN '<=1.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 13 | The current targeting rule is ignored and the evaluation continues with the next rule. 14 | - IF User.Custom1 > '2.0.0' THEN '>2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 15 | The current targeting rule is ignored and the evaluation continues with the next rule. 16 | - IF User.Custom1 >= '2.0.0' THEN '>=2.0.0' => cannot evaluate, the User.Custom1 attribute is invalid ('wrong_semver' is not a valid semantic version) 17 | The current targeting rule is ignored and the evaluation continues with the next rule. 18 | Returning 'Default'. 19 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/simple_value.json: -------------------------------------------------------------------------------- 1 | { 2 | "configUrl": "https://app.configcat.com/08d5a03c-feb7-af1e-a1fa-40b3329f8bed/08d62463-86ec-8fde-f5b5-1c5c426fc830/244cf8b0-f604-11e8-b543-f23c917f9d8d", 3 | "sdkKey": "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A", 4 | "tests": [ 5 | { 6 | "key": "boolDefaultFalse", 7 | "defaultValue": true, 8 | "returnValue": false, 9 | "expectedLog": "off_flag.txt" 10 | }, 11 | { 12 | "key": "boolDefaultTrue", 13 | "defaultValue": false, 14 | "returnValue": true, 15 | "expectedLog": "on_flag.txt" 16 | }, 17 | { 18 | "key": "stringDefaultCat", 19 | "defaultValue": "Default", 20 | "returnValue": "Cat", 21 | "expectedLog": "text_setting.txt" 22 | }, 23 | { 24 | "key": "integerDefaultOne", 25 | "defaultValue": 0, 26 | "returnValue": 1, 27 | "expectedLog": "int_setting.txt" 28 | }, 29 | { 30 | "testName": "double_setting", 31 | "key": "doubleDefaultPi", 32 | "defaultValue": 0.0, 33 | "returnValue": 3.1415, 34 | "expectedLog": "double_setting.txt" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/simple_value/double_setting.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'doubleDefaultPi' 2 | Returning '3.1415'. 3 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/simple_value/int_setting.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'integerDefaultOne' 2 | Returning '1'. 3 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/simple_value/off_flag.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'boolDefaultFalse' 2 | Returning 'False'. 3 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/simple_value/on_flag.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'boolDefaultTrue' 2 | Returning 'True'. 3 | -------------------------------------------------------------------------------- /configcatclienttests/data/evaluation/simple_value/text_setting.txt: -------------------------------------------------------------------------------- 1 | INFO [5000] Evaluating 'stringDefaultCat' 2 | Returning 'Cat'. 3 | -------------------------------------------------------------------------------- /configcatclienttests/data/test-simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "flags": { 3 | "disabledFeature": false, 4 | "enabledFeature": true, 5 | "intSetting": 5, 6 | "doubleSetting": 3.14, 7 | "stringSetting": "test" 8 | } 9 | } -------------------------------------------------------------------------------- /configcatclienttests/data/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "f": { 3 | "disabledFeature": { 4 | "t": 0, 5 | "v": { "b": false } 6 | }, 7 | "enabledFeature": { 8 | "t": 0, 9 | "v": { "b": true } 10 | }, 11 | "intSetting": { 12 | "t": 2, 13 | "v": { "i": 5 } 14 | }, 15 | "doubleSetting": { 16 | "t": 3, 17 | "v": { "d": 3.14 } 18 | }, 19 | "stringSetting": { 20 | "t": 1, 21 | "v": { "s": "test" } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /configcatclienttests/data/test_circulardependency_v6.json: -------------------------------------------------------------------------------- 1 | { 2 | "p": { 3 | "u": "https://cdn-global.configcat.com", 4 | "r": 0 5 | }, 6 | "f": { 7 | "key1": { 8 | "t": 1, 9 | "v": { "s": "key1-value" }, 10 | "r": [ 11 | { 12 | "c": [ 13 | { 14 | "p": { 15 | "f": "key1", 16 | "c": 0, 17 | "v": { "s": "key1-prereq" } 18 | } 19 | } 20 | ], 21 | "s": { "v": { "s": "key1-prereq" } } 22 | } 23 | ] 24 | }, 25 | "key2": { 26 | "t": 1, 27 | "v": { "s": "key2-value" }, 28 | "r": [ 29 | { 30 | "c": [ 31 | { 32 | "p": { 33 | "f": "key3", 34 | "c": 0, 35 | "v": { "s": "key3-prereq" } 36 | } 37 | } 38 | ], 39 | "s": { "v": { "s": "key2-prereq" } } 40 | } 41 | ] 42 | }, 43 | "key3": { 44 | "t": 1, 45 | "v": { "s": "key3-value" }, 46 | "r": [ 47 | { 48 | "c": [ 49 | { 50 | "p": { 51 | "f": "key2", 52 | "c": 0, 53 | "v": { "s": "key2-prereq" } 54 | } 55 | } 56 | ], 57 | "s": { "v": { "s": "key3-prereq" } } 58 | } 59 | ] 60 | }, 61 | "key4": { 62 | "t": 1, 63 | "v": { "s": "key4-value" }, 64 | "r": [ 65 | { 66 | "c": [ 67 | { 68 | "p": { 69 | "f": "key3", 70 | "c": 0, 71 | "v": { "s": "key3-prereq" } 72 | } 73 | } 74 | ], 75 | "s": { "v": { "s": "key4-prereq" } } 76 | } 77 | ] 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /configcatclienttests/data/test_override_flagdependency_v6.json: -------------------------------------------------------------------------------- 1 | { 2 | "p": { 3 | "u": "https://test-cdn-eu.configcat.com", 4 | "r": 0, 5 | "s": "TsTuRHo\u002BMHs8h8j16HQY83sooJsLg34Ir5KIVOletFU=" 6 | }, 7 | "f": { 8 | "mainStringFlag": { 9 | "t": 1, 10 | "v": { 11 | "s": "private" 12 | }, 13 | "i": "24c96275" 14 | }, 15 | "stringDependsOnInt": { 16 | "t": 1, 17 | "r": [ 18 | { 19 | "c": [ 20 | { 21 | "p": { 22 | "f": "mainIntFlag", 23 | "c": 0, 24 | "v": { 25 | "i": 42 26 | } 27 | } 28 | } 29 | ], 30 | "s": { 31 | "v": { 32 | "s": "Dog" 33 | }, 34 | "i": "12531eec" 35 | } 36 | } 37 | ], 38 | "v": { 39 | "s": "Cat" 40 | }, 41 | "i": "e227d926" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /configcatclienttests/data/test_override_segments_v6.json: -------------------------------------------------------------------------------- 1 | { 2 | "p": { 3 | "u": "https://test-cdn-eu.configcat.com", 4 | "r": 0, 5 | "s": "80xCU/SlDz1lCiWFaxIBjyJeJecWjq46T4eu6GtozkM=" 6 | }, 7 | "s": [ 8 | { 9 | "n": "Beta Users", 10 | "r": [ 11 | { 12 | "a": "Email", 13 | "c": 16, 14 | "l": [ 15 | "9189c42f6035bd1d2df5eda347a4f62926d27c80540a7aa6cc72cc75bc6757ff" 16 | ] 17 | } 18 | ] 19 | }, 20 | { 21 | "n": "Developers", 22 | "r": [ 23 | { 24 | "a": "Email", 25 | "c": 16, 26 | "l": [ 27 | "a7cdf54e74b5527bd2617889ec47f6d29b825ccfc97ff00832886bcb735abded" 28 | ] 29 | } 30 | ] 31 | } 32 | ], 33 | "f": { 34 | "developerAndBetaUserSegment": { 35 | "t": 0, 36 | "r": [ 37 | { 38 | "c": [ 39 | { 40 | "s": { 41 | "s": 1, 42 | "c": 0 43 | } 44 | }, 45 | { 46 | "s": { 47 | "s": 0, 48 | "c": 1 49 | } 50 | } 51 | ], 52 | "s": { 53 | "v": { 54 | "b": true 55 | }, 56 | "i": "ddc50638" 57 | } 58 | } 59 | ], 60 | "v": { 61 | "b": false 62 | }, 63 | "i": "6427f4b8" 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /configcatclienttests/data/testmatrix_and_or.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;mainFeature;dependentFeature;emailAnd;emailOr 2 | ##null##;;;;public;Chicken;Cat;Cat 3 | ;;;;public;Chicken;Cat;Cat 4 | jane@example.com;jane@example.com;##null##;##null##;public;Chicken;Cat;Jane 5 | john@example.com;john@example.com;##null##;##null##;public;Chicken;Cat;John 6 | a@example.com;a@example.com;USA;##null##;target;Cat;Cat;Cat 7 | mark@example.com;mark@example.com;USA;##null##;target;Dog;Cat;Mark 8 | nora@example.com;nora@example.com;USA;##null##;target;Falcon;Cat;Cat 9 | stern@msn.com;stern@msn.com;USA;##null##;target;Horse;Cat;Cat 10 | jane@sensitivecompany.com;jane@sensitivecompany.com;England;##null##;private;Chicken;Dog;Jane 11 | anna@sensitivecompany.com;anna@sensitivecompany.com;France;##null##;private;Chicken;Cat;Cat 12 | jane@sensitivecompany.com;jane@sensitivecompany.com;england;##null##;public;Chicken;Dog;Jane 13 | jane;jane;##null##;##null##;public;Chicken;Cat;Cat 14 | @sensitivecompany.com;@sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat 15 | jane.sensitivecompany.com;jane.sensitivecompany.com;##null##;##null##;public;Chicken;Cat;Cat 16 | -------------------------------------------------------------------------------- /configcatclienttests/data/testmatrix_comparators_v6.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;boolTrueIn202304;stringEqualsDogDefaultCat;stringEqualsCleartextDogDefaultCat;stringDoseNotEqualDogDefaultCat;stringNotEqualsCleartextDogDefaultCat;stringStartsWithDogDefaultCat;stringNotStartsWithDogDefaultCat;stringEndsWithDogDefaultCat;stringNotEndsWithDogDefaultCat;arrayContainsDogDefaultCat;arrayDoesNotContainDogDefaultCat;arrayContainsCaseCheckDogDefaultCat;arrayDoesNotContainCaseCheckDogDefaultCat;customPercentageAttribute;missingPercentageAttribute;countryPercentageAttribute;stringContainsAnyOfDogDefaultCat;stringNotContainsAnyOfDogDefaultCat;stringStartsWithAnyOfDogDefaultCat;stringStartsWithAnyOfCleartextDogDefaultCat;stringNotStartsWithAnyOfDogDefaultCat;stringNotStartsWithAnyOfCleartextDogDefaultCat;stringEndsWithAnyOfDogDefaultCat;stringEndsWithAnyOfCleartextDogDefaultCat;stringNotEndsWithAnyOfDogDefaultCat;stringNotEndsWithAnyOfCleartextDogDefaultCat;stringArrayContainsAnyOfDogDefaultCat;stringArrayContainsAnyOfCleartextDogDefaultCat;stringArrayNotContainsAnyOfDogDefaultCat;stringArrayNotContainsAnyOfCleartextDogDefaultCat 2 | ##null##;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat 3 | ;;;;False;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Chicken;Chicken;Chicken;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat;Cat 4 | a@configcat.com;a@configcat.com;##null##;##null##;False;Dog;Dog;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Chicken;NotFound;Chicken;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 5 | b@configcat.com;b@configcat.com;Hungary;0;False;Cat;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Falcon;Cat;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 6 | c@configcat.com;c@configcat.com;United Kingdom;1680307199.9;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 7 | anna@configcat.com;anna@configcat.com;Hungary;1681118000.56;True;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 8 | bogjobber@verizon.net;bogjobber@verizon.net;##null##;1682899200.1;False;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Horse;Chicken;Chicken;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat 9 | cliffordj@aol.com;cliffordj@aol.com;Austria;1682999200;False;Cat;Cat;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Falcon;Chicken;Falcon;Dog;Cat;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Cat 10 | reader@configcat.com;reader@configcat.com;Bahamas;read,execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 11 | writer@configcat.com;writer@configcat.com;Belgium;write, execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 12 | reader@configcat.com;reader@configcat.com;Canada;execute, Read;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 13 | writer@configcat.com;writer@configcat.com;China;Write;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 14 | admin@configcat.com;admin@configcat.com;France;read, write,execute;False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 15 | user@configcat.com;user@configcat.com;Greece;,execute;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 16 | reader@configcat.com;reader@configcat.com;Bahamas;["read","execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Dog;Falcon;NotFound;Falcon;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 17 | writer@configcat.com;writer@configcat.com;Belgium;["write", "execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 18 | reader@configcat.com;reader@configcat.com;Canada;["execute", "Read"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 19 | writer@configcat.com;writer@configcat.com;China;["Write"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog 20 | admin@configcat.com;admin@configcat.com;France;["read", "write","execute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Dog;Cat;Cat;Dog;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 21 | admin@configcat.com;admin@configcat.com;France;["Read", "Write", "execute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 22 | admin@configcat.com;admin@configcat.com;France;["Read", "Write", "eXecute"];False;Cat;Cat;Dog;Dog;Dog;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog 23 | user@configcat.com;user@configcat.com;Greece;["","execute"];False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Dog;Cat;Dog;Horse;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Dog;Dog;Cat;Cat 24 | user@configcat.com;user@configcat.com;Monaco;,null, ,,nil, None;False;Cat;Cat;Dog;Dog;Cat;Dog;Dog;Cat;Cat;Cat;Cat;Cat;Falcon;NotFound;Horse;Cat;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Dog;Dog;Cat;Cat;Cat;Cat 25 | -------------------------------------------------------------------------------- /configcatclienttests/data/testmatrix_number.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;numberWithPercentage;number 2 | ##null##;;;;Default;Default 3 | id1;;;0;<2.1;<>5 4 | id1;;;0.0;<2.1;<>5 5 | id1;;;0,0;<2.1;<>5 6 | id1;;;0.2;<2.1;<>5 7 | id2;;;0,2;<2.1;<>5 8 | id3;;;1;<2.1;<>5 9 | id4;;;1.0;<2.1;<>5 10 | id5;;;1,0;<2.1;<>5 11 | id6;;;1.5;<2.1;<>5 12 | id7;;;1,5;<2.1;<>5 13 | id8;;;2.1;<=2,1;<>5 14 | id9;;;2,1;<=2,1;<>5 15 | id10;;;3.50;=3.5;<>5 16 | id11;;;3,50;=3.5;<>5 17 | id12;;;5;>=5;Default 18 | id13;;;5.0;>=5;Default 19 | id14;;;5,0;>=5;Default 20 | id13;;;5.76;>5;<>5 21 | id14;;;5,76;>5;<>5 22 | id15;;;4;<>4.2;<>5 23 | id16;;;4.0;<>4.2;<>5 24 | id17;;;4,0;<>4.2;<>5 25 | id18;;;4.2;80%;<>5 26 | id19;;;4,2;20%;<>5 27 | -------------------------------------------------------------------------------- /configcatclienttests/data/testmatrix_prerequisite_flag.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;mainBoolFlag;mainStringFlag;mainIntFlag;mainDoubleFlag;stringDependsOnBool;stringDependsOnString;stringDependsOnStringCaseCheck;stringDependsOnInt;stringDependsOnDouble;stringDependsOnDoubleIntValue;boolDependsOnBool;intDependsOnBool;doubleDependsOnBool;boolDependsOnBoolDependsOnBool;mainBoolFlagEmpty;stringDependsOnEmptyBool;stringInverseDependsOnEmptyBool;mainBoolFlagInverse;boolDependsOnBoolInverse 2 | ##null##;;;;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True 3 | ;;;;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True 4 | john@sensitivecompany.com;john@sensitivecompany.com;##null##;##null##;False;private;2;0.1;Cat;Dog;Cat;Dog;Dog;Cat;False;42;3.14;True;True;EmptyOn;EmptyOn;True;False 5 | jane@example.com;jane@example.com;##null##;##null##;True;public;42;3.14;Dog;Cat;Cat;Cat;Cat;Cat;True;1;1.1;False;True;EmptyOn;EmptyOn;False;True 6 | -------------------------------------------------------------------------------- /configcatclienttests/data/testmatrix_segments.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;developerAndBetaUserSegment;developerAndBetaUserCleartextSegment;notDeveloperAndNotBetaUserSegment;notDeveloperAndNotBetaUserCleartextSegment 2 | ##null##;;;;False;False;False;False 3 | ;;;;False;False;False;False 4 | john@example.com;john@example.com;##null##;##null##;False;False;False;False 5 | jane@example.com;jane@example.com;##null##;##null##;False;False;False;False 6 | kate@example.com;kate@example.com;##null##;##null##;True;True;True;True 7 | -------------------------------------------------------------------------------- /configcatclienttests/data/testmatrix_segments_old.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;featureWithSegmentTargeting;featureWithSegmentTargetingCleartext;featureWithNegatedSegmentTargeting;featureWithNegatedSegmentTargetingCleartext;featureWithSegmentTargetingInverse;featureWithSegmentTargetingInverseCleartext;featureWithNegatedSegmentTargetingInverse;featureWithNegatedSegmentTargetingInverseCleartext 2 | ##null##;;;;False;False;False;False;False;False;False;False 3 | ;;;;False;False;False;False;False;False;False;False 4 | john@example.com;john@example.com;##null##;##null##;True;True;False;False;False;False;True;True 5 | jane@example.com;jane@example.com;##null##;##null##;True;True;False;False;False;False;True;True 6 | kate@example.com;kate@example.com;##null##;##null##;False;False;True;True;True;True;False;False 7 | -------------------------------------------------------------------------------- /configcatclienttests/data/testmatrix_semantic.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;isOneOf;isOneOfWithPercentage;isNotOneOf;isNotOneOfWithPercentage;lessThanWithPercentage;relations 2 | ##null##;;;;Default;Default;Default;Default;Default;Default 3 | id1;;;0.0.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );< 1.0.0;< 1.0.0 4 | id1;;;0.1.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );< 1.0.0;< 1.0.0 5 | id1;;;0.2.1;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );< 1.0.0;< 1.0.0 6 | id1;;;1;Default;80%;Default;80%;20%;Default 7 | id2;;;1.0;Default;80%;Default;80%;80%;Default 8 | id3;;;1.0.0;Is one of (1.0.0);is one of (1.0.0);Default;80%;80%;<=1.0.0 9 | id4;;;1.0.0.0;Default;80%;Default;20%;20%;Default 10 | id5;;;1.0.0.0.0;Default;80%;Default;80%;80%;Default 11 | id6;;;1.0.1;Default;80%;Is not one of (1.0.0, 3.0.1);Is not one of (1.0.0, 3.0.1);80%;Default 12 | id7;;;1.0.11;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;Default 13 | id8;;;1.0.111;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 14 | id9;;;1.0.2;Default;20%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 15 | id10;;;1.0.3;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 16 | id11;;;1.0.4;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 17 | id12;;;1.0.5;Default;20%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 18 | id13;;;1.1.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 19 | id14;;;1.1.1;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 20 | id15;;;1.1.2;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 21 | id16;;;1.1.3;Default;20%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;Default 22 | id17;;;1.1.4;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;Default 23 | id18;;;1.1.5;Default;20%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 24 | id19;;;1.9.0;Default;20%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;Default 25 | id20;;;1.9.99;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;Default 26 | id21;;;2.0.0;Default;80%;Is not one of (1.0.0, 3.0.1);Is not one of (1.0.0, 3.0.1);20%;>=2.0.0 27 | id22;;;2.0.1;Is one of ( , 2.0.1, 2.0.2, );80%;Is not one of (1.0.0, 3.0.1);Is not one of (1.0.0, 3.0.1);80%;>2.0.0 28 | id23;;;2.0.11;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;>2.0.0 29 | id24;;;2.0.2;Is one of ( , 2.0.1, 2.0.2, );80%;Is not one of (1.0.0, 3.0.1);Is not one of (1.0.0, 3.0.1);80%;>2.0.0 30 | id25;;;2.0.3;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 31 | id26;;;3.0.0;Is one of (3.0.0);80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 32 | id27;;;3.0.1;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;>2.0.0 33 | id28;;;3.1.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 34 | id28;;;3.1.1;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 35 | id29;;;5.0.0;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );80%;>2.0.0 36 | id30;;;5.99.999;Default;80%;Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );Is not one of (1.0.0, 1.0.1, 2.0.0 , 2.0.1, 2.0.2, );20%;>2.0.0 37 | -------------------------------------------------------------------------------- /configcatclienttests/data/testmatrix_semantic_2.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;AppVersion;precedenceTests 2 | dontcare;;;1.9.1-1;< 1.9.1-2 3 | dontcare;;;1.9.1-2;< 1.9.1-10 4 | dontcare;;;1.9.1-10;< 1.9.1-10a 5 | dontcare;;;1.9.1-10a;< 1.9.1-1a 6 | dontcare;;;1.9.1-1a;< 1.9.1-alpha 7 | dontcare;;;1.9.1-alpha;< 1.9.99-alpha 8 | dontcare;;;1.9.99-alpha;= 1.9.99-alpha 9 | dontcare;;;1.9.99-alpha+build1;= 1.9.99-alpha 10 | dontcare;;;1.9.99-alpha+build2;= 1.9.99-alpha 11 | dontcare;;;1.9.99-alpha2;< 1.9.99-beta 12 | dontcare;;;1.9.99-beta;< 1.9.99-rc 13 | dontcare;;;1.9.99-rc;< 1.9.99-rc.1 14 | dontcare;;;1.9.99-rc.1;< 1.9.99-rc.2 15 | dontcare;;;1.9.99-rc.2;< 1.9.99-rc.20 16 | dontcare;;;1.9.99-rc.9;< 1.9.99-rc.20 17 | dontcare;;;1.9.99-rc.20;< 1.9.99-rc.20a 18 | dontcare;;;1.9.99-rc.20a;< 1.9.99-rc.2a 19 | dontcare;;;1.9.99-rc.2a;< 1.9.99 20 | dontcare;;;1.9.99;< 1.9.100 21 | dontcare;;;1.9.100;< 1.10.0-alpha 22 | dontcare;;;1.10.0-alpha;<= 1.10.0-alpha 23 | dontcare;;;1.10.0;<= 1.10.0 24 | dontcare;;;1.10.1;<= 1.10.1 25 | dontcare;;;1.10.2;<= 1.10.3 26 | dontcare;;;2.0.0;= 2.0.0 27 | dontcare;;;2.0.0+build3;= 2.0.0 28 | dontcare;;;2.0.0+001;= 2.0.0 29 | dontcare;;;2.0.0+20130313144700;= 2.0.0 30 | dontcare;;;2.0.0+exp.sha.5114f85;= 2.0.0 31 | dontcare;;;3.0.0;= 3.0.0+build3 32 | dontcare;;;4.0.0;= 4.0.0+001 33 | dontcare;;;5.0.0;= 5.0.0+20130313144700 34 | dontcare;;;6.0.0;= 6.0.0+exp.sha.5114f85 35 | dontcare;;;7.0.0-patch+metadata;= 7.0.0-patch 36 | dontcare;;;8.0.0-patch+metadata;= 8.0.0-patch+anothermetadata 37 | dontcare;;;9.0.0-patch;= 9.0.0-patch+metadata 38 | dontcare;;;10.0.0;DEFAULT-FROM-CC-APP 39 | dontcare;;;104.0.0;> 103.0.0 40 | dontcare;;;103.0.0;>= 103.0.0 41 | dontcare;;;102.0.0;>= 101.0.0 42 | dontcare;;;101.0.0;>= 101.0.0 43 | dontcare;;;90.104.0;> 90.103.0 44 | dontcare;;;90.103.0;>= 90.103.0 45 | dontcare;;;90.102.0;>= 90.101.0 46 | dontcare;;;90.101.0;>= 90.101.0 47 | dontcare;;;80.0.104;> 80.0.103 48 | dontcare;;;80.0.103;>= 80.0.103 49 | dontcare;;;80.0.102;>= 80.0.101 50 | dontcare;;;80.0.101;>= 80.0.101 51 | dontcare;;;73.0.0;>= 73.0.0-beta.2 52 | dontcare;;;72.0.0;> 72.0.0-beta.2 53 | dontcare;;;72.0.0-beta.2;> 72.0.0-beta.1 54 | dontcare;;;72.0.0-beta.1;> 72.0.0-beta 55 | dontcare;;;72.0.0-beta;> 72.0.0-alpha 56 | dontcare;;;72.0.0-alpha;> 72.0.0-1a 57 | dontcare;;;72.0.0-1a;> 72.0.0-10a 58 | dontcare;;;72.0.0-10aa;> 72.0.0-10a 59 | dontcare;;;72.0.0-10a;> 72.0.0-2 60 | dontcare;;;72.0.0-2;> 72.0.0-1 61 | dontcare;;;71.0.0+metadata;>= 71.0.0+anothermetadata 62 | dontcare;;;71.0.0-patch3+metadata;>= 71.0.0-patch3+anothermetadata 63 | dontcare;;;71.0.0-patch2+metadata;>= 71.0.0-patch2 64 | dontcare;;;71.0.0-patch1;>= 71.0.0-patch1+metadata 65 | dontcare;;;60.73.0;>= 60.73.0-beta.2 66 | dontcare;;;60.72.0;> 60.72.0-beta.2 67 | dontcare;;;60.72.0-beta.2;> 60.72.0-beta.1 68 | dontcare;;;60.72.0-beta.1;> 60.72.0-beta 69 | dontcare;;;60.72.0-beta;> 60.72.0-alpha 70 | dontcare;;;60.72.0-alpha;> 60.72.0-1a 71 | dontcare;;;60.72.0-1a;> 60.72.0-10a 72 | dontcare;;;60.72.0-10aa;> 60.72.0-10a 73 | dontcare;;;60.72.0-10a;> 60.72.0-2 74 | dontcare;;;60.72.0-2;> 60.72.0-1 75 | dontcare;;;60.71.0+metadata;>= 60.71.0+anothermetadata 76 | dontcare;;;60.71.0-patch3+metadata;>= 60.71.0-patch3+anothermetadata 77 | dontcare;;;60.71.0-patch2+metadata;>= 60.71.0-patch2 78 | dontcare;;;60.71.0-patch1;>= 60.71.0-patch1+metadata 79 | dontcare;;;50.60.73;>= 50.60.73-beta.2 80 | dontcare;;;50.60.72;> 50.60.72-beta.2 81 | dontcare;;;50.60.72-beta.2;> 50.60.72-beta.1 82 | dontcare;;;50.60.72-beta.1;> 50.60.72-beta 83 | dontcare;;;50.60.72-beta;> 50.60.72-alpha 84 | dontcare;;;50.60.72-alpha;> 50.60.72-1a 85 | dontcare;;;50.60.72-1a;> 50.60.72-10a 86 | dontcare;;;50.60.72-10aa;> 50.60.72-10a 87 | dontcare;;;50.60.72-10a;> 50.60.72-2 88 | dontcare;;;50.60.72-2;> 50.60.72-1 89 | dontcare;;;50.60.71+metadata;>= 50.60.71+anothermetadata 90 | dontcare;;;50.60.71-patch3+metadata;>= 50.60.71-patch3+anothermetadata 91 | dontcare;;;50.60.71-patch2+metadata;>= 50.60.71-patch2 92 | dontcare;;;50.60.71-patch1;>= 50.60.71-patch1+metadata 93 | dontcare;;;50.60.71-patch1+anothermetadata;>= 50.60.71-patch1+metadata 94 | dontcare;;;40.0.0-patch;>= 40.0.0-patch 95 | dontcare;;;30.0.0-beta;>= 30.0.0-alpha 96 | -------------------------------------------------------------------------------- /configcatclienttests/data/testmatrix_sensitive.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;isOneOfSensitive;isNotOneOfSensitive 2 | ##null##;;;;ToAll;ToAll 3 | id1;macska@example.com;;;Macska;Kigyo 4 | Kutya;;;;Allat;ToAll 5 | Sas;;;;ToAll;Kigyo 6 | Kutya;macska@example.com;;;Macska;ToAll 7 | id1;;Scotland;;Britt;Kigyo 8 | Macska;;USA;;ToAll;Ireland -------------------------------------------------------------------------------- /configcatclienttests/data/testmatrix_unicode.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;🆃🅴🆇🆃;boolTextEqualsHashed;boolTextEqualsCleartext;boolTextNotEqualsHashed;boolTextNotEqualsCleartext;boolIsOneOfHashed;boolIsOneOfCleartext;boolIsNotOneOfHashed;boolIsNotOneOfCleartext;boolStartsWithHashed;boolStartsWithCleartext;boolNotStartsWithHashed;boolNotStartsWithCleartext;boolEndsWithHashed;boolEndsWithCleartext;boolNotEndsWithHashed;boolNotEndsWithCleartext;boolContainsCleartext;boolNotContainsCleartext;boolArrayContainsHashed;boolArrayContainsCleartext;boolArrayNotContainsHashed;boolArrayNotContainsCleartext 2 | 1;;;ʄǟռƈʏ ȶɛӼȶ;True;True;False;False;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False 3 | 1;;;ʄaռƈʏ ȶɛӼȶ;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False 4 | 1;;;ÁRVÍZTŰRŐ tükörfúrógép;False;False;True;True;True;True;False;False;True;True;False;False;True;True;False;False;True;False;False;False;False;False 5 | 1;;;árvíztűrő tükörfúrógép;False;False;True;True;False;False;True;True;False;False;True;True;True;True;False;False;True;False;False;False;False;False 6 | 1;;;ÁRVÍZTŰRŐ TÜKÖRFÚRÓGÉP;False;False;True;True;False;False;True;True;True;True;False;False;False;False;True;True;True;False;False;False;False;False 7 | 1;;;árvíztűrő TÜKÖRFÚRÓGÉP;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False 8 | 1;;;u𝖓𝖎𝖈𝖔𝖉e;False;False;True;True;True;True;False;False;True;True;False;False;True;True;False;False;True;False;False;False;False;False 9 | ;;;𝖚𝖓𝖎𝖈𝖔𝖉e;False;False;True;True;False;False;True;True;False;False;True;True;True;True;False;False;True;False;False;False;False;False 10 | ;;;u𝖓𝖎𝖈𝖔𝖉𝖊;False;False;True;True;False;False;True;True;True;True;False;False;False;False;True;True;True;False;False;False;False;False 11 | ;;;𝖚𝖓𝖎𝖈𝖔𝖉𝖊;False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;False;True;False;False;False;False 12 | 1;;;["ÁRVÍZTŰRŐ tükörfúrógép", "unicode"];False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;True;False;True;True;False;False 13 | 1;;;["ÁRVÍZTŰRŐ", "tükörfúrógép", "u𝖓𝖎𝖈𝖔𝖉e"];False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;True;False;True;True;False;False 14 | 1;;;["ÁRVÍZTŰRŐ", "tükörfúrógép", "unicode"];False;False;True;True;False;False;True;True;False;False;True;True;False;False;True;True;True;False;False;False;True;True 15 | -------------------------------------------------------------------------------- /configcatclienttests/data/testmatrix_variationId.csv: -------------------------------------------------------------------------------- 1 | Identifier;Email;Country;Custom1;boolean;decimal;text;whole 2 | ##null##;;;;a0e56eda;63612d39;3f05be89;cf2e9162; 3 | a@configcat.com;a@configcat.com;Hungary;admin;67787ae4;8f9559cf;9bdc6a1f;ab30533b; 4 | b@configcat.com;b@configcat.com;Hungary;admin;67787ae4;8f9559cf;9bdc6a1f;ab30533b; 5 | a@test.com;a@test.com;Hungary;admin;67787ae4;d66c5781;65310deb;ec14f6a9; 6 | b@test.com;b@test.com;Hungary;admin;a0e56eda;d66c5781;65310deb;ec14f6a9; 7 | cliffordj@aol.com;cliffordj@aol.com;Hungary;admin;67787ae4;8155ad7b;cf19e913;ec14f6a9; 8 | bryanw@verizon.net;bryanw@verizon.net;Hungary;;a0e56eda;d0dbc27f;30ba32b9;61a5a033; 9 | -------------------------------------------------------------------------------- /configcatclienttests/mocks.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | import logging 4 | 5 | from configcatclient.config import SettingType 6 | from configcatclient.configentry import ConfigEntry 7 | from configcatclient.utils import get_utc_now_seconds_since_epoch, distant_past 8 | from configcatclient.configfetcher import FetchResponse, ConfigFetcher 9 | from configcatclient.interfaces import ConfigCache 10 | 11 | TEST_SDK_KEY = 'configcat-sdk-test-key/0000000000000000000000' 12 | TEST_SDK_KEY1 = 'configcat-sdk-test-key/0000000000000000000001' 13 | TEST_SDK_KEY2 = 'configcat-sdk-test-key/0000000000000000000002' 14 | 15 | TEST_JSON = r'''{ 16 | "p": { 17 | "u": "https://cdn-global.configcat.com", 18 | "r": 0 19 | }, 20 | "f": { 21 | "testKey": { "v": { "s": "testValue" }, "t": 1 } 22 | } 23 | }''' 24 | 25 | TEST_JSON_FORMAT = '{{ "f": {{ "testKey": {{ "t": {value_type}, "v": {value}, "p": [], "r": [] }} }} }}' 26 | 27 | TEST_JSON2 = r'''{ 28 | "p": { 29 | "u": "https://cdn-global.configcat.com", 30 | "r": 0 31 | }, 32 | "f": { 33 | "testKey": { "v": { "s": "testValue" }, "t": 1 }, 34 | "testKey2": { "v": { "s": "testValue2" }, "t": 1 } 35 | } 36 | }''' 37 | 38 | TEST_OBJECT = json.loads(r'''{ 39 | "p": { 40 | "u": "https://cdn-global.configcat.com", 41 | "r": 0 42 | }, 43 | "s": [ 44 | {"n": "id1", "r": [{"a": "Identifier", "c": 2, "l": ["@test1.com"]}]}, 45 | {"n": "id2", "r": [{"a": "Identifier", "c": 2, "l": ["@test2.com"]}]} 46 | ], 47 | "f": { 48 | "testBoolKey": {"v": {"b": true}, "t": 0}, 49 | "testStringKey": {"v": {"s": "testValue"}, "i": "id", "t": 1, "r": [ 50 | {"c": [{"s": {"s": 0, "c": 0}}], "s": {"v": {"s": "fake1"}, "i": "id1"}}, 51 | {"c": [{"s": {"s": 1, "c": 0}}], "s": {"v": {"s": "fake2"}, "i": "id2"}} 52 | ]}, 53 | "testIntKey": {"v": {"i": 1}, "t": 2}, 54 | "testDoubleKey": {"v": {"d": 1.1}, "t": 3}, 55 | "key1": {"v": {"b": true}, "t": 0, "i": "id3"}, 56 | "key2": {"v": {"s": "fake4"}, "t": 1, "i": "id4", 57 | "r": [ 58 | {"c": [{"s": {"s": 0, "c": 0}}], "p": [ 59 | {"p": 50, "v": {"s": "fake5"}, "i": "id5"}, {"p": 50, "v": {"s": "fake6"}, "i": "id6"} 60 | ]} 61 | ], 62 | "p": [ 63 | {"p": 50, "v": {"s": "fake7"}, "i": "id7"}, {"p": 50, "v": {"s": "fake8"}, "i": "id8"} 64 | ] 65 | } 66 | } 67 | }''') 68 | 69 | 70 | class ConfigFetcherMock(ConfigFetcher): 71 | def __init__(self): 72 | self._call_count = 0 73 | self._fetch_count = 0 74 | self._configuration = TEST_JSON 75 | self._etag = 'test_etag' 76 | 77 | def get_configuration(self, etag=''): 78 | self._call_count += 1 79 | if etag != self._etag: 80 | self._fetch_count += 1 81 | return FetchResponse.success( 82 | ConfigEntry(json.loads(self._configuration), self._etag, self._configuration, get_utc_now_seconds_since_epoch()) 83 | ) 84 | return FetchResponse.not_modified() 85 | 86 | def set_configuration_json(self, value): 87 | if self._configuration != value: 88 | self._configuration = value 89 | self._etag += '_etag' 90 | 91 | @property 92 | def get_call_count(self): 93 | return self._call_count 94 | 95 | @property 96 | def get_fetch_count(self): 97 | return self._fetch_count 98 | 99 | 100 | class ConfigFetcherWithErrorMock(ConfigFetcher): 101 | def __init__(self, error): 102 | self._error = error 103 | 104 | def get_configuration(self, etag=''): 105 | return FetchResponse.failure(self._error, True) 106 | 107 | 108 | class ConfigFetcherWaitMock(ConfigFetcher): 109 | def __init__(self, wait_seconds): 110 | self._wait_seconds = wait_seconds 111 | 112 | def get_configuration(self, etag=''): 113 | time.sleep(self._wait_seconds) 114 | return FetchResponse.success(ConfigEntry(json.loads(TEST_JSON), etag, TEST_JSON)) 115 | 116 | 117 | class ConfigFetcherCountMock(ConfigFetcher): 118 | def __init__(self): 119 | self._value = 0 120 | 121 | def get_configuration(self, etag=''): 122 | self._value += 1 123 | value_string = '{ "i": %s }' % self._value 124 | config_json_string = TEST_JSON_FORMAT.format(value_type=SettingType.INT, value=value_string) 125 | config = json.loads(config_json_string) 126 | return FetchResponse.success(ConfigEntry(config, etag, config_json_string)) 127 | 128 | 129 | class ConfigCacheMock(ConfigCache): 130 | def get(self, key): 131 | return '\n'.join([str(distant_past), 'test-etag', json.dumps(TEST_OBJECT)]) 132 | 133 | def set(self, key, value): 134 | pass 135 | 136 | 137 | class SingleValueConfigCache(ConfigCache): 138 | 139 | def __init__(self, value): 140 | self._value = value 141 | 142 | def get(self, key): 143 | return self._value 144 | 145 | def set(self, key, value): 146 | self._value = value 147 | 148 | 149 | class MockHeader: 150 | def __init__(self, etag): 151 | self.etag = etag 152 | 153 | def get(self, name): 154 | if name == 'ETag': 155 | return self.etag 156 | return None 157 | 158 | 159 | class MockResponse: 160 | def __init__(self, json_data, status_code, etag=None): 161 | self.json_data = json_data 162 | self.text = json.dumps(json_data) 163 | self.status_code = status_code 164 | self.headers = MockHeader(etag) 165 | 166 | def json(self): 167 | return self.json_data 168 | 169 | def raise_for_status(self): 170 | if 200 <= self.status_code < 300 or self.status_code == 304: 171 | return 172 | raise Exception(self.status_code) 173 | 174 | 175 | class HookCallbacks(object): 176 | def __init__(self): 177 | self.is_ready = False 178 | self.is_ready_call_count = 0 179 | self.changed_config = None 180 | self.changed_config_call_count = 0 181 | self.evaluation_details = None 182 | self.evaluation_details_call_count = 0 183 | self.error = None 184 | self.error_call_count = 0 185 | self.callback_exception_call_count = 0 186 | 187 | def on_client_ready(self): 188 | self.is_ready = True 189 | self.is_ready_call_count += 1 190 | 191 | def on_config_changed(self, config): 192 | self.changed_config = config 193 | self.changed_config_call_count += 1 194 | 195 | def on_flag_evaluated(self, evaluation_details): 196 | self.evaluation_details = evaluation_details 197 | self.evaluation_details_call_count += 1 198 | 199 | def on_error(self, error): 200 | self.error = error 201 | self.error_call_count += 1 202 | 203 | def callback_exception(self, *args, **kwargs): 204 | self.callback_exception_call_count += 1 205 | raise Exception("error") 206 | 207 | 208 | class MockLogHandler(logging.Handler): 209 | def __init__(self, *args, **kwargs): 210 | super(MockLogHandler, self).__init__(*args, **kwargs) 211 | self.error_logs = [] 212 | self.warning_logs = [] 213 | self.info_logs = [] 214 | 215 | def clear(self): 216 | self.error_logs = [] 217 | self.warning_logs = [] 218 | self.info_logs = [] 219 | 220 | def emit(self, record): 221 | if record.levelno == logging.ERROR: 222 | self.error_logs.append(record.getMessage()) 223 | elif record.levelno == logging.WARNING: 224 | self.warning_logs.append(record.getMessage()) 225 | elif record.levelno == logging.INFO: 226 | self.info_logs.append(record.getMessage()) 227 | -------------------------------------------------------------------------------- /configcatclienttests/test_concurrency.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | import unittest 4 | import multiprocessing 5 | from time import sleep 6 | 7 | import pytest 8 | 9 | import configcatclient 10 | from configcatclient.user import User 11 | 12 | logging.basicConfig(level=logging.WARN) 13 | 14 | 15 | def _manual_force_refresh(client, repeat=10, delay=0.1): 16 | for i in range(repeat): 17 | client.force_refresh() 18 | sleep(delay) 19 | 20 | 21 | class ConcurrencyTests(unittest.TestCase): 22 | @pytest.mark.skipif(sys.platform == 'win32' or sys.platform == 'darwin', reason="TypeError: can't pickle _thread.lock objects") 23 | def test_concurrency_process(self): 24 | client = configcatclient.get('PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A') 25 | value = client.get_value('keySampleText', False, User('key')) 26 | print("'keySampleText' value from ConfigCat: " + str(value)) 27 | 28 | p1 = multiprocessing.Process(target=_manual_force_refresh, args=(client,)) 29 | p2 = multiprocessing.Process(target=_manual_force_refresh, args=(client,)) 30 | p1.start() 31 | p2.start() 32 | p1.join() 33 | p2.join() 34 | 35 | client.close() 36 | -------------------------------------------------------------------------------- /configcatclienttests/test_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | 4 | import pytest 5 | 6 | from configcatclient.config import get_value, SETTING_TYPE 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | 10 | 11 | class ConfigTests(unittest.TestCase): 12 | def test_value_setting_type_is_missing(self): 13 | value_dictionary = { 14 | 't': 6, # unsupported setting type 15 | 'v': { 16 | 'b': True 17 | } 18 | } 19 | setting_type = value_dictionary.get(SETTING_TYPE) 20 | with pytest.raises(ValueError) as e: 21 | get_value(value_dictionary, setting_type) 22 | assert str(e.value) == "Unsupported setting type" 23 | 24 | def test_value_setting_type_is_valid_but_return_value_is_missing(self): 25 | value_dictionary = { 26 | 't': 0, # boolean 27 | 'v': { 28 | 's': True # the wrong property is set ("b" should be set) 29 | } 30 | } 31 | setting_type = value_dictionary.get(SETTING_TYPE) 32 | with pytest.raises(ValueError) as e: 33 | get_value(value_dictionary, setting_type) 34 | assert str(e.value) == "Setting value is not of the expected type " 35 | 36 | def test_value_setting_type_is_valid_and_the_return_value_is_present_but_it_is_invalid(self): 37 | value_dictionary = { 38 | 't': 0, # boolean 39 | 'v': { 40 | 'b': 'True' # the value is a string instead of a boolean 41 | } 42 | } 43 | setting_type = value_dictionary.get(SETTING_TYPE) 44 | with pytest.raises(ValueError) as e: 45 | get_value(value_dictionary, setting_type) 46 | assert str(e.value) == "Setting value is not of the expected type " 47 | 48 | 49 | if __name__ == '__main__': 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /configcatclienttests/test_configcache.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import unittest 4 | 5 | from configcatclient import ConfigCatClient, ConfigCatOptions, PollingMode 6 | from configcatclient.config import SettingType 7 | from configcatclient.configcache import InMemoryConfigCache 8 | from configcatclient.configcatoptions import Hooks 9 | from configcatclient.configentry import ConfigEntry 10 | from configcatclient.configservice import ConfigService 11 | from configcatclient.utils import get_utc_now_seconds_since_epoch 12 | from configcatclienttests.mocks import TEST_JSON, SingleValueConfigCache, HookCallbacks, TEST_JSON_FORMAT, TEST_SDK_KEY 13 | 14 | logging.basicConfig() 15 | 16 | 17 | class ConfigCacheTests(unittest.TestCase): 18 | 19 | def test_cache(self): 20 | config_store = InMemoryConfigCache() 21 | 22 | value = config_store.get('key') 23 | self.assertEqual(value, None) 24 | 25 | config_store.set('key', TEST_JSON) 26 | value = config_store.get('key') 27 | self.assertEqual(value, TEST_JSON) 28 | 29 | value2 = config_store.get('key2') 30 | self.assertEqual(value2, None) 31 | 32 | def test_cache_key(self): 33 | self.assertEqual("f83ba5d45bceb4bb704410f51b704fb6dfa19942", ConfigService._get_cache_key('configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012')) 34 | self.assertEqual("da7bfd8662209c8ed3f9db96daed4f8d91ba5876", ConfigService._get_cache_key('configcat-sdk-1/TEST_KEY2-123456789012/1234567890123456789012')) 35 | 36 | def test_cache_payload(self): 37 | now_seconds = 1686756435.8449 38 | etag = 'test-etag' 39 | entry = ConfigEntry(json.loads(TEST_JSON), etag, TEST_JSON, now_seconds) 40 | self.assertEqual('1686756435844' + '\n' + etag + '\n' + TEST_JSON, entry.serialize()) 41 | 42 | def test_invalid_cache_content(self): 43 | hook_callbacks = HookCallbacks() 44 | hooks = Hooks(on_error=hook_callbacks.on_error) 45 | config_json_string = TEST_JSON_FORMAT.format(value_type=SettingType.STRING, value='{"s": "test"}') 46 | config_cache = SingleValueConfigCache(ConfigEntry( 47 | config=json.loads(config_json_string), 48 | etag='test-etag', 49 | config_json_string=config_json_string, 50 | fetch_time=get_utc_now_seconds_since_epoch()).serialize() 51 | ) 52 | 53 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(), 54 | config_cache=config_cache, 55 | hooks=hooks)) 56 | 57 | self.assertEqual('test', client.get_value('testKey', 'default')) 58 | self.assertEqual(0, hook_callbacks.error_call_count) 59 | 60 | # Invalid fetch time in cache 61 | config_cache._value = '\n'.join(['text', 62 | 'test-etag', 63 | TEST_JSON_FORMAT.format(value_type=SettingType.STRING, value='{"s": "test2"}')]) 64 | 65 | self.assertEqual('test', client.get_value('testKey', 'default')) 66 | self.assertTrue('Error occurred while reading the cache.\nInvalid fetch time: text' in hook_callbacks.error) 67 | 68 | # Number of values is fewer than expected 69 | config_cache._value = '\n'.join([str(get_utc_now_seconds_since_epoch()), 70 | TEST_JSON_FORMAT.format(value_type=SettingType.STRING, value='{"s": "test2"}')]) 71 | 72 | self.assertEqual('test', client.get_value('testKey', 'default')) 73 | self.assertTrue('Error occurred while reading the cache.\nNumber of values is fewer than expected.' 74 | in hook_callbacks.error) 75 | 76 | # Invalid config JSON 77 | config_cache._value = '\n'.join([str(get_utc_now_seconds_since_epoch()), 78 | 'test-etag', 79 | 'wrong-json']) 80 | 81 | self.assertEqual('test', client.get_value('testKey', 'default')) 82 | self.assertTrue('Error occurred while reading the cache.\nInvalid config JSON: wrong-json.' 83 | in hook_callbacks.error) 84 | 85 | client.close() 86 | 87 | 88 | if __name__ == '__main__': 89 | unittest.main() 90 | -------------------------------------------------------------------------------- /configcatclienttests/test_configfetcher.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | import requests 4 | from configcatclient.configcatoptions import Hooks 5 | from configcatclient.logger import Logger 6 | 7 | from unittest import mock 8 | from unittest.mock import Mock 9 | 10 | from configcatclient.configfetcher import ConfigFetcher 11 | 12 | logging.basicConfig(level=logging.WARN) 13 | log = Logger('configcat', Hooks()) 14 | 15 | 16 | class ConfigFetcherTests(unittest.TestCase): 17 | def test_simple_fetch_success(self): 18 | with mock.patch.object(requests, 'get') as request_get: 19 | test_json = {"test": "json"} 20 | response_mock = Mock() 21 | request_get.return_value = response_mock 22 | response_mock.json.return_value = test_json 23 | response_mock.status_code = 200 24 | response_mock.headers = {} 25 | fetcher = ConfigFetcher(sdk_key='', log=log, mode='m') 26 | fetch_response = fetcher.get_configuration() 27 | self.assertTrue(fetch_response.is_fetched()) 28 | self.assertEqual(test_json, fetch_response.entry.config) 29 | 30 | def test_fetch_not_modified_etag(self): 31 | with mock.patch.object(requests, 'get') as request_get: 32 | etag = 'test' 33 | test_json = {"test": "json"} 34 | fetcher = ConfigFetcher(sdk_key='', log=log, mode='m') 35 | 36 | response_mock = Mock() 37 | response_mock.json.return_value = test_json 38 | response_mock.status_code = 200 39 | response_mock.headers = {'ETag': etag} 40 | 41 | request_get.return_value = response_mock 42 | fetch_response = fetcher.get_configuration() 43 | self.assertTrue(fetch_response.is_fetched()) 44 | self.assertEqual(test_json, fetch_response.entry.config) 45 | self.assertEqual(etag, fetch_response.entry.etag) 46 | 47 | response_not_modified_mock = Mock() 48 | response_not_modified_mock.json.return_value = {} 49 | response_not_modified_mock.status_code = 304 50 | response_not_modified_mock.headers = {'ETag': etag} 51 | 52 | request_get.return_value = response_not_modified_mock 53 | fetch_response = fetcher.get_configuration(etag) 54 | self.assertFalse(fetch_response.is_fetched()) 55 | 56 | args, kwargs = request_get.call_args 57 | request_headers = kwargs.get('headers') 58 | self.assertEqual(request_headers.get('If-None-Match'), etag) 59 | 60 | def test_http_error(self): 61 | with mock.patch.object(requests, 'get') as request_get: 62 | request_get.side_effect = requests.HTTPError("error") 63 | fetcher = ConfigFetcher(sdk_key='', log=log, mode='m') 64 | fetch_response = fetcher.get_configuration() 65 | self.assertTrue(fetch_response.is_failed()) 66 | self.assertTrue(fetch_response.is_transient_error) 67 | self.assertTrue(fetch_response.entry.is_empty()) 68 | 69 | def test_exception(self): 70 | with mock.patch.object(requests, 'get') as request_get: 71 | request_get.side_effect = Exception("error") 72 | fetcher = ConfigFetcher(sdk_key='', log=log, mode='m') 73 | fetch_response = fetcher.get_configuration() 74 | self.assertTrue(fetch_response.is_failed()) 75 | self.assertTrue(fetch_response.is_transient_error) 76 | self.assertTrue(fetch_response.entry.is_empty()) 77 | 78 | def test_404_failed_fetch_response(self): 79 | with mock.patch.object(requests, 'get') as request_get: 80 | response_mock = Mock() 81 | request_get.return_value = response_mock 82 | response_mock.json.return_value = {} 83 | response_mock.status_code = 404 84 | response_mock.headers = {} 85 | fetcher = ConfigFetcher(sdk_key='', log=log, mode='m') 86 | fetch_response = fetcher.get_configuration() 87 | self.assertTrue(fetch_response.is_failed()) 88 | self.assertFalse(fetch_response.is_transient_error) 89 | self.assertFalse(fetch_response.is_fetched()) 90 | self.assertTrue(fetch_response.entry.is_empty()) 91 | 92 | def test_403_failed_fetch_response(self): 93 | with mock.patch.object(requests, 'get') as request_get: 94 | response_mock = Mock() 95 | request_get.return_value = response_mock 96 | response_mock.json.return_value = {} 97 | response_mock.status_code = 403 98 | response_mock.headers = {} 99 | fetcher = ConfigFetcher(sdk_key='', log=log, mode='m') 100 | fetch_response = fetcher.get_configuration() 101 | self.assertTrue(fetch_response.is_failed()) 102 | self.assertFalse(fetch_response.is_transient_error) 103 | self.assertFalse(fetch_response.is_fetched()) 104 | self.assertTrue(fetch_response.entry.is_empty()) 105 | 106 | def test_server_side_etag(self): 107 | fetcher = ConfigFetcher(sdk_key='PKDVCLf-Hq-h-kCzMp-L7Q/HhOWfwVtZ0mb30i9wi17GQ', 108 | log=log, 109 | mode='m', base_url='https://cdn-eu.configcat.com') 110 | fetch_response = fetcher.get_configuration() 111 | etag = fetch_response.entry.etag 112 | self.assertIsNotNone(etag) 113 | self.assertNotEqual('', etag) 114 | self.assertTrue(fetch_response.is_fetched()) 115 | self.assertFalse(fetch_response.is_not_modified()) 116 | 117 | fetch_response = fetcher.get_configuration(etag) 118 | self.assertFalse(fetch_response.is_fetched()) 119 | self.assertTrue(fetch_response.is_not_modified()) 120 | 121 | fetch_response = fetcher.get_configuration('') 122 | self.assertTrue(fetch_response.is_fetched()) 123 | self.assertFalse(fetch_response.is_not_modified()) 124 | -------------------------------------------------------------------------------- /configcatclienttests/test_evaluationlog.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import unittest 5 | import re 6 | import sys 7 | 8 | from io import StringIO 9 | 10 | from configcatclient import ConfigCatClient, ConfigCatOptions, PollingMode 11 | from configcatclient.localfiledatasource import LocalFileFlagOverrides 12 | from configcatclient.overridedatasource import OverrideBehaviour 13 | from configcatclient.user import User 14 | from configcatclienttests.mocks import TEST_SDK_KEY 15 | 16 | logging.basicConfig(level=logging.INFO) 17 | 18 | 19 | class EvaluationLogTests(unittest.TestCase): 20 | def test_simple_value(self): 21 | self.assertTrue(self._test_evaluation_log('data/evaluation/simple_value.json')) 22 | 23 | def test_1_targeting_rule(self): 24 | self.assertTrue(self._test_evaluation_log('data/evaluation/1_targeting_rule.json')) 25 | 26 | def test_2_targeting_rules(self): 27 | self.assertTrue(self._test_evaluation_log('data/evaluation/2_targeting_rules.json')) 28 | 29 | def test_options_based_on_user_id(self): 30 | self.assertTrue(self._test_evaluation_log('data/evaluation/options_based_on_user_id.json')) 31 | 32 | def test_options_based_on_custom_attr(self): 33 | self.assertTrue(self._test_evaluation_log('data/evaluation/options_based_on_custom_attr.json')) 34 | 35 | def test_options_after_targeting_rule(self): 36 | self.assertTrue(self._test_evaluation_log('data/evaluation/options_after_targeting_rule.json')) 37 | 38 | def test_options_within_targeting_rule(self): 39 | self.assertTrue(self._test_evaluation_log('data/evaluation/options_within_targeting_rule.json')) 40 | 41 | def test_and_rules(self): 42 | self.assertTrue(self._test_evaluation_log('data/evaluation/and_rules.json')) 43 | 44 | def test_segment(self): 45 | self.assertTrue(self._test_evaluation_log('data/evaluation/segment.json')) 46 | 47 | def test_prerequisite_flag(self): 48 | self.assertTrue(self._test_evaluation_log('data/evaluation/prerequisite_flag.json')) 49 | 50 | def test_semver_validation(self): 51 | self.assertTrue(self._test_evaluation_log('data/evaluation/semver_validation.json')) 52 | 53 | def test_epoch_date_validation(self): 54 | self.assertTrue(self._test_evaluation_log('data/evaluation/epoch_date_validation.json')) 55 | 56 | def test_number_validation(self): 57 | self.assertTrue(self._test_evaluation_log('data/evaluation/number_validation.json')) 58 | 59 | def test_comparators_validation(self): 60 | self.maxDiff = None 61 | self.assertTrue(self._test_evaluation_log('data/evaluation/comparators.json')) 62 | 63 | def test_list_truncation_validation(self): 64 | self.assertTrue(self._test_evaluation_log('data/evaluation/list_truncation.json')) 65 | 66 | def _test_evaluation_log(self, file_path, test_filter=None, generate_expected_log=False): 67 | script_dir = os.path.dirname(__file__) 68 | file_path = os.path.join(script_dir, file_path) 69 | self.assertTrue(os.path.isfile(file_path)) 70 | name = os.path.basename(file_path)[:-5] 71 | file_dir = os.path.join(os.path.dirname(file_path), name) 72 | 73 | with open(file_path, 'r') as f: 74 | data = json.load(f) 75 | sdk_key = data.get('sdkKey') 76 | base_url = data.get('baseUrl') 77 | json_override = data.get('jsonOverride') 78 | flag_overrides = None 79 | if json_override: 80 | flag_overrides = LocalFileFlagOverrides( 81 | file_path=os.path.join(file_dir, json_override), 82 | override_behaviour=OverrideBehaviour.LocalOnly 83 | ) 84 | if not sdk_key: 85 | sdk_key = TEST_SDK_KEY 86 | 87 | client = ConfigCatClient.get(sdk_key, ConfigCatOptions( 88 | polling_mode=PollingMode.manual_poll(), 89 | flag_overrides=flag_overrides, 90 | base_url=base_url 91 | )) 92 | client.force_refresh() 93 | 94 | # setup logging 95 | log_stream = StringIO() 96 | log_handler = logging.StreamHandler(log_stream) 97 | log_handler.setFormatter(logging.Formatter('%(levelname)s %(message)s')) 98 | logger = logging.getLogger('configcat') 99 | logger.setLevel(logging.INFO) 100 | logger.addHandler(log_handler) 101 | 102 | for test in data['tests']: 103 | key = test.get('key') 104 | default_value = test.get('defaultValue') 105 | return_value = test.get('returnValue') 106 | user = test.get('user') 107 | expected_log_file = test.get('expectedLog') 108 | test_name = expected_log_file[:-4] 109 | 110 | # apply test filter 111 | if test_filter and test_name not in test_filter: 112 | continue 113 | 114 | expected_log_file_path = os.path.join(file_dir, expected_log_file) 115 | user_object = None 116 | if user: 117 | custom = {k: v for k, v in user.items() if k not in {'Identifier', 'Email', 'Country'}} 118 | if len(custom) == 0: 119 | custom = None 120 | user_object = User(user.get('Identifier'), user.get('Email'), user.get('Country'), custom) 121 | 122 | # clear log 123 | log_stream.seek(0) 124 | log_stream.truncate() 125 | 126 | value = client.get_value(key, default_value, user_object) 127 | log = log_stream.getvalue() 128 | 129 | if generate_expected_log: 130 | # create directory if needed 131 | if not os.path.exists(file_dir): 132 | os.makedirs(file_dir) 133 | 134 | with open(expected_log_file_path, 'w') as file: 135 | file.write(log) 136 | else: 137 | self.assertTrue(os.path.isfile(expected_log_file_path)) 138 | with open(expected_log_file_path, 'r') as file: 139 | expected_log = file.read() 140 | 141 | # On <= Python 3.5 the order of the keys in the serialized user object is random. 142 | # We need to cut out the JSON part and compare the JSON objects separately. 143 | if sys.version_info[:2] <= (3, 5): 144 | if expected_log.startswith('INFO [5000]') and log.startswith('INFO [5000]'): 145 | # Extract the JSON part from expected_log 146 | match = re.search(r'(\{.*?\})', expected_log) 147 | expected_log_json = None 148 | if match: 149 | expected_log_json = json.loads(match.group(1)) 150 | # Remove the JSON-like part from the original string 151 | expected_log = re.sub(r'\{.*?\}', '', expected_log) 152 | 153 | # Extract the JSON part from log 154 | log_json = None 155 | match = re.search(r'(\{.*?\})', log) 156 | if match: 157 | log_json = json.loads(match.group(1)) 158 | # Remove the JSON-like part from the original string 159 | log = re.sub(r'\{.*?\}', '', log) 160 | 161 | self.assertEqual(expected_log_json, log_json, 'User object mismatch for test: ' + test_name) 162 | 163 | self.assertEqual(expected_log, log, 'Log mismatch for test: ' + test_name) 164 | self.assertEqual(return_value, value, 'Return value mismatch for test: ' + test_name) 165 | 166 | client.close() 167 | return True 168 | 169 | return False 170 | 171 | 172 | ''' 173 | def test_generate_all_evaluation_logs(self): 174 | script_dir = os.path.dirname(__file__) 175 | file_path = os.path.join(script_dir, 'data/evaluation') 176 | self.assertTrue(os.path.isdir(file_path)) 177 | for file in os.listdir(file_path): 178 | if file.endswith('.json'): 179 | self._evaluation_log(os.path.join('data/evaluation', file), generate_expected_log=True) 180 | ''' 181 | 182 | 183 | if __name__ == '__main__': 184 | unittest.main() 185 | -------------------------------------------------------------------------------- /configcatclienttests/test_hooks.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import unittest 4 | import requests 5 | 6 | from configcatclient.configcatclient import ConfigCatClient 7 | from configcatclient.config import FEATURE_FLAGS, VALUE, SERVED_VALUE, STRING_VALUE, \ 8 | fixup_config_salt_and_segments 9 | from configcatclient.user import User 10 | from configcatclient.configcatoptions import ConfigCatOptions, Hooks 11 | from configcatclient.pollingmode import PollingMode 12 | from configcatclient.utils import get_utc_now 13 | from configcatclienttests.mocks import ConfigCacheMock, HookCallbacks, TEST_OBJECT, TEST_SDK_KEY 14 | 15 | from unittest import mock 16 | from unittest.mock import Mock 17 | 18 | logging.basicConfig(level=logging.INFO) 19 | 20 | 21 | class HooksTests(unittest.TestCase): 22 | 23 | def test_init(self): 24 | hook_callbacks = HookCallbacks() 25 | hooks = Hooks( 26 | on_client_ready=hook_callbacks.on_client_ready, 27 | on_config_changed=hook_callbacks.on_config_changed, 28 | on_flag_evaluated=hook_callbacks.on_flag_evaluated, 29 | on_error=hook_callbacks.on_error 30 | ) 31 | 32 | config_cache = ConfigCacheMock() 33 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(), 34 | config_cache=config_cache, 35 | hooks=hooks)) 36 | 37 | value = client.get_value('testStringKey', '') 38 | 39 | self.assertEqual('testValue', value) 40 | self.assertTrue(hook_callbacks.is_ready) 41 | self.assertEqual(1, hook_callbacks.is_ready_call_count) 42 | extended_config = TEST_OBJECT 43 | fixup_config_salt_and_segments(extended_config) 44 | self.assertEqual(extended_config.get(FEATURE_FLAGS), hook_callbacks.changed_config) 45 | self.assertEqual(1, hook_callbacks.changed_config_call_count) 46 | self.assertTrue(hook_callbacks.evaluation_details) 47 | self.assertEqual(1, hook_callbacks.evaluation_details_call_count) 48 | self.assertIsNone(hook_callbacks.error) 49 | self.assertEqual(0, hook_callbacks.error_call_count) 50 | 51 | client.close() 52 | 53 | def test_subscribe(self): 54 | hook_callbacks = HookCallbacks() 55 | hooks = Hooks() 56 | hooks.add_on_client_ready(hook_callbacks.on_client_ready) 57 | hooks.add_on_config_changed(hook_callbacks.on_config_changed) 58 | hooks.add_on_flag_evaluated(hook_callbacks.on_flag_evaluated) 59 | hooks.add_on_error(hook_callbacks.on_error) 60 | 61 | config_cache = ConfigCacheMock() 62 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(), 63 | config_cache=config_cache, 64 | hooks=hooks)) 65 | 66 | value = client.get_value('testStringKey', '') 67 | 68 | self.assertEqual('testValue', value) 69 | self.assertTrue(hook_callbacks.is_ready) 70 | self.assertEqual(1, hook_callbacks.is_ready_call_count) 71 | self.assertEqual(TEST_OBJECT.get(FEATURE_FLAGS), hook_callbacks.changed_config) 72 | self.assertEqual(1, hook_callbacks.changed_config_call_count) 73 | self.assertTrue(hook_callbacks.evaluation_details) 74 | self.assertEqual(1, hook_callbacks.evaluation_details_call_count) 75 | self.assertIsNone(hook_callbacks.error) 76 | self.assertEqual(0, hook_callbacks.error_call_count) 77 | 78 | client.close() 79 | 80 | def test_evaluation(self): 81 | with mock.patch.object(requests, 'get') as request_get: 82 | response_mock = Mock() 83 | request_get.return_value = response_mock 84 | response_mock.json.return_value = TEST_OBJECT 85 | response_mock.status_code = 200 86 | response_mock.headers = {} 87 | 88 | hook_callbacks = HookCallbacks() 89 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll())) 90 | 91 | client.get_hooks().add_on_flag_evaluated(hook_callbacks.on_flag_evaluated) 92 | 93 | client.force_refresh() 94 | 95 | user = User("test@test1.com") 96 | value = client.get_value('testStringKey', '', user) 97 | self.assertEqual('fake1', value) 98 | 99 | details = hook_callbacks.evaluation_details 100 | self.assertEqual('fake1', details.value) 101 | self.assertEqual('testStringKey', details.key) 102 | self.assertEqual('id1', details.variation_id) 103 | self.assertFalse(details.is_default_value) 104 | self.assertIsNone(details.error) 105 | self.assertIsNone(details.matched_percentage_option) 106 | self.assertEqual('fake1', details.matched_targeting_rule[SERVED_VALUE][VALUE][STRING_VALUE]) 107 | self.assertEqual(str(user), str(details.user)) 108 | now = get_utc_now() 109 | self.assertGreaterEqual(now, details.fetch_time) 110 | self.assertLessEqual(now, details.fetch_time + + datetime.timedelta(seconds=1)) 111 | 112 | client.close() 113 | 114 | def test_callback_exception(self): 115 | with mock.patch.object(requests, 'get') as request_get: 116 | response_mock = Mock() 117 | request_get.return_value = response_mock 118 | response_mock.json.return_value = TEST_OBJECT 119 | response_mock.status_code = 200 120 | response_mock.headers = {} 121 | 122 | hook_callbacks = HookCallbacks() 123 | hooks = Hooks( 124 | on_client_ready=hook_callbacks.callback_exception, 125 | on_config_changed=hook_callbacks.callback_exception, 126 | on_flag_evaluated=hook_callbacks.callback_exception, 127 | on_error=hook_callbacks.callback_exception 128 | ) 129 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(), 130 | hooks=hooks)) 131 | 132 | client.force_refresh() 133 | 134 | value = client.get_value('testStringKey', '') 135 | self.assertEqual('testValue', value) 136 | 137 | value = client.get_value('', 'default') 138 | self.assertEqual('default', value) 139 | 140 | 141 | if __name__ == '__main__': 142 | unittest.main() 143 | -------------------------------------------------------------------------------- /configcatclienttests/test_specialcharacter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import unittest 4 | 5 | import configcatclient 6 | from configcatclient.user import User 7 | 8 | logging.basicConfig(level=logging.INFO) 9 | 10 | _SDK_KEY = 'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/u28_1qNyZ0Wz-ldYHIU7-g' 11 | 12 | 13 | class SpecialCharacterTests(unittest.TestCase): 14 | def setUp(self): 15 | self.client = configcatclient.get(_SDK_KEY) 16 | 17 | def tearDown(self): 18 | self.client.close() 19 | 20 | def test_special_characters_works_cleartext(self): 21 | actual = self.client.get_value("specialCharacters", "NOT_CAT", User('äöüÄÖÜçéèñışğ⢙✓😀')) 22 | self.assertEqual(actual, 'äöüÄÖÜçéèñışğ⢙✓😀') 23 | 24 | def test_special_characters_works_hashed(self): 25 | actual = self.client.get_value("specialCharactersHashed", "NOT_CAT", User('äöüÄÖÜçéèñışğ⢙✓😀')) 26 | self.assertEqual(actual, 'äöüÄÖÜçéèñışğ⢙✓😀') 27 | 28 | 29 | if __name__ == '__main__': 30 | unittest.main() 31 | -------------------------------------------------------------------------------- /configcatclienttests/test_user.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | import json 4 | from datetime import datetime 5 | from configcatclient.user import User 6 | from datetime import timezone 7 | 8 | logging.basicConfig() 9 | 10 | 11 | class UserTests(unittest.TestCase): 12 | def test_empty_or_none_identifier(self): 13 | u1 = User(None) 14 | self.assertEqual('', u1.get_identifier()) 15 | u2 = User('') 16 | self.assertEqual('', u2.get_identifier()) 17 | 18 | def test_attribute_case_sensitivity(self): 19 | user_id = 'id' 20 | email = 'test@test.com' 21 | country = 'country' 22 | custom = {'custom': 'test'} 23 | user = User(identifier=user_id, email=email, country=country, custom=custom) 24 | 25 | self.assertEqual(user_id, user.get_identifier()) 26 | 27 | self.assertEqual(email, user.get_attribute('Email')) 28 | self.assertIsNone(user.get_attribute('EMAIL')) 29 | self.assertIsNone(user.get_attribute('email')) 30 | 31 | self.assertEqual(country, user.get_attribute('Country')) 32 | self.assertIsNone(user.get_attribute('COUNTRY')) 33 | self.assertIsNone(user.get_attribute('country')) 34 | 35 | self.assertEqual('test', user.get_attribute('custom')) 36 | self.assertIsNone(user.get_attribute('non-existing')) 37 | 38 | def test_to_str(self): 39 | user_id = 'id' 40 | email = 'test@test.com' 41 | country = 'country' 42 | custom = { 43 | 'string': 'test', 44 | 'datetime': datetime(2023, 9, 19, 11, 1, 35, 999000, tzinfo=timezone.utc), 45 | 'int': 42, 46 | 'float': 3.14 47 | } 48 | user = User(identifier=user_id, email=email, country=country, custom=custom) 49 | 50 | user_json = json.loads(str(user)) 51 | 52 | self.assertEqual(user_id, user_json['Identifier']) 53 | self.assertEqual(email, user_json['Email']) 54 | self.assertEqual(country, user_json['Country']) 55 | self.assertEqual('test', user_json['string']) 56 | self.assertEqual(42, user_json['int']) 57 | self.assertEqual(3.14, user_json['float']) 58 | self.assertEqual("2023-09-19T11:01:35.999000+00:00", user_json['datetime']) 59 | -------------------------------------------------------------------------------- /configcatclienttests/test_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | 4 | from configcatclient.utils import method_is_called_from 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | 8 | 9 | def no_operation(): 10 | pass 11 | 12 | 13 | def test_method_is_called_from(): 14 | pass 15 | 16 | 17 | class OtherClass(object): 18 | def no_operation(self): 19 | pass 20 | 21 | def test_method_is_called_from(self): 22 | pass 23 | 24 | 25 | class UtilsTests(unittest.TestCase): 26 | def no_operation(self): 27 | pass 28 | 29 | def test_method_is_called_from(self): 30 | class TestClass(object): 31 | @classmethod 32 | def class_method(cls, method): 33 | return method_is_called_from(method) 34 | 35 | def object_method(self, method): 36 | return method_is_called_from(method) 37 | 38 | self.assertTrue(TestClass.class_method(UtilsTests.test_method_is_called_from)) 39 | self.assertTrue(TestClass().object_method(UtilsTests.test_method_is_called_from)) 40 | 41 | self.assertFalse(TestClass.class_method(UtilsTests.no_operation)) 42 | self.assertFalse(TestClass().object_method(UtilsTests.no_operation)) 43 | 44 | self.assertFalse(TestClass.class_method(no_operation)) 45 | self.assertFalse(TestClass().object_method(test_method_is_called_from)) 46 | self.assertFalse(TestClass.class_method(OtherClass.no_operation)) 47 | self.assertFalse(TestClass().object_method(OtherClass.test_method_is_called_from)) 48 | 49 | 50 | if __name__ == '__main__': 51 | unittest.main() 52 | -------------------------------------------------------------------------------- /configcatclienttests/test_variation_id.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | 4 | from configcatclient.configcatclient import ConfigCatClient 5 | from configcatclienttests.mocks import ConfigCacheMock, TEST_SDK_KEY 6 | from configcatclient.configcatoptions import ConfigCatOptions 7 | from configcatclient.pollingmode import PollingMode 8 | 9 | logging.basicConfig(level=logging.INFO) 10 | 11 | 12 | class VariationIdTests(unittest.TestCase): 13 | def test_get_variation_id(self): 14 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(), 15 | config_cache=ConfigCacheMock())) 16 | self.assertEqual('id3', client.get_value_details('key1', None).variation_id) 17 | self.assertEqual('id4', client.get_value_details('key2', None).variation_id) 18 | client.close() 19 | 20 | def test_get_variation_id_not_found(self): 21 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(), 22 | config_cache=ConfigCacheMock())) 23 | self.assertEqual(None, client.get_value_details('nonexisting', 'default_value').variation_id) 24 | client.close() 25 | 26 | def test_get_variation_id_empty_config(self): 27 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(), 28 | config_cache=ConfigCacheMock())) 29 | self.assertEqual(None, client.get_value_details('nonexisting', 'default_value').variation_id) 30 | client.close() 31 | 32 | def test_get_key_and_value(self): 33 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(), 34 | config_cache=ConfigCacheMock())) 35 | result = client.get_key_and_value('id1') 36 | self.assertEqual('testStringKey', result.key) 37 | self.assertEqual('fake1', result.value) 38 | 39 | result = client.get_key_and_value('id2') 40 | self.assertEqual('testStringKey', result.key) 41 | self.assertEqual('fake2', result.value) 42 | 43 | result = client.get_key_and_value('id3') 44 | self.assertEqual('key1', result.key) 45 | self.assertTrue(result.value) 46 | 47 | result = client.get_key_and_value('id4') 48 | self.assertEqual('key2', result.key) 49 | self.assertEqual('fake4', result.value) 50 | 51 | result = client.get_key_and_value('id5') 52 | self.assertEqual('key2', result.key) 53 | self.assertEqual('fake5', result.value) 54 | 55 | result = client.get_key_and_value('id6') 56 | self.assertEqual('key2', result.key) 57 | self.assertEqual('fake6', result.value) 58 | 59 | result = client.get_key_and_value('id7') 60 | self.assertEqual('key2', result.key) 61 | self.assertEqual('fake7', result.value) 62 | 63 | result = client.get_key_and_value('id8') 64 | self.assertEqual('key2', result.key) 65 | self.assertEqual('fake8', result.value) 66 | 67 | client.close() 68 | 69 | def test_get_key_and_value_not_found(self): 70 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(), 71 | config_cache=ConfigCacheMock())) 72 | result = client.get_key_and_value('nonexisting') 73 | self.assertIsNone(result) 74 | client.close() 75 | 76 | def test_get_key_and_value_empty_config(self): 77 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll())) 78 | result = client.get_key_and_value('nonexisting') 79 | self.assertIsNone(result) 80 | client.close() 81 | 82 | 83 | if __name__ == '__main__': 84 | unittest.main() 85 | -------------------------------------------------------------------------------- /media/readme02-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/python-sdk/95e1f9f3a5bee4868299b25d8e974de7421ac8f5/media/readme02-3.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.18.4; python_version < "3.7" 2 | requests>=2.31.0; python_version >= "3.7" and python_version < "3.8" 3 | requests>=2.32.0; python_version >= "3.8" 4 | semver>=2.10.2 5 | enum-compat>=0.0.3 6 | qualname>=0.1.0 7 | -------------------------------------------------------------------------------- /samples/consolesample/README.md: -------------------------------------------------------------------------------- 1 | # ConfigCat Console Sample App 2 | 3 | To run the sample project you need [ConfigCatClient](https://pypi.org/project/configcat-client/) installed. 4 | ``` 5 | pip install configcat-client 6 | ``` 7 | 8 | ### Start sample: 9 | ``` 10 | python consolesample.py 11 | ``` 12 | or 13 | ``` 14 | python consolesample2.py 15 | ``` -------------------------------------------------------------------------------- /samples/consolesample/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/python-sdk/95e1f9f3a5bee4868299b25d8e974de7421ac8f5/samples/consolesample/__init__.py -------------------------------------------------------------------------------- /samples/consolesample/consolesample.py: -------------------------------------------------------------------------------- 1 | """ 2 | You should install the ConfigCat-Client package before using this sample project 3 | pip install configcat-client 4 | """ 5 | 6 | import configcatclient 7 | import logging 8 | from configcatclient.user import User 9 | 10 | # Info level logging helps to inspect the feature flag evaluation process. 11 | # Use the default warning level to avoid too detailed logging in your application. 12 | logging.basicConfig(level=logging.INFO) 13 | 14 | if __name__ == '__main__': 15 | # Initialize the ConfigCatClient with an SDK Key. 16 | client = configcatclient.get('configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/AG6C1ngVb0CvM07un6JisQ') 17 | 18 | # In the project there is a 'keySampleText' setting with the following rules: 19 | # 1. If the User's country is Hungary, the value should be 'Dog' 20 | # 2. If the User's custom property - SubscriptionType - is unlimited, the value should be 'Lion' 21 | # 3. In other cases there is a percentage rollout configured with 50% 'Falcon' and 50% 'Horse' rules. 22 | # 4. There is also a default value configured: 'Cat' 23 | 24 | # 1. As the passed User's country is Hungary this will print 'Dog' 25 | my_setting_value = client.get_value('keySampleText', 'default value', User('key', country='Hungary')) 26 | print("'keySampleText' value from ConfigCat: " + str(my_setting_value)) 27 | 28 | # 2. As the passed User's custom attribute - SubscriptionType - is unlimited this will print 'Lion' 29 | my_setting_value = client.get_value('keySampleText', 'default value', 30 | User('key', custom={'SubscriptionType': 'unlimited'})) 31 | print("'keySampleText' value from ConfigCat: " + str(my_setting_value)) 32 | 33 | # 3/a. As the passed User doesn't fill in any rules, this will serve 'Falcon' or 'Horse'. 34 | my_setting_value = client.get_value('keySampleText', 'default value', User('key')) 35 | print("'keySampleText' value from ConfigCat: " + str(my_setting_value)) 36 | 37 | # 3/b. As this is the same user from 3/a., this will print the same value as the previous one ('Falcon' or 'Horse') 38 | my_setting_value = client.get_value('keySampleText', 'default value', User('key')) 39 | print("'keySampleText' value from ConfigCat: " + str(my_setting_value)) 40 | 41 | # 4. As we don't pass a User object to this call, this will print the setting's default value - 'Cat' 42 | my_setting_value = client.get_value('keySampleText', 'default value') 43 | print("'keySampleText' value from ConfigCat: " + str(my_setting_value)) 44 | 45 | # 'myKeyNotExits' setting doesn't exist in the project configuration and the client returns default value ('N/A'); 46 | my_setting_not_exists = client.get_value('myKeyNotExists', 'N/A') 47 | print("'myKeyNotExists' value from ConfigCat: " + str(my_setting_not_exists)) 48 | 49 | client.close() 50 | -------------------------------------------------------------------------------- /samples/consolesample/consolesample2.py: -------------------------------------------------------------------------------- 1 | """ 2 | You should install the ConfigCat-Client package before using this sample project 3 | pip install configcat-client 4 | """ 5 | 6 | import configcatclient 7 | import logging 8 | from configcatclient.user import User 9 | 10 | # Info level logging helps to inspect the feature flag evaluation process. 11 | # Use the default warning level to avoid too detailed logging in your application. 12 | logging.basicConfig(level=logging.INFO) 13 | 14 | if __name__ == '__main__': 15 | # Initialize the ConfigCatClient with an SDK Key. 16 | client = configcatclient.get( 17 | 'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/tiOvFw5gkky9LFu1Duuvzw') 18 | 19 | # Creating a user object to identify your user (optional). 20 | userObject = User('Some UserID', email='configcat@example.com', custom={ 21 | 'version': '1.0.0'}) 22 | 23 | value = client.get_value( 24 | 'isPOCFeatureEnabled', False, userObject) 25 | print("'isPOCFeatureEnabled' value from ConfigCat: " + str(value)) 26 | 27 | client.close() 28 | -------------------------------------------------------------------------------- /samples/webappsample/README.md: -------------------------------------------------------------------------------- 1 | # ConfigCat Django Sample App 2 | 3 | To run the sample project you need [Django](https://www.djangoproject.com/) and [ConfigCatClient](https://pypi.org/project/configcat-client/) installed. 4 | ``` 5 | pip install Django 6 | pip install configcat-client 7 | ``` 8 | 9 | ### Start sample: 10 | 1. Apply migrations (Required for first time only) 11 | ``` 12 | python manage.py migrate 13 | ``` 14 | 2. Run sample app 15 | ``` 16 | python manage.py runserver 17 | ``` 18 | 19 | 3. Open browser at `http://127.0.0.1:8000/` 20 | -------------------------------------------------------------------------------- /samples/webappsample/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webappsample.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | 16 | execute_from_command_line(sys.argv) 17 | -------------------------------------------------------------------------------- /samples/webappsample/requirements.txt: -------------------------------------------------------------------------------- 1 | configcat-client>=5.0.0 2 | -------------------------------------------------------------------------------- /samples/webappsample/templates/index.html: -------------------------------------------------------------------------------- 1 |

2 | In the project there is a 'keySampleText' setting with the following rules:
3 | 1. If the User's country is Hungary, the value should be 'Dog'
4 | 2. If the User's custom property - SubscriptionType - is unlimited, the value should be 'Lion'
5 | 3. In other cases there is a percentage rollout configured with 50% 'Falcon' and 50% 'Horse' rules
6 | 4. There is also a default value configured: 'Cat'
7 |

8 |
9 |

10 | 1. As the passed User's country is Hungary this will return 'Dog': 11 | index1 12 |

13 |

14 | 2. As the passed User's custom attribute - SubscriptionType - is unlimited this will return 'Lion': 15 | index2 16 |

17 |

18 | 3/a. As the passed User doesn't fill in any rules, this will return 'Falcon' or 'Horse': 19 | index3a 20 |

21 |

22 | 3/b. As this is the same user from 3/a., this will return the same value as the previous one ('Falcon' or 'Horse'): 23 | index3b 24 |

25 |

26 | 4. As we don't pass an User object to this call, this will return the setting's default value - 'Cat': 27 | index4 28 |

29 |

30 | 5. 'myKeyNotExits' setting doesn't exist in the project configuration and the client returns default value ('N/A'): 31 | index5 32 |

33 | -------------------------------------------------------------------------------- /samples/webappsample/webapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/python-sdk/95e1f9f3a5bee4868299b25d8e974de7421ac8f5/samples/webappsample/webapp/__init__.py -------------------------------------------------------------------------------- /samples/webappsample/webapp/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /samples/webappsample/webapp/apps.py: -------------------------------------------------------------------------------- 1 | import configcatclient 2 | from django.apps import AppConfig 3 | 4 | 5 | class WebappConfig(AppConfig): 6 | name = 'webapp' 7 | configcat_client = configcatclient.get('PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A') 8 | -------------------------------------------------------------------------------- /samples/webappsample/webapp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/python-sdk/95e1f9f3a5bee4868299b25d8e974de7421ac8f5/samples/webappsample/webapp/migrations/__init__.py -------------------------------------------------------------------------------- /samples/webappsample/webapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /samples/webappsample/webapp/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /samples/webappsample/webapp/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | path('', views.index, name='index'), 7 | path('index1', views.index1, name='index1'), 8 | path('index2', views.index2, name='index2'), 9 | path('index3a', views.index3a, name='index3a'), 10 | path('index3b', views.index3b, name='index3b'), 11 | path('index4', views.index4, name='index4'), 12 | path('index5', views.index5, name='index5'), 13 | ] 14 | -------------------------------------------------------------------------------- /samples/webappsample/webapp/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from configcatclient.user import User 3 | from webapp.apps import WebappConfig 4 | from django.template import loader 5 | 6 | # In the project there is a 'keySampleText' setting with the following rules: 7 | # 1. If the User's country is Hungary, the value should be 'Dog' 8 | # 2. If the User's custom property - SubscriptionType - is unlimited, the value should be 'Lion' 9 | # 3. In other cases there is a percentage rollout configured with 50% 'Falcon' and 50% 'Horse' rules. 10 | # 4. There is also a default value configured: 'Cat' 11 | 12 | 13 | def index(request): 14 | template = loader.get_template('index.html') 15 | return HttpResponse(template.render(request=request)) 16 | 17 | 18 | # 1. As the passed User's country is Hungary this will return 'Dog'. 19 | def index1(request): 20 | my_setting_value = WebappConfig.configcat_client.get_value('keySampleText', 'default value', 21 | User('key', country='Hungary')) 22 | return HttpResponse("Hello, world. 'keySampleText' value from ConfigCat: " + str(my_setting_value)) 23 | 24 | 25 | # 2. As the passed User's custom attribute - SubscriptionType - is unlimited this will return 'Lion'. 26 | def index2(request): 27 | my_setting_value = WebappConfig.configcat_client.get_value('keySampleText', 'default value', 28 | User('key', custom={'SubscriptionType': 'unlimited'})) 29 | return HttpResponse("Hello, world. 'keySampleText' value from ConfigCat: " + str(my_setting_value)) 30 | 31 | 32 | # 3/a. As the passed User doesn't fill in any rules, this will return 'Falcon' or 'Horse'. 33 | def index3a(request): 34 | my_setting_value = WebappConfig.configcat_client.get_value('keySampleText', 'default value', User('key')) 35 | return HttpResponse("Hello, world. 'keySampleText' value from ConfigCat: " + str(my_setting_value)) 36 | 37 | 38 | # 3/b. As this is the same user from 3/a., this will return the same value as the previous one ('Falcon' or 'Horse'). 39 | def index3b(request): 40 | my_setting_value = WebappConfig.configcat_client.get_value('keySampleText', 'default value', User('key')) 41 | return HttpResponse("Hello, world. 'keySampleText' value from ConfigCat: " + str(my_setting_value)) 42 | 43 | 44 | # 4. As we don't pass an User object to this call, this will return the setting's default value - 'Cat'. 45 | def index4(request): 46 | my_setting_value = WebappConfig.configcat_client.get_value('keySampleText', 'default value') 47 | return HttpResponse("Hello, world. 'keySampleText' value from ConfigCat: " + str(my_setting_value)) 48 | 49 | 50 | # 5. 'myKeyNotExits' setting doesn't exist in the project configuration and the client returns default value ('N/A'); 51 | def index5(request): 52 | my_setting_value = WebappConfig.configcat_client.get_value('myKeyNotExits', 'N/A') 53 | return HttpResponse("Hello, world. 'keySampleText' value from ConfigCat: " + str(my_setting_value)) 54 | -------------------------------------------------------------------------------- /samples/webappsample/webappsample/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/configcat/python-sdk/95e1f9f3a5bee4868299b25d8e974de7421ac8f5/samples/webappsample/webappsample/__init__.py -------------------------------------------------------------------------------- /samples/webappsample/webappsample/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for webappsample project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '(xsxoxa&7%zsz95g*k%(6e+&d-9$$&c5%+a7wo+uqnhzw05z%h' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | ] 41 | 42 | MIDDLEWARE = [ 43 | 'django.middleware.security.SecurityMiddleware', 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | 'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.messages.middleware.MessageMiddleware', 49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 50 | ] 51 | 52 | ROOT_URLCONF = 'webappsample.urls' 53 | 54 | TEMPLATES = [ 55 | { 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': [ 58 | os.path.join(BASE_DIR, 'templates') 59 | ], 60 | 'APP_DIRS': True, 61 | 'OPTIONS': { 62 | 'context_processors': [ 63 | 'django.template.context_processors.debug', 64 | 'django.template.context_processors.request', 65 | 'django.contrib.auth.context_processors.auth', 66 | 'django.contrib.messages.context_processors.messages', 67 | ], 68 | }, 69 | }, 70 | ] 71 | 72 | WSGI_APPLICATION = 'webappsample.wsgi.application' 73 | 74 | 75 | # Database 76 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases 77 | 78 | DATABASES = { 79 | 'default': { 80 | 'ENGINE': 'django.db.backends.sqlite3', 81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 82 | } 83 | } 84 | 85 | 86 | # Password validation 87 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 88 | 89 | AUTH_PASSWORD_VALIDATORS = [ 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 92 | }, 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 101 | }, 102 | ] 103 | 104 | 105 | # Internationalization 106 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 107 | 108 | LANGUAGE_CODE = 'en-us' 109 | 110 | TIME_ZONE = 'UTC' 111 | 112 | USE_I18N = True 113 | 114 | USE_L10N = True 115 | 116 | USE_TZ = True 117 | 118 | 119 | # Static files (CSS, JavaScript, Images) 120 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 121 | 122 | STATIC_URL = '/static/' 123 | -------------------------------------------------------------------------------- /samples/webappsample/webappsample/urls.py: -------------------------------------------------------------------------------- 1 | """webappsample URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | path('', include('webapp.urls')) 22 | ] 23 | -------------------------------------------------------------------------------- /samples/webappsample/webappsample/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for webappsample project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webappsample.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [flake8] 5 | max-complexity = 10 6 | max-line-length = 127 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | def parse_requirements(filename): 5 | lines = (line.strip() for line in open(filename)) 6 | return [line for line in lines if line] 7 | 8 | 9 | configcatclient_version = '10.0.0' 10 | 11 | requirements = parse_requirements('requirements.txt') 12 | 13 | setup( 14 | name='configcat-client', 15 | version=configcatclient_version, 16 | packages=['configcatclient'], 17 | url='https://github.com/configcat/python-sdk', 18 | license='MIT', 19 | author='ConfigCat', 20 | author_email='developer@configcat.com', 21 | description='ConfigCat SDK for Python. https://configcat.com', 22 | long_description='Feature Flags created by developers for developers with <3. ConfigCat lets you manage ' 23 | 'feature flags across frontend, backend, mobile, and desktop apps without (re)deploying code. ' 24 | '% rollouts, user targeting, segmentation. Feature toggle SDKs for all main languages. ' 25 | 'Alternative to LaunchDarkly. ' 26 | 'Host yourself, or use the hosted management app at https://configcat.com.', 27 | install_requires=requirements, 28 | classifiers=[ 29 | 'Intended Audience :: Developers', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python :: 3', 33 | 'Programming Language :: Python :: 3.5', 34 | 'Programming Language :: Python :: 3.6', 35 | 'Programming Language :: Python :: 3.7', 36 | 'Programming Language :: Python :: 3.8', 37 | 'Programming Language :: Python :: 3.9', 38 | 'Programming Language :: Python :: 3.10', 39 | 'Programming Language :: Python :: 3.11', 40 | 'Programming Language :: Python :: 3.12', 41 | 'Topic :: Software Development', 42 | 'Topic :: Software Development :: Libraries', 43 | ], 44 | ) 45 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{35,36,37,38,39,310,311},lint 3 | passenv = LD_PRELOAD 4 | 5 | [testenv] 6 | deps = 7 | pytest 8 | pytest-cov 9 | parameterized 10 | commands = 11 | pytest --cov=configcatclient configcatclienttests 12 | 13 | [testenv:lint] 14 | deps = 15 | flake8 16 | commands = 17 | # Statical analysis 18 | flake8 configcatclient --count --show-source --statistics 19 | --------------------------------------------------------------------------------