karakeep-python-api 0.1.1__tar.gz → 0.1.3__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.1/karakeep_python_api.egg-info → karakeep_python_api-0.1.3}/PKG-INFO +45 -38
  2. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/README.md +44 -37
  3. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/karakeep_python_api/__main__.py +72 -26
  4. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/karakeep_python_api/karakeep_api.py +36 -4
  5. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/karakeep_python_api/openapi_reference.json +1 -1
  6. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3/karakeep_python_api.egg-info}/PKG-INFO +45 -38
  7. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/setup.py +1 -1
  8. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/tests/test_karakeep_api.py +212 -105
  9. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/LICENSE +0 -0
  10. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/MANIFEST.in +0 -0
  11. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/karakeep_python_api/__init__.py +0 -0
  12. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/karakeep_python_api/datatypes.py +0 -0
  13. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/karakeep_python_api.egg-info/SOURCES.txt +0 -0
  14. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/karakeep_python_api.egg-info/dependency_links.txt +0 -0
  15. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/karakeep_python_api.egg-info/entry_points.txt +0 -0
  16. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/karakeep_python_api.egg-info/requires.txt +0 -0
  17. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/karakeep_python_api.egg-info/top_level.txt +0 -0
  18. {karakeep_python_api-0.1.1 → karakeep_python_api-0.1.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karakeep_python_api
3
- Version: 0.1.1
3
+ Version: 0.1.3
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
@@ -72,43 +72,46 @@ The development process involved:
72
72
 
73
73
  ## API Method Coverage
74
74
 
75
- The following table lists the public methods available in the `KarakeepAPI` class and indicates whether they are currently covered by the automated test suite (`tests/test_karakeep_api.py`). Methods marked as "No" should be used with caution as their behavior has not been automatically verified.
76
-
77
- | Method Name | Tested | Remarks |
78
- | -------------------------------- | :----: | -------------------------------------------- |
79
- | `get_all_bookmarks` | ✅ | Tested with pagination. |
80
- | `create_a_new_bookmark` | ✅ | Only tested for `type="link"`. |
81
- | `search_bookmarks` | ✅ | Tested as part of create/delete flow. |
82
- | `get_a_single_bookmark` | ✅ | Tested as part of create/delete flow. |
83
- | `delete_a_bookmark` | ✅ | Tested as part of create/delete flow. |
84
- | `update_a_bookmark` | | |
85
- | `summarize_a_bookmark` | | |
86
- | `attach_tags_to_a_bookmark` | | |
87
- | `detach_tags_from_a_bookmark` | | |
88
- | `get_highlights_of_a_bookmark` | ❌ | |
89
- | `attach_asset` | | |
90
- | `replace_asset` | | |
91
- | `detach_asset` | ❌ | |
92
- | `get_all_lists` | | |
93
- | `create_a_new_list` | | Tested as part of create/delete flow. |
94
- | `get_a_single_list` | | Tested as part of create/delete flow. |
95
- | `delete_a_list` | ✅ | Tested as part of create/delete flow. |
96
- | `update_a_list` | | |
97
- | `get_a_bookmarks_in_a_list` | | |
98
- | `add_a_bookmark_to_a_list` | | |
99
- | `remove_a_bookmark_from_a_list` | ❌ | |
100
- | `get_all_tags` | | |
101
- | `get_a_single_tag` | ❌ | |
102
- | `delete_a_tag` | ❌ | |
103
- | `update_a_tag` | | |
104
- | `get_a_bookmarks_with_the_tag` | | |
105
- | `get_all_highlights` | ✅ | Tested with pagination. |
106
- | `create_a_new_highlight` | | |
107
- | `get_a_single_highlight` | ❌ | |
108
- | `delete_a_highlight` | | |
109
- | `update_a_highlight` | ❌ | |
110
- | `get_current_user_info` | | Tested indirectly during client initialization. |
111
- | `get_current_user_stats` | | |
75
+ The following table lists the public methods available in the `KarakeepAPI` class.
76
+ * The "Pytest" column indicates whether the Python library method is covered by the automated test suite (`tests/test_karakeep_api.py`).
77
+ * The "CLI" column indicates whether the corresponding CLI command for that method is tested within the Pytest suite (typically via `subprocess`).
78
+ Methods or CLI commands marked with ❌ should be used with caution as their behavior has not been automatically verified within the test suite.
79
+
80
+ | Method Name | Pytest | CLI | Remarks |
81
+ | -------------------------------- | :----: | :--: | -------------------------------------------- |
82
+ | `get_all_bookmarks` | ✅ | ✅ | Tested with pagination. |
83
+ | `create_a_new_bookmark` | ✅ | ❌ | Pytest for `type="link"` via fixture. CLI not directly tested. |
84
+ | `search_bookmarks` | || Seems to be nondeterministic and fails if using more than 3 words |
85
+ | `get_a_single_bookmark` | || |
86
+ | `delete_a_bookmark` | || |
87
+ | `update_a_bookmark` | || Tested for title updates. |
88
+ | `summarize_a_bookmark` | ❌ | ❌ | |
89
+ | `attach_tags_to_a_bookmark` | || |
90
+ | `detach_tags_from_a_bookmark` | || |
91
+ | `get_highlights_of_a_bookmark` | ❌ || Works from the CLI; not yet added to Pytest. |
92
+ | `attach_asset` | | ❌ | |
93
+ | `replace_asset` | || |
94
+ | `detach_asset` | || |
95
+ | `get_all_lists` | ✅ || |
96
+ | `create_a_new_list` | || |
97
+ | `get_a_single_list` | || |
98
+ | `delete_a_list` | || |
99
+ | `update_a_list` | ❌ | ❌ | |
100
+ | `get_a_bookmarks_in_a_list` | | ❌ | |
101
+ | `add_a_bookmark_to_a_list` | ❌ | ❌ | |
102
+ | `remove_a_bookmark_from_a_list` | ❌ | ❌ | |
103
+ | `get_all_tags` | | ✅ | |
104
+ | `get_a_single_tag` | || |
105
+ | `delete_a_tag` | ✅ || |
106
+ | `update_a_tag` | || No output validation due to [server bug](https://github.com/karakeep-app/karakeep/issues/1365). |
107
+ | `get_a_bookmarks_with_the_tag` | ❌ | ❌ | |
108
+ | `get_all_highlights` | || Tested with pagination. |
109
+ | `create_a_new_highlight` | ❌ | ❌ | |
110
+ | `get_a_single_highlight` | || |
111
+ | `delete_a_highlight` | || Works from the CLI; not yet added to Pytest. |
112
+ | `update_a_highlight` | ❌ | ❌ | |
113
+ | `get_current_user_info` | ✅ | ❌ | Pytest: Tested indirectly during client init. CLI not directly tested. |
114
+ | `get_current_user_stats` | ✅ | ✅ | |
112
115
 
113
116
  ## Installation
114
117
 
@@ -137,6 +140,7 @@ The client can be configured using the following environment variables:
137
140
  * `KARAKEEP_PYTHON_API_VERIFY_SSL`: Set to `false` to disable SSL certificate verification (default: `true`).
138
141
  * `KARAKEEP_PYTHON_API_VERBOSE`: Set to `true` to enable verbose debug logging for the client and CLI (default: `false`).
139
142
  * `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`).
143
+ * `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
144
 
141
145
  ### Command Line Interface (CLI)
142
146
 
@@ -174,6 +178,9 @@ python -m karakeep_python_api get-all-lists | jq '.[0]'
174
178
  # Create a new bookmark from a link (body provided as JSON string)
175
179
  python -m karakeep_python_api create-a-new-bookmark --data '{"type": "link", "url": "https://example.com"}'
176
180
 
181
+ # Get all tags and ensure ASCII output (e.g., for compatibility with systems that don't handle Unicode well)
182
+ python -m karakeep_python_api --ascii get-all-tags
183
+
177
184
  # Dump the raw OpenAPI spec used by the client
178
185
  python -m karakeep_python_api --dump-openapi-specification
179
186
  ```
@@ -37,43 +37,46 @@ The development process involved:
37
37
 
38
38
  ## API Method Coverage
39
39
 
40
- The following table lists the public methods available in the `KarakeepAPI` class and indicates whether they are currently covered by the automated test suite (`tests/test_karakeep_api.py`). Methods marked as "No" should be used with caution as their behavior has not been automatically verified.
41
-
42
- | Method Name | Tested | Remarks |
43
- | -------------------------------- | :----: | -------------------------------------------- |
44
- | `get_all_bookmarks` | ✅ | Tested with pagination. |
45
- | `create_a_new_bookmark` | ✅ | Only tested for `type="link"`. |
46
- | `search_bookmarks` | ✅ | Tested as part of create/delete flow. |
47
- | `get_a_single_bookmark` | ✅ | Tested as part of create/delete flow. |
48
- | `delete_a_bookmark` | ✅ | Tested as part of create/delete flow. |
49
- | `update_a_bookmark` | | |
50
- | `summarize_a_bookmark` | | |
51
- | `attach_tags_to_a_bookmark` | | |
52
- | `detach_tags_from_a_bookmark` | | |
53
- | `get_highlights_of_a_bookmark` | ❌ | |
54
- | `attach_asset` | | |
55
- | `replace_asset` | | |
56
- | `detach_asset` | ❌ | |
57
- | `get_all_lists` | | |
58
- | `create_a_new_list` | | Tested as part of create/delete flow. |
59
- | `get_a_single_list` | | Tested as part of create/delete flow. |
60
- | `delete_a_list` | ✅ | Tested as part of create/delete flow. |
61
- | `update_a_list` | | |
62
- | `get_a_bookmarks_in_a_list` | | |
63
- | `add_a_bookmark_to_a_list` | | |
64
- | `remove_a_bookmark_from_a_list` | ❌ | |
65
- | `get_all_tags` | | |
66
- | `get_a_single_tag` | ❌ | |
67
- | `delete_a_tag` | ❌ | |
68
- | `update_a_tag` | | |
69
- | `get_a_bookmarks_with_the_tag` | | |
70
- | `get_all_highlights` | ✅ | Tested with pagination. |
71
- | `create_a_new_highlight` | | |
72
- | `get_a_single_highlight` | ❌ | |
73
- | `delete_a_highlight` | | |
74
- | `update_a_highlight` | ❌ | |
75
- | `get_current_user_info` | | Tested indirectly during client initialization. |
76
- | `get_current_user_stats` | | |
40
+ The following table lists the public methods available in the `KarakeepAPI` class.
41
+ * The "Pytest" column indicates whether the Python library method is covered by the automated test suite (`tests/test_karakeep_api.py`).
42
+ * The "CLI" column indicates whether the corresponding CLI command for that method is tested within the Pytest suite (typically via `subprocess`).
43
+ Methods or CLI commands marked with ❌ should be used with caution as their behavior has not been automatically verified within the test suite.
44
+
45
+ | Method Name | Pytest | CLI | Remarks |
46
+ | -------------------------------- | :----: | :--: | -------------------------------------------- |
47
+ | `get_all_bookmarks` | ✅ | ✅ | Tested with pagination. |
48
+ | `create_a_new_bookmark` | ✅ | ❌ | Pytest for `type="link"` via fixture. CLI not directly tested. |
49
+ | `search_bookmarks` | || Seems to be nondeterministic and fails if using more than 3 words |
50
+ | `get_a_single_bookmark` | || |
51
+ | `delete_a_bookmark` | || |
52
+ | `update_a_bookmark` | || Tested for title updates. |
53
+ | `summarize_a_bookmark` | ❌ | ❌ | |
54
+ | `attach_tags_to_a_bookmark` | || |
55
+ | `detach_tags_from_a_bookmark` | || |
56
+ | `get_highlights_of_a_bookmark` | ❌ || Works from the CLI; not yet added to Pytest. |
57
+ | `attach_asset` | | ❌ | |
58
+ | `replace_asset` | || |
59
+ | `detach_asset` | || |
60
+ | `get_all_lists` | ✅ || |
61
+ | `create_a_new_list` | || |
62
+ | `get_a_single_list` | || |
63
+ | `delete_a_list` | || |
64
+ | `update_a_list` | ❌ | ❌ | |
65
+ | `get_a_bookmarks_in_a_list` | | ❌ | |
66
+ | `add_a_bookmark_to_a_list` | ❌ | ❌ | |
67
+ | `remove_a_bookmark_from_a_list` | ❌ | ❌ | |
68
+ | `get_all_tags` | | ✅ | |
69
+ | `get_a_single_tag` | || |
70
+ | `delete_a_tag` | ✅ || |
71
+ | `update_a_tag` | || No output validation due to [server bug](https://github.com/karakeep-app/karakeep/issues/1365). |
72
+ | `get_a_bookmarks_with_the_tag` | ❌ | ❌ | |
73
+ | `get_all_highlights` | || Tested with pagination. |
74
+ | `create_a_new_highlight` | ❌ | ❌ | |
75
+ | `get_a_single_highlight` | || |
76
+ | `delete_a_highlight` | || Works from the CLI; not yet added to Pytest. |
77
+ | `update_a_highlight` | ❌ | ❌ | |
78
+ | `get_current_user_info` | ✅ | ❌ | Pytest: Tested indirectly during client init. CLI not directly tested. |
79
+ | `get_current_user_stats` | ✅ | ✅ | |
77
80
 
78
81
  ## Installation
79
82
 
@@ -102,6 +105,7 @@ The client can be configured using the following environment variables:
102
105
  * `KARAKEEP_PYTHON_API_VERIFY_SSL`: Set to `false` to disable SSL certificate verification (default: `true`).
103
106
  * `KARAKEEP_PYTHON_API_VERBOSE`: Set to `true` to enable verbose debug logging for the client and CLI (default: `false`).
104
107
  * `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`).
108
+ * `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
109
 
106
110
  ### Command Line Interface (CLI)
107
111
 
@@ -139,6 +143,9 @@ python -m karakeep_python_api get-all-lists | jq '.[0]'
139
143
  # Create a new bookmark from a link (body provided as JSON string)
140
144
  python -m karakeep_python_api create-a-new-bookmark --data '{"type": "link", "url": "https://example.com"}'
141
145
 
146
+ # Get all tags and ensure ASCII output (e.g., for compatibility with systems that don't handle Unicode well)
147
+ python -m karakeep_python_api --ascii get-all-tags
148
+
142
149
  # Dump the raw OpenAPI spec used by the client
143
150
  python -m karakeep_python_api --dump-openapi-specification
144
151
  ```
@@ -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.1"
88
+ VERSION: str = "0.1.3"
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"}`
@@ -1415,6 +1435,11 @@ class KarakeepAPI:
1415
1435
  logger.debug("Skipping response validation as requested.")
1416
1436
  return response_data
1417
1437
  else:
1438
+ # As of version 0.24.1 of karakeep: we do not check the correct
1439
+ # validation type because there is an error on the
1440
+ # server side: https://github.com/karakeep-app/karakeep/issues/1365
1441
+ return response_data
1442
+
1418
1443
  # Response should match Tag1 schema
1419
1444
  return datatypes.Tag1.model_validate(response_data)
1420
1445
 
@@ -1429,6 +1454,7 @@ class KarakeepAPI:
1429
1454
  """
1430
1455
  Get the bookmarks associated with a specific tag. Corresponds to GET /tags/{tagId}/bookmarks.
1431
1456
 
1457
+ Args:
1432
1458
  tag_id: The ID (string) of the tag.
1433
1459
  limit: Maximum number of bookmarks to return (optional).
1434
1460
  cursor: Pagination cursor for the next page (optional).
@@ -1493,6 +1519,7 @@ class KarakeepAPI:
1493
1519
  """
1494
1520
  Create a new highlight on a bookmark. Corresponds to POST /highlights.
1495
1521
 
1522
+ Args:
1496
1523
  highlight_data: Dictionary containing the data for the new highlight. Requires "bookmarkId" (string),
1497
1524
  "startOffset" (number), "endOffset" (number). Optional fields include "color", "text", "note".
1498
1525
  See the OpenAPI spec for details. Example: `{"bookmarkId": "...", "startOffset": 10, "endOffset": 25}`
@@ -1521,6 +1548,7 @@ class KarakeepAPI:
1521
1548
  """
1522
1549
  Get a single highlight by its ID. Corresponds to GET /highlights/{highlightId}.
1523
1550
 
1551
+ Args:
1524
1552
  highlight_id: The ID (string) of the highlight to retrieve.
1525
1553
 
1526
1554
  Returns:
@@ -1549,6 +1577,7 @@ class KarakeepAPI:
1549
1577
  Delete a highlight by its ID. Corresponds to DELETE /highlights/{highlightId}.
1550
1578
  Note: Unlike most DELETE endpoints, this returns the deleted highlight object on success (status 200).
1551
1579
 
1580
+ Args:
1552
1581
  highlight_id: The ID (string) of the highlight to delete.
1553
1582
 
1554
1583
  Returns:
@@ -1577,6 +1606,7 @@ class KarakeepAPI:
1577
1606
  Update a highlight by its ID. Currently only supports updating the "color".
1578
1607
  Corresponds to PATCH /highlights/{highlightId}.
1579
1608
 
1609
+ Args:
1580
1610
  highlight_id: The ID (string) of the highlight to update.
1581
1611
  update_data: A dictionary containing the fields to update. Must include "color" (string enum).
1582
1612
  See `datatypes.Color` enum. Example: `{"color": "red"}`
@@ -1604,6 +1634,7 @@ class KarakeepAPI:
1604
1634
  """
1605
1635
  Get information about the current authenticated user. Corresponds to GET /users/me.
1606
1636
 
1637
+ Returns:
1607
1638
  dict: A dictionary containing user information ('id', 'name', 'email').
1608
1639
  Validation is not performed on this response type by default.
1609
1640
 
@@ -1619,6 +1650,7 @@ class KarakeepAPI:
1619
1650
  """
1620
1651
  Get statistics about the current authenticated user's data. Corresponds to GET /users/me/stats.
1621
1652
 
1653
+ Returns:
1622
1654
  dict: A dictionary containing user statistics ('numBookmarks', 'numFavorites', 'numArchived', etc.).
1623
1655
  Validation is not performed on this response type by default.
1624
1656
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karakeep_python_api
3
- Version: 0.1.1
3
+ Version: 0.1.3
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
@@ -72,43 +72,46 @@ The development process involved:
72
72
 
73
73
  ## API Method Coverage
74
74
 
75
- The following table lists the public methods available in the `KarakeepAPI` class and indicates whether they are currently covered by the automated test suite (`tests/test_karakeep_api.py`). Methods marked as "No" should be used with caution as their behavior has not been automatically verified.
76
-
77
- | Method Name | Tested | Remarks |
78
- | -------------------------------- | :----: | -------------------------------------------- |
79
- | `get_all_bookmarks` | ✅ | Tested with pagination. |
80
- | `create_a_new_bookmark` | ✅ | Only tested for `type="link"`. |
81
- | `search_bookmarks` | ✅ | Tested as part of create/delete flow. |
82
- | `get_a_single_bookmark` | ✅ | Tested as part of create/delete flow. |
83
- | `delete_a_bookmark` | ✅ | Tested as part of create/delete flow. |
84
- | `update_a_bookmark` | | |
85
- | `summarize_a_bookmark` | | |
86
- | `attach_tags_to_a_bookmark` | | |
87
- | `detach_tags_from_a_bookmark` | | |
88
- | `get_highlights_of_a_bookmark` | ❌ | |
89
- | `attach_asset` | | |
90
- | `replace_asset` | | |
91
- | `detach_asset` | ❌ | |
92
- | `get_all_lists` | | |
93
- | `create_a_new_list` | | Tested as part of create/delete flow. |
94
- | `get_a_single_list` | | Tested as part of create/delete flow. |
95
- | `delete_a_list` | ✅ | Tested as part of create/delete flow. |
96
- | `update_a_list` | | |
97
- | `get_a_bookmarks_in_a_list` | | |
98
- | `add_a_bookmark_to_a_list` | | |
99
- | `remove_a_bookmark_from_a_list` | ❌ | |
100
- | `get_all_tags` | | |
101
- | `get_a_single_tag` | ❌ | |
102
- | `delete_a_tag` | ❌ | |
103
- | `update_a_tag` | | |
104
- | `get_a_bookmarks_with_the_tag` | | |
105
- | `get_all_highlights` | ✅ | Tested with pagination. |
106
- | `create_a_new_highlight` | | |
107
- | `get_a_single_highlight` | ❌ | |
108
- | `delete_a_highlight` | | |
109
- | `update_a_highlight` | ❌ | |
110
- | `get_current_user_info` | | Tested indirectly during client initialization. |
111
- | `get_current_user_stats` | | |
75
+ The following table lists the public methods available in the `KarakeepAPI` class.
76
+ * The "Pytest" column indicates whether the Python library method is covered by the automated test suite (`tests/test_karakeep_api.py`).
77
+ * The "CLI" column indicates whether the corresponding CLI command for that method is tested within the Pytest suite (typically via `subprocess`).
78
+ Methods or CLI commands marked with ❌ should be used with caution as their behavior has not been automatically verified within the test suite.
79
+
80
+ | Method Name | Pytest | CLI | Remarks |
81
+ | -------------------------------- | :----: | :--: | -------------------------------------------- |
82
+ | `get_all_bookmarks` | ✅ | ✅ | Tested with pagination. |
83
+ | `create_a_new_bookmark` | ✅ | ❌ | Pytest for `type="link"` via fixture. CLI not directly tested. |
84
+ | `search_bookmarks` | || Seems to be nondeterministic and fails if using more than 3 words |
85
+ | `get_a_single_bookmark` | || |
86
+ | `delete_a_bookmark` | || |
87
+ | `update_a_bookmark` | || Tested for title updates. |
88
+ | `summarize_a_bookmark` | ❌ | ❌ | |
89
+ | `attach_tags_to_a_bookmark` | || |
90
+ | `detach_tags_from_a_bookmark` | || |
91
+ | `get_highlights_of_a_bookmark` | ❌ || Works from the CLI; not yet added to Pytest. |
92
+ | `attach_asset` | | ❌ | |
93
+ | `replace_asset` | || |
94
+ | `detach_asset` | || |
95
+ | `get_all_lists` | ✅ || |
96
+ | `create_a_new_list` | || |
97
+ | `get_a_single_list` | || |
98
+ | `delete_a_list` | || |
99
+ | `update_a_list` | ❌ | ❌ | |
100
+ | `get_a_bookmarks_in_a_list` | | ❌ | |
101
+ | `add_a_bookmark_to_a_list` | ❌ | ❌ | |
102
+ | `remove_a_bookmark_from_a_list` | ❌ | ❌ | |
103
+ | `get_all_tags` | | ✅ | |
104
+ | `get_a_single_tag` | || |
105
+ | `delete_a_tag` | ✅ || |
106
+ | `update_a_tag` | || No output validation due to [server bug](https://github.com/karakeep-app/karakeep/issues/1365). |
107
+ | `get_a_bookmarks_with_the_tag` | ❌ | ❌ | |
108
+ | `get_all_highlights` | || Tested with pagination. |
109
+ | `create_a_new_highlight` | ❌ | ❌ | |
110
+ | `get_a_single_highlight` | || |
111
+ | `delete_a_highlight` | || Works from the CLI; not yet added to Pytest. |
112
+ | `update_a_highlight` | ❌ | ❌ | |
113
+ | `get_current_user_info` | ✅ | ❌ | Pytest: Tested indirectly during client init. CLI not directly tested. |
114
+ | `get_current_user_stats` | ✅ | ✅ | |
112
115
 
113
116
  ## Installation
114
117
 
@@ -137,6 +140,7 @@ The client can be configured using the following environment variables:
137
140
  * `KARAKEEP_PYTHON_API_VERIFY_SSL`: Set to `false` to disable SSL certificate verification (default: `true`).
138
141
  * `KARAKEEP_PYTHON_API_VERBOSE`: Set to `true` to enable verbose debug logging for the client and CLI (default: `false`).
139
142
  * `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`).
143
+ * `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
144
 
141
145
  ### Command Line Interface (CLI)
142
146
 
@@ -174,6 +178,9 @@ python -m karakeep_python_api get-all-lists | jq '.[0]'
174
178
  # Create a new bookmark from a link (body provided as JSON string)
175
179
  python -m karakeep_python_api create-a-new-bookmark --data '{"type": "link", "url": "https://example.com"}'
176
180
 
181
+ # Get all tags and ensure ASCII output (e.g., for compatibility with systems that don't handle Unicode well)
182
+ python -m karakeep_python_api --ascii get-all-tags
183
+
177
184
  # Dump the raw OpenAPI spec used by the client
178
185
  python -m karakeep_python_api --dump-openapi-specification
179
186
  ```
@@ -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.1",
10
+ version="0.1.3",
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
@@ -339,55 +340,66 @@ def test_create_and_delete_list(karakeep_client: KarakeepAPI):
339
340
  print("\nSkipping deletion because list creation failed or ID was not obtained.")
340
341
 
341
342
 
342
- def test_create_and_delete_bookmark(karakeep_client: KarakeepAPI):
343
- """Test creating a new URL bookmark and then deleting it."""
344
- created_bookmark_id = None
345
- test_url = "https://en.wikipedia.org/wiki/Example"
343
+ def test_create_and_delete_bookmark(karakeep_client: KarakeepAPI, managed_bookmark: datatypes.Bookmark):
344
+ """
345
+ Test verifying a created bookmark (via fixture) and searching for it.
346
+ The fixture handles creation and deletion.
347
+ """
348
+ created_bookmark_id = managed_bookmark.id
349
+ test_url = managed_bookmark.content.url # Get URL from fixture
350
+ original_title = managed_bookmark.title # Get title from fixture
351
+
346
352
  try:
347
- # 1. Define bookmark payload
348
- print(f"\nAttempting to create bookmark for URL: {test_url}")
353
+ # 1. Bookmark is already created by the 'managed_bookmark' fixture.
354
+ print(f"\nUsing managed bookmark ID: {created_bookmark_id}, URL: '{test_url}', Title: '{original_title}'")
349
355
 
350
- # 2. Create the bookmark
351
- # Call the method with keyword arguments matching its signature
352
- created_bookmark = karakeep_client.create_a_new_bookmark(
353
- type="link", url=test_url
354
- )
355
- assert isinstance(
356
- created_bookmark, datatypes.Bookmark
357
- ), "Response should be a Bookmark model"
358
- assert created_bookmark.content.url == test_url, "Created bookmark URL should match"
359
- assert created_bookmark.id, "Created bookmark must have an ID"
360
- created_bookmark_id = created_bookmark.id
361
- print(f"✓ Successfully created bookmark with ID: {created_bookmark_id}")
362
-
363
- # 3. Verify the bookmark exists by getting it directly
356
+ # 2. Verify the bookmark exists by getting it directly
364
357
  retrieved_bookmark = karakeep_client.get_a_single_bookmark(
365
358
  bookmark_id=created_bookmark_id
366
359
  )
367
360
  assert isinstance(retrieved_bookmark, datatypes.Bookmark)
368
361
  assert retrieved_bookmark.id == created_bookmark_id
369
362
  assert retrieved_bookmark.content.url == test_url
370
- print(f"✓ Successfully retrieved the created bookmark by ID.")
371
-
372
- # 4. Search for the created bookmark
373
- print(f"\nAttempting to search for bookmark with query: 'wikipedia'")
374
- search_query = "Example - Wikipedia"
375
- search_results = karakeep_client.search_bookmarks(q=search_query, limit=50)
376
- assert isinstance(
377
- search_results, datatypes.PaginatedBookmarks
378
- ), "Search response should be PaginatedBookmarks model"
379
- assert isinstance(
380
- search_results.bookmarks, list
381
- ), "Search results bookmarks attribute should be a list"
382
- assert any(
383
- b.id == created_bookmark_id for b in search_results.bookmarks
384
- ), f"Created bookmark {created_bookmark_id} not found in search results for '{search_query}'"
385
- print(f" Found created bookmark in search results for '{search_query}'.")
386
-
387
- # 4a. Test CLI search equivalent
388
- print(f"\n Running CLI equivalent: search-bookmarks --q '{search_query}' --limit 10")
363
+ assert retrieved_bookmark.title == original_title
364
+ print(f"✓ Successfully retrieved the managed bookmark by ID.")
365
+
366
+
367
+ # 3. Search for the created bookmark
368
+ # Use a search query that is likely to match the fixture's title
369
+ # The fixture title is "Managed Fixture Bookmark {timestamp}-{random_suffix}"
370
+ # A simple search for "Managed Fixture Bookmark" should work.
371
+ # If the title is very dynamic, searching by URL might be more robust if supported,
372
+ # or by a known part of the title.
373
+
374
+ # waiting a bit for the indexation just in case
375
+ time.sleep(30)
376
+
377
+ search_query_component = original_title.split(" ")[0] + " " + original_title.split(" ")[1]
378
+ print(f"\nAttempting to search for bookmark with query based on title: '{search_query_component}'. Retrying multiple times because search is nondeterministic.")
379
+
380
+ for trial in range(5):
381
+ search_results = karakeep_client.search_bookmarks(q=search_query_component, limit=100, include_content=False)
382
+ assert isinstance(
383
+ search_results, datatypes.PaginatedBookmarks
384
+ ), "Search response should be PaginatedBookmarks model"
385
+ assert isinstance(
386
+ search_results.bookmarks, list
387
+ ), "Search results bookmarks attribute should be a list"
388
+
389
+ titles_in_search = [b.title for b in search_results.bookmarks]
390
+ found_in_search = any(b.id == created_bookmark_id for b in search_results.bookmarks)
391
+ if found_in_search:
392
+ break
393
+ else:
394
+ time.sleep(3)
395
+ assert found_in_search, \
396
+ f"Managed bookmark {created_bookmark_id} (Title: '{original_title}') not found in {trial} different search results for '{search_query_component}'. Titles were: '{titles_in_search}'."
397
+ print(f"✓ Found managed bookmark in search results for '{search_query_component}'.")
398
+
399
+ # 4. Test CLI search equivalent
400
+ print(f"\n Running CLI equivalent: search-bookmarks --q '{search_query_component}' --limit 10 --include-content false")
389
401
  try:
390
- cli_search_command = f"python -m karakeep_python_api search-bookmarks --q '{search_query}' --limit 10"
402
+ cli_search_command = f"python -m karakeep_python_api search-bookmarks --q '{search_query_component}' --limit 10 --include-content false"
391
403
  search_cli_output = subprocess.run(
392
404
  cli_search_command,
393
405
  shell=True,
@@ -395,93 +407,188 @@ def test_create_and_delete_bookmark(karakeep_client: KarakeepAPI):
395
407
  capture_output=True,
396
408
  text=True,
397
409
  )
398
- # Basic check: Ensure the created ID is somewhere in the output JSON
399
- # A more robust check would parse the JSON and verify structure/content
400
- assert created_bookmark_id in search_cli_output.stdout, f"Created bookmark ID {created_bookmark_id} not found in CLI search output for '{search_query}'"
410
+ assert created_bookmark_id in search_cli_output.stdout, \
411
+ f"Managed bookmark ID {created_bookmark_id} not found in CLI search output for '{search_query_component}'"
401
412
  print("✓ CLI search command executed successfully and contained the bookmark ID.")
402
413
  except subprocess.CalledProcessError as e:
403
414
  print(f" CLI search command failed with exit code {e.returncode}")
404
415
  print(f" Stdout: {e.stdout}")
405
416
  print(f" Stderr: {e.stderr}")
406
- pytest.fail(f"CLI command 'search-bookmarks --q {search_query}' failed: {e}")
417
+ pytest.fail(f"CLI command 'search-bookmarks --q \"{search_query_component}\"' failed: {e}")
407
418
  except Exception as e:
408
419
  pytest.fail(f"An unexpected error occurred running the CLI search command: {e}")
409
420
 
410
-
411
421
  except (APIError, AuthenticationError) as e:
412
- pytest.fail(f"API error during bookmark creation/verification/search: {e}")
422
+ pytest.fail(f"API error during bookmark verification/search: {e}")
413
423
  except Exception as e:
414
424
  pytest.fail(
415
- f"An unexpected error occurred during bookmark creation/verification/search: {e}"
425
+ f"An unexpected error occurred during bookmark verification/search: {e}"
426
+ )
427
+ # No 'finally' block for deletion needed, as 'managed_bookmark' fixture handles it.
428
+ # The fixture also handles verification of deletion.
429
+
430
+
431
+ def test_update_bookmark_title(karakeep_client: KarakeepAPI, managed_bookmark: datatypes.Bookmark):
432
+ """Test updating a bookmark's title via API and CLI, using a managed bookmark."""
433
+ created_bookmark_id = managed_bookmark.id
434
+ original_title = managed_bookmark.title # Get the original title from the fixture
435
+
436
+ target_api_title = "this is a test title"
437
+ target_cli_title = "this is a test title (CLI)"
438
+
439
+ try:
440
+ # The bookmark is already created by the 'managed_bookmark' fixture.
441
+ # We have its ID in created_bookmark_id and its original title.
442
+ print(f"\nUsing managed bookmark ID: {created_bookmark_id}, Original Title: '{original_title}'")
443
+
444
+ # 1. Update the bookmark's title using the API client
445
+ print(f"\nAttempting to update bookmark ID {created_bookmark_id} title to: '{target_api_title}' via API")
446
+ update_payload_api = {"title": target_api_title}
447
+ updated_bookmark_partial = karakeep_client.update_a_bookmark(
448
+ bookmark_id=created_bookmark_id, update_data=update_payload_api
449
+ )
450
+ assert isinstance(updated_bookmark_partial, dict), "Update response should be a dict"
451
+ assert updated_bookmark_partial.get("title") == target_api_title, \
452
+ f"Partial response title '{updated_bookmark_partial.get('title')}' does not match target API title '{target_api_title}'"
453
+ print(f"✓ API call to update_a_bookmark successful. Partial response title: '{updated_bookmark_partial.get('title')}'")
454
+
455
+ # 2. Verify the API update by fetching the bookmark again
456
+ print(f"\nFetching bookmark ID {created_bookmark_id} to verify API title update.")
457
+ retrieved_bookmark_after_api_update = karakeep_client.get_a_single_bookmark(
458
+ bookmark_id=created_bookmark_id
459
+ )
460
+ assert isinstance(retrieved_bookmark_after_api_update, datatypes.Bookmark)
461
+ assert retrieved_bookmark_after_api_update.title == target_api_title, \
462
+ f"Retrieved bookmark title '{retrieved_bookmark_after_api_update.title}' does not match expected API-updated title '{target_api_title}'"
463
+ print(f"✓ Successfully verified bookmark title updated by API to: '{retrieved_bookmark_after_api_update.title}'")
464
+
465
+ # 3. Test CLI equivalent for updating the bookmark's title
466
+ print(f"\n Running CLI equivalent to update title to: '{target_cli_title}'")
467
+ cli_update_payload_json = json.dumps({"title": target_cli_title})
468
+ # Ensure the JSON string is properly quoted for the shell command
469
+ cli_update_command = f"python -m karakeep_python_api update-a-bookmark --bookmark-id {created_bookmark_id} --update-data '{cli_update_payload_json}'"
470
+
471
+ try:
472
+ subprocess.run(
473
+ cli_update_command,
474
+ shell=True,
475
+ check=True,
476
+ capture_output=True,
477
+ text=True,
478
+ )
479
+ print("✓ CLI update command executed successfully.")
480
+
481
+ # 4. Verify CLI update by fetching the bookmark again
482
+ print(f"\nFetching bookmark ID {created_bookmark_id} to verify CLI title update.")
483
+ retrieved_bookmark_after_cli_update = karakeep_client.get_a_single_bookmark(
484
+ bookmark_id=created_bookmark_id
485
+ )
486
+ assert isinstance(retrieved_bookmark_after_cli_update, datatypes.Bookmark)
487
+ assert retrieved_bookmark_after_cli_update.title == target_cli_title, \
488
+ f"Retrieved bookmark title '{retrieved_bookmark_after_cli_update.title}' after CLI update does not match expected '{target_cli_title}'"
489
+ print(f"✓ Successfully verified bookmark title updated by CLI to: '{retrieved_bookmark_after_cli_update.title}'")
490
+
491
+ except subprocess.CalledProcessError as e:
492
+ print(f" CLI update command failed with exit code {e.returncode}")
493
+ print(f" Command: {cli_update_command}")
494
+ print(f" Stdout: {e.stdout}")
495
+ print(f" Stderr: {e.stderr}")
496
+ pytest.fail(f"CLI command for update-a-bookmark failed: {e}")
497
+ except Exception as e:
498
+ pytest.fail(f"An unexpected error occurred running the CLI update command: {e}")
499
+
500
+ except (APIError, AuthenticationError) as e:
501
+ pytest.fail(f"API error during bookmark title update test: {e}")
502
+ except Exception as e:
503
+ pytest.fail(f"An unexpected error occurred during bookmark title update test: {e}")
504
+ # No finally block needed for deletion, as 'managed_bookmark' fixture handles it.
505
+
506
+
507
+ def test_tag_lifecycle_on_bookmark(karakeep_client: KarakeepAPI, managed_bookmark: datatypes.Bookmark):
508
+ """
509
+ Test attaching a tag to a bookmark, updating the tag, detaching it, and deleting it.
510
+ Uses the managed_bookmark fixture.
511
+ """
512
+ bookmark_id = managed_bookmark.id
513
+ timestamp = int(time.time())
514
+ random_chars = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
515
+ initial_tag_name = f"test-tag-{timestamp}-{random_chars}"
516
+ updated_tag_name = f"updated-tag-{timestamp}-{random_chars}"
517
+ tag_id_to_manage = None
518
+
519
+ try:
520
+ # 1. Attach a new tag by name to the bookmark
521
+ print(f"\nAttempting to attach tag '{initial_tag_name}' to bookmark {bookmark_id}")
522
+ attach_payload = {"tags": [{"tagName": initial_tag_name}]}
523
+ attach_response = karakeep_client.attach_tags_to_a_bookmark(
524
+ bookmark_id=bookmark_id, tags_data=attach_payload
525
+ )
526
+ assert "attached" in attach_response and len(attach_response["attached"]) == 1, \
527
+ "Failed to attach tag or response format incorrect"
528
+ tag_id_to_manage = attach_response["attached"][0]
529
+ assert isinstance(tag_id_to_manage, str), "Attached tag ID should be a string"
530
+ print(f"✓ Tag '{initial_tag_name}' attached with ID: {tag_id_to_manage}")
531
+
532
+ # 2. Update the tag's name
533
+ print(f"\nAttempting to update tag {tag_id_to_manage} to name '{updated_tag_name}'")
534
+ update_payload = {"name": updated_tag_name}
535
+ updated_tag = karakeep_client.update_a_tag(
536
+ tag_id=tag_id_to_manage, update_data=update_payload
537
+ )
538
+ # Do not check the type because karakeep 0.24.1 has a server side bug
539
+ # assert isinstance(updated_tag, datatypes.Tag1), "Update tag response should be Tag1 model"
540
+ # assert updated_tag.name == updated_tag_name, "Tag name was not updated as expected"
541
+ # print(f"✓ Tag {tag_id_to_manage} updated to name '{updated_tag.name}'")
542
+ assert updated_tag["name"] == updated_tag_name, "Tag name was not updated as expected"
543
+ print(f"✓ Tag {tag_id_to_manage} updated to name '{updated_tag['name']}'")
544
+
545
+ # 3. Verify tag update by getting it directly
546
+ print(f"\nFetching tag {tag_id_to_manage} to verify its name is '{updated_tag_name}'")
547
+ retrieved_tag = karakeep_client.get_a_single_tag(tag_id=tag_id_to_manage)
548
+ assert isinstance(retrieved_tag, datatypes.Tag1), "Get single tag response should be Tag1 model"
549
+ assert retrieved_tag.name == updated_tag_name, "Retrieved tag name does not match updated name"
550
+ assert retrieved_tag.id == tag_id_to_manage, "Retrieved tag ID does not match"
551
+ print(f"✓ Verified tag {tag_id_to_manage} has name '{retrieved_tag.name}'")
552
+
553
+ # 4. Detach the tag from the bookmark
554
+ print(f"\nAttempting to detach tag {tag_id_to_manage} from bookmark {bookmark_id}")
555
+ detach_payload = {"tags": [{"tagId": tag_id_to_manage}]}
556
+ detach_response = karakeep_client.detach_tags_from_a_bookmark(
557
+ bookmark_id=bookmark_id, tags_data=detach_payload
416
558
  )
559
+ assert "detached" in detach_response and tag_id_to_manage in detach_response["detached"], \
560
+ "Failed to detach tag or response format incorrect"
561
+ print(f"✓ Tag {tag_id_to_manage} detached from bookmark {bookmark_id}")
562
+
563
+ except (APIError, AuthenticationError) as e:
564
+ pytest.fail(f"API error during tag lifecycle test: {e}")
565
+ except Exception as e:
566
+ pytest.fail(f"An unexpected error occurred during tag lifecycle test: {e}")
417
567
  finally:
418
- # 5. Delete the bookmark (ensure cleanup)
419
- if created_bookmark_id:
420
- print(f"\nAttempting to delete bookmark with ID: {created_bookmark_id}")
568
+ # 5. Delete the tag (ensure cleanup even if assertions fail mid-test)
569
+ if tag_id_to_manage:
570
+ print(f"\nAttempting to delete tag {tag_id_to_manage} (cleanup)")
421
571
  try:
422
- karakeep_client.delete_a_bookmark(bookmark_id=created_bookmark_id)
423
- print(f"✓ Successfully deleted bookmark with ID: {created_bookmark_id}")
572
+ karakeep_client.delete_a_tag(tag_id=tag_id_to_manage)
573
+ print(f"✓ Successfully deleted tag {tag_id_to_manage}")
424
574
 
425
- # 6. Verify the bookmark is gone by trying to get it (should fail)
575
+ # 6. Verify the tag is gone by trying to get it (should fail with 404)
426
576
  try:
427
- karakeep_client.get_a_single_bookmark(
428
- bookmark_id=created_bookmark_id
429
- )
577
+ karakeep_client.get_a_single_tag(tag_id=tag_id_to_manage)
430
578
  pytest.fail(
431
- f"Bookmark with ID {created_bookmark_id} should not exist after deletion, but get_a_single_bookmark succeeded."
579
+ f"Tag {tag_id_to_manage} should not exist after deletion, but get_a_single_tag succeeded."
432
580
  )
433
581
  except APIError as e:
434
- assert (
435
- e.status_code == 404
436
- ), f"Expected 404 Not Found when getting deleted bookmark, but got status {e.status_code}"
437
- print(
438
- f"✓ Confirmed bookmark {created_bookmark_id} is deleted via API (received 404)."
439
- )
440
-
441
- # 7. Attempt to delete the same bookmark via CLI (should ideally fail or do nothing)
442
- print(f"\n Attempting CLI deletion for already deleted bookmark ID: {created_bookmark_id}")
443
- try:
444
- cli_delete_command = f"python -m karakeep_python_api delete-a-bookmark --bookmark-id {created_bookmark_id}"
445
- subprocess.run(
446
- cli_delete_command,
447
- shell=True,
448
- check=True, # Set to False if CLI errors on 404, True if it exits 0
449
- capture_output=True,
450
- text=True,
451
- )
452
- # If check=True and it succeeds, it means the CLI might not error on 404.
453
- print(f" ✓ CLI delete command executed (check=True implies it might not error on 404).")
454
- except subprocess.CalledProcessError as e:
455
- # If check=True, this block means the CLI command failed, which *might* be expected if it errors on 404.
456
- print(f" ✓ CLI delete command failed as expected (exit code {e.returncode}), likely because bookmark was already deleted.")
457
- # Optional: Assert specific exit code if known
458
- # assert e.returncode == EXPECTED_EXIT_CODE_FOR_NOT_FOUND
459
- except Exception as cli_e:
460
- pytest.fail(f"An unexpected error occurred running the CLI delete command: {cli_e}")
461
-
462
- # 8. Verify again via API that the bookmark is still gone after CLI attempt
463
- try:
464
- karakeep_client.get_a_single_bookmark(bookmark_id=created_bookmark_id)
465
- pytest.fail(
466
- f"Bookmark {created_bookmark_id} should *still* not exist after CLI deletion attempt, but get_a_single_bookmark succeeded."
467
- )
468
- except APIError as e:
469
- assert (
470
- e.status_code == 404
471
- ), f"Expected 404 Not Found after CLI deletion attempt, but got status {e.status_code}"
472
- print(
473
- f"✓ Confirmed bookmark {created_bookmark_id} is *still* deleted after CLI attempt (received 404)."
474
- )
475
-
582
+ assert e.status_code == 404, \
583
+ f"Expected 404 Not Found when getting deleted tag, but got status {e.status_code}"
584
+ print(f" Confirmed tag {tag_id_to_manage} is deleted (received 404).")
476
585
  except (APIError, AuthenticationError) as e:
477
- # Catch errors from the initial Python API deletion or subsequent checks
478
- pytest.fail(f"API error during bookmark deletion/verification: {e}")
586
+ # Log error during cleanup but don't let it mask original test failure
587
+ print(f" API error during tag deletion (cleanup) for ID {tag_id_to_manage}: {e}")
479
588
  except Exception as e:
480
- pytest.fail(f"An unexpected error occurred during bookmark deletion: {e}")
589
+ print(f" Unexpected error during tag deletion (cleanup) for ID {tag_id_to_manage}: {e}")
481
590
  else:
482
- print(
483
- "\nSkipping deletion because bookmark creation failed or ID was not obtained."
484
- )
591
+ print("\nSkipping tag deletion (cleanup) because tag_id was not obtained or test failed before creation.")
485
592
 
486
593
 
487
594
  # --- Test User Info/Stats Endpoints ---