docmost-cli 0.4.0__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.
Files changed (56) hide show
  1. docmost_cli/__init__.py +5 -0
  2. docmost_cli/__main__.py +18 -0
  3. docmost_cli/api/__init__.py +5 -0
  4. docmost_cli/api/attachments.py +30 -0
  5. docmost_cli/api/auth.py +202 -0
  6. docmost_cli/api/client.py +296 -0
  7. docmost_cli/api/comments.py +103 -0
  8. docmost_cli/api/pages.py +530 -0
  9. docmost_cli/api/pagination.py +94 -0
  10. docmost_cli/api/search.py +40 -0
  11. docmost_cli/api/spaces.py +141 -0
  12. docmost_cli/api/users.py +25 -0
  13. docmost_cli/api/workspace.py +43 -0
  14. docmost_cli/cli/__init__.py +3 -0
  15. docmost_cli/cli/attachment.py +30 -0
  16. docmost_cli/cli/comment.py +83 -0
  17. docmost_cli/cli/config_cmd.py +143 -0
  18. docmost_cli/cli/main.py +133 -0
  19. docmost_cli/cli/page.py +382 -0
  20. docmost_cli/cli/search.py +33 -0
  21. docmost_cli/cli/space.py +57 -0
  22. docmost_cli/cli/sync_cmd.py +122 -0
  23. docmost_cli/cli/user.py +25 -0
  24. docmost_cli/cli/workspace.py +40 -0
  25. docmost_cli/config/__init__.py +23 -0
  26. docmost_cli/config/settings.py +23 -0
  27. docmost_cli/config/store.py +160 -0
  28. docmost_cli/convert/__init__.py +3 -0
  29. docmost_cli/convert/prosemirror_to_md.py +300 -0
  30. docmost_cli/models/__init__.py +3 -0
  31. docmost_cli/models/common.py +3 -0
  32. docmost_cli/output/__init__.py +17 -0
  33. docmost_cli/output/formatter.py +85 -0
  34. docmost_cli/output/tree.py +66 -0
  35. docmost_cli/py.typed +0 -0
  36. docmost_cli/sync/__init__.py +57 -0
  37. docmost_cli/sync/diff.py +156 -0
  38. docmost_cli/sync/frontmatter.py +152 -0
  39. docmost_cli/sync/manifest.py +195 -0
  40. docmost_cli/sync/pull.py +158 -0
  41. docmost_cli/sync/push.py +374 -0
  42. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-attachment.1 +57 -0
  43. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-comment.1 +92 -0
  44. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-config.1 +127 -0
  45. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-page.1 +412 -0
  46. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-search.1 +90 -0
  47. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-space.1 +111 -0
  48. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-sync.1 +206 -0
  49. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-user.1 +39 -0
  50. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-workspace.1 +68 -0
  51. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli.1 +301 -0
  52. docmost_cli-0.4.0.dist-info/METADATA +241 -0
  53. docmost_cli-0.4.0.dist-info/RECORD +56 -0
  54. docmost_cli-0.4.0.dist-info/WHEEL +4 -0
  55. docmost_cli-0.4.0.dist-info/entry_points.txt +2 -0
  56. docmost_cli-0.4.0.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,530 @@
