karakeep-python-api 1.7.0__py3-none-any.whl → 1.9.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.
- karakeep_python_api/__main__.py +17 -1
- karakeep_python_api/datatypes.py +65 -2
- karakeep_python_api/karakeep_api.py +433 -5
- karakeep_python_api/openapi_reference.json +2000 -702
- {karakeep_python_api-1.7.0.dist-info → karakeep_python_api-1.9.0.dist-info}/METADATA +7 -2
- karakeep_python_api-1.9.0.dist-info/RECORD +11 -0
- {karakeep_python_api-1.7.0.dist-info → karakeep_python_api-1.9.0.dist-info}/WHEEL +1 -1
- karakeep_python_api-1.7.0.dist-info/RECORD +0 -11
- {karakeep_python_api-1.7.0.dist-info → karakeep_python_api-1.9.0.dist-info}/entry_points.txt +0 -0
- {karakeep_python_api-1.7.0.dist-info → karakeep_python_api-1.9.0.dist-info}/licenses/LICENSE +0 -0
- {karakeep_python_api-1.7.0.dist-info → karakeep_python_api-1.9.0.dist-info}/top_level.txt +0 -0
karakeep_python_api/__main__.py
CHANGED
|
@@ -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
|
|
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(
|
karakeep_python_api/datatypes.py
CHANGED
|
@@ -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
|
|
@@ -100,22 +109,43 @@ class BookmarkAsset(BaseModel):
|
|
|
100
109
|
fileName: Optional[str] = None
|
|
101
110
|
|
|
102
111
|
|
|
103
|
-
class
|
|
112
|
+
class UploadedAsset(BaseModel):
|
|
104
113
|
assetId: str
|
|
105
114
|
contentType: str
|
|
106
115
|
size: float
|
|
107
116
|
fileName: str
|
|
108
117
|
|
|
109
118
|
|
|
119
|
+
# Backwards-compatible alias: the upstream OpenAPI schema was renamed
|
|
120
|
+
# from "Asset" to "UploadedAsset".
|
|
121
|
+
Asset = UploadedAsset
|
|
122
|
+
|
|
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
|
+
|
|
110
132
|
class Bookmark(BaseModel):
|
|
111
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
|
|
112
138
|
createdAt: str
|
|
113
139
|
modifiedAt: Optional[str]
|
|
114
140
|
title: Optional[str] = None
|
|
115
141
|
archived: bool
|
|
116
142
|
favourited: bool
|
|
117
|
-
taggingStatus: Literal["success", "failure", "pending"]
|
|
143
|
+
taggingStatus: Optional[Literal["success", "failure", "pending"]] = None
|
|
118
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"]]
|
|
119
149
|
note: Optional[str] = None
|
|
120
150
|
summary: Optional[str] = None
|
|
121
151
|
source: Optional[
|
|
@@ -131,6 +161,28 @@ class Bookmark(BaseModel):
|
|
|
131
161
|
assets: List[BookmarkAsset]
|
|
132
162
|
|
|
133
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
|
+
|
|
134
186
|
class PaginatedBookmarks(BaseModel):
|
|
135
187
|
bookmarks: List[Bookmark]
|
|
136
188
|
nextCursor: Optional[str] = ""
|
|
@@ -184,3 +236,14 @@ class Backup(BaseModel):
|
|
|
184
236
|
bookmarkCount: int
|
|
185
237
|
status: Literal["pending", "success", "failure"]
|
|
186
238
|
errorMessage: Optional[str] = None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class Feed(BaseModel):
|
|
242
|
+
id: str
|
|
243
|
+
name: str
|
|
244
|
+
url: str
|
|
245
|
+
enabled: bool
|
|
246
|
+
importTags: bool
|
|
247
|
+
lastFetchedStatus: Optional[Literal["success", "failure", "pending"]]
|
|
248
|
+
lastFetchedAt: Optional[str]
|
|
249
|
+
lastSuccessfulFetchAt: 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.
|
|
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
|
|
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
|
"""
|
|
@@ -2182,7 +2287,7 @@ class KarakeepAPI:
|
|
|
2182
2287
|
@optional_typecheck
|
|
2183
2288
|
def upload_a_new_asset(
|
|
2184
2289
|
self, file: str
|
|
2185
|
-
) -> Union[datatypes.
|
|
2290
|
+
) -> Union[datatypes.UploadedAsset, Dict[str, Any], List[Any]]:
|
|
2186
2291
|
"""
|
|
2187
2292
|
Upload a new asset file. Corresponds to POST /assets.
|
|
2188
2293
|
|
|
@@ -2190,7 +2295,7 @@ class KarakeepAPI:
|
|
|
2190
2295
|
file: Path to the file to upload.
|
|
2191
2296
|
|
|
2192
2297
|
Returns:
|
|
2193
|
-
datatypes.
|
|
2298
|
+
datatypes.UploadedAsset: Details about the uploaded asset (assetId, contentType, size, fileName).
|
|
2194
2299
|
If response validation is disabled, returns the raw API response (dict/list).
|
|
2195
2300
|
|
|
2196
2301
|
Raises:
|
|
@@ -2233,7 +2338,7 @@ class KarakeepAPI:
|
|
|
2233
2338
|
return response_data
|
|
2234
2339
|
else:
|
|
2235
2340
|
# Response should match Asset schema
|
|
2236
|
-
return datatypes.
|
|
2341
|
+
return datatypes.UploadedAsset.model_validate(response_data)
|
|
2237
2342
|
|
|
2238
2343
|
@optional_typecheck
|
|
2239
2344
|
def get_all_backups(
|
|
@@ -2480,3 +2585,326 @@ class KarakeepAPI:
|
|
|
2480
2585
|
|
|
2481
2586
|
logger.error(error_msg)
|
|
2482
2587
|
raise APIError(error_msg)
|
|
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
|
+
|
|
2627
|
+
# --- Admin: Job Triggers ---
|
|
2628
|
+
|
|
2629
|
+
@optional_typecheck
|
|
2630
|
+
def admin_trigger_recrawl(
|
|
2631
|
+
self,
|
|
2632
|
+
crawl_status: Literal["success", "failure", "pending", "all"] = "all",
|
|
2633
|
+
run_inference: bool = False,
|
|
2634
|
+
modified_within_seconds: Optional[int] = None,
|
|
2635
|
+
) -> Dict[str, Any]:
|
|
2636
|
+
"""
|
|
2637
|
+
Trigger a recrawl of link bookmarks. Admin only.
|
|
2638
|
+
Corresponds to POST /admin/jobs/trigger/recrawl.
|
|
2639
|
+
|
|
2640
|
+
Args:
|
|
2641
|
+
crawl_status: Filter bookmarks by their current crawl status.
|
|
2642
|
+
Use "failure" to retry only failed crawls. Default: "all".
|
|
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).
|
|
2647
|
+
|
|
2648
|
+
Returns:
|
|
2649
|
+
dict: A dictionary with a "success" boolean field.
|
|
2650
|
+
|
|
2651
|
+
Raises:
|
|
2652
|
+
APIError: If the API request fails (e.g., 403 admin access required).
|
|
2653
|
+
"""
|
|
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
|
|
2662
|
+
return self._call("POST", "admin/jobs/trigger/recrawl", data=body)
|
|
2663
|
+
|
|
2664
|
+
@optional_typecheck
|
|
2665
|
+
def admin_trigger_reindex(
|
|
2666
|
+
self,
|
|
2667
|
+
modified_within_seconds: Optional[int] = None,
|
|
2668
|
+
) -> Dict[str, Any]:
|
|
2669
|
+
"""
|
|
2670
|
+
Trigger a reindex of bookmarks in the search engine. Admin only.
|
|
2671
|
+
Corresponds to POST /admin/jobs/trigger/reindex.
|
|
2672
|
+
|
|
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).
|
|
2682
|
+
|
|
2683
|
+
Returns:
|
|
2684
|
+
dict: A dictionary with a "success" boolean field.
|
|
2685
|
+
|
|
2686
|
+
Raises:
|
|
2687
|
+
APIError: If the API request fails (e.g., 403 admin access required).
|
|
2688
|
+
"""
|
|
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)
|
|
2698
|
+
|
|
2699
|
+
@optional_typecheck
|
|
2700
|
+
def admin_trigger_inference(
|
|
2701
|
+
self,
|
|
2702
|
+
type: Literal["tag", "summarize"],
|
|
2703
|
+
status: Literal["success", "failure", "pending", "all"] = "all",
|
|
2704
|
+
modified_within_seconds: Optional[int] = None,
|
|
2705
|
+
) -> Dict[str, Any]:
|
|
2706
|
+
"""
|
|
2707
|
+
Trigger AI inference (tagging or summarization) on bookmarks. Admin only.
|
|
2708
|
+
Corresponds to POST /admin/jobs/trigger/inference.
|
|
2709
|
+
|
|
2710
|
+
Args:
|
|
2711
|
+
type: The type of inference to run: "tag" for AI tagging,
|
|
2712
|
+
"summarize" for AI summarization.
|
|
2713
|
+
status: Filter bookmarks by their current inference status.
|
|
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).
|
|
2718
|
+
|
|
2719
|
+
Returns:
|
|
2720
|
+
dict: A dictionary with a "success" boolean field.
|
|
2721
|
+
|
|
2722
|
+
Raises:
|
|
2723
|
+
APIError: If the API request fails (e.g., 403 admin access required).
|
|
2724
|
+
"""
|
|
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
|
|
2729
|
+
return self._call("POST", "admin/jobs/trigger/inference", data=body)
|
|
2730
|
+
|
|
2731
|
+
# --- Feeds ---
|
|
2732
|
+
|
|
2733
|
+
@optional_typecheck
|
|
2734
|
+
def get_all_feeds(
|
|
2735
|
+
self,
|
|
2736
|
+
) -> Union[List[datatypes.Feed], Dict[str, Any], List[Any]]:
|
|
2737
|
+
"""
|
|
2738
|
+
Get all RSS feed subscriptions for the current user. Corresponds to GET /feeds.
|
|
2739
|
+
|
|
2740
|
+
Returns:
|
|
2741
|
+
List[datatypes.Feed]: A list of feed objects.
|
|
2742
|
+
If response validation is disabled, returns the raw API response (dict/list).
|
|
2743
|
+
|
|
2744
|
+
Raises:
|
|
2745
|
+
APIError: If the API request fails.
|
|
2746
|
+
pydantic.ValidationError: If response validation fails (and is not disabled).
|
|
2747
|
+
"""
|
|
2748
|
+
response_data = self._call("GET", "feeds")
|
|
2749
|
+
|
|
2750
|
+
if self.disable_response_validation:
|
|
2751
|
+
logger.debug("Skipping response validation as requested.")
|
|
2752
|
+
return response_data
|
|
2753
|
+
if (
|
|
2754
|
+
isinstance(response_data, dict)
|
|
2755
|
+
and "feeds" in response_data
|
|
2756
|
+
and isinstance(response_data["feeds"], list)
|
|
2757
|
+
):
|
|
2758
|
+
return [
|
|
2759
|
+
datatypes.Feed.model_validate(feed) for feed in response_data["feeds"]
|
|
2760
|
+
]
|
|
2761
|
+
raise APIError(
|
|
2762
|
+
f"Unexpected response format for get_all_feeds when validation is enabled: {response_data}"
|
|
2763
|
+
)
|
|
2764
|
+
|
|
2765
|
+
@optional_typecheck
|
|
2766
|
+
def create_a_new_feed(
|
|
2767
|
+
self,
|
|
2768
|
+
name: str,
|
|
2769
|
+
url: str,
|
|
2770
|
+
enabled: bool = True,
|
|
2771
|
+
import_tags: bool = False,
|
|
2772
|
+
) -> Union[datatypes.Feed, Dict[str, Any], List[Any]]:
|
|
2773
|
+
"""
|
|
2774
|
+
Create a new RSS feed subscription. Corresponds to POST /feeds.
|
|
2775
|
+
|
|
2776
|
+
Args:
|
|
2777
|
+
name: Display name for the feed (1-100 characters).
|
|
2778
|
+
url: The RSS feed URL.
|
|
2779
|
+
enabled: Whether the feed is active and will be fetched (default: True).
|
|
2780
|
+
import_tags: Whether to import tags from the feed items (default: False).
|
|
2781
|
+
|
|
2782
|
+
Returns:
|
|
2783
|
+
datatypes.Feed: The created feed object.
|
|
2784
|
+
If response validation is disabled, returns the raw API response (dict/list).
|
|
2785
|
+
|
|
2786
|
+
Raises:
|
|
2787
|
+
APIError: If the API request fails (e.g., 400 quota exceeded).
|
|
2788
|
+
pydantic.ValidationError: If response validation fails (and is not disabled).
|
|
2789
|
+
"""
|
|
2790
|
+
feed_data = {
|
|
2791
|
+
"name": name,
|
|
2792
|
+
"url": url,
|
|
2793
|
+
"enabled": enabled,
|
|
2794
|
+
"importTags": import_tags,
|
|
2795
|
+
}
|
|
2796
|
+
response_data = self._call("POST", "feeds", data=feed_data)
|
|
2797
|
+
|
|
2798
|
+
if self.disable_response_validation:
|
|
2799
|
+
logger.debug("Skipping response validation as requested.")
|
|
2800
|
+
return response_data
|
|
2801
|
+
return datatypes.Feed.model_validate(response_data)
|
|
2802
|
+
|
|
2803
|
+
@optional_typecheck
|
|
2804
|
+
def get_a_single_feed(
|
|
2805
|
+
self, feed_id: str
|
|
2806
|
+
) -> Union[datatypes.Feed, Dict[str, Any], List[Any]]:
|
|
2807
|
+
"""
|
|
2808
|
+
Get a single RSS feed by its ID. Corresponds to GET /feeds/{feedId}.
|
|
2809
|
+
|
|
2810
|
+
Args:
|
|
2811
|
+
feed_id: The ID (string) of the feed to retrieve.
|
|
2812
|
+
|
|
2813
|
+
Returns:
|
|
2814
|
+
datatypes.Feed: The requested feed object.
|
|
2815
|
+
If response validation is disabled, returns the raw API response (dict/list).
|
|
2816
|
+
|
|
2817
|
+
Raises:
|
|
2818
|
+
APIError: If the API request fails (e.g., 404 feed not found).
|
|
2819
|
+
"""
|
|
2820
|
+
response_data = self._call("GET", f"feeds/{feed_id}")
|
|
2821
|
+
|
|
2822
|
+
if self.disable_response_validation:
|
|
2823
|
+
logger.debug("Skipping response validation as requested.")
|
|
2824
|
+
return response_data
|
|
2825
|
+
return datatypes.Feed.model_validate(response_data)
|
|
2826
|
+
|
|
2827
|
+
@optional_typecheck
|
|
2828
|
+
def update_a_feed(
|
|
2829
|
+
self,
|
|
2830
|
+
feed_id: str,
|
|
2831
|
+
name: Optional[str] = None,
|
|
2832
|
+
url: Optional[str] = None,
|
|
2833
|
+
enabled: Optional[bool] = None,
|
|
2834
|
+
import_tags: Optional[bool] = None,
|
|
2835
|
+
) -> Union[datatypes.Feed, Dict[str, Any], List[Any]]:
|
|
2836
|
+
"""
|
|
2837
|
+
Update an RSS feed subscription. Corresponds to PATCH /feeds/{feedId}.
|
|
2838
|
+
|
|
2839
|
+
Args:
|
|
2840
|
+
feed_id: The ID (string) of the feed to update.
|
|
2841
|
+
name: Optional new display name for the feed (1-100 characters).
|
|
2842
|
+
url: Optional new feed URL.
|
|
2843
|
+
enabled: Optional new enabled state.
|
|
2844
|
+
import_tags: Optional new importTags flag.
|
|
2845
|
+
|
|
2846
|
+
Returns:
|
|
2847
|
+
datatypes.Feed: The updated feed object.
|
|
2848
|
+
If response validation is disabled, returns the raw API response (dict/list).
|
|
2849
|
+
|
|
2850
|
+
Raises:
|
|
2851
|
+
ValueError: If no fields are provided to update.
|
|
2852
|
+
APIError: If the API request fails (e.g., 404 feed not found).
|
|
2853
|
+
"""
|
|
2854
|
+
update_data: Dict[str, Any] = {}
|
|
2855
|
+
if name is not None:
|
|
2856
|
+
update_data["name"] = name
|
|
2857
|
+
if url is not None:
|
|
2858
|
+
update_data["url"] = url
|
|
2859
|
+
if enabled is not None:
|
|
2860
|
+
update_data["enabled"] = enabled
|
|
2861
|
+
if import_tags is not None:
|
|
2862
|
+
update_data["importTags"] = import_tags
|
|
2863
|
+
|
|
2864
|
+
if not update_data:
|
|
2865
|
+
raise ValueError("At least one field must be provided to update.")
|
|
2866
|
+
|
|
2867
|
+
response_data = self._call("PATCH", f"feeds/{feed_id}", data=update_data)
|
|
2868
|
+
|
|
2869
|
+
if self.disable_response_validation:
|
|
2870
|
+
logger.debug("Skipping response validation as requested.")
|
|
2871
|
+
return response_data
|
|
2872
|
+
return datatypes.Feed.model_validate(response_data)
|
|
2873
|
+
|
|
2874
|
+
@optional_typecheck
|
|
2875
|
+
def delete_a_feed(self, feed_id: str) -> None:
|
|
2876
|
+
"""
|
|
2877
|
+
Delete an RSS feed subscription. Corresponds to DELETE /feeds/{feedId}.
|
|
2878
|
+
|
|
2879
|
+
Previously imported bookmarks are not affected.
|
|
2880
|
+
|
|
2881
|
+
Args:
|
|
2882
|
+
feed_id: The ID (string) of the feed to delete.
|
|
2883
|
+
|
|
2884
|
+
Returns:
|
|
2885
|
+
None: Returns None upon successful deletion (204 No Content).
|
|
2886
|
+
|
|
2887
|
+
Raises:
|
|
2888
|
+
APIError: If the API request fails (e.g., 404 feed not found).
|
|
2889
|
+
"""
|
|
2890
|
+
self._call("DELETE", f"feeds/{feed_id}")
|
|
2891
|
+
return None
|
|
2892
|
+
|
|
2893
|
+
@optional_typecheck
|
|
2894
|
+
def fetch_a_feed(self, feed_id: str) -> None:
|
|
2895
|
+
"""
|
|
2896
|
+
Trigger an immediate fetch of an RSS feed. Corresponds to POST /feeds/{feedId}/fetch.
|
|
2897
|
+
|
|
2898
|
+
The fetch is enqueued and processed asynchronously by the server.
|
|
2899
|
+
|
|
2900
|
+
Args:
|
|
2901
|
+
feed_id: The ID (string) of the feed to fetch.
|
|
2902
|
+
|
|
2903
|
+
Returns:
|
|
2904
|
+
None: Returns None upon successful enqueue (204 No Content).
|
|
2905
|
+
|
|
2906
|
+
Raises:
|
|
2907
|
+
APIError: If the API request fails (e.g., 404 feed not found).
|
|
2908
|
+
"""
|
|
2909
|
+
self._call("POST", f"feeds/{feed_id}/fetch")
|
|
2910
|
+
return None
|