unique-sdk 2026.44.0.dev11__tar.gz → 2026.44.0.dev13__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 (92) hide show
  1. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/PKG-INFO +1 -1
  2. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/pyproject.toml +6 -3
  3. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_briefing.py +108 -9
  4. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/files.py +19 -42
  5. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/navigation.py +16 -12
  6. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/metadata_filter.py +14 -0
  7. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/README.md +0 -0
  8. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/__init__.py +0 -0
  9. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/_api_requestor.py +0 -0
  10. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/_api_resource.py +0 -0
  11. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/_api_version.py +0 -0
  12. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/_error.py +0 -0
  13. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/_http_client.py +0 -0
  14. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/_list_object.py +0 -0
  15. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/_object_classes.py +0 -0
  16. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/_request_options.py +0 -0
  17. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/_unique_object.py +0 -0
  18. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/_unique_ql.py +0 -0
  19. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/_unique_response.py +0 -0
  20. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/_util.py +0 -0
  21. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/_version.py +0 -0
  22. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/_webhook.py +0 -0
  23. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/__init__.py +0 -0
  24. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_acronyms.py +0 -0
  25. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_agentic_table.py +0 -0
  26. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_analytics_order.py +0 -0
  27. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_benchmarking.py +0 -0
  28. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_chat_completion.py +0 -0
  29. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_content.py +0 -0
  30. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_context_memory.py +0 -0
  31. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_elicitation.py +0 -0
  32. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_embedding.py +0 -0
  33. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_event.py +0 -0
  34. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_folder.py +0 -0
  35. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_group.py +0 -0
  36. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_integrated.py +0 -0
  37. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_llm_models.py +0 -0
  38. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_mcp.py +0 -0
  39. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_message.py +0 -0
  40. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_message_assessment.py +0 -0
  41. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_message_execution.py +0 -0
  42. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_message_log.py +0 -0
  43. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_message_tool.py +0 -0
  44. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_module.py +0 -0
  45. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_scheduled_task.py +0 -0
  46. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_search.py +0 -0
  47. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_search_string.py +0 -0
  48. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_short_term_memory.py +0 -0
  49. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_space.py +0 -0
  50. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_user.py +0 -0
  51. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/api_resources/_web_search.py +0 -0
  52. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/__init__.py +0 -0
  53. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/__main__.py +0 -0
  54. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/cli.py +0 -0
  55. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/__init__.py +0 -0
  56. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
  57. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/agentic_table.py +0 -0
  58. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/agentic_table_write.py +0 -0
  59. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/browser.py +0 -0
  60. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/cite_file.py +0 -0
  61. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/elicitation.py +0 -0
  62. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/folders.py +0 -0
  63. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/mcp.py +0 -0
  64. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/read.py +0 -0
  65. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
  66. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/search.py +0 -0
  67. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/subagent.py +0 -0
  68. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/web_search.py +0 -0
  69. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/commands/web_search_config.py +0 -0
  70. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/config.py +0 -0
  71. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/formatting.py +0 -0
  72. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/identity.py +0 -0
  73. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/paths.py +0 -0
  74. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/shell.py +0 -0
  75. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/skills/unique-cli-agentic-table/SKILL.md +0 -0
  76. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
  77. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +0 -0
  78. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
  79. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
  80. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
  81. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
  82. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/skills/unique-cli-uploaded-search/SKILL.md +0 -0
  83. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
  84. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/state.py +0 -0
  85. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/cli/workspace.py +0 -0
  86. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/utils/analytics_order_run.py +0 -0
  87. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/utils/benchmarking_run.py +0 -0
  88. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/utils/chat_history.py +0 -0
  89. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/utils/chat_in_space.py +0 -0
  90. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/utils/file_io.py +0 -0
  91. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/unique_sdk/utils/sources.py +0 -0
  92. {unique_sdk-2026.44.0.dev11 → unique_sdk-2026.44.0.dev13}/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.44.0.dev11
3
+ Version: 2026.44.0.dev13
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.44.0.dev11"
3
+ version = "2026.44.0.dev13"
4
4
  description = ""
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -43,7 +43,9 @@ module-root = ""
43
43
  exclude-newer = "2 weeks"
44
44
 
45
45
  [dependency-groups]
46
- dev = []
46
+ dev = [
47
+ "python-dotenv>=1.2.2",
48
+ ]
47
49
 
