karakeep-python-api 0.2.2__tar.gz → 1.0.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-0.2.2/karakeep_python_api.egg-info → karakeep_python_api-1.0.0}/PKG-INFO +10 -3
  2. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/README.md +9 -2
  3. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api/karakeep_api.py +110 -31
  4. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0/karakeep_python_api.egg-info}/PKG-INFO +10 -3
  5. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/setup.py +1 -1
  6. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/tests/test_karakeep_api.py +183 -85
  7. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/LICENSE +0 -0
  8. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/MANIFEST.in +0 -0
  9. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api/__init__.py +0 -0
  10. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api/__main__.py +0 -0
  11. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api/datatypes.py +0 -0
  12. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api/openapi_reference.json +0 -0
  13. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api.egg-info/SOURCES.txt +0 -0
  14. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api.egg-info/dependency_links.txt +0 -0
  15. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api.egg-info/entry_points.txt +0 -0
  16. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api.egg-info/requires.txt +0 -0
  17. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api.egg-info/top_level.txt +0 -0
  18. {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karakeep_python_api
3
- Version: 0.2.2
3
+ Version: 1.0.0
4
4
  Summary: Community python client for the Karakeep API.
5
5
  Home-page: https://github.com/thiswillbeyourgithub/karakeep_python_api/
6
6
  License: GPLv3
@@ -52,6 +52,7 @@ A community-developed Python client for the [Karakeep](https://karakeep.app/) AP
52
52
  - [Environment Variables](#environment-variables)
53
53
  - [Command Line Interface (CLI)](#command-line-interface-cli)
54
54
  - [Python Library](#python-library)
55
+ - [Community Scripts](#community-scripts)
55
56
  - [Development](#development)
56
57
  - [License](#license)
57
58
 
@@ -239,11 +240,17 @@ except Exception as e:
239
240
 
240
241
  ```
241
242
 
242
- ### Usage Examples
243
+ ## Community Scripts
243
244
 
244
245
  Examples of the API being used can be found in the [`./examples`](./examples) folder. Don't hesitate to submit yours!
245
246
 
246
- - **Omnivore Archiving Status Updater**: A script to fix the archive status of bookmarks imported from Omnivore into Karakeep. See [`examples/omnivore_archiving_status_updater/README.md`](./examples/omnivore_archiving_status_updater/README.md) for details.
247
+ | Example Script | Description | Documentation |
248
+ |----------------|-------------|---------------|
249
+ | **Add Time-to-Read Tags** | Automatically adds time-to-read tags (`0-5m`, `5-10m`, etc.) to bookmarks based on content length analysis. Includes systemd service and timer files for automated periodic execution. | [`README.md`](./examples/add_time_to_read_tag/README.md) |
250
+ | **List to Tag Converter** | Converts a Karakeep list into tags by adding a specified tag to all bookmarks within that list. | [`README.md`](./examples/list_to_tag/README.md) |
251
+ | **Omnivore Highlights Importer** | Imports highlights from Omnivore export data to Karakeep, with intelligent position detection and bookmark matching. Supports dry-run mode for testing. | [`README.md`](./examples/omnivore_highlights_importer/README.md) |
252
+ | **Omnivore Archiving Status Updater** | Fixes the archived status of bookmarks imported from Omnivore by reading export data and updating Karakeep accordingly. | [`README.md`](./examples/omnivore_archiving_status_updater/README.md) |
253
+ | **Pocket Archiving Status Updater** | Fixes the archived status of bookmarks imported from Pocket by reading export data and updating Karakeep accordingly. | [`README.md`](./examples/pocket_archiving_status_updater/README.md) |
247
254
 
248
255
  ## Development
249
256
 
@@ -17,6 +17,7 @@ A community-developed Python client for the [Karakeep](https://karakeep.app/) AP
17
17
  - [Environment Variables](#environment-variables)
18
18
  - [Command Line Interface (CLI)](#command-line-interface-cli)
19
19
  - [Python Library](#python-library)
20
+ - [Community Scripts](#community-scripts)
20
21
  - [Development](#development)
21
22
  - [License](#license)
22
23
 
@@ -204,11 +205,17 @@ except Exception as e:
204
205
 
205
206
  ```
206
207
 
207
- ### Usage Examples
208
+ ## Community Scripts
208
209
 
209
210
  Examples of the API being used can be found in the [`./examples`](./examples) folder. Don't hesitate to submit yours!
210
211
 
211
- - **Omnivore Archiving Status Updater**: A script to fix the archive status of bookmarks imported from Omnivore into Karakeep. See [`examples/omnivore_archiving_status_updater/README.md`](./examples/omnivore_archiving_status_updater/README.md) for details.
212
+ | Example Script | Description | Documentation |
213
+ |----------------|-------------|---------------|
214
+ | **Add Time-to-Read Tags** | Automatically adds time-to-read tags (`0-5m`, `5-10m`, etc.) to bookmarks based on content length analysis. Includes systemd service and timer files for automated periodic execution. | [`README.md`](./examples/add_time_to_read_tag/README.md) |
215
+ | **List to Tag Converter** | Converts a Karakeep list into tags by adding a specified tag to all bookmarks within that list. | [`README.md`](./examples/list_to_tag/README.md) |
216
+ | **Omnivore Highlights Importer** | Imports highlights from Omnivore export data to Karakeep, with intelligent position detection and bookmark matching. Supports dry-run mode for testing. | [`README.md`](./examples/omnivore_highlights_importer/README.md) |
217
+ | **Omnivore Archiving Status Updater** | Fixes the archived status of bookmarks imported from Omnivore by reading export data and updating Karakeep accordingly. | [`README.md`](./examples/omnivore_archiving_status_updater/README.md) |
218
+ | **Pocket Archiving Status Updater** | Fixes the archived status of bookmarks imported from Pocket by reading export data and updating Karakeep accordingly. | [`README.md`](./examples/pocket_archiving_status_updater/README.md) |
212
219
 
213
220
  ## Development
214
221
 
@@ -85,7 +85,7 @@ class KarakeepAPI:
85
85
  """
86
86
 
87
87
  # Version reflects the client library version, updated by bumpver
88
- VERSION: str = "0.2.2"
88
+ VERSION: str = "1.0.0"
89
89
 
90
90
  def __init__(
91
91
  self,
@@ -95,7 +95,9 @@ class KarakeepAPI:
95
95
  verify_ssl: bool = True,
96
96
  verbose: bool = False,
97
97
  disable_response_validation: Optional[bool] = None,
98
- rate_limit: float = 0.0, # Minimum interval between API calls in seconds
98
+ rate_limit: Union[
99
+ float, int
100
+ ] = 0.0, # Minimum interval between API calls in seconds
99
101
  ):
100
102
  """
101
103
  Initialize the Karakeep API client.
@@ -905,15 +907,21 @@ class KarakeepAPI:
905
907
  return None # Explicitly return None for 204
906
908
 
907
909
  @optional_typecheck
908
- def update_a_bookmark(self, bookmark_id: str, update_data: dict) -> Dict[str, Any]:
910
+ def update_a_bookmark(
911
+ self,
912
+ bookmark_id: str,
913
+ update_data: Dict[str, Any],
914
+ ) -> Dict[str, Any]:
909
915
  """
910
916
  Update a bookmark by its ID. Corresponds to PATCH /bookmarks/{bookmarkId}.
911
- Allows updating fields like 'archived', 'favourited', 'summary', 'note', 'title', etc.
917
+ Allows updating various bookmark fields including metadata, content, and status.
912
918
 
913
919
  Args:
914
920
  bookmark_id: The ID (string) of the bookmark to update.
915
- update_data: A dictionary containing the fields to update (e.g., `{"archived": True}`).
916
- See the OpenAPI spec for allowed fields in the request body.
921
+ update_data: Dictionary containing the fields to update. Supported keys include:
922
+ 'title', 'archived', 'favourited', 'note', 'summary', 'createdAt',
923
+ 'url', 'description', 'author', 'publisher', 'datePublished',
924
+ 'dateModified', 'text', 'assetContent'.
917
925
 
918
926
  Returns:
919
927
  dict: A dictionary representing the updated bookmark (partial representation).
@@ -922,8 +930,13 @@ class KarakeepAPI:
922
930
  Validation is not performed on this response type by default.
923
931
 
924
932
  Raises:
933
+ ValueError: If update_data is empty or no valid fields are provided to update.
925
934
  APIError: If the API request fails (e.g., 404 bookmark not found).
926
935
  """
936
+ # Ensure at least one field is being updated
937
+ if not update_data:
938
+ raise ValueError("update_data must contain at least one field to update.")
939
+
927
940
  endpoint = f"bookmarks/{bookmark_id}"
928
941
  response_data = self._call("PATCH", endpoint, data=update_data)
929
942
  # The response schema is a subset of Bookmark, return as dict as specified in spec
@@ -979,7 +992,9 @@ class KarakeepAPI:
979
992
  """
980
993
  # Validate that at least one tag source is provided
981
994
  if not tag_ids and not tag_names:
982
- raise ValueError("At least one of 'tag_ids' or 'tag_names' must be provided")
995
+ raise ValueError(
996
+ "At least one of 'tag_ids' or 'tag_names' must be provided"
997
+ )
983
998
 
984
999
  # Validate input types
985
1000
  if tag_ids is not None and not isinstance(tag_ids, list):
@@ -997,7 +1012,9 @@ class KarakeepAPI:
997
1012
  if tag_names:
998
1013
  for i, tag_name in enumerate(tag_names):
999
1014
  if not isinstance(tag_name, str) or not tag_name.strip():
1000
- raise ValueError(f"Tag name at index {i} must be a non-empty string")
1015
+ raise ValueError(
1016
+ f"Tag name at index {i} must be a non-empty string"
1017
+ )
1001
1018
 
1002
1019
  # Construct the tags_data dict in the format expected by the API
1003
1020
  tags_list = []
@@ -1050,7 +1067,9 @@ class KarakeepAPI:
1050
1067
  """
1051
1068
  # Validate that at least one tag source is provided
1052
1069
  if not tag_ids and not tag_names:
1053
- raise ValueError("At least one of 'tag_ids' or 'tag_names' must be provided")
1070
+ raise ValueError(
1071
+ "At least one of 'tag_ids' or 'tag_names' must be provided"
1072
+ )
1054
1073
 
1055
1074
  # Validate input types
1056
1075
  if tag_ids is not None and not isinstance(tag_ids, list):
@@ -1068,7 +1087,9 @@ class KarakeepAPI:
1068
1087
  if tag_names:
1069
1088
  for i, tag_name in enumerate(tag_names):
1070
1089
  if not isinstance(tag_name, str) or not tag_name.strip():
1071
- raise ValueError(f"Tag name at index {i} must be a non-empty string")
1090
+ raise ValueError(
1091
+ f"Tag name at index {i} must be a non-empty string"
1092
+ )
1072
1093
 
1073
1094
  # Construct the tags_data dict in the format expected by the API
1074
1095
  tags_list = []
@@ -1145,16 +1166,28 @@ class KarakeepAPI:
1145
1166
 
1146
1167
  @optional_typecheck
1147
1168
  def attach_asset(
1148
- self, bookmark_id: str, asset_data: dict
1169
+ self,
1170
+ bookmark_id: str,
1171
+ asset_id: str,
1172
+ asset_type: Literal[
1173
+ "screenshot",
1174
+ "assetScreenshot",
1175
+ "bannerImage",
1176
+ "fullPageArchive",
1177
+ "video",
1178
+ "bookmarkAsset",
1179
+ "precrawledArchive",
1180
+ "unknown",
1181
+ ],
1149
1182
  ) -> Union[datatypes.Asset, Dict[str, Any], List[Any]]:
1150
1183
  """
1151
1184
  Attach a new asset to a bookmark. Corresponds to POST /bookmarks/{bookmarkId}/assets.
1152
1185
 
1153
1186
  Args:
1154
1187
  bookmark_id: The ID (string) of the bookmark.
1155
- asset_data: Dictionary specifying the asset to attach. Must contain "id" (string) and "assetType" (string enum).
1156
- Example: `{"id": "asset_id_string", "assetType": "screenshot"}`
1157
- See `datatypes.AssetType` enum for possible asset types.
1188
+ asset_id: The ID (string) of the asset to attach.
1189
+ asset_type: The type of asset being attached. Must be one of: "screenshot", "assetScreenshot",
1190
+ "bannerImage", "fullPageArchive", "video", "bookmarkAsset", "precrawledArchive", "unknown".
1158
1191
 
1159
1192
  Returns:
1160
1193
  datatypes.Asset: The attached asset object.
@@ -1164,6 +1197,9 @@ class KarakeepAPI:
1164
1197
  APIError: If the API request fails (e.g., 404 bookmark not found).
1165
1198
  pydantic.ValidationError: If response validation fails (and is not disabled).
1166
1199
  """
1200
+ # Construct the asset data dict as expected by the API
1201
+ asset_data = {"id": asset_id, "assetType": asset_type}
1202
+
1167
1203
  endpoint = f"bookmarks/{bookmark_id}/assets"
1168
1204
  response_data = self._call("POST", endpoint, data=asset_data)
1169
1205
 
@@ -1175,9 +1211,7 @@ class KarakeepAPI:
1175
1211
  return datatypes.Asset.model_validate(response_data)
1176
1212
 
1177
1213
  @optional_typecheck
1178
- def replace_asset(
1179
- self, bookmark_id: str, asset_id: str, new_asset_data: dict
1180
- ) -> None:
1214
+ def replace_asset(self, bookmark_id: str, asset_id: str, new_asset_id: str) -> None:
1181
1215
  """
1182
1216
  Replace an existing asset associated with a bookmark with a new one.
1183
1217
  Corresponds to PUT /bookmarks/{bookmarkId}/assets/{assetId}.
@@ -1185,8 +1219,7 @@ class KarakeepAPI:
1185
1219
  Args:
1186
1220
  bookmark_id: The ID (string) of the bookmark.
1187
1221
  asset_id: The ID (string) of the asset to be replaced.
1188
- new_asset_data: Dictionary specifying the new asset ID. Must contain "assetId" (string).
1189
- Example: `{"assetId": "new_asset_id_string"}`
1222
+ new_asset_id: The ID (string) of the new asset to replace with.
1190
1223
 
1191
1224
  Returns:
1192
1225
  None: Returns None upon successful replacement (204 No Content).
@@ -1194,6 +1227,9 @@ class KarakeepAPI:
1194
1227
  Raises:
1195
1228
  APIError: If the API request fails (e.g., 404 bookmark or asset not found).
1196
1229
  """
1230
+ # Construct the request body as expected by the API
1231
+ new_asset_data = {"assetId": new_asset_id}
1232
+
1197
1233
  endpoint = f"bookmarks/{bookmark_id}/assets/{asset_id}"
1198
1234
  self._call("PUT", endpoint, data=new_asset_data) # Expects 204 No Content
1199
1235
  return None # Explicitly return None for 204
@@ -1367,25 +1403,52 @@ class KarakeepAPI:
1367
1403
 
1368
1404
  @optional_typecheck
1369
1405
  def update_a_list(
1370
- self, list_id: str, update_data: dict
1406
+ self,
1407
+ list_id: str,
1408
+ name: Optional[str] = None,
1409
+ description: Optional[str] = None,
1410
+ icon: Optional[str] = None,
1411
+ parent_id: Optional[str] = None,
1412
+ query: Optional[str] = None,
1371
1413
  ) -> Union[datatypes.ListModel, Dict[str, Any], List[Any]]:
1372
1414
  """
1373
1415
  Update a list by its ID. Corresponds to PATCH /lists/{listId}.
1374
- Allows updating fields like "name", "description", "icon", "parentId", "query".
1416
+ Allows updating various list fields including name, description, icon, parent relationship, and query.
1375
1417
 
1376
1418
  Args:
1377
1419
  list_id: The ID (string) of the list to update.
1378
- update_data: A dictionary containing the fields to update (e.g., `{"name": "new name"}`).
1379
- See the OpenAPI spec for allowed fields.
1420
+ name: Optional new name for the list (1-40 characters).
1421
+ description: Optional new description for the list (0-100 characters, can be None to clear).
1422
+ icon: Optional new icon for the list.
1423
+ parent_id: Optional new parent list ID (can be None to remove parent relationship).
1424
+ query: Optional new query string for smart lists (minimum 1 character).
1380
1425
 
1381
1426
  Returns:
1382
1427
  datatypes.ListModel: The updated list object.
1383
1428
  If response validation is disabled, returns the raw API response (dict/list).
1384
1429
 
1385
1430
  Raises:
1431
+ ValueError: If no fields are provided to update.
1386
1432
  APIError: If the API request fails (e.g., 404 list not found).
1387
1433
  pydantic.ValidationError: If response validation fails (and is not disabled).
1388
1434
  """
1435
+ # Construct update_data from provided arguments, excluding None values that weren't explicitly passed
1436
+ update_data = {}
1437
+ if name is not None:
1438
+ update_data["name"] = name
1439
+ if description is not None:
1440
+ update_data["description"] = description
1441
+ if icon is not None:
1442
+ update_data["icon"] = icon
1443
+ if parent_id is not None:
1444
+ update_data["parentId"] = parent_id
1445
+ if query is not None:
1446
+ update_data["query"] = query
1447
+
1448
+ # Ensure at least one field is being updated
1449
+ if not update_data:
1450
+ raise ValueError("At least one field must be provided to update.")
1451
+
1389
1452
  endpoint = f"lists/{list_id}"
1390
1453
  response_data = self._call("PATCH", endpoint, data=update_data)
1391
1454
 
@@ -1594,23 +1657,28 @@ class KarakeepAPI:
1594
1657
  return None # Explicitly return None for 204
1595
1658
 
1596
1659
  @optional_typecheck
1597
- def update_a_tag(self, tag_id: str, update_data: dict) -> Dict[str, Any]:
1660
+ def update_a_tag(self, tag_id: str, update_data: Dict[str, Any]) -> Dict[str, Any]:
1598
1661
  """
1599
1662
  Update a tag by its ID. Currently only supports updating the "name".
1600
1663
  Corresponds to PATCH /tags/{tagId}.
1601
1664
 
1602
1665
  Args:
1603
1666
  tag_id: The ID (string) of the tag to update.
1604
- update_data: A dictionary containing the fields to update. Must include "name" (string).
1605
- Example: `{"name": "new tag name"}`
1667
+ update_data: Dictionary containing the fields to update. Supported keys include:
1668
+ 'name' (string).
1606
1669
 
1607
1670
  Returns:
1608
1671
  dict: A dictionary containing the updated tag information with "id" and "name" fields.
1609
1672
  Validation is not performed on this response type by default.
1610
1673
 
1611
1674
  Raises:
1675
+ ValueError: If update_data is empty or no valid fields are provided to update.
1612
1676
  APIError: If the API request fails (e.g., 404 tag not found).
1613
1677
  """
1678
+ # Ensure at least one field is being updated
1679
+ if not update_data:
1680
+ raise ValueError("update_data must contain at least one field to update.")
1681
+
1614
1682
  endpoint = f"tags/{tag_id}"
1615
1683
  response_data = self._call("PATCH", endpoint, data=update_data)
1616
1684
  # Response schema is a simple dict with id and name, return as dict
@@ -1693,8 +1761,8 @@ class KarakeepAPI:
1693
1761
  def create_a_new_highlight(
1694
1762
  self,
1695
1763
  bookmark_id: str,
1696
- start_offset: float,
1697
- end_offset: float,
1764
+ start_offset: Union[float, int],
1765
+ end_offset: Union[float, int],
1698
1766
  color: Optional[Literal["yellow", "red", "green", "blue"]] = "yellow",
1699
1767
  text: Optional[str] = None,
1700
1768
  note: Optional[str] = None,
@@ -1796,7 +1864,9 @@ class KarakeepAPI:
1796
1864
 
1797
1865
  @optional_typecheck
1798
1866
  def update_a_highlight(
1799
- self, highlight_id: str, update_data: dict
1867
+ self,
1868
+ highlight_id: str,
1869
+ color: Optional[Literal["yellow", "red", "green", "blue"]] = None,
1800
1870
  ) -> Union[datatypes.Highlight, Dict[str, Any], List[Any]]:
1801
1871
  """
1802
1872
  Update a highlight by its ID. Currently only supports updating the "color".
@@ -1804,17 +1874,26 @@ class KarakeepAPI:
1804
1874
 
1805
1875
  Args:
1806
1876
  highlight_id: The ID (string) of the highlight to update.
1807
- update_data: A dictionary containing the fields to update. Must include "color" (string enum).
1808
- See `datatypes.Color` enum. Example: `{"color": "red"}`
1877
+ color: Optional new color for the highlight ("yellow", "red", "green", "blue").
1809
1878
 
1810
1879
  Returns:
1811
1880
  datatypes.Highlight: The updated highlight object.
1812
1881
  If response validation is disabled, returns the raw API response (dict/list).
1813
1882
 
1814
1883
  Raises:
1884
+ ValueError: If no fields are provided to update.
1815
1885
  APIError: If the API request fails (e.g., 404 highlight not found).
1816
1886
  pydantic.ValidationError: If response validation fails (and is not disabled).
1817
1887
  """
1888
+ # Construct update_data from provided arguments, excluding None values
1889
+ update_data = {}
1890
+ if color is not None:
1891
+ update_data["color"] = color
1892
+
1893
+ # Ensure at least one field is being updated
1894
+ if not update_data:
1895
+ raise ValueError("At least one field must be provided to update.")
1896
+
1818
1897
  endpoint = f"highlights/{highlight_id}"
1819
1898
  response_data = self._call("PATCH", endpoint, data=update_data)
1820
1899
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karakeep_python_api
3
- Version: 0.2.2
3
+ Version: 1.0.0
4
4
  Summary: Community python client for the Karakeep API.
5
5
  Home-page: https://github.com/thiswillbeyourgithub/karakeep_python_api/
6
6
  License: GPLv3
@@ -52,6 +52,7 @@ A community-developed Python client for the [Karakeep](https://karakeep.app/) AP
52
52
  - [Environment Variables](#environment-variables)
53
53
  - [Command Line Interface (CLI)](#command-line-interface-cli)
54
54
  - [Python Library](#python-library)
55
+ - [Community Scripts](#community-scripts)
55
56
  - [Development](#development)
56
57
  - [License](#license)
57
58
 
@@ -239,11 +240,17 @@ except Exception as e:
239
240
 
240
241
  ```
241
242
 
242
- ### Usage Examples
243
+ ## Community Scripts
243
244
 
244
245
  Examples of the API being used can be found in the [`./examples`](./examples) folder. Don't hesitate to submit yours!
245
246
 
246
- - **Omnivore Archiving Status Updater**: A script to fix the archive status of bookmarks imported from Omnivore into Karakeep. See [`examples/omnivore_archiving_status_updater/README.md`](./examples/omnivore_archiving_status_updater/README.md) for details.
247
+ | Example Script | Description | Documentation |
248
+ |----------------|-------------|---------------|
249
+ | **Add Time-to-Read Tags** | Automatically adds time-to-read tags (`0-5m`, `5-10m`, etc.) to bookmarks based on content length analysis. Includes systemd service and timer files for automated periodic execution. | [`README.md`](./examples/add_time_to_read_tag/README.md) |
250
+ | **List to Tag Converter** | Converts a Karakeep list into tags by adding a specified tag to all bookmarks within that list. | [`README.md`](./examples/list_to_tag/README.md) |
251
+ | **Omnivore Highlights Importer** | Imports highlights from Omnivore export data to Karakeep, with intelligent position detection and bookmark matching. Supports dry-run mode for testing. | [`README.md`](./examples/omnivore_highlights_importer/README.md) |
252
+ | **Omnivore Archiving Status Updater** | Fixes the archived status of bookmarks imported from Omnivore by reading export data and updating Karakeep accordingly. | [`README.md`](./examples/omnivore_archiving_status_updater/README.md) |
253
+ | **Pocket Archiving Status Updater** | Fixes the archived status of bookmarks imported from Pocket by reading export data and updating Karakeep accordingly. | [`README.md`](./examples/pocket_archiving_status_updater/README.md) |
247
254
 
248
255
  ## Development
249
256
 
@@ -7,7 +7,7 @@ with open("README.md", "r") as readme:
7
7
 
8
8
  setup(
9
9
  name="karakeep_python_api",
10
- version="0.2.2",
10
+ version="1.0.0",
11
11
  description="Community python client for the Karakeep API.", # Simplified description
12
12
  long_description=long_description,
13
13
  long_description_content_type="text/markdown",
@@ -6,7 +6,7 @@ import random
6
6
  import string
7
7
  import time
8
8
  import beartype # to trigger the runtime typechecking
9
- import json # Added for CLI test payload generation
9
+ import json # Added for CLI test payload generation
10
10
 
11
11
  # Import API, errors, and datatypes from the main package
12
12
  from karakeep_python_api import KarakeepAPI, APIError, AuthenticationError, datatypes
@@ -30,12 +30,16 @@ def test_get_all_bookmarks_paginated(karakeep_client: KarakeepAPI):
30
30
 
31
31
  # If there's a next cursor, get the next page
32
32
  if page1.nextCursor:
33
- logger.info(f" Attempting to fetch next page with cursor: {page1.nextCursor}")
33
+ logger.info(
34
+ f" Attempting to fetch next page with cursor: {page1.nextCursor}"
35
+ )
34
36
  page2 = karakeep_client.get_all_bookmarks(limit=2, cursor=page1.nextCursor)
35
37
  assert isinstance(page2, datatypes.PaginatedBookmarks)
36
38
  assert isinstance(page2.bookmarks, list)
37
39
  assert len(page2.bookmarks) <= 2
38
- logger.info(f"✓ Retrieved second page with {len(page2.bookmarks)} bookmarks.")
40
+ logger.info(
41
+ f"✓ Retrieved second page with {len(page2.bookmarks)} bookmarks."
42
+ )
39
43
  # Ensure bookmarks are different from page 1 (simple check)
40
44
  if page1.bookmarks and page2.bookmarks:
41
45
  assert (
@@ -163,12 +167,16 @@ def test_get_all_highlights_paginated(karakeep_client: KarakeepAPI):
163
167
 
164
168
  # If there's a next cursor, get the next page
165
169
  if page1.nextCursor:
166
- logger.info(f" Attempting to fetch next page with cursor: {page1.nextCursor}")
170
+ logger.info(
171
+ f" Attempting to fetch next page with cursor: {page1.nextCursor}"
172
+ )
167
173
  page2 = karakeep_client.get_all_highlights(limit=3, cursor=page1.nextCursor)
168
174
  assert isinstance(page2, datatypes.PaginatedHighlights)
169
175
  assert isinstance(page2.highlights, list)
170
176
  assert len(page2.highlights) <= 3
171
- logger.info(f"✓ Retrieved second page with {len(page2.highlights)} highlights.")
177
+ logger.info(
178
+ f"✓ Retrieved second page with {len(page2.highlights)} highlights."
179
+ )
172
180
  # Ensure highlights are different from page 1 (simple check)
173
181
  if page1.highlights and page2.highlights:
174
182
  assert (
@@ -257,11 +265,15 @@ def test_create_and_delete_list(karakeep_client: KarakeepAPI):
257
265
  try:
258
266
  # 1. Generate a unique list name
259
267
  timestamp = int(time.time())
260
- random_suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
268
+ random_suffix = "".join(
269
+ random.choices(string.ascii_lowercase + string.digits, k=6)
270
+ )
261
271
  list_name = f"Test List {timestamp}-{random_suffix}"
262
272
  list_icon = "🧪" # Test tube icon
263
273
 
264
- logger.info(f"\nAttempting to create list: Name='{list_name}', Icon='{list_icon}'")
274
+ logger.info(
275
+ f"\nAttempting to create list: Name='{list_name}', Icon='{list_icon}'"
276
+ )
265
277
 
266
278
  # 2. Get initial list count (optional, for comparison)
267
279
  initial_lists = karakeep_client.get_all_lists()
@@ -270,9 +282,7 @@ def test_create_and_delete_list(karakeep_client: KarakeepAPI):
270
282
 
271
283
  # 3. Create the new list
272
284
  created_list = karakeep_client.create_a_new_list(
273
- name=list_name,
274
- icon=list_icon,
275
- list_type="manual"
285
+ name=list_name, icon=list_icon, list_type="manual"
276
286
  )
277
287
  assert isinstance(
278
288
  created_list, datatypes.ListModel
@@ -294,7 +304,6 @@ def test_create_and_delete_list(karakeep_client: KarakeepAPI):
294
304
  logger.info(f" List count after creation: {len(current_lists_after_create)}")
295
305
  logger.info(f"✓ Verified list {created_list_id} is present in get_all_lists.")
296
306
 
297
-
298
307
  # 5. Verify the list exists by getting it directly (redundant but good check)
299
308
  retrieved_list = karakeep_client.get_a_single_list(list_id=created_list_id)
300
309
  assert isinstance(retrieved_list, datatypes.ListModel)
@@ -304,7 +313,9 @@ def test_create_and_delete_list(karakeep_client: KarakeepAPI):
304
313
  except (APIError, AuthenticationError) as e:
305
314
  pytest.fail(f"API error during list creation/verification: {e}")
306
315
  except Exception as e:
307
- pytest.fail(f"An unexpected error occurred during list creation/verification: {e}")
316
+ pytest.fail(
317
+ f"An unexpected error occurred during list creation/verification: {e}"
318
+ )
308
319
  finally:
309
320
  # 6. Delete the list (ensure cleanup even if assertions fail)
310
321
  if created_list_id:
@@ -342,21 +353,27 @@ def test_create_and_delete_list(karakeep_client: KarakeepAPI):
342
353
  except Exception as e:
343
354
  pytest.fail(f"An unexpected error occurred during list deletion: {e}")
344
355
  else:
345
- logger.info("\nSkipping deletion because list creation failed or ID was not obtained.")
356
+ logger.info(
357
+ "\nSkipping deletion because list creation failed or ID was not obtained."
358
+ )
346
359
 
347
360
 
348
- def test_create_and_delete_bookmark(karakeep_client: KarakeepAPI, managed_bookmark: datatypes.Bookmark):
361
+ def test_create_and_delete_bookmark(
362
+ karakeep_client: KarakeepAPI, managed_bookmark: datatypes.Bookmark
363
+ ):
349
364
  """
350
365
  Test verifying a created bookmark (via fixture) and searching for it.
351
366
  The fixture handles creation and deletion.
352
367
  """
353
368
  created_bookmark_id = managed_bookmark.id
354
- test_url = managed_bookmark.content.url # Get URL from fixture
355
- original_title = managed_bookmark.title # Get title from fixture
369
+ test_url = managed_bookmark.content.url # Get URL from fixture
370
+ original_title = managed_bookmark.title # Get title from fixture
356
371
 
357
372
  try:
358
373
  # 1. Bookmark is already created by the 'managed_bookmark' fixture.
359
- logger.info(f"\nUsing managed bookmark ID: {created_bookmark_id}, URL: '{test_url}', Title: '{original_title}'")
374
+ logger.info(
375
+ f"\nUsing managed bookmark ID: {created_bookmark_id}, URL: '{test_url}', Title: '{original_title}'"
376
+ )
360
377
 
361
378
  # 2. Verify the bookmark exists by getting it directly
362
379
  retrieved_bookmark = karakeep_client.get_a_single_bookmark(
@@ -368,14 +385,13 @@ def test_create_and_delete_bookmark(karakeep_client: KarakeepAPI, managed_bookma
368
385
  assert retrieved_bookmark.title == original_title
369
386
  logger.info(f"✓ Successfully retrieved the managed bookmark by ID.")
370
387
 
371
-
372
388
  # 3. Search for the created bookmark
373
389
  # Use a search query that is likely to match the fixture's title
374
390
  # The fixture title is "Managed Fixture Bookmark {timestamp}-{random_suffix}"
375
391
  # A simple search for "Managed Fixture Bookmark" should work.
376
392
  # If the title is very dynamic, searching by URL might be more robust if supported,
377
393
  # or by a known part of the title.
378
-
394
+
379
395
  # waiting a bit for the indexation just in case
380
396
  time.sleep(30)
381
397
 
@@ -388,27 +404,38 @@ def test_create_and_delete_bookmark(karakeep_client: KarakeepAPI, managed_bookma
388
404
  '"fixture"',
389
405
  ]
390
406
  for trial, search_query_component in enumerate(search_queries):
391
- logger.info(f"\nAttempting to search for bookmark with query based on title: '{search_query_component}'. Retrying multiple times because search is nondeterministic.")
392
- search_results = karakeep_client.search_bookmarks(q=search_query_component, limit=100, include_content=False)
407
+ logger.info(
408
+ f"\nAttempting to search for bookmark with query based on title: '{search_query_component}'. Retrying multiple times because search is nondeterministic."
409
+ )
410
+ search_results = karakeep_client.search_bookmarks(
411
+ q=search_query_component, limit=100, include_content=False
412
+ )
393
413
  assert isinstance(
394
414
  search_results, datatypes.PaginatedBookmarks
395
415
  ), "Search response should be PaginatedBookmarks model"
396
416
  assert isinstance(
397
417
  search_results.bookmarks, list
398
418
  ), "Search results bookmarks attribute should be a list"
399
-
419
+
400
420
  titles_in_search = [b.title for b in search_results.bookmarks]
401
- found_in_search = any(b.id == created_bookmark_id for b in search_results.bookmarks)
421
+ found_in_search = any(
422
+ b.id == created_bookmark_id for b in search_results.bookmarks
423
+ )
402
424
  if found_in_search:
403
425
  break
404
426
  else:
405
427
  time.sleep(3)
406
- assert found_in_search, \
407
- f"Managed bookmark {created_bookmark_id} (Title: '{original_title}') not found in {trial + 1} different search results for '{search_query_component}'. Titles were: '{titles_in_search}'."
408
- logger.info(f" Found managed bookmark in search results for '{search_query_component}'.")
428
+ assert (
429
+ found_in_search
430
+ ), f"Managed bookmark {created_bookmark_id} (Title: '{original_title}') not found in {trial + 1} different search results for '{search_query_component}'. Titles were: '{titles_in_search}'."
431
+ logger.info(
432
+ f"✓ Found managed bookmark in search results for '{search_query_component}'."
433
+ )
409
434
 
410
435
  # 4. Test CLI search equivalent
411
- logger.info(f"\n Running CLI equivalent: search-bookmarks --q '{search_query_component}' --limit 10 --include-content false")
436
+ logger.info(
437
+ f"\n Running CLI equivalent: search-bookmarks --q '{search_query_component}' --limit 10 --include-content false"
438
+ )
412
439
  try:
413
440
  cli_search_command = f"python -m karakeep_python_api search-bookmarks --q '{search_query_component}' --limit 10 --include-content false"
414
441
  search_cli_output = subprocess.run(
@@ -418,16 +445,23 @@ def test_create_and_delete_bookmark(karakeep_client: KarakeepAPI, managed_bookma
418
445
  capture_output=True,
419
446
  text=True,
420
447
  )
421
- assert created_bookmark_id in search_cli_output.stdout, \
422
- f"Managed bookmark ID {created_bookmark_id} not found in CLI search output for '{search_query_component}'"
423
- logger.info(" CLI search command executed successfully and contained the bookmark ID.")
448
+ assert (
449
+ created_bookmark_id in search_cli_output.stdout
450
+ ), f"Managed bookmark ID {created_bookmark_id} not found in CLI search output for '{search_query_component}'"
451
+ logger.info(
452
+ "✓ CLI search command executed successfully and contained the bookmark ID."
453
+ )
424
454
  except subprocess.CalledProcessError as e:
425
455
  logger.info(f" CLI search command failed with exit code {e.returncode}")
426
456
  logger.info(f" Stdout: {e.stdout}")
427
457
  logger.info(f" Stderr: {e.stderr}")
428
- pytest.fail(f"CLI command 'search-bookmarks --q \"{search_query_component}\"' failed: {e}")
458
+ pytest.fail(
459
+ f"CLI command 'search-bookmarks --q \"{search_query_component}\"' failed: {e}"
460
+ )
429
461
  except Exception as e:
430
- pytest.fail(f"An unexpected error occurred running the CLI search command: {e}")
462
+ pytest.fail(
463
+ f"An unexpected error occurred running the CLI search command: {e}"
464
+ )
431
465
 
432
466
  except (APIError, AuthenticationError) as e:
433
467
  pytest.fail(f"API error during bookmark verification/search: {e}")
@@ -439,46 +473,64 @@ def test_create_and_delete_bookmark(karakeep_client: KarakeepAPI, managed_bookma
439
473
  # The fixture also handles verification of deletion.
440
474
 
441
475
 
442
- def test_update_bookmark_title(karakeep_client: KarakeepAPI, managed_bookmark: datatypes.Bookmark):
476
+ def test_update_bookmark_title(
477
+ karakeep_client: KarakeepAPI, managed_bookmark: datatypes.Bookmark
478
+ ):
443
479
  """Test updating a bookmark's title via API and CLI, using a managed bookmark."""
444
480
  created_bookmark_id = managed_bookmark.id
445
- original_title = managed_bookmark.title # Get the original title from the fixture
446
-
481
+ original_title = managed_bookmark.title # Get the original title from the fixture
482
+
447
483
  target_api_title = "this is a test title"
448
484
  target_cli_title = "this is a test title (CLI)"
449
485
 
450
486
  try:
451
487
  # The bookmark is already created by the 'managed_bookmark' fixture.
452
488
  # We have its ID in created_bookmark_id and its original title.
453
- logger.info(f"\nUsing managed bookmark ID: {created_bookmark_id}, Original Title: '{original_title}'")
489
+ logger.info(
490
+ f"\nUsing managed bookmark ID: {created_bookmark_id}, Original Title: '{original_title}'"
491
+ )
454
492
 
455
493
  # 1. Update the bookmark's title using the API client
456
- logger.info(f"\nAttempting to update bookmark ID {created_bookmark_id} title to: '{target_api_title}' via API")
494
+ logger.info(
495
+ f"\nAttempting to update bookmark ID {created_bookmark_id} title to: '{target_api_title}' via API"
496
+ )
457
497
  update_payload_api = {"title": target_api_title}
458
498
  updated_bookmark_partial = karakeep_client.update_a_bookmark(
459
499
  bookmark_id=created_bookmark_id, update_data=update_payload_api
460
500
  )
461
- assert isinstance(updated_bookmark_partial, dict), "Update response should be a dict"
462
- assert updated_bookmark_partial.get("title") == target_api_title, \
463
- f"Partial response title '{updated_bookmark_partial.get('title')}' does not match target API title '{target_api_title}'"
464
- logger.info(f"✓ API call to update_a_bookmark successful. Partial response title: '{updated_bookmark_partial.get('title')}'")
501
+ assert isinstance(
502
+ updated_bookmark_partial, dict
503
+ ), "Update response should be a dict"
504
+ assert (
505
+ updated_bookmark_partial.get("title") == target_api_title
506
+ ), f"Partial response title '{updated_bookmark_partial.get('title')}' does not match target API title '{target_api_title}'"
507
+ logger.info(
508
+ f"✓ API call to update_a_bookmark successful. Partial response title: '{updated_bookmark_partial.get('title')}'"
509
+ )
465
510
 
466
511
  # 2. Verify the API update by fetching the bookmark again
467
- logger.info(f"\nFetching bookmark ID {created_bookmark_id} to verify API title update.")
512
+ logger.info(
513
+ f"\nFetching bookmark ID {created_bookmark_id} to verify API title update."
514
+ )
468
515
  retrieved_bookmark_after_api_update = karakeep_client.get_a_single_bookmark(
469
516
  bookmark_id=created_bookmark_id
470
517
  )
471
518
  assert isinstance(retrieved_bookmark_after_api_update, datatypes.Bookmark)
472
- assert retrieved_bookmark_after_api_update.title == target_api_title, \
473
- f"Retrieved bookmark title '{retrieved_bookmark_after_api_update.title}' does not match expected API-updated title '{target_api_title}'"
474
- logger.info(f" Successfully verified bookmark title updated by API to: '{retrieved_bookmark_after_api_update.title}'")
519
+ assert (
520
+ retrieved_bookmark_after_api_update.title == target_api_title
521
+ ), f"Retrieved bookmark title '{retrieved_bookmark_after_api_update.title}' does not match expected API-updated title '{target_api_title}'"
522
+ logger.info(
523
+ f"✓ Successfully verified bookmark title updated by API to: '{retrieved_bookmark_after_api_update.title}'"
524
+ )
475
525
 
476
526
  # 3. Test CLI equivalent for updating the bookmark's title
477
- logger.info(f"\n Running CLI equivalent to update title to: '{target_cli_title}'")
527
+ logger.info(
528
+ f"\n Running CLI equivalent to update title to: '{target_cli_title}'"
529
+ )
478
530
  cli_update_payload_json = json.dumps({"title": target_cli_title})
479
531
  # Ensure the JSON string is properly quoted for the shell command
480
532
  cli_update_command = f"python -m karakeep_python_api update-a-bookmark --bookmark-id {created_bookmark_id} --update-data '{cli_update_payload_json}'"
481
-
533
+
482
534
  try:
483
535
  subprocess.run(
484
536
  cli_update_command,
@@ -490,14 +542,19 @@ def test_update_bookmark_title(karakeep_client: KarakeepAPI, managed_bookmark: d
490
542
  logger.info("✓ CLI update command executed successfully.")
491
543
 
492
544
  # 4. Verify CLI update by fetching the bookmark again
493
- logger.info(f"\nFetching bookmark ID {created_bookmark_id} to verify CLI title update.")
545
+ logger.info(
546
+ f"\nFetching bookmark ID {created_bookmark_id} to verify CLI title update."
547
+ )
494
548
  retrieved_bookmark_after_cli_update = karakeep_client.get_a_single_bookmark(
495
549
  bookmark_id=created_bookmark_id
496
550
  )
497
551
  assert isinstance(retrieved_bookmark_after_cli_update, datatypes.Bookmark)
498
- assert retrieved_bookmark_after_cli_update.title == target_cli_title, \
499
- f"Retrieved bookmark title '{retrieved_bookmark_after_cli_update.title}' after CLI update does not match expected '{target_cli_title}'"
500
- logger.info(f" Successfully verified bookmark title updated by CLI to: '{retrieved_bookmark_after_cli_update.title}'")
552
+ assert (
553
+ retrieved_bookmark_after_cli_update.title == target_cli_title
554
+ ), f"Retrieved bookmark title '{retrieved_bookmark_after_cli_update.title}' after CLI update does not match expected '{target_cli_title}'"
555
+ logger.info(
556
+ f"✓ Successfully verified bookmark title updated by CLI to: '{retrieved_bookmark_after_cli_update.title}'"
557
+ )
501
558
 
502
559
  except subprocess.CalledProcessError as e:
503
560
  logger.info(f" CLI update command failed with exit code {e.returncode}")
@@ -506,16 +563,22 @@ def test_update_bookmark_title(karakeep_client: KarakeepAPI, managed_bookmark: d
506
563
  logger.info(f" Stderr: {e.stderr}")
507
564
  pytest.fail(f"CLI command for update-a-bookmark failed: {e}")
508
565
  except Exception as e:
509
- pytest.fail(f"An unexpected error occurred running the CLI update command: {e}")
566
+ pytest.fail(
567
+ f"An unexpected error occurred running the CLI update command: {e}"
568
+ )
510
569
 
511
570
  except (APIError, AuthenticationError) as e:
512
571
  pytest.fail(f"API error during bookmark title update test: {e}")
513
572
  except Exception as e:
514
- pytest.fail(f"An unexpected error occurred during bookmark title update test: {e}")
573
+ pytest.fail(
574
+ f"An unexpected error occurred during bookmark title update test: {e}"
575
+ )
515
576
  # No finally block needed for deletion, as 'managed_bookmark' fixture handles it.
516
577
 
517
578
 
518
- def test_tag_lifecycle_on_bookmark(karakeep_client: KarakeepAPI, managed_bookmark: datatypes.Bookmark):
579
+ def test_tag_lifecycle_on_bookmark(
580
+ karakeep_client: KarakeepAPI, managed_bookmark: datatypes.Bookmark
581
+ ):
519
582
  """
520
583
  Test attaching a tag to a bookmark, updating the tag, detaching it, and deleting it.
521
584
  Uses the managed_bookmark fixture.
@@ -529,18 +592,23 @@ def test_tag_lifecycle_on_bookmark(karakeep_client: KarakeepAPI, managed_bookmar
529
592
 
530
593
  try:
531
594
  # 1. Attach a new tag by name to the bookmark
532
- logger.info(f"\nAttempting to attach tag '{initial_tag_name}' to bookmark {bookmark_id}")
595
+ logger.info(
596
+ f"\nAttempting to attach tag '{initial_tag_name}' to bookmark {bookmark_id}"
597
+ )
533
598
  attach_response = karakeep_client.attach_tags_to_a_bookmark(
534
599
  bookmark_id=bookmark_id, tag_names=[initial_tag_name]
535
600
  )
536
- assert "attached" in attach_response and len(attach_response["attached"]) == 1, \
537
- "Failed to attach tag or response format incorrect"
601
+ assert (
602
+ "attached" in attach_response and len(attach_response["attached"]) == 1
603
+ ), "Failed to attach tag or response format incorrect"
538
604
  tag_id_to_manage = attach_response["attached"][0]
539
605
  assert isinstance(tag_id_to_manage, str), "Attached tag ID should be a string"
540
606
  logger.info(f"✓ Tag '{initial_tag_name}' attached with ID: {tag_id_to_manage}")
541
607
 
542
608
  # 2. Update the tag's name
543
- logger.info(f"\nAttempting to update tag {tag_id_to_manage} to name '{updated_tag_name}'")
609
+ logger.info(
610
+ f"\nAttempting to update tag {tag_id_to_manage} to name '{updated_tag_name}'"
611
+ )
544
612
  update_payload = {"name": updated_tag_name}
545
613
  updated_tag = karakeep_client.update_a_tag(
546
614
  tag_id=tag_id_to_manage, update_data=update_payload
@@ -549,24 +617,38 @@ def test_tag_lifecycle_on_bookmark(karakeep_client: KarakeepAPI, managed_bookmar
549
617
  # assert isinstance(updated_tag, datatypes.Tag), "Update tag response should be Tag model"
550
618
  # assert updated_tag.name == updated_tag_name, "Tag name was not updated as expected"
551
619
  # logger.info(f"✓ Tag {tag_id_to_manage} updated to name '{updated_tag.name}'")
552
- assert updated_tag["name"] == updated_tag_name, "Tag name was not updated as expected"
620
+ assert (
621
+ updated_tag["name"] == updated_tag_name
622
+ ), "Tag name was not updated as expected"
553
623
  logger.info(f"✓ Tag {tag_id_to_manage} updated to name '{updated_tag['name']}'")
554
624
 
555
625
  # 3. Verify tag update by getting it directly
556
- logger.info(f"\nFetching tag {tag_id_to_manage} to verify its name is '{updated_tag_name}'")
626
+ logger.info(
627
+ f"\nFetching tag {tag_id_to_manage} to verify its name is '{updated_tag_name}'"
628
+ )
557
629
  retrieved_tag = karakeep_client.get_a_single_tag(tag_id=tag_id_to_manage)
558
- assert isinstance(retrieved_tag, datatypes.Tag), "Get single tag response should be Tag model"
559
- assert retrieved_tag.name == updated_tag_name, "Retrieved tag name does not match updated name"
630
+ assert isinstance(
631
+ retrieved_tag, datatypes.Tag
632
+ ), "Get single tag response should be Tag model"
633
+ assert (
634
+ retrieved_tag.name == updated_tag_name
635
+ ), "Retrieved tag name does not match updated name"
560
636
  assert retrieved_tag.id == tag_id_to_manage, "Retrieved tag ID does not match"
561
- logger.info(f"✓ Verified tag {tag_id_to_manage} has name '{retrieved_tag.name}'")
637
+ logger.info(
638
+ f"✓ Verified tag {tag_id_to_manage} has name '{retrieved_tag.name}'"
639
+ )
562
640
 
563
641
  # 4. Detach the tag from the bookmark
564
- logger.info(f"\nAttempting to detach tag {tag_id_to_manage} from bookmark {bookmark_id}")
642
+ logger.info(
643
+ f"\nAttempting to detach tag {tag_id_to_manage} from bookmark {bookmark_id}"
644
+ )
565
645
  detach_response = karakeep_client.detach_tags_from_a_bookmark(
566
646
  bookmark_id=bookmark_id, tag_ids=[tag_id_to_manage]
567
647
  )
568
- assert "detached" in detach_response and tag_id_to_manage in detach_response["detached"], \
569
- "Failed to detach tag or response format incorrect"
648
+ assert (
649
+ "detached" in detach_response
650
+ and tag_id_to_manage in detach_response["detached"]
651
+ ), "Failed to detach tag or response format incorrect"
570
652
  logger.info(f"✓ Tag {tag_id_to_manage} detached from bookmark {bookmark_id}")
571
653
 
572
654
  except (APIError, AuthenticationError) as e:
@@ -588,20 +670,30 @@ def test_tag_lifecycle_on_bookmark(karakeep_client: KarakeepAPI, managed_bookmar
588
670
  f"Tag {tag_id_to_manage} should not exist after deletion, but get_a_single_tag succeeded."
589
671
  )
590
672
  except APIError as e:
591
- assert e.status_code == 404, \
592
- f"Expected 404 Not Found when getting deleted tag, but got status {e.status_code}"
593
- logger.info(f" Confirmed tag {tag_id_to_manage} is deleted (received 404).")
673
+ assert (
674
+ e.status_code == 404
675
+ ), f"Expected 404 Not Found when getting deleted tag, but got status {e.status_code}"
676
+ logger.info(
677
+ f"✓ Confirmed tag {tag_id_to_manage} is deleted (received 404)."
678
+ )
594
679
  except (APIError, AuthenticationError) as e:
595
680
  # Log error during cleanup but don't let it mask original test failure
596
- logger.info(f" API error during tag deletion (cleanup) for ID {tag_id_to_manage}: {e}")
681
+ logger.info(
682
+ f" API error during tag deletion (cleanup) for ID {tag_id_to_manage}: {e}"
683
+ )
597
684
  except Exception as e:
598
- logger.info(f" Unexpected error during tag deletion (cleanup) for ID {tag_id_to_manage}: {e}")
685
+ logger.info(
686
+ f" Unexpected error during tag deletion (cleanup) for ID {tag_id_to_manage}: {e}"
687
+ )
599
688
  else:
600
- logger.info("\nSkipping tag deletion (cleanup) because tag_id was not obtained or test failed before creation.")
689
+ logger.info(
690
+ "\nSkipping tag deletion (cleanup) because tag_id was not obtained or test failed before creation."
691
+ )
601
692
 
602
693
 
603
694
  # --- Test User Info/Stats Endpoints ---
604
695
 
696
+
605
697
  def test_cli_get_bookmarks_count_with_jq(karakeep_client: KarakeepAPI):
606
698
  """Test that CLI get-all-bookmarks with --limit returns the expected number of items."""
607
699
  # Skip test if jq is not installed
@@ -609,15 +701,15 @@ def test_cli_get_bookmarks_count_with_jq(karakeep_client: KarakeepAPI):
609
701
  subprocess.run(["jq", "--version"], check=True, capture_output=True)
610
702
  except (subprocess.CalledProcessError, FileNotFoundError):
611
703
  pytest.skip("jq is not installed. This test requires jq for JSON processing.")
612
-
704
+
613
705
  # Define the limit we want to test
614
706
  test_limit = 200
615
-
707
+
616
708
  try:
617
709
  logger.info(f"\nRunning CLI command: get-all-bookmarks --limit={test_limit}")
618
710
  # Use a two-command pipe: Run the CLI command and pipe to jq to count array length
619
711
  cmd = f"python -m karakeep_python_api --verbose get-all-bookmarks --limit={test_limit} | jq 'length'"
620
-
712
+
621
713
  # Execute the piped command
622
714
  result = subprocess.run(
623
715
  cmd,
@@ -626,30 +718,36 @@ def test_cli_get_bookmarks_count_with_jq(karakeep_client: KarakeepAPI):
626
718
  capture_output=True,
627
719
  text=True,
628
720
  )
629
-
721
+
630
722
  # Parse the output (should be just a number)
631
723
  try:
632
724
  actual_count = int(result.stdout.strip())
633
725
  logger.info(f"✓ Command returned {actual_count} bookmarks")
634
-
726
+
635
727
  # Check if we got exactly the requested number or fewer (if there aren't enough bookmarks)
636
- assert actual_count <= test_limit, f"Expected at most {test_limit} bookmarks, got {actual_count}"
637
-
728
+ assert (
729
+ actual_count <= test_limit
730
+ ), f"Expected at most {test_limit} bookmarks, got {actual_count}"
731
+
638
732
  # Check if we got any bookmarks at all (to ensure the test is meaningful)
639
733
  # This could fail if the account has no bookmarks
640
734
  assert actual_count > 0, "Expected at least some bookmarks to be returned"
641
-
735
+
642
736
  # If the account has enough bookmarks, we should get exactly the limit
643
737
  # But we can't assert this because we don't know how many bookmarks exist
644
738
  if actual_count < test_limit:
645
- logger.info(f"Note: Only {actual_count} bookmarks were returned, which is less than the requested limit of {test_limit}. This is acceptable if the account doesn't have {test_limit} bookmarks.")
739
+ logger.info(
740
+ f"Note: Only {actual_count} bookmarks were returned, which is less than the requested limit of {test_limit}. This is acceptable if the account doesn't have {test_limit} bookmarks."
741
+ )
646
742
  else:
647
- logger.info(f"✓ Command returned exactly the requested limit of {test_limit} bookmarks")
648
-
743
+ logger.info(
744
+ f"✓ Command returned exactly the requested limit of {test_limit} bookmarks"
745
+ )
746
+
649
747
  except ValueError:
650
748
  logger.error(f"Failed to parse jq output as integer: '{result.stdout}'")
651
749
  pytest.fail(f"jq output is not a valid integer: '{result.stdout}'")
652
-
750
+
653
751
  except subprocess.CalledProcessError as e:
654
752
  logger.error(f"Command failed with exit code {e.returncode}")
655
753
  logger.error(f"Stdout: {e.stdout}")
@@ -691,7 +789,7 @@ def test_get_current_user_stats(karakeep_client: KarakeepAPI):
691
789
  "python -m karakeep_python_api get-current-user-stats",
692
790
  shell=True,
693
791
  check=True,
694
- capture_output=True, # Capture output to avoid logger.infoing it during tests unless verbose
792
+ capture_output=True, # Capture output to avoid logger.infoing it during tests unless verbose
695
793
  text=True,
696
794
  )
697
795
  logger.info("✓ CLI command executed successfully.")