Skip to content

Commit a599fbf

Browse files
committed
🐛 fix: crash when computing sleep time
1 parent 1705e0b commit a599fbf

17 files changed

Lines changed: 258 additions & 106 deletions

osc_sdk_python/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .outscale_gateway import LOG_MEMORY
66
from .version import get_version
77
from .problem import Problem, ProblemDecoder
8+
from .limiter import RateLimiter
89

910
# what to Log
1011
from .outscale_gateway import LOG_ALL
@@ -23,5 +24,6 @@
2324
"LOG_ALL",
2425
"LOG_KEEP_ONLY_LAST_REQ",
2526
"Problem",
26-
"ProblemDecoder"
27+
"ProblemDecoder",
28+
"RateLimiter",
2729
]

osc_sdk_python/authentication.py

Lines changed: 82 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,23 @@
55

66
from .version import get_version
77
from .credentials import Profile
8+
89
VERSION: str = get_version()
910
DEFAULT_USER_AGENT = "osc-sdk-python/" + VERSION
1011

12+
1113
class Authentication:
12-
def __init__(self, credentials: Profile, host: str,
13-
method='POST', service='api',
14-
content_type='application/json; charset=utf-8',
15-
algorithm='OSC4-HMAC-SHA256',
16-
signed_headers = 'content-type;host;x-osc-date',
17-
user_agent = DEFAULT_USER_AGENT):
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+
):
1825
self.access_key = credentials.access_key
1926
self.secret_key = credentials.secret_key
2027
self.login = credentials.login
@@ -31,34 +38,37 @@ def __init__(self, credentials: Profile, host: str,
3138

3239
def forge_headers_signed(self, uri, request_data):
3340
date_iso, date = self.build_dates()
34-
credential_scope = '{}/{}/{}/osc4_request'.format(date, self.region, self.service)
41+
credential_scope = "{}/{}/{}/osc4_request".format(
42+
date, self.region, self.service
43+
)
3544

3645
canonical_request = self.build_canonical_request(date_iso, uri, request_data)
37-
str_to_sign = self.create_string_to_sign(date_iso, credential_scope, canonical_request)
46+
str_to_sign = self.create_string_to_sign(
47+
date_iso, credential_scope, canonical_request
48+
)
3849
signature = self.compute_signature(date, str_to_sign)
3950
authorisation = self.build_authorization_header(credential_scope, signature)
4051

4152
return {
42-
'Content-Type': self.content_type,
43-
'X-Osc-Date': date_iso,
44-
'Authorization': authorisation,
45-
'User-Agent': self.user_agent,
53+
"Content-Type": self.content_type,
54+
"X-Osc-Date": date_iso,
55+
"Authorization": authorisation,
56+
"User-Agent": self.user_agent,
4657
}
4758

4859
def build_dates(self):
49-
'''Return YYYYMMDDTHHmmssZ, YYYYMMDD
50-
'''
60+
"""Return YYYYMMDDTHHmmssZ, YYYYMMDD"""
5161
t = datetime.datetime.now(datetime.timezone.utc)
52-
return t.strftime('%Y%m%dT%H%M%SZ'), t.strftime('%Y%m%d')
62+
return t.strftime("%Y%m%dT%H%M%SZ"), t.strftime("%Y%m%d")
5363

5464
def sign(self, key, msg):
5565
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
5666

5767
def get_signature_key(self, key, date_stamp_value):
58-
k_date = self.sign(('OSC4' + key).encode('utf-8'), date_stamp_value)
68+
k_date = self.sign(("OSC4" + key).encode("utf-8"), date_stamp_value)
5969
k_region = self.sign(k_date, self.region)
6070
k_service = self.sign(k_region, self.service)
61-
k_signing = self.sign(k_service, 'osc4_request')
71+
k_signing = self.sign(k_service, "osc4_request")
6272
return k_signing
6373

6474
def build_canonical_request(self, date_iso, canonical_uri, request_data):
@@ -81,44 +91,74 @@ def build_canonical_request(self, date_iso, canonical_uri, request_data):
8191
# Step 6: Create payload hash. In this example, the payload (body of
8292
# the request) contains the request parameters.
8393
# Step 7: Combine elements to create canonical request
84-
canonical_querystring = ''
85-
canonical_headers = 'content-type:' + self.content_type + '\n' \
86-
+ 'host:' + self.host + '\n' \
87-
+ 'x-osc-date:' + date_iso + '\n'
88-
payload_hash = hashlib.sha256(request_data.encode('utf-8')).hexdigest()
89-
return self.method + '\n' \
90-
+ canonical_uri + '\n' \
91-
+ canonical_querystring + '\n' \
92-
+ canonical_headers + '\n' \
93-
+ self.signed_headers + '\n' \
94-
+ payload_hash
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+
)
95120

