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.
Files changed (59) hide show
  1. version.py +9 -0
  2. yicloud/__init__.py +7 -0
  3. yicloud/base/__init__.py +58 -0
  4. yicloud/base/auth/__init__.py +10 -0
  5. yicloud/base/auth/credential.py +64 -0
  6. yicloud/base/auth/sign.py +80 -0
  7. yicloud/base/client.py +313 -0
  8. yicloud/base/config.py +62 -0
  9. yicloud/base/errs/__init__.py +292 -0
  10. yicloud/base/log/__init__.py +43 -0
  11. yicloud/base/log/logger.py +123 -0
  12. yicloud/base/log/std.py +70 -0
  13. yicloud/base/msgs/__init__.py +226 -0
  14. yicloud/base/utils/__init__.py +6 -0
  15. yicloud/base/utils/helps.py +110 -0
  16. yicloud/base/utils/retry/__init__.py +27 -0
  17. yicloud/base/utils/retry/retry.py +162 -0
  18. yicloud/services/__init__.py +24 -0
  19. yicloud/services/bc/__init__.py +9 -0
  20. yicloud/services/bc/actions.py +23 -0
  21. yicloud/services/bc/client.py +19 -0
  22. yicloud/services/bc/models.py +61 -0
  23. yicloud/services/fs/__init__.py +29 -0
  24. yicloud/services/fs/actions.py +109 -0
  25. yicloud/services/fs/client.py +19 -0
  26. yicloud/services/fs/models.py +152 -0
  27. yicloud/services/iam/__init__.py +27 -0
  28. yicloud/services/iam/actions.py +304 -0
  29. yicloud/services/iam/client.py +19 -0
  30. yicloud/services/iam/models.py +276 -0
  31. yicloud/services/job/__init__.py +44 -0
  32. yicloud/services/job/actions.py +167 -0
  33. yicloud/services/job/client.py +19 -0
  34. yicloud/services/job/models.py +268 -0
  35. yicloud/services/mc/__init__.py +59 -0
  36. yicloud/services/mc/actions.py +221 -0
  37. yicloud/services/mc/client.py +21 -0
  38. yicloud/services/mc/models.py +322 -0
  39. yicloud/services/modelrepo/__init__.py +33 -0
  40. yicloud/services/modelrepo/actions.py +163 -0
  41. yicloud/services/modelrepo/client.py +19 -0
  42. yicloud/services/modelrepo/models.py +146 -0
  43. yicloud/services/modelset/__init__.py +45 -0
  44. yicloud/services/modelset/actions.py +130 -0
  45. yicloud/services/modelset/client.py +19 -0
  46. yicloud/services/modelset/models.py +356 -0
  47. yicloud/services/oss/__init__.py +25 -0
  48. yicloud/services/oss/actions.py +83 -0
  49. yicloud/services/oss/client.py +19 -0
  50. yicloud/services/oss/models.py +113 -0
  51. yicloud/services/registry/__init__.py +42 -0
  52. yicloud/services/registry/actions.py +208 -0
  53. yicloud/services/registry/client.py +19 -0
  54. yicloud/services/registry/models.py +183 -0
  55. yicloud_sdk_python-0.1.0.dist-info/METADATA +145 -0
  56. yicloud_sdk_python-0.1.0.dist-info/RECORD +59 -0
  57. yicloud_sdk_python-0.1.0.dist-info/WHEEL +5 -0
  58. yicloud_sdk_python-0.1.0.dist-info/licenses/LICENSE +202 -0
  59. yicloud_sdk_python-0.1.0.dist-info/top_level.txt +2 -0
version.py ADDED
@@ -0,0 +1,9 @@
1
+ __version__ = "0.1.0"
2
+
3
+
4
+ def Version() -> str:
5
+ return __version__
6
+
7
+
8
+ def UserAgent() -> str:
9
+ return f"openapi-sdk-python/{__version__}"
yicloud/__init__.py ADDED
@@ -0,0 +1,7 @@
1
+ """
2
+ YiCloud Python SDK
3
+
4
+ A Python SDK for the YiCloud OpenAPI service.
5
+ """
6
+
7
+ __version__ = "0.1.0"
@@ -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,10 @@
1
+ from yicloud.base.auth.credential import Credential, DefaultCredential, sign
2
+ from yicloud.base.auth.sign import hmac_sign, parse_and_sort_query
3
+
4
+ __all__ = [
5
+ "Credential",
6
+ "DefaultCredential",
7
+ "sign",
8
+ "hmac_sign",
9
+ "parse_and_sort_query",
10
+ ]
@@ -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()