dreadnode 1.0.0rc3__tar.gz → 1.0.2__tar.gz
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.
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/PKG-INFO +1 -2
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/api/client.py +61 -14
- dreadnode-1.0.2/dreadnode/api/models.py +304 -0
- dreadnode-1.0.2/dreadnode/api/util.py +173 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/integrations/transformers.py +3 -3
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/main.py +29 -8
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/object.py +3 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/task.py +51 -13
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/tracing/span.py +29 -11
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/types.py +8 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/pyproject.toml +3 -4
- dreadnode-1.0.0rc3/dreadnode/api/models.py +0 -210
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/README.md +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/__init__.py +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/api/__init__.py +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/artifact/__init__.py +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/artifact/merger.py +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/artifact/storage.py +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/artifact/tree_builder.py +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/constants.py +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/integrations/__init__.py +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/metric.py +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/py.typed +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/serialization.py +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/tracing/__init__.py +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/tracing/constants.py +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/tracing/exporters.py +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/util.py +0 -0
- {dreadnode-1.0.0rc3 → dreadnode-1.0.2}/dreadnode/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: dreadnode
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Dreadnode SDK
|
|
5
5
|
Author: Nick Landers
|
|
6
6
|
Author-email: monoxgas@gmail.com
|
|
@@ -12,7 +12,6 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.13
|
|
13
13
|
Provides-Extra: training
|
|
14
14
|
Requires-Dist: coolname (>=2.2.0,<3.0.0)
|
|
15
|
-
Requires-Dist: fast-depends (>=2.4.12,<3.0.0)
|
|
16
15
|
Requires-Dist: fsspec[s3] (>=2023.1.0,<=2025.3.0)
|
|
17
16
|
Requires-Dist: httpx (>=0.28.0,<0.29.0)
|
|
18
17
|
Requires-Dist: logfire (>=3.5.3,<4.0.0)
|
|
@@ -7,18 +7,29 @@ import pandas as pd
|
|
|
7
7
|
from pydantic import BaseModel
|
|
8
8
|
from ulid import ULID
|
|
9
9
|
|
|
10
|
+
from dreadnode.api.util import (
|
|
11
|
+
convert_flat_tasks_to_tree,
|
|
12
|
+
convert_flat_trace_to_tree,
|
|
13
|
+
process_run,
|
|
14
|
+
process_task,
|
|
15
|
+
)
|
|
10
16
|
from dreadnode.util import logger
|
|
11
17
|
from dreadnode.version import VERSION
|
|
12
18
|
|
|
13
19
|
from .models import (
|
|
14
20
|
MetricAggregationType,
|
|
15
21
|
Project,
|
|
22
|
+
RawRun,
|
|
23
|
+
RawTask,
|
|
16
24
|
Run,
|
|
25
|
+
RunSummary,
|
|
17
26
|
StatusFilter,
|
|
18
27
|
Task,
|
|
28
|
+
TaskTree,
|
|
19
29
|
TimeAggregationType,
|
|
20
30
|
TimeAxisType,
|
|
21
31
|
TraceSpan,
|
|
32
|
+
TraceTree,
|
|
22
33
|
UserDataCredentials,
|
|
23
34
|
)
|
|
24
35
|
|
|
@@ -119,27 +130,63 @@ class ApiClient:
|
|
|
119
130
|
response = self.request("GET", f"/strikes/projects/{project!s}")
|
|
120
131
|
return Project(**response.json())
|
|
121
132
|
|
|
122
|
-
def list_runs(self, project: str) -> list[
|
|
133
|
+
def list_runs(self, project: str) -> list[RunSummary]:
|
|
123
134
|
response = self.request("GET", f"/strikes/projects/{project!s}/runs")
|
|
124
|
-
return [
|
|
135
|
+
return [RunSummary(**run) for run in response.json()]
|
|
125
136
|
|
|
126
|
-
def
|
|
137
|
+
def _get_run(self, run: str | ULID) -> RawRun:
|
|
127
138
|
response = self.request("GET", f"/strikes/projects/runs/{run!s}")
|
|
128
|
-
return
|
|
129
|
-
|
|
130
|
-
def get_run_tasks(self, run: str | ULID) -> list[Task]:
|
|
131
|
-
response = self.request("GET", f"/strikes/projects/runs/{run!s}/tasks")
|
|
132
|
-
return [Task(**task) for task in response.json()]
|
|
139
|
+
return RawRun(**response.json())
|
|
133
140
|
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
def get_run(self, run: str | ULID) -> Run:
|
|
142
|
+
return process_run(self._get_run(run))
|
|
143
|
+
|
|
144
|
+
TraceFormat = t.Literal["tree", "flat"]
|
|
145
|
+
|
|
146
|
+
@t.overload
|
|
147
|
+
def get_run_tasks(
|
|
148
|
+
self, run: str | ULID, *, format: t.Literal["tree"] = "tree"
|
|
149
|
+
) -> list[TaskTree]: ...
|
|
150
|
+
|
|
151
|
+
@t.overload
|
|
152
|
+
def get_run_tasks(
|
|
153
|
+
self, run: str | ULID, *, format: t.Literal["flat"] = "flat"
|
|
154
|
+
) -> list[Task]: ...
|
|
155
|
+
|
|
156
|
+
def get_run_tasks(
|
|
157
|
+
self, run: str | ULID, *, format: TraceFormat = "flat"
|
|
158
|
+
) -> list[Task] | list[TaskTree]:
|
|
159
|
+
raw_run = self._get_run(run)
|
|
160
|
+
response = self.request("GET", f"/strikes/projects/runs/{run!s}/tasks/full")
|
|
161
|
+
raw_tasks = [RawTask(**task) for task in response.json()]
|
|
162
|
+
tasks = [process_task(task, raw_run) for task in raw_tasks]
|
|
163
|
+
tasks = sorted(tasks, key=lambda x: x.timestamp)
|
|
164
|
+
return tasks if format == "flat" else convert_flat_tasks_to_tree(tasks)
|
|
165
|
+
|
|
166
|
+
@t.overload
|
|
167
|
+
def get_run_trace(
|
|
168
|
+
self, run: str | ULID, *, format: t.Literal["tree"] = "tree"
|
|
169
|
+
) -> list[TraceTree]: ...
|
|
170
|
+
|
|
171
|
+
@t.overload
|
|
172
|
+
def get_run_trace(
|
|
173
|
+
self, run: str | ULID, *, format: t.Literal["flat"] = "flat"
|
|
174
|
+
) -> list[Task | TraceSpan]: ...
|
|
175
|
+
|
|
176
|
+
def get_run_trace(
|
|
177
|
+
self, run: str | ULID, *, format: TraceFormat = "flat"
|
|
178
|
+
) -> list[Task | TraceSpan] | list[TraceTree]:
|
|
179
|
+
raw_run = self._get_run(run)
|
|
180
|
+
response = self.request("GET", f"/strikes/projects/runs/{run!s}/spans/full")
|
|
181
|
+
trace: list[Task | TraceSpan] = []
|
|
137
182
|
for item in response.json():
|
|
138
183
|
if "parent_task_span_id" in item:
|
|
139
|
-
|
|
184
|
+
trace.append(process_task(RawTask(**item), raw_run))
|
|
140
185
|
else:
|
|
141
|
-
|
|
142
|
-
|
|
186
|
+
trace.append(TraceSpan(**item))
|
|
187
|
+
|
|
188
|
+
trace = sorted(trace, key=lambda x: x.timestamp)
|
|
189
|
+
return trace if format == "flat" else convert_flat_trace_to_tree(trace)
|
|
143
190
|
|
|
144
191
|
# Data exports
|
|
145
192
|
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import typing as t
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from functools import cached_property
|
|
5
|
+
from uuid import UUID
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from pydantic import (
|
|
9
|
+
BaseModel,
|
|
10
|
+
ConfigDict,
|
|
11
|
+
Field,
|
|
12
|
+
PrivateAttr,
|
|
13
|
+
TypeAdapter,
|
|
14
|
+
ValidationError,
|
|
15
|
+
field_validator,
|
|
16
|
+
)
|
|
17
|
+
from ulid import ULID
|
|
18
|
+
|
|
19
|
+
AnyDict = dict[str, t.Any]
|
|
20
|
+
|
|
21
|
+
# User
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class UserAPIKey(BaseModel):
|
|
25
|
+
key: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class UserResponse(BaseModel):
|
|
29
|
+
id: UUID
|
|
30
|
+
email_address: str
|
|
31
|
+
username: str
|
|
32
|
+
api_key: UserAPIKey
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Strikes
|
|
36
|
+
|
|
37
|
+
SpanStatus = t.Literal[
|
|
38
|
+
"pending", # A pending span has been created
|
|
39
|
+
"completed", # The span has been finished
|
|
40
|
+
"failed", # The raised an exception
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
ExportFormat = t.Literal["csv", "json", "jsonl", "parquet"]
|
|
44
|
+
StatusFilter = t.Literal["all", "completed", "failed"]
|
|
45
|
+
TimeAxisType = t.Literal["wall", "relative", "step"]
|
|
46
|
+
TimeAggregationType = t.Literal["max", "min", "sum", "count"]
|
|
47
|
+
MetricAggregationType = t.Literal[
|
|
48
|
+
"avg",
|
|
49
|
+
"median",
|
|
50
|
+
"min",
|
|
51
|
+
"max",
|
|
52
|
+
"sum",
|
|
53
|
+
"first",
|
|
54
|
+
"last",
|
|
55
|
+
"count",
|
|
56
|
+
"std",
|
|
57
|
+
"var",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SpanException(BaseModel):
|
|
62
|
+
type: str
|
|
63
|
+
message: str
|
|
64
|
+
stacktrace: str
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SpanEvent(BaseModel):
|
|
68
|
+
timestamp: datetime
|
|
69
|
+
name: str
|
|
70
|
+
attributes: AnyDict
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class SpanLink(BaseModel):
|
|
74
|
+
trace_id: str
|
|
75
|
+
span_id: str
|
|
76
|
+
attributes: AnyDict
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TraceLog(BaseModel):
|
|
80
|
+
timestamp: datetime
|
|
81
|
+
body: str
|
|
82
|
+
severity: str
|
|
83
|
+
service: str | None
|
|
84
|
+
trace_id: str | None
|
|
85
|
+
span_id: str | None
|
|
86
|
+
attributes: AnyDict
|
|
87
|
+
container: str | None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class TraceSpan(BaseModel):
|
|
91
|
+
timestamp: datetime
|
|
92
|
+
duration: int
|
|
93
|
+
trace_id: str = Field(repr=False)
|
|
94
|
+
span_id: str
|
|
95
|
+
parent_span_id: str | None = Field(repr=False)
|
|
96
|
+
service_name: str | None = Field(repr=False)
|
|
97
|
+
status: SpanStatus
|
|
98
|
+
exception: SpanException | None
|
|
99
|
+
name: str
|
|
100
|
+
attributes: AnyDict = Field(repr=False)
|
|
101
|
+
resource_attributes: AnyDict = Field(repr=False)
|
|
102
|
+
events: list[SpanEvent] = Field(repr=False)
|
|
103
|
+
links: list[SpanLink] = Field(repr=False)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Metric(BaseModel):
|
|
107
|
+
value: float
|
|
108
|
+
step: int
|
|
109
|
+
timestamp: datetime
|
|
110
|
+
attributes: AnyDict
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class ObjectRef(BaseModel):
|
|
114
|
+
name: str
|
|
115
|
+
label: str
|
|
116
|
+
hash: str
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class RawObjectUri(BaseModel):
|
|
120
|
+
hash: str
|
|
121
|
+
schema_hash: str
|
|
122
|
+
uri: str
|
|
123
|
+
size: int
|
|
124
|
+
type: t.Literal["uri"]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class RawObjectVal(BaseModel):
|
|
128
|
+
hash: str
|
|
129
|
+
schema_hash: str
|
|
130
|
+
value: t.Any
|
|
131
|
+
type: t.Literal["val"]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
RawObject = RawObjectUri | RawObjectVal
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class V0Object(BaseModel):
|
|
138
|
+
name: str
|
|
139
|
+
label: str
|
|
140
|
+
value: t.Any
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class ObjectVal(BaseModel):
|
|
144
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
145
|
+
|
|
146
|
+
name: str
|
|
147
|
+
label: str
|
|
148
|
+
hash: str = Field(repr=False)
|
|
149
|
+
schema_: AnyDict
|
|
150
|
+
schema_hash: str = Field(repr=False)
|
|
151
|
+
value: t.Any
|
|
152
|
+
|
|
153
|
+
@field_validator("value")
|
|
154
|
+
@classmethod
|
|
155
|
+
def validate_value(cls, value: t.Any) -> t.Any:
|
|
156
|
+
if isinstance(value, str):
|
|
157
|
+
with contextlib.suppress(ValidationError):
|
|
158
|
+
return TypeAdapter(t.Any).validate_json(value)
|
|
159
|
+
|
|
160
|
+
return value
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ObjectUri(BaseModel):
|
|
164
|
+
name: str
|
|
165
|
+
label: str
|
|
166
|
+
hash: str = Field(repr=False)
|
|
167
|
+
schema_: AnyDict
|
|
168
|
+
schema_hash: str = Field(repr=False)
|
|
169
|
+
uri: str
|
|
170
|
+
size: int
|
|
171
|
+
|
|
172
|
+
_value: t.Any = PrivateAttr(default=None)
|
|
173
|
+
|
|
174
|
+
@cached_property
|
|
175
|
+
def value(self) -> t.Any:
|
|
176
|
+
if self._value is not None:
|
|
177
|
+
return self._value
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
response = requests.get(self.uri, timeout=5)
|
|
181
|
+
response.raise_for_status()
|
|
182
|
+
self._value = response.text
|
|
183
|
+
except requests.RequestException as e:
|
|
184
|
+
raise RuntimeError(f"Failed to fetch object from {self.uri}") from e
|
|
185
|
+
|
|
186
|
+
if isinstance(self._value, str):
|
|
187
|
+
with contextlib.suppress(ValidationError):
|
|
188
|
+
self._value = TypeAdapter(t.Any).validate_json(self._value)
|
|
189
|
+
|
|
190
|
+
return self._value
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
Object = ObjectVal | ObjectUri
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class ArtifactFile(BaseModel):
|
|
197
|
+
hash: str
|
|
198
|
+
uri: str
|
|
199
|
+
size_bytes: int
|
|
200
|
+
final_real_path: str
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class ArtifactDir(BaseModel):
|
|
204
|
+
dir_path: str
|
|
205
|
+
hash: str
|
|
206
|
+
children: list[t.Union["ArtifactDir", ArtifactFile]]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class RunSummary(BaseModel):
|
|
210
|
+
id: ULID
|
|
211
|
+
name: str
|
|
212
|
+
span_id: str = Field(repr=False)
|
|
213
|
+
trace_id: str = Field(repr=False)
|
|
214
|
+
timestamp: datetime
|
|
215
|
+
duration: int
|
|
216
|
+
status: SpanStatus
|
|
217
|
+
exception: SpanException | None
|
|
218
|
+
tags: set[str]
|
|
219
|
+
params: AnyDict = Field(repr=False)
|
|
220
|
+
metrics: dict[str, list[Metric]] = Field(repr=False)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class RawRun(RunSummary):
|
|
224
|
+
inputs: list[ObjectRef] = Field(repr=False)
|
|
225
|
+
outputs: list[ObjectRef] = Field(repr=False)
|
|
226
|
+
objects: dict[str, RawObject] = Field(repr=False)
|
|
227
|
+
object_schemas: AnyDict = Field(repr=False)
|
|
228
|
+
artifacts: list[ArtifactDir] = Field(repr=False)
|
|
229
|
+
schema_: AnyDict = Field(alias="schema", repr=False)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class Run(RunSummary):
|
|
233
|
+
inputs: dict[str, Object] = Field(repr=False)
|
|
234
|
+
outputs: dict[str, Object] = Field(repr=False)
|
|
235
|
+
artifacts: list[ArtifactDir] = Field(repr=False)
|
|
236
|
+
schema_: AnyDict = Field(alias="schema", repr=False)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class _Task(BaseModel):
|
|
240
|
+
name: str
|
|
241
|
+
span_id: str
|
|
242
|
+
trace_id: str = Field(repr=False)
|
|
243
|
+
parent_span_id: str | None = Field(repr=False)
|
|
244
|
+
parent_task_span_id: str | None = Field(repr=False)
|
|
245
|
+
timestamp: datetime
|
|
246
|
+
duration: int
|
|
247
|
+
status: SpanStatus
|
|
248
|
+
exception: SpanException | None
|
|
249
|
+
tags: set[str]
|
|
250
|
+
params: AnyDict = Field(repr=False)
|
|
251
|
+
metrics: dict[str, list[Metric]] = Field(repr=False)
|
|
252
|
+
schema_: AnyDict = Field(alias="schema", repr=False)
|
|
253
|
+
attributes: AnyDict = Field(repr=False)
|
|
254
|
+
resource_attributes: AnyDict = Field(repr=False)
|
|
255
|
+
events: list[SpanEvent] = Field(repr=False)
|
|
256
|
+
links: list[SpanLink] = Field(repr=False)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class RawTask(_Task):
|
|
260
|
+
inputs: list[ObjectRef] | list[V0Object] = Field(repr=False)
|
|
261
|
+
outputs: list[ObjectRef] | list[V0Object] = Field(repr=False)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
class Task(_Task):
|
|
265
|
+
inputs: dict[str, Object] = Field(repr=False)
|
|
266
|
+
outputs: dict[str, Object] = Field(repr=False)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class Project(BaseModel):
|
|
270
|
+
id: UUID = Field(repr=False)
|
|
271
|
+
key: str
|
|
272
|
+
name: str
|
|
273
|
+
description: str | None = Field(repr=False)
|
|
274
|
+
created_at: datetime
|
|
275
|
+
updated_at: datetime
|
|
276
|
+
run_count: int
|
|
277
|
+
last_run: RawRun | None = Field(repr=False)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# Derived types
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class TaskTree(BaseModel):
|
|
284
|
+
task: Task
|
|
285
|
+
children: list["TaskTree"] = []
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class TraceTree(BaseModel):
|
|
289
|
+
span: Task | TraceSpan
|
|
290
|
+
children: list["TraceTree"] = []
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# User data credentials
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
class UserDataCredentials(BaseModel):
|
|
297
|
+
access_key_id: str
|
|
298
|
+
secret_access_key: str
|
|
299
|
+
session_token: str
|
|
300
|
+
expiration: datetime
|
|
301
|
+
region: str
|
|
302
|
+
bucket: str
|
|
303
|
+
prefix: str
|
|
304
|
+
endpoint: str | None
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
|
|
3
|
+
from .models import (
|
|
4
|
+
Object,
|
|
5
|
+
ObjectUri,
|
|
6
|
+
ObjectVal,
|
|
7
|
+
RawObjectVal,
|
|
8
|
+
RawRun,
|
|
9
|
+
RawTask,
|
|
10
|
+
Run,
|
|
11
|
+
Task,
|
|
12
|
+
TaskTree,
|
|
13
|
+
TraceSpan,
|
|
14
|
+
TraceTree,
|
|
15
|
+
V0Object,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
logger = getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def convert_flat_tasks_to_tree(tasks: list[Task]) -> list[TaskTree]:
|
|
22
|
+
tree_nodes: dict[str, TaskTree] = {}
|
|
23
|
+
for task in tasks:
|
|
24
|
+
tree_nodes[task.span_id] = TaskTree(task=task)
|
|
25
|
+
|
|
26
|
+
roots: list[TaskTree] = []
|
|
27
|
+
for task in tasks:
|
|
28
|
+
if task.parent_task_span_id not in tree_nodes:
|
|
29
|
+
roots.append(tree_nodes[task.span_id])
|
|
30
|
+
else:
|
|
31
|
+
parent_node = tree_nodes.get(task.parent_task_span_id)
|
|
32
|
+
if parent_node:
|
|
33
|
+
parent_node.children.append(tree_nodes[task.span_id])
|
|
34
|
+
|
|
35
|
+
return roots
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def convert_flat_trace_to_tree(trace: list[Task | TraceSpan]) -> list[TraceTree]:
|
|
39
|
+
tree_nodes: dict[str, TraceTree] = {}
|
|
40
|
+
for span in trace:
|
|
41
|
+
tree_nodes[span.span_id] = TraceTree(span=span)
|
|
42
|
+
|
|
43
|
+
roots: list[TraceTree] = []
|
|
44
|
+
for span in trace:
|
|
45
|
+
if span.parent_span_id not in tree_nodes:
|
|
46
|
+
roots.append(tree_nodes[span.span_id])
|
|
47
|
+
else:
|
|
48
|
+
parent_node = tree_nodes.get(span.parent_span_id)
|
|
49
|
+
if parent_node:
|
|
50
|
+
parent_node.children.append(tree_nodes[span.span_id])
|
|
51
|
+
|
|
52
|
+
return roots
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def process_run(run: RawRun) -> Run:
|
|
56
|
+
inputs: dict[str, Object] = {}
|
|
57
|
+
outputs: dict[str, Object] = {}
|
|
58
|
+
|
|
59
|
+
for references, converted in ((run.inputs, inputs), (run.outputs, outputs)):
|
|
60
|
+
for ref in references:
|
|
61
|
+
if (_object := run.objects.get(ref.hash)) is None:
|
|
62
|
+
logger.error("Object %s not found in run %s", ref.hash, run.id)
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
if (_schema := run.object_schemas.get(_object.schema_hash)) is None:
|
|
66
|
+
logger.error("Schema for object %s not found in run %s", ref.hash, run.id)
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
if isinstance(_object, RawObjectVal):
|
|
70
|
+
converted[ref.name] = ObjectVal(
|
|
71
|
+
name=ref.name,
|
|
72
|
+
label=ref.label,
|
|
73
|
+
hash=ref.hash,
|
|
74
|
+
schema_=_schema,
|
|
75
|
+
schema_hash=_object.schema_hash,
|
|
76
|
+
value=_object.value,
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
converted[ref.name] = ObjectUri(
|
|
80
|
+
name=ref.name,
|
|
81
|
+
label=ref.label,
|
|
82
|
+
hash=ref.hash,
|
|
83
|
+
schema_=_schema,
|
|
84
|
+
schema_hash=_object.schema_hash,
|
|
85
|
+
uri=_object.uri,
|
|
86
|
+
size=_object.size,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return Run(
|
|
90
|
+
id=run.id,
|
|
91
|
+
name=run.name,
|
|
92
|
+
span_id=run.span_id,
|
|
93
|
+
trace_id=run.trace_id,
|
|
94
|
+
timestamp=run.timestamp,
|
|
95
|
+
duration=run.duration,
|
|
96
|
+
status=run.status,
|
|
97
|
+
exception=run.exception,
|
|
98
|
+
tags=run.tags,
|
|
99
|
+
params=run.params,
|
|
100
|
+
metrics=run.metrics,
|
|
101
|
+
inputs=inputs,
|
|
102
|
+
outputs=outputs,
|
|
103
|
+
artifacts=run.artifacts,
|
|
104
|
+
schema=run.schema_,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def process_task(task: RawTask, run: RawRun) -> Task:
|
|
109
|
+
inputs: dict[str, Object] = {}
|
|
110
|
+
outputs: dict[str, Object] = {}
|
|
111
|
+
|
|
112
|
+
for references, converted in ((task.inputs, inputs), (task.outputs, outputs)):
|
|
113
|
+
for ref in references:
|
|
114
|
+
if isinstance(ref, V0Object):
|
|
115
|
+
converted[ref.name] = ObjectVal(
|
|
116
|
+
name=ref.name,
|
|
117
|
+
label=ref.label,
|
|
118
|
+
hash="",
|
|
119
|
+
schema_={},
|
|
120
|
+
schema_hash="",
|
|
121
|
+
value=ref.value,
|
|
122
|
+
)
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
if (_object := run.objects.get(ref.hash)) is None:
|
|
126
|
+
logger.error("Object %s not found in run %s", ref.hash, run.id)
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
if (_schema := run.object_schemas.get(_object.schema_hash)) is None:
|
|
130
|
+
logger.error("Schema for object %s not found in run %s", ref.hash, run.id)
|
|
131
|
+
continue
|
|
132
|
+
|
|
133
|
+
if isinstance(_object, RawObjectVal):
|
|
134
|
+
converted[ref.name] = ObjectVal(
|
|
135
|
+
name=ref.name,
|
|
136
|
+
label=ref.label,
|
|
137
|
+
hash=ref.hash,
|
|
138
|
+
schema_=_schema,
|
|
139
|
+
schema_hash=_object.schema_hash,
|
|
140
|
+
value=_object.value,
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
converted[ref.name] = ObjectUri(
|
|
144
|
+
name=ref.name,
|
|
145
|
+
label=ref.label,
|
|
146
|
+
hash=ref.hash,
|
|
147
|
+
schema_=_schema,
|
|
148
|
+
schema_hash=_object.schema_hash,
|
|
149
|
+
uri=_object.uri,
|
|
150
|
+
size=_object.size,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return Task(
|
|
154
|
+
name=task.name,
|
|
155
|
+
span_id=task.span_id,
|
|
156
|
+
trace_id=task.trace_id,
|
|
157
|
+
parent_span_id=task.parent_span_id,
|
|
158
|
+
parent_task_span_id=task.parent_task_span_id,
|
|
159
|
+
timestamp=task.timestamp,
|
|
160
|
+
duration=task.duration,
|
|
161
|
+
status=task.status,
|
|
162
|
+
exception=task.exception,
|
|
163
|
+
tags=task.tags,
|
|
164
|
+
params=task.params,
|
|
165
|
+
metrics=task.metrics,
|
|
166
|
+
inputs=inputs,
|
|
167
|
+
outputs=outputs,
|
|
168
|
+
schema=task.schema_,
|
|
169
|
+
attributes=task.attributes,
|
|
170
|
+
resource_attributes=task.resource_attributes,
|
|
171
|
+
events=task.events,
|
|
172
|
+
links=task.links,
|
|
173
|
+
)
|
|
@@ -123,7 +123,7 @@ class DreadnodeCallback(TrainerCallback):
|
|
|
123
123
|
if self._run is None or state.epoch is None:
|
|
124
124
|
return
|
|
125
125
|
|
|
126
|
-
dn.log_metric("epoch", state.epoch)
|
|
126
|
+
dn.log_metric("epoch", state.epoch, to="run")
|
|
127
127
|
|
|
128
128
|
self._epoch_span = dn.task_span(f"Epoch {state.epoch}")
|
|
129
129
|
self._epoch_span.__enter__()
|
|
@@ -149,7 +149,7 @@ class DreadnodeCallback(TrainerCallback):
|
|
|
149
149
|
if self._run is None:
|
|
150
150
|
return
|
|
151
151
|
|
|
152
|
-
dn.log_metric("step", state.global_step)
|
|
152
|
+
dn.log_metric("step", state.global_step, to="run")
|
|
153
153
|
|
|
154
154
|
self._step_span = dn.span(f"Step {state.global_step}")
|
|
155
155
|
self._step_span.__enter__()
|
|
@@ -178,6 +178,6 @@ class DreadnodeCallback(TrainerCallback):
|
|
|
178
178
|
|
|
179
179
|
for key, value in _clean_keys(logs).items():
|
|
180
180
|
if isinstance(value, float | int):
|
|
181
|
-
dn.log_metric(key, value, step=state.global_step)
|
|
181
|
+
dn.log_metric(key, value, step=state.global_step, to="run")
|
|
182
182
|
|
|
183
183
|
dn.push_update()
|
|
@@ -7,7 +7,7 @@ import typing as t
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
from pathlib import Path
|
|
10
|
-
from urllib.parse import urljoin
|
|
10
|
+
from urllib.parse import urljoin, urlparse, urlunparse
|
|
11
11
|
|
|
12
12
|
import coolname # type: ignore [import-untyped]
|
|
13
13
|
import logfire
|
|
@@ -47,7 +47,10 @@ from dreadnode.tracing.span import (
|
|
|
47
47
|
current_task_span,
|
|
48
48
|
)
|
|
49
49
|
from dreadnode.types import (
|
|
50
|
+
INHERITED,
|
|
50
51
|
AnyDict,
|
|
52
|
+
Inherited,
|
|
53
|
+
JsonDict,
|
|
51
54
|
JsonValue,
|
|
52
55
|
)
|
|
53
56
|
from dreadnode.util import handle_internal_errors
|
|
@@ -204,6 +207,16 @@ class Dreadnode:
|
|
|
204
207
|
category=DreadnodeConfigWarning,
|
|
205
208
|
)
|
|
206
209
|
|
|
210
|
+
if self.server:
|
|
211
|
+
parsed_url = urlparse(self.server)
|
|
212
|
+
if not parsed_url.scheme:
|
|
213
|
+
netloc = parsed_url.path.split("/")[0]
|
|
214
|
+
path = "/".join(parsed_url.path.split("/")[1:])
|
|
215
|
+
parsed_new = parsed_url._replace(
|
|
216
|
+
scheme="https", netloc=netloc, path=f"/{path}" if path else ""
|
|
217
|
+
)
|
|
218
|
+
self.server = urlunparse(parsed_new)
|
|
219
|
+
|
|
207
220
|
if self.local_dir is not False:
|
|
208
221
|
config = FileExportConfig(
|
|
209
222
|
base_path=self.local_dir,
|
|
@@ -411,8 +424,8 @@ class Dreadnode:
|
|
|
411
424
|
name: str | None = None,
|
|
412
425
|
label: str | None = None,
|
|
413
426
|
log_params: t.Sequence[str] | bool = False,
|
|
414
|
-
log_inputs: t.Sequence[str] | bool =
|
|
415
|
-
log_output: bool =
|
|
427
|
+
log_inputs: t.Sequence[str] | bool | Inherited = INHERITED,
|
|
428
|
+
log_output: bool | Inherited = INHERITED,
|
|
416
429
|
tags: t.Sequence[str] | None = None,
|
|
417
430
|
**attributes: t.Any,
|
|
418
431
|
) -> TaskDecorator: ...
|
|
@@ -425,8 +438,8 @@ class Dreadnode:
|
|
|
425
438
|
name: str | None = None,
|
|
426
439
|
label: str | None = None,
|
|
427
440
|
log_params: t.Sequence[str] | bool = False,
|
|
428
|
-
log_inputs: t.Sequence[str] | bool =
|
|
429
|
-
log_output: bool =
|
|
441
|
+
log_inputs: t.Sequence[str] | bool | Inherited = INHERITED,
|
|
442
|
+
log_output: bool | Inherited = INHERITED,
|
|
430
443
|
tags: t.Sequence[str] | None = None,
|
|
431
444
|
**attributes: t.Any,
|
|
432
445
|
) -> ScoredTaskDecorator[R]: ...
|
|
@@ -438,8 +451,8 @@ class Dreadnode:
|
|
|
438
451
|
name: str | None = None,
|
|
439
452
|
label: str | None = None,
|
|
440
453
|
log_params: t.Sequence[str] | bool = False,
|
|
441
|
-
log_inputs: t.Sequence[str] | bool =
|
|
442
|
-
log_output: bool =
|
|
454
|
+
log_inputs: t.Sequence[str] | bool | Inherited = INHERITED,
|
|
455
|
+
log_output: bool | Inherited = INHERITED,
|
|
443
456
|
tags: t.Sequence[str] | None = None,
|
|
444
457
|
**attributes: t.Any,
|
|
445
458
|
) -> TaskDecorator:
|
|
@@ -621,6 +634,7 @@ class Dreadnode:
|
|
|
621
634
|
tags: t.Sequence[str] | None = None,
|
|
622
635
|
params: AnyDict | None = None,
|
|
623
636
|
project: str | None = None,
|
|
637
|
+
autolog: bool = True,
|
|
624
638
|
**attributes: t.Any,
|
|
625
639
|
) -> RunSpan:
|
|
626
640
|
"""
|
|
@@ -646,6 +660,7 @@ class Dreadnode:
|
|
|
646
660
|
project: The project name to associate the run with. If not provided,
|
|
647
661
|
the project passed to `configure()` will be used, or the
|
|
648
662
|
run will be associated with a default project.
|
|
663
|
+
autolog: Whether to automatically log task inputs, outputs, and execution metrics if unspecified.
|
|
649
664
|
**attributes: Additional attributes to attach to the run span.
|
|
650
665
|
"""
|
|
651
666
|
if not self._initialized:
|
|
@@ -663,6 +678,7 @@ class Dreadnode:
|
|
|
663
678
|
tags=tags,
|
|
664
679
|
file_system=self._fs,
|
|
665
680
|
prefix_path=self._fs_prefix,
|
|
681
|
+
autolog=autolog,
|
|
666
682
|
)
|
|
667
683
|
|
|
668
684
|
@handle_internal_errors()
|
|
@@ -759,6 +775,7 @@ class Dreadnode:
|
|
|
759
775
|
origin: t.Any | None = None,
|
|
760
776
|
timestamp: datetime | None = None,
|
|
761
777
|
mode: MetricAggMode | None = None,
|
|
778
|
+
attributes: JsonDict | None = None,
|
|
762
779
|
to: ToObject = "task-or-run",
|
|
763
780
|
) -> None:
|
|
764
781
|
"""
|
|
@@ -788,6 +805,7 @@ class Dreadnode:
|
|
|
788
805
|
- avg: the average of all reported values for this metric
|
|
789
806
|
- sum: the cumulative sum of all reported values for this metric
|
|
790
807
|
- count: increment every time this metric is logged - disregard value
|
|
808
|
+
attributes: A dictionary of additional attributes to attach to the metric.
|
|
791
809
|
to: The target object to log the metric to. Can be "task-or-run" or "run".
|
|
792
810
|
Defaults to "task-or-run". If "task-or-run", the metric will be logged
|
|
793
811
|
to the current task or run, whichever is the nearest ancestor.
|
|
@@ -842,6 +860,7 @@ class Dreadnode:
|
|
|
842
860
|
origin: t.Any | None = None,
|
|
843
861
|
timestamp: datetime | None = None,
|
|
844
862
|
mode: MetricAggMode | None = None,
|
|
863
|
+
attributes: JsonDict | None = None,
|
|
845
864
|
to: ToObject = "task-or-run",
|
|
846
865
|
) -> None:
|
|
847
866
|
task = current_task_span.get()
|
|
@@ -854,7 +873,9 @@ class Dreadnode:
|
|
|
854
873
|
metric = (
|
|
855
874
|
value
|
|
856
875
|
if isinstance(value, Metric)
|
|
857
|
-
else Metric(
|
|
876
|
+
else Metric(
|
|
877
|
+
float(value), step, timestamp or datetime.now(timezone.utc), attributes or {}
|
|
878
|
+
)
|
|
858
879
|
)
|
|
859
880
|
target.log_metric(key, metric, origin=origin, mode=mode)
|
|
860
881
|
|
|
@@ -9,6 +9,7 @@ from opentelemetry.trace import Tracer
|
|
|
9
9
|
|
|
10
10
|
from dreadnode.metric import Scorer, ScorerCallable
|
|
11
11
|
from dreadnode.tracing.span import TaskSpan, current_run_span
|
|
12
|
+
from dreadnode.types import INHERITED, Inherited
|
|
12
13
|
|
|
13
14
|
P = t.ParamSpec("P")
|
|
14
15
|
R = t.TypeVar("R")
|
|
@@ -114,9 +115,9 @@ class Task(t.Generic[P, R]):
|
|
|
114
115
|
|
|
115
116
|
log_params: t.Sequence[str] | bool = False
|
|
116
117
|
"Whether to log all, or specific, incoming arguments to the function as parameters."
|
|
117
|
-
log_inputs: t.Sequence[str] | bool =
|
|
118
|
+
log_inputs: t.Sequence[str] | bool | Inherited = INHERITED
|
|
118
119
|
"Whether to log all, or specific, incoming arguments to the function as inputs."
|
|
119
|
-
log_output: bool =
|
|
120
|
+
log_output: bool | Inherited = INHERITED
|
|
120
121
|
"Whether to automatically log the result of the function as an output."
|
|
121
122
|
|
|
122
123
|
def __post_init__(self) -> None:
|
|
@@ -128,6 +129,25 @@ class Task(t.Generic[P, R]):
|
|
|
128
129
|
self.__name__ = getattr(self.func, "__name__", self.name)
|
|
129
130
|
self.__doc__ = getattr(self.func, "__doc__", None)
|
|
130
131
|
|
|
132
|
+
def __get__(self, obj: t.Any, objtype: t.Any) -> "Task[P, R]":
|
|
133
|
+
if obj is None:
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
bound_func = self.func.__get__(obj, objtype)
|
|
137
|
+
|
|
138
|
+
return Task(
|
|
139
|
+
tracer=self.tracer,
|
|
140
|
+
name=self.name,
|
|
141
|
+
label=self.label,
|
|
142
|
+
attributes=self.attributes,
|
|
143
|
+
func=bound_func,
|
|
144
|
+
scorers=[scorer.clone() for scorer in self.scorers],
|
|
145
|
+
tags=self.tags.copy(),
|
|
146
|
+
log_params=self.log_params,
|
|
147
|
+
log_inputs=self.log_inputs,
|
|
148
|
+
log_output=self.log_output,
|
|
149
|
+
)
|
|
150
|
+
|
|
131
151
|
def _bind_args(self, *args: P.args, **kwargs: P.kwargs) -> dict[str, t.Any]:
|
|
132
152
|
signature = inspect.signature(self.func)
|
|
133
153
|
bound_args = signature.bind(*args, **kwargs)
|
|
@@ -220,6 +240,9 @@ class Task(t.Generic[P, R]):
|
|
|
220
240
|
if run is None or not run.is_recording:
|
|
221
241
|
raise RuntimeError("Tasks must be executed within a run")
|
|
222
242
|
|
|
243
|
+
log_inputs = run.autolog if isinstance(self.log_inputs, Inherited) else self.log_inputs
|
|
244
|
+
log_output = run.autolog if isinstance(self.log_output, Inherited) else self.log_output
|
|
245
|
+
|
|
223
246
|
bound_args = self._bind_args(*args, **kwargs)
|
|
224
247
|
|
|
225
248
|
params_to_log = (
|
|
@@ -231,9 +254,9 @@ class Task(t.Generic[P, R]):
|
|
|
231
254
|
)
|
|
232
255
|
inputs_to_log = (
|
|
233
256
|
bound_args
|
|
234
|
-
if
|
|
235
|
-
else {k: v for k, v in bound_args.items() if k in
|
|
236
|
-
if
|
|
257
|
+
if log_inputs is True
|
|
258
|
+
else {k: v for k, v in bound_args.items() if k in log_inputs}
|
|
259
|
+
if log_inputs is not False
|
|
237
260
|
else {}
|
|
238
261
|
)
|
|
239
262
|
|
|
@@ -246,13 +269,16 @@ class Task(t.Generic[P, R]):
|
|
|
246
269
|
run_id=run.run_id,
|
|
247
270
|
tracer=self.tracer,
|
|
248
271
|
) as span:
|
|
249
|
-
|
|
272
|
+
if run.autolog:
|
|
273
|
+
span.run.log_metric(
|
|
274
|
+
"count", 1, prefix=f"{self.label}.exec", mode="count", attributes={"auto": True}
|
|
275
|
+
)
|
|
250
276
|
|
|
251
277
|
for name, value in params_to_log.items():
|
|
252
278
|
span.log_param(name, value)
|
|
253
279
|
|
|
254
280
|
input_object_hashes: list[str] = [
|
|
255
|
-
span.log_input(name, value, label=f"{self.label}.input.{name}")
|
|
281
|
+
span.log_input(name, value, label=f"{self.label}.input.{name}", auto=True)
|
|
256
282
|
for name, value in inputs_to_log.items()
|
|
257
283
|
]
|
|
258
284
|
|
|
@@ -261,17 +287,29 @@ class Task(t.Generic[P, R]):
|
|
|
261
287
|
if inspect.isawaitable(output):
|
|
262
288
|
output = await output
|
|
263
289
|
except Exception:
|
|
264
|
-
|
|
290
|
+
if run.autolog:
|
|
291
|
+
span.run.log_metric(
|
|
292
|
+
"success_rate",
|
|
293
|
+
0,
|
|
294
|
+
prefix=f"{self.label}.exec",
|
|
295
|
+
mode="avg",
|
|
296
|
+
attributes={"auto": True},
|
|
297
|
+
)
|
|
265
298
|
raise
|
|
266
299
|
|
|
267
|
-
|
|
300
|
+
if run.autolog:
|
|
301
|
+
span.run.log_metric(
|
|
302
|
+
"success_rate",
|
|
303
|
+
1,
|
|
304
|
+
prefix=f"{self.label}.exec",
|
|
305
|
+
mode="avg",
|
|
306
|
+
attributes={"auto": True},
|
|
307
|
+
)
|
|
268
308
|
span.output = output
|
|
269
309
|
|
|
270
|
-
if
|
|
310
|
+
if log_output:
|
|
271
311
|
output_object_hash = span.log_output(
|
|
272
|
-
"output",
|
|
273
|
-
output,
|
|
274
|
-
label=f"{self.label}.output",
|
|
312
|
+
"output", output, label=f"{self.label}.output", auto=True
|
|
275
313
|
)
|
|
276
314
|
|
|
277
315
|
# Link the output to the inputs
|
|
@@ -250,11 +250,15 @@ class RunSpan(Span):
|
|
|
250
250
|
tracer: Tracer,
|
|
251
251
|
file_system: AbstractFileSystem,
|
|
252
252
|
prefix_path: str,
|
|
253
|
+
*,
|
|
253
254
|
params: AnyDict | None = None,
|
|
254
255
|
metrics: MetricDict | None = None,
|
|
255
256
|
run_id: str | None = None,
|
|
256
257
|
tags: t.Sequence[str] | None = None,
|
|
258
|
+
autolog: bool = True,
|
|
257
259
|
) -> None:
|
|
260
|
+
self.autolog = autolog
|
|
261
|
+
|
|
258
262
|
self._params = params or {}
|
|
259
263
|
self._metrics = metrics or {}
|
|
260
264
|
self._objects: dict[str, Object] = {}
|
|
@@ -301,6 +305,7 @@ class RunSpan(Span):
|
|
|
301
305
|
) -> None:
|
|
302
306
|
self.set_attribute(SPAN_ATTRIBUTE_PARAMS, self._params)
|
|
303
307
|
self.set_attribute(SPAN_ATTRIBUTE_INPUTS, self._inputs, schema=False)
|
|
308
|
+
self.set_attribute(SPAN_ATTRIBUTE_OUTPUTS, self._outputs, schema=False)
|
|
304
309
|
self.set_attribute(SPAN_ATTRIBUTE_METRICS, self._metrics, schema=False)
|
|
305
310
|
self.set_attribute(SPAN_ATTRIBUTE_OBJECTS, self._objects, schema=False)
|
|
306
311
|
self.set_attribute(
|
|
@@ -485,9 +490,8 @@ class RunSpan(Span):
|
|
|
485
490
|
value,
|
|
486
491
|
label=label,
|
|
487
492
|
event_name=EVENT_NAME_OBJECT_INPUT,
|
|
488
|
-
**attributes,
|
|
489
493
|
)
|
|
490
|
-
self._inputs.append(ObjectRef(name, label=label, hash=hash_))
|
|
494
|
+
self._inputs.append(ObjectRef(name, label=label, hash=hash_, attributes=attributes))
|
|
491
495
|
|
|
492
496
|
def log_artifact(
|
|
493
497
|
self,
|
|
@@ -527,6 +531,8 @@ class RunSpan(Span):
|
|
|
527
531
|
origin: t.Any | None = None,
|
|
528
532
|
timestamp: datetime | None = None,
|
|
529
533
|
mode: MetricAggMode | None = None,
|
|
534
|
+
prefix: str | None = None,
|
|
535
|
+
attributes: JsonDict | None = None,
|
|
530
536
|
) -> None: ...
|
|
531
537
|
|
|
532
538
|
@t.overload
|
|
@@ -537,6 +543,7 @@ class RunSpan(Span):
|
|
|
537
543
|
*,
|
|
538
544
|
origin: t.Any | None = None,
|
|
539
545
|
mode: MetricAggMode | None = None,
|
|
546
|
+
prefix: str | None = None,
|
|
540
547
|
) -> None: ...
|
|
541
548
|
|
|
542
549
|
def log_metric(
|
|
@@ -548,13 +555,21 @@ class RunSpan(Span):
|
|
|
548
555
|
origin: t.Any | None = None,
|
|
549
556
|
timestamp: datetime | None = None,
|
|
550
557
|
mode: MetricAggMode | None = None,
|
|
558
|
+
prefix: str | None = None,
|
|
559
|
+
attributes: JsonDict | None = None,
|
|
551
560
|
) -> None:
|
|
552
561
|
metric = (
|
|
553
562
|
value
|
|
554
563
|
if isinstance(value, Metric)
|
|
555
|
-
else Metric(
|
|
564
|
+
else Metric(
|
|
565
|
+
float(value), step, timestamp or datetime.now(timezone.utc), attributes or {}
|
|
566
|
+
)
|
|
556
567
|
)
|
|
557
568
|
|
|
569
|
+
key = re.sub(r"[^\w/]+", "_", key.lower())
|
|
570
|
+
if prefix is not None:
|
|
571
|
+
key = f"{prefix}.{key}"
|
|
572
|
+
|
|
558
573
|
if origin is not None:
|
|
559
574
|
origin_hash = self.log_object(
|
|
560
575
|
origin,
|
|
@@ -585,9 +600,8 @@ class RunSpan(Span):
|
|
|
585
600
|
value,
|
|
586
601
|
label=label,
|
|
587
602
|
event_name=EVENT_NAME_OBJECT_OUTPUT,
|
|
588
|
-
**attributes,
|
|
589
603
|
)
|
|
590
|
-
self._outputs.append(ObjectRef(name, label=label, hash=hash_))
|
|
604
|
+
self._outputs.append(ObjectRef(name, label=label, hash=hash_, attributes=attributes))
|
|
591
605
|
|
|
592
606
|
|
|
593
607
|
class TaskSpan(Span, t.Generic[R]):
|
|
@@ -689,9 +703,8 @@ class TaskSpan(Span, t.Generic[R]):
|
|
|
689
703
|
value,
|
|
690
704
|
label=label,
|
|
691
705
|
event_name=EVENT_NAME_OBJECT_OUTPUT,
|
|
692
|
-
**attributes,
|
|
693
706
|
)
|
|
694
|
-
self._outputs.append(ObjectRef(name, label=label, hash=hash_))
|
|
707
|
+
self._outputs.append(ObjectRef(name, label=label, hash=hash_, attributes=attributes))
|
|
695
708
|
return hash_
|
|
696
709
|
|
|
697
710
|
@property
|
|
@@ -721,9 +734,8 @@ class TaskSpan(Span, t.Generic[R]):
|
|
|
721
734
|
value,
|
|
722
735
|
label=label,
|
|
723
736
|
event_name=EVENT_NAME_OBJECT_INPUT,
|
|
724
|
-
**attributes,
|
|
725
737
|
)
|
|
726
|
-
self._inputs.append(ObjectRef(name, label=label, hash=hash_))
|
|
738
|
+
self._inputs.append(ObjectRef(name, label=label, hash=hash_, attributes=attributes))
|
|
727
739
|
return hash_
|
|
728
740
|
|
|
729
741
|
@property
|
|
@@ -740,6 +752,7 @@ class TaskSpan(Span, t.Generic[R]):
|
|
|
740
752
|
origin: t.Any | None = None,
|
|
741
753
|
timestamp: datetime | None = None,
|
|
742
754
|
mode: MetricAggMode | None = None,
|
|
755
|
+
attributes: JsonDict | None = None,
|
|
743
756
|
) -> None: ...
|
|
744
757
|
|
|
745
758
|
@t.overload
|
|
@@ -761,13 +774,18 @@ class TaskSpan(Span, t.Generic[R]):
|
|
|
761
774
|
origin: t.Any | None = None,
|
|
762
775
|
timestamp: datetime | None = None,
|
|
763
776
|
mode: MetricAggMode | None = None,
|
|
777
|
+
attributes: JsonDict | None = None,
|
|
764
778
|
) -> None:
|
|
765
779
|
metric = (
|
|
766
780
|
value
|
|
767
781
|
if isinstance(value, Metric)
|
|
768
|
-
else Metric(
|
|
782
|
+
else Metric(
|
|
783
|
+
float(value), step, timestamp or datetime.now(timezone.utc), attributes or {}
|
|
784
|
+
)
|
|
769
785
|
)
|
|
770
786
|
|
|
787
|
+
key = re.sub(r"[^\w/]+", "_", key.lower())
|
|
788
|
+
|
|
771
789
|
if origin is not None:
|
|
772
790
|
origin_hash = self.run.log_object(
|
|
773
791
|
origin,
|
|
@@ -786,7 +804,7 @@ class TaskSpan(Span, t.Generic[R]):
|
|
|
786
804
|
#
|
|
787
805
|
# Don't include `source` and `mode` as we handled it here.
|
|
788
806
|
if (run := current_run_span.get()) is not None:
|
|
789
|
-
run.log_metric(
|
|
807
|
+
run.log_metric(key, metric, prefix=self._label)
|
|
790
808
|
|
|
791
809
|
def get_average_metric_value(self, key: str | None = None) -> float:
|
|
792
810
|
metrics = (
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "dreadnode"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.2"
|
|
4
4
|
description = "Dreadnode SDK"
|
|
5
5
|
requires-python = ">=3.10,<3.14"
|
|
6
6
|
|
|
7
7
|
[tool.poetry]
|
|
8
8
|
name = "dreadnode"
|
|
9
|
-
version = "1.0.
|
|
9
|
+
version = "1.0.2"
|
|
10
10
|
description = "Dreadnode SDK"
|
|
11
11
|
authors = ["Nick Landers <monoxgas@gmail.com>"]
|
|
12
12
|
repository = "https://github.com/dreadnode/sdk"
|
|
@@ -18,7 +18,6 @@ pydantic = "^2.9.2"
|
|
|
18
18
|
httpx = "^0.28.0"
|
|
19
19
|
logfire = "^3.5.3"
|
|
20
20
|
python-ulid = "^3.0.0"
|
|
21
|
-
fast-depends = "^2.4.12"
|
|
22
21
|
coolname = "^2.2.0"
|
|
23
22
|
pandas = "^2.2.3"
|
|
24
23
|
fsspec = {version = ">=2023.1.0,<=2025.3.0", extras = ["s3"]} # Pinned for datasets compatibility
|
|
@@ -31,7 +30,7 @@ training = ["transformers"]
|
|
|
31
30
|
[tool.poetry.group.dev.dependencies]
|
|
32
31
|
mypy = "^1.8.0"
|
|
33
32
|
ruff = "^0.11.6"
|
|
34
|
-
pre-commit = "^
|
|
33
|
+
pre-commit = "^4.0.0"
|
|
35
34
|
pytest = "^8.3.3"
|
|
36
35
|
pytest-asyncio = "^0.24.0"
|
|
37
36
|
types-protobuf = "^5.29.1.20250208"
|
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import typing as t
|
|
2
|
-
from datetime import datetime
|
|
3
|
-
from uuid import UUID
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel, Field
|
|
6
|
-
from ulid import ULID
|
|
7
|
-
|
|
8
|
-
AnyDict = dict[str, t.Any]
|
|
9
|
-
|
|
10
|
-
# User
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class UserAPIKey(BaseModel):
|
|
14
|
-
key: str
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class UserResponse(BaseModel):
|
|
18
|
-
id: UUID
|
|
19
|
-
email_address: str
|
|
20
|
-
username: str
|
|
21
|
-
api_key: UserAPIKey
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
# Strikes
|
|
25
|
-
|
|
26
|
-
SpanStatus = t.Literal[
|
|
27
|
-
"pending", # A pending span has been created
|
|
28
|
-
"completed", # The span has been finished
|
|
29
|
-
"failed", # The raised an exception
|
|
30
|
-
]
|
|
31
|
-
|
|
32
|
-
ExportFormat = t.Literal["csv", "json", "jsonl", "parquet"]
|
|
33
|
-
StatusFilter = t.Literal["all", "completed", "failed"]
|
|
34
|
-
TimeAxisType = t.Literal["wall", "relative", "step"]
|
|
35
|
-
TimeAggregationType = t.Literal["max", "min", "sum", "count"]
|
|
36
|
-
MetricAggregationType = t.Literal[
|
|
37
|
-
"avg",
|
|
38
|
-
"median",
|
|
39
|
-
"min",
|
|
40
|
-
"max",
|
|
41
|
-
"sum",
|
|
42
|
-
"first",
|
|
43
|
-
"last",
|
|
44
|
-
"count",
|
|
45
|
-
"std",
|
|
46
|
-
"var",
|
|
47
|
-
]
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class SpanException(BaseModel):
|
|
51
|
-
type: str
|
|
52
|
-
message: str
|
|
53
|
-
stacktrace: str
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class SpanEvent(BaseModel):
|
|
57
|
-
timestamp: datetime
|
|
58
|
-
name: str
|
|
59
|
-
attributes: AnyDict
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class SpanLink(BaseModel):
|
|
63
|
-
trace_id: str
|
|
64
|
-
span_id: str
|
|
65
|
-
attributes: AnyDict
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
class TraceLog(BaseModel):
|
|
69
|
-
timestamp: datetime
|
|
70
|
-
body: str
|
|
71
|
-
severity: str
|
|
72
|
-
service: str | None
|
|
73
|
-
trace_id: str | None
|
|
74
|
-
span_id: str | None
|
|
75
|
-
attributes: AnyDict
|
|
76
|
-
container: str | None
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class TraceSpan(BaseModel):
|
|
80
|
-
timestamp: datetime
|
|
81
|
-
duration: int
|
|
82
|
-
trace_id: str
|
|
83
|
-
span_id: str
|
|
84
|
-
parent_span_id: str | None
|
|
85
|
-
service_name: str | None
|
|
86
|
-
status: SpanStatus
|
|
87
|
-
exception: SpanException | None
|
|
88
|
-
name: str
|
|
89
|
-
attributes: AnyDict
|
|
90
|
-
resource_attributes: AnyDict
|
|
91
|
-
events: list[SpanEvent]
|
|
92
|
-
links: list[SpanLink]
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
class Metric(BaseModel):
|
|
96
|
-
value: float
|
|
97
|
-
step: int
|
|
98
|
-
timestamp: datetime
|
|
99
|
-
attributes: AnyDict
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class ObjectRef(BaseModel):
|
|
103
|
-
name: str
|
|
104
|
-
label: str
|
|
105
|
-
hash: str
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
class ObjectUri(BaseModel):
|
|
109
|
-
hash: str
|
|
110
|
-
schema_hash: str
|
|
111
|
-
uri: str
|
|
112
|
-
size: int
|
|
113
|
-
type: t.Literal["uri"]
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
class ObjectVal(BaseModel):
|
|
117
|
-
hash: str
|
|
118
|
-
schema_hash: str
|
|
119
|
-
value: t.Any
|
|
120
|
-
type: t.Literal["val"]
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
Object = ObjectUri | ObjectVal
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
class V0Object(BaseModel):
|
|
127
|
-
name: str
|
|
128
|
-
label: str
|
|
129
|
-
value: t.Any
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
class Run(BaseModel):
|
|
133
|
-
id: ULID
|
|
134
|
-
name: str
|
|
135
|
-
span_id: str
|
|
136
|
-
trace_id: str
|
|
137
|
-
timestamp: datetime
|
|
138
|
-
duration: int
|
|
139
|
-
status: SpanStatus
|
|
140
|
-
exception: SpanException | None
|
|
141
|
-
tags: set[str]
|
|
142
|
-
params: AnyDict
|
|
143
|
-
metrics: dict[str, list[Metric]]
|
|
144
|
-
inputs: list[ObjectRef]
|
|
145
|
-
outputs: list[ObjectRef]
|
|
146
|
-
objects: dict[str, Object]
|
|
147
|
-
object_schemas: AnyDict
|
|
148
|
-
schema_: AnyDict = Field(alias="schema")
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
class Task(BaseModel):
|
|
152
|
-
name: str
|
|
153
|
-
span_id: str
|
|
154
|
-
trace_id: str
|
|
155
|
-
parent_span_id: str | None
|
|
156
|
-
parent_task_span_id: str | None
|
|
157
|
-
timestamp: datetime
|
|
158
|
-
duration: int
|
|
159
|
-
status: SpanStatus
|
|
160
|
-
exception: SpanException | None
|
|
161
|
-
tags: set[str]
|
|
162
|
-
params: AnyDict
|
|
163
|
-
metrics: dict[str, list[Metric]]
|
|
164
|
-
inputs: list[ObjectRef] | list[V0Object] # v0 compat
|
|
165
|
-
outputs: list[ObjectRef] | list[V0Object] # v0 compat
|
|
166
|
-
schema_: AnyDict = Field(alias="schema")
|
|
167
|
-
attributes: AnyDict
|
|
168
|
-
resource_attributes: AnyDict
|
|
169
|
-
events: list[SpanEvent]
|
|
170
|
-
links: list[SpanLink]
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class Project(BaseModel):
|
|
174
|
-
id: UUID
|
|
175
|
-
key: str
|
|
176
|
-
name: str
|
|
177
|
-
description: str | None
|
|
178
|
-
created_at: datetime
|
|
179
|
-
updated_at: datetime
|
|
180
|
-
run_count: int
|
|
181
|
-
last_run: Run | None
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
# Derived types
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
class TaskTree(BaseModel):
|
|
188
|
-
task: Task
|
|
189
|
-
children: list["TaskTree"] = []
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
class SpanTree(BaseModel):
|
|
193
|
-
"""Tree representation of a trace span with its children"""
|
|
194
|
-
|
|
195
|
-
span: Task | TraceSpan
|
|
196
|
-
children: list["SpanTree"] = []
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
# User data credentials
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
class UserDataCredentials(BaseModel):
|
|
203
|
-
access_key_id: str
|
|
204
|
-
secret_access_key: str
|
|
205
|
-
session_token: str
|
|
206
|
-
expiration: datetime
|
|
207
|
-
region: str
|
|
208
|
-
bucket: str
|
|
209
|
-
prefix: str
|
|
210
|
-
endpoint: str | None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|