devcycle-python-server-sdk 3.5.0__py3-none-any.whl → 3.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- 3.5.0
1
+ 3.6.0
@@ -2,7 +2,7 @@ import logging
2
2
  import time
3
3
  from http import HTTPStatus
4
4
  from typing import Optional, Tuple
5
-
5
+ import email.utils
6
6
  import requests
7
7
 
8
8
  from devcycle_python_sdk.api.backoff import exponential_backoff
@@ -36,15 +36,15 @@ class ConfigAPIClient:
36
36
  )
37
37
 
38
38
  def get_config(
39
- self, config_etag: Optional[str] = None
40
- ) -> Tuple[Optional[dict], Optional[str]]:
39
+ self, config_etag: Optional[str] = None, last_modified: Optional[str] = None
40
+ ) -> Tuple[Optional[dict], Optional[str], Optional[str]]:
41
41
  """
42
42
  Get the config from the server. If the config_etag is provided, the server will only return the config if it
43
43
  has changed since the last request. If the config hasn't changed, the server will return a 304 Not Modified
44
44
  response.
45
45
 
46
46
  :param config_etag: The etag of the last config request
47
-
47
+ :param last_modified: Last modified RFC1123 Timestamp of the stored config
48
48
  :return: A tuple containing the config and the etag of the config. If the config hasn't changed since the last
49
49
  request, the config will be None and the etag will be the same as the last request.
50
50
  """
@@ -56,6 +56,8 @@ class ConfigAPIClient:
56
56
  headers = {}
57
57
  if config_etag:
58
58
  headers["If-None-Match"] = config_etag
59
+ if last_modified:
60
+ headers["If-Modified-Since"] = last_modified
59
61
 
60
62
  attempts = 1
61
63
  while retries_remaining > 0:
@@ -74,7 +76,7 @@ class ConfigAPIClient:
74
76
  elif res.status_code == HTTPStatus.NOT_MODIFIED:
75
77
  # the config hasn't changed since the last request
76
78
  # don't return anything
77
- return None, config_etag
79
+ return None, config_etag, last_modified
78
80
  elif res.status_code == HTTPStatus.NOT_FOUND:
79
81
  # Not a retryable error
80
82
  raise NotFoundError(url)
@@ -113,6 +115,16 @@ class ConfigAPIClient:
113
115
  raise APIClientError(message="Retries exceeded", cause=request_error)
114
116
 
115
117
  new_etag = res.headers.get("ETag", None)
118
+ new_lastmodified = res.headers.get("Last-Modified", None)
119
+
120
+ if last_modified and new_lastmodified:
121
+ stored_lm = email.utils.parsedate_to_datetime(last_modified)
122
+ response_lm = email.utils.parsedate_to_datetime(new_lastmodified)
123
+ if stored_lm > response_lm:
124
+ logger.warning(
125
+ "Request returned a last modified header older than the current stored timestamp. not saving config"
126
+ )
127
+ return None, None, None
116
128
 
117
129
  data: dict = res.json()
118
- return data, new_etag
130
+ return data, new_etag, new_lastmodified
@@ -30,6 +30,7 @@ class EnvironmentConfigManager(threading.Thread):
30
30
 
31
31
  self._config: Optional[dict] = None
32
32
  self._config_etag: Optional[str] = None
33
+ self._config_lastmodified: Optional[str] = None
33
34
 
34
35
  self._config_api_client = ConfigAPIClient(self._sdk_key, self._options)
35
36
 
@@ -42,10 +43,13 @@ class EnvironmentConfigManager(threading.Thread):
42
43
 
43
44
  def _get_config(self):
44
45
  try:
45
- new_config, new_etag = self._config_api_client.get_config(
46
- config_etag=self._config_etag
46
+ new_config, new_etag, new_lastmodified = self._config_api_client.get_config(
47
+ config_etag=self._config_etag, last_modified=self._config_lastmodified
47
48
  )
48
49
 
50
+ # Abort early if the last modified is before the sent one.
51
+ if new_config is None and new_etag is None and new_lastmodified is None:
52
+ return
49
53
  if new_config is None and new_etag == self._config_etag:
50
54
  # api not returning data and the etag is the same
51
55
  # no change to the config since last request
@@ -60,6 +64,7 @@ class EnvironmentConfigManager(threading.Thread):
60
64
 
61
65
  self._config = new_config
62
66
  self._config_etag = new_etag
67
+ self._config_lastmodified = new_lastmodified
63
68
 
64
69
  json_config = json.dumps(self._config)
65
70
  self._local_bucketing.store_config(json_config)
@@ -60,9 +60,9 @@ class ProjectSettings:
60
60
  @classmethod
61
61
  def from_json(cls, data: dict) -> "ProjectSettings":
62
62
  return cls(
63
- edge_db=EdgeDBSettings.from_json(data["edgeDB"])
64
- if "edgeDB" in data
65
- else None,
63
+ edge_db=(
64
+ EdgeDBSettings.from_json(data["edgeDB"]) if "edgeDB" in data else None
65
+ ),
66
66
  opt_in=OptInSettings.from_json(data["optIn"]) if "optIn" in data else None,
67
67
  disable_passthrough_rollouts=data.get("disablePassthroughRollouts", False),
68
68
  )
@@ -81,9 +81,11 @@ class Project:
81
81
  id=data["_id"],
82
82
  key=data["key"],
83
83
  a0_organization=data["a0_organization"],
84
- settings=ProjectSettings.from_json(data["settings"])
85
- if "settings" in data
86
- else None,
84
+ settings=(
85
+ ProjectSettings.from_json(data["settings"])
86
+ if "settings" in data
87
+ else None
88
+ ),
87
89
  )
88
90
 
89
91
 
@@ -58,9 +58,11 @@ class DevCycleProvider(AbstractProvider):
58
58
  else:
59
59
  return FlagResolutionDetails(
60
60
  value=variable.value,
61
- reason=Reason.DEFAULT
62
- if variable.isDefaulted
63
- else Reason.TARGETING_MATCH,
61
+ reason=(
62
+ Reason.DEFAULT
63
+ if variable.isDefaulted
64
+ else Reason.TARGETING_MATCH
65
+ ),
64
66
  )
65
67
  except ValueError as e:
66
68
  # occurs if the key or default value is None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devcycle-python-server-sdk
3
- Version: 3.5.0
3
+ Version: 3.6.0
4
4
  Summary: DevCycle Python SDK
5
5
  Home-page: https://github.com/devcycleHQ/python-server-sdk
6
6
  Author: UNKNOWN
@@ -10,7 +10,7 @@ Keywords: DevCycle
10
10
  Platform: UNKNOWN
11
11
  Requires-Dist: setuptools >=21.0.0
12
12
  Requires-Dist: urllib3 >=1.15.1
13
- Requires-Dist: requests ~=2.31.0
13
+ Requires-Dist: requests ~=2.31
14
14
  Requires-Dist: wasmtime ==9.0.0
15
15
  Requires-Dist: protobuf >=4.23.3
16
16
  Requires-Dist: openfeature-sdk >=0.7.0
@@ -1,4 +1,4 @@
1
- devcycle_python_sdk/VERSION.txt,sha256=dhIMHyxDEcIVTCKmtY4A5BHg6-dq-uDa9gjZGWxh8RQ,6
1
+ devcycle_python_sdk/VERSION.txt,sha256=E4ID4_YEoI-tDE0KKX7-LEhiGYINzdBT5yt9hDUEx6E,6
2
2
  devcycle_python_sdk/__init__.py,sha256=30TcYb9PbvVi97gwk16omCGG1UoUI9lmmU7xpvxNMvc,336
3
3
  devcycle_python_sdk/bucketing-lib.release.wasm,sha256=yrNDrAxZS1h9VlTzsdOrNLP4ngnM04PmKxcWop1yrt4,202623
4
4
  devcycle_python_sdk/cloud_client.py,sha256=UMfXvXwO4v3pZW9ZA52ivxPH1Lp4Goy5gpV_s0qCpxQ,7352
@@ -10,14 +10,14 @@ devcycle_python_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
10
10
  devcycle_python_sdk/api/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
11
11
  devcycle_python_sdk/api/backoff.py,sha256=F6WYCloaZD9CjNu6mKBMq7K20S8-i2WVU6X05ZRoawE,303
