unique_toolkit 2026.36.0.dev3__py3-none-any.whl → 2026.36.0.dev4__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.
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/postprocessors/artifacts.py +129 -19
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/postprocessors/generated_files.py +10 -0
- unique_toolkit/agentic/tools/openai_builtin/code_interpreter/postprocessors/office_preview.py +154 -0
- {unique_toolkit-2026.36.0.dev3.dist-info → unique_toolkit-2026.36.0.dev4.dist-info}/METADATA +1 -1
- {unique_toolkit-2026.36.0.dev3.dist-info → unique_toolkit-2026.36.0.dev4.dist-info}/RECORD +6 -5
- {unique_toolkit-2026.36.0.dev3.dist-info → unique_toolkit-2026.36.0.dev4.dist-info}/WHEEL +0 -0
|
@@ -1,11 +1,21 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import logging
|
|
3
|
+
import tempfile
|
|
2
4
|
from mimetypes import guess_type
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
3
7
|
|
|
8
|
+
import unique_sdk.utils.file_io as file_io
|
|
4
9
|
from openai.types.responses.response_output_text import AnnotationContainerFileCitation
|
|
5
10
|
from pydantic import BaseModel
|
|
6
11
|
|
|
7
12
|
from unique_toolkit import ChatService
|
|
8
13
|
from unique_toolkit._common.utils.files import FileMimeType, ImageMimeType
|
|
14
|
+
from unique_toolkit.agentic.tools.openai_builtin.code_interpreter.postprocessors.office_preview import (
|
|
15
|
+
OFFICE_PREVIEW_MIME_TYPES,
|
|
16
|
+
convert_office_bytes_to_preview_pdf,
|
|
17
|
+
office_extension,
|
|
18
|
+
)
|
|
9
19
|
from unique_toolkit.content.schemas import Content
|
|
10
20
|
|
|
11
21
|
_CODE_EXECUTION_ARTIFACT_METADATA_KEY: str = "codeExecutionArtifactMetadata"
|
|
@@ -50,22 +60,24 @@ def _kb_safe_mime(mime: str) -> str:
|
|
|
50
60
|
return "text/plain"
|
|
51
61
|
|
|
52
62
|
|
|
53
|
-
|
|
63
|
+
def _artifact_metadata(
|
|
64
|
+
file: AnnotationContainerFileCitation,
|
|
65
|
+
) -> dict[str, Any]:
|
|
66
|
+
return {
|
|
67
|
+
_CODE_EXECUTION_ARTIFACT_METADATA_KEY: CodeExecutionArtifactMetadata(
|
|
68
|
+
container_id=file.container_id,
|
|
69
|
+
file_id=file.file_id,
|
|
70
|
+
filepath=f"/mnt/data/{file.filename}",
|
|
71
|
+
).model_dump()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
async def _upload_artifact_bytes(
|
|
54
76
|
chat_service: ChatService,
|
|
55
77
|
file: AnnotationContainerFileCitation,
|
|
56
78
|
file_bytes: bytes,
|
|
79
|
+
mime: str,
|
|
57
80
|
) -> Content:
|
|
58
|
-
raw_mime = guess_type(file.filename)[0] or "text/plain"
|
|
59
|
-
mime = _kb_safe_mime(raw_mime)
|
|
60
|
-
|
|
61
|
-
if mime != raw_mime:
|
|
62
|
-
_LOGGER.info(
|
|
63
|
-
"MIME type '%s' is not supported by the Unique KB; "
|
|
64
|
-
"uploading '%s' as 'text/plain' so the file can be stored and downloaded.",
|
|
65
|
-
raw_mime,
|
|
66
|
-
file.filename,
|
|
67
|
-
)
|
|
68
|
-
|
|
69
81
|
_LOGGER.info(
|
|
70
82
|
"Uploading '%s' to knowledge base (%d bytes, mime type %s)",
|
|
71
83
|
file.filename,
|
|
@@ -79,13 +91,111 @@ async def save_code_execution_artifact(
|
|
|
79
91
|
mime_type=mime,
|
|
80
92
|
skip_ingestion=True,
|
|
81
93
|
hide_in_chat=True,
|
|
82
|
-
metadata=
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
metadata=_artifact_metadata(file),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
async def _upload_office_artifact_with_preview(
|
|
99
|
+
chat_service: ChatService,
|
|
100
|
+
file: AnnotationContainerFileCitation,
|
|
101
|
+
file_bytes: bytes,
|
|
102
|
+
mime: str,
|
|
103
|
+
) -> Content | None:
|
|
104
|
+
"""Upload an Office artifact with a PDF preview, or ``None`` to fall back."""
|
|
105
|
+
with tempfile.TemporaryDirectory(prefix="code-interpreter-preview-") as tmp:
|
|
106
|
+
output_dir = Path(tmp)
|
|
107
|
+
preview_path = await convert_office_bytes_to_preview_pdf(
|
|
108
|
+
filename=file.filename,
|
|
109
|
+
file_bytes=file_bytes,
|
|
110
|
+
tmp_dir=output_dir,
|
|
111
|
+
logger=_LOGGER,
|
|
112
|
+
)
|
|
113
|
+
if preview_path is None:
|
|
114
|
+
_LOGGER.warning(
|
|
115
|
+
"Code interpreter Office artifact '%s' could not be converted "
|
|
116
|
+
"to PDF; uploading without preview.",
|
|
117
|
+
file.filename,
|
|
118
|
+
)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
source_path = output_dir / Path(file.filename).name
|
|
122
|
+
_LOGGER.info(
|
|
123
|
+
"Uploading code interpreter Office artifact '%s' with PDF preview '%s'",
|
|
124
|
+
file.filename,
|
|
125
|
+
preview_path.name,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
uploaded = await asyncio.to_thread(
|
|
129
|
+
file_io.upload_file,
|
|
130
|
+
chat_service._user_id,
|
|
131
|
+
chat_service._company_id,
|
|
132
|
+
str(source_path),
|
|
133
|
+
file.filename,
|
|
134
|
+
mime,
|
|
135
|
+
chat_id=chat_service._chat_id,
|
|
136
|
+
ingestion_config={
|
|
137
|
+
"uniqueIngestionMode": "SKIP_INGESTION",
|
|
138
|
+
"hideInChat": True,
|
|
139
|
+
},
|
|
140
|
+
metadata=_artifact_metadata(file),
|
|
141
|
+
preview_pdf_path=str(preview_path),
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return Content.model_validate(uploaded, by_alias=True, by_name=True)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
async def save_code_execution_artifact(
|
|
148
|
+
chat_service: ChatService,
|
|
149
|
+
file: AnnotationContainerFileCitation,
|
|
150
|
+
file_bytes: bytes,
|
|
151
|
+
*,
|
|
152
|
+
attach_office_preview: bool = False,
|
|
153
|
+
) -> Content:
|
|
154
|
+
"""Upload a code-interpreter artifact to the chat knowledge base.
|
|
155
|
+
|
|
156
|
+
``attach_office_preview`` is opt-in (default ``False``) so existing
|
|
157
|
+
Unique AI callers keep the historical bytes-only upload. When ``True``,
|
|
158
|
+
Word / PowerPoint / Excel files are converted to a sibling PDF and
|
|
159
|
+
uploaded with ``preview_pdf_path``. Conversion is best-effort: if
|
|
160
|
+
LibreOffice is missing or conversion fails, the original bytes are
|
|
161
|
+
uploaded instead. Upload failures are not retried via the bytes path,
|
|
162
|
+
because ``file_io.upload_file`` may already have created a content row.
|
|
163
|
+
"""
|
|
164
|
+
if attach_office_preview:
|
|
165
|
+
extension = office_extension(file.filename)
|
|
166
|
+
if extension is not None:
|
|
167
|
+
mime = _kb_safe_mime(OFFICE_PREVIEW_MIME_TYPES[extension])
|
|
168
|
+
content = await _upload_office_artifact_with_preview(
|
|
169
|
+
chat_service=chat_service,
|
|
170
|
+
file=file,
|
|
171
|
+
file_bytes=file_bytes,
|
|
172
|
+
mime=mime,
|
|
173
|
+
)
|
|
174
|
+
if content is not None:
|
|
175
|
+
return content
|
|
176
|
+
return await _upload_artifact_bytes(
|
|
177
|
+
chat_service=chat_service,
|
|
178
|
+
file=file,
|
|
179
|
+
file_bytes=file_bytes,
|
|
180
|
+
mime=mime,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
raw_mime = guess_type(file.filename)[0] or "text/plain"
|
|
184
|
+
mime = _kb_safe_mime(raw_mime)
|
|
185
|
+
|
|
186
|
+
if mime != raw_mime:
|
|
187
|
+
_LOGGER.info(
|
|
188
|
+
"MIME type '%s' is not supported by the Unique KB; "
|
|
189
|
+
"uploading '%s' as 'text/plain' so the file can be stored and downloaded.",
|
|
190
|
+
raw_mime,
|
|
191
|
+
file.filename,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return await _upload_artifact_bytes(
|
|
195
|
+
chat_service=chat_service,
|
|
196
|
+
file=file,
|
|
197
|
+
file_bytes=file_bytes,
|
|
198
|
+
mime=mime,
|
|
89
199
|
)
|
|
90
200
|
|
|
91
201
|
|
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/postprocessors/generated_files.py
CHANGED
|
@@ -117,6 +117,15 @@ class DisplayCodeInterpreterFilesPostProcessorConfig(BaseModel):
|
|
|
117
117
|
"Applies per SDK attempt. The OpenAI SDK default of 600s is too generous "
|
|
118
118
|
"for small generated files; a shorter timeout lets us retry sooner.",
|
|
119
119
|
)
|
|
120
|
+
attach_office_preview: SkipJsonSchema[bool] = Field(
|
|
121
|
+
default=False,
|
|
122
|
+
description=(
|
|
123
|
+
"When True, convert Word/PowerPoint/Excel artifacts to a sibling "
|
|
124
|
+
"PDF and attach it as the chat side-panel preview. Default False "
|
|
125
|
+
"keeps the historical bytes-only upload. Conversion is best-effort "
|
|
126
|
+
"and falls back to the original file."
|
|
127
|
+
),
|
|
128
|
+
)
|
|
120
129
|
|
|
121
130
|
|
|
122
131
|
class _ContentInfo(NamedTuple):
|
|
@@ -762,6 +771,7 @@ class DisplayCodeInterpreterFilesPostProcessor(
|
|
|
762
771
|
chat_service=self._chat_service,
|
|
763
772
|
file=container_file,
|
|
764
773
|
file_bytes=file_bytes,
|
|
774
|
+
attach_office_preview=self._config.attach_office_preview,
|
|
765
775
|
)
|
|
766
776
|
upload_ms = (time.monotonic() - upload_t0) * 1000
|
|
767
777
|
if upload_ms > 2000:
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""LibreOffice PDF previews for code-interpreter Office artifacts.
|
|
2
|
+
|
|
3
|
+
When the OpenAI code interpreter emits a Word, PowerPoint, or Excel file,
|
|
4
|
+
the chat UI can render a sibling PDF in the side panel far more reliably
|
|
5
|
+
than the Office binary. Conversion is best-effort: if ``soffice`` is
|
|
6
|
+
missing, times out, or rejects the file, callers upload the original
|
|
7
|
+
without a preview.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
import logging
|
|
14
|
+
import shutil
|
|
15
|
+
import subprocess
|
|
16
|
+
import tempfile
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
_SOFFICE_TIMEOUT_SECONDS = 180
|
|
20
|
+
|
|
21
|
+
# Canonical MIME types so slim containers without ``/etc/mime.types`` still
|
|
22
|
+
# upload Office files with a KB-accepted type instead of falling back to
|
|
23
|
+
# ``text/plain`` via ``mimetypes.guess_type``.
|
|
24
|
+
OFFICE_PREVIEW_MIME_TYPES: dict[str, str] = {
|
|
25
|
+
".doc": "application/msword",
|
|
26
|
+
".docx": (
|
|
27
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
28
|
+
),
|
|
29
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
30
|
+
".pptx": (
|
|
31
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
32
|
+
),
|
|
33
|
+
".xls": "application/vnd.ms-excel",
|
|
34
|
+
".xlsx": ("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def office_extension(filename: str) -> str | None:
|
|
39
|
+
"""Return the lowercased Office suffix, or ``None`` if not previewable."""
|
|
40
|
+
suffix = Path(filename).suffix.lower()
|
|
41
|
+
return suffix if suffix in OFFICE_PREVIEW_MIME_TYPES else None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _resolve_soffice_binary() -> str | None:
|
|
45
|
+
return shutil.which("soffice") or shutil.which("libreoffice")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _run_soffice(
|
|
49
|
+
soffice: str,
|
|
50
|
+
source: Path,
|
|
51
|
+
output_dir: Path,
|
|
52
|
+
logger: logging.Logger,
|
|
53
|
+
) -> None:
|
|
54
|
+
"""Invoke headless ``soffice`` to convert *source* into *output_dir*.
|
|
55
|
+
|
|
56
|
+
Uses an isolated ``UserInstallation`` profile so concurrent LibreOffice
|
|
57
|
+
invocations do not collide on ``$HOME/.config/libreoffice``.
|
|
58
|
+
"""
|
|
59
|
+
with tempfile.TemporaryDirectory(
|
|
60
|
+
prefix=".libreoffice-profile-",
|
|
61
|
+
dir=output_dir,
|
|
62
|
+
) as profile_dir:
|
|
63
|
+
proc = subprocess.run(
|
|
64
|
+
[
|
|
65
|
+
soffice,
|
|
66
|
+
f"-env:UserInstallation={Path(profile_dir).resolve().as_uri()}",
|
|
67
|
+
"--headless",
|
|
68
|
+
"--convert-to",
|
|
69
|
+
"pdf",
|
|
70
|
+
"--outdir",
|
|
71
|
+
str(output_dir),
|
|
72
|
+
str(source),
|
|
73
|
+
],
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
timeout=_SOFFICE_TIMEOUT_SECONDS,
|
|
77
|
+
)
|
|
78
|
+
if proc.returncode != 0:
|
|
79
|
+
logger.warning(
|
|
80
|
+
"office_preview: soffice exited %d for '%s' "
|
|
81
|
+
"(stdout=%d bytes, stderr=%d bytes)",
|
|
82
|
+
proc.returncode,
|
|
83
|
+
source.name,
|
|
84
|
+
len((proc.stdout or "").strip()),
|
|
85
|
+
len((proc.stderr or "").strip()),
|
|
86
|
+
)
|
|
87
|
+
raise subprocess.CalledProcessError(
|
|
88
|
+
proc.returncode, proc.args, proc.stdout, proc.stderr
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
async def convert_office_bytes_to_preview_pdf(
|
|
93
|
+
filename: str,
|
|
94
|
+
file_bytes: bytes,
|
|
95
|
+
tmp_dir: Path,
|
|
96
|
+
logger: logging.Logger,
|
|
97
|
+
) -> Path | None:
|
|
98
|
+
"""Write *file_bytes* into *tmp_dir* and convert to a sibling preview PDF.
|
|
99
|
+
|
|
100
|
+
The original file is written as ``tmp_dir / Path(filename).name`` so the
|
|
101
|
+
caller can pass that path to the SDK uploader. The preview is rendered
|
|
102
|
+
into ``tmp_dir / .previews / <stem>.pdf`` so it cannot collide with a
|
|
103
|
+
same-named PDF the interpreter also produced.
|
|
104
|
+
|
|
105
|
+
Returns the preview path, or ``None`` when conversion is unavailable or
|
|
106
|
+
fails. Never raises for expected conversion problems.
|
|
107
|
+
"""
|
|
108
|
+
safe_filename = Path(filename).name
|
|
109
|
+
source_path = tmp_dir / safe_filename
|
|
110
|
+
source_path.write_bytes(file_bytes)
|
|
111
|
+
|
|
112
|
+
soffice = _resolve_soffice_binary()
|
|
113
|
+
if soffice is None:
|
|
114
|
+
logger.warning(
|
|
115
|
+
"office_preview: soffice/libreoffice not found on PATH — "
|
|
116
|
+
"uploading '%s' without a PDF preview.",
|
|
117
|
+
safe_filename,
|
|
118
|
+
)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
previews_dir = tmp_dir / ".previews"
|
|
122
|
+
previews_dir.mkdir(exist_ok=True)
|
|
123
|
+
target = previews_dir / f"{source_path.stem}.pdf"
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
await asyncio.to_thread(
|
|
127
|
+
_run_soffice, soffice, source_path, previews_dir, logger
|
|
128
|
+
)
|
|
129
|
+
except subprocess.TimeoutExpired:
|
|
130
|
+
logger.warning(
|
|
131
|
+
"office_preview: soffice timed out after %ds for '%s' — "
|
|
132
|
+
"uploading original only",
|
|
133
|
+
_SOFFICE_TIMEOUT_SECONDS,
|
|
134
|
+
safe_filename,
|
|
135
|
+
)
|
|
136
|
+
return None
|
|
137
|
+
except subprocess.CalledProcessError:
|
|
138
|
+
return None
|
|
139
|
+
except Exception:
|
|
140
|
+
logger.exception(
|
|
141
|
+
"office_preview: unexpected error for '%s' — uploading original only",
|
|
142
|
+
safe_filename,
|
|
143
|
+
)
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
if target.is_file():
|
|
147
|
+
return target
|
|
148
|
+
|
|
149
|
+
logger.warning(
|
|
150
|
+
"office_preview: soffice returned 0 but '%s' is missing for '%s'",
|
|
151
|
+
target.name,
|
|
152
|
+
safe_filename,
|
|
153
|
+
)
|
|
154
|
+
return None
|
{unique_toolkit-2026.36.0.dev3.dist-info → unique_toolkit-2026.36.0.dev4.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: unique-toolkit
|
|
3
|
-
Version: 2026.36.0.
|
|
3
|
+
Version: 2026.36.0.dev4
|
|
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>
|
|
@@ -216,9 +216,10 @@ unique_toolkit/agentic/tools/openai_builtin/code_interpreter/builder/_memory.py,
|
|
|
216
216
|
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/builder/builder.py,sha256=QFvfJT7PE6XIf1mftURptTevOmYBp5Ln_eIsro5O7s8,6546
|
|
217
217
|
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/config.py,sha256=C4Vrj_xLgLB8AEUz-cp0JpXAIwYwz-VaSRh-xAG7PW8,2381
|
|
218
218
|
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/postprocessors/__init__.py,sha256=SzEiUqxIw8D4_51SvTt-8oTOYTGirYkmH4J6SgojzME,634
|
|
219
|
-
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/postprocessors/artifacts.py,sha256=
|
|
219
|
+
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/postprocessors/artifacts.py,sha256=0ZnEOh8aKH-oUPPXWR3yeVuf_IxKZSxg8GLtEcpG_fA,6832
|
|
220
220
|
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/postprocessors/code_display.py,sha256=WC__H2lhprchVVKsQzPeckhq-EcYg1ePYtrHIAoVzpQ,3165
|
|
221
|
-
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/postprocessors/generated_files.py,sha256=
|
|
221
|
+
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/postprocessors/generated_files.py,sha256=7jvYG13oYI00ohrvTnrqIJszkRBZVbTSjtcjEtlEEGQ,63610
|
|
222
|
+
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/postprocessors/office_preview.py,sha256=5DtJWkzJwEFsh2uyHjT5qU5G277qXaHun1JRCYVhZFE,4951
|
|
222
223
|
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/schemas.py,sha256=W7egCmYrPrzic0aXsu9wY5fvQ8y_cl4Tzq_cjVs0mZs,542
|
|
223
224
|
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/tool/__init__.py,sha256=r6yXmGTLJzXNq5laHOlre9veu--VbCQVlDRVm2QUGzI,326
|
|
224
225
|
unique_toolkit/agentic/tools/openai_builtin/code_interpreter/tool/config.py,sha256=sbLV-fYt_veBAdonqAHY9sm-eHxpjIZgc1KVbm03pZY,19042
|
|
@@ -420,6 +421,6 @@ unique_toolkit/short_term_memory/service.py,sha256=2JMBEBn6GoyK-a0xvdcWpPft-3C74
|
|
|
420
421
|
unique_toolkit/smart_rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
421
422
|
unique_toolkit/smart_rules/compile.py,sha256=vRe0aMJWuVIK-VsqomgxQImzke7MDmwgSxxJfYCa7zw,1386
|
|
422
423
|
unique_toolkit/test_utilities/events.py,sha256=FLrhMh0A_mQutAUjoD0FdMcVQ4Gq31r35OVGzRjvLro,6760
|
|
423
|
-
unique_toolkit-2026.36.0.
|
|
424
|
-
unique_toolkit-2026.36.0.
|
|
425
|
-
unique_toolkit-2026.36.0.
|
|
424
|
+
unique_toolkit-2026.36.0.dev4.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
425
|
+
unique_toolkit-2026.36.0.dev4.dist-info/METADATA,sha256=cFB1qZ_frQUp2w6_eMAL5auiBr8ELgaGhfLApCzNywI,7821
|
|
426
|
+
unique_toolkit-2026.36.0.dev4.dist-info/RECORD,,
|
|
File without changes
|