48
50
  [tool.deptry]
49
51
  known_first_party = ["unique_sdk"]
@@ -60,7 +62,8 @@ extend-select = ["I"]
60
62
 
61
63
  [tool.basedpyright]
62
64
  typeCheckingMode = "recommended"
63
- include = ["unique_sdk"]
65
+ include = ["unique_sdk", "examples/basics"]
66
+ extraPaths = ["examples/basics"]
64
67
  exclude = ["build", "**/__pycache__", "**/tests/**", "**/test_*.py"]
65
68
  allowedUntypedLibraries = ["openai.types.responses", "openai.types.shared_params"]
66
69
  # Suppress rules that require full annotation coverage of legacy internals —
@@ -1,4 +1,4 @@
1
- """Assistant briefing operations (``PUT /briefings/{assistantId}``).
1
+ """Assistant briefing operations on ``/briefings/{assistantId}``.
2
2
 
3
3
  Request and response shapes follow OpenAPI ``PublicUpsertBriefingRequestDto`` and
4
4
  ``PublicBriefingDto``. See :class:`Briefing.UpsertForAssistantParams`.
@@ -16,6 +16,7 @@ from unique_sdk._request_options import RequestOptions
16
16
  from unique_sdk._util import classproperty
17
17
 
18
18
  _MAX_TEXT_LEN = 4000
19
+ _MAX_TITLE_LEN = 100
19
20
  _MAX_PROMPTS = 200
20
21
  _MAX_PROMPT_TITLE_LEN = 100
21
22
  _MAX_PROMPT_BODY_LEN = 4000
@@ -65,15 +66,16 @@ def _validate_prompts(raw: Any) -> list[dict[str, str]]:
65
66
 
66
67
 
67
68
  class _PublicUpsertBriefingWire(TypedDict):
68
- """Exact JSON keys for ``PUT`` body (`PublicUpsertBriefingRequestDto`)."""
69
+ """Exact JSON keys for ``PUT`` body (``PublicUpsertBriefingRequestDto``)."""
69
70
 
70
71
  text: str
71
72
  generatedAt: str
72
73
  prompts: list[dict[str, str]]
74
+ title: NotRequired[str]
73
75
 
74
76
 
75
77
  class Briefing(APIResource["Briefing"]):
76
- """Create or replace the briefing attached to an assistant."""
78
+ """Read, upsert, or detach the briefing attached to an assistant."""
77
79
 
78
80
  @classproperty
79
81
  def OBJECT_NAME(cls) -> Literal["briefing"]:
@@ -82,7 +84,7 @@ class Briefing(APIResource["Briefing"]):
82
84
  RESOURCE_URL = "/briefings"
83
85
 
84
86
  class BriefingPromptItem(TypedDict):
85
- """One entry in ``prompts`` (``PublicPromptItemDto``)."""
87
+ """One entry in ``prompts`` (``PromptItemDto``)."""
86
88
 
87
89
  title: str
88
90
  body: str
@@ -93,9 +95,21 @@ class Briefing(APIResource["Briefing"]):
93
95
  text: NotRequired[str]
94
96
  generatedAt: NotRequired[str]
95
97
  prompts: list["Briefing.BriefingPromptItem"]
98
+ title: NotRequired[str]
96
99
  markdown: NotRequired[str]
97
100
  content: NotRequired[str]
98
101
 
102
+ class DeletedObject(TypedDict):
103
+ """Response from ``DELETE /briefings/{assistantId}`` (detach; record may remain)."""
104
+
105
+ id: str
106
+ object: str
107
+ deleted: bool
108
+
109
+ @classmethod
110
+ def _assistant_url(cls, assistant_id: str) -> str:
111
+ return "%s/%s" % (cls.RESOURCE_URL, quote_plus(assistant_id))
112
+
99
113
  @classmethod
