comb-utils 0.1.9__tar.gz → 0.2.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.
- {comb_utils-0.1.9/src/comb_utils.egg-info → comb_utils-0.2.0}/PKG-INFO +9 -4
- {comb_utils-0.1.9 → comb_utils-0.2.0}/README.md +7 -3
- {comb_utils-0.1.9 → comb_utils-0.2.0}/setup.cfg +1 -1
- comb_utils-0.2.0/src/comb_utils/__init__.py +14 -0
- comb_utils-0.2.0/src/comb_utils/lib/__init__.py +13 -0
- comb_utils-0.2.0/src/comb_utils/lib/api_callers.py +444 -0
- comb_utils-0.2.0/src/comb_utils/lib/constants.py +14 -0
- {comb_utils-0.1.9 → comb_utils-0.2.0}/src/comb_utils/lib/docs.py +0 -1
- {comb_utils-0.1.9 → comb_utils-0.2.0/src/comb_utils.egg-info}/PKG-INFO +9 -4
- {comb_utils-0.1.9 → comb_utils-0.2.0}/src/comb_utils.egg-info/SOURCES.txt +2 -0
- comb_utils-0.1.9/src/comb_utils/__init__.py +0 -3
- comb_utils-0.1.9/src/comb_utils/lib/__init__.py +0 -3
- {comb_utils-0.1.9 → comb_utils-0.2.0}/LICENSE +0 -0
- {comb_utils-0.1.9 → comb_utils-0.2.0}/pyproject.toml +0 -0
- {comb_utils-0.1.9 → comb_utils-0.2.0}/src/comb_utils.egg-info/dependency_links.txt +0 -0
- {comb_utils-0.1.9 → comb_utils-0.2.0}/src/comb_utils.egg-info/requires.txt +0 -0
- {comb_utils-0.1.9 → comb_utils-0.2.0}/src/comb_utils.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: comb_utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Handy utils for Python projects.
|
|
5
5
|
Author: Kaleb Coberly
|
|
6
6
|
Maintainer-email: kaleb.coberly@gmail.com
|
|
@@ -40,13 +40,14 @@ Provides-Extra: test
|
|
|
40
40
|
Requires-Dist: coverage[toml]; extra == "test"
|
|
41
41
|
Requires-Dist: pytest; extra == "test"
|
|
42
42
|
Requires-Dist: pytest-cov; extra == "test"
|
|
43
|
+
Dynamic: license-file
|
|
43
44
|
|
|
44
45
|
# Comb Utils
|
|
45
46
|
|
|
46
|
-
## Summary
|
|
47
|
-
|
|
48
47
|
Some handy utils for Python projects. Repo is made from the `reference_package` template repo: https://github.com/crickets-and-comb/reference_package. See the docs: https://crickets-and-comb.github.io/shared_utils/.
|
|
49
48
|
|
|
49
|
+
This is a [Crickets and Comb](https://cricketsandcomb.org) resource.
|
|
50
|
+
|
|
50
51
|
## Structure
|
|
51
52
|
|
|
52
53
|
```bash
|
|
@@ -122,6 +123,8 @@ When making smaller commits, you might just want to run some of the smaller comm
|
|
|
122
123
|
|
|
123
124
|
$ make clean format full-qc full-test
|
|
124
125
|
|
|
126
|
+
#### Using act
|
|
127
|
+
|
|
125
128
|
As a final step, it's good practice to test run the workflow before opening a PR or pushing to an open PR. We don't want to waste GitHub runtime on a glitch that we could have caught before. You can use a make target for that:
|
|
126
129
|
|
|
127
130
|
$ make run-act
|
|
@@ -131,3 +134,5 @@ That will run `.github/workflows/CI_CD.yml`. But, you can also run any workflow
|
|
|
131
134
|
To use this tool, you'll need to have Docker installed and running on your machine: https://www.docker.com/. You'll also need to install `act` in your terminal:
|
|
132
135
|
|
|
133
136
|
$ brew install act
|
|
137
|
+
|
|
138
|
+
NOTE: To be more accurate, we've overridden `set-CI-CD-file` to create a local `CI_CD_act.yml` (which we ignore with Git) as a copy of `CI_CD.yml` and replace one of the workflow call URLs with a relative path. We use a relative path because otherwise `act` will not honor the overridden `full-test` make target and will run the shared version. That will fail because the shared `full-test` target includes running integration and e2e tests, which this repo does not include.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Comb Utils
|
|
2
2
|
|
|
3
|
-
## Summary
|
|
4
|
-
|
|
5
3
|
Some handy utils for Python projects. Repo is made from the `reference_package` template repo: https://github.com/crickets-and-comb/reference_package. See the docs: https://crickets-and-comb.github.io/shared_utils/.
|
|
6
4
|
|
|
5
|
+
This is a [Crickets and Comb](https://cricketsandcomb.org) resource.
|
|
6
|
+
|
|
7
7
|
## Structure
|
|
8
8
|
|
|
9
9
|
```bash
|
|
@@ -79,6 +79,8 @@ When making smaller commits, you might just want to run some of the smaller comm
|
|
|
79
79
|
|
|
80
80
|
$ make clean format full-qc full-test
|
|
81
81
|
|
|
82
|
+
#### Using act
|
|
83
|
+
|
|
82
84
|
As a final step, it's good practice to test run the workflow before opening a PR or pushing to an open PR. We don't want to waste GitHub runtime on a glitch that we could have caught before. You can use a make target for that:
|
|
83
85
|
|
|
84
86
|
$ make run-act
|
|
@@ -87,4 +89,6 @@ That will run `.github/workflows/CI_CD.yml`. But, you can also run any workflow
|
|
|
87
89
|
|
|
88
90
|
To use this tool, you'll need to have Docker installed and running on your machine: https://www.docker.com/. You'll also need to install `act` in your terminal:
|
|
89
91
|
|
|
90
|
-
$ brew install act
|
|
92
|
+
$ brew install act
|
|
93
|
+
|
|
94
|
+
NOTE: To be more accurate, we've overridden `set-CI-CD-file` to create a local `CI_CD_act.yml` (which we ignore with Git) as a copy of `CI_CD.yml` and replace one of the workflow call URLs with a relative path. We use a relative path because otherwise `act` will not honor the overridden `full-test` make target and will run the shared version. That will fail because the shared `full-test` target includes running integration and e2e tests, which this repo does not include.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Top-level init."""
|
|
2
|
+
|
|
3
|
+
from comb_utils.lib import (
|
|
4
|
+
BaseCaller,
|
|
5
|
+
BaseDeleteCaller,
|
|
6
|
+
BaseGetCaller,
|
|
7
|
+
BasePagedResponseGetter,
|
|
8
|
+
BasePostCaller,
|
|
9
|
+
DocString,
|
|
10
|
+
ErrorDocString,
|
|
11
|
+
concat_response_pages,
|
|
12
|
+
get_response_dict,
|
|
13
|
+
get_responses,
|
|
14
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""lib init."""
|
|
2
|
+
|
|
3
|
+
from comb_utils.lib.api_callers import (
|
|
4
|
+
BaseCaller,
|
|
5
|
+
BaseDeleteCaller,
|
|
6
|
+
BaseGetCaller,
|
|
7
|
+
BasePagedResponseGetter,
|
|
8
|
+
BasePostCaller,
|
|
9
|
+
concat_response_pages,
|
|
10
|
+
get_response_dict,
|
|
11
|
+
get_responses,
|
|
12
|
+
)
|
|
13
|
+
from comb_utils.lib.docs import DocString, ErrorDocString
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""Classes for making API calls."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from time import sleep
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
from requests.auth import HTTPBasicAuth
|
|
11
|
+
from typeguard import typechecked
|
|
12
|
+
|
|
13
|
+
from comb_utils.lib.constants import RateLimits
|
|
14
|
+
|
|
15
|
+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# TODO: https://github.com/crickets-and-comb/comb_utils/issues/38:
|
|
20
|
+
# Why are we using _set_url instead of the url property?
|
|
21
|
+
# Why are we using _set_request_call instead of the _request_call property?
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BaseCaller:
|
|
25
|
+
"""An abstract class for making API calls.
|
|
26
|
+
|
|
27
|
+
See :doc:`api_callers`.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
.. code:: python
|
|
31
|
+
|
|
32
|
+
class MyGetCaller(BaseCaller):
|
|
33
|
+
target_response_value
|
|
34
|
+
|
|
35
|
+
_min_wait_seconds: float = 0.1
|
|
36
|
+
# Initialize _wait_seconds and timeout as a class variable.
|
|
37
|
+
# Instances will adjust for class.
|
|
38
|
+
_wait_seconds: float = _min_wait_seconds
|
|
39
|
+
_timeout: float = 10
|
|
40
|
+
|
|
41
|
+
def _set_request_call(self):
|
|
42
|
+
self._request_call = requests.get
|
|
43
|
+
|
|
44
|
+
def _set_url(self):
|
|
45
|
+
self._url = "https://example.com/public/v0.2b/"
|
|
46
|
+
|
|
47
|
+
def _get_API_key(self) -> str | None:
|
|
48
|
+
# Optionally wrap your own API key retrieval function here.
|
|
49
|
+
return my_custom_key_retrieval_function()
|
|
50
|
+
|
|
51
|
+
def _handle_200(self):
|
|
52
|
+
super()._handle_200()
|
|
53
|
+
self.target_response_value = self.response_json["target_key"]
|
|
54
|
+
|
|
55
|
+
my_caller = MyCaller()
|
|
56
|
+
my_caller.call_api()
|
|
57
|
+
target_response_value = my_caller.target_response_value
|
|
58
|
+
|
|
59
|
+
.. important::
|
|
60
|
+
You must initialize _wait_seconds, _timeout, and _min_wait_seconds in child classes.
|
|
61
|
+
This allows child class instances to adjust the wait/timeout time for the child class.
|
|
62
|
+
|
|
63
|
+
.. warning::
|
|
64
|
+
There is a potential for this to run indefinitely for rate limiting and timeouts.
|
|
65
|
+
It handles them somewhat intelligently, but the assumption is that someone is watching
|
|
66
|
+
this run in the background and will stop it if it runs too long. It will eventually
|
|
67
|
+
at least crash the memory, depending on available memory, mean time to failure, and
|
|
68
|
+
time left in the universe.
|
|
69
|
+
|
|
70
|
+
.. note::
|
|
71
|
+
The `_set_request_call` and `_set_url` methods will be deprecated in favor of setting
|
|
72
|
+
the request call member at child class definition and passing the URL to `__init__`.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
# Set by object:
|
|
76
|
+
#: The JSON from the response.
|
|
77
|
+
response_json: dict[str, Any]
|
|
78
|
+
#: The response from the API call.
|
|
79
|
+
_response: requests.Response
|
|
80
|
+
|
|
81
|
+
# Must set in child class with _set*:
|
|
82
|
+
#: The requests call method. (get, post, etc.)
|
|
83
|
+
_request_call: Callable
|
|
84
|
+
#: The URL for the API call.
|
|
85
|
+
_url: str
|
|
86
|
+
|
|
87
|
+
# Must set in child class:
|
|
88
|
+
#: The timeout for the API call.
|
|
89
|
+
_timeout: float
|
|
90
|
+
#: The minimum wait time between API calls.
|
|
91
|
+
_min_wait_seconds: float
|
|
92
|
+
#: The wait time between API calls. (Adjusted by instances, at class level.)
|
|
93
|
+
_wait_seconds: float
|
|
94
|
+
|
|
95
|
+
# Optionally set in child class, to pass to _request_call if needed:
|
|
96
|
+
#: The kwargs to pass to the requests call.
|
|
97
|
+
_call_kwargs: dict[str, Any] = {}
|
|
98
|
+
#: The scalar to increase wait time on rate limiting.
|
|
99
|
+
_wait_increase_scalar: float = RateLimits.WAIT_INCREASE_SCALAR
|
|
100
|
+
#: The scalar to decrease wait time on success.
|
|
101
|
+
_wait_decrease_scalar: float = RateLimits.WAIT_DECREASE_SECONDS
|
|
102
|
+
|
|
103
|
+
@typechecked
|
|
104
|
+
def __init__(self) -> None: # noqa: ANN401
|
|
105
|
+
"""Initialize the BaseCaller object."""
|
|
106
|
+
self._set_request_call()
|
|
107
|
+
self._set_url()
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
@typechecked
|
|
111
|
+
def _set_request_call(self) -> None:
|
|
112
|
+
"""Set the requests call method.
|
|
113
|
+
|
|
114
|
+
requests.get, requests.post, etc.
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
NotImplementedError: If not implemented in child class.
|
|
118
|
+
"""
|
|
119
|
+
raise NotImplementedError
|
|
120
|
+
|
|
121
|
+
@abstractmethod
|
|
122
|
+
@typechecked
|
|
123
|
+
def _set_url(self) -> None:
|
|
124
|
+
"""Set the URL for the API call.
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
NotImplementedError: If not implemented in child class.
|
|
128
|
+
"""
|
|
129
|
+
raise NotImplementedError
|
|
130
|
+
|
|
131
|
+
@typechecked
|
|
132
|
+
def call_api(self) -> None:
|
|
133
|
+
"""The main method for making the API call.
|
|
134
|
+
|
|
135
|
+
Handle errors, parse response, and decrease class wait time on success.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
ValueError: If the response status code is not expected.
|
|
139
|
+
requests.exceptions.HTTPError: For non-rate-limiting errors.
|
|
140
|
+
"""
|
|
141
|
+
# Separated to allow for recursive calls on rate limiting.
|
|
142
|
+
self._call_api()
|
|
143
|
+
self._decrease_wait_time()
|
|
144
|
+
|
|
145
|
+
@typechecked
|
|
146
|
+
def _call_api(self) -> None:
|
|
147
|
+
"""Wait and make and handle the API call.
|
|
148
|
+
|
|
149
|
+
Wrapped separately to allow for recursive calls on rate limiting and timeout.
|
|
150
|
+
"""
|
|
151
|
+
sleep(type(self)._wait_seconds)
|
|
152
|
+
self._make_call()
|
|
153
|
+
self._raise_for_status()
|
|
154
|
+
self._parse_response()
|
|
155
|
+
|
|
156
|
+
@typechecked
|
|
157
|
+
def _make_call(self) -> None:
|
|
158
|
+
"""Make the API call."""
|
|
159
|
+
self._response = self._request_call(
|
|
160
|
+
url=self._url,
|
|
161
|
+
auth=HTTPBasicAuth(self._get_API_key(), ""),
|
|
162
|
+
timeout=self._timeout,
|
|
163
|
+
**self._call_kwargs,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
@typechecked
|
|
167
|
+
def _raise_for_status(self) -> None:
|
|
168
|
+
"""Handle error responses.
|
|
169
|
+
|
|
170
|
+
For 429 (rate limiting), increases wait time and recursively calls the API.
|
|
171
|
+
For timeout, increases timeout and recursively calls the API.
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
requests.exceptions.HTTPError: For non-rate-limiting errors.
|
|
175
|
+
requests.exceptions.Timeout: For timeouts.
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
self._response.raise_for_status()
|
|
179
|
+
except requests.exceptions.HTTPError as http_e:
|
|
180
|
+
if self._response.status_code == 429:
|
|
181
|
+
self._handle_429()
|
|
182
|
+
else:
|
|
183
|
+
self._handle_unknown_error(e=http_e)
|
|
184
|
+
except requests.exceptions.Timeout:
|
|
185
|
+
self._handle_timeout()
|
|
186
|
+
|
|
187
|
+
@typechecked
|
|
188
|
+
def _parse_response(self) -> None:
|
|
189
|
+
"""Parse the non-error reponse (200).
|
|
190
|
+
|
|
191
|
+
Raises:
|
|
192
|
+
ValueError: If the response status code is not expected.
|
|
193
|
+
"""
|
|
194
|
+
if self._response.status_code == 200:
|
|
195
|
+
self._handle_200()
|
|
196
|
+
elif self._response.status_code == 204:
|
|
197
|
+
self._handle_204()
|
|
198
|
+
elif self._response.status_code == 429:
|
|
199
|
+
# This is here as well as in the _raise_for_status method because there was a case
|
|
200
|
+
# when the status code was 429 but the response didn't raise.
|
|
201
|
+
self._handle_429()
|
|
202
|
+
else:
|
|
203
|
+
response_dict = get_response_dict(response=self._response)
|
|
204
|
+
raise ValueError(
|
|
205
|
+
f"Unexpected response {self._response.status_code}:\n{response_dict}"
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
@typechecked
|
|
209
|
+
def _get_API_key(self) -> str | None:
|
|
210
|
+
"""Get the API key.
|
|
211
|
+
|
|
212
|
+
Defaults to None, but can be overridden in child class.
|
|
213
|
+
"""
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
@typechecked
|
|
217
|
+
def _handle_429(self) -> None:
|
|
218
|
+
"""Handle a 429 response.
|
|
219
|
+
|
|
220
|
+
Inreases the class wait time and recursively calls the API.
|
|
221
|
+
"""
|
|
222
|
+
self._increase_wait_time()
|
|
223
|
+
logger.warning(f"Rate limited. Waiting {type(self)._wait_seconds} seconds to retry.")
|
|
224
|
+
self._call_api()
|
|
225
|
+
|
|
226
|
+
@typechecked
|
|
227
|
+
def _handle_timeout(self) -> None:
|
|
228
|
+
"""Handle a timeout response.
|
|
229
|
+
|
|
230
|
+
Increases the class timeout and recursively calls the API.
|
|
231
|
+
"""
|
|
232
|
+
self._increase_timeout()
|
|
233
|
+
response_dict = get_response_dict(response=self._response)
|
|
234
|
+
logger.warning(
|
|
235
|
+
f"Request timed out.\n{response_dict}"
|
|
236
|
+
f"\nTrying again with longer timeout: {type(self)._timeout} seconds."
|
|
237
|
+
)
|
|
238
|
+
self._call_api()
|
|
239
|
+
|
|
240
|
+
@typechecked
|
|
241
|
+
def _handle_200(self) -> None:
|
|
242
|
+
"""Handle a 200 response.
|
|
243
|
+
|
|
244
|
+
Just gets the JSON from the response and sets it to `response_json`.
|
|
245
|
+
"""
|
|
246
|
+
self.response_json = self._response.json()
|
|
247
|
+
|
|
248
|
+
@typechecked
|
|
249
|
+
def _handle_204(self) -> None:
|
|
250
|
+
"""Handle a 204 response.
|
|
251
|
+
|
|
252
|
+
Just sets `response_json` to an empty dictionary.
|
|
253
|
+
"""
|
|
254
|
+
self.response_json = {}
|
|
255
|
+
|
|
256
|
+
@typechecked
|
|
257
|
+
def _handle_unknown_error(self, e: Exception) -> None:
|
|
258
|
+
"""Handle an unknown error response.
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
Exception: The original error.
|
|
262
|
+
"""
|
|
263
|
+
response_dict = get_response_dict(response=self._response)
|
|
264
|
+
err_msg = f"Got {self._response.status_code} response:\n{response_dict}"
|
|
265
|
+
raise requests.exceptions.HTTPError(err_msg) from e
|
|
266
|
+
|
|
267
|
+
@typechecked
|
|
268
|
+
def _decrease_wait_time(self) -> None:
|
|
269
|
+
"""Decrease the wait time between API calls for whole class."""
|
|
270
|
+
cls = type(self)
|
|
271
|
+
cls._wait_seconds = max(
|
|
272
|
+
cls._wait_seconds * self._wait_decrease_scalar, cls._min_wait_seconds
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
@typechecked
|
|
276
|
+
def _increase_wait_time(self) -> None:
|
|
277
|
+
"""Increase the wait time between API calls for whole class."""
|
|
278
|
+
cls = type(self)
|
|
279
|
+
cls._wait_seconds = cls._wait_seconds * self._wait_increase_scalar
|
|
280
|
+
|
|
281
|
+
@typechecked
|
|
282
|
+
def _increase_timeout(self) -> None:
|
|
283
|
+
"""Increase the timeout for the API call for whole class."""
|
|
284
|
+
cls = type(self)
|
|
285
|
+
cls._timeout = cls._timeout * self._wait_increase_scalar
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class BaseGetCaller(BaseCaller):
|
|
289
|
+
"""A base class for making GET API calls.
|
|
290
|
+
|
|
291
|
+
Presets the timeout, initial wait time, and requests method.
|
|
292
|
+
"""
|
|
293
|
+
|
|
294
|
+
_timeout: float = RateLimits.READ_TIMEOUT_SECONDS
|
|
295
|
+
_min_wait_seconds: float = RateLimits.READ_SECONDS
|
|
296
|
+
_wait_seconds: float = _min_wait_seconds
|
|
297
|
+
|
|
298
|
+
@typechecked
|
|
299
|
+
def _set_request_call(self) -> None:
|
|
300
|
+
"""Set the requests call method to `requests.get`."""
|
|
301
|
+
self._request_call = requests.get
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
class BasePostCaller(BaseCaller):
|
|
305
|
+
"""A base class for making POST API calls.
|
|
306
|
+
|
|
307
|
+
Presets the timeout, initial wait time, and requests method.
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
_timeout: float = RateLimits.WRITE_TIMEOUT_SECONDS
|
|
311
|
+
_min_wait_seconds: float = RateLimits.WRITE_SECONDS
|
|
312
|
+
_wait_seconds: float = _min_wait_seconds
|
|
313
|
+
|
|
314
|
+
@typechecked
|
|
315
|
+
def _set_request_call(self) -> None:
|
|
316
|
+
"""Set the requests call method to `requests.post`."""
|
|
317
|
+
self._request_call = requests.post
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
class BaseDeleteCaller(BasePostCaller):
|
|
321
|
+
"""A base class for making DELETE API calls.
|
|
322
|
+
|
|
323
|
+
Presets the timeout, initial wait time, and requests method.
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
@typechecked
|
|
327
|
+
def _set_request_call(self) -> None:
|
|
328
|
+
"""Set the requests call method to `requests.delete`."""
|
|
329
|
+
self._request_call = requests.delete
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class BasePagedResponseGetter(BaseGetCaller):
|
|
333
|
+
"""Class for getting paged responses."""
|
|
334
|
+
|
|
335
|
+
#: The nextPageToken returned, but called salsa to avoid bandit.
|
|
336
|
+
next_page_salsa: str | None
|
|
337
|
+
|
|
338
|
+
#: The URL for the page.
|
|
339
|
+
_page_url: str
|
|
340
|
+
|
|
341
|
+
@typechecked
|
|
342
|
+
def __init__(self, page_url: str) -> None:
|
|
343
|
+
"""Initialize the BasePagedResponseGetter object.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
page_url: The URL for the page. (Optionally contains nextPageToken.)
|
|
347
|
+
"""
|
|
348
|
+
self._page_url = page_url
|
|
349
|
+
super().__init__()
|
|
350
|
+
|
|
351
|
+
@typechecked
|
|
352
|
+
def _set_url(self) -> None:
|
|
353
|
+
"""Set the URL for the API call to the `page_url`."""
|
|
354
|
+
self._url = self._page_url
|
|
355
|
+
|
|
356
|
+
@typechecked
|
|
357
|
+
def _handle_200(self) -> None:
|
|
358
|
+
"""Handle a 200 response.
|
|
359
|
+
|
|
360
|
+
Sets `next_page_salsa` to the nextPageToken.
|
|
361
|
+
"""
|
|
362
|
+
super()._handle_200()
|
|
363
|
+
self.next_page_salsa = self.response_json.get("nextPageToken", None)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@typechecked
|
|
367
|
+
def get_response_dict(response: requests.Response) -> dict[str, Any]:
|
|
368
|
+
"""Safely handle a response that may not be JSON.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
response: The response from the API call.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
A dictionary containing the response data.
|
|
375
|
+
"""
|
|
376
|
+
try:
|
|
377
|
+
response_dict: dict = response.json()
|
|
378
|
+
except Exception as e:
|
|
379
|
+
response_dict = {
|
|
380
|
+
"reason": response.reason,
|
|
381
|
+
"additional_notes": "No-JSON response.",
|
|
382
|
+
"No-JSON response exception:": str(e),
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return response_dict
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# TODO: Pass params instead of forming URL first. ("params", not "json")
|
|
389
|
+
# (Would need to then grab params URL for next page, or just add nextpage to params?)
|
|
390
|
+
# https://github.com/crickets-and-comb/bfb_delivery/issues/61
|
|
391
|
+
# TODO: bfb_delivery issue 59, comb_utils issue 24: move above issue to comb_utils.
|
|
392
|
+
# TODO: bfb_delivery issue 59, comb_utils issue 24:
|
|
393
|
+
# Switch to default getter if key retriever can be empty.
|
|
394
|
+
@typechecked
|
|
395
|
+
def get_responses(
|
|
396
|
+
url: str, paged_response_class: type[BasePagedResponseGetter]
|
|
397
|
+
) -> list[dict[str, Any]]:
|
|
398
|
+
"""Get all responses from a paginated API endpoint.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
url: The base URL of the API endpoint.
|
|
402
|
+
paged_response_class: The class used to get the paginated response.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
A list of dictionaries containing the responses from all pages.
|
|
406
|
+
"""
|
|
407
|
+
# Calling the token salsa to trick bandit into ignoring what looks like a hardcoded token.
|
|
408
|
+
next_page_salsa = ""
|
|
409
|
+
next_page_cookie = ""
|
|
410
|
+
responses = []
|
|
411
|
+
|
|
412
|
+
while next_page_salsa is not None:
|
|
413
|
+
paged_response_getter = paged_response_class(page_url=url + str(next_page_cookie))
|
|
414
|
+
paged_response_getter.call_api()
|
|
415
|
+
|
|
416
|
+
stops = paged_response_getter._response.json()
|
|
417
|
+
responses.append(stops)
|
|
418
|
+
next_page_salsa = paged_response_getter.next_page_salsa
|
|
419
|
+
|
|
420
|
+
if next_page_salsa:
|
|
421
|
+
salsa_prefix = "?" if "?" not in url else "&"
|
|
422
|
+
next_page_cookie = f"{salsa_prefix}pageToken={next_page_salsa}"
|
|
423
|
+
|
|
424
|
+
return responses
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@typechecked
|
|
428
|
+
def concat_response_pages(
|
|
429
|
+
page_list: list[dict[str, Any]], data_key: str
|
|
430
|
+
) -> list[dict[str, Any]]:
|
|
431
|
+
"""Extract and concatenate the data lists from response pages.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
page_list: A list of response page dictionaries.
|
|
435
|
+
data_key: The key to extract the data from each page.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
A list of dictionaries containing the data from each page.
|
|
439
|
+
"""
|
|
440
|
+
data_list = []
|
|
441
|
+
for page in page_list:
|
|
442
|
+
data_list += page[data_key]
|
|
443
|
+
|
|
444
|
+
return data_list
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Constants for the comb_utils library."""
|
|
2
|
+
|
|
3
|
+
from typing import Final
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RateLimits:
|
|
7
|
+
"""Default rate limits for :doc:`api_callers`."""
|
|
8
|
+
|
|
9
|
+
READ_TIMEOUT_SECONDS: Final[float] = 10
|
|
10
|
+
READ_SECONDS: Final[float] = 0.1
|
|
11
|
+
WAIT_DECREASE_SECONDS: Final[float] = 0.6
|
|
12
|
+
WAIT_INCREASE_SCALAR: Final[float] = 2
|
|
13
|
+
WRITE_SECONDS: Final[float] = 0.2
|
|
14
|
+
WRITE_TIMEOUT_SECONDS: Final[float] = 10
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: comb_utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Handy utils for Python projects.
|
|
5
5
|
Author: Kaleb Coberly
|
|
6
6
|
Maintainer-email: kaleb.coberly@gmail.com
|
|
@@ -40,13 +40,14 @@ Provides-Extra: test
|
|
|
40
40
|
Requires-Dist: coverage[toml]; extra == "test"
|
|
41
41
|
Requires-Dist: pytest; extra == "test"
|
|
42
42
|
Requires-Dist: pytest-cov; extra == "test"
|
|
43
|
+
Dynamic: license-file
|
|
43
44
|
|
|
44
45
|
# Comb Utils
|
|
45
46
|
|
|
46
|
-
## Summary
|
|
47
|
-
|
|
48
47
|
Some handy utils for Python projects. Repo is made from the `reference_package` template repo: https://github.com/crickets-and-comb/reference_package. See the docs: https://crickets-and-comb.github.io/shared_utils/.
|
|
49
48
|
|
|
49
|
+
This is a [Crickets and Comb](https://cricketsandcomb.org) resource.
|
|
50
|
+
|
|
50
51
|
## Structure
|
|
51
52
|
|
|
52
53
|
```bash
|
|
@@ -122,6 +123,8 @@ When making smaller commits, you might just want to run some of the smaller comm
|
|
|
122
123
|
|
|
123
124
|
$ make clean format full-qc full-test
|
|
124
125
|
|
|
126
|
+
#### Using act
|
|
127
|
+
|
|
125
128
|
As a final step, it's good practice to test run the workflow before opening a PR or pushing to an open PR. We don't want to waste GitHub runtime on a glitch that we could have caught before. You can use a make target for that:
|
|
126
129
|
|
|
127
130
|
$ make run-act
|
|
@@ -131,3 +134,5 @@ That will run `.github/workflows/CI_CD.yml`. But, you can also run any workflow
|
|
|
131
134
|
To use this tool, you'll need to have Docker installed and running on your machine: https://www.docker.com/. You'll also need to install `act` in your terminal:
|
|
132
135
|
|
|
133
136
|
$ brew install act
|
|
137
|
+
|
|
138
|
+
NOTE: To be more accurate, we've overridden `set-CI-CD-file` to create a local `CI_CD_act.yml` (which we ignore with Git) as a copy of `CI_CD.yml` and replace one of the workflow call URLs with a relative path. We use a relative path because otherwise `act` will not honor the overridden `full-test` make target and will run the shared version. That will fail because the shared `full-test` target includes running integration and e2e tests, which this repo does not include.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|