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.
Files changed (18) hide show
  1. {karakeep_python_api-0.1.6/karakeep_python_api.egg-info → karakeep_python_api-0.2.0}/PKG-INFO +11 -4
  2. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/README.md +10 -3
  3. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api/__main__.py +201 -32
  4. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api/datatypes.py +46 -45
  5. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api/karakeep_api.py +269 -87
  6. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api/openapi_reference.json +158 -8
  7. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0/karakeep_python_api.egg-info}/PKG-INFO +11 -4
  8. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/setup.py +1 -1
  9. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/tests/test_karakeep_api.py +68 -6
  10. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/LICENSE +0 -0
  11. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/MANIFEST.in +0 -0
  12. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api/__init__.py +0 -0
  13. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api.egg-info/SOURCES.txt +0 -0
  14. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api.egg-info/dependency_links.txt +0 -0
  15. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api.egg-info/entry_points.txt +0 -0
  16. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api.egg-info/requires.txt +0 -0
  17. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/karakeep_python_api.egg-info/top_level.txt +0 -0
  18. {karakeep_python_api-0.1.6 → karakeep_python_api-0.2.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karakeep_python_api
3
- Version: 0.1.6
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
- | `get_a_bookmarks_in_a_list` | ❌ | ❌ | |
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
- | `get_a_bookmarks_with_the_tag` | ❌ | ❌ | |
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
- | `get_a_bookmarks_in_a_list` | ❌ | ❌ | |
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
- | `get_a_bookmarks_with_the_tag` | ❌ | ❌ | |
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 Any, List, Dict, Optional, Callable, Union, get_origin, get_args, Literal
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", # Use 'ensure_ascii' as the destination variable name
95
+ "ensure_ascii", # Use 'ensure_ascii' as the destination variable name
86
96
  is_flag=True,
87
- default=False, # Default is False, meaning ensure_ascii=False by default
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(shared_options) # Apply shared options to the group (ensure_ascii is now included)
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(ctx, base_url, api_key, verify_ssl, verbose, disable_response_validation, ensure_ascii):
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 # Store the ensure_ascii flag
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) # Default format for INFO
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"] # Retrieve ensure_ascii flag
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
- (param_annotation in (dict, list) or origin in (dict, list, Dict, List))
272
- and isinstance(param_value, str)
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(f"Parsed JSON string for parameter '{param_name}'.")
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
- logger.debug(
290
- f"Calling API method '{method_name}' with args: {call_args}"
291
- )
292
- result = instance_method(**call_args)
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(output_data, indent=2, ensure_ascii=ensure_ascii_output)
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(docstring.split("\n\n")[0].splitlines()).strip() # First lines as short help
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(f"Parsed docstring param: '{param_name}' -> '{description}'")
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 # Fallback
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
- param_help += f" (Choices: {', '.join(click_type.choices)})"
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=click_required,
469
- default=default_value if not is_flag else None,
470
- help=param_help,
471
- is_flag=(is_flag if len(option_names) == 1 else False),
472
- show_default=not is_flag and default_value is not None, # Show default unless it's a flag or None
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 TagId(RootModel[str]):
25
+ class HighlightId(RootModel[str]):
26
26
  root: str = Field(..., example="ieidlxygmwj87oxz5hxttoc8")
27
27
 
28
28
 
29
- class HighlightId(RootModel[str]):
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 Tag(BaseModel):
44
+ class TagShort(BaseModel):
45
45
  id: str
46
46
  name: str
47
47
  attachedBy: AttachedBy
48
48
 
49
49
 
50
- class Type(Enum):
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 Type1(Enum):
81
+ class TypeText(str, Enum):
75
82
  text = "text"
76
83
 
77
84
 
78
- class Content1(BaseModel):
79
- type: Type1
80
- text: str
81
- sourceUrl: Optional[str] = None
85
+ class TypeAsset(str, Enum):
86
+ asset = "asset"
82
87
 
83
88
 
84
- class Type2(Enum):
85
- asset = "asset"
89
+ class TypeUnknown(str, Enum):
90
+ unknown = "unknown"
91
+
86
92
 
93
+ class ContentUnknown(BaseModel):
94
+ type: TypeUnknown
87
95
 
88
- class AssetType(Enum):
96
+
97
+ class AssetTypeAssetType(str, Enum):
89
98
  image = "image"
90
99
  pdf = "pdf"
91
100
 
92
101
 
93
- class Content2(BaseModel):
94
- type: Type2
95
- assetType: AssetType
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 Type3(Enum):
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: AssetType1
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[Tag]
138
- content: Union[Content, Content1, Content2, Content3]
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] = None # Allow nextCursor to be None
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] = None # Allow None for text
164
- note: Optional[str] = None # Allow None for note
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 Type4(Enum):
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] = None # Allow parentId to be None (nullable)
181
- type: Optional[Type4] = "manual"
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] = None # Allow nextCursor to be None
200
+ nextCursor: Optional[str]