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
osc_sdk_python/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.39.2
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .outscale_gateway import OutscaleGateway as Gateway
|
|
2
|
+
from .outscale_gateway import LOG_NONE
|
|
3
|
+
from .outscale_gateway import LOG_STDERR
|
|
4
|
+
from .outscale_gateway import LOG_STDIO
|
|
5
|
+
from .outscale_gateway import LOG_MEMORY
|
|
6
|
+
from .version import get_version
|
|
7
|
+
from .problem import Problem, ProblemDecoder
|
|
8
|
+
from .limiter import RateLimiter
|
|
9
|
+
from .retry import Retry
|
|
10
|
+
|
|
11
|
+
# what to Log
|
|
12
|
+
from .outscale_gateway import LOG_ALL
|
|
13
|
+
from .outscale_gateway import LOG_KEEP_ONLY_LAST_REQ
|
|
14
|
+
|
|
15
|
+
__author__ = "Outscale SAS"
|
|
16
|
+
__version__ = get_version()
|
|
17
|
+
__all__ = [
|
|
18
|
+
"__version__",
|
|
19
|
+
"__author__",
|
|
20
|
+
"Gateway",
|
|
21
|
+
"LOG_NONE",
|
|
22
|
+
"LOG_STDERR",
|
|
23
|
+
"LOG_STDIO",
|
|
24
|
+
"LOG_MEMORY",
|
|
25
|
+
"LOG_ALL",
|
|
26
|
+
"LOG_KEEP_ONLY_LAST_REQ",
|
|
27
|
+
"Problem",
|
|
28
|
+
"ProblemDecoder",
|
|
29
|
+
"RateLimiter",
|
|
30
|
+
"Retry",
|
|
31
|
+
]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import hashlib
|
|
3
|
+
import hmac
|
|
4
|
+
import base64
|
|
5
|
+
|
|
6
|
+
from .version import get_version
|
|
7
|
+
from .credentials import Profile
|
|
8
|
+
|
|
9
|
+
VERSION: str = get_version()
|
|
10
|
+
DEFAULT_USER_AGENT = "osc-sdk-python/" + VERSION
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Authentication:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
credentials: Profile,
|
|
17
|
+
host: str,
|
|
18
|
+
method="POST",
|
|
19
|
+
service="api",
|
|
20
|
+
content_type="application/json; charset=utf-8",
|
|
21
|
+
algorithm="OSC4-HMAC-SHA256",
|
|
22
|
+
signed_headers="content-type;host;x-osc-date",
|
|
23
|
+
user_agent=DEFAULT_USER_AGENT,
|
|
24
|
+
):
|
|
25
|
+
self.access_key = credentials.access_key
|
|
26
|
+
self.secret_key = credentials.secret_key
|
|
27
|
+
self.login = credentials.login
|
|
28
|
+
self.password = credentials.password
|
|
29
|
+
self.host = host
|
|
30
|
+
self.region = credentials.region
|
|
31
|
+
self.content_type = content_type
|
|
32
|
+
self.method = method
|
|
33
|
+
self.service = service
|
|
34
|
+
self.algorithm = algorithm
|
|
35
|
+
self.signed_headers = signed_headers
|
|
36
|
+
self.user_agent = user_agent
|
|
37
|
+
self.x509_client_cert = credentials.x509_client_cert
|
|
38
|
+
|
|
39
|
+
def forge_headers_signed(self, uri, request_data):
|
|
40
|
+
date_iso, date = self.build_dates()
|
|
41
|
+
credential_scope = "{}/{}/{}/osc4_request".format(
|
|
42
|
+
date, self.region, self.service
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
canonical_request = self.build_canonical_request(date_iso, uri, request_data)
|
|
46
|
+
str_to_sign = self.create_string_to_sign(
|
|
47
|
+
date_iso, credential_scope, canonical_request
|
|
48
|
+
)
|
|
49
|
+
signature = self.compute_signature(date, str_to_sign)
|
|
50
|
+
authorisation = self.build_authorization_header(credential_scope, signature)
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
"Content-Type": self.content_type,
|
|
54
|
+
"X-Osc-Date": date_iso,
|
|
55
|
+
"Authorization": authorisation,
|
|
56
|
+
"User-Agent": self.user_agent,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def build_dates(self):
|
|
60
|
+
"""Return YYYYMMDDTHHmmssZ, YYYYMMDD"""
|
|
61
|
+
t = datetime.datetime.now(datetime.timezone.utc)
|
|
62
|
+
return t.strftime("%Y%m%dT%H%M%SZ"), t.strftime("%Y%m%d")
|
|
63
|
+
|
|
64
|
+
def sign(self, key, msg):
|
|
65
|
+
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
|
|
66
|
+
|
|
67
|
+
def get_signature_key(self, key, date_stamp_value):
|
|
68
|
+
k_date = self.sign(("OSC4" + key).encode("utf-8"), date_stamp_value)
|
|
69
|
+
k_region = self.sign(k_date, self.region)
|
|
70
|
+
k_service = self.sign(k_region, self.service)
|
|
71
|
+
k_signing = self.sign(k_service, "osc4_request")
|
|
72
|
+
return k_signing
|
|
73
|
+
|
|
74
|
+
def build_canonical_request(self, date_iso, canonical_uri, request_data):
|
|
75
|
+
#
|
|
76
|
+
# Step 1 is to define the verb (GET, POST, etc.)--already done.
|
|
77
|
+
# Step 2: Create canonical URI--the part of the URI from domain to query
|
|
78
|
+
# string (use '/' if no path)
|
|
79
|
+
# canonical_uri = '/'
|
|
80
|
+
# Step 3: Create the canonical query string. In this example, request
|
|
81
|
+
# parameters are passed in the body of the request and the query string
|
|
82
|
+
# is blank.
|
|
83
|
+
# Step 4: Create the canonical headers. Header names must be trimmed
|
|
84
|
+
# and lowercase, and sorted in code point order from low to high.
|
|
85
|
+
# Note that there is a trailing \n.
|
|
86
|
+
# Step 5: Create the list of signed headers. This lists the headers
|
|
87
|
+
# in the canonical_headers list, delimited with ";" and in alpha order.
|
|
88
|
+
# Note: The request can include any headers; canonical_headers and
|
|
89
|
+
# signed_headers include those that you want to be included in the
|
|
90
|
+
# hash of the request. "Host" and "x-amz-date" are always required.
|
|
91
|
+
# Step 6: Create payload hash. In this example, the payload (body of
|
|
92
|
+
# the request) contains the request parameters.
|
|
93
|
+
# Step 7: Combine elements to create canonical request
|
|
94
|
+
canonical_querystring = ""
|
|
95
|
+
canonical_headers = (
|
|
96
|
+
"content-type:"
|
|
97
|
+
+ self.content_type
|
|
98
|
+
+ "\n"
|
|
99
|
+
+ "host:"
|
|
100
|
+
+ self.host
|
|
101
|
+
+ "\n"
|
|
102
|
+
+ "x-osc-date:"
|
|
103
|
+
+ date_iso
|
|
104
|
+
+ "\n"
|
|
105
|
+
)
|
|
106
|
+
payload_hash = hashlib.sha256(request_data.encode("utf-8")).hexdigest()
|
|
107
|
+
return (
|
|
108
|
+
self.method
|
|
109
|
+
+ "\n"
|
|
110
|
+
+ canonical_uri
|
|
111
|
+
+ "\n"
|
|
112
|
+
+ canonical_querystring
|
|
113
|
+
+ "\n"
|
|
114
|
+
+ canonical_headers
|
|
115
|
+
+ "\n"
|
|
116
|
+
+ self.signed_headers
|
|
117
|
+
+ "\n"
|
|
118
|
+
+ payload_hash
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def create_string_to_sign(self, date_iso, credential_scope, canonical_request):
|
|
122
|
+
# ************* TASK 2: CREATE THE STRING TO SIGN*************
|
|
123
|
+
# Match the algorithm to the hashing algorithm you use, either SHA-1 or
|
|
124
|
+
# SHA-256 (recommended)
|
|
125
|
+
return (
|
|
126
|
+
self.algorithm
|
|
127
|
+
+ "\n"
|
|
128
|
+
+ date_iso
|
|
129
|
+
+ "\n"
|
|
130
|
+
+ credential_scope
|
|
131
|
+
+ "\n"
|
|
132
|
+
+ hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def compute_signature(self, date, string_to_sign):
|
|
136
|
+
# ************* TASK 3: CALCULATE THE SIGNATURE *************
|
|
137
|
+
# Create the signing key using the function defined above.
|
|
138
|
+
signing_key = self.get_signature_key(self.secret_key, date)
|
|
139
|
+
|
|
140
|
+
# Sign the string_to_sign using the signing_key
|
|
141
|
+
return hmac.new(
|
|
142
|
+
signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
|
|
143
|
+
).hexdigest()
|
|
144
|
+
|
|
145
|
+
def build_authorization_header(self, credential_scope, signature):
|
|
146
|
+
# ************* TASK 4: ADD SIGNING INFORMATION TO THE REQUEST *************
|
|
147
|
+
# Put the signature information in a header named Authorization.
|
|
148
|
+
return (
|
|
149
|
+
self.algorithm
|
|
150
|
+
+ " "
|
|
151
|
+
+ "Credential="
|
|
152
|
+
+ self.access_key
|
|
153
|
+
+ "/"
|
|
154
|
+
+ credential_scope
|
|
155
|
+
+ ", "
|
|
156
|
+
+ "SignedHeaders="
|
|
157
|
+
+ self.signed_headers
|
|
158
|
+
+ ", "
|
|
159
|
+
+ "Signature="
|
|
160
|
+
+ signature
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def is_basic_auth_configured(self):
|
|
164
|
+
return self.login is not None and self.password is not None
|
|
165
|
+
|
|
166
|
+
def get_basic_auth_header(self):
|
|
167
|
+
if not self.is_basic_auth_configured():
|
|
168
|
+
raise Exception("email or password not set")
|
|
169
|
+
creds = self.login + ":" + self.password
|
|
170
|
+
b64_creds = str(base64.b64encode(creds.encode("utf-8")), "utf-8")
|
|
171
|
+
date_iso, _ = self.build_dates()
|
|
172
|
+
return {
|
|
173
|
+
"Content-Type": self.content_type,
|
|
174
|
+
"X-Osc-Date": date_iso,
|
|
175
|
+
"Authorization": "Basic " + b64_creds,
|
|
176
|
+
}
|
osc_sdk_python/call.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from .authentication import Authentication
|
|
2
|
+
from .authentication import DEFAULT_USER_AGENT
|
|
3
|
+
from .credentials import Profile
|
|
4
|
+
from .requester import Requester
|
|
5
|
+
from requests import Session
|
|
6
|
+
from urllib3.util import parse_url
|
|
7
|
+
from datetime import timedelta
|
|
8
|
+
from .limiter import RateLimiter
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import warnings
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Call(object):
|
|
15
|
+
def __init__(self, logger=None, limiter=None, **kwargs):
|
|
16
|
+
self.version = kwargs.pop("version", "latest")
|
|
17
|
+
self.host = kwargs.pop("host", None)
|
|
18
|
+
self.ssl = kwargs.pop("_ssl", True)
|
|
19
|
+
self.user_agent = kwargs.pop("user_agent", DEFAULT_USER_AGENT)
|
|
20
|
+
self.logger = logger
|
|
21
|
+
self.limiter: RateLimiter | None = limiter
|
|
22
|
+
self.retry_kwargs = {}
|
|
23
|
+
self.session = Session()
|
|
24
|
+
|
|
25
|
+
kwargs = self.update_limiter(**kwargs)
|
|
26
|
+
kwargs = self.update_retry(**kwargs)
|
|
27
|
+
self.update_profile(**kwargs)
|
|
28
|
+
|
|
29
|
+
def update_credentials(self, **kwargs):
|
|
30
|
+
warnings.warn(
|
|
31
|
+
"update_credentials is deprecated, use update_profile instead",
|
|
32
|
+
DeprecationWarning,
|
|
33
|
+
stacklevel=2,
|
|
34
|
+
)
|
|
35
|
+
return self.update_profile(**kwargs)
|
|
36
|
+
|
|
37
|
+
def update_profile(self, **kwargs):
|
|
38
|
+
self.profile = Profile.from_standard_configuration(
|
|
39
|
+
kwargs.pop("path", None), kwargs.pop("profile", None)
|
|
40
|
+
)
|
|
41
|
+
self.profile.merge(Profile(**kwargs))
|
|
42
|
+
return kwargs
|
|
43
|
+
|
|
44
|
+
def update_limiter(self, **kwargs):
|
|
45
|
+
limiter_window = kwargs.pop("limiter_window", None)
|
|
46
|
+
if limiter_window is not None and self.limiter is not None:
|
|
47
|
+
self.limiter.window = timedelta(seconds=int(limiter_window))
|
|
48
|
+
|
|
49
|
+
limiter_max_requests = kwargs.pop("limiter_max_requests", None)
|
|
50
|
+
if limiter_max_requests is not None and self.limiter is not None:
|
|
51
|
+
self.limiter.max_requests = limiter_max_requests
|
|
52
|
+
|
|
53
|
+
return kwargs
|
|
54
|
+
|
|
55
|
+
def update_retry(self, **kwargs):
|
|
56
|
+
max_retries = kwargs.pop("max_retries", None)
|
|
57
|
+
if max_retries is not None:
|
|
58
|
+
self.retry_kwargs["max_retries"] = int(max_retries)
|
|
59
|
+
|
|
60
|
+
for key in ["backoff_factor", "backoff_jitter", "backoff_max"]:
|
|
61
|
+
value = kwargs.pop(f"retry_{key}", None)
|
|
62
|
+
if value is not None:
|
|
63
|
+
self.retry_kwargs[key] = float(value)
|
|
64
|
+
return kwargs
|
|
65
|
+
|
|
66
|
+
def api(self, action, service="api", **data):
|
|
67
|
+
try:
|
|
68
|
+
endpoint = self.profile.get_endpoint(service) + "/" + action
|
|
69
|
+
parsed_url = parse_url(endpoint)
|
|
70
|
+
uri = parsed_url.path
|
|
71
|
+
host = parsed_url.host
|
|
72
|
+
|
|
73
|
+
if self.limiter is not None:
|
|
74
|
+
self.limiter.acquire()
|
|
75
|
+
|
|
76
|
+
requester = Requester(
|
|
77
|
+
self.session,
|
|
78
|
+
Authentication(
|
|
79
|
+
self.profile,
|
|
80
|
+
host,
|
|
81
|
+
user_agent=self.user_agent,
|
|
82
|
+
),
|
|
83
|
+
endpoint,
|
|
84
|
+
**self.retry_kwargs,
|
|
85
|
+
)
|
|
86
|
+
if self.logger is not None:
|
|
87
|
+
self.logger.do_log(
|
|
88
|
+
"uri: " + uri + "\npayload:\n" + json.dumps(data, indent=2)
|
|
89
|
+
)
|
|
90
|
+
return requester.send(uri, json.dumps(data))
|
|
91
|
+
except Exception as err:
|
|
92
|
+
raise err
|
|
93
|
+
|
|
94
|
+
def close(self):
|
|
95
|
+
if self.session:
|
|
96
|
+
self.session.close()
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
ORIGINAL_PATH = os.path.join(os.path.expanduser("~"), ".oapi_credentials")
|
|
6
|
+
STD_PATH = os.path.join(os.path.expanduser("~"), ".osc/config.json")
|
|
7
|
+
DEFAULT_REGION = "eu-west-2"
|
|
8
|
+
DEFAULT_PROFILE = "default"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Endpoint:
|
|
12
|
+
def __init__(self, **kwargs):
|
|
13
|
+
self.api: str = kwargs.pop("api", None)
|
|
14
|
+
self.oks: str = kwargs.pop("oks", None)
|
|
15
|
+
self.lbu: str = kwargs.pop("lbu", None)
|
|
16
|
+
self.oos: str = kwargs.pop("oos", None)
|
|
17
|
+
self.fcu: str = kwargs.pop("fcu", None)
|
|
18
|
+
self.eim: str = kwargs.pop("eim", None)
|
|
19
|
+
self.direct_link: str = kwargs.pop("direct_link", None)
|
|
20
|
+
|
|
21
|
+
if kwargs:
|
|
22
|
+
unexpected = ", ".join(f"'{k}'" for k in kwargs.keys())
|
|
23
|
+
raise TypeError(
|
|
24
|
+
f"Endpoint() got unexpected keyword arguments: {unexpected}"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Profile:
|
|
29
|
+
def __init__(self, **kwargs):
|
|
30
|
+
self.access_key: str = kwargs.pop("access_key", None)
|
|
31
|
+
self.secret_key: str = kwargs.pop("secret_key", None)
|
|
32
|
+
self.access_key_v2: str = kwargs.pop("access_key_v2", None)
|
|
33
|
+
self.secret_key_v2: str = kwargs.pop("secret_key_v2", None)
|
|
34
|
+
self.iam_v2_services: list[str] = kwargs.pop("iam_v2_services", [])
|
|
35
|
+
self.x509_client_cert: str = kwargs.pop("x509_client_cert", None)
|
|
36
|
+
self.x509_client_cert_b64: str = kwargs.pop("x509_client_cert_b64", None)
|
|
37
|
+
self.x509_client_key: str = kwargs.pop("x509_client_key", None)
|
|
38
|
+
self.x509_client_key_b64: str = kwargs.pop("x509_client_key_b64", None)
|
|
39
|
+
self.tls_skip_verify: bool = kwargs.pop("tls_skip_verify", False)
|
|
40
|
+
self.login: str = kwargs.pop("login", None) or kwargs.pop("email", None)
|
|
41
|
+
self.password: str = kwargs.pop("password", None)
|
|
42
|
+
self.protocol: str = kwargs.pop("protocol", None)
|
|
43
|
+
self.region: str = kwargs.pop("region", None)
|
|
44
|
+
self.endpoints: "Endpoint" = kwargs.pop(
|
|
45
|
+
"endpoints", Endpoint()
|
|
46
|
+
) # Forward reference
|
|
47
|
+
|
|
48
|
+
if kwargs:
|
|
49
|
+
unexpected = ", ".join(f"'{k}'" for k in kwargs.keys())
|
|
50
|
+
raise TypeError(f"Profile() got unexpected keyword arguments: {unexpected}")
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def email(self) -> str:
|
|
54
|
+
# For some reason, login is called email
|
|
55
|
+
return self.login
|
|
56
|
+
|
|
57
|
+
def get_endpoint(self, service: str) -> str:
|
|
58
|
+
endpoint = getattr(self.endpoints, service)
|
|
59
|
+
if not endpoint:
|
|
60
|
+
endpoint = self.get_default_endpoint(service)
|
|
61
|
+
|
|
62
|
+
return endpoint
|
|
63
|
+
|
|
64
|
+
def get_default_endpoint(self, service: str) -> str:
|
|
65
|
+
if service == "oks":
|
|
66
|
+
return f"{self.protocol}://api.{self.region}.oks.outscale.com/api/v2"
|
|
67
|
+
elif service == "api":
|
|
68
|
+
return f"{self.protocol}://api.{self.region}.outscale.com/api/v1"
|
|
69
|
+
elif service == "lbu":
|
|
70
|
+
return f"{self.protocol}://lbu.{self.region}.outscale.com"
|
|
71
|
+
elif service == "oos":
|
|
72
|
+
return f"{self.protocol}://oos.{self.region}.outscale.com"
|
|
73
|
+
elif service == "fcu":
|
|
74
|
+
return f"{self.protocol}://fcu.{self.region}.outscale.com"
|
|
75
|
+
elif service == "eim":
|
|
76
|
+
return f"{self.protocol}://eim.{self.region}.outscale.com"
|
|
77
|
+
elif service == "directlink":
|
|
78
|
+
return f"{self.protocol}://directlink.{self.region}.outscale.com"
|
|
79
|
+
else:
|
|
80
|
+
raise ValueError("Unknown service")
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def from_env() -> "Profile":
|
|
84
|
+
endpoint_kwargs = {
|
|
85
|
+
"api": os.environ.get("OSC_ENDPOINT_API"),
|
|
86
|
+
"oks": os.environ.get("OSC_ENDPOINT_OKS"),
|
|
87
|
+
"lbu": os.environ.get("OSC_ENDPOINT_LBU"),
|
|
88
|
+
"oos": os.environ.get("OSC_ENDPOINT_OOS"),
|
|
89
|
+
"fcu": os.environ.get("OSC_ENDPOINT_FCU"),
|
|
90
|
+
"eim": os.environ.get("OSC_ENDPOINT_EIM"),
|
|
91
|
+
"direct_link": os.environ.get("OSC_ENDPOINT_DIRECT_LINK"),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
profile_kwargs = {
|
|
95
|
+
"access_key": os.environ.get("OSC_ACCESS_KEY"),
|
|
96
|
+
"secret_key": os.environ.get("OSC_SECRET_KEY"),
|
|
97
|
+
"access_key_v2": os.environ.get("OSC_ACCESS_KEY_V2"),
|
|
98
|
+
"secret_key_v2": os.environ.get("OSC_SECRET_KEY_V2"),
|
|
99
|
+
"x509_client_cert": os.environ.get("OSC_X509_CLIENT_CERT"),
|
|
100
|
+
"x509_client_cert_b64": os.environ.get("OSC_X509_CLIENT_CERT_B64"),
|
|
101
|
+
"x509_client_key": os.environ.get("OSC_X509_CLIENT_KEY"),
|
|
102
|
+
"x509_client_key_b64": os.environ.get("OSC_X509_CLIENT_KEY_B64"),
|
|
103
|
+
"tls_skip_verify": os.environ.get("OSC_TLS_SKIP_VERIFY", "False").lower()
|
|
104
|
+
in ("true"),
|
|
105
|
+
"login": os.environ.get("OSC_LOGIN"),
|
|
106
|
+
"password": os.environ.get("OSC_PASSWORD"),
|
|
107
|
+
"protocol": os.environ.get("OSC_PROTOCOL"),
|
|
108
|
+
"region": os.environ.get("OSC_REGION"),
|
|
109
|
+
"endpoints": Endpoint(**endpoint_kwargs),
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
iam_v2_services_env = os.environ.get("OSC_IAM_V2_SERVICES", "")
|
|
113
|
+
if iam_v2_services_env:
|
|
114
|
+
profile_kwargs["iam_v2_services"] = [
|
|
115
|
+
s.strip() for s in iam_v2_services_env.split(",")
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
return Profile(**profile_kwargs)
|
|
119
|
+
|
|
120
|
+
@staticmethod
|
|
121
|
+
def __from_file(path: str, profile: str) -> "Profile":
|
|
122
|
+
with open(path) as f:
|
|
123
|
+
config = json.load(f)
|
|
124
|
+
kwargs_profile = config.get(profile)
|
|
125
|
+
kwargs_endpoints = kwargs_profile.get("endpoints", {})
|
|
126
|
+
kwargs_profile["endpoints"] = Endpoint(**kwargs_endpoints)
|
|
127
|
+
return Profile(**kwargs_profile)
|
|
128
|
+
|
|
129
|
+
def merge(self, other: "Profile"):
|
|
130
|
+
self.__dict__.update(
|
|
131
|
+
{
|
|
132
|
+
k: v
|
|
133
|
+
for k, v in other.__dict__.items()
|
|
134
|
+
if v is not None and k != "endpoints"
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
self.endpoints.__dict__.update(
|
|
138
|
+
{k: v for k, v in other.endpoints.__dict__.items() if v is not None}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def from_standard_configuration(path: str, profile: str) -> "Profile":
|
|
143
|
+
# 1. Load profile from environmental
|
|
144
|
+
merged_profile = Profile.from_env()
|
|
145
|
+
|
|
146
|
+
# 2. Load additional config from environment
|
|
147
|
+
if not profile:
|
|
148
|
+
value = os.environ.get("OSC_PROFILE")
|
|
149
|
+
if value:
|
|
150
|
+
profile = value
|
|
151
|
+
else:
|
|
152
|
+
profile = "default"
|
|
153
|
+
|
|
154
|
+
if not path:
|
|
155
|
+
value = os.environ.get("OSC_CONFIG_FILE")
|
|
156
|
+
if value:
|
|
157
|
+
path = value
|
|
158
|
+
else:
|
|
159
|
+
path = STD_PATH
|
|
160
|
+
|
|
161
|
+
# 3. Load profile for config file
|
|
162
|
+
try:
|
|
163
|
+
file_profile = Profile.__from_file(path, profile)
|
|
164
|
+
merged_profile.merge(file_profile)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
if path != STD_PATH or profile != "default":
|
|
167
|
+
raise e
|
|
168
|
+
|
|
169
|
+
# 4. Load default
|
|
170
|
+
if not merged_profile.protocol:
|
|
171
|
+
merged_profile.protocol = "https"
|
|
172
|
+
|
|
173
|
+
if not merged_profile.region:
|
|
174
|
+
merged_profile.region = "eu-west-2"
|
|
175
|
+
|
|
176
|
+
return merged_profile
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class Credentials(Profile):
|
|
180
|
+
def __init__(self, **kwargs):
|
|
181
|
+
warnings.warn(
|
|
182
|
+
"Credentials class is deprecated. Use Profile class instead.",
|
|
183
|
+
DeprecationWarning,
|
|
184
|
+
stacklevel=2,
|
|
185
|
+
)
|
|
186
|
+
super().__init__(**kwargs)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from datetime import datetime, timezone, timedelta
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RateLimiter:
|
|
6
|
+
def __init__(self, window: timedelta, max_requests: int, datetime_cls=datetime):
|
|
7
|
+
self.datetime_cls = datetime_cls
|
|
8
|
+
self.window: timedelta = window
|
|
9
|
+
self.max_requests: int = max_requests
|
|
10
|
+
self.requests = []
|
|
11
|
+
|
|
12
|
+
def acquire(self):
|
|
13
|
+
now = self.datetime_cls.now(timezone.utc)
|
|
14
|
+
|
|
15
|
+
self.clean_old_requests(now)
|
|
16
|
+
|
|
17
|
+
if len(self.requests) >= self.max_requests:
|
|
18
|
+
oldest = self.requests[0]
|
|
19
|
+
wait_time = self.window - (now - oldest)
|
|
20
|
+
time.sleep(wait_time.total_seconds())
|
|
21
|
+
|
|
22
|
+
now = self.datetime_cls.now(timezone.utc)
|
|
23
|
+
self.clean_old_requests(now)
|
|
24
|
+
|
|
25
|
+
self.requests.append(now)
|
|
26
|
+
|
|
27
|
+
def clean_old_requests(self, now):
|
|
28
|
+
while len(self.requests) > 0 and self.requests[0] <= now - self.window:
|
|
29
|
+
self.requests.pop(0)
|