yicloud-sdk-python 0.1.0__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.
- version.py +9 -0
- yicloud/__init__.py +7 -0
- yicloud/base/__init__.py +58 -0
- yicloud/base/auth/__init__.py +10 -0
- yicloud/base/auth/credential.py +64 -0
- yicloud/base/auth/sign.py +80 -0
- yicloud/base/client.py +313 -0
- yicloud/base/config.py +62 -0
- yicloud/base/errs/__init__.py +292 -0
- yicloud/base/log/__init__.py +43 -0
- yicloud/base/log/logger.py +123 -0
- yicloud/base/log/std.py +70 -0
- yicloud/base/msgs/__init__.py +226 -0
- yicloud/base/utils/__init__.py +6 -0
- yicloud/base/utils/helps.py +110 -0
- yicloud/base/utils/retry/__init__.py +27 -0
- yicloud/base/utils/retry/retry.py +162 -0
- yicloud/services/__init__.py +24 -0
- yicloud/services/bc/__init__.py +9 -0
- yicloud/services/bc/actions.py +23 -0
- yicloud/services/bc/client.py +19 -0
- yicloud/services/bc/models.py +61 -0
- yicloud/services/fs/__init__.py +29 -0
- yicloud/services/fs/actions.py +109 -0
- yicloud/services/fs/client.py +19 -0
- yicloud/services/fs/models.py +152 -0
- yicloud/services/iam/__init__.py +27 -0
- yicloud/services/iam/actions.py +304 -0
- yicloud/services/iam/client.py +19 -0
- yicloud/services/iam/models.py +276 -0
- yicloud/services/job/__init__.py +44 -0
- yicloud/services/job/actions.py +167 -0
- yicloud/services/job/client.py +19 -0
- yicloud/services/job/models.py +268 -0
- yicloud/services/mc/__init__.py +59 -0
- yicloud/services/mc/actions.py +221 -0
- yicloud/services/mc/client.py +21 -0
- yicloud/services/mc/models.py +322 -0
- yicloud/services/modelrepo/__init__.py +33 -0
- yicloud/services/modelrepo/actions.py +163 -0
- yicloud/services/modelrepo/client.py +19 -0
- yicloud/services/modelrepo/models.py +146 -0
- yicloud/services/modelset/__init__.py +45 -0
- yicloud/services/modelset/actions.py +130 -0
- yicloud/services/modelset/client.py +19 -0
- yicloud/services/modelset/models.py +356 -0
- yicloud/services/oss/__init__.py +25 -0
- yicloud/services/oss/actions.py +83 -0
- yicloud/services/oss/client.py +19 -0
- yicloud/services/oss/models.py +113 -0
- yicloud/services/registry/__init__.py +42 -0
- yicloud/services/registry/actions.py +208 -0
- yicloud/services/registry/client.py +19 -0
- yicloud/services/registry/models.py +183 -0
- yicloud_sdk_python-0.1.0.dist-info/METADATA +145 -0
- yicloud_sdk_python-0.1.0.dist-info/RECORD +59 -0
- yicloud_sdk_python-0.1.0.dist-info/WHEEL +5 -0
- yicloud_sdk_python-0.1.0.dist-info/licenses/LICENSE +202 -0
- yicloud_sdk_python-0.1.0.dist-info/top_level.txt +2 -0
version.py
ADDED
yicloud/__init__.py
ADDED
yicloud/base/__init__.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from yicloud.base.client import (
|
|
2
|
+
Client,
|
|
3
|
+
DefaultClient,
|
|
4
|
+
new_client,
|
|
5
|
+
get,
|
|
6
|
+
post,
|
|
7
|
+
with_transport,
|
|
8
|
+
with_http_client,
|
|
9
|
+
with_host,
|
|
10
|
+
with_config,
|
|
11
|
+
with_credential,
|
|
12
|
+
with_logger,
|
|
13
|
+
new_net_err,
|
|
14
|
+
is_net_err,
|
|
15
|
+
action_of_path,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from yicloud.base import auth, config, errs, log, msgs, utils
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def new_config() -> config.Config:
|
|
22
|
+
"""Create a new config with default values from environment."""
|
|
23
|
+
return config.Config.new()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def new_credential() -> auth.Credential:
|
|
27
|
+
"""Create a new credential with default values from environment."""
|
|
28
|
+
return auth.Credential.new_credential()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"Client",
|
|
33
|
+
"DefaultClient",
|
|
34
|
+
"new_client",
|
|
35
|
+
"new_config",
|
|
36
|
+
"new_credential",
|
|
37
|
+
"get",
|
|
38
|
+
"post",
|
|
39
|
+
"with_transport",
|
|
40
|
+
"with_http_client",
|
|
41
|
+
"with_host",
|
|
42
|
+
"with_config",
|
|
43
|
+
"with_credential",
|
|
44
|
+
"with_logger",
|
|
45
|
+
"new_net_err",
|
|
46
|
+
"is_net_err",
|
|
47
|
+
"action_of_path",
|
|
48
|
+
"auth",
|
|
49
|
+
"config",
|
|
50
|
+
"errs",
|
|
51
|
+
"exc",
|
|
52
|
+
"log",
|
|
53
|
+
"msgs",
|
|
54
|
+
"utils",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
# exc alias for convenience, matches ucloud-sdk pattern
|
|
58
|
+
exc = errs
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
import json as json_module
|
|
4
|
+
from typing import Any
|
|
5
|
+
from yicloud.base.auth.sign import hmac_sign
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Credential:
|
|
9
|
+
"""Credential struct matching Go SDK's auth.Credential."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, public_key: str = "", private_key: str = ""):
|
|
12
|
+
self.public_key = public_key
|
|
13
|
+
self.private_key = private_key
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def new_credential(cls) -> "Credential":
|
|
17
|
+
"""Return credential with default values from env."""
|
|
18
|
+
cred = cls()
|
|
19
|
+
cred._apply_env()
|
|
20
|
+
return cred
|
|
21
|
+
|
|
22
|
+
def _apply_env(self):
|
|
23
|
+
"""Apply environment variables to credential."""
|
|
24
|
+
v = os.getenv("YICLOUD_PUBLIC_KEY")
|
|
25
|
+
if v and v.strip():
|
|
26
|
+
self.public_key = v
|
|
27
|
+
|
|
28
|
+
v = os.getenv("YICLOUD_SECRET_KEY")
|
|
29
|
+
if v and v.strip():
|
|
30
|
+
self.private_key = v
|
|
31
|
+
|
|
32
|
+
def sign(self, tick: time.struct_time, query: str, body: Any) -> str:
|
|
33
|
+
"""Create signature encoding query string to credential signature."""
|
|
34
|
+
body_str = Credential._any_to_string(body)
|
|
35
|
+
timestamp = str(int(time.mktime(tick) * 1000))
|
|
36
|
+
return hmac_sign(timestamp, self.public_key, self.private_key, query, body_str)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _any_to_string(value: Any) -> str:
|
|
40
|
+
"""Convert any type to signature string, matching Go SDK's anyToString."""
|
|
41
|
+
if value is None:
|
|
42
|
+
return ""
|
|
43
|
+
|
|
44
|
+
if isinstance(value, str):
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
if isinstance(value, bytes):
|
|
48
|
+
return value.decode("utf-8")
|
|
49
|
+
|
|
50
|
+
# For complex types (dict, list, etc.), use JSON serialization
|
|
51
|
+
if isinstance(value, (dict, list)):
|
|
52
|
+
return json_module.dumps(value, separators=(",", ":"))
|
|
53
|
+
|
|
54
|
+
# For simple types, use str()
|
|
55
|
+
return str(value)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Default credential singleton
|
|
59
|
+
DefaultCredential = Credential.new_credential()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def sign(tick: time.struct_time, query: str, body: Any) -> str:
|
|
63
|
+
"""Package-level sign function using DefaultCredential."""
|
|
64
|
+
return DefaultCredential.sign(tick, query, body)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import hmac
|
|
2
|
+
import hashlib
|
|
3
|
+
from urllib.parse import unquote, quote
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class KV:
|
|
7
|
+
"""KV represents a query parameter (preserve order)."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, key: str, value: str):
|
|
10
|
+
self.key = key
|
|
11
|
+
self.value = value
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_and_sort_query(raw: str) -> str:
|
|
15
|
+
"""
|
|
16
|
+
Parse and sort query string.
|
|
17
|
+
Matches Go SDK's parseAndSortQuery function.
|
|
18
|
+
"""
|
|
19
|
+
if not raw:
|
|
20
|
+
return ""
|
|
21
|
+
|
|
22
|
+
parts = raw.split("&")
|
|
23
|
+
kvs = []
|
|
24
|
+
|
|
25
|
+
for p in parts:
|
|
26
|
+
if not p:
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
if "=" in p:
|
|
30
|
+
arr = p.split("=", 1)
|
|
31
|
+
key = unquote(arr[0]) if arr[0] else ""
|
|
32
|
+
val = unquote(arr[1]) if len(arr) > 1 else ""
|
|
33
|
+
else:
|
|
34
|
+
key = unquote(p) if p else ""
|
|
35
|
+
val = ""
|
|
36
|
+
|
|
37
|
+
kvs.append(KV(key, val))
|
|
38
|
+
|
|
39
|
+
# Stable sort: key ASC, same key keeps original order
|
|
40
|
+
kvs.sort(key=lambda kv: kv.key)
|
|
41
|
+
|
|
42
|
+
# Concatenate: key + value
|
|
43
|
+
result = ""
|
|
44
|
+
for kv in kvs:
|
|
45
|
+
result += kv.key + kv.value
|
|
46
|
+
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def hmac_sign(
|
|
51
|
+
ts: str, accessKey: str, secretKey: str, query: str, body: str
|
|
52
|
+
) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Generate HMAC-SHA256 signature following the pattern:
|
|
55
|
+
ts + accessKey + sortedQuery + body
|
|
56
|
+
Matches Go SDK's HmacSign function.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
ts: timestamp string, e.g., "1678901234"
|
|
60
|
+
accessKey: user's accessKey
|
|
61
|
+
secretKey: user's secretKey
|
|
62
|
+
query: query parameters
|
|
63
|
+
body: request body, if JSON object, structure as dict, no body can be None
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
hex-encoded HMAC-SHA256 signature
|
|
67
|
+
"""
|
|
68
|
+
# 1. Process query parameters
|
|
69
|
+
QS = parse_and_sort_query(query)
|
|
70
|
+
|
|
71
|
+
# 2. Build string to sign
|
|
72
|
+
toSign = ts + accessKey + QS + body
|
|
73
|
+
|
|
74
|
+
# 3. HMAC-SHA256
|
|
75
|
+
mac = hmac.new(
|
|
76
|
+
secretKey.encode("utf-8"), toSign.encode("utf-8"), hashlib.sha256
|
|
77
|
+
)
|
|
78
|
+
signature = mac.hexdigest()
|
|
79
|
+
|
|
80
|
+
return signature
|
yicloud/base/client.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Callable, Optional
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from yicloud.base import config, auth, errs, log, msgs, utils
|
|
9
|
+
from yicloud.base.utils.retry import retry, with_attempts
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DataclassEncoder(json.JSONEncoder):
|
|
13
|
+
def default(self, o):
|
|
14
|
+
if dataclasses.is_dataclass(o):
|
|
15
|
+
return dataclasses.asdict(o)
|
|
16
|
+
return super().default(o)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Context = Optional[dict]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Client:
|
|
23
|
+
"""Client struct matching Go SDK's base.Client."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
cfg: config.Config = None,
|
|
28
|
+
crede: auth.Credential = None,
|
|
29
|
+
http_client=None,
|
|
30
|
+
transport=None,
|
|
31
|
+
logger: log.Logger = None,
|
|
32
|
+
host: str = "",
|
|
33
|
+
):
|
|
34
|
+
self.config = cfg or config.DefaultConfig
|
|
35
|
+
self.crede = crede or auth.DefaultCredential
|
|
36
|
+
self.transport = transport
|
|
37
|
+
self.logger = logger or log.get_default_logger()
|
|
38
|
+
self.host = host
|
|
39
|
+
|
|
40
|
+
# Create requests session
|
|
41
|
+
if http_client:
|
|
42
|
+
self.http_client = http_client
|
|
43
|
+
else:
|
|
44
|
+
self.http_client = requests.Session()
|
|
45
|
+
self.http_client.verify = True # SSL verification
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def new(cls, *options: Callable[["Client"], None]) -> "Client":
|
|
49
|
+
"""Create client with options."""
|
|
50
|
+
c = cls(
|
|
51
|
+
cfg=config.DefaultConfig,
|
|
52
|
+
crede=auth.DefaultCredential,
|
|
53
|
+
logger=log.get_default_logger(),
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
for opt in options:
|
|
57
|
+
opt(c)
|
|
58
|
+
|
|
59
|
+
# Recreate http_client if transport was set
|
|
60
|
+
if c.transport:
|
|
61
|
+
c.http_client = requests.Session()
|
|
62
|
+
c.http_client.verify = True
|
|
63
|
+
|
|
64
|
+
return c
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def new_client(*options: Callable[["Client"], None]) -> Client:
|
|
68
|
+
"""Module-level function to create a client with options."""
|
|
69
|
+
return Client.new(*options)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Default client singleton
|
|
73
|
+
DefaultClient = new_client()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def with_transport(transport) -> Callable[[Client], None]:
|
|
77
|
+
return lambda c: setattr(c, "transport", transport)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def with_http_client(http_client) -> Callable[[Client], None]:
|
|
81
|
+
return lambda c: setattr(c, "http_client", http_client)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def with_host(host: str) -> Callable[[Client], None]:
|
|
85
|
+
return lambda c: setattr(c, "host", host)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def with_config(cfg: config.Config) -> Callable[[Client], None]:
|
|
89
|
+
return lambda c: setattr(c, "config", cfg)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def with_credential(crede: auth.Credential) -> Callable[[Client], None]:
|
|
93
|
+
return lambda c: setattr(c, "crede", crede)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def with_logger(l: log.Logger) -> Callable[[Client], None]:
|
|
97
|
+
return lambda c: setattr(c, "logger", l)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class netErr(Exception):
|
|
101
|
+
"""Network error type matching Go SDK's netErr."""
|
|
102
|
+
def __init__(self, desc: str):
|
|
103
|
+
self.desc = desc
|
|
104
|
+
|
|
105
|
+
def __str__(self) -> str:
|
|
106
|
+
return self.desc
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def new_net_err(desc: str) -> netErr:
|
|
110
|
+
return netErr(desc)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def is_net_err(err: Exception) -> bool:
|
|
114
|
+
return isinstance(err, netErr)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def action_of_path(p: str) -> str:
|
|
118
|
+
"""Extract action name from path."""
|
|
119
|
+
return Path(p).name
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Module-level get using DefaultClient
|
|
123
|
+
def get(ctx: Context, path: str, query: Any, rsp: msgs.Response) -> None:
|
|
124
|
+
"""GET request using DefaultClient.
|
|
125
|
+
|
|
126
|
+
Raises YiCloudException on error.
|
|
127
|
+
"""
|
|
128
|
+
DefaultClient.get(ctx, path, query, rsp)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get(self, ctx: Context, path: str, query: Any, rsp: msgs.Response) -> None:
|
|
132
|
+
"""GET request, matching Go SDK's Get.
|
|
133
|
+
|
|
134
|
+
Raises YiCloudException on error.
|
|
135
|
+
"""
|
|
136
|
+
query_str = utils.build_query(query)
|
|
137
|
+
err = self._request(ctx, "GET", path, query_str, None, rsp)
|
|
138
|
+
if err:
|
|
139
|
+
raise err
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Add get method to Client class
|
|
143
|
+
Client.get = get
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# Module-level post using DefaultClient
|
|
147
|
+
def post(ctx: Context, path: str, body: Any, rsp: msgs.Response) -> None:
|
|
148
|
+
"""POST request using DefaultClient.
|
|
149
|
+
|
|
150
|
+
Raises YiCloudException on error.
|
|
151
|
+
"""
|
|
152
|
+
DefaultClient.post(ctx, path, body, rsp)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def post(self, ctx: Context, path: str, body: Any, rsp: msgs.Response) -> None:
|
|
156
|
+
"""POST request, matching Go SDK's Post.
|
|
157
|
+
|
|
158
|
+
Raises YiCloudException on error.
|
|
159
|
+
"""
|
|
160
|
+
err = self._request(ctx, "POST", path, "", body, rsp)
|
|
161
|
+
if err:
|
|
162
|
+
raise err
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# Add post method to Client class
|
|
166
|
+
Client.post = post
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _request(
|
|
170
|
+
self,
|
|
171
|
+
ctx: Context,
|
|
172
|
+
method: str,
|
|
173
|
+
path: str,
|
|
174
|
+
query: str,
|
|
175
|
+
body: Any,
|
|
176
|
+
rsp: msgs.Response,
|
|
177
|
+
) -> Optional[errs.YiCloudException]:
|
|
178
|
+
"""
|
|
179
|
+
Core request method matching Go SDK's request.
|
|
180
|
+
|
|
181
|
+
Returns None on success, or a YiCloudException on error.
|
|
182
|
+
"""
|
|
183
|
+
action = action_of_path(path)
|
|
184
|
+
|
|
185
|
+
# Validate credential
|
|
186
|
+
if not self.crede.public_key or not self.crede.private_key:
|
|
187
|
+
return errs.new_client_error(
|
|
188
|
+
action,
|
|
189
|
+
Exception("public key or secret key not provided")
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
now = time.localtime()
|
|
193
|
+
tick_str = str(int(time.mktime(now) * 1000))
|
|
194
|
+
|
|
195
|
+
# Validate response
|
|
196
|
+
if rsp is None:
|
|
197
|
+
return errs.new_client_error(action, Exception("rsp cannot be None"))
|
|
198
|
+
|
|
199
|
+
# Build URL
|
|
200
|
+
full_url = self.config.host + path
|
|
201
|
+
if query:
|
|
202
|
+
full_url += "?" + query
|
|
203
|
+
|
|
204
|
+
# Build body
|
|
205
|
+
req_body = None
|
|
206
|
+
body_str = ""
|
|
207
|
+
if body is not None:
|
|
208
|
+
try:
|
|
209
|
+
bs = json.dumps(body, separators=(",", ":"), cls=DataclassEncoder)
|
|
210
|
+
body_str = bs
|
|
211
|
+
req_body = bs
|
|
212
|
+
except Exception as e:
|
|
213
|
+
return errs.new_client_error(action, e)
|
|
214
|
+
|
|
215
|
+
# Generate signature
|
|
216
|
+
try:
|
|
217
|
+
sign = self.crede.sign(now, query, body_str)
|
|
218
|
+
except Exception as e:
|
|
219
|
+
return errs.new_client_error(action, e)
|
|
220
|
+
|
|
221
|
+
# Build headers
|
|
222
|
+
headers = {
|
|
223
|
+
"Content-Type": "application/json",
|
|
224
|
+
"User-Agent": self.config.get_user_agent(),
|
|
225
|
+
"X-OGW-PUBLIC-KEY": self.crede.public_key,
|
|
226
|
+
"X-OGW-TICK": tick_str,
|
|
227
|
+
"X-OGW-SIGN": sign,
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# Execute request
|
|
231
|
+
resp: Optional[requests.Response] = None
|
|
232
|
+
try:
|
|
233
|
+
if self.config.max_retries > 0:
|
|
234
|
+
# Retry logic for network errors only
|
|
235
|
+
def request_fn(cl_ctx: Context) -> requests.Response:
|
|
236
|
+
nonlocal resp
|
|
237
|
+
resp = self.http_client.request(
|
|
238
|
+
method=method,
|
|
239
|
+
url=full_url,
|
|
240
|
+
headers=headers,
|
|
241
|
+
data=req_body,
|
|
242
|
+
timeout=self.config.timeout,
|
|
243
|
+
)
|
|
244
|
+
return resp
|
|
245
|
+
|
|
246
|
+
resp = retry(ctx, request_fn, with_attempts(self.config.max_retries))
|
|
247
|
+
else:
|
|
248
|
+
resp = self.http_client.request(
|
|
249
|
+
method=method,
|
|
250
|
+
url=full_url,
|
|
251
|
+
headers=headers,
|
|
252
|
+
data=req_body,
|
|
253
|
+
timeout=self.config.timeout,
|
|
254
|
+
)
|
|
255
|
+
except requests.RequestException as e:
|
|
256
|
+
return errs.new_client_error(action, e)
|
|
257
|
+
|
|
258
|
+
# Parse response headers
|
|
259
|
+
request_id = resp.headers.get("X-OGW-Request-Id", "")
|
|
260
|
+
|
|
261
|
+
# Write meta to context
|
|
262
|
+
meta, ok = msgs.get_rsp_meta(ctx)
|
|
263
|
+
if ok and meta:
|
|
264
|
+
meta.request_id = request_id
|
|
265
|
+
|
|
266
|
+
# HTTP status check
|
|
267
|
+
if resp.status_code != 200:
|
|
268
|
+
return errs.new_server_error(action, resp.status_code)
|
|
269
|
+
|
|
270
|
+
# Parse response body
|
|
271
|
+
resp_bytes = resp.content
|
|
272
|
+
|
|
273
|
+
# Deserialize
|
|
274
|
+
try:
|
|
275
|
+
resp_dict = json.loads(resp_bytes)
|
|
276
|
+
except Exception as e:
|
|
277
|
+
return errs.new_client_error(action, e)
|
|
278
|
+
|
|
279
|
+
# Fill response from JSON
|
|
280
|
+
if isinstance(rsp, msgs.Rsp):
|
|
281
|
+
# Header
|
|
282
|
+
rsp.header = msgs.Header(
|
|
283
|
+
code=resp_dict.get("Code", 0),
|
|
284
|
+
msg=resp_dict.get("Msg", ""),
|
|
285
|
+
cost=resp_dict.get("_cost", 0),
|
|
286
|
+
err=resp_dict.get("_err", "")
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Data
|
|
290
|
+
if "Data" in resp_dict and resp_dict["Data"] is not None:
|
|
291
|
+
rsp.data = resp_dict["Data"]
|
|
292
|
+
|
|
293
|
+
# Update meta with cost and error
|
|
294
|
+
if ok and meta:
|
|
295
|
+
if hasattr(rsp, "header"):
|
|
296
|
+
meta.cost = rsp.header.cost
|
|
297
|
+
meta.err = rsp.header.err
|
|
298
|
+
|
|
299
|
+
# Business return code check
|
|
300
|
+
if rsp.get_code() != 0:
|
|
301
|
+
return errs.new_server_error(
|
|
302
|
+
action,
|
|
303
|
+
resp.status_code,
|
|
304
|
+
rsp.get_code(),
|
|
305
|
+
rsp.get_msg(),
|
|
306
|
+
request_id
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# Add _request method to Client class
|
|
313
|
+
Client._request = _request
|
yicloud/base/config.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import version
|
|
3
|
+
from yicloud.base.log import Level
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Config:
|
|
7
|
+
"""Config struct matching Go SDK's config.Config."""
|
|
8
|
+
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
host: str = "",
|
|
12
|
+
user_agent: str = "",
|
|
13
|
+
timeout: float = 30.0,
|
|
14
|
+
max_retries: int = 0,
|
|
15
|
+
log_level: int = Level.WARN,
|
|
16
|
+
):
|
|
17
|
+
self.host = host
|
|
18
|
+
self.user_agent = user_agent
|
|
19
|
+
self.timeout = timeout
|
|
20
|
+
self.max_retries = max_retries
|
|
21
|
+
self.log_level = log_level
|
|
22
|
+
|
|
23
|
+
def get_user_agent(self) -> str:
|
|
24
|
+
return self.user_agent
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def new_default_config() -> "Config":
|
|
28
|
+
cfg = Config(
|
|
29
|
+
host="https://gate.yicloud.com",
|
|
30
|
+
user_agent=version.UserAgent(),
|
|
31
|
+
timeout=30.0,
|
|
32
|
+
max_retries=0,
|
|
33
|
+
log_level=Level.WARN,
|
|
34
|
+
)
|
|
35
|
+
cfg._apply_env()
|
|
36
|
+
return cfg
|
|
37
|
+
|
|
38
|
+
def _apply_env(self):
|
|
39
|
+
"""Apply environment variables to config."""
|
|
40
|
+
v = os.getenv("YICLOUD_API_HOST")
|
|
41
|
+
if v and v.strip():
|
|
42
|
+
self.host = v
|
|
43
|
+
|
|
44
|
+
v = os.getenv("YICLOUD_TIMEOUT")
|
|
45
|
+
if v and v.strip():
|
|
46
|
+
if v.isdigit():
|
|
47
|
+
self.timeout = float(v)
|
|
48
|
+
|
|
49
|
+
v = os.getenv("YICLOUD_MAX_RETRIES")
|
|
50
|
+
if v and v.strip():
|
|
51
|
+
try:
|
|
52
|
+
self.max_retries = int(v)
|
|
53
|
+
except ValueError:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def new() -> "Config":
|
|
58
|
+
return Config.new_default_config()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Default config singleton
|
|
62
|
+
DefaultConfig = Config.new_default_config()
|