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.
- stbkit/__main__.py +10 -0
- stbkit/api/__init__.py +48 -0
- stbkit/api/experimental/__init__.py +19 -0
- stbkit/api/experimental/repository/__init__.py +30 -0
- stbkit/api/py.typed +0 -0
- stbkit/api/stb_latest.py +7 -0
- stbkit/api/stb_v2_0_2.py +11 -0
- stbkit/api/stb_v2_1_1.py +9 -0
- stbkit/cli/__init__.py +17 -0
- stbkit/cli/_internal/cli_main.py +114 -0
- stbkit/cli/_internal/common.py +126 -0
- stbkit/cli/_internal/constants_exit_code.py +59 -0
- stbkit/cli/_internal/converter.py +693 -0
- stbkit/cli/_internal/diff.py +213 -0
- stbkit/cli/_internal/entry_points.py +25 -0
- stbkit/cli/_internal/global_parent.py +70 -0
- stbkit/cli/_internal/show_versions.py +36 -0
- stbkit/cli/_internal/validate.py +310 -0
- stbkit/cli/commands/convert.py +16 -0
- stbkit/cli/commands/diff.py +16 -0
- stbkit/cli/commands/validate.py +16 -0
- stbkit/cli/py.typed +0 -0
- stbkit/tools/__init__.py +12 -0
- stbkit/tools/_internal/__init__.py +5 -0
- stbkit/tools/_internal/constants.py +21 -0
- stbkit/tools/_internal/data_model/building_geometry.py +1208 -0
- stbkit/tools/_internal/data_model/building_geometry_web.py +106 -0
- stbkit/tools/_internal/data_model/element_data.py +93 -0
- stbkit/tools/_internal/data_model/ifc_data/entity_data.py +67 -0
- stbkit/tools/_internal/data_model/ifc_data/entity_names.py +32 -0
- stbkit/tools/_internal/data_model/shape_data.py +1089 -0
- stbkit/tools/_internal/diff/__init__.py +5 -0
- stbkit/tools/_internal/diff/common.py +50 -0
- stbkit/tools/_internal/diff/diff_xml.py +796 -0
- stbkit/tools/_internal/matrix.py +51 -0
- stbkit/tools/_internal/optional_dependencies/__init__.py +5 -0
- stbkit/tools/_internal/optional_dependencies/ifc_common.py +28 -0
- stbkit/tools/_internal/optional_dependencies/ifc_exporter/ifc_entity_exporter.py +473 -0
- stbkit/tools/_internal/optional_dependencies/ifc_exporter/ifc_profile_exporter.py +267 -0
- stbkit/tools/_internal/optional_dependencies/ifc_exporter/ifc_representation_exporter.py +178 -0
- stbkit/tools/_internal/optional_dependencies/ifc_pipeline.py +133 -0
- stbkit/tools/_internal/optional_dependencies/ifc_to_building_geometry.py +64 -0
- stbkit/tools/_internal/stb_io.py +116 -0
- stbkit/tools/_internal/stb_repair/_common.py +24 -0
- stbkit/tools/_internal/stb_repair/_stb_repair.py +197 -0
- stbkit/tools/_internal/stb_repair/_stb_v2_0_2_repair.py +53 -0
- stbkit/tools/_internal/stb_repair/_stb_v2_1_0_repair.py +109 -0
- stbkit/tools/_internal/stb_repair/_stb_v2_1_1_repair.py +109 -0
- stbkit/tools/_internal/studio_report.py +49 -0
- stbkit/tools/_internal/utils/atomic_writer.py +57 -0
- stbkit/tools/_internal/utils/float_rounding.py +49 -0
- stbkit/tools/_internal/utils/girder_utils.py +91 -0
- stbkit/tools/_internal/utils/guid_utils.py +30 -0
- stbkit/tools/_internal/utils/mesh_utils.py +1305 -0
- stbkit/tools/_internal/utils/node_utils.py +50 -0
- stbkit/tools/_internal/utils/unit_utils.py +23 -0
- stbkit/tools/_internal/vectors.py +222 -0
- stbkit/tools/converters/__init__.py +39 -0
- stbkit/tools/converters/_internal/__init__.py +5 -0
- stbkit/tools/converters/_internal/building_geometry_to_ply_text.py +32 -0
- stbkit/tools/converters/_internal/coord_converter.py +151 -0
- stbkit/tools/converters/_internal/element_data_to_building_geometry.py +549 -0
- stbkit/tools/converters/_internal/ifc_to_ply_text.py +21 -0
- stbkit/tools/converters/_internal/name_converter.py +23 -0
- stbkit/tools/converters/_internal/pipeline/__init__.py +5 -0
- stbkit/tools/converters/_internal/pipeline/data.py +188 -0
- stbkit/tools/converters/_internal/pipeline/execution.py +569 -0
- stbkit/tools/converters/_internal/pipeline/ply_pipeline.py +52 -0
- stbkit/tools/converters/_internal/pipeline/stb_pipeline.py +226 -0
- stbkit/tools/converters/_internal/pipeline/stb_pipeline_latest.py +39 -0
- stbkit/tools/converters/_internal/pipeline/step.py +139 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/__init__.py +17 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/_stb_latest_to_element_data.py +186 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_footing.py +49 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_line.py +533 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_pile.py +100 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/member/member_plane.py +103 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/open.py +70 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec.py +214 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_beam_rc.py +67 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_beam_s.py +97 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_beam_src.py +28 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_brace_s.py +49 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_cft.py +217 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_rc.py +41 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_s.py +127 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_column_src.py +173 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_foundation.py +225 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_parapet_rc.py +53 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_pile_rc.py +106 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_pile_s.py +100 -0
- stbkit/tools/converters/_internal/stb_latest_to_element_data/section/sec_steel.py +432 -0
- stbkit/tools/converters/_internal/stb_to_building_geometry.py +44 -0
- stbkit/tools/converters/_internal/stb_to_ifc_data/__init__.py +5 -0
- stbkit/tools/converters/_internal/stb_to_ifc_data/_stb_to_ifc_data.py +59 -0
- stbkit/tools/converters/_internal/stb_to_ifc_data/convert_option.py +22 -0
- stbkit/tools/converters/_internal/stb_to_ply_text.py +22 -0
- stbkit/tools/py.typed +0 -0
- stbkit/tools/upgrade/__init__.py +13 -0
- stbkit/tools/upgrade/_internal/__init__.py +5 -0
- stbkit/tools/upgrade/_internal/common.py +205 -0
- stbkit/tools/upgrade/_internal/upgrade.py +228 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/__init__.py +60 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_common.py +62 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_joint_beam_shape_h.py +44 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_pile_rc.py +101 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_pile_s.py +78 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_slab_deck.py +56 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/hook_sec_slab_rc.py +128 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_girder.py +126 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_open.py +110 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_sec_beam_rc.py +227 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hook_funcs/post_sec_beam_s.py +1153 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/hooks.py +22 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/post.py +42 -0
- stbkit/tools/upgrade/_internal/v2_0_2_to_v2_1_0/tables.py +99 -0
- stbkit/tools/upgrade/_internal/v2_1_0_to_v2_1_1/__init__.py +58 -0
- stbkit/tools/upgrade/_internal/v2_1_0_to_v2_1_1/hooks.py +32 -0
- stbkit/tools/upgrade/_internal/v2_1_0_to_v2_1_1/tables.py +56 -0
- stbkit-0.1.0a9.dev1.dist-info/METADATA +196 -0
- stbkit-0.1.0a9.dev1.dist-info/RECORD +125 -0
- stbkit-0.1.0a9.dev1.dist-info/WHEEL +5 -0
- stbkit-0.1.0a9.dev1.dist-info/entry_points.txt +2 -0
- stbkit-0.1.0a9.dev1.dist-info/licenses/LICENSE +373 -0
- 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)
|