12
12
  devcycle_python_sdk/api/bucketing_client.py,sha256=-S0y9uAffippCTfjYgxI5j4j7YNfMxQJYUba1219nps,5111
13
- devcycle_python_sdk/api/config_client.py,sha256=BbPAjvB8c4engCiVnrRDXAXabHJLKYc1uxCtoPpvffg,4293
13
+ devcycle_python_sdk/api/config_client.py,sha256=NnqHmPcwK__LNqPzmKo5EBqULAQEteH1YmzRE_eIXJs,5077
14
14
  devcycle_python_sdk/api/event_client.py,sha256=WxkuV6geEC9MgG4FN36HeEQQa22ilq2mkOVMZkpiEk4,3356
15
15
  devcycle_python_sdk/api/local_bucketing.py,sha256=-TfG3Jb0syVF3sqzzlFyKxHqLQ6a37C4mW0oj53J-yQ,16332
16
16
  devcycle_python_sdk/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- devcycle_python_sdk/managers/config_manager.py,sha256=_-1kWwyyAcwVF_DibXjnehXi3Qj6HR8orYfgNfgWJOc,3248
17
+ devcycle_python_sdk/managers/config_manager.py,sha256=YfgGKW4mtMhqdwWZiZ_Y67AYLC9VTpTjYhpDgfGFA4g,3599
18
18
  devcycle_python_sdk/managers/event_queue_manager.py,sha256=06TcdlRoB89C7vGH1UTnnoDFsSnT71tPwf0gn1NK-W0,8071
19
19
  devcycle_python_sdk/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- devcycle_python_sdk/models/bucketed_config.py,sha256=76bdF7pgUdIAn2y8YPs3AO7Qa2eyJQ_jf4QhG3IERaE,3951
20
+ devcycle_python_sdk/models/bucketed_config.py,sha256=NsdT-6RbEuDy5QuK5qlr6UqNKQQnwjoXVYN-uQp0P-E,3999
21
21
  devcycle_python_sdk/models/error_response.py,sha256=r_dRUDAbral9UGaMYcnBsZOr9Hmkyi1SQbhGmZilgpA,442
22
22
  devcycle_python_sdk/models/event.py,sha256=-6bhKp72mAgaoSxvhS__G4vvfQ9Psj83fc0Igelzw3o,3713
23
23
  devcycle_python_sdk/models/feature.py,sha256=m3wxxIjC5jxhKpPLI4RcsBaNLuy-zsMqoAgc8ySPe0I,852
@@ -25,7 +25,7 @@ devcycle_python_sdk/models/platform_data.py,sha256=kIFevH5trsxI32qp7BW6glUCpipgp
25
25
  devcycle_python_sdk/models/user.py,sha256=57d_n8Yy8g-zsGzVF0hmSf4McPtFPJJ0LLY_ql75FWk,6266
26
26
  devcycle_python_sdk/models/variable.py,sha256=qp00SyCNyfYiMlTwnDSDJ4ZetRF35JHTbYZQOBxFIqY,1711
27
27
  devcycle_python_sdk/open_feature_provider/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- devcycle_python_sdk/open_feature_provider/provider.py,sha256=FjNxmyKrnUO4ma4c-Z39qWigLVC_P-L_Nly0mycA1Vc,4475
28
+ devcycle_python_sdk/open_feature_provider/provider.py,sha256=VjUMhCYiMLixXUSSIjQ7kVCQrls1QD_Nltooe5EjGks,4539
29
29
  devcycle_python_sdk/protobuf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  devcycle_python_sdk/protobuf/utils.py,sha256=zi-kVx6yvgS2c04cUobLd2IDc_ZqKkWduay9YcpjoGc,4684
31
31
  devcycle_python_sdk/protobuf/variableForUserParams_pb2.py,sha256=Zf4-8n67UnulC0Sz90aqLYsXCFEgywOqfiSHuXZomLk,4244
@@ -41,13 +41,13 @@ test/test_cloud_client.py,sha256=zjk302m7qKMCQryolkc1WKLFmbGWUxA0VtHlZk0rY-0,114
41
41
  test/test_local_client.py,sha256=Ds6VIfLcaefboyvGrhQTGsdJj_DGB4IVnR2_16xDpSY,13022
42
42
  test/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
