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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: syneto-api
3
- Version: 0.2.20
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.20"
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
- ['inflection>=0.5,<0.6',
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.20',
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