codex-meter 0.3.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.
- codex_meter/__init__.py +12 -0
- codex_meter/__main__.py +4 -0
- codex_meter/aggregation.py +127 -0
- codex_meter/budgets.py +133 -0
- codex_meter/cli.py +2455 -0
- codex_meter/config.py +164 -0
- codex_meter/exporters.py +339 -0
- codex_meter/forecasts.py +92 -0
- codex_meter/humanize.py +38 -0
- codex_meter/insights.py +132 -0
- codex_meter/intervals.py +129 -0
- codex_meter/live.py +286 -0
- codex_meter/models.py +282 -0
- codex_meter/parse_cache.py +189 -0
- codex_meter/parser.py +498 -0
- codex_meter/pricing.py +311 -0
- codex_meter/prom_export.py +116 -0
- codex_meter/py.typed +0 -0
- codex_meter/render.py +545 -0
- codex_meter/timeutil.py +75 -0
- codex_meter/windows.py +153 -0
- codex_meter-0.3.0.dist-info/METADATA +304 -0
- codex_meter-0.3.0.dist-info/RECORD +26 -0
- codex_meter-0.3.0.dist-info/WHEEL +4 -0
- codex_meter-0.3.0.dist-info/entry_points.txt +2 -0
- codex_meter-0.3.0.dist-info/licenses/LICENSE +21 -0
codex_meter/models.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as dt
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from decimal import Decimal
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _safe_int(value: object) -> int:
|
|
10
|
+
try:
|
|
11
|
+
return int(value or 0)
|
|
12
|
+
except (TypeError, ValueError):
|
|
13
|
+
return 0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def decimal_value(value: object) -> Decimal:
|
|
17
|
+
if isinstance(value, Decimal):
|
|
18
|
+
return value
|
|
19
|
+
return Decimal(str(value))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def decimal_string(value: object) -> str:
|
|
23
|
+
amount = decimal_value(value)
|
|
24
|
+
if amount == 0:
|
|
25
|
+
return "0"
|
|
26
|
+
return format(amount.normalize(), "f")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class Usage:
|
|
31
|
+
input_tokens: int = 0
|
|
32
|
+
cached_input_tokens: int = 0
|
|
33
|
+
output_tokens: int = 0
|
|
34
|
+
reasoning_output_tokens: int = 0
|
|
35
|
+
total_tokens: int = 0
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def uncached_input_tokens(self) -> int:
|
|
39
|
+
return max(0, self.input_tokens - self.cached_input_tokens)
|
|
40
|
+
|
|
41
|
+
def is_zero(self) -> bool:
|
|
42
|
+
return not (
|
|
43
|
+
self.input_tokens
|
|
44
|
+
or self.cached_input_tokens
|
|
45
|
+
or self.output_tokens
|
|
46
|
+
or self.reasoning_output_tokens
|
|
47
|
+
or self.total_tokens
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_dict(cls, raw: object) -> Usage:
|
|
52
|
+
if not isinstance(raw, dict):
|
|
53
|
+
return cls()
|
|
54
|
+
return cls(
|
|
55
|
+
input_tokens=_safe_int(raw.get("input_tokens")),
|
|
56
|
+
cached_input_tokens=_safe_int(raw.get("cached_input_tokens")),
|
|
57
|
+
output_tokens=_safe_int(raw.get("output_tokens")),
|
|
58
|
+
reasoning_output_tokens=_safe_int(raw.get("reasoning_output_tokens")),
|
|
59
|
+
total_tokens=_safe_int(raw.get("total_tokens")),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class Rates:
|
|
65
|
+
input: Decimal | float
|
|
66
|
+
cached_input: Decimal | float
|
|
67
|
+
output: Decimal | float
|
|
68
|
+
reasoning_output: Decimal | float | None = None
|
|
69
|
+
|
|
70
|
+
def __post_init__(self) -> None:
|
|
71
|
+
object.__setattr__(self, "input", decimal_value(self.input))
|
|
72
|
+
object.__setattr__(self, "cached_input", decimal_value(self.cached_input))
|
|
73
|
+
object.__setattr__(self, "output", decimal_value(self.output))
|
|
74
|
+
if self.reasoning_output is not None:
|
|
75
|
+
object.__setattr__(self, "reasoning_output", decimal_value(self.reasoning_output))
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def effective_reasoning_output(self) -> Decimal:
|
|
79
|
+
return self.reasoning_output if self.reasoning_output is not None else self.output
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class LongContextRule:
|
|
84
|
+
threshold: int
|
|
85
|
+
input_mult: float
|
|
86
|
+
output_mult: float
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class PricingSource:
|
|
91
|
+
name: str
|
|
92
|
+
url: str
|
|
93
|
+
checked: str
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True)
|
|
97
|
+
class RuntimeOptions:
|
|
98
|
+
session_root: Path
|
|
99
|
+
state_db: Path
|
|
100
|
+
config_path: Path
|
|
101
|
+
start: dt.datetime
|
|
102
|
+
end: dt.datetime
|
|
103
|
+
timezone: str
|
|
104
|
+
pricing_mode: str
|
|
105
|
+
service_tier: str
|
|
106
|
+
unknown_service_tier: str
|
|
107
|
+
tier_overrides: Path | None
|
|
108
|
+
rates_file: Path | None
|
|
109
|
+
dedupe: bool
|
|
110
|
+
parse_cache: bool
|
|
111
|
+
default_model: str
|
|
112
|
+
show_prompts: bool
|
|
113
|
+
offline: bool
|
|
114
|
+
compact: bool
|
|
115
|
+
width: int | None
|
|
116
|
+
top_threads: int
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class ThreadMeta:
|
|
121
|
+
rollout_path: str = ""
|
|
122
|
+
title: str = ""
|
|
123
|
+
first_user_message: str = ""
|
|
124
|
+
cwd: str = ""
|
|
125
|
+
git_branch: str = ""
|
|
126
|
+
git_origin_url: str = ""
|
|
127
|
+
model: str = ""
|
|
128
|
+
reasoning_effort: str = ""
|
|
129
|
+
created_at: int = 0
|
|
130
|
+
updated_at: int = 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass(frozen=True)
|
|
134
|
+
class TierOverride:
|
|
135
|
+
service_tier: str
|
|
136
|
+
session: str | None = None
|
|
137
|
+
start: dt.datetime | None = None
|
|
138
|
+
end: dt.datetime | None = None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass(frozen=True)
|
|
142
|
+
class UsageEvent:
|
|
143
|
+
timestamp: dt.datetime
|
|
144
|
+
path: Path
|
|
145
|
+
session_id: str
|
|
146
|
+
usage: Usage
|
|
147
|
+
model: str
|
|
148
|
+
service_tier: str
|
|
149
|
+
tier_source: str
|
|
150
|
+
thread: ThreadMeta
|
|
151
|
+
usage_source: str = "last_token_usage"
|
|
152
|
+
model_context_window: int = 0
|
|
153
|
+
plan_type: str = ""
|
|
154
|
+
credits: object = None
|
|
155
|
+
primary_used_percent: object = None
|
|
156
|
+
primary_window_minutes: object = None
|
|
157
|
+
primary_resets_at: object = None
|
|
158
|
+
secondary_used_percent: object = None
|
|
159
|
+
secondary_window_minutes: object = None
|
|
160
|
+
secondary_resets_at: object = None
|
|
161
|
+
rate_limit_reached_type: str = ""
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@dataclass(frozen=True)
|
|
165
|
+
class RateLimitSample:
|
|
166
|
+
timestamp: dt.datetime
|
|
167
|
+
path: Path
|
|
168
|
+
session_id: str
|
|
169
|
+
plan_type: str = ""
|
|
170
|
+
credits: object = None
|
|
171
|
+
primary_used_percent: object = None
|
|
172
|
+
primary_window_minutes: object = None
|
|
173
|
+
primary_resets_at: object = None
|
|
174
|
+
secondary_used_percent: object = None
|
|
175
|
+
secondary_window_minutes: object = None
|
|
176
|
+
secondary_resets_at: object = None
|
|
177
|
+
rate_limit_reached_type: str = ""
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class CostTotals:
|
|
182
|
+
api_dollars: Decimal | float = Decimal("0")
|
|
183
|
+
standard_credits: Decimal | float = Decimal("0")
|
|
184
|
+
adjusted_credits: Decimal | float = Decimal("0")
|
|
185
|
+
api_unpriced_events: int = 0
|
|
186
|
+
credit_unpriced_events: int = 0
|
|
187
|
+
estimated_events: int = 0
|
|
188
|
+
ambiguous_reasoning_events: int = 0
|
|
189
|
+
local_override_events: int = 0
|
|
190
|
+
|
|
191
|
+
def __post_init__(self) -> None:
|
|
192
|
+
self.api_dollars = decimal_value(self.api_dollars)
|
|
193
|
+
self.standard_credits = decimal_value(self.standard_credits)
|
|
194
|
+
self.adjusted_credits = decimal_value(self.adjusted_credits)
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def unpriced_events(self) -> int:
|
|
198
|
+
return max(self.api_unpriced_events, self.credit_unpriced_events)
|
|
199
|
+
|
|
200
|
+
def add(self, other: CostTotals) -> None:
|
|
201
|
+
self.api_dollars += other.api_dollars
|
|
202
|
+
self.standard_credits += other.standard_credits
|
|
203
|
+
self.adjusted_credits += other.adjusted_credits
|
|
204
|
+
self.api_unpriced_events += other.api_unpriced_events
|
|
205
|
+
self.credit_unpriced_events += other.credit_unpriced_events
|
|
206
|
+
self.estimated_events += other.estimated_events
|
|
207
|
+
self.ambiguous_reasoning_events += other.ambiguous_reasoning_events
|
|
208
|
+
self.local_override_events += other.local_override_events
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass
|
|
212
|
+
class TokenTotals:
|
|
213
|
+
events: int = 0
|
|
214
|
+
input_tokens: int = 0
|
|
215
|
+
cached_input_tokens: int = 0
|
|
216
|
+
output_tokens: int = 0
|
|
217
|
+
reasoning_output_tokens: int = 0
|
|
218
|
+
total_tokens: int = 0
|
|
219
|
+
|
|
220
|
+
@property
|
|
221
|
+
def uncached_input_tokens(self) -> int:
|
|
222
|
+
return max(0, self.input_tokens - self.cached_input_tokens)
|
|
223
|
+
|
|
224
|
+
def add_usage(self, usage: Usage) -> None:
|
|
225
|
+
self.events += 1
|
|
226
|
+
self.input_tokens += usage.input_tokens
|
|
227
|
+
self.cached_input_tokens += usage.cached_input_tokens
|
|
228
|
+
self.output_tokens += usage.output_tokens
|
|
229
|
+
self.reasoning_output_tokens += usage.reasoning_output_tokens
|
|
230
|
+
self.total_tokens += usage.total_tokens
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass
|
|
234
|
+
class Aggregate:
|
|
235
|
+
key: str
|
|
236
|
+
label: str
|
|
237
|
+
totals: TokenTotals = field(default_factory=TokenTotals)
|
|
238
|
+
costs: CostTotals = field(default_factory=CostTotals)
|
|
239
|
+
cache_savings: CostTotals = field(default_factory=CostTotals)
|
|
240
|
+
models: set[str] = field(default_factory=set)
|
|
241
|
+
service_tiers: set[str] = field(default_factory=set)
|
|
242
|
+
plan_types: set[str] = field(default_factory=set)
|
|
243
|
+
usage_sources: set[str] = field(default_factory=set)
|
|
244
|
+
model_context_window: int = 0
|
|
245
|
+
long_context_events: int = 0
|
|
246
|
+
unknown_model_events: int = 0
|
|
247
|
+
unknown_tier_events: int = 0
|
|
248
|
+
|
|
249
|
+
def add_event(
|
|
250
|
+
self,
|
|
251
|
+
event: UsageEvent,
|
|
252
|
+
costs: CostTotals,
|
|
253
|
+
cache_savings: CostTotals,
|
|
254
|
+
long_context: bool,
|
|
255
|
+
unknown_model: bool,
|
|
256
|
+
unknown_tier: bool,
|
|
257
|
+
) -> None:
|
|
258
|
+
self.totals.add_usage(event.usage)
|
|
259
|
+
self.costs.add(costs)
|
|
260
|
+
self.cache_savings.add(cache_savings)
|
|
261
|
+
if event.model:
|
|
262
|
+
self.models.add(event.model)
|
|
263
|
+
if event.service_tier:
|
|
264
|
+
self.service_tiers.add(event.service_tier)
|
|
265
|
+
if event.plan_type:
|
|
266
|
+
self.plan_types.add(event.plan_type)
|
|
267
|
+
if event.usage_source:
|
|
268
|
+
self.usage_sources.add(event.usage_source)
|
|
269
|
+
self.model_context_window = max(self.model_context_window, event.model_context_window)
|
|
270
|
+
self.long_context_events += int(long_context)
|
|
271
|
+
self.unknown_model_events += int(unknown_model)
|
|
272
|
+
self.unknown_tier_events += int(unknown_tier)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@dataclass(frozen=True)
|
|
276
|
+
class LoadResult:
|
|
277
|
+
events: list[UsageEvent]
|
|
278
|
+
duplicates: int
|
|
279
|
+
tier_sources: dict[str, int]
|
|
280
|
+
plan_types: set[str]
|
|
281
|
+
credit_samples: list[RateLimitSample]
|
|
282
|
+
warnings: list[str]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime as dt
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sqlite3
|
|
7
|
+
from contextlib import closing
|
|
8
|
+
from dataclasses import asdict, dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from codex_meter.models import RateLimitSample, ThreadMeta, Usage, UsageEvent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def default_cache_path() -> Path:
|
|
15
|
+
override = os.environ.get("CODEX_METER_CACHE_DIR")
|
|
16
|
+
if override:
|
|
17
|
+
return Path(override).expanduser() / "parse_cache.sqlite"
|
|
18
|
+
xdg = os.environ.get("XDG_CACHE_HOME")
|
|
19
|
+
if xdg:
|
|
20
|
+
return Path(xdg).expanduser() / "codex-meter" / "parse_cache.sqlite"
|
|
21
|
+
return Path.home() / ".cache" / "codex-meter" / "parse_cache.sqlite"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class CacheStats:
|
|
26
|
+
path: Path
|
|
27
|
+
hits: int = 0
|
|
28
|
+
misses: int = 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ParseCache:
|
|
32
|
+
def __init__(self, path: Path | None = None) -> None:
|
|
33
|
+
self.path = path or default_cache_path()
|
|
34
|
+
self.hits = 0
|
|
35
|
+
self.misses = 0
|
|
36
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
with closing(sqlite3.connect(self.path)) as conn, conn:
|
|
38
|
+
conn.execute(
|
|
39
|
+
"""
|
|
40
|
+
create table if not exists parsed_sessions (
|
|
41
|
+
path text not null,
|
|
42
|
+
signature text not null,
|
|
43
|
+
mtime_ns integer not null,
|
|
44
|
+
size integer not null,
|
|
45
|
+
byte_offset integer not null,
|
|
46
|
+
payload blob not null,
|
|
47
|
+
primary key (path, signature)
|
|
48
|
+
)
|
|
49
|
+
"""
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def default(cls) -> ParseCache:
|
|
54
|
+
return cls(default_cache_path())
|
|
55
|
+
|
|
56
|
+
def get(self, path: Path, signature: str):
|
|
57
|
+
stat = path.stat()
|
|
58
|
+
with closing(sqlite3.connect(self.path)) as conn:
|
|
59
|
+
row = conn.execute(
|
|
60
|
+
"""
|
|
61
|
+
select payload from parsed_sessions
|
|
62
|
+
where path = ? and signature = ? and mtime_ns = ? and size = ? and byte_offset = ?
|
|
63
|
+
""",
|
|
64
|
+
(str(path), signature, stat.st_mtime_ns, stat.st_size, stat.st_size),
|
|
65
|
+
).fetchone()
|
|
66
|
+
if row is None:
|
|
67
|
+
self.misses += 1
|
|
68
|
+
return None
|
|
69
|
+
parsed = _decode_payload(row[0])
|
|
70
|
+
if parsed is None:
|
|
71
|
+
self.misses += 1
|
|
72
|
+
return None
|
|
73
|
+
self.hits += 1
|
|
74
|
+
return parsed
|
|
75
|
+
|
|
76
|
+
def put(self, path: Path, signature: str, parsed) -> None:
|
|
77
|
+
stat = path.stat()
|
|
78
|
+
payload = _encode_payload(parsed)
|
|
79
|
+
with closing(sqlite3.connect(self.path)) as conn, conn:
|
|
80
|
+
conn.execute(
|
|
81
|
+
"""
|
|
82
|
+
insert or replace into parsed_sessions
|
|
83
|
+
(path, signature, mtime_ns, size, byte_offset, payload)
|
|
84
|
+
values (?, ?, ?, ?, ?, ?)
|
|
85
|
+
""",
|
|
86
|
+
(str(path), signature, stat.st_mtime_ns, stat.st_size, stat.st_size, payload),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def stats(self) -> CacheStats:
|
|
90
|
+
return CacheStats(path=self.path, hits=self.hits, misses=self.misses)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _encode_datetime(value: dt.datetime) -> str:
|
|
94
|
+
return value.astimezone(dt.UTC).isoformat()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _decode_datetime(value: str) -> dt.datetime:
|
|
98
|
+
return dt.datetime.fromisoformat(value).astimezone(dt.UTC)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _usage_to_dict(usage: Usage) -> dict:
|
|
102
|
+
return asdict(usage)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _usage_from_dict(raw: dict) -> Usage:
|
|
106
|
+
return Usage.from_dict(raw)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _thread_to_dict(thread: ThreadMeta) -> dict:
|
|
110
|
+
return asdict(thread)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _thread_from_dict(raw: dict) -> ThreadMeta:
|
|
114
|
+
return ThreadMeta(**raw)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _event_to_dict(event: UsageEvent) -> dict:
|
|
118
|
+
item = asdict(event)
|
|
119
|
+
item["timestamp"] = _encode_datetime(event.timestamp)
|
|
120
|
+
item["path"] = str(event.path)
|
|
121
|
+
item["usage"] = _usage_to_dict(event.usage)
|
|
122
|
+
item["thread"] = _thread_to_dict(event.thread)
|
|
123
|
+
return item
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _event_from_dict(raw: dict) -> UsageEvent:
|
|
127
|
+
item = dict(raw)
|
|
128
|
+
item["timestamp"] = _decode_datetime(str(item["timestamp"]))
|
|
129
|
+
item["path"] = Path(str(item["path"]))
|
|
130
|
+
item["usage"] = _usage_from_dict(item["usage"])
|
|
131
|
+
item["thread"] = _thread_from_dict(item["thread"])
|
|
132
|
+
return UsageEvent(**item)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _sample_to_dict(sample: RateLimitSample) -> dict:
|
|
136
|
+
item = asdict(sample)
|
|
137
|
+
item["timestamp"] = _encode_datetime(sample.timestamp)
|
|
138
|
+
item["path"] = str(sample.path)
|
|
139
|
+
return item
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _sample_from_dict(raw: dict) -> RateLimitSample:
|
|
143
|
+
item = dict(raw)
|
|
144
|
+
item["timestamp"] = _decode_datetime(str(item["timestamp"]))
|
|
145
|
+
item["path"] = Path(str(item["path"]))
|
|
146
|
+
return RateLimitSample(**item)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _encode_payload(parsed) -> bytes:
|
|
150
|
+
records = []
|
|
151
|
+
for event, reset, sample in parsed:
|
|
152
|
+
records.append(
|
|
153
|
+
{
|
|
154
|
+
"event": _event_to_dict(event) if event is not None else None,
|
|
155
|
+
"reset": bool(reset),
|
|
156
|
+
"sample": _sample_to_dict(sample) if sample is not None else None,
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
return json.dumps({"version": 1, "records": records}, separators=(",", ":")).encode("utf-8")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _decode_payload(raw) -> list[tuple[UsageEvent | None, bool, RateLimitSample | None]] | None:
|
|
163
|
+
try:
|
|
164
|
+
text = raw.decode("utf-8") if isinstance(raw, bytes) else str(raw)
|
|
165
|
+
payload = json.loads(text)
|
|
166
|
+
except (AttributeError, TypeError, UnicodeDecodeError, json.JSONDecodeError):
|
|
167
|
+
return None
|
|
168
|
+
if not isinstance(payload, dict) or payload.get("version") != 1:
|
|
169
|
+
return None
|
|
170
|
+
records = payload.get("records")
|
|
171
|
+
if not isinstance(records, list):
|
|
172
|
+
return None
|
|
173
|
+
parsed: list[tuple[UsageEvent | None, bool, RateLimitSample | None]] = []
|
|
174
|
+
try:
|
|
175
|
+
for record in records:
|
|
176
|
+
if not isinstance(record, dict):
|
|
177
|
+
return None
|
|
178
|
+
event_raw = record.get("event")
|
|
179
|
+
sample_raw = record.get("sample")
|
|
180
|
+
parsed.append(
|
|
181
|
+
(
|
|
182
|
+
_event_from_dict(event_raw) if isinstance(event_raw, dict) else None,
|
|
183
|
+
bool(record.get("reset")),
|
|
184
|
+
_sample_from_dict(sample_raw) if isinstance(sample_raw, dict) else None,
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
except (KeyError, TypeError, ValueError):
|
|
188
|
+
return None
|
|
189
|
+
return parsed
|