unique-sdk 2026.20.0.dev2__tar.gz → 2026.20.0.dev4__tar.gz

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.
Files changed (69) hide show
  1. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/PKG-INFO +1 -1
  2. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/pyproject.toml +1 -1
  3. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_content.py +15 -0
  4. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/utils/file_io.py +130 -1
  5. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/README.md +0 -0
  6. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/__init__.py +0 -0
  7. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/_api_requestor.py +0 -0
  8. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/_api_resource.py +0 -0
  9. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/_api_version.py +0 -0
  10. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/_error.py +0 -0
  11. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/_http_client.py +0 -0
  12. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/_list_object.py +0 -0
  13. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/_object_classes.py +0 -0
  14. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/_request_options.py +0 -0
  15. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/_unique_object.py +0 -0
  16. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/_unique_ql.py +0 -0
  17. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/_unique_response.py +0 -0
  18. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/_util.py +0 -0
  19. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/_version.py +0 -0
  20. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/_webhook.py +0 -0
  21. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/__init__.py +0 -0
  22. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_acronyms.py +0 -0
  23. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_agentic_table.py +0 -0
  24. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_benchmarking.py +0 -0
  25. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_chat_completion.py +0 -0
  26. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_elicitation.py +0 -0
  27. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_embedding.py +0 -0
  28. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_event.py +0 -0
  29. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_folder.py +0 -0
  30. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_group.py +0 -0
  31. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_integrated.py +0 -0
  32. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_llm_models.py +0 -0
  33. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_mcp.py +0 -0
  34. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_message.py +0 -0
  35. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_message_assessment.py +0 -0
  36. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_message_execution.py +0 -0
  37. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_message_log.py +0 -0
  38. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_message_tool.py +0 -0
  39. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  40. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_search.py +0 -0
  41. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_search_string.py +0 -0
  42. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  43. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_space.py +0 -0
  44. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/api_resources/_user.py +0 -0
  45. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/__init__.py +0 -0
  46. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/__main__.py +0 -0
  47. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/cli.py +0 -0
  48. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/commands/__init__.py +0 -0
  49. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/commands/elicitation.py +0 -0
  50. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/commands/files.py +0 -0
  51. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/commands/folders.py +0 -0
  52. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/commands/mcp.py +0 -0
  53. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/commands/navigation.py +0 -0
  54. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  55. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/commands/search.py +0 -0
  56. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/config.py +0 -0
  57. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/formatting.py +0 -0
  58. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/shell.py +0 -0
  59. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
  60. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +0 -0
  61. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
  62. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  63. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
  64. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/cli/state.py +0 -0
  65. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/utils/benchmarking_run.py +0 -0
  66. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/utils/chat_history.py +0 -0
  67. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/utils/chat_in_space.py +0 -0
  68. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/utils/sources.py +0 -0
  69. {unique_sdk-2026.20.0.dev2 → unique_sdk-2026.20.0.dev4}/unique_sdk/utils/token.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-sdk
3
- Version: 2026.20.0.dev2
3
+ Version: 2026.20.0.dev4
4
4
  Summary:
5
5
  Author: Martin Fadler, Konstantin Krauss, Andreas Hauri
6
6
  Author-email: Martin Fadler <martin.fadler@unique.ch>, Konstantin Krauss <konstantin@unique.ch>, Andreas Hauri <andreas@unique.ch>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique_sdk"
3
- version = "2026.20.0.dev2"
3
+ version = "2026.20.0.dev4"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -31,6 +31,16 @@ class Content(APIResource["Content"]):
31
31
  expiredAt: str | None
32
32
  appliedIngestionConfig: dict[str, Any] | None
33
33
  mimeType: str | None
34
+ # Optional preview PDF blob registered against this content. When the
35
+ # caller passes ``previewPdfFileName`` to :meth:`upsert` (or to
36
+ # :func:`unique_sdk.utils.file_io.upload_file` via ``preview_pdf_path``),
37
+ # the platform stores the filename here and returns
38
+ # ``pdfPreviewWriteUrl`` so the PDF bytes can be uploaded directly to
39
+ # blob storage. The chat side panel resolves this filename to the
40
+ # ``/v1/content/{id}/preview-file`` endpoint when rendering Office /
41
+ # other formats whose in-browser preview is unreliable.
42
+ previewPdfFileName: str | None
43
+ pdfPreviewWriteUrl: str | None
34
44
 
35
45
  class QueryMode(Enum):
36
46
  Default = "default"
@@ -155,6 +165,11 @@ class Content(APIResource["Content"]):
155
165
  sourceOwnerType: NotRequired[str | None]
156
166
  storeInternally: NotRequired[bool | None]
