dma-cli 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.
- dma_cli/__init__.py +3 -0
- dma_cli/__main__.py +6 -0
- dma_cli/calc.py +42 -0
- dma_cli/cli.py +148 -0
- dma_cli/client.py +245 -0
- dma_cli/resources.py +251 -0
- dma_cli/state.py +65 -0
- dma_cli/submit.py +673 -0
- dma_cli-0.1.0.data/data/share/dma-cli/batch.example.json +16 -0
- dma_cli-0.1.0.data/data/share/dma-cli/config.example.yaml +18 -0
- dma_cli-0.1.0.data/data/share/dma-cli/input/calc.py +42 -0
- dma_cli-0.1.0.data/data/share/dma-cli/input/case1/calc.py +42 -0
- dma_cli-0.1.0.data/data/share/dma-cli/input/case2/calc.py +42 -0
- dma_cli-0.1.0.data/data/share/dma-cli/job.example.json +13 -0
- dma_cli-0.1.0.dist-info/METADATA +269 -0
- dma_cli-0.1.0.dist-info/RECORD +19 -0
- dma_cli-0.1.0.dist-info/WHEEL +5 -0
- dma_cli-0.1.0.dist-info/entry_points.txt +2 -0
- dma_cli-0.1.0.dist-info/top_level.txt +1 -0
dma_cli/__init__.py
ADDED
dma_cli/__main__.py
ADDED
dma_cli/calc.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""内置默认任务脚本。
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
import argparse
|
|
5
|
+
import time
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
parser = argparse.ArgumentParser()
|
|
11
|
+
parser.add_argument("--num1", type=float, default=1)
|
|
12
|
+
parser.add_argument("--operator", choices=["+", "-"], default="+")
|
|
13
|
+
parser.add_argument("--num2", type=float, default=1)
|
|
14
|
+
args = parser.parse_args()
|
|
15
|
+
|
|
16
|
+
# 打开 run.log 文件并将 print 输出重定向追加至日志
|
|
17
|
+
with open("run.log", "a", encoding="utf-8") as f:
|
|
18
|
+
f.write(f"--- 任务启动时间: {time.strftime('%Y-%m-%d %H:%M:%S')} ---\n")
|
|
19
|
+
f.flush()
|
|
20
|
+
|
|
21
|
+
total_seconds = 10 * 60
|
|
22
|
+
interval = 5
|
|
23
|
+
elapsed = 0
|
|
24
|
+
while elapsed < total_seconds:
|
|
25
|
+
msg = "我大概运行10分钟,你可以先检查其他功能\n"
|
|
26
|
+
sys.stdout.write(msg)
|
|
27
|
+
sys.stdout.flush()
|
|
28
|
+
f.write(msg)
|
|
29
|
+
f.flush()
|
|
30
|
+
time.sleep(interval)
|
|
31
|
+
elapsed += interval
|
|
32
|
+
|
|
33
|
+
result = args.num1 + args.num2 if args.operator == "+" else args.num1 - args.num2
|
|
34
|
+
res_msg = f"计算结果: {args.num1} {args.operator} {args.num2} = {result}\n"
|
|
35
|
+
sys.stdout.write(res_msg)
|
|
36
|
+
sys.stdout.flush()
|
|
37
|
+
f.write(res_msg)
|
|
38
|
+
f.flush()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
main()
|
dma_cli/cli.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""DMA CLI —— 任务提交命令行工具入口。
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
dma --help
|
|
6
|
+
dma login
|
|
7
|
+
dma whoami
|
|
8
|
+
dma project list
|
|
9
|
+
dma project use 3948
|
|
10
|
+
dma sku list --kind cpu
|
|
11
|
+
dma image list
|
|
12
|
+
dma job submit --machine_type c2_m2_cpu --image_address <url> --command "python calc.py"
|
|
13
|
+
dma job submit -i job.example.json
|
|
14
|
+
dma job submit --batch_file batch.json
|
|
15
|
+
"""
|
|
16
|
+
import argparse
|
|
17
|
+
import os
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from .client import DmaClient
|
|
21
|
+
from .resources import (
|
|
22
|
+
handle_project_list, handle_project_use, handle_project_current,
|
|
23
|
+
handle_sku_list,
|
|
24
|
+
handle_image_list,
|
|
25
|
+
handle_jobs, handle_job_rm,
|
|
26
|
+
)
|
|
27
|
+
from .submit import handle_submit
|
|
28
|
+
|
|
29
|
+
# 不强制鉴权的命令
|
|
30
|
+
_NO_AUTH = {"login", "whoami"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def cmd_login(client, args):
|
|
34
|
+
creds = client.login(args.email)
|
|
35
|
+
print(f"✅ 登录成功: email={creds.get('email')} user_id={creds.get('user_id')} uexid={creds.get('uexid')}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _fmt_project(p):
|
|
39
|
+
if not p:
|
|
40
|
+
return "未设置"
|
|
41
|
+
return f"{p['projectId']} | {p['name']}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def cmd_whoami(client, args):
|
|
45
|
+
u = client.current_user()
|
|
46
|
+
print(f"base_url: {client.base_url}")
|
|
47
|
+
print(f"email: {u['email']}")
|
|
48
|
+
print(f"user_id: {u['user_id']}")
|
|
49
|
+
print(f"uexid: {u['uexid']}")
|
|
50
|
+
print(f"token: {'已配置' if client.is_authed() else '未配置'} (来源: {u['token_source']})")
|
|
51
|
+
print(f"当前项目: {_fmt_project(client.current_project())}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
55
|
+
p = argparse.ArgumentParser(prog="dma", description="DMA 任务提交命令行工具")
|
|
56
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
57
|
+
|
|
58
|
+
# login
|
|
59
|
+
sp = sub.add_parser("login", help="验证码登录,缓存 token/userId/uexid")
|
|
60
|
+
sp.add_argument("--email", default=None, help="登录邮箱(默认用 config.yaml 的 auth.email)")
|
|
61
|
+
sp.set_defaults(func=cmd_login)
|
|
62
|
+
|
|
63
|
+
# whoami
|
|
64
|
+
sp = sub.add_parser("whoami", help="显示当前用户与当前项目")
|
|
65
|
+
sp.set_defaults(func=cmd_whoami)
|
|
66
|
+
|
|
67
|
+
# project
|
|
68
|
+
sp = sub.add_parser("project", help="项目管理")
|
|
69
|
+
ps = sp.add_subparsers(dest="subcmd", required=True)
|
|
70
|
+
ps.add_parser("list", help="列项目").set_defaults(func=handle_project_list)
|
|
71
|
+
sp_use = ps.add_parser("use", help="设为当前项目")
|
|
72
|
+
sp_use.add_argument("project_id", type=int)
|
|
73
|
+
sp_use.set_defaults(func=handle_project_use)
|
|
74
|
+
ps.add_parser("current", help="显示当前项目").set_defaults(func=handle_project_current)
|
|
75
|
+
|
|
76
|
+
# sku (仅查询,不持久化)
|
|
77
|
+
sp = sub.add_parser("sku", help="机型查询(参考)")
|
|
78
|
+
ps = sp.add_subparsers(dest="subcmd", required=True)
|
|
79
|
+
sp_list = ps.add_parser("list", help="列机型")
|
|
80
|
+
sp_list.add_argument("--kind", choices=["cpu", "gpu"], default="cpu", help="机型类型(默认 cpu)")
|
|
81
|
+
sp_list.set_defaults(func=handle_sku_list)
|
|
82
|
+
|
|
83
|
+
# image (软件目录,仅查询,不持久化)
|
|
84
|
+
sp = sub.add_parser("image", help="镜像(软件目录)查询")
|
|
85
|
+
ps = sp.add_subparsers(dest="subcmd", required=True)
|
|
86
|
+
ps.add_parser("list", help="列软件及其版本").set_defaults(func=handle_image_list)
|
|
87
|
+
|
|
88
|
+
# job (任务操作)
|
|
89
|
+
sp = sub.add_parser("job", help="任务操作")
|
|
90
|
+
ps = sp.add_subparsers(dest="subcmd", required=True)
|
|
91
|
+
|
|
92
|
+
# job submit
|
|
93
|
+
sp_submit = ps.add_parser("submit", help="提交任务(单个或批量)")
|
|
94
|
+
sp_submit.add_argument("-i", "--config_file", default=None, help="配置 JSON 文件路径")
|
|
95
|
+
sp_submit.add_argument("--input_directory", default="./", help="输入文件目录(目录下所有文件上传)")
|
|
96
|
+
sp_submit.add_argument("-g", "--job_group_id", type=int, default=0, help="Job Group ID(0=自动创建)")
|
|
97
|
+
sp_submit.add_argument("--project_id", type=int, default=0, help="项目 ID(0=用当前选择)")
|
|
98
|
+
sp_submit.add_argument("-n", "--job_name", default=None, help="任务名称")
|
|
99
|
+
sp_submit.add_argument("--name_prefix", default=None, help="批量任务名前缀")
|
|
100
|
+
sp_submit.add_argument("-m", "--image_address", default=None, help="镜像地址(自动查询匹配)")
|
|
101
|
+
sp_submit.add_argument("-t", "--machine_type", default=None, help="机器类型名(自动查询匹配)")
|
|
102
|
+
sp_submit.add_argument("-c", "--command", default=None, help="执行命令(未指定时取软件默认)")
|
|
103
|
+
sp_submit.add_argument("-r", "--result_path", default=None, help="结果路径")
|
|
104
|
+
sp_submit.add_argument("-l", "--log_file", default=None, help="日志文件名")
|
|
105
|
+
sp_submit.add_argument("--max_run_time", type=int, default=0, help="最大运行时间(分钟,0=用默认)")
|
|
106
|
+
sp_submit.add_argument("--max_reschedule_times", type=int, default=0, help="最大重调度次数")
|
|
107
|
+
sp_submit.add_argument("--nnode", type=int, default=1, help="节点数")
|
|
108
|
+
sp_submit.add_argument("--out_files", nargs="+", default=None, help="输出文件列表")
|
|
109
|
+
sp_submit.add_argument("--dataset_path", nargs="+", default=None, help="数据集路径列表")
|
|
110
|
+
sp_submit.add_argument("--count", type=int, default=1, help="批量提交任务数")
|
|
111
|
+
sp_submit.add_argument("--concurrency", type=int, default=5, help="批量并发数")
|
|
112
|
+
sp_submit.add_argument("--batch_file", default=None, help="批量任务 JSON 数组文件路径")
|
|
113
|
+
sp_submit.add_argument("--batch_input_dir", default=None, help="批量目录扫描路径")
|
|
114
|
+
sp_submit.add_argument("--no_check", action="store_true", help="跳过 check_machine_type 预检")
|
|
115
|
+
sp_submit.add_argument("--dry_run", action="store_true", help="只打印 snapshot body 不提交")
|
|
116
|
+
sp_submit.add_argument("--poll", action="store_true", help="提交后轮询状态")
|
|
117
|
+
sp_submit.set_defaults(func=handle_submit)
|
|
118
|
+
|
|
119
|
+
# jobs (隐藏命令)
|
|
120
|
+
sp_jobs = ps.add_parser("jobs", help=argparse.SUPPRESS)
|
|
121
|
+
sp_jobs.add_argument("--status", type=int, default=-1, help="-1=全部 0=待运行 1=运行中 2=成功 3=失败 4=取消 5=排队 6=超时")
|
|
122
|
+
sp_jobs.add_argument("--project_id", type=int, default=None, help="按项目过滤")
|
|
123
|
+
sp_jobs.set_defaults(func=handle_jobs)
|
|
124
|
+
|
|
125
|
+
# job rm (隐藏命令)
|
|
126
|
+
sp_rm = ps.add_parser("rm", help=argparse.SUPPRESS)
|
|
127
|
+
sp_rm.add_argument("job_id", help="任务 ID")
|
|
128
|
+
sp_rm.set_defaults(func=handle_job_rm)
|
|
129
|
+
|
|
130
|
+
return p
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main():
|
|
134
|
+
parser = build_parser()
|
|
135
|
+
args = parser.parse_args()
|
|
136
|
+
client = DmaClient()
|
|
137
|
+
if args.cmd not in _NO_AUTH and not client.is_authed():
|
|
138
|
+
print("❌ 未登录/未配 token,请先 `dma login` 或在 config.yaml / 环境变量 DMA_TOKEN 填写")
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
try:
|
|
141
|
+
args.func(client, args)
|
|
142
|
+
except RuntimeError as e:
|
|
143
|
+
print(f"❌ {e}")
|
|
144
|
+
sys.exit(1)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
main()
|
dma_cli/client.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""DmaClient: 配置/状态加载、鉴权头、登录、GET/POST、重试。
|
|
2
|
+
|
|
3
|
+
接口形态移植自压测脚本(prepare_users.py 登录、cleanup_jobs.py 鉴权头与 job/list),
|
|
4
|
+
改为 requests 同步版。仅引用接口形态,不修改任何压测文件。
|
|
5
|
+
"""
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
import urllib3
|
|
15
|
+
import yaml
|
|
16
|
+
|
|
17
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
18
|
+
|
|
19
|
+
from .state import load_session, load_creds, save_creds, creds_path
|
|
20
|
+
|
|
21
|
+
# ---- 配置文件定位 ----
|
|
22
|
+
_PKG_DIR = Path(__file__).parent
|
|
23
|
+
# 配置文件查找顺序:当前目录 -> 包目录
|
|
24
|
+
CONFIG_FILE = Path.cwd() / "config.yaml" if (Path.cwd() / "config.yaml").exists() else _PKG_DIR / "config.yaml"
|
|
25
|
+
EXAMPLE_CONFIG = _PKG_DIR / "config.example.yaml"
|
|
26
|
+
|
|
27
|
+
DEFAULT_CONFIG = {
|
|
28
|
+
"base_url": "https://szc.test.dp.tech",
|
|
29
|
+
"auth": {"email": "", "captcha": "952754", "token": "", "uexid": "", "user_id": ""},
|
|
30
|
+
"defaults": {
|
|
31
|
+
"biz_platform": 2,
|
|
32
|
+
"name_prefix": "cli-job",
|
|
33
|
+
"cmd": "python calc.py",
|
|
34
|
+
"max_run_time": 15,
|
|
35
|
+
"max_run_time_unit": "d",
|
|
36
|
+
"concurrency": 5,
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# HAR 实测鉴权头(登录来源标注)
|
|
41
|
+
X_BOHRIUM_PARAMS = "eyJsb2dpblNvdXJjZSI6ImVtYWlsX2NvZGUiLCJ1dG1Tb3VyY2UiOiIifQ=="
|
|
42
|
+
LOGIN_PATH = "/platform-gateway/api/account/login/email_code"
|
|
43
|
+
|
|
44
|
+
# 任务状态映射(移植自 cleanup_jobs.py)
|
|
45
|
+
STATUS_MAP = {
|
|
46
|
+
-1: "异常/未知",
|
|
47
|
+
0: "待运行",
|
|
48
|
+
1: "运行中",
|
|
49
|
+
2: "运行成功",
|
|
50
|
+
3: "运行失败",
|
|
51
|
+
4: "已取消",
|
|
52
|
+
5: "排队中",
|
|
53
|
+
6: "超时",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _load_config() -> dict:
|
|
58
|
+
"""加载 config.yaml(缺失则用 config.example.yaml),与内置默认浅合并。"""
|
|
59
|
+
cfg = {k: (v.copy() if isinstance(v, dict) else v) for k, v in DEFAULT_CONFIG.items()}
|
|
60
|
+
src = CONFIG_FILE if CONFIG_FILE.exists() else EXAMPLE_CONFIG
|
|
61
|
+
try:
|
|
62
|
+
with open(src, "r", encoding="utf-8") as f:
|
|
63
|
+
user_cfg = yaml.safe_load(f) or {}
|
|
64
|
+
except FileNotFoundError:
|
|
65
|
+
user_cfg = {}
|
|
66
|
+
for k in cfg:
|
|
67
|
+
if k not in user_cfg:
|
|
68
|
+
continue
|
|
69
|
+
if isinstance(cfg[k], dict) and isinstance(user_cfg[k], dict):
|
|
70
|
+
cfg[k].update(user_cfg[k])
|
|
71
|
+
else:
|
|
72
|
+
cfg[k] = user_cfg[k]
|
|
73
|
+
return cfg
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DmaClient:
|
|
77
|
+
def __init__(self, http_session=None):
|
|
78
|
+
# requests.Session 非线程安全;批量并发时每线程传入独立 Session
|
|
79
|
+
self._http = http_session or requests.Session()
|
|
80
|
+
self.config = _load_config()
|
|
81
|
+
# 环境变量覆盖(DMA_BASE_URL / DMA_TOKEN)
|
|
82
|
+
env_base = os.environ.get("DMA_BASE_URL")
|
|
83
|
+
if env_base:
|
|
84
|
+
self.config["base_url"] = env_base
|
|
85
|
+
env_token = os.environ.get("DMA_TOKEN")
|
|
86
|
+
if env_token:
|
|
87
|
+
self.config["auth"]["token"] = env_token
|
|
88
|
+
self.session = load_session()
|
|
89
|
+
self.creds = load_creds()
|
|
90
|
+
|
|
91
|
+
# ---- 基本属性 ----
|
|
92
|
+
@property
|
|
93
|
+
def base_url(self) -> str:
|
|
94
|
+
return str(self.config["base_url"]).rstrip("/")
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def defaults(self) -> dict:
|
|
98
|
+
return self.config.get("defaults", {})
|
|
99
|
+
|
|
100
|
+
def reload_session(self) -> dict:
|
|
101
|
+
self.session = load_session()
|
|
102
|
+
return self.session
|
|
103
|
+
|
|
104
|
+
# ---- 凭据 ----
|
|
105
|
+
def _token(self) -> str:
|
|
106
|
+
return self.config["auth"].get("token") or self.creds.get("token") or ""
|
|
107
|
+
|
|
108
|
+
def _uexid(self) -> str:
|
|
109
|
+
u = self.config["auth"].get("uexid") or self.creds.get("uexid")
|
|
110
|
+
if u:
|
|
111
|
+
return u
|
|
112
|
+
# 留空则从 JWT 解析 fe_<userId>
|
|
113
|
+
token = self._token()
|
|
114
|
+
if token:
|
|
115
|
+
_, uexid = self._parse_jwt(token)
|
|
116
|
+
return uexid
|
|
117
|
+
return ""
|
|
118
|
+
|
|
119
|
+
def _user_id(self) -> str:
|
|
120
|
+
u = self.config["auth"].get("user_id") or self.creds.get("user_id")
|
|
121
|
+
if u:
|
|
122
|
+
return u
|
|
123
|
+
token = self._token()
|
|
124
|
+
if token:
|
|
125
|
+
uid, _ = self._parse_jwt(token)
|
|
126
|
+
return uid
|
|
127
|
+
return ""
|
|
128
|
+
|
|
129
|
+
def is_authed(self) -> bool:
|
|
130
|
+
return bool(self._token() and self._uexid())
|
|
131
|
+
|
|
132
|
+
def current_user(self) -> dict:
|
|
133
|
+
return {
|
|
134
|
+
"email": self.creds.get("email") or self.config["auth"].get("email") or "",
|
|
135
|
+
"user_id": self._user_id(),
|
|
136
|
+
"uexid": self._uexid(),
|
|
137
|
+
"token_source": "config" if self.config["auth"].get("token") else ("cache" if self.creds.get("token") else "none"),
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
def auth_headers(self) -> dict:
|
|
141
|
+
"""通用鉴权头(HAR 实测)。JSON body 时由调用方补 Content-Type。"""
|
|
142
|
+
return {
|
|
143
|
+
"Authorization": f"Bearer {self._token()}",
|
|
144
|
+
"Uexid": self._uexid(),
|
|
145
|
+
"Accept": "application/json, text/plain, */*",
|
|
146
|
+
"Content-Language": "zh-cn",
|
|
147
|
+
"X-Bohrium-Params": X_BOHRIUM_PARAMS,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# ---- 登录 ----
|
|
151
|
+
def login(self, email: str = None) -> dict:
|
|
152
|
+
"""验证码登录,解析 JWT 缓存 token/user_id/uexid。"""
|
|
153
|
+
email = email or self.config["auth"].get("email")
|
|
154
|
+
if not email:
|
|
155
|
+
raise RuntimeError("未配置 auth.email,无法验证码登录(请在 config.yaml 填写或改用 token)")
|
|
156
|
+
captcha = self.config["auth"].get("captcha") or "952754"
|
|
157
|
+
url = self.base_url + LOGIN_PATH
|
|
158
|
+
payload = {
|
|
159
|
+
"channel": "gkx-talent-universe",
|
|
160
|
+
"businessLine": "bohrium",
|
|
161
|
+
"email": email,
|
|
162
|
+
"emailCode": captcha,
|
|
163
|
+
"code": captcha,
|
|
164
|
+
}
|
|
165
|
+
resp = self._http.post(url, json=payload, timeout=15, verify=False)
|
|
166
|
+
data = resp.json()
|
|
167
|
+
if data.get("code") != 0:
|
|
168
|
+
raise RuntimeError(
|
|
169
|
+
f"登录失败: code={data.get('code')} msg={data.get('message') or data.get('msg')}"
|
|
170
|
+
)
|
|
171
|
+
token = (data.get("data") or {}).get("token") or (data.get("data") or {}).get("accessToken")
|
|
172
|
+
if not token:
|
|
173
|
+
raise RuntimeError("登录返回无 token")
|
|
174
|
+
user_id, uexid = self._parse_jwt(token)
|
|
175
|
+
self.creds = {"token": token, "user_id": user_id, "uexid": uexid, "email": email}
|
|
176
|
+
save_creds(self.creds)
|
|
177
|
+
return self.creds
|
|
178
|
+
|
|
179
|
+
@staticmethod
|
|
180
|
+
def _parse_jwt(token: str):
|
|
181
|
+
"""从 JWT payload 解析 userId,推导 uexid = fe_<userId>(不校验签名)。"""
|
|
182
|
+
try:
|
|
183
|
+
payload_b64 = token.split(".")[1]
|
|
184
|
+
payload_b64 += "=" * (-len(payload_b64) % 4)
|
|
185
|
+
payload = json.loads(base64.b64decode(payload_b64))
|
|
186
|
+
uid = str((payload.get("identity") or {}).get("userId", ""))
|
|
187
|
+
uexid = f"fe_{uid}" if uid else "fe_0"
|
|
188
|
+
return uid, uexid
|
|
189
|
+
except Exception:
|
|
190
|
+
return "", "fe_0"
|
|
191
|
+
|
|
192
|
+
# ---- 通用请求(带重试)----
|
|
193
|
+
def _request(self, method, path, *, params=None, json_body=None, data=None,
|
|
194
|
+
headers=None, timeout=30, retries=3):
|
|
195
|
+
url = self.base_url + path if path.startswith("/") else path
|
|
196
|
+
h = self.auth_headers()
|
|
197
|
+
if headers:
|
|
198
|
+
h.update(headers)
|
|
199
|
+
if json_body is not None and "Content-Type" not in h:
|
|
200
|
+
h["Content-Type"] = "application/json;charset=UTF-8"
|
|
201
|
+
last_err = None
|
|
202
|
+
for attempt in range(retries):
|
|
203
|
+
try:
|
|
204
|
+
r = self._http.request(
|
|
205
|
+
method, url, headers=h, params=params, json=json_body,
|
|
206
|
+
data=data, timeout=timeout, verify=False,
|
|
207
|
+
)
|
|
208
|
+
try:
|
|
209
|
+
return r.json()
|
|
210
|
+
except ValueError:
|
|
211
|
+
return {"code": -1, "msg": f"非 JSON 响应: HTTP {r.status_code}", "_raw": r.text[:200]}
|
|
212
|
+
except (requests.ConnectionError, requests.Timeout) as e:
|
|
213
|
+
last_err = e
|
|
214
|
+
if attempt < retries - 1:
|
|
215
|
+
time.sleep(1.0 * (attempt + 1))
|
|
216
|
+
raise RuntimeError(f"请求失败({retries} 次重试后): {last_err}")
|
|
217
|
+
|
|
218
|
+
def get(self, path, params=None, timeout=30):
|
|
219
|
+
return self._request("GET", path, params=params, timeout=timeout)
|
|
220
|
+
|
|
221
|
+
def post(self, path, data=None, json_body=None, headers=None, timeout=30):
|
|
222
|
+
"""通用 POST 方法,支持 data 或 json_body。"""
|
|
223
|
+
return self._request("POST", path, data=data, json_body=json_body, headers=headers, timeout=timeout)
|
|
224
|
+
|
|
225
|
+
def post_json(self, path, body=None, headers=None, timeout=30):
|
|
226
|
+
# body 为 None 时发空 JSON 对象 {}
|
|
227
|
+
return self._request("POST", path, json_body=(body if body is not None else {}), headers=headers, timeout=timeout)
|
|
228
|
+
|
|
229
|
+
def post_raw(self, path, data, headers=None, timeout=30):
|
|
230
|
+
"""发送原始 body(如 upload 步骤的文件原文),调用方需在 headers 指定 Content-Type。"""
|
|
231
|
+
return self._request("POST", path, data=data, headers=headers, timeout=timeout)
|
|
232
|
+
|
|
233
|
+
def post_empty(self, path, headers=None, timeout=30):
|
|
234
|
+
"""发送空 body(如 commit / del)。"""
|
|
235
|
+
return self._request("POST", path, headers=headers, timeout=timeout)
|
|
236
|
+
|
|
237
|
+
# ---- 当前选择快捷访问 ----
|
|
238
|
+
def current_project(self) -> Optional[dict]:
|
|
239
|
+
return self.session.get("project")
|
|
240
|
+
|
|
241
|
+
def current_sku(self) -> Optional[dict]:
|
|
242
|
+
return self.session.get("sku")
|
|
243
|
+
|
|
244
|
+
def current_software(self) -> Optional[dict]:
|
|
245
|
+
return self.session.get("software")
|