karakeep-python-api 0.1.0__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (18) hide show
  1. {karakeep_python_api-0.1.0/karakeep_python_api.egg-info → karakeep_python_api-0.1.2}/PKG-INFO +6 -2
  2. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/README.md +5 -1
  3. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/karakeep_python_api/__main__.py +72 -26
  4. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/karakeep_python_api/karakeep_api.py +31 -4
  5. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2/karakeep_python_api.egg-info}/PKG-INFO +6 -2
  6. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/setup.py +1 -1
  7. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/tests/test_karakeep_api.py +99 -0
  8. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/LICENSE +0 -0
  9. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/MANIFEST.in +0 -0
  10. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/karakeep_python_api/__init__.py +0 -0
  11. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/karakeep_python_api/datatypes.py +0 -0
  12. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/karakeep_python_api/openapi_reference.json +0 -0
  13. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/karakeep_python_api.egg-info/SOURCES.txt +0 -0
  14. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/karakeep_python_api.egg-info/dependency_links.txt +0 -0
  15. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/karakeep_python_api.egg-info/entry_points.txt +0 -0
  16. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/karakeep_python_api.egg-info/requires.txt +0 -0
  17. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/karakeep_python_api.egg-info/top_level.txt +0 -0
  18. {karakeep_python_api-0.1.0 → karakeep_python_api-0.1.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karakeep_python_api
3
- Version: 0.1.0
3
+ Version: 0.1.2
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
@@ -81,7 +81,7 @@ The following table lists the public methods available in the `KarakeepAPI` clas
81
81
  | `search_bookmarks` | ✅ | Tested as part of create/delete flow. |
82
82
  | `get_a_single_bookmark` | ✅ | Tested as part of create/delete flow. |
83
83
  | `delete_a_bookmark` | ✅ | Tested as part of create/delete flow. |
84
- | `update_a_bookmark` | | |
84
+ | `update_a_bookmark` | | Tested for title updates. |
85
85
  | `summarize_a_bookmark` | ❌ | |
86
86
  | `attach_tags_to_a_bookmark` | ❌ | |
87
87
  | `detach_tags_from_a_bookmark` | ❌ | |
@@ -137,6 +137,7 @@ The client can be configured using the following environment variables:
137
137
  * `KARAKEEP_PYTHON_API_VERIFY_SSL`: Set to `false` to disable SSL certificate verification (default: `true`).
138
138
  * `KARAKEEP_PYTHON_API_VERBOSE`: Set to `true` to enable verbose debug logging for the client and CLI (default: `false`).
139
139
  * `KARAKEEP_PYTHON_API_DISABLE_RESPONSE_VALIDATION`: Set to `true` to disable Pydantic validation of API responses. The client will return raw dictionary/list data instead of Pydantic models (default: `false`).
140
+ * `KARAKEEP_PYTHON_API_ENSURE_ASCII`: Set to `true` to escape non-ASCII characters in the JSON output (default: `false`, which means Unicode characters are kept).
140
141
 
141
142
  ### Command Line Interface (CLI)
142
143
 
@@ -174,6 +175,9 @@ python -m karakeep_python_api get-all-lists | jq '.[0]'
174
175
  # Create a new bookmark from a link (body provided as JSON string)
175
176
  python -m karakeep_python_api create-a-new-bookmark --data '{"type": "link", "url": "https://example.com"}'
176
177
 
178
+ # Get all tags and ensure ASCII output (e.g., for compatibility with systems that don't handle Unicode well)
179
+ python -m karakeep_python_api --ascii get-all-tags
180
+
177
181
  # Dump the raw OpenAPI spec used by the client
178
182
  python -m karakeep_python_api --dump-openapi-specification
179
183
  ```
@@ -46,7 +46,7 @@ The following table lists the public methods available in the `KarakeepAPI` clas
46
46
  | `search_bookmarks` | ✅ | Tested as part of create/delete flow. |
47
47
  | `get_a_single_bookmark` | ✅ | Tested as part of create/delete flow. |
48
48
  | `delete_a_bookmark` | ✅ | Tested as part of create/delete flow. |
49
- | `update_a_bookmark` | | |
49
+ | `update_a_bookmark` | | Tested for title updates. |
50
50
  | `summarize_a_bookmark` | ❌ | |
51
51
  | `attach_tags_to_a_bookmark` | ❌ | |
52
52
  | `detach_tags_from_a_bookmark` | ❌ | |
@@ -102,6 +102,7 @@ The client can be configured using the following environment variables:
102
102
  * `KARAKEEP_PYTHON_API_VERIFY_SSL`: Set to `false` to disable SSL certificate verification (default: `true`).
103
103
  * `KARAKEEP_PYTHON_API_VERBOSE`: Set to `true` to enable verbose debug logging for the client and CLI (default: `false`).
104
104
  * `KARAKEEP_PYTHON_API_DISABLE_RESPONSE_VALIDATION`: Set to `true` to disable Pydantic validation of API responses. The client will return raw dictionary/list data instead of Pydantic models (default: `false`).
105
+ * `KARAKEEP_PYTHON_API_ENSURE_ASCII`: Set to `true` to escape non-ASCII characters in the JSON output (default: `false`, which means Unicode characters are kept).
105
106
 
106
107
  ### Command Line Interface (CLI)
107
108
 
@@ -139,6 +140,9 @@ python -m karakeep_python_api get-all-lists | jq '.[0]'
139
140
  # Create a new bookmark from a link (body provided as JSON string)
140
141
  python -m karakeep_python_api create-a-new-bookmark --data '{"type": "link", "url": "https://example.com"}'
141
142
 
143
+ # Get all tags and ensure ASCII output (e.g., for compatibility with systems that don't handle Unicode well)
144
+ python -m karakeep_python_api --ascii get-all-tags
145
+
142
146
  # Dump the raw OpenAPI spec used by the client
143
147
  python -m karakeep_python_api --dump-openapi-specification
144
148
  ```
@@ -1,8 +1,10 @@
1
+ from textwrap import dedent
1
2
  import inspect
2
3
  import json
3
4
  import sys
4
5
  import os
5
6
  import functools
7
+ import re # Import re module
6
8
  import click
7
9
  import traceback # Moved import to top
8
10
  from typing import Any, List, Dict, Optional, Callable, Union, get_origin, get_args, Literal
@@ -79,6 +81,14 @@ shared_options = [
79
81
  envvar="KARAKEEP_PYTHON_API_DISABLE_RESPONSE_VALIDATION",
80
82
  help="Disable Pydantic validation of API responses (returns raw data).",
81
83
  ),
84
+ click.option(
85
+ "--ascii",
86
+ "ensure_ascii", # Use 'ensure_ascii' as the destination variable name
87
+ is_flag=True,
88
+ default=False, # Default is False, meaning ensure_ascii=False by default
89
+ envvar="KARAKEEP_PYTHON_API_ENSURE_ASCII",
90
+ help="Escape non-ASCII characters in the JSON output (default: keep Unicode).",
91
+ ),
82
92
  ]
