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.
- {karakeep_python_api-0.2.2/karakeep_python_api.egg-info → karakeep_python_api-1.0.0}/PKG-INFO +10 -3
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/README.md +9 -2
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api/karakeep_api.py +110 -31
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0/karakeep_python_api.egg-info}/PKG-INFO +10 -3
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/setup.py +1 -1
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/tests/test_karakeep_api.py +183 -85
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/LICENSE +0 -0
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/MANIFEST.in +0 -0
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api/__init__.py +0 -0
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api/__main__.py +0 -0
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api/datatypes.py +0 -0
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api/openapi_reference.json +0 -0
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api.egg-info/SOURCES.txt +0 -0
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api.egg-info/dependency_links.txt +0 -0
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api.egg-info/entry_points.txt +0 -0
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api.egg-info/requires.txt +0 -0
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api.egg-info/top_level.txt +0 -0
- {karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/setup.cfg +0 -0
{karakeep_python_api-0.2.2/karakeep_python_api.egg-info → karakeep_python_api-1.0.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: karakeep_python_api
|
|
3
|
-
Version: 0.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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(
|
|
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
|
|
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:
|
|
916
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
1379
|
-
|
|
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:
|
|
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:
|
|
1605
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
{karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0/karakeep_python_api.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: karakeep_python_api
|
|
3
|
-
Version: 0.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
355
|
-
original_title = managed_bookmark.title
|
|
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(
|
|
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(
|
|
392
|
-
|
|
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(
|
|
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
|
|
407
|
-
|
|
408
|
-
|
|
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(
|
|
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
|
|
422
|
-
|
|
423
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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(
|
|
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
|
|
473
|
-
|
|
474
|
-
|
|
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(
|
|
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(
|
|
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
|
|
499
|
-
|
|
500
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
537
|
-
"
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
559
|
-
|
|
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(
|
|
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(
|
|
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
|
|
569
|
-
"
|
|
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
|
|
592
|
-
|
|
593
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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,
|
|
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.")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api/openapi_reference.json
RENAMED
|
File without changes
|
{karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api.egg-info/requires.txt
RENAMED
|
File without changes
|
{karakeep_python_api-0.2.2 → karakeep_python_api-1.0.0}/karakeep_python_api.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|