157
167
  fileUrl: NotRequired[str | None]
168
+ # Pass a blob filename to attach a PDF preview to the upserted
169
+ # content. The response then carries ``pdfPreviewWriteUrl`` —
170
+ # a SAS URL the caller PUTs the PDF bytes to. ``file_io.upload_file``
171
+ # exposes this as ``preview_pdf_path`` for a one-call flow.
172
+ previewPdfFileName: NotRequired[str | None]
158
173
 
159
174
  class UpdateParams(RequestOptions):
160
175
  contentId: NotRequired[str]
@@ -2,7 +2,7 @@ import asyncio
2
2
  import os
3
3
  import tempfile
4
4
  from pathlib import Path
5
- from typing import Any
5
+ from typing import Any, TypedDict
6
6
 
7
7
  import requests
8
8
 
@@ -10,6 +10,15 @@ import unique_sdk
10
10
  from unique_sdk.api_resources._content import Content
11
11
 
12
12
 
13
+ class _PreviewKwargs(TypedDict, total=False):
14
+ """Subset of ``Content.UpsertParams`` that we conditionally forward
15
+ to ``upsert``. Typing it as a ``total=False`` TypedDict lets us
16
+ expand ``**preview_kwargs`` without basedpyright assuming the dict
17
+ could collide with non-string params like ``headers``."""
18
+
19
+ previewPdfFileName: str
20
+
21
+
13
22
  # download readUrl a random directory in /tmp
14
23
  def download_file(url: str, filename: str):
15
24
  # Guard for callers without a type checker: fail fast with a clear error before reaching requests.
@@ -32,6 +41,38 @@ def download_file(url: str, filename: str):
32
41
  return file_path
33
42
 
34
43
 
44
+ _PREVIEW_PDF_MIME_TYPE = "application/pdf"
45
+
46
+
47
+ def _derive_preview_pdf_filename(displayed_filename: str) -> str:
48
+ """Return ``<stem>_preview.pdf`` for *displayed_filename*.
49
+
50
+ Keeps the preview blob filename predictable and collision-free with
51
+ the original (which has the user-visible extension), while still
52
+ making it obvious in storage which content the preview belongs to.
53
+ """
54
+ stem = displayed_filename.rsplit(".", 1)[0] or displayed_filename
55
+ return f"{stem}_preview.pdf"
56
+
57
+
58
+ def _put_preview_pdf(write_url: str, preview_pdf_path: str) -> None:
59
+ """PUT *preview_pdf_path* bytes to the SAS URL returned by upsert."""
60
+ with open(preview_pdf_path, "rb") as preview_file:
61
+ response = requests.put(
62
+ write_url,
63
+ data=preview_file,
64
+ headers={
65
+ "X-Ms-Blob-Content-Type": _PREVIEW_PDF_MIME_TYPE,
66
+ "X-Ms-Blob-Type": "BlockBlob",
67
+ },
68
+ )
69
+ if response.status_code >= 400:
70
+ raise RuntimeError(
71
+ f"Preview PDF upload failed with status {response.status_code}: "
72
+ f"{response.text[:500]}"
73
+ )
74
+
75
+
35
76
  def upload_file(
36
77
  userId,
37
78
  companyId,
@@ -43,12 +84,77 @@ def upload_file(
43
84
  chat_id=None,
44
85
  ingestion_config: Content.IngestionConfig | None = None,
45
86
  metadata: dict[str, Any] | None = None,
87
+ preview_pdf_path: str | None = None,
88
+ preview_pdf_filename: str | None = None,
46
89
  ):
90
+ """Upload *path_to_file* as a Unique :class:`Content`.
91
+
92
+ When ``preview_pdf_path`` is provided, the caller gets a single-call
93
+ Office → PDF preview flow: we attach a ``previewPdfFileName`` to the
94
+ upserted content (so the platform mints a ``pdfPreviewWriteUrl``),
95
+ PUT the original blob, then PUT the PDF preview blob. The chat side
96
+ panel will pick the preview up automatically when rendering
97
+ PowerPoint / Word / similar formats whose in-browser preview is
98
+ unreliable.
99
+
100
+ Args:
101
+ userId: Acting user id.
102
+ companyId: Tenant id.
103
+ path_to_file: Path to the original blob to upload.
104
+ displayed_filename: Human-readable filename. Used as the content
105
+ ``key`` and ``title``.
106
+ mime_type: MIME type of the original blob.
107
+ description: Optional description.
108
+ scope_or_unique_path: Either a scope id, a folder path, or
109
+ ``None`` (when uploading into a chat).
110
+ chat_id: Chat id when uploading a chat attachment.
111
+ ingestion_config: Ingestion options.
112
+ metadata: Free-form metadata.
113
+ preview_pdf_path: Optional path to a sibling PDF that should be
114
+ rendered as the side-panel preview for this content. When
115
+ set, the upsert registers ``previewPdfFileName`` on the row
116
+ and the PDF bytes are PUT to ``pdfPreviewWriteUrl`` after the
117
+ original upload.
118
+ preview_pdf_filename: Optional override for the blob filename
119
+ stored against ``previewPdfFileName``. Defaults to
120
+ ``<displayed_filename without extension>_preview.pdf`` so
121
+ the preview blob is uniquely identifiable in storage.
122
+ """
47
123
  # check that chatid or scope_or_unique_path is provided
48
124
  if not chat_id and not scope_or_unique_path:
49
125
  raise ValueError("chat_id or scope_or_unique_path must be provided")
50
126
 
127
+ if preview_pdf_path is not None and not os.path.isfile(preview_pdf_path):
128
+ raise ValueError(
129
+ f"preview_pdf_path does not point to a readable file: {preview_pdf_path}"
130
+ )
131
+
132
+ # A filename without a path would register a phantom preview on the
133
+ # Content row (the upsert mints a pdfPreviewWriteUrl, but we never
134
+ # PUT any bytes), leaving the side panel pointing at a nonexistent
135
+ # blob. Treat it as a programmer mistake and fail fast.
136
+ if preview_pdf_filename is not None and preview_pdf_path is None:
137
+ raise ValueError(
138
+ "preview_pdf_filename was provided without preview_pdf_path; "
139
+ "pass the PDF path so the preview blob actually gets uploaded."
140
+ )
141
+
51
142
  size = os.path.getsize(path_to_file)
143
+ resolved_preview_filename = (
144
+ (preview_pdf_filename or _derive_preview_pdf_filename(displayed_filename))
145
+ if preview_pdf_path is not None
146
+ else None
147
+ )
148
+ # Only forward ``previewPdfFileName`` when we actually have one.
149
+ # Sending ``None`` would serialize as ``"previewPdfFileName": null``,
150
+ # which the server treats as a clearing operation and would erase
151
+ # an existing preview on a re-upload that doesn't ship one.
152
+ preview_kwargs: _PreviewKwargs = (
153
+ {"previewPdfFileName": resolved_preview_filename}
154
+ if resolved_preview_filename is not None
155
+ else {}
156
+ )
157
+
52
158
  createdContent = Content.upsert(
53
159
  user_id=userId,
54
160
  company_id=companyId,
@@ -62,6 +168,7 @@ def upload_file(
62
168
  },
63
169
  scopeId=scope_or_unique_path,
64
170
  chatId=chat_id,
171
+ **preview_kwargs,
65
172
  )
66
173
 
67
174
  uploadUrl = createdContent.writeUrl
@@ -79,6 +186,26 @@ def upload_file(
79
186
  },
80
187
  )