100
114
  def _wire_json_from_upsert_params(
101
115
  cls, params: Mapping[str, Any]
@@ -149,7 +163,26 @@ class Briefing(APIResource["Briefing"]):
149
163
  prompts_raw = kw.pop("prompts", None)
150
164
  prompts_list = _validate_prompts(prompts_raw)
151
165
 
152
- return {"text": text, "generatedAt": ga, "prompts": prompts_list}
166
+ wire: _PublicUpsertBriefingWire = {
167
+ "text": text,
168
+ "generatedAt": ga,
169
+ "prompts": prompts_list,
170
+ }
171
+
172
+ title_raw = kw.pop("title", None)
173
+ if title_raw is not None:
174
+ if not isinstance(title_raw, str):
175
+ raise TypeError(
176
+ f"Briefing title must be a string, got {type(title_raw)!r}"
177
+ )
178
+ if len(title_raw) > _MAX_TITLE_LEN:
179
+ raise ValueError(
180
+ f"Briefing title must be at most {_MAX_TITLE_LEN} characters "
181
+ f"(got {len(title_raw)})"
182
+ )
183
+ wire["title"] = title_raw
184
+
185
+ return wire
153
186
 
154
187
  id: str
155
188
  object: str
@@ -163,6 +196,39 @@ class Briefing(APIResource["Briefing"]):
163
196
  createdAt: str | None
164
197
  updatedAt: str | None
165
198
 
199
+ @classmethod
200
+ def retrieve_for_assistant(
201
+ cls,
202
+ *,
203
+ user_id: str,
204
+ company_id: str,
205
+ assistant_id: str,
206
+ ) -> "Briefing":
207
+ """Return the briefing attached to ``assistant_id``.
208
+
209
+ Mirrors ``BriefingController_getForAssistant`` (``GET /briefings/{assistantId}``).
210
+ """
211
+ url = cls._assistant_url(assistant_id)
212
+ return cast(
213
+ "Briefing",
214
+ cls._static_request("get", url, user_id, company_id),
215
+ )
216
+
217
+ @classmethod
218
+ async def retrieve_for_assistant_async(
219
+ cls,
220
+ *,
221
+ user_id: str,
222
+ company_id: str,
223
+ assistant_id: str,
224
+ ) -> "Briefing":
225
+ """Async variant of :meth:`retrieve_for_assistant`."""
226
+ url = cls._assistant_url(assistant_id)
227
+ return cast(
228
+ "Briefing",
229
+ await cls._static_request_async("get", url, user_id, company_id),
230
+ )
231
+
166
232
  @classmethod
167
233
  def upsert_for_assistant(
168
234
  cls,
@@ -174,10 +240,9 @@ class Briefing(APIResource["Briefing"]):
174
240
  ) -> "Briefing":
175
241
  """Upsert the briefing for ``assistant_id`` (external id = assistant id).
176
242
 
177
- Mirrors ``PublicBriefingController_upsertForAssistant`` (
178
- ``PUT /public/briefings/{assistantId}`` on the upstream OpenAPI surface).
243
+ Mirrors ``BriefingController_upsertForAssistant`` (``PUT /briefings/{assistantId}``).
179
244
  """
180
- url = "%s/%s" % (cls.RESOURCE_URL, quote_plus(assistant_id))
245
+ url = cls._assistant_url(assistant_id)
181
246
  payload = cls._wire_json_from_upsert_params(params)
182
247
  return cast(
183
248
  "Briefing",
@@ -194,9 +259,43 @@ class Briefing(APIResource["Briefing"]):
194
259
  **params: Unpack["Briefing.UpsertForAssistantParams"],
195
260
  ) -> "Briefing":
196
261
  """Async variant of :meth:`upsert_for_assistant`."""
197
- url = "%s/%s" % (cls.RESOURCE_URL, quote_plus(assistant_id))
262
+ url = cls._assistant_url(assistant_id)
198
263
  payload = cls._wire_json_from_upsert_params(params)
199
264
  return cast(
200
265
  "Briefing",
201
266
  await cls._static_request_async("put", url, user_id, company_id, payload),
202
267
  )
268
+
269
+ @classmethod
270
+ def delete_for_assistant(
271
+ cls,
272
+ *,
273
+ user_id: str,
274
+ company_id: str,
275
+ assistant_id: str,
276
+ ) -> "Briefing.DeletedObject":
277
+ """Detach the briefing from ``assistant_id`` (underlying record may remain).
278
+
279
+ Mirrors ``BriefingController_detachForAssistant`` (
280
+ ``DELETE /briefings/{assistantId}``).
281
+ """
282
+ url = cls._assistant_url(assistant_id)
283
+ return cast(
284
+ "Briefing.DeletedObject",
285
+ cls._static_request("delete", url, user_id, company_id),
286
+ )
287
+
288
+ @classmethod
289
+ async def delete_for_assistant_async(
290
+ cls,
291
+ *,
292
+ user_id: str,
293
+ company_id: str,
294
+ assistant_id: str,
295
+ ) -> "Briefing.DeletedObject":
296
+ """Async variant of :meth:`delete_for_assistant`."""
297
+ url = cls._assistant_url(assistant_id)
298
+ return cast(
299
+ "Briefing.DeletedObject",
300
+ await cls._static_request_async("delete", url, user_id, company_id),
301
+ )
@@ -11,6 +11,7 @@ from typing import Any
11
11
 
12
12
  import unique_sdk
13
13
  from unique_sdk.cli.formatting import format_content_info
14
+ from unique_sdk.cli.metadata_filter import direct_children_filter
14
15
  from unique_sdk.cli.paths import canonical_folder_path, same_folder_path
15
16
  from unique_sdk.cli.state import ShellState
16
17
  from unique_sdk.utils.file_io import download_content, upload_file
@@ -40,9 +41,6 @@ _SUPPORTED_UPLOAD_MIME_TYPES = {
40
41
 
41
42
  # Bounds so one name resolve can never page through the whole knowledge base.
42
43
  _MAX_RESOLVE_MATCH_CANDIDATES = 100
43
- _MAX_RESOLVE_INFOS_PAGES = 10
44
- _RESOLVE_INFOS_PAGE_SIZE = 100
45
- _MAX_RESOLVE_INFOS_ENTRIES = _MAX_RESOLVE_INFOS_PAGES * _RESOLVE_INFOS_PAGE_SIZE
46
44
 
47
45
 
48
46
  def _normalize_unique_file_path(cwd: str, path: str) -> str:
@@ -178,19 +176,10 @@ def _resolve_content_id_in_folder(
178
176
  ),
179
177
  lookup_name=lookup_name,
180
178
  )
181
- capped = False
182
179
  if match is None:
183
- hits, capped = _hits_by_search(
184
- state=state, lookup_name=lookup_name, scope_id=scope_id
185
- )
180
+ hits = _hits_by_search(state=state, lookup_name=lookup_name, scope_id=scope_id)
186
181
  match = _pick_match(hits=hits, lookup_name=lookup_name)
187
182
  if match is None:
188
- if capped:
189
- raise ValueError(
190
- f"File not found in the first {_MAX_RESOLVE_INFOS_ENTRIES} "
191
- f"entries of this folder: {name_or_id}. Use 'unique-cli "
192
- "search' or pass a content id (cont_...)."
193
- )
194
183
  raise ValueError(f"File not found: {name_or_id}")
195
184
 
196
185
  resolved_id, display_name = match
@@ -234,8 +223,7 @@ def _hits_by_file_path(
234
223
 
235
224
  def _hits_by_search(
236
225
  *, state: ShellState, lookup_name: str, scope_id: str
237
- ) -> tuple[Sequence[Any], bool]:
238
- """Return the hits, and whether the get_infos fallback hit its page cap."""
226
+ ) -> Sequence[Any]:
239
227
  try:
240
228
  hits = unique_sdk.Content.search(
241
229
  user_id=state.config.user_id,
@@ -259,7 +247,7 @@ def _hits_by_search(
259
247
  lookup_name=lookup_name,
260
248
  scope_id=scope_id,
261
249
  )
262
- return hits, False
250
+ return hits
263
251
 
264
252
 
265
253
  def _pick_match(*, hits: Sequence[Any], lookup_name: str) -> tuple[str, str] | None:
@@ -290,32 +278,21 @@ def _resolve_content_id_via_infos_fallback(
290
278
  state: ShellState,
291
279
  lookup_name: str,
292
280
  scope_id: str,
293
- ) -> tuple[list[Mapping[str, Any]], bool]:
294
- """Folder-scoped get_infos fallback with a hard page cap.
295
-
296
- Returns the match (if any), and whether the cap stopped the scan early.
297
- """
298
- skip = 0
299
- for _ in range(_MAX_RESOLVE_INFOS_PAGES):
300
- result = unique_sdk.Content.get_infos(
301
- user_id=state.config.user_id,
302
- company_id=state.config.company_id,
303
- parentId=scope_id,
304
- skip=skip,
305
- take=_RESOLVE_INFOS_PAGE_SIZE,
306
- )
307
- content_infos = result.get("contentInfos", [])
308
- if not content_infos:
309
- return [], False
310
- for info in content_infos:
311
- title = info.get("title") or ""
312
- key = info.get("key") or ""
313
- if lookup_name in {title, key}:
314
- return [info], False
315
- if len(content_infos) < _RESOLVE_INFOS_PAGE_SIZE:
316
- return [], False
317
- skip += len(content_infos)
318
- return [], True
281
+ ) -> list[Mapping[str, Any]]:
282
+ """Folder-scoped get_infos fallback, filtered by name so one page suffices."""
283
+ name_filter = {
284
+ "or": [
285
+ {"path": ["title"], "operator": "equals", "value": lookup_name},
286
+ {"path": ["key"], "operator": "equals", "value": lookup_name},
287
+ ]
288
+ }
289
+ result = unique_sdk.Content.get_infos(
290
+ user_id=state.config.user_id,
291
+ company_id=state.config.company_id,
292
+ metadataFilter={"and": [direct_children_filter(scope_id), name_filter]},
293
+ take=_MAX_RESOLVE_MATCH_CANDIDATES,
294
+ )
295
+ return list(result.get("contentInfos", []))
319
296
 
320
297
 
321
298
  def _detect_upload_mime_type(path: Path) -> str:
@@ -6,7 +6,10 @@ from typing import Any
6
6
 
7
7
  import unique_sdk
8
8
  from unique_sdk.cli.formatting import format_ls
9
- from unique_sdk.cli.metadata_filter import _collect_filter_targets
9
+ from unique_sdk.cli.metadata_filter import (
10
+ _collect_filter_targets,
11
+ direct_children_filter,
12
+ )
10
13
  from unique_sdk.cli.paths import canonical_folder_path, same_folder_path
11
14
  from unique_sdk.cli.state import ShellState
12
15
 
@@ -188,10 +191,8 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
188
191
  return path_error
189
192
 
190
193
  folder_params: dict[str, Any] = {}
191
- content_params: dict[str, Any] = {}
192
194
  if scope_id:
193
195
  folder_params["parentId"] = scope_id
194
- content_params["parentId"] = scope_id
195
196
 
196
197
  folder_result = unique_sdk.Folder.get_infos(
197
198
  user_id=state.config.user_id,
@@ -199,16 +200,19 @@ def cmd_ls(state: ShellState, target: str | None = None) -> str:
199
200
  **folder_params,
200
201
  )
201
202
  folders = folder_result.get("folderInfos", [])
202
-
203
- content_result = unique_sdk.Content.get_infos(
204
- user_id=state.config.user_id,
205
- company_id=state.config.company_id,
206
- **content_params,
207
- )
208
- files = content_result.get("contentInfos", [])
209
-
210
203
  total_folders = folder_result.get("totalCount", len(folders))
211
- total_files = content_result.get("totalCount", len(files))
204
+
205
+ # Every file lives in a folder, so the root has no files of its own.
206
+ files: list[Any] = []
207
+ total_files = 0
208
+ if scope_id:
209
+ content_result = unique_sdk.Content.get_infos(
210
+ user_id=state.config.user_id,
211
+ company_id=state.config.company_id,
212
+ metadataFilter=direct_children_filter(scope_id),
213
+ )
214
+ files = content_result.get("contentInfos", [])
215
+ total_files = content_result.get("totalCount", len(files))
212
216
 
213
217
  if scope_id and not folders and not files:
214
218
  label = target if target is not None else resolved_path
@@ -38,6 +38,20 @@ ScopePathResolver = Callable[[str], "str | None"]
38
38
  ContentOwnerPathResolver = Callable[[str], "str | None"]
39
39
 
40
40
 
41
+ def direct_children_filter(scope_id: str) -> dict[str, Any]:
42
+ """UniqueQL for files directly in ``scope_id``, not in its subfolders."""
43
+ return {
44
+ "and": [
45
+ {"path": ["folderIdPath"], "operator": "contains", "value": scope_id},
46
+ {
47
+ "path": ["folderIdPath"],
48
+ "operator": "notContains",
49
+ "value": f"{scope_id}/",
50
+ },
51
+ ]
52
+ }
53
+
54
+
41
55
  def _extract_target_scope_id(value: Any) -> str | None:
42
56
  """Deepest folder scope id referenced by a ``folderIdPath`` filter value.
43
57