43
  test/api/test_bucketing_client.py,sha256=M2mjACowLvbsIjXdP-w8-fwY02xqv1X1r_h9v-pXa7A,7739
44
- test/api/test_config_client.py,sha256=oZMBlXGw4UHb5-rSNF_ARTrAjvbvkbFgb8GAK4ExR14,4413
44
+ test/api/test_config_client.py,sha256=jT48_llov5QQs0jNDbHANRqySVT1HamXm82uPZXKSrE,5129
45
45
  test/api/test_event_client.py,sha256=v4Xoum-PdywfczrfMV1llM5vrIJoqHybzxPLVokh09g,3310
46
46
  test/api/test_local_bucketing.py,sha256=lPL3ThZcGG-8-67jFxRvGrwL8NB6-sM5qGJb0p2O__Q,15254
47
47
  test/fixture/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
48
  test/fixture/data.py,sha256=6ReLxWPMhCDbJ5Ex24_iR0PJNjKzPFV1I9j_v0FVbBU,1462
49
49
  test/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
- test/managers/test_config_manager.py,sha256=_MtvyxK06xpBzzEsN1zsLdYrk3DsPsbCmSj7XorINmI,5366
50
+ test/managers/test_config_manager.py,sha256=X0DxzWdjkvkUmarJSgzTDvO2Ma0TU6RNwL0WUwX2uA8,5963
51
51
  test/managers/test_event_queue_manager.py,sha256=KquHBuYiDdoVStm5Ga9mIB-wvmJnChiFRBNC8sMKaxQ,11053
52
52
  test/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
53
  test/models/test_bucketed_config.py,sha256=8wCEyxaHxY21nl7ZbiFzp-IzZQBZIPIVPPzeREjumQk,858
@@ -59,8 +59,8 @@ test/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
59
  test/util/test_strings.py,sha256=T5H5U0af7_ld1yJC8IdDmay_Mx-iJQ5Fi5fbvKcAc7A,862
60
60
  test/util/test_utils.py,sha256=tVBJm_qnqg-4PssLpXIxqlk0JehuiAIPs4zpi2oNVnY,6671
61
61
  test/util/test_version.py,sha256=3Xg0_84m0bdHXz6pxd2Z85Oz9DoUC1T0Ol_WqEH_jac,335
62
- devcycle_python_server_sdk-3.5.0.dist-info/LICENSE,sha256=tHQMqu-Jic2UpMGqb-vNFbGygysl8IG4l1clnnh0OFo,1071
63
- devcycle_python_server_sdk-3.5.0.dist-info/METADATA,sha256=IAUJWGuZDWtOoCfJ6Tj6aKURxJODAtUuutO1g94hLho,510
64
- devcycle_python_server_sdk-3.5.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
65
- devcycle_python_server_sdk-3.5.0.dist-info/top_level.txt,sha256=34f8zDzihAHoH7siAFlmscyO_QLNWWq9HApFlwtYL4k,33
66
- devcycle_python_server_sdk-3.5.0.dist-info/RECORD,,
62
+ devcycle_python_server_sdk-3.6.0.dist-info/LICENSE,sha256=tHQMqu-Jic2UpMGqb-vNFbGygysl8IG4l1clnnh0OFo,1071
63
+ devcycle_python_server_sdk-3.6.0.dist-info/METADATA,sha256=mEHWiA0xrfnZ4-GLr2uyc0_HOPyEc2ENqvD6CLe8SKo,508
64
+ devcycle_python_server_sdk-3.6.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
65
+ devcycle_python_server_sdk-3.6.0.dist-info/top_level.txt,sha256=34f8zDzihAHoH7siAFlmscyO_QLNWWq9HApFlwtYL4k,33
66
+ devcycle_python_server_sdk-3.6.0.dist-info/RECORD,,
@@ -2,6 +2,9 @@ import logging
2
2
  import unittest
3
3
  import uuid
4
4
  from http import HTTPStatus
5
+ from datetime import datetime
6
+ from email.utils import formatdate
7
+ from time import mktime
5
8
 
6
9
  import requests
7
10
  import responses
@@ -36,6 +39,9 @@ class ConfigAPIClientTest(unittest.TestCase):
36
39
 
37
40
  options = DevCycleLocalOptions(config_retry_delay_ms=0)
