karakeep-python-api 1.4.0__tar.gz → 1.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (18) hide show
  1. {karakeep_python_api-1.4.0/karakeep_python_api.egg-info → karakeep_python_api-1.5.0}/PKG-INFO +8 -2
  2. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/README.md +7 -1
  3. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/karakeep_python_api/datatypes.py +16 -0
  4. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/karakeep_python_api/karakeep_api.py +194 -4
  5. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/karakeep_python_api/openapi_reference.json +422 -4
  6. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0/karakeep_python_api.egg-info}/PKG-INFO +8 -2
  7. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/setup.py +1 -1
  8. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/tests/test_karakeep_api.py +130 -0
  9. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/LICENSE +0 -0
  10. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/MANIFEST.in +0 -0
  11. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/karakeep_python_api/__init__.py +0 -0
  12. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/karakeep_python_api/__main__.py +0 -0
  13. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/karakeep_python_api.egg-info/SOURCES.txt +0 -0
  14. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/karakeep_python_api.egg-info/dependency_links.txt +0 -0
  15. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/karakeep_python_api.egg-info/entry_points.txt +0 -0
  16. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/karakeep_python_api.egg-info/requires.txt +0 -0
  17. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/karakeep_python_api.egg-info/top_level.txt +0 -0
  18. {karakeep_python_api-1.4.0 → karakeep_python_api-1.5.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karakeep_python_api
3
- Version: 1.4.0
3
+ Version: 1.5.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
@@ -70,6 +70,7 @@ The development process involved:
70
70
 
71
71
  * **Experimental Methods:** The included Pytest suite currently only covers a subset of the available API methods (primarily 'get all' endpoints and client initialization). Methods *not* explicitly tested should be considered **experimental**.
72
72
  * **Ongoing Development:** The author intends to improve and validate methods as they are needed for personal use cases. Contributions and bug reports are welcome!
73
+ * **Updating process**: I have local scripts to keep track of the changes in the OpenAPI specs on the server side. From time to time I go and use aider to make the code reflect the latest openapi and push that to the `dev` branch. When there is a new karakeep release, I will merge the `dev` branch to the `main` branch and create my own release. This way, `dev` hopefully is up to date with the latest code of karakeep, while `main` is up to date with the latest release of karakeep.
73
74
 
74
75
  ## API Method Coverage
75
76
 
@@ -116,7 +117,12 @@ Methods or CLI commands marked with ❌ should be used with caution as their beh
116
117
  | `get_a_single_asset` | ✅ | ❌ | Tested in PDF asset lifecycle test. |
117
118
  | `get_current_user_info` | ✅ | ❌ | Pytest: Tested indirectly during client init. CLI not directly tested. |
118
119
  | `get_current_user_stats` | ✅ | ✅ | |
119
- | `update_user` | ❌ | ❌ | |
120
+ | `update_user` | ❌ | ❌ | |
121
+ | `get_all_backups` | ✅ | ✅ | Tested in backup lifecycle test. |
122
+ | `trigger_a_new_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
123
+ | `get_a_single_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
124
+ | `delete_a_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
125
+ | `download_a_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
120
126
 
121
127
  ## Installation
122
128
 
@@ -35,6 +35,7 @@ The development process involved:
35
35
 
36
36
  * **Experimental Methods:** The included Pytest suite currently only covers a subset of the available API methods (primarily 'get all' endpoints and client initialization). Methods *not* explicitly tested should be considered **experimental**.
37
37
  * **Ongoing Development:** The author intends to improve and validate methods as they are needed for personal use cases. Contributions and bug reports are welcome!
38
+ * **Updating process**: I have local scripts to keep track of the changes in the OpenAPI specs on the server side. From time to time I go and use aider to make the code reflect the latest openapi and push that to the `dev` branch. When there is a new karakeep release, I will merge the `dev` branch to the `main` branch and create my own release. This way, `dev` hopefully is up to date with the latest code of karakeep, while `main` is up to date with the latest release of karakeep.
38
39
 
39
40
  ## API Method Coverage
40
41
 
@@ -81,7 +82,12 @@ Methods or CLI commands marked with ❌ should be used with caution as their beh
81
82
  | `get_a_single_asset` | ✅ | ❌ | Tested in PDF asset lifecycle test. |
82
83
  | `get_current_user_info` | ✅ | ❌ | Pytest: Tested indirectly during client init. CLI not directly tested. |
83
84
  | `get_current_user_stats` | ✅ | ✅ | |
84
- | `update_user` | ❌ | ❌ | |
85
+ | `update_user` | ❌ | ❌ | |
86
+ | `get_all_backups` | ✅ | ✅ | Tested in backup lifecycle test. |
87
+ | `trigger_a_new_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
88
+ | `get_a_single_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
89
+ | `delete_a_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
90
+ | `download_a_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
85
91
 
86
92
  ## Installation
87
93
 
@@ -90,8 +90,10 @@ class BookmarkAsset(BaseModel):
90
90
  "video",
91
91
  "bookmarkAsset",
92
92
  "precrawledArchive",
93
+ "userUploaded",
93
94
  "unknown",
94
95
  ]