83
93
 
84
94
 
@@ -126,9 +136,9 @@ def print_openapi_spec(ctx, param, value):
126
136
  is_eager=True, # Process this option before others
127
137
  help="Dump the OpenAPI specification JSON to stdout and exit.",
128
138
  )
129
- @add_options(shared_options) # Apply shared options to the group
139
+ @add_options(shared_options) # Apply shared options to the group (ensure_ascii is now included)
130
140
  @click.pass_context
131
- def cli(ctx, base_url, api_key, verify_ssl, verbose, disable_response_validation):
141
+ def cli(ctx, base_url, api_key, verify_ssl, verbose, disable_response_validation, ensure_ascii):
132
142
  """
133
143
  Karakeep Python API Command Line Interface.
134
144
 
@@ -162,6 +172,7 @@ def cli(ctx, base_url, api_key, verify_ssl, verbose, disable_response_validation
162
172
  ctx.obj["DISABLE_RESPONSE_VALIDATION"] = (
163
173
  disable_response_validation # Store the flag
164
174
  )
175
+ ctx.obj["ENSURE_ASCII"] = ensure_ascii # Store the ensure_ascii flag
165
176
 
166
177
  # --- Configure Logger ---
167
178
  log_level = "DEBUG" if verbose else "INFO"
@@ -198,6 +209,7 @@ def create_click_command(
198
209
  verify_ssl = ctx.obj["VERIFY_SSL"]
199
210
  verbose = ctx.obj["VERBOSE"]
200
211
  disable_validation = ctx.obj["DISABLE_RESPONSE_VALIDATION"] # Retrieve flag
212
+ ensure_ascii_output = ctx.obj["ENSURE_ASCII"] # Retrieve ensure_ascii flag
201
213
 
202
214
  if not api_key:
203
215
  click.echo(
@@ -238,9 +250,32 @@ def create_click_command(
238
250
  k: v for k, v in call_args.items() if k in valid_arg_names
239
251
  }
240
252
 
241
- # Note: No special '--data' handling needed anymore for create_a_new_bookmark
242
- # Type conversion for basic types (str, int, bool) is handled by Click.
243
- # List/Dict parameters still expect JSON strings if used elsewhere.
253
+ # --- JSON Parsing for Dict/List Parameters ---
254
+ # Iterate through the expected parameters from the signature
255
+ for param_name, param_sig in signature.parameters.items():
256
+ if param_name in call_args:
257
+ param_value = call_args[param_name]
258
+ param_annotation = param_sig.annotation
259
+ origin = getattr(param_annotation, "__origin__", None)
260
+
261
+ # Check if the annotation is dict/list or typing.Dict/List
262
+ # and if the received value is a string (needs parsing)
263
+ if (
264
+ (param_annotation in (dict, list) or origin in (dict, list, Dict, List))
265
+ and isinstance(param_value, str)
266
+ ):
267
+ try:
268
+ # Attempt to parse the JSON string
269
+ call_args[param_name] = json.loads(param_value)
270
+ logger.debug(f"Parsed JSON string for parameter '{param_name}'.")
271
+ except json.JSONDecodeError as json_err:
272
+ # Handle invalid JSON input from the user
273
+ click.echo(
274
+ f"Error: Invalid JSON provided for parameter '{param_name.replace('_', '-')}': {json_err}",
275
+ err=True,
276
+ )
277
+ click.echo(f"Provided value: {param_value}", err=True)
278
+ ctx.exit(1)
244
279
 
245
280
  # Call the API method
246
281
  try:
@@ -262,9 +297,10 @@ def create_click_command(
262
297
  # Serialize and print the result
263
298
  if result is not None:
264
299
  output_data = serialize_output(result)
300
+ # Use ensure_ascii_output flag to control JSON encoding
265
301
  click.echo(
266
- json.dumps(output_data, indent=2)
267
- ) # Keep this for stdout result
302
+ json.dumps(output_data, indent=2, ensure_ascii=ensure_ascii_output)
303
+ )
268
304
  else:
269
305
  # Handle None result (e.g., 204 No Content) gracefully
270
306
  # Verbose check is implicitly handled by logger level
@@ -302,35 +338,45 @@ def create_click_command(
302
338
  click_params = []
303
339
  # Use the docstring from the original method (captured by functools.update_wrapper)
304
340
  docstring = api_method.__doc__ or f"Execute the {api_method_name} API operation."
341
+ docstring = dedent(docstring)
305
342
  docstring_lines = docstring.split("\n")
306
- help_text = docstring_lines[0].strip() # First line as short help
307
- full_help = docstring # Full docstring as help
343
+ help_text = " ".join(docstring.split("\n\n")[0].splitlines()).strip() # First lines as short help
344
+ # Full docstring as help
345
+ full_help = docstring
346
+
347
+ # tweak the whitespaces in the full help:
348
+ # full_help = full_help.replace("\n", "\n\n").replace("\n ", " ")
349
+ full_help = full_help.replace("\n ", " ")
350
+ full_help = full_help.replace("\n", "\n\n")
308
351
 
309
352
  # Extract parameter descriptions from the Args section of the docstring
310
353
  param_descriptions = {}
311
354
  in_args_section = False
312
355
  args_section_lines = []
356
+ assert "Returns:" in docstring
357
+ assert "Raises:" in docstring
313
358
  for line in docstring_lines:
314
- stripped_line = line.strip() # Corrected indentation
315
- if stripped_line == "Args:": # Corrected indentation
316
- in_args_section = True # Corrected indentation
359
+ stripped_line = line.strip()
360
+ if stripped_line == "Args:":
361
+ in_args_section = True
317
362
  elif (
318
363
  stripped_line == "Returns:" or stripped_line == "Raises:"
319
- ): # Corrected indentation
320
- in_args_section = False # Stop capturing when Returns/Raises section starts # Corrected indentation
321
- elif in_args_section and stripped_line: # Corrected indentation
322
- args_section_lines.append(stripped_line) # Corrected indentation
323
- # Try parsing the parameter name and description
324
- parts = stripped_line.split(":", 1) # Corrected indentation
325
- if len(parts) == 2: # Corrected indentation
326
- # Extract name, assuming format "name (type): description"
327
- name_part = parts[0].split(" ")[0] # Corrected indentation
328
- # Clean potential trailing parenthesis from type hint parsing
329
- name_part = name_part.rstrip(")") # Corrected indentation
330
- param_descriptions[name_part] = parts[
331
- 1
332
- ].strip() # Corrected indentation
364
+ ):
365
+ in_args_section = False # Stop capturing when Returns/Raises section starts
366
+ elif in_args_section and stripped_line:
367
+ args_section_lines.append(stripped_line)
368
+ # Use regex to capture 'param_name: description' structure, allowing leading whitespace
369
+ # Pattern: ^\s+ (parameter_name): \s* (description) $
370
+ match = re.match(r"^\s+([a-zA-Z_][a-zA-Z0-9_]*):\s+(.*)$", line)
371
+ if match and match.group(1) != "Example":
372
+ param_name = match.group(1)
373
+ description = match.group(2).strip()
374
+ param_descriptions[param_name] = description
375
+ logger.trace(f"Parsed docstring param: '{param_name}' -> '{description}'")
376
+ else:
377
+ param_descriptions[param_name] += " " + stripped_line
333
378
 
379
+ # Removed breakpoint() that was added for debugging
334
380
  # Add parameters from signature to Click command
335
381
  for param in params: # Use the filtered list from signature inspection
336
382
  param_name_cli = param.name.replace("_", "-") # Use kebab-case for CLI options
@@ -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.1.0"
88
+ VERSION: str = "0.1.2"
89
89
 
90
90
  def __init__(
91
91
  self,
@@ -209,7 +209,7 @@ class KarakeepAPI:
209
209
  self.strict_response_parsing = (
210
210
  strict_response_parsing # Currently unused but kept
211
211
  )
212
- self.last_request_time: float = 0.0 # Initialize timestamp for rate limiting
212
+ self.last_request_time: float = time.monotonic() # Initialize timestamp for rate limiting
213
213
 
214
214
  # --- Response Validation Setting ---
215
215
  # Argument takes precedence over environment variable
@@ -463,7 +463,7 @@ class KarakeepAPI:
463
463
  logger.warning(
464
464
  f"Error encounterd during requests. Trial={trial}/{max_trial}. Retrying after a small wait.\nError: {e}"
465
465
  )
466
- time.sleep(trial * 2)
466
+ time.sleep(trial * 3)
467
467
  else:
468
468
  raise
469
469
 
@@ -590,7 +590,7 @@ class KarakeepAPI:
590
590
  ) from e
591
591
 
592
592
  @optional_typecheck
593
- def _enforce_rate_limit(self, min_interval_sec: float = 1.0) -> None:
593
+ def _enforce_rate_limit(self, min_interval_sec: float = 2.0) -> None:
594
594
  """