81
188
 
189
+ # Attach the optional preview-PDF blob in the same call. We do this
190
+ # *before* the second upsert so the byteSize patch and the preview
191
+ # don't race; if the SAS URL is missing we surface a clear error
192
+ # (the server only mints it when previewPdfFileName is set, so a
193
+ # missing URL signals a server-side regression).
194
+ if preview_pdf_path is not None:
195
+ # ``getattr`` so an older gateway that omits the field falls
196
+ # through to the RuntimeError below instead of raising the
197
+ # opaque ``AttributeError`` that ``UniqueObject.__getattr__``
198
+ # produces on a missing key.
199
+ preview_write_url = getattr(createdContent, "pdfPreviewWriteUrl", None)
200
+ if not preview_write_url:
201
+ raise RuntimeError(
202
+ "preview_pdf_path was provided but the upsert response carries "
203
+ "no pdfPreviewWriteUrl — refusing to silently drop the preview. "
204
+ "Verify the API gateway and node-ingestion expose "
205
+ "previewPdfFileName on the upsert mutation."
206
+ )
207
+ _put_preview_pdf(preview_write_url, preview_pdf_path)
208
+
82
209
  if chat_id:
83
210
  unique_sdk.Content.upsert(
84
211
  user_id=userId,
@@ -94,6 +221,7 @@ def upload_file(
94
221
  },
95
222
  fileUrl=createdContent.readUrl,
96
223
  chatId=chat_id,
224
+ **preview_kwargs,
97
225
  )
98
226
  else:
99
227
  unique_sdk.Content.upsert(
@@ -110,6 +238,7 @@ def upload_file(
110
238
  },
111
239
  fileUrl=createdContent.readUrl,
112
240
  scopeId=scope_or_unique_path,
241
+ **preview_kwargs,
113
242
  )
114
243
 
115
244
  return createdContent