96
+ fileName: Optional[str] = None
95
97
 
96
98
 
97
99
  class Asset(BaseModel):
@@ -117,6 +119,7 @@ class Bookmark(BaseModel):
117
119
  "api", "web", "cli", "mobile", "extension", "singlefile", "rss", "import"
118
120
  ]
119
121
  ] = None
122
+ userId: str
120
123
  tags: List[TagShort]
121
124
  content: Union[
122
125
  ContentTypeLink, ContentTypeText, ContentTypeAsset, ContentTypeUnknown
@@ -138,6 +141,8 @@ class ListModel(BaseModel):
138
141
  type: Optional[Literal["manual", "smart"]] = "manual"
139
142
  query: Optional[str] = None
140
143
  public: bool
144
+ hasCollaborators: bool
145
+ userRole: Literal["owner", "editor", "viewer", "public"]
141
146
 
142
147
 
143
148
  class Highlight(BaseModel):
@@ -160,3 +165,14 @@ class PaginatedHighlights(BaseModel):
160
165
  class PaginatedTags(BaseModel):
161
166
  tags: List[Tag]
162
167
  nextCursor: Optional[str] = ""
168
+
169
+
170
+ class Backup(BaseModel):
171
+ id: str
172
+ userId: str
173
+ assetId: Optional[str]
174
+ createdAt: str
175
+ size: float
176
+ bookmarkCount: int
177
+ status: Literal["pending", "success", "failure"]
178
+ errorMessage: Optional[str] = None
@@ -85,7 +85,7 @@ class KarakeepAPI:
85
85
  """
86
86
 
87
87
  # Version reflects the client library version, updated by bumpver
88
- VERSION: str = "1.4.0"
88
+ VERSION: str = "1.5.0"
89
89
 
90
90
  def __init__(
91
91
  self,
@@ -134,7 +134,7 @@ class KarakeepAPI:
134
134
 
135
135
  # Configure logger based on verbose setting
136
136
  log_level = "DEBUG" if self.verbose else "INFO"
137
- logger.remove() # Remove default handler
137
+ # logger.remove() # Remove default handler
138
138
  if self.verbose:
139
139
  logger.add(
140
140
  sys.stderr,
@@ -1301,6 +1301,7 @@ class KarakeepAPI:
1301
1301
  "video",
1302
1302
  "bookmarkAsset",
1303
1303
  "precrawledArchive",
1304
+ "userUploaded",
1304
1305
  "unknown",
1305
1306
  ],
1306
1307
  ) -> Union[datatypes.BookmarkAsset, Dict[str, Any], List[Any]]:
@@ -1311,7 +1312,8 @@ class KarakeepAPI:
1311
1312
  bookmark_id: The ID (string) of the bookmark.
1312
1313
  asset_id: The ID (string) of the asset to attach.
1313
1314
  asset_type: The type of asset being attached. Must be one of: "screenshot", "assetScreenshot",
1314
- "bannerImage", "fullPageArchive", "video", "bookmarkAsset", "precrawledArchive", "unknown".
1315
+ "bannerImage", "fullPageArchive", "video", "bookmarkAsset", "precrawledArchive",
1316
+ "userUploaded", "unknown".
1315
1317
 
1316
1318
  Returns:
1317
1319
  datatypes.BookmarkAsset: The attached asset object.
@@ -2058,7 +2060,15 @@ class KarakeepAPI:
2058
2060
  Get statistics about the current authenticated user's data. Corresponds to GET /users/me/stats.
2059
2061
 
2060
2062
  Returns:
2061
- dict: A dictionary containing user statistics ('numBookmarks', 'numFavorites', 'numArchived', etc.).
2063
+ dict: A dictionary containing user statistics including:
2064
+ - numBookmarks, numFavorites, numArchived, numTags, numLists, numHighlights
2065
+ - bookmarksByType: breakdown by content type (link, text, asset)
2066
+ - topDomains: most bookmarked domains
2067
+ - totalAssetSize: total size of stored assets
2068
+ - assetsByType: breakdown of assets by type
2069
+ - bookmarkingActivity: activity metrics (thisWeek, thisMonth, thisYear, byHour, byDayOfWeek)
2070
+ - tagUsage: most used tags
2071
+ - bookmarksBySource: breakdown by source (api, web, cli, mobile, extension, singlefile, rss, import)
2062
2072
  Validation is not performed on this response type by default.
2063
2073
 
2064
2074
  Raises:
@@ -2175,6 +2185,186 @@ class KarakeepAPI:
2175
2185
  # Response should match Asset schema
2176
2186
  return datatypes.Asset.model_validate(response_data)
2177
2187
 
2188
+ @optional_typecheck
2189
+ def get_all_backups(
2190
+ self,
2191
+ ) -> Union[List[datatypes.Backup], Dict[str, Any], List[Any]]:
2192
+ """
2193
+ Get all backups for the current user. Corresponds to GET /backups.
2194
+
2195
+ Returns:
2196
+ List[datatypes.Backup]: A list of backup objects.
2197
+ If response validation is disabled, returns the raw API response (dict/list).
2198
+
2199
+ Raises:
2200
+ APIError: If the API request fails.
2201
+ pydantic.ValidationError: If response validation fails (and is not disabled).
2202
+ """
2203
+ response_data = self._call("GET", "backups")
2204
+
2205
+ if self.disable_response_validation:
2206
+ logger.debug("Skipping response validation as requested.")
2207
+ # Return raw data, which might be {"backups": [...]} or something else
2208
+ return response_data
2209
+ else:
2210
+ # Response schema is {"backups": [Backup]}, extract the list and validate
2211
+ if (
2212
+ isinstance(response_data, dict)
2213
+ and "backups" in response_data
2214
+ and isinstance(response_data["backups"], list)
2215
+ ):
2216
+ try:
2217
+ return [
2218
+ datatypes.Backup.model_validate(backup)
2219
+ for backup in response_data["backups"]
2220
+ ]
2221
+ except (
2222
+ Exception
2223
+ ) as e: # Catch validation errors during list comprehension
2224
+ logger.error(f"Validation failed for one or more backups: {e}")
2225
+ raise # Re-raise the validation error
2226
+ else:
2227
+ # Raise error if format is unexpected and validation is enabled
2228
+ raise APIError(
2229
+ f"Unexpected response format for get_all_backups when validation is enabled: {response_data}"
2230
+ )
2231
+
2232
+ @optional_typecheck
2233
+ def trigger_a_new_backup(
2234
+ self,
2235
+ ) -> Union[datatypes.Backup, Dict[str, Any], List[Any]]:
2236
+ """
2237
+ Trigger a new backup. Corresponds to POST /backups.
2238
+
2239
+ Returns:
2240
+ datatypes.Backup: The created backup object.
2241
+ If response validation is disabled, returns the raw API response (dict/list).
2242
+
2243
+ Raises:
2244
+ APIError: If the API request fails.
2245
+ pydantic.ValidationError: If response validation fails (and is not disabled).
2246
+ """
2247
+ response_data = self._call("POST", "backups")
2248
+
2249
+ if self.disable_response_validation:
2250
+ logger.debug("Skipping response validation as requested.")
2251
+ return response_data
2252
+ else:
2253
+ # Response should match Backup schema
2254
+ return datatypes.Backup.model_validate(response_data)
2255
+
2256
+ @optional_typecheck
2257
+ def get_a_single_backup(
2258
+ self, backup_id: str
2259
+ ) -> Union[datatypes.Backup, Dict[str, Any], List[Any]]:
2260
+ """
2261
+ Get a single backup by its ID. Corresponds to GET /backups/{backupId}.
2262
+
2263
+ Args:
2264
+ backup_id: The ID (string) of the backup to retrieve.
2265
+
2266
+ Returns:
2267
+ datatypes.Backup: The requested backup object.
2268
+ If response validation is disabled, returns the raw API response (dict/list).
2269
+
2270
+ Raises:
2271
+ APIError: If the API request fails (e.g., 404 backup not found).
2272
+ pydantic.ValidationError: If response validation fails (and is not disabled).
2273
+ """
2274
+ endpoint = f"backups/{backup_id}"
2275
+ response_data = self._call("GET", endpoint)
2276
+
2277
+ if self.disable_response_validation:
2278
+ logger.debug("Skipping response validation as requested.")
2279
+ return response_data
2280
+ else:
2281
+ # Response should match Backup schema
2282
+ return datatypes.Backup.model_validate(response_data)
2283
+
2284
+ @optional_typecheck
2285
+ def delete_a_backup(self, backup_id: str) -> None:
2286
+ """
2287
+ Delete a backup by its ID. Corresponds to DELETE /backups/{backupId}.
2288
+
2289
+ Args:
2290
+ backup_id: The ID (string) of the backup to delete.
2291
+
2292
+ Returns:
2293
+ None: Returns None upon successful deletion (204 No Content).
2294
+
2295
+ Raises:
2296
+ APIError: If the API request fails (e.g., 404 backup not found).
2297
+ """
2298
+ endpoint = f"backups/{backup_id}"
2299
+ self._call("DELETE", endpoint) # Expects 204 No Content
2300
+ return None # Explicitly return None for 204
2301
+
2302
+ @optional_typecheck
2303
+ def download_a_backup(self, backup_id: str) -> bytes:
2304
+ """
2305
+ Download the backup file (zip archive) by its ID. Corresponds to GET /backups/{backupId}/download.
2306
+
2307
+ Args:
2308
+ backup_id: The ID (string) of the backup to download.
2309
+
2310
+ Returns:
2311
+ bytes: The raw backup file content (zip archive).
2312
+
2313
+ Raises:
2314
+ APIError: If the API request fails (e.g., 404 backup not found).
2315
+ ValueError: If backup_id is empty or invalid.
2316
+
2317
+ Note:
2318
+ This method always returns raw bytes regardless of the disable_response_validation setting,
2319
+ as the response is binary content (zip file) rather than JSON.
2320
+ """
2321
+ # Validate backup_id
2322
+ if not backup_id or not backup_id.strip():
2323
+ raise ValueError("backup_id cannot be empty")
2324
+
2325
+ backup_id = backup_id.strip()
2326
+
2327
+ # Validate backup_id format (basic check for reasonable ID format)
2328
+ if len(backup_id) < 5: # Assuming backup IDs are at least 5 characters
2329
+ raise ValueError(f"backup_id appears to be invalid: {backup_id}")
2330
+
2331
+ endpoint = f"backups/{backup_id}/download"
2332
+
2333
+ # Override the Accept header to get the zip file instead of JSON
2334
+ extra_headers = {"Accept": "application/zip"}
2335
+
2336
+ if self.verbose:
2337
+ logger.debug(f"Downloading backup: {backup_id}")
2338
+
2339
+ response_data = self._call("GET", endpoint, extra_headers=extra_headers)
2340
+
2341
+ # The _call method should return bytes for non-JSON responses
2342
+ if isinstance(response_data, bytes):
2343
+ if self.verbose:
2344
+ logger.debug(
2345
+ f"Downloaded backup {backup_id}: {len(response_data)} bytes"
2346
+ )
2347
+ return response_data
2348
+ elif response_data is None:
2349
+ # Handle empty response
2350
+ if self.verbose:
2351
+ logger.debug(f"Downloaded empty backup {backup_id}")
2352
+ return b""
2353
+ else:
2354
+ # This shouldn't happen with the updated _call method, but handle gracefully
2355
+ error_msg = f"Expected bytes from backup download endpoint for backup {backup_id}, got {type(response_data).__name__}"
2356
+ if isinstance(response_data, (dict, list)):
2357
+ # If we got JSON, it might be an error response that wasn't caught
2358
+ error_detail = (
2359
+ str(response_data)[:200] + "..."
2360
+ if len(str(response_data)) > 200
2361
+ else str(response_data)
2362
+ )
2363
+ error_msg += f". Response content: {error_detail}"
2364
+
2365
+ logger.error(error_msg)
2366
+ raise APIError(error_msg)
2367
+
2178
2368
  @optional_typecheck
2179
2369
  def get_a_single_asset(self, asset_id: str) -> bytes:
2180
2370
  """
@@ -45,6 +45,10 @@
45
45
  "type": "string",
46
46
  "example": "ieidlxygmwj87oxz5hxttoc8"
47
47
  },