595
595
  Ensures a minimum time interval between consecutive API calls.
596
596
 
@@ -855,6 +855,7 @@ class KarakeepAPI:
855
855
  """
856
856
  Delete a bookmark by its ID. Corresponds to DELETE /bookmarks/{bookmarkId}.
857
857
 
858
+ Args:
858
859
  bookmark_id: The ID (string) of the bookmark to delete.
859
860
 
860
861
  Returns:
@@ -901,6 +902,7 @@ class KarakeepAPI:
901
902
  Summarize a bookmark by its ID. Corresponds to POST /bookmarks/{bookmarkId}/summarize.
902
903
  This triggers the summarization process and returns the updated bookmark record (partially).
903
904
 
905
+ Args:
904
906
  bookmark_id: The ID (string) of the bookmark to summarize.
905
907
 
906
908
  Returns:
@@ -924,6 +926,7 @@ class KarakeepAPI:
924
926
  """
925
927
  Attach one or more tags to a bookmark. Corresponds to POST /bookmarks/{bookmarkId}/tags.
926
928
 
929
+ Args:
927
930
  bookmark_id: The ID (string) of the bookmark.
928
931
  tags_data: Dictionary specifying the tags to attach. Must contain a "tags" key
929
932
  which is a list of objects, each having *either* "tagId" (string) *or* "tagName" (string).
