wayscribe 0.2.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.
- wayscribe/__init__.py +30 -0
- wayscribe/_capture.py +473 -0
- wayscribe/_config.py +253 -0
- wayscribe/_diagnostics.py +456 -0
- wayscribe/_errors.py +243 -0
- wayscribe/_event.py +352 -0
- wayscribe/_transport.py +676 -0
- wayscribe/_version.py +5 -0
- wayscribe/propagation.py +197 -0
- wayscribe/py.typed +0 -0
- wayscribe/recorder.py +658 -0
- wayscribe/timing.py +217 -0
- wayscribe-0.2.0.dist-info/METADATA +378 -0
- wayscribe-0.2.0.dist-info/RECORD +18 -0
- wayscribe-0.2.0.dist-info/WHEEL +5 -0
- wayscribe-0.2.0.dist-info/licenses/LICENSE +202 -0
- wayscribe-0.2.0.dist-info/licenses/NOTICE +8 -0
- wayscribe-0.2.0.dist-info/top_level.txt +1 -0
wayscribe/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Wayscribe native Python recorder."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .propagation import (
|
|
5
|
+
extract_http_context,
|
|
6
|
+
extract_payload,
|
|
7
|
+
extract_sqs_context,
|
|
8
|
+
has_journey,
|
|
9
|
+
inject_http_headers,
|
|
10
|
+
inject_payload,
|
|
11
|
+
inject_sqs_attributes,
|
|
12
|
+
)
|
|
13
|
+
from .recorder import Journey, Recorder, create_recorder
|
|
14
|
+
from .timing import http_metadata, queue_metadata
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"__version__",
|
|
18
|
+
"Journey",
|
|
19
|
+
"Recorder",
|
|
20
|
+
"create_recorder",
|
|
21
|
+
"extract_http_context",
|
|
22
|
+
"extract_payload",
|
|
23
|
+
"extract_sqs_context",
|
|
24
|
+
"has_journey",
|
|
25
|
+
"http_metadata",
|
|
26
|
+
"inject_http_headers",
|
|
27
|
+
"inject_payload",
|
|
28
|
+
"inject_sqs_attributes",
|
|
29
|
+
"queue_metadata",
|
|
30
|
+
]
|
wayscribe/_capture.py
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""Bounded synchronous capture. Walk only explicit containers, never attributes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ._config import Config
|
|
9
|
+
import base64
|
|
10
|
+
import datetime as dt
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
import re
|
|
14
|
+
from collections.abc import Mapping
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from decimal import Decimal
|
|
17
|
+
|
|
18
|
+
from ._diagnostics import (
|
|
19
|
+
MAX_SECRET_NAME_LENGTH,
|
|
20
|
+
MAX_SECRET_NAMES,
|
|
21
|
+
MAX_SECRET_PATH_LENGTH,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
UNSET = object()
|
|
25
|
+
REDACTED = "[REDACTED]"
|
|
26
|
+
TOO_LARGE = "[PAYLOAD_TOO_LARGE]"
|
|
27
|
+
UNCAPTURABLE = "[UNCAPTURABLE]"
|
|
28
|
+
SECRET_NAMES = [
|
|
29
|
+
"authorization",
|
|
30
|
+
"proxy-authorization",
|
|
31
|
+
"cookie",
|
|
32
|
+
"set-cookie",
|
|
33
|
+
"x-api-key",
|
|
34
|
+
"password",
|
|
35
|
+
"access_token",
|
|
36
|
+
"refresh_token",
|
|
37
|
+
"client_secret",
|
|
38
|
+
"api_key",
|
|
39
|
+
"secret",
|
|
40
|
+
"stripe-signature",
|
|
41
|
+
"x-hub-signature",
|
|
42
|
+
"x-hub-signature-256",
|
|
43
|
+
"x-slack-signature",
|
|
44
|
+
"x-hubspot-signature",
|
|
45
|
+
"x-hubspot-signature-v3",
|
|
46
|
+
"x-twilio-signature",
|
|
47
|
+
"x-shopify-hmac-sha256",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def fold(name: str) -> str:
|
|
52
|
+
return name.lower().replace("-", "").replace("_", "")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
SECRETS = frozenset(map(fold, SECRET_NAMES))
|
|
56
|
+
KNOWN_HEADERS = frozenset(
|
|
57
|
+
[
|
|
58
|
+
":authority",
|
|
59
|
+
":method",
|
|
60
|
+
":path",
|
|
61
|
+
":protocol",
|
|
62
|
+
":scheme",
|
|
63
|
+
":status",
|
|
64
|
+
"accept",
|
|
65
|
+
"accept-encoding",
|
|
66
|
+
"accept-language",
|
|
67
|
+
"authorization",
|
|
68
|
+
"cache-control",
|
|
69
|
+
"connection",
|
|
70
|
+
"content-length",
|
|
71
|
+
"content-type",
|
|
72
|
+
"cookie",
|
|
73
|
+
"date",
|
|
74
|
+
"etag",
|
|
75
|
+
"host",
|
|
76
|
+
"if-none-match",
|
|
77
|
+
"location",
|
|
78
|
+
"origin",
|
|
79
|
+
"proxy-authorization",
|
|
80
|
+
"referer",
|
|
81
|
+
"set-cookie",
|
|
82
|
+
"transfer-encoding",
|
|
83
|
+
"user-agent",
|
|
84
|
+
"vary",
|
|
85
|
+
"www-authenticate",
|
|
86
|
+
"x-api-key",
|
|
87
|
+
"x-forwarded-for",
|
|
88
|
+
"x-request-id",
|
|
89
|
+
]
|
|
90
|
+
)
|
|
91
|
+
TOKEN = re.compile("^:?[!#$%&'*+.^_`|~0-9A-Za-z-]+$")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def repair_text(text: str) -> str:
|
|
95
|
+
return (
|
|
96
|
+
text.replace("\x00", "")
|
|
97
|
+
.encode("utf-16-le", "surrogatepass")
|
|
98
|
+
.decode("utf-16-le", "replace")
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def utf16_len(text: str) -> int:
|
|
103
|
+
return len(text.encode("utf-16-le")) // 2
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def truncate_text(text: str, limit: int = 65536) -> str:
|
|
107
|
+
encoded = text.encode("utf-16-le")
|
|
108
|
+
length = len(encoded) // 2
|
|
109
|
+
if length <= limit:
|
|
110
|
+
return text
|
|
111
|
+
separator = "\r\n" if "\r\n" in text else ""
|
|
112
|
+
removed = length - limit
|
|
113
|
+
while True:
|
|
114
|
+
marker = separator + f"[TRUNCATED: {removed} characters removed]"
|
|
115
|
+
kept = max(0, limit - len(marker))
|
|
116
|
+
settled = length - kept
|
|
117
|
+
if settled == removed:
|
|
118
|
+
return encoded[: kept * 2].decode("utf-16-le", "replace") + marker[:limit]
|
|
119
|
+
removed = settled
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def json_bytes(value: Any) -> bytes:
|
|
123
|
+
return json.dumps(
|
|
124
|
+
value, ensure_ascii=False, allow_nan=False, separators=(",", ":")
|
|
125
|
+
).encode("utf-8")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class Captured:
|
|
130
|
+
value: object
|
|
131
|
+
truncated: bool = False
|
|
132
|
+
omitted: bool = False
|
|
133
|
+
unreadable: bool = False
|
|
134
|
+
names: list[tuple[str, str]] = field(default_factory=list)
|
|
135
|
+
strings_cut: int = 0
|
|
136
|
+
characters_removed: int = 0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class _TooLarge(Exception):
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def looks_secret(name: str, value: object) -> bool:
|
|
144
|
+
if type(value) not in (str, int, float) or type(value) is bool:
|
|
145
|
+
return False
|
|
146
|
+
if type(value) is str and (
|
|
147
|
+
not value
|
|
148
|
+
or value == REDACTED
|
|
149
|
+
or value.strip().lower()
|
|
150
|
+
in ("true", "false", "none", "basic", "bearer", "oauth", "required", "optional")
|
|
151
|
+
):
|
|
152
|
+
return False
|
|
153
|
+
name = re.sub("v?[0-9]+$", "", fold(name))
|
|
154
|
+
terms = [
|
|
155
|
+
"token",
|
|
156
|
+
"secret",
|
|
157
|
+
"password",
|
|
158
|
+
"passwd",
|
|
159
|
+
"passphrase",
|
|
160
|
+
"passcode",
|
|
161
|
+
"credential",
|
|
162
|
+
"credentials",
|
|
163
|
+
"authorization",
|
|
164
|
+
"auth",
|
|
165
|
+
"bearer",
|
|
166
|
+
"cookie",
|
|
167
|
+
"cookies",
|
|
168
|
+
"signature",
|
|
169
|
+
"jwt",
|
|
170
|
+
"otp",
|
|
171
|
+
"cvv",
|
|
172
|
+
"cvc",
|
|
173
|
+
"apikey",
|
|
174
|
+
"accesskey",
|
|
175
|
+
"secretkey",
|
|
176
|
+
"privatekey",
|
|
177
|
+
"signingkey",
|
|
178
|
+
"encryptionkey",
|
|
179
|
+
"masterkey",
|
|
180
|
+
"sessionkey",
|
|
181
|
+
"authkey",
|
|
182
|
+
"hmackey",
|
|
183
|
+
"sharedkey",
|
|
184
|
+
"subscriptionkey",
|
|
185
|
+
"sessionid",
|
|
186
|
+
"sessid",
|
|
187
|
+
"secretstring",
|
|
188
|
+
"secretvalue",
|
|
189
|
+
"codeverifier",
|
|
190
|
+
"clientassertion",
|
|
191
|
+
"authcode",
|
|
192
|
+
"authorizationcode",
|
|
193
|
+
"otpcode",
|
|
194
|
+
"mfacode",
|
|
195
|
+
"recoverycode",
|
|
196
|
+
"connectionstring",
|
|
197
|
+
"databaseurl",
|
|
198
|
+
"dsn",
|
|
199
|
+
"passwordconfirmation",
|
|
200
|
+
]
|
|
201
|
+
for term in terms:
|
|
202
|
+
if not name.endswith(term):
|
|
203
|
+
continue
|
|
204
|
+
prefix = name[: -len(term)]
|
|
205
|
+
if term == "token" and prefix.endswith(
|
|
206
|
+
tuple(
|
|
207
|
+
[
|
|
208
|
+
"page",
|
|
209
|
+
"next",
|
|
210
|
+
"continuation",
|
|
211
|
+
"pagination",
|
|
212
|
+
"sync",
|
|
213
|
+
"client",
|
|
214
|
+
"clientrequest",
|
|
215
|
+
"idempotency",
|
|
216
|
+
"resume",
|
|
217
|
+
"cancel",
|
|
218
|
+
"cursor",
|
|
219
|
+
"start",
|
|
220
|
+
"stop",
|
|
221
|
+
"bos",
|
|
222
|
+
"eos",
|
|
223
|
+
"pad",
|
|
224
|
+
"unk",
|
|
225
|
+
"sep",
|
|
226
|
+
"cls",
|
|
227
|
+
"mask",
|
|
228
|
+
]
|
|
229
|
+
)
|
|
230
|
+
):
|
|
231
|
+
return False
|
|
232
|
+
if term == "signature" and prefix.endswith("email"):
|
|
233
|
+
return False
|
|
234
|
+
if term == "auth" and type(value) is str and (len(value) < 8):
|
|
235
|
+
return False
|
|
236
|
+
return True
|
|
237
|
+
return (
|
|
238
|
+
name == "hmac"
|
|
239
|
+
or name
|
|
240
|
+
in ("pin",)
|
|
241
|
+
+ tuple(
|
|
242
|
+
x + "pin"
|
|
243
|
+
for x in [
|
|
244
|
+
"card",
|
|
245
|
+
"atm",
|
|
246
|
+
"user",
|
|
247
|
+
"account",
|
|
248
|
+
"security",
|
|
249
|
+
"login",
|
|
250
|
+
"new",
|
|
251
|
+
"old",
|
|
252
|
+
"current",
|
|
253
|
+
]
|
|
254
|
+
)
|
|
255
|
+
or name in tuple(x + "pwd" for x in ["db", "user", "admin", "root", "database"])
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def capture(value: object, config: Config, *, field_name: str = "input") -> Captured:
|
|
260
|
+
result = Captured(None)
|
|
261
|
+
any_depth = set(SECRETS)
|
|
262
|
+
paths = []
|
|
263
|
+
if config.capture_mode != "full-payload":
|
|
264
|
+
for rule in config.redact:
|
|
265
|
+
if rule.startswith("**.") and (not any(c in rule[3:] for c in ".*[]")):
|
|
266
|
+
any_depth.add(fold(rule[3:]))
|
|
267
|
+
else:
|
|
268
|
+
paths.append(
|
|
269
|
+
tuple(fold(x) for x in rule.replace("[*]", ".[*]").split("."))
|
|
270
|
+
)
|
|
271
|
+
known = set(config.known_safe_names)
|
|
272
|
+
ancestors = set()
|
|
273
|
+
visited = 0
|
|
274
|
+
budget = 0
|
|
275
|
+
|
|
276
|
+
def matches(path):
|
|
277
|
+
return any(
|
|
278
|
+
len(rule) == len(path)
|
|
279
|
+
and all((r == "*" or r == fold(p) for r, p in zip(rule, path)))
|
|
280
|
+
for rule in paths
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def observe(name, child, path):
|
|
284
|
+
if (
|
|
285
|
+
fold(name) not in known
|
|
286
|
+
and looks_secret(name, child)
|
|
287
|
+
and (len(result.names) < MAX_SECRET_NAMES)
|
|
288
|
+
):
|
|
289
|
+
result.names.append(
|
|
290
|
+
(
|
|
291
|
+
name[:MAX_SECRET_NAME_LENGTH],
|
|
292
|
+
".".join(path).replace(".[*]", "[*]")[:MAX_SECRET_PATH_LENGTH],
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
def text(value):
|
|
297
|
+
fixed = repair_text(value)
|
|
298
|
+
if "\r\n" in fixed or re.search(
|
|
299
|
+
"\\[TRUNCATED: [0-9]+ characters removed\\]$", fixed
|
|
300
|
+
):
|
|
301
|
+
lines = fixed.split("\r\n")
|
|
302
|
+
done = False
|
|
303
|
+
for i, line in enumerate(lines):
|
|
304
|
+
if not line and i > 0:
|
|
305
|
+
done = True
|
|
306
|
+
if done:
|
|
307
|
+
continue
|
|
308
|
+
match = re.match("^([!#$%&'*+.^_`|~0-9A-Za-z-]+):([ \\t]*)(.*)$", line)
|
|
309
|
+
if match:
|
|
310
|
+
name, space, child = match.groups()
|
|
311
|
+
if child and fold(name) in any_depth:
|
|
312
|
+
lines[i] = name + ":" + space + REDACTED
|
|
313
|
+
elif child:
|
|
314
|
+
observe(name, child, ())
|
|
315
|
+
fixed = "\r\n".join(lines)
|
|
316
|
+
cut = truncate_text(fixed)
|
|
317
|
+
if cut != fixed:
|
|
318
|
+
result.truncated = True
|
|
319
|
+
result.strings_cut += 1
|
|
320
|
+
removed = re.search("\\[TRUNCATED: ([0-9]+) characters removed\\]$", cut)
|
|
321
|
+
if removed:
|
|
322
|
+
result.characters_removed += int(removed[1])
|
|
323
|
+
return cut
|
|
324
|
+
|
|
325
|
+
def walk(value, depth, path):
|
|
326
|
+
nonlocal visited, budget
|
|
327
|
+
visited += 1
|
|
328
|
+
if visited > config.max_event_bytes or depth > 30:
|
|
329
|
+
raise _TooLarge()
|
|
330
|
+
if value is UNSET:
|
|
331
|
+
out = None
|
|
332
|
+
elif value is None or type(value) is bool:
|
|
333
|
+
out = value
|
|
334
|
+
elif type(value) is str:
|
|
335
|
+
out = text(value)
|
|
336
|
+
elif type(value) is int:
|
|
337
|
+
out = value if abs(value) <= 9007199254740991 else str(Decimal(value))
|
|
338
|
+
# A decimal repair must retain every digit. Truncating it would
|
|
339
|
+
# invent a different numeric value, so omit an over-limit payload.
|
|
340
|
+
if type(out) is str and len(out) > 65536:
|
|
341
|
+
raise _TooLarge()
|
|
342
|
+
elif type(value) is float:
|
|
343
|
+
out = value if math.isfinite(value) else None
|
|
344
|
+
elif type(value) in (bytes, bytearray):
|
|
345
|
+
if len(value) > config.max_event_bytes:
|
|
346
|
+
raise _TooLarge()
|
|
347
|
+
# The rendered object occupies this depth, and its strings occupy
|
|
348
|
+
# the next. Apply the same limits to the representation we send.
|
|
349
|
+
return walk(
|
|
350
|
+
{"type": "bytes", "base64": base64.b64encode(value).decode("ascii")},
|
|
351
|
+
depth,
|
|
352
|
+
path,
|
|
353
|
+
)
|
|
354
|
+
elif type(value) in (dt.datetime, dt.date):
|
|
355
|
+
if type(value) is dt.date:
|
|
356
|
+
value = dt.datetime.combine(value, dt.time(), dt.timezone.utc)
|
|
357
|
+
if value.tzinfo is None:
|
|
358
|
+
value = value.replace(tzinfo=dt.timezone.utc)
|
|
359
|
+
out = (
|
|
360
|
+
value.astimezone(dt.timezone.utc)
|
|
361
|
+
.isoformat(timespec="milliseconds")
|
|
362
|
+
.replace("+00:00", "Z")
|
|
363
|
+
)
|
|
364
|
+
elif isinstance(value, Mapping) or type(value) in (tuple, list):
|
|
365
|
+
if id(value) in ancestors:
|
|
366
|
+
return "[CIRCULAR]"
|
|
367
|
+
ancestors.add(id(value))
|
|
368
|
+
try:
|
|
369
|
+
if len(value) > 1000:
|
|
370
|
+
raise _TooLarge()
|
|
371
|
+
if isinstance(value, Mapping):
|
|
372
|
+
out = {}
|
|
373
|
+
for index, key in enumerate(value):
|
|
374
|
+
if index >= 1000:
|
|
375
|
+
raise _TooLarge()
|
|
376
|
+
if type(key) is not str:
|
|
377
|
+
result.unreadable = True
|
|
378
|
+
continue
|
|
379
|
+
fixed = repair_text(key)
|
|
380
|
+
newpath = path + (key,)
|
|
381
|
+
if fold(key) in any_depth or matches(newpath):
|
|
382
|
+
out[fixed] = REDACTED
|
|
383
|
+
continue
|
|
384
|
+
try:
|
|
385
|
+
child = value[key]
|
|
386
|
+
except Exception:
|
|
387
|
+
child = UNCAPTURABLE
|
|
388
|
+
result.unreadable = True
|
|
389
|
+
if child is UNSET:
|
|
390
|
+
continue
|
|
391
|
+
observe(key, child, newpath)
|
|
392
|
+
out[fixed] = walk(child, depth + 1, newpath)
|
|
393
|
+
budget += len(fixed.encode("utf-8"))
|
|
394
|
+
if budget > config.max_event_bytes:
|
|
395
|
+
raise _TooLarge()
|
|
396
|
+
else:
|
|
397
|
+
interleaved = (
|
|
398
|
+
len(value) > 0
|
|
399
|
+
and len(value) % 2 == 0
|
|
400
|
+
and all(type(x) is str for x in value)
|
|
401
|
+
and all(TOKEN.fullmatch(x) for x in value[::2])
|
|
402
|
+
and any(x.lower() in KNOWN_HEADERS for x in value[::2])
|
|
403
|
+
)
|
|
404
|
+
out = []
|
|
405
|
+
for index, child in enumerate(value):
|
|
406
|
+
subpath = path + ("[*]",)
|
|
407
|
+
if matches(subpath):
|
|
408
|
+
out.append(REDACTED)
|
|
409
|
+
continue
|
|
410
|
+
if interleaved and index % 2:
|
|
411
|
+
name = value[index - 1]
|
|
412
|
+
if (
|
|
413
|
+
fold(name) in any_depth
|
|
414
|
+
and child.lower() not in KNOWN_HEADERS
|
|
415
|
+
):
|
|
416
|
+
out.append(REDACTED)
|
|
417
|
+
continue
|
|
418
|
+
if fold(name) not in any_depth:
|
|
419
|
+
observe(name, child, subpath)
|
|
420
|
+
elif (
|
|
421
|
+
type(child) in (list, tuple)
|
|
422
|
+
and len(child) == 2
|
|
423
|
+
and (type(child[0]) is str)
|
|
424
|
+
):
|
|
425
|
+
name, val = child
|
|
426
|
+
if fold(name) in any_depth and not (
|
|
427
|
+
type(val) is str and val.lower() in KNOWN_HEADERS
|
|
428
|
+
):
|
|
429
|
+
out.append([repair_text(name), REDACTED])
|
|
430
|
+
continue
|
|
431
|
+
if fold(name) not in any_depth:
|
|
432
|
+
observe(name, val, subpath)
|
|
433
|
+
elif type(child) is dict and "value" in child:
|
|
434
|
+
name = child.get("name")
|
|
435
|
+
if type(name) is not str:
|
|
436
|
+
name = child.get("key")
|
|
437
|
+
if type(name) is str:
|
|
438
|
+
val = child["value"]
|
|
439
|
+
if fold(name) in any_depth and not (
|
|
440
|
+
type(val) is str and val.lower() in KNOWN_HEADERS
|
|
441
|
+
):
|
|
442
|
+
child = {**child, "value": REDACTED}
|
|
443
|
+
elif fold(name) not in any_depth:
|
|
444
|
+
observe(name, val, subpath)
|
|
445
|
+
out.append(walk(child, depth + 1, subpath))
|
|
446
|
+
except _TooLarge:
|
|
447
|
+
raise
|
|
448
|
+
except Exception:
|
|
449
|
+
out = UNCAPTURABLE
|
|
450
|
+
result.unreadable = True
|
|
451
|
+
finally:
|
|
452
|
+
ancestors.remove(id(value))
|
|
453
|
+
else:
|
|
454
|
+
out = UNCAPTURABLE
|
|
455
|
+
result.unreadable = True
|
|
456
|
+
budget += len(out.encode("utf-8")) + 2 if type(out) is str else 1
|
|
457
|
+
if budget > config.max_event_bytes:
|
|
458
|
+
raise _TooLarge()
|
|
459
|
+
return out
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
result.value = walk(value, 0, ())
|
|
463
|
+
except _TooLarge:
|
|
464
|
+
result.value = TOO_LARGE
|
|
465
|
+
result.omitted = True
|
|
466
|
+
result.truncated = False
|
|
467
|
+
result.names = []
|
|
468
|
+
except Exception:
|
|
469
|
+
result.value = UNCAPTURABLE
|
|
470
|
+
result.unreadable = True
|
|
471
|
+
result.truncated = False
|
|
472
|
+
result.names = []
|
|
473
|
+
return result
|