48
+ "BackupId": {
49
+ "type": "string",
50
+ "example": "ieidlxygmwj87oxz5hxttoc8"
51
+ },
48
52
  "Bookmark": {
49
53
  "type": "object",
50
54
  "properties": {
@@ -108,6 +112,9 @@
108
112
  "import"
109
113
  ]
110
114
  },
115
+ "userId": {
116
+ "type": "string"
117
+ },
111
118
  "tags": {
112
119
  "type": "array",
113
120
  "items": {
@@ -317,8 +324,13 @@
317
324
  "video",
318
325
  "bookmarkAsset",
319
326
  "precrawledArchive",
327
+ "userUploaded",
320
328
  "unknown"
321
329
  ]
330
+ },
331
+ "fileName": {
332
+ "type": "string",
333
+ "nullable": true
322
334
  }
323
335
  },
324
336
  "required": [
@@ -336,6 +348,7 @@
336
348
  "favourited",
337
349
  "taggingStatus",
338
350
  "summarizationStatus",
351
+ "userId",
339
352
  "tags",
340
353
  "content",
341
354
  "assets"
@@ -397,6 +410,18 @@
397
410
  },
398
411
  "public": {
399
412
  "type": "boolean"
413
+ },
414
+ "hasCollaborators": {
415
+ "type": "boolean"
416
+ },
417
+ "userRole": {
418
+ "type": "string",
419
+ "enum": [
420
+ "owner",
421
+ "editor",
422
+ "viewer",
423
+ "public"
424
+ ]
400
425
  }