@@ -950,6 +953,7 @@ class KarakeepAPI:
950
953
  """
951
954
  Detach one or more tags from a bookmark. Corresponds to DELETE /bookmarks/{bookmarkId}/tags.
952
955
 
956
+ Args:
953
957
  bookmark_id: The ID (string) of the bookmark.
954
958
  tags_data: Dictionary specifying the tags to detach. Must contain a "tags" key
955
959
  which is a list of objects, each having *either* "tagId" (string) *or* "tagName" (string).
@@ -976,6 +980,7 @@ class KarakeepAPI:
976
980
  """
977
981
  Get all highlights associated with a specific bookmark. Corresponds to GET /bookmarks/{bookmarkId}/highlights.
978
982
 
983
+ Args:
979
984
  bookmark_id: The ID (string) of the bookmark.
980
985
 
981
986
  Returns:
@@ -1023,6 +1028,7 @@ class KarakeepAPI:
1023
1028
  """
1024
1029
  Attach a new asset to a bookmark. Corresponds to POST /bookmarks/{bookmarkId}/assets.
1025
1030
 
1031
+ Args:
1026
1032
  bookmark_id: The ID (string) of the bookmark.
1027
1033
  asset_data: Dictionary specifying the asset to attach. Must contain "id" (string) and "assetType" (string enum).
1028
1034
  Example: `{"id": "asset_id_string", "assetType": "screenshot"}`
@@ -1054,6 +1060,7 @@ class KarakeepAPI:
1054
1060
  Replace an existing asset associated with a bookmark with a new one.
1055
1061
  Corresponds to PUT /bookmarks/{bookmarkId}/assets/{assetId}.
1056
1062
 
1063
+ Args:
1057
1064
  bookmark_id: The ID (string) of the bookmark.
1058
1065
  asset_id: The ID (string) of the asset to be replaced.
1059
1066
  new_asset_data: Dictionary specifying the new asset ID. Must contain "assetId" (string).
@@ -1074,6 +1081,7 @@ class KarakeepAPI:
1074
1081
  """