1
+ """Page API methods."""
2
+
3
+ from typing import Any
4
+
5
+ from docmost_cli.api.client import DocmostClient
6
+ from docmost_cli.api.pagination import build_body
7
+ from docmost_cli.output.formatter import print_error
8
+
9
+ __all__ = [
10
+ "POSITION_FIRST",
11
+ "build_page_tree",
12
+ "copy_page",
13
+ "create_and_place_page",
14
+ "create_page_via_import",
15
+ "delete_page",
16
+ "duplicate_page",
17
+ "export_page",
18
+ "get_page_children",
19
+ "get_page_content",
20
+ "get_page_history",
21
+ "get_page_info",
22
+ "get_sidebar_pages",
23
+ "import_page",
24
+ "list_recent_pages",
25
+ "move_page",
26
+ "try_update_page_content",
27
+ "update_page_content",
28
+ "update_page_meta",
29
+ ]
30
+
31
+ # Fractional index string meaning "place at beginning" in Docmost's ordering.
32
+ POSITION_FIRST = "aaaaa"
33
+
34
+
35
+ def get_page_info(client: DocmostClient, page_id: str) -> dict[str, Any]:
36
+ """Get page metadata by ID.
37
+
38
+ Args:
39
+ client: Authenticated Docmost client.
40
+ page_id: Page UUID.
41
+
42
+ Returns:
43
+ Page info dict (unwrapped from data envelope).
44
+ """
45
+ result = client.post("/pages/info", json={"pageId": page_id})
46
+ return result.get("data", result)
47
+
48
+
49
+ def create_page_via_import(
50
+ client: DocmostClient,
51
+ *,
52
+ space_id: str,
53
+ title: str,
54
+ content: str,
55
+ parent_page_id: str | None = None,
56
+ ) -> dict[str, Any]:
57
+ """Create a page using the import endpoint (server-side MD→ProseMirror).
58
+
59
+ Sends Markdown as a .md file via multipart upload. Available on both
60
+ Community and Enterprise editions.
61
+
62
+ Args:
63
+ client: Authenticated Docmost client.
64
+ space_id: Target space UUID.
65
+ title: Page title.
66
+ content: Markdown content.
67
+ parent_page_id: Parent page UUID (optional).
68
+
69
+ Returns:
70
+ Raw API response dict (should contain page ID).
71
+ """
72
+ # Ensure content has the title as H1 if not already present
73
+ md_content = content
74
+ if md_content and not md_content.lstrip().startswith("#"):
75
+ md_content = f"# {title}\n\n{md_content}"
76
+ elif not md_content:
77
+ md_content = f"# {title}\n"
78
+
79
+ file_bytes = md_content.encode("utf-8")
80
+ files = {"file": (f"{title}.md", file_bytes, "text/markdown")}
81
+ data = build_body({"spaceId": space_id}, parentPageId=parent_page_id)
82
+
83
+ return client.post_multipart("/pages/import", data=data, files=files)
84
+
85
+
86
+ def update_page_meta(
87
+ client: DocmostClient,
88
+ *,
89
+ page_id: str,
90
+ title: str | None = None,
91
+ icon: str | None = None,
92
+ ) -> dict[str, Any]:
93
+ """Update page metadata (title, icon).
94
+
95
+ Available on both Community and Enterprise editions.
96
+
97
+ Args:
98
+ client: Authenticated Docmost client.
99
+ page_id: Page UUID.
100
+ title: New title.
101
+ icon: New icon emoji.
102
+
103
+ Returns:
104
+ Raw API response dict.
105
+ """
106
+ body = build_body({"pageId": page_id}, title=title, icon=icon)
107
+ return client.post("/pages/update", json=body)
108
+
109
+
110
+ def update_page_content(
111
+ client: DocmostClient,
112
+ *,
113
+ page_id: str,
114
+ content: str,
115
+ fmt: str = "markdown",
116
+ ) -> dict[str, Any]:
117
+ """Update page content via REST endpoint.
118
+
119
+ This endpoint may only be available on Enterprise edition (v0.70+).
120
+ On Community edition, this may return 404/405.
121
+
122
+ Args:
123
+ client: Authenticated Docmost client.
124
+ page_id: Page UUID.
125
+ content: Markdown or HTML content.
126
+ fmt: Content format ("markdown" or "html").
127
+
128
+ Returns:
129
+ Raw API response dict.
130
+ """
131
+ try:
132
+ return client.post(
133
+ "/pages/content/update",
134
+ json={"pageId": page_id, "content": content, "format": fmt},
135
+ )
136
+ except SystemExit as exc:
137
+ if exc.code == 4: # 404 — endpoint not available
138
+ print_error(
139
+ "Content update is not available on this Docmost instance. "
140
+ "This feature may require Enterprise edition (v0.70+). "
141
+ "Use 'docmost-cli page delete' + 'docmost-cli page create' "
142
+ "to replace page content.",
143
+ exit_code=1,
144
+ )
145
+ raise
146
+
147
+
148
+ def try_update_page_content(
149
+ client: DocmostClient,
150
+ *,
151
+ page_id: str,
152
+ content: str,
153
+ fmt: str = "markdown",
154
+ ) -> bool:
155
+ """Try updating page content via Enterprise endpoint.
156
+
157
+ Silently probes the endpoint without raising on failure.
158
+ Use this to detect whether the Enterprise content-update API is available.
159
+
160
+ Args:
161
+ client: Authenticated Docmost client.
162
+ page_id: Page UUID.
163
+ content: Markdown or HTML content.
164
+ fmt: Content format ("markdown" or "html").
165
+
166
+ Returns:
167
+ True if the update succeeded, False if the endpoint is unavailable.
168
+ """
169
+ response = client.post_raw(
170
+ "/pages/content/update",
171
+ json={"pageId": page_id, "content": content, "format": fmt},
172
+ raise_on_error=False,
173
+ )
174
+ return response.is_success
175
+
176
+
177
+ def create_and_place_page(
178
+ client: DocmostClient,
179
+ *,
180
+ space_id: str,
181
+ title: str,
182
+ content: str,
183
+ parent_page_id: str | None = None,
184
+ icon: str | None = None,
185
+ ) -> str:
186
+ """Create a page via import, then move to parent and set icon.
187
+
188
+ Combines the three-step create+move+icon workflow that the import
189
+ endpoint requires (it ignores parentPageId and icon).
190
+
191
+ Args:
192
+ client: Authenticated Docmost client.
193
+ space_id: Target space UUID.
194
+ title: Page title.
195
+ content: Markdown content.
196
+ parent_page_id: Parent page UUID (optional).
197
+ icon: Page icon emoji (optional).
198
+
199
+ Returns:
200
+ The new page's UUID.
201
+ """
202
+ from docmost_cli.api.pagination import extract_id
203
+
204
+ result = create_page_via_import(client, space_id=space_id, title=title, content=content)
205
+ page_id = extract_id(result)
206
+
207
+ if parent_page_id:
208
+ move_page(
209
+ client,
210
+ page_id=page_id,
211
+ parent_page_id=parent_page_id,
212
+ position=POSITION_FIRST,
213
+ )
214
+ if icon:
215
+ update_page_meta(client, page_id=page_id, icon=icon)
216
+
217
+ return page_id
218
+
219
+
220
+ def delete_page(client: DocmostClient, page_id: str) -> dict[str, Any]:
221
+ """Delete a page by ID.
222
+
223
+ Available on both Community and Enterprise editions.
224
+
225
+ Args:
226
+ client: Authenticated Docmost client.
227
+ page_id: Page UUID.
228
+
229
+ Returns:
230
+ Raw API response dict.
231
+ """
232
+ return client.post("/pages/delete", json={"pageId": page_id})
233
+
234
+
235
+ def move_page(
236
+ client: DocmostClient,
237
+ *,
238
+ page_id: str,
239
+ parent_page_id: str | None = None,
240
+ space_id: str | None = None,
241
+ position: str | int | None = None,
242
+ ) -> dict[str, Any]:
243
+ """Move a page to a new location.
244
+
245
+ Available on both Community and Enterprise editions.
246
+
247
+ Args:
248
+ client: Authenticated Docmost client.
249
+ page_id: Page UUID.
250
+ parent_page_id: New parent page UUID (omit for root).
251
+ space_id: Target space UUID (for cross-space moves).
252
+ position: Position among siblings (fractional index string, 5-12 chars).
253
+
254
+ Returns:
255
+ Raw API response dict.
256
+ """
257
+ body = build_body(
258
+ {"pageId": page_id},
259
+ parentPageId=parent_page_id,
260
+ spaceId=space_id,
261
+ position=position,
262
+ )
263
+ return client.post("/pages/move", json=body)
264
+
265
+
266
+ def get_page_content(client: DocmostClient, page_id: str) -> dict[str, Any]:
267
+ """Get page content and metadata.
268
+
269
+ Tries POST /pages/content (Enterprise v0.70+) first, then falls back
270
+ to POST /pages/info which may include content on both editions.
271
+
272
+ Args:
273
+ client: Authenticated Docmost client.
274
+ page_id: Page UUID.
275
+
276
+ Returns:
277
+ Dict with page metadata and content (ProseMirror JSON).
278
+ """
279
+ # Get page info first (needed for metadata and fallback content)
280
+ info = get_page_info(client, page_id)
281
+
282
+ # Try Enterprise content endpoint (silently — may not exist on Community)
283
+ response = client.post_raw("/pages/content", json={"pageId": page_id}, raise_on_error=False)
284
+ if response.is_success:
285
+ try:
286
+ content_data = response.json()
287
+ data = content_data.get("data", content_data)
288
+ info["content"] = data.get("content", data)
289
+ return info
290
+ except (ValueError, KeyError):
291
+ pass
292
+
293
+ # Fall back to content from /pages/info (already fetched)
294
+ if "content" in info and info["content"]:
295
+ return info
296
+
297
+ print_error(
298
+ "Page content not available via REST on this instance. "
299
+ "This may require Enterprise edition (v0.70+). "
300
+ "Try 'docmost-cli page get <id> --raw' or access the page in the web UI.",
301
+ exit_code=1,
302
+ )
303
+
304
+
305
+ def list_recent_pages(
306
+ client: DocmostClient,
307
+ space_id: str,
308
+ *,
309
+ limit: int | None = None,
310
+ cursor: str | None = None,
311
+ ) -> dict[str, Any]:
312
+ """List recent pages in a space with cursor-based pagination.
313
+
314
+ Args:
315
+ client: Authenticated Docmost client.
316
+ space_id: Space UUID.
317
+ limit: Max results to return.
318
+ cursor: Pagination cursor.
319
+
320
+ Returns:
321
+ Raw API response dict.
322
+ """
323
+ body = build_body({"spaceId": space_id}, limit=limit, cursor=cursor)
324
+ return client.post("/pages/recent", json=body)
325
+
326
+
327
+ def duplicate_page(client: DocmostClient, page_id: str) -> dict[str, Any]:
328
+ """Duplicate a page.
329
+
330
+ Args:
331
+ client: Authenticated Docmost client.
332
+ page_id: Page UUID to duplicate.
333
+
334
+ Returns:
335
+ Raw API response dict (should contain new page ID).
336
+ """
337
+ return client.post("/pages/duplicate", json={"pageId": page_id})
338
+
339
+
340
+ def copy_page(client: DocmostClient, page_id: str, space_id: str) -> dict[str, Any]:
341
+ """Copy a page to a different space.
342
+
343
+ Args:
344
+ client: Authenticated Docmost client.
345
+ page_id: Page UUID to copy.
346
+ space_id: Target space UUID.
347
+
348
+ Returns:
349
+ Raw API response dict (should contain new page ID).
350
+ """
351
+ return client.post("/pages/copy", json={"pageId": page_id, "spaceId": space_id})
352
+
353
+
354
+ def get_page_children(
355
+ client: DocmostClient,
356
+ page_id: str,
357
+ *,
358
+ space_id: str | None = None,
359
+ ) -> dict[str, Any]:
360
+ """List direct child pages.
361
+
362
+ Uses /pages/sidebar-pages with pageId (works on Community edition).
363
+ If space_id is not provided, resolves it from the page's metadata.
364
+
365
+ Args:
366
+ client: Authenticated Docmost client.
367
+ page_id: Parent page UUID.
368
+ space_id: Space UUID (resolved from page info if not provided).
369
+
370
+ Returns:
371
+ Raw API response dict.
372
+ """
373
+ if not space_id:
374
+ info = get_page_info(client, page_id)
375
+ space_id = info.get("spaceId", "")
376
+ return client.post("/pages/sidebar-pages", json={"spaceId": space_id, "pageId": page_id})
377
+
378
+
379
+ def get_page_history(
380
+ client: DocmostClient,
381
+ page_id: str,
382
+ *,
383
+ limit: int | None = None,
384
+ cursor: str | None = None,
385
+ ) -> dict[str, Any]:
386
+ """Get page version history.
387
+
388
+ Args:
389
+ client: Authenticated Docmost client.
390
+ page_id: Page UUID.
391
+ limit: Max results.
392
+ cursor: Pagination cursor.
393
+
394
+ Returns:
395
+ Raw API response dict.
396
+ """
397
+ body = build_body({"pageId": page_id}, limit=limit, cursor=cursor)
398
+ return client.post("/pages/history", json=body)
399
+
400
+
401
+ def export_page(client: DocmostClient, page_id: str, fmt: str = "md") -> str:
402
+ """Export page content.
403
+
404
+ Docmost returns a ZIP file containing the exported content.
405
+ This function extracts the content from the ZIP.
406
+
407
+ Args:
408
+ client: Authenticated Docmost client.
409
+ page_id: Page UUID.
410
+ fmt: Export format ("md" or "html"). Accepts "md" as alias for "markdown".
411
+
412
+ Returns:
413
+ Exported content as a string.
414
+ """
415
+ import io
416
+ import zipfile
417
+
418
+ # Docmost expects "markdown" not "md"
419
+ api_format = "markdown" if fmt == "md" else fmt
420
+ response = client.post_raw("/pages/export", json={"pageId": page_id, "format": api_format})
421
+
422
+ # Response is a ZIP file — extract content from it
423
+ with zipfile.ZipFile(io.BytesIO(response.content)) as zf:
424
+ names = zf.namelist()
425
+ if not names:
426
+ print_error("Export ZIP is empty.", exit_code=1)
427
+ return zf.read(names[0]).decode("utf-8")
428
+
429
+
430
+ def get_sidebar_pages(client: DocmostClient, space_id: str) -> dict[str, Any]:
431
+ """Get page tree structure for a space.
432
+
433
+ Returns nested structure with children arrays, used for --tree view.
434
+
435
+ Args:
436
+ client: Authenticated Docmost client.
437
+ space_id: Space UUID.
438
+
439
+ Returns:
440
+ Raw API response dict with nested page tree.
441
+ """
442
+ return client.post("/pages/sidebar-pages", json={"spaceId": space_id})
443
+
444
+
445
+ def import_page(
446
+ client: DocmostClient,
447
+ *,
448
+ space_id: str,
449
+ file_name: str,
450
+ file_bytes: bytes,
451
+ parent_page_id: str | None = None,
452
+ ) -> dict[str, Any]:
453
+ """Import a file as a new page via multipart upload.
454
+
455
+ Args:
456
+ client: Authenticated Docmost client.
457
+ space_id: Target space UUID.
458
+ file_name: Original filename (used for MIME detection and upload).
459
+ file_bytes: Raw file content bytes.
460
+ parent_page_id: Parent page UUID (optional).
461
+
462
+ Returns:
463
+ Raw API response dict (should contain new page ID).
464
+ """
465
+ mime = "text/html" if file_name.lower().endswith((".html", ".htm")) else "text/markdown"
466
+ files = {"file": (file_name, file_bytes, mime)}
467
+ data = build_body({"spaceId": space_id}, parentPageId=parent_page_id)
468
+ return client.post_multipart("/pages/import", data=data, files=files)
469
+
470
+
471
+ def build_page_tree(
472
+ client: DocmostClient,
473
+ space_id: str,
474
+ *,
475
+ max_depth: int = 10,
476
+ ) -> list[dict[str, Any]]:
477
+ """Build full page tree, filling in missing children recursively.
478
+
479
+ Starts with /pages/sidebar-pages, then uses /pages/children to
480
+ fill in any empty children arrays (sidebar API may not return them).
481
+
482
+ Args:
483
+ client: Authenticated Docmost client.
484
+ space_id: Space UUID.
485
+ max_depth: Maximum recursion depth to prevent runaway.
486
+
487
+ Returns:
488
+ List of page dicts with populated children arrays.
489
+ """
490
+ from docmost_cli.api.pagination import extract_items
491
+
492
+ result = get_sidebar_pages(client, space_id)
493
+ pages = extract_items(result)
494
+
495
+ for page in pages:
496
+ _fill_children(client, page, space_id=space_id, depth=0, max_depth=max_depth)
497
+
498
+ return pages
499
+
500
+
501
+ def _fill_children(
502
+ client: DocmostClient,
503
+ page: dict[str, Any],
504
+ *,
505
+ space_id: str,
506
+ depth: int,
507
+ max_depth: int,
508
+ ) -> None:
509
+ """Recursively fetch children if the sidebar API returned them empty."""
510
+ if depth >= max_depth:
511
+ return
512
+
513
+ children = page.get("children", [])
514
+
515
+ # If sidebar returned empty children, fetch via sidebar-pages with pageId
516
+ if not children and page.get("hasChildren", False):
517
+ try:
518
+ from docmost_cli.api.pagination import extract_items
519
+
520
+ result = get_page_children(client, page["id"], space_id=space_id)
521
+ children = extract_items(result)
522
+ page["children"] = children
523
+ except SystemExit as exc:
524
+ if exc.code not in (4,):
525
+ raise
526
+ page["children"] = []
527
+ return
528
+
529
+ for child in children:
530
+ _fill_children(client, child, space_id=space_id, depth=depth + 1, max_depth=max_depth)
@@ -0,0 +1,94 @@
1
+ """Pagination utilities for cursor-based API responses.
2
+
3
+ Provides shared helpers for extracting items from varying response shapes
4
+ and auto-following pagination cursors.
5
+ """
6
+
7
+ from collections.abc import Callable
8
+ from typing import Any
9
+
10
+ __all__ = ["build_body", "extract_id", "extract_items", "get_cursor", "paginate_all"]
11
+
12
+
13
+ def extract_id(response: dict[str, Any]) -> str:
14
+ """Extract resource ID from API response, handling nested shapes."""
15
+ return response.get("id") or response.get("data", {}).get("id", "")
16
+
17
+
18
+ def build_body(required: dict[str, Any], **optional: Any) -> dict[str, Any]:
19
+ """Build API request body, filtering out None optional values."""
20
+ body = dict(required)
21
+ for key, value in optional.items():
22
+ if value is not None:
23
+ body[key] = value
24
+ return body
25
+
26
+
27
+ def extract_items(response: dict[str, Any]) -> list[dict[str, Any]]:
28
+ """Extract items list from API response, handling nested shapes.
29
+
30
+ Handles: {data: {items: [...]}}, {data: [...]}, {items: [...]}, and flat dicts.
31
+
32
+ Args:
33
+ response: Raw API response dict.
34
+
35
+ Returns:
36
+ List of item dicts.
37
+ """
38
+ if "data" in response and isinstance(response["data"], dict):
39
+ return response["data"].get("items", [])
40
+ if "data" in response and isinstance(response["data"], list):
41
+ return response["data"]
42
+ return response.get("items", [response] if "id" in response else [])
43
+
44
+
45
+ def get_cursor(response: dict[str, Any]) -> str | None:
46
+ """Extract pagination cursor from API response.
47
+
48
+ Args:
49
+ response: Raw API response dict.
50
+
51
+ Returns:
52
+ Next cursor string, or None if no more pages.
53
+ """
54
+ if "data" in response and isinstance(response["data"], dict):
55
+ return response["data"].get("cursor")
56
+ return response.get("cursor")
57
+
58
+
59
+ def paginate_all(
60
+ fetch_func: Callable[..., dict[str, Any]],
61
+ *,
62
+ limit: int | None = None,
63
+ **kwargs: Any,
64
+ ) -> list[dict[str, Any]]:
65
+ """Auto-follow pagination until exhausted or limit reached.
66
+
67
+ Calls fetch_func repeatedly with cursor parameter until no more
68
+ pages are available or the total item count reaches limit.
69
+
70
+ Args:
71
+ fetch_func: API function that accepts cursor= keyword arg.
72
+ limit: Max total items to collect (None = all).
73
+ **kwargs: Additional arguments passed to fetch_func.
74
+
75
+ Returns:
76
+ Combined list of all items across pages.
77
+ """
78
+ max_iterations = 1000 # Safety guard against infinite loops
79
+ all_items: list[dict[str, Any]] = []
80
+ cursor: str | None = None
81
+
82
+ for _ in range(max_iterations):
83
+ response = fetch_func(**kwargs, cursor=cursor)
84
+ items = extract_items(response)
85
+ all_items.extend(items)
86
+
87
+ if limit and len(all_items) >= limit:
88
+ return all_items[:limit]
89
+
90
+ cursor = get_cursor(response)
91
+ if not cursor:
92
+ break
93
+
94
+ return all_items
@@ -0,0 +1,40 @@
1
+ """Search API methods."""
2
+
3
+ from typing import Any
4
+
5
+ from docmost_cli.api.client import DocmostClient
6
+ from docmost_cli.api.pagination import build_body
7
+
8
+ __all__ = ["search"]
9
+
10
+
11
+ def search(
12
+ client: DocmostClient,
13
+ query: str,
14
+ *,
15
+ space_id: str | None = None,
16
+ result_type: str | None = None,
17
+ limit: int | None = None,
18
+ cursor: str | None = None,
19
+ ) -> dict[str, Any]:
20
+ """Full-text search across the wiki.
21
+
22
+ Args:
23
+ client: Authenticated Docmost client.
24
+ query: Search query string.
25
+ space_id: Optional space UUID to filter results.
26
+ result_type: Optional type filter ("page" or "attachment").
27
+ limit: Max results (default server-side, typically 20).
28
+ cursor: Pagination cursor.
29
+
30
+ Returns:
31
+ Raw API response dict.
32
+ """
33
+ body = build_body(
34
+ {"query": query},
35
+ spaceId=space_id,
36
+ type=result_type,
37
+ limit=limit,
38
+ cursor=cursor,
39
+ )
40
+ return client.post("/search", json=body)