OVAPortableText 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.
- ova_portable_text/__init__.py +93 -0
- ova_portable_text/base.py +165 -0
- ova_portable_text/block_objects.py +139 -0
- ova_portable_text/content.py +74 -0
- ova_portable_text/document.py +303 -0
- ova_portable_text/exceptions.py +234 -0
- ova_portable_text/helpers.py +203 -0
- ova_portable_text/inline.py +87 -0
- ova_portable_text/numbering.py +227 -0
- ova_portable_text/py.typed +0 -0
- ova_portable_text/registry.py +555 -0
- ova_portable_text/resolver.py +340 -0
- ova_portable_text/section.py +412 -0
- ova_portable_text/text.py +426 -0
- ova_portable_text/theme.py +46 -0
- ova_portable_text/validator.py +511 -0
- ova_portable_text/version.py +9 -0
- ovaportabletext-0.1.0.dist-info/METADATA +241 -0
- ovaportabletext-0.1.0.dist-info/RECORD +21 -0
- ovaportabletext-0.1.0.dist-info/WHEEL +4 -0
- ovaportabletext-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Registry-entry models for OVAPortableText.
|
|
5
|
+
OVAPortableText 的 registry 条目模型。
|
|
6
|
+
|
|
7
|
+
This module contains typed implementations of the top-level registries.
|
|
8
|
+
本模块提供顶层 registry 的强类型实现。
|
|
9
|
+
|
|
10
|
+
Current implementation focus / 当前实现重点:
|
|
11
|
+
- assets.images / 图片资源
|
|
12
|
+
- assets.logos / 品牌 logo 资源
|
|
13
|
+
- assets.backgrounds / 背景资源
|
|
14
|
+
- assets.icons / 图标资源
|
|
15
|
+
- assets.attachments / 附件资源
|
|
16
|
+
- datasets.tables / 表格数据
|
|
17
|
+
- datasets.charts (pie only for now) / 图表数据(当前仅正式支持 pie)
|
|
18
|
+
- datasets.metrics / 指标数据占位
|
|
19
|
+
- bibliography / footnotes / glossary
|
|
20
|
+
|
|
21
|
+
Important note / 重要说明:
|
|
22
|
+
The protocol intentionally leaves some registries less detailed in v1.
|
|
23
|
+
协议在 v1 中有意没有把部分 registry 细字段彻底写死。
|
|
24
|
+
So we provide small typed cores with extension-friendly behaviour.
|
|
25
|
+
因此这里采用“最小强类型核心 + 允许扩展”的方式。
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from typing import Any, Literal
|
|
29
|
+
|
|
30
|
+
from pydantic import ConfigDict, Field, field_validator, model_validator
|
|
31
|
+
|
|
32
|
+
from .base import OvaBaseModel
|
|
33
|
+
from .text import TextBlock
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RegistryEntryBase(OvaBaseModel):
|
|
37
|
+
"""
|
|
38
|
+
Common base for registry entries.
|
|
39
|
+
registry 条目的公共基类。
|
|
40
|
+
|
|
41
|
+
The protocol repeatedly uses a common entry pattern:
|
|
42
|
+
协议中多次复用了一类公共条目模式:
|
|
43
|
+
- `id`: stable unique identifier / 稳定唯一标识
|
|
44
|
+
- `anchor`: render-time anchor / 渲染时锚点
|
|
45
|
+
- `label`: human-friendly label / 人类可读标签
|
|
46
|
+
- `meta`: extension bag / 扩展元数据
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
id: str
|
|
50
|
+
anchor: str | None = None
|
|
51
|
+
label: str | None = None
|
|
52
|
+
meta: dict[str, Any] = Field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
@model_validator(mode="after")
|
|
55
|
+
def set_default_anchor(self) -> "RegistryEntryBase":
|
|
56
|
+
"""
|
|
57
|
+
Use `id` as fallback anchor.
|
|
58
|
+
当未提供 anchor 时,使用 `id` 作为默认锚点。
|
|
59
|
+
"""
|
|
60
|
+
if self.anchor is None:
|
|
61
|
+
self.anchor = self.id
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class StaticAssetBase(RegistryEntryBase):
|
|
66
|
+
"""
|
|
67
|
+
Common base for lightweight static assets.
|
|
68
|
+
轻量静态资源条目的公共基类。
|
|
69
|
+
|
|
70
|
+
Why introduce this layer now?
|
|
71
|
+
为什么现在引入这一层?
|
|
72
|
+
Because the protocol has already fixed the top-level buckets:
|
|
73
|
+
因为协议已经冻结了顶层桶结构:
|
|
74
|
+
- images
|
|
75
|
+
- logos
|
|
76
|
+
- backgrounds
|
|
77
|
+
- icons
|
|
78
|
+
- attachments
|
|
79
|
+
|
|
80
|
+
Even though their detailed fields are not all frozen yet,
|
|
81
|
+
it is still useful to keep a shared strongly typed minimum shape.
|
|
82
|
+
即便它们的详细字段还没有全部冻结,
|
|
83
|
+
共享一个最小强类型形态仍然很有价值。
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
87
|
+
|
|
88
|
+
src: str
|
|
89
|
+
mimeType: str | None = None
|
|
90
|
+
checksum: str | None = None
|
|
91
|
+
source: str | None = None
|
|
92
|
+
copyright: str | None = None
|
|
93
|
+
language: str | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ImageAsset(StaticAssetBase):
|
|
97
|
+
"""
|
|
98
|
+
Entry in `assets.images`.
|
|
99
|
+
`assets.images` 中的图片资源条目。
|
|
100
|
+
|
|
101
|
+
Notes / 说明:
|
|
102
|
+
- `src` is the actual image location.
|
|
103
|
+
`src` 是图片资源实际位置。
|
|
104
|
+
- `alt` is not the caption.
|
|
105
|
+
`alt` 不是图片题注。
|
|
106
|
+
- caption should still prefer adjacent `figure_caption` text blocks.
|
|
107
|
+
图片题注仍然优先由相邻 `figure_caption` 文本块承载。
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
alt: str | None = None
|
|
111
|
+
width: int | None = None
|
|
112
|
+
height: int | None = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class LogoAsset(StaticAssetBase):
|
|
116
|
+
"""
|
|
117
|
+
Entry in `assets.logos`.
|
|
118
|
+
`assets.logos` 中的品牌 logo 条目。
|
|
119
|
+
|
|
120
|
+
The protocol only fixes the existence of the bucket in v1.
|
|
121
|
+
协议在 v1 中主要冻结了这个 bucket 的存在,而不是全部细字段。
|
|
122
|
+
So we keep the model deliberately small.
|
|
123
|
+
因此这里刻意保持字段较少。
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
alt: str | None = None
|
|
127
|
+
width: int | None = None
|
|
128
|
+
height: int | None = None
|
|
129
|
+
variant: str | None = None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class BackgroundAsset(StaticAssetBase):
|
|
133
|
+
"""
|
|
134
|
+
Entry in `assets.backgrounds`.
|
|
135
|
+
`assets.backgrounds` 中的背景资源条目。
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
width: int | None = None
|
|
139
|
+
height: int | None = None
|
|
140
|
+
usage: str | None = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class IconAsset(StaticAssetBase):
|
|
144
|
+
"""
|
|
145
|
+
Entry in `assets.icons`.
|
|
146
|
+
`assets.icons` 中的图标资源条目。
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
alt: str | None = None
|
|
150
|
+
family: str | None = None
|
|
151
|
+
size: int | None = None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class AttachmentAsset(StaticAssetBase):
|
|
155
|
+
"""
|
|
156
|
+
Entry in `assets.attachments`.
|
|
157
|
+
`assets.attachments` 中的附件资源条目。
|
|
158
|
+
|
|
159
|
+
Attachments are not part of the main body flow,
|
|
160
|
+
but they may still need stable IDs and anchors for future linking.
|
|
161
|
+
附件不属于正文主内容流,
|
|
162
|
+
但未来仍可能需要稳定 ID 与 anchor 来承接链接或下载入口。
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
fileName: str | None = None
|
|
166
|
+
description: str | None = None
|
|
167
|
+
sizeBytes: int | None = None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TableColumn(OvaBaseModel):
|
|
171
|
+
"""
|
|
172
|
+
Column definition for `datasets.tables[].columns[]`.
|
|
173
|
+
`datasets.tables[].columns[]` 的列定义对象。
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
key: str
|
|
177
|
+
header: str
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class TableDataset(RegistryEntryBase):
|
|
181
|
+
"""
|
|
182
|
+
Entry in `datasets.tables`.
|
|
183
|
+
`datasets.tables` 中的表格数据条目。
|
|
184
|
+
|
|
185
|
+
Structure / 结构:
|
|
186
|
+
- `columns[]` defines order and headers
|
|
187
|
+
`columns[]` 定义列顺序和列标题
|
|
188
|
+
- `rows[]` is an object-array, each row keyed by `columns[].key`
|
|
189
|
+
`rows[]` 是对象数组,每行使用 `columns[].key` 作为键
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
columns: list[TableColumn] = Field(default_factory=list)
|
|
193
|
+
rows: list[dict[str, str | int | float | bool | None]] = Field(default_factory=list)
|
|
194
|
+
columnGroups: list[dict[str, Any]] | None = None
|
|
195
|
+
footerRows: list[dict[str, Any]] | None = None
|
|
196
|
+
notes: list[str] | None = None
|
|
197
|
+
defaultAlign: str | None = None
|
|
198
|
+
cellFormatRules: list[dict[str, Any]] | None = None
|
|
199
|
+
rowOrder: list[str] | None = None
|
|
200
|
+
source: str | None = None
|
|
201
|
+
|
|
202
|
+
@field_validator("columns")
|
|
203
|
+
@classmethod
|
|
204
|
+
def validate_columns(cls, value: list[TableColumn]) -> list[TableColumn]:
|
|
205
|
+
"""
|
|
206
|
+
Require unique `columns[].key` values.
|
|
207
|
+
要求 `columns[].key` 在同一张表内唯一。
|
|
208
|
+
"""
|
|
209
|
+
seen: set[str] = set()
|
|
210
|
+
for col in value:
|
|
211
|
+
if col.key in seen:
|
|
212
|
+
raise ValueError(f"Duplicate table column key: {col.key}")
|
|
213
|
+
seen.add(col.key)
|
|
214
|
+
return value
|
|
215
|
+
|
|
216
|
+
@model_validator(mode="after")
|
|
217
|
+
def validate_rows_against_columns(self) -> "TableDataset":
|
|
218
|
+
"""
|
|
219
|
+
Ensure all row keys can be found in `columns[].key`.
|
|
220
|
+
确保每一行中出现的键都能在 `columns[].key` 中找到。
|
|
221
|
+
"""
|
|
222
|
+
allowed_keys = {col.key for col in self.columns}
|
|
223
|
+
for row_index, row in enumerate(self.rows):
|
|
224
|
+
extra_keys = [key for key in row.keys() if key not in allowed_keys]
|
|
225
|
+
if extra_keys:
|
|
226
|
+
raise ValueError(
|
|
227
|
+
f"Row {row_index} contains keys not declared in columns: {extra_keys}"
|
|
228
|
+
)
|
|
229
|
+
return self
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class PieSlice(OvaBaseModel):
|
|
233
|
+
"""
|
|
234
|
+
One slice in a pie chart dataset.
|
|
235
|
+
pie chart 数据条目中的一个扇区对象。
|
|
236
|
+
|
|
237
|
+
The protocol recommends object-array slices rather than parallel arrays.
|
|
238
|
+
协议推荐使用对象数组 slices,而不是并行数组。
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
key: str
|
|
242
|
+
label: dict[str, str] = Field(default_factory=dict)
|
|
243
|
+
value: int | float
|
|
244
|
+
description: dict[str, str] = Field(default_factory=dict)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class PieChartDataset(RegistryEntryBase):
|
|
248
|
+
"""
|
|
249
|
+
Pie chart dataset entry in `datasets.charts`.
|
|
250
|
+
`datasets.charts` 中的 pie chart 数据条目。
|
|
251
|
+
|
|
252
|
+
Current boundary / 当前边界:
|
|
253
|
+
only `chartType="pie"` is formally implemented right now.
|
|
254
|
+
当前仅正式实现 `chartType="pie"`。
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
chartType: Literal["pie"] = "pie"
|
|
258
|
+
valueUnit: str | None = None
|
|
259
|
+
slices: list[PieSlice] = Field(default_factory=list)
|
|
260
|
+
|
|
261
|
+
@field_validator("slices")
|
|
262
|
+
@classmethod
|
|
263
|
+
def validate_unique_slice_keys(cls, value: list[PieSlice]) -> list[PieSlice]:
|
|
264
|
+
"""
|
|
265
|
+
Require unique slice keys within the chart.
|
|
266
|
+
要求同一张饼图内的扇区 key 唯一。
|
|
267
|
+
"""
|
|
268
|
+
seen: set[str] = set()
|
|
269
|
+
for item in value:
|
|
270
|
+
if item.key in seen:
|
|
271
|
+
raise ValueError(f"Duplicate pie slice key: {item.key}")
|
|
272
|
+
seen.add(item.key)
|
|
273
|
+
return value
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def from_parallel_arrays(
|
|
277
|
+
cls,
|
|
278
|
+
*,
|
|
279
|
+
id: str,
|
|
280
|
+
area_en: list[str],
|
|
281
|
+
area_zh: list[str] | None,
|
|
282
|
+
value: list[int | float],
|
|
283
|
+
description_en: list[str] | None = None,
|
|
284
|
+
description_zh: list[str] | None = None,
|
|
285
|
+
label: str | None = None,
|
|
286
|
+
anchor: str | None = None,
|
|
287
|
+
meta: dict[str, Any] | None = None,
|
|
288
|
+
valueUnit: str | None = None,
|
|
289
|
+
sort_desc: bool = True,
|
|
290
|
+
) -> "PieChartDataset":
|
|
291
|
+
"""
|
|
292
|
+
Compatibility helper for older parallel-array pie-chart input.
|
|
293
|
+
兼容旧的并行数组饼图输入风格。
|
|
294
|
+
|
|
295
|
+
The formal protocol prefers normalized `slices[]` objects.
|
|
296
|
+
正式协议更推荐归一化后的 `slices[]` 对象数组。
|
|
297
|
+
This helper accepts an ergonomic legacy input shape and converts it.
|
|
298
|
+
这个 helper 接收更顺手的旧输入形态,并自动转换。
|
|
299
|
+
"""
|
|
300
|
+
area_zh = area_zh or [""] * len(area_en)
|
|
301
|
+
description_en = description_en or [""] * len(area_en)
|
|
302
|
+
description_zh = description_zh or [""] * len(area_en)
|
|
303
|
+
|
|
304
|
+
lengths = {len(area_en), len(area_zh), len(value), len(description_en), len(description_zh)}
|
|
305
|
+
if len(lengths) != 1:
|
|
306
|
+
raise ValueError("All pie-chart parallel arrays must have the same length")
|
|
307
|
+
|
|
308
|
+
rows = []
|
|
309
|
+
for idx, (en, zh, val, den, dzh) in enumerate(
|
|
310
|
+
zip(area_en, area_zh, value, description_en, description_zh, strict=True)
|
|
311
|
+
):
|
|
312
|
+
key = cls._slugify_key(en or zh or f"slice-{idx + 1}")
|
|
313
|
+
item = PieSlice(
|
|
314
|
+
key=key,
|
|
315
|
+
label={k: v for k, v in {"en": en, "zh": zh}.items() if v},
|
|
316
|
+
value=val,
|
|
317
|
+
description={k: v for k, v in {"en": den, "zh": dzh}.items() if v},
|
|
318
|
+
)
|
|
319
|
+
rows.append(item)
|
|
320
|
+
|
|
321
|
+
if sort_desc:
|
|
322
|
+
rows.sort(key=lambda item: item.value, reverse=True)
|
|
323
|
+
|
|
324
|
+
return cls(
|
|
325
|
+
id=id,
|
|
326
|
+
anchor=anchor,
|
|
327
|
+
label=label,
|
|
328
|
+
meta=meta or {},
|
|
329
|
+
chartType="pie",
|
|
330
|
+
valueUnit=valueUnit,
|
|
331
|
+
slices=rows,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _slugify_key(text: str) -> str:
|
|
336
|
+
"""
|
|
337
|
+
Create a stable-ish key from human text.
|
|
338
|
+
根据可读文本生成相对稳定的 key。
|
|
339
|
+
"""
|
|
340
|
+
text = text.strip().lower()
|
|
341
|
+
output = []
|
|
342
|
+
prev_dash = False
|
|
343
|
+
for ch in text:
|
|
344
|
+
if ch.isalnum():
|
|
345
|
+
output.append(ch)
|
|
346
|
+
prev_dash = False
|
|
347
|
+
else:
|
|
348
|
+
if not prev_dash:
|
|
349
|
+
output.append("-")
|
|
350
|
+
prev_dash = True
|
|
351
|
+
result = "".join(output).strip("-")
|
|
352
|
+
return result or "slice"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class MetricValue(OvaBaseModel):
|
|
356
|
+
"""
|
|
357
|
+
One metric item in a metric dataset.
|
|
358
|
+
metric dataset 中的单个指标对象。
|
|
359
|
+
|
|
360
|
+
The protocol currently only fixes the existence of `datasets.metrics`.
|
|
361
|
+
协议当前主要冻结了 `datasets.metrics` 的存在,而不是完整细字段。
|
|
362
|
+
So this model intentionally stays small but typed.
|
|
363
|
+
因此这个模型刻意保持小而强类型。
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
key: str
|
|
367
|
+
label: str | None = None
|
|
368
|
+
value: str | int | float | bool | None = None
|
|
369
|
+
unit: str | None = None
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
class MetricDataset(RegistryEntryBase):
|
|
373
|
+
"""
|
|
374
|
+
Entry in `datasets.metrics`.
|
|
375
|
+
`datasets.metrics` 中的指标数据条目。
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
values: list[MetricValue] = Field(default_factory=list)
|
|
379
|
+
source: str | None = None
|
|
380
|
+
|
|
381
|
+
@field_validator("values")
|
|
382
|
+
@classmethod
|
|
383
|
+
def validate_unique_metric_keys(cls, value: list[MetricValue]) -> list[MetricValue]:
|
|
384
|
+
"""
|
|
385
|
+
Require unique metric keys within one metric dataset.
|
|
386
|
+
要求同一个 metric dataset 内部 key 唯一。
|
|
387
|
+
"""
|
|
388
|
+
seen: set[str] = set()
|
|
389
|
+
for item in value:
|
|
390
|
+
if item.key in seen:
|
|
391
|
+
raise ValueError(f"Duplicate metric key: {item.key}")
|
|
392
|
+
seen.add(item.key)
|
|
393
|
+
return value
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class BibliographyEntry(RegistryEntryBase):
|
|
397
|
+
"""
|
|
398
|
+
Bibliography entry.
|
|
399
|
+
参考文献条目。
|
|
400
|
+
|
|
401
|
+
Protocol alignment / 与协议对齐:
|
|
402
|
+
the protocol's minimum semantic shape is roughly:
|
|
403
|
+
协议推荐的最小语义结构大致为:
|
|
404
|
+
- `type`
|
|
405
|
+
- `title`
|
|
406
|
+
- `authors`
|
|
407
|
+
- `year`
|
|
408
|
+
|
|
409
|
+
Backward-compatible note / 向后兼容说明:
|
|
410
|
+
older internal examples sometimes used a single `text` field.
|
|
411
|
+
较早的内部示例有时只使用一个 `text` 字段。
|
|
412
|
+
We still accept that optional field as a convenience fallback.
|
|
413
|
+
这里仍保留这个可选字段作为兼容性回退。
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
417
|
+
|
|
418
|
+
type: str = "misc"
|
|
419
|
+
title: str
|
|
420
|
+
authors: list[str] = Field(default_factory=list)
|
|
421
|
+
year: int | None = None
|
|
422
|
+
text: str | None = None
|
|
423
|
+
journal: str | None = None
|
|
424
|
+
publisher: str | None = None
|
|
425
|
+
volume: str | None = None
|
|
426
|
+
issue: str | None = None
|
|
427
|
+
pages: str | None = None
|
|
428
|
+
doi: str | None = None
|
|
429
|
+
url: str | None = None
|
|
430
|
+
accessedAt: str | None = None
|
|
431
|
+
edition: str | None = None
|
|
432
|
+
institution: str | None = None
|
|
433
|
+
language: str | None = None
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class FootnoteEntry(RegistryEntryBase):
|
|
437
|
+
"""
|
|
438
|
+
Footnote entry.
|
|
439
|
+
脚注条目。
|
|
440
|
+
|
|
441
|
+
The protocol allows `footnotes[].blocks` to reuse the text-layer rules.
|
|
442
|
+
协议允许 `footnotes[].blocks` 复用文本层规则。
|
|
443
|
+
"""
|
|
444
|
+
|
|
445
|
+
blocks: list[TextBlock] = Field(default_factory=list)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
class GlossaryEntry(RegistryEntryBase):
|
|
449
|
+
"""
|
|
450
|
+
Glossary / term entry.
|
|
451
|
+
术语表条目。
|
|
452
|
+
|
|
453
|
+
Minimum structure / 最小结构:
|
|
454
|
+
- `term`
|
|
455
|
+
- `definition`
|
|
456
|
+
- optional `aliases`
|
|
457
|
+
可选 `aliases`
|
|
458
|
+
|
|
459
|
+
Backward-compatible note / 向后兼容说明:
|
|
460
|
+
since some earlier local examples occasionally use `short`,
|
|
461
|
+
较早的本地示例偶尔会用 `short`,
|
|
462
|
+
we keep it as an optional compatibility field.
|
|
463
|
+
因此这里保留为可选兼容字段。
|
|
464
|
+
"""
|
|
465
|
+
|
|
466
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
467
|
+
|
|
468
|
+
term: str
|
|
469
|
+
definition: str
|
|
470
|
+
aliases: list[str] | None = None
|
|
471
|
+
short: str | None = None
|
|
472
|
+
|
|
473
|
+
@field_validator("aliases")
|
|
474
|
+
@classmethod
|
|
475
|
+
def validate_aliases(cls, value: list[str] | None) -> list[str] | None:
|
|
476
|
+
"""
|
|
477
|
+
If aliases exist, they must be a list.
|
|
478
|
+
如果 aliases 存在,则必须是一个列表。
|
|
479
|
+
"""
|
|
480
|
+
return value
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
class AssetsRegistry(OvaBaseModel):
|
|
484
|
+
"""
|
|
485
|
+
Top-level assets registry.
|
|
486
|
+
顶层 assets registry。
|
|
487
|
+
|
|
488
|
+
Protocol alignment / 与协议对齐:
|
|
489
|
+
the v1 protocol already freezes these buckets:
|
|
490
|
+
v1 协议已经冻结了这些 bucket:
|
|
491
|
+
- images
|
|
492
|
+
- logos
|
|
493
|
+
- backgrounds
|
|
494
|
+
- icons
|
|
495
|
+
- attachments
|
|
496
|
+
|
|
497
|
+
This implementation now gives all of them lightweight typed models.
|
|
498
|
+
这一版实现已经为它们全部提供了轻量强类型模型。
|
|
499
|
+
"""
|
|
500
|
+
|
|
501
|
+
images: list[ImageAsset] = Field(default_factory=list)
|
|
502
|
+
logos: list[LogoAsset] = Field(default_factory=list)
|
|
503
|
+
backgrounds: list[BackgroundAsset] = Field(default_factory=list)
|
|
504
|
+
icons: list[IconAsset] = Field(default_factory=list)
|
|
505
|
+
attachments: list[AttachmentAsset] = Field(default_factory=list)
|
|
506
|
+
|
|
507
|
+
def append_image(self, asset: ImageAsset) -> "AssetsRegistry":
|
|
508
|
+
self.images.append(asset)
|
|
509
|
+
return self
|
|
510
|
+
|
|
511
|
+
def append_logo(self, asset: LogoAsset) -> "AssetsRegistry":
|
|
512
|
+
self.logos.append(asset)
|
|
513
|
+
return self
|
|
514
|
+
|
|
515
|
+
def append_background(self, asset: BackgroundAsset) -> "AssetsRegistry":
|
|
516
|
+
self.backgrounds.append(asset)
|
|
517
|
+
return self
|
|
518
|
+
|
|
519
|
+
def append_icon(self, asset: IconAsset) -> "AssetsRegistry":
|
|
520
|
+
self.icons.append(asset)
|
|
521
|
+
return self
|
|
522
|
+
|
|
523
|
+
def append_attachment(self, asset: AttachmentAsset) -> "AssetsRegistry":
|
|
524
|
+
self.attachments.append(asset)
|
|
525
|
+
return self
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class DatasetsRegistry(OvaBaseModel):
|
|
529
|
+
"""
|
|
530
|
+
Top-level datasets registry.
|
|
531
|
+
顶层 datasets registry。
|
|
532
|
+
|
|
533
|
+
Protocol alignment / 与协议对齐:
|
|
534
|
+
the v1 protocol already freezes these buckets:
|
|
535
|
+
v1 协议已经冻结了这些 bucket:
|
|
536
|
+
- charts
|
|
537
|
+
- tables
|
|
538
|
+
- metrics
|
|
539
|
+
"""
|
|
540
|
+
|
|
541
|
+
charts: list[PieChartDataset] = Field(default_factory=list)
|
|
542
|
+
tables: list[TableDataset] = Field(default_factory=list)
|
|
543
|
+
metrics: list[MetricDataset] = Field(default_factory=list)
|
|
544
|
+
|
|
545
|
+
def append_chart(self, chart: PieChartDataset) -> "DatasetsRegistry":
|
|
546
|
+
self.charts.append(chart)
|
|
547
|
+
return self
|
|
548
|
+
|
|
549
|
+
def append_table(self, table: TableDataset) -> "DatasetsRegistry":
|
|
550
|
+
self.tables.append(table)
|
|
551
|
+
return self
|
|
552
|
+
|
|
553
|
+
def append_metric(self, metric: MetricDataset) -> "DatasetsRegistry":
|
|
554
|
+
self.metrics.append(metric)
|
|
555
|
+
return self
|