karakeep-python-api 1.2.4__tar.gz → 1.3.1__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.2.4/karakeep_python_api.egg-info → karakeep_python_api-1.3.1}/PKG-INFO +1 -1
  2. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/karakeep_python_api/datatypes.py +26 -16
  3. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/karakeep_python_api/karakeep_api.py +100 -28
  4. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/karakeep_python_api/openapi_reference.json +237 -46
  5. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1/karakeep_python_api.egg-info}/PKG-INFO +1 -1
  6. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/setup.py +1 -1
  7. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/tests/test_karakeep_api.py +95 -91
  8. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/LICENSE +0 -0
  9. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/MANIFEST.in +0 -0
  10. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/README.md +0 -0
  11. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/karakeep_python_api/__init__.py +0 -0
  12. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/karakeep_python_api/__main__.py +0 -0
  13. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/karakeep_python_api.egg-info/SOURCES.txt +0 -0
  14. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/karakeep_python_api.egg-info/dependency_links.txt +0 -0
  15. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/karakeep_python_api.egg-info/entry_points.txt +0 -0
  16. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/karakeep_python_api.egg-info/requires.txt +0 -0
  17. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/karakeep_python_api.egg-info/top_level.txt +0 -0
  18. {karakeep_python_api-1.2.4 → karakeep_python_api-1.3.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karakeep_python_api
3
- Version: 1.2.4
3
+ Version: 1.3.1
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
@@ -17,8 +17,8 @@ class StatusTypes(str, Enum):
17
17
 
18
18
 
19
19
  class NumBookmarksByAttachedType(BaseModel):
20
- ai: Optional[float] = None
21
- human: Optional[float] = None
20
+ ai: Optional[int] = None
21
+ human: Optional[int] = None
22
22
 
23
23
 
24
24
  class TagShort(BaseModel):
@@ -30,7 +30,7 @@ class TagShort(BaseModel):
30
30
  class Tag(BaseModel):
31
31
  id: str
32
32
  name: str
33
- numBookmarks: float
33
+ numBookmarks: int
34
34
  numBookmarksByAttachedType: NumBookmarksByAttachedType
35
35
 
36
36
 
@@ -112,6 +112,11 @@ class Bookmark(BaseModel):
112
112
  summarizationStatus: Optional[Literal["success", "failure", "pending"]] = None
113
113
  note: Optional[str] = None
114
114
  summary: Optional[str] = None
115
+ source: Optional[
116
+ Literal[
117
+ "api", "web", "cli", "mobile", "extension", "singlefile", "rss", "import"
118
+ ]
119
+ ] = None
115
120
  tags: List[TagShort]
116
121
  content: Union[
117
122
  ContentTypeLink, ContentTypeText, ContentTypeAsset, ContentTypeUnknown
@@ -121,7 +126,18 @@ class Bookmark(BaseModel):
121
126
 
122
127
  class PaginatedBookmarks(BaseModel):
123
128
  bookmarks: List[Bookmark]
124
- nextCursor: Optional[str]
129
+ nextCursor: Optional[str] = ""
130
+
131
+
132
+ class ListModel(BaseModel):
133
+ id: str
134
+ name: str
135
+ description: Optional[str] = None
136
+ icon: str
137
+ parentId: Optional[str]
138
+ type: Optional[Literal["manual", "smart"]] = "manual"
139
+ query: Optional[str] = None
140
+ public: bool
125
141
 
126
142
 
127
143
  class Highlight(BaseModel):
@@ -136,17 +152,11 @@ class Highlight(BaseModel):
136
152
  createdAt: str
137
153
 
138
154
 
139
- class ListModel(BaseModel):
140
- id: str
141
- name: str
142
- description: Optional[str] = None
143
- icon: str
144
- parentId: Optional[str]
145
- type: Optional[Literal["manual", "smart"]] = "manual"
146
- query: Optional[str] = None
147
- public: bool
148
-
149
-
150
155
  class PaginatedHighlights(BaseModel):
151
156
  highlights: List[Highlight]
152
- nextCursor: Optional[str]
157
+ nextCursor: Optional[str] = ""
158
+
159
+
160
+ class PaginatedTags(BaseModel):
161
+ tags: List[Tag]
162
+ nextCursor: Optional[str] = ""
@@ -85,7 +85,7 @@ class KarakeepAPI:
85
85
  """
86
86
 
87
87
  # Version reflects the client library version, updated by bumpver
88
- VERSION: str = "1.2.4"
88
+ VERSION: str = "1.3.1"
89
89
 
90
90
  def __init__(
91
91
  self,
@@ -758,6 +758,19 @@ class KarakeepAPI:
758
758
  summary: Optional[str] = None,
759
759
  createdAt: Optional[str] = None, # ISO 8601 format string
760
760
  crawlPriority: Optional[Literal["low", "normal"]] = None,
761
+ importSessionId: Optional[str] = None,
762
+ source: Optional[
763
+ Literal[
764
+ "api",
765
+ "web",
766
+ "cli",
767
+ "mobile",
768
+ "extension",
769
+ "singlefile",
770
+ "rss",
771
+ "import",
772
+ ]
773
+ ] = None,
761
774
  # Link specific
762
775
  url: Optional[str] = None,
763
776
  precrawledArchiveId: Optional[str] = None,
@@ -783,6 +796,8 @@ class KarakeepAPI:
783
796
  summary: Optional summary content for the bookmark.
784
797
  createdAt: Optional creation timestamp override (ISO 8601 format string).
785
798
  crawlPriority: Optional either 'low' or 'normal'.
799
+ importSessionId: Optional import session ID for tracking imported bookmarks.
800
+ source: Optional source of the bookmark ("api", "web", "cli", "mobile", "extension", "singlefile", "rss", "import").
786
801
 
787
802
  --- Link Type Specific ---
788
803
  url: The URL for the link bookmark. Required if type='link'.
@@ -799,7 +814,9 @@ class KarakeepAPI:
799
814
  sourceUrl: Optional source URL where the asset originated.
800
815
 
801
816
  Returns:
802
- datatypes.Bookmark: The created bookmark.
817
+ datatypes.Bookmark: The bookmark object.
818
+ Returns with status 200 if the bookmark already exists,
819
+ or status 201 if the bookmark was newly created.
803
820
  If response validation is disabled, returns the raw API response (dict/list).
804
821
 
805
822
  Raises:
@@ -825,6 +842,10 @@ class KarakeepAPI:
825
842
  request_body["createdAt"] = createdAt
826
843
  if crawlPriority is not None:
827
844
  request_body["crawlPriority"] = crawlPriority
845
+ if importSessionId is not None:
846
+ request_body["importSessionId"] = importSessionId
847
+ if source is not None:
848
+ request_body["source"] = source
828
849
 
829
850
  # Add type-specific fields and perform validation
830
851
  if type == "link":
@@ -911,7 +932,9 @@ class KarakeepAPI:
911
932
 
912
933
  @optional_typecheck
913
934
  def get_a_single_bookmark(
914
- self, bookmark_id: str, include_content: bool = True # Default from spec
935
+ self,
936
+ bookmark_id: str,
937
+ include_content: bool = True, # Default from spec
915
938
  ) -> Union[datatypes.Bookmark, Dict[str, Any], List[Any]]:
916
939
  """
917
940
  Get a single bookmark by its ID. Corresponds to GET /bookmarks/{bookmarkId}.
@@ -1169,6 +1192,54 @@ class KarakeepAPI:
1169
1192
  # No Pydantic validation applied here as the spec defines a simple dict response
1170
1193
  return response_data
1171
1194
 
1195
+ @optional_typecheck
1196
+ def get_lists_of_a_bookmark(
1197
+ self, bookmark_id: str
1198
+ ) -> Union[List[datatypes.ListModel], Dict[str, Any], List[Any]]:
1199
+ """
1200
+ Get all lists associated with a specific bookmark. Corresponds to GET /bookmarks/{bookmarkId}/lists.
1201
+
1202
+ Args:
1203
+ bookmark_id: The ID (string) of the bookmark.
1204
+
1205
+ Returns:
1206
+ List[datatypes.ListModel]: A list of list objects associated with the bookmark.
1207
+ If response validation is disabled, returns the raw API response (dict/list).
1208
+
1209
+ Raises:
1210
+ APIError: If the API request fails (e.g., 404 bookmark not found).
1211
+ pydantic.ValidationError: If response validation fails (and is not disabled).
1212
+ """
1213
+ endpoint = f"bookmarks/{bookmark_id}/lists"
1214
+ response_data = self._call("GET", endpoint)
1215
+
1216
+ if self.disable_response_validation:
1217
+ logger.debug("Skipping response validation as requested.")
1218
+ # Return raw data, which might be {"lists": [...]} or something else
1219
+ return response_data
1220
+ else:
1221
+ # Response schema is {"lists": [ListModel]}, extract the list and validate
1222
+ if (
1223
+ isinstance(response_data, dict)
1224
+ and "lists" in response_data
1225
+ and isinstance(response_data["lists"], list)
1226
+ ):
1227
+ try:
1228
+ return [
1229
+ datatypes.ListModel.model_validate(lst)
1230
+ for lst in response_data["lists"]
1231
+ ]
1232
+ except (
1233
+ Exception
1234
+ ) as e: # Catch validation errors during list comprehension
1235
+ logger.error(f"Validation failed for one or more lists: {e}")
1236
+ raise # Re-raise the validation error
1237
+ else:
1238
+ # Raise error if format is unexpected and validation is enabled
1239
+ raise APIError(
1240
+ f"Unexpected response format for get_lists_of_a_bookmark when validation is enabled: {response_data}"
1241
+ )
1242
+
1172
1243
  @optional_typecheck
1173
1244
  def get_highlights_of_a_bookmark(
1174
1245
  self, bookmark_id: str
@@ -1601,46 +1672,47 @@ class KarakeepAPI:
1601
1672
  return None # Explicitly return None for 204
1602
1673
 
1603
1674
  @optional_typecheck
1604
- def get_all_tags(self) -> Union[List[datatypes.Tag], Dict[str, Any], List[Any]]:
1675
+ def get_all_tags(
1676
+ self,
1677
+ name_contains: Optional[str] = None,
1678
+ sort: Optional[Literal["name", "usage", "relevance"]] = None,
1679
+ attached_by: Optional[Literal["ai", "human", "none"]] = None,
1680
+ cursor: Optional[str] = None,
1681
+ limit: Optional[int] = None,
1682
+ ) -> Union[datatypes.PaginatedTags, Dict[str, Any], List[Any]]:
1605
1683
  """
1606
1684
  Get all tags for the current user. Corresponds to GET /tags.
1607
1685
 
1686
+ Args:
1687
+ name_contains: Filter tags by name substring (optional).
1688
+ sort: Sort order for tags ("name", "usage", "relevance"). Default from API is "usage" (optional).
1689
+ attached_by: Filter tags by how they were attached ("ai", "human", "none") (optional).
1690
+ cursor: Pagination cursor for the next page (optional).
1691
+ limit: Maximum number of tags to return (optional).
1692
+
1608
1693
  Returns:
1609
- List[datatypes.Tag]: A list of tag objects, including bookmark counts.
1694
+ datatypes.PaginatedTags: Paginated list of tags with nextCursor for pagination.
1610
1695
  If response validation is disabled, returns the raw API response (dict/list).
1611
1696
 
1612
1697
  Raises:
1613
1698
  APIError: If the API request fails.
1614
1699
  pydantic.ValidationError: If response validation fails (and is not disabled).
1615
1700
  """
1616
- response_data = self._call("GET", "tags")
1701
+ params = {
1702
+ "nameContains": name_contains,
1703
+ "sort": sort,
1704
+ "attachedBy": attached_by,
1705
+ "cursor": cursor,
1706
+ "limit": limit,
1707
+ }
1708
+ response_data = self._call("GET", "tags", params=params)
1617
1709
 
1618
1710
  if self.disable_response_validation:
1619
1711
  logger.debug("Skipping response validation as requested.")
1620
- # Return raw data, which might be {"tags": [...]} or something else
1621
1712
  return response_data
1622
1713
  else:
1623
- # Response schema is {"tags": [Tag]}, extract the list and validate
1624
- if (
1625
- isinstance(response_data, dict)
1626
- and "tags" in response_data
1627
- and isinstance(response_data["tags"], list)
1628
- ):
1629
- try:
1630
- return [
1631
- datatypes.Tag.model_validate(tag)
1632
- for tag in response_data["tags"]
1633
- ]
1634
- except (
1635
- Exception
1636
- ) as e: # Catch validation errors during list comprehension
1637
- logger.error(f"Validation failed for one or more tags: {e}")
1638
- raise # Re-raise the validation error
1639
- else:
1640
- # Raise error if format is unexpected and validation is enabled
1641
- raise APIError(
1642
- f"Unexpected response format for get_all_tags when validation is enabled: {response_data}"
1643
- )
1714
+ # Response should match PaginatedTags schema
1715
+ return datatypes.PaginatedTags.model_validate(response_data)
1644
1716
 
1645
1717
  @optional_typecheck
1646
1718
  def create_a_new_tag(self, name: str) -> Dict[str, Any]:
@@ -94,6 +94,20 @@
94
94
  "type": "string",
95
95
  "nullable": true
96
96
  },
97
+ "source": {
98
+ "type": "string",
99
+ "nullable": true,
100
+ "enum": [
101
+ "api",
102
+ "web",
103
+ "cli",
104
+ "mobile",
105
+ "extension",
106
+ "singlefile",
107
+ "rss",
108
+ "import"
109
+ ]
110
+ },
97
111
  "tags": {
98
112
  "type": "array",
99
113
  "items": {
@@ -349,6 +363,50 @@
349
363
  "Cursor": {
350
364
  "type": "string"
351
365
  },
366
+ "List": {
367
+ "type": "object",
368
+ "properties": {
369
+ "id": {
370
+ "type": "string"
371
+ },
372
+ "name": {
373
+ "type": "string"
374
+ },
375
+ "description": {
376
+ "type": "string",
377
+ "nullable": true
378
+ },
379
+ "icon": {
380
+ "type": "string"
381
+ },
382
+ "parentId": {
383
+ "type": "string",
384
+ "nullable": true
385
+ },
386
+ "type": {
387
+ "type": "string",
388
+ "enum": [
389
+ "manual",
390
+ "smart"
391
+ ],
392
+ "default": "manual"
393
+ },
394
+ "query": {
395
+ "type": "string",
396
+ "nullable": true
397
+ },
398
+ "public": {
399
+ "type": "boolean"
400
+ }
401
+ },
402
+ "required": [
403
+ "id",
404
+ "name",
405
+ "icon",
406
+ "parentId",
407
+ "public"
408
+ ]
409
+ },
352
410
  "Highlight": {
353
411
  "type": "object",
354
412
  "properties": {
@@ -400,50 +458,6 @@
400
458
  "createdAt"
401
459
  ]
402
460
  },
403
- "List": {
404
- "type": "object",
405
- "properties": {
406
- "id": {
407
- "type": "string"
408
- },
409
- "name": {
410
- "type": "string"
411
- },
412
- "description": {
413
- "type": "string",
414
- "nullable": true
415
- },
416
- "icon": {
417
- "type": "string"
418
- },
419
- "parentId": {
420
- "type": "string",
421
- "nullable": true
422
- },
423
- "type": {
424
- "type": "string",
425
- "enum": [
426
- "manual",
427
- "smart"
428
- ],
429
- "default": "manual"
430
- },
431
- "query": {
432
- "type": "string",
433
- "nullable": true
434
- },
435
- "public": {
436
- "type": "boolean"
437
- }
438
- },
439
- "required": [
440
- "id",
441
- "name",
442
- "icon",
443
- "parentId",
444
- "public"
445
- ]
446
- },
447
461
  "Tag": {
448
462
  "type": "object",
449
463
  "properties": {
@@ -693,6 +707,22 @@
693
707
  "low",
694
708
  "normal"
695
709
  ]
710
+ },
711
+ "importSessionId": {
712
+ "type": "string"
713
+ },
714
+ "source": {
715
+ "type": "string",
716
+ "enum": [
717
+ "api",
718
+ "web",
719
+ "cli",
720
+ "mobile",
721
+ "extension",
722
+ "singlefile",
723
+ "rss",
724
+ "import"
725
+ ]
696
726
  }
697
727
  }
698
728
  },
@@ -781,8 +811,18 @@
781
811
  }
782
812
  },
