haiway 0.18.2__py3-none-any.whl → 0.19.1__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.
- haiway/__init__.py +0 -2
- haiway/context/access.py +53 -64
- haiway/context/observability.py +53 -48
- haiway/helpers/__init__.py +1 -2
- haiway/helpers/observability.py +53 -35
- haiway/helpers/tracing.py +50 -43
- haiway/opentelemetry/observability.py +53 -31
- haiway/state/structure.py +3 -151
- haiway/utils/__init__.py +2 -0
- haiway/utils/formatting.py +151 -0
- {haiway-0.18.2.dist-info → haiway-0.19.1.dist-info}/METADATA +1 -1
- {haiway-0.18.2.dist-info → haiway-0.19.1.dist-info}/RECORD +14 -13
- {haiway-0.18.2.dist-info → haiway-0.19.1.dist-info}/WHEEL +0 -0
- {haiway-0.18.2.dist-info → haiway-0.19.1.dist-info}/licenses/LICENSE +0 -0
haiway/helpers/tracing.py
CHANGED
@@ -1,40 +1,14 @@
|
|
1
1
|
from asyncio import iscoroutinefunction
|
2
2
|
from collections.abc import Callable, Coroutine
|
3
|
-
from typing import Any,
|
3
|
+
from typing import Any, cast, overload
|
4
4
|
|
5
5
|
from haiway.context import ctx
|
6
|
-
from haiway.
|
7
|
-
from haiway.types import MISSING
|
6
|
+
from haiway.context.observability import ObservabilityLevel
|
7
|
+
from haiway.types import MISSING
|
8
8
|
from haiway.utils import mimic_function
|
9
|
+
from haiway.utils.formatting import format_str
|
9
10
|
|
10
|
-
__all__ = (
|
11
|
-
"ResultTrace",
|
12
|
-
"traced",
|
13
|
-
)
|
14
|
-
|
15
|
-
|
16
|
-
class ResultTrace(State):
|
17
|
-
if __debug__:
|
18
|
-
|
19
|
-
@classmethod
|
20
|
-
def of(
|
21
|
-
cls,
|
22
|
-
value: Any,
|
23
|
-
/,
|
24
|
-
) -> Self:
|
25
|
-
return cls(result=f"{value}")
|
26
|
-
|
27
|
-
else: # remove tracing for non debug runs to prevent accidental secret leaks
|
28
|
-
|
29
|
-
@classmethod
|
30
|
-
def of(
|
31
|
-
cls,
|
32
|
-
value: Any,
|
33
|
-
/,
|
34
|
-
) -> Self:
|
35
|
-
return cls(result=MISSING)
|
36
|
-
|
37
|
-
result: str | Missing
|
11
|
+
__all__ = ("traced",)
|
38
12
|
|
39
13
|
|
40
14
|
@overload
|
@@ -47,6 +21,7 @@ def traced[**Args, Result](
|
|
47
21
|
@overload
|
48
22
|
def traced[**Args, Result](
|
49
23
|
*,
|
24
|
+
level: ObservabilityLevel = ObservabilityLevel.DEBUG,
|
50
25
|
label: str,
|
51
26
|
) -> Callable[[Callable[Args, Result]], Callable[Args, Result]]: ...
|
52
27
|
|
@@ -55,6 +30,7 @@ def traced[**Args, Result](
|
|
55
30
|
function: Callable[Args, Result] | None = None,
|
56
31
|
/,
|
57
32
|
*,
|
33
|
+
level: ObservabilityLevel = ObservabilityLevel.DEBUG,
|
58
34
|
label: str | None = None,
|
59
35
|
) -> Callable[[Callable[Args, Result]], Callable[Args, Result]] | Callable[Args, Result]:
|
60
36
|
def wrap(
|
@@ -67,6 +43,7 @@ def traced[**Args, Result](
|
|
67
43
|
_traced_async(
|
68
44
|
wrapped,
|
69
45
|
label=label or wrapped.__name__,
|
46
|
+
level=level,
|
70
47
|
),
|
71
48
|
)
|
72
49
|
|
@@ -74,6 +51,7 @@ def traced[**Args, Result](
|
|
74
51
|
return _traced_sync(
|
75
52
|
wrapped,
|
76
53
|
label=label or wrapped.__name__,
|
54
|
+
level=level,
|
77
55
|
)
|
78
56
|
|
79
57
|
else: # do not trace on non debug runs
|
@@ -90,24 +68,39 @@ def _traced_sync[**Args, Result](
|
|
90
68
|
function: Callable[Args, Result],
|
91
69
|
/,
|
92
70
|
label: str,
|
71
|
+
level: ObservabilityLevel,
|
93
72
|
) -> Callable[Args, Result]:
|
94
73
|
def traced(
|
95
74
|
*args: Args.args,
|
96
75
|
**kwargs: Args.kwargs,
|
97
76
|
) -> Result:
|
98
77
|
with ctx.scope(label):
|
99
|
-
ctx.
|
100
|
-
|
78
|
+
ctx.record(
|
79
|
+
level,
|
80
|
+
attributes={
|
81
|
+
f"[{idx}]": f"{arg}" for idx, arg in enumerate(args) if arg is not MISSING
|
82
|
+
},
|
83
|
+
)
|
84
|
+
ctx.record(
|
85
|
+
level,
|
86
|
+
attributes={key: f"{arg}" for key, arg in kwargs.items() if arg is not MISSING},
|
101
87
|
)
|
102
|
-
ctx.attributes(**{key: f"{arg}" for key, arg in kwargs.items() if arg is not MISSING})
|
103
88
|
|
104
89
|
try:
|
105
90
|
result: Result = function(*args, **kwargs)
|
106
|
-
ctx.
|
91
|
+
ctx.record(
|
92
|
+
level,
|
93
|
+
event="result",
|
94
|
+
attributes={"value": format_str(result)},
|
95
|
+
)
|
107
96
|
return result
|
108
97
|
|
109
98
|
except BaseException as exc:
|
110
|
-
ctx.
|
99
|
+
ctx.record(
|
100
|
+
level,
|
101
|
+
event="result",
|
102
|
+
attributes={"error": f"{type(exc)}: {exc}"},
|
103
|
+
)
|
111
104
|
raise exc
|
112
105
|
|
113
106
|
return mimic_function(
|
@@ -120,25 +113,39 @@ def _traced_async[**Args, Result](
|
|
120
113
|
function: Callable[Args, Coroutine[Any, Any, Result]],
|
121
114
|
/,
|
122
115
|
label: str,
|
116
|
+
level: ObservabilityLevel,
|
123
117
|
) -> Callable[Args, Coroutine[Any, Any, Result]]:
|
124
118
|
async def traced(
|
125
119
|
*args: Args.args,
|
126
120
|
**kwargs: Args.kwargs,
|
127
121
|
) -> Result:
|
128
122
|
with ctx.scope(label):
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
123
|
+
ctx.record(
|
124
|
+
level,
|
125
|
+
attributes={
|
126
|
+
f"[{idx}]": f"{arg}" for idx, arg in enumerate(args) if arg is not MISSING
|
127
|
+
},
|
128
|
+
)
|
129
|
+
ctx.record(
|
130
|
+
level,
|
131
|
+
attributes={key: f"{arg}" for key, arg in kwargs.items() if arg is not MISSING},
|
132
|
+
)
|
134
133
|
|
135
134
|
try:
|
136
135
|
result: Result = await function(*args, **kwargs)
|
137
|
-
ctx.
|
136
|
+
ctx.record(
|
137
|
+
level,
|
138
|
+
event="result",
|
139
|
+
attributes={"value": format_str(result)},
|
140
|
+
)
|
138
141
|
return result
|
139
142
|
|
140
143
|
except BaseException as exc:
|
141
|
-
ctx.
|
144
|
+
ctx.record(
|
145
|
+
level,
|
146
|
+
event="result",
|
147
|
+
attributes={"error": f"{type(exc)}: {exc}"},
|
148
|
+
)
|
142
149
|
raise exc
|
143
150
|
|
144
151
|
return mimic_function(
|
@@ -29,6 +29,7 @@ from opentelemetry.sdk.resources import Resource
|
|
29
29
|
from opentelemetry.sdk.trace import TracerProvider
|
30
30
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter, SpanExporter
|
31
31
|
from opentelemetry.trace import Span, StatusCode, Tracer
|
32
|
+
from opentelemetry.trace.span import SpanContext
|
32
33
|
|
33
34
|
from haiway.context import Observability, ObservabilityLevel, ScopeIdentifier
|
34
35
|
from haiway.context.observability import ObservabilityAttribute
|
@@ -44,9 +45,9 @@ class ScopeStore:
|
|
44
45
|
"_counters",
|
45
46
|
"_exited",
|
46
47
|
"_histograms",
|
48
|
+
"_span_context",
|
47
49
|
"identifier",
|
48
50
|
"logger",
|
49
|
-
"logger_2",
|
50
51
|
"meter",
|
51
52
|
"nested",
|
52
53
|
"span",
|
@@ -67,6 +68,7 @@ class ScopeStore:
|
|
67
68
|
self._exited: bool = False
|
68
69
|
self._completed: bool = False
|
69
70
|
self.span: Span = span
|
71
|
+
self._span_context: SpanContext = span.get_span_context()
|
70
72
|
self.meter: Meter = meter
|
71
73
|
self.logger: Logger = logger
|
72
74
|
|
@@ -104,16 +106,16 @@ class ScopeStore:
|
|
104
106
|
) -> None:
|
105
107
|
self.logger.emit(
|
106
108
|
LogRecord(
|
107
|
-
span_id=self.
|
108
|
-
trace_id=self.
|
109
|
-
trace_flags=self.
|
109
|
+
span_id=self._span_context.span_id,
|
110
|
+
trace_id=self._span_context.trace_id,
|
111
|
+
trace_flags=self._span_context.trace_flags,
|
110
112
|
body=message,
|
111
113
|
severity_text=level.name,
|
112
114
|
severity_number=SEVERITY_MAPPING[level],
|
113
115
|
attributes={
|
114
|
-
"trace_id": self.identifier.trace_id,
|
115
|
-
"scope_id": self.identifier.scope_id,
|
116
|
-
"parent_id": self.identifier.parent_id,
|
116
|
+
"context.trace_id": self.identifier.trace_id,
|
117
|
+
"context.scope_id": self.identifier.scope_id,
|
118
|
+
"context.parent_id": self.identifier.parent_id,
|
117
119
|
},
|
118
120
|
)
|
119
121
|
)
|
@@ -127,12 +129,18 @@ class ScopeStore:
|
|
127
129
|
|
128
130
|
def record_event(
|
129
131
|
self,
|
130
|
-
event:
|
132
|
+
event: str,
|
131
133
|
/,
|
134
|
+
*,
|
135
|
+
attributes: Mapping[str, ObservabilityAttribute],
|
132
136
|
) -> None:
|
133
137
|
self.span.add_event(
|
134
|
-
|
135
|
-
attributes=
|
138
|
+
event,
|
139
|
+
attributes={
|
140
|
+
key: cast(Any, value)
|
141
|
+
for key, value in attributes.items()
|
142
|
+
if value is not None and value is not MISSING
|
143
|
+
},
|
136
144
|
)
|
137
145
|
|
138
146
|
def record_metric(
|
@@ -142,13 +150,8 @@ class ScopeStore:
|
|
142
150
|
*,
|
143
151
|
value: float | int,
|
144
152
|
unit: str | None,
|
153
|
+
attributes: Mapping[str, ObservabilityAttribute],
|
145
154
|
) -> None:
|
146
|
-
attributes: Mapping[str, Any] = {
|
147
|
-
"trace_id": self.identifier.trace_id,
|
148
|
-
"scope_id": self.identifier.scope_id,
|
149
|
-
"parent_id": self.identifier.parent_id,
|
150
|
-
}
|
151
|
-
|
152
155
|
if name not in self._counters:
|
153
156
|
self._counters[name] = self.meter.create_counter(
|
154
157
|
name=name,
|
@@ -157,7 +160,18 @@ class ScopeStore:
|
|
157
160
|
|
158
161
|
self._counters[name].add(
|
159
162
|
value,
|
160
|
-
attributes=
|
163
|
+
attributes={
|
164
|
+
**{
|
165
|
+
"context.trace_id": self.identifier.trace_id,
|
166
|
+
"context.scope_id": self.identifier.scope_id,
|
167
|
+
"context.parent_id": self.identifier.parent_id,
|
168
|
+
},
|
169
|
+
**{
|
170
|
+
key: cast(Any, value)
|
171
|
+
for key, value in attributes.items()
|
172
|
+
if value is not None and value is not MISSING
|
173
|
+
},
|
174
|
+
},
|
161
175
|
)
|
162
176
|
|
163
177
|
def record_attribute(
|
@@ -193,8 +207,8 @@ class OpenTelemetry:
|
|
193
207
|
{
|
194
208
|
"service.name": service,
|
195
209
|
"service.version": version,
|
196
|
-
"deployment.environment": environment,
|
197
210
|
"service.pid": os.getpid(),
|
211
|
+
"deployment.environment": environment,
|
198
212
|
**(attributes if attributes is not None else {}),
|
199
213
|
},
|
200
214
|
)
|
@@ -273,7 +287,6 @@ class OpenTelemetry:
|
|
273
287
|
message: str,
|
274
288
|
*args: Any,
|
275
289
|
exception: BaseException | None,
|
276
|
-
**extra: Any,
|
277
290
|
) -> None:
|
278
291
|
assert root_scope is not None # nosec: B101
|
279
292
|
assert scope.scope_id in scopes # nosec: B101
|
@@ -291,10 +304,10 @@ class OpenTelemetry:
|
|
291
304
|
def event_recording(
|
292
305
|
scope: ScopeIdentifier,
|
293
306
|
/,
|
294
|
-
*,
|
295
307
|
level: ObservabilityLevel,
|
296
|
-
|
297
|
-
|
308
|
+
*,
|
309
|
+
event: str,
|
310
|
+
attributes: Mapping[str, ObservabilityAttribute],
|
298
311
|
) -> None:
|
299
312
|
assert root_scope is not None # nosec: B101
|
300
313
|
assert scope.scope_id in scopes # nosec: B101
|
@@ -302,16 +315,20 @@ class OpenTelemetry:
|
|
302
315
|
if level < observed_level:
|
303
316
|
return
|
304
317
|
|
305
|
-
scopes[scope.scope_id].record_event(
|
318
|
+
scopes[scope.scope_id].record_event(
|
319
|
+
event,
|
320
|
+
attributes=attributes,
|
321
|
+
)
|
306
322
|
|
307
323
|
def metric_recording(
|
308
324
|
scope: ScopeIdentifier,
|
309
325
|
/,
|
326
|
+
level: ObservabilityLevel,
|
310
327
|
*,
|
311
328
|
metric: str,
|
312
329
|
value: float | int,
|
313
330
|
unit: str | None,
|
314
|
-
|
331
|
+
attributes: Mapping[str, ObservabilityAttribute],
|
315
332
|
) -> None:
|
316
333
|
assert root_scope is not None # nosec: B101
|
317
334
|
assert scope.scope_id in scopes # nosec: B101
|
@@ -323,13 +340,18 @@ class OpenTelemetry:
|
|
323
340
|
metric,
|
324
341
|
value=value,
|
325
342
|
unit=unit,
|
343
|
+
attributes=attributes,
|
326
344
|
)
|
327
345
|
|
328
346
|
def attributes_recording(
|
329
347
|
scope: ScopeIdentifier,
|
330
348
|
/,
|
331
|
-
|
349
|
+
level: ObservabilityLevel,
|
350
|
+
attributes: Mapping[str, ObservabilityAttribute],
|
332
351
|
) -> None:
|
352
|
+
if level < observed_level:
|
353
|
+
return
|
354
|
+
|
333
355
|
if not attributes:
|
334
356
|
return
|
335
357
|
|
@@ -363,9 +385,9 @@ class OpenTelemetry:
|
|
363
385
|
parent_id=scope.parent_id,
|
364
386
|
),
|
365
387
|
attributes={
|
366
|
-
"trace_id": scope.trace_id,
|
367
|
-
"scope_id": scope.scope_id,
|
368
|
-
"parent_id": scope.parent_id,
|
388
|
+
"context.trace_id": scope.trace_id,
|
389
|
+
"context.scope_id": scope.scope_id,
|
390
|
+
"context.parent_id": scope.parent_id,
|
369
391
|
},
|
370
392
|
),
|
371
393
|
meter=meter,
|
@@ -390,9 +412,9 @@ class OpenTelemetry:
|
|
390
412
|
),
|
391
413
|
),
|
392
414
|
attributes={
|
393
|
-
"trace_id": scope.trace_id,
|
394
|
-
"scope_id": scope.scope_id,
|
395
|
-
"parent_id": scope.parent_id,
|
415
|
+
"context.trace_id": scope.trace_id,
|
416
|
+
"context.scope_id": scope.scope_id,
|
417
|
+
"context.parent_id": scope.parent_id,
|
396
418
|
},
|
397
419
|
),
|
398
420
|
meter=meter,
|
haiway/state/structure.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import typing
|
2
|
-
from collections.abc import
|
2
|
+
from collections.abc import Mapping
|
3
3
|
from types import EllipsisType, GenericAlias
|
4
4
|
from typing import (
|
5
5
|
Any,
|
@@ -366,15 +366,8 @@ class State(metaclass=StateMeta):
|
|
366
366
|
) -> Self:
|
367
367
|
return self.__replace__(**kwargs)
|
368
368
|
|
369
|
-
def to_str(
|
370
|
-
self
|
371
|
-
pretty: bool = False,
|
372
|
-
) -> str:
|
373
|
-
if pretty:
|
374
|
-
return _state_str(self)
|
375
|
-
|
376
|
-
else:
|
377
|
-
return self.__str__()
|
369
|
+
def to_str(self) -> str:
|
370
|
+
return self.__str__()
|
378
371
|
|
379
372
|
def to_mapping(
|
380
373
|
self,
|
@@ -448,144 +441,3 @@ class State(metaclass=StateMeta):
|
|
448
441
|
**kwargs,
|
449
442
|
}
|
450
443
|
)
|
451
|
-
|
452
|
-
|
453
|
-
def _attribute_str(
|
454
|
-
*,
|
455
|
-
key: str,
|
456
|
-
value: str,
|
457
|
-
) -> str:
|
458
|
-
return f"┝ {key}: {value}"
|
459
|
-
|
460
|
-
|
461
|
-
def _element_str(
|
462
|
-
*,
|
463
|
-
key: Any,
|
464
|
-
value: Any,
|
465
|
-
) -> str:
|
466
|
-
return f"[{key}]: {value}"
|
467
|
-
|
468
|
-
|
469
|
-
def _state_str(
|
470
|
-
state: State,
|
471
|
-
/,
|
472
|
-
) -> str:
|
473
|
-
variables: ItemsView[str, Any] = vars(state).items()
|
474
|
-
|
475
|
-
parts: list[str] = [f"┍━ {type(state).__name__}:"]
|
476
|
-
for key, value in variables:
|
477
|
-
value_string: str | None = _value_str(value)
|
478
|
-
|
479
|
-
if value_string:
|
480
|
-
parts.append(
|
481
|
-
_attribute_str(
|
482
|
-
key=key,
|
483
|
-
value=value_string,
|
484
|
-
)
|
485
|
-
)
|
486
|
-
|
487
|
-
else:
|
488
|
-
continue # skip empty elements
|
489
|
-
|
490
|
-
if parts:
|
491
|
-
return "\n".join(parts) + "\n┕━"
|
492
|
-
|
493
|
-
else:
|
494
|
-
return "╍"
|
495
|
-
|
496
|
-
|
497
|
-
def _mapping_str(
|
498
|
-
dictionary: Mapping[Any, Any],
|
499
|
-
/,
|
500
|
-
) -> str | None:
|
501
|
-
elements: ItemsView[Any, Any] = dictionary.items()
|
502
|
-
|
503
|
-
parts: list[str] = []
|
504
|
-
for key, value in elements:
|
505
|
-
value_string: str | None = _value_str(value)
|
506
|
-
|
507
|
-
if value_string:
|
508
|
-
parts.append(
|
509
|
-
_element_str(
|
510
|
-
key=key,
|
511
|
-
value=value_string,
|
512
|
-
)
|
513
|
-
)
|
514
|
-
|
515
|
-
else:
|
516
|
-
continue # skip empty elements
|
517
|
-
|
518
|
-
if parts:
|
519
|
-
return "\n| " + "\n".join(parts).replace("\n", "\n| ")
|
520
|
-
|
521
|
-
else:
|
522
|
-
return None
|
523
|
-
|
524
|
-
|
525
|
-
def _sequence_str(
|
526
|
-
sequence: Sequence[Any],
|
527
|
-
/,
|
528
|
-
) -> str | None:
|
529
|
-
parts: list[str] = []
|
530
|
-
for idx, element in enumerate(sequence):
|
531
|
-
element_string: str | None = _value_str(element)
|
532
|
-
|
533
|
-
if element_string:
|
534
|
-
parts.append(
|
535
|
-
_element_str(
|
536
|
-
key=idx,
|
537
|
-
value=element_string,
|
538
|
-
)
|
539
|
-
)
|
540
|
-
|
541
|
-
else:
|
542
|
-
continue # skip empty elements
|
543
|
-
|
544
|
-
if parts:
|
545
|
-
return "\n| " + "\n".join(parts).replace("\n", "\n| ")
|
546
|
-
|
547
|
-
else:
|
548
|
-
return None
|
549
|
-
|
550
|
-
|
551
|
-
def _raw_value_str(
|
552
|
-
value: Any,
|
553
|
-
/,
|
554
|
-
) -> str | None:
|
555
|
-
if value is MISSING:
|
556
|
-
return None # skip missing
|
557
|
-
|
558
|
-
else:
|
559
|
-
return str(value).strip().replace("\n", "\n| ")
|
560
|
-
|
561
|
-
|
562
|
-
def _value_str( # noqa: PLR0911
|
563
|
-
value: Any,
|
564
|
-
/,
|
565
|
-
) -> str | None:
|
566
|
-
# check for string
|
567
|
-
if isinstance(value, str):
|
568
|
-
if "\n" in value:
|
569
|
-
return f'"""\n{value}\n"""'.replace("\n", "\n| ")
|
570
|
-
|
571
|
-
else:
|
572
|
-
return f'"{value}"'
|
573
|
-
|
574
|
-
# check for bytes
|
575
|
-
elif isinstance(value, bytes):
|
576
|
-
return f'b"{value}"'
|
577
|
-
|
578
|
-
# try unpack state
|
579
|
-
elif isinstance(value, State):
|
580
|
-
return _state_str(value)
|
581
|
-
|
582
|
-
# try unpack mapping
|
583
|
-
elif isinstance(value, Mapping):
|
584
|
-
return _mapping_str(value)
|
585
|
-
|
586
|
-
# try unpack sequence
|
587
|
-
elif isinstance(value, Sequence):
|
588
|
-
return _sequence_str(value)
|
589
|
-
|
590
|
-
else: # fallback to other
|
591
|
-
return _raw_value_str(value)
|
haiway/utils/__init__.py
CHANGED
@@ -8,6 +8,7 @@ from haiway.utils.env import (
|
|
8
8
|
getenv_str,
|
9
9
|
load_env,
|
10
10
|
)
|
11
|
+
from haiway.utils.formatting import format_str
|
11
12
|
from haiway.utils.freezing import freeze
|
12
13
|
from haiway.utils.logs import setup_logging
|
13
14
|
from haiway.utils.mimic import mimic_function
|
@@ -25,6 +26,7 @@ __all__ = (
|
|
25
26
|
"as_tuple",
|
26
27
|
"async_always",
|
27
28
|
"async_noop",
|
29
|
+
"format_str",
|
28
30
|
"freeze",
|
29
31
|
"getenv_base64",
|
30
32
|
"getenv_bool",
|
@@ -0,0 +1,151 @@
|
|
1
|
+
from collections.abc import ItemsView, Mapping, Sequence
|
2
|
+
from typing import Any
|
3
|
+
|
4
|
+
from haiway.types.missing import MISSING
|
5
|
+
|
6
|
+
__all__ = ("format_str",)
|
7
|
+
|
8
|
+
|
9
|
+
def format_str( # noqa: PLR0911
|
10
|
+
value: Any,
|
11
|
+
/,
|
12
|
+
) -> str:
|
13
|
+
# check for string
|
14
|
+
if isinstance(value, str):
|
15
|
+
if "\n" in value:
|
16
|
+
return f'"""\n{value.replace("\n", "\n ")}\n"""'
|
17
|
+
|
18
|
+
else:
|
19
|
+
return f'"{value}"'
|
20
|
+
|
21
|
+
# check for bytes
|
22
|
+
elif isinstance(value, bytes):
|
23
|
+
return f"b'{value}'"
|
24
|
+
|
25
|
+
# try unpack mapping
|
26
|
+
elif isinstance(value, Mapping):
|
27
|
+
return _mapping_str(value)
|
28
|
+
|
29
|
+
# try unpack sequence
|
30
|
+
elif isinstance(value, Sequence):
|
31
|
+
return _sequence_str(value)
|
32
|
+
|
33
|
+
elif value is MISSING:
|
34
|
+
return ""
|
35
|
+
|
36
|
+
else: # fallback to object
|
37
|
+
return _object_str(value)
|
38
|
+
|
39
|
+
|
40
|
+
def _attribute_str(
|
41
|
+
*,
|
42
|
+
key: str,
|
43
|
+
value: str,
|
44
|
+
) -> str:
|
45
|
+
if "\n" in value:
|
46
|
+
formatted_value: str = value.replace("\n", "\n| ")
|
47
|
+
return f"┝ {key}:\n{formatted_value}"
|
48
|
+
|
49
|
+
else:
|
50
|
+
return f"┝ {key}: {value}"
|
51
|
+
|
52
|
+
|
53
|
+
def _element_str(
|
54
|
+
*,
|
55
|
+
key: Any,
|
56
|
+
value: Any,
|
57
|
+
) -> str:
|
58
|
+
if "\n" in value:
|
59
|
+
formatted_value: str = value.replace("\n", "\n ")
|
60
|
+
return f"[{key}]:\n{formatted_value}"
|
61
|
+
|
62
|
+
else:
|
63
|
+
return f"[{key}]: {value}"
|
64
|
+
|
65
|
+
|
66
|
+
def _object_str(
|
67
|
+
other: object,
|
68
|
+
/,
|
69
|
+
) -> str:
|
70
|
+
if not hasattr(other, "__dict__"):
|
71
|
+
return str(other)
|
72
|
+
|
73
|
+
variables: ItemsView[str, Any] = vars(other).items()
|
74
|
+
|
75
|
+
parts: list[str] = [f"┍━ {type(other).__name__}:"]
|
76
|
+
for key, value in variables:
|
77
|
+
if key.startswith("_"):
|
78
|
+
continue # skip private and dunder
|
79
|
+
|
80
|
+
value_string: str = format_str(value)
|
81
|
+
|
82
|
+
if value_string:
|
83
|
+
parts.append(
|
84
|
+
_attribute_str(
|
85
|
+
key=key,
|
86
|
+
value=value_string,
|
87
|
+
)
|
88
|
+
)
|
89
|
+
|
90
|
+
else:
|
91
|
+
continue # skip empty elements
|
92
|
+
|
93
|
+
if parts:
|
94
|
+
return "\n".join(parts) + "\n┕━"
|
95
|
+
|
96
|
+
else:
|
97
|
+
return ""
|
98
|
+
|
99
|
+
|
100
|
+
def _mapping_str(
|
101
|
+
mapping: Mapping[Any, Any],
|
102
|
+
/,
|
103
|
+
) -> str:
|
104
|
+
items: ItemsView[Any, Any] = mapping.items()
|
105
|
+
|
106
|
+
parts: list[str] = []
|
107
|
+
for key, value in items:
|
108
|
+
value_string: str = format_str(value)
|
109
|
+
|
110
|
+
if value_string:
|
111
|
+
parts.append(
|
112
|
+
_element_str(
|
113
|
+
key=key,
|
114
|
+
value=value_string,
|
115
|
+
)
|
116
|
+
)
|
117
|
+
|
118
|
+
else:
|
119
|
+
continue # skip empty items
|
120
|
+
|
121
|
+
if parts:
|
122
|
+
return "{\n " + "\n".join(parts) + "\n}"
|
123
|
+
|
124
|
+
else:
|
125
|
+
return "{}"
|
126
|
+
|
127
|
+
|
128
|
+
def _sequence_str(
|
129
|
+
sequence: Sequence[Any],
|
130
|
+
/,
|
131
|
+
) -> str:
|
132
|
+
parts: list[str] = []
|
133
|
+
for idx, element in enumerate(sequence):
|
134
|
+
element_string: str = format_str(element)
|
135
|
+
|
136
|
+
if element_string:
|
137
|
+
parts.append(
|
138
|
+
_element_str(
|
139
|
+
key=idx,
|
140
|
+
value=element_string,
|
141
|
+
)
|
142
|
+
)
|
143
|
+
|
144
|
+
else:
|
145
|
+
continue # skip empty elements
|
146
|
+
|
147
|
+
if parts:
|
148
|
+
return "[\n " + "\n".join(parts) + "\n]"
|
149
|
+
|
150
|
+
else:
|
151
|
+
return "[]"
|