forked from DevCycleHQ/python-server-sdk
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconfig_client.py
More file actions
116 lines (99 loc) · 4.13 KB
/
Copy pathconfig_client.py
File metadata and controls
116 lines (99 loc) · 4.13 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
import logging
import time
from http import HTTPStatus
from os.path import join
from typing import Optional, Tuple
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,
)
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 = (
join(self.options.config_cdn_uri, "config", "v1", "server", self.sdk_key)
+ ".json"
)
def get_config(
self, config_etag: Optional[str] = None
) -> Tuple[Optional[dict], 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
: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
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
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)
data: dict = res.json()
return data, new_etag