1075
1082
  Detach an asset from a bookmark. Corresponds to DELETE /bookmarks/{bookmarkId}/assets/{assetId}.
1076
1083
 
1084
+ Args:
1077
1085
  bookmark_id: The ID (string) of the bookmark.
1078
1086
  asset_id: The ID (string) of the asset to detach.
1079
1087
 
@@ -1092,6 +1100,7 @@ class KarakeepAPI:
1092
1100
  """
1093
1101
  Get all lists for the current user. Corresponds to GET /lists.
1094
1102
 
1103
+ Returns:
1095
1104
  List[datatypes.ListModel]: A list of list objects.
1096
1105
  If response validation is disabled, returns the raw API response (dict/list).
1097
1106
 
@@ -1135,6 +1144,7 @@ class KarakeepAPI:
1135
1144
  """
1136
1145
  Create a new list (manual or smart). Corresponds to POST /lists.
1137
1146
 
1147
+ Args:
1138
1148
  list_data: Dictionary containing the data for the new list. Requires "name" (string) and "icon" (string).
1139
1149
  Optional fields include "description", "parentId", "type" ('manual' or 'smart'), "query".
1140
1150
  See the OpenAPI spec for details. Example: `{"name": "My List", "icon": "📚"}`
@@ -1163,6 +1173,7 @@ class KarakeepAPI:
1163
1173
  """
1164
1174
  Get a single list by its ID. Corresponds to GET /lists/{listId}.
1165
1175
 
1176
+ Args:
1166
1177
  list_id: The ID (string) of the list to retrieve.
1167
1178
 
1168
1179
  Returns:
@@ -1188,6 +1199,7 @@ class KarakeepAPI:
1188
1199
  """
1189
1200
  Delete a list by its ID. Corresponds to DELETE /lists/{listId}.
1190
1201
 
1202
+ Args:
1191
1203
  list_id: The ID (string) of the list to delete.
1192
1204
 
1193
1205
  Returns:
@@ -1208,6 +1220,7 @@ class KarakeepAPI:
1208
1220
  Update a list by its ID. Corresponds to PATCH /lists/{listId}.
1209
1221
  Allows updating fields like "name", "description", "icon", "parentId", "query".
1210
1222
 
1223
+ Args:
1211
1224
  list_id: The ID (string) of the list to update.
1212
1225
  update_data: A dictionary containing the fields to update (e.g., `{"name": "new name"}`).
1213
1226
  See the OpenAPI spec for allowed fields.
@@ -1241,6 +1254,7 @@ class KarakeepAPI:
1241
1254
  """
1242
1255
  Get the bookmarks contained within a specific list. Corresponds to GET /lists/{listId}/bookmarks.
1243
1256
 
1257
+ Args:
1244
1258
  list_id: The ID (string) of the list.
1245
1259
  limit: Maximum number of bookmarks to return (optional).
1246
1260
  cursor: Pagination cursor for the next page (optional).
@@ -1274,6 +1288,7 @@ class KarakeepAPI:
1274
1288
  """
1275
1289
  Add a bookmark to a specific list. Corresponds to PUT /lists/{listId}/bookmarks/{bookmarkId}.
1276
1290
 
1291
+ Args:
1277
1292
  list_id: The ID (string) of the list.
1278
1293
  bookmark_id: The ID (string) of the bookmark to add.
1279
1294
 
@@ -1292,6 +1307,7 @@ class KarakeepAPI:
1292
1307
  """
1293
1308
  Remove a bookmark from a specific list. Corresponds to DELETE /lists/{listId}/bookmarks/{bookmarkId}.
1294
1309
 
1310
+ Args:
1295
1311
  list_id: The ID (string) of the list.
1296
1312
  bookmark_id: The ID (string) of the bookmark to remove.
1297
1313
 
@@ -1310,6 +1326,7 @@ class KarakeepAPI:
1310
1326
  """