783
813
  "responses": {
814
+ "200": {
815
+ "description": "The bookmark already exists",
816
+ "content": {
817
+ "application/json": {
818
+ "schema": {
819
+ "$ref": "#/components/schemas/Bookmark"
820
+ }
821
+ }
822
+ }
823
+ },
784
824
  "201": {
785
- "description": "The created bookmark",
825
+ "description": "The bookmark got created",
786
826
  "content": {
787
827
  "application/json": {
788
828
  "schema": {
@@ -1135,6 +1175,20 @@
1135
1175
  "summary": {
1136
1176
  "type": "string",
1137
1177
  "nullable": true
1178
+ },
1179
+ "source": {
1180
+ "type": "string",
1181
+ "nullable": true,
1182
+ "enum": [
1183
+ "api",
1184
+ "web",
1185
+ "cli",
1186
+ "mobile",
1187
+ "extension",
1188
+ "singlefile",
1189
+ "rss",
1190
+ "import"
1191
+ ]
1138
1192
  }
1139
1193
  },
1140
1194
  "required": [
@@ -1245,6 +1299,20 @@
1245
1299
  "summary": {
1246
1300
  "type": "string",
1247
1301
  "nullable": true
1302
+ },
1303
+ "source": {
1304
+ "type": "string",
1305
+ "nullable": true,
1306
+ "enum": [
1307
+ "api",
1308
+ "web",
1309
+ "cli",
1310
+ "mobile",
1311
+ "extension",
1312
+ "singlefile",
1313
+ "rss",
1314
+ "import"
1315
+ ]
1248
1316
  }
1249
1317
  },
1250
1318
  "required": [
@@ -1469,6 +1537,70 @@
1469
1537
  }
1470
1538
  }
1471
1539
  },
1540
+ "/bookmarks/{bookmarkId}/lists": {
1541
+ "get": {
1542
+ "description": "Get lists of a bookmark",
1543
+ "summary": "Get lists of a bookmark",
1544
+ "tags": [
1545
+ "Bookmarks"
1546
+ ],
1547
+ "security": [
1548
+ {
1549
+ "bearerAuth": []
1550
+ }
1551
+ ],
1552
+ "parameters": [
1553
+ {
1554
+ "$ref": "#/components/parameters/BookmarkId"
1555
+ }
1556
+ ],
1557
+ "responses": {
1558
+ "200": {
1559
+ "description": "The list of highlights",
1560
+ "content": {
1561
+ "application/json": {
1562
+ "schema": {
1563
+ "type": "object",
1564
+ "properties": {
1565
+ "lists": {
1566
+ "type": "array",
1567
+ "items": {
1568
+ "$ref": "#/components/schemas/List"
1569
+ }
1570
+ }
1571
+ },
1572
+ "required": [
1573
+ "lists"
1574
+ ]
1575
+ }
1576
+ }
1577
+ }
1578
+ },
1579
+ "404": {
1580
+ "description": "Bookmark not found",
1581
+ "content": {
1582
+ "application/json": {
1583
+ "schema": {
1584
+ "type": "object",
1585
+ "properties": {
1586
+ "code": {
1587
+ "type": "string"
1588
+ },
1589
+ "message": {
1590
+ "type": "string"
1591
+ }
1592
+ },
1593
+ "required": [
1594
+ "code",
1595
+ "message"
1596
+ ]
1597
+ }
1598
+ }
1599
+ }
1600
+ }
1601
+ }
1602
+ }
1603
+ },
1472
1604
  "/bookmarks/{bookmarkId}/highlights": {
1473
1605
  "get": {
1474
1606
  "description": "Get highlights of a bookmark",
@@ -2292,6 +2424,60 @@
2292
2424
  "bearerAuth": []
2293
2425
  }
2294
2426
  ],
2427
+ "parameters": [
2428
+ {
2429
+ "schema": {
2430
+ "type": "string"
2431
+ },
2432
+ "required": false,
2433
+ "name": "nameContains",
2434
+ "in": "query"
2435
+ },
2436
+ {
2437
+ "schema": {
2438
+ "type": "string",
2439
+ "enum": [
2440
+ "name",
2441
+ "usage",
2442
+ "relevance"
2443
+ ],
2444
+ "default": "usage"
2445
+ },
2446
+ "required": false,
2447
+ "name": "sort",
2448
+ "in": "query"
2449
+ },
2450
+ {
2451
+ "schema": {
2452
+ "type": "string",
2453
+ "enum": [
2454
+ "ai",
2455
+ "human",
2456
+ "none"
2457
+ ]
2458
+ },
2459
+ "required": false,
2460
+ "name": "attachedBy",
2461
+ "in": "query"
2462
+ },
2463
+ {
2464
+ "schema": {
2465
+ "type": "string"
2466
+ },
2467
+ "required": false,
2468
+ "name": "cursor",
2469
+ "in": "query"
2470
+ },
2471
+ {
2472
+ "schema": {
2473
+ "type": "number",
2474
+ "nullable": true
2475
+ },
2476
+ "required": false,
2477
+ "name": "limit",
2478
+ "in": "query"
2479
+ }
2480
+ ],
2295
2481
  "responses": {
2296
2482
  "200": {
2297
2483
  "description": "Object with all tags data.",
@@ -2305,10 +2491,15 @@
2305
2491
  "items": {
2306
2492
  "$ref": "#/components/schemas/Tag"
2307
2493
  }
2494
+ },
2495
+ "nextCursor": {
2496
+ "type": "string",
2497
+ "nullable": true
2308
2498
  }
2309
2499
  },
2310
2500
  "required": [
2311
- "tags"
2501
+ "tags",
2502
+ "nextCursor"
2312
2503
  ]
2313
2504
  }
2314
2505
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karakeep_python_api
3
- Version: 1.2.4
3
+ Version: 1.3.1
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
@@ -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.2.4",
10
+ version="1.3.1",
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",
@@ -21,9 +21,9 @@ def test_get_all_bookmarks_paginated(karakeep_client: KarakeepAPI):
21
21
  try:
22
22
  # Get the first page
23
23
  page1 = karakeep_client.get_all_bookmarks(limit=2)
24
- assert isinstance(
25
- page1, datatypes.PaginatedBookmarks
26
- ), "Response should be PaginatedBookmarks model"
24
+ assert isinstance(page1, datatypes.PaginatedBookmarks), (
25
+ "Response should be PaginatedBookmarks model"
26
+ )
27
27
  assert isinstance(page1.bookmarks, list), "Bookmarks attribute should be a list"
28
28
  assert len(page1.bookmarks) <= 2, "Should return at most 'limit' bookmarks"
29
29
  logger.info(f"✓ Retrieved first page with {len(page1.bookmarks)} bookmarks.")
@@ -42,9 +42,9 @@ def test_get_all_bookmarks_paginated(karakeep_client: KarakeepAPI):
42
42
  )