38
41
  self.test_client = ConfigAPIClient(self.sdk_key, options)
42
+ now = datetime.now()
43
+ stamp = mktime(now.timetuple())
44
+ self.test_lastmodified = formatdate(timeval=stamp, localtime=False, usegmt=True)
39
45
  self.test_etag = str(uuid.uuid4())
40
46
  self.test_config_json: dict = small_config_json()
41
47
 
@@ -45,14 +51,20 @@ class ConfigAPIClientTest(unittest.TestCase):
45
51
  @responses.activate
46
52
  def test_get_config(self):
47
53
  new_etag = str(uuid.uuid4())
54
+ now = datetime.now()
55
+ stamp = mktime(now.timetuple())
56
+ new_lastmodified = formatdate(timeval=stamp, localtime=False, usegmt=True)
48
57
  responses.add(
49
58
  responses.GET,
50
59
  self.config_url,
51
- headers={"ETag": new_etag},
60
+ headers={"ETag": new_etag, "Last-Modified": new_lastmodified},
52
61
  json=self.test_config_json,
53
62
  )
54
- result, etag = self.test_client.get_config(config_etag=self.test_etag)
63
+ result, etag, lastmodified = self.test_client.get_config(
64
+ config_etag=self.test_etag, last_modified=self.test_lastmodified
65
+ )
55
66
  self.assertDictEqual(result, self.test_config_json)
67
+ self.assertEqual(lastmodified, new_lastmodified)
56
68
  self.assertEqual(etag, new_etag)
57
69
 
58
70
  @responses.activate(registry=OrderedRegistry)
@@ -68,7 +80,9 @@ class ConfigAPIClientTest(unittest.TestCase):
68
80
  headers={"ETag": self.test_etag},
69
81
  json=self.test_config_json,
70
82
  )
71
- result, etag = self.test_client.get_config(config_etag=self.test_etag)
83
+ result, etag, lastmodified = self.test_client.get_config(
84
+ config_etag=self.test_etag
85
+ )
72
86
 
73
87
  self.assertDictEqual(result, self.test_config_json)
74
88
  self.assertEqual(etag, self.test_etag)
@@ -83,10 +97,12 @@ class ConfigAPIClientTest(unittest.TestCase):
83
97
  responses.add(
84
98
  responses.GET,
85
99
  self.config_url,
86
- headers={"ETag": self.test_etag},
100
+ headers={"ETag": self.test_etag, "Last-Modified": self.test_lastmodified},
87
101
  json=self.test_config_json,
88
102
  )
89
- result, etag = self.test_client.get_config(config_etag=self.test_etag)
103
+ result, etag, last_modified = self.test_client.get_config(
104
+ config_etag=self.test_etag
105
+ )
90
106
  self.assertDictEqual(result, self.test_config_json)
91
107
  self.assertEqual(etag, self.test_etag)
92
108
 
@@ -131,6 +147,8 @@ class ConfigAPIClientTest(unittest.TestCase):
131
147
  status=HTTPStatus.NOT_MODIFIED,
132
148
  )
133
149
 
134
- new_config, new_etag = self.test_client.get_config(config_etag=self.test_etag)
150
+ new_config, new_etag, last_modified = self.test_client.get_config(
151
+ config_etag=self.test_etag
152
+ )
135
153
  self.assertIsNone(new_config)
136
154
  self.assertEqual(new_etag, self.test_etag)
@@ -1,8 +1,11 @@
1
- import logging
2
1
  import json
2
+ import logging
3
3
  import time
4
- import uuid
5
4
  import unittest
5
+ import uuid
6
+ from datetime import datetime
7
+ from email.utils import formatdate
8
+ from time import mktime
6
9
  from unittest.mock import patch, MagicMock
7
10
 
8
11
  from devcycle_python_sdk import DevCycleLocalOptions
@@ -18,20 +21,27 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
18
21
  self.test_local_bucketing = MagicMock()
19
22
  self.test_options = DevCycleLocalOptions(config_polling_interval_ms=500)
20
23
 
24
+ now = datetime.now()
25
+ stamp = mktime(now.timetuple())
26
+ self.test_lastmodified = formatdate(timeval=stamp, localtime=False, usegmt=True)
21
27
  self.test_etag = str(uuid.uuid4())
