osc_sdk_python 0.39.2__py3-none-any.whl
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.
- osc_sdk_python/VERSION +1 -0
- osc_sdk_python/__init__.py +31 -0
- osc_sdk_python/authentication.py +176 -0
- osc_sdk_python/call.py +96 -0
- osc_sdk_python/credentials.py +186 -0
- osc_sdk_python/limiter.py +29 -0
- osc_sdk_python/outscale_gateway.py +322 -0
- osc_sdk_python/problem.py +100 -0
- osc_sdk_python/requester.py +29 -0
- osc_sdk_python/resources/gateway_errors.yaml +1220 -0
- osc_sdk_python/resources/outscale.yaml +25166 -0
- osc_sdk_python/retry.py +138 -0
- osc_sdk_python/version.py +7 -0
- osc_sdk_python-0.39.2.dist-info/METADATA +259 -0
- osc_sdk_python-0.39.2.dist-info/RECORD +17 -0
- osc_sdk_python-0.39.2.dist-info/WHEEL +4 -0
- osc_sdk_python-0.39.2.dist-info/licenses/LICENSE +29 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from .call import Call
|
|
4
|
+
from .limiter import RateLimiter
|
|
5
|
+
import ruamel.yaml
|
|
6
|
+
from .version import get_version
|
|
7
|
+
import warnings
|
|
8
|
+
from datetime import timedelta
|
|
9
|
+
|
|
10
|
+
type_mapping = {"boolean": "bool", "string": "str", "integer": "int", "array": "list"}
|
|
11
|
+
|
|
12
|
+
# Logs Output Options
|
|
13
|
+
LOG_NONE = 0
|
|
14
|
+
LOG_STDERR = 1
|
|
15
|
+
LOG_STDIO = 2
|
|
16
|
+
LOG_MEMORY = 4
|
|
17
|
+
|
|
18
|
+
# what to Log
|
|
19
|
+
LOG_ALL = 0
|
|
20
|
+
LOG_KEEP_ONLY_LAST_REQ = 1
|
|
21
|
+
|
|
22
|
+
# Default
|
|
23
|
+
DEFAULT_LIMITER_WINDOW = timedelta(seconds=1) # 1 second
|
|
24
|
+
DEFAULT_LIMITER_MAX_REQUESTS = 5 # 5 requests / sec
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ActionNotExists(NotImplementedError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ParameterNotValid(NotImplementedError):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ParameterIsRequired(NotImplementedError):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ParameterHasWrongType(NotImplementedError):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Logger:
|
|
44
|
+
string = ""
|
|
45
|
+
type = LOG_NONE
|
|
46
|
+
what = LOG_ALL
|
|
47
|
+
|
|
48
|
+
def config(self, type=None, what=None):
|
|
49
|
+
if type is not None:
|
|
50
|
+
self.type = type
|
|
51
|
+
if what is not None:
|
|
52
|
+
self.what = what
|
|
53
|
+
|
|
54
|
+
def str(self):
|
|
55
|
+
if self.type == LOG_MEMORY:
|
|
56
|
+
return self.string
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
def do_log(self, s):
|
|
60
|
+
if self.type & LOG_MEMORY:
|
|
61
|
+
if self.what == LOG_KEEP_ONLY_LAST_REQ:
|
|
62
|
+
self.string = s
|
|
63
|
+
else:
|
|
64
|
+
self.string = self.string + "\n" + s
|
|
65
|
+
|
|
66
|
+
if self.type & LOG_STDIO:
|
|
67
|
+
print(s)
|
|
68
|
+
if self.type & LOG_STDERR:
|
|
69
|
+
print(s, file=sys.stderr)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class BaseAPI:
|
|
73
|
+
def __init__(self, spec, **kwargs):
|
|
74
|
+
self._load_gateway_structure(spec)
|
|
75
|
+
self._load_errors()
|
|
76
|
+
self.log = Logger()
|
|
77
|
+
self.limiter = RateLimiter(DEFAULT_LIMITER_WINDOW, DEFAULT_LIMITER_MAX_REQUESTS)
|
|
78
|
+
self.call = Call(
|
|
79
|
+
logger=self.log,
|
|
80
|
+
version=self.endpoint_api_version,
|
|
81
|
+
limiter=self.limiter,
|
|
82
|
+
**kwargs,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def update_credentials(self, **kwargs):
|
|
86
|
+
warnings.warn(
|
|
87
|
+
"update_credentials in deprecated. Use update_profile instead.",
|
|
88
|
+
DeprecationWarning,
|
|
89
|
+
stacklevel=2,
|
|
90
|
+
)
|
|
91
|
+
self.update_profile(**kwargs)
|
|
92
|
+
|
|
93
|
+
def update_profile(self, **kwargs):
|
|
94
|
+
"""
|
|
95
|
+
destroy and create a new credential map use for each call.
|
|
96
|
+
so you can change your ak/sk, region without having to recreate the whole Gateway
|
|
97
|
+
as the object is recreate, you can't expect to keep parameter from the old configuration
|
|
98
|
+
example: just updating the password, without renter the login will fail
|
|
99
|
+
"""
|
|
100
|
+
self.call.update_profile(**kwargs)
|
|
101
|
+
|
|
102
|
+
def access_key(self):
|
|
103
|
+
return self.call.profile.access_key
|
|
104
|
+
|
|
105
|
+
def secret_key(self):
|
|
106
|
+
return self.call.profile.secret_key
|
|
107
|
+
|
|
108
|
+
def region(self):
|
|
109
|
+
return self.call.profile.region
|
|
110
|
+
|
|
111
|
+
def email(self):
|
|
112
|
+
warnings.warn(
|
|
113
|
+
"email in deprecated. Use login instead.",
|
|
114
|
+
DeprecationWarning,
|
|
115
|
+
stacklevel=2,
|
|
116
|
+
)
|
|
117
|
+
return self.login()
|
|
118
|
+
|
|
119
|
+
def login(self):
|
|
120
|
+
return self.call.profile.login
|
|
121
|
+
|
|
122
|
+
def password(self):
|
|
123
|
+
return self.call.profile.password
|
|
124
|
+
|
|
125
|
+
def _convert(self, input_file):
|
|
126
|
+
structure = {}
|
|
127
|
+
try:
|
|
128
|
+
with open(input_file, "r") as fi:
|
|
129
|
+
yaml = ruamel.yaml.YAML(typ="safe")
|
|
130
|
+
content = yaml.load(fi.read())
|
|
131
|
+
except Exception as err:
|
|
132
|
+
print("Problem reading {}:{}".format(input_file, str(err)))
|
|
133
|
+
self.api_version = content["info"]["version"]
|
|
134
|
+
self.endpoint_api_version = content["servers"][0]["url"].split("/")[-1]
|
|
135
|
+
for action, params in content["components"]["schemas"].items():
|
|
136
|
+
if action.endswith("Request"):
|
|
137
|
+
action_name = action.split("Request")[0]
|
|
138
|
+
structure[action_name] = {}
|
|
139
|
+
for propertie_name, properties in params["properties"].items():
|
|
140
|
+
if propertie_name == "DryRun":
|
|
141
|
+
continue
|
|
142
|
+
if "type" not in properties.keys():
|
|
143
|
+
action_type = None
|
|
144
|
+
else:
|
|
145
|
+
action_type = type_mapping[properties["type"]]
|
|
146
|
+
structure[action_name][propertie_name] = {
|
|
147
|
+
"type": action_type,
|
|
148
|
+
"required": False,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if "required" in params.keys():
|
|
152
|
+
for required in params["required"]:
|
|
153
|
+
structure[action_name][required]["required"] = True
|
|
154
|
+
return structure
|
|
155
|
+
|
|
156
|
+
def _load_gateway_structure(self, spec):
|
|
157
|
+
self.gateway_structure = self._convert(spec)
|
|
158
|
+
|
|
159
|
+
def _load_errors(self):
|
|
160
|
+
dir_path = os.path.join(os.path.dirname(__file__))
|
|
161
|
+
yaml_file = os.path.abspath("{}/resources/gateway_errors.yaml".format(dir_path))
|
|
162
|
+
with open(yaml_file, "r") as yam:
|
|
163
|
+
yaml = ruamel.yaml.YAML(typ="safe")
|
|
164
|
+
self.gateway_errors = yaml.load(yam.read())
|
|
165
|
+
|
|
166
|
+
def _check_parameters_type(self, action_structure, input_structure):
|
|
167
|
+
for i_param, i_value in input_structure.items():
|
|
168
|
+
if (
|
|
169
|
+
i_param != "Filters"
|
|
170
|
+
and action_structure[i_param]["type"] is not None
|
|
171
|
+
and action_structure[i_param]["type"] != i_value.__class__.__name__
|
|
172
|
+
):
|
|
173
|
+
raise ParameterHasWrongType(
|
|
174
|
+
"{} is <{}> instead of <{}>".format(
|
|
175
|
+
i_param,
|
|
176
|
+
i_value.__class__.__name__,
|
|
177
|
+
action_structure[i_param]["type"],
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def _check_parameters_required(self, action_structure, input_structure):
|
|
182
|
+
action_mandatory_params = [
|
|
183
|
+
param for param in action_structure if action_structure[param]["required"]
|
|
184
|
+
]
|
|
185
|
+
difference = set(action_mandatory_params).difference(
|
|
186
|
+
set(input_structure.keys())
|
|
187
|
+
)
|
|
188
|
+
if difference:
|
|
189
|
+
raise ParameterIsRequired(
|
|
190
|
+
"Missing {}. Required parameters are {}".format(
|
|
191
|
+
", ".join(list(difference)), ", ".join(action_mandatory_params)
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def _check_parameters_valid(self, action_name, params):
|
|
196
|
+
structure_parameters = self.gateway_structure[action_name].keys()
|
|
197
|
+
input_parameters = set(params)
|
|
198
|
+
different_parameters = list(
|
|
199
|
+
input_parameters.difference(set(structure_parameters))
|
|
200
|
+
)
|
|
201
|
+
if different_parameters:
|
|
202
|
+
raise ParameterNotValid(
|
|
203
|
+
"""{}. Available parameters on sdk: {} api: {} are: {}.""".format(
|
|
204
|
+
", ".join(different_parameters),
|
|
205
|
+
get_version(),
|
|
206
|
+
self.api_version,
|
|
207
|
+
", ".join(structure_parameters),
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def _check(self, action_name, **params):
|
|
212
|
+
if action_name not in self.gateway_structure:
|
|
213
|
+
raise ActionNotExists(
|
|
214
|
+
"Action {} does not exists for python sdk: {} with api: {}".format(
|
|
215
|
+
action_name, get_version(), self.api_version
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
self._check_parameters_valid(action_name, params)
|
|
219
|
+
self._check_parameters_required(self.gateway_structure[action_name], params)
|
|
220
|
+
self._check_parameters_type(self.gateway_structure[action_name], params)
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def _remove_none_parameters(**params):
|
|
224
|
+
"""
|
|
225
|
+
Remove parameters having None as value
|
|
226
|
+
to perform CreateVolumes(Iops=None, Size=10)
|
|
227
|
+
"""
|
|
228
|
+
return {key: value for key, value in params.items() if value is not None}
|
|
229
|
+
|
|
230
|
+
def _get_action(self, action_name):
|
|
231
|
+
def action(**kwargs):
|
|
232
|
+
kwargs = self._remove_none_parameters(**kwargs)
|
|
233
|
+
self._check(action_name, **kwargs)
|
|
234
|
+
result = self.call.api(action_name, **kwargs)
|
|
235
|
+
return result
|
|
236
|
+
|
|
237
|
+
return action
|
|
238
|
+
|
|
239
|
+
def __getattr__(self, attr):
|
|
240
|
+
return self._get_action(attr)
|
|
241
|
+
|
|
242
|
+
def __dir__(self):
|
|
243
|
+
return self.gateway_structure.keys()
|
|
244
|
+
|
|
245
|
+
def raw(self, action_name, **kwargs):
|
|
246
|
+
return self.call.api(action_name, **kwargs)
|
|
247
|
+
|
|
248
|
+
def __enter__(self):
|
|
249
|
+
return self
|
|
250
|
+
|
|
251
|
+
def __exit__(self, type, value, traceback):
|
|
252
|
+
self.call.close()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class OutscaleGateway(BaseAPI):
|
|
256
|
+
def __init__(self, **kwargs):
|
|
257
|
+
super().__init__(
|
|
258
|
+
os.path.join(os.path.dirname(__file__), "resources/outscale.yaml"), **kwargs
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test():
|
|
263
|
+
a = OutscaleGateway()
|
|
264
|
+
a.CreateVms(
|
|
265
|
+
ImageId="ami-xx",
|
|
266
|
+
BlockDeviceMappings=[{"/dev/sda1": {"Size": 10}}],
|
|
267
|
+
SecurityGroupIds=["sg-aaa", "sg-bbb"],
|
|
268
|
+
)
|
|
269
|
+
try:
|
|
270
|
+
a.CreateVms(
|
|
271
|
+
ImageId="ami-xx",
|
|
272
|
+
BlockDeviceMappings=[{"/dev/sda1": {"Size": 10}}],
|
|
273
|
+
SecurityGroupIds=["sg-aaa", "sg-bbb"],
|
|
274
|
+
Wrong="wrong",
|
|
275
|
+
)
|
|
276
|
+
except ParameterNotValid:
|
|
277
|
+
pass
|
|
278
|
+
else:
|
|
279
|
+
raise AssertionError()
|
|
280
|
+
try:
|
|
281
|
+
a.CreateVms(
|
|
282
|
+
BlockDeviceMappings=[{"/dev/sda1": {"Size": 10}}],
|
|
283
|
+
SecurityGroupIds=["sg-aaa", "sg-bbb"],
|
|
284
|
+
)
|
|
285
|
+
except ParameterIsRequired:
|
|
286
|
+
pass
|
|
287
|
+
else:
|
|
288
|
+
raise AssertionError()
|
|
289
|
+
try:
|
|
290
|
+
a.CreateVms(
|
|
291
|
+
ImageId=["ami-xxx"],
|
|
292
|
+
BlockDeviceMappings=[{"/dev/sda1": {"Size": 10}}],
|
|
293
|
+
SecurityGroupIds=["sg-aaa", "sg-bbb"],
|
|
294
|
+
)
|
|
295
|
+
except ParameterHasWrongType:
|
|
296
|
+
pass
|
|
297
|
+
else:
|
|
298
|
+
raise AssertionError()
|
|
299
|
+
try:
|
|
300
|
+
a.CreateVms(
|
|
301
|
+
ImageId="ami-xxx",
|
|
302
|
+
BlockDeviceMappings=[{"/dev/sda1": {"Size": 10}}],
|
|
303
|
+
SecurityGroupIds="wrong",
|
|
304
|
+
)
|
|
305
|
+
except ParameterHasWrongType:
|
|
306
|
+
pass
|
|
307
|
+
else:
|
|
308
|
+
raise AssertionError()
|
|
309
|
+
try:
|
|
310
|
+
a.CreateVms(
|
|
311
|
+
ImageId=["ami-wrong"],
|
|
312
|
+
BlockDeviceMappings=[{"/dev/sda1": {"Size": 10}}],
|
|
313
|
+
SecurityGroupIds="wrong",
|
|
314
|
+
)
|
|
315
|
+
except ParameterHasWrongType:
|
|
316
|
+
pass
|
|
317
|
+
else:
|
|
318
|
+
raise AssertionError()
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
if __name__ == "__main__":
|
|
322
|
+
test()
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ProblemDecoder(json.JSONDecoder):
|
|
5
|
+
def decode(self, s):
|
|
6
|
+
data = super().decode(s)
|
|
7
|
+
if isinstance(data, dict):
|
|
8
|
+
return self._make_problem(data)
|
|
9
|
+
return data
|
|
10
|
+
|
|
11
|
+
def _make_problem(self, data):
|
|
12
|
+
type_ = data.pop("type", None)
|
|
13
|
+
status = data.pop("status", None)
|
|
14
|
+
title = data.pop("title", None)
|
|
15
|
+
detail = data.pop("detail", None)
|
|
16
|
+
instance = data.pop("instance", None)
|
|
17
|
+
return Problem(type_, status, title, detail, instance, **data)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Problem(Exception):
|
|
21
|
+
def __init__(self, type_, status, title, detail, instance, **kwargs):
|
|
22
|
+
self._type = type_ or "about:blank"
|
|
23
|
+
self.status = status
|
|
24
|
+
self.title = title
|
|
25
|
+
self.detail = detail
|
|
26
|
+
self.instance = instance
|
|
27
|
+
self.extras = kwargs
|
|
28
|
+
|
|
29
|
+
for k in self.extras:
|
|
30
|
+
if k in ["type", "status", "title", "detail", "instance"]:
|
|
31
|
+
raise ValueError(f"Reserved key '{k}' used in Problem extra arguments.")
|
|
32
|
+
|
|
33
|
+
def __str__(self):
|
|
34
|
+
return self.title
|
|
35
|
+
|
|
36
|
+
def __repr__(self):
|
|
37
|
+
return f"{self.__class__.__name__}<type={self._type}; status={self.status}; title={self.title}>"
|
|
38
|
+
|
|
39
|
+
def msg(self):
|
|
40
|
+
msg = (
|
|
41
|
+
f"type = {self._type}, "
|
|
42
|
+
f"status = {self.status}, "
|
|
43
|
+
f"title = {self.title}, "
|
|
44
|
+
f"detail = {self.detail}, "
|
|
45
|
+
f"instance = {self.instance}, "
|
|
46
|
+
f"extras = {self.extras}"
|
|
47
|
+
)
|
|
48
|
+
return msg
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def type(self):
|
|
52
|
+
return self._type
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class LegacyProblemDecoder(json.JSONDecoder):
|
|
56
|
+
def decode(self, s):
|
|
57
|
+
data = super().decode(s)
|
|
58
|
+
if isinstance(data, dict):
|
|
59
|
+
return self._make_legacy_problem(data)
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
def _make_legacy_problem(self, data):
|
|
63
|
+
request_id = None
|
|
64
|
+
error_code = None
|
|
65
|
+
code_type = None
|
|
66
|
+
|
|
67
|
+
if "__type" in data:
|
|
68
|
+
error_code = data.get("__type")
|
|
69
|
+
else:
|
|
70
|
+
request_id = (data.get("ResponseContext") or {}).get("RequestId")
|
|
71
|
+
errors = data.get("Errors")
|
|
72
|
+
if errors:
|
|
73
|
+
error = errors[0]
|
|
74
|
+
error_code = error.get("Code")
|
|
75
|
+
reason = error.get("Type")
|
|
76
|
+
if error.get("Details"):
|
|
77
|
+
code_type = reason
|
|
78
|
+
else:
|
|
79
|
+
code_type = None
|
|
80
|
+
return LegacyProblem(None, error_code, code_type, request_id, None)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class LegacyProblem(Exception):
|
|
84
|
+
def __init__(self, status, error_code, code_type, request_id, url):
|
|
85
|
+
self.status = status
|
|
86
|
+
self.error_code = error_code
|
|
87
|
+
self.code_type = code_type
|
|
88
|
+
self.request_id = request_id
|
|
89
|
+
self.url = url
|
|
90
|
+
|
|
91
|
+
def msg(self):
|
|
92
|
+
msg = (
|
|
93
|
+
f"status = {self.status}, "
|
|
94
|
+
f"code = {self.error_code}, "
|
|
95
|
+
f"{'code_type = ' if self.code_type is not None else ''}"
|
|
96
|
+
f"{self.code_type + ', ' if self.code_type is not None else ''}"
|
|
97
|
+
f"request_id = {self.request_id}, "
|
|
98
|
+
f"url = {self.url}"
|
|
99
|
+
)
|
|
100
|
+
return msg
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from .retry import Retry
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Requester:
|
|
5
|
+
def __init__(self, session, auth, endpoint, **kwargs):
|
|
6
|
+
self.session = session
|
|
7
|
+
self.auth = auth
|
|
8
|
+
self.endpoint = endpoint
|
|
9
|
+
self.request_kwargs = kwargs
|
|
10
|
+
|
|
11
|
+
def send(self, uri, payload):
|
|
12
|
+
headers = None
|
|
13
|
+
if self.auth.is_basic_auth_configured():
|
|
14
|
+
headers = self.auth.get_basic_auth_header()
|
|
15
|
+
else:
|
|
16
|
+
headers = self.auth.forge_headers_signed(uri, payload)
|
|
17
|
+
|
|
18
|
+
if self.auth.x509_client_cert is not None:
|
|
19
|
+
cert_file = self.auth.x509_client_cert
|
|
20
|
+
else:
|
|
21
|
+
cert_file = None
|
|
22
|
+
|
|
23
|
+
retry_kwargs = self.request_kwargs.copy()
|
|
24
|
+
retry_kwargs.update(
|
|
25
|
+
{"data": payload, "headers": headers, "verify": True, "cert": cert_file}
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
response = Retry(self.session, "post", self.endpoint, **retry_kwargs)
|
|
29
|
+
return response.execute().json()
|