├── configcatclient
├── py.typed
├── version.py
├── refreshresult.py
├── datagovernance.py
├── configcache.py
├── interfaces.py
├── evaluationcontext.py
├── __init__.py
├── logger.py
├── localdictionarydatasource.py
├── overridedatasource.py
├── evaluationdetails.py
├── configentry.py
├── pollingmode.py
├── evaluationlogbuilder.py
├── utils.py
├── localfiledatasource.py
├── user.py
├── configcatoptions.py
└── configservice.py
├── configcatclienttests
├── __init__.py
├── data
│ ├── evaluation
│ │ ├── simple_value
│ │ │ ├── int_setting.txt
│ │ │ ├── off_flag.txt
│ │ │ ├── on_flag.txt
│ │ │ ├── text_setting.txt
│ │ │ └── double_setting.txt
│ │ ├── 1_targeting_rule
│ │ │ ├── 1_rule_not_matching_targeted_attribute.txt
│ │ │ ├── 1_rule_matching_targeted_attribute.txt
│ │ │ ├── 1_rule_no_user.txt
│ │ │ └── 1_rule_no_targeted_attribute.txt
│ │ ├── list_truncation.json
│ │ ├── options_after_targeting_rule
│ │ │ ├── options_after_targeting_rule_matching_targeted_attribute.txt
│ │ │ ├── options_after_targeting_rule_not_matching_targeted_attribute.txt
│ │ │ ├── options_after_targeting_rule_no_user.txt
│ │ │ └── options_after_targeting_rule_no_targeted_attribute.txt
│ │ ├── options_within_targeting_rule
│ │ │ ├── options_within_targeting_rule_not_matching_targeted_attribute.txt
│ │ │ ├── options_within_targeting_rule_matching_targeted_attribute_options_attribute.txt
│ │ │ ├── options_within_targeting_rule_no_user.txt
│ │ │ ├── options_within_targeting_rule_no_targeted_attribute.txt
│ │ │ └── options_within_targeting_rule_matching_targeted_attribute_no_options_attribute.txt
│ │ ├── options_based_on_user_id
│ │ │ ├── options_user_attribute_user.txt
│ │ │ └── options_user_attribute_no_user.txt
│ │ ├── options_based_on_custom_attr
│ │ │ ├── matching_options_custom_attribute.txt
│ │ │ ├── no_options_custom_attribute.txt
│ │ │ └── options_custom_attribute_no_user.txt
│ │ ├── and_rules
│ │ │ ├── and_rules_user.txt
│ │ │ └── and_rules_no_user.txt
│ │ ├── number_validation.json
│ │ ├── segment
│ │ │ ├── segment_matching.txt
│ │ │ ├── segment_no_matching.txt
│ │ │ ├── segment_no_user.txt
│ │ │ ├── segment_no_user_multi_conditions.txt
│ │ │ └── segment_no_targeted_attribute.txt
│ │ ├── list_truncation
│ │ │ ├── list_truncation.txt
│ │ │ └── test_list_truncation.json
│ │ ├── epoch_date_validation.json
│ │ ├── number_validation
│ │ │ └── number_error.txt
│ │ ├── comparators.json
│ │ ├── options_based_on_user_id.json
│ │ ├── and_rules.json
│ │ ├── semver_validation.json
│ │ ├── 2_targeting_rules
│ │ │ ├── 2_rules_not_matching_targeted_attribute.txt
│ │ │ ├── 2_rules_matching_targeted_attribute.txt
│ │ │ ├── 2_rules_no_user.txt
│ │ │ └── 2_rules_no_targeted_attribute.txt
│ │ ├── epoch_date_validation
│ │ │ └── date_error.txt
│ │ ├── simple_value.json
│ │ ├── options_based_on_custom_attr.json
│ │ ├── prerequisite_flag
│ │ │ ├── prerequisite_flag_no_user_needed_by_dep.txt
│ │ │ ├── prerequisite_flag_multilevel.txt
│ │ │ ├── prerequisite_flag_no_user_needed_by_prereq.txt
│ │ │ ├── prerequisite_flag.txt
│ │ │ └── prerequisite_flag_no_user_needed_by_both.txt
│ │ ├── 2_targeting_rules.json
│ │ ├── 1_targeting_rule.json
│ │ ├── semver_validation
│ │ │ ├── semver_error.txt
│ │ │ └── semver_relations_error.txt
│ │ ├── options_after_targeting_rule.json
│ │ ├── prerequisite_flag.json
│ │ ├── segment.json
│ │ ├── options_within_targeting_rule.json
│ │ └── comparators
│ │ │ └── allinone.txt
│ ├── test-simple.json
│ ├── testmatrix_sensitive.csv
│ ├── test.json
│ ├── testmatrix_segments.csv
│ ├── testmatrix_variationId.csv
│ ├── testmatrix_number.csv
│ ├── testmatrix_segments_old.csv
│ ├── testmatrix_prerequisite_flag.csv
│ ├── test_override_flagdependency_v6.json
│ ├── testmatrix_and_or.csv
│ ├── test_override_segments_v6.json
│ ├── test_circulardependency_v6.json
│ ├── testmatrix_unicode.csv
│ ├── testmatrix_semantic_2.csv
│ ├── testmatrix_semantic.csv
│ └── testmatrix_comparators_v6.csv
├── test_specialcharacter.py
├── test_concurrency.py
├── test_utils.py
├── test_config.py
├── test_user.py
├── test_variation_id.py
├── test_configcache.py
├── test_configfetcher.py
├── test_hooks.py
├── mocks.py
└── test_evaluationlog.py
├── samples
├── consolesample
│ ├── __init__.py
│ ├── README.md
│ ├── consolesample2.py
│ └── consolesample.py
└── webappsample
│ ├── webapp
│ ├── __init__.py
│ ├── migrations
│ │ └── __init__.py
│ ├── models.py
│ ├── tests.py
│ ├── admin.py
│ ├── apps.py
│ ├── urls.py
│ └── views.py
│ ├── webappsample
│ ├── __init__.py
│ ├── wsgi.py
│ ├── urls.py
│ └── settings.py
│ ├── requirements.txt
│ ├── README.md
│ ├── manage.py
│ └── templates
│ └── index.html
├── .github
├── CODEOWNERS
├── dependabot.yml
└── workflows
│ ├── stale.yml
│ ├── publish.yml
│ └── python-ci.yml
├── .sonarcloud.properties
├── MANIFEST.in
├── media
└── readme02-3.png
├── setup.cfg
├── CHANGELOG.md
├── requirements.txt
├── tox.ini
├── DEPLOY.md
├── LICENSE.txt
├── LICENSE
├── .gitignore
├── setup.py
├── CONTRIBUTING.md
└── README.md
/configcatclient/py.typed:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/configcatclienttests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/samples/consolesample/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/samples/webappsample/webapp/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @configcat/developers
2 |
--------------------------------------------------------------------------------
/samples/webappsample/webappsample/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.sonarcloud.properties:
--------------------------------------------------------------------------------
1 | sonar.exclusions=samples/**
--------------------------------------------------------------------------------
/samples/webappsample/webapp/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/configcatclient/version.py:
--------------------------------------------------------------------------------
1 | CONFIGCATCLIENT_VERSION = "10.0.0"
2 |
--------------------------------------------------------------------------------
/samples/webappsample/requirements.txt:
--------------------------------------------------------------------------------
1 | configcat-client>=5.0.0
2 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include requirements.txt
2 | include LICENSE.txt
3 | include README.md
--------------------------------------------------------------------------------
/media/readme02-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/configcat/python-sdk/HEAD/media/readme02-3.png
--------------------------------------------------------------------------------
/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/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal=1
3 |
4 | [flake8]
5 | max-complexity = 10
6 | max-line-length = 127
7 |
--------------------------------------------------------------------------------
/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/evaluation/simple_value/double_setting.txt:
--------------------------------------------------------------------------------
1 | INFO [5000] Evaluating 'doubleDefaultPi'
2 | Returning '3.1415'.
3 |
--------------------------------------------------------------------------------
/configcatclient/refreshresult.py:
--------------------------------------------------------------------------------
1 | from collections import namedtuple
2 |
3 | RefreshResult = namedtuple('RefreshResult', 'is_success error')
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | - package-ecosystem: github-actions
5 | directory: /
6 | schedule:
7 | interval: weekly
8 | groups:
9 | all_dependencies:
10 | patterns:
11 | - "*"
12 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 | ```
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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_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/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_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/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/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/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/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/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/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_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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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@v5
17 | - name: Set up Python
18 | uses: actions/setup-python@v6
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 |
--------------------------------------------------------------------------------
/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_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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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(sdk_key, repeat=10, delay=0.1):
16 | client = configcatclient.get(sdk_key)
17 | for _ in range(repeat):
18 | client.force_refresh()
19 | sleep(delay)
20 |
21 |
22 | class ConcurrencyTests(unittest.TestCase):
23 |
24 | def test_concurrency_process(self):
25 | sdk_key = "PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A"
26 | client = configcatclient.get(sdk_key)
27 | value = client.get_value('keySampleText', False, User('key'))
28 | print("'keySampleText' value from ConfigCat: " + str(value))
29 |
30 | p1 = multiprocessing.Process(target=_manual_force_refresh, args=(sdk_key,))
31 | p2 = multiprocessing.Process(target=_manual_force_refresh, args=(sdk_key,))
32 | p1.start()
33 | p2.start()
34 | p1.join()
35 | p2.join()
36 |
37 | client.close()
38 |
39 | self.assertEqual(p1.exitcode, 0, "Process {0} exited with code {1}".format(p1.pid, p1.exitcode))
40 | self.assertEqual(p2.exitcode, 0, "Process {0} exited with code {1}".format(p2.pid, p2.exitcode))
41 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 | 'Programming Language :: Python :: 3.13',
42 | 'Programming Language :: Python :: 3.14',
43 | 'Topic :: Software Development',
44 | 'Topic :: Software Development :: Libraries',
45 | ],
46 | )
47 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 | fail-fast: false
20 | matrix:
21 | python-version:
22 | [
23 | "3.5",
24 | "3.6",
25 | "3.7",
26 | "3.8",
27 | "3.9",
28 | "3.10",
29 | "3.11",
30 | "3.12",
31 | "3.13",
32 | "3.14",
33 | ]
34 | os: [windows-latest, macos-latest, ubuntu-latest]
35 | exclude: # Python < v3.8 does not support Apple Silicon ARM64.
36 | - python-version: "3.5"
37 | os: macos-latest
38 | - python-version: "3.6"
39 | os: macos-latest
40 | - python-version: "3.7"
41 | os: macos-latest
42 |
43 | steps:
44 | - uses: actions/checkout@v5
45 |
46 | - name: Run tests in Docker for legacy Python
47 | if: matrix.os == 'ubuntu-latest' && contains(fromJSON('["3.5","3.6","3.7"]'), matrix.python-version)
48 | run: |
49 | docker run --rm -v ${{ github.workspace }}:/app -w /app python:${{ matrix.python-version }} bash -c "
50 | pip install --upgrade pip &&
51 | pip install pytest pytest-cov parameterized mock flake8 &&
52 | pip install -r requirements.txt &&
53 | flake8 configcatclient --count --show-source --statistics &&
54 | pytest configcatclienttests
55 | "
56 |
57 | - name: Set up Python ${{ matrix.python-version }}
58 | if: ${{ !(matrix.os == 'ubuntu-latest' && contains(fromJSON('["3.5","3.6","3.7"]'), matrix.python-version)) }}
59 | uses: actions/setup-python@v6
60 | with:
61 | python-version: ${{ matrix.python-version }}
62 | env:
63 | # Needed on Ubuntu for Python 3.5 build.
64 | PIP_TRUSTED_HOST: "pypi.python.org pypi.org files.pythonhosted.org"
65 |
66 | - name: Install dependencies
67 | if: ${{ !(matrix.os == 'ubuntu-latest' && contains(fromJSON('["3.5","3.6","3.7"]'), matrix.python-version)) }}
68 | run: |
69 | python -m pip install --upgrade pip
70 | pip install pytest pytest-cov parameterized mock flake8
71 | pip install -r requirements.txt
72 |
73 | - name: Lint with flake8
74 | if: ${{ !(matrix.os == 'ubuntu-latest' && contains(fromJSON('["3.5","3.6","3.7"]'), matrix.python-version)) }}
75 | run: |
76 | # Statical analysis
77 | flake8 configcatclient --count --show-source --statistics
78 |
79 | - name: Test
80 | if: ${{ !(matrix.os == 'ubuntu-latest' && contains(fromJSON('["3.5","3.6","3.7"]'), matrix.python-version)) }}
81 | run: pytest configcatclienttests
82 |
83 | coverage:
84 | needs: [test]
85 | runs-on: ubuntu-latest
86 | steps:
87 | - uses: actions/checkout@v5
88 |
89 | - name: Set up Python
90 | uses: actions/setup-python@v6
91 | with:
92 | python-version: "3.11"
93 |
94 | - name: Install dependencies
95 | run: |
96 | python -m pip install --upgrade pip
97 | pip install pytest pytest-cov parameterized mock flake8
98 | pip install -r requirements.txt
99 |
100 | - name: Lint with flake8
101 | run: |
102 | # Statical analysis
103 | flake8 configcatclient --count --show-source --statistics
104 |
105 | - name: Run coverage
106 | run: pytest --cov=configcatclient configcatclienttests
107 |
108 | - name: Upload coverage report
109 | uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
110 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | [](https://github.com/configcat/python-sdk/actions/workflows/python-ci.yml)
10 | [](https://codecov.io/gh/ConfigCat/python-sdk)
11 | [](https://pypi.python.org/pypi/configcat-client)
12 | [](https://pypi.python.org/pypi/configcat-client)
13 | [](https://snyk.io/test/github/configcat/python-sdk?targetFile=requirements.txt)
14 | [](https://sonarcloud.io/dashboard?id=configcat_python-sdk)
15 | 
16 |
17 | [](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 | 
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/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 |
--------------------------------------------------------------------------------
/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/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_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/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_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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------