"
47 |
48 |
49 | if __name__ == '__main__':
50 | unittest.main()
51 |
--------------------------------------------------------------------------------
/configcatclienttests/test_configcache.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import unittest
4 |
5 | from configcatclient import ConfigCatClient, ConfigCatOptions, PollingMode
6 | from configcatclient.config import SettingType
7 | from configcatclient.configcache import InMemoryConfigCache
8 | from configcatclient.configcatoptions import Hooks
9 | from configcatclient.configentry import ConfigEntry
10 | from configcatclient.configservice import ConfigService
11 | from configcatclient.utils import get_utc_now_seconds_since_epoch
12 | from configcatclienttests.mocks import TEST_JSON, SingleValueConfigCache, HookCallbacks, TEST_JSON_FORMAT, TEST_SDK_KEY
13 |
14 | logging.basicConfig()
15 |
16 |
17 | class ConfigCacheTests(unittest.TestCase):
18 |
19 | def test_cache(self):
20 | config_store = InMemoryConfigCache()
21 |
22 | value = config_store.get('key')
23 | self.assertEqual(value, None)
24 |
25 | config_store.set('key', TEST_JSON)
26 | value = config_store.get('key')
27 | self.assertEqual(value, TEST_JSON)
28 |
29 | value2 = config_store.get('key2')
30 | self.assertEqual(value2, None)
31 |
32 | def test_cache_key(self):
33 | self.assertEqual("f83ba5d45bceb4bb704410f51b704fb6dfa19942", ConfigService._get_cache_key('configcat-sdk-1/TEST_KEY-0123456789012/1234567890123456789012'))
34 | self.assertEqual("da7bfd8662209c8ed3f9db96daed4f8d91ba5876", ConfigService._get_cache_key('configcat-sdk-1/TEST_KEY2-123456789012/1234567890123456789012'))
35 |
36 | def test_cache_payload(self):
37 | now_seconds = 1686756435.8449
38 | etag = 'test-etag'
39 | entry = ConfigEntry(json.loads(TEST_JSON), etag, TEST_JSON, now_seconds)
40 | self.assertEqual('1686756435844' + '\n' + etag + '\n' + TEST_JSON, entry.serialize())
41 |
42 | def test_invalid_cache_content(self):
43 | hook_callbacks = HookCallbacks()
44 | hooks = Hooks(on_error=hook_callbacks.on_error)
45 | config_json_string = TEST_JSON_FORMAT.format(value_type=SettingType.STRING, value='{"s": "test"}')
46 | config_cache = SingleValueConfigCache(ConfigEntry(
47 | config=json.loads(config_json_string),
48 | etag='test-etag',
49 | config_json_string=config_json_string,
50 | fetch_time=get_utc_now_seconds_since_epoch()).serialize()
51 | )
52 |
53 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
54 | config_cache=config_cache,
55 | hooks=hooks))
56 |
57 | self.assertEqual('test', client.get_value('testKey', 'default'))
58 | self.assertEqual(0, hook_callbacks.error_call_count)
59 |
60 | # Invalid fetch time in cache
61 | config_cache._value = '\n'.join(['text',
62 | 'test-etag',
63 | TEST_JSON_FORMAT.format(value_type=SettingType.STRING, value='{"s": "test2"}')])
64 |
65 | self.assertEqual('test', client.get_value('testKey', 'default'))
66 | self.assertTrue('Error occurred while reading the cache.\nInvalid fetch time: text' in hook_callbacks.error)
67 |
68 | # Number of values is fewer than expected
69 | config_cache._value = '\n'.join([str(get_utc_now_seconds_since_epoch()),
70 | TEST_JSON_FORMAT.format(value_type=SettingType.STRING, value='{"s": "test2"}')])
71 |
72 | self.assertEqual('test', client.get_value('testKey', 'default'))
73 | self.assertTrue('Error occurred while reading the cache.\nNumber of values is fewer than expected.'
74 | in hook_callbacks.error)
75 |
76 | # Invalid config JSON
77 | config_cache._value = '\n'.join([str(get_utc_now_seconds_since_epoch()),
78 | 'test-etag',
79 | 'wrong-json'])
80 |
81 | self.assertEqual('test', client.get_value('testKey', 'default'))
82 | self.assertTrue('Error occurred while reading the cache.\nInvalid config JSON: wrong-json.'
83 | in hook_callbacks.error)
84 |
85 | client.close()
86 |
87 |
88 | if __name__ == '__main__':
89 | unittest.main()
90 |
--------------------------------------------------------------------------------
/configcatclienttests/test_configfetcher.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import unittest
3 | import requests
4 | from configcatclient.configcatoptions import Hooks
5 | from configcatclient.logger import Logger
6 |
7 | from unittest import mock
8 | from unittest.mock import Mock
9 |
10 | from configcatclient.configfetcher import ConfigFetcher
11 |
12 | logging.basicConfig(level=logging.WARN)
13 | log = Logger('configcat', Hooks())
14 |
15 |
16 | class ConfigFetcherTests(unittest.TestCase):
17 | def test_simple_fetch_success(self):
18 | with mock.patch.object(requests, 'get') as request_get:
19 | test_json = {"test": "json"}
20 | response_mock = Mock()
21 | request_get.return_value = response_mock
22 | response_mock.json.return_value = test_json
23 | response_mock.status_code = 200
24 | response_mock.headers = {}
25 | fetcher = ConfigFetcher(sdk_key='', log=log, mode='m')
26 | fetch_response = fetcher.get_configuration()
27 | self.assertTrue(fetch_response.is_fetched())
28 | self.assertEqual(test_json, fetch_response.entry.config)
29 |
30 | def test_fetch_not_modified_etag(self):
31 | with mock.patch.object(requests, 'get') as request_get:
32 | etag = 'test'
33 | test_json = {"test": "json"}
34 | fetcher = ConfigFetcher(sdk_key='', log=log, mode='m')
35 |
36 | response_mock = Mock()
37 | response_mock.json.return_value = test_json
38 | response_mock.status_code = 200
39 | response_mock.headers = {'ETag': etag}
40 |
41 | request_get.return_value = response_mock
42 | fetch_response = fetcher.get_configuration()
43 | self.assertTrue(fetch_response.is_fetched())
44 | self.assertEqual(test_json, fetch_response.entry.config)
45 | self.assertEqual(etag, fetch_response.entry.etag)
46 |
47 | response_not_modified_mock = Mock()
48 | response_not_modified_mock.json.return_value = {}
49 | response_not_modified_mock.status_code = 304
50 | response_not_modified_mock.headers = {'ETag': etag}
51 |
52 | request_get.return_value = response_not_modified_mock
53 | fetch_response = fetcher.get_configuration(etag)
54 | self.assertFalse(fetch_response.is_fetched())
55 |
56 | args, kwargs = request_get.call_args
57 | request_headers = kwargs.get('headers')
58 | self.assertEqual(request_headers.get('If-None-Match'), etag)
59 |
60 | def test_http_error(self):
61 | with mock.patch.object(requests, 'get') as request_get:
62 | request_get.side_effect = requests.HTTPError("error")
63 | fetcher = ConfigFetcher(sdk_key='', log=log, mode='m')
64 | fetch_response = fetcher.get_configuration()
65 | self.assertTrue(fetch_response.is_failed())
66 | self.assertTrue(fetch_response.is_transient_error)
67 | self.assertTrue(fetch_response.entry.is_empty())
68 |
69 | def test_exception(self):
70 | with mock.patch.object(requests, 'get') as request_get:
71 | request_get.side_effect = Exception("error")
72 | fetcher = ConfigFetcher(sdk_key='', log=log, mode='m')
73 | fetch_response = fetcher.get_configuration()
74 | self.assertTrue(fetch_response.is_failed())
75 | self.assertTrue(fetch_response.is_transient_error)
76 | self.assertTrue(fetch_response.entry.is_empty())
77 |
78 | def test_404_failed_fetch_response(self):
79 | with mock.patch.object(requests, 'get') as request_get:
80 | response_mock = Mock()
81 | request_get.return_value = response_mock
82 | response_mock.json.return_value = {}
83 | response_mock.status_code = 404
84 | response_mock.headers = {}
85 | fetcher = ConfigFetcher(sdk_key='', log=log, mode='m')
86 | fetch_response = fetcher.get_configuration()
87 | self.assertTrue(fetch_response.is_failed())
88 | self.assertFalse(fetch_response.is_transient_error)
89 | self.assertFalse(fetch_response.is_fetched())
90 | self.assertTrue(fetch_response.entry.is_empty())
91 |
92 | def test_403_failed_fetch_response(self):
93 | with mock.patch.object(requests, 'get') as request_get:
94 | response_mock = Mock()
95 | request_get.return_value = response_mock
96 | response_mock.json.return_value = {}
97 | response_mock.status_code = 403
98 | response_mock.headers = {}
99 | fetcher = ConfigFetcher(sdk_key='', log=log, mode='m')
100 | fetch_response = fetcher.get_configuration()
101 | self.assertTrue(fetch_response.is_failed())
102 | self.assertFalse(fetch_response.is_transient_error)
103 | self.assertFalse(fetch_response.is_fetched())
104 | self.assertTrue(fetch_response.entry.is_empty())
105 |
106 | def test_server_side_etag(self):
107 | fetcher = ConfigFetcher(sdk_key='PKDVCLf-Hq-h-kCzMp-L7Q/HhOWfwVtZ0mb30i9wi17GQ',
108 | log=log,
109 | mode='m', base_url='https://cdn-eu.configcat.com')
110 | fetch_response = fetcher.get_configuration()
111 | etag = fetch_response.entry.etag
112 | self.assertIsNotNone(etag)
113 | self.assertNotEqual('', etag)
114 | self.assertTrue(fetch_response.is_fetched())
115 | self.assertFalse(fetch_response.is_not_modified())
116 |
117 | fetch_response = fetcher.get_configuration(etag)
118 | self.assertFalse(fetch_response.is_fetched())
119 | self.assertTrue(fetch_response.is_not_modified())
120 |
121 | fetch_response = fetcher.get_configuration('')
122 | self.assertTrue(fetch_response.is_fetched())
123 | self.assertFalse(fetch_response.is_not_modified())
124 |
--------------------------------------------------------------------------------
/configcatclienttests/test_evaluationlog.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import os
4 | import unittest
5 | import re
6 | import sys
7 |
8 | from io import StringIO
9 |
10 | from configcatclient import ConfigCatClient, ConfigCatOptions, PollingMode
11 | from configcatclient.localfiledatasource import LocalFileFlagOverrides
12 | from configcatclient.overridedatasource import OverrideBehaviour
13 | from configcatclient.user import User
14 | from configcatclienttests.mocks import TEST_SDK_KEY
15 |
16 | logging.basicConfig(level=logging.INFO)
17 |
18 |
19 | class EvaluationLogTests(unittest.TestCase):
20 | def test_simple_value(self):
21 | self.assertTrue(self._test_evaluation_log('data/evaluation/simple_value.json'))
22 |
23 | def test_1_targeting_rule(self):
24 | self.assertTrue(self._test_evaluation_log('data/evaluation/1_targeting_rule.json'))
25 |
26 | def test_2_targeting_rules(self):
27 | self.assertTrue(self._test_evaluation_log('data/evaluation/2_targeting_rules.json'))
28 |
29 | def test_options_based_on_user_id(self):
30 | self.assertTrue(self._test_evaluation_log('data/evaluation/options_based_on_user_id.json'))
31 |
32 | def test_options_based_on_custom_attr(self):
33 | self.assertTrue(self._test_evaluation_log('data/evaluation/options_based_on_custom_attr.json'))
34 |
35 | def test_options_after_targeting_rule(self):
36 | self.assertTrue(self._test_evaluation_log('data/evaluation/options_after_targeting_rule.json'))
37 |
38 | def test_options_within_targeting_rule(self):
39 | self.assertTrue(self._test_evaluation_log('data/evaluation/options_within_targeting_rule.json'))
40 |
41 | def test_and_rules(self):
42 | self.assertTrue(self._test_evaluation_log('data/evaluation/and_rules.json'))
43 |
44 | def test_segment(self):
45 | self.assertTrue(self._test_evaluation_log('data/evaluation/segment.json'))
46 |
47 | def test_prerequisite_flag(self):
48 | self.assertTrue(self._test_evaluation_log('data/evaluation/prerequisite_flag.json'))
49 |
50 | def test_semver_validation(self):
51 | self.assertTrue(self._test_evaluation_log('data/evaluation/semver_validation.json'))
52 |
53 | def test_epoch_date_validation(self):
54 | self.assertTrue(self._test_evaluation_log('data/evaluation/epoch_date_validation.json'))
55 |
56 | def test_number_validation(self):
57 | self.assertTrue(self._test_evaluation_log('data/evaluation/number_validation.json'))
58 |
59 | def test_comparators_validation(self):
60 | self.maxDiff = None
61 | self.assertTrue(self._test_evaluation_log('data/evaluation/comparators.json'))
62 |
63 | def test_list_truncation_validation(self):
64 | self.assertTrue(self._test_evaluation_log('data/evaluation/list_truncation.json'))
65 |
66 | def _test_evaluation_log(self, file_path, test_filter=None, generate_expected_log=False):
67 | script_dir = os.path.dirname(__file__)
68 | file_path = os.path.join(script_dir, file_path)
69 | self.assertTrue(os.path.isfile(file_path))
70 | name = os.path.basename(file_path)[:-5]
71 | file_dir = os.path.join(os.path.dirname(file_path), name)
72 |
73 | with open(file_path, 'r') as f:
74 | data = json.load(f)
75 | sdk_key = data.get('sdkKey')
76 | base_url = data.get('baseUrl')
77 | json_override = data.get('jsonOverride')
78 | flag_overrides = None
79 | if json_override:
80 | flag_overrides = LocalFileFlagOverrides(
81 | file_path=os.path.join(file_dir, json_override),
82 | override_behaviour=OverrideBehaviour.LocalOnly
83 | )
84 | if not sdk_key:
85 | sdk_key = TEST_SDK_KEY
86 |
87 | client = ConfigCatClient.get(sdk_key, ConfigCatOptions(
88 | polling_mode=PollingMode.manual_poll(),
89 | flag_overrides=flag_overrides,
90 | base_url=base_url
91 | ))
92 | client.force_refresh()
93 |
94 | # setup logging
95 | log_stream = StringIO()
96 | log_handler = logging.StreamHandler(log_stream)
97 | log_handler.setFormatter(logging.Formatter('%(levelname)s %(message)s'))
98 | logger = logging.getLogger('configcat')
99 | logger.setLevel(logging.INFO)
100 | logger.addHandler(log_handler)
101 |
102 | for test in data['tests']:
103 | key = test.get('key')
104 | default_value = test.get('defaultValue')
105 | return_value = test.get('returnValue')
106 | user = test.get('user')
107 | expected_log_file = test.get('expectedLog')
108 | test_name = expected_log_file[:-4]
109 |
110 | # apply test filter
111 | if test_filter and test_name not in test_filter:
112 | continue
113 |
114 | expected_log_file_path = os.path.join(file_dir, expected_log_file)
115 | user_object = None
116 | if user:
117 | custom = {k: v for k, v in user.items() if k not in {'Identifier', 'Email', 'Country'}}
118 | if len(custom) == 0:
119 | custom = None
120 | user_object = User(user.get('Identifier'), user.get('Email'), user.get('Country'), custom)
121 |
122 | # clear log
123 | log_stream.seek(0)
124 | log_stream.truncate()
125 |
126 | value = client.get_value(key, default_value, user_object)
127 | log = log_stream.getvalue()
128 |
129 | if generate_expected_log:
130 | # create directory if needed
131 | if not os.path.exists(file_dir):
132 | os.makedirs(file_dir)
133 |
134 | with open(expected_log_file_path, 'w') as file:
135 | file.write(log)
136 | else:
137 | self.assertTrue(os.path.isfile(expected_log_file_path))
138 | with open(expected_log_file_path, 'r') as file:
139 | expected_log = file.read()
140 |
141 | # On <= Python 3.5 the order of the keys in the serialized user object is random.
142 | # We need to cut out the JSON part and compare the JSON objects separately.
143 | if sys.version_info[:2] <= (3, 5):
144 | if expected_log.startswith('INFO [5000]') and log.startswith('INFO [5000]'):
145 | # Extract the JSON part from expected_log
146 | match = re.search(r'(\{.*?\})', expected_log)
147 | expected_log_json = None
148 | if match:
149 | expected_log_json = json.loads(match.group(1))
150 | # Remove the JSON-like part from the original string
151 | expected_log = re.sub(r'\{.*?\}', '', expected_log)
152 |
153 | # Extract the JSON part from log
154 | log_json = None
155 | match = re.search(r'(\{.*?\})', log)
156 | if match:
157 | log_json = json.loads(match.group(1))
158 | # Remove the JSON-like part from the original string
159 | log = re.sub(r'\{.*?\}', '', log)
160 |
161 | self.assertEqual(expected_log_json, log_json, 'User object mismatch for test: ' + test_name)
162 |
163 | self.assertEqual(expected_log, log, 'Log mismatch for test: ' + test_name)
164 | self.assertEqual(return_value, value, 'Return value mismatch for test: ' + test_name)
165 |
166 | client.close()
167 | return True
168 |
169 | return False
170 |
171 |
172 | '''
173 | def test_generate_all_evaluation_logs(self):
174 | script_dir = os.path.dirname(__file__)
175 | file_path = os.path.join(script_dir, 'data/evaluation')
176 | self.assertTrue(os.path.isdir(file_path))
177 | for file in os.listdir(file_path):
178 | if file.endswith('.json'):
179 | self._evaluation_log(os.path.join('data/evaluation', file), generate_expected_log=True)
180 | '''
181 |
182 |
183 | if __name__ == '__main__':
184 | unittest.main()
185 |
--------------------------------------------------------------------------------
/configcatclienttests/test_hooks.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import logging
3 | import unittest
4 | import requests
5 |
6 | from configcatclient.configcatclient import ConfigCatClient
7 | from configcatclient.config import FEATURE_FLAGS, VALUE, SERVED_VALUE, STRING_VALUE, \
8 | fixup_config_salt_and_segments
9 | from configcatclient.user import User
10 | from configcatclient.configcatoptions import ConfigCatOptions, Hooks
11 | from configcatclient.pollingmode import PollingMode
12 | from configcatclient.utils import get_utc_now
13 | from configcatclienttests.mocks import ConfigCacheMock, HookCallbacks, TEST_OBJECT, TEST_SDK_KEY
14 |
15 | from unittest import mock
16 | from unittest.mock import Mock
17 |
18 | logging.basicConfig(level=logging.INFO)
19 |
20 |
21 | class HooksTests(unittest.TestCase):
22 |
23 | def test_init(self):
24 | hook_callbacks = HookCallbacks()
25 | hooks = Hooks(
26 | on_client_ready=hook_callbacks.on_client_ready,
27 | on_config_changed=hook_callbacks.on_config_changed,
28 | on_flag_evaluated=hook_callbacks.on_flag_evaluated,
29 | on_error=hook_callbacks.on_error
30 | )
31 |
32 | config_cache = ConfigCacheMock()
33 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
34 | config_cache=config_cache,
35 | hooks=hooks))
36 |
37 | value = client.get_value('testStringKey', '')
38 |
39 | self.assertEqual('testValue', value)
40 | self.assertTrue(hook_callbacks.is_ready)
41 | self.assertEqual(1, hook_callbacks.is_ready_call_count)
42 | extended_config = TEST_OBJECT
43 | fixup_config_salt_and_segments(extended_config)
44 | self.assertEqual(extended_config.get(FEATURE_FLAGS), hook_callbacks.changed_config)
45 | self.assertEqual(1, hook_callbacks.changed_config_call_count)
46 | self.assertTrue(hook_callbacks.evaluation_details)
47 | self.assertEqual(1, hook_callbacks.evaluation_details_call_count)
48 | self.assertIsNone(hook_callbacks.error)
49 | self.assertEqual(0, hook_callbacks.error_call_count)
50 |
51 | client.close()
52 |
53 | def test_subscribe(self):
54 | hook_callbacks = HookCallbacks()
55 | hooks = Hooks()
56 | hooks.add_on_client_ready(hook_callbacks.on_client_ready)
57 | hooks.add_on_config_changed(hook_callbacks.on_config_changed)
58 | hooks.add_on_flag_evaluated(hook_callbacks.on_flag_evaluated)
59 | hooks.add_on_error(hook_callbacks.on_error)
60 |
61 | config_cache = ConfigCacheMock()
62 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
63 | config_cache=config_cache,
64 | hooks=hooks))
65 |
66 | value = client.get_value('testStringKey', '')
67 |
68 | self.assertEqual('testValue', value)
69 | self.assertTrue(hook_callbacks.is_ready)
70 | self.assertEqual(1, hook_callbacks.is_ready_call_count)
71 | self.assertEqual(TEST_OBJECT.get(FEATURE_FLAGS), hook_callbacks.changed_config)
72 | self.assertEqual(1, hook_callbacks.changed_config_call_count)
73 | self.assertTrue(hook_callbacks.evaluation_details)
74 | self.assertEqual(1, hook_callbacks.evaluation_details_call_count)
75 | self.assertIsNone(hook_callbacks.error)
76 | self.assertEqual(0, hook_callbacks.error_call_count)
77 |
78 | client.close()
79 |
80 | def test_evaluation(self):
81 | with mock.patch.object(requests, 'get') as request_get:
82 | response_mock = Mock()
83 | request_get.return_value = response_mock
84 | response_mock.json.return_value = TEST_OBJECT
85 | response_mock.status_code = 200
86 | response_mock.headers = {}
87 |
88 | hook_callbacks = HookCallbacks()
89 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
90 |
91 | client.get_hooks().add_on_flag_evaluated(hook_callbacks.on_flag_evaluated)
92 |
93 | client.force_refresh()
94 |
95 | user = User("test@test1.com")
96 | value = client.get_value('testStringKey', '', user)
97 | self.assertEqual('fake1', value)
98 |
99 | details = hook_callbacks.evaluation_details
100 | self.assertEqual('fake1', details.value)
101 | self.assertEqual('testStringKey', details.key)
102 | self.assertEqual('id1', details.variation_id)
103 | self.assertFalse(details.is_default_value)
104 | self.assertIsNone(details.error)
105 | self.assertIsNone(details.matched_percentage_option)
106 | self.assertEqual('fake1', details.matched_targeting_rule[SERVED_VALUE][VALUE][STRING_VALUE])
107 | self.assertEqual(str(user), str(details.user))
108 | now = get_utc_now()
109 | self.assertGreaterEqual(now, details.fetch_time)
110 | self.assertLessEqual(now, details.fetch_time + + datetime.timedelta(seconds=1))
111 |
112 | client.close()
113 |
114 | def test_callback_exception(self):
115 | with mock.patch.object(requests, 'get') as request_get:
116 | response_mock = Mock()
117 | request_get.return_value = response_mock
118 | response_mock.json.return_value = TEST_OBJECT
119 | response_mock.status_code = 200
120 | response_mock.headers = {}
121 |
122 | hook_callbacks = HookCallbacks()
123 | hooks = Hooks(
124 | on_client_ready=hook_callbacks.callback_exception,
125 | on_config_changed=hook_callbacks.callback_exception,
126 | on_flag_evaluated=hook_callbacks.callback_exception,
127 | on_error=hook_callbacks.callback_exception
128 | )
129 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
130 | hooks=hooks))
131 |
132 | client.force_refresh()
133 |
134 | value = client.get_value('testStringKey', '')
135 | self.assertEqual('testValue', value)
136 |
137 | value = client.get_value('', 'default')
138 | self.assertEqual('default', value)
139 |
140 |
141 | if __name__ == '__main__':
142 | unittest.main()
143 |
--------------------------------------------------------------------------------
/configcatclienttests/test_specialcharacter.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | import logging
3 | import unittest
4 |
5 | import configcatclient
6 | from configcatclient.user import User
7 |
8 | logging.basicConfig(level=logging.INFO)
9 |
10 | _SDK_KEY = 'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/u28_1qNyZ0Wz-ldYHIU7-g'
11 |
12 |
13 | class SpecialCharacterTests(unittest.TestCase):
14 | def setUp(self):
15 | self.client = configcatclient.get(_SDK_KEY)
16 |
17 | def tearDown(self):
18 | self.client.close()
19 |
20 | def test_special_characters_works_cleartext(self):
21 | actual = self.client.get_value("specialCharacters", "NOT_CAT", User('äöüÄÖÜçéèñışğ⢙✓😀'))
22 | self.assertEqual(actual, 'äöüÄÖÜçéèñışğ⢙✓😀')
23 |
24 | def test_special_characters_works_hashed(self):
25 | actual = self.client.get_value("specialCharactersHashed", "NOT_CAT", User('äöüÄÖÜçéèñışğ⢙✓😀'))
26 | self.assertEqual(actual, 'äöüÄÖÜçéèñışğ⢙✓😀')
27 |
28 |
29 | if __name__ == '__main__':
30 | unittest.main()
31 |
--------------------------------------------------------------------------------
/configcatclienttests/test_user.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import unittest
3 | import json
4 | from datetime import datetime
5 | from configcatclient.user import User
6 | from datetime import timezone
7 |
8 | logging.basicConfig()
9 |
10 |
11 | class UserTests(unittest.TestCase):
12 | def test_empty_or_none_identifier(self):
13 | u1 = User(None)
14 | self.assertEqual('', u1.get_identifier())
15 | u2 = User('')
16 | self.assertEqual('', u2.get_identifier())
17 |
18 | def test_attribute_case_sensitivity(self):
19 | user_id = 'id'
20 | email = 'test@test.com'
21 | country = 'country'
22 | custom = {'custom': 'test'}
23 | user = User(identifier=user_id, email=email, country=country, custom=custom)
24 |
25 | self.assertEqual(user_id, user.get_identifier())
26 |
27 | self.assertEqual(email, user.get_attribute('Email'))
28 | self.assertIsNone(user.get_attribute('EMAIL'))
29 | self.assertIsNone(user.get_attribute('email'))
30 |
31 | self.assertEqual(country, user.get_attribute('Country'))
32 | self.assertIsNone(user.get_attribute('COUNTRY'))
33 | self.assertIsNone(user.get_attribute('country'))
34 |
35 | self.assertEqual('test', user.get_attribute('custom'))
36 | self.assertIsNone(user.get_attribute('non-existing'))
37 |
38 | def test_to_str(self):
39 | user_id = 'id'
40 | email = 'test@test.com'
41 | country = 'country'
42 | custom = {
43 | 'string': 'test',
44 | 'datetime': datetime(2023, 9, 19, 11, 1, 35, 999000, tzinfo=timezone.utc),
45 | 'int': 42,
46 | 'float': 3.14
47 | }
48 | user = User(identifier=user_id, email=email, country=country, custom=custom)
49 |
50 | user_json = json.loads(str(user))
51 |
52 | self.assertEqual(user_id, user_json['Identifier'])
53 | self.assertEqual(email, user_json['Email'])
54 | self.assertEqual(country, user_json['Country'])
55 | self.assertEqual('test', user_json['string'])
56 | self.assertEqual(42, user_json['int'])
57 | self.assertEqual(3.14, user_json['float'])
58 | self.assertEqual("2023-09-19T11:01:35.999000+00:00", user_json['datetime'])
59 |
--------------------------------------------------------------------------------
/configcatclienttests/test_utils.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import unittest
3 |
4 | from configcatclient.utils import method_is_called_from
5 |
6 | logging.basicConfig(level=logging.INFO)
7 |
8 |
9 | def no_operation():
10 | pass
11 |
12 |
13 | def test_method_is_called_from():
14 | pass
15 |
16 |
17 | class OtherClass(object):
18 | def no_operation(self):
19 | pass
20 |
21 | def test_method_is_called_from(self):
22 | pass
23 |
24 |
25 | class UtilsTests(unittest.TestCase):
26 | def no_operation(self):
27 | pass
28 |
29 | def test_method_is_called_from(self):
30 | class TestClass(object):
31 | @classmethod
32 | def class_method(cls, method):
33 | return method_is_called_from(method)
34 |
35 | def object_method(self, method):
36 | return method_is_called_from(method)
37 |
38 | self.assertTrue(TestClass.class_method(UtilsTests.test_method_is_called_from))
39 | self.assertTrue(TestClass().object_method(UtilsTests.test_method_is_called_from))
40 |
41 | self.assertFalse(TestClass.class_method(UtilsTests.no_operation))
42 | self.assertFalse(TestClass().object_method(UtilsTests.no_operation))
43 |
44 | self.assertFalse(TestClass.class_method(no_operation))
45 | self.assertFalse(TestClass().object_method(test_method_is_called_from))
46 | self.assertFalse(TestClass.class_method(OtherClass.no_operation))
47 | self.assertFalse(TestClass().object_method(OtherClass.test_method_is_called_from))
48 |
49 |
50 | if __name__ == '__main__':
51 | unittest.main()
52 |
--------------------------------------------------------------------------------
/configcatclienttests/test_variation_id.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import unittest
3 |
4 | from configcatclient.configcatclient import ConfigCatClient
5 | from configcatclienttests.mocks import ConfigCacheMock, TEST_SDK_KEY
6 | from configcatclient.configcatoptions import ConfigCatOptions
7 | from configcatclient.pollingmode import PollingMode
8 |
9 | logging.basicConfig(level=logging.INFO)
10 |
11 |
12 | class VariationIdTests(unittest.TestCase):
13 | def test_get_variation_id(self):
14 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
15 | config_cache=ConfigCacheMock()))
16 | self.assertEqual('id3', client.get_value_details('key1', None).variation_id)
17 | self.assertEqual('id4', client.get_value_details('key2', None).variation_id)
18 | client.close()
19 |
20 | def test_get_variation_id_not_found(self):
21 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
22 | config_cache=ConfigCacheMock()))
23 | self.assertEqual(None, client.get_value_details('nonexisting', 'default_value').variation_id)
24 | client.close()
25 |
26 | def test_get_variation_id_empty_config(self):
27 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
28 | config_cache=ConfigCacheMock()))
29 | self.assertEqual(None, client.get_value_details('nonexisting', 'default_value').variation_id)
30 | client.close()
31 |
32 | def test_get_key_and_value(self):
33 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
34 | config_cache=ConfigCacheMock()))
35 | result = client.get_key_and_value('id1')
36 | self.assertEqual('testStringKey', result.key)
37 | self.assertEqual('fake1', result.value)
38 |
39 | result = client.get_key_and_value('id2')
40 | self.assertEqual('testStringKey', result.key)
41 | self.assertEqual('fake2', result.value)
42 |
43 | result = client.get_key_and_value('id3')
44 | self.assertEqual('key1', result.key)
45 | self.assertTrue(result.value)
46 |
47 | result = client.get_key_and_value('id4')
48 | self.assertEqual('key2', result.key)
49 | self.assertEqual('fake4', result.value)
50 |
51 | result = client.get_key_and_value('id5')
52 | self.assertEqual('key2', result.key)
53 | self.assertEqual('fake5', result.value)
54 |
55 | result = client.get_key_and_value('id6')
56 | self.assertEqual('key2', result.key)
57 | self.assertEqual('fake6', result.value)
58 |
59 | result = client.get_key_and_value('id7')
60 | self.assertEqual('key2', result.key)
61 | self.assertEqual('fake7', result.value)
62 |
63 | result = client.get_key_and_value('id8')
64 | self.assertEqual('key2', result.key)
65 | self.assertEqual('fake8', result.value)
66 |
67 | client.close()
68 |
69 | def test_get_key_and_value_not_found(self):
70 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll(),
71 | config_cache=ConfigCacheMock()))
72 | result = client.get_key_and_value('nonexisting')
73 | self.assertIsNone(result)
74 | client.close()
75 |
76 | def test_get_key_and_value_empty_config(self):
77 | client = ConfigCatClient.get(TEST_SDK_KEY, ConfigCatOptions(polling_mode=PollingMode.manual_poll()))
78 | result = client.get_key_and_value('nonexisting')
79 | self.assertIsNone(result)
80 | client.close()
81 |
82 |
83 | if __name__ == '__main__':
84 | unittest.main()
85 |
--------------------------------------------------------------------------------
/media/readme02-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/configcat/python-sdk/95e1f9f3a5bee4868299b25d8e974de7421ac8f5/media/readme02-3.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | requests>=2.18.4; python_version < "3.7"
2 | requests>=2.31.0; python_version >= "3.7" and python_version < "3.8"
3 | requests>=2.32.0; python_version >= "3.8"
4 | semver>=2.10.2
5 | enum-compat>=0.0.3
6 | qualname>=0.1.0
7 |
--------------------------------------------------------------------------------
/samples/consolesample/README.md:
--------------------------------------------------------------------------------
1 | # ConfigCat Console Sample App
2 |
3 | To run the sample project you need [ConfigCatClient](https://pypi.org/project/configcat-client/) installed.
4 | ```
5 | pip install configcat-client
6 | ```
7 |
8 | ### Start sample:
9 | ```
10 | python consolesample.py
11 | ```
12 | or
13 | ```
14 | python consolesample2.py
15 | ```
--------------------------------------------------------------------------------
/samples/consolesample/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/configcat/python-sdk/95e1f9f3a5bee4868299b25d8e974de7421ac8f5/samples/consolesample/__init__.py
--------------------------------------------------------------------------------
/samples/consolesample/consolesample.py:
--------------------------------------------------------------------------------
1 | """
2 | You should install the ConfigCat-Client package before using this sample project
3 | pip install configcat-client
4 | """
5 |
6 | import configcatclient
7 | import logging
8 | from configcatclient.user import User
9 |
10 | # Info level logging helps to inspect the feature flag evaluation process.
11 | # Use the default warning level to avoid too detailed logging in your application.
12 | logging.basicConfig(level=logging.INFO)
13 |
14 | if __name__ == '__main__':
15 | # Initialize the ConfigCatClient with an SDK Key.
16 | client = configcatclient.get('configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/AG6C1ngVb0CvM07un6JisQ')
17 |
18 | # In the project there is a 'keySampleText' setting with the following rules:
19 | # 1. If the User's country is Hungary, the value should be 'Dog'
20 | # 2. If the User's custom property - SubscriptionType - is unlimited, the value should be 'Lion'
21 | # 3. In other cases there is a percentage rollout configured with 50% 'Falcon' and 50% 'Horse' rules.
22 | # 4. There is also a default value configured: 'Cat'
23 |
24 | # 1. As the passed User's country is Hungary this will print 'Dog'
25 | my_setting_value = client.get_value('keySampleText', 'default value', User('key', country='Hungary'))
26 | print("'keySampleText' value from ConfigCat: " + str(my_setting_value))
27 |
28 | # 2. As the passed User's custom attribute - SubscriptionType - is unlimited this will print 'Lion'
29 | my_setting_value = client.get_value('keySampleText', 'default value',
30 | User('key', custom={'SubscriptionType': 'unlimited'}))
31 | print("'keySampleText' value from ConfigCat: " + str(my_setting_value))
32 |
33 | # 3/a. As the passed User doesn't fill in any rules, this will serve 'Falcon' or 'Horse'.
34 | my_setting_value = client.get_value('keySampleText', 'default value', User('key'))
35 | print("'keySampleText' value from ConfigCat: " + str(my_setting_value))
36 |
37 | # 3/b. As this is the same user from 3/a., this will print the same value as the previous one ('Falcon' or 'Horse')
38 | my_setting_value = client.get_value('keySampleText', 'default value', User('key'))
39 | print("'keySampleText' value from ConfigCat: " + str(my_setting_value))
40 |
41 | # 4. As we don't pass a User object to this call, this will print the setting's default value - 'Cat'
42 | my_setting_value = client.get_value('keySampleText', 'default value')
43 | print("'keySampleText' value from ConfigCat: " + str(my_setting_value))
44 |
45 | # 'myKeyNotExits' setting doesn't exist in the project configuration and the client returns default value ('N/A');
46 | my_setting_not_exists = client.get_value('myKeyNotExists', 'N/A')
47 | print("'myKeyNotExists' value from ConfigCat: " + str(my_setting_not_exists))
48 |
49 | client.close()
50 |
--------------------------------------------------------------------------------
/samples/consolesample/consolesample2.py:
--------------------------------------------------------------------------------
1 | """
2 | You should install the ConfigCat-Client package before using this sample project
3 | pip install configcat-client
4 | """
5 |
6 | import configcatclient
7 | import logging
8 | from configcatclient.user import User
9 |
10 | # Info level logging helps to inspect the feature flag evaluation process.
11 | # Use the default warning level to avoid too detailed logging in your application.
12 | logging.basicConfig(level=logging.INFO)
13 |
14 | if __name__ == '__main__':
15 | # Initialize the ConfigCatClient with an SDK Key.
16 | client = configcatclient.get(
17 | 'configcat-sdk-1/PKDVCLf-Hq-h-kCzMp-L7Q/tiOvFw5gkky9LFu1Duuvzw')
18 |
19 | # Creating a user object to identify your user (optional).
20 | userObject = User('Some UserID', email='configcat@example.com', custom={
21 | 'version': '1.0.0'})
22 |
23 | value = client.get_value(
24 | 'isPOCFeatureEnabled', False, userObject)
25 | print("'isPOCFeatureEnabled' value from ConfigCat: " + str(value))
26 |
27 | client.close()
28 |
--------------------------------------------------------------------------------
/samples/webappsample/README.md:
--------------------------------------------------------------------------------
1 | # ConfigCat Django Sample App
2 |
3 | To run the sample project you need [Django](https://www.djangoproject.com/) and [ConfigCatClient](https://pypi.org/project/configcat-client/) installed.
4 | ```
5 | pip install Django
6 | pip install configcat-client
7 | ```
8 |
9 | ### Start sample:
10 | 1. Apply migrations (Required for first time only)
11 | ```
12 | python manage.py migrate
13 | ```
14 | 2. Run sample app
15 | ```
16 | python manage.py runserver
17 | ```
18 |
19 | 3. Open browser at `http://127.0.0.1:8000/`
20 |
--------------------------------------------------------------------------------
/samples/webappsample/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webappsample.settings")
7 | try:
8 | from django.core.management import execute_from_command_line
9 | except ImportError as exc:
10 | raise ImportError(
11 | "Couldn't import Django. Are you sure it's installed and "
12 | "available on your PYTHONPATH environment variable? Did you "
13 | "forget to activate a virtual environment?"
14 | ) from exc
15 |
16 | execute_from_command_line(sys.argv)
17 |
--------------------------------------------------------------------------------
/samples/webappsample/requirements.txt:
--------------------------------------------------------------------------------
1 | configcat-client>=5.0.0
2 |
--------------------------------------------------------------------------------
/samples/webappsample/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 | In the project there is a 'keySampleText' setting with the following rules:
3 | 1. If the User's country is Hungary, the value should be 'Dog'
4 | 2. If the User's custom property - SubscriptionType - is unlimited, the value should be 'Lion'
5 | 3. In other cases there is a percentage rollout configured with 50% 'Falcon' and 50% 'Horse' rules
6 | 4. There is also a default value configured: 'Cat'
7 |
8 |
9 |
10 | 1. As the passed User's country is Hungary this will return 'Dog':
11 | index1
12 |
13 |
14 | 2. As the passed User's custom attribute - SubscriptionType - is unlimited this will return 'Lion':
15 | index2
16 |
17 |
18 | 3/a. As the passed User doesn't fill in any rules, this will return 'Falcon' or 'Horse':
19 | index3a
20 |
21 |
22 | 3/b. As this is the same user from 3/a., this will return the same value as the previous one ('Falcon' or 'Horse'):
23 | index3b
24 |
25 |
26 | 4. As we don't pass an User object to this call, this will return the setting's default value - 'Cat':
27 | index4
28 |
29 |
30 | 5. 'myKeyNotExits' setting doesn't exist in the project configuration and the client returns default value ('N/A'):
31 | index5
32 |
33 |
--------------------------------------------------------------------------------
/samples/webappsample/webapp/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/configcat/python-sdk/95e1f9f3a5bee4868299b25d8e974de7421ac8f5/samples/webappsample/webapp/__init__.py
--------------------------------------------------------------------------------
/samples/webappsample/webapp/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/samples/webappsample/webapp/apps.py:
--------------------------------------------------------------------------------
1 | import configcatclient
2 | from django.apps import AppConfig
3 |
4 |
5 | class WebappConfig(AppConfig):
6 | name = 'webapp'
7 | configcat_client = configcatclient.get('PKDVCLf-Hq-h-kCzMp-L7Q/psuH7BGHoUmdONrzzUOY7A')
8 |
--------------------------------------------------------------------------------
/samples/webappsample/webapp/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/configcat/python-sdk/95e1f9f3a5bee4868299b25d8e974de7421ac8f5/samples/webappsample/webapp/migrations/__init__.py
--------------------------------------------------------------------------------
/samples/webappsample/webapp/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 | # Create your models here.
4 |
--------------------------------------------------------------------------------
/samples/webappsample/webapp/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/samples/webappsample/webapp/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from . import views
4 |
5 | urlpatterns = [
6 | path('', views.index, name='index'),
7 | path('index1', views.index1, name='index1'),
8 | path('index2', views.index2, name='index2'),
9 | path('index3a', views.index3a, name='index3a'),
10 | path('index3b', views.index3b, name='index3b'),
11 | path('index4', views.index4, name='index4'),
12 | path('index5', views.index5, name='index5'),
13 | ]
14 |
--------------------------------------------------------------------------------
/samples/webappsample/webapp/views.py:
--------------------------------------------------------------------------------
1 | from django.http import HttpResponse
2 | from configcatclient.user import User
3 | from webapp.apps import WebappConfig
4 | from django.template import loader
5 |
6 | # In the project there is a 'keySampleText' setting with the following rules:
7 | # 1. If the User's country is Hungary, the value should be 'Dog'
8 | # 2. If the User's custom property - SubscriptionType - is unlimited, the value should be 'Lion'
9 | # 3. In other cases there is a percentage rollout configured with 50% 'Falcon' and 50% 'Horse' rules.
10 | # 4. There is also a default value configured: 'Cat'
11 |
12 |
13 | def index(request):
14 | template = loader.get_template('index.html')
15 | return HttpResponse(template.render(request=request))
16 |
17 |
18 | # 1. As the passed User's country is Hungary this will return 'Dog'.
19 | def index1(request):
20 | my_setting_value = WebappConfig.configcat_client.get_value('keySampleText', 'default value',
21 | User('key', country='Hungary'))
22 | return HttpResponse("Hello, world. 'keySampleText' value from ConfigCat: " + str(my_setting_value))
23 |
24 |
25 | # 2. As the passed User's custom attribute - SubscriptionType - is unlimited this will return 'Lion'.
26 | def index2(request):
27 | my_setting_value = WebappConfig.configcat_client.get_value('keySampleText', 'default value',
28 | User('key', custom={'SubscriptionType': 'unlimited'}))
29 | return HttpResponse("Hello, world. 'keySampleText' value from ConfigCat: " + str(my_setting_value))
30 |
31 |
32 | # 3/a. As the passed User doesn't fill in any rules, this will return 'Falcon' or 'Horse'.
33 | def index3a(request):
34 | my_setting_value = WebappConfig.configcat_client.get_value('keySampleText', 'default value', User('key'))
35 | return HttpResponse("Hello, world. 'keySampleText' value from ConfigCat: " + str(my_setting_value))
36 |
37 |
38 | # 3/b. As this is the same user from 3/a., this will return the same value as the previous one ('Falcon' or 'Horse').
39 | def index3b(request):
40 | my_setting_value = WebappConfig.configcat_client.get_value('keySampleText', 'default value', User('key'))
41 | return HttpResponse("Hello, world. 'keySampleText' value from ConfigCat: " + str(my_setting_value))
42 |
43 |
44 | # 4. As we don't pass an User object to this call, this will return the setting's default value - 'Cat'.
45 | def index4(request):
46 | my_setting_value = WebappConfig.configcat_client.get_value('keySampleText', 'default value')
47 | return HttpResponse("Hello, world. 'keySampleText' value from ConfigCat: " + str(my_setting_value))
48 |
49 |
50 | # 5. 'myKeyNotExits' setting doesn't exist in the project configuration and the client returns default value ('N/A');
51 | def index5(request):
52 | my_setting_value = WebappConfig.configcat_client.get_value('myKeyNotExits', 'N/A')
53 | return HttpResponse("Hello, world. 'keySampleText' value from ConfigCat: " + str(my_setting_value))
54 |
--------------------------------------------------------------------------------
/samples/webappsample/webappsample/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/configcat/python-sdk/95e1f9f3a5bee4868299b25d8e974de7421ac8f5/samples/webappsample/webappsample/__init__.py
--------------------------------------------------------------------------------
/samples/webappsample/webappsample/settings.py:
--------------------------------------------------------------------------------
1 | """
2 | Django settings for webappsample project.
3 |
4 | Generated by 'django-admin startproject' using Django 2.0.2.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.0/topics/settings/
8 |
9 | For the full list of settings and their values, see
10 | https://docs.djangoproject.com/en/2.0/ref/settings/
11 | """
12 |
13 | import os
14 |
15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17 |
18 |
19 | # Quick-start development settings - unsuitable for production
20 | # See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/
21 |
22 | # SECURITY WARNING: keep the secret key used in production secret!
23 | SECRET_KEY = '(xsxoxa&7%zsz95g*k%(6e+&d-9$$&c5%+a7wo+uqnhzw05z%h'
24 |
25 | # SECURITY WARNING: don't run with debug turned on in production!
26 | DEBUG = True
27 |
28 | ALLOWED_HOSTS = []
29 |
30 |
31 | # Application definition
32 |
33 | INSTALLED_APPS = [
34 | 'django.contrib.admin',
35 | 'django.contrib.auth',
36 | 'django.contrib.contenttypes',
37 | 'django.contrib.sessions',
38 | 'django.contrib.messages',
39 | 'django.contrib.staticfiles',
40 | ]
41 |
42 | MIDDLEWARE = [
43 | 'django.middleware.security.SecurityMiddleware',
44 | 'django.contrib.sessions.middleware.SessionMiddleware',
45 | 'django.middleware.common.CommonMiddleware',
46 | 'django.middleware.csrf.CsrfViewMiddleware',
47 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
48 | 'django.contrib.messages.middleware.MessageMiddleware',
49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
50 | ]
51 |
52 | ROOT_URLCONF = 'webappsample.urls'
53 |
54 | TEMPLATES = [
55 | {
56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
57 | 'DIRS': [
58 | os.path.join(BASE_DIR, 'templates')
59 | ],
60 | 'APP_DIRS': True,
61 | 'OPTIONS': {
62 | 'context_processors': [
63 | 'django.template.context_processors.debug',
64 | 'django.template.context_processors.request',
65 | 'django.contrib.auth.context_processors.auth',
66 | 'django.contrib.messages.context_processors.messages',
67 | ],
68 | },
69 | },
70 | ]
71 |
72 | WSGI_APPLICATION = 'webappsample.wsgi.application'
73 |
74 |
75 | # Database
76 | # https://docs.djangoproject.com/en/2.0/ref/settings/#databases
77 |
78 | DATABASES = {
79 | 'default': {
80 | 'ENGINE': 'django.db.backends.sqlite3',
81 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
82 | }
83 | }
84 |
85 |
86 | # Password validation
87 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators
88 |
89 | AUTH_PASSWORD_VALIDATORS = [
90 | {
91 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
92 | },
93 | {
94 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
95 | },
96 | {
97 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
98 | },
99 | {
100 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
101 | },
102 | ]
103 |
104 |
105 | # Internationalization
106 | # https://docs.djangoproject.com/en/2.0/topics/i18n/
107 |
108 | LANGUAGE_CODE = 'en-us'
109 |
110 | TIME_ZONE = 'UTC'
111 |
112 | USE_I18N = True
113 |
114 | USE_L10N = True
115 |
116 | USE_TZ = True
117 |
118 |
119 | # Static files (CSS, JavaScript, Images)
120 | # https://docs.djangoproject.com/en/2.0/howto/static-files/
121 |
122 | STATIC_URL = '/static/'
123 |
--------------------------------------------------------------------------------
/samples/webappsample/webappsample/urls.py:
--------------------------------------------------------------------------------
1 | """webappsample URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: path('', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Import the include() function: from django.urls import include, path
14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
15 | """
16 | from django.contrib import admin
17 | from django.urls import path, include
18 |
19 | urlpatterns = [
20 | path('admin/', admin.site.urls),
21 | path('', include('webapp.urls'))
22 | ]
23 |
--------------------------------------------------------------------------------
/samples/webappsample/webappsample/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for webappsample project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webappsample.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal=1
3 |
4 | [flake8]
5 | max-complexity = 10
6 | max-line-length = 127
7 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 |
4 | def parse_requirements(filename):
5 | lines = (line.strip() for line in open(filename))
6 | return [line for line in lines if line]
7 |
8 |
9 | configcatclient_version = '10.0.0'
10 |
11 | requirements = parse_requirements('requirements.txt')
12 |
13 | setup(
14 | name='configcat-client',
15 | version=configcatclient_version,
16 | packages=['configcatclient'],
17 | url='https://github.com/configcat/python-sdk',
18 | license='MIT',
19 | author='ConfigCat',
20 | author_email='developer@configcat.com',
21 | description='ConfigCat SDK for Python. https://configcat.com',
22 | long_description='Feature Flags created by developers for developers with <3. ConfigCat lets you manage '
23 | 'feature flags across frontend, backend, mobile, and desktop apps without (re)deploying code. '
24 | '% rollouts, user targeting, segmentation. Feature toggle SDKs for all main languages. '
25 | 'Alternative to LaunchDarkly. '
26 | 'Host yourself, or use the hosted management app at https://configcat.com.',
27 | install_requires=requirements,
28 | classifiers=[
29 | 'Intended Audience :: Developers',
30 | 'License :: OSI Approved :: MIT License',
31 | 'Operating System :: OS Independent',
32 | 'Programming Language :: Python :: 3',
33 | 'Programming Language :: Python :: 3.5',
34 | 'Programming Language :: Python :: 3.6',
35 | 'Programming Language :: Python :: 3.7',
36 | 'Programming Language :: Python :: 3.8',
37 | 'Programming Language :: Python :: 3.9',
38 | 'Programming Language :: Python :: 3.10',
39 | 'Programming Language :: Python :: 3.11',
40 | 'Programming Language :: Python :: 3.12',
41 | 'Topic :: Software Development',
42 | 'Topic :: Software Development :: Libraries',
43 | ],
44 | )
45 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py{35,36,37,38,39,310,311},lint
3 | passenv = LD_PRELOAD
4 |
5 | [testenv]
6 | deps =
7 | pytest
8 | pytest-cov
9 | parameterized
10 | commands =
11 | pytest --cov=configcatclient configcatclienttests
12 |
13 | [testenv:lint]
14 | deps =
15 | flake8
16 | commands =
17 | # Statical analysis
18 | flake8 configcatclient --count --show-source --statistics
19 |
--------------------------------------------------------------------------------