devcycle-python-server-sdk 1.2.1__tar.gz → 3.5.0__tar.gz

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.
Files changed (99) hide show
  1. devcycle-python-server-sdk-3.5.0/PKG-INFO +12 -0
  2. devcycle-python-server-sdk-3.5.0/README.md +119 -0
  3. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/__init__.py +6 -0
  4. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/api/backoff.py +11 -0
  5. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/api/bucketing_client.py +145 -0
  6. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/api/config_client.py +118 -0
  7. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/api/event_client.py +95 -0
  8. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/api/local_bucketing.py +436 -0
  9. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/cloud_client.py +201 -0
  10. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/devcycle_client.py +40 -0
  11. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/exceptions.py +43 -0
  12. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/local_client.py +242 -0
  13. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/managers/config_manager.py +99 -0
  14. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/managers/event_queue_manager.py +227 -0
  15. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/models/bucketed_config.py +152 -0
  16. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/models/error_response.py +17 -0
  17. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/models/event.py +123 -0
  18. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/models/feature.py +33 -0
  19. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/models/platform_data.py +33 -0
  20. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/models/user.py +159 -0
  21. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/models/variable.py +65 -0
  22. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/open_feature_provider/__init__.py +0 -0
  23. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/open_feature_provider/provider.py +123 -0
  24. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/options.py +106 -0
  25. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/protobuf/__init__.py +0 -0
  26. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/protobuf/utils.py +129 -0
  27. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/protobuf/variableForUserParams_pb2.py +46 -0
  28. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/util/__init__.py +1 -0
  29. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/util/strings.py +6 -0
  30. devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/util/version.py +9 -0
  31. devcycle-python-server-sdk-3.5.0/devcycle_python_server_sdk.egg-info/PKG-INFO +12 -0
  32. devcycle-python-server-sdk-3.5.0/devcycle_python_server_sdk.egg-info/SOURCES.txt +68 -0
  33. devcycle-python-server-sdk-3.5.0/devcycle_python_server_sdk.egg-info/dependency_links.txt +1 -0
  34. devcycle-python-server-sdk-3.5.0/devcycle_python_server_sdk.egg-info/requires.txt +6 -0
  35. devcycle-python-server-sdk-3.5.0/example/__init__.py +0 -0
  36. devcycle-python-server-sdk-3.5.0/example/cloud_client_example.py +68 -0
  37. devcycle-python-server-sdk-3.5.0/example/local_bucketing_client_example.py +79 -0
  38. devcycle-python-server-sdk-3.5.0/example/openfeature_example.py +77 -0
  39. devcycle-python-server-sdk-3.5.0/pyproject.toml +56 -0
  40. devcycle-python-server-sdk-3.5.0/setup.py +36 -0
  41. devcycle-python-server-sdk-3.5.0/test/__init__.py +0 -0
  42. devcycle-python-server-sdk-3.5.0/test/api/__init__.py +0 -0
  43. devcycle-python-server-sdk-3.5.0/test/api/test_bucketing_client.py +237 -0
  44. devcycle-python-server-sdk-3.5.0/test/api/test_config_client.py +136 -0
  45. devcycle-python-server-sdk-3.5.0/test/api/test_event_client.py +100 -0
  46. devcycle-python-server-sdk-3.5.0/test/api/test_local_bucketing.py +395 -0
  47. devcycle-python-server-sdk-3.5.0/test/fixture/__init__.py +0 -0
  48. devcycle-python-server-sdk-3.5.0/test/fixture/data.py +57 -0
  49. devcycle-python-server-sdk-3.5.0/test/managers/__init__.py +0 -0
  50. devcycle-python-server-sdk-3.5.0/test/managers/test_config_manager.py +132 -0
  51. devcycle-python-server-sdk-3.5.0/test/managers/test_event_queue_manager.py +271 -0
  52. devcycle-python-server-sdk-3.5.0/test/models/__init__.py +0 -0
  53. devcycle-python-server-sdk-3.5.0/test/models/test_bucketed_config.py +26 -0
  54. devcycle-python-server-sdk-3.5.0/test/models/test_user.py +188 -0
  55. devcycle-python-server-sdk-3.5.0/test/openfeature_test/__init__.py +0 -0
  56. devcycle-python-server-sdk-3.5.0/test/openfeature_test/test_provider.py +219 -0
  57. devcycle-python-server-sdk-3.5.0/test/openfeature_test/test_provider_local_sdk.py +136 -0
  58. devcycle-python-server-sdk-3.5.0/test/test_cloud_client.py +286 -0
  59. devcycle-python-server-sdk-3.5.0/test/test_local_client.py +407 -0
  60. devcycle-python-server-sdk-3.5.0/test/util/__init__.py +0 -0
  61. devcycle-python-server-sdk-3.5.0/test/util/test_strings.py +25 -0
  62. devcycle-python-server-sdk-3.5.0/test/util/test_utils.py +183 -0
  63. devcycle-python-server-sdk-3.5.0/test/util/test_version.py +17 -0
  64. devcycle-python-server-sdk-1.2.1/PKG-INFO +0 -12
  65. devcycle-python-server-sdk-1.2.1/README.md +0 -53
  66. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/VERSION.txt +0 -1
  67. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/__init__.py +0 -30
  68. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/api/__init__.py +0 -6
  69. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/api/dvc_client.py +0 -482
  70. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/api_client.py +0 -628
  71. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/configuration.py +0 -251
  72. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/dvc_options.py +0 -3
  73. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/models/__init__.py +0 -23
  74. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/models/error_response.py +0 -141
  75. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/models/event.py +0 -225
  76. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/models/feature.py +0 -294
  77. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/models/inline_response201.py +0 -110
  78. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/models/user_data.py +0 -539
  79. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/models/user_data_and_events_body.py +0 -136
  80. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/models/variable.py +0 -230
  81. devcycle-python-server-sdk-1.2.1/devcycle_python_sdk/rest.py +0 -322
  82. devcycle-python-server-sdk-1.2.1/devcycle_python_server_sdk.egg-info/PKG-INFO +0 -12
  83. devcycle-python-server-sdk-1.2.1/devcycle_python_server_sdk.egg-info/SOURCES.txt +0 -35
  84. devcycle-python-server-sdk-1.2.1/devcycle_python_server_sdk.egg-info/requires.txt +0 -4
  85. devcycle-python-server-sdk-1.2.1/example/example.py +0 -78
  86. devcycle-python-server-sdk-1.2.1/setup.py +0 -43
  87. devcycle-python-server-sdk-1.2.1/test/test_devcycle_api.py +0 -61
  88. devcycle-python-server-sdk-1.2.1/test/test_error_response.py +0 -39
  89. devcycle-python-server-sdk-1.2.1/test/test_feature.py +0 -39
  90. devcycle-python-server-sdk-1.2.1/test/test_inline_response200.py +0 -39
  91. devcycle-python-server-sdk-1.2.1/test/test_user_data.py +0 -39
  92. devcycle-python-server-sdk-1.2.1/test/test_user_data_and_event_body.py +0 -39
  93. devcycle-python-server-sdk-1.2.1/test/test_variable.py +0 -39
  94. {devcycle-python-server-sdk-1.2.1 → devcycle-python-server-sdk-3.5.0}/LICENSE +0 -0
  95. /devcycle-python-server-sdk-1.2.1/devcycle_python_server_sdk.egg-info/dependency_links.txt → /devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/api/__init__.py +0 -0
  96. {devcycle-python-server-sdk-1.2.1/example → devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/managers}/__init__.py +0 -0
  97. {devcycle-python-server-sdk-1.2.1/test → devcycle-python-server-sdk-3.5.0/devcycle_python_sdk/models}/__init__.py +0 -0
  98. {devcycle-python-server-sdk-1.2.1 → devcycle-python-server-sdk-3.5.0}/devcycle_python_server_sdk.egg-info/top_level.txt +0 -0
  99. {devcycle-python-server-sdk-1.2.1 → devcycle-python-server-sdk-3.5.0}/setup.cfg +0 -0
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 1.0
2
+ Name: devcycle-python-server-sdk
3
+ Version: 3.5.0
4
+ Summary: DevCycle Python SDK
5
+ Home-page: https://github.com/devcycleHQ/python-server-sdk
6
+ Author: UNKNOWN
7
+ Author-email: UNKNOWN
8
+ License: UNKNOWN
9
+ Description: The DevCycle Python SDK used for feature management.
10
+
11
+ Keywords: DevCycle
12
+ Platform: UNKNOWN
@@ -0,0 +1,119 @@
1
+ # DevCycle Python Server SDK
2
+
3
+ The DevCycle Python SDK used for feature management.
4
+
5
+ This SDK allows your application to interface with the [DevCycle Bucketing API](https://docs.devcycle.com/bucketing-api/#tag/devcycle).
6
+
7
+ ## Requirements.
8
+
9
+ * Python 3.8+
10
+
11
+ ## Installation
12
+
13
+ ```sh
14
+ pip install devcycle-python-server-sdk
15
+ ```
16
+ (you may need to run `pip` with root permission: `sudo pip install devcycle-python-server-sdk`)
17
+
18
+ ## Getting Started
19
+
20
+ The core DevCycle objects are in the `devcycle_python_sdk` package. To get started, import the `DevCycleLocalClient` class and the `DevCycleLocalOptions` class. The `DevCycleLocalClient` class is used to interact with the DevCycle API. The `DevCycleLocalOptions` class is used to configure the client.
21
+
22
+ ```python
23
+ from devcycle_python_sdk import DevCycleLocalClient, DevCycleLocalOptions
24
+ from devcycle_python_sdk.models.user import DevCycleUser
25
+
26
+ options = DevCycleLocalOptions()
27
+
28
+ # create an instance of the client class
29
+ client = DevCycleLocalClient('DEVCYCLE_SERVER_SDK_KEY', options)
30
+
31
+ user = DevCycleUser(
32
+ user_id='test',
33
+ email='example@example.ca',
34
+ country='CA'
35
+ )
36
+
37
+ value = client.variable_value(user, 'variable-key', 'default-value')
38
+ ```
39
+
40
+ The DevCycle client is designed to work as a singleton in your application. You should create a single instance of the client during application initialization
41
+
42
+ ## OpenFeature Support
43
+
44
+ This SDK provides an alpha implementation of the [OpenFeature](https://openfeature.dev/) Provider interface. Use the `get_openfeature_provider()` function on the DevCycle SDK client to obtain a provider for OpenFeature.
45
+
46
+ ```python
47
+ from openfeature import api
48
+
49
+ devcycle_client = DevCycleLocalClient('DEVCYCLE_SERVER_SDK_KEY', options)
50
+ api.set_provider(devcycle_client.get_openfeature_provider())
51
+ ```
52
+
53
+ More details are in the [DevCycle Python SDK OpenFeature Provider](OpenFeature.md) guide.
54
+
55
+ > :warning: **OpenFeature support is in an early release and may have some rough edges**. Please report any issues to us and we'll be happy to help!
56
+
57
+ ## Usage
58
+
59
+ To find usage documentation, visit our [docs](https://docs.devcycle.com/docs/sdk/server-side-sdks/python#usage).
60
+
61
+ ## Development
62
+
63
+ When developing the SDK it is recommended that you have both a 3.8 and 3.12 python interpreter installed in order to verify changes across different versions of python.
64
+
65
+ ### Dependencies
66
+
67
+ To set up dependencies for local development, run:
68
+ ```
69
+ pip install -r requirements.test.txt
70
+ ```
71
+
72
+ To run the example app against the local version of the API for testing and development, run:
73
+ ```sh
74
+ pip install --editable .
75
+ ```
76
+ from the top level of the repo (same level as setup.py). Then run the example app as normal.
77
+
78
+
79
+ ### Linting & Formatting
80
+
81
+ Linting checks on PRs are run using [ruff](https://github.com/charliermarsh/ruff), and are configured using `.ruff.toml`. To run the linter locally, run this command from the top level of the repo:
82
+ ```
83
+ ruff check .
84
+ ```
85
+
86
+ Ruff can automatically fix simple linting errors (the ones marked with `[*]`). To do so, run:
87
+ ```
88
+ ruff check . --fix
89
+ ```
90
+
91
+ Formatting checks on PRs are done using [black](https://github.com/psf/black). To run the formatter locally, run this command from the top level of the repo:
92
+
93
+ ```
94
+ black .
95
+ ```
96
+
97
+ ### Unit Tests
98
+
99
+ To run the unit tests, run:
100
+ ```bash
101
+ pytest
102
+ ```
103
+
104
+ ### Benchmarks
105
+
106
+ To run the benchmarks, run:
107
+ ```bash
108
+ pytest --benchmark-only
109
+ ```
110
+
111
+ ### Protobuf Code Generation
112
+
113
+ To generate the protobuf source files run the following from the root of the project. Ensure you have `protoc` installed.
114
+
115
+ ```bash
116
+ protoc --proto_path=./protobuf/ --python_out=devcycle_python_sdk/protobuf --pyi_out=devcycle_python_sdk/protobuf variableForUserParams.proto
117
+ ```
118
+
119
+ This will rebuild the `variableForUserParams_pb2.py` file. DO NOT edit this file directly.
@@ -0,0 +1,6 @@
1
+ # Simplify imports for the SDK entry point objects
2
+
3
+ from devcycle_python_sdk.options import DevCycleCloudOptions, DevCycleLocalOptions
4
+ from devcycle_python_sdk.devcycle_client import AbstractDevCycleClient
5
+ from devcycle_python_sdk.cloud_client import DevCycleCloudClient
6
+ from devcycle_python_sdk.local_client import DevCycleLocalClient
@@ -0,0 +1,11 @@
1
+ import math
2
+ import random
3
+
4
+
5
+ def exponential_backoff(attempt: int, base_delay: float) -> float:
6
+ """
7
+ Exponential backoff starting with 200ms +- 0...40ms jitter
8
+ """
9
+ delay = math.pow(2, attempt) * base_delay / 2.0
10
+ random_sum = delay * 0.1 * random.random()
11
+ return delay + random_sum
@@ -0,0 +1,145 @@
1
+ import logging
2
+ import time
3
+ from typing import Dict, List, Optional
4
+
5
+ import requests
6
+
7
+ from devcycle_python_sdk.api.backoff import exponential_backoff
8
+ from devcycle_python_sdk.options import DevCycleCloudOptions
9
+ from devcycle_python_sdk.exceptions import (
10
+ CloudClientError,
11
+ NotFoundError,
12
+ CloudClientUnauthorizedError,
13
+ )
14
+ from devcycle_python_sdk.models.event import DevCycleEvent
15
+ from devcycle_python_sdk.models.feature import Feature
16
+ from devcycle_python_sdk.models.user import DevCycleUser
17
+ from devcycle_python_sdk.models.variable import Variable
18
+ from devcycle_python_sdk.util.strings import slash_join
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class BucketingAPIClient:
24
+ def __init__(self, sdk_key: str, options: DevCycleCloudOptions):
25
+ self.sdk_key = sdk_key
26
+ self.options = options
27
+ self.session = requests.Session()
28
+ self.session.headers = {
29
+ "Authorization": sdk_key,
30
+ "Content-Type": "application/json",
31
+ "Accept": "application/json",
32
+ }
33
+ self.session.max_redirects = 0
34
+
35
+ def _url(self, *path_args: str) -> str:
36
+ return slash_join(self.options.bucketing_api_uri, "v1", *path_args)
37
+
38
+ def request(self, method: str, url: str, **kwargs) -> dict:
39
+ retries_remaining = self.options.request_retries + 1
40
+ timeout = self.options.request_timeout
41
+
42
+ query_params = {}
43
+ if self.options.enable_edge_db:
44
+ query_params["enableEdgeDB"] = "true"
45
+
46
+ attempts = 1
47
+ while retries_remaining > 0:
48
+ request_error: Optional[Exception] = None
49
+ try:
50
+ res: requests.Response = self.session.request(
51
+ method, url, params=query_params, timeout=timeout, **kwargs
52
+ )
53
+
54
+ if res.status_code == 401:
55
+ # Not a retryable error
56
+ raise CloudClientUnauthorizedError("Invalid SDK Key")
57
+ elif res.status_code == 404:
58
+ # Not a retryable error
59
+ raise NotFoundError(url)
60
+ elif 400 <= res.status_code < 500:
61
+ # Not a retryable error
62
+ raise CloudClientError(f"Bad request: HTTP {res.status_code}")
63
+ elif res.status_code >= 500:
64
+ # Retryable error
65
+ request_error = CloudClientError(
66
+ f"Server error: HTTP {res.status_code}"
67
+ )
68
+ except requests.exceptions.RequestException as e:
69
+ request_error = e
70
+
71
+ if not request_error:
72
+ break
73
+
74
+ logger.debug(
75
+ f"DevCycle cloud bucketing request failed (attempt {attempts}): {request_error}"
76
+ )
77
+ retries_remaining -= 1
78
+ if retries_remaining:
79
+ retry_delay = exponential_backoff(
80
+ attempts, self.options.retry_delay / 1000.0
81
+ )
82
+ time.sleep(retry_delay)
83
+ attempts += 1
84
+ continue
85
+
86
+ raise CloudClientError(message="Retries exceeded", cause=request_error)
87
+
88
+ data: dict = res.json()
89
+ return data
90
+
91
+ def variable(self, key: str, user: DevCycleUser) -> Variable:
92
+ data = self.request("POST", self._url("variables", key), json=user.to_json())
93
+
94
+ return Variable(
95
+ _id=data.get("_id"),
96
+ key=data.get("key", ""),
97
+ type=data.get("type", ""),
98
+ value=data.get("value"),
99
+ )
100
+
101
+ def variables(self, user: DevCycleUser) -> Dict[str, Variable]:
102
+ data = self.request("POST", self._url("variables"), json=user.to_json())
103
+
104
+ result: Dict[str, Variable] = {}
105
+ for key, value in data.items():
106
+ result[key] = Variable(
107
+ _id=str(value.get("_id")),
108
+ key=str(value.get("key")),
109
+ type=str(value.get("type")),
110
+ value=value.get("value"),
111
+ isDefaulted=None,
112
+ )
113
+
114
+ return result
115
+
116
+ def features(self, user: DevCycleUser) -> Dict[str, Feature]:
117
+ data = self.request("POST", self._url("features"), json=user.to_json())
118
+
119
+ result: Dict[str, Feature] = {}
120
+ for key, value in data.items():
121
+ result[key] = Feature(
122
+ _id=value.get("_id"),
123
+ key=value.get("key"),
124
+ type=value.get("type"),
125
+ _variation=value.get("_variation"),
126
+ variationKey=value.get("variationKey"),
127
+ variationName=value.get("variationName"),
128
+ evalReason=value.get("evalReason"),
129
+ )
130
+
131
+ return result
132
+
133
+ def track(self, user: DevCycleUser, events: List[DevCycleEvent]) -> str:
134
+ data = self.request(
135
+ "POST",
136
+ self._url("track"),
137
+ json={
138
+ "user": user.to_json(),
139
+ "events": [
140
+ event.to_json(use_bucketing_api_format=True) for event in events
141
+ ],
142
+ },
143
+ )
144
+ message = data.get("message", "")
145
+ return message
@@ -0,0 +1,118 @@
1
+ import logging
2
+ import time
3
+ from http import HTTPStatus
4
+ from typing import Optional, Tuple
5
+
6
+ import requests
7
+
8
+ from devcycle_python_sdk.api.backoff import exponential_backoff
9
+ from devcycle_python_sdk.options import DevCycleLocalOptions
10
+ from devcycle_python_sdk.exceptions import (
11
+ APIClientError,
12
+ NotFoundError,
13
+ APIClientUnauthorizedError,
14
+ )
15
+ from devcycle_python_sdk.util.strings import slash_join
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ConfigAPIClient:
21
+ def __init__(self, sdk_key: str, options: DevCycleLocalOptions):
22
+ self.sdk_key = sdk_key
23
+ self.options = options
24
+ self.session = requests.Session()
25
+ self.session.headers = {
26
+ "Content-Type": "application/json",
27
+ "Accept": "application/json",
28
+ }
29
+ self.session.max_redirects = 0
30
+ self.max_config_retries = 2
31
+ self.config_file_url = (
32
+ slash_join(
33
+ self.options.config_cdn_uri, "config", "v1", "server", self.sdk_key
34
+ )
35
+ + ".json"
36
+ )
37
+
38
+ def get_config(
39
+ self, config_etag: Optional[str] = None
40
+ ) -> Tuple[Optional[dict], Optional[str]]:
41
+ """
42
+ Get the config from the server. If the config_etag is provided, the server will only return the config if it
43
+ has changed since the last request. If the config hasn't changed, the server will return a 304 Not Modified
44
+ response.
45
+
46
+ :param config_etag: The etag of the last config request
47
+
48
+ :return: A tuple containing the config and the etag of the config. If the config hasn't changed since the last
49
+ request, the config will be None and the etag will be the same as the last request.
50
+ """
51
+ retries_remaining = self.max_config_retries
52
+ timeout = self.options.config_request_timeout_ms / 1000.0
53
+
54
+ url = self.config_file_url
55
+
56
+ headers = {}
57
+ if config_etag:
58
+ headers["If-None-Match"] = config_etag
59
+
60
+ attempts = 1
61
+ while retries_remaining > 0:
62
+ request_error: Optional[Exception] = None
63
+ try:
64
+ res: requests.Response = self.session.request(
65
+ "GET", url, params={}, timeout=timeout, headers=headers
66
+ )
67
+
68
+ if (
69
+ res.status_code == HTTPStatus.UNAUTHORIZED
70
+ or res.status_code == HTTPStatus.FORBIDDEN
71
+ ):
72
+ # Not a retryable error
73
+ raise APIClientUnauthorizedError("Invalid SDK Key")
74
+ elif res.status_code == HTTPStatus.NOT_MODIFIED:
75
+ # the config hasn't changed since the last request
76
+ # don't return anything
77
+ return None, config_etag
78
+ elif res.status_code == HTTPStatus.NOT_FOUND:
79
+ # Not a retryable error
80
+ raise NotFoundError(url)
81
+ elif (
82
+ HTTPStatus.BAD_REQUEST
83
+ <= res.status_code
84
+ < HTTPStatus.INTERNAL_SERVER_ERROR
85
+ ):
86
+ # Not a retryable error
87
+ raise APIClientError(f"Bad request: HTTP {res.status_code}")
88
+ elif res.status_code >= HTTPStatus.INTERNAL_SERVER_ERROR:
89
+ # Retryable error
90
+ request_error = APIClientError(
91
+ f"Server error: HTTP {res.status_code}"
92
+ )
93
+ else:
94
+ pass
95
+ except requests.exceptions.RequestException as e:
96
+ request_error = e
97
+
98
+ if not request_error:
99
+ break
100
+
101
+ logger.debug(
102
+ f"DevCycle config CDN request failed (attempt {attempts}): {request_error}"
103
+ )
104
+ retries_remaining -= 1
105
+ if retries_remaining:
106
+ retry_delay = exponential_backoff(
107
+ attempts, self.options.config_retry_delay_ms / 1000.0
108
+ )
109
+ time.sleep(retry_delay)
110
+ attempts += 1
111
+ continue
112
+
113
+ raise APIClientError(message="Retries exceeded", cause=request_error)
114
+
115
+ new_etag = res.headers.get("ETag", None)
116
+
117
+ data: dict = res.json()
118
+ return data, new_etag
@@ -0,0 +1,95 @@
1
+ import json
2
+ import logging
3
+ import time
4
+ from typing import Optional, List
5
+
6
+ import requests
7
+
8
+ from devcycle_python_sdk.api.backoff import exponential_backoff
9
+ from devcycle_python_sdk.options import DevCycleLocalOptions
10
+ from devcycle_python_sdk.exceptions import (
11
+ APIClientError,
12
+ NotFoundError,
13
+ APIClientUnauthorizedError,
14
+ )
15
+ from devcycle_python_sdk.models.event import UserEventsBatchRecord
16
+ from devcycle_python_sdk.util.strings import slash_join
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class EventAPIClient:
22
+ def __init__(self, sdk_key: str, options: DevCycleLocalOptions):
23
+ self.options = options
24
+ self.session = requests.Session()
25
+ self.session.headers = {
26
+ "Content-Type": "application/json",
27
+ "Accept": "application/json",
28
+ "Authorization": sdk_key,
29
+ }
30
+ self.session.max_redirects = 0
31
+ self.max_batch_retries = 0 # we don't retry events batches
32
+ self.batch_url = slash_join(self.options.events_api_uri, "v1/events/batch")
33
+
34
+ def publish_events(self, batch: List[UserEventsBatchRecord]) -> str:
35
+ """
36
+ Attempts to send a batch of events to the server
37
+ """
38
+ retries_remaining = self.max_batch_retries + 1
39
+ timeout = self.options.event_request_timeout_ms
40
+
41
+ payload_json = json.dumps(
42
+ {
43
+ "batch": [record.to_json() for record in batch],
44
+ }
45
+ )
46
+
47
+ attempts = 1
48
+ while retries_remaining > 0:
49
+ request_error: Optional[Exception] = None
50
+ try:
51
+ res: requests.Response = self.session.request(
52
+ "POST",
53
+ self.batch_url,
54
+ params={},
55
+ timeout=timeout,
56
+ data=payload_json,
57
+ )
58
+ if res.status_code == 401 or res.status_code == 403:
59
+ # Not a retryable error
60
+ raise APIClientUnauthorizedError("Invalid SDK Key")
61
+ elif res.status_code == 404:
62
+ # Not a retryable error - bad URL
63
+ raise NotFoundError(self.batch_url)
64
+ elif 400 <= res.status_code < 500:
65
+ # Not a retryable error
66
+ raise APIClientError(
67
+ f"Bad request: HTTP {res.status_code} - {res.text}"
68
+ )
69
+ elif res.status_code >= 500:
70
+ # Retryable error
71
+ request_error = APIClientError(
72
+ f"Server error: HTTP {res.status_code}"
73
+ )
74
+ except requests.exceptions.RequestException as e:
75
+ request_error = e
76
+
77
+ if not request_error:
78
+ break
79
+
80
+ logger.debug(
81
+ f"DevCycle event batch request failed (attempt {attempts}): {request_error}"
82
+ )
83
+ retries_remaining -= 1
84
+ if retries_remaining:
85
+ retry_delay = exponential_backoff(
86
+ attempts, self.options.event_retry_delay_ms / 1000.0
87
+ )
88
+ time.sleep(retry_delay)
89
+ attempts += 1
90
+ continue
91
+
92
+ raise APIClientError(message="Retries exceeded", cause=request_error)
93
+
94
+ data: dict = res.json()
95
+ return data.get("message", None)