22
28
  self.test_config_json = small_config_json()
23
29
  self.test_config_string = json.dumps(self.test_config_json)
24
30
 
25
31
  @patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
26
32
  def test_init(self, mock_get_config):
27
- mock_get_config.return_value = (self.test_config_json, self.test_etag)
33
+ mock_get_config.return_value = (
34
+ self.test_config_json,
35
+ self.test_etag,
36
+ self.test_lastmodified,
37
+ )
28
38
  config_manager = EnvironmentConfigManager(
29
39
  self.sdk_key, self.test_options, self.test_local_bucketing
30
40
  )
31
41
 
32
42
  # sleep to allow polling thread to load the config
33
43
  time.sleep(0.1)
34
- mock_get_config.assert_called_once_with(config_etag=None)
44
+ mock_get_config.assert_called_once_with(config_etag=None, last_modified=None)
35
45
 
36
46
  self.assertTrue(config_manager._polling_enabled)
37
47
  self.assertTrue(config_manager.is_alive())
@@ -45,7 +55,11 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
45
55
 
46
56
  @patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
47
57
  def test_init_with_client_callback(self, mock_get_config):
48
- mock_get_config.return_value = (self.test_config_json, self.test_etag)
58
+ mock_get_config.return_value = (
59
+ self.test_config_json,
60
+ self.test_etag,
61
+ self.test_lastmodified,
62
+ )
49
63
 
50
64
  mock_callback = MagicMock()
51
65
 
@@ -55,7 +69,7 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
55
69
  self.sdk_key, self.test_options, self.test_local_bucketing
56
70
  )
57
71
  time.sleep(0.1)
58
- mock_get_config.assert_called_once_with(config_etag=None)
72
+ mock_get_config.assert_called_once_with(config_etag=None, last_modified=None)
59
73
  self.assertEqual(config_manager._config_etag, self.test_etag)
60
74
  self.assertDictEqual(config_manager._config, self.test_config_json)
61
75
  self.test_local_bucketing.store_config.assert_called_once_with(
@@ -66,7 +80,11 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
66
80
 
67
81
  @patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
68
82
  def test_init_with_client_callback_with_error(self, mock_get_config):
69
- mock_get_config.return_value = (self.test_config_json, self.test_etag)
83
+ mock_get_config.return_value = (
84
+ self.test_config_json,
85
+ self.test_etag,
86
+ self.test_lastmodified,
87
+ )
70
88
  mock_callback = MagicMock()
71
89
  mock_callback.side_effect = Exception(
72
90
  "Badly written callback generates an exception"
@@ -79,7 +97,7 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
79
97
  )
80
98
  # the callback error should not negatively impact initialization of the config manager
81
99
  time.sleep(0.1)
82
- mock_get_config.assert_called_once_with(config_etag=None)
100
+ mock_get_config.assert_called_once_with(config_etag=None, last_modified=None)
83
101
  self.assertEqual(config_manager._config_etag, self.test_etag)
84
102
  self.assertDictEqual(config_manager._config, self.test_config_json)
85
103
  self.test_local_bucketing.store_config.assert_called_once_with(
@@ -105,7 +123,11 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
105
123
 
106
124
  @patch("devcycle_python_sdk.api.config_client.ConfigAPIClient.get_config")
107
125
  def test_get_config_unchanged(self, mock_get_config):
108
- mock_get_config.return_value = (self.test_config_json, self.test_etag)
126
+ mock_get_config.return_value = (
127
+ self.test_config_json,
128
+ self.test_etag,
129
+ self.test_lastmodified,
130
+ )
109
131
 
110
132
  self.test_options.config_polling_interval_ms = 200
111
133
  config_manager = EnvironmentConfigManager(
@@ -117,7 +139,7 @@ class EnvironmentConfigManagerTest(unittest.TestCase):
117
139
  config_manager.close()
118
140
 
119
141
  self.test_local_bucketing.store_config.reset_mock()
120
- mock_get_config.return_value = (None, config_manager._config_etag)
142
+ mock_get_config.return_value = (None, config_manager._config_etag, None)
121
143
 
122
144
  # trigger refresh of the config directly
123
145
  config_manager._get_config()