-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathconfig_client.py
More file actions
130 lines (114 loc) · 4.96 KB
/
Copy pathconfig_client.py
File metadata and controls
130 lines (114 loc) · 4.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import logging
import time
from http import HTTPStatus
from typing import Optional, Tuple
import email.utils
import requests
from devcycle_python_sdk.api.backoff import exponential_backoff
from devcycle_python_sdk.options import DevCycleLocalOptions
from devcycle_python_sdk.exceptions import (
APIClientError,
NotFoundError,
APIClientUnauthorizedError,
)
from devcycle_python_sdk.util.strings import slash_join
logger = logging.getLogger(__name__)
class ConfigAPIClient:
def __init__(self, sdk_key: str, options: DevCycleLocalOptions):
self.sdk_key = sdk_key
self.options = options
self.session = requests.Session()
self.session.headers = {
"Content-Type": "application/json",
"Accept": "application/json",
}
self.session.max_redirects = 0
self.max_config_retries = 2
self.config_file_url = (
slash_join(
self.options.config_cdn_uri, "config", "v2", "server", self.sdk_key
)
+ ".json"
)
def get_config(
self, config_etag: Optional[str] = None, last_modified: Optional[str] = None
) -> Tuple[Optional[dict], Optional[str], Optional[str]]:
"""
Get the config from the server. If the config_etag is provided, the server will only return the config if it
has changed since the last request. If the config hasn't changed, the server will return a 304 Not Modified
response.
:param config_etag: The etag of the last config request
:param last_modified: Last modified RFC1123 Timestamp of the stored config
:return: A tuple containing the config and the etag of the config. If the config hasn't changed since the last
request, the config will be None and the etag will be the same as the last request.
"""
retries_remaining = self.max_config_retries
timeout = self.options.config_request_timeout_ms / 1000.0
url = self.config_file_url
headers = {}
if config_etag:
headers["If-None-Match"] = config_etag
if last_modified:
headers["If-Modified-Since"] = last_modified
attempts = 1
while retries_remaining > 0:
request_error: Optional[Exception] = None
try:
res: requests.Response = self.session.request(
"GET", url, params={}, timeout=timeout, headers=headers
)
if (
res.status_code == HTTPStatus.UNAUTHORIZED
or res.status_code == HTTPStatus.FORBIDDEN
):
# Not a retryable error
raise APIClientUnauthorizedError("Invalid SDK Key")
elif res.status_code == HTTPStatus.NOT_MODIFIED:
# the config hasn't changed since the last request
# don't return anything
return None, config_etag, last_modified
elif res.status_code == HTTPStatus.NOT_FOUND:
# Not a retryable error
raise NotFoundError(url)
elif (
HTTPStatus.BAD_REQUEST
<= res.status_code
< HTTPStatus.INTERNAL_SERVER_ERROR
):
# Not a retryable error
raise APIClientError(f"Bad request: HTTP {res.status_code}")
elif res.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR:
# Retryable error
request_error = APIClientError(
f"Server error: HTTP {res.status_code}"
)
else:
pass
except requests.exceptions.RequestException as e:
request_error = e
if not request_error:
break
logger.debug(
f"DevCycle config CDN request failed (attempt {attempts}): {request_error}"
)
retries_remaining -= 1
if retries_remaining:
retry_delay = exponential_backoff(
attempts, self.options.config_retry_delay_ms / 1000.0
)
time.sleep(retry_delay)
attempts += 1
continue
raise APIClientError(message="Retries exceeded", cause=request_error)
new_etag = res.headers.get("ETag", None)
new_lastmodified = res.headers.get("Last-Modified", None)
if last_modified and new_lastmodified:
stored_lm = email.utils.parsedate_to_datetime(last_modified)
response_lm = email.utils.parsedate_to_datetime(new_lastmodified)
if stored_lm > response_lm:
logger.warning(
"Request returned a last modified header older than the current stored timestamp. not saving config"
)
return None, None, None
data: dict = res.json()
return data, new_etag, new_lastmodified