stbkit-core 0.1.0a9.dev1__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.
- stbkit/core/__init__.py +12 -0
- stbkit/core/_internal/constants.py +42 -0
- stbkit/core/_internal/extension_utils.py +238 -0
- stbkit/core/_internal/io_text.py +48 -0
- stbkit/core/_internal/name_converter.py +170 -0
- stbkit/core/_internal/optional_dependencies/xmlschema_validator.py +163 -0
- stbkit/core/_internal/stb_logger.py +86 -0
- stbkit/core/_internal/transform/__init__.py +5 -0
- stbkit/core/_internal/transform/guid_tools.py +73 -0
- stbkit/core/_internal/transform/select.py +114 -0
- stbkit/core/_internal/xml_value_converter.py +16 -0
- stbkit/core/data_model/__init__.py +5 -0
- stbkit/core/data_model/_internal/filter_option.py +25 -0
- stbkit/core/data_model/_internal/stb_types.py +98 -0
- stbkit/core/data_model/_internal/type_validation.py +755 -0
- stbkit/core/data_model/common.py +967 -0
- stbkit/core/data_model/stb_v2_0_0.py +13633 -0
- stbkit/core/data_model/stb_v2_0_0.pyi +15327 -0
- stbkit/core/data_model/stb_v2_0_1.py +13702 -0
- stbkit/core/data_model/stb_v2_0_1.pyi +15450 -0
- stbkit/core/data_model/stb_v2_0_2.py +21677 -0
- stbkit/core/data_model/stb_v2_0_2.pyi +23319 -0
- stbkit/core/data_model/stb_v2_1_0.py +27541 -0
- stbkit/core/data_model/stb_v2_1_0.pyi +31553 -0
- stbkit/core/data_model/stb_v2_1_1.py +27577 -0
- stbkit/core/data_model/stb_v2_1_1.pyi +31590 -0
- stbkit/core/py.typed +0 -0
- stbkit/core/repository/__init__.py +67 -0
- stbkit/core/repository/_internal/ref_def_v2_0_2.py +191 -0
- stbkit/core/repository/_internal/ref_def_v2_1_0.py +216 -0
- stbkit/core/repository/_internal/ref_def_v2_1_1.py +216 -0
- stbkit/core/repository/repo_common.py +363 -0
- stbkit/core/repository/repo_v2_0_2.py +48 -0
- stbkit/core/repository/repo_v2_0_2.pyi +257 -0
- stbkit/core/repository/repo_v2_1_0.py +48 -0
- stbkit/core/repository/repo_v2_1_0.pyi +272 -0
- stbkit/core/repository/repo_v2_1_1.py +50 -0
- stbkit/core/repository/repo_v2_1_1.pyi +272 -0
- stbkit/core/serialization/__init__.py +21 -0
- stbkit/core/serialization/_dict_converter.py +1448 -0
- stbkit/core/serialization/config.py +114 -0
- stbkit/core/serialization/profiles.py +57 -0
- stbkit/core/stb_exceptions.py +203 -0
- stbkit/core/stb_io/__init__.py +17 -0
- stbkit/core/stb_io/_internal/__init__.py +5 -0
- stbkit/core/stb_io/_internal/loader.py +63 -0
- stbkit/core/stb_io/_internal/serializer.py +157 -0
- stbkit/core/stb_io/_internal/stream_reader.py +346 -0
- stbkit/core/stb_io/_internal/xml_io.py +482 -0
- stbkit/core/stb_reporting.py +1061 -0
- stbkit/core/stb_typing.py +61 -0
- stbkit/core/validation/__init__.py +9 -0
- stbkit/core/validation/_internal/common.py +78 -0
- stbkit/core/validation/_internal/schema_validator.py +294 -0
- stbkit/core/validation/_internal/validator.py +109 -0
- stbkit_core-0.1.0a9.dev1.dist-info/METADATA +121 -0
- stbkit_core-0.1.0a9.dev1.dist-info/RECORD +60 -0
- stbkit_core-0.1.0a9.dev1.dist-info/WHEEL +5 -0
- stbkit_core-0.1.0a9.dev1.dist-info/licenses/LICENSE +373 -0
- stbkit_core-0.1.0a9.dev1.dist-info/top_level.txt +1 -0
stbkit/core/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Copyright 2025-2026 TAISEI CORPORATION
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
7
|
+
"""stbkitのコアモジュール
|
|
8
|
+
|
|
9
|
+
※このモジュール内のAPIは暫定的なものであり、今後変更される可能性があります。
|
|
10
|
+
継続的に利用するコードから使う場合は、この点を了解したうえで、バージョンを固定して使用してください。
|
|
11
|
+
互換性に関する方針は、GitHubリポジトリのCOMPATIBILITY.mdを参照してください。
|
|
12
|
+
"""
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Copyright 2025-2026 TAISEI CORPORATION
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
7
|
+
import importlib.metadata
|
|
8
|
+
from typing import Final
|
|
9
|
+
|
|
10
|
+
PACKAGE_NAME: Final = "stbkit-core"
|
|
11
|
+
|
|
12
|
+
_tmp_version: str
|
|
13
|
+
try:
|
|
14
|
+
_tmp_version = importlib.metadata.version(PACKAGE_NAME)
|
|
15
|
+
except ImportError:
|
|
16
|
+
_tmp_version = "unknown"
|
|
17
|
+
PACKAGE_VERSION: Final[str] = _tmp_version
|
|
18
|
+
|
|
19
|
+
LATEST_STB_VERSION: Final = "2.1.1"
|
|
20
|
+
"""stbkit-coreがサポートしているST-Bridgeの最新バージョン"""
|
|
21
|
+
SUPPORTED_STB_VERSIONS: Final = ("2.0.0", "2.0.1", "2.0.2", "2.1.0", "2.1.1")
|
|
22
|
+
"""stbkit-coreがサポートしているST-Bridgeのバージョン"""
|
|
23
|
+
|
|
24
|
+
ENV_NAME_SCHEMA_PATH_STB_V2_0_0: Final = "STBKIT_SCHEMA_PATH_STB_V2_0_0"
|
|
25
|
+
ENV_NAME_SCHEMA_PATH_STB_V2_0_1: Final = "STBKIT_SCHEMA_PATH_STB_V2_0_1"
|
|
26
|
+
ENV_NAME_SCHEMA_PATH_STB_V2_0_2: Final = "STBKIT_SCHEMA_PATH_STB_V2_0_2"
|
|
27
|
+
ENV_NAME_SCHEMA_PATH_STB_V2_1_0: Final = "STBKIT_SCHEMA_PATH_STB_V2_1_0"
|
|
28
|
+
ENV_NAME_SCHEMA_PATH_STB_V2_1_1: Final = "STBKIT_SCHEMA_PATH_STB_V2_1_1"
|
|
29
|
+
|
|
30
|
+
BYTES_PER_MB: Final[int] = 1024 * 1024
|
|
31
|
+
|
|
32
|
+
DEFAULT_MAX_XML_SIZE: Final[int] = 128 * BYTES_PER_MB
|
|
33
|
+
"""読み込めるXMLファイルのデフォルトのサイズ上限(byte)"""
|
|
34
|
+
|
|
35
|
+
DEFAULT_MAX_XML_DEPTH: Final[int] = 15
|
|
36
|
+
"""読み込めるXMLの要素の階層の上限。ST-Bridgeの階層は10程度だが、拡張要素もあるので少し余裕を持たせる"""
|
|
37
|
+
|
|
38
|
+
XML_READ_CHUNK_SIZE: Final[int] = 64 * 1024
|
|
39
|
+
"""XMLを逐次読み込みする際の1回あたりの文字数"""
|
|
40
|
+
|
|
41
|
+
XML_READ_HEADER_CHUNK_SIZE: Final[int] = 8 * 1024
|
|
42
|
+
"""XMLの前文を逐次読み込みする際の1回あたりの文字数"""
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Copyright 2026 TAISEI CORPORATION
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
|
|
9
|
+
from ..data_model.common import StBridgeElement, StBridgeRoot, _FieldKind, _InvalidValue
|
|
10
|
+
from ..stb_reporting import Code, Phase, Reporter
|
|
11
|
+
from .name_converter import private_field_name
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(kw_only=True, frozen=True)
|
|
15
|
+
class AttributeInfo:
|
|
16
|
+
name: str
|
|
17
|
+
type: str
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(kw_only=True)
|
|
21
|
+
class ExtensionInfo:
|
|
22
|
+
"""拡張情報により定義された、要素の拡張。
|
|
23
|
+
|
|
24
|
+
リポジトリで処理しやすいように、ST-Bridge上の表記と表現を変えているので注意。
|
|
25
|
+
|
|
26
|
+
- element_name: 対象の要素。
|
|
27
|
+
- attribute_infos: その要素が持ってよい属性一覧
|
|
28
|
+
- parent_name: 対象の要素が所属する親要素。既存要素は既に親が決まっており、親チェックが不要なため省略。
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
element_name: str
|
|
32
|
+
attribute_infos: set[AttributeInfo] = field(default_factory=set)
|
|
33
|
+
parent_name: str | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ExtensionInfoRepository:
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
self._registry: dict[str, ExtensionInfo] = {}
|
|
39
|
+
|
|
40
|
+
def register(
|
|
41
|
+
self,
|
|
42
|
+
stb: StBridgeRoot,
|
|
43
|
+
*,
|
|
44
|
+
reporter: Reporter,
|
|
45
|
+
phase: Phase = Phase.VALIDATE,
|
|
46
|
+
) -> None:
|
|
47
|
+
from stbkit.core.data_model import (
|
|
48
|
+
stb_v2_0_1,
|
|
49
|
+
stb_v2_0_2,
|
|
50
|
+
stb_v2_1_0,
|
|
51
|
+
stb_v2_1_1,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if isinstance(
|
|
55
|
+
stb,
|
|
56
|
+
(
|
|
57
|
+
stb_v2_0_1.StBridge,
|
|
58
|
+
stb_v2_0_2.StBridge,
|
|
59
|
+
stb_v2_1_0.StBridge,
|
|
60
|
+
stb_v2_1_1.StBridge,
|
|
61
|
+
),
|
|
62
|
+
):
|
|
63
|
+
if stb.stb_extensions_or_none is None:
|
|
64
|
+
return
|
|
65
|
+
for extension in stb.stb_extensions.stb_extension:
|
|
66
|
+
for ext_elm in extension.stb_ext_element:
|
|
67
|
+
if ext_elm.object_name_or_none is None:
|
|
68
|
+
continue
|
|
69
|
+
element_name: str
|
|
70
|
+
parent_name: str | None
|
|
71
|
+
if ext_elm.element_name_or_none is None:
|
|
72
|
+
element_name = ext_elm.object_name
|
|
73
|
+
parent_name = None
|
|
74
|
+
else:
|
|
75
|
+
element_name = ext_elm.element_name
|
|
76
|
+
parent_name = ext_elm.object_name
|
|
77
|
+
ext_info: ExtensionInfo = self._register_element(
|
|
78
|
+
element_name,
|
|
79
|
+
parent_name,
|
|
80
|
+
definition=ext_elm,
|
|
81
|
+
reporter=reporter,
|
|
82
|
+
phase=phase,
|
|
83
|
+
)
|
|
84
|
+
for ext_prop in ext_elm.stb_ext_property_def:
|
|
85
|
+
if ext_prop.key_or_none is None:
|
|
86
|
+
continue
|
|
87
|
+
if ext_prop.type_or_none is None:
|
|
88
|
+
continue
|
|
89
|
+
attr_info = AttributeInfo(
|
|
90
|
+
name=ext_prop.key,
|
|
91
|
+
type=ext_prop.type,
|
|
92
|
+
)
|
|
93
|
+
if attr_info not in ext_info.attribute_infos:
|
|
94
|
+
ext_info.attribute_infos.add(attr_info)
|
|
95
|
+
|
|
96
|
+
def _register_element(
|
|
97
|
+
self,
|
|
98
|
+
element_name: str,
|
|
99
|
+
parent_name: str | None,
|
|
100
|
+
*,
|
|
101
|
+
definition: StBridgeElement,
|
|
102
|
+
reporter: Reporter,
|
|
103
|
+
phase: Phase,
|
|
104
|
+
) -> ExtensionInfo:
|
|
105
|
+
"""対象要素の拡張定義登録。
|
|
106
|
+
|
|
107
|
+
同じ要素名に対して別の定義ある場合は、拡張定義として矛盾しているためエラー出力し、先にでてきた定義を優先します。
|
|
108
|
+
"""
|
|
109
|
+
ext_info: ExtensionInfo | None = self._registry.get(element_name)
|
|
110
|
+
if ext_info is None:
|
|
111
|
+
ext_info = ExtensionInfo(
|
|
112
|
+
element_name=element_name,
|
|
113
|
+
attribute_infos=set(),
|
|
114
|
+
parent_name=parent_name,
|
|
115
|
+
)
|
|
116
|
+
self._registry[element_name] = ext_info
|
|
117
|
+
return ext_info
|
|
118
|
+
if ext_info.parent_name != parent_name:
|
|
119
|
+
reporter.error(
|
|
120
|
+
message=(
|
|
121
|
+
f"要素[{element_name}]の拡張定義が重複しています。"
|
|
122
|
+
"element_nameに記載する、拡張する子要素の名前は、拡張する子要素同志で重複しないことが求められます。"
|
|
123
|
+
"処理では先に定義した内容を採用します"
|
|
124
|
+
),
|
|
125
|
+
code=Code.EXTENSION_ERROR,
|
|
126
|
+
phase=phase,
|
|
127
|
+
stb_element=definition,
|
|
128
|
+
value=f"採用:element_name={ext_info.element_name},object_name={ext_info.parent_name},",
|
|
129
|
+
ref_value=f"無視:element_name={element_name},object_name={parent_name},",
|
|
130
|
+
)
|
|
131
|
+
return ext_info
|
|
132
|
+
|
|
133
|
+
def _allowed_attribute_names_by_element_name(self, element_name: str) -> set[str]:
|
|
134
|
+
ext_info: ExtensionInfo | None = self._registry.get(element_name)
|
|
135
|
+
if ext_info is None:
|
|
136
|
+
return set()
|
|
137
|
+
return {attr.name for attr in ext_info.attribute_infos}
|
|
138
|
+
|
|
139
|
+
def _allowed_attribute_names(self, stb_element: StBridgeElement) -> set[str]:
|
|
140
|
+
return self._allowed_attribute_names_by_element_name(stb_element._xml_name())
|
|
141
|
+
|
|
142
|
+
def _is_allowed_child_name(self, *, child_name: str, parent_name: str) -> bool:
|
|
143
|
+
ext_info: ExtensionInfo | None = self._registry.get(child_name)
|
|
144
|
+
if ext_info is None or ext_info.parent_name is None:
|
|
145
|
+
return False
|
|
146
|
+
return ext_info.parent_name == parent_name
|
|
147
|
+
|
|
148
|
+
def _is_allowed_child(
|
|
149
|
+
self, *, child: StBridgeElement, parent: StBridgeElement
|
|
150
|
+
) -> bool:
|
|
151
|
+
return self._is_allowed_child_name(
|
|
152
|
+
child_name=child._xml_name(), parent_name=parent._xml_name()
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _attribute_infos(
|
|
156
|
+
self, stb_element: StBridgeElement
|
|
157
|
+
) -> tuple[AttributeInfo, ...]:
|
|
158
|
+
ext_info: ExtensionInfo | None = self._registry.get(stb_element._xml_name())
|
|
159
|
+
if ext_info is None:
|
|
160
|
+
return ()
|
|
161
|
+
return tuple(sorted(ext_info.attribute_infos, key=lambda info: info.name))
|
|
162
|
+
|
|
163
|
+
def _child_element_names(self, parent: StBridgeElement) -> tuple[str, ...]:
|
|
164
|
+
parent_name: str = parent._xml_name()
|
|
165
|
+
return tuple(
|
|
166
|
+
ext_info.element_name
|
|
167
|
+
for ext_info in self._registry.values()
|
|
168
|
+
if ext_info.parent_name == parent_name
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def repair_stb(
|
|
172
|
+
self,
|
|
173
|
+
stb_element: StBridgeElement,
|
|
174
|
+
reporter: Reporter,
|
|
175
|
+
phase: Phase = Phase.VALIDATE,
|
|
176
|
+
) -> None:
|
|
177
|
+
ext = stb_element._extension
|
|
178
|
+
element_name: str = stb_element._xml_name()
|
|
179
|
+
# 拡張情報で説明できない属性・子要素は不正値として退避させる
|
|
180
|
+
allowed_attribute_names: set[str] = self._allowed_attribute_names(stb_element)
|
|
181
|
+
if ext is not None:
|
|
182
|
+
keys = list(ext._attributes.keys()) if ext._attributes else []
|
|
183
|
+
for key in keys:
|
|
184
|
+
if ext._attributes and key not in allowed_attribute_names:
|
|
185
|
+
value = ext._attributes.pop(key)
|
|
186
|
+
# 未知属性はモデルの_field_infoに存在しないため、XPathを直接生成する
|
|
187
|
+
path_xml = f"{stb_element._path_xml()}/@{key}"
|
|
188
|
+
reporter.error(
|
|
189
|
+
message=(
|
|
190
|
+
f"要素[{element_name}]の不明な拡張属性[{key}]を削除しました"
|
|
191
|
+
),
|
|
192
|
+
code=Code.EXTENSION_ERROR,
|
|
193
|
+
phase=phase,
|
|
194
|
+
value=value,
|
|
195
|
+
xpath=path_xml,
|
|
196
|
+
)
|
|
197
|
+
ext._set_invalid_value(
|
|
198
|
+
_InvalidValue(
|
|
199
|
+
kind=_FieldKind.ATTRIBUTE,
|
|
200
|
+
value=value,
|
|
201
|
+
path=path_xml,
|
|
202
|
+
reason="拡張定義されていない拡張属性",
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
if ext._children:
|
|
206
|
+
for i in reversed(range(len(ext._children))):
|
|
207
|
+
child = ext._children[i]
|
|
208
|
+
child_name: str = child._xml_name()
|
|
209
|
+
if not self._is_allowed_child(child=child, parent=stb_element):
|
|
210
|
+
reporter.error(
|
|
211
|
+
message=(
|
|
212
|
+
f"要素[{element_name}]の不明な拡張要素[{child_name}]を削除しました"
|
|
213
|
+
),
|
|
214
|
+
code=Code.EXTENSION_ERROR,
|
|
215
|
+
phase=phase,
|
|
216
|
+
xpath=child._path_xml(),
|
|
217
|
+
)
|
|
218
|
+
ext._set_invalid_value(
|
|
219
|
+
_InvalidValue(
|
|
220
|
+
kind=_FieldKind.ELEMENT,
|
|
221
|
+
value=child,
|
|
222
|
+
path=child._path_xml(),
|
|
223
|
+
reason="拡張定義されていない拡張子要素",
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
ext._children.pop(i)
|
|
227
|
+
for child in ext._children:
|
|
228
|
+
self.repair_stb(child, reporter, phase)
|
|
229
|
+
for key, fi in stb_element._fields.items():
|
|
230
|
+
if fi.kind is _FieldKind.ELEMENT:
|
|
231
|
+
p_name: str = private_field_name(key)
|
|
232
|
+
value = getattr(stb_element, p_name)
|
|
233
|
+
if isinstance(value, StBridgeElement):
|
|
234
|
+
self.repair_stb(value, reporter, phase)
|
|
235
|
+
elif isinstance(value, list):
|
|
236
|
+
for v in value:
|
|
237
|
+
if isinstance(v, StBridgeElement):
|
|
238
|
+
self.repair_stb(v, reporter, phase)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Copyright 2025-2026 TAISEI CORPORATION
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import IO
|
|
9
|
+
|
|
10
|
+
from ..stb_exceptions import XmlLimitExceededError, XmlLimitKind
|
|
11
|
+
from ..stb_typing import TextSource
|
|
12
|
+
from .constants import DEFAULT_MAX_XML_SIZE, XML_READ_CHUNK_SIZE
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_text(
|
|
16
|
+
src: TextSource, *, encoding: str, max_size: int = DEFAULT_MAX_XML_SIZE
|
|
17
|
+
) -> str:
|
|
18
|
+
"""テキストの読み込み
|
|
19
|
+
|
|
20
|
+
メモリ保護のため、サイズの上限を設ける。
|
|
21
|
+
"""
|
|
22
|
+
# ファイルオブジェクトの場合は上限より1文字多く読み、超過したら例外を投げる
|
|
23
|
+
if not isinstance(src, (str, os.PathLike)):
|
|
24
|
+
return _read_limited(src, max_size=max_size)
|
|
25
|
+
# ファイルパスの場合はファイルサイズを確認し、上限を超えるファイルは例外を投げる
|
|
26
|
+
size: int = os.stat(src).st_size
|
|
27
|
+
if size > max_size:
|
|
28
|
+
raise XmlLimitExceededError(kind=XmlLimitKind.SIZE, limit=max_size, actual=size)
|
|
29
|
+
with open(src, encoding=encoding) as f:
|
|
30
|
+
return _read_limited(f, max_size=max_size)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _read_limited(src: IO[str], *, max_size: int) -> str:
|
|
34
|
+
"""上限より1文字多く読み、超過したら例外を投げる"""
|
|
35
|
+
# readにmax_sizeをそのまま渡すと、実際のデータ量と無関係にその大きさのバッファを確保するため、分割して読みこむ。
|
|
36
|
+
chunks: list[str] = []
|
|
37
|
+
remaining: int = max_size + 1
|
|
38
|
+
while remaining > 0:
|
|
39
|
+
chunk: str = src.read(min(remaining, XML_READ_CHUNK_SIZE))
|
|
40
|
+
if not chunk:
|
|
41
|
+
break
|
|
42
|
+
chunks.append(chunk)
|
|
43
|
+
remaining -= len(chunk)
|
|
44
|
+
|
|
45
|
+
result: str = "".join(chunks)
|
|
46
|
+
if len(result) > max_size:
|
|
47
|
+
raise XmlLimitExceededError(kind=XmlLimitKind.SIZE, limit=max_size)
|
|
48
|
+
return result
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Copyright 2025-2026 TAISEI CORPORATION
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Final
|
|
9
|
+
|
|
10
|
+
RESERVED_WORDS: Final[set[str]] = {"as", "ensure"}
|
|
11
|
+
"""Pythonの予約語及び、基底クラスで利用している属性名。かぶったら末尾にアンダーバーを付ける。"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def pascal_case_to_snake_case(input_string: str) -> str:
|
|
15
|
+
"""
|
|
16
|
+
PascalCase -> snake_case。camelCase -> snake_caseにも対応。
|
|
17
|
+
"""
|
|
18
|
+
if not input_string:
|
|
19
|
+
return input_string
|
|
20
|
+
|
|
21
|
+
# ColumnSSame -> ColumnS_Same などの対策
|
|
22
|
+
result = re.sub(
|
|
23
|
+
r"([A-Z]+)([A-Z][a-z])",
|
|
24
|
+
r"\1_\2",
|
|
25
|
+
input_string,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
result = re.sub(
|
|
29
|
+
r"([a-z0-9])([A-Z])",
|
|
30
|
+
r"\1_\2",
|
|
31
|
+
result,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
return result.lower()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def snake_case_to_large_snake_case(input_string: str) -> str:
|
|
38
|
+
return input_string.upper()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def snake_case_to_lower_camel_case(input_string: str) -> str:
|
|
42
|
+
if not input_string:
|
|
43
|
+
return input_string
|
|
44
|
+
first, *rest = input_string.split("_")
|
|
45
|
+
return first + "".join(part[:1].upper() + part[1:] for part in rest if part)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def xml_element_name_to_pascal_case(xml_element_name: str) -> str:
|
|
49
|
+
result: str = (
|
|
50
|
+
xml_element_name.replace("-", "_")
|
|
51
|
+
.replace("_member", "_Member")
|
|
52
|
+
.replace("_RC_", "Rc")
|
|
53
|
+
.replace("_S_", "S")
|
|
54
|
+
.replace("_SRC_", "Src")
|
|
55
|
+
.replace("_CFT_", "Cft")
|
|
56
|
+
.replace("ST_BRIDGE", "StBridge")
|
|
57
|
+
.replace("NRBProperty", "NrbProperty")
|
|
58
|
+
.replace("HDRProperty", "HdrProperty")
|
|
59
|
+
.replace("LRBProperty", "LrbProperty")
|
|
60
|
+
.replace("TRBProperty", "TrbProperty")
|
|
61
|
+
.replace("SDRBProperty", "SdrbProperty")
|
|
62
|
+
.replace("ESBProperty", "EsbProperty")
|
|
63
|
+
.replace("RSBProperty", "RsbProperty")
|
|
64
|
+
.replace("CSBProperty", "CsbProperty")
|
|
65
|
+
.replace("CSLBProperty", "CslbProperty")
|
|
66
|
+
.replace("CLBProperty", "ClbProperty")
|
|
67
|
+
.replace("EGring", "Egring")
|
|
68
|
+
.replace("FRring", "Frring")
|
|
69
|
+
.replace("OSring", "Osring")
|
|
70
|
+
.replace("Lip2C", "Lip2c")
|
|
71
|
+
)
|
|
72
|
+
suffix_map = [
|
|
73
|
+
("_2C", "2c"),
|
|
74
|
+
("_2L", "2l"),
|
|
75
|
+
("_BOX", "Box"),
|
|
76
|
+
("_C", "C"),
|
|
77
|
+
("_CFT", "Cft"),
|
|
78
|
+
("CMQ", "Cmq"),
|
|
79
|
+
("_CPRC", "Cprc"),
|
|
80
|
+
("_H", "H"),
|
|
81
|
+
("_HAssymmetric", "HAssymmetric"),
|
|
82
|
+
("_HAsymmetric", "HAsymmetric"),
|
|
83
|
+
("_L", "L"),
|
|
84
|
+
("_PHC", "Phc"),
|
|
85
|
+
("_PRC", "Prc"),
|
|
86
|
+
("_RC", "Rc"),
|
|
87
|
+
("_S", "S"),
|
|
88
|
+
("_SC", "Sc"),
|
|
89
|
+
("_SRC", "Src"),
|
|
90
|
+
("_ST", "St"),
|
|
91
|
+
("_T", "T"),
|
|
92
|
+
("NRB", "Nrb"),
|
|
93
|
+
("SP", "Sp"),
|
|
94
|
+
("HDR", "Hdr"),
|
|
95
|
+
("LRB", "Lrb"),
|
|
96
|
+
("TRB", "Trb"),
|
|
97
|
+
("SDRB", "Sdrb"),
|
|
98
|
+
("ESB", "Esb"),
|
|
99
|
+
("RSB", "Rsb"),
|
|
100
|
+
("CSB", "Csb"),
|
|
101
|
+
("CLSB", "Clsb"),
|
|
102
|
+
("CLB", "Clb"),
|
|
103
|
+
("LB", "Lb"),
|
|
104
|
+
("RB", "Rb"),
|
|
105
|
+
]
|
|
106
|
+
for suffix, replacement in suffix_map:
|
|
107
|
+
if result.endswith(suffix):
|
|
108
|
+
result = result[: -len(suffix)] + replacement
|
|
109
|
+
break
|
|
110
|
+
return result
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def xml_element_name_to_snake_case(xml_element_name: str) -> str:
|
|
114
|
+
result: str = (
|
|
115
|
+
pascal_case_to_snake_case(xml_element_name_to_pascal_case(xml_element_name))
|
|
116
|
+
.replace("rc2_way", "rc_2_way")
|
|
117
|
+
.replace("rc1_way", "rc_1_way")
|
|
118
|
+
.replace("roll2c", "roll_2c")
|
|
119
|
+
.replace("roll2l", "roll_2l")
|
|
120
|
+
.replace("lip2c", "lip_2c")
|
|
121
|
+
)
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def xml_element_name_to_large_snake_case(xml_element_name: str) -> str:
|
|
126
|
+
return snake_case_to_large_snake_case(
|
|
127
|
+
xml_element_name_to_snake_case(xml_element_name)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def xml_element_name_to_python_class_name(xml_element_name: str) -> str:
|
|
132
|
+
return xml_element_name_to_pascal_case(xml_element_name)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def xml_attribute_name_to_snake_case(xml_attribute_name: str) -> str:
|
|
136
|
+
return pascal_case_to_snake_case(xml_attribute_name)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def snake_case_to_python_attribute_name(name: str) -> str:
|
|
140
|
+
if name in RESERVED_WORDS:
|
|
141
|
+
name = f"{name}_"
|
|
142
|
+
return name
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def xml_element_name_to_key(element_name: str) -> str:
|
|
146
|
+
return xml_attribute_name_to_python_attribute_name(
|
|
147
|
+
xml_element_name_to_snake_case(element_name)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def xml_attribute_name_to_python_attribute_name(attribute_name: str) -> str:
|
|
152
|
+
return snake_case_to_python_attribute_name(
|
|
153
|
+
xml_attribute_name_to_snake_case(attribute_name.replace("@", ""))
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def content_py_attr_name() -> str:
|
|
158
|
+
return "content"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def private_field_name(key: str) -> str:
|
|
162
|
+
return f"_{key}"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def property_name(key: str) -> str:
|
|
166
|
+
return key
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def or_none_property_name(key: str) -> str:
|
|
170
|
+
return f"{key}_or_none"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Copyright 2026 TAISEI CORPORATION
|
|
2
|
+
# SPDX-License-Identifier: MPL-2.0
|
|
3
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
4
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
5
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from functools import cache
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ...stb_reporting import Code, NullReporter, Phase
|
|
14
|
+
from ...validation._internal.common import ValidationContext
|
|
15
|
+
from ..extension_utils import ExtensionInfoRepository
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _print_import_error() -> None:
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
print(
|
|
22
|
+
"xmlschemaがインストールされていません。下記コマンドでインストールしてください。\n"
|
|
23
|
+
"pip install xmlschema\n"
|
|
24
|
+
"または\n"
|
|
25
|
+
"pip install stbkit[xsd]\n",
|
|
26
|
+
file=sys.stderr,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@cache
|
|
31
|
+
def _load_xmlschema(xsd_path: Path) -> Any:
|
|
32
|
+
import xmlschema
|
|
33
|
+
|
|
34
|
+
xsd_str: str = xsd_path.read_text(encoding="utf-8")
|
|
35
|
+
# 公式スキーマのファイル頭に余計な文字が入っているのを取り除く処理
|
|
36
|
+
while xsd_str and xsd_str[0] != "<":
|
|
37
|
+
xsd_str = xsd_str[1:]
|
|
38
|
+
|
|
39
|
+
return xmlschema.XMLSchema(xsd_str)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# 定義に無い属性のエラーメッセージを検出する正規表現。
|
|
43
|
+
_ATTRIBUTE_NOT_ALLOWED_RE = re.compile(
|
|
44
|
+
r"^'(?P<name>[^']+)' attribute not allowed for element$"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _is_legal_extension_error(error: Any, *, ext_repo: ExtensionInfoRepository) -> bool:
|
|
49
|
+
"""XSDのエラーが、ST-Bridge仕様で認められる拡張かを返す
|
|
50
|
+
|
|
51
|
+
判断できないエラーはFalse
|
|
52
|
+
"""
|
|
53
|
+
from xmlschema.validators import XMLSchemaChildrenValidationError
|
|
54
|
+
|
|
55
|
+
from ...stb_io._internal.stream_reader import _delete_namespace
|
|
56
|
+
|
|
57
|
+
elem: Any = getattr(error, "elem", None)
|
|
58
|
+
if elem is None:
|
|
59
|
+
return False
|
|
60
|
+
element_name: str = _delete_namespace(str(elem.tag))
|
|
61
|
+
|
|
62
|
+
if isinstance(error, XMLSchemaChildrenValidationError):
|
|
63
|
+
if error.invalid_tag is None:
|
|
64
|
+
return False
|
|
65
|
+
return ext_repo._is_allowed_child_name(
|
|
66
|
+
child_name=_delete_namespace(str(error.invalid_tag)),
|
|
67
|
+
parent_name=element_name,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
matched = _ATTRIBUTE_NOT_ALLOWED_RE.match(str(getattr(error, "reason", None) or ""))
|
|
71
|
+
if matched is None:
|
|
72
|
+
return False
|
|
73
|
+
attribute_name: str = matched.group("name")
|
|
74
|
+
# 解釈が適切か、要素が実際にその属性を持つかで確認する
|
|
75
|
+
attributes: Any = getattr(error, "obj", None)
|
|
76
|
+
if not isinstance(attributes, Mapping) or attribute_name not in attributes:
|
|
77
|
+
return False
|
|
78
|
+
return attribute_name in ext_repo._allowed_attribute_names_by_element_name(
|
|
79
|
+
element_name
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def validate_by_xsd(
|
|
84
|
+
xsd_path: Path | None,
|
|
85
|
+
ctx: ValidationContext,
|
|
86
|
+
*,
|
|
87
|
+
require_xsd: bool = False,
|
|
88
|
+
use_cache: bool = True,
|
|
89
|
+
exclude_legal_extensions: bool = False,
|
|
90
|
+
) -> bool:
|
|
91
|
+
try:
|
|
92
|
+
if xsd_path is None:
|
|
93
|
+
ctx.reporter.error(
|
|
94
|
+
message="xsd:XSDファイルのパスが指定されていません。XSDによるスキーマチェックはスキップします。",
|
|
95
|
+
code=Code.SCHEMA_ERROR,
|
|
96
|
+
phase=Phase.VALIDATE,
|
|
97
|
+
)
|
|
98
|
+
return not require_xsd
|
|
99
|
+
import xmlschema
|
|
100
|
+
from xmlschema import XMLSchemaException
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
resolved_xsd_path: Path = xsd_path.resolve()
|
|
104
|
+
if use_cache:
|
|
105
|
+
schema = _load_xmlschema(resolved_xsd_path)
|
|
106
|
+
else:
|
|
107
|
+
schema = xmlschema.XMLSchema(resolved_xsd_path)
|
|
108
|
+
except XMLSchemaException as e:
|
|
109
|
+
ctx.reporter.error(
|
|
110
|
+
message=f"xsd:XSDファイルの読み込みに失敗しました: {e}。詳細スキーマチェックはスキップします。",
|
|
111
|
+
code=Code.UNEXPECTED_ERROR,
|
|
112
|
+
phase=Phase.VALIDATE,
|
|
113
|
+
)
|
|
114
|
+
return not require_xsd
|
|
115
|
+
ext_repo: ExtensionInfoRepository | None = None
|
|
116
|
+
if exclude_legal_extensions:
|
|
117
|
+
ext_repo = ExtensionInfoRepository()
|
|
118
|
+
# 拡張定義のエラーメッセージはload時に出力しているため、ここでは出力しない
|
|
119
|
+
ext_repo.register(ctx.obj(), reporter=NullReporter())
|
|
120
|
+
errors = []
|
|
121
|
+
xpaths = []
|
|
122
|
+
for error in schema.iter_errors(ctx.xml()):
|
|
123
|
+
if ext_repo is not None and _is_legal_extension_error(
|
|
124
|
+
error, ext_repo=ext_repo
|
|
125
|
+
):
|
|
126
|
+
continue
|
|
127
|
+
line = getattr(error, "line", None)
|
|
128
|
+
xpath = error.path
|
|
129
|
+
raw_msg = (
|
|
130
|
+
getattr(error, "reason", None)
|
|
131
|
+
or getattr(error, "message", None)
|
|
132
|
+
or str(error)
|
|
133
|
+
)
|
|
134
|
+
msg = str(raw_msg).strip()
|
|
135
|
+
info = []
|
|
136
|
+
if line is not None:
|
|
137
|
+
info.append(f"line-{line}")
|
|
138
|
+
info_str = " / ".join(info)
|
|
139
|
+
if info_str:
|
|
140
|
+
errors.append(f"{info_str}: {msg}")
|
|
141
|
+
else:
|
|
142
|
+
errors.append(f"{msg}")
|
|
143
|
+
xpaths.append(xpath)
|
|
144
|
+
|
|
145
|
+
if not errors:
|
|
146
|
+
return True
|
|
147
|
+
else:
|
|
148
|
+
for err, xpath in zip(errors, xpaths):
|
|
149
|
+
ctx.reporter.error(
|
|
150
|
+
message=f"xsd:{err}",
|
|
151
|
+
code=Code.SCHEMA_ERROR,
|
|
152
|
+
phase=Phase.VALIDATE,
|
|
153
|
+
xpath=xpath,
|
|
154
|
+
)
|
|
155
|
+
return False
|
|
156
|
+
except ImportError:
|
|
157
|
+
ctx.reporter.warning(
|
|
158
|
+
message="xmlschemaパッケージがインストールされていません。XSDによるスキーマチェックはスキップします。",
|
|
159
|
+
code=Code.OPTIONAL_DEPENDENCY_MISSING,
|
|
160
|
+
phase=Phase.VALIDATE,
|
|
161
|
+
)
|
|
162
|
+
_print_import_error()
|
|
163
|
+
return not require_xsd
|