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.
Files changed (60) hide show
  1. stbkit/core/__init__.py +12 -0
  2. stbkit/core/_internal/constants.py +42 -0
  3. stbkit/core/_internal/extension_utils.py +238 -0
  4. stbkit/core/_internal/io_text.py +48 -0
  5. stbkit/core/_internal/name_converter.py +170 -0
  6. stbkit/core/_internal/optional_dependencies/xmlschema_validator.py +163 -0
  7. stbkit/core/_internal/stb_logger.py +86 -0
  8. stbkit/core/_internal/transform/__init__.py +5 -0
  9. stbkit/core/_internal/transform/guid_tools.py +73 -0
  10. stbkit/core/_internal/transform/select.py +114 -0
  11. stbkit/core/_internal/xml_value_converter.py +16 -0
  12. stbkit/core/data_model/__init__.py +5 -0
  13. stbkit/core/data_model/_internal/filter_option.py +25 -0
  14. stbkit/core/data_model/_internal/stb_types.py +98 -0
  15. stbkit/core/data_model/_internal/type_validation.py +755 -0
  16. stbkit/core/data_model/common.py +967 -0
  17. stbkit/core/data_model/stb_v2_0_0.py +13633 -0
  18. stbkit/core/data_model/stb_v2_0_0.pyi +15327 -0
  19. stbkit/core/data_model/stb_v2_0_1.py +13702 -0
  20. stbkit/core/data_model/stb_v2_0_1.pyi +15450 -0
  21. stbkit/core/data_model/stb_v2_0_2.py +21677 -0
  22. stbkit/core/data_model/stb_v2_0_2.pyi +23319 -0
  23. stbkit/core/data_model/stb_v2_1_0.py +27541 -0
  24. stbkit/core/data_model/stb_v2_1_0.pyi +31553 -0
  25. stbkit/core/data_model/stb_v2_1_1.py +27577 -0
  26. stbkit/core/data_model/stb_v2_1_1.pyi +31590 -0
  27. stbkit/core/py.typed +0 -0
  28. stbkit/core/repository/__init__.py +67 -0
  29. stbkit/core/repository/_internal/ref_def_v2_0_2.py +191 -0
  30. stbkit/core/repository/_internal/ref_def_v2_1_0.py +216 -0
  31. stbkit/core/repository/_internal/ref_def_v2_1_1.py +216 -0
  32. stbkit/core/repository/repo_common.py +363 -0
  33. stbkit/core/repository/repo_v2_0_2.py +48 -0
  34. stbkit/core/repository/repo_v2_0_2.pyi +257 -0
  35. stbkit/core/repository/repo_v2_1_0.py +48 -0
  36. stbkit/core/repository/repo_v2_1_0.pyi +272 -0
  37. stbkit/core/repository/repo_v2_1_1.py +50 -0
  38. stbkit/core/repository/repo_v2_1_1.pyi +272 -0
  39. stbkit/core/serialization/__init__.py +21 -0
  40. stbkit/core/serialization/_dict_converter.py +1448 -0
  41. stbkit/core/serialization/config.py +114 -0
  42. stbkit/core/serialization/profiles.py +57 -0
  43. stbkit/core/stb_exceptions.py +203 -0
  44. stbkit/core/stb_io/__init__.py +17 -0
  45. stbkit/core/stb_io/_internal/__init__.py +5 -0
  46. stbkit/core/stb_io/_internal/loader.py +63 -0
  47. stbkit/core/stb_io/_internal/serializer.py +157 -0
  48. stbkit/core/stb_io/_internal/stream_reader.py +346 -0
  49. stbkit/core/stb_io/_internal/xml_io.py +482 -0
  50. stbkit/core/stb_reporting.py +1061 -0
  51. stbkit/core/stb_typing.py +61 -0
  52. stbkit/core/validation/__init__.py +9 -0
  53. stbkit/core/validation/_internal/common.py +78 -0
  54. stbkit/core/validation/_internal/schema_validator.py +294 -0
  55. stbkit/core/validation/_internal/validator.py +109 -0
  56. stbkit_core-0.1.0a9.dev1.dist-info/METADATA +121 -0
  57. stbkit_core-0.1.0a9.dev1.dist-info/RECORD +60 -0
  58. stbkit_core-0.1.0a9.dev1.dist-info/WHEEL +5 -0
  59. stbkit_core-0.1.0a9.dev1.dist-info/licenses/LICENSE +373 -0
  60. stbkit_core-0.1.0a9.dev1.dist-info/top_level.txt +1 -0
@@ -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