dna-builder-sdk 0.1.0__tar.gz
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.
- dna_builder_sdk-0.1.0/LICENSE +21 -0
- dna_builder_sdk-0.1.0/PKG-INFO +29 -0
- dna_builder_sdk-0.1.0/README.md +118 -0
- dna_builder_sdk-0.1.0/pyproject.toml +40 -0
- dna_builder_sdk-0.1.0/setup.cfg +4 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/__init__.py +27 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/backend.py +202 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/calc/__init__.py +13 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/calc/ast.py +254 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/calc/build.py +694 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/calc/curves.py +246 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/calc/damage.py +634 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/calc/engine.py +1107 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/calc/entities.py +635 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/calc/evaluate.py +364 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/calc/gamedata.py +112 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/calc/jscode.py +613 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/data_pack.py +263 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/errors.py +26 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/game_data.py +118 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/module_store.py +220 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk/snapshots.py +141 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk.egg-info/PKG-INFO +29 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk.egg-info/SOURCES.txt +36 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk.egg-info/dependency_links.txt +1 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk.egg-info/requires.txt +1 -0
- dna_builder_sdk-0.1.0/src/dna_builder_sdk.egg-info/top_level.txt +1 -0
- dna_builder_sdk-0.1.0/tests/test_ast_evaluate.py +77 -0
- dna_builder_sdk-0.1.0/tests/test_calc_perf.py +164 -0
- dna_builder_sdk-0.1.0/tests/test_data_pack.py +47 -0
- dna_builder_sdk-0.1.0/tests/test_datapack_perf.py +105 -0
- dna_builder_sdk-0.1.0/tests/test_gamedata_shape.py +55 -0
- dna_builder_sdk-0.1.0/tests/test_jscode_corpus.py +54 -0
- dna_builder_sdk-0.1.0/tests/test_online_targets.py +144 -0
- dna_builder_sdk-0.1.0/tests/test_shardbuild_parity.py +111 -0
- dna_builder_sdk-0.1.0/tests/test_skill_panels.py +141 -0
- dna_builder_sdk-0.1.0/tests/test_snapshots.py +103 -0
- dna_builder_sdk-0.1.0/tests/test_table_source.py +210 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 pa001024
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dna-builder-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: dna-builder 自建后端 + 数据包 + 纯表达式计算的 Python SDK(公开读接口 / 双加载 / 无编辑语义)
|
|
5
|
+
Author: pa001024
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pa001024/dna-builder
|
|
8
|
+
Project-URL: Repository, https://github.com/pa001024/dna-builder
|
|
9
|
+
Project-URL: Issues, https://github.com/pa001024/dna-builder/issues
|
|
10
|
+
Keywords: dna-builder,game-data,sdk,duet-night-abyss
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Games/Entertainment
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: msgpack>=1.0
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
README.md
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# dna-builder-sdk
|
|
2
|
+
|
|
3
|
+
dna-builder 后端服务 + 数据包 + 纯表达式计算的 Python SDK。
|
|
4
|
+
|
|
5
|
+
- **后端**:只覆盖无需登录的公开读接口(评论 / 构筑 / 攻略 / 时间轴 / 榜单 / 脚本 / MOD / DPS / `gameData*`)。写接口、登录态、`admin`、`shop` 私有接口一律不在本包。
|
|
6
|
+
- **数据双加载(同一抽象接口)**:全量 datapack 下载(`versions.json` + `<ver>.zip`),或按模块按需经 `gameData` GraphQL 分页拉取;两者都实现 `TableSource.load_tables()` → `GameDataTables`,上层(`Engine` / `snapshot_from_online` / 示例)只依赖该接口。磁盘持久化并支持运行时热重载。
|
|
7
|
+
- **计算**:纯 BD JSON(CharSettings + charId)进,目标值出。attrs/面板/技能表/伤害乘区
|
|
8
|
+
全由引擎自算(实体等级化→加成汇总→条件/code 不动点→武器面板→技能结算链),
|
|
9
|
+
真实 BD 42 个表达式与 bun 真机逐位一致。不含编辑语义(无 AutoBuild、无配装寻优)。
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from dna_builder_sdk import BackendClient, GameDataClient, DataPackStore, ModuleStore
|
|
13
|
+
|
|
14
|
+
backend = BackendClient() # 默认 https://api.dna-builder.cn,本地联调传 http://localhost:8887
|
|
15
|
+
gamedata = GameDataClient(backend)
|
|
16
|
+
print(gamedata.modules())
|
|
17
|
+
|
|
18
|
+
# 数据源抽象接口:两者都实现 load_tables() → GameDataTables(直接给 Engine 用)
|
|
19
|
+
pack = DataPackStore()
|
|
20
|
+
pack.ensure() # 本地有缓存即激活最新,无缓存则下载远端最新
|
|
21
|
+
print(pack.load_module("char").keys())
|
|
22
|
+
|
|
23
|
+
# 按需通道:模块列表在声明时确定(缺省 8 张全量表),load_tables() 只拉这些表
|
|
24
|
+
store = ModuleStore(gamedata, modules=["chars", "mods", "weapons", "buffs", "effects"])
|
|
25
|
+
print(store.tables, store.datasets)
|
|
26
|
+
store.preload() # 可选:一次性预拉取声明过的模块
|
|
27
|
+
store.reload("mod")
|
|
28
|
+
|
|
29
|
+
# 计算:纯 BD JSON 进,目标值出(表经抽象接口来,测试用最小 fixture)
|
|
30
|
+
from dna_builder_sdk.calc import Engine, GameDataTables, build_state
|
|
31
|
+
|
|
32
|
+
tables = pack.load_tables() # 或 store.load_tables(),或 GameDataTables.from_dict({...})
|
|
33
|
+
engine = Engine(build_state(char_id, settings, tables), tables)
|
|
34
|
+
engine.calculate() # 目标函数值;engine.calculate("[若华纯蓄]DPS") 指定表达式
|
|
35
|
+
|
|
36
|
+
# 面板与技能:指定等级/MOD/技能等级/溯源
|
|
37
|
+
engine.char_panel() # 角色面板(全量角色属性)
|
|
38
|
+
engine.weapon_panel("melee") # 武器面板(melee/ranged/skill 或技能名)
|
|
39
|
+
engine.skill_levels_final() # 最终技能等级(含 trio + 溯源加成,钳制 1-12)
|
|
40
|
+
engine.skill_fields("以坚忍之名") # 技能字段面板数值
|
|
41
|
+
# 技能等级 trio(E/Q/P)与溯源等级(单数;7 即含 1-7 节点效果)
|
|
42
|
+
engine2 = Engine(build_state(cid, settings, tables, skill_levels=[10, 10, 10], traces=7), tables)
|
|
43
|
+
|
|
44
|
+
# 在线快照:bdid → 线上 JSON → 完整快照(表达式/属性/面板/技能表)
|
|
45
|
+
# 快照与 Engine 走同一条数据通路:传抽象数据源,不再另分支 data_source
|
|
46
|
+
from dna_builder_sdk import snapshot_from_online, snapshot_from_settings
|
|
47
|
+
|
|
48
|
+
snapshot = snapshot_from_online("Svmqw3LGoY", source=pack) # 全量通道
|
|
49
|
+
snapshot = snapshot_from_online("Svmqw3LGoY", source=store) # 或按需通道
|
|
50
|
+
snapshot.calculate() # 目标函数值
|
|
51
|
+
snapshot.evaluate("角色::攻击!") # 任意表达式
|
|
52
|
+
snapshot.char_panel() # 角色面板
|
|
53
|
+
snapshot.summary() # 一页总览
|
|
54
|
+
# 注:线上实时数据随版本更新,结果跟踪数据版本;回归测试用钉住版本的 fixture。
|
|
55
|
+
|
|
56
|
+
# 示例:sdk/python/examples/snapshot.py、sdk/python/examples/panels.py
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
更多接口见各模块 docstring。缓存根目录可用 `DNA_BUILDER_CACHE` 覆盖。
|
|
60
|
+
|
|
61
|
+
## 发版(GitHub Actions → PyPI,无 token)
|
|
62
|
+
|
|
63
|
+
本仓用 Trusted Publishing:打 tag 即发版。
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
git tag v0.1.0 && git push origin v0.1.0
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- 工作流 `.github/workflows/publish.yml`:先跑全量单测,再校验 tag 与
|
|
70
|
+
`pyproject.toml` 版本一致,最后 `build` + 上传。
|
|
71
|
+
- 只需在 PyPI 上配一次:在账号设置 → Publishing 里加 **pending publisher**
|
|
72
|
+
(owner `pa001024` / repo `dob-python-sdk` / workflow `publish.yml`),
|
|
73
|
+
首次上传会自动创建 `dna-builder-sdk` 项目。之后如需轮换,可改用限定项目的作用域。
|
|
74
|
+
|
|
75
|
+
## 测试报告
|
|
76
|
+
|
|
77
|
+
运行(仓库根目录,纯本地,无网络):
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
PYTHONPATH=sdk/python/src python -m unittest discover -s sdk/python/tests
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
52 tests 全过(含 4 个性能基线)。测试清单:
|
|
84
|
+
|
|
85
|
+
| 文件 | 覆盖 |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `test_ast_evaluate.py` | 表达式 AST 解析与求值 |
|
|
88
|
+
| `test_data_pack.py` | 数据包 revive/编解码往返 |
|
|
89
|
+
| `test_gamedata_shape.py` | gameData ↔ datapack 形状归一化 |
|
|
90
|
+
| `test_jscode_corpus.py` | 14 段真实 BUFF code 可解析可执行 |
|
|
91
|
+
| `test_shardbuild_parity.py` | 纯 BD JSON:42 表达式 + 全量 attrs/panels 与 bun 真机逐位一致 |
|
|
92
|
+
| `test_online_targets.py` | 6 个线上真实 BD 的变量/target/attrs parity(DOT 分流跳过) |
|
|
93
|
+
| `test_skill_panels.py` | 技能等级 trio + 溯源 + 字段面板 |
|
|
94
|
+
| `test_snapshots.py` | 在线快照装配(fake 后端 + fixture 表) |
|
|
95
|
+
| `test_table_source.py` | 抽象数据源:模块声明、`load_tables()`、快照经 Engine 通路 |
|
|
96
|
+
| `test_datapack_perf.py` | 真实包加载性能基线(`mock/data-pack/1.6.208.6.zip` 钉住版本) |
|
|
97
|
+
| `test_calc_perf.py` | fixture BD 批量计算性能基线(online 六连批 + shard 42 式) |
|
|
98
|
+
|
|
99
|
+
### 表名映射核验(`calc/gamedata.py` 三处常量,均已对真实包 + 前端 + 服务端验过)
|
|
100
|
+
|
|
101
|
+
- `MODULE_DEFAULTS`:7 个 datapack 模块(`char/mod/buff/effect/weapon/pet/monster.data`)
|
|
102
|
+
在真实包内全部存在且皆有 `default` 导出,条数与基线锚点一致。
|
|
103
|
+
- `PET_EXPORTS`:`pets → default`(152 条,对齐前端 `import petData from "./pet.data"`;
|
|
104
|
+
曾误写 `petData` 导致魔灵表为空,已修正,Go SDK 同步改)、`pet_entries → petEntrys`(73 条)。
|
|
105
|
+
- `DATASET_DEFAULTS`:8 张表 → gameData 数据集 id(`char/mod/buff/effect/weapon/pet/monster` +
|
|
106
|
+
具名 `pet:petEntrys`),与服务端 `gameDataRegistry` 的模块/数据集 id 逐项一致。
|
|
107
|
+
|
|
108
|
+
### 性能基线(best 实测,供后人对比;预算只防病态劣化,约实测百倍)
|
|
109
|
+
|
|
110
|
+
| 基线 | best | 预算 | 锚点 |
|
|
111
|
+
|---|---|---|---|
|
|
112
|
+
| `activate(1.6.208.6)`(manifest + 全包 zip 校验,23MB) | ~166ms(best-of-3) | 60s | manifest.version 吻合 |
|
|
113
|
+
| `load_tables()`(8 表 msgpack 解码 + 索引装配) | ~27ms(best-of-3) | 60s | 8 表条数 `33/586/187/65/71/152/73/393` |
|
|
114
|
+
| online 六 BD 一批(装配 + 目标值 + 全量属性) | ~20ms(best-of-5) | 5s | 目标值 + 全量 attrs 对 `online_expected.json` |
|
|
115
|
+
| shard 一批(42 表达式 + 全量属性) | ~56ms(best-of-3,~750 expr/s) | 10s | 逐式对 `shardBuild1_expr.json` 真机值 |
|
|
116
|
+
|
|
117
|
+
以上数为开发机实测(Windows / Python 3.14),换机器只对比量级;基线方法(checksum
|
|
118
|
+
锚点先行、best 取 min、每轮等价断言)见技能 `.agents/skills/perf-hotspot-profiling/SKILL.md`。
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dna-builder-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "dna-builder 自建后端 + 数据包 + 纯表达式计算的 Python SDK(公开读接口 / 双加载 / 无编辑语义)"
|
|
9
|
+
readme = { text = "README.md", content-type = "text/markdown" }
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE*"]
|
|
13
|
+
authors = [{ name = "pa001024" }]
|
|
14
|
+
keywords = ["dna-builder", "game-data", "sdk", "duet-night-abyss"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Topic :: Games/Entertainment",
|
|
26
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = ["msgpack>=1.0"]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://github.com/pa001024/dna-builder"
|
|
33
|
+
Repository = "https://github.com/pa001024/dna-builder"
|
|
34
|
+
Issues = "https://github.com/pa001024/dna-builder/issues"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.package-dir]
|
|
40
|
+
"" = "src"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""dna-builder Python SDK:自建后端公开读接口 + 数据双加载 + 纯表达式计算。"""
|
|
2
|
+
|
|
3
|
+
from .backend import BackendClient
|
|
4
|
+
from .calc.gamedata import TableSource, resolve_tables
|
|
5
|
+
from .data_pack import DataPackStore, revive_packed_value
|
|
6
|
+
from .errors import DobApiError, DobGraphQLError, DobHttpError
|
|
7
|
+
from .game_data import GameDataClient
|
|
8
|
+
from .module_store import ModuleStore
|
|
9
|
+
from .snapshots import BuildSnapshot, snapshot_from_online, snapshot_from_settings
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"BackendClient",
|
|
13
|
+
"GameDataClient",
|
|
14
|
+
"DataPackStore",
|
|
15
|
+
"ModuleStore",
|
|
16
|
+
"TableSource",
|
|
17
|
+
"BuildSnapshot",
|
|
18
|
+
"resolve_tables",
|
|
19
|
+
"snapshot_from_online",
|
|
20
|
+
"snapshot_from_settings",
|
|
21
|
+
"revive_packed_value",
|
|
22
|
+
"DobApiError",
|
|
23
|
+
"DobHttpError",
|
|
24
|
+
"DobGraphQLError",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""自建后端(Elysia)传输层 + 公开读接口。
|
|
2
|
+
|
|
3
|
+
只覆盖无需登录的 Query:评论 / 构筑 / 攻略 / 时间轴 / 榜单 / 脚本 /
|
|
4
|
+
MOD / DPS / gameData*。Mutation、admin、shop 私有、my* 一律不实现。
|
|
5
|
+
标准库 urllib 实现零额外 HTTP 依赖,302 默认跟随。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import urllib.error
|
|
12
|
+
import urllib.parse
|
|
13
|
+
import urllib.request
|
|
14
|
+
|
|
15
|
+
from .errors import DobGraphQLError, DobHttpError
|
|
16
|
+
|
|
17
|
+
_DEFAULT_TIMEOUT = 30
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _post_json(url: str, payload: dict, timeout: float) -> tuple[int, dict | list, str]:
|
|
21
|
+
"""POST JSON 并返回 (status, body, final_url),302 由 opener 自动跟随。"""
|
|
22
|
+
data = json.dumps(payload).encode("utf-8")
|
|
23
|
+
req = urllib.request.Request(
|
|
24
|
+
url,
|
|
25
|
+
data=data,
|
|
26
|
+
headers={"content-type": "application/json", "accept": "application/json"},
|
|
27
|
+
method="POST",
|
|
28
|
+
)
|
|
29
|
+
try:
|
|
30
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
31
|
+
return resp.status, json.loads(resp.read().decode("utf-8")), resp.geturl()
|
|
32
|
+
except urllib.error.HTTPError as exc:
|
|
33
|
+
try:
|
|
34
|
+
body = json.loads(exc.read().decode("utf-8"))
|
|
35
|
+
except Exception:
|
|
36
|
+
body = {"message": str(exc)}
|
|
37
|
+
raise DobHttpError(f"HTTP {exc.code}: {url}", status=exc.code, payload=body) from exc
|
|
38
|
+
except urllib.error.URLError as exc:
|
|
39
|
+
raise DobHttpError(f"连接失败: {url} ({exc.reason})", status=0) from exc
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_json(url: str, timeout: float):
|
|
43
|
+
req = urllib.request.Request(url, headers={"accept": "application/json"}, method="GET")
|
|
44
|
+
try:
|
|
45
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
46
|
+
return json.loads(resp.read().decode("utf-8"))
|
|
47
|
+
except urllib.error.HTTPError as exc:
|
|
48
|
+
raise DobHttpError(f"HTTP {exc.code}: {url}", status=exc.code) from exc
|
|
49
|
+
except urllib.error.URLError as exc:
|
|
50
|
+
raise DobHttpError(f"连接失败: {url} ({exc.reason})", status=0) from exc
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BackendClient:
|
|
54
|
+
"""自建后端客户端,默认线上端点;本地联调传 http://localhost:8887。"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, base_url: str = "https://api.dna-builder.cn", timeout: float = _DEFAULT_TIMEOUT):
|
|
57
|
+
self.base_url = base_url.rstrip("/")
|
|
58
|
+
self.timeout = timeout
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def graphql_url(self) -> str:
|
|
62
|
+
return f"{self.base_url}/graphql"
|
|
63
|
+
|
|
64
|
+
def graphql(self, query: str, variables: dict | None = None) -> dict:
|
|
65
|
+
"""裸 GraphQL 调用,errors 非空时抛 DobGraphQLError。"""
|
|
66
|
+
_, body, _ = _post_json(self.graphql_url, {"query": query, "variables": variables or {}}, self.timeout)
|
|
67
|
+
if not isinstance(body, dict):
|
|
68
|
+
raise DobGraphQLError(f"网关返回非对象: {body!r}", errors=[body])
|
|
69
|
+
if body.get("errors"):
|
|
70
|
+
first = body["errors"][0]
|
|
71
|
+
msg = first.get("message", "graphql error") if isinstance(first, dict) else str(first)
|
|
72
|
+
raise DobGraphQLError(msg, errors=body["errors"])
|
|
73
|
+
return body.get("data") or {}
|
|
74
|
+
|
|
75
|
+
# ---- 评论(读) ----
|
|
76
|
+
def comments(self, target_id: str, limit: int = 50, offset: int = 0) -> list:
|
|
77
|
+
data = self.graphql(
|
|
78
|
+
"query($t:String!,$l:Int,$o:Int){comments(targetId:$t,limit:$l,offset:$o){id targetId content createdAt user{id name}}}",
|
|
79
|
+
{"t": target_id, "l": limit, "o": offset},
|
|
80
|
+
)
|
|
81
|
+
return data.get("comments") or []
|
|
82
|
+
|
|
83
|
+
def comments_count(self, target_id: str) -> int:
|
|
84
|
+
data = self.graphql("query($t:String!){commentsCount(targetId:$t)}", {"t": target_id})
|
|
85
|
+
return int(data.get("commentsCount") or 0)
|
|
86
|
+
|
|
87
|
+
# ---- 构筑(读) ----
|
|
88
|
+
def builds(self, search=None, char_id=None, user_id=None, limit=20, offset=0, sort_by="latest") -> list:
|
|
89
|
+
data = self.graphql(
|
|
90
|
+
"query($s:String,$c:Int,$u:String,$l:Int,$o:Int,$sb:String)"
|
|
91
|
+
"{builds(search:$s,charId:$c,userId:$u,limit:$l,offset:$o,sortBy:$sb)"
|
|
92
|
+
"{id title desc charId userId views likes isRecommended isPinned createdAt updateAt targetValue charSettings}}",
|
|
93
|
+
{"s": search, "c": char_id, "u": user_id, "l": limit, "o": offset, "sb": sort_by},
|
|
94
|
+
)
|
|
95
|
+
return data.get("builds") or []
|
|
96
|
+
|
|
97
|
+
def builds_count(self, search=None, char_id=None) -> int:
|
|
98
|
+
data = self.graphql(
|
|
99
|
+
"query($s:String,$c:Int){buildsCount(search:$s,charId:$c)}", {"s": search, "c": char_id}
|
|
100
|
+
)
|
|
101
|
+
return int(data.get("buildsCount") or 0)
|
|
102
|
+
|
|
103
|
+
def build(self, build_id: str) -> dict | None:
|
|
104
|
+
data = self.graphql(
|
|
105
|
+
"query($id:String!){build(id:$id){id title desc charId charSettings userId views likes createdAt updateAt targetValue}}",
|
|
106
|
+
{"id": build_id},
|
|
107
|
+
)
|
|
108
|
+
return data.get("build")
|
|
109
|
+
|
|
110
|
+
def recommended_builds(self, limit=10) -> list:
|
|
111
|
+
return (self.graphql("query($l:Int){recommendedBuilds(limit:$l){id title charId likes views}}", {"l": limit}).get("recommendedBuilds") or [])
|
|
112
|
+
|
|
113
|
+
def trending_builds(self, limit=10) -> list:
|
|
114
|
+
return (self.graphql("query($l:Int){trendingBuilds(limit:$l){id title charId likes views}}", {"l": limit}).get("trendingBuilds") or [])
|
|
115
|
+
|
|
116
|
+
# ---- 攻略(读) ----
|
|
117
|
+
def guides(self, search=None, type=None, char_id=None, user_id=None, limit=20, offset=0) -> list:
|
|
118
|
+
data = self.graphql(
|
|
119
|
+
"query($s:String,$t:String,$c:Int,$u:String,$l:Int,$o:Int)"
|
|
120
|
+
"{guides(search:$s,type:$t,charId:$c,userId:$u,limit:$l,offset:$o){id title type charId views likes createdAt updateAt}}",
|
|
121
|
+
{"s": search, "t": type, "c": char_id, "u": user_id, "l": limit, "o": offset},
|
|
122
|
+
)
|
|
123
|
+
return data.get("guides") or []
|
|
124
|
+
|
|
125
|
+
def guide(self, guide_id: str) -> dict | None:
|
|
126
|
+
data = self.graphql(
|
|
127
|
+
"query($id:String!){guide(id:$id){id title type content images charId buildId views likes createdAt updateAt}}",
|
|
128
|
+
{"id": guide_id},
|
|
129
|
+
)
|
|
130
|
+
return data.get("guide")
|
|
131
|
+
|
|
132
|
+
# ---- 时间轴(读) ----
|
|
133
|
+
def timelines(self, search=None, char_id=None, user_id=None, limit=20, offset=0, sort_by="recent") -> list:
|
|
134
|
+
data = self.graphql(
|
|
135
|
+
"query($s:String,$c:Int,$u:String,$l:Int,$o:Int,$sb:String)"
|
|
136
|
+
"{timelines(search:$s,charId:$c,userId:$u,limit:$l,offset:$o,sortBy:$sb){id title charId views likes createdAt updateAt}}",
|
|
137
|
+
{"s": search, "c": char_id, "u": user_id, "l": limit, "o": offset, "sb": sort_by},
|
|
138
|
+
)
|
|
139
|
+
return data.get("timelines") or []
|
|
140
|
+
|
|
141
|
+
def timeline(self, timeline_id: str) -> dict | None:
|
|
142
|
+
data = self.graphql(
|
|
143
|
+
"query($id:String!){timeline(id:$id){id title charId views likes createdAt updateAt}}",
|
|
144
|
+
{"id": timeline_id},
|
|
145
|
+
)
|
|
146
|
+
return data.get("timeline")
|
|
147
|
+
|
|
148
|
+
# ---- 榜单(读) ----
|
|
149
|
+
def ranking_lists(self) -> list:
|
|
150
|
+
return (self.graphql("{rankingLists{id name desc createdAt updateAt}}").get("rankingLists") or [])
|
|
151
|
+
|
|
152
|
+
def ranking_list(self, list_id: str) -> dict | None:
|
|
153
|
+
data = self.graphql(
|
|
154
|
+
"query($id:String!){rankingList(id:$id){id name desc items{id charId buildId sortOrder}}}",
|
|
155
|
+
{"id": list_id},
|
|
156
|
+
)
|
|
157
|
+
return data.get("rankingList")
|
|
158
|
+
|
|
159
|
+
def ranking_list_items(self, ranking_list_id: str) -> list:
|
|
160
|
+
data = self.graphql(
|
|
161
|
+
"query($id:String!){rankingListItems(rankingListId:$id){id charId buildId sortOrder}}",
|
|
162
|
+
{"id": ranking_list_id},
|
|
163
|
+
)
|
|
164
|
+
return data.get("rankingListItems") or []
|
|
165
|
+
|
|
166
|
+
# ---- 脚本 / MOD / DPS(读) ----
|
|
167
|
+
def scripts(self, search=None, category=None, user_id=None, limit=20, offset=0) -> list:
|
|
168
|
+
data = self.graphql(
|
|
169
|
+
"query($s:String,$c:String,$u:String,$l:Int,$o:Int)"
|
|
170
|
+
"{scripts(search:$s,category:$c,userId:$u,limit:$l,offset:$o){id title category createdAt updateAt}}",
|
|
171
|
+
{"s": search, "c": category, "u": user_id, "l": limit, "o": offset},
|
|
172
|
+
)
|
|
173
|
+
return data.get("scripts") or []
|
|
174
|
+
|
|
175
|
+
def script(self, script_id: str, preview: bool = False) -> dict | None:
|
|
176
|
+
data = self.graphql(
|
|
177
|
+
"query($id:String!,$p:Boolean){script(id:$id,preview:$p){id title category content}}",
|
|
178
|
+
{"id": script_id, "p": preview},
|
|
179
|
+
)
|
|
180
|
+
return data.get("script")
|
|
181
|
+
|
|
182
|
+
def game_mods(self, search=None, category=None, entity=None, limit=20, offset=0, sort_by=None) -> list:
|
|
183
|
+
data = self.graphql(
|
|
184
|
+
"query($s:String,$c:String,$e:String,$l:Int,$o:Int,$sb:String)"
|
|
185
|
+
"{gameMods(search:$s,category:$c,entity:$e,limit:$l,offset:$o,sortBy:$sb){id title category entity status createdAt updateAt}}",
|
|
186
|
+
{"s": search, "c": category, "e": entity, "l": limit, "o": offset, "sb": sort_by},
|
|
187
|
+
)
|
|
188
|
+
return data.get("gameMods") or []
|
|
189
|
+
|
|
190
|
+
def game_mod(self, mod_id: str) -> dict | None:
|
|
191
|
+
data = self.graphql(
|
|
192
|
+
"query($id:String!){gameMod(id:$id){id title category entity status}}", {"id": mod_id}
|
|
193
|
+
)
|
|
194
|
+
return data.get("gameMod")
|
|
195
|
+
|
|
196
|
+
def dps_list(self, char_id=None, build_id=None, timeline_id=None, limit=20, offset=0, sort_by="dpsValue") -> list:
|
|
197
|
+
data = self.graphql(
|
|
198
|
+
"query($c:Int,$b:String,$t:String,$l:Int,$o:Int,$sb:String)"
|
|
199
|
+
"{dpsList(charId:$c,buildId:$b,timelineId:$t,limit:$l,offset:$o,sortBy:$sb){id charId buildId dpsValue createdAt}}",
|
|
200
|
+
{"c": char_id, "b": build_id, "t": timeline_id, "l": limit, "o": offset, "sb": sort_by},
|
|
201
|
+
)
|
|
202
|
+
return data.get("dpsList") or []
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""calc 包:BD 装配 + 聚合引擎 + 表达式/伤害结算(无编辑语义)。"""
|
|
2
|
+
|
|
3
|
+
from .ast import AstError, parse_ast, tokenize_ast
|
|
4
|
+
from .build import build_state, parse_trace_levels
|
|
5
|
+
from .damage import DamageContext
|
|
6
|
+
from .engine import Engine
|
|
7
|
+
from .evaluate import evaluate
|
|
8
|
+
from .gamedata import GameDataTables, TableSource, resolve_tables
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"AstError", "DamageContext", "Engine", "GameDataTables", "TableSource",
|
|
12
|
+
"build_state", "parse_trace_levels", "parse_ast", "tokenize_ast", "evaluate", "resolve_tables",
|
|
13
|
+
]
|