v8unpack-agent 0.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.
- v8unpack_agent/__init__.py +322 -0
- v8unpack_agent/_safe_paths.py +135 -0
- v8unpack_agent/catalog_resolver.py +354 -0
- v8unpack_agent/chain_data_path.py +546 -0
- v8unpack_agent/cli.py +230 -0
- v8unpack_agent/common_modules.py +117 -0
- v8unpack_agent/coverage_metric.py +364 -0
- v8unpack_agent/drift_checker.py +494 -0
- v8unpack_agent/elem_parser.py +1452 -0
- v8unpack_agent/form_artifact.py +118 -0
- v8unpack_agent/form_classifier.py +462 -0
- v8unpack_agent/form_context.py +493 -0
- v8unpack_agent/form_identity.py +299 -0
- v8unpack_agent/form_paths.py +81 -0
- v8unpack_agent/form_router.py +168 -0
- v8unpack_agent/form_summary.py +163 -0
- v8unpack_agent/forms_index.py +145 -0
- v8unpack_agent/managed_form_summary.py +71 -0
- v8unpack_agent/managed_forms.py +338 -0
- v8unpack_agent/object_decoder.py +662 -0
- v8unpack_agent/pipeline.py +202 -0
- v8unpack_agent/platform_reference_types.py +68 -0
- v8unpack_agent/run_report.py +381 -0
- v8unpack_agent/runner.py +583 -0
- v8unpack_agent/scan_forms.py +998 -0
- v8unpack_agent/skd_extractor.py +273 -0
- v8unpack_agent-0.1.0.dist-info/METADATA +285 -0
- v8unpack_agent-0.1.0.dist-info/RECORD +32 -0
- v8unpack_agent-0.1.0.dist-info/WHEEL +5 -0
- v8unpack_agent-0.1.0.dist-info/entry_points.txt +2 -0
- v8unpack_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- v8unpack_agent-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""v8unpack-agent — надстройка над v8unpack для агентных пайплайнов.
|
|
2
|
+
|
|
3
|
+
Реализует доработки из статьи «Обычные формы 1С в агентном пайплайне:
|
|
4
|
+
пошаговая распаковка»: фабрику путей по конвенции, FormArtifact с флагом
|
|
5
|
+
полноты распаковки, forms_index с контролем рассинхрона и распаковку как
|
|
6
|
+
pre-step индексации.
|
|
7
|
+
|
|
8
|
+
Публичная поверхность
|
|
9
|
+
---------------------
|
|
10
|
+
- :func:`~v8unpack_agent.form_paths.form_paths`
|
|
11
|
+
- :func:`~v8unpack_agent.form_paths.item_modules`
|
|
12
|
+
- :func:`~v8unpack_agent.form_paths.all_module_paths`
|
|
13
|
+
- :class:`~v8unpack_agent.form_artifact.FormArtifact`
|
|
14
|
+
- :class:`~v8unpack_agent.forms_index.FormsIndex`
|
|
15
|
+
- :class:`~v8unpack_agent.forms_index.FormsIndexEntry`
|
|
16
|
+
- :func:`~v8unpack_agent.forms_index.is_form_stale`
|
|
17
|
+
- :func:`~v8unpack_agent.pipeline.unpack_all_forms`
|
|
18
|
+
- :func:`~v8unpack_agent.pipeline.update_forms_index`
|
|
19
|
+
- :func:`~v8unpack_agent.pipeline.discover_form_bins`
|
|
20
|
+
- :func:`~v8unpack_agent.scan_forms.scan_forms`
|
|
21
|
+
- :class:`~v8unpack_agent.scan_forms.FormEntry`
|
|
22
|
+
- :class:`~v8unpack_agent.scan_forms.FormScanIndex`
|
|
23
|
+
- :func:`~v8unpack_agent.drift_checker.check_drift`
|
|
24
|
+
- :class:`~v8unpack_agent.drift_checker.DriftReport`
|
|
25
|
+
- :func:`~v8unpack_agent.managed_forms.discover_elem_forms` (issue #55)
|
|
26
|
+
- :class:`~v8unpack_agent.managed_forms.ElemFormEntry` (issue #55)
|
|
27
|
+
- :class:`~v8unpack_agent.form_summary.FormSummary` (issue #69)
|
|
28
|
+
- :func:`~v8unpack_agent.form_summary.build_form_summary` (issue #69)
|
|
29
|
+
- :func:`~v8unpack_agent.form_summary.build_form_summary_from_elem_index` (issue #69)
|
|
30
|
+
- :class:`~v8unpack_agent.form_classifier.FormClass` (issue #98)
|
|
31
|
+
- :func:`~v8unpack_agent.form_classifier.classify_form` (issue #98)
|
|
32
|
+
- :func:`~v8unpack_agent.form_classifier.classify_form_by_name` (issue #98)
|
|
33
|
+
- :func:`~v8unpack_agent.form_classifier.classify_form_by_bindings` (issue #98)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from v8unpack_agent.form_paths import (
|
|
37
|
+
all_module_paths,
|
|
38
|
+
form_paths,
|
|
39
|
+
form_root,
|
|
40
|
+
item_modules,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def __getattr__(name: str):
|
|
45
|
+
"""Lazy-load selected exports to keep `python -m v8unpack_agent.scan_forms` clean."""
|
|
46
|
+
if name in {
|
|
47
|
+
"SERVICE_FORM_NAME_PATTERNS",
|
|
48
|
+
"FormClass",
|
|
49
|
+
"classify_form",
|
|
50
|
+
"classify_form_by_bindings",
|
|
51
|
+
"classify_form_by_name",
|
|
52
|
+
}:
|
|
53
|
+
# Ленивая группа form_classifier (issue #140, часть A). Вместе с ней
|
|
54
|
+
# перестаёт загружаться транзитивный coverage_metric: его тянет
|
|
55
|
+
# form_classifier ради DATA_ELEMENT_TYPES.
|
|
56
|
+
from v8unpack_agent.form_classifier import (
|
|
57
|
+
SERVICE_FORM_NAME_PATTERNS,
|
|
58
|
+
FormClass,
|
|
59
|
+
classify_form,
|
|
60
|
+
classify_form_by_bindings,
|
|
61
|
+
classify_form_by_name,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
values = {
|
|
65
|
+
"SERVICE_FORM_NAME_PATTERNS": SERVICE_FORM_NAME_PATTERNS,
|
|
66
|
+
"FormClass": FormClass,
|
|
67
|
+
"classify_form": classify_form,
|
|
68
|
+
"classify_form_by_bindings": classify_form_by_bindings,
|
|
69
|
+
"classify_form_by_name": classify_form_by_name,
|
|
70
|
+
}
|
|
71
|
+
globals().update(values)
|
|
72
|
+
return values[name]
|
|
73
|
+
if name in {"FormRouter", "RouteResult"}:
|
|
74
|
+
# Ленивая группа form_router (issue #140, часть B). Публичные имена
|
|
75
|
+
# FormRouter и RouteResult остаются в __all__ и доступны как раньше.
|
|
76
|
+
from v8unpack_agent.form_router import FormRouter, RouteResult
|
|
77
|
+
|
|
78
|
+
values = {
|
|
79
|
+
"FormRouter": FormRouter,
|
|
80
|
+
"RouteResult": RouteResult,
|
|
81
|
+
}
|
|
82
|
+
globals().update(values)
|
|
83
|
+
return values[name]
|
|
84
|
+
if name in {"FormsIndex", "FormsIndexEntry", "is_form_stale"}:
|
|
85
|
+
from v8unpack_agent.forms_index import (
|
|
86
|
+
FormsIndex,
|
|
87
|
+
FormsIndexEntry,
|
|
88
|
+
is_form_stale,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
values = {
|
|
92
|
+
"FormsIndex": FormsIndex,
|
|
93
|
+
"FormsIndexEntry": FormsIndexEntry,
|
|
94
|
+
"is_form_stale": is_form_stale,
|
|
95
|
+
}
|
|
96
|
+
globals().update(values)
|
|
97
|
+
return values[name]
|
|
98
|
+
|
|
99
|
+
if name in {"SkdResult", "SkdBatchResult", "extract_skd_queries",
|
|
100
|
+
"extract_all_skd_queries"}:
|
|
101
|
+
from v8unpack_agent.skd_extractor import (
|
|
102
|
+
SkdBatchResult,
|
|
103
|
+
SkdResult,
|
|
104
|
+
extract_all_skd_queries,
|
|
105
|
+
extract_skd_queries,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
values = {
|
|
109
|
+
"SkdResult": SkdResult,
|
|
110
|
+
"SkdBatchResult": SkdBatchResult,
|
|
111
|
+
"extract_skd_queries": extract_skd_queries,
|
|
112
|
+
"extract_all_skd_queries": extract_all_skd_queries,
|
|
113
|
+
}
|
|
114
|
+
globals().update(values)
|
|
115
|
+
return values[name]
|
|
116
|
+
|
|
117
|
+
if name in {"ElemIndexResult", "parse_elem_json"}:
|
|
118
|
+
from v8unpack_agent.elem_parser import ElemIndexResult, parse_elem_json
|
|
119
|
+
|
|
120
|
+
values = {
|
|
121
|
+
"ElemIndexResult": ElemIndexResult,
|
|
122
|
+
"parse_elem_json": parse_elem_json,
|
|
123
|
+
}
|
|
124
|
+
globals().update(values)
|
|
125
|
+
return values[name]
|
|
126
|
+
|
|
127
|
+
if name in {"FormUnpacker", "ErfUnpacker", "discover_form_bins",
|
|
128
|
+
"unpack_all_forms", "unpack_erf", "update_forms_index"}:
|
|
129
|
+
from v8unpack_agent.pipeline import (
|
|
130
|
+
ErfUnpacker,
|
|
131
|
+
FormUnpacker,
|
|
132
|
+
discover_form_bins,
|
|
133
|
+
unpack_all_forms,
|
|
134
|
+
unpack_erf,
|
|
135
|
+
update_forms_index,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
values = {
|
|
139
|
+
"FormUnpacker": FormUnpacker,
|
|
140
|
+
"ErfUnpacker": ErfUnpacker,
|
|
141
|
+
"discover_form_bins": discover_form_bins,
|
|
142
|
+
"unpack_all_forms": unpack_all_forms,
|
|
143
|
+
"unpack_erf": unpack_erf,
|
|
144
|
+
"update_forms_index": update_forms_index,
|
|
145
|
+
}
|
|
146
|
+
globals().update(values)
|
|
147
|
+
return values[name]
|
|
148
|
+
|
|
149
|
+
if name in {"FormArtifact"}:
|
|
150
|
+
from v8unpack_agent.form_artifact import FormArtifact
|
|
151
|
+
|
|
152
|
+
values = {
|
|
153
|
+
"FormArtifact": FormArtifact,
|
|
154
|
+
}
|
|
155
|
+
globals().update(values)
|
|
156
|
+
return values[name]
|
|
157
|
+
|
|
158
|
+
if name in {"check_drift", "DriftReport"}:
|
|
159
|
+
from v8unpack_agent.drift_checker import DriftReport, check_drift
|
|
160
|
+
|
|
161
|
+
values = {
|
|
162
|
+
"check_drift": check_drift,
|
|
163
|
+
"DriftReport": DriftReport,
|
|
164
|
+
}
|
|
165
|
+
globals().update(values)
|
|
166
|
+
return values[name]
|
|
167
|
+
|
|
168
|
+
if name in {
|
|
169
|
+
"AmbiguousFormNameError",
|
|
170
|
+
"FormBinSource",
|
|
171
|
+
"FormIdentityError",
|
|
172
|
+
"adapt_legacy_unpacker",
|
|
173
|
+
"discover_form_sources",
|
|
174
|
+
}:
|
|
175
|
+
from v8unpack_agent.form_identity import (
|
|
176
|
+
AmbiguousFormNameError,
|
|
177
|
+
FormBinSource,
|
|
178
|
+
FormIdentityError,
|
|
179
|
+
adapt_legacy_unpacker,
|
|
180
|
+
discover_form_sources,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
_identity_exports = {
|
|
184
|
+
"AmbiguousFormNameError": AmbiguousFormNameError,
|
|
185
|
+
"FormBinSource": FormBinSource,
|
|
186
|
+
"FormIdentityError": FormIdentityError,
|
|
187
|
+
"adapt_legacy_unpacker": adapt_legacy_unpacker,
|
|
188
|
+
"discover_form_sources": discover_form_sources,
|
|
189
|
+
}
|
|
190
|
+
globals().update(_identity_exports)
|
|
191
|
+
return _identity_exports[name]
|
|
192
|
+
|
|
193
|
+
if name in {"scan_forms", "FormEntry", "FormScanIndex"}:
|
|
194
|
+
from v8unpack_agent.scan_forms import FormEntry, FormScanIndex, scan_forms
|
|
195
|
+
|
|
196
|
+
values = {
|
|
197
|
+
"scan_forms": scan_forms,
|
|
198
|
+
"FormEntry": FormEntry,
|
|
199
|
+
"FormScanIndex": FormScanIndex,
|
|
200
|
+
}
|
|
201
|
+
globals().update(values)
|
|
202
|
+
return values[name]
|
|
203
|
+
|
|
204
|
+
if name in {"discover_elem_forms", "ElemFormEntry",
|
|
205
|
+
"discover_managed_forms", "ManagedFormEntry"}:
|
|
206
|
+
from v8unpack_agent.managed_forms import (
|
|
207
|
+
ElemFormEntry,
|
|
208
|
+
ManagedFormEntry,
|
|
209
|
+
discover_elem_forms,
|
|
210
|
+
discover_managed_forms,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
values = {
|
|
214
|
+
"discover_elem_forms": discover_elem_forms,
|
|
215
|
+
"ElemFormEntry": ElemFormEntry,
|
|
216
|
+
"discover_managed_forms": discover_managed_forms,
|
|
217
|
+
"ManagedFormEntry": ManagedFormEntry,
|
|
218
|
+
}
|
|
219
|
+
globals().update(values)
|
|
220
|
+
return values[name]
|
|
221
|
+
|
|
222
|
+
if name in {"FormSummary", "build_form_summary",
|
|
223
|
+
"build_form_summary_from_elem_index"}:
|
|
224
|
+
from v8unpack_agent.form_summary import (
|
|
225
|
+
FormSummary,
|
|
226
|
+
build_form_summary,
|
|
227
|
+
build_form_summary_from_elem_index,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
values = {
|
|
231
|
+
"FormSummary": FormSummary,
|
|
232
|
+
"build_form_summary": build_form_summary,
|
|
233
|
+
"build_form_summary_from_elem_index": build_form_summary_from_elem_index,
|
|
234
|
+
}
|
|
235
|
+
globals().update(values)
|
|
236
|
+
return values[name]
|
|
237
|
+
|
|
238
|
+
if name in {"FormContext", "build_form_context",
|
|
239
|
+
"to_llm_prompt_fragment"}:
|
|
240
|
+
from v8unpack_agent.form_context import (
|
|
241
|
+
FormContext,
|
|
242
|
+
build_form_context,
|
|
243
|
+
to_llm_prompt_fragment,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
values = {
|
|
247
|
+
"FormContext": FormContext,
|
|
248
|
+
"build_form_context": build_form_context,
|
|
249
|
+
"to_llm_prompt_fragment": to_llm_prompt_fragment,
|
|
250
|
+
}
|
|
251
|
+
globals().update(values)
|
|
252
|
+
return values[name]
|
|
253
|
+
|
|
254
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# Публичная поверхность пакета. Порядок — isort-style (требование RUF022):
|
|
258
|
+
# SCREAMING_SNAKE_CASE → CamelCase → snake_case. Порядок не является
|
|
259
|
+
# контрактом: тесты #124/#128/#131/#134 проверяют состав через set()/in,
|
|
260
|
+
# а не последовательность. Ленивые имена разрешаются через PEP 562
|
|
261
|
+
# __getattr__, сортировка не делает импорты eager.
|
|
262
|
+
#
|
|
263
|
+
# Происхождение экспортов (карта сохранена при сортировке):
|
|
264
|
+
# issue #55 — discover_elem_forms
|
|
265
|
+
# ElemFormEntry
|
|
266
|
+
# deprecated aliases (обратная совместимость) — discover_managed_forms
|
|
267
|
+
# ManagedFormEntry
|
|
268
|
+
# issue #69 — FormSummary
|
|
269
|
+
# build_form_summary
|
|
270
|
+
# build_form_summary_from_elem_index
|
|
271
|
+
# issue #124 (form_context, issue #77) — FormContext
|
|
272
|
+
# build_form_context
|
|
273
|
+
# to_llm_prompt_fragment
|
|
274
|
+
# issue #98 — FormClass
|
|
275
|
+
# classify_form
|
|
276
|
+
# classify_form_by_name
|
|
277
|
+
# classify_form_by_bindings
|
|
278
|
+
# SERVICE_FORM_NAME_PATTERNS
|
|
279
|
+
__all__ = [
|
|
280
|
+
"SERVICE_FORM_NAME_PATTERNS",
|
|
281
|
+
"DriftReport",
|
|
282
|
+
"ElemFormEntry",
|
|
283
|
+
"ElemIndexResult",
|
|
284
|
+
"ErfUnpacker",
|
|
285
|
+
"FormArtifact",
|
|
286
|
+
"FormClass",
|
|
287
|
+
"FormContext",
|
|
288
|
+
"FormEntry",
|
|
289
|
+
"FormRouter",
|
|
290
|
+
"FormScanIndex",
|
|
291
|
+
"FormSummary",
|
|
292
|
+
"FormUnpacker",
|
|
293
|
+
"FormsIndex",
|
|
294
|
+
"FormsIndexEntry",
|
|
295
|
+
"ManagedFormEntry",
|
|
296
|
+
"RouteResult",
|
|
297
|
+
"SkdBatchResult",
|
|
298
|
+
"SkdResult",
|
|
299
|
+
"all_module_paths",
|
|
300
|
+
"build_form_context",
|
|
301
|
+
"build_form_summary",
|
|
302
|
+
"build_form_summary_from_elem_index",
|
|
303
|
+
"check_drift",
|
|
304
|
+
"classify_form",
|
|
305
|
+
"classify_form_by_bindings",
|
|
306
|
+
"classify_form_by_name",
|
|
307
|
+
"discover_elem_forms",
|
|
308
|
+
"discover_form_bins",
|
|
309
|
+
"discover_managed_forms",
|
|
310
|
+
"extract_all_skd_queries",
|
|
311
|
+
"extract_skd_queries",
|
|
312
|
+
"form_paths",
|
|
313
|
+
"form_root",
|
|
314
|
+
"is_form_stale",
|
|
315
|
+
"item_modules",
|
|
316
|
+
"parse_elem_json",
|
|
317
|
+
"scan_forms",
|
|
318
|
+
"to_llm_prompt_fragment",
|
|
319
|
+
"unpack_all_forms",
|
|
320
|
+
"unpack_erf",
|
|
321
|
+
"update_forms_index",
|
|
322
|
+
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Обезличенные ссылки на пути файловой системы (issue #123).
|
|
2
|
+
|
|
3
|
+
Предупреждения парсера регулярно публикуются: в отчётах, логах CI, теле
|
|
4
|
+
issue и PR, промптах к модели. Абсолютный путь несёт имя пользователя и
|
|
5
|
+
структуру локальной ФС, поэтому в текст предупреждения он попадать не
|
|
6
|
+
должен. При этом предупреждение обязано оставаться полезным: по нему
|
|
7
|
+
нужно понять, о какой форме речь.
|
|
8
|
+
|
|
9
|
+
Модуль решает эту задачу функциями :func:`safe_path_ref` и
|
|
10
|
+
:func:`safe_error_text`. Он намеренно не зависит ни от чего внутри пакета,
|
|
11
|
+
чтобы его могли использовать ``elem_parser`` и ``object_decoder`` без
|
|
12
|
+
циклических импортов.
|
|
13
|
+
|
|
14
|
+
Почему не :mod:`pathlib`
|
|
15
|
+
------------------------
|
|
16
|
+
|
|
17
|
+
На POSIX ``Path`` не считает ``\\`` разделителем, поэтому строка вида
|
|
18
|
+
``C:\\dump\\Catalog\\Объект\\CatalogForm\\Форма`` осталась бы единым
|
|
19
|
+
сегментом и утечка сохранилась бы. Разбор идёт регулярным выражением по
|
|
20
|
+
обоим разделителям, поэтому результат не зависит от ОС, на которой
|
|
21
|
+
запущен код или тесты.
|
|
22
|
+
|
|
23
|
+
Гарантии
|
|
24
|
+
--------
|
|
25
|
+
|
|
26
|
+
* результат ``safe_path_ref`` никогда не является абсолютным путём;
|
|
27
|
+
* у абсолютного входа сохраняются последние значимые сегменты — для
|
|
28
|
+
выгрузки 1С это «тип / объект / контейнер / форма»;
|
|
29
|
+
* относительный путь не искажается, пока он не длиннее хвоста;
|
|
30
|
+
* в выводе всегда posix-разделитель ``/``;
|
|
31
|
+
* ``safe_error_text`` сохраняет смысл ошибки, но заменяет переданные
|
|
32
|
+
связанные пути, включая варианты с ``/`` и ``\\``;
|
|
33
|
+
* функции детерминированы и не обращаются к файловой системе.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import re
|
|
39
|
+
|
|
40
|
+
__all__ = [
|
|
41
|
+
"SAFE_TAIL_SEGMENTS",
|
|
42
|
+
"TRUNCATION_MARKER",
|
|
43
|
+
"UNKNOWN_REF",
|
|
44
|
+
"safe_error_text",
|
|
45
|
+
"safe_path_ref",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
#: Сколько хвостовых сегментов сохраняется по умолчанию.
|
|
49
|
+
#: Для выгрузки 1С это тип объекта / объект / контейнер форм / форма.
|
|
50
|
+
SAFE_TAIL_SEGMENTS = 4
|
|
51
|
+
|
|
52
|
+
#: Признак того, что начало пути отброшено осознанно.
|
|
53
|
+
TRUNCATION_MARKER = "..."
|
|
54
|
+
|
|
55
|
+
#: Замена для пустого или отсутствующего пути.
|
|
56
|
+
UNKNOWN_REF = "<путь не указан>"
|
|
57
|
+
|
|
58
|
+
_SEPARATORS_RE = re.compile(r"[\\/]+")
|
|
59
|
+
_WINDOWS_DRIVE_RE = re.compile(r"^[A-Za-z]:$")
|
|
60
|
+
_ABSOLUTE_RE = re.compile(r"^(?:[\\/]|[A-Za-z]:[\\/])")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def safe_path_ref(value: object, tail: int = SAFE_TAIL_SEGMENTS) -> str:
|
|
64
|
+
"""Вернуть обезличенную ссылку на путь для текста предупреждения.
|
|
65
|
+
|
|
66
|
+
``value`` может быть ``Path``, строкой с ``/`` или ``\\`` либо ``None``.
|
|
67
|
+
``tail`` задаёт число сохраняемых хвостовых сегментов; ``tail <= 0``
|
|
68
|
+
оставляет только последний сегмент.
|
|
69
|
+
"""
|
|
70
|
+
if value is None:
|
|
71
|
+
return UNKNOWN_REF
|
|
72
|
+
|
|
73
|
+
text = str(value).strip().strip('"')
|
|
74
|
+
if not text:
|
|
75
|
+
return UNKNOWN_REF
|
|
76
|
+
|
|
77
|
+
absolute = bool(_ABSOLUTE_RE.match(text))
|
|
78
|
+
segments = [
|
|
79
|
+
segment
|
|
80
|
+
for segment in _SEPARATORS_RE.split(text)
|
|
81
|
+
if segment not in ("", ".", "..") and not _WINDOWS_DRIVE_RE.match(segment)
|
|
82
|
+
]
|
|
83
|
+
if not segments:
|
|
84
|
+
return UNKNOWN_REF
|
|
85
|
+
|
|
86
|
+
if tail <= 0:
|
|
87
|
+
return segments[-1]
|
|
88
|
+
|
|
89
|
+
if not absolute:
|
|
90
|
+
if len(segments) <= tail:
|
|
91
|
+
return "/".join(segments)
|
|
92
|
+
return f"{TRUNCATION_MARKER}/" + "/".join(segments[-tail:])
|
|
93
|
+
|
|
94
|
+
# Абсолютный путь: начало отбрасывается всегда. Если сегментов меньше
|
|
95
|
+
# хвоста, надёжного слоя-ориентира нет — остаётся только имя.
|
|
96
|
+
kept = segments[-tail:] if len(segments) > tail else segments[-1:]
|
|
97
|
+
return f"{TRUNCATION_MARKER}/" + "/".join(kept)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def safe_error_text(
|
|
101
|
+
error: BaseException | object,
|
|
102
|
+
*paths: object,
|
|
103
|
+
tail: int = SAFE_TAIL_SEGMENTS,
|
|
104
|
+
) -> str:
|
|
105
|
+
"""Вернуть текст ошибки без известных абсолютных путей.
|
|
106
|
+
|
|
107
|
+
``OSError`` и производные часто повторяют имя файла внутри ``str(exc)``.
|
|
108
|
+
На Windows это имя может быть записано с удвоенными обратными слешами,
|
|
109
|
+
поскольку форматируется через строковое представление ``filename``.
|
|
110
|
+
Для каждого связанного пути заменяются исходная запись, варианты с обоими
|
|
111
|
+
разделителями и escaped Windows-вариант. Остальной текст сохраняется.
|
|
112
|
+
|
|
113
|
+
``tail`` передаётся в :func:`safe_path_ref`: object JSON использует
|
|
114
|
+
три сегмента «тип / объект / файл», пути форм сохраняют default из
|
|
115
|
+
четырёх сегментов.
|
|
116
|
+
"""
|
|
117
|
+
text = str(error)
|
|
118
|
+
for path in paths:
|
|
119
|
+
if path is None:
|
|
120
|
+
continue
|
|
121
|
+
raw = str(path)
|
|
122
|
+
if not raw:
|
|
123
|
+
continue
|
|
124
|
+
replacement = safe_path_ref(raw, tail=tail)
|
|
125
|
+
backslash = raw.replace("/", "\\")
|
|
126
|
+
variants = {
|
|
127
|
+
raw,
|
|
128
|
+
raw.replace("\\", "/"),
|
|
129
|
+
backslash,
|
|
130
|
+
backslash.replace("\\", "\\\\"),
|
|
131
|
+
}
|
|
132
|
+
for variant in sorted(variants, key=len, reverse=True):
|
|
133
|
+
if variant:
|
|
134
|
+
text = text.replace(variant, replacement)
|
|
135
|
+
return text
|