dreadnode 1.0.0rc0__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.
dreadnode/metric.py ADDED
@@ -0,0 +1,225 @@
1
+ import inspect
2
+ import typing as t
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timezone
5
+
6
+ from logfire._internal.utils import safe_repr
7
+ from opentelemetry.trace import Tracer
8
+
9
+ from dreadnode.types import JsonDict, JsonValue
10
+
11
+ T = t.TypeVar("T")
12
+
13
+ MetricMode = t.Literal["direct", "avg", "sum", "min", "max", "count"]
14
+
15
+
16
+ @dataclass
17
+ class Metric:
18
+ """
19
+ Any reported value regarding the state of a run, task, and optionally object (input/output).
20
+ """
21
+
22
+ value: float
23
+ "The value of the metric, e.g. 0.5, 1.0, 2.0, etc."
24
+ step: int = 0
25
+ "An step value to indicate when this metric was reported."
26
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
27
+ "The timestamp when the metric was reported."
28
+ attributes: JsonDict = field(default_factory=dict)
29
+ "A dictionary of attributes to attach to the metric."
30
+
31
+ @classmethod
32
+ def from_many(
33
+ cls,
34
+ values: t.Sequence[tuple[str, float, float]],
35
+ step: int = 0,
36
+ **attributes: JsonValue,
37
+ ) -> "Metric":
38
+ """
39
+ Create a composite metric from individual values and weights.
40
+
41
+ This is useful for creating a metric that is the weighted average of multiple values.
42
+ The values should be a sequence of tuples, where each tuple contains the name of the metric,
43
+ the value of the metric, and the weight of the metric.
44
+
45
+ The individual values will be reported in the attributes of the metric.
46
+
47
+ Args:
48
+ values: A sequence of tuples containing the name, value, and weight of each metric.
49
+ step: The step value to attach to the metric.
50
+ **attributes: Additional attributes to attach to the metric.
51
+
52
+ Returns:
53
+ A composite Metric
54
+ """
55
+ total = sum(value * weight for _, value, weight in values)
56
+ weight = sum(weight for _, _, weight in values)
57
+ score_attributes = {name: value for name, value, _ in values}
58
+ return cls(value=total / weight, step=step, attributes={**attributes, **score_attributes})
59
+
60
+ def apply_mode(self, mode: MetricMode, others: "list[Metric]") -> "Metric":
61
+ """
62
+ Apply an aggregation mode to the metric.
63
+ This will modify the metric in place.
64
+
65
+ Args:
66
+ mode: The mode to apply. One of "sum", "min", "max", or "inc".
67
+ others: A list of other metrics to apply the mode to.
68
+
69
+ Returns:
70
+ self
71
+ """
72
+ previous_mode = next((m.attributes.get("mode") for m in others), mode) or "direct"
73
+ if mode != previous_mode:
74
+ raise ValueError(
75
+ f"Cannot mix metric modes {mode} != {previous_mode}",
76
+ )
77
+
78
+ if mode == "direct":
79
+ return self
80
+
81
+ self.attributes["original"] = self.value
82
+ self.attributes["mode"] = mode
83
+
84
+ prior_values = [m.value for m in sorted(others, key=lambda m: m.timestamp)]
85
+
86
+ if mode == "sum":
87
+ self.value += max(prior_values)
88
+ elif mode == "min":
89
+ self.value = min([self.value, *prior_values])
90
+ elif mode == "max":
91
+ self.value = max([self.value, *prior_values])
92
+ elif mode == "count":
93
+ self.value = len(others) + 1
94
+ elif mode == "avg" and prior_values:
95
+ current_avg = prior_values[-1]
96
+ self.value = current_avg + (self.value - current_avg) / (len(prior_values) + 1)
97
+
98
+ return self
99
+
100
+
101
+ MetricDict = dict[str, list[Metric]]
102
+
103
+ ScorerResult = float | int | bool | Metric
104
+ ScorerCallable = t.Callable[[T], t.Awaitable[ScorerResult]] | t.Callable[[T], ScorerResult]
105
+
106
+
107
+ @dataclass
108
+ class Scorer(t.Generic[T]):
109
+ tracer: Tracer
110
+
111
+ name: str
112
+ "The name of the scorer, used for reporting metrics."
113
+ tags: t.Sequence[str]
114
+ "A list of tags to attach to the metric."
115
+ attributes: dict[str, t.Any]
116
+ "A dictionary of attributes to attach to the metric."
117
+ func: ScorerCallable[T]
118
+ "The function to call to get the metric."
119
+ step: int = 0
120
+ "The step value to attach to metrics produced by this Scorer."
121
+ auto_increment_step: bool = False
122
+ "Whether to automatically increment the step for each time this scorer is called."
123
+
124
+ @classmethod
125
+ def from_callable(
126
+ cls,
127
+ tracer: Tracer,
128
+ func: "ScorerCallable[T] | Scorer[T]",
129
+ *,
130
+ name: str | None = None,
131
+ tags: t.Sequence[str] | None = None,
132
+ **attributes: t.Any,
133
+ ) -> "Scorer[T]":
134
+ """
135
+ Create a scorer from a callable function.
136
+
137
+ Args:
138
+ tracer: The tracer to use for reporting metrics.
139
+ func: The function to call to get the metric.
140
+ name: The name of the scorer, used for reporting metrics.
141
+ tags: A list of tags to attach to the metric.
142
+ **attributes: A dictionary of attributes to attach to the metric.
143
+
144
+ Returns:
145
+ A Scorer object.
146
+ """
147
+ if isinstance(func, Scorer):
148
+ if name is not None or attributes is not None:
149
+ func = func.clone()
150
+ func.name = name or func.name
151
+ func.attributes.update(attributes or {})
152
+ return func
153
+
154
+ func = inspect.unwrap(func)
155
+ func_name = getattr(
156
+ func,
157
+ "__qualname__",
158
+ getattr(func, "__name__", safe_repr(func)),
159
+ )
160
+ name = name or func_name
161
+ return cls(
162
+ tracer=tracer,
163
+ name=name,
164
+ tags=tags or [],
165
+ attributes=attributes or {},
166
+ func=func,
167
+ )
168
+
169
+ def __post_init__(self) -> None:
170
+ self.__signature__ = inspect.signature(self.func)
171
+ self.__name__ = self.name
172
+
173
+ def clone(self) -> "Scorer[T]":
174
+ """
175
+ Clone the scorer.
176
+
177
+ Returns:
178
+ A new Scorer.
179
+ """
180
+ return Scorer(
181
+ tracer=self.tracer,
182
+ name=self.name,
183
+ tags=self.tags,
184
+ attributes=self.attributes,
185
+ func=self.func,
186
+ step=self.step,
187
+ auto_increment_step=self.auto_increment_step,
188
+ )
189
+
190
+ async def __call__(self, object: T) -> Metric:
191
+ """
192
+ Execute the scorer and return the metric.
193
+
194
+ Any output value will be converted to a Metric object.
195
+
196
+ Args:
197
+ object: The object to score.
198
+
199
+ Returns:
200
+ A Metric object.
201
+ """
202
+ from dreadnode.tracing.span import Span
203
+
204
+ with Span(
205
+ name=self.name,
206
+ tags=self.tags,
207
+ attributes=self.attributes,
208
+ tracer=self.tracer,
209
+ ):
210
+ metric = self.func(object)
211
+ if inspect.isawaitable(metric):
212
+ metric = await metric
213
+
214
+ if not isinstance(metric, Metric):
215
+ metric = Metric(
216
+ float(metric),
217
+ step=self.step,
218
+ timestamp=datetime.now(timezone.utc),
219
+ attributes=self.attributes,
220
+ )
221
+
222
+ if self.auto_increment_step:
223
+ self.step += 1
224
+
225
+ return metric
dreadnode/object.py ADDED
@@ -0,0 +1,29 @@
1
+ import typing as t
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass
6
+ class ObjectRef:
7
+ name: str
8
+ label: str
9
+ hash: str
10
+
11
+
12
+ @dataclass
13
+ class ObjectUri:
14
+ hash: str
15
+ schema_hash: str
16
+ uri: str
17
+ size: int
18
+ type: t.Literal["uri"] = "uri"
19
+
20
+
21
+ @dataclass
22
+ class ObjectVal:
23
+ hash: str
24
+ schema_hash: str
25
+ value: t.Any
26
+ type: t.Literal["val"] = "val"
27
+
28
+
29
+ Object = ObjectUri | ObjectVal
dreadnode/py.typed ADDED
File without changes