96121
def create_string_to_sign(self, date_iso, credential_scope, canonical_request):
97122
# ************* TASK 2: CREATE THE STRING TO SIGN*************
98123
# Match the algorithm to the hashing algorithm you use, either SHA-1 or
99124
# SHA-256 (recommended)
100-
return self.algorithm + '\n' \
101-
+ date_iso + '\n' \
102-
+ credential_scope + '\n' \
103-
+ hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()
104-
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+
)
105134

106135
def compute_signature(self, date, string_to_sign):
107136
# ************* TASK 3: CALCULATE THE SIGNATURE *************
108137
# Create the signing key using the function defined above.
109138
signing_key = self.get_signature_key(self.secret_key, date)
110139

111140
# Sign the string_to_sign using the signing_key
112-
return hmac.new(signing_key, string_to_sign.encode('utf-8'),
113-
hashlib.sha256).hexdigest()
114-
141+
return hmac.new(
142+
signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
143+
).hexdigest()
115144

116145
def build_authorization_header(self, credential_scope, signature):
117146
# ************* TASK 4: ADD SIGNING INFORMATION TO THE REQUEST *************
118147
# Put the signature information in a header named Authorization.
119-
return self.algorithm + ' ' + 'Credential=' + self.access_key + '/' + credential_scope + ', ' \
120-
+ 'SignedHeaders=' + self.signed_headers + ', ' \
121-
+ 'Signature=' + signature
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+
)
122162

123163
def is_basic_auth_configured(self):
124164
return self.login is not None and self.password is not None
@@ -130,7 +170,7 @@ def get_basic_auth_header(self):
130170
b64_creds = str(base64.b64encode(creds.encode("utf-8")), "utf-8")
131171
date_iso, _ = self.build_dates()
132172
return {
133-
'Content-Type': self.content_type,
134-
'X-Osc-Date': date_iso,
135-
'Authorization': "Basic " + b64_creds
173+
"Content-Type": self.content_type,
174+
"X-Osc-Date": date_iso,
175+
"Authorization": "Basic " + b64_creds,
136176
}

osc_sdk_python/call.py

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import string
12
from .authentication import Authentication
23
from .authentication import DEFAULT_USER_AGENT
34
from .credentials import Profile
@@ -6,29 +7,35 @@
67
from requests.adapters import HTTPAdapter
78
from urllib3.util.retry import Retry
89
from urllib3.util import parse_url
10+
from datetime import timedelta
11+
from .limiter import RateLimiter
912

1013
import json
1114
import warnings
1215

13-
MAX_RETRIES = "3"
16+
MAX_RETRIES = 3
1417
RETRY_BACKOFF_FACTOR = "1"
1518
RETRY_BACKOFF_JITTER = "3"
1619
RETRY_BACKOFF_MAX = "30"
1720

21+
1822
class Call(object):
1923
def __init__(self, logger=None, limiter=None, **kwargs):
2024
self.version = kwargs.pop("version", "latest")
2125
self.host = kwargs.pop("host", None)
2226
self.ssl = kwargs.pop("_ssl", True)
2327
self.user_agent = kwargs.pop("user_agent", DEFAULT_USER_AGENT)
2428
self.logger = logger
25-
self.limiter = limiter
29+
self.limiter: RateLimiter | None = limiter
30+
self.adapter = None
31+
self.session = Session()
32+
2633
kwargs = self.update_limiter(**kwargs)
2734
kwargs = self.update_adapter(**kwargs)
2835
self.update_profile(**kwargs)
29-
self.session = Session()
30-
self.session.mount("https://", self.adapter)
31-
self.session.mount("http://", self.adapter)
36+
if self.adapter:
37+
self.session.mount("https://", self.adapter)
38+
self.session.mount("http://", self.adapter)
3239