43
43
  # Ensure bookmarks are different from page 1 (simple check)
44
44
  if page1.bookmarks and page2.bookmarks:
45
- assert (
46
- page1.bookmarks[0].id != page2.bookmarks[0].id
47
- ), "Bookmarks on page 1 and 2 should differ"
45
+ assert page1.bookmarks[0].id != page2.bookmarks[0].id, (
46
+ "Bookmarks on page 1 and 2 should differ"
47
+ )
48
48
  else:
49
49
  logger.info(" No next cursor found, pagination test ends.")
50
50
 
@@ -83,9 +83,9 @@ def test_get_all_lists(karakeep_client: KarakeepAPI):
83
83
  lists = karakeep_client.get_all_lists()
84
84
  assert isinstance(lists, list), "Response should be a list"
85
85
  if lists: # Only check elements if the list is not empty
86
- assert all(
87
- isinstance(item, datatypes.ListModel) for item in lists
88
- ), "All items should be ListModel instances"
86
+ assert all(isinstance(item, datatypes.ListModel) for item in lists), (
87
+ "All items should be ListModel instances"
88
+ )
89
89
  logger.info(f"✓ Successfully retrieved {len(lists)} lists.")
90
90
  except (APIError, AuthenticationError) as e:
91
91
  pytest.fail(f"API error during list retrieval: {e}")
