syneto_api 0.2.20__tar.gz → 0.2.22__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.
- {syneto_api-0.2.20 → syneto_api-0.2.22}/PKG-INFO +2 -1
- {syneto_api-0.2.20 → syneto_api-0.2.22}/pyproject.toml +2 -1
- {syneto_api-0.2.20 → syneto_api-0.2.22}/setup.py +3 -2
- {syneto_api-0.2.20 → syneto_api-0.2.22}/src/syneto_api/__init__.py +2 -2
- syneto_api-0.2.22/src/syneto_api/api_client.py +301 -0
- {syneto_api-0.2.20 → syneto_api-0.2.22}/src/syneto_api/auth_api.py +2 -7
- syneto_api-0.2.20/src/syneto_api/api_client.py +0 -192
- {syneto_api-0.2.20 → syneto_api-0.2.22}/README.md +0 -0
- {syneto_api-0.2.20 → syneto_api-0.2.22}/src/syneto_api/central_api.py +0 -0
- {syneto_api-0.2.20 → syneto_api-0.2.22}/src/syneto_api/jobs_api.py +0 -0
- {syneto_api-0.2.20 → syneto_api-0.2.22}/src/syneto_api/protection_api.py +0 -0
- {syneto_api-0.2.20 → syneto_api-0.2.22}/src/syneto_api/settings.py +0 -0
- {syneto_api-0.2.20 → syneto_api-0.2.22}/src/syneto_api/storage_api.py +0 -0
- {syneto_api-0.2.20 → syneto_api-0.2.22}/src/syneto_api/virt_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: syneto-api
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.22
|
|
4
4
|
Summary: Syneto Client API library
|
|
5
5
|
Home-page: https://github.com/SynetoNet/syneto-api
|
|
6
6
|
License: Proprietary
|
|
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.6
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.7
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Requires-Dist: aiohttp[speedups] (>=3.7,<4.0)
|
|
15
16
|
Requires-Dist: inflection (>=0.5,<0.6)
|
|
16
17
|
Requires-Dist: python-dotenv (>=0.16,<0.17)
|
|
17
18
|
Requires-Dist: requests[secure] (>=2.25,<3.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "syneto_api"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.22"
|
|
4
4
|
description = "Syneto Client API library"
|
|
5
5
|
authors = ["Your Name <you@example.com>"]
|
|
6
6
|
license = "Proprietary"
|
|
@@ -13,6 +13,7 @@ python = "^3.6"
|
|
|
13
13
|
python-dotenv = "^0.16"
|
|
14
14
|
requests = {extras = ["secure"], version = "^2.25"}
|
|
15
15
|
inflection = "^0.5"
|
|
16
|
+
aiohttp = {version = "^3.7", extras = ["speedups"]}
|
|
16
17
|
|
|
17
18
|
[tool.poetry.dev-dependencies]
|
|
18
19
|
pytest = "^6.2.2"
|
|
@@ -11,13 +11,14 @@ package_data = \
|
|
|
11
11
|
{'': ['*']}
|
|
12
12
|
|
|
13
13
|
install_requires = \
|
|
14
|
-
['
|
|
14
|
+
['aiohttp[speedups]>=3.7,<4.0',
|
|
15
|
+
'inflection>=0.5,<0.6',
|
|
15
16
|
'python-dotenv>=0.16,<0.17',
|
|
16
17
|
'requests[secure]>=2.25,<3.0']
|
|
17
18
|
|
|
18
19
|
setup_kwargs = {
|
|
19
20
|
'name': 'syneto-api',
|
|
20
|
-
'version': '0.2.
|
|
21
|
+
'version': '0.2.22',
|
|
21
22
|
'description': 'Syneto Client API library',
|
|
22
23
|
'long_description': '# Syneto API\n\nSyneto Client API library: authentication, storage, virtualization and protection\n\n# Installation\n\n```\n$ pip install syneto-api\n```\n\n# Basic Usage\n\n```\nfrom syneto_api import Authentication, Virtualization, Storage, Protection\n\nauth_api = Authentication(url_base="https://syneto-instance-ip-address/api/auth", insecure_ssl=True)\nresponse = auth_api.login(username="admin", password="admin")\njwt = response[\'jwt\']\n\nvirt_api = Virtualization(url_base="https://syneto-instance-ip-address/api/virtualization", insecure_ssl=True)\nvirt_api.set_auth_jwt(jwt)\nprint(virt_api.get_vms())\n\nstorage_api = Storage(url_base="https://syneto-instance-ip-address/api/storage", insecure_ssl=True)\nstorage_api.set_auth_jwt(jwt)\nprint(storage_api.get_pools())\n```\n\n# Environment Variables\n\nFor conveninence, the base urls for the api endpoints are also accepted as environment variables, please see below.\n\n```\nAUTH_SERVICE=https://syneto-instance-ip-address/api/auth\nVIRTUALIZATION_SERVICE=https://syneto-instance-ip-address/api/virtualization\nSTORAGE_SERVICE=https://syneto-instance-ip-address/api/storage\nPROTECTION_SERVICE=https://syneto-instance-ip-address/api/protection\n```\n\nIf you are using self-signed SSL certificates, set the following env. so that the http request library does not perform ssl verification. \n\n```\nALLOW_INSECURE_SSL=True\n```\n',
|
|
23
24
|
'author': 'Your Name',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from .api_client import APIException
|
|
1
|
+
from .api_client import APIException, APIClientBase
|
|
2
2
|
from .auth_api import Authentication
|
|
3
3
|
from .virt_api import Virtualization
|
|
4
4
|
from .storage_api import Storage
|
|
5
5
|
from .protection_api import Protection
|
|
6
6
|
from .jobs_api import Jobs
|
|
7
|
-
from .central_api import Central
|
|
7
|
+
from .central_api import Central
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import asyncio, aiohttp
|
|
2
|
+
import os
|
|
3
|
+
import inflection
|
|
4
|
+
import requests
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import logging
|
|
7
|
+
import traceback
|
|
8
|
+
|
|
9
|
+
from .settings import get_env_flag
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def make_url(url_base, *path_args, query_args: dict = None, **path_kwargs):
|
|
15
|
+
path_args = [urllib.parse.quote(v, safe="") for v in path_args]
|
|
16
|
+
path_kwargs = {k: urllib.parse.quote(v, safe="") for k, v in path_kwargs.items()}
|
|
17
|
+
path = url_base.format(*path_args, **path_kwargs)
|
|
18
|
+
|
|
19
|
+
if query_args is None or len(query_args) == 0:
|
|
20
|
+
return path
|
|
21
|
+
query_string = urllib.parse.urlencode(query_args, doseq=True, quote_via=urllib.parse.quote)
|
|
22
|
+
return f"{path}?{query_string}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class APIException(Exception):
|
|
26
|
+
def __init__(
|
|
27
|
+
self, url: str, status_code: int = 0, message: str = None, request_body: dict = None, response=None,
|
|
28
|
+
):
|
|
29
|
+
self.url = url
|
|
30
|
+
self.status_code = status_code
|
|
31
|
+
self.request_body = request_body
|
|
32
|
+
self.response = response
|
|
33
|
+
|
|
34
|
+
if not message and self.status_code and self.response is not None:
|
|
35
|
+
message = f"API {self.url} returned HTTP {self.status_code}: {self.response.text}"
|
|
36
|
+
elif not message:
|
|
37
|
+
message = f"API Error: {self.url}"
|
|
38
|
+
|
|
39
|
+
super().__init__(message)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class APIClientBase:
|
|
43
|
+
SUCCESS_CODES = {
|
|
44
|
+
"GET": [200],
|
|
45
|
+
"POST": [200, 201, 202],
|
|
46
|
+
"PUT": [200, 202],
|
|
47
|
+
"PATCH": [200, 202],
|
|
48
|
+
"DELETE": [200, 202, 204],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def __init__(self, url_base=None, default_protocol="http", insecure_ssl=None):
|
|
52
|
+
self.url_base = url_base
|
|
53
|
+
protocols = ["http://", "https://"]
|
|
54
|
+
if not any(self.url_base.lower().startswith(p) for p in protocols):
|
|
55
|
+
self.url_base = f"{default_protocol}://{self.url_base}"
|
|
56
|
+
|
|
57
|
+
self.url_base = self.url_base.rstrip("/")
|
|
58
|
+
|
|
59
|
+
self.headers = {"accept": "application/json"}
|
|
60
|
+
self._verify_ssl_cert = True
|
|
61
|
+
self._force_json = True
|
|
62
|
+
if insecure_ssl is not None:
|
|
63
|
+
self.set_insecure_ssl(insecure_ssl)
|
|
64
|
+
elif get_env_flag("ALLOW_INSECURE_SSL"):
|
|
65
|
+
self.set_insecure_ssl(True)
|
|
66
|
+
|
|
67
|
+
self._use_asyncio = True
|
|
68
|
+
|
|
69
|
+
def _request(
|
|
70
|
+
self,
|
|
71
|
+
method: str,
|
|
72
|
+
relative_url: str,
|
|
73
|
+
*args,
|
|
74
|
+
query_args: dict = None,
|
|
75
|
+
body: dict = None,
|
|
76
|
+
success_codes: list = None,
|
|
77
|
+
**kwargs,
|
|
78
|
+
):
|
|
79
|
+
url = self._make_url(relative_url, *args, query_args=query_args, **kwargs)
|
|
80
|
+
error_subject = get_human_readable_entry_point(self)
|
|
81
|
+
|
|
82
|
+
if not success_codes:
|
|
83
|
+
success_codes = APIClientBase.SUCCESS_CODES[method]
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
if self._use_asyncio and is_event_loop_available():
|
|
87
|
+
logger.debug(f"Calling {method} {url} with aiohttp lib")
|
|
88
|
+
loop = asyncio.get_event_loop()
|
|
89
|
+
return loop.run_until_complete(self._async_request(method, url, body, success_codes, error_subject))
|
|
90
|
+
else:
|
|
91
|
+
logger.debug(f"Calling {method} {url} with requests lib")
|
|
92
|
+
return self._sync_request(method, url, body, success_codes, error_subject)
|
|
93
|
+
|
|
94
|
+
except APIException as e:
|
|
95
|
+
raise
|
|
96
|
+
|
|
97
|
+
except (aiohttp.ServerTimeoutError, requests.Timeout) as e:
|
|
98
|
+
logger.warning(f"Failed {method} request to {url}, network timeout: {str(e)}")
|
|
99
|
+
error = self._append_url_if_not_local_service("network timeout")
|
|
100
|
+
raise APIException(url=url, message=f"{error_subject} has failed ({error})", request_body=body) from e
|
|
101
|
+
|
|
102
|
+
except (aiohttp.ClientSSLError, requests.exceptions.SSLError) as e:
|
|
103
|
+
logger.warning(f"Failed {method} request to {url}, ssl error: {str(e)}")
|
|
104
|
+
error = self._append_url_if_not_local_service("ssl error")
|
|
105
|
+
raise APIException(url=url, message=f"{error_subject} has failed ({error})", request_body=body) from e
|
|
106
|
+
|
|
107
|
+
except (aiohttp.ClientConnectionError, requests.ConnectionError) as e:
|
|
108
|
+
logger.warning(f"Failed {method} request to {url}, connect error: {str(e)}")
|
|
109
|
+
error = self._append_url_if_not_local_service("network connect error")
|
|
110
|
+
raise APIException(url=url, message=f"{error_subject} has failed ({error})", request_body=body) from e
|
|
111
|
+
|
|
112
|
+
except (aiohttp.ClientError, requests.RequestException) as e:
|
|
113
|
+
logger.warning(f"Failed {method} request to {url}, network error: {str(e)}")
|
|
114
|
+
error = self._append_url_if_not_local_service("network error")
|
|
115
|
+
raise APIException(url=url, message=f"{error_subject} has failed ({error})", request_body=body) from e
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
logger.warning(f"Failed {method} request to {url}: {str(e)}")
|
|
119
|
+
error = self._append_url_if_not_local_service(str(e) or type(e).__name__)
|
|
120
|
+
raise APIException(url=url, message=f"{error_subject} has failed ({error})", request_body=body) from e
|
|
121
|
+
|
|
122
|
+
async def _async_request(self, method: str, url: str, body: dict, success_codes: list, error_subject: str):
|
|
123
|
+
headers = None
|
|
124
|
+
if self.headers is not None and len(self.headers) > 0:
|
|
125
|
+
headers = self.headers
|
|
126
|
+
|
|
127
|
+
method_args = {}
|
|
128
|
+
if not self._verify_ssl_cert:
|
|
129
|
+
method_args["ssl"] = False
|
|
130
|
+
if body is not None:
|
|
131
|
+
method_args["json"] = body
|
|
132
|
+
|
|
133
|
+
async with aiohttp.ClientSession(headers=headers) as session:
|
|
134
|
+
request_methods = {
|
|
135
|
+
"GET": session.get,
|
|
136
|
+
"POST": session.post,
|
|
137
|
+
"PUT": session.put,
|
|
138
|
+
"PATCH": session.patch,
|
|
139
|
+
"DELETE": session.delete,
|
|
140
|
+
}
|
|
141
|
+
request_method = request_methods[method]
|
|
142
|
+
async with request_method(url, **method_args) as response:
|
|
143
|
+
is_json = response.headers.get("content-type") == "application/json"
|
|
144
|
+
|
|
145
|
+
if response.status in success_codes:
|
|
146
|
+
logger.debug(f"Received {response.status} from {method} {url}")
|
|
147
|
+
if is_json or self._force_json:
|
|
148
|
+
return await response.json()
|
|
149
|
+
else:
|
|
150
|
+
return await response.read()
|
|
151
|
+
|
|
152
|
+
logger.info(f"Received {response.status} from {method} {url}")
|
|
153
|
+
error = get_http_code_error_message(response.status)
|
|
154
|
+
raise APIException(
|
|
155
|
+
url=url,
|
|
156
|
+
status_code=response.status,
|
|
157
|
+
message=f"{error_subject} has failed ({error})",
|
|
158
|
+
request_body=body,
|
|
159
|
+
response=response,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def _sync_request(self, method: str, url: str, body: dict, success_codes: list, error_subject: str):
|
|
163
|
+
method_func = self._get_method_func(method)
|
|
164
|
+
method_args = {"verify": self._verify_ssl_cert}
|
|
165
|
+
if self.headers is not None and len(self.headers) > 0:
|
|
166
|
+
method_args["headers"] = self.headers
|
|
167
|
+
|
|
168
|
+
if body is not None:
|
|
169
|
+
method_args["json"] = body
|
|
170
|
+
|
|
171
|
+
response = method_func(url, timeout=60, **method_args)
|
|
172
|
+
is_json = "json" in response.headers.get("content-type", "").lower()
|
|
173
|
+
if response.status_code in success_codes:
|
|
174
|
+
logger.debug(f"Received {response.status_code} from {method} {url}")
|
|
175
|
+
if is_json or self._force_json:
|
|
176
|
+
return response.json()
|
|
177
|
+
else:
|
|
178
|
+
return response.content
|
|
179
|
+
|
|
180
|
+
logger.info(f"Received {response.status_code} from {method} {url}")
|
|
181
|
+
error = get_http_code_error_message(response.status_code)
|
|
182
|
+
raise APIException(
|
|
183
|
+
url=url,
|
|
184
|
+
status_code=response.status_code,
|
|
185
|
+
message=f"{error_subject} has failed ({error})",
|
|
186
|
+
request_body=body,
|
|
187
|
+
response=response,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def _make_url(self, relative_url: str, *args, query_args: dict = None, **kwargs):
|
|
191
|
+
relative_url = relative_url.lstrip("/")
|
|
192
|
+
return make_url(self.url_base + "/" + relative_url, *args, query_args=query_args, **kwargs)
|
|
193
|
+
|
|
194
|
+
def _is_local_service(self):
|
|
195
|
+
return "default.svc.cluster.local" in self.url_base.lower()
|
|
196
|
+
|
|
197
|
+
def _append_url_if_not_local_service(self, message):
|
|
198
|
+
if self._is_local_service():
|
|
199
|
+
return message
|
|
200
|
+
else:
|
|
201
|
+
hostname = self._get_hostname()
|
|
202
|
+
return f"{message}: {hostname}"
|
|
203
|
+
|
|
204
|
+
def _get_hostname(self):
|
|
205
|
+
parsed_uri = urllib.parse.urlparse(self.url_base)
|
|
206
|
+
hostname = parsed_uri.netloc
|
|
207
|
+
if ":" in hostname:
|
|
208
|
+
hostname, port = parsed_uri.netloc.split(":")[:2]
|
|
209
|
+
return hostname
|
|
210
|
+
|
|
211
|
+
def _get_method_func(self, method):
|
|
212
|
+
requests_methods = {
|
|
213
|
+
"GET": requests.get,
|
|
214
|
+
"POST": requests.post,
|
|
215
|
+
"PUT": requests.put,
|
|
216
|
+
"PATCH": requests.patch,
|
|
217
|
+
"DELETE": requests.delete,
|
|
218
|
+
}
|
|
219
|
+
return requests_methods[method]
|
|
220
|
+
|
|
221
|
+
def get_request(
|
|
222
|
+
self, relative_url: str, *url_path_args, query_args: dict = None, success_codes: list = None, **url_path_kwargs,
|
|
223
|
+
):
|
|
224
|
+
return self._request(
|
|
225
|
+
"GET", relative_url, *url_path_args, query_args=query_args, success_codes=success_codes, **url_path_kwargs,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def delete_request(self, *args, **kwargs):
|
|
229
|
+
# Same args as get_request
|
|
230
|
+
return self._request("DELETE", *args, **kwargs)
|
|
231
|
+
|
|
232
|
+
def post_request(
|
|
233
|
+
self,
|
|
234
|
+
relative_url: str,
|
|
235
|
+
*url_path_args,
|
|
236
|
+
query_args: dict = None,
|
|
237
|
+
body: dict = None,
|
|
238
|
+
success_codes: list = None,
|
|
239
|
+
**url_path_kwargs,
|
|
240
|
+
):
|
|
241
|
+
return self._request(
|
|
242
|
+
"POST",
|
|
243
|
+
relative_url,
|
|
244
|
+
*url_path_args,
|
|
245
|
+
query_args=query_args,
|
|
246
|
+
body=body,
|
|
247
|
+
success_codes=success_codes,
|
|
248
|
+
**url_path_kwargs,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def put_request(self, *args, **kwargs):
|
|
252
|
+
# Same args as post_request
|
|
253
|
+
return self._request("PUT", *args, **kwargs)
|
|
254
|
+
|
|
255
|
+
def patch_request(self, *args, **kwargs):
|
|
256
|
+
# Same args as post_request
|
|
257
|
+
return self._request("PATCH", *args, **kwargs)
|
|
258
|
+
|
|
259
|
+
def set_insecure_ssl(self, insecure=True):
|
|
260
|
+
self._verify_ssl_cert = not insecure
|
|
261
|
+
|
|
262
|
+
def set_auth_jwt(self, jwt):
|
|
263
|
+
self.headers["Authorization"] = f"Bearer {jwt}"
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def get_http_code_error_message(status_code):
|
|
267
|
+
if status_code == 404:
|
|
268
|
+
return "object nout found"
|
|
269
|
+
if status_code == 401:
|
|
270
|
+
return "unauthorized"
|
|
271
|
+
if status_code == 403:
|
|
272
|
+
return "forbidden"
|
|
273
|
+
return f"{status_code}"
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def get_object_entry_method_name(obj):
|
|
277
|
+
frames = traceback.StackSummary.extract(traceback.walk_stack(f=None), limit=20)
|
|
278
|
+
entry_method_name = None
|
|
279
|
+
for summary, raw_frame in zip(frames[1:], traceback.walk_stack(f=None)):
|
|
280
|
+
frame, line_no = raw_frame
|
|
281
|
+
frame_self = frame.f_locals.get("self")
|
|
282
|
+
if obj == frame_self:
|
|
283
|
+
entry_method_name = summary.name
|
|
284
|
+
return entry_method_name
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def get_human_readable_entry_point(obj):
|
|
288
|
+
entry_method_name = get_object_entry_method_name(obj)
|
|
289
|
+
return inflection.humanize(entry_method_name)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def is_event_loop_available():
|
|
293
|
+
try:
|
|
294
|
+
asyncio.get_event_loop()
|
|
295
|
+
return True
|
|
296
|
+
except RuntimeError as ex:
|
|
297
|
+
if "There is no current event loop in thread" in str(ex):
|
|
298
|
+
return False
|
|
299
|
+
|
|
300
|
+
logger.debug(f"Failed to get asyncio event loop: {str(e)}")
|
|
301
|
+
return False
|
|
@@ -4,9 +4,7 @@ from .api_client import APIClientBase
|
|
|
4
4
|
|
|
5
5
|
class Authentication(APIClientBase):
|
|
6
6
|
def __init__(self, url_base=None, **kwargs):
|
|
7
|
-
super().__init__(
|
|
8
|
-
url_base or os.environ.get("AUTHORIZATION_SERVICE", ""), **kwargs
|
|
9
|
-
)
|
|
7
|
+
super().__init__(url_base or os.environ.get("AUTHORIZATION_SERVICE", ""), **kwargs)
|
|
10
8
|
|
|
11
9
|
def login(self, username: str, password: str):
|
|
12
10
|
body = {"username": username, "password": password}
|
|
@@ -17,8 +15,5 @@ class Authentication(APIClientBase):
|
|
|
17
15
|
|
|
18
16
|
def add_authorized_key(self, username: str, public_key: str):
|
|
19
17
|
return self.post_request(
|
|
20
|
-
"/authorized-keys",
|
|
21
|
-
query_args={"username": username},
|
|
22
|
-
body={"public_key": public_key},
|
|
18
|
+
"/authorized-keys", query_args={"username": username}, body={"public_key": public_key},
|
|
23
19
|
)
|
|
24
|
-
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import inflection
|
|
3
|
-
import requests
|
|
4
|
-
import urllib.parse
|
|
5
|
-
import logging
|
|
6
|
-
import traceback
|
|
7
|
-
|
|
8
|
-
from .settings import get_env_flag
|
|
9
|
-
|
|
10
|
-
logger = logging.getLogger(__name__)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def make_url(url_base, *path_args, query_args: dict = None, **path_kwargs):
|
|
14
|
-
path_args = [urllib.parse.quote(v, safe="") for v in path_args]
|
|
15
|
-
path_kwargs = {k: urllib.parse.quote(v, safe="") for k, v in path_kwargs.items()}
|
|
16
|
-
path = url_base.format(*path_args, **path_kwargs)
|
|
17
|
-
|
|
18
|
-
if query_args is None or len(query_args) == 0:
|
|
19
|
-
return path
|
|
20
|
-
query_string = urllib.parse.urlencode(query_args, doseq=True, quote_via=urllib.parse.quote)
|
|
21
|
-
return f"{path}?{query_string}"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class APIException(Exception):
|
|
25
|
-
def __init__(
|
|
26
|
-
self, url: str, status_code: int = 0, message: str = None, request_body: dict = None, response=None,
|
|
27
|
-
):
|
|
28
|
-
self.url = url
|
|
29
|
-
self.status_code = status_code
|
|
30
|
-
self.request_body = request_body
|
|
31
|
-
self.response = response
|
|
32
|
-
|
|
33
|
-
if not message and self.status_code and self.response is not None:
|
|
34
|
-
message = f"API {self.url} returned HTTP {self.status_code}: {self.response.text}"
|
|
35
|
-
elif not message:
|
|
36
|
-
message = f"API Error: {self.url}"
|
|
37
|
-
|
|
38
|
-
super().__init__(message)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
class APIClientBase:
|
|
42
|
-
SUCCESS_CODES = {
|
|
43
|
-
"GET": [200],
|
|
44
|
-
"POST": [200, 201, 202],
|
|
45
|
-
"PUT": [200, 202],
|
|
46
|
-
"PATCH": [200, 202],
|
|
47
|
-
"DELETE": [200, 202, 204],
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
def __init__(self, url_base=None, default_protocol="http", insecure_ssl=None):
|
|
51
|
-
self.url_base = url_base
|
|
52
|
-
protocols = ["http://", "https://"]
|
|
53
|
-
if not any(self.url_base.lower().startswith(p) for p in protocols):
|
|
54
|
-
self.url_base = f"{default_protocol}://{self.url_base}"
|
|
55
|
-
|
|
56
|
-
self.url_base = self.url_base.rstrip("/")
|
|
57
|
-
|
|
58
|
-
self.headers = {}
|
|
59
|
-
self._verify_ssl_cert = True
|
|
60
|
-
if insecure_ssl is not None:
|
|
61
|
-
self.set_insecure_ssl(insecure_ssl)
|
|
62
|
-
elif get_env_flag("ALLOW_INSECURE_SSL"):
|
|
63
|
-
self.set_insecure_ssl(True)
|
|
64
|
-
|
|
65
|
-
def _request(
|
|
66
|
-
self,
|
|
67
|
-
method: str,
|
|
68
|
-
relative_url: str,
|
|
69
|
-
*args,
|
|
70
|
-
query_args: dict = None,
|
|
71
|
-
body: dict = None,
|
|
72
|
-
success_codes: list = None,
|
|
73
|
-
**kwargs,
|
|
74
|
-
):
|
|
75
|
-
relative_url = relative_url.lstrip("/")
|
|
76
|
-
url = make_url(self.url_base + "/" + relative_url, *args, query_args=query_args, **kwargs)
|
|
77
|
-
logger.debug(f"Calling {method} {url}")
|
|
78
|
-
|
|
79
|
-
if not success_codes:
|
|
80
|
-
success_codes = APIClientBase.SUCCESS_CODES[method]
|
|
81
|
-
|
|
82
|
-
method_func = self._get_method_func(method)
|
|
83
|
-
method_args = {"verify": self._verify_ssl_cert}
|
|
84
|
-
if self.headers is not None and len(self.headers) > 0:
|
|
85
|
-
method_args["headers"] = self.headers
|
|
86
|
-
|
|
87
|
-
if body is not None:
|
|
88
|
-
method_args["json"] = body
|
|
89
|
-
|
|
90
|
-
try:
|
|
91
|
-
response = method_func(url, timeout=60, **method_args)
|
|
92
|
-
if response.status_code in success_codes:
|
|
93
|
-
logger.debug(f"Received {response.status_code} from {method} {url}")
|
|
94
|
-
return response.json()
|
|
95
|
-
|
|
96
|
-
logger.info(f"Received {response.status_code} from {method} {url}")
|
|
97
|
-
subject = get_human_readable_entry_point(self)
|
|
98
|
-
raise APIException(
|
|
99
|
-
url=url,
|
|
100
|
-
status_code=response.status_code,
|
|
101
|
-
message=f"{subject} has failed ({response.status_code})",
|
|
102
|
-
request_body=body,
|
|
103
|
-
response=response,
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
except APIException as e:
|
|
107
|
-
raise
|
|
108
|
-
|
|
109
|
-
except TypeError:
|
|
110
|
-
raise
|
|
111
|
-
|
|
112
|
-
except Exception as e:
|
|
113
|
-
logger.warning(f"Failed {method} request to {url}: {str(e)}")
|
|
114
|
-
subject = get_human_readable_entry_point(self)
|
|
115
|
-
hostname = self._get_hostname()
|
|
116
|
-
raise APIException(
|
|
117
|
-
url=url, message=f"{subject} has failed ({type(e)}: {hostname})", request_body=body,
|
|
118
|
-
) from e
|
|
119
|
-
|
|
120
|
-
def _get_hostname(self):
|
|
121
|
-
parsed_uri = urllib.parse.urlparse(self.url_base)
|
|
122
|
-
return parsed_uri.netloc
|
|
123
|
-
|
|
124
|
-
def _get_method_func(self, method):
|
|
125
|
-
requests_methods = {
|
|
126
|
-
"GET": requests.get,
|
|
127
|
-
"POST": requests.post,
|
|
128
|
-
"PUT": requests.put,
|
|
129
|
-
"PATCH": requests.patch,
|
|
130
|
-
"DELETE": requests.delete,
|
|
131
|
-
}
|
|
132
|
-
return requests_methods[method]
|
|
133
|
-
|
|
134
|
-
def get_request(
|
|
135
|
-
self, relative_url: str, *url_path_args, query_args: dict = None, success_codes: list = None, **url_path_kwargs,
|
|
136
|
-
):
|
|
137
|
-
return self._request(
|
|
138
|
-
"GET", relative_url, *url_path_args, query_args=query_args, success_codes=success_codes, **url_path_kwargs,
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
def delete_request(self, *args, **kwargs):
|
|
142
|
-
# Same args as get_request
|
|
143
|
-
return self._request("DELETE", *args, **kwargs)
|
|
144
|
-
|
|
145
|
-
def post_request(
|
|
146
|
-
self,
|
|
147
|
-
relative_url: str,
|
|
148
|
-
*url_path_args,
|
|
149
|
-
query_args: dict = None,
|
|
150
|
-
body: dict = None,
|
|
151
|
-
success_codes: list = None,
|
|
152
|
-
**url_path_kwargs,
|
|
153
|
-
):
|
|
154
|
-
return self._request(
|
|
155
|
-
"POST",
|
|
156
|
-
relative_url,
|
|
157
|
-
*url_path_args,
|
|
158
|
-
query_args=query_args,
|
|
159
|
-
body=body,
|
|
160
|
-
success_codes=success_codes,
|
|
161
|
-
**url_path_kwargs,
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
def put_request(self, *args, **kwargs):
|
|
165
|
-
# Same args as post_request
|
|
166
|
-
return self._request("PUT", *args, **kwargs)
|
|
167
|
-
|
|
168
|
-
def patch_request(self, *args, **kwargs):
|
|
169
|
-
# Same args as post_request
|
|
170
|
-
return self._request("PATCH", *args, **kwargs)
|
|
171
|
-
|
|
172
|
-
def set_insecure_ssl(self, insecure=True):
|
|
173
|
-
self._verify_ssl_cert = not insecure
|
|
174
|
-
|
|
175
|
-
def set_auth_jwt(self, jwt):
|
|
176
|
-
self.headers["Authorization"] = f"Bearer {jwt}"
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def get_object_entry_method_name(obj):
|
|
180
|
-
frames = traceback.StackSummary.extract(traceback.walk_stack(f=None), limit=20)
|
|
181
|
-
entry_method_name = None
|
|
182
|
-
for summary, raw_frame in zip(frames[1:], traceback.walk_stack(f=None)):
|
|
183
|
-
frame, line_no = raw_frame
|
|
184
|
-
frame_self = frame.f_locals.get("self")
|
|
185
|
-
if obj == frame_self:
|
|
186
|
-
entry_method_name = summary.name
|
|
187
|
-
return entry_method_name
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
def get_human_readable_entry_point(obj):
|
|
191
|
-
entry_method_name = get_object_entry_method_name(obj)
|
|
192
|
-
return inflection.humanize(entry_method_name)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|