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 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)