1311
1327
  Get all tags for the current user. Corresponds to GET /tags.
1312
1328
 
1329
+ Returns:
1313
1330
  List[datatypes.Tag1]: A list of tag objects, including bookmark counts.
1314
1331
  If response validation is disabled, returns the raw API response (dict/list).
1315
1332
 
@@ -1351,6 +1368,7 @@ class KarakeepAPI:
1351
1368
  """
1352
1369
  Get a single tag by its ID. Corresponds to GET /tags/{tagId}.
1353
1370
 
1371
+ Args:
1354
1372
  tag_id: The ID (string) of the tag to retrieve.
1355
1373
 
1356
1374
  Returns:
@@ -1376,6 +1394,7 @@ class KarakeepAPI:
1376
1394
  """
1377
1395
  Delete a tag by its ID. Corresponds to DELETE /tags/{tagId}.
1378
1396
 
1397
+ Args:
1379
1398
  tag_id: The ID (string) of the tag to delete.
1380
1399
 
1381
1400
  Returns:
@@ -1396,6 +1415,7 @@ class KarakeepAPI:
1396
1415
  Update a tag by its ID. Currently only supports updating the "name".
1397
1416
  Corresponds to PATCH /tags/{tagId}.
1398
1417
 
1418
+ Args:
1399
1419
  tag_id: The ID (string) of the tag to update.
1400
1420
  update_data: A dictionary containing the fields to update. Must include "name" (string).
1401
1421
  Example: `{"name": "new tag name"}`
@@ -1429,6 +1449,7 @@ class KarakeepAPI:
1429
1449
  """
1430
1450
  Get the bookmarks associated with a specific tag. Corresponds to GET /tags/{tagId}/bookmarks.
1431
1451
 
1452
+ Args:
1432
1453
  tag_id: The ID (string) of the tag.
1433
1454
  limit: Maximum number of bookmarks to return (optional).
1434
1455
  cursor: Pagination cursor for the next page (optional).
@@ -1493,6 +1514,7 @@ class KarakeepAPI:
1493
1514
  """
1494
1515
  Create a new highlight on a bookmark. Corresponds to POST /highlights.
1495
1516
 
1517
+ Args:
1496
1518
  highlight_data: Dictionary containing the data for the new highlight. Requires "bookmarkId" (string),
1497
1519
  "startOffset" (number), "endOffset" (number). Optional fields include "color", "text", "note".
1498
1520
  See the OpenAPI spec for details. Example: `{"bookmarkId": "...", "startOffset": 10, "endOffset": 25}`
@@ -1521,6 +1543,7 @@ class KarakeepAPI:
1521
1543
  """
1522
1544
  Get a single highlight by its ID. Corresponds to GET /highlights/{highlightId}.
1523
1545
 
1546
+ Args:
1524
1547
  highlight_id: The ID (string) of the highlight to retrieve.
1525
1548
 
1526
1549
  Returns:
@@ -1549,6 +1572,7 @@ class KarakeepAPI:
1549
1572
  Delete a highlight by its ID. Corresponds to DELETE /highlights/{highlightId}.
1550
1573
  Note: Unlike most DELETE endpoints, this returns the deleted highlight object on success (status 200).
1551
1574
 
1575
+ Args:
1552
1576
  highlight_id: The ID (string) of the highlight to delete.
1553
1577
 
1554
1578
  Returns:
@@ -1577,6 +1601,7 @@ class KarakeepAPI:
1577
1601
  Update a highlight by its ID. Currently only supports updating the "color".
1578
1602
  Corresponds to PATCH /highlights/{highlightId}.
1579
1603
 
1604
+ Args:
1580
1605
  highlight_id: The ID (string) of the highlight to update.
1581
1606
  update_data: A dictionary containing the fields to update. Must include "color" (string enum).
1582
1607
  See `datatypes.Color` enum. Example: `{"color": "red"}`
@@ -1604,6 +1629,7 @@ class KarakeepAPI:
1604
1629
  """
1605
1630
  Get information about the current authenticated user. Corresponds to GET /users/me.
1606
1631
 
1632
+ Returns:
1607
1633
  dict: A dictionary containing user information ('id', 'name', 'email').
1608
1634
  Validation is not performed on this response type by default.
1609
1635
 
