karakeep-python-api 1.6.0__tar.gz → 1.8.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.6.0/karakeep_python_api.egg-info → karakeep_python_api-1.8.0}/PKG-INFO +1 -1
  2. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/karakeep_python_api/datatypes.py +22 -2
  3. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/karakeep_python_api/karakeep_api.py +304 -8
  4. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/karakeep_python_api/openapi_reference.json +1648 -660
  5. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0/karakeep_python_api.egg-info}/PKG-INFO +1 -1
  6. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/setup.py +1 -1
  7. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/tests/test_karakeep_api.py +99 -0
  8. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/LICENSE +0 -0
  9. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/MANIFEST.in +0 -0
  10. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/README.md +0 -0
  11. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/karakeep_python_api/__init__.py +0 -0
  12. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/karakeep_python_api/__main__.py +0 -0
  13. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/karakeep_python_api.egg-info/SOURCES.txt +0 -0
  14. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/karakeep_python_api.egg-info/dependency_links.txt +0 -0
  15. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/karakeep_python_api.egg-info/entry_points.txt +0 -0
  16. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/karakeep_python_api.egg-info/requires.txt +0 -0
  17. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/karakeep_python_api.egg-info/top_level.txt +0 -0
  18. {karakeep_python_api-1.6.0 → karakeep_python_api-1.8.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karakeep_python_api
3
- Version: 1.6.0
3
+ Version: 1.8.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
@@ -100,13 +100,18 @@ class BookmarkAsset(BaseModel):
100
100
  fileName: Optional[str] = None
101
101
 
102
102
 
103
- class Asset(BaseModel):
103
+ class UploadedAsset(BaseModel):
104
104
  assetId: str
105
105
  contentType: str
106
106
  size: float
107
107
  fileName: str
108
108
 
109
109
 
110
+ # Backwards-compatible alias: the upstream OpenAPI schema was renamed
111
+ # from "Asset" to "UploadedAsset".
112
+ Asset = UploadedAsset
113
+
114
+
110
115
  class Bookmark(BaseModel):
111
116
  id: str
112
117
  createdAt: str
@@ -114,7 +119,7 @@ class Bookmark(BaseModel):
114
119
  title: Optional[str] = None
115
120
  archived: bool
116
121
  favourited: bool
117
- taggingStatus: Literal["success", "failure", "pending"]
122
+ taggingStatus: Optional[Literal["success", "failure", "pending"]] = None
118
123
  summarizationStatus: Optional[Literal["success", "failure", "pending"]] = None
119
124
  note: Optional[str] = None
120
125
  summary: Optional[str] = None
@@ -136,6 +141,10 @@ class PaginatedBookmarks(BaseModel):
136
141
  nextCursor: Optional[str] = ""
137
142
 
138
143
 
144
+ class CheckUrlResponse(BaseModel):
145
+ bookmarkId: Optional[str]
146
+
147
+
139
148
  class ListModel(BaseModel):
140
149
  id: str
141
150
  name: str
@@ -180,3 +189,14 @@ class Backup(BaseModel):
180
189
  bookmarkCount: int
181
190
  status: Literal["pending", "success", "failure"]
182
191
  errorMessage: Optional[str] = None
192
+
193
+
194
+ class Feed(BaseModel):
195
+ id: str
196
+ name: str
197
+ url: str
198
+ enabled: bool
199
+ importTags: bool
200
+ lastFetchedStatus: Optional[Literal["success", "failure", "pending"]]
201
+ lastFetchedAt: Optional[str]
202
+ 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.6.0"
88
+ VERSION: str = "1.8.0"
89
89
 
90
90
  def __init__(
91
91
  self,
@@ -930,6 +930,38 @@ class KarakeepAPI:
930
930
  # Response should match PaginatedBookmarks schema
931
931
  return datatypes.PaginatedBookmarks.model_validate(response_data)
932
932
 
933
+ @optional_typecheck
934
+ def check_url(
935
+ self,
936
+ url: str,
937
+ ) -> Union[datatypes.CheckUrlResponse, Dict[str, Any], List[Any]]:
938
+ """
939
+ Check if a URL is already bookmarked. Corresponds to GET /bookmarks/check-url.
940
+ Uses substring matching to find candidates, then normalizes URLs (ignoring hash fragments and trailing slashes) for exact comparison.
941
+
942
+ Args:
943
+ url: The URL to check.
944
+
945
+ Returns:
946
+ datatypes.CheckUrlResponse: Object indicating whether the URL is bookmarked. bookmarkId is null if not found.
947
+ If response validation is disabled, returns the raw API response (dict/list).
948
+
949
+ Raises:
950
+ APIError: If the API request fails.
951
+ pydantic.ValidationError: If response validation fails (and is not disabled).
952
+ """
953
+ params = {
954
+ "url": url,
955
+ }
956
+ response_data = self._call("GET", "bookmarks/check-url", params=params)
957
+
958
+ if self.disable_response_validation:
959
+ logger.debug("Skipping response validation as requested.")
960
+ return response_data
961
+ else:
962
+ # Response should match CheckUrlResponse schema
963
+ return datatypes.CheckUrlResponse.model_validate(response_data)
964
+
933
965
  @optional_typecheck
934
966
  def get_a_single_bookmark(
935
967
  self,
@@ -1048,6 +1080,7 @@ class KarakeepAPI:
1048
1080
  bookmark_id: str,
1049
1081
  tag_ids: Optional[List[str]] = None,
1050
1082
  tag_names: Optional[List[str]] = None,
1083
+ attached_by: Optional[Literal["ai", "human"]] = "human",
1051
1084
  ) -> Dict[str, Any]:
1052
1085
  """
1053
1086
  Attach one or more tags to a bookmark. Corresponds to POST /bookmarks/{bookmarkId}/tags.
@@ -1056,6 +1089,7 @@ class KarakeepAPI:
1056
1089
  bookmark_id: The ID (string) of the bookmark.
1057
1090
  tag_ids: List of existing tag IDs to attach (optional).
1058
1091
  tag_names: List of tag names to attach (will create tags if they don't exist) (optional).
1092
+ attached_by: Who attached the tag, either "ai" or "human" (default: "human").
1059
1093
 
1060
1094
  Returns:
1061
1095
  dict: A dictionary containing the list of attached tag IDs under the key "attached".
@@ -1097,11 +1131,17 @@ class KarakeepAPI:
1097
1131
 
1098
1132
  if tag_ids:
1099
1133
  for tag_id in tag_ids:
1100
- tags_list.append({"tagId": tag_id.strip()})
1134
+ tag_entry: Dict[str, Any] = {"tagId": tag_id.strip()}
1135
+ if attached_by is not None:
1136
+ tag_entry["attachedBy"] = attached_by
1137
+ tags_list.append(tag_entry)
1101
1138
 
1102
1139
  if tag_names:
1103
1140
  for tag_name in tag_names:
1104
- tags_list.append({"tagName": tag_name.strip()})
1141
+ tag_entry = {"tagName": tag_name.strip()}
1142
+ if attached_by is not None:
1143
+ tag_entry["attachedBy"] = attached_by
1144
+ tags_list.append(tag_entry)
1105
1145
 
1106
1146
  tags_data = {"tags": tags_list}
1107
1147
 
@@ -1123,6 +1163,7 @@ class KarakeepAPI:
1123
1163
  bookmark_id: str,
1124
1164
  tag_ids: Optional[List[str]] = None,
1125
1165
  tag_names: Optional[List[str]] = None,
1166
+ attached_by: Optional[Literal["ai", "human"]] = "human",
1126
1167
  ) -> Dict[str, Any]:
1127
1168
  """
1128
1169
  Detach one or more tags from a bookmark. Corresponds to DELETE /bookmarks/{bookmarkId}/tags.
@@ -1131,6 +1172,7 @@ class KarakeepAPI:
1131
1172
  bookmark_id: The ID (string) of the bookmark.
1132
1173
  tag_ids: List of existing tag IDs to detach (optional).
1133
1174
  tag_names: List of tag names to detach (optional).
1175
+ attached_by: Who attached the tag, either "ai" or "human" (default: "human").
1134
1176
 
1135
1177
  Returns:
1136
1178
  dict: A dictionary containing the list of detached tag IDs under the key "detached".
@@ -1172,11 +1214,17 @@ class KarakeepAPI:
1172
1214
 
1173
1215
  if tag_ids:
1174
1216
  for tag_id in tag_ids:
1175
- tags_list.append({"tagId": tag_id.strip()})
1217
+ tag_entry: Dict[str, Any] = {"tagId": tag_id.strip()}
1218
+ if attached_by is not None:
1219
+ tag_entry["attachedBy"] = attached_by
1220
+ tags_list.append(tag_entry)
1176
1221
 
1177
1222
  if tag_names:
1178
1223
  for tag_name in tag_names:
1179
- tags_list.append({"tagName": tag_name.strip()})
1224
+ tag_entry = {"tagName": tag_name.strip()}
1225
+ if attached_by is not None:
1226
+ tag_entry["attachedBy"] = attached_by
1227
+ tags_list.append(tag_entry)
1180
1228
 
1181
1229
  tags_data = {"tags": tags_list}
1182
1230
 
@@ -2134,7 +2182,7 @@ class KarakeepAPI:
2134
2182
  @optional_typecheck
2135
2183
  def upload_a_new_asset(
2136
2184
  self, file: str
2137
- ) -> Union[datatypes.Asset, Dict[str, Any], List[Any]]:
2185
+ ) -> Union[datatypes.UploadedAsset, Dict[str, Any], List[Any]]:
2138
2186
  """
2139
2187
  Upload a new asset file. Corresponds to POST /assets.
2140
2188
 
@@ -2142,7 +2190,7 @@ class KarakeepAPI:
2142
2190
  file: Path to the file to upload.
2143
2191
 
2144
2192
  Returns:
2145
- datatypes.Asset: Details about the uploaded asset (assetId, contentType, size, fileName).
2193
+ datatypes.UploadedAsset: Details about the uploaded asset (assetId, contentType, size, fileName).
2146
2194
  If response validation is disabled, returns the raw API response (dict/list).
2147
2195
 
2148
2196
  Raises:
@@ -2185,7 +2233,7 @@ class KarakeepAPI:
2185
2233
  return response_data
2186
2234
  else:
2187
2235
  # Response should match Asset schema
2188
- return datatypes.Asset.model_validate(response_data)
2236
+ return datatypes.UploadedAsset.model_validate(response_data)
2189
2237
 
2190
2238
  @optional_typecheck
2191
2239
  def get_all_backups(
@@ -2432,3 +2480,251 @@ class KarakeepAPI:
2432
2480
 
2433
2481
  logger.error(error_msg)
2434
2482
  raise APIError(error_msg)
2483
+
2484
+ # --- Admin: Job Triggers ---
2485
+
2486
+ @optional_typecheck
2487
+ def admin_trigger_recrawl(
2488
+ self,
2489
+ crawl_status: Literal["success", "failure", "pending", "all"] = "all",
2490
+ run_inference: bool = False,
2491
+ ) -> Dict[str, Any]:
2492
+ """
2493
+ Trigger a recrawl of link bookmarks. Admin only.
2494
+ Corresponds to POST /admin/jobs/trigger/recrawl.
2495
+
2496
+ Args:
2497
+ crawl_status: Filter bookmarks by their current crawl status.
2498
+ Use "failure" to retry only failed crawls. Default: "all".
2499
+ run_inference: Whether to run AI inference after crawling. Default: False.
2500
+
2501
+ Returns:
2502
+ dict: A dictionary with a "success" boolean field.
2503
+
2504
+ Raises:
2505
+ APIError: If the API request fails (e.g., 403 admin access required).
2506
+ """
2507
+ body = {"crawlStatus": crawl_status, "runInference": run_inference}
2508
+ return self._call("POST", "admin/jobs/trigger/recrawl", data=body)
2509
+
2510
+ @optional_typecheck
2511
+ def admin_trigger_reindex(self) -> Dict[str, Any]:
2512
+ """
2513
+ Trigger a reindex of all bookmarks in the search engine. Admin only.
2514
+ Corresponds to POST /admin/jobs/trigger/reindex.
2515
+
2516
+ Clears the existing index and re-queues all bookmarks for indexing.
2517
+
2518
+ Returns:
2519
+ dict: A dictionary with a "success" boolean field.
2520
+
2521
+ Raises:
2522
+ APIError: If the API request fails (e.g., 403 admin access required).
2523
+ """
2524
+ return self._call("POST", "admin/jobs/trigger/reindex")
2525
+
2526
+ @optional_typecheck
2527
+ def admin_trigger_inference(
2528
+ self,
2529
+ type: Literal["tag", "summarize"],
2530
+ status: Literal["success", "failure", "pending", "all"] = "all",
2531
+ ) -> Dict[str, Any]:
2532
+ """
2533
+ Trigger AI inference (tagging or summarization) on bookmarks. Admin only.
2534
+ Corresponds to POST /admin/jobs/trigger/inference.
2535
+
2536
+ Args:
2537
+ type: The type of inference to run: "tag" for AI tagging,
2538
+ "summarize" for AI summarization.
2539
+ status: Filter bookmarks by their current inference status.
2540
+ Use "failure" to retry only failed ones. Default: "all".
2541
+
2542
+ Returns:
2543
+ dict: A dictionary with a "success" boolean field.
2544
+
2545
+ Raises:
2546
+ APIError: If the API request fails (e.g., 403 admin access required).
2547
+ """
2548
+ body = {"type": type, "status": status}
2549
+ return self._call("POST", "admin/jobs/trigger/inference", data=body)
2550
+
2551
+ # --- Feeds ---
2552
+
2553
+ @optional_typecheck
2554
+ def get_all_feeds(
2555
+ self,
2556
+ ) -> Union[List[datatypes.Feed], Dict[str, Any], List[Any]]:
2557
+ """
2558
+ Get all RSS feed subscriptions for the current user. Corresponds to GET /feeds.
2559
+
2560
+ Returns:
2561
+ List[datatypes.Feed]: A list of feed objects.
2562
+ If response validation is disabled, returns the raw API response (dict/list).
2563
+
2564
+ Raises:
2565
+ APIError: If the API request fails.
2566
+ pydantic.ValidationError: If response validation fails (and is not disabled).
2567
+ """
2568
+ response_data = self._call("GET", "feeds")
2569
+
2570
+ if self.disable_response_validation:
2571
+ logger.debug("Skipping response validation as requested.")
2572
+ return response_data
2573
+ if (
2574
+ isinstance(response_data, dict)
2575
+ and "feeds" in response_data
2576
+ and isinstance(response_data["feeds"], list)
2577
+ ):
2578
+ return [
2579
+ datatypes.Feed.model_validate(feed) for feed in response_data["feeds"]
2580
+ ]
2581
+ raise APIError(
2582
+ f"Unexpected response format for get_all_feeds when validation is enabled: {response_data}"
2583
+ )
2584
+
2585
+ @optional_typecheck
2586
+ def create_a_new_feed(
2587
+ self,
2588
+ name: str,
2589
+ url: str,
2590
+ enabled: bool = True,
2591
+ import_tags: bool = False,
2592
+ ) -> Union[datatypes.Feed, Dict[str, Any], List[Any]]:
2593
+ """
2594
+ Create a new RSS feed subscription. Corresponds to POST /feeds.
2595
+
2596
+ Args:
2597
+ name: Display name for the feed (1-100 characters).
2598
+ url: The RSS feed URL.
2599
+ enabled: Whether the feed is active and will be fetched (default: True).
2600
+ import_tags: Whether to import tags from the feed items (default: False).
2601
+
2602
+ Returns:
2603
+ datatypes.Feed: The created feed object.
2604
+ If response validation is disabled, returns the raw API response (dict/list).
2605
+
2606
+ Raises:
2607
+ APIError: If the API request fails (e.g., 400 quota exceeded).
2608
+ pydantic.ValidationError: If response validation fails (and is not disabled).
2609
+ """
2610
+ feed_data = {
2611
+ "name": name,
2612
+ "url": url,
2613
+ "enabled": enabled,
2614
+ "importTags": import_tags,
2615
+ }
2616
+ response_data = self._call("POST", "feeds", data=feed_data)
2617
+
2618
+ if self.disable_response_validation:
2619
+ logger.debug("Skipping response validation as requested.")
2620
+ return response_data
2621
+ return datatypes.Feed.model_validate(response_data)
2622
+
2623
+ @optional_typecheck
2624
+ def get_a_single_feed(
2625
+ self, feed_id: str
2626
+ ) -> Union[datatypes.Feed, Dict[str, Any], List[Any]]:
2627
+ """
2628
+ Get a single RSS feed by its ID. Corresponds to GET /feeds/{feedId}.
2629
+
2630
+ Args:
2631
+ feed_id: The ID (string) of the feed to retrieve.
2632
+
2633
+ Returns:
2634
+ datatypes.Feed: The requested feed object.
2635
+ If response validation is disabled, returns the raw API response (dict/list).
2636
+
2637
+ Raises:
2638
+ APIError: If the API request fails (e.g., 404 feed not found).
2639
+ """
2640
+ response_data = self._call("GET", f"feeds/{feed_id}")
2641
+
2642
+ if self.disable_response_validation:
2643
+ logger.debug("Skipping response validation as requested.")
2644
+ return response_data
2645
+ return datatypes.Feed.model_validate(response_data)
2646
+
2647
+ @optional_typecheck
2648
+ def update_a_feed(
2649
+ self,
2650
+ feed_id: str,
2651
+ name: Optional[str] = None,
2652
+ url: Optional[str] = None,
2653
+ enabled: Optional[bool] = None,
2654
+ import_tags: Optional[bool] = None,
2655
+ ) -> Union[datatypes.Feed, Dict[str, Any], List[Any]]:
2656
+ """
2657
+ Update an RSS feed subscription. Corresponds to PATCH /feeds/{feedId}.
2658
+
2659
+ Args:
2660
+ feed_id: The ID (string) of the feed to update.
2661
+ name: Optional new display name for the feed (1-100 characters).
2662
+ url: Optional new feed URL.
2663
+ enabled: Optional new enabled state.
2664
+ import_tags: Optional new importTags flag.
2665
+
2666
+ Returns:
2667
+ datatypes.Feed: The updated feed object.
2668
+ If response validation is disabled, returns the raw API response (dict/list).
2669
+
2670
+ Raises:
2671
+ ValueError: If no fields are provided to update.
2672
+ APIError: If the API request fails (e.g., 404 feed not found).
2673
+ """
2674
+ update_data: Dict[str, Any] = {}
2675
+ if name is not None:
2676
+ update_data["name"] = name
2677
+ if url is not None:
2678
+ update_data["url"] = url
2679
+ if enabled is not None:
2680
+ update_data["enabled"] = enabled
2681
+ if import_tags is not None:
2682
+ update_data["importTags"] = import_tags
2683
+
2684
+ if not update_data:
2685
+ raise ValueError("At least one field must be provided to update.")
2686
+
2687
+ response_data = self._call("PATCH", f"feeds/{feed_id}", data=update_data)
2688
+
2689
+ if self.disable_response_validation:
2690
+ logger.debug("Skipping response validation as requested.")
2691
+ return response_data
2692
+ return datatypes.Feed.model_validate(response_data)
2693
+
2694
+ @optional_typecheck
2695
+ def delete_a_feed(self, feed_id: str) -> None:
2696
+ """
2697
+ Delete an RSS feed subscription. Corresponds to DELETE /feeds/{feedId}.
2698
+
2699
+ Previously imported bookmarks are not affected.
2700
+
2701
+ Args:
2702
+ feed_id: The ID (string) of the feed to delete.
2703
+
2704
+ Returns:
2705
+ None: Returns None upon successful deletion (204 No Content).
2706
+
2707
+ Raises:
2708
+ APIError: If the API request fails (e.g., 404 feed not found).
2709
+ """
2710
+ self._call("DELETE", f"feeds/{feed_id}")
2711
+ return None
2712
+
2713
+ @optional_typecheck
2714
+ def fetch_a_feed(self, feed_id: str) -> None:
2715
+ """
2716
+ Trigger an immediate fetch of an RSS feed. Corresponds to POST /feeds/{feedId}/fetch.
2717
+
2718
+ The fetch is enqueued and processed asynchronously by the server.
2719
+
2720
+ Args:
2721
+ feed_id: The ID (string) of the feed to fetch.
2722
+
2723
+ Returns:
2724
+ None: Returns None upon successful enqueue (204 No Content).
2725
+
2726
+ Raises:
2727
+ APIError: If the API request fails (e.g., 404 feed not found).
2728
+ """
2729
+ self._call("POST", f"feeds/{feed_id}/fetch")
2730
+ return None