401
426
  },
402
427
  "required": [
@@ -404,7 +429,9 @@
404
429
  "name",
405
430
  "icon",
406
431
  "parentId",
407
- "public"
432
+ "public",
433
+ "hasCollaborators",
434
+ "userRole"
408
435
  ]
409
436
  },
410
437
  "Highlight": {
@@ -573,6 +600,14 @@
573
600
  "required": true,
574
601
  "name": "assetId",
575
602
  "in": "path"
603
+ },
604
+ "BackupId": {
605
+ "schema": {
606
+ "$ref": "#/components/schemas/BackupId"
607
+ },
608
+ "required": true,
609
+ "name": "backupId",
610
+ "in": "path"
576
611
  }
577
612
  }
578
613
  },
@@ -1189,6 +1224,9 @@
1189
1224
  "rss",
1190
1225
  "import"
1191
1226
  ]
1227
+ },
1228
+ "userId": {
1229
+ "type": "string"
1192
1230
  }
1193
1231
  },
1194
1232
  "required": [
@@ -1198,7 +1236,8 @@
1198
1236
  "archived",
1199
1237
  "favourited",
1200
1238
  "taggingStatus",
1201
- "summarizationStatus"
1239
+ "summarizationStatus",
1240
+ "userId"
1202
1241
  ]
