karakeep-python-api 0.1.6__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {karakeep_python_api-0.1.6/karakeep_python_api.egg-info → karakeep_python_api-0.2.0}/PKG-INFO +11 -4
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/README.md +10 -3
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api/__main__.py +201 -32
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api/datatypes.py +46 -45
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api/karakeep_api.py +269 -87
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api/openapi_reference.json +158 -8
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0/karakeep_python_api.egg-info}/PKG-INFO +11 -4
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/setup.py +1 -1
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/tests/test_karakeep_api.py +68 -6
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/LICENSE +0 -0
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/MANIFEST.in +0 -0
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api/__init__.py +0 -0
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api.egg-info/SOURCES.txt +0 -0
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api.egg-info/dependency_links.txt +0 -0
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api.egg-info/entry_points.txt +0 -0
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api.egg-info/requires.txt +0 -0
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api.egg-info/top_level.txt +0 -0
- {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/setup.cfg +0 -0
{karakeep_python_api-0.1.6/karakeep_python_api.egg-info → karakeep_python_api-0.2.0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: karakeep_python_api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Community python client for the Karakeep API.
|
|
5
5
|
Home-page: https://github.com/thiswillbeyourgithub/karakeep_python_api/
|
|
6
6
|
License: GPLv3
|
|
@@ -57,7 +57,7 @@ A community-developed Python client for the [Karakeep](https://karakeep.app/) AP
|
|
|
57
57
|
|
|
58
58
|
## Overview
|
|
59
59
|
|
|
60
|
-
This library provides a Python interface (both a class and a command-line tool) to interact with a Karakeep instance's API.
|
|
60
|
+
This library provides a Python interface (both a class and a command-line tool) to interact with a Karakeep instance's API. The author also developed [freshrss_to_karakeep](https://github.com/thiswillbeyourgithub/freshrss_to_karakeep), a Python script that periodically sends FreshRSS "favourite" articles to Karakeep (a bookmarking and read-it-later app, see [Karakeep on GitHub](https://github.com/karakeep-app/karakeep)).
|
|
61
61
|
|
|
62
62
|
The development process involved:
|
|
63
63
|
|
|
@@ -97,14 +97,15 @@ Methods or CLI commands marked with ❌ should be used with caution as their beh
|
|
|
97
97
|
| `get_a_single_list` | ✅ | ❌ | |
|
|
98
98
|
| `delete_a_list` | ✅ | ❌ | |
|
|
99
99
|
| `update_a_list` | ❌ | ❌ | |
|
|
100
|
-
| `
|
|
100
|
+
| `get_bookmarks_in_the_list` | ❌ | ❌ | |
|
|
101
101
|
| `add_a_bookmark_to_a_list` | ❌ | ❌ | |
|
|
102
102
|
| `remove_a_bookmark_from_a_list` | ❌ | ❌ | |
|
|
103
103
|
| `get_all_tags` | ✅ | ✅ | |
|
|
104
|
+
| `create_a_new_tag` | ❌ | ❌ | |
|
|
104
105
|
| `get_a_single_tag` | ✅ | ❌ | |
|
|
105
106
|
| `delete_a_tag` | ✅ | ❌ | |
|
|
106
107
|
| `update_a_tag` | ✅ | ❌ | No output validation due to [server bug](https://github.com/karakeep-app/karakeep/issues/1365). |
|
|
107
|
-
| `
|
|
108
|
+
| `get_bookmarks_with_the_tag` | ❌ | ❌ | |
|
|
108
109
|
| `get_all_highlights` | ✅ | ✅ | Tested with pagination. |
|
|
109
110
|
| `create_a_new_highlight` | ❌ | ❌ | |
|
|
110
111
|
| `get_a_single_highlight` | ❌ | ❌ | |
|
|
@@ -238,6 +239,12 @@ except Exception as e:
|
|
|
238
239
|
|
|
239
240
|
```
|
|
240
241
|
|
|
242
|
+
### Usage Examples
|
|
243
|
+
|
|
244
|
+
Examples of the API being used can be found in the [`./examples`](./examples) folder. Don't hesitate to submit yours!
|
|
245
|
+
|
|
246
|
+
- **Omnivore Archiving Status Updater**: A script to fix the archive status of bookmarks imported from Omnivore into Karakeep. See [`examples/omnivore_archiving_status_updater/README.md`](./examples/omnivore_archiving_status_updater/README.md) for details.
|
|
247
|
+
|
|
241
248
|
## Development
|
|
242
249
|
|
|
243
250
|
1. Clone the repository.
|
|
@@ -22,7 +22,7 @@ A community-developed Python client for the [Karakeep](https://karakeep.app/) AP
|
|
|
22
22
|
|
|
23
23
|
## Overview
|
|
24
24
|
|
|
25
|
-
This library provides a Python interface (both a class and a command-line tool) to interact with a Karakeep instance's API.
|
|
25
|
+
This library provides a Python interface (both a class and a command-line tool) to interact with a Karakeep instance's API. The author also developed [freshrss_to_karakeep](https://github.com/thiswillbeyourgithub/freshrss_to_karakeep), a Python script that periodically sends FreshRSS "favourite" articles to Karakeep (a bookmarking and read-it-later app, see [Karakeep on GitHub](https://github.com/karakeep-app/karakeep)).
|
|
26
26
|
|
|
27
27
|
The development process involved:
|
|
28
28
|
|
|
@@ -62,14 +62,15 @@ Methods or CLI commands marked with ❌ should be used with caution as their beh
|
|
|
62
62
|
| `get_a_single_list` | ✅ | ❌ | |
|
|
63
63
|
| `delete_a_list` | ✅ | ❌ | |
|
|
64
64
|
| `update_a_list` | ❌ | ❌ | |
|
|
65
|
-
| `
|
|
65
|
+
| `get_bookmarks_in_the_list` | ❌ | ❌ | |
|
|
66
66
|
| `add_a_bookmark_to_a_list` | ❌ | ❌ | |
|
|
67
67
|
| `remove_a_bookmark_from_a_list` | ❌ | ❌ | |
|
|
68
68
|
| `get_all_tags` | ✅ | ✅ | |
|
|
69
|
+
| `create_a_new_tag` | ❌ | ❌ | |
|
|
69
70
|
| `get_a_single_tag` | ✅ | ❌ | |
|
|
70
71
|
| `delete_a_tag` | ✅ | ❌ | |
|
|
71
72
|
| `update_a_tag` | ✅ | ❌ | No output validation due to [server bug](https://github.com/karakeep-app/karakeep/issues/1365). |
|
|
72
|
-
| `
|
|
73
|
+
| `get_bookmarks_with_the_tag` | ❌ | ❌ | |
|
|
73
74
|
| `get_all_highlights` | ✅ | ✅ | Tested with pagination. |
|
|
74
75
|
| `create_a_new_highlight` | ❌ | ❌ | |
|
|
75
76
|
| `get_a_single_highlight` | ❌ | ❌ | |
|
|
@@ -203,6 +204,12 @@ except Exception as e:
|
|
|
203
204
|
|
|
204
205
|
```
|
|
205
206
|
|
|
207
|
+
### Usage Examples
|
|
208
|
+
|
|
209
|
+
Examples of the API being used can be found in the [`./examples`](./examples) folder. Don't hesitate to submit yours!
|
|
210
|
+
|
|
211
|
+
- **Omnivore Archiving Status Updater**: A script to fix the archive status of bookmarks imported from Omnivore into Karakeep. See [`examples/omnivore_archiving_status_updater/README.md`](./examples/omnivore_archiving_status_updater/README.md) for details.
|
|
212
|
+
|
|
206
213
|
## Development
|
|
207
214
|
|
|
208
215
|
1. Clone the repository.
|
|
@@ -7,7 +7,17 @@ import functools
|
|
|
7
7
|
import re # Import re module
|
|
8
8
|
import click
|
|
9
9
|
import traceback # Moved import to top
|
|
10
|
-
from typing import
|
|
10
|
+
from typing import (
|
|
11
|
+
Any,
|
|
12
|
+
List,
|
|
13
|
+
Dict,
|
|
14
|
+
Optional,
|
|
15
|
+
Callable,
|
|
16
|
+
Union,
|
|
17
|
+
get_origin,
|
|
18
|
+
get_args,
|
|
19
|
+
Literal,
|
|
20
|
+
)
|
|
11
21
|
from pydantic import BaseModel, ValidationError
|
|
12
22
|
from loguru import logger # Import logger
|
|
13
23
|
|
|
@@ -82,9 +92,9 @@ shared_options = [
|
|
|
82
92
|
),
|
|
83
93
|
click.option(
|
|
84
94
|
"--ascii",
|
|
85
|
-
"ensure_ascii",
|
|
95
|
+
"ensure_ascii", # Use 'ensure_ascii' as the destination variable name
|
|
86
96
|
is_flag=True,
|
|
87
|
-
default=False,
|
|
97
|
+
default=False, # Default is False, meaning ensure_ascii=False by default
|
|
88
98
|
envvar="KARAKEEP_PYTHON_API_ENSURE_ASCII",
|
|
89
99
|
help="Escape non-ASCII characters in the JSON output (default: keep Unicode).",
|
|
90
100
|
),
|
|
@@ -135,9 +145,19 @@ def print_openapi_spec(ctx, param, value):
|
|
|
135
145
|
is_eager=True, # Process this option before others
|
|
136
146
|
help="Dump the OpenAPI specification JSON to stdout and exit.",
|
|
137
147
|
)
|
|
138
|
-
@add_options(
|
|
148
|
+
@add_options(
|
|
149
|
+
shared_options
|
|
150
|
+
) # Apply shared options to the group (ensure_ascii is now included)
|
|
139
151
|
@click.pass_context
|
|
140
|
-
def cli(
|
|
152
|
+
def cli(
|
|
153
|
+
ctx,
|
|
154
|
+
base_url,
|
|
155
|
+
api_key,
|
|
156
|
+
verify_ssl,
|
|
157
|
+
verbose,
|
|
158
|
+
disable_response_validation,
|
|
159
|
+
ensure_ascii,
|
|
160
|
+
):
|
|
141
161
|
"""
|
|
142
162
|
Karakeep Python API Command Line Interface.
|
|
143
163
|
|
|
@@ -171,7 +191,7 @@ def cli(ctx, base_url, api_key, verify_ssl, verbose, disable_response_validation
|
|
|
171
191
|
ctx.obj["DISABLE_RESPONSE_VALIDATION"] = (
|
|
172
192
|
disable_response_validation # Store the flag
|
|
173
193
|
)
|
|
174
|
-
ctx.obj["ENSURE_ASCII"] = ensure_ascii
|
|
194
|
+
ctx.obj["ENSURE_ASCII"] = ensure_ascii # Store the ensure_ascii flag
|
|
175
195
|
|
|
176
196
|
# --- Configure Logger ---
|
|
177
197
|
log_level = "DEBUG" if verbose else "INFO"
|
|
@@ -184,7 +204,7 @@ def cli(ctx, base_url, api_key, verify_ssl, verbose, disable_response_validation
|
|
|
184
204
|
)
|
|
185
205
|
logger.debug("Verbose logging enabled with detailed format.")
|
|
186
206
|
else:
|
|
187
|
-
logger.add(sys.stderr, level=log_level)
|
|
207
|
+
logger.add(sys.stderr, level=log_level) # Default format for INFO
|
|
188
208
|
logger.debug("Logger configured for level: {}", log_level)
|
|
189
209
|
logger.debug("CLI context initialized.")
|
|
190
210
|
|
|
@@ -216,7 +236,7 @@ def create_click_command(
|
|
|
216
236
|
verify_ssl = ctx.obj["VERIFY_SSL"]
|
|
217
237
|
verbose = ctx.obj["VERBOSE"]
|
|
218
238
|
disable_validation = ctx.obj["DISABLE_RESPONSE_VALIDATION"] # Retrieve flag
|
|
219
|
-
ensure_ascii_output = ctx.obj["ENSURE_ASCII"]
|
|
239
|
+
ensure_ascii_output = ctx.obj["ENSURE_ASCII"] # Retrieve ensure_ascii flag
|
|
220
240
|
|
|
221
241
|
if not api_key:
|
|
222
242
|
click.echo(
|
|
@@ -253,9 +273,7 @@ def create_click_command(
|
|
|
253
273
|
# Remove arguments that are not part of the method signature
|
|
254
274
|
# (e.g., if extra options were somehow passed)
|
|
255
275
|
valid_arg_names = set(signature.parameters.keys())
|
|
256
|
-
call_args = {
|
|
257
|
-
k: v for k, v in call_args.items() if k in valid_arg_names
|
|
258
|
-
}
|
|
276
|
+
call_args = {k: v for k, v in call_args.items() if k in valid_arg_names}
|
|
259
277
|
|
|
260
278
|
# --- JSON Parsing for Dict/List Parameters ---
|
|
261
279
|
# Iterate through the expected parameters from the signature
|
|
@@ -268,13 +286,15 @@ def create_click_command(
|
|
|
268
286
|
# Check if the annotation is dict/list or typing.Dict/List
|
|
269
287
|
# and if the received value is a string (needs parsing)
|
|
270
288
|
if (
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
):
|
|
289
|
+
param_annotation in (dict, list)
|
|
290
|
+
or origin in (dict, list, Dict, List)
|
|
291
|
+
) and isinstance(param_value, str):
|
|
274
292
|
try:
|
|
275
293
|
# Attempt to parse the JSON string
|
|
276
294
|
call_args[param_name] = json.loads(param_value)
|
|
277
|
-
logger.debug(
|
|
295
|
+
logger.debug(
|
|
296
|
+
f"Parsed JSON string for parameter '{param_name}'."
|
|
297
|
+
)
|
|
278
298
|
except json.JSONDecodeError as json_err:
|
|
279
299
|
# Handle invalid JSON input from the user
|
|
280
300
|
click.echo(
|
|
@@ -286,10 +306,130 @@ def create_click_command(
|
|
|
286
306
|
|
|
287
307
|
# Call the API method
|
|
288
308
|
try:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
309
|
+
if method_name == "get_all_bookmarks":
|
|
310
|
+
logger.debug(
|
|
311
|
+
f"Special CLI pagination handling for '{method_name}'."
|
|
312
|
+
)
|
|
313
|
+
cli_total_limit = call_args.pop("limit", None)
|
|
314
|
+
# Other relevant params for get_all_bookmarks
|
|
315
|
+
archived_filter = call_args.get("archived")
|
|
316
|
+
favourited_filter = call_args.get("favourited")
|
|
317
|
+
include_content_cli = call_args.get("include_content", True)
|
|
318
|
+
|
|
319
|
+
call_args.pop("cursor", None) # Ignore CLI cursor
|
|
320
|
+
|
|
321
|
+
all_bookmarks_data = []
|
|
322
|
+
current_page_api_cursor = None
|
|
323
|
+
fetched_count = 0
|
|
324
|
+
API_INTERNAL_PAGE_SIZE = 50 # Define a page size for API calls
|
|
325
|
+
|
|
326
|
+
while True:
|
|
327
|
+
api_call_limit = API_INTERNAL_PAGE_SIZE
|
|
328
|
+
if cli_total_limit is not None:
|
|
329
|
+
remaining_needed = cli_total_limit - fetched_count
|
|
330
|
+
if remaining_needed <= 0:
|
|
331
|
+
break # Reached or exceeded CLI total limit
|
|
332
|
+
api_call_limit = min(
|
|
333
|
+
API_INTERNAL_PAGE_SIZE, remaining_needed
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
if (
|
|
337
|
+
api_call_limit <= 0 and cli_total_limit is not None
|
|
338
|
+
): # Avoid asking for 0 or negative items unless fetching all
|
|
339
|
+
break
|
|
340
|
+
|
|
341
|
+
logger.debug(
|
|
342
|
+
f"Fetching page for '{method_name}' with cursor: {current_page_api_cursor}, api_limit: {api_call_limit}"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
page_call_args = {
|
|
346
|
+
"archived": archived_filter,
|
|
347
|
+
"favourited": favourited_filter,
|
|
348
|
+
"limit": api_call_limit,
|
|
349
|
+
"cursor": current_page_api_cursor,
|
|
350
|
+
"include_content": include_content_cli,
|
|
351
|
+
}
|
|
352
|
+
page_call_args_filtered = {
|
|
353
|
+
k: v for k, v in page_call_args.items() if v is not None
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
page_result_obj = instance_method(
|
|
358
|
+
**page_call_args_filtered
|
|
359
|
+
)
|
|
360
|
+
except TypeError as call_error_page:
|
|
361
|
+
logger.error(
|
|
362
|
+
f"Error calling API method '{method_name}' (paginated): {call_error_page}"
|
|
363
|
+
)
|
|
364
|
+
logger.error(
|
|
365
|
+
f"Provided arguments for page: {page_call_args_filtered}"
|
|
366
|
+
)
|
|
367
|
+
if verbose:
|
|
368
|
+
logger.debug(traceback.format_exc())
|
|
369
|
+
ctx.exit(1)
|
|
370
|
+
|
|
371
|
+
bookmarks_on_this_page = []
|
|
372
|
+
next_api_cursor = None
|
|
373
|
+
|
|
374
|
+
# Convert Pydantic model to dict using model_dump if available
|
|
375
|
+
if hasattr(page_result_obj, "model_dump"):
|
|
376
|
+
result_dict = page_result_obj.model_dump()
|
|
377
|
+
elif isinstance(page_result_obj, dict):
|
|
378
|
+
result_dict = page_result_obj
|
|
379
|
+
else:
|
|
380
|
+
logger.warning(
|
|
381
|
+
f"Unexpected result type: {type(page_result_obj)}"
|
|
382
|
+
)
|
|
383
|
+
result_dict = {}
|
|
384
|
+
|
|
385
|
+
# Extract data and cursor from the dict
|
|
386
|
+
bookmarks_on_this_page = result_dict.get("bookmarks", [])
|
|
387
|
+
next_api_cursor = result_dict.get("nextCursor")
|
|
388
|
+
|
|
389
|
+
logger.debug(
|
|
390
|
+
f"Extracted {len(bookmarks_on_this_page)} bookmarks and cursor: {next_api_cursor}"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if not isinstance(bookmarks_on_this_page, list):
|
|
394
|
+
logger.warning(
|
|
395
|
+
f"Expected a list of bookmarks, got {type(bookmarks_on_this_page)}. Stopping pagination."
|
|
396
|
+
)
|
|
397
|
+
break
|
|
398
|
+
|
|
399
|
+
all_bookmarks_data.extend(bookmarks_on_this_page)
|
|
400
|
+
fetched_count += len(bookmarks_on_this_page)
|
|
401
|
+
logger.debug(
|
|
402
|
+
f"Fetched {len(bookmarks_on_this_page)} bookmarks this page. Total fetched: {fetched_count}."
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
current_page_api_cursor = next_api_cursor
|
|
406
|
+
if not current_page_api_cursor:
|
|
407
|
+
logger.debug(
|
|
408
|
+
"No nextCursor from API, pagination complete."
|
|
409
|
+
)
|
|
410
|
+
break
|
|
411
|
+
if (
|
|
412
|
+
cli_total_limit is not None
|
|
413
|
+
and fetched_count >= cli_total_limit
|
|
414
|
+
):
|
|
415
|
+
logger.debug(
|
|
416
|
+
f"CLI total limit of {cli_total_limit} reached or exceeded."
|
|
417
|
+
)
|
|
418
|
+
break
|
|
419
|
+
if not bookmarks_on_this_page and api_call_limit > 0:
|
|
420
|
+
logger.debug(
|
|
421
|
+
"API returned an empty list of bookmarks while a positive limit was set, assuming end of data."
|
|
422
|
+
)
|
|
423
|
+
break
|
|
424
|
+
|
|
425
|
+
result = all_bookmarks_data # This will be a list of Bookmark models or dicts
|
|
426
|
+
else:
|
|
427
|
+
# Original behavior for other commands
|
|
428
|
+
logger.debug(
|
|
429
|
+
f"Calling API method '{method_name}' with args: {call_args}"
|
|
430
|
+
)
|
|
431
|
+
result = instance_method(**call_args)
|
|
432
|
+
|
|
293
433
|
except TypeError as call_error:
|
|
294
434
|
logger.error(
|
|
295
435
|
f"Error calling API method '{method_name}': {call_error}"
|
|
@@ -306,7 +446,9 @@ def create_click_command(
|
|
|
306
446
|
output_data = serialize_output(result)
|
|
307
447
|
# Use ensure_ascii_output flag to control JSON encoding
|
|
308
448
|
click.echo(
|
|
309
|
-
json.dumps(
|
|
449
|
+
json.dumps(
|
|
450
|
+
output_data, indent=2, ensure_ascii=ensure_ascii_output
|
|
451
|
+
)
|
|
310
452
|
)
|
|
311
453
|
else:
|
|
312
454
|
# Handle None result (e.g., 204 No Content) gracefully
|
|
@@ -347,7 +489,9 @@ def create_click_command(
|
|
|
347
489
|
docstring = api_method.__doc__ or f"Execute the {api_method_name} API operation."
|
|
348
490
|
docstring = dedent(docstring)
|
|
349
491
|
docstring_lines = docstring.split("\n")
|
|
350
|
-
help_text = " ".join(
|
|
492
|
+
help_text = " ".join(
|
|
493
|
+
docstring.split("\n\n")[0].splitlines()
|
|
494
|
+
).strip() # First lines as short help
|
|
351
495
|
# Full docstring as help
|
|
352
496
|
full_help = docstring
|
|
353
497
|
|
|
@@ -365,9 +509,7 @@ def create_click_command(
|
|
|
365
509
|
stripped_line = line.strip()
|
|
366
510
|
if stripped_line == "Args:":
|
|
367
511
|
in_args_section = True
|
|
368
|
-
elif
|
|
369
|
-
stripped_line == "Returns:" or stripped_line == "Raises:"
|
|
370
|
-
):
|
|
512
|
+
elif stripped_line == "Returns:" or stripped_line == "Raises:":
|
|
371
513
|
in_args_section = False # Stop capturing when Returns/Raises section starts
|
|
372
514
|
elif in_args_section and stripped_line:
|
|
373
515
|
args_section_lines.append(stripped_line)
|
|
@@ -378,7 +520,9 @@ def create_click_command(
|
|
|
378
520
|
param_name = match.group(1)
|
|
379
521
|
description = match.group(2).strip()
|
|
380
522
|
param_descriptions[param_name] = description
|
|
381
|
-
logger.trace(
|
|
523
|
+
logger.trace(
|
|
524
|
+
f"Parsed docstring param: '{param_name}' -> '{description}'"
|
|
525
|
+
)
|
|
382
526
|
else:
|
|
383
527
|
param_descriptions[param_name] += " " + stripped_line
|
|
384
528
|
|
|
@@ -437,7 +581,7 @@ def create_click_command(
|
|
|
437
581
|
logger.warning(
|
|
438
582
|
f"Parameter '{param.name}' is Literal but contains non-string types. Treating as STRING."
|
|
439
583
|
)
|
|
440
|
-
click_type = click.STRING
|
|
584
|
+
click_type = click.STRING # Fallback
|
|
441
585
|
|
|
442
586
|
# Determine option name(s) and help text
|
|
443
587
|
option_names = [f"--{param_name_cli}"]
|
|
@@ -456,20 +600,45 @@ def create_click_command(
|
|
|
456
600
|
):
|
|
457
601
|
param_help += " (Provide as JSON string)"
|
|
458
602
|
elif isinstance(click_type, click.Choice):
|
|
459
|
-
|
|
603
|
+
param_help += f" (Choices: {', '.join(click_type.choices)})"
|
|
460
604
|
|
|
461
605
|
click_required = is_required_in_sig and default_value is None and not is_flag
|
|
462
606
|
|
|
607
|
+
# Make copies of properties that might be modified for specific commands/params
|
|
608
|
+
current_param_help = param_help
|
|
609
|
+
current_click_required = click_required
|
|
610
|
+
current_default_value = default_value
|
|
611
|
+
current_is_flag = (
|
|
612
|
+
is_flag # Though is_flag interpretation might change help/required
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# Special handling for 'get_all_bookmarks' command parameters
|
|
616
|
+
if api_method_name == "get_all_bookmarks":
|
|
617
|
+
if param.name == "cursor":
|
|
618
|
+
current_param_help = (
|
|
619
|
+
"[Ignored by CLI for get-all-bookmarks] " + param_help
|
|
620
|
+
)
|
|
621
|
+
current_click_required = (
|
|
622
|
+
False # Cursor is handled by CLI, not required from user
|
|
623
|
+
)
|
|
624
|
+
current_default_value = (
|
|
625
|
+
None # Explicitly set default to None for ignored param
|
|
626
|
+
)
|
|
627
|
+
elif param.name == "limit":
|
|
628
|
+
current_param_help = "Total maximum number of bookmarks to fetch across pages for get-all-bookmarks. If omitted, all are fetched."
|
|
629
|
+
# For 'limit', required status and default remain as derived from its Optional[int] type hint
|
|
630
|
+
# current_click_required and current_default_value will be correctly False and None respectively.
|
|
631
|
+
|
|
463
632
|
# Add the Click Option
|
|
464
633
|
click_params.append(
|
|
465
634
|
click.Option(
|
|
466
635
|
option_names,
|
|
467
636
|
type=click_type,
|
|
468
|
-
required=
|
|
469
|
-
default=
|
|
470
|
-
help=
|
|
471
|
-
is_flag=(
|
|
472
|
-
show_default=not
|
|
637
|
+
required=current_click_required,
|
|
638
|
+
default=current_default_value if not current_is_flag else None,
|
|
639
|
+
help=current_param_help,
|
|
640
|
+
is_flag=(current_is_flag if len(option_names) == 1 else False),
|
|
641
|
+
show_default=not current_is_flag and current_default_value is not None,
|
|
473
642
|
# Click derives the Python identifier (e.g., 'bookmark_id') from the first long option name
|
|
474
643
|
)
|
|
475
644
|
)
|
|
@@ -22,32 +22,39 @@ class ListId(RootModel[str]):
|
|
|
22
22
|
root: str = Field(..., example="ieidlxygmwj87oxz5hxttoc8")
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
class
|
|
25
|
+
class HighlightId(RootModel[str]):
|
|
26
26
|
root: str = Field(..., example="ieidlxygmwj87oxz5hxttoc8")
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
class
|
|
29
|
+
class TagId(RootModel[str]):
|
|
30
30
|
root: str = Field(..., example="ieidlxygmwj87oxz5hxttoc8")
|
|
31
31
|
|
|
32
32
|
|
|
33
|
-
class TaggingStatus(Enum):
|
|
33
|
+
class TaggingStatus(str, Enum):
|
|
34
34
|
success = "success"
|
|
35
35
|
failure = "failure"
|
|
36
36
|
pending = "pending"
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
class AttachedBy(Enum):
|
|
39
|
+
class AttachedBy(str, Enum):
|
|
40
40
|
ai = "ai"
|
|
41
41
|
human = "human"
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
class
|
|
44
|
+
class TagShort(BaseModel):
|
|
45
45
|
id: str
|
|
46
46
|
name: str
|
|
47
47
|
attachedBy: AttachedBy
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
class
|
|
50
|
+
class Tag(BaseModel):
|
|
51
|
+
id: str
|
|
52
|
+
name: str
|
|
53
|
+
numBookmarks: float
|
|
54
|
+
numBookmarksByAttachedType: NumBookmarksByAttachedType
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Type(str, Enum):
|
|
51
58
|
link = "link"
|
|
52
59
|
|
|
53
60
|
|
|
@@ -71,28 +78,36 @@ class Content(BaseModel):
|
|
|
71
78
|
dateModified: Optional[str] = None
|
|
72
79
|
|
|
73
80
|
|
|
74
|
-
class
|
|
81
|
+
class TypeText(str, Enum):
|
|
75
82
|
text = "text"
|
|
76
83
|
|
|
77
84
|
|
|
78
|
-
class
|
|
79
|
-
|
|
80
|
-
text: str
|
|
81
|
-
sourceUrl: Optional[str] = None
|
|
85
|
+
class TypeAsset(str, Enum):
|
|
86
|
+
asset = "asset"
|
|
82
87
|
|
|
83
88
|
|
|
84
|
-
class
|
|
85
|
-
|
|
89
|
+
class TypeUnknown(str, Enum):
|
|
90
|
+
unknown = "unknown"
|
|
91
|
+
|
|
86
92
|
|
|
93
|
+
class ContentUnknown(BaseModel):
|
|
94
|
+
type: TypeUnknown
|
|
87
95
|
|
|
88
|
-
|
|
96
|
+
|
|
97
|
+
class AssetTypeAssetType(str, Enum):
|
|
89
98
|
image = "image"
|
|
90
99
|
pdf = "pdf"
|
|
91
100
|
|
|
92
101
|
|
|
93
|
-
class
|
|
94
|
-
type:
|
|
95
|
-
|
|
102
|
+
class ContentTypeText(BaseModel):
|
|
103
|
+
type: TypeText
|
|
104
|
+
text: str
|
|
105
|
+
sourceUrl: Optional[str] = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ContentTypeAsset(BaseModel):
|
|
109
|
+
type: TypeAsset
|
|
110
|
+
assetType: AssetTypeAssetType
|
|
96
111
|
assetId: str
|
|
97
112
|
fileName: Optional[str] = None
|
|
98
113
|
sourceUrl: Optional[str] = None
|
|
@@ -100,15 +115,7 @@ class Content2(BaseModel):
|
|
|
100
115
|
content: Optional[str] = None
|
|
101
116
|
|
|
102
117
|
|
|
103
|
-
class
|
|
104
|
-
unknown = "unknown"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
class Content3(BaseModel):
|
|
108
|
-
type: Type3
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
class AssetType1(Enum):
|
|
118
|
+
class AssetType(str, Enum):
|
|
112
119
|
screenshot = "screenshot"
|
|
113
120
|
assetScreenshot = "assetScreenshot"
|
|
114
121
|
bannerImage = "bannerImage"
|
|
@@ -121,34 +128,35 @@ class AssetType1(Enum):
|
|
|
121
128
|
|
|
122
129
|
class Asset(BaseModel):
|
|
123
130
|
id: str
|
|
124
|
-
assetType:
|
|
131
|
+
assetType: AssetType
|
|
125
132
|
|
|
126
133
|
|
|
127
134
|
class Bookmark(BaseModel):
|
|
128
135
|
id: str
|
|
129
136
|
createdAt: str
|
|
130
|
-
modifiedAt: str
|
|
137
|
+
modifiedAt: Optional[str]
|
|
131
138
|
title: Optional[str] = None
|
|
132
139
|
archived: bool
|
|
133
140
|
favourited: bool
|
|
134
141
|
taggingStatus: TaggingStatus
|
|
142
|
+
summarizationStatus: Optional[TaggingStatus] = None
|
|
135
143
|
note: Optional[str] = None
|
|
136
144
|
summary: Optional[str] = None
|
|
137
|
-
tags: List[
|
|
138
|
-
content: Union[Content,
|
|
145
|
+
tags: List[TagShort]
|
|
146
|
+
content: Union[Content, ContentTypeText, ContentTypeAsset, ContentUnknown]
|
|
139
147
|
assets: List[Asset]
|
|
140
148
|
|
|
141
149
|
|
|
142
150
|
class PaginatedBookmarks(BaseModel):
|
|
143
151
|
bookmarks: List[Bookmark]
|
|
144
|
-
nextCursor: Optional[str]
|
|
152
|
+
nextCursor: Optional[str]
|
|
145
153
|
|
|
146
154
|
|
|
147
155
|
class Cursor(RootModel[str]):
|
|
148
156
|
root: str
|
|
149
157
|
|
|
150
158
|
|
|
151
|
-
class Color(Enum):
|
|
159
|
+
class Color(str, Enum):
|
|
152
160
|
yellow = "yellow"
|
|
153
161
|
red = "red"
|
|
154
162
|
green = "green"
|
|
@@ -160,14 +168,14 @@ class Highlight(BaseModel):
|
|
|
160
168
|
startOffset: float
|
|
161
169
|
endOffset: float
|
|
162
170
|
color: Optional[Color] = "yellow"
|
|
163
|
-
text: Optional[str]
|
|
164
|
-
note: Optional[str]
|
|
171
|
+
text: Optional[str]
|
|
172
|
+
note: Optional[str]
|
|
165
173
|
id: str
|
|
166
174
|
userId: str
|
|
167
175
|
createdAt: str
|
|
168
176
|
|
|
169
177
|
|
|
170
|
-
class
|
|
178
|
+
class ListType(str, Enum):
|
|
171
179
|
manual = "manual"
|
|
172
180
|
smart = "smart"
|
|
173
181
|
|
|
@@ -177,8 +185,8 @@ class ListModel(BaseModel):
|
|
|
177
185
|
name: str
|
|
178
186
|
description: Optional[str] = None
|
|
179
187
|
icon: str
|
|
180
|
-
parentId: Optional[str]
|
|
181
|
-
type: Optional[
|
|
188
|
+
parentId: Optional[str]
|
|
189
|
+
type: Optional[ListType] = "manual"
|
|
182
190
|
query: Optional[str] = None
|
|
183
191
|
|
|
184
192
|
|
|
@@ -187,13 +195,6 @@ class NumBookmarksByAttachedType(BaseModel):
|
|
|
187
195
|
human: Optional[float] = None
|
|
188
196
|
|
|
189
197
|
|
|
190
|
-
class Tag1(BaseModel):
|
|
191
|
-
id: str
|
|
192
|
-
name: str
|
|
193
|
-
numBookmarks: float
|
|
194
|
-
numBookmarksByAttachedType: NumBookmarksByAttachedType
|
|
195
|
-
|
|
196
|
-
|
|
197
198
|
class PaginatedHighlights(BaseModel):
|
|
198
199
|
highlights: List[Highlight]
|
|
199
|
-
nextCursor: Optional[str]
|
|
200
|
+
nextCursor: Optional[str]
|