karakeep-python-api 0.1.0__py3-none-any.whl
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/__init__.py +19 -0
- karakeep_python_api/__main__.py +509 -0
- karakeep_python_api/datatypes.py +199 -0
- karakeep_python_api/karakeep_api.py +1630 -0
- karakeep_python_api/openapi_reference.json +2901 -0
- karakeep_python_api-0.1.0.dist-info/METADATA +256 -0
- karakeep_python_api-0.1.0.dist-info/RECORD +11 -0
- karakeep_python_api-0.1.0.dist-info/WHEEL +5 -0
- karakeep_python_api-0.1.0.dist-info/entry_points.txt +2 -0
- karakeep_python_api-0.1.0.dist-info/licenses/LICENSE +674 -0
- karakeep_python_api-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Import API class and errors directly from the module
|
|
2
|
+
from .karakeep_api import KarakeepAPI, APIError, AuthenticationError
|
|
3
|
+
|
|
4
|
+
# Import the datatypes module so users can do `from karakeep_python_api.datatypes import ...`
|
|
5
|
+
from . import datatypes
|
|
6
|
+
|
|
7
|
+
# Define the package version
|
|
8
|
+
# This is the single source of truth, read by setup.py and updated by bumpver.
|
|
9
|
+
__version__ = KarakeepAPI.VERSION
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"KarakeepAPI",
|
|
13
|
+
"APIError",
|
|
14
|
+
"AuthenticationError",
|
|
15
|
+
"datatypes", # Expose the datatypes module
|
|
16
|
+
"__version__",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# Models are available via `from karakeep_python_api.datatypes import ...`
|
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import functools
|
|
6
|
+
import click
|
|
7
|
+
import traceback # Moved import to top
|
|
8
|
+
from typing import Any, List, Dict, Optional, Callable, Union, get_origin, get_args, Literal
|
|
9
|
+
from pydantic import BaseModel, ValidationError
|
|
10
|
+
from loguru import logger # Import logger
|
|
11
|
+
|
|
12
|
+
# Attempt relative imports for package execution
|
|
13
|
+
try:
|
|
14
|
+
# Import API class and errors directly from the module
|
|
15
|
+
from .karakeep_api import KarakeepAPI, APIError, AuthenticationError
|
|
16
|
+
|
|
17
|
+
# Models are not directly used here, API methods handle data types
|
|
18
|
+
except ImportError:
|
|
19
|
+
# Fallback for direct script execution (e.g., python -m karakeep_python_api ...)
|
|
20
|
+
# Import API class and errors directly from the module
|
|
21
|
+
from karakeep_api import KarakeepAPI, APIError, AuthenticationError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# --- Serialization Helper ---
|
|
25
|
+
def serialize_output(data: Any) -> Any:
|
|
26
|
+
"""
|
|
27
|
+
Recursively serialize data for JSON output, handling Pydantic models,
|
|
28
|
+
dataclasses, lists, and dicts.
|
|
29
|
+
"""
|
|
30
|
+
if isinstance(data, BaseModel):
|
|
31
|
+
return data.model_dump(
|
|
32
|
+
mode="json"
|
|
33
|
+
) # Use Pydantic's built-in JSON serialization
|
|
34
|
+
elif isinstance(data, list):
|
|
35
|
+
return [serialize_output(item) for item in data]
|
|
36
|
+
# Removed dataclass handling as API uses Pydantic models primarily
|
|
37
|
+
elif isinstance(data, dict):
|
|
38
|
+
# Serialize dictionary values
|
|
39
|
+
return {k: serialize_output(v) for k, v in data.items()}
|
|
40
|
+
# Add handling for other types like datetime if needed, though Pydantic's
|
|
41
|
+
# model_dump(mode='json') often handles them.
|
|
42
|
+
# Basic types (str, int, float, bool, None) are returned as is.
|
|
43
|
+
return data
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# --- Click CLI Setup ---
|
|
47
|
+
|
|
48
|
+
# Shared options for the API client
|
|
49
|
+
shared_options = [
|
|
50
|
+
click.option(
|
|
51
|
+
"--base-url",
|
|
52
|
+
envvar="KARAKEEP_PYTHON_API_BASE_URL",
|
|
53
|
+
help="Full Karakeep API base URL, including /api/v1/ (e.g., https://instance.com/api/v1/).",
|
|
54
|
+
),
|
|
55
|
+
click.option(
|
|
56
|
+
"--api-key",
|
|
57
|
+
envvar="KARAKEEP_PYTHON_API_KEY",
|
|
58
|
+
help="Karakeep API Key (required, uses env var if not provided).",
|
|
59
|
+
required=False,
|
|
60
|
+
), # Made not required here, checked in context
|
|
61
|
+
click.option(
|
|
62
|
+
"--verify-ssl/--no-verify-ssl",
|
|
63
|
+
default=True,
|
|
64
|
+
envvar="KARAKEEP_PYTHON_API_VERIFY_SSL",
|
|
65
|
+
help="Verify SSL certificates.",
|
|
66
|
+
),
|
|
67
|
+
click.option(
|
|
68
|
+
"--verbose",
|
|
69
|
+
"-v",
|
|
70
|
+
is_flag=True,
|
|
71
|
+
default=False,
|
|
72
|
+
envvar="KARAKEEP_PYTHON_API_VERBOSE",
|
|
73
|
+
help="Enable verbose logging.",
|
|
74
|
+
),
|
|
75
|
+
click.option(
|
|
76
|
+
"--disable-response-validation",
|
|
77
|
+
is_flag=True,
|
|
78
|
+
default=False,
|
|
79
|
+
envvar="KARAKEEP_PYTHON_API_DISABLE_RESPONSE_VALIDATION",
|
|
80
|
+
help="Disable Pydantic validation of API responses (returns raw data).",
|
|
81
|
+
),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def add_options(options):
|
|
86
|
+
"""Decorator to add a list of click options to a command."""
|
|
87
|
+
|
|
88
|
+
def _add_options(func):
|
|
89
|
+
for option in reversed(options):
|
|
90
|
+
func = option(func)
|
|
91
|
+
return func
|
|
92
|
+
|
|
93
|
+
return _add_options
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# --- Callback for --dump-openapi-specification ---
|
|
97
|
+
def print_openapi_spec(ctx, param, value):
|
|
98
|
+
"""Callback function for the --dump-openapi-specification option."""
|
|
99
|
+
if not value or ctx.resilient_parsing:
|
|
100
|
+
# Exit if the flag is not set, or if Click is doing resilient parsing (e.g., for completion)
|
|
101
|
+
return
|
|
102
|
+
try:
|
|
103
|
+
package_dir = os.path.dirname(__file__)
|
|
104
|
+
spec_path = os.path.join(package_dir, "openapi_reference.json")
|
|
105
|
+
if not os.path.exists(spec_path):
|
|
106
|
+
click.echo(
|
|
107
|
+
f"Error: Specification file not found at expected location: {spec_path}",
|
|
108
|
+
err=True,
|
|
109
|
+
)
|
|
110
|
+
ctx.exit(1) # Use ctx.exit
|
|
111
|
+
with open(spec_path, "r") as f:
|
|
112
|
+
click.echo(f.read()) # Use click.echo
|
|
113
|
+
except Exception as e:
|
|
114
|
+
click.echo(f"Error reading or printing specification file: {e}", err=True)
|
|
115
|
+
ctx.exit(1) # Exit with error code if reading failed
|
|
116
|
+
# Exit successfully *after* the try/except block if no error occurred
|
|
117
|
+
ctx.exit(0)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@click.group(context_settings=dict(help_option_names=["-h", "--help"]))
|
|
121
|
+
@click.option(
|
|
122
|
+
"--dump-openapi-specification",
|
|
123
|
+
is_flag=True,
|
|
124
|
+
callback=print_openapi_spec,
|
|
125
|
+
expose_value=False, # Don't pass the value to the main cli function
|
|
126
|
+
is_eager=True, # Process this option before others
|
|
127
|
+
help="Dump the OpenAPI specification JSON to stdout and exit.",
|
|
128
|
+
)
|
|
129
|
+
@add_options(shared_options) # Apply shared options to the group
|
|
130
|
+
@click.pass_context
|
|
131
|
+
def cli(ctx, base_url, api_key, verify_ssl, verbose, disable_response_validation):
|
|
132
|
+
"""
|
|
133
|
+
Karakeep Python API Command Line Interface.
|
|
134
|
+
|
|
135
|
+
Dynamically generates commands based on the OpenAPI specification.
|
|
136
|
+
Requires KARAKEEP_PYTHON_API_KEY environment variable or --api-key option.
|
|
137
|
+
"""
|
|
138
|
+
# Ensure the context object exists
|
|
139
|
+
ctx.ensure_object(dict)
|
|
140
|
+
|
|
141
|
+
# --- Strict Check for API Key and Base URL ---
|
|
142
|
+
# Check for API key (must be provided via arg or env)
|
|
143
|
+
resolved_api_key = api_key or os.environ.get("KARAKEEP_PYTHON_API_KEY")
|
|
144
|
+
if not resolved_api_key:
|
|
145
|
+
raise click.UsageError(
|
|
146
|
+
"API Key is required. Provide --api-key option or set KARAKEEP_PYTHON_API_KEY environment variable."
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Check for Base URL (must be provided via arg or env)
|
|
150
|
+
resolved_base_url = base_url or os.environ.get("KARAKEEP_PYTHON_API_BASE_URL")
|
|
151
|
+
if not resolved_base_url:
|
|
152
|
+
raise click.UsageError(
|
|
153
|
+
"API Base URL is required. Provide --base-url option or set KARAKEEP_PYTHON_API_BASE_URL environment variable. "
|
|
154
|
+
"The URL must include the API path, e.g., 'https://your-instance.com/api/v1/'."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Store common API parameters in the context for commands to use
|
|
158
|
+
ctx.obj["BASE_URL"] = resolved_base_url # Store the resolved URL
|
|
159
|
+
ctx.obj["API_KEY"] = resolved_api_key # Store the resolved key
|
|
160
|
+
ctx.obj["VERIFY_SSL"] = verify_ssl
|
|
161
|
+
ctx.obj["VERBOSE"] = verbose
|
|
162
|
+
ctx.obj["DISABLE_RESPONSE_VALIDATION"] = (
|
|
163
|
+
disable_response_validation # Store the flag
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# --- Configure Logger ---
|
|
167
|
+
log_level = "DEBUG" if verbose else "INFO"
|
|
168
|
+
logger.remove() # Remove default handler
|
|
169
|
+
logger.add(sys.stderr, level=log_level)
|
|
170
|
+
logger.debug("Logger configured for level: {}", log_level)
|
|
171
|
+
logger.debug("CLI context initialized.")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def create_click_command(
|
|
175
|
+
api_method_name: str, api_method: Callable
|
|
176
|
+
) -> Optional[click.Command]:
|
|
177
|
+
"""
|
|
178
|
+
Dynamically creates a Click command for a given API method instance,
|
|
179
|
+
inspecting its signature for arguments. Returns None if creation fails.
|
|
180
|
+
"""
|
|
181
|
+
# Get the signature from the bound method (which copied it from the original function)
|
|
182
|
+
try:
|
|
183
|
+
sig = inspect.signature(api_method)
|
|
184
|
+
# Exclude 'self' parameter from the signature when creating CLI options
|
|
185
|
+
params = [p for p in sig.parameters.values() if p.name != "self"]
|
|
186
|
+
except (ValueError, TypeError) as e:
|
|
187
|
+
logger.warning(f"Could not get signature for method '{api_method_name}': {e}")
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
# Define the command function template using a closure
|
|
191
|
+
def command_func_factory(method_name, signature):
|
|
192
|
+
@click.pass_context
|
|
193
|
+
def command_func(ctx, **kwargs):
|
|
194
|
+
"""Dynamically generated command function wrapper."""
|
|
195
|
+
# Retrieve API parameters from context, ensuring API key is present now
|
|
196
|
+
base_url = ctx.obj["BASE_URL"]
|
|
197
|
+
api_key = ctx.obj["API_KEY"]
|
|
198
|
+
verify_ssl = ctx.obj["VERIFY_SSL"]
|
|
199
|
+
verbose = ctx.obj["VERBOSE"]
|
|
200
|
+
disable_validation = ctx.obj["DISABLE_RESPONSE_VALIDATION"] # Retrieve flag
|
|
201
|
+
|
|
202
|
+
if not api_key:
|
|
203
|
+
click.echo(
|
|
204
|
+
"Error: API Key is required via --api-key or KARAKEEP_PYTHON_API_KEY environment variable.",
|
|
205
|
+
err=True,
|
|
206
|
+
)
|
|
207
|
+
ctx.exit(1)
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
# Initialize API client within the command context
|
|
211
|
+
# Method generation already happened during inspection phase or initial load
|
|
212
|
+
api = KarakeepAPI(
|
|
213
|
+
api_key=api_key,
|
|
214
|
+
base_url=base_url,
|
|
215
|
+
verify_ssl=verify_ssl,
|
|
216
|
+
verbose=verbose,
|
|
217
|
+
disable_response_validation=disable_validation, # Pass flag to constructor
|
|
218
|
+
)
|
|
219
|
+
# Get the actual bound method from the initialized API instance
|
|
220
|
+
instance_method = getattr(
|
|
221
|
+
api, method_name
|
|
222
|
+
) # Use the captured method_name
|
|
223
|
+
|
|
224
|
+
# Prepare arguments for the API call from Click's kwargs
|
|
225
|
+
call_args = {}
|
|
226
|
+
sig_params = signature.parameters # Use captured signature
|
|
227
|
+
|
|
228
|
+
# Process Click kwargs into API call arguments
|
|
229
|
+
# Convert kebab-case keys from Click back to snake_case for Python call
|
|
230
|
+
call_args = {
|
|
231
|
+
k.replace("-", "_"): v for k, v in kwargs.items() if v is not None
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# Remove arguments that are not part of the method signature
|
|
235
|
+
# (e.g., if extra options were somehow passed)
|
|
236
|
+
valid_arg_names = set(signature.parameters.keys())
|
|
237
|
+
call_args = {
|
|
238
|
+
k: v for k, v in call_args.items() if k in valid_arg_names
|
|
239
|
+
}
|
|
240
|
+
|
|
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.
|
|
244
|
+
|
|
245
|
+
# Call the API method
|
|
246
|
+
try:
|
|
247
|
+
logger.debug(
|
|
248
|
+
f"Calling API method '{method_name}' with args: {call_args}"
|
|
249
|
+
)
|
|
250
|
+
result = instance_method(**call_args)
|
|
251
|
+
except TypeError as call_error:
|
|
252
|
+
logger.error(
|
|
253
|
+
f"Error calling API method '{method_name}': {call_error}"
|
|
254
|
+
)
|
|
255
|
+
logger.error(f"Provided arguments: {call_args}")
|
|
256
|
+
logger.error(f"Expected signature: {signature}")
|
|
257
|
+
# Add traceback in verbose mode
|
|
258
|
+
if verbose:
|
|
259
|
+
logger.debug(traceback.format_exc())
|
|
260
|
+
ctx.exit(1)
|
|
261
|
+
|
|
262
|
+
# Serialize and print the result
|
|
263
|
+
if result is not None:
|
|
264
|
+
output_data = serialize_output(result)
|
|
265
|
+
click.echo(
|
|
266
|
+
json.dumps(output_data, indent=2)
|
|
267
|
+
) # Keep this for stdout result
|
|
268
|
+
else:
|
|
269
|
+
# Handle None result (e.g., 204 No Content) gracefully
|
|
270
|
+
# Verbose check is implicitly handled by logger level
|
|
271
|
+
logger.debug("Operation successful (No content returned).")
|
|
272
|
+
|
|
273
|
+
except (
|
|
274
|
+
APIError,
|
|
275
|
+
AuthenticationError,
|
|
276
|
+
ValueError,
|
|
277
|
+
ValidationError,
|
|
278
|
+
TypeError,
|
|
279
|
+
) as e:
|
|
280
|
+
logger.error(f"Error: {e}")
|
|
281
|
+
# Provide more detail for TypeErrors during binding/call
|
|
282
|
+
if isinstance(e, TypeError):
|
|
283
|
+
logger.error(f"Error: {e}")
|
|
284
|
+
# Provide more detail for TypeErrors during binding/call
|
|
285
|
+
if isinstance(e, TypeError) and verbose:
|
|
286
|
+
logger.debug(traceback.format_exc()) # Use top-level import
|
|
287
|
+
sys.exit(1)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.error(f"An unexpected error occurred: {e}")
|
|
290
|
+
if verbose:
|
|
291
|
+
logger.debug(traceback.format_exc()) # Use top-level import
|
|
292
|
+
sys.exit(1)
|
|
293
|
+
|
|
294
|
+
# Set the name of the inner function for help display purposes
|
|
295
|
+
command_func.__name__ = method_name
|
|
296
|
+
return command_func
|
|
297
|
+
|
|
298
|
+
# Create the actual command function instance using the factory
|
|
299
|
+
command_func = command_func_factory(api_method_name, sig)
|
|
300
|
+
|
|
301
|
+
# --- Add Click options/arguments based on the captured method signature ---
|
|
302
|
+
click_params = []
|
|
303
|
+
# Use the docstring from the original method (captured by functools.update_wrapper)
|
|
304
|
+
docstring = api_method.__doc__ or f"Execute the {api_method_name} API operation."
|
|
305
|
+
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
|
|
308
|
+
|
|
309
|
+
# Extract parameter descriptions from the Args section of the docstring
|
|
310
|
+
param_descriptions = {}
|
|
311
|
+
in_args_section = False
|
|
312
|
+
args_section_lines = []
|
|
313
|
+
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
|
|
317
|
+
elif (
|
|
318
|
+
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
|
|
333
|
+
|
|
334
|
+
# Add parameters from signature to Click command
|
|
335
|
+
for param in params: # Use the filtered list from signature inspection
|
|
336
|
+
param_name_cli = param.name.replace("_", "-") # Use kebab-case for CLI options
|
|
337
|
+
is_required_in_sig = param.default is inspect.Parameter.empty
|
|
338
|
+
default_value = param.default if not is_required_in_sig else None
|
|
339
|
+
param_type = click.STRING # Default to string for CLI
|
|
340
|
+
|
|
341
|
+
# Basic type mapping for Click
|
|
342
|
+
annotation = param.annotation
|
|
343
|
+
origin = getattr(annotation, "__origin__", None)
|
|
344
|
+
args = getattr(annotation, "__args__", [])
|
|
345
|
+
|
|
346
|
+
# Determine Click type and if it's a flag
|
|
347
|
+
is_flag = False
|
|
348
|
+
click_type = click.STRING
|
|
349
|
+
if annotation is int:
|
|
350
|
+
click_type = click.INT
|
|
351
|
+
elif annotation is float:
|
|
352
|
+
click_type = click.FLOAT
|
|
353
|
+
elif annotation is bool:
|
|
354
|
+
click_type = click.BOOL
|
|
355
|
+
# Boolean options are flags if they don't have a default or default is False
|
|
356
|
+
is_flag = is_required_in_sig or default_value is False
|
|
357
|
+
# Handle Optional[T] - makes the option not required unless T is bool
|
|
358
|
+
elif origin is Union and type(None) in args and len(args) == 2:
|
|
359
|
+
non_none_type = args[0] if args[1] is type(None) else args[1]
|
|
360
|
+
if non_none_type is int:
|
|
361
|
+
click_type = click.INT
|
|
362
|
+
elif non_none_type is float:
|
|
363
|
+
click_type = click.FLOAT
|
|
364
|
+
elif non_none_type is bool:
|
|
365
|
+
click_type = click.BOOL
|
|
366
|
+
# Optional bools are typically flags like --enable-feature/--disable-feature
|
|
367
|
+
# For simplicity, treat as a standard option unless explicitly designed as toggle
|
|
368
|
+
is_flag = (
|
|
369
|
+
False # Treat Optional[bool] as --option/--no-option by default
|
|
370
|
+
)
|
|
371
|
+
# Keep click_type as STRING for Optional[List/Dict/str/Any]
|
|
372
|
+
is_required_in_sig = False # Optional means not required
|
|
373
|
+
default_value = None # Default for Optional is None
|
|
374
|
+
|
|
375
|
+
# Handle List[T] or Dict[K, V] - expect JSON string
|
|
376
|
+
elif origin in (list, dict, List, Dict) or annotation in (list, dict):
|
|
377
|
+
click_type = click.STRING # Expect JSON string
|
|
378
|
+
# Handle Literal[...] for choices
|
|
379
|
+
elif origin is Literal:
|
|
380
|
+
choices = get_args(annotation)
|
|
381
|
+
# Ensure all choices are strings for click.Choice
|
|
382
|
+
if all(isinstance(c, str) for c in choices):
|
|
383
|
+
click_type = click.Choice(choices, case_sensitive=False)
|
|
384
|
+
else:
|
|
385
|
+
logger.warning(
|
|
386
|
+
f"Parameter '{param.name}' is Literal but contains non-string types. Treating as STRING."
|
|
387
|
+
)
|
|
388
|
+
click_type = click.STRING # Fallback
|
|
389
|
+
|
|
390
|
+
# Determine option name(s) and help text
|
|
391
|
+
option_names = [f"--{param_name_cli}"]
|
|
392
|
+
# Add /--no- option for boolean flags that are not required and default to True
|
|
393
|
+
if (
|
|
394
|
+
is_flag
|
|
395
|
+
and annotation is bool
|
|
396
|
+
and not is_required_in_sig
|
|
397
|
+
and default_value is True
|
|
398
|
+
):
|
|
399
|
+
option_names.append(f"--no-{param_name_cli}")
|
|
400
|
+
|
|
401
|
+
param_help = param_descriptions.get(param.name, f"Parameter '{param.name}'.")
|
|
402
|
+
if click_type is click.STRING and (
|
|
403
|
+
origin in (list, dict) or annotation in (list, dict)
|
|
404
|
+
):
|
|
405
|
+
param_help += " (Provide as JSON string)"
|
|
406
|
+
elif isinstance(click_type, click.Choice):
|
|
407
|
+
param_help += f" (Choices: {', '.join(click_type.choices)})"
|
|
408
|
+
|
|
409
|
+
# Standard parameter handling (no special '--data' mapping anymore)
|
|
410
|
+
click_required = is_required_in_sig and default_value is None and not is_flag
|
|
411
|
+
|
|
412
|
+
# Add the Click Option
|
|
413
|
+
click_params.append(
|
|
414
|
+
click.Option(
|
|
415
|
+
option_names,
|
|
416
|
+
type=click_type,
|
|
417
|
+
required=click_required,
|
|
418
|
+
default=default_value if not is_flag else None,
|
|
419
|
+
help=param_help,
|
|
420
|
+
is_flag=(is_flag if len(option_names) == 1 else False),
|
|
421
|
+
show_default=not is_flag and default_value is not None, # Show default unless it's a flag or None
|
|
422
|
+
# Click derives the Python identifier (e.g., 'bookmark_id') from the first long option name
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Create the Click command
|
|
427
|
+
try:
|
|
428
|
+
dynamic_command = click.Command(
|
|
429
|
+
name=api_method_name.replace("_", "-"), # Use kebab-case for command names
|
|
430
|
+
callback=command_func,
|
|
431
|
+
params=click_params,
|
|
432
|
+
help=full_help,
|
|
433
|
+
short_help=help_text,
|
|
434
|
+
)
|
|
435
|
+
return dynamic_command
|
|
436
|
+
except Exception as e:
|
|
437
|
+
logger.warning(f"Failed to create click command for '{api_method_name}': {e}")
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# --- Dynamically Add Commands to CLI Group ---
|
|
442
|
+
def add_commands_to_cli(cli_group):
|
|
443
|
+
"""
|
|
444
|
+
Inspects the KarakeepAPI class *statically* to find public methods
|
|
445
|
+
and adds them as Click commands. Does NOT require API keys or URL for inspection.
|
|
446
|
+
"""
|
|
447
|
+
logger.info("Statically inspecting KarakeepAPI class and generating commands...")
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
added_count = 0
|
|
451
|
+
skipped_count = 0
|
|
452
|
+
# Inspect the KarakeepAPI class directly, not an instance
|
|
453
|
+
for name, member in inspect.getmembers(KarakeepAPI):
|
|
454
|
+
# Check if it's a public function/method defined in the class
|
|
455
|
+
if (
|
|
456
|
+
not name.startswith("_")
|
|
457
|
+
and inspect.isfunction(
|
|
458
|
+
member
|
|
459
|
+
) # Check if it's a function (method in class def)
|
|
460
|
+
# Add further checks if needed, e.g., based on naming convention or decorators
|
|
461
|
+
):
|
|
462
|
+
try:
|
|
463
|
+
# Attempt to create a command for the method
|
|
464
|
+
# We pass the function object directly. The command_func will later
|
|
465
|
+
# get the bound method from the API instance created at runtime.
|
|
466
|
+
command = create_click_command(name, member)
|
|
467
|
+
if command:
|
|
468
|
+
cli_group.add_command(command)
|
|
469
|
+
added_count += 1
|
|
470
|
+
else:
|
|
471
|
+
logger.warning(f"Skipped command generation for method: {name}")
|
|
472
|
+
skipped_count += 1
|
|
473
|
+
except Exception as cmd_gen_e:
|
|
474
|
+
logger.warning(
|
|
475
|
+
f"Failed to create command for method '{name}': {cmd_gen_e}"
|
|
476
|
+
)
|
|
477
|
+
skipped_count += 1
|
|
478
|
+
|
|
479
|
+
if added_count == 0:
|
|
480
|
+
logger.warning(
|
|
481
|
+
"No API commands were dynamically added. Check KarakeepAPI class definition and logs."
|
|
482
|
+
)
|
|
483
|
+
else:
|
|
484
|
+
logger.info(f"Added {added_count} API commands. Skipped {skipped_count}.")
|
|
485
|
+
|
|
486
|
+
except Exception as e:
|
|
487
|
+
# Handle errors during static inspection or command creation
|
|
488
|
+
logger.error(f"Unexpected error during dynamic command setup: {e}")
|
|
489
|
+
# Determine verbosity from environment for traceback logging during setup
|
|
490
|
+
verbose_setup = os.environ.get("KARAKEEP_PYTHON_API_VERBOSE", "").lower() in (
|
|
491
|
+
"true",
|
|
492
|
+
"1",
|
|
493
|
+
"yes",
|
|
494
|
+
)
|
|
495
|
+
if verbose_setup:
|
|
496
|
+
logger.debug(traceback.format_exc()) # Use top-level import
|
|
497
|
+
# Raise an exception to halt execution if setup fails
|
|
498
|
+
error_message = f"Error: Unexpected error during dynamic command setup: {e}"
|
|
499
|
+
raise click.ClickException(error_message)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# Add commands when the script is loaded by calling the function
|
|
503
|
+
add_commands_to_cli(cli)
|
|
504
|
+
|
|
505
|
+
# Main entry point for the script
|
|
506
|
+
if __name__ == "__main__":
|
|
507
|
+
# Normal Click execution starts here. The --dump-openapi-specification
|
|
508
|
+
# is now handled by its callback function defined above.
|
|
509
|
+
cli(obj={}) # Pass initial empty object for context
|