1203
1242
  }
1204
1243
  }
@@ -1313,6 +1352,9 @@
1313
1352
  "rss",
1314
1353
  "import"
1315
1354
  ]
1355
+ },
1356
+ "userId": {
1357
+ "type": "string"
1316
1358
  }
1317
1359
  },
1318
1360
  "required": [
@@ -1322,7 +1364,8 @@
1322
1364
  "archived",
1323
1365
  "favourited",
1324
1366
  "taggingStatus",
1325
- "summarizationStatus"
1367
+ "summarizationStatus",
1368
+ "userId"
1326
1369
  ]
1327
1370
  }
1328
1371
  }
@@ -1703,6 +1746,7 @@
1703
1746
  "video",
1704
1747
  "bookmarkAsset",
1705
1748
  "precrawledArchive",
1749
+ "userUploaded",
1706
1750
  "unknown"
1707
1751
  ]
1708
1752
  }
@@ -1737,8 +1781,13 @@
1737
1781
  "video",
1738
1782
  "bookmarkAsset",
1739
1783
  "precrawledArchive",
1784
+ "userUploaded",
1740
1785
  "unknown"
1741
1786
  ]
1787
+ },
1788
+ "fileName": {
1789
+ "type": "string",
1790
+ "nullable": true
1742
1791
  }
1743
1792
  },
1744
1793
  "required": [
@@ -3122,6 +3171,10 @@
3122
3171
  "green",
3123
3172
  "blue"
3124
3173
  ]
3174
+ },
3175
+ "note": {
3176
+ "type": "string",
3177
+ "nullable": true
3125
3178
  }
3126
3179
  }
3127
3180
  }
@@ -3386,6 +3439,35 @@
3386
3439
  ]
3387
3440
  },
3388
3441
  "maxItems": 10
3442
+ },
3443
+ "bookmarksBySource": {
3444
+ "type": "array",
3445
+ "items": {
3446
+ "type": "object",
3447
+ "properties": {
3448
+ "source": {
3449
+ "type": "string",
3450
+ "nullable": true,
3451
+ "enum": [
3452
+ "api",
3453
+ "web",
3454
+ "cli",
3455
+ "mobile",
3456
+ "extension",
3457
+ "singlefile",
3458
+ "rss",
3459
+ "import"
3460
+ ]
3461
+ },
3462
+ "count": {
3463
+ "type": "number"
3464
+ }
3465
+ },
3466
+ "required": [
3467
+ "source",
3468
+ "count"
3469
+ ]
3470
+ }
3389
3471
  }
3390
3472
  },
3391
3473
  "required": [
@@ -3400,7 +3482,8 @@
3400
3482
  "totalAssetSize",
3401
3483
  "assetsByType",
3402
3484
  "bookmarkingActivity",
3403
- "tagUsage"
3485
+ "tagUsage",
3486
+ "bookmarksBySource"
3404
3487
  ]
3405
3488
  }
3406
3489
  }
@@ -3632,6 +3715,341 @@
3632
3715
  }
3633
3716
  }
3634
3717
  }