@@ -118,11 +118,15 @@ def test_get_all_tags(karakeep_client: KarakeepAPI):
118
118
  """Test retrieving all tags."""
119
119
  try:
120
120
  tags = karakeep_client.get_all_tags()
121
- assert isinstance(tags, list), "Response should be a list"
121
+ assert isinstance(tags, datatypes.PaginatedTags), (
122
+ "Response should be of type PaginatedTags"
123
+ )
124
+ tags = tags.tags
125
+ assert isinstance(tags, list), "tags var should be of type list at this point"
122
126
  if tags: # Only check elements if the list is not empty
123
- assert all(
124
- isinstance(item, datatypes.Tag) for item in tags
125
- ), "All items should be Tag instances"
127
+ assert all(isinstance(item, datatypes.Tag) for item in tags), (
128
+ "All items should be Tag instances"
129
+ )
126
130
  logger.info(f"✓ Successfully retrieved {len(tags)} tags.")
127
131
  except (APIError, AuthenticationError) as e:
128
132
  pytest.fail(f"API error during tag retrieval: {e}")
@@ -156,12 +160,12 @@ def test_get_all_highlights_paginated(karakeep_client: KarakeepAPI):
156
160
  try:
157
161
  # Get the first page
158
162
  page1 = karakeep_client.get_all_highlights(limit=3)
