karakeep-python-api 1.8.0__tar.gz → 1.9.0__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 (18) hide show
  1. {karakeep_python_api-1.8.0/karakeep_python_api.egg-info → karakeep_python_api-1.9.0}/PKG-INFO +7 -2
  2. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/README.md +6 -1
  3. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/karakeep_python_api/__main__.py +17 -1
  4. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/karakeep_python_api/datatypes.py +47 -0
  5. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/karakeep_python_api/karakeep_api.py +188 -8
  6. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/karakeep_python_api/openapi_reference.json +379 -8
  7. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0/karakeep_python_api.egg-info}/PKG-INFO +7 -2
  8. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/setup.py +1 -1
  9. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/tests/test_karakeep_api.py +423 -0
  10. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/LICENSE +0 -0
  11. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/MANIFEST.in +0 -0
  12. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/karakeep_python_api/__init__.py +0 -0
  13. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/karakeep_python_api.egg-info/SOURCES.txt +0 -0
  14. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/karakeep_python_api.egg-info/dependency_links.txt +0 -0
  15. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/karakeep_python_api.egg-info/entry_points.txt +0 -0
  16. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/karakeep_python_api.egg-info/requires.txt +0 -0
  17. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/karakeep_python_api.egg-info/top_level.txt +0 -0
  18. {karakeep_python_api-1.8.0 → karakeep_python_api-1.9.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karakeep_python_api
3
- Version: 1.8.0
3
+ Version: 1.9.0
4
4
  Summary: Community python client for the Karakeep API.
5
5
  Home-page: https://github.com/thiswillbeyourgithub/karakeep_python_api/
6
6
  Keywords: rss,karakeep,hoarder,data-hoarding,python,api,feeds,openapi
@@ -82,6 +82,7 @@ Methods or CLI commands marked with ❌ should be used with caution as their beh
82
82
  | `get_a_single_bookmark` | ✅ | ❌ | |
83
83
  | `delete_a_bookmark` | ✅ | ❌ | |
84
84
  | `update_a_bookmark` | ✅ | ✅ | Tested for title updates. |
85
+ | `get_bookmark_readable_content` | ✅ | ❌ | Chunk merging tested offline; live test reads a text bookmark. |
85
86
  | `summarize_a_bookmark` | ❌ | ❌ | |
86
87
  | `attach_tags_to_a_bookmark` | ✅ | ❌ | |
87
88
  | `detach_tags_from_a_bookmark` | ✅ | ❌ | |
@@ -110,6 +111,7 @@ Methods or CLI commands marked with ❌ should be used with caution as their beh
110
111
  | `update_a_highlight` | ❌ | ❌ | |
111
112
  | `upload_a_new_asset` | ✅ | ❌ | Tested in PDF asset lifecycle test. |
112
113
  | `get_a_single_asset` | ✅ | ❌ | Tested in PDF asset lifecycle test. |
114
+ | `get_asset_signed_url` | ✅ | ❌ | Tested in PDF asset lifecycle test (downloads without an API key). |
113
115
  | `get_current_user_info` | ✅ | ❌ | Pytest: Tested indirectly during client init. CLI not directly tested. |
114
116
  | `get_current_user_stats` | ✅ | ✅ | |
115
117
  | `update_user` | ❌ | ❌ | |
@@ -118,6 +120,9 @@ Methods or CLI commands marked with ❌ should be used with caution as their beh
118
120
  | `get_a_single_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
119
121
  | `delete_a_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
120
122
  | `download_a_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
123
+ | `admin_trigger_recrawl` | ❌ | ❌ | Request body construction tested offline only. |
124
+ | `admin_trigger_reindex` | ❌ | ❌ | Request body construction tested offline only. |
125
+ | `admin_trigger_inference` | ❌ | ❌ | Request body construction tested offline only. |
121
126
 
122
127
  ## Installation
123
128
 
@@ -281,4 +286,4 @@ They can be found in the [./community_scripts](https://github.com/thiswillbeyour
281
286
 
282
287
  ---
283
288
 
284
- *This README was generated with assistance from [aider.chat](https://aider.chat).*
289
+ *This README was generated with assistance from [aider.chat](https://aider.chat), and later kept up to date with [Claude Code](https://claude.com/claude-code).*
@@ -50,6 +50,7 @@ Methods or CLI commands marked with ❌ should be used with caution as their beh
50
50
  | `get_a_single_bookmark` | ✅ | ❌ | |
51
51
  | `delete_a_bookmark` | ✅ | ❌ | |
52
52
  | `update_a_bookmark` | ✅ | ✅ | Tested for title updates. |
53
+ | `get_bookmark_readable_content` | ✅ | ❌ | Chunk merging tested offline; live test reads a text bookmark. |
53
54
  | `summarize_a_bookmark` | ❌ | ❌ | |
54
55
  | `attach_tags_to_a_bookmark` | ✅ | ❌ | |
55
56
  | `detach_tags_from_a_bookmark` | ✅ | ❌ | |
@@ -78,6 +79,7 @@ Methods or CLI commands marked with ❌ should be used with caution as their beh
78
79
  | `update_a_highlight` | ❌ | ❌ | |
79
80
  | `upload_a_new_asset` | ✅ | ❌ | Tested in PDF asset lifecycle test. |
80
81
  | `get_a_single_asset` | ✅ | ❌ | Tested in PDF asset lifecycle test. |
82
+ | `get_asset_signed_url` | ✅ | ❌ | Tested in PDF asset lifecycle test (downloads without an API key). |
81
83
  | `get_current_user_info` | ✅ | ❌ | Pytest: Tested indirectly during client init. CLI not directly tested. |
82
84
  | `get_current_user_stats` | ✅ | ✅ | |
83
85
  | `update_user` | ❌ | ❌ | |
@@ -86,6 +88,9 @@ Methods or CLI commands marked with ❌ should be used with caution as their beh
86
88
  | `get_a_single_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
87
89
  | `delete_a_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
88
90
  | `download_a_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
91
+ | `admin_trigger_recrawl` | ❌ | ❌ | Request body construction tested offline only. |
92
+ | `admin_trigger_reindex` | ❌ | ❌ | Request body construction tested offline only. |
93
+ | `admin_trigger_inference` | ❌ | ❌ | Request body construction tested offline only. |
89
94
 
90
95
  ## Installation
91
96
 
@@ -249,4 +254,4 @@ They can be found in the [./community_scripts](https://github.com/thiswillbeyour
249
254
 
250
255
  ---
251
256
 
252
- *This README was generated with assistance from [aider.chat](https://aider.chat).*
257
+ *This README was generated with assistance from [aider.chat](https://aider.chat), and later kept up to date with [Claude Code](https://claude.com/claude-code).*
@@ -538,7 +538,23 @@ def create_click_command(
538
538
  ctx.exit(1)
539
539
 
540
540
  # Serialize and print the result
541
- if result is not None:
541
+ if isinstance(result, (bytes, bytearray)):
542
+ # Binary payloads (e.g. download_a_backup, get_a_single_asset)
543
+ # cannot be JSON encoded: write them raw to stdout so the
544
+ # output can be redirected straight into a file.
545
+ logger.debug(f"Writing {len(result)} raw bytes to stdout.")
546
+ stdout_buffer = getattr(sys.stdout, "buffer", None)
547
+ if stdout_buffer is not None:
548
+ stdout_buffer.write(result)
549
+ stdout_buffer.flush()
550
+ else:
551
+ # Some test/capture harnesses replace sys.stdout with a
552
+ # text-only stream that has no .buffer attribute.
553
+ sys.stdout.write(
554
+ bytes(result).decode("utf-8", errors="surrogateescape")
555
+ )
556
+ sys.stdout.flush()
557
+ elif result is not None:
542
558
  output_data = serialize_output(result)
543
559
  # Use ensure_ascii_output flag to control JSON encoding
544
560
  click.echo(
@@ -53,6 +53,15 @@ class ContentTypeLink(BaseModel):
53
53
  favicon: Optional[str] = None
54
54
  htmlContent: Optional[str] = None
55
55
  contentAssetId: Optional[str] = None
56
+ # Reader-view triage produced by the crawler: readerViewStatus says whether a
57
+ # distraction-free rendering could be extracted, readerViewScore (0-100) how
58
+ # confident that extraction is, and preferredPreview which of the available
59
+ # renderings the UI should show by default.
60
+ readerViewStatus: Optional[
61
+ Literal["readable", "not_readable", "uncertain", "unavailable"]
62
+ ] = None
63
+ readerViewScore: Optional[int] = None
64
+ preferredPreview: Optional[Literal["reader_view", "screenshot", "overview"]] = None
56
65
  crawledAt: Optional[str] = None
57
66
  crawlStatus: Optional[Literal["success", "failure", "pending"]] = None
58
67
  author: Optional[str] = None
@@ -112,8 +121,20 @@ class UploadedAsset(BaseModel):
112
121
  Asset = UploadedAsset
113
122
 
114
123
 
124
+ class SignedAssetUrl(BaseModel):
125
+ assetId: str
126
+ # Temporary download URL that carries its own signature, so it works without
127
+ # the Authorization header and can be handed to a browser or media player.
128
+ signedUrl: str
129
+ expiresAt: str
130
+
131
+
115
132
  class Bookmark(BaseModel):
116
133
  id: str
134
+ # firstCreatedAt records the original creation time when a bookmark is
135
+ # recreated/re-imported, so it can predate createdAt. Optional in the spec
136
+ # and absent on Karakeep servers older than the one that introduced it.
137
+ firstCreatedAt: Optional[str] = None
117
138
  createdAt: str
118
139
  modifiedAt: Optional[str]
119
140
  title: Optional[str] = None
@@ -121,6 +142,10 @@ class Bookmark(BaseModel):
121
142
  favourited: bool
122
143
  taggingStatus: Optional[Literal["success", "failure", "pending"]] = None
123
144
  summarizationStatus: Optional[Literal["success", "failure", "pending"]] = None
145
+ # Status of the vector-embedding job used by semantic/hybrid search. The
146
+ # spec marks it required-but-nullable, so no default is given here: a
147
+ # missing key is a genuine mismatch with the documented server response.
148
+ embeddingStatus: Optional[Literal["success", "failure", "pending"]]
124
149
  note: Optional[str] = None
125
150
  summary: Optional[str] = None
126
151
  source: Optional[
@@ -136,6 +161,28 @@ class Bookmark(BaseModel):
136
161
  assets: List[BookmarkAsset]
137
162
 
138
163
 
164
+ class ReadableContentRange(BaseModel):
165
+ # Offsets are in Unicode characters over the *rendered* content, not bytes:
166
+ # start is inclusive, end is exclusive, and total is the full rendered length.
167
+ start: int
168
+ end: int
169
+ total: int
170
+
171
+
172
+ class BookmarkReadableContent(BaseModel):
173
+ bookmarkId: str
174
+ bookmarkType: Literal["link", "text", "asset"]
175
+ format: Literal["markdown", "text"]
176
+ content: str
177
+ # Hash of the rendered content the cursor was issued against. The server
178
+ # answers 409 if the bookmark changed between two chunks, so this value must
179
+ # not be mixed across a paginated read.
180
+ contentVersion: str
181
+ range: ReadableContentRange
182
+ nextCursor: Optional[str]
183
+ truncated: bool
184
+
185
+
139
186
  class PaginatedBookmarks(BaseModel):
140
187
  bookmarks: List[Bookmark]
141
188
  nextCursor: Optional[str] = ""
@@ -85,7 +85,7 @@ class KarakeepAPI:
85
85
  """
86
86
 
87
87
  # Version reflects the client library version, updated by bumpver
88
- VERSION: str = "1.8.0"
88
+ VERSION: str = "1.9.0"
89
89
 
90
90
  def __init__(
91
91
  self,
@@ -891,16 +891,25 @@ class KarakeepAPI:
891
891
  def search_bookmarks(
892
892
  self,
893
893
  q: str, # Search query is required
894
+ search_mode: Optional[Literal["fts", "semantic", "hybrid"]] = None,
894
895
  sort_order: Optional[Literal["asc", "desc", "relevance"]] = None,
895
896
  limit: Optional[int] = None,
896
897
  cursor: Optional[str] = None,
897
898
  include_content: bool = True, # Default from spec
898
899
  ) -> Union[datatypes.PaginatedBookmarks, Dict[str, Any], List[Any]]:
899
900
  """
900
- Search bookmarks. Corresponds to GET /bookmarks/search.
901
+ Search bookmarks using full-text, semantic or hybrid ranking.
902
+ Corresponds to GET /bookmarks/search.
901
903
 
902
904
  Args:
903
905
  q: The search query string.
906
+ search_mode: Search strategy (optional). "fts" is full-text search (API default),
907
+ "semantic" ranks with bookmark embeddings, and "hybrid" fuses both.
908
+ Hybrid falls back to full-text search when the query has no free-text
909
+ terms or when embedding infrastructure is unavailable. Semantic hits
910
+ below a minimum similarity are dropped, so "semantic" and "hybrid" may
911
+ return fewer results than `limit`. Note that the semantic modes only
912
+ support sort_order="relevance".
904
913
  sort_order: Sort order for results ("asc", "desc", "relevance"). Default from API is "relevance" (optional).
905
914
  limit: Maximum number of bookmarks to return (optional).
906
915
  cursor: Pagination cursor for the next page (optional).
@@ -916,6 +925,7 @@ class KarakeepAPI:
916
925
  """
917
926
  params = {
918
927
  "q": q,
928
+ "searchMode": search_mode,
919
929
  "sortOrder": sort_order,
920
930
  "limit": limit,
921
931
  "cursor": cursor,
@@ -1051,6 +1061,101 @@ class KarakeepAPI:
1051
1061
  # No Pydantic validation applied here as the spec defines a partial response (dict)
1052
1062
  return response_data
1053
1063
 
1064
+ @optional_typecheck
1065
+ def get_bookmark_readable_content(
1066
+ self,
1067
+ bookmark_id: str,
1068
+ format: Optional[Literal["markdown", "text"]] = None,
1069
+ max_chars: Optional[int] = None,
1070
+ cursor: Optional[str] = None,
1071
+ fetch_all: bool = False,
1072
+ ) -> Union[datatypes.BookmarkReadableContent, Dict[str, Any], List[Any]]:
1073
+ """
1074
+ Get an agent-readable rendering of a bookmark's content.
1075
+ Corresponds to GET /bookmarks/{bookmarkId}/content.
1076
+
1077
+ Link content is rendered from the extracted HTML; text and asset bookmarks use
1078
+ their stored or extracted text. The endpoint returns a bounded chunk plus an
1079
+ opaque `nextCursor`; pass that cursor back to continue reading, or set
1080
+ `fetch_all=True` to let this method walk the cursors and return the whole
1081
+ document as a single object.
1082
+
1083
+ Args:
1084
+ bookmark_id: The ID (string) of the bookmark to read.
1085
+ format: Readable representation, "markdown" or "text" (optional). If omitted
1086
+ together with a cursor, the cursor's own format is reused; otherwise
1087
+ the API defaults to "markdown".
1088
+ max_chars: Maximum number of Unicode characters per chunk, 1 to 50000
1089
+ (optional, API default 12000). A chunk may end earlier, at a
1090
+ paragraph or line boundary.
1091
+ cursor: Opaque continuation cursor returned as `nextCursor` by a previous
1092
+ response (optional).
1093
+ fetch_all: If True, keep following `nextCursor` until the document is
1094
+ exhausted and return one merged object whose `content` is the
1095
+ concatenation of every chunk, with `nextCursor=None` and
1096
+ `truncated=False` (default: False).
1097
+
1098
+ Returns:
1099
+ datatypes.BookmarkReadableContent: A chunk of readable content (or the whole
1100
+ document when fetch_all is True).
1101
+ If response validation is disabled, returns the raw API response (dict/list).
1102
+
1103
+ Raises:
1104
+ APIError: If the API request fails (e.g. 404 bookmark not found, or 409 if
1105
+ the bookmark content changed after the supplied cursor was issued).
1106
+ pydantic.ValidationError: If response validation fails (and is not disabled).
1107
+ """
1108
+ endpoint = f"bookmarks/{bookmark_id}/content"
1109
+ params = {
1110
+ "format": format,
1111
+ "maxChars": max_chars,
1112
+ "cursor": cursor,
1113
+ }
1114
+ response_data = self._call("GET", endpoint, params=params)
1115
+
1116
+ if fetch_all:
1117
+ # Walk the cursor chain and merge the chunks. The raw dicts are merged
1118
+ # rather than the validated models so that the loop behaves identically
1119
+ # whether or not response validation is enabled.
1120
+ if not isinstance(response_data, dict):
1121
+ raise APIError(
1122
+ f"Unexpected response format for get_bookmark_readable_content: {response_data}"
1123
+ )
1124
+ merged = dict(response_data)
1125
+ chunks = [merged.get("content", "")]
1126
+ next_cursor = merged.get("nextCursor")
1127
+ while next_cursor:
1128
+ params["cursor"] = next_cursor
1129
+ # The format is carried by the cursor itself, so it is left as passed.
1130
+ page = self._call("GET", endpoint, params=params)
1131
+ if not isinstance(page, dict):
1132
+ raise APIError(
1133
+ f"Unexpected response format for get_bookmark_readable_content: {page}"
1134
+ )
1135
+ chunks.append(page.get("content", ""))
1136
+ # Keep the latest range end/total: the server may only know the true
1137
+ # total once it has rendered further into the document.
1138
+ if "range" in page and "range" in merged:
1139
+ merged["range"] = {
1140
+ **page["range"],
1141
+ "start": merged["range"].get("start", 0),
1142
+ }
1143
+ merged["contentVersion"] = page.get(
1144
+ "contentVersion", merged.get("contentVersion")
1145
+ )
1146
+ next_cursor = page.get("nextCursor")
1147
+ merged["content"] = "".join(chunks)
1148
+ merged["nextCursor"] = None
1149
+ merged["truncated"] = False
1150
+ response_data = merged
1151
+
1152
+ if self.disable_response_validation:
1153
+ logger.debug("Skipping response validation as requested.")
1154
+ return response_data
1155
+ else:
1156
+ # Response should match BookmarkReadableContent schema
1157
+ return datatypes.BookmarkReadableContent.model_validate(response_data)
1158
+
1054
1159
  @optional_typecheck
1055
1160
  def summarize_a_bookmark(self, bookmark_id: str) -> Dict[str, Any]:
1056
1161
  """
@@ -2481,6 +2586,44 @@ class KarakeepAPI:
2481
2586
  logger.error(error_msg)
2482
2587
  raise APIError(error_msg)
2483
2588
 
2589
+ @optional_typecheck
2590
+ def get_asset_signed_url(
2591
+ self, asset_id: str
2592
+ ) -> Union[datatypes.SignedAssetUrl, Dict[str, Any], List[Any]]:
2593
+ """
2594
+ Get a temporary signed URL for downloading an asset.
2595
+ Corresponds to GET /assets/{assetId}/signed-url.
2596
+
2597
+ The returned URL embeds its own signature and expiry, so it can be fetched
2598
+ without an API key. This is the way to hand an asset to something that cannot
2599
+ send the Authorization header (a browser tab, a media player, an <img> tag)
2600
+ without proxying the bytes through get_a_single_asset.
2601
+
2602
+ Args:
2603
+ asset_id: The ID (string) of the asset to sign.
2604
+
2605
+ Returns:
2606
+ datatypes.SignedAssetUrl: The asset id, the temporary URL and its expiry.
2607
+ If response validation is disabled, returns the raw API response (dict/list).
2608
+
2609
+ Raises:
2610
+ APIError: If the API request fails.
2611
+ ValueError: If asset_id is empty.
2612
+ pydantic.ValidationError: If response validation fails (and is not disabled).
2613
+ """
2614
+ if not asset_id or not asset_id.strip():
2615
+ raise ValueError("asset_id cannot be empty")
2616
+
2617
+ endpoint = f"assets/{asset_id.strip()}/signed-url"
2618
+ response_data = self._call("GET", endpoint)
2619
+
2620
+ if self.disable_response_validation:
2621
+ logger.debug("Skipping response validation as requested.")
2622
+ return response_data
2623
+ else:
2624
+ # Response should match SignedAssetUrl schema
2625
+ return datatypes.SignedAssetUrl.model_validate(response_data)
2626
+
2484
2627
  # --- Admin: Job Triggers ---
2485
2628
 
2486
2629
  @optional_typecheck
@@ -2488,6 +2631,7 @@ class KarakeepAPI:
2488
2631
  self,
2489
2632
  crawl_status: Literal["success", "failure", "pending", "all"] = "all",
2490
2633
  run_inference: bool = False,
2634
+ modified_within_seconds: Optional[int] = None,
2491
2635
  ) -> Dict[str, Any]:
2492
2636
  """
2493
2637
  Trigger a recrawl of link bookmarks. Admin only.
@@ -2497,6 +2641,9 @@ class KarakeepAPI:
2497
2641
  crawl_status: Filter bookmarks by their current crawl status.
2498
2642
  Use "failure" to retry only failed crawls. Default: "all".
2499
2643
  run_inference: Whether to run AI inference after crawling. Default: False.
2644
+ modified_within_seconds: Only process bookmarks modified within this many
2645
+ seconds. Must be > 0. Omit to process all matching
2646
+ bookmarks (optional).
2500
2647
 
2501
2648
  Returns:
2502
2649
  dict: A dictionary with a "success" boolean field.
@@ -2504,16 +2651,34 @@ class KarakeepAPI:
2504
2651
  Raises:
2505
2652
  APIError: If the API request fails (e.g., 403 admin access required).
2506
2653
  """
2507
- body = {"crawlStatus": crawl_status, "runInference": run_inference}
2654
+ body: Dict[str, Any] = {
2655
+ "crawlStatus": crawl_status,
2656
+ "runInference": run_inference,
2657
+ }
2658
+ # Omitted rather than sent as null: the spec has no null case and the
2659
+ # absence of the key is what means "no time window".
2660
+ if modified_within_seconds is not None:
2661
+ body["modifiedWithinSeconds"] = modified_within_seconds
2508
2662
  return self._call("POST", "admin/jobs/trigger/recrawl", data=body)
2509
2663
 
2510
2664
  @optional_typecheck
2511
- def admin_trigger_reindex(self) -> Dict[str, Any]:
2665
+ def admin_trigger_reindex(
2666
+ self,
2667
+ modified_within_seconds: Optional[int] = None,
2668
+ ) -> Dict[str, Any]:
2512
2669
  """
2513
- Trigger a reindex of all bookmarks in the search engine. Admin only.
2670
+ Trigger a reindex of bookmarks in the search engine. Admin only.
2514
2671
  Corresponds to POST /admin/jobs/trigger/reindex.
2515
2672
 
2516
- Clears the existing index and re-queues all bookmarks for indexing.
2673
+ Without modified_within_seconds this clears the existing index and re-queues
2674
+ every bookmark. With it, only bookmarks modified within that window are
2675
+ re-queued and the existing index is left in place, which makes it usable as a
2676
+ cheap catch-up job rather than a full rebuild.
2677
+
2678
+ Args:
2679
+ modified_within_seconds: Only process bookmarks modified within this many
2680
+ seconds. Must be > 0. Omit to reindex everything
2681
+ from scratch (optional).
2517
2682
 
2518
2683
  Returns:
2519
2684
  dict: A dictionary with a "success" boolean field.
@@ -2521,13 +2686,22 @@ class KarakeepAPI:
2521
2686
  Raises:
2522
2687
  APIError: If the API request fails (e.g., 403 admin access required).
2523
2688
  """
2524
- return self._call("POST", "admin/jobs/trigger/reindex")
2689
+ # The request body is optional in the spec, so nothing is sent when no window
2690
+ # is given; that keeps the "clear and rebuild" behaviour byte-identical to
2691
+ # what older servers expect.
2692
+ body = (
2693
+ None
2694
+ if modified_within_seconds is None
2695
+ else {"modifiedWithinSeconds": modified_within_seconds}
2696
+ )
2697
+ return self._call("POST", "admin/jobs/trigger/reindex", data=body)
2525
2698
 
2526
2699
  @optional_typecheck
2527
2700
  def admin_trigger_inference(
2528
2701
  self,
2529
2702
  type: Literal["tag", "summarize"],
2530
2703
  status: Literal["success", "failure", "pending", "all"] = "all",
2704
+ modified_within_seconds: Optional[int] = None,
2531
2705
  ) -> Dict[str, Any]:
2532
2706
  """
2533
2707
  Trigger AI inference (tagging or summarization) on bookmarks. Admin only.
@@ -2538,6 +2712,9 @@ class KarakeepAPI:
2538
2712
  "summarize" for AI summarization.
2539
2713
  status: Filter bookmarks by their current inference status.
2540
2714
  Use "failure" to retry only failed ones. Default: "all".
2715
+ modified_within_seconds: Only process bookmarks modified within this many
2716
+ seconds. Must be > 0. Omit to process all matching
2717
+ bookmarks (optional).
2541
2718
 
2542
2719
  Returns:
2543
2720
  dict: A dictionary with a "success" boolean field.
@@ -2545,7 +2722,10 @@ class KarakeepAPI:
2545
2722
  Raises:
2546
2723
  APIError: If the API request fails (e.g., 403 admin access required).
2547
2724
  """
2548
- body = {"type": type, "status": status}
2725
+ body: Dict[str, Any] = {"type": type, "status": status}
2726
+ # See admin_trigger_recrawl: the key is omitted rather than sent as null.
2727
+ if modified_within_seconds is not None:
2728
+ body["modifiedWithinSeconds"] = modified_within_seconds
2549
2729
  return self._call("POST", "admin/jobs/trigger/inference", data=body)
2550
2730
 
2551
2731
  # --- Feeds ---