3718
+ },
3719
+ "/backups": {
3720
+ "get": {
3721
+ "description": "Get all backups",
3722
+ "summary": "Get all backups",
3723
+ "tags": [
3724
+ "Backups"
3725
+ ],
3726
+ "security": [
3727
+ {
3728
+ "bearerAuth": []
3729
+ }
3730
+ ],
3731
+ "responses": {
3732
+ "200": {
3733
+ "description": "Object with all backups data.",
3734
+ "content": {
3735
+ "application/json": {
3736
+ "schema": {
3737
+ "type": "object",
3738
+ "properties": {
3739
+ "backups": {
3740
+ "type": "array",
3741
+ "items": {
3742
+ "type": "object",
3743
+ "properties": {
3744
+ "id": {
3745
+ "type": "string"
3746
+ },
3747
+ "userId": {
3748
+ "type": "string"
3749
+ },
3750
+ "assetId": {
3751
+ "type": "string",
3752
+ "nullable": true
3753
+ },
3754
+ "createdAt": {
3755
+ "type": "string"
3756
+ },
3757
+ "size": {
3758
+ "type": "number"
3759
+ },
3760
+ "bookmarkCount": {
3761
+ "type": "number"
3762
+ },
3763
+ "status": {
3764
+ "type": "string",
3765
+ "enum": [
3766
+ "pending",
3767
+ "success",
3768
+ "failure"
3769
+ ]
3770
+ },
3771
+ "errorMessage": {
3772
+ "type": "string",
3773
+ "nullable": true
3774
+ }
3775
+ },
3776
+ "required": [
3777
+ "id",
3778
+ "userId",
3779
+ "assetId",
3780
+ "createdAt",
3781
+ "size",
3782
+ "bookmarkCount",
3783
+ "status"
3784
+ ]
3785
+ }
3786
+ }
3787
+ },
3788
+ "required": [
3789
+ "backups"
3790
+ ]
3791
+ }
3792
+ }
3793
+ }
3794
+ }
3795
+ }
3796
+ },
3797
+ "post": {
3798
+ "description": "Trigger a new backup",
3799
+ "summary": "Trigger a new backup",
3800
+ "tags": [
3801
+ "Backups"
3802
+ ],
3803
+ "security": [
3804
+ {
3805
+ "bearerAuth": []
3806
+ }
3807
+ ],
3808
+ "responses": {
3809
+ "201": {
3810
+ "description": "Backup created successfully",
3811
+ "content": {
3812
+ "application/json": {
3813
+ "schema": {
3814
+ "type": "object",
3815
+ "properties": {
3816
+ "id": {
3817
+ "type": "string"
3818
+ },
3819
+ "userId": {
3820
+ "type": "string"
3821
+ },
3822
+ "assetId": {
3823
+ "type": "string",
3824
+ "nullable": true
3825
+ },
3826
+ "createdAt": {
3827
+ "type": "string"
3828
+ },
3829
+ "size": {
3830
+ "type": "number"
3831
+ },
3832
+ "bookmarkCount": {
3833
+ "type": "number"
3834
+ },
3835
+ "status": {
3836
+ "type": "string",
3837
+ "enum": [
3838
+ "pending",
3839
+ "success",
3840
+ "failure"
3841
+ ]
3842
+ },
3843
+ "errorMessage": {
3844
+ "type": "string",
3845
+ "nullable": true
3846
+ }
3847
+ },
3848
+ "required": [
3849
+ "id",
3850
+ "userId",
3851
+ "assetId",
3852
+ "createdAt",
3853
+ "size",
3854
+ "bookmarkCount",
3855
+ "status"
3856
+ ]
3857
+ }
3858
+ }
3859
+ }
3860
+ }
3861
+ }
3862
+ }
3863
+ },
3864
+ "/backups/{backupId}": {
3865
+ "get": {
3866
+ "description": "Get backup by its id",
3867
+ "summary": "Get a single backup",
3868
+ "tags": [
3869
+ "Backups"
3870
+ ],
3871
+ "security": [
3872
+ {
3873
+ "bearerAuth": []
3874
+ }
3875
+ ],
3876
+ "parameters": [
3877
+ {
3878
+ "$ref": "#/components/parameters/BackupId"
3879
+ }
3880
+ ],
3881
+ "responses": {
3882
+ "200": {
3883
+ "description": "Object with backup data.",
3884
+ "content": {
3885
+ "application/json": {
3886
+ "schema": {
3887
+ "type": "object",
3888
+ "properties": {
3889
+ "id": {
3890
+ "type": "string"
3891
+ },
3892
+ "userId": {
3893
+ "type": "string"
3894
+ },
3895
+ "assetId": {
3896
+ "type": "string",
3897
+ "nullable": true
3898
+ },
3899
+ "createdAt": {
3900
+ "type": "string"
3901
+ },
3902
+ "size": {
3903
+ "type": "number"
3904
+ },
3905
+ "bookmarkCount": {
3906
+ "type": "number"
3907
+ },
3908
+ "status": {
3909
+ "type": "string",
3910
+ "enum": [
3911
+ "pending",
3912
+ "success",
3913
+ "failure"
3914
+ ]
3915
+ },
3916
+ "errorMessage": {
3917
+ "type": "string",
3918
+ "nullable": true
3919
+ }
3920
+ },
3921
+ "required": [
3922
+ "id",
3923
+ "userId",
3924
+ "assetId",
3925
+ "createdAt",
3926
+ "size",
3927
+ "bookmarkCount",
3928
+ "status"
3929
+ ]
3930
+ }
3931
+ }
3932
+ }
3933
+ },
3934
+ "404": {
3935
+ "description": "Backup not found",
3936
+ "content": {
3937
+ "application/json": {
3938
+ "schema": {
3939
+ "type": "object",
3940
+ "properties": {
3941
+ "code": {
3942
+ "type": "string"
3943
+ },
3944
+ "message": {
3945
+ "type": "string"
3946
+ }
3947
+ },
3948
+ "required": [
3949
+ "code",
3950
+ "message"
3951
+ ]
3952
+ }
3953
+ }
3954
+ }
3955
+ }
3956
+ }
3957
+ },
3958
+ "delete": {
3959
+ "description": "Delete backup by its id",
3960
+ "summary": "Delete a backup",
3961
+ "tags": [
3962
+ "Backups"
3963
+ ],
3964
+ "security": [
3965
+ {
3966
+ "bearerAuth": []
3967
+ }
3968
+ ],
3969
+ "parameters": [
3970
+ {
3971
+ "$ref": "#/components/parameters/BackupId"
3972
+ }
3973
+ ],
3974
+ "responses": {
3975
+ "204": {
3976
+ "description": "No content - the backup was deleted"
3977
+ },
3978
+ "404": {
3979
+ "description": "Backup not found",
3980
+ "content": {
3981
+ "application/json": {
3982
+ "schema": {
3983
+ "type": "object",
3984
+ "properties": {
3985
+ "code": {
3986
+ "type": "string"
3987
+ },
3988
+ "message": {
3989
+ "type": "string"
3990
+ }
3991
+ },
3992
+ "required": [
3993
+ "code",
3994
+ "message"
3995
+ ]
3996
+ }
3997
+ }
3998
+ }
3999
+ }
4000
+ }
4001
+ }
4002
+ },
4003
+ "/backups/{backupId}/download": {
4004
+ "get": {
4005
+ "description": "Download backup file",
4006
+ "summary": "Download a backup",
4007
+ "tags": [
4008
+ "Backups"
4009
+ ],
4010
+ "security": [
4011
+ {
4012
+ "bearerAuth": []
4013
+ }
4014
+ ],
4015
+ "parameters": [
4016
+ {
4017
+ "$ref": "#/components/parameters/BackupId"
4018
+ }
4019
+ ],
4020
+ "responses": {
4021
+ "200": {
4022
+ "description": "Backup file (zip archive)",
4023
+ "content": {
4024
+ "application/zip": {
4025
+ "schema": {}
4026
+ }
4027
+ }
4028
+ },
4029
+ "404": {
4030
+ "description": "Backup not found",
4031
+ "content": {
4032
+ "application/json": {
4033
+ "schema": {
4034
+ "type": "object",
4035
+ "properties": {
4036
+ "code": {
4037
+ "type": "string"
4038
+ },
4039
+ "message": {
4040
+ "type": "string"
4041
+ }
4042
+ },
4043
+ "required": [
4044
+ "code",
4045
+ "message"
4046
+ ]
4047
+ }
4048
+ }
4049
+ }
4050
+ }
4051
+ }
4052
+ }
3635
4053
  }
