stbkit 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 (125) hide show
  1. stbkit/__main__.py +10 -0
  2. stbkit/api/__init__.py +48 -0
  3. stbkit/api/experimental/__init__.py +19 -0
  4. stbkit/api/experimental/repository/__init__.py +30 -0
  5. stbkit/api/py.typed +0 -0
  6. stbkit/api/stb_latest.py +7 -0
  7. stbkit/api/stb_v2_0_2.py +11 -0
  8. stbkit/api/stb_v2_1_1.py +9 -0
  9. stbkit/cli/__init__.py +17 -0
  10. stbkit/cli/_internal/cli_main.py +114 -0
  11. stbkit/cli/_internal/common.py +126 -0
  12. stbkit/cli/_internal/constants_exit_code.py +59 -0
  13. stbkit/cli/_internal/converter.py +693 -0
  14. stbkit/cli/_internal/diff.py +213 -0
  15. stbkit/cli/_internal/entry_points.py +25 -0
  16. stbkit/cli/_internal/global_parent.py +70 -0
  17. stbkit/cli/_internal/show_versions.py +36 -0
  18. stbkit/cli/_internal/validate.py +310 -0
  19. stbkit/cli/commands/convert.py +16 -0
  20. stbkit/cli/commands/diff.py +16 -0
  21. stbkit/cli/commands/validate.py +16 -0
  22. stbkit/cli/py.typed +0 -0
  23. stbkit/tools/__init__.py +12 -0
  24. stbkit/tools/_internal/__init__.py +5 -0
  25. stbkit/tools/_internal/constants.py +21 -0
  26. stbkit/tools/_internal/data_model/building_geometry.py +1208 -0
  27. stbkit/tools/_internal/data_model/building_geometry_web.py +106 -0
  28. stbkit/tools/_internal/data_model/element_data.py +93 -0
  29. stbkit/tools/_internal/data_model/ifc_data/entity_data.py +67 -0
  30. stbkit/tools/_internal/data_model/ifc_data/entity_names.py +32 -0
  31. stbkit/tools/_internal/data_model/shape_data.py +1089 -0
  32. stbkit/tools/_internal/diff/__init__.py +5 -0
  33. stbkit/tools/_internal/diff/common.py +50 -0
  34. stbkit/tools/_internal/diff/diff_xml.py +796 -0
  35. stbkit/tools/_internal/matrix.py +51 -0
  36. stbkit/tools/_internal/optional_dependencies/__init__.py +5 -0
  37. stbkit/tools/_internal/optional_dependencies/ifc_common.py +28 -0
  38. stbkit/tools/_internal/optional_dependencies/ifc_exporter/ifc_entity_exporter.py +473 -0
  39. stbkit/tools/_internal/optional_dependencies/ifc_exporter/ifc_profile_exporter.py +267 -0
  40. stbkit/tools/_internal/optional_dependencies/ifc_exporter/ifc_representation_exporter.py +178 -0
  41. stbkit/tools/_internal/optional_dependencies/ifc_pipeline.py +133 -0
  42. stbkit/tools/_internal/optional_dependencies/ifc_to_building_geometry.py +64 -0
  43. stbkit/tools/_internal/stb_io.py +116 -0
  44. stbkit/tools/_internal/stb_repair/_common.py +24 -0
  45. stbkit/tools/_internal/stb_repair/_stb_repair.py +197 -0
  46. stbkit/tools/_internal/stb_repair/_stb_v2_0_2_repair.py +53 -0
  47. stbkit/tools/_internal/stb_repair/_stb_v2_1_0_repair.py +109 -0
  48. stbkit/tools/_internal/stb_repair/_stb_v2_1_1_repair.py +109 -0
  49. stbkit/tools/_internal/studio_report.py +49 -0
  50. stbkit/tools/_internal/utils/atomic_writer.py +57 -0
  51. stbkit/tools/_internal/utils/float_rounding.py +49 -0
  52. stbkit/tools/_internal/utils/girder_utils.py +91 -0
  53. stbkit/tools/_internal/utils/guid_utils.py +30 -0
  54. stbkit/tools/_internal/utils/mesh_utils.py +1305 -0
  55. stbkit/tools/_internal/utils/node_utils.py +50 -0
  56. stbkit/tools/_internal/utils/unit_utils.py +23 -0
  57. stbkit/tools/_internal/vectors.py +222 -0
  58. stbkit/tools/converters/__init__.py +39 -0
  59. stbkit/tools/converters/_internal/__init__.py +5 -0
  60. stbkit/tools/converters/_internal/building_geometry_to_ply_text.py +32 -0
  61. stbkit/tools/converters/_internal/coord_converter.py +151 -0
  62. stbkit/tools/converters/_internal/element_data_to_building_geometry.py +549 -0
  63. stbkit/tools/converters/_internal/ifc_to_ply_text.py +21 -0
  64. stbkit/tools/converters/_internal/name_converter.py +23 -0
  65. stbkit/tools/converters/_internal/pipeline/__init__.py +5 -0
  66. stbkit/tools/converters/_internal/pipeline/data.py +188 -0
  67. stbkit/tools/converters/_internal/pipeline/execution.py +569 -0
  68. stbkit/tools/converters/_internal/pipeline/ply_pipeline.py +52 -0
  69. stbkit/tools/converters/_internal/pipeline/stb_pipeline.py +226 -0
  70. stbkit/tools/converters/_internal/pipeline/stb_pipeline_latest.py +39 -0
  71. stbkit/tools/converters/_internal/pipeline/step.py +139 -0
  72. stbkit/tools/converters/_internal/stb_latest_to_element_data/__init__.py +17 -0
  73. stbkit/tools/converters/_internal/stb_latest_to_element_data/_stb_latest_to_element_data.py +186 -0
  74. stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_footing.py +49 -0
  75. stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_line.py +533 -0
  76. stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_pile.py +100 -0
  77. stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_plane.py +103 -0
  78. stbkit/tools/converters/_internal/stb_latest_to_element_data/open.py +70 -0
  79. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec.py +214 -0
  80. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_beam_rc.py +67 -0
  81. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_beam_s.py +97 -0
  82. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_beam_src.py +28 -0
  83. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_brace_s.py +49 -0
  84. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_cft.py +217 -0
  85. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_rc.py +41 -0
  86. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_s.py +127 -0
  87. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_src.py +173 -0
  88. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_foundation.py +225 -0
  89. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_parapet_rc.py +53 -0
  90. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_pile_rc.py +106 -0
  91. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_pile_s.py +100 -0
  92. stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_steel.py +432 -0
  93. stbkit/tools/converters/_internal/stb_to_building_geometry.py +44 -0
  94. stbkit/tools/converters/_internal/stb_to_ifc_data/__init__.py +5 -0
  95. stbkit/tools/converters/_internal/stb_to_ifc_data/_stb_to_ifc_data.py +59 -0
  96. stbkit/tools/converters/_internal/stb_to_ifc_data/convert_option.py +22 -0
  97. stbkit/tools/converters/_internal/stb_to_ply_text.py +22 -0
  98. stbkit/tools/py.typed +0 -0
  99. stbkit/tools/upgrade/__init__.py +13 -0
  100. stbkit/tools/upgrade/_internal/__init__.py +5 -0
  101. stbkit/tools/upgrade/_internal/common.py +205 -0
  102. stbkit/tools/upgrade/_internal/upgrade.py +228 -0
  103. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/__init__.py +60 -0
  104. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_common.py +62 -0
  105. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_joint_beam_shape_h.py +44 -0
  106. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_pile_rc.py +101 -0
  107. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_pile_s.py +78 -0
  108. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_slab_deck.py +56 -0
  109. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_slab_rc.py +128 -0
  110. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_girder.py +126 -0
  111. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_open.py +110 -0
  112. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_sec_beam_rc.py +227 -0
  113. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_sec_beam_s.py +1153 -0
  114. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hooks.py +22 -0
  115. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/post.py +42 -0
  116. stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/tables.py +99 -0
  117. stbkit/tools/upgrade/_internal/v2_1_0_to_v2_1_1/__init__.py +58 -0
  118. stbkit/tools/upgrade/_internal/v2_1_0_to_v2_1_1/hooks.py +32 -0
  119. stbkit/tools/upgrade/_internal/v2_1_0_to_v2_1_1/tables.py +56 -0
  120. stbkit-0.1.0a9.dev1.dist-info/METADATA +196 -0
  121. stbkit-0.1.0a9.dev1.dist-info/RECORD +125 -0
  122. stbkit-0.1.0a9.dev1.dist-info/WHEEL +5 -0
  123. stbkit-0.1.0a9.dev1.dist-info/entry_points.txt +2 -0
  124. stbkit-0.1.0a9.dev1.dist-info/licenses/LICENSE +373 -0
  125. stbkit-0.1.0a9.dev1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,693 @@
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
+ from __future__ import annotations
8
+
9
+ import os
10
+ import sys
11
+ from argparse import Namespace, _SubParsersAction
12
+ from dataclasses import dataclass
13
+ from logging import Logger
14
+ from pathlib import Path
15
+ from typing import Any
16
+ from xml.etree.ElementTree import ParseError
17
+
18
+ from stbkit.core.data_model.common import StBridgeRoot
19
+ from stbkit.core.stb_exceptions import DeveloperError, StbError
20
+ from stbkit.core.stb_reporting import (
21
+ CollectingReporter,
22
+ Reporter,
23
+ Severity,
24
+ get_reporter,
25
+ )
26
+
27
+ from stbkit.tools.converters._internal.pipeline.data import (
28
+ _Data,
29
+ _DataFormat,
30
+ _DataFormatSet,
31
+ _DataKind,
32
+ _get_extension,
33
+ _StbVersion,
34
+ )
35
+ from stbkit.tools.converters._internal.pipeline.execution import (
36
+ _UnsupportedConversionError,
37
+ cleanup_paths,
38
+ execute_conversion,
39
+ get_step,
40
+ open_data,
41
+ open_text_data,
42
+ save_data,
43
+ to_text,
44
+ )
45
+ from stbkit.tools.converters._internal.pipeline.stb_pipeline import StbData
46
+ from stbkit.tools.converters._internal.pipeline.step import _ExecutionResult
47
+
48
+ from . import common
49
+ from .constants_exit_code import (
50
+ EXIT_EXECUTION_ERROR,
51
+ EXIT_INVALID_ARGUMENT,
52
+ EXIT_ISSUE_FOUND,
53
+ EXIT_OK,
54
+ EXIT_OPTIONAL_DEPENDENCY_MISSING,
55
+ )
56
+ from .global_parent import (
57
+ get_interactive,
58
+ get_logger,
59
+ get_max_size,
60
+ make_global_parent,
61
+ )
62
+
63
+
64
+ @dataclass(kw_only=True)
65
+ class ConverterArgs:
66
+ input: str
67
+ output: str
68
+ from_format: str
69
+ to_format: str
70
+ yes: bool
71
+ interactive: bool
72
+ input_encoding: str | None
73
+ via: str | None
74
+ save_intermediate: dict[str, str | None]
75
+ max_size: int
76
+ logger: Logger
77
+ reporter: Reporter
78
+
79
+ def print_stb_info(self, stb: StBridgeRoot) -> None:
80
+ logger = self.logger
81
+ logger.info("ST-Bridgeモデル情報:")
82
+ model = stb.stb_model_or_none # type: ignore[attr-defined]
83
+ if model is None:
84
+ return
85
+ if model.stb_nodes_or_none:
86
+ logger.info(f" node数:{len(model.stb_nodes.stb_node)}")
87
+ members = model.stb_members_or_none
88
+ if members is None:
89
+ return
90
+ member_types = [
91
+ "column",
92
+ "girder",
93
+ "post",
94
+ "beam",
95
+ "brace",
96
+ "wall",
97
+ "slab",
98
+ ]
99
+ for member_type in member_types:
100
+ member_group = getattr(
101
+ members,
102
+ f"stb_{member_type}s_or_none",
103
+ None,
104
+ )
105
+ if member_group is None:
106
+ continue
107
+ member_list = getattr(
108
+ member_group,
109
+ f"stb_{member_type}",
110
+ None,
111
+ )
112
+ if member_list:
113
+ logger.info(f" {member_type}数:{len(member_list)}")
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class _CliFormatSpec:
118
+ data_format: _DataFormat
119
+ label: str
120
+ aliases: tuple[str, ...]
121
+ description: tuple[str, ...] = ()
122
+
123
+
124
+ @dataclass(frozen=True)
125
+ class _CliRouteSpec:
126
+ title: str
127
+ from_format: str
128
+ to_format: str
129
+ via: tuple[str, ...] = ()
130
+ note: str | None = None
131
+
132
+ @property
133
+ def command_example(self) -> str:
134
+ command = f"--from {self.from_format} --to {self.to_format}"
135
+ if self.via:
136
+ command += f" --via {','.join(self.via)}"
137
+ return command
138
+
139
+
140
+ _CLI_FORMAT_SPECS: tuple[_CliFormatSpec, ...] = (
141
+ _CliFormatSpec(
142
+ data_format=_DataFormat.STB,
143
+ label="ST-Bridge",
144
+ aliases=("stb",),
145
+ description=(
146
+ "拡張子.stbはST-Bridgeファイルと判定します。",
147
+ (
148
+ "バージョンの指定は stb-2.0.1 / stb-2.0.2 / stb-2.1.0 / stb-2.1.1 / "
149
+ "stb-latest で指定できます。"
150
+ ),
151
+ "指定がない場合入力のST-Bridgeはバージョンを自動検出します。",
152
+ "指定がない場合出力のST-Bridgeは最新版を使用します。",
153
+ ),
154
+ ),
155
+ _CliFormatSpec(
156
+ data_format=_DataFormat.IFC,
157
+ label="IFC",
158
+ aliases=("ifc", "ifc4"),
159
+ description=("拡張子.ifcはIFCファイルと判定します。",),
160
+ ),
161
+ _CliFormatSpec(
162
+ data_format=_DataFormat.PLY,
163
+ label="PLY",
164
+ aliases=("ply",),
165
+ description=("拡張子.plyはPLYファイルと判定します。",),
166
+ ),
167
+ )
168
+
169
+ _CLI_ROUTE_SPECS: tuple[_CliRouteSpec, ...] = (
170
+ _CliRouteSpec(
171
+ title="ST-Bridgeのバージョンアップ",
172
+ from_format="stb-2.0.2",
173
+ to_format="stb-latest",
174
+ note="バージョンダウンはサポートしていません。",
175
+ ),
176
+ _CliRouteSpec(
177
+ title="ST-BridgeからIFCへの変換",
178
+ from_format="stb",
179
+ to_format="ifc",
180
+ ),
181
+ _CliRouteSpec(
182
+ title="ST-BridgeからPLYへの直接変換",
183
+ from_format="stb",
184
+ to_format="ply",
185
+ ),
186
+ _CliRouteSpec(
187
+ title="ST-BridgeからIFCを経由したPLYへの変換",
188
+ from_format="stb",
189
+ to_format="ply",
190
+ via=("ifc",),
191
+ ),
192
+ )
193
+
194
+ _STB_VERSION_ALIASES: dict[str, _StbVersion] = {
195
+ "stb-latest": _StbVersion.LATEST,
196
+ "stb-2.0.1": _StbVersion.V2_0_1,
197
+ "stb-2.0.2": _StbVersion.V2_0_2,
198
+ "stb-2.1.0": _StbVersion.V2_1_0,
199
+ "stb-2.1.1": _StbVersion.V2_1_1,
200
+ }
201
+
202
+
203
+ def _get_format_spec(data_format: _DataFormat) -> _CliFormatSpec:
204
+ for spec in _CLI_FORMAT_SPECS:
205
+ if spec.data_format == data_format:
206
+ return spec
207
+ raise ValueError(f"CLIで対応していないデータ形式です: {data_format}")
208
+
209
+
210
+ def _get_stb_version(
211
+ normalized_format: str,
212
+ *,
213
+ for_output: bool,
214
+ ) -> _StbVersion:
215
+ if normalized_format == "stb":
216
+ return _StbVersion.LATEST if for_output else _StbVersion.UNSPECIFIED
217
+
218
+ version: _StbVersion | None = _STB_VERSION_ALIASES.get(normalized_format)
219
+ if version is None:
220
+ return _StbVersion.LATEST if for_output else _StbVersion.UNSPECIFIED
221
+ return version
222
+
223
+
224
+ def _guess_format(path: str) -> str | None:
225
+ if path == "-" or not path:
226
+ return None
227
+ return os.path.splitext(path)[1].lower().lstrip(".")
228
+
229
+
230
+ def _get_pipeline_format(
231
+ format_name: str,
232
+ *,
233
+ for_output: bool,
234
+ ) -> _DataFormatSet | None:
235
+ """CLIのフォーマット指定をパイプラインのデータ形式へ変換する。"""
236
+ normalized_format = format_name.strip().casefold().replace("_", "-")
237
+
238
+ if normalized_format == "stb" or normalized_format in _STB_VERSION_ALIASES:
239
+ return _DataFormatSet(
240
+ _DataFormat.STB,
241
+ _DataKind.OBJECT,
242
+ stb_version=_get_stb_version(
243
+ normalized_format,
244
+ for_output=for_output,
245
+ ),
246
+ )
247
+
248
+ for spec in _CLI_FORMAT_SPECS:
249
+ if spec.data_format == _DataFormat.STB:
250
+ continue
251
+ if normalized_format in spec.aliases:
252
+ return _DataFormatSet(spec.data_format, _DataKind.OBJECT)
253
+ return None
254
+
255
+
256
+ def _require_pipeline_format(
257
+ format_name: str,
258
+ *,
259
+ for_output: bool,
260
+ role: str,
261
+ ) -> _DataFormatSet:
262
+ format_set: _DataFormatSet | None = _get_pipeline_format(
263
+ format_name,
264
+ for_output=for_output,
265
+ )
266
+ if format_set is None:
267
+ raise _UnsupportedConversionError(f"{role}形式が不正です: {format_name}")
268
+ return format_set
269
+
270
+
271
+ def _get_route_formats(via: str | None) -> list[_DataFormatSet]:
272
+ if not via:
273
+ return []
274
+ result: list[_DataFormatSet] = []
275
+ for item in via.split(","):
276
+ format_name = item.strip()
277
+ if not format_name:
278
+ raise _UnsupportedConversionError("--via に空の文字列が指定されています")
279
+ result.append(
280
+ _require_pipeline_format(
281
+ format_name,
282
+ for_output=True,
283
+ role="中間",
284
+ )
285
+ )
286
+ return result
287
+
288
+
289
+ class _OverwriteDeniedError(Exception):
290
+ """既存出力の上書きが許可されていないことを表す。"""
291
+
292
+
293
+ class _OutputPathConflictError(Exception):
294
+ """入力・出力・中間出力のパスが衝突していることを表す。"""
295
+
296
+
297
+ def _confirm_overwrite(path: Path, *, yes: bool, interactive: bool) -> bool:
298
+ if not path.exists() or yes:
299
+ return True
300
+ if not interactive:
301
+ return False
302
+ response: str = input(f"{path}は既に存在します。上書きしますか?(y/N):")
303
+ return response.strip().casefold() in {"y", "yes"}
304
+
305
+
306
+ def _paths_conflict(path_a: Path, path_b: Path) -> bool:
307
+ resolved_a: Path = path_a.resolve()
308
+ resolved_b: Path = path_b.resolve()
309
+ if resolved_a == resolved_b:
310
+ return True
311
+
312
+ if path_a.exists() and path_b.exists():
313
+ try:
314
+ return path_a.samefile(path_b)
315
+ except OSError:
316
+ return False
317
+ return False
318
+
319
+
320
+ def _get_intermediate_paths(
321
+ args: ConverterArgs,
322
+ *,
323
+ output_format: _DataFormat,
324
+ ) -> list[tuple[str, Path]]:
325
+ destinations: list[tuple[str, Path]] = []
326
+ for format_name, specified_path in args.save_intermediate.items():
327
+ format_set = _get_pipeline_format(format_name, for_output=True)
328
+ if format_set is None:
329
+ args.logger.warning("中間形式が不正です: %s", format_name)
330
+ continue
331
+ data_format = format_set.format
332
+ if data_format == output_format:
333
+ args.logger.info(
334
+ "中間出力[%s]は最終出力と同じであるため省略します。",
335
+ data_format.value,
336
+ )
337
+ continue
338
+
339
+ path_text = specified_path
340
+ if not path_text and args.output and args.output != "-":
341
+ path_text = f"{args.output}{_get_extension(data_format)}"
342
+ if not path_text:
343
+ args.logger.warning(
344
+ "中間形式[%s]の保存先を決定できません。"
345
+ "標準出力を使う場合はpathを明示してください。",
346
+ data_format.value,
347
+ )
348
+ continue
349
+ destinations.append((f"中間出力({format_name})", Path(path_text)))
350
+ return destinations
351
+
352
+
353
+ def _validate_path_collisions(
354
+ args: ConverterArgs,
355
+ *,
356
+ output_path: Path | None,
357
+ output_format: _DataFormat,
358
+ ) -> list[tuple[str, Path]]:
359
+ destinations = _get_intermediate_paths(args, output_format=output_format)
360
+ targets: list[tuple[str, Path]] = []
361
+ if args.input != "-":
362
+ targets.append(("入力", Path(args.input)))
363
+ if output_path is not None:
364
+ targets.append(("最終出力", output_path))
365
+ targets.extend(destinations)
366
+
367
+ for index, (left_name, left_path) in enumerate(targets):
368
+ for right_name, right_path in targets[index + 1 :]:
369
+ if _paths_conflict(left_path, right_path):
370
+ raise _OutputPathConflictError(
371
+ "入力ファイル・最終出力・中間出力の保存先は重複できません: "
372
+ f"{left_name}={left_path} / {right_name}={right_path}"
373
+ )
374
+ return destinations
375
+
376
+
377
+ def _save_intermediate_data(
378
+ args: ConverterArgs,
379
+ destinations: list[tuple[str, Path]],
380
+ observed_data: dict[_DataFormat, _Data],
381
+ output_format: _DataFormat,
382
+ ) -> None:
383
+ """各ステップで最後に得られたデータを保存する。"""
384
+ destination_map: dict[str, Path] = {
385
+ label.removeprefix("中間出力(").removesuffix(")"): path
386
+ for label, path in destinations
387
+ }
388
+ for format_name in args.save_intermediate:
389
+ path = destination_map.get(format_name)
390
+ if path is None:
391
+ continue
392
+
393
+ format_set = _get_pipeline_format(format_name, for_output=True)
394
+ if format_set is None:
395
+ continue
396
+ data_format = format_set.format
397
+ data = observed_data.get(data_format)
398
+ if data is None:
399
+ args.logger.warning(
400
+ "変換ルート上に中間形式[%s]がありません。",
401
+ data_format.value,
402
+ )
403
+ continue
404
+ if not _confirm_overwrite(path, yes=args.yes, interactive=args.interactive):
405
+ args.logger.info(
406
+ "中間ファイルの保存を省略しました: %s",
407
+ path,
408
+ )
409
+ continue
410
+ save_data(data, path, args.reporter)
411
+ args.logger.info("中間ファイルを保存しました: %s", path)
412
+
413
+
414
+ def _pipeline_convert(args: ConverterArgs) -> None:
415
+ input_format: _DataFormatSet = _require_pipeline_format(
416
+ args.from_format,
417
+ for_output=False,
418
+ role="入力",
419
+ )
420
+ output_format: _DataFormatSet = _require_pipeline_format(
421
+ args.to_format,
422
+ for_output=True,
423
+ role="出力",
424
+ )
425
+ route_formats: list[_DataFormatSet] = _get_route_formats(args.via)
426
+
427
+ get_step(
428
+ input_format,
429
+ output_format,
430
+ route_formats=route_formats,
431
+ assign_guids=False,
432
+ )
433
+
434
+ output_path: Path | None = (
435
+ None if not args.output or args.output == "-" else Path(args.output)
436
+ )
437
+ intermediate_paths: list[tuple[str, Path]] = _validate_path_collisions(
438
+ args,
439
+ output_path=output_path,
440
+ output_format=output_format.format,
441
+ )
442
+ if (
443
+ output_path is not None
444
+ and output_path.exists()
445
+ and not args.yes
446
+ and args.input == "-"
447
+ ):
448
+ raise _OverwriteDeniedError(
449
+ "標準入力を入力に使う場合、既存出力を上書きするには-y/--yesの指定が必要です。"
450
+ )
451
+ if output_path is not None and not _confirm_overwrite(
452
+ output_path,
453
+ yes=args.yes,
454
+ interactive=args.interactive,
455
+ ):
456
+ raise _OverwriteDeniedError(
457
+ "出力先ファイルが既に存在します。上書きするには-y/--yesの指定が必要です。"
458
+ )
459
+
460
+ if not args.input or args.input == "-":
461
+ input_data: _Data = open_text_data(
462
+ common.read_stdin_xml(max_size=args.max_size, encoding=args.input_encoding),
463
+ input_format,
464
+ args.reporter,
465
+ max_size=args.max_size,
466
+ )
467
+ else:
468
+ input_data = open_data(
469
+ Path(args.input),
470
+ input_format,
471
+ args.reporter,
472
+ encoding=args.input_encoding,
473
+ max_size=args.max_size,
474
+ )
475
+
476
+ if isinstance(input_data, StbData):
477
+ args.print_stb_info(input_data.value)
478
+
479
+ observed_data: dict[_DataFormat, _Data] = {
480
+ input_data.format: input_data,
481
+ }
482
+
483
+ def on_step_completed(step_name: str, data: _Data) -> None:
484
+ args.logger.debug(
485
+ "変換ステップ完了: %s (%s)",
486
+ step_name,
487
+ data.format_set,
488
+ )
489
+ observed_data[data.format] = data
490
+
491
+ result: _ExecutionResult[_Data] = execute_conversion(
492
+ input_data,
493
+ output_format,
494
+ route_formats=route_formats,
495
+ reporter=args.reporter,
496
+ assign_guids=False,
497
+ on_step_completed=on_step_completed,
498
+ )
499
+ try:
500
+ if output_path is None:
501
+ sys.stdout.write(
502
+ to_text(
503
+ result.output,
504
+ args.reporter,
505
+ )
506
+ )
507
+ sys.stdout.flush()
508
+ else:
509
+ save_data(result.output, output_path, args.reporter)
510
+ _save_intermediate_data(
511
+ args,
512
+ intermediate_paths,
513
+ observed_data,
514
+ output_format.format,
515
+ )
516
+ finally:
517
+ cleanup_paths(result.cleanup_paths)
518
+
519
+
520
+ def register(subparsers: _SubParsersAction[Any]) -> None:
521
+ parser = subparsers.add_parser(
522
+ "convert",
523
+ parents=[make_global_parent()],
524
+ help=("ファイルフォーマット変換ツール。入出力形式を指定して変換します。"),
525
+ )
526
+ parser.add_argument(
527
+ "input",
528
+ nargs="?",
529
+ help="入力ファイルのパス。-で標準入力を使用します。",
530
+ )
531
+ parser.add_argument(
532
+ "-o",
533
+ "--output",
534
+ default="-",
535
+ help="出力ファイルのパス。-で標準出力を使用します。",
536
+ )
537
+ parser.add_argument(
538
+ "-f",
539
+ "--from",
540
+ dest="from_format",
541
+ help="入力ファイル形式。省略時は拡張子から推測します。",
542
+ )
543
+ parser.add_argument(
544
+ "-t",
545
+ "--to",
546
+ dest="to_format",
547
+ help="出力ファイル形式。省略時は拡張子から推測します。",
548
+ )
549
+ parser.add_argument(
550
+ "-y", "--yes", action="store_true", help="確認なしで上書きします。"
551
+ )
552
+ parser.add_argument(
553
+ "--input-encoding",
554
+ help="入力ファイルのエンコーディングを指定します。"
555
+ "省略時は自動検出またはutf-8を使用します。",
556
+ )
557
+ parser.add_argument(
558
+ "--via",
559
+ help=("中間形式を指定します。複数指定する場合はカンマで区切ります。"),
560
+ )
561
+ parser.add_argument(
562
+ "--save-intermediate",
563
+ help="中間ファイルを保存します。例: stb,ifc=result.ifc",
564
+ )
565
+ parser.add_argument(
566
+ "--list-formats",
567
+ action="store_true",
568
+ help="対応しているファイル形式を表示します。",
569
+ )
570
+
571
+ parser.set_defaults(func=_run, _parser=parser)
572
+
573
+
574
+ def _print_list_formats() -> None:
575
+ print("対応しているファイル形式")
576
+ for spec in _CLI_FORMAT_SPECS:
577
+ print(f"- {spec.label}: {'/'.join(spec.aliases)}")
578
+ for line in spec.description:
579
+ print(f" {line}")
580
+
581
+ print("対応している変換形式の組み合わせ")
582
+ for route in _CLI_ROUTE_SPECS:
583
+ print(f"- {route.title}")
584
+ print(f" {route.command_example}")
585
+ if route.note:
586
+ print(f" {route.note}")
587
+
588
+
589
+ def _get_save_intermediate(value: str | None) -> dict[str, str | None]:
590
+ result: dict[str, str | None] = {}
591
+ if not value:
592
+ return result
593
+ for raw_item in value.split(","):
594
+ item: str = raw_item.strip()
595
+ if not item:
596
+ continue
597
+ key, separator, path = item.partition("=")
598
+ key = key.strip()
599
+ if not key:
600
+ continue
601
+ result[key] = path.strip() if separator and path.strip() else None
602
+ return result
603
+
604
+
605
+ def _run(args: Namespace) -> None:
606
+ logger: Logger = get_logger(args)
607
+ logger.info("---変換処理開始---")
608
+ if args.list_formats:
609
+ _print_list_formats()
610
+ raise SystemExit(EXIT_OK)
611
+
612
+ interactive: bool = get_interactive(args)
613
+ input_path: str = args.input or ""
614
+ if not input_path:
615
+ if interactive:
616
+ print("入力ファイルパスを入力してください: ")
617
+ input_path = input().strip()
618
+ else:
619
+ input_path = "-"
620
+
621
+ output_path: str = args.output or ""
622
+ if not output_path:
623
+ if interactive:
624
+ print("出力ファイルパスを入力してください: ")
625
+ output_path = input().strip()
626
+ else:
627
+ output_path = "-"
628
+
629
+ from_format: str | None = args.from_format or _guess_format(input_path)
630
+ if not from_format and interactive:
631
+ print("入力ファイル形式を指定してください: ")
632
+ from_format = input().strip()
633
+ if not from_format:
634
+ args._parser.error("入力ファイル形式が不明です")
635
+ raise AssertionError("到達しない場所: ArgumentParser.error() must not return")
636
+
637
+ to_format: str | None = args.to_format or _guess_format(output_path)
638
+ if not to_format and interactive:
639
+ print("出力ファイル形式を指定してください: ")
640
+ to_format = input().strip()
641
+ if not to_format:
642
+ args._parser.error("出力ファイル形式が不明です")
643
+ raise AssertionError("到達しない場所: ArgumentParser.error() must not return")
644
+
645
+ collecting_reporter: CollectingReporter = CollectingReporter()
646
+ reporter: Reporter = get_reporter(logger=logger, reporter=collecting_reporter)
647
+ converter_args: ConverterArgs = ConverterArgs(
648
+ input=input_path,
649
+ output=output_path,
650
+ from_format=from_format,
651
+ to_format=to_format,
652
+ yes=bool(args.yes),
653
+ interactive=interactive,
654
+ input_encoding=args.input_encoding,
655
+ via=args.via,
656
+ save_intermediate=_get_save_intermediate(args.save_intermediate),
657
+ max_size=get_max_size(args),
658
+ logger=logger,
659
+ reporter=reporter,
660
+ )
661
+
662
+ try:
663
+ _pipeline_convert(converter_args)
664
+ except _UnsupportedConversionError as error:
665
+ logger.error(
666
+ "サポートされない変換です。\n"
667
+ f" 入力 format: {from_format} path:{input_path}\n"
668
+ f" 出力 format: {to_format} path:{output_path}\n"
669
+ f" 詳細: {error}"
670
+ )
671
+ raise SystemExit(EXIT_INVALID_ARGUMENT) from None
672
+ except _OverwriteDeniedError as error:
673
+ logger.error(str(error))
674
+ raise SystemExit(EXIT_INVALID_ARGUMENT) from None
675
+ except _OutputPathConflictError as error:
676
+ logger.error(str(error))
677
+ raise SystemExit(EXIT_INVALID_ARGUMENT) from None
678
+ except FileNotFoundError as error:
679
+ logger.error(f"入力ファイルが見つかりません: {error.filename or input_path}")
680
+ raise SystemExit(EXIT_INVALID_ARGUMENT) from None
681
+ except ImportError as error:
682
+ logger.error(str(error))
683
+ raise SystemExit(EXIT_OPTIONAL_DEPENDENCY_MISSING) from None
684
+ except DeveloperError:
685
+ raise
686
+ except (StbError, ParseError, UnicodeDecodeError) as error:
687
+ logger.error(f"入力ファイルを処理できません: {error}")
688
+ raise SystemExit(EXIT_EXECUTION_ERROR) from None
689
+
690
+ logger.info("---変換完了---")
691
+ if not collecting_reporter.is_valid(min_level=Severity.ERROR):
692
+ raise SystemExit(EXIT_ISSUE_FOUND)
693
+ raise SystemExit(EXIT_OK)