unique-sdk 2026.26.0.dev8__tar.gz → 2026.26.0.dev10__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.
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/PKG-INFO +1 -1
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/pyproject.toml +1 -1
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_content.py +158 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/cli.py +128 -15
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/commands/files.py +136 -10
- unique_sdk-2026.26.0.dev10/unique_sdk/cli/commands/read.py +176 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/shell.py +188 -32
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +81 -3
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/utils/file_io.py +7 -0
- unique_sdk-2026.26.0.dev8/unique_sdk/cli/commands/read.py +0 -93
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/README.md +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/__init__.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/_api_requestor.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/_api_resource.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/_api_version.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/_error.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/_http_client.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/_list_object.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/_object_classes.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/_request_options.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/_unique_object.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/_unique_ql.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/_unique_response.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/_util.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/_version.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/_webhook.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/__init__.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_acronyms.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_agentic_table.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_analytics_order.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_benchmarking.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_briefing.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_chat_completion.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_dynamic_frontend.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_elicitation.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_embedding.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_event.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_folder.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_group.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_integrated.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_llm_models.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_mcp.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_message.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_message_assessment.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_message_execution.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_message_log.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_message_tool.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_module.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_scheduled_task.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_search.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_search_string.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_short_term_memory.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_space.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_user.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_web_search.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/__init__.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/__main__.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/commands/__init__.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/commands/cite_file.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/commands/dynamic_frontend.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/commands/elicitation.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/commands/folders.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/commands/mcp.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/commands/navigation.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/commands/search.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/commands/subagent.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/commands/web_search.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/commands/web_search_config.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/config.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/formatting.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/cli/state.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/utils/analytics_order_run.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/utils/benchmarking_run.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/utils/chat_history.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/utils/chat_in_space.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/utils/sources.py +0 -0
- {unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/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.26.0.
|
|
3
|
+
Version: 2026.26.0.dev10
|
|
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>
|
{unique_sdk-2026.26.0.dev8 → unique_sdk-2026.26.0.dev10}/unique_sdk/api_resources/_content.py
RENAMED
|
@@ -170,6 +170,50 @@ class Content(APIResource["Content"]):
|
|
|
170
170
|
# a SAS URL the caller PUTs the PDF bytes to. ``file_io.upload_file``
|
|
171
171
|
# exposes this as ``preview_pdf_path`` for a one-call flow.
|
|
172
172
|
previewPdfFileName: NotRequired[str | None]
|
|
173
|
+
# When true, the platform archives previous blobs for this
|
|
174
|
+
# content and makes them restorable through the version APIs.
|
|
175
|
+
versioningEnabled: NotRequired[bool | None]
|
|
176
|
+
|
|
177
|
+
class VersionsParams(RequestOptions):
|
|
178
|
+
contentId: str
|
|
179
|
+
skip: NotRequired[int | None]
|
|
180
|
+
take: NotRequired[int | None]
|
|
181
|
+
|
|
182
|
+
class ContentVersion(TypedDict):
|
|
183
|
+
id: str
|
|
184
|
+
contentId: str
|
|
185
|
+
versionNumber: int
|
|
186
|
+
reason: str
|
|
187
|
+
blobObjectKey: str
|
|
188
|
+
key: str
|
|
189
|
+
title: str | None
|
|
190
|
+
description: str | None
|
|
191
|
+
url: str | None
|
|
192
|
+
byteSize: int
|
|
193
|
+
mimeType: str
|
|
194
|
+
ownerType: str
|
|
195
|
+
ownerId: str
|
|
196
|
+
contentHash: str | None
|
|
197
|
+
archivedAt: str
|
|
198
|
+
archivedBy: str | None
|
|
199
|
+
originalCreatedAt: str
|
|
200
|
+
originalCreatedBy: str | None
|
|
201
|
+
createdBy: str | None
|
|
202
|
+
|
|
203
|
+
class PaginatedContentVersions(TypedDict):
|
|
204
|
+
data: list["Content.ContentVersion"]
|
|
205
|
+
object: str
|
|
206
|
+
|
|
207
|
+
class VersionDownloadUrlParams(RequestOptions):
|
|
208
|
+
contentVersionId: str
|
|
209
|
+
|
|
210
|
+
class ContentVersionDownloadUrl(TypedDict):
|
|
211
|
+
id: str
|
|
212
|
+
object: str
|
|
213
|
+
url: str
|
|
214
|
+
|
|
215
|
+
class RestoreVersionParams(RequestOptions):
|
|
216
|
+
contentVersionId: str
|
|
173
217
|
|
|
174
218
|
class UpdateParams(RequestOptions):
|
|
175
219
|
contentId: NotRequired[str]
|
|
@@ -467,6 +511,120 @@ class Content(APIResource["Content"]):
|
|
|
467
511
|
),
|
|
468
512
|
)
|
|
469
513
|
|
|
514
|
+
@classmethod
|
|
515
|
+
def versions(
|
|
516
|
+
cls,
|
|
517
|
+
user_id: str,
|
|
518
|
+
company_id: str,
|
|
519
|
+
**params: Unpack["Content.VersionsParams"],
|
|
520
|
+
) -> "Content.PaginatedContentVersions":
|
|
521
|
+
content_id = params.pop("contentId")
|
|
522
|
+
return cast(
|
|
523
|
+
Content.PaginatedContentVersions,
|
|
524
|
+
cls._static_request(
|
|
525
|
+
"get",
|
|
526
|
+
f"/content/{content_id}/versions",
|
|
527
|
+
user_id,
|
|
528
|
+
company_id,
|
|
529
|
+
params=params,
|
|
530
|
+
),
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
@classmethod
|
|
534
|
+
async def versions_async(
|
|
535
|
+
cls,
|
|
536
|
+
user_id: str,
|
|
537
|
+
company_id: str,
|
|
538
|
+
**params: Unpack["Content.VersionsParams"],
|
|
539
|
+
) -> "Content.PaginatedContentVersions":
|
|
540
|
+
content_id = params.pop("contentId")
|
|
541
|
+
return cast(
|
|
542
|
+
Content.PaginatedContentVersions,
|
|
543
|
+
await cls._static_request_async(
|
|
544
|
+
"get",
|
|
545
|
+
f"/content/{content_id}/versions",
|
|
546
|
+
user_id,
|
|
547
|
+
company_id,
|
|
548
|
+
params=params,
|
|
549
|
+
),
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
@classmethod
|
|
553
|
+
def version_download_url(
|
|
554
|
+
cls,
|
|
555
|
+
user_id: str,
|
|
556
|
+
company_id: str,
|
|
557
|
+
**params: Unpack["Content.VersionDownloadUrlParams"],
|
|
558
|
+
) -> "Content.ContentVersionDownloadUrl":
|
|
559
|
+
content_version_id = params.pop("contentVersionId")
|
|
560
|
+
return cast(
|
|
561
|
+
Content.ContentVersionDownloadUrl,
|
|
562
|
+
cls._static_request(
|
|
563
|
+
"get",
|
|
564
|
+
f"/content/versions/{content_version_id}/download-url",
|
|
565
|
+
user_id,
|
|
566
|
+
company_id,
|
|
567
|
+
params=params,
|
|
568
|
+
),
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
@classmethod
|
|
572
|
+
async def version_download_url_async(
|
|
573
|
+
cls,
|
|
574
|
+
user_id: str,
|
|
575
|
+
company_id: str,
|
|
576
|
+
**params: Unpack["Content.VersionDownloadUrlParams"],
|
|
577
|
+
) -> "Content.ContentVersionDownloadUrl":
|
|
578
|
+
content_version_id = params.pop("contentVersionId")
|
|
579
|
+
return cast(
|
|
580
|
+
Content.ContentVersionDownloadUrl,
|
|
581
|
+
await cls._static_request_async(
|
|
582
|
+
"get",
|
|
583
|
+
f"/content/versions/{content_version_id}/download-url",
|
|
584
|
+
user_id,
|
|
585
|
+
company_id,
|
|
586
|
+
params=params,
|
|
587
|
+
),
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
@classmethod
|
|
591
|
+
def restore_version(
|
|
592
|
+
cls,
|
|
593
|
+
user_id: str,
|
|
594
|
+
company_id: str,
|
|
595
|
+
**params: Unpack["Content.RestoreVersionParams"],
|
|
596
|
+
) -> "Content.ContentInfo":
|
|
597
|
+
content_version_id = params.pop("contentVersionId")
|
|
598
|
+
return cast(
|
|
599
|
+
Content.ContentInfo,
|
|
600
|
+
cls._static_request(
|
|
601
|
+
"post",
|
|
602
|
+
f"/content/versions/{content_version_id}/restore",
|
|
603
|
+
user_id,
|
|
604
|
+
company_id,
|
|
605
|
+
params=params,
|
|
606
|
+
),
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
@classmethod
|
|
610
|
+
async def restore_version_async(
|
|
611
|
+
cls,
|
|
612
|
+
user_id: str,
|
|
613
|
+
company_id: str,
|
|
614
|
+
**params: Unpack["Content.RestoreVersionParams"],
|
|
615
|
+
) -> "Content.ContentInfo":
|
|
616
|
+
content_version_id = params.pop("contentVersionId")
|
|
617
|
+
return cast(
|
|
618
|
+
Content.ContentInfo,
|
|
619
|
+
await cls._static_request_async(
|
|
620
|
+
"post",
|
|
621
|
+
f"/content/versions/{content_version_id}/restore",
|
|
622
|
+
user_id,
|
|
623
|
+
company_id,
|
|
624
|
+
params=params,
|
|
625
|
+
),
|
|
626
|
+
)
|
|
627
|
+
|
|
470
628
|
@classmethod
|
|
471
629
|
def ingest_magic_table_sheets(
|
|
472
630
|
cls,
|
|
@@ -20,7 +20,14 @@ from unique_sdk.cli.commands.elicitation import (
|
|
|
20
20
|
cmd_elicit_respond,
|
|
21
21
|
cmd_elicit_wait,
|
|
22
22
|
)
|
|
23
|
-
from unique_sdk.cli.commands.files import
|
|
23
|
+
from unique_sdk.cli.commands.files import (
|
|
24
|
+
cmd_download,
|
|
25
|
+
cmd_mv_file,
|
|
26
|
+
cmd_restore_version,
|
|
27
|
+
cmd_rm,
|
|
28
|
+
cmd_upload,
|
|
29
|
+
cmd_versions,
|
|
30
|
+
)
|
|
24
31
|
from unique_sdk.cli.commands.folders import cmd_mkdir, cmd_mvdir, cmd_rmdir
|
|
25
32
|
from unique_sdk.cli.commands.mcp import cmd_mcp
|
|
26
33
|
from unique_sdk.cli.commands.navigation import cmd_cd, cmd_ls, cmd_pwd
|
|
@@ -91,6 +98,7 @@ Path formats accepted by all commands:
|
|
|
91
98
|
\b
|
|
92
99
|
File identifiers:
|
|
93
100
|
report.pdf File name (matched in current directory)
|
|
101
|
+
/Reports/report.pdf File path (absolute or relative)
|
|
94
102
|
cont_abc123 Content ID (used directly)
|
|
95
103
|
|
|
96
104
|
\b
|
|
@@ -99,8 +107,10 @@ Examples:
|
|
|
99
107
|
unique-cli ls List root folders
|
|
100
108
|
unique-cli ls /Reports List a specific folder
|
|
101
109
|
unique-cli search "revenue" -l 50 Search with custom limit
|
|
102
|
-
unique-cli upload ./file.pdf Upload to current folder
|
|
110
|
+
unique-cli upload ./file.pdf Upload versioned to current folder
|
|
103
111
|
unique-cli download cont_abc123 Download by content ID
|
|
112
|
+
unique-cli versions cont_abc123 List archived file versions
|
|
113
|
+
unique-cli restore-version cver_1 Restore a file from a version
|
|
104
114
|
unique-cli elicit ask "Which?" Ask the user a question synchronously
|
|
105
115
|
unique-cli subagent Legal "Review" Invoke a connected space/subagent
|
|
106
116
|
unique-cli web-search search "x" Search the web via the public API
|
|
@@ -261,12 +271,13 @@ def mvdir(ctx: click.Context, old_name: str, new_name: str) -> None:
|
|
|
261
271
|
@click.argument("destination", required=False, default=None)
|
|
262
272
|
@click.pass_context
|
|
263
273
|
def upload(ctx: click.Context, local_path: str, destination: str | None) -> None:
|
|
264
|
-
"""Upload a local file (works like Linux cp).
|
|
274
|
+
"""Upload a local file with versioning enabled (works like Linux cp).
|
|
265
275
|
|
|
266
276
|
\b
|
|
267
|
-
Uploads LOCAL_PATH to the Unique platform
|
|
268
|
-
the target in cp -- it can be a
|
|
269
|
-
a combination of both. MIME type is
|
|
277
|
+
Uploads LOCAL_PATH to the Unique platform with immutable versioning
|
|
278
|
+
enabled. DESTINATION works like the target in cp -- it can be a
|
|
279
|
+
folder path, a new filename, or a combination of both. MIME type is
|
|
280
|
+
auto-detected.
|
|
270
281
|
|
|
271
282
|
\b
|
|
272
283
|
Destination formats:
|
|
@@ -288,6 +299,48 @@ def upload(ctx: click.Context, local_path: str, destination: str | None) -> None
|
|
|
288
299
|
click.echo(cmd_upload(LazyState.get(ctx), local_path, destination))
|
|
289
300
|
|
|
290
301
|
|
|
302
|
+
@main.command()
|
|
303
|
+
@click.argument("name_or_id")
|
|
304
|
+
@click.option("--skip", type=int, default=None, help="Number of versions to skip.")
|
|
305
|
+
@click.option("--take", type=int, default=None, help="Number of versions to return.")
|
|
306
|
+
@click.pass_context
|
|
307
|
+
def versions(
|
|
308
|
+
ctx: click.Context,
|
|
309
|
+
name_or_id: str,
|
|
310
|
+
skip: int | None,
|
|
311
|
+
take: int | None,
|
|
312
|
+
) -> None:
|
|
313
|
+
"""List archived versions for a file.
|
|
314
|
+
|
|
315
|
+
\b
|
|
316
|
+
NAME_OR_ID is a file path, a file name matched in the current
|
|
317
|
+
directory, or a content ID (cont_...) which is resolved directly.
|
|
318
|
+
|
|
319
|
+
\b
|
|
320
|
+
Examples:
|
|
321
|
+
unique-cli versions report.pdf
|
|
322
|
+
unique-cli versions /Reports/Q1/report.pdf
|
|
323
|
+
unique-cli versions cont_abc123 --take 10
|
|
324
|
+
"""
|
|
325
|
+
click.echo(cmd_versions(LazyState.get(ctx), name_or_id, skip=skip, take=take))
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
@main.command(name="restore-version")
|
|
329
|
+
@click.argument("content_version_id")
|
|
330
|
+
@click.pass_context
|
|
331
|
+
def restore_version(ctx: click.Context, content_version_id: str) -> None:
|
|
332
|
+
"""Restore a file from a content version ID.
|
|
333
|
+
|
|
334
|
+
\b
|
|
335
|
+
CONTENT_VERSION_ID is returned by `unique-cli versions`.
|
|
336
|
+
|
|
337
|
+
\b
|
|
338
|
+
Examples:
|
|
339
|
+
unique-cli restore-version cver_abc123
|
|
340
|
+
"""
|
|
341
|
+
click.echo(cmd_restore_version(LazyState.get(ctx), content_version_id))
|
|
342
|
+
|
|
343
|
+
|
|
291
344
|
@main.command()
|
|
292
345
|
@click.argument("name_or_id")
|
|
293
346
|
@click.argument("local_dest", required=False, default=None)
|
|
@@ -296,8 +349,8 @@ def download(ctx: click.Context, name_or_id: str, local_dest: str | None) -> Non
|
|
|
296
349
|
"""Download a file to your local machine.
|
|
297
350
|
|
|
298
351
|
\b
|
|
299
|
-
NAME_OR_ID is a file name
|
|
300
|
-
a content ID (cont_...) which is resolved directly.
|
|
352
|
+
NAME_OR_ID is a file path, a file name matched in the current
|
|
353
|
+
directory, or a content ID (cont_...) which is resolved directly.
|
|
301
354
|
|
|
302
355
|
\b
|
|
303
356
|
LOCAL_DEST is an optional path (directory or file) to save to.
|
|
@@ -306,6 +359,7 @@ def download(ctx: click.Context, name_or_id: str, local_dest: str | None) -> Non
|
|
|
306
359
|
\b
|
|
307
360
|
Examples:
|
|
308
361
|
unique-cli download annual.pdf
|
|
362
|
+
unique-cli download /Reports/Q1/annual.pdf
|
|
309
363
|
unique-cli download annual.pdf ./downloads/
|
|
310
364
|
unique-cli download cont_abc123 ~/Desktop/
|
|
311
365
|
"""
|
|
@@ -331,10 +385,12 @@ def cite(
|
|
|
331
385
|
\b
|
|
332
386
|
Registers [filesourceN] markers for pages you referenced in your answer.
|
|
333
387
|
Does NOT read or extract the file — use your own tools for that.
|
|
388
|
+
NAME_OR_ID can be a file path, current-directory file name, or content ID.
|
|
334
389
|
|
|
335
390
|
\b
|
|
336
391
|
Examples:
|
|
337
392
|
unique-cli cite report.pdf --pages 3,5,7
|
|
393
|
+
unique-cli cite /Reports/Q1/report.pdf --pages 3,5,7
|
|
338
394
|
unique-cli cite cont_abc123 --pages 1-4
|
|
339
395
|
"""
|
|
340
396
|
click.echo(cmd_cite_file(LazyState.get(ctx), name_or_id, pages))
|
|
@@ -342,9 +398,41 @@ def cite(
|
|
|
342
398
|
|
|
343
399
|
@main.command(name="read")
|
|
344
400
|
@click.argument("cont_id")
|
|
401
|
+
@click.option(
|
|
402
|
+
"--page",
|
|
403
|
+
"-p",
|
|
404
|
+
type=int,
|
|
405
|
+
default=None,
|
|
406
|
+
help="Read a single page (shorthand for --from-page N --to-page N).",
|
|
407
|
+
)
|
|
408
|
+
@click.option(
|
|
409
|
+
"--from-page",
|
|
410
|
+
type=int,
|
|
411
|
+
default=None,
|
|
412
|
+
help="First page to include (inclusive).",
|
|
413
|
+
)
|
|
414
|
+
@click.option(
|
|
415
|
+
"--to-page",
|
|
416
|
+
type=int,
|
|
417
|
+
default=None,
|
|
418
|
+
help="Last page to include (inclusive).",
|
|
419
|
+
)
|
|
420
|
+
@click.option(
|
|
421
|
+
"--max-chars",
|
|
422
|
+
type=int,
|
|
423
|
+
default=None,
|
|
424
|
+
help="Truncate the printed text to at most N characters.",
|
|
425
|
+
)
|
|
345
426
|
@click.pass_context
|
|
346
|
-
def read_cmd(
|
|
347
|
-
|
|
427
|
+
def read_cmd(
|
|
428
|
+
ctx: click.Context,
|
|
429
|
+
cont_id: str,
|
|
430
|
+
page: int | None,
|
|
431
|
+
from_page: int | None,
|
|
432
|
+
to_page: int | None,
|
|
433
|
+
max_chars: int | None,
|
|
434
|
+
) -> None:
|
|
435
|
+
"""Read indexed text chunks for a known content ID.
|
|
348
436
|
|
|
349
437
|
\b
|
|
350
438
|
CONT_ID must be a content ID (cont_...) obtained from a prior `ls` or
|
|
@@ -355,11 +443,34 @@ def read_cmd(ctx: click.Context, cont_id: str) -> None:
|
|
|
355
443
|
Use `search` when you need to find documents by topic or keyword.
|
|
356
444
|
Use `read` when you already know the content ID and want the full text.
|
|
357
445
|
|
|
446
|
+
\b
|
|
447
|
+
Restrict to a page range with --page (single page) or --from-page/--to-page.
|
|
448
|
+
A chunk spanning pages 2-4 is returned for any overlapping request; files
|
|
449
|
+
without page numbers (e.g. plain text/markdown) are returned only without a
|
|
450
|
+
page range.
|
|
451
|
+
|
|
358
452
|
\b
|
|
359
453
|
Examples:
|
|
360
454
|
unique-cli read cont_abc123
|
|
455
|
+
unique-cli read cont_abc123 --page 12
|
|
456
|
+
unique-cli read cont_abc123 --from-page 5 --to-page 9
|
|
457
|
+
unique-cli read cont_abc123 --to-page 3 --max-chars 8000
|
|
361
458
|
"""
|
|
362
|
-
|
|
459
|
+
if page is not None and (from_page is not None or to_page is not None):
|
|
460
|
+
click.echo(
|
|
461
|
+
"read: use either --page or --from-page/--to-page, not both", err=True
|
|
462
|
+
)
|
|
463
|
+
raise SystemExit(1)
|
|
464
|
+
if page is not None:
|
|
465
|
+
from_page = page
|
|
466
|
+
to_page = page
|
|
467
|
+
output = cmd_read(
|
|
468
|
+
LazyState.get(ctx),
|
|
469
|
+
cont_id,
|
|
470
|
+
from_page=from_page,
|
|
471
|
+
to_page=to_page,
|
|
472
|
+
max_chars=max_chars,
|
|
473
|
+
)
|
|
363
474
|
if _is_read_error_output(output):
|
|
364
475
|
click.echo(output, err=True)
|
|
365
476
|
raise SystemExit(1)
|
|
@@ -442,15 +553,16 @@ def dynamic_frontend_list(ctx: click.Context, output_json: bool) -> None:
|
|
|
442
553
|
@click.argument("name_or_id")
|
|
443
554
|
@click.pass_context
|
|
444
555
|
def rm(ctx: click.Context, name_or_id: str) -> None:
|
|
445
|
-
"""Delete a file by name or content ID.
|
|
556
|
+
"""Delete a file by path, name, or content ID.
|
|
446
557
|
|
|
447
558
|
\b
|
|
448
|
-
NAME_OR_ID is a file name
|
|
449
|
-
a content ID (cont_...).
|
|
559
|
+
NAME_OR_ID is a file path, a file name matched in the current
|
|
560
|
+
directory, or a content ID (cont_...).
|
|
450
561
|
|
|
451
562
|
\b
|
|
452
563
|
Examples:
|
|
453
564
|
unique-cli rm report.pdf
|
|
565
|
+
unique-cli rm /Reports/Q1/report.pdf
|
|
454
566
|
unique-cli rm cont_abc123
|
|
455
567
|
"""
|
|
456
568
|
click.echo(cmd_rm(LazyState.get(ctx), name_or_id))
|
|
@@ -465,11 +577,12 @@ def mv(ctx: click.Context, old_name: str, new_name: str) -> None:
|
|
|
465
577
|
|
|
466
578
|
\b
|
|
467
579
|
Changes the file's display title without changing its content ID
|
|
468
|
-
or location. OLD_NAME can be a file name or content ID.
|
|
580
|
+
or location. OLD_NAME can be a file path, file name, or content ID.
|
|
469
581
|
|
|
470
582
|
\b
|
|
471
583
|
Examples:
|
|
472
584
|
unique-cli mv annual.pdf annual-2025.pdf
|
|
585
|
+
unique-cli mv /Reports/Q1/annual.pdf annual-2025.pdf
|
|
473
586
|
unique-cli mv cont_abc123 "New Title.pdf"
|
|
474
587
|
"""
|
|
475
588
|
click.echo(cmd_mv_file(LazyState.get(ctx), old_name, new_name))
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import mimetypes
|
|
6
6
|
import shutil
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Any
|
|
9
10
|
|
|
@@ -13,32 +14,113 @@ from unique_sdk.cli.state import ShellState
|
|
|
13
14
|
from unique_sdk.utils.file_io import download_content, upload_file
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
def _normalize_unique_file_path(cwd: str, path: str) -> str:
|
|
18
|
+
"""Normalize a Unique file path without allowing traversal above root."""
|
|
19
|
+
raw_parts = (
|
|
20
|
+
path.split("/") if path.startswith("/") else [*cwd.split("/"), *path.split("/")]
|
|
21
|
+
)
|
|
22
|
+
parts: list[str] = []
|
|
23
|
+
for part in raw_parts:
|
|
24
|
+
if part in ("", "."):
|
|
25
|
+
continue
|
|
26
|
+
if part == "..":
|
|
27
|
+
if not parts:
|
|
28
|
+
raise ValueError(f"File path escapes root: {path}")
|
|
29
|
+
parts.pop()
|
|
30
|
+
continue
|
|
31
|
+
parts.append(part)
|
|
32
|
+
return "/" + "/".join(parts)
|
|
33
|
+
|
|
34
|
+
|
|
16
35
|
def _resolve_content_id(state: ShellState, name_or_id: str) -> tuple[str, str]:
|
|
17
36
|
"""Resolve a file name or content ID to (content_id, display_name).
|
|
18
37
|
|
|
19
|
-
Accepts
|
|
38
|
+
Accepts a content ID (cont_...), a file name in the current folder,
|
|
39
|
+
or an absolute/relative Unique file path.
|
|
20
40
|
"""
|
|
21
41
|
if name_or_id.startswith("cont_"):
|
|
42
|
+
if not state.is_content_within_workspace(name_or_id):
|
|
43
|
+
raise ValueError("permission denied (outside workspace scope)")
|
|
22
44
|
return name_or_id, name_or_id
|
|
23
45
|
|
|
46
|
+
lookup_name = name_or_id
|
|
24
47
|
scope_id = state.scope_id
|
|
48
|
+
if "/" in name_or_id:
|
|
49
|
+
unique_path = _normalize_unique_file_path(state.cwd, name_or_id)
|
|
50
|
+
folder_path, lookup_name = unique_path.rsplit("/", 1)
|
|
51
|
+
if not lookup_name:
|
|
52
|
+
raise ValueError(f"File path must include a file name: {name_or_id}")
|
|
53
|
+
if not folder_path:
|
|
54
|
+
folder_path = "/"
|
|
55
|
+
if not state.is_folder_target_within_workspace(folder_path):
|
|
56
|
+
raise ValueError("permission denied (outside workspace scope)")
|
|
57
|
+
|
|
58
|
+
info = unique_sdk.Folder.get_info(
|
|
59
|
+
user_id=state.config.user_id,
|
|
60
|
+
company_id=state.config.company_id,
|
|
61
|
+
folderPath=folder_path,
|
|
62
|
+
)
|
|
63
|
+
scope_id = info.get("id")
|
|
64
|
+
if not scope_id:
|
|
65
|
+
raise ValueError(f"folder not found: {folder_path}")
|
|
66
|
+
elif not state.is_within_workspace():
|
|
67
|
+
raise ValueError("permission denied (outside workspace scope)")
|
|
68
|
+
|
|
25
69
|
params: dict[str, Any] = {}
|
|
26
70
|
if scope_id:
|
|
27
71
|
params["parentId"] = scope_id
|
|
28
72
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
73
|
+
take = 100
|
|
74
|
+
skip = 0
|
|
75
|
+
while True:
|
|
76
|
+
result = unique_sdk.Content.get_infos(
|
|
77
|
+
user_id=state.config.user_id,
|
|
78
|
+
company_id=state.config.company_id,
|
|
79
|
+
skip=skip,
|
|
80
|
+
take=take,
|
|
81
|
+
**params,
|
|
82
|
+
)
|
|
83
|
+
content_infos = result.get("contentInfos", [])
|
|
84
|
+
if not content_infos:
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
for info in content_infos:
|
|
88
|
+
title = info.get("title") or ""
|
|
89
|
+
key = info.get("key") or ""
|
|
90
|
+
if lookup_name in {title, key}:
|
|
91
|
+
return info["id"], title or key
|
|
92
|
+
|
|
93
|
+
skip += len(content_infos)
|
|
38
94
|
|
|
39
95
|
raise ValueError(f"File not found: {name_or_id}")
|
|
40
96
|
|
|
41
97
|
|
|
98
|
+
def _format_version_value(value: Any) -> str:
|
|
99
|
+
if value is None:
|
|
100
|
+
return ""
|
|
101
|
+
return str(value)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _format_content_versions(versions: Sequence[Mapping[str, Any]]) -> str:
|
|
105
|
+
if not versions:
|
|
106
|
+
return "No versions found."
|
|
107
|
+
|
|
108
|
+
lines = ["VERSION VERSION_ID ARCHIVED_AT REASON TITLE"]
|
|
109
|
+
for version in versions:
|
|
110
|
+
lines.append(
|
|
111
|
+
" ".join(
|
|
112
|
+
[
|
|
113
|
+
_format_version_value(version.get("versionNumber")),
|
|
114
|
+
_format_version_value(version.get("id")),
|
|
115
|
+
_format_version_value(version.get("archivedAt")),
|
|
116
|
+
_format_version_value(version.get("reason")),
|
|
117
|
+
_format_version_value(version.get("title") or version.get("key")),
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
return "\n".join(lines)
|
|
122
|
+
|
|
123
|
+
|
|
42
124
|
def _resolve_upload_destination(
|
|
43
125
|
state: ShellState,
|
|
44
126
|
local_filename: str,
|
|
@@ -144,6 +226,7 @@ def cmd_upload(
|
|
|
144
226
|
displayed_filename=display_name,
|
|
145
227
|
mime_type=mime_type,
|
|
146
228
|
scope_or_unique_path=scope_id,
|
|
229
|
+
versioning_enabled=True,
|
|
147
230
|
)
|
|
148
231
|
|
|
149
232
|
content_id = result.id if hasattr(result, "id") else "?"
|
|
@@ -158,6 +241,49 @@ def cmd_upload(
|
|
|
158
241
|
return f"upload: {e}"
|
|
159
242
|
|
|
160
243
|
|
|
244
|
+
def cmd_versions(
|
|
245
|
+
state: ShellState,
|
|
246
|
+
name_or_id: str,
|
|
247
|
+
skip: int | None = None,
|
|
248
|
+
take: int | None = None,
|
|
249
|
+
) -> str:
|
|
250
|
+
"""List archived versions for a file by name or content ID."""
|
|
251
|
+
try:
|
|
252
|
+
content_id, display_name = _resolve_content_id(state, name_or_id)
|
|
253
|
+
params: dict[str, Any] = {"contentId": content_id}
|
|
254
|
+
if skip is not None:
|
|
255
|
+
params["skip"] = skip
|
|
256
|
+
if take is not None:
|
|
257
|
+
params["take"] = take
|
|
258
|
+
|
|
259
|
+
result = unique_sdk.Content.versions(
|
|
260
|
+
user_id=state.config.user_id,
|
|
261
|
+
company_id=state.config.company_id,
|
|
262
|
+
**params,
|
|
263
|
+
)
|
|
264
|
+
data = result.get("data", [])
|
|
265
|
+
return f"Versions for {display_name} ({content_id}):\n{_format_content_versions(data)}"
|
|
266
|
+
except (ValueError, unique_sdk.APIError) as e:
|
|
267
|
+
return f"versions: {e}"
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def cmd_restore_version(state: ShellState, content_version_id: str) -> str:
|
|
271
|
+
"""Restore a file from an archived content version ID."""
|
|
272
|
+
if not state.is_within_workspace():
|
|
273
|
+
return "restore-version: permission denied (outside workspace scope)"
|
|
274
|
+
try:
|
|
275
|
+
result = unique_sdk.Content.restore_version(
|
|
276
|
+
user_id=state.config.user_id,
|
|
277
|
+
company_id=state.config.company_id,
|
|
278
|
+
contentVersionId=content_version_id,
|
|
279
|
+
)
|
|
280
|
+
title = result.get("title") or result.get("key") or result.get("id", "?")
|
|
281
|
+
content_id = result.get("id", "?")
|
|
282
|
+
return f"Restored: {title} ({content_id}) from version {content_version_id}"
|
|
283
|
+
except (ValueError, unique_sdk.APIError) as e:
|
|
284
|
+
return f"restore-version: {e}"
|
|
285
|
+
|
|
286
|
+
|
|
161
287
|
def cmd_download(
|
|
162
288
|
state: ShellState,
|
|
163
289
|
name_or_id: str,
|