3636
4054
  }
3637
4055
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karakeep_python_api
3
- Version: 1.4.0
3
+ Version: 1.5.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
@@ -70,6 +70,7 @@ The development process involved:
70
70
 
71
71
  * **Experimental Methods:** The included Pytest suite currently only covers a subset of the available API methods (primarily 'get all' endpoints and client initialization). Methods *not* explicitly tested should be considered **experimental**.
72
72
  * **Ongoing Development:** The author intends to improve and validate methods as they are needed for personal use cases. Contributions and bug reports are welcome!
73
+ * **Updating process**: I have local scripts to keep track of the changes in the OpenAPI specs on the server side. From time to time I go and use aider to make the code reflect the latest openapi and push that to the `dev` branch. When there is a new karakeep release, I will merge the `dev` branch to the `main` branch and create my own release. This way, `dev` hopefully is up to date with the latest code of karakeep, while `main` is up to date with the latest release of karakeep.
73
74
 
74
75
  ## API Method Coverage
75
76
 
@@ -116,7 +117,12 @@ Methods or CLI commands marked with ❌ should be used with caution as their beh
116
117
  | `get_a_single_asset` | ✅ | ❌ | Tested in PDF asset lifecycle test. |
117
118
  | `get_current_user_info` | ✅ | ❌ | Pytest: Tested indirectly during client init. CLI not directly tested. |
118
119
  | `get_current_user_stats` | ✅ | ✅ | |
