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,303 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Top-level document models for OVAPortableText.
|
|
5
|
+
OVAPortableText 的顶层文档模型。
|
|
6
|
+
|
|
7
|
+
This module intentionally keeps the document object as the primary user entry.
|
|
8
|
+
本模块刻意让 document 对象成为用户的主入口。
|
|
9
|
+
|
|
10
|
+
Why design it this way?
|
|
11
|
+
为什么这样设计?
|
|
12
|
+
Because the target usage style of this package is close to common document libraries:
|
|
13
|
+
因为本包希望尽量接近常见文档库的使用方式:
|
|
14
|
+
- create one top-level document object / 先创建一个顶层 document 对象
|
|
15
|
+
- keep appending sections and resources / 再不断 append section 与资源
|
|
16
|
+
- finally validate, resolve, number, and export / 最后统一校验、解析、编号、导出
|
|
17
|
+
|
|
18
|
+
This file therefore contains:
|
|
19
|
+
因此本文件同时承载:
|
|
20
|
+
1. top-level document state / 顶层文档状态
|
|
21
|
+
2. typed-but-extensible metadata / 强类型但可扩展的元信息模型
|
|
22
|
+
3. convenience methods for append-style authoring / 便于 append 风格写作的快捷方法
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from pydantic import ConfigDict, Field
|
|
28
|
+
|
|
29
|
+
from .base import OvaBaseModel
|
|
30
|
+
from .numbering import DocumentNumbering, NumberingConfig
|
|
31
|
+
from .registry import (
|
|
32
|
+
AssetsRegistry,
|
|
33
|
+
AttachmentAsset,
|
|
34
|
+
BackgroundAsset,
|
|
35
|
+
BibliographyEntry,
|
|
36
|
+
DatasetsRegistry,
|
|
37
|
+
FootnoteEntry,
|
|
38
|
+
GlossaryEntry,
|
|
39
|
+
IconAsset,
|
|
40
|
+
ImageAsset,
|
|
41
|
+
LogoAsset,
|
|
42
|
+
MetricDataset,
|
|
43
|
+
PieChartDataset,
|
|
44
|
+
TableDataset,
|
|
45
|
+
)
|
|
46
|
+
from .section import Section
|
|
47
|
+
from .theme import ThemeConfig
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class DocumentMeta(OvaBaseModel):
|
|
51
|
+
"""
|
|
52
|
+
Top-level document metadata.
|
|
53
|
+
文档顶层元数据。
|
|
54
|
+
|
|
55
|
+
Why keep this model "typed but extensible"?
|
|
56
|
+
为什么把它设计成“强类型但可扩展”?
|
|
57
|
+
Because the protocol explicitly lists common metadata directions,
|
|
58
|
+
but also states that the full detailed field table is not frozen in v1.0.
|
|
59
|
+
因为协议已经明确列出了常见元信息方向,
|
|
60
|
+
但同时也说明 v1.0 尚未冻结完整字段表。
|
|
61
|
+
|
|
62
|
+
Therefore / 因此:
|
|
63
|
+
- common fields get typed access / 常见字段提供强类型访问
|
|
64
|
+
- unknown extra fields are still allowed / 仍允许附加未知扩展字段
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
model_config = ConfigDict(populate_by_name=True, extra="allow")
|
|
68
|
+
|
|
69
|
+
title: str | None = None
|
|
70
|
+
subtitle: str | None = None
|
|
71
|
+
language: str | None = None
|
|
72
|
+
author: str | None = None
|
|
73
|
+
date: str | None = None
|
|
74
|
+
reportNumber: str | None = None
|
|
75
|
+
documentType: str | None = None
|
|
76
|
+
confidentiality: str | None = None
|
|
77
|
+
generatedBy: str | None = None
|
|
78
|
+
generatedAt: str | None = None
|
|
79
|
+
clientId: str | None = None
|
|
80
|
+
projectId: str | None = None
|
|
81
|
+
|
|
82
|
+
# Protocol-mentioned likely extension directions.
|
|
83
|
+
# 协议中明确提到、后续大概率会继续使用的扩展方向。
|
|
84
|
+
reportType: str | None = None
|
|
85
|
+
clientName: str | None = None
|
|
86
|
+
locale: str | None = None
|
|
87
|
+
source: str | None = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Document(OvaBaseModel):
|
|
91
|
+
"""
|
|
92
|
+
Top-level report document.
|
|
93
|
+
顶层报告文档对象。
|
|
94
|
+
|
|
95
|
+
Important protocol alignment / 关键协议对齐点:
|
|
96
|
+
- `schemaVersion` defaults to `report.v1`
|
|
97
|
+
`schemaVersion` 默认值为 `report.v1`
|
|
98
|
+
- `theme` is preserved as a placeholder, but now has a lightweight typed model
|
|
99
|
+
`theme` 仍然是占位层,但现在有一个轻量强类型模型
|
|
100
|
+
- top-level registries should always exist, even when empty
|
|
101
|
+
顶层 registry 即使为空也应存在
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
schemaVersion: str = "report.v1"
|
|
105
|
+
meta: DocumentMeta = Field(default_factory=DocumentMeta)
|
|
106
|
+
theme: ThemeConfig = Field(default_factory=ThemeConfig)
|
|
107
|
+
assets: AssetsRegistry = Field(default_factory=AssetsRegistry)
|
|
108
|
+
datasets: DatasetsRegistry = Field(default_factory=DatasetsRegistry)
|
|
109
|
+
bibliography: list[BibliographyEntry] = Field(default_factory=list)
|
|
110
|
+
footnotes: list[FootnoteEntry] = Field(default_factory=list)
|
|
111
|
+
glossary: list[GlossaryEntry] = Field(default_factory=list)
|
|
112
|
+
sections: list[Section] = Field(default_factory=list)
|
|
113
|
+
|
|
114
|
+
def append_section(self, section: Section) -> "Document":
|
|
115
|
+
"""
|
|
116
|
+
Append one top-level section.
|
|
117
|
+
追加一个顶层 section。
|
|
118
|
+
"""
|
|
119
|
+
self.sections.append(section)
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
def append_sections(self, *sections: Section) -> "Document":
|
|
123
|
+
"""
|
|
124
|
+
Append multiple top-level sections in one call.
|
|
125
|
+
一次追加多个顶层 section。
|
|
126
|
+
"""
|
|
127
|
+
self.sections.extend(sections)
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
def new_section(
|
|
131
|
+
self,
|
|
132
|
+
*,
|
|
133
|
+
id: str,
|
|
134
|
+
level: int,
|
|
135
|
+
title: str,
|
|
136
|
+
numbering: str = "auto",
|
|
137
|
+
anchor: str | None = None,
|
|
138
|
+
append: bool = True,
|
|
139
|
+
) -> Section:
|
|
140
|
+
"""
|
|
141
|
+
Create a new top-level section and optionally append it immediately.
|
|
142
|
+
创建一个新的顶层 section,并可选择立刻 append 到 document。
|
|
143
|
+
|
|
144
|
+
Why return the section object?
|
|
145
|
+
为什么返回 section 对象?
|
|
146
|
+
So callers can immediately continue writing content like:
|
|
147
|
+
这样调用者可以立刻继续写内容,例如:
|
|
148
|
+
|
|
149
|
+
sec = doc.new_section(...)
|
|
150
|
+
sec.append_paragraph(...)
|
|
151
|
+
"""
|
|
152
|
+
section = Section(id=id, level=level, title=title, numbering=numbering, anchor=anchor)
|
|
153
|
+
if append:
|
|
154
|
+
self.append_section(section)
|
|
155
|
+
return section
|
|
156
|
+
|
|
157
|
+
# ------------------------------------------------------------------
|
|
158
|
+
# Asset registry helpers / 资源 registry helper
|
|
159
|
+
# ------------------------------------------------------------------
|
|
160
|
+
def add_image_asset(self, asset: ImageAsset) -> "Document":
|
|
161
|
+
"""
|
|
162
|
+
Append an `assets.images` entry.
|
|
163
|
+
追加一个 `assets.images` 条目。
|
|
164
|
+
"""
|
|
165
|
+
self.assets.append_image(asset)
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
def add_logo_asset(self, asset: LogoAsset) -> "Document":
|
|
169
|
+
"""
|
|
170
|
+
Append an `assets.logos` entry.
|
|
171
|
+
追加一个 `assets.logos` 条目。
|
|
172
|
+
"""
|
|
173
|
+
self.assets.append_logo(asset)
|
|
174
|
+
return self
|
|
175
|
+
|
|
176
|
+
def add_background_asset(self, asset: BackgroundAsset) -> "Document":
|
|
177
|
+
"""
|
|
178
|
+
Append an `assets.backgrounds` entry.
|
|
179
|
+
追加一个 `assets.backgrounds` 条目。
|
|
180
|
+
"""
|
|
181
|
+
self.assets.append_background(asset)
|
|
182
|
+
return self
|
|
183
|
+
|
|
184
|
+
def add_icon_asset(self, asset: IconAsset) -> "Document":
|
|
185
|
+
"""
|
|
186
|
+
Append an `assets.icons` entry.
|
|
187
|
+
追加一个 `assets.icons` 条目。
|
|
188
|
+
"""
|
|
189
|
+
self.assets.append_icon(asset)
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
def add_attachment_asset(self, asset: AttachmentAsset) -> "Document":
|
|
193
|
+
"""
|
|
194
|
+
Append an `assets.attachments` entry.
|
|
195
|
+
追加一个 `assets.attachments` 条目。
|
|
196
|
+
"""
|
|
197
|
+
self.assets.append_attachment(asset)
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
# ------------------------------------------------------------------
|
|
201
|
+
# Dataset registry helpers / 数据 registry helper
|
|
202
|
+
# ------------------------------------------------------------------
|
|
203
|
+
def add_table_dataset(self, table: TableDataset) -> "Document":
|
|
204
|
+
"""
|
|
205
|
+
Append a `datasets.tables` entry.
|
|
206
|
+
追加一个 `datasets.tables` 条目。
|
|
207
|
+
"""
|
|
208
|
+
self.datasets.append_table(table)
|
|
209
|
+
return self
|
|
210
|
+
|
|
211
|
+
def add_chart_dataset(self, chart: PieChartDataset) -> "Document":
|
|
212
|
+
"""
|
|
213
|
+
Append a `datasets.charts` entry.
|
|
214
|
+
追加一个 `datasets.charts` 条目。
|
|
215
|
+
"""
|
|
216
|
+
self.datasets.append_chart(chart)
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
def add_metric_dataset(self, metric: MetricDataset) -> "Document":
|
|
220
|
+
"""
|
|
221
|
+
Append a `datasets.metrics` entry.
|
|
222
|
+
追加一个 `datasets.metrics` 条目。
|
|
223
|
+
"""
|
|
224
|
+
self.datasets.append_metric(metric)
|
|
225
|
+
return self
|
|
226
|
+
|
|
227
|
+
# ------------------------------------------------------------------
|
|
228
|
+
# Academic / auxiliary registry helpers / 学术与辅助 registry helper
|
|
229
|
+
# ------------------------------------------------------------------
|
|
230
|
+
def add_bibliography_entry(self, entry: BibliographyEntry) -> "Document":
|
|
231
|
+
"""
|
|
232
|
+
Append one bibliography entry.
|
|
233
|
+
追加一个 bibliography 条目。
|
|
234
|
+
"""
|
|
235
|
+
self.bibliography.append(entry)
|
|
236
|
+
return self
|
|
237
|
+
|
|
238
|
+
def add_footnote(self, entry: FootnoteEntry) -> "Document":
|
|
239
|
+
"""
|
|
240
|
+
Append one footnote entry.
|
|
241
|
+
追加一个 footnote 条目。
|
|
242
|
+
"""
|
|
243
|
+
self.footnotes.append(entry)
|
|
244
|
+
return self
|
|
245
|
+
|
|
246
|
+
def add_glossary_entry(self, entry: GlossaryEntry) -> "Document":
|
|
247
|
+
"""
|
|
248
|
+
Append one glossary entry.
|
|
249
|
+
追加一个 glossary 条目。
|
|
250
|
+
"""
|
|
251
|
+
self.glossary.append(entry)
|
|
252
|
+
return self
|
|
253
|
+
|
|
254
|
+
def build_resolver(self):
|
|
255
|
+
"""
|
|
256
|
+
Build a global resolver / index for the current document.
|
|
257
|
+
为当前文档构建全局 resolver / 索引器。
|
|
258
|
+
"""
|
|
259
|
+
from .resolver import DocumentResolver
|
|
260
|
+
return DocumentResolver.from_document(self)
|
|
261
|
+
|
|
262
|
+
def build_numbering(self, config: NumberingConfig | None = None) -> DocumentNumbering:
|
|
263
|
+
"""
|
|
264
|
+
Build logical numbering hints for sections / figures / tables / equations.
|
|
265
|
+
为 sections / figures / tables / equations 构建逻辑编号辅助。
|
|
266
|
+
|
|
267
|
+
Important boundary / 重要边界:
|
|
268
|
+
this method only computes logical display-number hints.
|
|
269
|
+
这个方法只计算逻辑层 display number 辅助。
|
|
270
|
+
It does NOT decide final renderer wording such as "Figure 3" vs "Fig. 3".
|
|
271
|
+
它不会决定最终渲染层文本,例如到底显示为 “Figure 3” 还是 “Fig. 3”。
|
|
272
|
+
"""
|
|
273
|
+
return DocumentNumbering.from_document(self, config=config)
|
|
274
|
+
|
|
275
|
+
def validate(self):
|
|
276
|
+
"""
|
|
277
|
+
Validate the whole document and return a structured report.
|
|
278
|
+
校验整份文档并返回结构化报告。
|
|
279
|
+
"""
|
|
280
|
+
from .validator import validate_document
|
|
281
|
+
return validate_document(self)
|
|
282
|
+
|
|
283
|
+
def assert_valid(self) -> "Document":
|
|
284
|
+
"""
|
|
285
|
+
Validate the document and raise if invalid.
|
|
286
|
+
校验文档;若无效则抛错。
|
|
287
|
+
"""
|
|
288
|
+
from .validator import assert_valid_document
|
|
289
|
+
return assert_valid_document(self)
|
|
290
|
+
|
|
291
|
+
@classmethod
|
|
292
|
+
def from_meta(
|
|
293
|
+
cls,
|
|
294
|
+
*,
|
|
295
|
+
theme: ThemeConfig | dict[str, Any] | None = None,
|
|
296
|
+
**meta_fields: Any,
|
|
297
|
+
) -> "Document":
|
|
298
|
+
"""
|
|
299
|
+
Alternate constructor that builds a document from metadata fields directly.
|
|
300
|
+
一个备用构造器:直接从 metadata 字段创建 document。
|
|
301
|
+
"""
|
|
302
|
+
theme_value = theme if isinstance(theme, ThemeConfig) else ThemeConfig(**(theme or {}))
|
|
303
|
+
return cls(meta=DocumentMeta(**meta_fields), theme=theme_value)
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Validation-related exception and report models for OVAPortableText.
|
|
5
|
+
OVAPortableText 的校验异常与报告模型。
|
|
6
|
+
|
|
7
|
+
This file is intentionally part of the public API.
|
|
8
|
+
本文件刻意作为公开 API 的一部分。
|
|
9
|
+
|
|
10
|
+
Why?
|
|
11
|
+
为什么?
|
|
12
|
+
Because validation is not only an internal implementation detail.
|
|
13
|
+
因为校验并不只是内部实现细节。
|
|
14
|
+
For this package, validation is one of the core user-facing workflows:
|
|
15
|
+
对本包而言,校验本身就是核心对外工作流之一:
|
|
16
|
+
- build a document / 构建文档
|
|
17
|
+
- inspect a structured validation report / 查看结构化校验报告
|
|
18
|
+
- optionally fail fast / 必要时快速抛错中止
|
|
19
|
+
|
|
20
|
+
Step 8 extends the report so issues carry more maintenance-friendly context,
|
|
21
|
+
which is especially useful when the user later debugs large report documents.
|
|
22
|
+
第 8 步进一步扩展了报告结构,让 issue 带有更多便于维护的上下文;
|
|
23
|
+
当后续调试大型报告文档时,这会非常有帮助。
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from collections import Counter
|
|
27
|
+
from typing import Literal
|
|
28
|
+
|
|
29
|
+
from pydantic import Field
|
|
30
|
+
|
|
31
|
+
from .base import OvaBaseModel
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ValidationIssue(OvaBaseModel):
|
|
35
|
+
"""
|
|
36
|
+
One validation issue found in the document.
|
|
37
|
+
文档中发现的一条校验问题。
|
|
38
|
+
|
|
39
|
+
Besides the basic code / message / path fields,
|
|
40
|
+
Step 8 also carries optional context that helps users quickly locate the
|
|
41
|
+
problematic semantic object.
|
|
42
|
+
除了基础的 code / message / path 字段外,
|
|
43
|
+
第 8 步还补充了若干可选上下文字段,便于用户快速定位出问题的语义对象。
|
|
44
|
+
|
|
45
|
+
Common examples / 常见示例:
|
|
46
|
+
- which section the issue belongs to / 属于哪个 section
|
|
47
|
+
- which object id or anchor is involved / 涉及哪个对象 id 或 anchor
|
|
48
|
+
- what kind of object it is / 它属于哪种对象类型
|
|
49
|
+
- what to try next / 建议下一步怎么修
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
code: str
|
|
53
|
+
message: str
|
|
54
|
+
path: str | None = None
|
|
55
|
+
severity: Literal["error", "warning"] = "error"
|
|
56
|
+
|
|
57
|
+
# Maintenance-oriented context fields.
|
|
58
|
+
# 面向维护与调试的上下文字段。
|
|
59
|
+
location: str | None = None
|
|
60
|
+
contextType: str | None = None
|
|
61
|
+
contextId: str | None = None
|
|
62
|
+
contextAnchor: str | None = None
|
|
63
|
+
sectionId: str | None = None
|
|
64
|
+
sectionTitle: str | None = None
|
|
65
|
+
suggestion: str | None = None
|
|
66
|
+
|
|
67
|
+
def to_text(self) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Render one issue as a compact human-readable line.
|
|
70
|
+
将单条 issue 渲染为便于人工阅读的简洁文本。
|
|
71
|
+
"""
|
|
72
|
+
pieces: list[str] = [f"[{self.severity.upper()}] [{self.code}] {self.message}"]
|
|
73
|
+
if self.path:
|
|
74
|
+
pieces.append(f"path={self.path}")
|
|
75
|
+
if self.sectionId:
|
|
76
|
+
section_text = self.sectionId
|
|
77
|
+
if self.sectionTitle:
|
|
78
|
+
section_text += f" ({self.sectionTitle})"
|
|
79
|
+
pieces.append(f"section={section_text}")
|
|
80
|
+
if self.contextType:
|
|
81
|
+
pieces.append(f"contextType={self.contextType}")
|
|
82
|
+
if self.contextId:
|
|
83
|
+
pieces.append(f"contextId={self.contextId}")
|
|
84
|
+
if self.contextAnchor:
|
|
85
|
+
pieces.append(f"contextAnchor={self.contextAnchor}")
|
|
86
|
+
if self.suggestion:
|
|
87
|
+
pieces.append(f"suggestion={self.suggestion}")
|
|
88
|
+
return " | ".join(pieces)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ValidationReport(OvaBaseModel):
|
|
92
|
+
"""
|
|
93
|
+
Structured validation report for an entire document.
|
|
94
|
+
整份文档的结构化校验报告。
|
|
95
|
+
|
|
96
|
+
Design intent / 设计意图:
|
|
97
|
+
- keep the raw issue list machine-friendly / 保持 issue 列表机器可读
|
|
98
|
+
- but also offer direct summary helpers / 同时提供直接可用的摘要 helper
|
|
99
|
+
- so the report can be used in tests, logs, CLI output, or notebooks
|
|
100
|
+
从而既可用于测试,也可用于日志、CLI 输出或 notebook 调试
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
isValid: bool = True
|
|
104
|
+
issues: list[ValidationIssue] = Field(default_factory=list)
|
|
105
|
+
|
|
106
|
+
def add_issue(
|
|
107
|
+
self,
|
|
108
|
+
*,
|
|
109
|
+
code: str,
|
|
110
|
+
message: str,
|
|
111
|
+
path: str | None = None,
|
|
112
|
+
severity: Literal["error", "warning"] = "error",
|
|
113
|
+
location: str | None = None,
|
|
114
|
+
contextType: str | None = None,
|
|
115
|
+
contextId: str | None = None,
|
|
116
|
+
contextAnchor: str | None = None,
|
|
117
|
+
sectionId: str | None = None,
|
|
118
|
+
sectionTitle: str | None = None,
|
|
119
|
+
suggestion: str | None = None,
|
|
120
|
+
) -> "ValidationReport":
|
|
121
|
+
"""
|
|
122
|
+
Append one issue into the report.
|
|
123
|
+
向报告中追加一条问题记录。
|
|
124
|
+
|
|
125
|
+
`location` defaults to `path` when omitted.
|
|
126
|
+
若未显式提供 `location`,则默认回退到 `path`。
|
|
127
|
+
"""
|
|
128
|
+
self.issues.append(
|
|
129
|
+
ValidationIssue(
|
|
130
|
+
code=code,
|
|
131
|
+
message=message,
|
|
132
|
+
path=path,
|
|
133
|
+
severity=severity,
|
|
134
|
+
location=location or path,
|
|
135
|
+
contextType=contextType,
|
|
136
|
+
contextId=contextId,
|
|
137
|
+
contextAnchor=contextAnchor,
|
|
138
|
+
sectionId=sectionId,
|
|
139
|
+
sectionTitle=sectionTitle,
|
|
140
|
+
suggestion=suggestion,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
if severity == "error":
|
|
144
|
+
self.isValid = False
|
|
145
|
+
return self
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def is_valid(self) -> bool:
|
|
149
|
+
"""
|
|
150
|
+
Python-friendly alias for `isValid`.
|
|
151
|
+
提供一个更符合 Python 命名习惯的 `isValid` 别名。
|
|
152
|
+
|
|
153
|
+
Why keep both names?
|
|
154
|
+
为什么同时保留两个名字?
|
|
155
|
+
- `isValid` matches the JSON-facing protocol field shape.
|
|
156
|
+
`isValid` 对齐 JSON / 协议输出字段风格。
|
|
157
|
+
- `is_valid` feels more natural when the report is used as a Python object.
|
|
158
|
+
当把报告当作 Python 对象使用时,`is_valid` 更顺手。
|
|
159
|
+
"""
|
|
160
|
+
return self.isValid
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def error_count(self) -> int:
|
|
164
|
+
"""
|
|
165
|
+
Number of issues with severity = error.
|
|
166
|
+
严重级别为 error 的问题数量。
|
|
167
|
+
"""
|
|
168
|
+
return sum(1 for issue in self.issues if issue.severity == "error")
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def warning_count(self) -> int:
|
|
172
|
+
"""
|
|
173
|
+
Number of issues with severity = warning.
|
|
174
|
+
严重级别为 warning 的问题数量。
|
|
175
|
+
"""
|
|
176
|
+
return sum(1 for issue in self.issues if issue.severity == "warning")
|
|
177
|
+
|
|
178
|
+
def codes(self) -> list[str]:
|
|
179
|
+
"""
|
|
180
|
+
Return issue codes in order.
|
|
181
|
+
按原顺序返回全部 issue code。
|
|
182
|
+
"""
|
|
183
|
+
return [issue.code for issue in self.issues]
|
|
184
|
+
|
|
185
|
+
def counts_by_code(self) -> dict[str, int]:
|
|
186
|
+
"""
|
|
187
|
+
Return a frequency mapping grouped by code.
|
|
188
|
+
返回按 code 聚合的出现次数映射。
|
|
189
|
+
"""
|
|
190
|
+
return dict(Counter(self.codes()))
|
|
191
|
+
|
|
192
|
+
def to_text(self, *, include_warnings: bool = True) -> str:
|
|
193
|
+
"""
|
|
194
|
+
Render the whole report into a readable multi-line string.
|
|
195
|
+
将整份报告渲染为便于阅读的多行文本。
|
|
196
|
+
"""
|
|
197
|
+
lines = [
|
|
198
|
+
"OVAPortableText validation report:",
|
|
199
|
+
f"isValid={self.isValid}",
|
|
200
|
+
f"errors={self.error_count}",
|
|
201
|
+
f"warnings={self.warning_count}",
|
|
202
|
+
]
|
|
203
|
+
for idx, issue in enumerate(self.issues, start=1):
|
|
204
|
+
if issue.severity == "warning" and not include_warnings:
|
|
205
|
+
continue
|
|
206
|
+
lines.append(f"{idx}. {issue.to_text()}")
|
|
207
|
+
return "\n".join(lines)
|
|
208
|
+
|
|
209
|
+
def raise_for_errors(self) -> "ValidationReport":
|
|
210
|
+
"""
|
|
211
|
+
Raise `DocumentValidationError` if the report is invalid.
|
|
212
|
+
若报告包含错误,则抛出 `DocumentValidationError`。
|
|
213
|
+
"""
|
|
214
|
+
if not self.isValid:
|
|
215
|
+
raise DocumentValidationError(self)
|
|
216
|
+
return self
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class DocumentValidationError(ValueError):
|
|
220
|
+
"""
|
|
221
|
+
Exception raised when document validation fails.
|
|
222
|
+
当文档校验失败时抛出的异常。
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
def __init__(self, report_or_issues: ValidationReport | list[ValidationIssue]):
|
|
226
|
+
if isinstance(report_or_issues, ValidationReport):
|
|
227
|
+
self.report = report_or_issues
|
|
228
|
+
self.issues = report_or_issues.issues
|
|
229
|
+
message = report_or_issues.to_text(include_warnings=True)
|
|
230
|
+
else:
|
|
231
|
+
self.issues = report_or_issues
|
|
232
|
+
self.report = ValidationReport(isValid=False, issues=report_or_issues)
|
|
233
|
+
message = self.report.to_text(include_warnings=True)
|
|
234
|
+
super().__init__(message)
|