159
- assert isinstance(
160
- page1, datatypes.PaginatedHighlights
161
- ), "Response should be PaginatedHighlights model"
162
- assert isinstance(
163
- page1.highlights, list
164
- ), "Highlights attribute should be a list"
163
+ assert isinstance(page1, datatypes.PaginatedHighlights), (
164
+ "Response should be PaginatedHighlights model"
165
+ )
166
+ assert isinstance(page1.highlights, list), (
167
+ "Highlights attribute should be a list"
168
+ )
165
169
  assert len(page1.highlights) <= 3, "Should return at most 'limit' highlights"
166
170
  logger.info(f"✓ Retrieved first page with {len(page1.highlights)} highlights.")
167
171
 
@@ -179,9 +183,9 @@ def test_get_all_highlights_paginated(karakeep_client: KarakeepAPI):
179
183
  )
180
184
  # Ensure highlights are different from page 1 (simple check)
181
185
  if page1.highlights and page2.highlights:
182
- assert (
183
- page1.highlights[0].id != page2.highlights[0].id
184
- ), "Highlights on page 1 and 2 should differ"
186
+ assert page1.highlights[0].id != page2.highlights[0].id, (
187
+ "Highlights on page 1 and 2 should differ"
188
+ )
185
189
  else:
186
190
  logger.info(" No next cursor found, pagination test ends.")