119
- | `update_user` | ❌ | ❌ | |
120
+ | `update_user` | ❌ | ❌ | |
121
+ | `get_all_backups` | ✅ | ✅ | Tested in backup lifecycle test. |
122
+ | `trigger_a_new_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
123
+ | `get_a_single_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
124
+ | `delete_a_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
125
+ | `download_a_backup` | ✅ | ❌ | Tested in backup lifecycle test. |
120
126
 
121
127
  ## Installation
122
128
 
@@ -7,7 +7,7 @@ with open("README.md", "r") as readme:
7
7
 
8
8
  setup(
9
9
  name="karakeep_python_api",
10
- version="1.4.0",
10
+ version="1.5.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",
@@ -892,4 +892,134 @@ def test_asset_lifecycle_with_pdf(karakeep_client: KarakeepAPI):
892
892
  logger.info("\nNo bookmark to clean up")
893
893
 
894
894
 
895
+ def test_backup_lifecycle(karakeep_client: KarakeepAPI):
896
+ """Test creating, retrieving, downloading, and deleting a backup."""
897
+ created_backup_id = None
898
+
899
+ try:
900
+ # 1. Get initial backup count
901
+ logger.info("\nGetting initial backup list")
902
+ initial_backups = karakeep_client.get_all_backups()
903
+ assert isinstance(initial_backups, list), "Response should be a list"
904
+ initial_backup_count = len(initial_backups)
905
+ logger.info(f" Initial backup count: {initial_backup_count}")
906
+
907
+ # 2. Trigger a new backup
908
+ logger.info("\nTriggering a new backup")
909
+ created_backup = karakeep_client.trigger_a_new_backup()
910
+ assert isinstance(created_backup, datatypes.Backup), (
911
+ "Response should be a Backup model"
912
+ )
913
+ assert created_backup.id, "Created backup must have an ID"
914
+ assert created_backup.status in ["pending", "success", "failure"], (
915
+ "Backup status should be one of the valid enum values"
916
+ )
917
+ created_backup_id = created_backup.id
918
+ logger.info(f"✓ Successfully triggered backup with ID: {created_backup_id}")
919
+ logger.info(f" Backup status: {created_backup.status}")
920
+
921
+ # 3. Verify the backup appears in get_all_backups
922
+ logger.info(f"\nVerifying backup {created_backup_id} appears in backup list")
923
+ current_backups = karakeep_client.get_all_backups()
924
+ assert len(current_backups) >= initial_backup_count + 1, (
925
+ "Backup count should increase after creation"
926
+ )
927
+ assert any(backup.id == created_backup_id for backup in current_backups), (
928
+ "Created backup should be present in the list of all backups"
929
+ )
930
+ logger.info(
931
+ f"✓ Verified backup {created_backup_id} is present in get_all_backups"
932
+ )
933
+
934
+ # 4. Get the backup by ID to verify it exists
935
+ logger.info(f"\nRetrieving backup {created_backup_id} by ID")
936
+ retrieved_backup = karakeep_client.get_a_single_backup(
937
+ backup_id=created_backup_id
938
+ )
939
+ assert isinstance(retrieved_backup, datatypes.Backup)
940
+ assert retrieved_backup.id == created_backup_id
941
+ logger.info(f"✓ Successfully retrieved backup by ID")
942
+ logger.info(f" Status: {retrieved_backup.status}")
943
+ logger.info(f" Bookmark count: {retrieved_backup.bookmarkCount}")
944
+ logger.info(f" Size: {retrieved_backup.size} bytes")
945
+
946
+ # 5. Try to download the backup (only if status is "success")
947
+ if retrieved_backup.status == "success" and retrieved_backup.assetId:
948
+ logger.info(f"\nAttempting to download backup {created_backup_id}")
949
+ try:
950
+ backup_data = karakeep_client.download_a_backup(
951
+ backup_id=created_backup_id
952
+ )
953
+ assert isinstance(backup_data, bytes), (
954
+ "Downloaded backup should be bytes"
955
+ )
956
+ assert len(backup_data) > 0, "Downloaded backup should not be empty"
957
+ # Verify it's a zip file by checking the magic number
958
+ assert backup_data.startswith(b"PK\x03\x04") or backup_data.startswith(
959
+ b"PK\x05\x06"
960
+ ), "Downloaded file should be a valid ZIP archive"
961
+ logger.info(
962
+ f"✓ Successfully downloaded backup ({len(backup_data)} bytes)"
963
+ )
964
+ except APIError as e:
965
+ # Some backups might not be downloadable immediately, log but don't fail
966
+ logger.info(f" Note: Could not download backup: {e}")
967
+ else:
968
+ logger.info(
969
+ f" Skipping download test (status: {retrieved_backup.status}, assetId: {retrieved_backup.assetId})"
970
+ )
971
+
972
+ # 6. Test CLI equivalent for getting all backups
973
+ logger.info("\n Running CLI equivalent: get-all-backups")
974
+ try:
975
+ subprocess.run(
976
+ "python -m karakeep_python_api get-all-backups",
977
+ shell=True,
978
+ check=True,
979
+ capture_output=True,
980
+ text=True,
981
+ )
982
+ logger.info("✓ CLI command executed successfully.")
983
+ except subprocess.CalledProcessError as e:
984
+ logger.info(f" CLI command failed with exit code {e.returncode}")
985
+ logger.info(f" Stdout: {e.stdout}")
986
+ logger.info(f" Stderr: {e.stderr}")
987
+ pytest.fail(f"CLI command 'get-all-backups' failed: {e}")
988
+
989
+ except (APIError, AuthenticationError) as e:
990
+ pytest.fail(f"API error during backup lifecycle test: {e}")
991
+ except Exception as e:
992
+ pytest.fail(f"An unexpected error occurred during backup lifecycle test: {e}")
993
+ finally:
994
+ # 7. Clean up: Delete the backup
995
+ if created_backup_id:
996
+ logger.info(f"\nCleaning up: Deleting backup {created_backup_id}")
997
+ try:
998
+ karakeep_client.delete_a_backup(backup_id=created_backup_id)
999
+ logger.info(f"✓ Successfully deleted backup {created_backup_id}")
1000
+
1001
+ # 8. Verify the backup is deleted
1002
+ try:
1003
+ karakeep_client.get_a_single_backup(backup_id=created_backup_id)
1004
+ pytest.fail(
1005
+ f"Backup {created_backup_id} should not exist after deletion, but get_a_single_backup succeeded."
1006
+ )
1007
+ except APIError as e:
1008
+ assert e.status_code == 404, (
1009
+ f"Expected 404 Not Found when getting deleted backup, but got status {e.status_code}"
1010
+ )
1011
+ logger.info(
1012
+ f"✓ Confirmed backup {created_backup_id} is deleted (received 404)"
1013
+ )
1014
+
1015
+ except (APIError, AuthenticationError) as e:
1016
+ logger.info(
1017
+ f" Error during cleanup - failed to delete backup {created_backup_id}: {e}"
1018
+ )
1019
+ except Exception as e:
1020
+ logger.info(f" Unexpected error during cleanup: {e}")
1021
+ else:
1022
+ logger.info("\nNo backup to clean up")
1023
+
1024
+
895
1025
  # --- End of Tests ---