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
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import contextvars
|
|
2
|
+
from typing import Any, Dict, Generic, Optional, Tuple, TypeVar, Union, get_args, get_origin
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, fields, is_dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# === Response Types ===
|
|
8
|
+
|
|
9
|
+
class Response(ABC):
|
|
10
|
+
"""Response interface matching Go SDK's Response."""
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def get_code(self) -> int: ...
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def get_msg(self) -> str: ...
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def get_data(self) -> Any: ...
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def get_cost(self) -> int: ...
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def get_err(self) -> str: ...
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def fill_from_json(self, data: bytes): ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class Header:
|
|
33
|
+
"""Header struct matching Go SDK's Header."""
|
|
34
|
+
code: int
|
|
35
|
+
msg: str
|
|
36
|
+
cost: int = 0
|
|
37
|
+
err: str = ""
|
|
38
|
+
|
|
39
|
+
def get_code(self) -> int:
|
|
40
|
+
return self.code
|
|
41
|
+
|
|
42
|
+
def get_msg(self) -> str:
|
|
43
|
+
return self.msg
|
|
44
|
+
|
|
45
|
+
def get_cost(self) -> int:
|
|
46
|
+
return self.cost
|
|
47
|
+
|
|
48
|
+
def get_err(self) -> str:
|
|
49
|
+
return self.err
|
|
50
|
+
|
|
51
|
+
def __str__(self) -> str:
|
|
52
|
+
return f"code:{self.code}, msg:{self.msg}, cost:{self.cost}, error:{self.err}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
T = TypeVar("T")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _dict_to_dataclass(target_type, data):
|
|
59
|
+
"""Recursively convert a dict to a typed dataclass instance."""
|
|
60
|
+
if not is_dataclass(target_type) or not isinstance(data, dict):
|
|
61
|
+
return data
|
|
62
|
+
|
|
63
|
+
field_types = {f.name: f.type for f in fields(target_type)}
|
|
64
|
+
|
|
65
|
+
kwargs = {}
|
|
66
|
+
for key, value in data.items():
|
|
67
|
+
if key in field_types:
|
|
68
|
+
kwargs[key] = _convert_value(field_types[key], value)
|
|
69
|
+
|
|
70
|
+
return target_type(**kwargs)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _convert_value(field_type, value):
|
|
74
|
+
"""Convert a value to match the target type annotation."""
|
|
75
|
+
if value is None:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
origin = get_origin(field_type)
|
|
79
|
+
args = get_args(field_type)
|
|
80
|
+
|
|
81
|
+
# Handle Optional[X] = Union[X, None]
|
|
82
|
+
if origin is Union:
|
|
83
|
+
non_none = [a for a in args if a is not type(None)]
|
|
84
|
+
if len(non_none) == 1:
|
|
85
|
+
return _convert_value(non_none[0], value)
|
|
86
|
+
return value
|
|
87
|
+
|
|
88
|
+
# Handle List[X]
|
|
89
|
+
if origin is list:
|
|
90
|
+
if args and is_dataclass(args[0]):
|
|
91
|
+
return [_dict_to_dataclass(args[0], item) if isinstance(item, dict) else item for item in value]
|
|
92
|
+
return value
|
|
93
|
+
|
|
94
|
+
# Handle Dict[str, X]
|
|
95
|
+
if origin is dict:
|
|
96
|
+
if args and len(args) == 2 and is_dataclass(args[1]):
|
|
97
|
+
return {k: _dict_to_dataclass(args[1], v) if isinstance(v, dict) else v for k, v in value.items()}
|
|
98
|
+
return value
|
|
99
|
+
|
|
100
|
+
# Handle direct dataclass field
|
|
101
|
+
if is_dataclass(field_type) and isinstance(value, dict):
|
|
102
|
+
return _dict_to_dataclass(field_type, value)
|
|
103
|
+
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class Rsp(Generic[T], Response):
|
|
109
|
+
"""Generic response type matching Go SDK's Rsp[T]."""
|
|
110
|
+
header: Header
|
|
111
|
+
data: Optional[T] = None
|
|
112
|
+
|
|
113
|
+
def __init__(self, header: Optional[Header] = None, data: Optional[T] = None):
|
|
114
|
+
if header is None:
|
|
115
|
+
header = Header(code=0, msg="")
|
|
116
|
+
self.header = header
|
|
117
|
+
self.data = data
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def empty(cls) -> "Rsp[None]":
|
|
121
|
+
return Rsp(header=Header(code=0, msg=""), data=None)
|
|
122
|
+
|
|
123
|
+
def get_code(self) -> int:
|
|
124
|
+
return self.header.code
|
|
125
|
+
|
|
126
|
+
def get_msg(self) -> str:
|
|
127
|
+
return self.header.msg
|
|
128
|
+
|
|
129
|
+
def get_data(self) -> Any:
|
|
130
|
+
return self.data
|
|
131
|
+
|
|
132
|
+
def get_cost(self) -> int:
|
|
133
|
+
return self.header.cost
|
|
134
|
+
|
|
135
|
+
def get_err(self) -> str:
|
|
136
|
+
return self.header.err
|
|
137
|
+
|
|
138
|
+
def fill_from_json(self, data: bytes):
|
|
139
|
+
"""Fill data from JSON bytes."""
|
|
140
|
+
pass # Implementation optional
|
|
141
|
+
|
|
142
|
+
def get_typed_data(self) -> Optional[T]:
|
|
143
|
+
if self.data is None:
|
|
144
|
+
return None
|
|
145
|
+
if not isinstance(self.data, dict):
|
|
146
|
+
return self.data
|
|
147
|
+
|
|
148
|
+
orig = getattr(self, "__orig_class__", None)
|
|
149
|
+
if orig is not None:
|
|
150
|
+
type_args = getattr(orig, "__args__", None)
|
|
151
|
+
if type_args and type_args[0] is not None and type_args[0] is not type(None):
|
|
152
|
+
target = type_args[0]
|
|
153
|
+
if is_dataclass(target):
|
|
154
|
+
return _dict_to_dataclass(target, self.data)
|
|
155
|
+
|
|
156
|
+
return self.data
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# Type aliases matching Go SDK
|
|
160
|
+
RawRsp = Rsp[Any]
|
|
161
|
+
Empty = Rsp[None]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# === Context Metadata ===
|
|
165
|
+
|
|
166
|
+
# Context var for storing response metadata
|
|
167
|
+
_ctx_meta = contextvars.ContextVar[Optional["RspMeta"]]("rsp_meta", default=None)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass
|
|
171
|
+
class RspMeta:
|
|
172
|
+
"""Response metadata matching Go SDK's RspMeta."""
|
|
173
|
+
request_id: str = ""
|
|
174
|
+
cost: int = 0
|
|
175
|
+
err: str = ""
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# Type alias for context (None represents no context)
|
|
179
|
+
Context = Optional[Dict[str, Any]]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def with_meta_ctx(ctx: Context = None) -> Tuple[Context, RspMeta]:
|
|
183
|
+
"""Create a new context with response metadata."""
|
|
184
|
+
meta = RspMeta()
|
|
185
|
+
# Store meta reference in context dict
|
|
186
|
+
if ctx is None:
|
|
187
|
+
ctx = {}
|
|
188
|
+
ctx["_meta"] = meta
|
|
189
|
+
return ctx, meta
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def new_meta_ctx() -> Tuple[Context, RspMeta]:
|
|
193
|
+
"""Create a new meta context with empty base context."""
|
|
194
|
+
return with_meta_ctx(None)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def get_rsp_meta(ctx: Context) -> Tuple[Optional[RspMeta], bool]:
|
|
198
|
+
"""Get response metadata from context."""
|
|
199
|
+
if ctx is None:
|
|
200
|
+
return None, False
|
|
201
|
+
|
|
202
|
+
meta = ctx.get("_meta")
|
|
203
|
+
if meta is not None:
|
|
204
|
+
return meta, True
|
|
205
|
+
return None, False
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def clear_meta_ctx(ctx: Context = None):
|
|
209
|
+
"""Clear the meta context."""
|
|
210
|
+
if ctx is not None:
|
|
211
|
+
ctx.pop("_meta", None)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
__all__ = [
|
|
215
|
+
"Response",
|
|
216
|
+
"Header",
|
|
217
|
+
"Rsp",
|
|
218
|
+
"RawRsp",
|
|
219
|
+
"Empty",
|
|
220
|
+
"RspMeta",
|
|
221
|
+
"with_meta_ctx",
|
|
222
|
+
"new_meta_ctx",
|
|
223
|
+
"get_rsp_meta",
|
|
224
|
+
"clear_meta_ctx",
|
|
225
|
+
"Context",
|
|
226
|
+
]
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import dataclasses
|
|
3
|
+
from typing import Any, Optional, Type, get_type_hints
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from urllib.parse import urlencode
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_current_func_name() -> str:
|
|
9
|
+
"""Get current function name for path construction."""
|
|
10
|
+
try:
|
|
11
|
+
frame = inspect.stack()[1]
|
|
12
|
+
return frame.function
|
|
13
|
+
except (IndexError, ValueError):
|
|
14
|
+
return "unknown"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def parse_json_tag(field_name: str, field_type: Type, default_value: Any = None):
|
|
18
|
+
"""
|
|
19
|
+
Parse json tag from field (simulating Go's json tag behavior).
|
|
20
|
+
|
|
21
|
+
Since Python doesn't have struct tags, we use a convention:
|
|
22
|
+
- Field name in snake_case maps to PascalCase for json key
|
|
23
|
+
- We'll use a helper class attribute or __json__ dict for customization
|
|
24
|
+
|
|
25
|
+
For simplicity, we'll convert snake_case to CamelCase like Go.
|
|
26
|
+
"""
|
|
27
|
+
# Default key: convert to PascalCase from snake_case
|
|
28
|
+
if "_" in field_name:
|
|
29
|
+
parts = field_name.split("_")
|
|
30
|
+
key = "".join(p.capitalize() for p in parts)
|
|
31
|
+
else:
|
|
32
|
+
key = field_name[0].upper() + field_name[1:] if field_name else field_name
|
|
33
|
+
|
|
34
|
+
# For now, we don't have a way to specify omitempty or skip in Python
|
|
35
|
+
# without using custom decorators or separate metadata
|
|
36
|
+
omitempty = False
|
|
37
|
+
skip = False
|
|
38
|
+
|
|
39
|
+
# Check if field value is Optional (has None as default)
|
|
40
|
+
if default_value is None:
|
|
41
|
+
# For Optional fields, we treat them as pointer-like in Go
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
return key, omitempty, skip
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def build_query(kvs: Any) -> str:
|
|
48
|
+
"""
|
|
49
|
+
Build query string from dataclass/dict using field annotations.
|
|
50
|
+
Matches Go SDK's BuildQuery behavior.
|
|
51
|
+
|
|
52
|
+
For dataclasses, uses the field name (converted to CamelCase) as key.
|
|
53
|
+
For None values in Optional fields, skips them.
|
|
54
|
+
For zero values in non-Optional fields without omitempty, includes them.
|
|
55
|
+
|
|
56
|
+
Supported types: str, int, float, bool
|
|
57
|
+
"""
|
|
58
|
+
if kvs is None:
|
|
59
|
+
return ""
|
|
60
|
+
|
|
61
|
+
if isinstance(kvs, str):
|
|
62
|
+
return kvs
|
|
63
|
+
|
|
64
|
+
values = OrderedDict()
|
|
65
|
+
|
|
66
|
+
# Handle dataclass
|
|
67
|
+
if dataclasses.is_dataclass(kvs):
|
|
68
|
+
for field in dataclasses.fields(kvs):
|
|
69
|
+
field_name = field.name
|
|
70
|
+
field_value = getattr(kvs, field_name)
|
|
71
|
+
|
|
72
|
+
# Parse the json tag (key name)
|
|
73
|
+
# In Python, we convert snake_case to CamelCase
|
|
74
|
+
key, omitempty, skip = parse_json_tag(field_name, field.type)
|
|
75
|
+
|
|
76
|
+
if skip:
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
# Handle Optional/None fields (like Go pointers)
|
|
80
|
+
if field_value is None:
|
|
81
|
+
# Check if field has a default of None (Optional)
|
|
82
|
+
# If so, skip (like nil pointer in Go)
|
|
83
|
+
if field.default is dataclasses.MISSING and field.default_factory is dataclasses.MISSING:
|
|
84
|
+
# No default, None means explicit skip? Actually in Go, nil = always skip
|
|
85
|
+
continue
|
|
86
|
+
else:
|
|
87
|
+
# Has default of None, skip if None
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
# Handle omitempty for non-None values
|
|
91
|
+
if omitempty and field_value in ("", 0, False, [], {}):
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
# Convert value to string
|
|
95
|
+
if isinstance(field_value, bool):
|
|
96
|
+
values[key] = "true" if field_value else "false"
|
|
97
|
+
else:
|
|
98
|
+
values[key] = str(field_value)
|
|
99
|
+
|
|
100
|
+
# Handle dict
|
|
101
|
+
elif isinstance(kvs, dict):
|
|
102
|
+
for key, value in kvs.items():
|
|
103
|
+
if value is None:
|
|
104
|
+
continue
|
|
105
|
+
if isinstance(value, bool):
|
|
106
|
+
values[key] = "true" if value else "false"
|
|
107
|
+
else:
|
|
108
|
+
values[key] = str(value)
|
|
109
|
+
|
|
110
|
+
return urlencode(values)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from yicloud.base.utils.retry.retry import (
|
|
2
|
+
retry,
|
|
3
|
+
is_retry_err,
|
|
4
|
+
retryErr,
|
|
5
|
+
with_attempts,
|
|
6
|
+
with_logs,
|
|
7
|
+
with_retry_if,
|
|
8
|
+
with_on_retry,
|
|
9
|
+
with_duration,
|
|
10
|
+
with_factor,
|
|
11
|
+
with_jitter,
|
|
12
|
+
Context,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"retry",
|
|
17
|
+
"is_retry_err",
|
|
18
|
+
"retryErr",
|
|
19
|
+
"with_attempts",
|
|
20
|
+
"with_logs",
|
|
21
|
+
"with_retry_if",
|
|
22
|
+
"with_on_retry",
|
|
23
|
+
"with_duration",
|
|
24
|
+
"with_factor",
|
|
25
|
+
"with_jitter",
|
|
26
|
+
"Context",
|
|
27
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import random
|
|
3
|
+
import math
|
|
4
|
+
from typing import Callable, Optional, Any, Dict, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class retryErr(Exception):
|
|
8
|
+
"""Retry error wrapping the original error."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, desc: str, origin: Optional[Exception] = None):
|
|
11
|
+
self.desc = desc
|
|
12
|
+
self.origin = origin
|
|
13
|
+
|
|
14
|
+
def __str__(self) -> str:
|
|
15
|
+
if self.origin:
|
|
16
|
+
return f"{self.desc}: {self.origin}"
|
|
17
|
+
return self.desc
|
|
18
|
+
|
|
19
|
+
def __cause__(self) -> Optional[Exception]:
|
|
20
|
+
return self.origin
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_retry_err(err: Exception) -> Tuple[Optional[Exception], bool]:
|
|
24
|
+
"""Check if error is a retry error."""
|
|
25
|
+
if isinstance(err, retryErr):
|
|
26
|
+
return err.origin, True
|
|
27
|
+
return None, False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class retryOptions:
|
|
31
|
+
"""Retry options."""
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
self.attempts = 3
|
|
35
|
+
self.duration = 1.0 # seconds
|
|
36
|
+
self.factor = 2.0
|
|
37
|
+
self.jitter = 0.1
|
|
38
|
+
self.enable_log = False
|
|
39
|
+
self.retry_if: Optional[Callable[[Exception], bool]] = None
|
|
40
|
+
self.on_retry: Optional[Callable[[int, Exception, float], None]] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def default_options() -> retryOptions:
|
|
44
|
+
return retryOptions()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def with_attempts(n: int) -> Callable[[retryOptions], None]:
|
|
48
|
+
return lambda o: setattr(o, 'attempts', n)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def with_logs(enable: bool) -> Callable[[retryOptions], None]:
|
|
52
|
+
return lambda o: setattr(o, 'enable_log', enable)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def with_retry_if(fn: Callable[[Exception], bool]) -> Callable[[retryOptions], None]:
|
|
56
|
+
return lambda o: setattr(o, 'retry_if', fn)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def with_on_retry(fn: Callable[[int, Exception, float], None]) -> Callable[[retryOptions], None]:
|
|
60
|
+
return lambda o: setattr(o, 'on_retry', fn)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def with_duration(d: float) -> Callable[[retryOptions], None]:
|
|
64
|
+
return lambda o: setattr(o, 'duration', d)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def with_factor(f: float) -> Callable[[retryOptions], None]:
|
|
68
|
+
return lambda o: setattr(o, 'factor', f)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def with_jitter(j: float) -> Callable[[retryOptions], None]:
|
|
72
|
+
return lambda o: setattr(o, 'jitter', j)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def jitter(duration: float, max_factor: float) -> float:
|
|
76
|
+
"""Add random jitter to duration."""
|
|
77
|
+
if max_factor <= 0:
|
|
78
|
+
return duration
|
|
79
|
+
return duration * (1 + random.random() * max_factor)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def next_sleep(o: retryOptions, step: int) -> float:
|
|
83
|
+
"""Calculate next sleep duration."""
|
|
84
|
+
d = float(o.duration)
|
|
85
|
+
|
|
86
|
+
if o.factor != 0:
|
|
87
|
+
d = d * math.pow(o.factor, float(step))
|
|
88
|
+
|
|
89
|
+
if o.jitter > 0:
|
|
90
|
+
d = jitter(d, float(o.jitter))
|
|
91
|
+
|
|
92
|
+
return d
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
Context = Optional[Dict[str, Any]]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def retry(
|
|
99
|
+
ctx: Context,
|
|
100
|
+
fn: Callable[[Context], Any],
|
|
101
|
+
*opts: Callable[[retryOptions], None]
|
|
102
|
+
) -> Any:
|
|
103
|
+
"""
|
|
104
|
+
Retry function with exponential backoff and jitter.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
ctx: Optional context dict (can be None)
|
|
108
|
+
fn: Function to execute, takes context as argument
|
|
109
|
+
*opts: Retry options
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Result of fn if successful
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
retryErr if all attempts fail
|
|
116
|
+
"""
|
|
117
|
+
cfg = default_options()
|
|
118
|
+
for opt in opts:
|
|
119
|
+
opt(cfg)
|
|
120
|
+
|
|
121
|
+
err: Optional[Exception] = None
|
|
122
|
+
for i in range(cfg.attempts):
|
|
123
|
+
# Execute the function
|
|
124
|
+
try:
|
|
125
|
+
result = fn(ctx)
|
|
126
|
+
return result
|
|
127
|
+
except Exception as e:
|
|
128
|
+
err = e
|
|
129
|
+
|
|
130
|
+
# Check if should retry based on retry_if
|
|
131
|
+
if cfg.retry_if is not None and not cfg.retry_if(e):
|
|
132
|
+
raise e
|
|
133
|
+
|
|
134
|
+
# Last attempt, break
|
|
135
|
+
if i == cfg.attempts - 1:
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
# Calculate sleep time
|
|
139
|
+
sleep = next_sleep(cfg, i)
|
|
140
|
+
|
|
141
|
+
# Call on_retry callback
|
|
142
|
+
if cfg.on_retry is not None:
|
|
143
|
+
cfg.on_retry(i + 1, e, sleep)
|
|
144
|
+
|
|
145
|
+
# Log if enabled
|
|
146
|
+
if cfg.enable_log:
|
|
147
|
+
from ... import log
|
|
148
|
+
log.warnf(
|
|
149
|
+
"retry %d/%d after %s, err: %s",
|
|
150
|
+
i + 1,
|
|
151
|
+
cfg.attempts,
|
|
152
|
+
sleep,
|
|
153
|
+
str(e),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Sleep
|
|
157
|
+
time.sleep(sleep)
|
|
158
|
+
|
|
159
|
+
raise retryErr(
|
|
160
|
+
f"after {cfg.attempts} attempts, last error: {err}",
|
|
161
|
+
err if err else None
|
|
162
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Services package
|
|
2
|
+
# All service modules matching Go SDK structure
|
|
3
|
+
|
|
4
|
+
from . import iam
|
|
5
|
+
from . import job
|
|
6
|
+
from . import bc
|
|
7
|
+
from . import mc
|
|
8
|
+
from . import oss
|
|
9
|
+
from . import registry
|
|
10
|
+
from . import modelset
|
|
11
|
+
from . import modelrepo
|
|
12
|
+
from . import fs
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"iam",
|
|
16
|
+
"job",
|
|
17
|
+
"bc",
|
|
18
|
+
"mc",
|
|
19
|
+
"oss",
|
|
20
|
+
"registry",
|
|
21
|
+
"modelset",
|
|
22
|
+
"modelrepo",
|
|
23
|
+
"fs",
|
|
24
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from yicloud.base import msgs
|
|
4
|
+
from yicloud.services.bc import models
|
|
5
|
+
from yicloud.services.bc.client import client
|
|
6
|
+
|
|
7
|
+
_PRODUCT_CODE = "bc"
|
|
8
|
+
_LAST_VERSION = "v1alpha1"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def list_billing_details(ctx: Optional[dict], req: models.ListBillingDetailsReq) -> models.ListBillingDetailsRes:
|
|
12
|
+
"""
|
|
13
|
+
GET /bc/v1alpha1/ListBillingDetails
|
|
14
|
+
Action: ListBillingDetails
|
|
15
|
+
|
|
16
|
+
Returns ListBillingDetailsRes on success.
|
|
17
|
+
|
|
18
|
+
Raises YiCloudException on error.
|
|
19
|
+
"""
|
|
20
|
+
path = f"/{_PRODUCT_CODE}/{_LAST_VERSION}/ListBillingDetails"
|
|
21
|
+
rsp = msgs.Rsp[models.ListBillingDetailsRes]()
|
|
22
|
+
client.base.get(ctx, path, req, rsp)
|
|
23
|
+
return rsp.get_typed_data()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from yicloud.base import Client
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BCClient:
|
|
5
|
+
"""BC (Billing Center) service client, matching Go SDK's services/bc/client.go."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, base_client: Client = None):
|
|
8
|
+
self.base = base_client
|
|
9
|
+
self.product_code = "bc"
|
|
10
|
+
self.last_version = "v1alpha1"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Singleton matching Go SDK
|
|
14
|
+
client: BCClient = BCClient()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def use_client(cli: Client) -> None:
|
|
18
|
+
"""Set the base client for BC service."""
|
|
19
|
+
client.base = cli
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Code generated from Go SDK's services/bc/models.go. DO NOT EDIT.
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ListBillingDetailsReq:
|
|
9
|
+
"""List billing details request."""
|
|
10
|
+
Tenant: str = ""
|
|
11
|
+
Project: str = ""
|
|
12
|
+
StartTime: str = ""
|
|
13
|
+
EndTime: str = ""
|
|
14
|
+
BillingCycle: str = ""
|
|
15
|
+
ProductName: str = ""
|
|
16
|
+
BillingMode: str = ""
|
|
17
|
+
InstanceId: str = ""
|
|
18
|
+
InstanceName: str = ""
|
|
19
|
+
ResourceCreator: str = ""
|
|
20
|
+
RelatedResourceId: str = ""
|
|
21
|
+
RelatedResourceName: str = ""
|
|
22
|
+
RelatedResourceType: str = ""
|
|
23
|
+
SortBy: str = ""
|
|
24
|
+
SortOrder: str = ""
|
|
25
|
+
Limit: int = 0
|
|
26
|
+
Offset: int = 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class InstanceUsageView:
|
|
31
|
+
"""Instance usage view."""
|
|
32
|
+
Tenant: str = ""
|
|
33
|
+
BillingCycle: str = ""
|
|
34
|
+
BillingTime: str = ""
|
|
35
|
+
ProductName: str = ""
|
|
36
|
+
BillingMode: str = ""
|
|
37
|
+
MetricCode: str = ""
|
|
38
|
+
MetricUnit: str = ""
|
|
39
|
+
UsageDurationHours: float = 0.0
|
|
40
|
+
UsageAmount: float = 0.0
|
|
41
|
+
InstanceCount: int = 0
|
|
42
|
+
UsageTimeRangeStart: str = ""
|
|
43
|
+
UsageTimeRangeEnd: str = ""
|
|
44
|
+
InstanceId: str = ""
|
|
45
|
+
InstanceName: str = ""
|
|
46
|
+
InstanceIdName: str = ""
|
|
47
|
+
RayJobName: str = ""
|
|
48
|
+
RayGroupName: str = ""
|
|
49
|
+
SpecDesc: str = ""
|
|
50
|
+
RelatedResourceName: str = ""
|
|
51
|
+
Project: str = ""
|
|
52
|
+
ResourceCreator: str = ""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class ListBillingDetailsRes:
|
|
57
|
+
"""List billing details response."""
|
|
58
|
+
Items: list[InstanceUsageView] = field(default_factory=list)
|
|
59
|
+
Total: int = 0
|
|
60
|
+
Limit: int = 0
|
|
61
|
+
Offset: int = 0
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from yicloud.services.fs.client import FSClient as Client, client, use_client
|
|
2
|
+
from . import actions, models
|
|
3
|
+
|
|
4
|
+
from .actions import (
|
|
5
|
+
list_dir,
|
|
6
|
+
get_file_info,
|
|
7
|
+
create_fileset_with_quota,
|
|
8
|
+
delete_fileset,
|
|
9
|
+
list_filesets_with_quota_info,
|
|
10
|
+
set_fileset_quota,
|
|
11
|
+
get_fileset,
|
|
12
|
+
)
|
|
13
|
+
from .models import (
|
|
14
|
+
FileInfo,
|
|
15
|
+
FilesetInfo,
|
|
16
|
+
PaginatedFileInfo,
|
|
17
|
+
FilesetWithQuotaInfo,
|
|
18
|
+
GetFSBucketResponse,
|
|
19
|
+
BucketResponse,
|
|
20
|
+
PaginatedFileSetWithQuotaInfo,
|
|
21
|
+
ListDirRequest,
|
|
22
|
+
GetFileInfoRequest,
|
|
23
|
+
Authorization,
|
|
24
|
+
CreateFilesetWithQuotaRequest,
|
|
25
|
+
DeleteFilesetRequest,
|
|
26
|
+
SetFilesetQuotaRequest,
|
|
27
|
+
GetFilesetRequest,
|
|
28
|
+
ListFilesetsWithQuotaInfoRequest,
|
|
29
|
+
)
|