3340
def update_credentials(self, **kwargs):
3441
warnings.warn(
@@ -39,16 +46,27 @@ def update_credentials(self, **kwargs):
3946
return self.update_profile(**kwargs)
4047

4148
def update_adapter(self, **kwargs):
42-
self.adapter = HTTPAdapter(
43-
max_retries=Retry(
44-
total=int(kwargs.pop("max_retries", MAX_RETRIES)),
45-
backoff_factor=float(kwargs.pop("retry_backoff_factor", RETRY_BACKOFF_FACTOR)),
46-
backoff_jitter=float(kwargs.pop("retry_backoff_jitter", RETRY_BACKOFF_JITTER)),
47-
backoff_max=float(kwargs.pop("retry_backoff_max", RETRY_BACKOFF_MAX)),
48-
status_forcelist=(400, 429, 500, 503),
49-
allowed_methods=("POST", "GET"),
49+
max_retries: int | str | None = kwargs.pop("max_retries", None)
50+
if max_retries:
51+
max_retries = int(max_retries)
52+
53+
if max_retries:
54+
self.adapter = HTTPAdapter(
55+
max_retries=Retry(
56+
total=max_retries or MAX_RETRIES,
57+
backoff_factor=float(
58+
kwargs.pop("retry_backoff_factor", RETRY_BACKOFF_FACTOR)
59+
),
60+
backoff_jitter=float(
61+
kwargs.pop("retry_backoff_jitter", RETRY_BACKOFF_JITTER)
62+
),
63+
backoff_max=float(
64+
kwargs.pop("retry_backoff_max", RETRY_BACKOFF_MAX)
65+
),
66+
status_forcelist=(400, 429, 500, 503),
67+
allowed_methods=("POST", "GET"),
68+
)
5069
)
51-
)
5270
return kwargs
5371

5472
def update_profile(self, **kwargs):
@@ -58,16 +76,13 @@ def update_profile(self, **kwargs):
5876
self.profile.merge(Profile(**kwargs))
5977
return kwargs
6078

61-
def update_limiter(
62-
self,
63-
**kwargs
64-
):
79+
def update_limiter(self, **kwargs):
6580
limiter_window = kwargs.pop("limiter_window", None)
66-
if limiter_window is not None:
67-
self.limiter.window = limiter_window
81+
if limiter_window is not None and self.limiter is not None:
82+
self.limiter.window = timedelta(seconds=int(limiter_window))
6883

6984
limiter_max_requests = kwargs.pop("limiter_max_requests", None)
70-
if limiter_max_requests is not None:
85+
if limiter_max_requests is not None and self.limiter is not None:
7186
self.limiter.max_requests = limiter_max_requests
7287

7388
return kwargs

osc_sdk_python/credentials.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
DEFAULT_PROFILE = "default"
99

1010

11-
12-
1311
class Endpoint:
1412
def __init__(self, **kwargs):
1513
self.api: str = kwargs.pop("api", None)
@@ -102,7 +100,8 @@ def from_env() -> "Profile":
102100
"x509_client_cert_b64": os.environ.get("OSC_X509_CLIENT_CERT_B64"),
103101
"x509_client_key": os.environ.get("OSC_X509_CLIENT_KEY"),
104102
"x509_client_key_b64": os.environ.get("OSC_X509_CLIENT_KEY_B64"),
105-
"tls_skip_verify": os.environ.get("OSC_TLS_SKIP_VERIFY", "False").lower() in ("true"),
103+
"tls_skip_verify": os.environ.get("OSC_TLS_SKIP_VERIFY", "False").lower()
104+
in ("true"),
106105
"login": os.environ.get("OSC_LOGIN"),
107106
"password": os.environ.get("OSC_PASSWORD"),
108107
"protocol": os.environ.get("OSC_PROTOCOL"),
@@ -176,10 +175,12 @@ def from_standard_configuration(path: str, profile: str) -> "Profile":
176175

177176
return merged_profile
178177

178+
179179
class Credentials(Profile):
180180
def __init__(self, **kwargs):
181-
warnings.warn("Credentials class is deprecated. Use Profile class instead.",
181+
warnings.warn(
182+
"Credentials class is deprecated. Use Profile class instead.",
182183
DeprecationWarning,
183-
stacklevel=2
184+
stacklevel=2,
184185
)
185-
super().__init__(**kwargs)
186+
super().__init__(**kwargs)

osc_sdk_python/limiter.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
from datetime import datetime, timezone, timedelta
1+
from datetime import datetime, timezone, timedelta, date
22
import time
33

44

55
class RateLimiter:
6-
def __init__(self, window: int, max_requests: int):
7-
self.window = window
8-
self.max_requests = max_requests
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
910
self.requests = []
1011

1112
def acquire(self):
12-
now = datetime.now(timezone.utc)
13+
now = self.datetime_cls.now(timezone.utc)
1314

1415
self.clean_old_requests(now)
1516

@@ -18,13 +19,11 @@ def acquire(self):
1819
wait_time = self.window - (now - oldest)
1920
time.sleep(wait_time.total_seconds())
2021

21-
now = datetime.now(timezone.utc)
22+
now = self.datetime_cls.now(timezone.utc)
2223
self.clean_old_requests(now)
2324

2425
self.requests.append(now)
2526

2627
def clean_old_requests(self, now):
27-
while len(self.requests) > 0 and self.requests[0] <= now - timedelta(
28-
seconds=self.window
29-
):
28+
while len(self.requests) > 0 and self.requests[0] <= now - self.window:
3029
self.requests.pop(0)

0 commit comments

Comments
 (0)