unique_toolkit 2026.24.0.dev6__py3-none-any.whl → 2026.24.0.dev7__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.
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+
6
+ def child_exit(server, worker) -> None:
7
+ """Gunicorn child_exit hook: clean up dead worker's per-PID metric files.
8
+
9
+ Wire this up in gunicorn.conf.py:
10
+
11
+ from unique_toolkit.monitoring.gunicorn import child_exit # noqa: F401
12
+
13
+ Gunicorn calls any function named child_exit(server, worker) in the
14
+ config file whenever a worker process exits. Without this, dead workers'
15
+ .db files accumulate in PROMETHEUS_MULTIPROC_DIR and inflate aggregated
16
+ metric values on every future scrape.
17
+
18
+ No-ops when PROMETHEUS_MULTIPROC_DIR is not set (single-process mode).
19
+ """
20
+ if not os.environ.get("PROMETHEUS_MULTIPROC_DIR"):
21
+ return
22
+ from prometheus_client import multiprocess # pyright: ignore[reportMissingImports]
23
+
24
+ multiprocess.mark_process_dead(worker.pid)
@@ -11,22 +11,23 @@ from prometheus_client import ( # pyright: ignore[reportMissingImports]
11
11
  from .registry import REGISTRY
12
12
 
13
13
  _http_requests_total = Counter(
14
- "http_requests_total",
14
+ "python_http_requests_total",
15
15
  "Total HTTP requests",
16
16
  ["method", "path", "status_code"],
17
17
  registry=REGISTRY,
18
18
  )
19
19
  _http_request_duration_seconds = Histogram(
20
- "http_request_duration_seconds",
20
+ "python_http_request_duration_seconds",
21
21
  "HTTP request duration",
22
22
  ["method", "path"],
23
23
  registry=REGISTRY,
24
24
  )
25
25
  _http_requests_in_progress = Gauge(
26
- "http_requests_in_progress",
26
+ "python_http_requests_in_progress",
27
27
  "In-flight HTTP requests",
28
28
  ["method"],
29
29
  registry=REGISTRY,
30
+ multiprocess_mode="livesum",
30
31
  )
31
32
 
32
33
  _DEFAULT_EXCLUDED_PATHS = frozenset({"/health", "/metrics", "/"})
@@ -2,9 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import functools
5
+ import os
5
6
  import time
7
+ import warnings
6
8
  from contextlib import contextmanager
7
- from typing import Any, Callable
9
+ from typing import Any, Callable, Literal
8
10
 
9
11
  from prometheus_client import ( # pyright: ignore[reportMissingImports]
10
12
  CollectorRegistry,
@@ -18,7 +20,36 @@ REGISTRY = CollectorRegistry()
18
20
 
19
21
 
20
22
  def get_metrics() -> bytes:
21
- """Returns all registered metrics in Prometheus text format."""
23
+ """Returns all registered metrics in Prometheus text format.
24
+
25
+ In multiprocess mode (PROMETHEUS_MULTIPROC_DIR is set), aggregates
26
+ per-PID .db files from all Gunicorn workers via MultiProcessCollector.
27
+ In single-process mode, returns metrics from the in-process REGISTRY.
28
+
29
+ NOTE: PROMETHEUS_MULTIPROC_DIR must be set in the shell/entrypoint
30
+ *before* Python starts. Setting it from Python after prometheus_client
31
+ has been imported has no effect — metrics will silently bypass per-PID
32
+ files and this function will emit a warning.
33
+ """
34
+ if os.environ.get("PROMETHEUS_MULTIPROC_DIR"):
35
+ from prometheus_client import ( # pyright: ignore[reportMissingImports]
36
+ multiprocess,
37
+ values,
38
+ )
39
+
40
+ if not getattr(values.ValueClass, "_multiprocess", False):
41
+ warnings.warn(
42
+ "PROMETHEUS_MULTIPROC_DIR is set but prometheus_client was imported "
43
+ "before the env var — metrics will silently bypass per-PID files. "
44
+ "Set PROMETHEUS_MULTIPROC_DIR in the shell/entrypoint before launching Python.",
45
+ RuntimeWarning,
46
+ stacklevel=2,
47
+ )
48
+
49
+ registry = CollectorRegistry()
50
+ multiprocess.MultiProcessCollector(registry)
51
+ return generate_latest(registry)
52
+
22
53
  return generate_latest(REGISTRY)
23
54
 
24
55
 
@@ -43,8 +74,31 @@ class MetricNamespace:
43
74
  def counter(self, name: str, description: str, labels: list[str]) -> Counter:
44
75
  return Counter(f"{self._prefix}_{name}", description, labels, registry=REGISTRY)
45
76
 
46
- def gauge(self, name: str, description: str, labels: list[str]) -> Gauge:
47
- return Gauge(f"{self._prefix}_{name}", description, labels, registry=REGISTRY)
77
+ def gauge(
78
+ self,
79
+ name: str,
80
+ description: str,
81
+ labels: list[str],
82
+ multiprocess_mode: Literal[
83
+ "all",
84
+ "liveall",
85
+ "min",
86
+ "livemin",
87
+ "max",
88
+ "livemax",
89
+ "sum",
90
+ "livesum",
91
+ "mostrecent",
92
+ "livemostrecent",
93
+ ] = "all",
94
+ ) -> Gauge:
95
+ return Gauge(
96
+ f"{self._prefix}_{name}",
97
+ description,
98
+ labels,
99
+ registry=REGISTRY,
100
+ multiprocess_mode=multiprocess_mode,
101
+ )
48
102
 
49
103
 
50
104
  @contextmanager
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.24.0.dev6
3
+ Version: 2026.24.0.dev7
4
4
  Summary:
5
5
  Author: Cedric Klinkert, Martin Fadler, Sadique Sheik, Fabian Schläpfer, Pascal Hauri, Ejiroghene Oki-Peter, Rami Azouz, Ricardo Visini, Sanzio Monti
6
6
  Author-email: Cedric Klinkert <cedric.klinkert@unique.ch>, Martin Fadler <martin.fadler@unique.ch>, Sadique Sheik <sadique@unique.ch>, Fabian Schläpfer <fabian@unique.ch>, Pascal Hauri <pascal@unique.ch>, Ejiroghene Oki-Peter <oki-peter.ejiroghene@unique.ai>, Rami Azouz <rami.ext@unique.ch>, Ricardo Visini <ricardo.visini@unique.ch>, Sanzio Monti <sanzio.monti@unique.ai>
@@ -376,8 +376,9 @@ unique_toolkit/language_model/settings.py,sha256=xkoMv3ATb2ft5V3MA8JvqW-EO8AnZ5t
376
376
  unique_toolkit/language_model/stream_transform.py,sha256=iNNGpLY_7G6Bpp9Y4KtbIkQm3WB4yZt75BXcqx0DVkI,2775
377
377
  unique_toolkit/language_model/utils.py,sha256=bPQ4l6_YO71w-zaIPanUUmtbXC1_hCvLK0tAFc3VCRc,1902
378
378
  unique_toolkit/monitoring/__init__.py,sha256=z1gkOCLxGcg936MFuLSeWFa0tREcBFSUbV09NxilBw4,1065
379
- unique_toolkit/monitoring/middleware.py,sha256=QSOcRwaByN4t6gWY5-IUgW2JXoN7Yl-0JR5kSmOjxM8,2877
380
- unique_toolkit/monitoring/registry.py,sha256=8VG1NkfzrDiMfPr7KsX7uWEJBK3rNSRWOB8HNQ5B9jY,3333
379
+ unique_toolkit/monitoring/gunicorn.py,sha256=urTFFS5zYHKoxb1TEQHcLqb2byi-aYgFRHGwgYRzYYI,844
380
+ unique_toolkit/monitoring/middleware.py,sha256=JNh5v_INAkexuLtXhCG9n5n2t6Pnf8ZdPTnimAVS0_s,2931
381
+ unique_toolkit/monitoring/registry.py,sha256=8r9jqaQoRHZveGRZe_GQvBzvQpmUV4cpw8Ej1ssDIdE,5056
381
382
  unique_toolkit/protocols/support.py,sha256=x5edzbBmKAUuvm3oLFh52gkgQsqR5LU7M0t8RHK0DZk,5161
382
383
  unique_toolkit/services/__init__.py,sha256=b9Wl7Wvz1JmZDbDexIZzBoXZ0wscXuczGAH8LoL-Qak,572
383
384
  unique_toolkit/services/chat_service.py,sha256=FPY-GTFAkD0C9pYfPfkfAvyIKnL9DX9iDk75gDeghCc,95209
@@ -391,6 +392,6 @@ unique_toolkit/short_term_memory/service.py,sha256=Ncw0_0CNM6kmzJSLVJGG5GcKpBduI
391
392
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
392
393
  unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
393
394
  unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
394
- unique_toolkit-2026.24.0.dev6.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
395
- unique_toolkit-2026.24.0.dev6.dist-info/METADATA,sha256=9iqeteqQZ9CeoDz6t3gI0L0YdK6AnpC376d7cqfdps4,7392
396
- unique_toolkit-2026.24.0.dev6.dist-info/RECORD,,
395
+ unique_toolkit-2026.24.0.dev7.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
396
+ unique_toolkit-2026.24.0.dev7.dist-info/METADATA,sha256=ThX3w1DhE99W9OR-nA3Kmd-hNVmqxdGU2xtsV9mUvqg,7392
397
+ unique_toolkit-2026.24.0.dev7.dist-info/RECORD,,