sldprt2xt 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sldprt2xt/__init__.py +23 -0
- sldprt2xt/builtin.py +121 -0
- sldprt2xt/carve.py +176 -0
- sldprt2xt/cli.py +140 -0
- sldprt2xt/container.py +374 -0
- sldprt2xt/convert.py +212 -0
- sldprt2xt/parasolid.py +413 -0
- sldprt2xt/schema_facts.py +274 -0
- sldprt2xt/schemas.py +121 -0
- sldprt2xt/xt/__init__.py +18 -0
- sldprt2xt/xt/binary.py +330 -0
- sldprt2xt/xt/edit.py +1144 -0
- sldprt2xt/xt/model.py +194 -0
- sldprt2xt/xt/schema.py +245 -0
- sldprt2xt/xt/text.py +384 -0
- sldprt2xt/xt/write.py +600 -0
- sldprt2xt-1.1.0.dist-info/METADATA +111 -0
- sldprt2xt-1.1.0.dist-info/RECORD +23 -0
- sldprt2xt-1.1.0.dist-info/WHEEL +5 -0
- sldprt2xt-1.1.0.dist-info/entry_points.txt +2 -0
- sldprt2xt-1.1.0.dist-info/licenses/LICENSE +661 -0
- sldprt2xt-1.1.0.dist-info/licenses/NOTICE +84 -0
- sldprt2xt-1.1.0.dist-info/top_level.txt +1 -0
sldprt2xt/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 Emilien-Etadam
|
|
3
|
+
|
|
4
|
+
"""Extraire la géométrie Parasolid d'un fichier SolidWorks, sans SolidWorks.
|
|
5
|
+
|
|
6
|
+
from sldprt2xt import to_x_t
|
|
7
|
+
|
|
8
|
+
to_x_t("piece.SLDPRT") # écrit piece.x_t à côté
|
|
9
|
+
to_x_t("piece.SLDPRT", "sorties/") # ailleurs
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .convert import ConversionError, bodies_in, to_x_t
|
|
13
|
+
from .schemas import SchemasNotFound, find_folder
|
|
14
|
+
|
|
15
|
+
__version__ = "1.1.0"
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ConversionError",
|
|
18
|
+
"SchemasNotFound",
|
|
19
|
+
"__version__",
|
|
20
|
+
"bodies_in",
|
|
21
|
+
"find_folder",
|
|
22
|
+
"to_x_t",
|
|
23
|
+
]
|
sldprt2xt/builtin.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 Emilien-Etadam
|
|
3
|
+
|
|
4
|
+
"""Les schémas que le paquet porte en lui — l'autonomie.
|
|
5
|
+
|
|
6
|
+
Trois sources, du plus sûr au plus souple :
|
|
7
|
+
|
|
8
|
+
- :func:`base_schema` — le schéma de base 13006, bâti depuis
|
|
9
|
+
:mod:`sldprt2xt.schema_facts`. Suffit à décoder n'importe quel transmit :
|
|
10
|
+
le fichier épisse lui-même la disposition de chaque type de nœud.
|
|
11
|
+
- :func:`envelope_for` — les dispositions ASSEMBLY/INSTANCE/TRANSFORM de la
|
|
12
|
+
version demandée, si cette version est dans la table. C'est ce que
|
|
13
|
+
l'écriture multi-corps exige et que rien dans une partition ne porte.
|
|
14
|
+
- :func:`envelope_from_x_t` — les mêmes dispositions, apprises d'un ``.x_t``
|
|
15
|
+
donneur exporté par le même SolidWorks : un export multi-corps greffe ces
|
|
16
|
+
dispositions dans son propre texte. C'est la sortie pour une version plus
|
|
17
|
+
récente que la table.
|
|
18
|
+
|
|
19
|
+
Un fichier de schéma déposé prime toujours sur tout ceci — voir
|
|
20
|
+
:mod:`sldprt2xt.convert`.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from functools import cache
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
from .schema_facts import BASE_KEY, BASE_NODES, ENVELOPE_BY_VERSION, ENVELOPE_FAMILIES
|
|
29
|
+
from .xt.schema import FieldSpec, NodeSpec, Schema
|
|
30
|
+
|
|
31
|
+
#: Les trois types de l'enveloppe, dans l'ordre des familles de la table.
|
|
32
|
+
_ASSEMBLY, _INSTANCE, _TRANSFORM = 10, 11, 100
|
|
33
|
+
_ENVELOPE_NAMES = {_ASSEMBLY: "ASSEMBLY", _INSTANCE: "INSTANCE", _TRANSFORM: "TRANSFORM"}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _fields(rows) -> tuple[FieldSpec, ...]:
|
|
37
|
+
return tuple(
|
|
38
|
+
FieldSpec(name=name, type=kind, transmitted=sent, ptr_class=at, n_elts=n)
|
|
39
|
+
for name, kind, sent, at, n in rows
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@cache
|
|
44
|
+
def base_schema() -> Schema:
|
|
45
|
+
"""Le schéma de base 13006, sans fichier."""
|
|
46
|
+
nodes = {
|
|
47
|
+
node_type: NodeSpec(
|
|
48
|
+
node_type=node_type,
|
|
49
|
+
name=name,
|
|
50
|
+
description="",
|
|
51
|
+
transmitted=sent,
|
|
52
|
+
variable=variable,
|
|
53
|
+
fields=_fields(rows),
|
|
54
|
+
)
|
|
55
|
+
for node_type, (name, sent, variable, rows) in BASE_NODES.items()
|
|
56
|
+
}
|
|
57
|
+
return Schema(key=BASE_KEY, modeller_version=None, nodes=nodes)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@cache
|
|
61
|
+
def envelope_for(version: str | int) -> Schema | None:
|
|
62
|
+
"""Les dispositions d'enveloppe de *version*, ou ``None`` si inconnue.
|
|
63
|
+
|
|
64
|
+
Le schéma rendu ne porte que les trois types de l'enveloppe : c'est tout
|
|
65
|
+
ce que l'écriture demande, et le recoupement de ``verify`` ne s'applique
|
|
66
|
+
alors qu'à eux — exactement ceux que nous venons d'écrire.
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
family = ENVELOPE_BY_VERSION[int(version)]
|
|
70
|
+
except (KeyError, ValueError):
|
|
71
|
+
return None
|
|
72
|
+
return _envelope(f"SCH_{version}", ENVELOPE_FAMILIES[family])
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def envelope_from_x_t(path: str | Path) -> Schema:
|
|
76
|
+
"""Les dispositions d'enveloppe apprises d'un ``.x_t`` donneur.
|
|
77
|
+
|
|
78
|
+
Le donneur doit être un export SolidWorks portant une enveloppe — un
|
|
79
|
+
multi-corps en porte toujours une. Ses greffes déclarent les dispositions
|
|
80
|
+
exactes que sa version écrit ; on les lit au lieu d'exiger la table.
|
|
81
|
+
"""
|
|
82
|
+
from .parasolid import TransmitError, read_text_transmit
|
|
83
|
+
from .xt import text as xt_text
|
|
84
|
+
|
|
85
|
+
data = Path(path).read_bytes()
|
|
86
|
+
transmit = read_text_transmit(data)
|
|
87
|
+
body = transmit.data[transmit.body_offset :].decode("latin-1")
|
|
88
|
+
layouts: dict[int, list[FieldSpec]] = {}
|
|
89
|
+
xt_text.parse_nodes(body, base_schema(), layouts=layouts)
|
|
90
|
+
|
|
91
|
+
missing = [t for t in (_ASSEMBLY, _INSTANCE, _TRANSFORM) if t not in layouts]
|
|
92
|
+
if missing:
|
|
93
|
+
raise TransmitError(
|
|
94
|
+
f"{Path(path).name} : pas d'enveloppe ASSEMBLY dans ce donneur — "
|
|
95
|
+
"prenez un .x_t multi-corps exporté par le même SolidWorks"
|
|
96
|
+
)
|
|
97
|
+
return _envelope(
|
|
98
|
+
f"SCH_{transmit.version_schema}",
|
|
99
|
+
tuple(
|
|
100
|
+
tuple(
|
|
101
|
+
(f.name, f.type, f.transmitted, f.ptr_class, f.n_elts)
|
|
102
|
+
for f in layouts[t]
|
|
103
|
+
)
|
|
104
|
+
for t in (_ASSEMBLY, _INSTANCE, _TRANSFORM)
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _envelope(key: str, family) -> Schema:
|
|
110
|
+
nodes = {
|
|
111
|
+
node_type: NodeSpec(
|
|
112
|
+
node_type=node_type,
|
|
113
|
+
name=_ENVELOPE_NAMES[node_type],
|
|
114
|
+
description="",
|
|
115
|
+
transmitted=True,
|
|
116
|
+
variable=False,
|
|
117
|
+
fields=_fields(rows),
|
|
118
|
+
)
|
|
119
|
+
for node_type, rows in zip((_ASSEMBLY, _INSTANCE, _TRANSFORM), family, strict=True)
|
|
120
|
+
}
|
|
121
|
+
return Schema(key=key, modeller_version=None, nodes=nodes)
|
sldprt2xt/carve.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 Emilien-Etadam
|
|
3
|
+
|
|
4
|
+
"""Find and inflate compressed regions inside a container blob.
|
|
5
|
+
|
|
6
|
+
Two mechanisms are known:
|
|
7
|
+
|
|
8
|
+
*wrapped sections*
|
|
9
|
+
A 16-byte magic followed by an uncompressed size, a compressed size and
|
|
10
|
+
a zlib member. This is what sits inside ``__ZLB`` compound streams and
|
|
11
|
+
what splits a ``Config-N-Partition`` stream into its transmit sections.
|
|
12
|
+
|
|
13
|
+
*bare zlib members*
|
|
14
|
+
Everything else. We scan for zlib headers and keep whatever inflates to
|
|
15
|
+
a non-trivial length. This is a discovery aid, deliberately noisy: it is
|
|
16
|
+
for looking at an unfamiliar stream, not for production extraction.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import struct
|
|
22
|
+
import zlib
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
|
|
25
|
+
from .container import SECTION_MAGIC
|
|
26
|
+
|
|
27
|
+
ZLIB_HEADERS = (b"\x78\x01", b"\x78\x9c", b"\x78\xda", b"\x78\x5e")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Region:
|
|
32
|
+
"""One inflated region of a blob."""
|
|
33
|
+
|
|
34
|
+
#: ``"section"`` (magic-framed) or ``"zlib"`` (bare scan hit).
|
|
35
|
+
kind: str
|
|
36
|
+
#: Offset of the region's frame within the containing blob.
|
|
37
|
+
offset: int
|
|
38
|
+
data: bytes
|
|
39
|
+
#: Fields read from the frame, for the record.
|
|
40
|
+
header: dict[str, int]
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def size(self) -> int:
|
|
44
|
+
return len(self.data)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def carve_sections(blob: bytes) -> list[Region]:
|
|
48
|
+
"""Inflate every magic-framed section found in ``blob``.
|
|
49
|
+
|
|
50
|
+
A section is a length-prefixed run of independently compressed members::
|
|
51
|
+
|
|
52
|
+
-0x04 u32 LE section length, counted from the magic
|
|
53
|
+
+0x00 16 B SECTION_MAGIC
|
|
54
|
+
+0x10 member, member, …, then a (0, 0) header to close the run
|
|
55
|
+
|
|
56
|
+
and each member is::
|
|
57
|
+
|
|
58
|
+
u32 LE uncompressed size
|
|
59
|
+
u32 LE compressed size
|
|
60
|
+
zlib member of exactly that many bytes
|
|
61
|
+
|
|
62
|
+
**Members must all be inflated and concatenated.** A small part fits in
|
|
63
|
+
one, which is why a single-member reading appears to work; a large one is
|
|
64
|
+
split into many, and stopping at the first truncates the payload without
|
|
65
|
+
any error — the exact failure this project cannot afford.
|
|
66
|
+
"""
|
|
67
|
+
regions: list[Region] = []
|
|
68
|
+
pos = blob.find(SECTION_MAGIC)
|
|
69
|
+
while pos != -1:
|
|
70
|
+
at = pos + len(SECTION_MAGIC)
|
|
71
|
+
|
|
72
|
+
# The length prefix bounds the member run. Without it — a magic at the
|
|
73
|
+
# very start of a blob — fall back to the end of the blob and let the
|
|
74
|
+
# member headers say where to stop.
|
|
75
|
+
end = len(blob)
|
|
76
|
+
if pos >= 4:
|
|
77
|
+
declared = struct.unpack_from("<I", blob, pos - 4)[0]
|
|
78
|
+
if 0 < declared <= len(blob) - pos + 4:
|
|
79
|
+
end = min(end, pos - 4 + 4 + declared)
|
|
80
|
+
|
|
81
|
+
parts: list[bytes] = []
|
|
82
|
+
members = 0
|
|
83
|
+
while at + 8 <= end:
|
|
84
|
+
uncomp_sz, comp_sz = struct.unpack_from("<II", blob, at)
|
|
85
|
+
if uncomp_sz == 0 and comp_sz == 0:
|
|
86
|
+
at += 8
|
|
87
|
+
break
|
|
88
|
+
if comp_sz == 0 or at + 8 + comp_sz > end:
|
|
89
|
+
break
|
|
90
|
+
try:
|
|
91
|
+
raw = zlib.decompress(blob[at + 8 : at + 8 + comp_sz])
|
|
92
|
+
except zlib.error:
|
|
93
|
+
break
|
|
94
|
+
if len(raw) != uncomp_sz:
|
|
95
|
+
break
|
|
96
|
+
parts.append(raw)
|
|
97
|
+
members += 1
|
|
98
|
+
at += 8 + comp_sz
|
|
99
|
+
|
|
100
|
+
if parts:
|
|
101
|
+
regions.append(
|
|
102
|
+
Region(
|
|
103
|
+
kind="section",
|
|
104
|
+
offset=pos,
|
|
105
|
+
data=b"".join(parts),
|
|
106
|
+
header={
|
|
107
|
+
"members": members,
|
|
108
|
+
"uncompressed": sum(len(p) for p in parts),
|
|
109
|
+
"consumed": at - pos,
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
pos = blob.find(SECTION_MAGIC, at)
|
|
114
|
+
continue
|
|
115
|
+
pos = blob.find(SECTION_MAGIC, pos + 1)
|
|
116
|
+
return regions
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def carve_zlib(blob: bytes, min_size: int = 64, limit: int = 64) -> list[Region]:
|
|
120
|
+
"""Scan for bare zlib members and inflate them.
|
|
121
|
+
|
|
122
|
+
Returns at most ``limit`` regions of at least ``min_size`` inflated bytes.
|
|
123
|
+
Overlapping hits are suppressed: a member found inside an already-inflated
|
|
124
|
+
span is skipped.
|
|
125
|
+
"""
|
|
126
|
+
regions: list[Region] = []
|
|
127
|
+
consumed_until = 0
|
|
128
|
+
pos = 0
|
|
129
|
+
while pos < len(blob) and len(regions) < limit:
|
|
130
|
+
nxt = min(
|
|
131
|
+
(p for p in (blob.find(h, pos) for h in ZLIB_HEADERS) if p != -1),
|
|
132
|
+
default=-1,
|
|
133
|
+
)
|
|
134
|
+
if nxt == -1:
|
|
135
|
+
break
|
|
136
|
+
if nxt < consumed_until:
|
|
137
|
+
pos = nxt + 1
|
|
138
|
+
continue
|
|
139
|
+
obj = zlib.decompressobj()
|
|
140
|
+
try:
|
|
141
|
+
raw = obj.decompress(blob[nxt:])
|
|
142
|
+
except zlib.error:
|
|
143
|
+
raw = b""
|
|
144
|
+
if len(raw) >= min_size:
|
|
145
|
+
used = len(blob) - nxt - len(obj.unused_data)
|
|
146
|
+
regions.append(
|
|
147
|
+
Region(
|
|
148
|
+
kind="zlib",
|
|
149
|
+
offset=nxt,
|
|
150
|
+
data=raw,
|
|
151
|
+
header={
|
|
152
|
+
"actual_uncompressed": len(raw),
|
|
153
|
+
"actual_compressed": used,
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
consumed_until = nxt + used
|
|
158
|
+
pos = consumed_until
|
|
159
|
+
else:
|
|
160
|
+
pos = nxt + 1
|
|
161
|
+
return regions
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def carve(blob: bytes, *, scan_bare: bool = True) -> list[Region]:
|
|
165
|
+
"""Inflate framed sections, then bare zlib members outside them."""
|
|
166
|
+
regions = carve_sections(blob)
|
|
167
|
+
if not scan_bare:
|
|
168
|
+
return regions
|
|
169
|
+
covered = [
|
|
170
|
+
(r.offset, r.offset + r.header.get("consumed", 0)) for r in regions
|
|
171
|
+
]
|
|
172
|
+
for region in carve_zlib(blob):
|
|
173
|
+
if any(start <= region.offset < end for start, end in covered):
|
|
174
|
+
continue
|
|
175
|
+
regions.append(region)
|
|
176
|
+
return sorted(regions, key=lambda r: r.offset)
|
sldprt2xt/cli.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 Emilien-Etadam
|
|
3
|
+
|
|
4
|
+
"""La ligne de commande."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import glob as globbing
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from .convert import ConversionError, to_x_t
|
|
15
|
+
from .schemas import SchemasNotFound, find_folder
|
|
16
|
+
|
|
17
|
+
USAGE = """\
|
|
18
|
+
sldprt2xt piece.SLDPRT le .x_t se pose a cote
|
|
19
|
+
sldprt2xt *.SLDPRT -o sorties/ plusieurs fichiers
|
|
20
|
+
sldprt2xt dossier/ -o sorties/ tout un dossier, sous-dossiers compris"""
|
|
21
|
+
|
|
22
|
+
#: SolidWorks pose un verrou ``~$piece.SLDPRT`` à côté de toute pièce ouverte.
|
|
23
|
+
#: Ce n'est pas une pièce, et un balayage qui le ramasse fait échouer le lot.
|
|
24
|
+
LOCK_PREFIX = "~$"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parts_under(paths: list[Path]) -> list[tuple[Path, Path]]:
|
|
28
|
+
"""Les pièces désignées, chacune avec son nom de sortie relatif.
|
|
29
|
+
|
|
30
|
+
Un dossier vaut tout ce qu'il contient, sous-dossiers compris — et la
|
|
31
|
+
hiérarchie suit : deux pièces de même nom dans deux sous-dossiers gardent
|
|
32
|
+
deux sorties. Les jokers sont développés ici parce que cmd et PowerShell
|
|
33
|
+
ne le font pas, et les verrous SolidWorks sont écartés des balayages ; un
|
|
34
|
+
fichier nommé explicitement passe tel quel, c'est lui le chef.
|
|
35
|
+
"""
|
|
36
|
+
found: list[tuple[Path, Path]] = []
|
|
37
|
+
for path, named_explicitly in _expanded(paths):
|
|
38
|
+
if path.is_dir():
|
|
39
|
+
for part in sorted(path.rglob("*")):
|
|
40
|
+
if part.suffix.lower() != ".sldprt":
|
|
41
|
+
continue
|
|
42
|
+
if part.name.startswith(LOCK_PREFIX):
|
|
43
|
+
continue
|
|
44
|
+
found.append((part, part.relative_to(path).with_suffix(".x_t")))
|
|
45
|
+
elif named_explicitly or not path.name.startswith(LOCK_PREFIX):
|
|
46
|
+
found.append((path, Path(path.stem + ".x_t")))
|
|
47
|
+
return found
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _expanded(paths: list[Path]) -> list[tuple[Path, bool]]:
|
|
51
|
+
"""Chaque argument, joker développé — avec qui fut nommé tel quel.
|
|
52
|
+
|
|
53
|
+
Un motif qui matche est un balayage (ses verrous s'écartent) ; un chemin
|
|
54
|
+
sans joker, ou un motif qui ne matche rien, reste l'argument de
|
|
55
|
+
l'utilisateur et sera traité — ou refusé — sous son propre nom.
|
|
56
|
+
"""
|
|
57
|
+
out: list[tuple[Path, bool]] = []
|
|
58
|
+
for path in paths:
|
|
59
|
+
raw = str(path)
|
|
60
|
+
if any(mark in raw for mark in "*?[") and not path.exists():
|
|
61
|
+
matches = sorted(Path(found) for found in globbing.glob(raw))
|
|
62
|
+
if matches:
|
|
63
|
+
out.extend((match, False) for match in matches)
|
|
64
|
+
continue
|
|
65
|
+
out.append((path, True))
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def main(argv: list[str] | None = None) -> int:
|
|
70
|
+
parser = argparse.ArgumentParser(
|
|
71
|
+
prog="sldprt2xt",
|
|
72
|
+
description="Extraire la geometrie Parasolid d'un fichier SolidWorks.",
|
|
73
|
+
epilog=USAGE,
|
|
74
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument("paths", nargs="+", type=Path, metavar="FICHIER")
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"-o", "--out", type=Path, metavar="DOSSIER", help="ou ecrire les .x_t"
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--schemas", type=Path, metavar="DOSSIER", help="dossier des schemas Parasolid"
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--donor",
|
|
85
|
+
type=Path,
|
|
86
|
+
metavar="FICHIER.x_t",
|
|
87
|
+
help="un .x_t multi-corps exporte par le meme SolidWorks, si la "
|
|
88
|
+
"version du fichier est plus recente que les tables integrees",
|
|
89
|
+
)
|
|
90
|
+
parser.add_argument(
|
|
91
|
+
"-q", "--quiet", action="store_true", help="ne rien dire si tout va bien"
|
|
92
|
+
)
|
|
93
|
+
args = parser.parse_args(argv)
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
folder = find_folder(args.schemas)
|
|
97
|
+
except SchemasNotFound as missing:
|
|
98
|
+
print(missing, file=sys.stderr)
|
|
99
|
+
return 2
|
|
100
|
+
|
|
101
|
+
parts = parts_under(args.paths)
|
|
102
|
+
if not parts:
|
|
103
|
+
print("aucun fichier .SLDPRT la-dedans", file=sys.stderr)
|
|
104
|
+
return 1
|
|
105
|
+
|
|
106
|
+
done = failed = 0
|
|
107
|
+
written: dict[Path, Path] = {}
|
|
108
|
+
for part, relative in parts:
|
|
109
|
+
destination = args.out / relative if args.out else part.with_suffix(".x_t")
|
|
110
|
+
if destination in written:
|
|
111
|
+
print(
|
|
112
|
+
f"{part} : meme destination que {written[destination]} "
|
|
113
|
+
f"({destination}) — non ecrit",
|
|
114
|
+
file=sys.stderr,
|
|
115
|
+
)
|
|
116
|
+
failed += 1
|
|
117
|
+
continue
|
|
118
|
+
started = time.monotonic()
|
|
119
|
+
try:
|
|
120
|
+
result = to_x_t(part, destination, schemas=folder, donor=args.donor)
|
|
121
|
+
except ConversionError as failure:
|
|
122
|
+
print(f"{part.name} : {failure}", file=sys.stderr)
|
|
123
|
+
failed += 1
|
|
124
|
+
except Exception as failure: # un fichier en echec n'arrete pas le lot
|
|
125
|
+
print(f"{part.name} : {type(failure).__name__}: {failure}", file=sys.stderr)
|
|
126
|
+
failed += 1
|
|
127
|
+
else:
|
|
128
|
+
done += 1
|
|
129
|
+
written[destination] = part
|
|
130
|
+
if not args.quiet:
|
|
131
|
+
size = result.stat().st_size / 1e6
|
|
132
|
+
print(f"{result} {size:.2f} Mo ({time.monotonic() - started:.1f} s)")
|
|
133
|
+
|
|
134
|
+
if len(parts) > 1 and not args.quiet:
|
|
135
|
+
print(f"\n{done} converti(s), {failed} en echec")
|
|
136
|
+
return 1 if failed else 0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
raise SystemExit(main())
|