187
191
 
@@ -224,9 +228,9 @@ def test_openapi_spec_accessible(karakeep_client: KarakeepAPI):
224
228
  assert spec is not None, "openapi_spec attribute should not be None"
225
229
  assert isinstance(spec, dict), "openapi_spec should be a dictionary"
226
230
  # Check for a top-level key expected in an OpenAPI spec
227
- assert (
228
- "openapi" in spec
229
- ), "openapi_spec should contain the 'openapi' version key"
231
+ assert "openapi" in spec, (
232
+ "openapi_spec should contain the 'openapi' version key"
233
+ )
230
234
  logger.info(
231
235
  f"✓ Successfully accessed openapi_spec attribute. Version: {spec.get('openapi', 'N/A')}"
232
236
  )
@@ -284,9 +288,9 @@ def test_create_and_delete_list(karakeep_client: KarakeepAPI):
284
288
  created_list = karakeep_client.create_a_new_list(
285
289
  name=list_name, icon=list_icon, list_type="manual"
286
290
  )
287
- assert isinstance(
288
- created_list, datatypes.ListModel
289
- ), "Response should be a ListModel"
291
+ assert isinstance(created_list, datatypes.ListModel), (
292
+ "Response should be a ListModel"
293
+ )
290
294
  assert created_list.name == list_name, "Created list name should match"
291
295
  assert created_list.icon == list_icon, "Created list icon should match"
292
296
  assert created_list.id, "Created list must have an ID"
@@ -295,12 +299,12 @@ def test_create_and_delete_list(karakeep_client: KarakeepAPI):
295
299
 
296
300
  # 4. Verify the list appears in get_all_lists
297
301
  current_lists_after_create = karakeep_client.get_all_lists()
298
- assert (
299
- len(current_lists_after_create) == initial_list_count + 1
300
- ), "List count should increase by one after creation"
301
- assert any(
302
- lst.id == created_list_id for lst in current_lists_after_create
303
- ), "Created list should be present in the list of all lists"
302
+ assert len(current_lists_after_create) == initial_list_count + 1, (
303
+ "List count should increase by one after creation"
304
+ )
305
+ assert any(lst.id == created_list_id for lst in current_lists_after_create), (
306
+ "Created list should be present in the list of all lists"
307
+ )
304
308
  logger.info(f" List count after creation: {len(current_lists_after_create)}")
305
309
  logger.info(f"✓ Verified list {created_list_id} is present in get_all_lists.")
306
310
 
@@ -331,21 +335,21 @@ def test_create_and_delete_list(karakeep_client: KarakeepAPI):
331
335
  f"List with ID {created_list_id} should not exist after deletion, but get_a_single_list succeeded."
332
336
  )
333
337
  except APIError as e:
334
- assert (
335
- e.status_code == 404
336
- ), f"Expected 404 Not Found when getting deleted list, but got status {e.status_code}"
338
+ assert e.status_code == 404, (
339
+ f"Expected 404 Not Found when getting deleted list, but got status {e.status_code}"
340
+ )
337
341
  logger.info(
338
342
  f"✓ Confirmed list {created_list_id} is deleted (received 404)."
339
343
  )
340
344
 
341
345
  # 8. Verify list count decreased (optional check)
342
346
  final_lists = karakeep_client.get_all_lists()
343
- assert (
344
- len(final_lists) == initial_list_count
345
- ), "List count should return to initial count after deletion"
346
- assert not any(
347
- lst.id == created_list_id for lst in final_lists
348
- ), "Deleted list should not be present in the final list of all lists"
347
+ assert len(final_lists) == initial_list_count, (
348
+ "List count should return to initial count after deletion"
349
+ )
350
+ assert not any(lst.id == created_list_id for lst in final_lists), (
351
+ "Deleted list should not be present in the final list of all lists"
352
+ )
349
353
  logger.info(f" Final list count: {len(final_lists)}")
350
354
 
351
355
  except (APIError, AuthenticationError) as e:
@@ -410,12 +414,12 @@ def test_create_and_delete_bookmark(
410
414
  search_results = karakeep_client.search_bookmarks(
411
415
  q=search_query_component, limit=100, include_content=False
412
416
  )
413
- assert isinstance(
414
- search_results, datatypes.PaginatedBookmarks
415
- ), "Search response should be PaginatedBookmarks model"
416
- assert isinstance(
417
- search_results.bookmarks, list
418
- ), "Search results bookmarks attribute should be a list"
417
+ assert isinstance(search_results, datatypes.PaginatedBookmarks), (
418
+ "Search response should be PaginatedBookmarks model"
419
+ )
420
+ assert isinstance(search_results.bookmarks, list), (
421
+ "Search results bookmarks attribute should be a list"
422
+ )
419
423
 
420
424
  titles_in_search = [b.title for b in search_results.bookmarks]
421
425
  found_in_search = any(
@@ -425,9 +429,9 @@ def test_create_and_delete_bookmark(
425
429
  break
426
430
  else:
427
431
  time.sleep(3)
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}'."
432
+ assert found_in_search, (
433
+ 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}'."
434
+ )
431
435
  logger.info(
432
436
  f"✓ Found managed bookmark in search results for '{search_query_component}'."
433
437
  )
@@ -445,9 +449,9 @@ def test_create_and_delete_bookmark(
445
449
  capture_output=True,
446
450
  text=True,
447
451
  )
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}'"
452
+ assert created_bookmark_id in search_cli_output.stdout, (
453
+ f"Managed bookmark ID {created_bookmark_id} not found in CLI search output for '{search_query_component}'"
454
+ )
451
455
  logger.info(
452
456
  "✓ CLI search command executed successfully and contained the bookmark ID."
453
457
  )
@@ -498,12 +502,12 @@ def test_update_bookmark_title(
498
502
  updated_bookmark_partial = karakeep_client.update_a_bookmark(
499
503
  bookmark_id=created_bookmark_id, update_data=update_payload_api
500
504
  )
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}'"
505
+ assert isinstance(updated_bookmark_partial, dict), (
506
+ "Update response should be a dict"
507
+ )
508
+ assert updated_bookmark_partial.get("title") == target_api_title, (
509
+ f"Partial response title '{updated_bookmark_partial.get('title')}' does not match target API title '{target_api_title}'"
510
+ )
507
511
  logger.info(
508
512
  f"✓ API call to update_a_bookmark successful. Partial response title: '{updated_bookmark_partial.get('title')}'"
509
513
  )
@@ -516,9 +520,9 @@ def test_update_bookmark_title(
516
520
  bookmark_id=created_bookmark_id
517
521
  )
518
522
  assert isinstance(retrieved_bookmark_after_api_update, datatypes.Bookmark)
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}'"
523
+ assert retrieved_bookmark_after_api_update.title == target_api_title, (
524
+ f"Retrieved bookmark title '{retrieved_bookmark_after_api_update.title}' does not match expected API-updated title '{target_api_title}'"
525
+ )
522
526
  logger.info(
523
527
  f"✓ Successfully verified bookmark title updated by API to: '{retrieved_bookmark_after_api_update.title}'"
524
528
  )
@@ -549,9 +553,9 @@ def test_update_bookmark_title(
549
553
  bookmark_id=created_bookmark_id
550
554
  )
551
555
  assert isinstance(retrieved_bookmark_after_cli_update, datatypes.Bookmark)
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}'"
556
+ assert retrieved_bookmark_after_cli_update.title == target_cli_title, (
557
+ f"Retrieved bookmark title '{retrieved_bookmark_after_cli_update.title}' after CLI update does not match expected '{target_cli_title}'"
558
+ )
555
559
  logger.info(
556
560
  f"✓ Successfully verified bookmark title updated by CLI to: '{retrieved_bookmark_after_cli_update.title}'"
557
561
  )