@@ -1619,6 +1645,7 @@ class KarakeepAPI:
1619
1645
  """
1620
1646
  Get statistics about the current authenticated user's data. Corresponds to GET /users/me/stats.
1621
1647
 
1648
+ Returns:
1622
1649
  dict: A dictionary containing user statistics ('numBookmarks', 'numFavorites', 'numArchived', etc.).
1623
1650
  Validation is not performed on this response type by default.
1624
1651
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karakeep_python_api
3
- Version: 0.1.0
3
+ Version: 0.1.2
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
@@ -81,7 +81,7 @@ The following table lists the public methods available in the `KarakeepAPI` clas
81
81
  | `search_bookmarks` | ✅ | Tested as part of create/delete flow. |
82
82
  | `get_a_single_bookmark` | ✅ | Tested as part of create/delete flow. |
83
83
  | `delete_a_bookmark` | ✅ | Tested as part of create/delete flow. |
84
- | `update_a_bookmark` | | |
84
+ | `update_a_bookmark` | | Tested for title updates. |
85
85
  | `summarize_a_bookmark` | ❌ | |
86
86
  | `attach_tags_to_a_bookmark` | ❌ | |
87
87
  | `detach_tags_from_a_bookmark` | ❌ | |
@@ -137,6 +137,7 @@ The client can be configured using the following environment variables:
137
137
  * `KARAKEEP_PYTHON_API_VERIFY_SSL`: Set to `false` to disable SSL certificate verification (default: `true`).
138
138
  * `KARAKEEP_PYTHON_API_VERBOSE`: Set to `true` to enable verbose debug logging for the client and CLI (default: `false`).
139
139
  * `KARAKEEP_PYTHON_API_DISABLE_RESPONSE_VALIDATION`: Set to `true` to disable Pydantic validation of API responses. The client will return raw dictionary/list data instead of Pydantic models (default: `false`).
140
+ * `KARAKEEP_PYTHON_API_ENSURE_ASCII`: Set to `true` to escape non-ASCII characters in the JSON output (default: `false`, which means Unicode characters are kept).
140
141
 
141
142
  ### Command Line Interface (CLI)
142
143
 
@@ -174,6 +175,9 @@ python -m karakeep_python_api get-all-lists | jq '.[0]'
174
175
  # Create a new bookmark from a link (body provided as JSON string)
175
176
  python -m karakeep_python_api create-a-new-bookmark --data '{"type": "link", "url": "https://example.com"}'
176
177
 
178
+ # Get all tags and ensure ASCII output (e.g., for compatibility with systems that don't handle Unicode well)
179
+ python -m karakeep_python_api --ascii get-all-tags
180
+
177
181
  # Dump the raw OpenAPI spec used by the client
178
182
  python -m karakeep_python_api --dump-openapi-specification
179
183
  ```
@@ -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.1.0",
10
+ version="0.1.2",
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",
@@ -4,6 +4,7 @@ import subprocess
4
4
  import random
5
5
  import string
6
6
  import time
7
+ import json # Added for CLI test payload generation
7
8
 
8
9
  # Import API, errors, and datatypes from the main package
9
10
  from karakeep_python_api import KarakeepAPI, APIError, AuthenticationError, datatypes
@@ -484,6 +485,104 @@ def test_create_and_delete_bookmark(karakeep_client: KarakeepAPI):
484
485
  )
485
486
 
486
487
 
488
+ def test_update_bookmark_title(karakeep_client: KarakeepAPI):
489
+ """Test updating a bookmark's title via API and CLI."""
490
+ created_bookmark_id = None
491
+ # Generate a unique suffix for the initial title to avoid collisions
492
+ timestamp = int(time.time())
493
+ random_suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
494
+ original_title = f"Initial Test Title {timestamp}-{random_suffix}"
495
+ test_url = "https://en.wikipedia.org/wiki/PageForBookmarkUpdateTest" # A distinct URL for this test
496
+
497
+ target_api_title = "this is a test title"
498
+ target_cli_title = "this is a test title (CLI)"
499
+
500
+ try:
501
+ # 1. Create a bookmark with an initial title
502
+ print(f"\nAttempting to create bookmark for URL: {test_url} with original title: '{original_title}'")
503
+ created_bookmark = karakeep_client.create_a_new_bookmark(
504
+ type="link", url=test_url, title=original_title
505
+ )
506
+ assert isinstance(created_bookmark, datatypes.Bookmark), "Response should be a Bookmark model"
507
+ created_bookmark_id = created_bookmark.id
508
+ assert created_bookmark.id, "Created bookmark must have an ID"
509
+ assert created_bookmark.title == original_title, f"Initial title '{created_bookmark.title}' does not match expected '{original_title}'"
510
+ print(f"✓ Successfully created bookmark ID: {created_bookmark_id}, Title: '{created_bookmark.title}'")
511
+
512
+ # 2. Update the bookmark's title using the API client
513
+ print(f"\nAttempting to update bookmark ID {created_bookmark_id} title to: '{target_api_title}' via API")
514
+ update_payload_api = {"title": target_api_title}
515
+ updated_bookmark_partial = karakeep_client.update_a_bookmark(
516
+ bookmark_id=created_bookmark_id, update_data=update_payload_api
517
+ )
518
+ assert isinstance(updated_bookmark_partial, dict), "Update response should be a dict"
519
+ assert updated_bookmark_partial.get("title") == target_api_title, \
520
+ f"Partial response title '{updated_bookmark_partial.get('title')}' does not match target API title '{target_api_title}'"
521
+ print(f"✓ API call to update_a_bookmark successful. Partial response title: '{updated_bookmark_partial.get('title')}'")
522
+
523
+ # 3. Verify the API update by fetching the bookmark again
524
+ print(f"\nFetching bookmark ID {created_bookmark_id} to verify API title update.")
525
+ retrieved_bookmark_after_api_update = karakeep_client.get_a_single_bookmark(
526
+ bookmark_id=created_bookmark_id
527
+ )
528
+ assert isinstance(retrieved_bookmark_after_api_update, datatypes.Bookmark)
529
+ assert retrieved_bookmark_after_api_update.title == target_api_title, \
530
+ f"Retrieved bookmark title '{retrieved_bookmark_after_api_update.title}' does not match expected API-updated title '{target_api_title}'"
531
+ print(f"✓ Successfully verified bookmark title updated by API to: '{retrieved_bookmark_after_api_update.title}'")
532
+
533
+ # 4. Test CLI equivalent for updating the bookmark's title
534
+ print(f"\n Running CLI equivalent to update title to: '{target_cli_title}'")
535
+ cli_update_payload_json = json.dumps({"title": target_cli_title})
536
+ # Ensure the JSON string is properly quoted for the shell command
537
+ cli_update_command = f"python -m karakeep_python_api update-a-bookmark --bookmark-id {created_bookmark_id} --update-data '{cli_update_payload_json}'"
538
+
539
+ try:
540
+ subprocess.run(
541
+ cli_update_command,
542
+ shell=True,
543
+ check=True,
544
+ capture_output=True,
545
+ text=True,
546
+ )
547
+ print("✓ CLI update command executed successfully.")
548
+
549
+ # 5. Verify CLI update by fetching the bookmark again
550
+ print(f"\nFetching bookmark ID {created_bookmark_id} to verify CLI title update.")
551
+ retrieved_bookmark_after_cli_update = karakeep_client.get_a_single_bookmark(
552
+ bookmark_id=created_bookmark_id
553
+ )
554
+ assert isinstance(retrieved_bookmark_after_cli_update, datatypes.Bookmark)
555
+ assert retrieved_bookmark_after_cli_update.title == target_cli_title, \
556
+ f"Retrieved bookmark title '{retrieved_bookmark_after_cli_update.title}' after CLI update does not match expected '{target_cli_title}'"
557
+ print(f"✓ Successfully verified bookmark title updated by CLI to: '{retrieved_bookmark_after_cli_update.title}'")
558
+
559
+ except subprocess.CalledProcessError as e:
560
+ print(f" CLI update command failed with exit code {e.returncode}")
561
+ print(f" Command: {cli_update_command}")
562
+ print(f" Stdout: {e.stdout}")
563
+ print(f" Stderr: {e.stderr}")
564
+ pytest.fail(f"CLI command for update-a-bookmark failed: {e}")
565
+ except Exception as e:
566
+ pytest.fail(f"An unexpected error occurred running the CLI update command: {e}")
567
+
568
+ except (APIError, AuthenticationError) as e:
569
+ pytest.fail(f"API error during bookmark title update test: {e}")
570
+ except Exception as e:
571
+ pytest.fail(f"An unexpected error occurred during bookmark title update test: {e}")
572
+ finally:
573
+ # 6. Delete the bookmark (ensure cleanup)
574
+ if created_bookmark_id:
575
+ print(f"\nAttempting to delete bookmark ID: {created_bookmark_id} during cleanup.")
576
+ try:
577
+ karakeep_client.delete_a_bookmark(bookmark_id=created_bookmark_id)
578
+ print(f"✓ Successfully deleted bookmark ID: {created_bookmark_id}")
579
+ except Exception as e:
580
+ # Log cleanup failure but don't obscure original test failure if any
581
+ print(f"Error during cleanup: Failed to delete bookmark {created_bookmark_id}: {e}")
582
+ # Depending on test policy, you might re-raise or just log
583
+ # pytest.fail(f"Failed to delete bookmark {created_bookmark_id} during cleanup: {e}")
584
+
585
+
487
586
  # --- Test User Info/Stats Endpoints ---
488
587
 
489
588
  def test_get_current_user_stats(karakeep_client: KarakeepAPI):