unique_toolkit 2026.32.0.dev4__py3-none-any.whl → 2026.32.0.dev6__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.
@@ -43,14 +43,18 @@ class AnalyticsToolName(TypedDict):
43
43
  display_name: str
44
44
 
45
45
 
46
- class AnalyticsTokenUsage(TypedDict):
47
- model_name: str
46
+ class AnalyticsConsumption(TypedDict):
48
47
  completion_tokens: int | None
49
48
  prompt_tokens: int | None
50
49
  total_tokens: int | None
51
50
  reasoning_tokens: int | None
52
51
  cached_tokens: int | None
53
52
  cache_write_tokens: int | None
53
+ cost_usd: float | None
54
+
55
+
56
+ class AnalyticsConsumptionByLlm(AnalyticsConsumption):
57
+ model_name: str
54
58
 
55
59
 
56
60
  class Analytics(TypedDict):
@@ -58,6 +62,10 @@ class Analytics(TypedDict):
58
62
  artifacts_created_count: int | None
59
63
  artifacts_created_filetype: list[str] | None
60
64
  context_memory_updated: bool | None
65
+ # Sum of all model usages this turn. None when no invocations reported usage.
66
+ consumption: AnalyticsConsumption | None
67
+ # Per-model usage totals, sorted by model_name.
68
+ consumption_by_llm: list[AnalyticsConsumptionByLlm]
61
69
  language_model: AnalyticsLanguageModel
62
70
  loop_iteration_count: int
63
71
  mcp_tool_names_used: list[AnalyticsToolName]
@@ -67,7 +75,6 @@ class Analytics(TypedDict):
67
75
  references_count: int
68
76
  skills_used: list[AnalyticsSkill]
69
77
  subagent_names_used: list[AnalyticsToolName]
70
- tokens: list[AnalyticsTokenUsage]
71
78
  tool_call_count: int
72
79
  tools_used: list[AnalyticsTool]
73
80
  total_time_to_answer_ms: int | None
@@ -149,6 +156,7 @@ class DebugInfoManager:
149
156
  # DisplayCodeInterpreterFilesPostProcessor) into debug_info["artifacts"].
150
157
  # Absent when no Code Interpreter ran this turn → artifact fields stay
151
158
  # None, preserving the always-present, null-when-unknown contract.