@@ -617,9 +621,9 @@ def test_tag_lifecycle_on_bookmark(
617
621
  # assert isinstance(updated_tag, datatypes.Tag), "Update tag response should be Tag model"
618
622
  # assert updated_tag.name == updated_tag_name, "Tag name was not updated as expected"
619
623
  # logger.info(f"✓ Tag {tag_id_to_manage} updated to name '{updated_tag.name}'")
620
- assert (
621
- updated_tag["name"] == updated_tag_name
622
- ), "Tag name was not updated as expected"
624
+ assert updated_tag["name"] == updated_tag_name, (
625
+ "Tag name was not updated as expected"
626
+ )
623
627
  logger.info(f"✓ Tag {tag_id_to_manage} updated to name '{updated_tag['name']}'")
624
628
 
625
629
  # 3. Verify tag update by getting it directly
@@ -627,12 +631,12 @@ def test_tag_lifecycle_on_bookmark(
627
631
  f"\nFetching tag {tag_id_to_manage} to verify its name is '{updated_tag_name}'"
628
632
  )
629
633
  retrieved_tag = karakeep_client.get_a_single_tag(tag_id=tag_id_to_manage)
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"
634
+ assert isinstance(retrieved_tag, datatypes.Tag), (
635
+ "Get single tag response should be Tag model"
636
+ )
637
+ assert retrieved_tag.name == updated_tag_name, (
638
+ "Retrieved tag name does not match updated name"
639
+ )
636
640
  assert retrieved_tag.id == tag_id_to_manage, "Retrieved tag ID does not match"
637
641
  logger.info(
638
642
  f"✓ Verified tag {tag_id_to_manage} has name '{retrieved_tag.name}'"
@@ -670,9 +674,9 @@ def test_tag_lifecycle_on_bookmark(
670
674
  f"Tag {tag_id_to_manage} should not exist after deletion, but get_a_single_tag succeeded."
671
675
  )
672
676
  except APIError as e:
673
- assert (
674
- e.status_code == 404
675
- ), f"Expected 404 Not Found when getting deleted tag, but got status {e.status_code}"
677
+ assert e.status_code == 404, (
678
+ f"Expected 404 Not Found when getting deleted tag, but got status {e.status_code}"
679
+ )
676
680
  logger.info(
677
681
  f"✓ Confirmed tag {tag_id_to_manage} is deleted (received 404)."
678
682
  )
@@ -725,9 +729,9 @@ def test_cli_get_bookmarks_count_with_jq(karakeep_client: KarakeepAPI):
725
729
  logger.info(f"✓ Command returned {actual_count} bookmarks")
726
730
 
727
731
  # Check if we got exactly the requested number or fewer (if there aren't enough bookmarks)
728
- assert (
729
- actual_count <= test_limit
730
- ), f"Expected at most {test_limit} bookmarks, got {actual_count}"
732
+ assert actual_count <= test_limit, (
733
+ f"Expected at most {test_limit} bookmarks, got {actual_count}"
734
+ )
731
735
 
732
736
  # Check if we got any bookmarks at all (to ensure the test is meaningful)
733
737
  # This could fail if the account has no bookmarks
@@ -821,9 +825,9 @@ def test_asset_lifecycle_with_pdf(karakeep_client: KarakeepAPI):
821
825
  assert isinstance(uploaded_asset, datatypes.Asset)
822
826
  assert uploaded_asset.assetId, "Uploaded asset must have an ID"
823
827
  assert "pdf" in uploaded_asset.contentType.lower(), "Asset should be PDF type"
824
- assert (
825
- uploaded_asset.fileName == "PDF Bookmark Sample.pdf"
826
- ), "Asset filename should match the original file"
828
+ assert uploaded_asset.fileName == "PDF Bookmark Sample.pdf", (
829
+ "Asset filename should match the original file"
830
+ )
827
831
  uploaded_asset_id = uploaded_asset.assetId
828
832
  logger.info(f"✓ PDF uploaded with asset ID: {uploaded_asset_id}")
829
833
 
@@ -848,15 +852,15 @@ def test_asset_lifecycle_with_pdf(karakeep_client: KarakeepAPI):
848
852
  bookmark_id=created_bookmark_id
849
853
  )
850
854
  assert isinstance(retrieved_bookmark, datatypes.Bookmark)
851
- assert (
852
- len(retrieved_bookmark.assets) > 0
853
- ), "Bookmark should have at least one asset"
855
+ assert len(retrieved_bookmark.assets) > 0, (
856
+ "Bookmark should have at least one asset"
857
+ )
854
858
 
855
859
  # Check that our uploaded asset is among the bookmark's assets
856
860
  asset_ids = [asset.id for asset in retrieved_bookmark.assets]
857
- assert (
858
- uploaded_asset_id in asset_ids
859
- ), f"Uploaded asset {uploaded_asset_id} should be attached to bookmark"
861
+ assert uploaded_asset_id in asset_ids, (
862
+ f"Uploaded asset {uploaded_asset_id} should be attached to bookmark"
863
+ )
860
864
  logger.info(f"✓ Verified bookmark contains the PDF asset {uploaded_asset_id}")
861
865
 
862
866
  # 4. Retrieve and verify the asset content