pi-codemcp 0.1.0 → 1.1.0

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.
@@ -0,0 +1,424 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import time
7
+ from contextlib import suppress
8
+ from dataclasses import dataclass, field
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING
11
+
12
+ from .json_types import JSON_OBJECT_ADAPTER, JSON_VALUE_ADAPTER, JsonObject
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Mapping
16
+
17
+ HISTOGRAM_BOUNDS_MS = (1, 5, 10, 25, 50, 100, 250, 500, 1_000, 2_500, 5_000, 10_000, 30_000)
18
+ HISTOGRAM_BOUNDS_BYTES = (64, 256, 1_024, 4_096, 16_384, 65_536, 262_144, 1_048_576)
19
+ RECENT_BUCKET_SECONDS = 60 * 60
20
+ RECENT_BUCKET_COUNT = 24
21
+ FLUSH_DELAY_SECONDS = 5.0
22
+ MAX_OPERATIONS = 32
23
+ MAX_PHASES = 16
24
+ MAX_SERVERS = 64
25
+ MAX_TOOLS = 384
26
+ MAX_FAILURE_STAGES = 16
27
+ OTHER_DIMENSION = "<other>"
28
+
29
+
30
+ @dataclass
31
+ class Histogram:
32
+ bounds: tuple[int, ...] = HISTOGRAM_BOUNDS_MS
33
+ counts: list[int] = field(default_factory=lambda: [0] * (len(HISTOGRAM_BOUNDS_MS) + 1))
34
+ count: int = 0
35
+ total: float = 0.0
36
+ maximum: float = 0.0
37
+
38
+ def observe(self, value: float) -> None:
39
+ bounded = max(0.0, value)
40
+ self.count += 1
41
+ self.total += bounded
42
+ self.maximum = max(self.maximum, bounded)
43
+ for index, bound in enumerate(self.bounds):
44
+ if bounded <= bound:
45
+ self.counts[index] += 1
46
+ return
47
+ self.counts[-1] += 1
48
+
49
+ def snapshot(self) -> JsonObject:
50
+ buckets: list[JsonObject] = [
51
+ {"le": bound, "count": count}
52
+ for bound, count in zip(self.bounds, self.counts[:-1], strict=True)
53
+ ]
54
+ buckets.append({"le": "inf", "count": self.counts[-1]})
55
+ return {
56
+ "count": self.count,
57
+ "sum": round(self.total, 3),
58
+ "average": round(self.total / self.count, 3) if self.count else 0.0,
59
+ "max": round(self.maximum, 3),
60
+ "buckets": JSON_VALUE_ADAPTER.validate_python(buckets),
61
+ }
62
+
63
+ @classmethod
64
+ def from_snapshot(
65
+ cls,
66
+ value: object,
67
+ *,
68
+ bounds: tuple[int, ...] = HISTOGRAM_BOUNDS_MS,
69
+ ) -> Histogram:
70
+ histogram = cls(bounds=bounds, counts=[0] * (len(bounds) + 1))
71
+ if not isinstance(value, dict):
72
+ return histogram
73
+ histogram.count = _integer(value.get("count"))
74
+ histogram.total = _number(value.get("sum"))
75
+ histogram.maximum = _number(value.get("max"))
76
+ raw_buckets = value.get("buckets")
77
+ if isinstance(raw_buckets, list) and len(raw_buckets) == len(histogram.counts):
78
+ histogram.counts = [
79
+ _integer(bucket.get("count")) if isinstance(bucket, dict) else 0
80
+ for bucket in raw_buckets
81
+ ]
82
+ return histogram
83
+
84
+
85
+ @dataclass
86
+ class Rollup:
87
+ count: int = 0
88
+ success: int = 0
89
+ failure: int = 0
90
+ input_bytes: int = 0
91
+ output_bytes: int = 0
92
+ calls: int = 0
93
+ chain_calls: int = 0
94
+ duration_ms: Histogram = field(default_factory=Histogram)
95
+ input_size_bytes: Histogram = field(
96
+ default_factory=lambda: Histogram(
97
+ bounds=HISTOGRAM_BOUNDS_BYTES,
98
+ counts=[0] * (len(HISTOGRAM_BOUNDS_BYTES) + 1),
99
+ )
100
+ )
101
+ output_size_bytes: Histogram = field(
102
+ default_factory=lambda: Histogram(
103
+ bounds=HISTOGRAM_BOUNDS_BYTES,
104
+ counts=[0] * (len(HISTOGRAM_BOUNDS_BYTES) + 1),
105
+ )
106
+ )
107
+
108
+ def observe(
109
+ self,
110
+ *,
111
+ duration_ms: float,
112
+ success: bool,
113
+ input_bytes: int = 0,
114
+ output_bytes: int = 0,
115
+ calls: int = 0,
116
+ chain_calls: int = 0,
117
+ ) -> None:
118
+ self.count += 1
119
+ if success:
120
+ self.success += 1
121
+ else:
122
+ self.failure += 1
123
+ self.input_bytes += max(0, input_bytes)
124
+ self.output_bytes += max(0, output_bytes)
125
+ self.calls += max(0, calls)
126
+ self.chain_calls += max(0, chain_calls)
127
+ self.duration_ms.observe(duration_ms)
128
+ self.input_size_bytes.observe(float(max(0, input_bytes)))
129
+ self.output_size_bytes.observe(float(max(0, output_bytes)))
130
+
131
+ def snapshot(self, *, include_distributions: bool = True) -> JsonObject:
132
+ values: JsonObject = {
133
+ "count": self.count,
134
+ "success": self.success,
135
+ "failure": self.failure,
136
+ "input_bytes": self.input_bytes,
137
+ "output_bytes": self.output_bytes,
138
+ "calls": self.calls,
139
+ "chain_calls": self.chain_calls,
140
+ }
141
+ if include_distributions:
142
+ values["duration_ms"] = self.duration_ms.snapshot()
143
+ values["input_size_bytes"] = self.input_size_bytes.snapshot()
144
+ values["output_size_bytes"] = self.output_size_bytes.snapshot()
145
+ return values
146
+
147
+ @classmethod
148
+ def from_snapshot(cls, value: object) -> Rollup:
149
+ if not isinstance(value, dict):
150
+ return cls()
151
+ return cls(
152
+ count=_integer(value.get("count")),
153
+ success=_integer(value.get("success")),
154
+ failure=_integer(value.get("failure")),
155
+ input_bytes=_integer(value.get("input_bytes")),
156
+ output_bytes=_integer(value.get("output_bytes")),
157
+ calls=_integer(value.get("calls")),
158
+ chain_calls=_integer(value.get("chain_calls")),
159
+ duration_ms=Histogram.from_snapshot(value.get("duration_ms")),
160
+ input_size_bytes=Histogram.from_snapshot(
161
+ value.get("input_size_bytes"), bounds=HISTOGRAM_BOUNDS_BYTES
162
+ ),
163
+ output_size_bytes=Histogram.from_snapshot(
164
+ value.get("output_size_bytes"), bounds=HISTOGRAM_BOUNDS_BYTES
165
+ ),
166
+ )
167
+
168
+
169
+ class StatsStore:
170
+ def __init__(self, path: Path) -> None:
171
+ self.path = path
172
+ self.lifetime = Rollup()
173
+ self.operations: dict[str, Rollup] = {}
174
+ self.phases: dict[str, Histogram] = {}
175
+ self.servers: dict[str, Rollup] = {}
176
+ self.tools: dict[str, Rollup] = {}
177
+ self.failures: dict[str, int] = {}
178
+ self.cache_hits = 0
179
+ self.cache_misses = 0
180
+ self.recent: dict[int, Rollup] = {}
181
+ self.updated_at = 0
182
+ self._dirty = False
183
+ self._closing = False
184
+ self._flush_task: asyncio.Task[None] | None = None
185
+ self._load()
186
+
187
+ def record_operation(
188
+ self,
189
+ name: str,
190
+ *,
191
+ duration_ms: float,
192
+ success: bool,
193
+ failure_stage: str | None = None,
194
+ input_bytes: int = 0,
195
+ output_bytes: int = 0,
196
+ calls: int = 0,
197
+ chain_calls: int = 0,
198
+ ) -> None:
199
+ self.lifetime.observe(
200
+ duration_ms=duration_ms,
201
+ success=success,
202
+ input_bytes=input_bytes,
203
+ output_bytes=output_bytes,
204
+ calls=calls,
205
+ chain_calls=chain_calls,
206
+ )
207
+ self._rollup_dimension(self.operations, name, MAX_OPERATIONS).observe(
208
+ duration_ms=duration_ms,
209
+ success=success,
210
+ input_bytes=input_bytes,
211
+ output_bytes=output_bytes,
212
+ calls=calls,
213
+ chain_calls=chain_calls,
214
+ )
215
+ self._recent_rollup().observe(
216
+ duration_ms=duration_ms,
217
+ success=success,
218
+ input_bytes=input_bytes,
219
+ output_bytes=output_bytes,
220
+ calls=calls,
221
+ chain_calls=chain_calls,
222
+ )
223
+ if failure_stage is not None:
224
+ key = self._bounded_key(self.failures, failure_stage, MAX_FAILURE_STAGES)
225
+ self.failures[key] = self.failures.get(key, 0) + 1
226
+ self._changed()
227
+
228
+ def record_phase(self, name: str, duration_ms: float) -> None:
229
+ self._histogram_dimension(self.phases, name, MAX_PHASES).observe(duration_ms)
230
+ self._changed()
231
+
232
+ def record_upstream(
233
+ self,
234
+ server: str,
235
+ tool: str,
236
+ *,
237
+ duration_ms: float,
238
+ success: bool,
239
+ input_bytes: int,
240
+ output_bytes: int,
241
+ ) -> None:
242
+ for rollup in (
243
+ self._rollup_dimension(self.servers, server, MAX_SERVERS),
244
+ self._rollup_dimension(self.tools, f"{server}.{tool}", MAX_TOOLS),
245
+ ):
246
+ rollup.observe(
247
+ duration_ms=duration_ms,
248
+ success=success,
249
+ input_bytes=input_bytes,
250
+ output_bytes=output_bytes,
251
+ calls=1,
252
+ )
253
+ self._changed()
254
+
255
+ def record_cache(self, *, hit: bool) -> None:
256
+ if hit:
257
+ self.cache_hits += 1
258
+ else:
259
+ self.cache_misses += 1
260
+ self._changed()
261
+
262
+ def snapshot(self) -> JsonObject:
263
+ recent = [
264
+ {"bucket_start": timestamp, **rollup.snapshot(include_distributions=False)}
265
+ for timestamp, rollup in sorted(self.recent.items())
266
+ ]
267
+ return JSON_OBJECT_ADAPTER.validate_python({
268
+ "version": 1,
269
+ "updated_at": self.updated_at,
270
+ "lifetime": self.lifetime.snapshot(),
271
+ "recent": recent,
272
+ "operations": _snapshot_mapping(self.operations),
273
+ "phases": _snapshot_mapping(self.phases),
274
+ "servers": _snapshot_mapping(self.servers),
275
+ "tools": _snapshot_mapping(self.tools, include_distributions=False),
276
+ "failures": dict(sorted(self.failures.items())),
277
+ "cache": {"hits": self.cache_hits, "misses": self.cache_misses},
278
+ })
279
+
280
+ def schedule_flush(self) -> None:
281
+ if self._closing:
282
+ return
283
+ if self._flush_task is not None and not self._flush_task.done():
284
+ return
285
+ try:
286
+ loop = asyncio.get_running_loop()
287
+ except RuntimeError:
288
+ return
289
+ self._flush_task = loop.create_task(self._delayed_flush())
290
+
291
+ async def flush(self) -> None:
292
+ if not self._dirty:
293
+ return
294
+ payload = self.snapshot()
295
+ self._dirty = False
296
+ try:
297
+ await asyncio.to_thread(self._write, payload)
298
+ except BaseException:
299
+ self._dirty = True
300
+ raise
301
+
302
+ async def close(self) -> None:
303
+ self._closing = True
304
+ task, self._flush_task = self._flush_task, None
305
+ if task is not None and not task.done():
306
+ task.cancel()
307
+ with suppress(asyncio.CancelledError):
308
+ await task
309
+ await self.flush()
310
+
311
+ async def _delayed_flush(self) -> None:
312
+ try:
313
+ await asyncio.sleep(FLUSH_DELAY_SECONDS)
314
+ await self.flush()
315
+ finally:
316
+ self._flush_task = None
317
+ if self._dirty and not self._closing:
318
+ self.schedule_flush()
319
+
320
+ def _changed(self) -> None:
321
+ self.updated_at = int(time.time())
322
+ self._dirty = True
323
+ self.schedule_flush()
324
+
325
+ def _recent_rollup(self) -> Rollup:
326
+ timestamp = int(time.time() // RECENT_BUCKET_SECONDS) * RECENT_BUCKET_SECONDS
327
+ rollup = self.recent.setdefault(timestamp, Rollup())
328
+ while len(self.recent) > RECENT_BUCKET_COUNT:
329
+ del self.recent[min(self.recent)]
330
+ return rollup
331
+
332
+ @staticmethod
333
+ def _rollup_dimension(values: dict[str, Rollup], key: str, limit: int) -> Rollup:
334
+ bounded = StatsStore._bounded_key(values, key, limit)
335
+ return values.setdefault(bounded, Rollup())
336
+
337
+ @staticmethod
338
+ def _histogram_dimension(values: dict[str, Histogram], key: str, limit: int) -> Histogram:
339
+ bounded = StatsStore._bounded_key(values, key, limit)
340
+ return values.setdefault(bounded, Histogram())
341
+
342
+ @staticmethod
343
+ def _bounded_key(values: Mapping[str, object], key: str, limit: int) -> str:
344
+ if key in values:
345
+ return key
346
+ if len(values) < max(1, limit - 1):
347
+ return key
348
+ return OTHER_DIMENSION
349
+
350
+ def _write(self, payload: JsonObject) -> None:
351
+ self.path.parent.mkdir(parents=True, exist_ok=True)
352
+ temporary = self.path.with_name(f"{self.path.name}.{os.getpid()}.tmp")
353
+ temporary.write_text(
354
+ json.dumps(payload, separators=(",", ":"), sort_keys=True),
355
+ encoding="utf-8",
356
+ )
357
+ Path(temporary).replace(self.path)
358
+
359
+ def _load(self) -> None:
360
+ try:
361
+ raw = json.loads(self.path.read_text(encoding="utf-8"))
362
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
363
+ return
364
+ if not isinstance(raw, dict) or raw.get("version") != 1:
365
+ return
366
+ self.updated_at = _integer(raw.get("updated_at"))
367
+ self.lifetime = Rollup.from_snapshot(raw.get("lifetime"))
368
+ self.operations = _load_rollups(raw.get("operations"), MAX_OPERATIONS)
369
+ self.phases = _load_histograms(raw.get("phases"), MAX_PHASES)
370
+ self.servers = _load_rollups(raw.get("servers"), MAX_SERVERS)
371
+ self.tools = _load_rollups(raw.get("tools"), MAX_TOOLS)
372
+ raw_failures = raw.get("failures")
373
+ if isinstance(raw_failures, dict):
374
+ self.failures = {
375
+ str(key): _integer(value)
376
+ for key, value in list(raw_failures.items())[:MAX_FAILURE_STAGES]
377
+ }
378
+ raw_cache = raw.get("cache")
379
+ if isinstance(raw_cache, dict):
380
+ self.cache_hits = _integer(raw_cache.get("hits"))
381
+ self.cache_misses = _integer(raw_cache.get("misses"))
382
+ raw_recent = raw.get("recent")
383
+ if isinstance(raw_recent, list):
384
+ for item in raw_recent[-RECENT_BUCKET_COUNT:]:
385
+ if not isinstance(item, dict):
386
+ continue
387
+ timestamp = _integer(item.get("bucket_start"))
388
+ if timestamp > 0:
389
+ self.recent[timestamp] = Rollup.from_snapshot(item)
390
+
391
+
392
+ def _snapshot_mapping(
393
+ values: Mapping[str, Rollup | Histogram],
394
+ *,
395
+ include_distributions: bool = True,
396
+ ) -> JsonObject:
397
+ return {
398
+ key: (
399
+ value.snapshot(include_distributions=include_distributions)
400
+ if isinstance(value, Rollup)
401
+ else value.snapshot()
402
+ )
403
+ for key, value in sorted(values.items())
404
+ }
405
+
406
+
407
+ def _load_rollups(value: object, limit: int) -> dict[str, Rollup]:
408
+ if not isinstance(value, dict):
409
+ return {}
410
+ return {str(key): Rollup.from_snapshot(item) for key, item in list(value.items())[:limit]}
411
+
412
+
413
+ def _load_histograms(value: object, limit: int) -> dict[str, Histogram]:
414
+ if not isinstance(value, dict):
415
+ return {}
416
+ return {str(key): Histogram.from_snapshot(item) for key, item in list(value.items())[:limit]}
417
+
418
+
419
+ def _integer(value: object) -> int:
420
+ return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else 0
421
+
422
+
423
+ def _number(value: object) -> float:
424
+ return float(value) if isinstance(value, (int, float)) and not isinstance(value, bool) else 0.0