dsh-scnet 0.6.1 → 0.6.2
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.
- package/README.md +10 -3
- package/README_CN.md +8 -2
- package/docs/testing.zh-CN.md +13 -13
- package/index.mjs +4 -1
- package/package.json +1 -1
- package/skills/scnet-hpc/VERSION +1 -1
- package/skills/scnet-hpc/references/openapi.md +7 -0
- package/skills/scnet-hpc/scripts/scnet_backends/openapi.py +81 -50
- package/skills/scnet-hpc/scripts/scnet_sdk/client.py +32 -18
package/README.md
CHANGED
|
@@ -4,11 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
DSH-SCNet (`dsh-scnet` on npm) is a community-maintained DSH bundle for operating Supercomputing Network (SCNet). It packages the source [`scnet-hpc`](https://github.com/lql341/scnet-hpc) Skill, profile-aware utilities, and bounded tools for SSH, Slurm, OpenAPI files, regional resources, and Notebook discovery.
|
|
6
6
|
|
|
7
|
-
Current release: **0.6.
|
|
7
|
+
Current release: **0.6.2**
|
|
8
8
|
|
|
9
9
|
> This is an independent community project. It is compatible with DeepSeek Harness but is not an official DeepSeek product and does not imply endorsement, partnership, or authorization by DeepSeek.
|
|
10
10
|
|
|
11
|
-
## What's new in 0.6.
|
|
11
|
+
## What's new in 0.6.2
|
|
12
|
+
|
|
13
|
+
- Completed jobs now resolve through the fast filtered history-list endpoint rather than timing
|
|
14
|
+
out on the empty history-detail response.
|
|
15
|
+
- Historical field aliases such as `workdir` are normalized for log-path discovery.
|
|
16
|
+
- OpenAPI operations continue without persistent caching when the cache directory is read-only.
|
|
17
|
+
|
|
18
|
+
## 0.6.1 highlights
|
|
12
19
|
|
|
13
20
|
- Job details now fall back to the history endpoint when realtime records expire, with complete
|
|
14
21
|
normalized terminal states.
|
|
@@ -113,7 +120,7 @@ synchronizes the skill and version into `dsh-scnet` and `codex-scnet-hpc`. Publi
|
|
|
113
120
|
`dsh-scnet` npm package only after that downstream change is reviewed.
|
|
114
121
|
|
|
115
122
|
For a synchronized release, run the package validation on `main`, then trigger the repository's
|
|
116
|
-
`Publish DSH-SCNet` workflow with the exact value from `package.json` (currently `0.6.
|
|
123
|
+
`Publish DSH-SCNet` workflow with the exact value from `package.json` (currently `0.6.2`). The
|
|
117
124
|
workflow verifies the package/Skill version match, runs validation, creates the tarball, publishes
|
|
118
125
|
stable releases with the `latest` tag, and creates the matching Git tag. Do not bump only
|
|
119
126
|
`package.json`: `skills/scnet-hpc/VERSION`, the package version, and both README release markers
|
package/README_CN.md
CHANGED
|
@@ -4,10 +4,16 @@
|
|
|
4
4
|
|
|
5
5
|
<p align="center">在 DeepSeek Harness 里操作超算互联网(SCNet):配置 SSH、管理 Slurm 作业、查询 OpenAPI 区域和文件、检查 Notebook 资源与实例,并保留海光 DCU/DTK 诊断能力。</p>
|
|
6
6
|
|
|
7
|
-
当前版本:**0.6.
|
|
7
|
+
当前版本:**0.6.2**
|
|
8
8
|
|
|
9
9
|
> 本项目是独立维护的社区项目,与 DeepSeek Harness 兼容,但不是 DeepSeek 官方产品,也不表示 DeepSeek 对本项目提供背书、合作或授权。
|
|
10
10
|
|
|
11
|
+
## 0.6.2 更新
|
|
12
|
+
|
|
13
|
+
- 已结束作业改用快速的历史列表过滤查询,不再卡在返回空数据的历史详情接口。
|
|
14
|
+
- 兼容历史记录中的 `workdir` 等字段变体,可继续推导日志路径。
|
|
15
|
+
- 缓存目录只读时自动降级为无持久缓存,OpenAPI 主功能不再被缓存故障阻断。
|
|
16
|
+
|
|
11
17
|
## 0.6.1 更新
|
|
12
18
|
|
|
13
19
|
- 作业实时记录过期时自动回退历史接口,并补全终态映射。
|
|
@@ -126,7 +132,7 @@ git 源、本地目录安装以及验证步骤见[安装说明](./docs/installat
|
|
|
126
132
|
才能发布对应版本的 `dsh-scnet` npm 包。
|
|
127
133
|
|
|
128
134
|
下游同步合并到 `main` 后,先运行本地校验,再手动触发仓库的 `Publish DSH-SCNet` workflow,
|
|
129
|
-
输入 `package.json` 中的精确版本(当前为 `0.6.
|
|
135
|
+
输入 `package.json` 中的精确版本(当前为 `0.6.2`)。workflow 会校验 package 与 Skill
|
|
130
136
|
版本一致,执行验证、生成 tarball、以稳定版 `latest` 标签发布,并创建对应 Git tag。
|
|
131
137
|
不要只修改 `package.json`;`skills/scnet-hpc/VERSION`、package 版本和两份 README 的版本
|
|
132
138
|
标记必须保持一致。
|
package/docs/testing.zh-CN.md
CHANGED
|
@@ -61,22 +61,22 @@ DEEPSEEK_API_KEY: sk-你的key
|
|
|
61
61
|
| 8 | 用 scnet_status 查看当前配置 | 返回脱敏配置 | 只读 |
|
|
62
62
|
| 9 | 用 scnet_openapi_regions 查看授权区域 | 返回区域且不含 token | 只读 |
|
|
63
63
|
| 10 | 用 scnet_job_queues 查看昆山队列 | 返回实时队列和空闲资源 | 只读 |
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
64
|
+
| 11 | 用 scnet_job_list 列出当前或历史作业 | 返回紧凑作业记录 | 只读 |
|
|
65
|
+
| 12 | 用 scnet_limits 查询资源限制 | 返回用户和调度器限制 | 只读 |
|
|
66
|
+
| 13 | 用 scnet_submit_job(dry_run=true)预览一次提交 | 返回将要发送的请求,不真正提交 | 只读(dry_run) |
|
|
67
|
+
| 14 | 用 scnet_submit_job 提交一个 5 秒小作业 | 返回 job_id、作业目录与日志路径 | 高(会消耗配额) |
|
|
68
|
+
| 15 | 用 scnet_job_show 查该 job_id | 返回状态与 stdout/stderr 路径 | 只读 |
|
|
69
|
+
| 16 | 用 scnet_job_logs 读该作业日志 | 返回日志内容(同时验证 path 与 job_id+work_dir 两种模式结果一致) | 只读 |
|
|
70
|
+
| 17 | 用 scnet_job_cancel 取消一个作业 | 建议先用 dry_run=true 预览 | 高(变更) |
|
|
71
|
+
| 18 | 用 scnet_file_list 查看默认目录 | 返回共享存储文件 | 只读 |
|
|
72
|
+
| 19 | 用 scnet_file_transfer 传输一个文件 | 默认不覆盖目标 | 有副作用 |
|
|
73
|
+
| 20 | 用 scnet_notebook_regions 查看区域 | 返回 Notebook 可用区域 | 只读 |
|
|
74
|
+
| 21 | 用 scnet_notebook_resources 查看昆山资源 | 返回 CPU/GPU/DCU 资源 | 只读 |
|
|
75
|
+
| 22 | 用 scnet_notebook_list 查看实例 | 返回脱敏实例列表 | 只读 |
|
|
76
76
|
|
|
77
77
|
第 4 至 7 步只有在你有私钥和目标集群时才测,测前确认副作用。
|
|
78
78
|
|
|
79
|
-
第
|
|
79
|
+
第 14、17 步是变更操作,会真实占用或释放调度器资源:先用 `dry_run=true` 预览,确认返回里的
|
|
80
80
|
目标区域/集群,并且**不要在提交超时后盲目重试**(平台没有幂等键),先查作业状态。
|
|
81
81
|
|
|
82
82
|
Notebook 创建、启动、停止和释放没有作为 DSH deterministic tool 暴露。此类操作通过
|
package/index.mjs
CHANGED
|
@@ -197,6 +197,9 @@ function translateScnetError(text) {
|
|
|
197
197
|
'若只想看日志,请直接给日志文件的 path;若刚提交不久,可稍后重试。'
|
|
198
198
|
)
|
|
199
199
|
}
|
|
200
|
+
if (/realtime and history endpoints returned no job record/i.test(value)) {
|
|
201
|
+
return '平台的实时和最近 30 天历史记录中都没有找到这个作业,请核对 job_id 和区域。'
|
|
202
|
+
}
|
|
200
203
|
return value
|
|
201
204
|
}
|
|
202
205
|
|
|
@@ -577,7 +580,7 @@ export function apply(ctx) {
|
|
|
577
580
|
defineTool({
|
|
578
581
|
name: 'scnet_job_show',
|
|
579
582
|
description:
|
|
580
|
-
'只读查询一个作业的状态与资源(state / exit_code / work_dir / stdout / stderr
|
|
583
|
+
'只读查询一个作业的状态与资源(state / exit_code / work_dir / stdout / stderr)。自动先查实时记录,再回退最近 30 天历史记录;用户只需提供 job_id。',
|
|
581
584
|
parameters: {
|
|
582
585
|
job_id: { type: 'string', required: true, description: '作业号' },
|
|
583
586
|
backend: { type: 'string', description: 'openapi(默认)或 ssh' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-scnet",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Community DeepSeek Harness bundle for SCNet SSH and OpenAPI jobs, files, Notebook resources, Slurm diagnostics, and Hygon DCU/DTK guidance.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
package/skills/scnet-hpc/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.6.
|
|
1
|
+
0.6.2
|
|
@@ -125,6 +125,13 @@ python3 scripts/scnet.py --backend openapi --region <id> \
|
|
|
125
125
|
jobs --scope history --days 30 --limit 20
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
+
`job <job-id>` checks the realtime endpoint first and automatically falls back to the filtered
|
|
129
|
+
history-list endpoint. The caller does not need to provide `acctTime`.
|
|
130
|
+
|
|
131
|
+
Region tokens are cached under `${XDG_CACHE_HOME:-~/.cache}/scnet-hpc` when that location is
|
|
132
|
+
writable. Cache locking or writes are best-effort; an unavailable cache never blocks an OpenAPI
|
|
133
|
+
operation.
|
|
134
|
+
|
|
128
135
|
Uploads larger than 8 MiB automatically use the documented burst/merge flow. Override the
|
|
129
136
|
threshold and chunk size with `--chunk-size <bytes>`; use `--chunk-size 0` to force ordinary
|
|
130
137
|
multipart upload. The current implementation keeps one chunk in memory at a time.
|
|
@@ -38,6 +38,7 @@ STATUS_MAP = {
|
|
|
38
38
|
"statRQ": "REQUEUED",
|
|
39
39
|
}
|
|
40
40
|
WALLTIME_RE = re.compile(r"^(?:[0-9]+-)?[0-9]{1,3}:[0-9]{2}:[0-9]{2}$")
|
|
41
|
+
SCNET_TIMEZONE = timezone(timedelta(hours=8))
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
def _bounded_int(options: Mapping[str, Any], name: str, minimum: int) -> int:
|
|
@@ -62,19 +63,23 @@ def normalize_job(item: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
62
63
|
"name": item.get("jobName") or item.get("name"),
|
|
63
64
|
"state": STATUS_MAP.get(str(raw_state), raw_state),
|
|
64
65
|
"raw_state": raw_state,
|
|
65
|
-
"queue": item.get("queue"),
|
|
66
|
-
"user": item.get("user"),
|
|
67
|
-
"nodes": item.get("nodeNumReq") or item.get("nodeUsed"),
|
|
68
|
-
"cpus": item.get("procNumReq") or item.get("procNumUsed"),
|
|
66
|
+
"queue": item.get("queue") or item.get("queueName"),
|
|
67
|
+
"user": item.get("user") or item.get("userName"),
|
|
68
|
+
"nodes": item.get("nodeNumReq") or item.get("nodeUsed") or item.get("nodeNum"),
|
|
69
|
+
"cpus": item.get("procNumReq") or item.get("procNumUsed") or item.get("nproc"),
|
|
69
70
|
"gpus": item.get("gpuNumReq") or item.get("gpuNumUsed"),
|
|
70
71
|
"dcus": item.get("dcuNumReq") or item.get("dcuNumUsed"),
|
|
71
|
-
"elapsed": item.get("jobRunTime"),
|
|
72
|
-
"submitted_at":
|
|
72
|
+
"elapsed": item.get("jobRunTime") or item.get("jobWalltimeUsed"),
|
|
73
|
+
"submitted_at": (
|
|
74
|
+
item.get("jobSubmitTime")
|
|
75
|
+
or item.get("jobQueueTime")
|
|
76
|
+
or item.get("queueTime")
|
|
77
|
+
),
|
|
73
78
|
"started_at": item.get("jobStartTime"),
|
|
74
|
-
"ended_at": item.get("jobEndTime"),
|
|
79
|
+
"ended_at": item.get("jobEndTime") or item.get("acctTime"),
|
|
75
80
|
"exit_code": item.get("exitCode"),
|
|
76
81
|
"reason": item.get("reason"),
|
|
77
|
-
"work_dir": item.get("workDir"),
|
|
82
|
+
"work_dir": item.get("workDir") or item.get("workdir"),
|
|
78
83
|
"stdout": item.get("outputPath"),
|
|
79
84
|
"stderr": item.get("errorPath"),
|
|
80
85
|
}
|
|
@@ -371,16 +376,23 @@ class OpenAPIBackend(Backend):
|
|
|
371
376
|
result["raw"] = candidate
|
|
372
377
|
return result
|
|
373
378
|
|
|
374
|
-
|
|
379
|
+
history_items, _, history_raw = self._history_jobs(
|
|
375
380
|
hpc_url,
|
|
376
381
|
token,
|
|
377
382
|
scheduler_id,
|
|
378
|
-
job_id,
|
|
379
|
-
|
|
383
|
+
job_id=job_id,
|
|
384
|
+
days=30,
|
|
385
|
+
limit=1,
|
|
380
386
|
)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
387
|
+
history = next(
|
|
388
|
+
(
|
|
389
|
+
item
|
|
390
|
+
for item in history_items
|
|
391
|
+
if str(item.get("jobId") or item.get("job_id") or "") == job_id
|
|
392
|
+
),
|
|
393
|
+
None,
|
|
394
|
+
)
|
|
395
|
+
if history is None:
|
|
384
396
|
detail = "realtime and history endpoints returned no job record"
|
|
385
397
|
if realtime_error:
|
|
386
398
|
detail += f" ({realtime_error})"
|
|
@@ -388,7 +400,10 @@ class OpenAPIBackend(Backend):
|
|
|
388
400
|
result = normalize_job(history)
|
|
389
401
|
result["source"] = "history"
|
|
390
402
|
if options.get("raw"):
|
|
391
|
-
result["raw"] =
|
|
403
|
+
result["raw"] = {
|
|
404
|
+
"item": history,
|
|
405
|
+
"response": history_raw,
|
|
406
|
+
}
|
|
392
407
|
return result
|
|
393
408
|
|
|
394
409
|
@staticmethod
|
|
@@ -405,25 +420,53 @@ class OpenAPIBackend(Backend):
|
|
|
405
420
|
)
|
|
406
421
|
)
|
|
407
422
|
|
|
408
|
-
|
|
423
|
+
@staticmethod
|
|
424
|
+
def _job_items(data: Any) -> tuple[list[dict[str, Any]], int | None]:
|
|
425
|
+
if isinstance(data, dict):
|
|
426
|
+
items = data.get("items") or data.get("list") or data.get("rows") or []
|
|
427
|
+
total = data.get("total")
|
|
428
|
+
else:
|
|
429
|
+
items = data or []
|
|
430
|
+
total = None
|
|
431
|
+
if not isinstance(items, list):
|
|
432
|
+
raise BackendError("job list endpoint returned an unexpected data shape")
|
|
433
|
+
normalized_items = [item for item in items if isinstance(item, dict)]
|
|
434
|
+
return normalized_items, total
|
|
435
|
+
|
|
436
|
+
def _history_jobs(
|
|
409
437
|
self,
|
|
410
438
|
hpc_url: str,
|
|
411
439
|
token: str,
|
|
412
440
|
scheduler_id: str,
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
441
|
+
*,
|
|
442
|
+
job_id: str | None = None,
|
|
443
|
+
days: int = 30,
|
|
444
|
+
limit: int = 20,
|
|
445
|
+
) -> tuple[list[dict[str, Any]], int | None, Any]:
|
|
416
446
|
if not scheduler_id:
|
|
417
447
|
raise BackendError("history lookup requires a scheduler id")
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
448
|
+
end = datetime.now(SCNET_TIMEZONE)
|
|
449
|
+
start = end - timedelta(days=days)
|
|
450
|
+
params = {
|
|
451
|
+
"strClusterNameList": scheduler_id,
|
|
452
|
+
"timeType": "CUSTOM",
|
|
453
|
+
"isQueryByQueueTime": "false",
|
|
454
|
+
"start": 0,
|
|
455
|
+
"limit": limit,
|
|
456
|
+
"startTime": start.strftime("%Y-%m-%d %H:%M:%S"),
|
|
457
|
+
"endTime": end.strftime("%Y-%m-%d %H:%M:%S"),
|
|
458
|
+
}
|
|
459
|
+
if job_id:
|
|
460
|
+
params["jobId"] = job_id
|
|
461
|
+
data = self._json_request(
|
|
423
462
|
"GET",
|
|
424
|
-
service_endpoint(hpc_url, "hpc",
|
|
463
|
+
service_endpoint(hpc_url, "hpc", "/openapi/v2/historyjobs")
|
|
464
|
+
+ "?"
|
|
465
|
+
+ urlencode(params),
|
|
425
466
|
token=token,
|
|
426
467
|
)
|
|
468
|
+
items, total = self._job_items(data)
|
|
469
|
+
return items, total, data
|
|
427
470
|
|
|
428
471
|
def op_jobs(self, options: Mapping[str, Any]) -> Any:
|
|
429
472
|
hpc_url, token, scheduler_id, _, _ = self._hpc_context(options)
|
|
@@ -440,35 +483,23 @@ class OpenAPIBackend(Backend):
|
|
|
440
483
|
"start": 0,
|
|
441
484
|
"limit": limit,
|
|
442
485
|
}
|
|
486
|
+
data = self._json_request(
|
|
487
|
+
"GET",
|
|
488
|
+
service_endpoint(hpc_url, "hpc", path) + "?" + urlencode(params),
|
|
489
|
+
token=token,
|
|
490
|
+
)
|
|
491
|
+
items, total = self._job_items(data)
|
|
443
492
|
else:
|
|
444
493
|
days = _bounded_int(options, "days", 1)
|
|
445
494
|
if days > 90:
|
|
446
495
|
raise BackendError("days must be <= 90")
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
"start": 0,
|
|
455
|
-
"limit": limit,
|
|
456
|
-
"startTime": start.strftime("%Y-%m-%d %H:%M:%S"),
|
|
457
|
-
"endTime": end.strftime("%Y-%m-%d %H:%M:%S"),
|
|
458
|
-
}
|
|
459
|
-
data = self._json_request(
|
|
460
|
-
"GET",
|
|
461
|
-
service_endpoint(hpc_url, "hpc", path) + "?" + urlencode(params),
|
|
462
|
-
token=token,
|
|
463
|
-
)
|
|
464
|
-
if isinstance(data, dict):
|
|
465
|
-
items = data.get("items") or data.get("list") or data.get("rows") or []
|
|
466
|
-
total = data.get("total")
|
|
467
|
-
else:
|
|
468
|
-
items = data or []
|
|
469
|
-
total = None
|
|
470
|
-
if not isinstance(items, list):
|
|
471
|
-
raise BackendError("job list endpoint returned an unexpected data shape")
|
|
496
|
+
items, total, data = self._history_jobs(
|
|
497
|
+
hpc_url,
|
|
498
|
+
token,
|
|
499
|
+
scheduler_id,
|
|
500
|
+
days=days,
|
|
501
|
+
limit=limit,
|
|
502
|
+
)
|
|
472
503
|
result = [normalize_job(item) for item in items if isinstance(item, dict)]
|
|
473
504
|
payload = {
|
|
474
505
|
"scope": scope,
|
|
@@ -86,28 +86,39 @@ class SCNetClient:
|
|
|
86
86
|
@classmethod
|
|
87
87
|
def _write_token_cache(
|
|
88
88
|
cls, fingerprint: str, regions: list[dict[str, Any]]
|
|
89
|
-
) ->
|
|
89
|
+
) -> bool:
|
|
90
90
|
path, _ = cls._token_cache_paths()
|
|
91
|
-
path.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
|
|
92
91
|
temporary = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
92
|
+
try:
|
|
93
|
+
path.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
|
|
94
|
+
temporary.write_text(
|
|
95
|
+
json.dumps(
|
|
96
|
+
{
|
|
97
|
+
"created_at": time.time(),
|
|
98
|
+
"fingerprint": fingerprint,
|
|
99
|
+
"regions": regions,
|
|
100
|
+
},
|
|
101
|
+
ensure_ascii=False,
|
|
102
|
+
),
|
|
103
|
+
encoding="utf-8",
|
|
104
|
+
)
|
|
105
|
+
os.chmod(temporary, 0o600)
|
|
106
|
+
os.replace(temporary, path)
|
|
107
|
+
return True
|
|
108
|
+
except OSError:
|
|
109
|
+
try:
|
|
110
|
+
temporary.unlink()
|
|
111
|
+
except OSError:
|
|
112
|
+
pass
|
|
113
|
+
return False
|
|
106
114
|
|
|
107
115
|
@classmethod
|
|
108
116
|
def _acquire_token_cache_lock(cls) -> Path | None:
|
|
109
117
|
_, lock = cls._token_cache_paths()
|
|
110
|
-
|
|
118
|
+
try:
|
|
119
|
+
lock.parent.mkdir(parents=True, mode=0o700, exist_ok=True)
|
|
120
|
+
except OSError:
|
|
121
|
+
return None
|
|
111
122
|
for _ in range(50):
|
|
112
123
|
try:
|
|
113
124
|
fd = os.open(lock, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
@@ -121,6 +132,8 @@ class SCNetClient:
|
|
|
121
132
|
except OSError:
|
|
122
133
|
pass
|
|
123
134
|
time.sleep(0.2)
|
|
135
|
+
except OSError:
|
|
136
|
+
return None
|
|
124
137
|
return None
|
|
125
138
|
|
|
126
139
|
@staticmethod
|
|
@@ -128,7 +141,7 @@ class SCNetClient:
|
|
|
128
141
|
if lock is not None:
|
|
129
142
|
try:
|
|
130
143
|
lock.unlink()
|
|
131
|
-
except
|
|
144
|
+
except OSError:
|
|
132
145
|
pass
|
|
133
146
|
|
|
134
147
|
def request(
|
|
@@ -257,7 +270,8 @@ class SCNetClient:
|
|
|
257
270
|
self._regions_cache = [
|
|
258
271
|
item for item in data if isinstance(item, dict)
|
|
259
272
|
]
|
|
260
|
-
|
|
273
|
+
if lock is not None:
|
|
274
|
+
self._write_token_cache(fingerprint, self._regions_cache)
|
|
261
275
|
return self._regions_cache
|
|
262
276
|
finally:
|
|
263
277
|
self._release_token_cache_lock(lock)
|