159
+ consumption_by_llm = _aggregate_consumption_by_llm(invocations or [])
152
160
  analytics = Analytics(
153
161
  tools_used=analytics_tools,
154
162
  tool_call_count=len(analytics_tools),
@@ -167,7 +175,8 @@ class DebugInfoManager:
167
175
  loop_iteration_count=loop_iteration_count,
168
176
  subagent_names_used=_unique_tool_names(analytics_tools, "is_sub_agent"),
169
177
  mcp_tool_names_used=_unique_tool_names(analytics_tools, "is_mcp"),
170
- tokens=_aggregate_tokens_by_model(invocations or []),
178
+ consumption_by_llm=consumption_by_llm,
179
+ consumption=_aggregate_consumption(invocations or []),
171
180
  total_time_to_answer_ms=total_time_to_answer_ms,
172
181
  artifacts_created_count=artifacts["count"] if artifacts else None,
173
182
  artifacts_created_filetype=artifacts["filetypes"] if artifacts else None,
@@ -179,27 +188,66 @@ class DebugInfoManager:
179
188
  self.add("analytics", analytics)
180
189
 
181
190
 
182
- def _aggregate_tokens_by_model(
191
+ def _sum_invocation_costs(
192
+ invocations: list[LanguageModelInvocationStats],
193
+ ) -> float | None:
194
+ """Sum costs only when every invocation has a known cost; else None."""
195
+ costs = [invocation.cost_usd for invocation in invocations]
196
+ if not costs or any(cost is None for cost in costs):
197
+ return None
198
+ return sum(cost for cost in costs if cost is not None)
199
+
200
+
201
+ def _to_analytics_consumption(
202
+ usage: LanguageModelTokenUsage,
203
+ cost_usd: float | None,
204
+ ) -> AnalyticsConsumption:
205
+ return AnalyticsConsumption(
206
+ completion_tokens=usage.completion_tokens,
207
+ prompt_tokens=usage.prompt_tokens,
208
+ total_tokens=usage.total_tokens,
209
+ reasoning_tokens=usage.reasoning_tokens,
210
+ cached_tokens=usage.cached_tokens,
211
+ cache_write_tokens=usage.cache_write_tokens,
212
+ cost_usd=cost_usd,
213
+ )
214
+
215
+
216
+ def _aggregate_consumption(
183
217
  invocations: list[LanguageModelInvocationStats],
184
- ) -> list[AnalyticsTokenUsage]:
185
- usages_by_model: dict[str, list[LanguageModelTokenUsage]] = defaultdict(list)
218
+ ) -> AnalyticsConsumption | None:
219
+ """Aggregate token usage and cost across all models for this turn."""
220
+ usage = LanguageModelTokenUsage.sum_usages(
221
+ invocation.token_usage for invocation in invocations
222
+ )
223
+ if usage is None:
224
+ return None
225
+ return _to_analytics_consumption(usage, _sum_invocation_costs(invocations))
226
+
227
+
228
+ def _aggregate_consumption_by_llm(
229
+ invocations: list[LanguageModelInvocationStats],
230
+ ) -> list[AnalyticsConsumptionByLlm]:
231
+ invocations_by_model: dict[str, list[LanguageModelInvocationStats]] = defaultdict(
232
+ list
233
+ )
186
234
  for invocation in invocations:
187
- usages_by_model[str(invocation.model_name)].append(invocation.token_usage)
235
+ invocations_by_model[str(invocation.model_name)].append(invocation)
188
236
 
189
- totals: list[AnalyticsTokenUsage] = []
190
- for model_name in sorted(usages_by_model):
191
- usage = LanguageModelTokenUsage.sum_usages(usages_by_model[model_name])
237
+ totals: list[AnalyticsConsumptionByLlm] = []
238
+ for model_name in sorted(invocations_by_model):
239
+ model_invocations = invocations_by_model[model_name]
240
+ usage = LanguageModelTokenUsage.sum_usages(
241
+ invocation.token_usage for invocation in model_invocations
242
+ )
192
243
  if usage is None:
193
244
  continue
194
245
  totals.append(
195
- AnalyticsTokenUsage(
246
+ AnalyticsConsumptionByLlm(
196
247
  model_name=model_name,
197
- completion_tokens=usage.completion_tokens,
198
- prompt_tokens=usage.prompt_tokens,
199
- total_tokens=usage.total_tokens,
200
- reasoning_tokens=usage.reasoning_tokens,
201
- cached_tokens=usage.cached_tokens,
202
- cache_write_tokens=usage.cache_write_tokens,
248
+ **_to_analytics_consumption(
249
+ usage, _sum_invocation_costs(model_invocations)
250
+ ),
203
251
  )
204
252
  )
205
253
  return totals
@@ -11,6 +11,7 @@ from humps import camelize
11
11
  from pydantic import BaseModel, BeforeValidator, ConfigDict
12
12
 
13
13
  from unique_toolkit.language_model.infos import LanguageModelName
14
+ from unique_toolkit.language_model.model_costs import calculate_invocation_cost_usd
14
15
  from unique_toolkit.language_model.schemas import LanguageModelTokenUsage
15
16
 
16
17
  # `protected_namespaces=()` allows the `model_name` field name.
@@ -59,6 +60,7 @@ class LanguageModelInvocationStats(BaseModel):
59
60
  model_name: ModelName
60
61
  token_usage: LanguageModelTokenUsage
61
62
  source: Source # e.g. "main_loop", tool/evaluation/postprocessor name
63
+ cost_usd: float | None = None
62
64
 
63
65
  @classmethod
64
66
  def from_usage(
@@ -67,4 +69,9 @@ class LanguageModelInvocationStats(BaseModel):
67
69
  token_usage: LanguageModelTokenUsage,
68
70
  source: str,
69
71
  ) -> Self:
70
- return cls(model_name=model_name, token_usage=token_usage, source=source)
72
+ return cls(
73
+ model_name=model_name,
74
+ token_usage=token_usage,
75
+ source=source,
76
+ cost_usd=calculate_invocation_cost_usd(str(model_name), token_usage),
77
+ )
@@ -0,0 +1,118 @@
1
+ """Load model prices and calculate the USD cost of LLM invocations."""
2
+
3
+ import logging
4
+ import os
5
+ import time
6
+ from pathlib import Path
7
+
8
+ import yaml
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+ from unique_toolkit.language_model.schemas import LanguageModelTokenUsage
12
+
13
+ logger = logging.getLogger(f"toolkit.language_model.{__name__}")
14
+
15
+ MODEL_COSTS_FILE_ENV = "MODEL_COSTS_FILE"
16
+ _LITELLM_PREFIX = "litellm:"
17
+ _TOKENS_PER_MILLION = 1_000_000
18
+
19
+ # Safety-only bound on how long a process may keep a loaded catalog. Not the
20
+ # primary refresh mechanism — just ensures a long-lived worker eventually
21
+ # re-reads a remounted price sheet.
22
+ _CACHE_MAX_AGE_SECONDS = 5 * 60
23
+
24
+
25
+ class ModelCost(BaseModel):
26
+ """Per-million-token prices for one language model."""
27
+
28
+ model_config = ConfigDict(extra="forbid")
29
+
30
+ input: float
31
+ completion: float
32
+ currency: str = Field(default="USD", min_length=1)
33
+
34
+
35
+ class ModelCostCatalog(BaseModel):
36
+ """Versioned model-price catalog rendered by the platform Helm charts."""
37
+
38
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
39
+
40
+ cost_schema_version: int = Field(alias="costSchemaVersion")
41
+ models: dict[str, ModelCost] = Field(min_length=1)
42
+
43
+
44
+ _catalog_cache: dict[Path, tuple[ModelCostCatalog, float]] = {}
45
+
46
+
47
+ def _parse_model_cost_catalog(path: Path) -> ModelCostCatalog:
48
+ payload = yaml.safe_load(path.read_text(encoding="utf-8"))
49
+ catalog = ModelCostCatalog.model_validate(payload)
50
+ if catalog.cost_schema_version != 1:
51
+ raise ValueError(
52
+ f"Unsupported model cost schema version: {catalog.cost_schema_version}"
53
+ )
54
+ return catalog
55
+
56
+
57
+ def _load_model_cost_catalog(path: Path) -> ModelCostCatalog:
58
+ now = time.monotonic()
59
+ cached = _catalog_cache.get(path)
60
+ if cached is not None:
61
+ catalog, loaded_at = cached
62
+ if now - loaded_at < _CACHE_MAX_AGE_SECONDS:
63
+ return catalog
64
+
65
+ catalog = _parse_model_cost_catalog(path)
66
+ _catalog_cache[path] = (catalog, now)
67
+ return catalog
68
+
69
+
70
+ def load_model_cost_catalog(path: str | Path | None = None) -> ModelCostCatalog | None:
71
+ """Load and cache the configured catalog, or return None when unavailable.
72
+
73
+ Returns None when the catalog path is unset, or when the file cannot be
74
+ read/parsed. Cost capture must never abort a successful LLM call over a
75
+ missing or invalid price sheet.
76
+ """
77
+
78
+ configured_path = path or os.getenv(MODEL_COSTS_FILE_ENV)
79
+ if configured_path is None or not str(configured_path).strip():
80
+ return None
81
+ try:
82
+ return _load_model_cost_catalog(Path(configured_path))
83
+ except Exception as exc:
84
+ logger.warning(
85
+ "Failed to load model cost catalog from %s: %s",
86
+ configured_path,
87
+ exc,
88
+ )
89
+ return None
90
+
91
+
92
+ def calculate_invocation_cost_usd(
93
+ model_name: str,
94
+ token_usage: LanguageModelTokenUsage,
95
+ catalog: ModelCostCatalog | None = None,
96
+ ) -> float | None:
97
+ """Calculate invocation cost using prompt and completion token totals."""
98
+
99
+ if token_usage.prompt_tokens is None or token_usage.completion_tokens is None:
100
+ return None
101
+
102
+ active_catalog = catalog if catalog is not None else load_model_cost_catalog()
103
+ if active_catalog is None:
104
+ return None
105
+
106
+ normalized_model_name = str(model_name)
107
+ model_cost = active_catalog.models.get(normalized_model_name)
108
+ if model_cost is None and normalized_model_name.startswith(_LITELLM_PREFIX):
109
+ model_cost = active_catalog.models.get(
110
+ normalized_model_name.removeprefix(_LITELLM_PREFIX)
111
+ )
112
+ if model_cost is None:
113
+ return None
114
+
115
+ return (
116
+ token_usage.prompt_tokens * model_cost.input
117
+ + token_usage.completion_tokens * model_cost.completion
118
+ ) / _TOKENS_PER_MILLION
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import time
4
+ from collections.abc import Sequence
4
5
 
5
6
  from prometheus_client import ( # pyright: ignore[reportMissingImports]
6
7
  Counter,
@@ -16,12 +17,6 @@ _http_requests_total = Counter(
16
17
  ["method", "path", "status_code"],
17
18
  registry=REGISTRY,
18
19
  )
19
- _http_request_duration_seconds = Histogram(
20
- "python_http_request_duration_seconds",
21
- "HTTP request duration",
22
- ["method", "path"],
23
- registry=REGISTRY,
24
- )
25
20
  _http_requests_in_progress = Gauge(
26
21
  "python_http_requests_in_progress",
27
22
  "In-flight HTTP requests",
@@ -30,6 +25,35 @@ _http_requests_in_progress = Gauge(
30
25
  multiprocess_mode="livesum",
31
26
  )
32
27
 
28
+ # Created lazily on first MetricsMiddleware construction so services can choose the
29
+ # bucket layout. Prometheus allows a metric name to be registered only once per
30
+ # registry, so the buckets passed to the first middleware win for the process.
31
+ _http_request_duration_seconds: Histogram | None = None
32
+
33
+
34
+ def _get_or_create_duration_histogram(
35
+ buckets: Sequence[float] | None,
36
+ ) -> Histogram:
37
+ global _http_request_duration_seconds
38
+ if _http_request_duration_seconds is None:
39
+ if buckets is None:
40
+ _http_request_duration_seconds = Histogram(
41
+ "python_http_request_duration_seconds",
42
+ "HTTP request duration",
43
+ ["method", "path"],
44
+ registry=REGISTRY,
45
+ )
46
+ else:
47
+ _http_request_duration_seconds = Histogram(
48
+ "python_http_request_duration_seconds",
49
+ "HTTP request duration",
50
+ ["method", "path"],
51
+ registry=REGISTRY,
52
+ buckets=tuple(buckets),
53
+ )
54
+ return _http_request_duration_seconds
55
+
56
+
33
57
  _DEFAULT_EXCLUDED_PATHS = frozenset({"/health", "/metrics", "/"})
34
58
 
35
59
 
@@ -51,13 +75,29 @@ class MetricsMiddleware:
51
75
 
52
76
  # Quart
53
77
  app.asgi_app = MetricsMiddleware(app.asgi_app)
78
+
79
+ ``duration_buckets`` overrides the latency histogram buckets (seconds). Without
80
+ it, prometheus_client's defaults apply and their top finite bucket is 10s —
81
+ histogram_quantile cannot resolve past the largest finite bucket, so services
82
+ with slower endpoints should pass buckets covering their full latency range::
83
+
84
+ app.add_middleware(MetricsMiddleware, duration_buckets=(1, 5, 30, 60, 120))
85
+
86
+ The histogram is registered once per process; the buckets of the first
87
+ constructed middleware apply.
54
88
  """
55
89
 
56
- def __init__(self, app, excluded_paths: set[str] | None = None):
90
+ def __init__(
91
+ self,
92
+ app,
93
+ excluded_paths: set[str] | None = None,
94
+ duration_buckets: Sequence[float] | None = None,
95
+ ):
57
96
  self.app = app
58
97
  self._excluded_paths = (
59
98
  excluded_paths if excluded_paths is not None else _DEFAULT_EXCLUDED_PATHS
60
99
  )
100
+ self._duration = _get_or_create_duration_histogram(duration_buckets)
61
101
 
62
102
  async def __call__(self, scope, receive, send):
63
103
  # Pass through non-HTTP (WebSocket, lifespan)
@@ -87,9 +127,7 @@ class MetricsMiddleware:
87
127
  finally:
88
128
  duration = time.perf_counter() - start
89
129
  _http_requests_in_progress.labels(method=method).dec()
90
- _http_request_duration_seconds.labels(method=method, path=path).observe(
91
- duration
92
- )
130
+ self._duration.labels(method=method, path=path).observe(duration)
93
131
  _http_requests_total.labels(
94
132
  method=method, path=path, status_code=status_code
95
133
  ).inc()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-toolkit
3
- Version: 2026.32.0.dev4
3
+ Version: 2026.32.0.dev6
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>
@@ -16,7 +16,7 @@ Requires-Dist: sseclient>=0.0.27,<0.0.28
16
16
  Requires-Dist: openai>=1.105.0,<3
17
17
  Requires-Dist: platformdirs>=4.0.0,<5
18
18
  Requires-Dist: pillow>=12.1.1
19
- Requires-Dist: unique-sdk>=2026.32.0.dev5,<2026.32.0rc0
19
+ Requires-Dist: unique-sdk>=2026.32.0.dev8,<2026.32.0rc0
20
20
  Requires-Dist: jambo>=0.1.2,<0.2
21
21
  Requires-Dist: jinja2>=3.1.6,<4
22
22
  Requires-Dist: docxtpl>=0.20.1,<0.21
@@ -33,6 +33,7 @@ Requires-Dist: tenacity>=9.1.4
33
33
  Requires-Dist: pydantic-core>=2.41.5
34
34
  Requires-Dist: cachetools>=5.0,<6
35
35
  Requires-Dist: rapidfuzz>=3.0,<4
36
+ Requires-Dist: pyyaml>=6.0.3
36
37
  Requires-Dist: fastapi>=0.133.0,<1.0.0 ; extra == 'fastapi'
37
38
  Requires-Dist: starlette>=1.3.1,<2.0.0 ; extra == 'fastapi'
38
39
  Requires-Dist: uvicorn>=0.37.0,<0.38 ; extra == 'fastapi'
@@ -83,7 +83,7 @@ unique_toolkit/_common/utils/write_configuration.py,sha256=fzvr4C-XBL3OSM3Od9Tbq
83
83
  unique_toolkit/_common/validate_required_values.py,sha256=cA8GPwkV1EPsG6ffIkmAn3Dx4LRRMC1EAURmT7PyuCg,551
84
84
  unique_toolkit/_common/validators.py,sha256=ElnkMsyEY24TfzfTVHvireyT39EnZgW5N40T0P4b6gE,2638
85
85
  unique_toolkit/agentic/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
86
- unique_toolkit/agentic/debug_info_manager/debug_info_manager.py,sha256=VZLDOTAJJ_jVOZ4Zw4C2F-uCv3vsjZTcvbljGBYKh-A,10501
86
+ unique_toolkit/agentic/debug_info_manager/debug_info_manager.py,sha256=fqhbKe0WWB8nZ7CzeEcgQGMHJajIMVOpKkUWOqiDdUE,12149
87
87
  unique_toolkit/agentic/debug_info_manager/test/test_debug_info_manager.py,sha256=bDjrHLqfwtd7ewdIdyTFHabG-15t3iXvs34ywoYXcik,11888
88
88
  unique_toolkit/agentic/evaluation/config.py,sha256=u5-iuT-4mfA2_9UZjZ-TN3YutVf35aR4EsF9l61Odnk,1696
89
89
  unique_toolkit/agentic/evaluation/context_relevancy/prompts/__init__.py,sha256=IKGvHW2viBylTUjEJNKvIXO4YrYwdJXBHSxA9rKtiRI,355
@@ -393,7 +393,8 @@ unique_toolkit/language_model/constants.py,sha256=B-topqW0r83dkC_25DeQfnPk3n53qz
393
393
  unique_toolkit/language_model/default_language_model.py,sha256=mcyuFs_V6tp52C8xJSHiEHO4kFGj6xed1gvozvCRD7k,1181
394
394
  unique_toolkit/language_model/functions.py,sha256=uYIsVQfF4uwW6cftlKhQElMLpXNaVe_57FFobhC7R4A,23503
395
395
  unique_toolkit/language_model/infos.py,sha256=y1JLszdCyKBUTCjPobYsB02G2WXZOGoHzQtjNqJ7EOc,140831
396
- unique_toolkit/language_model/invocation_stats.py,sha256=ApRy7YzlcWd5so3tm8RhkrJn6Ip5_rjZwYcyh92Wc_4,2244
396
+ unique_toolkit/language_model/invocation_stats.py,sha256=XAM_qckI-BoMBSgDpVcxFD1cFSdi1-GHZToHz_jLynM,2491
397
+ unique_toolkit/language_model/model_costs.py,sha256=WM1u2fWAKpPB_creS-ew8YAzc1V8oiaCbvhpSJaO8B0,3789
397
398
  unique_toolkit/language_model/prompt.py,sha256=JSawaLjQg3VR-E2fK8engFyJnNdk21zaO8pPIodzN4Q,3991
398
399
  unique_toolkit/language_model/reference.py,sha256=T3wr2jP_uRg5wKVIbKqLbxJlcgxfbZQGq3XdjbQ2kSo,6984
399
400
  unique_toolkit/language_model/schemas.py,sha256=IUmp_JMHx0czadJlmsLL9nmc-nCPmVRVLp2T5DSYsqY,35402
@@ -403,7 +404,7 @@ unique_toolkit/language_model/stream_transform.py,sha256=iNNGpLY_7G6Bpp9Y4KtbIkQ
403
404
  unique_toolkit/language_model/utils.py,sha256=bPQ4l6_YO71w-zaIPanUUmtbXC1_hCvLK0tAFc3VCRc,1902
404
405
  unique_toolkit/monitoring/__init__.py,sha256=z1gkOCLxGcg936MFuLSeWFa0tREcBFSUbV09NxilBw4,1065
405
406
  unique_toolkit/monitoring/gunicorn.py,sha256=urTFFS5zYHKoxb1TEQHcLqb2byi-aYgFRHGwgYRzYYI,844
406
- unique_toolkit/monitoring/middleware.py,sha256=JNh5v_INAkexuLtXhCG9n5n2t6Pnf8ZdPTnimAVS0_s,2931
407
+ unique_toolkit/monitoring/middleware.py,sha256=MNsUuKgt74mSnUwttXTB_AxX546Pz2zKoAP9OewVrwQ,4531
407
408
  unique_toolkit/monitoring/registry.py,sha256=De2Bx5QBBRJQY2MM3xtntiJ2JU2FoKkVT7rNMG7ewgA,5874
408
409
  unique_toolkit/protocols/support.py,sha256=x5edzbBmKAUuvm3oLFh52gkgQsqR5LU7M0t8RHK0DZk,5161
409
410
  unique_toolkit/services/__init__.py,sha256=b9Wl7Wvz1JmZDbDexIZzBoXZ0wscXuczGAH8LoL-Qak,572
@@ -418,6 +419,6 @@ unique_toolkit/short_term_memory/service.py,sha256=2JMBEBn6GoyK-a0xvdcWpPft-3C74
418
419
  unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
419
420
  unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
420
421
  unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
421
- unique_toolkit-2026.32.0.dev4.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
422
- unique_toolkit-2026.32.0.dev4.dist-info/METADATA,sha256=INpTCsVe9pxypJm__jhb4ItWNy616eN71Bn04P9Y6WU,7465
423
- unique_toolkit-2026.32.0.dev4.dist-info/RECORD,,
422
+ unique_toolkit-2026.32.0.dev6.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
423
+ unique_toolkit-2026.32.0.dev6.dist-info/METADATA,sha256=x3yw3QMqvumJ5CnzTP_nw7T1cOTgg6e3567TvDVCupk,7494
424
+ unique_toolkit-2026.32.0.dev6.dist-info/RECORD,,