karakeep-python-api 1.0.0__py3-none-any.whl → 1.2.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/__main__.py +14 -12
- karakeep_python_api/datatypes.py +10 -2
- karakeep_python_api/karakeep_api.py +221 -64
- karakeep_python_api/openapi_reference.json +113 -14
- {karakeep_python_api-1.0.0.dist-info → karakeep_python_api-1.2.0.dist-info}/METADATA +24 -18
- karakeep_python_api-1.2.0.dist-info/RECORD +11 -0
- karakeep_python_api-1.0.0.dist-info/RECORD +0 -11
- {karakeep_python_api-1.0.0.dist-info → karakeep_python_api-1.2.0.dist-info}/WHEEL +0 -0
- {karakeep_python_api-1.0.0.dist-info → karakeep_python_api-1.2.0.dist-info}/entry_points.txt +0 -0
- {karakeep_python_api-1.0.0.dist-info → karakeep_python_api-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {karakeep_python_api-1.0.0.dist-info → karakeep_python_api-1.2.0.dist-info}/top_level.txt +0 -0
karakeep_python_api/__main__.py
CHANGED
|
@@ -59,9 +59,9 @@ def serialize_output(data: Any) -> Any:
|
|
|
59
59
|
# Shared options for the API client
|
|
60
60
|
shared_options = [
|
|
61
61
|
click.option(
|
|
62
|
-
"--
|
|
63
|
-
envvar="
|
|
64
|
-
help="Full Karakeep API
|
|
62
|
+
"--api-endpoint",
|
|
63
|
+
envvar="KARAKEEP_PYTHON_API_ENDPOINT",
|
|
64
|
+
help="Full Karakeep API endpoint URL, including /api/v1/ (e.g., https://instance.com/api/v1/).",
|
|
65
65
|
),
|
|
66
66
|
click.option(
|
|
67
67
|
"--api-key",
|
|
@@ -151,7 +151,7 @@ def print_openapi_spec(ctx, param, value):
|
|
|
151
151
|
@click.pass_context
|
|
152
152
|
def cli(
|
|
153
153
|
ctx,
|
|
154
|
-
|
|
154
|
+
api_endpoint,
|
|
155
155
|
api_key,
|
|
156
156
|
verify_ssl,
|
|
157
157
|
verbose,
|
|
@@ -167,7 +167,7 @@ def cli(
|
|
|
167
167
|
# Ensure the context object exists
|
|
168
168
|
ctx.ensure_object(dict)
|
|
169
169
|
|
|
170
|
-
# --- Strict Check for API Key and
|
|
170
|
+
# --- Strict Check for API Key and Endpoint ---
|
|
171
171
|
# Check for API key (must be provided via arg or env)
|
|
172
172
|
resolved_api_key = api_key or os.environ.get("KARAKEEP_PYTHON_API_KEY")
|
|
173
173
|
if not resolved_api_key:
|
|
@@ -175,16 +175,18 @@ def cli(
|
|
|
175
175
|
"API Key is required. Provide --api-key option or set KARAKEEP_PYTHON_API_KEY environment variable."
|
|
176
176
|
)
|
|
177
177
|
|
|
178
|
-
# Check for
|
|
179
|
-
|
|
180
|
-
|
|
178
|
+
# Check for API endpoint (must be provided via arg or env)
|
|
179
|
+
resolved_api_endpoint = api_endpoint or os.environ.get(
|
|
180
|
+
"KARAKEEP_PYTHON_API_ENDPOINT"
|
|
181
|
+
)
|
|
182
|
+
if not resolved_api_endpoint:
|
|
181
183
|
raise click.UsageError(
|
|
182
|
-
"API
|
|
184
|
+
"API endpoint is required. Provide --api-endpoint option or set KARAKEEP_PYTHON_API_ENDPOINT environment variable. "
|
|
183
185
|
"The URL must include the API path, e.g., 'https://your-instance.com/api/v1/'."
|
|
184
186
|
)
|
|
185
187
|
|
|
186
188
|
# Store common API parameters in the context for commands to use
|
|
187
|
-
ctx.obj["
|
|
189
|
+
ctx.obj["API_ENDPOINT"] = resolved_api_endpoint # Store the resolved endpoint
|
|
188
190
|
ctx.obj["API_KEY"] = resolved_api_key # Store the resolved key
|
|
189
191
|
ctx.obj["VERIFY_SSL"] = verify_ssl
|
|
190
192
|
ctx.obj["VERBOSE"] = verbose
|
|
@@ -231,7 +233,7 @@ def create_click_command(
|
|
|
231
233
|
def command_func(ctx, **kwargs):
|
|
232
234
|
"""Dynamically generated command function wrapper."""
|
|
233
235
|
# Retrieve API parameters from context, ensuring API key is present now
|
|
234
|
-
|
|
236
|
+
api_endpoint = ctx.obj["API_ENDPOINT"]
|
|
235
237
|
api_key = ctx.obj["API_KEY"]
|
|
236
238
|
verify_ssl = ctx.obj["VERIFY_SSL"]
|
|
237
239
|
verbose = ctx.obj["VERBOSE"]
|
|
@@ -250,7 +252,7 @@ def create_click_command(
|
|
|
250
252
|
# Method generation already happened during inspection phase or initial load
|
|
251
253
|
api = KarakeepAPI(
|
|
252
254
|
api_key=api_key,
|
|
253
|
-
|
|
255
|
+
api_endpoint=api_endpoint,
|
|
254
256
|
verify_ssl=verify_ssl,
|
|
255
257
|
verbose=verbose,
|
|
256
258
|
disable_response_validation=disable_validation, # Pass flag to constructor
|
karakeep_python_api/datatypes.py
CHANGED
|
@@ -78,7 +78,7 @@ class ContentTypeAsset(BaseModel):
|
|
|
78
78
|
content: Optional[str] = None
|
|
79
79
|
|
|
80
80
|
|
|
81
|
-
class
|
|
81
|
+
class BookmarkAsset(BaseModel):
|
|
82
82
|
id: str
|
|
83
83
|
assetType: Literal[
|
|
84
84
|
"screenshot",
|
|
@@ -92,6 +92,13 @@ class Asset(BaseModel):
|
|
|
92
92
|
]
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
class Asset(BaseModel):
|
|
96
|
+
assetId: str
|
|
97
|
+
contentType: str
|
|
98
|
+
size: float
|
|
99
|
+
fileName: str
|
|
100
|
+
|
|
101
|
+
|
|
95
102
|
class Bookmark(BaseModel):
|
|
96
103
|
id: str
|
|
97
104
|
createdAt: str
|
|
@@ -107,7 +114,7 @@ class Bookmark(BaseModel):
|
|
|
107
114
|
content: Union[
|
|
108
115
|
ContentTypeLink, ContentTypeText, ContentTypeAsset, ContentTypeUnknown
|
|
109
116
|
]
|
|
110
|
-
assets: List[
|
|
117
|
+
assets: List[BookmarkAsset]
|
|
111
118
|
|
|
112
119
|
|
|
113
120
|
class PaginatedBookmarks(BaseModel):
|
|
@@ -135,6 +142,7 @@ class ListModel(BaseModel):
|
|
|
135
142
|
parentId: Optional[str]
|
|
136
143
|
type: Optional[Literal["manual", "smart"]] = "manual"
|
|
137
144
|
query: Optional[str] = None
|
|
145
|
+
public: bool
|
|
138
146
|
|
|
139
147
|
|
|
140
148
|
class PaginatedHighlights(BaseModel):
|
|
@@ -77,7 +77,7 @@ class KarakeepAPI:
|
|
|
77
77
|
|
|
78
78
|
Attributes:
|
|
79
79
|
api_key (str): The API key used for authentication.
|
|
80
|
-
|
|
80
|
+
api_endpoint (str): The endpoint of the Karakeep API instance, including /api/v1 (e.g., https://instance.com/api/v1/).
|
|
81
81
|
openapi_spec (dict): The parsed content of the OpenAPI specification file.
|
|
82
82
|
verify_ssl (bool): Whether SSL verification is enabled.
|
|
83
83
|
verbose (bool): Whether verbose logging is enabled.
|
|
@@ -85,12 +85,12 @@ class KarakeepAPI:
|
|
|
85
85
|
"""
|
|
86
86
|
|
|
87
87
|
# Version reflects the client library version, updated by bumpver
|
|
88
|
-
VERSION: str = "1.
|
|
88
|
+
VERSION: str = "1.2.0"
|
|
89
89
|
|
|
90
90
|
def __init__(
|
|
91
91
|
self,
|
|
92
92
|
api_key: Optional[str] = None,
|
|
93
|
-
|
|
93
|
+
api_endpoint: Optional[str] = None,
|
|
94
94
|
openapi_spec_path: Optional[str] = None, # Allow None, default handled below
|
|
95
95
|
verify_ssl: bool = True,
|
|
96
96
|
verbose: bool = False,
|
|
@@ -105,8 +105,9 @@ class KarakeepAPI:
|
|
|
105
105
|
Args:
|
|
106
106
|
api_key: Karakeep API key (Bearer token).
|
|
107
107
|
Defaults to KARAKEEP_PYTHON_API_KEY environment variable if not provided.
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
api_endpoint: Override the endpoint for the API. Must be provided either as an argument
|
|
109
|
+
or via the KARAKEEP_PYTHON_API_ENDPOINT environment variable.
|
|
110
|
+
Example: 'https://karakeep.domain.com/api/v1/'
|
|
110
111
|
openapi_spec_path: Path to the OpenAPI JSON specification file.
|
|
111
112
|
Defaults to 'openapi_reference.json' alongside the package code if not provided.
|
|
112
113
|
The loaded spec is available via the `openapi_spec` attribute.
|
|
@@ -130,47 +131,47 @@ class KarakeepAPI:
|
|
|
130
131
|
self.api_key = resolved_api_key
|
|
131
132
|
logger.debug("API Key loaded successfully.")
|
|
132
133
|
|
|
133
|
-
# ---
|
|
134
|
-
|
|
134
|
+
# --- Endpoint Validation ---
|
|
135
|
+
env_endpoint = os.environ.get("KARAKEEP_PYTHON_API_ENDPOINT")
|
|
135
136
|
logger.debug(
|
|
136
|
-
f"Checked
|
|
137
|
+
f"Checked KARAKEEP_PYTHON_API_ENDPOINT environment variable, found: '{env_endpoint}'"
|
|
137
138
|
)
|
|
138
|
-
logger.debug(f"
|
|
139
|
+
logger.debug(f"Endpoint provided as argument: '{api_endpoint}'")
|
|
139
140
|
|
|
140
|
-
if
|
|
141
|
-
self.
|
|
142
|
-
logger.info(f"Using provided
|
|
143
|
-
elif
|
|
144
|
-
self.
|
|
141
|
+
if api_endpoint:
|
|
142
|
+
self.api_endpoint = api_endpoint
|
|
143
|
+
logger.info(f"Using provided endpoint: {self.api_endpoint}")
|
|
144
|
+
elif env_endpoint:
|
|
145
|
+
self.api_endpoint = env_endpoint
|
|
145
146
|
logger.info(
|
|
146
|
-
f"Using
|
|
147
|
+
f"Using endpoint from KARAKEEP_PYTHON_API_ENDPOINT: {self.api_endpoint}"
|
|
147
148
|
)
|
|
148
149
|
else:
|
|
149
|
-
# No
|
|
150
|
+
# No api_endpoint from arg or env var - raise error as per requirement
|
|
150
151
|
raise ValueError(
|
|
151
|
-
"API
|
|
152
|
+
"API endpoint is required. Provide 'api_endpoint' argument or set KARAKEEP_PYTHON_API_ENDPOINT environment variable."
|
|
152
153
|
)
|
|
153
154
|
|
|
154
|
-
# Ensure
|
|
155
|
-
resolved_url = self.
|
|
156
|
-
if resolved_url.endswith("/v1"):
|
|
157
|
-
# Ends with /v1, needs a slash
|
|
158
|
-
self.
|
|
155
|
+
# Ensure endpoint ends with /api/v1/
|
|
156
|
+
resolved_url = self.api_endpoint # Use a temporary variable for checks
|
|
157
|
+
if resolved_url.endswith("/api/v1"):
|
|
158
|
+
# Ends with /api/v1, needs a slash
|
|
159
|
+
self.api_endpoint = resolved_url + "/"
|
|
159
160
|
logger.info(
|
|
160
|
-
f"Appended trailing slash to
|
|
161
|
+
f"Appended trailing slash to endpoint ending in /api/v1: {self.api_endpoint}"
|
|
161
162
|
)
|
|
162
|
-
elif resolved_url.endswith("/v1/"):
|
|
163
|
+
elif resolved_url.endswith("/api/v1/"):
|
|
163
164
|
# Already ends correctly, do nothing
|
|
164
|
-
logger.debug(f"
|
|
165
|
+
logger.debug(f"Endpoint already ends with /api/v1/: {self.api_endpoint}")
|
|
165
166
|
else:
|
|
166
|
-
# Doesn't end with /v1 or /v1/, append /v1/
|
|
167
|
-
# First, remove any existing trailing slash to avoid //v1/
|
|
167
|
+
# Doesn't end with /api/v1 or /api/v1/, append /api/v1/
|
|
168
|
+
# First, remove any existing trailing slash to avoid //api/v1/
|
|
168
169
|
if resolved_url.endswith("/"):
|
|
169
170
|
resolved_url = resolved_url[:-1]
|
|
170
|
-
self.
|
|
171
|
-
logger.info(f"Appended /v1/ to
|
|
171
|
+
self.api_endpoint = resolved_url + "/api/v1/"
|
|
172
|
+
logger.info(f"Appended /api/v1/ to endpoint: {self.api_endpoint}")
|
|
172
173
|
|
|
173
|
-
logger.debug(f"Final API
|
|
174
|
+
logger.debug(f"Final API Endpoint after /api/v1/ check: {self.api_endpoint}")
|
|
174
175
|
|
|
175
176
|
# --- Load and Parse OpenAPI Spec ---
|
|
176
177
|
if openapi_spec_path is None:
|
|
@@ -257,7 +258,7 @@ class KarakeepAPI:
|
|
|
257
258
|
# self.verbose is still used for conditional logging within the class methods.
|
|
258
259
|
|
|
259
260
|
logger.debug("KarakeepAPI client initialized.")
|
|
260
|
-
logger.debug(f"
|
|
261
|
+
logger.debug(f" Endpoint: {self.api_endpoint}")
|
|
261
262
|
logger.debug(f" Verify SSL: {self.verify_ssl}")
|
|
262
263
|
logger.debug(f" Verbose: {self.verbose}")
|
|
263
264
|
logger.debug(
|
|
@@ -294,34 +295,37 @@ class KarakeepAPI:
|
|
|
294
295
|
data: Optional[
|
|
295
296
|
Union[BaseModel, dict, list, str, bytes]
|
|
296
297
|
] = None, # More specific type hint
|
|
298
|
+
files: Optional[Dict[str, Any]] = None,
|
|
297
299
|
extra_headers: Optional[Dict[str, str]] = None,
|
|
298
|
-
) -> Union[Dict[str, Any], List[Any], None]:
|
|
300
|
+
) -> Union[Dict[str, Any], List[Any], None, bytes]:
|
|
299
301
|
"""
|
|
300
302
|
Internal method to make an HTTP call to the Karakeep API. Handles authentication,
|
|
301
303
|
request formatting, response parsing, and error handling.
|
|
302
304
|
|
|
303
305
|
Args:
|
|
304
306
|
method: HTTP method ('GET', 'POST', 'PUT', 'PATCH', 'DELETE').
|
|
305
|
-
endpoint: API endpoint path relative to the
|
|
307
|
+
endpoint: API endpoint path relative to the endpoint (e.g., 'bookmarks' or 'bookmarks/some_id').
|
|
306
308
|
Path parameters (like {bookmarkId}) MUST be substituted *before* calling _call.
|
|
307
309
|
params: Dictionary of URL query parameters. Values should be primitive types suitable for URLs.
|
|
308
310
|
data: Request body data. Can be a Pydantic model, dict, list, bytes, or str.
|
|
309
311
|
- Pydantic models, dicts, and lists will be automatically JSON-encoded
|
|
310
312
|
with 'Content-Type: application/json' unless overridden in extra_headers.
|
|
311
313
|
- For bytes or str, ensure 'Content-Type' is set correctly via extra_headers if needed.
|
|
314
|
+
files: Dictionary for file uploads (multipart/form-data). If provided, data parameter is ignored.
|
|
312
315
|
extra_headers: Additional headers to include or override default headers.
|
|
313
316
|
|
|
314
317
|
Returns:
|
|
315
|
-
The parsed JSON response from the API as a dict or list,
|
|
316
|
-
The calling wrapper method is responsible for further
|
|
318
|
+
The parsed JSON response from the API as a dict or list, None for 204 No Content responses,
|
|
319
|
+
or raw bytes for non-JSON responses. The calling wrapper method is responsible for further
|
|
320
|
+
parsing/validation into specific Pydantic models.
|
|
317
321
|
|
|
318
322
|
Raises:
|
|
319
323
|
AuthenticationError: If authentication fails (401).
|
|
320
324
|
APIError: For other HTTP errors or request issues.
|
|
321
325
|
"""
|
|
322
|
-
# Ensure endpoint doesn't start with / if
|
|
326
|
+
# Ensure endpoint doesn't start with / if endpoint ends with /
|
|
323
327
|
safe_endpoint = endpoint.lstrip("/")
|
|
324
|
-
url = urljoin(self.
|
|
328
|
+
url = urljoin(self.api_endpoint, safe_endpoint)
|
|
325
329
|
|
|
326
330
|
# Default headers
|
|
327
331
|
headers = {
|
|
@@ -345,7 +349,16 @@ class KarakeepAPI:
|
|
|
345
349
|
# Determine Content-Type, prioritizing extra_headers
|
|
346
350
|
content_type = headers.get("Content-Type")
|
|
347
351
|
|
|
348
|
-
|
|
352
|
+
# Handle file uploads (multipart/form-data)
|
|
353
|
+
if files is not None:
|
|
354
|
+
# When files are provided, let requests handle Content-Type automatically
|
|
355
|
+
# Don't set Content-Type header for multipart uploads
|
|
356
|
+
if "Content-Type" in headers:
|
|
357
|
+
# Remove Content-Type if it was set, let requests set it for multipart
|
|
358
|
+
headers.pop("Content-Type")
|
|
359
|
+
# Don't process data when files are provided
|
|
360
|
+
request_body_arg = None
|
|
361
|
+
elif data is not None:
|
|
349
362
|
if isinstance(data, BaseModel):
|
|
350
363
|
# Serialize Pydantic model to JSON bytes
|
|
351
364
|
request_body_arg = data.model_dump_json(
|
|
@@ -470,6 +483,7 @@ class KarakeepAPI:
|
|
|
470
483
|
url=url,
|
|
471
484
|
params=request_params, # Use params with stringified booleans
|
|
472
485
|
data=request_body_arg, # Serialized data (bytes or str)
|
|
486
|
+
files=files, # File uploads for multipart/form-data
|
|
473
487
|
headers=headers,
|
|
474
488
|
verify=self.verify_ssl,
|
|
475
489
|
timeout=60, # Increased default timeout
|
|
@@ -514,28 +528,42 @@ class KarakeepAPI:
|
|
|
514
528
|
logger.debug(" Body: None (204 No Content or empty response body)")
|
|
515
529
|
return None
|
|
516
530
|
|
|
517
|
-
#
|
|
518
|
-
|
|
519
|
-
|
|
531
|
+
# Check if the response is expected to be JSON based on Accept header
|
|
532
|
+
accept_header = headers.get("Accept", "application/json")
|
|
533
|
+
expects_json = "application/json" in accept_header
|
|
534
|
+
|
|
535
|
+
# Attempt to parse successful response as JSON if we expect JSON
|
|
536
|
+
if expects_json:
|
|
537
|
+
try:
|
|
538
|
+
result = response.json()
|
|
539
|
+
if self.verbose:
|
|
540
|
+
# Log parsed response body carefully
|
|
541
|
+
log_resp_str = repr(result)
|
|
542
|
+
if len(log_resp_str) > 1000:
|
|
543
|
+
log_resp_str = log_resp_str[:1000] + "...(truncated)"
|
|
544
|
+
logger.debug(f" Body (JSON Parsed): {log_resp_str}")
|
|
545
|
+
# Return the raw parsed JSON (dict/list). Deserialization into
|
|
546
|
+
# specific Pydantic models should happen in the calling wrapper method.
|
|
547
|
+
return result
|
|
548
|
+
except json.JSONDecodeError as e:
|
|
549
|
+
# Handle cases where the response is successful (2xx) but not valid JSON
|
|
550
|
+
logger.error(
|
|
551
|
+
f"API Error: Failed to decode JSON response from {method} {url}. Status: {response.status_code}. Content: {response.text[:500]}..."
|
|
552
|
+
)
|
|
553
|
+
# Raise APIError as the response format is unexpected
|
|
554
|
+
raise APIError(
|
|
555
|
+
message=f"Failed to parse successful API response JSON from {url}: {e}. Response text: {response.text[:200]}...",
|
|
556
|
+
status_code=response.status_code,
|
|
557
|
+
) from e
|
|
558
|
+
else:
|
|
559
|
+
# For non-JSON responses (like asset downloads), return raw bytes
|
|
520
560
|
if self.verbose:
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
# specific Pydantic models should happen in the calling wrapper method.
|
|
528
|
-
return result
|
|
529
|
-
except json.JSONDecodeError as e:
|
|
530
|
-
# Handle cases where the response is successful (2xx) but not valid JSON
|
|
531
|
-
logger.error(
|
|
532
|
-
f"API Error: Failed to decode JSON response from {method} {url}. Status: {response.status_code}. Content: {response.text[:500]}..."
|
|
533
|
-
)
|
|
534
|
-
# Raise APIError as the response format is unexpected
|
|
535
|
-
raise APIError(
|
|
536
|
-
message=f"Failed to parse successful API response JSON from {url}: {e}. Response text: {response.text[:200]}...",
|
|
537
|
-
status_code=response.status_code,
|
|
538
|
-
) from e
|
|
561
|
+
content_type = response.headers.get("Content-Type", "unknown")
|
|
562
|
+
content_length = len(response.content)
|
|
563
|
+
logger.debug(
|
|
564
|
+
f" Body (Binary): {content_length} bytes, Content-Type: {content_type}"
|
|
565
|
+
)
|
|
566
|
+
return response.content
|
|
539
567
|
|
|
540
568
|
except requests.exceptions.HTTPError as e:
|
|
541
569
|
# Handle 4xx/5xx errors raised by response.raise_for_status()
|
|
@@ -1179,7 +1207,7 @@ class KarakeepAPI:
|
|
|
1179
1207
|
"precrawledArchive",
|
|
1180
1208
|
"unknown",
|
|
1181
1209
|
],
|
|
1182
|
-
) -> Union[datatypes.
|
|
1210
|
+
) -> Union[datatypes.BookmarkAsset, Dict[str, Any], List[Any]]:
|
|
1183
1211
|
"""
|
|
1184
1212
|
Attach a new asset to a bookmark. Corresponds to POST /bookmarks/{bookmarkId}/assets.
|
|
1185
1213
|
|
|
@@ -1190,7 +1218,7 @@ class KarakeepAPI:
|
|
|
1190
1218
|
"bannerImage", "fullPageArchive", "video", "bookmarkAsset", "precrawledArchive", "unknown".
|
|
1191
1219
|
|
|
1192
1220
|
Returns:
|
|
1193
|
-
datatypes.
|
|
1221
|
+
datatypes.BookmarkAsset: The attached asset object.
|
|
1194
1222
|
If response validation is disabled, returns the raw API response (dict/list).
|
|
1195
1223
|
|
|
1196
1224
|
Raises:
|
|
@@ -1207,8 +1235,8 @@ class KarakeepAPI:
|
|
|
1207
1235
|
logger.debug("Skipping response validation as requested.")
|
|
1208
1236
|
return response_data
|
|
1209
1237
|
else:
|
|
1210
|
-
# Response should match
|
|
1211
|
-
return datatypes.
|
|
1238
|
+
# Response should match BookmarkAsset schema
|
|
1239
|
+
return datatypes.BookmarkAsset.model_validate(response_data)
|
|
1212
1240
|
|
|
1213
1241
|
@optional_typecheck
|
|
1214
1242
|
def replace_asset(self, bookmark_id: str, asset_id: str, new_asset_id: str) -> None:
|
|
@@ -1306,6 +1334,7 @@ class KarakeepAPI:
|
|
|
1306
1334
|
parent_id: Optional[str] = None,
|
|
1307
1335
|
list_type: Optional[Literal["manual", "smart"]] = "manual",
|
|
1308
1336
|
query: Optional[str] = None,
|
|
1337
|
+
public: bool = False,
|
|
1309
1338
|
) -> Union[datatypes.ListModel, Dict[str, Any], List[Any]]:
|
|
1310
1339
|
"""
|
|
1311
1340
|
Create a new list (manual or smart). Corresponds to POST /lists.
|
|
@@ -1317,6 +1346,7 @@ class KarakeepAPI:
|
|
|
1317
1346
|
parent_id: Optional parent list ID for nested lists.
|
|
1318
1347
|
list_type: The type of list ('manual' or 'smart'). Default is 'manual'.
|
|
1319
1348
|
query: Optional query string for smart lists (required if list_type is 'smart').
|
|
1349
|
+
public: Whether the list is public (default: False).
|
|
1320
1350
|
|
|
1321
1351
|
Returns:
|
|
1322
1352
|
datatypes.ListModel: The created list object.
|
|
@@ -1336,6 +1366,7 @@ class KarakeepAPI:
|
|
|
1336
1366
|
"name": name,
|
|
1337
1367
|
"icon": icon,
|
|
1338
1368
|
"type": list_type,
|
|
1369
|
+
"public": public,
|
|
1339
1370
|
}
|
|
1340
1371
|
|
|
1341
1372
|
# Add optional fields if provided
|
|
@@ -1410,10 +1441,11 @@ class KarakeepAPI:
|
|
|
1410
1441
|
icon: Optional[str] = None,
|
|
1411
1442
|
parent_id: Optional[str] = None,
|
|
1412
1443
|
query: Optional[str] = None,
|
|
1444
|
+
public: Optional[bool] = None,
|
|
1413
1445
|
) -> Union[datatypes.ListModel, Dict[str, Any], List[Any]]:
|
|
1414
1446
|
"""
|
|
1415
1447
|
Update a list by its ID. Corresponds to PATCH /lists/{listId}.
|
|
1416
|
-
Allows updating various list fields including name, description, icon, parent relationship, and
|
|
1448
|
+
Allows updating various list fields including name, description, icon, parent relationship, query, and public status.
|
|
1417
1449
|
|
|
1418
1450
|
Args:
|
|
1419
1451
|
list_id: The ID (string) of the list to update.
|
|
@@ -1422,6 +1454,7 @@ class KarakeepAPI:
|
|
|
1422
1454
|
icon: Optional new icon for the list.
|
|
1423
1455
|
parent_id: Optional new parent list ID (can be None to remove parent relationship).
|
|
1424
1456
|
query: Optional new query string for smart lists (minimum 1 character).
|
|
1457
|
+
public: Optional new public status for the list.
|
|
1425
1458
|
|
|
1426
1459
|
Returns:
|
|
1427
1460
|
datatypes.ListModel: The updated list object.
|
|
@@ -1444,6 +1477,8 @@ class KarakeepAPI:
|
|
|
1444
1477
|
update_data["parentId"] = parent_id
|
|
1445
1478
|
if query is not None:
|
|
1446
1479
|
update_data["query"] = query
|
|
1480
|
+
if public is not None:
|
|
1481
|
+
update_data["public"] = public
|
|
1447
1482
|
|
|
1448
1483
|
# Ensure at least one field is being updated
|
|
1449
1484
|
if not update_data:
|
|
@@ -1935,3 +1970,125 @@ class KarakeepAPI:
|
|
|
1935
1970
|
response_data = self._call("GET", "users/me/stats")
|
|
1936
1971
|
# No Pydantic validation applied here as the spec defines a simple dict response
|
|
1937
1972
|
return response_data
|
|
1973
|
+
|
|
1974
|
+
@optional_typecheck
|
|
1975
|
+
def upload_a_new_asset(
|
|
1976
|
+
self, file: str
|
|
1977
|
+
) -> Union[datatypes.Asset, Dict[str, Any], List[Any]]:
|
|
1978
|
+
"""
|
|
1979
|
+
Upload a new asset file. Corresponds to POST /assets.
|
|
1980
|
+
|
|
1981
|
+
Args:
|
|
1982
|
+
file: Path to the file to upload.
|
|
1983
|
+
|
|
1984
|
+
Returns:
|
|
1985
|
+
datatypes.Asset: Details about the uploaded asset (assetId, contentType, size, fileName).
|
|
1986
|
+
If response validation is disabled, returns the raw API response (dict/list).
|
|
1987
|
+
|
|
1988
|
+
Raises:
|
|
1989
|
+
FileNotFoundError: If the specified file does not exist.
|
|
1990
|
+
APIError: If the API request fails (e.g., unsupported file type, file too large).
|
|
1991
|
+
pydantic.ValidationError: If response validation fails (and is not disabled).
|
|
1992
|
+
"""
|
|
1993
|
+
import os
|
|
1994
|
+
import mimetypes
|
|
1995
|
+
|
|
1996
|
+
# Validate file path exists
|
|
1997
|
+
if not os.path.isfile(file):
|
|
1998
|
+
raise FileNotFoundError(f"File not found: {file}")
|
|
1999
|
+
|
|
2000
|
+
# Get filename from path
|
|
2001
|
+
file_name = os.path.basename(file)
|
|
2002
|
+
|
|
2003
|
+
# Detect MIME type
|
|
2004
|
+
mime_type, _ = mimetypes.guess_type(file)
|
|
2005
|
+
if mime_type is None:
|
|
2006
|
+
mime_type = "application/octet-stream"
|
|
2007
|
+
|
|
2008
|
+
if self.verbose:
|
|
2009
|
+
logger.debug(
|
|
2010
|
+
f"Uploading asset: {file} (filename: {file_name}, type: {mime_type})"
|
|
2011
|
+
)
|
|
2012
|
+
|
|
2013
|
+
# Prepare file for upload
|
|
2014
|
+
try:
|
|
2015
|
+
with open(file, "rb") as f:
|
|
2016
|
+
file_content = f.read()
|
|
2017
|
+
# Note: The 'file' key must match the OpenAPI spec parameter name
|
|
2018
|
+
files = {"file": (file_name, file_content, mime_type)}
|
|
2019
|
+
response_data = self._call("POST", "assets", files=files)
|
|
2020
|
+
except IOError as e:
|
|
2021
|
+
raise APIError(f"Failed to read file {file}: {e}") from e
|
|
2022
|
+
|
|
2023
|
+
if self.disable_response_validation:
|
|
2024
|
+
logger.debug("Skipping response validation as requested.")
|
|
2025
|
+
return response_data
|
|
2026
|
+
else:
|
|
2027
|
+
# Response should match Asset schema
|
|
2028
|
+
return datatypes.Asset.model_validate(response_data)
|
|
2029
|
+
|
|
2030
|
+
@optional_typecheck
|
|
2031
|
+
def get_a_single_asset(self, asset_id: str) -> bytes:
|
|
2032
|
+
"""
|
|
2033
|
+
Get the raw content of an asset by its ID. Corresponds to GET /assets/{assetId}.
|
|
2034
|
+
|
|
2035
|
+
Args:
|
|
2036
|
+
asset_id: The ID (string) of the asset to retrieve.
|
|
2037
|
+
|
|
2038
|
+
Returns:
|
|
2039
|
+
bytes: The raw asset content. The Content-Type is determined by the asset type.
|
|
2040
|
+
Use response headers to determine the actual content type if needed.
|
|
2041
|
+
|
|
2042
|
+
Raises:
|
|
2043
|
+
APIError: If the API request fails (e.g., 404 asset not found).
|
|
2044
|
+
ValueError: If asset_id is empty or invalid.
|
|
2045
|
+
|
|
2046
|
+
Note:
|
|
2047
|
+
This method always returns raw bytes regardless of the disable_response_validation setting,
|
|
2048
|
+
as the response is binary content rather than JSON.
|
|
2049
|
+
"""
|
|
2050
|
+
# Validate asset_id
|
|
2051
|
+
if not asset_id or not asset_id.strip():
|
|
2052
|
+
raise ValueError("asset_id cannot be empty")
|
|
2053
|
+
|
|
2054
|
+
asset_id = asset_id.strip()
|
|
2055
|
+
|
|
2056
|
+
# Validate asset_id format (basic check for reasonable ID format)
|
|
2057
|
+
if len(asset_id) < 5: # Assuming asset IDs are at least 5 characters
|
|
2058
|
+
raise ValueError(f"asset_id appears to be invalid: {asset_id}")
|
|
2059
|
+
|
|
2060
|
+
endpoint = f"assets/{asset_id}"
|
|
2061
|
+
|
|
2062
|
+
# Override the Accept header to get raw content instead of JSON
|
|
2063
|
+
# This is crucial for the assets endpoint to return binary data
|
|
2064
|
+
extra_headers = {"Accept": "*/*"}
|
|
2065
|
+
|
|
2066
|
+
if self.verbose:
|
|
2067
|
+
logger.debug(f"Retrieving asset: {asset_id}")
|
|
2068
|
+
|
|
2069
|
+
response_data = self._call("GET", endpoint, extra_headers=extra_headers)
|
|
2070
|
+
|
|
2071
|
+
# The _call method should return bytes for non-JSON responses when Accept is not application/json
|
|
2072
|
+
if isinstance(response_data, bytes):
|
|
2073
|
+
if self.verbose:
|
|
2074
|
+
logger.debug(f"Retrieved asset {asset_id}: {len(response_data)} bytes")
|
|
2075
|
+
return response_data
|
|
2076
|
+
elif response_data is None:
|
|
2077
|
+
# Handle empty response (valid for some assets like empty files)
|
|
2078
|
+
if self.verbose:
|
|
2079
|
+
logger.debug(f"Retrieved empty asset {asset_id}")
|
|
2080
|
+
return b""
|
|
2081
|
+
else:
|
|
2082
|
+
# This shouldn't happen with the updated _call method, but handle gracefully
|
|
2083
|
+
error_msg = f"Expected bytes from asset endpoint for asset {asset_id}, got {type(response_data).__name__}"
|
|
2084
|
+
if isinstance(response_data, (dict, list)):
|
|
2085
|
+
# If we got JSON, it might be an error response that wasn't caught
|
|
2086
|
+
error_detail = (
|
|
2087
|
+
str(response_data)[:200] + "..."
|
|
2088
|
+
if len(str(response_data)) > 200
|
|
2089
|
+
else str(response_data)
|
|
2090
|
+
)
|
|
2091
|
+
error_msg += f". Response content: {error_detail}"
|
|
2092
|
+
|
|
2093
|
+
logger.error(error_msg)
|
|
2094
|
+
raise APIError(error_msg)
|
|
@@ -25,10 +25,6 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"schemas": {
|
|
28
|
-
"AssetId": {
|
|
29
|
-
"type": "string",
|
|
30
|
-
"example": "ieidlxygmwj87oxz5hxttoc8"
|
|
31
|
-
},
|
|
32
28
|
"BookmarkId": {
|
|
33
29
|
"type": "string",
|
|
34
30
|
"example": "ieidlxygmwj87oxz5hxttoc8"
|
|
@@ -45,6 +41,10 @@
|
|
|
45
41
|
"type": "string",
|
|
46
42
|
"example": "ieidlxygmwj87oxz5hxttoc8"
|
|
47
43
|
},
|
|
44
|
+
"AssetId": {
|
|
45
|
+
"type": "string",
|
|
46
|
+
"example": "ieidlxygmwj87oxz5hxttoc8"
|
|
47
|
+
},
|
|
48
48
|
"Bookmark": {
|
|
49
49
|
"type": "object",
|
|
50
50
|
"properties": {
|
|
@@ -426,13 +426,17 @@
|
|
|
426
426
|
"query": {
|
|
427
427
|
"type": "string",
|
|
428
428
|
"nullable": true
|
|
429
|
+
},
|
|
430
|
+
"public": {
|
|
431
|
+
"type": "boolean"
|
|
429
432
|
}
|
|
430
433
|
},
|
|
431
434
|
"required": [
|
|
432
435
|
"id",
|
|
433
436
|
"name",
|
|
434
437
|
"icon",
|
|
435
|
-
"parentId"
|
|
438
|
+
"parentId",
|
|
439
|
+
"public"
|
|
436
440
|
]
|
|
437
441
|
},
|
|
438
442
|
"Tag": {
|
|
@@ -484,17 +488,33 @@
|
|
|
484
488
|
"highlights",
|
|
485
489
|
"nextCursor"
|
|
486
490
|
]
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
491
|
+
},
|
|
492
|
+
"Asset": {
|
|
493
|
+
"type": "object",
|
|
494
|
+
"properties": {
|
|
495
|
+
"assetId": {
|
|
496
|
+
"type": "string"
|
|
497
|
+
},
|
|
498
|
+
"contentType": {
|
|
499
|
+
"type": "string"
|
|
500
|
+
},
|
|
501
|
+
"size": {
|
|
502
|
+
"type": "number"
|
|
503
|
+
},
|
|
504
|
+
"fileName": {
|
|
505
|
+
"type": "string"
|
|
506
|
+
}
|
|
493
507
|
},
|
|
494
|
-
"required":
|
|
495
|
-
|
|
496
|
-
|
|
508
|
+
"required": [
|
|
509
|
+
"assetId",
|
|
510
|
+
"contentType",
|
|
511
|
+
"size",
|
|
512
|
+
"fileName"
|
|
513
|
+
]
|
|
497
514
|
},
|
|
515
|
+
"File to be uploaded": {}
|
|
516
|
+
},
|
|
517
|
+
"parameters": {
|
|
498
518
|
"BookmarkId": {
|
|
499
519
|
"schema": {
|
|
500
520
|
"$ref": "#/components/schemas/BookmarkId"
|
|
@@ -526,6 +546,14 @@
|
|
|
526
546
|
"required": true,
|
|
527
547
|
"name": "highlightId",
|
|
528
548
|
"in": "path"
|
|
549
|
+
},
|
|
550
|
+
"AssetId": {
|
|
551
|
+
"schema": {
|
|
552
|
+
"$ref": "#/components/schemas/AssetId"
|
|
553
|
+
},
|
|
554
|
+
"required": true,
|
|
555
|
+
"name": "assetId",
|
|
556
|
+
"in": "path"
|
|
529
557
|
}
|
|
530
558
|
}
|
|
531
559
|
},
|
|
@@ -1982,6 +2010,9 @@
|
|
|
1982
2010
|
"query": {
|
|
1983
2011
|
"type": "string",
|
|
1984
2012
|
"minLength": 1
|
|
2013
|
+
},
|
|
2014
|
+
"public": {
|
|
2015
|
+
"type": "boolean"
|
|
1985
2016
|
}
|
|
1986
2017
|
}
|
|
1987
2018
|
}
|
|
@@ -3024,6 +3055,74 @@
|
|
|
3024
3055
|
}
|
|
3025
3056
|
}
|
|
3026
3057
|
}
|
|
3058
|
+
},
|
|
3059
|
+
"/assets": {
|
|
3060
|
+
"post": {
|
|
3061
|
+
"description": "Upload a new asset",
|
|
3062
|
+
"summary": "Upload a new asset",
|
|
3063
|
+
"tags": [
|
|
3064
|
+
"Assets"
|
|
3065
|
+
],
|
|
3066
|
+
"security": [
|
|
3067
|
+
{
|
|
3068
|
+
"bearerAuth": []
|
|
3069
|
+
}
|
|
3070
|
+
],
|
|
3071
|
+
"requestBody": {
|
|
3072
|
+
"description": "The data to create the asset with.",
|
|
3073
|
+
"content": {
|
|
3074
|
+
"multipart/form-data": {
|
|
3075
|
+
"schema": {
|
|
3076
|
+
"type": "object",
|
|
3077
|
+
"properties": {
|
|
3078
|
+
"file": {
|
|
3079
|
+
"$ref": "#/components/schemas/File to be uploaded"
|
|
3080
|
+
}
|
|
3081
|
+
},
|
|
3082
|
+
"required": [
|
|
3083
|
+
"file"
|
|
3084
|
+
]
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
},
|
|
3089
|
+
"responses": {
|
|
3090
|
+
"200": {
|
|
3091
|
+
"description": "Details about the created asset",
|
|
3092
|
+
"content": {
|
|
3093
|
+
"application/json": {
|
|
3094
|
+
"schema": {
|
|
3095
|
+
"$ref": "#/components/schemas/Asset"
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
},
|
|
3103
|
+
"/assets/{assetId}": {
|
|
3104
|
+
"get": {
|
|
3105
|
+
"description": "Get asset by its id",
|
|
3106
|
+
"summary": "Get a single asset",
|
|
3107
|
+
"tags": [
|
|
3108
|
+
"Assets"
|
|
3109
|
+
],
|
|
3110
|
+
"security": [
|
|
3111
|
+
{
|
|
3112
|
+
"bearerAuth": []
|
|
3113
|
+
}
|
|
3114
|
+
],
|
|
3115
|
+
"parameters": [
|
|
3116
|
+
{
|
|
3117
|
+
"$ref": "#/components/parameters/AssetId"
|
|
3118
|
+
}
|
|
3119
|
+
],
|
|
3120
|
+
"responses": {
|
|
3121
|
+
"200": {
|
|
3122
|
+
"description": "Asset content. Content type is determined by the asset type."
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
}
|
|
3027
3126
|
}
|
|
3028
3127
|
}
|
|
3029
3128
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: karakeep_python_api
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.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
|
|
@@ -81,7 +81,7 @@ Methods or CLI commands marked with ❌ should be used with caution as their beh
|
|
|
81
81
|
| Method Name | Pytest | CLI | Remarks |
|
|
82
82
|
| -------------------------------- | :----: | :--: | -------------------------------------------- |
|
|
83
83
|
| `get_all_bookmarks` | ✅ | ✅ | Tested with pagination. |
|
|
84
|
-
| `create_a_new_bookmark` | ✅ | ❌ | Pytest for `type="link"` via fixture. CLI not directly tested. |
|
|
84
|
+
| `create_a_new_bookmark` | ✅ | ❌ | Pytest for `type="link"` via fixture and `type="asset"` via PDF test. CLI not directly tested. |
|
|
85
85
|
| `search_bookmarks` | ✅ | ✅ | Seems to be nondeterministic and fails if using more than 3 words |
|
|
86
86
|
| `get_a_single_bookmark` | ✅ | ❌ | |
|
|
87
87
|
| `delete_a_bookmark` | ✅ | ❌ | |
|
|
@@ -112,6 +112,8 @@ Methods or CLI commands marked with ❌ should be used with caution as their beh
|
|
|
112
112
|
| `get_a_single_highlight` | ❌ | ❌ | |
|
|
113
113
|
| `delete_a_highlight` | ❌ | ❌ | Works from the CLI; not yet added to Pytest. |
|
|
114
114
|
| `update_a_highlight` | ❌ | ❌ | |
|
|
115
|
+
| `upload_a_new_asset` | ✅ | ❌ | Tested in PDF asset lifecycle test. |
|
|
116
|
+
| `get_a_single_asset` | ✅ | ❌ | Tested in PDF asset lifecycle test. |
|
|
115
117
|
| `get_current_user_info` | ✅ | ❌ | Pytest: Tested indirectly during client init. CLI not directly tested. |
|
|
116
118
|
| `get_current_user_stats` | ✅ | ✅ | |
|
|
117
119
|
|
|
@@ -137,7 +139,7 @@ This package can be used as a Python library or as a command-line interface (CLI
|
|
|
137
139
|
|
|
138
140
|
The client can be configured using the following environment variables:
|
|
139
141
|
|
|
140
|
-
* `
|
|
142
|
+
* `KARAKEEP_PYTHON_API_ENDPOINT`: **Required**. The full URL of your Karakeep API, including the `/api/v1/` path (e.g., `https://karakeep.domain.com/api/v1/` or `https://try.karakeep.app/api/v1/`).
|
|
141
143
|
* `KARAKEEP_PYTHON_API_KEY`: **Required**. Your Karakeep API key (Bearer token).
|
|
142
144
|
* `KARAKEEP_PYTHON_API_VERIFY_SSL`: Set to `false` to disable SSL certificate verification (default: `true`).
|
|
143
145
|
* `KARAKEEP_PYTHON_API_VERBOSE`: Set to `true` to enable verbose debug logging for the client and CLI (default: `false`).
|
|
@@ -146,7 +148,7 @@ The client can be configured using the following environment variables:
|
|
|
146
148
|
|
|
147
149
|
### Command Line Interface (CLI)
|
|
148
150
|
|
|
149
|
-
The CLI dynamically generates commands based on the API methods. You need to provide your API key and
|
|
151
|
+
The CLI dynamically generates commands based on the API methods. You need to provide your API key and endpoint either via environment variables (recommended) or command-line options.
|
|
150
152
|
|
|
151
153
|
**Basic Structure:**
|
|
152
154
|
|
|
@@ -171,8 +173,8 @@ python -m karakeep_python_api get-all-bookmarks --help
|
|
|
171
173
|
python -m karakeep_python_api get-all-tags
|
|
172
174
|
|
|
173
175
|
# Get the first page of bookmarks with a limit, overriding env vars if needed
|
|
174
|
-
# Note:
|
|
175
|
-
python -m karakeep_python_api --base-url https://
|
|
176
|
+
# Note: The /api/v1/ path will be automatically appended if not present
|
|
177
|
+
python -m karakeep_python_api --base-url https://karakeep.domain.com/api/v1/ --api-key YOUR_API_KEY get-all-bookmarks --limit 10
|
|
176
178
|
|
|
177
179
|
# Get all lists and pipe the JSON output to jq to extract the first list
|
|
178
180
|
python -m karakeep_python_api get-all-lists | jq '.[0]'
|
|
@@ -196,14 +198,14 @@ import os
|
|
|
196
198
|
from karakeep_python_api import KarakeepAPI, APIError, AuthenticationError, datatypes
|
|
197
199
|
|
|
198
200
|
# Ensure required environment variables are set
|
|
199
|
-
# Example: os.environ["
|
|
201
|
+
# Example: os.environ["KARAKEEP_PYTHON_API_ENDPOINT"] = "https://karakeep.domain.com/api/v1/"
|
|
200
202
|
# Example: os.environ["KARAKEEP_PYTHON_API_KEY"] = "your_secret_api_key"
|
|
201
203
|
|
|
202
204
|
try:
|
|
203
205
|
# Initialize the client (reads from env vars by default)
|
|
204
206
|
client = KarakeepAPI(
|
|
205
207
|
# Optionally override env vars:
|
|
206
|
-
#
|
|
208
|
+
# api_endpoint="https://karakeep.domain.com/api/v1/",
|
|
207
209
|
# api_key="another_key",
|
|
208
210
|
# verbose=True,
|
|
209
211
|
# disable_response_validation=False
|
|
@@ -233,7 +235,7 @@ except AuthenticationError as e:
|
|
|
233
235
|
except APIError as e:
|
|
234
236
|
print(f"An API error occurred: {e}")
|
|
235
237
|
except ValueError as e:
|
|
236
|
-
# Handles missing API key/
|
|
238
|
+
# Handles missing API key/endpoint during initialization
|
|
237
239
|
print(f"Configuration error: {e}")
|
|
238
240
|
except Exception as e:
|
|
239
241
|
print(f"An unexpected error occurred: {e}")
|
|
@@ -242,15 +244,19 @@ except Exception as e:
|
|
|
242
244
|
|
|
243
245
|
## Community Scripts
|
|
244
246
|
|
|
245
|
-
|
|
247
|
+
Community Scripts are a bunch of scripts made to solve specific issues. They are made by the community so don't hesitate to submit yours or open an issue if you have a bug. They also serve as example of how to use the API.
|
|
246
248
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
|
250
|
-
|
|
251
|
-
| **
|
|
252
|
-
| **
|
|
253
|
-
| **
|
|
249
|
+
They can be found in the [./community_scripts](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts) folder. Don't hesitate to submit yours, the contribution guidelines are in the community_scripts directory README.md file.
|
|
250
|
+
|
|
251
|
+
| Community Script | Description | Documentation |
|
|
252
|
+
|----------------|--------------------------------------------------------------------------------------------------------------|---------------|
|
|
253
|
+
| **Karakeep-Time-Tagger** | Automatically adds time-to-read tags (`0-5m`, `5-10m`, etc.) to bookmarks based on content length analysis. Includes systemd service and timer files for automated periodic execution. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/karakeep-time-tagger) |
|
|
254
|
+
| **Karakeep-List-To-Tag** | Converts a Karakeep list into tags by adding a specified tag to all bookmarks within that list. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/karakeep-list-to-tag) |
|
|
255
|
+
| **Omnivore2Karakeep-Highlights** | Imports highlights from Omnivore export data to Karakeep, with intelligent position detection and bookmark matching. Supports dry-run mode for testing. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/omnivore2karakeep-highlights) |
|
|
256
|
+
| **Omnivore2Karakeep-Archived** | (Should not be needed anymore) Fixes the archived status of bookmarks imported from Omnivore by reading export data and updating Karakeep accordingly. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/omnivore2karakeep-archived) |
|
|
257
|
+
| **pocket2karakeep-archived** by [@youenchene](https://github.com/youenchene) | (Should not be needed anymore) Fixes the archived status of bookmarks imported from Pocket by reading export data and updating Karakeep accordingly. | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/pocket2karakeep-archived) |
|
|
258
|
+
| **Karakeep-Archive-Before-Date** by [@youenchene](https://github.com/youenchene) | Allow you to archive all not archived post before a given date | [`Link`](https://github.com/thiswillbeyourgithub/karakeep_python_api/tree/main/community_scripts/karakeep-archive-before-date) |
|
|
259
|
+
| **Freshrss-To-Karakeep** | Syncs some links from Freshrss to Karakeep | [`Link`](https://github.com/thiswillbeyourgithub/freshrss_to_karakeep) |
|
|
254
260
|
|
|
255
261
|
## Development
|
|
256
262
|
|
|
@@ -261,7 +267,7 @@ Examples of the API being used can be found in the [`./examples`](./examples) fo
|
|
|
261
267
|
```bash
|
|
262
268
|
uv pip install -e ".[dev]"
|
|
263
269
|
```
|
|
264
|
-
4. Set the required environment variables (`
|
|
270
|
+
4. Set the required environment variables (`KARAKEEP_PYTHON_API_ENDPOINT`, `KARAKEEP_PYTHON_API_KEY`) for running tests against a live instance.
|
|
265
271
|
5. Run tests:
|
|
266
272
|
|
|
267
273
|
```bash
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
karakeep_python_api/__init__.py,sha256=qk3MeIIvnCNAyYzJrv8dMKVXvA4ejLU3mhB3xFzdLDY,606
|
|
2
|
+
karakeep_python_api/__main__.py,sha256=X1GXwRvAAw8qFmMm2MKdmPNYkmlmEUUQalJX9kWxrS0,32402
|
|
3
|
+
karakeep_python_api/datatypes.py,sha256=VHUuSTifTnVnxYOA9TUGvGK4GiZLcXgQA8AqBQ6Kqgs,3529
|
|
4
|
+
karakeep_python_api/karakeep_api.py,sha256=wB2lTB0uHeiXr7sN9yikp5FkiRI1Dw4IDRRCOanf7HM,91802
|
|
5
|
+
karakeep_python_api/openapi_reference.json,sha256=J1dyFeXgcV6boh851xU3J3YRTz_5ixrGQjtxkJEj1RU,82686
|
|
6
|
+
karakeep_python_api-1.2.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
7
|
+
karakeep_python_api-1.2.0.dist-info/METADATA,sha256=VfqCwjsuJGhvTu5jTMtmJobLaVAVB5-NObs9URUqKus,15970
|
|
8
|
+
karakeep_python_api-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
karakeep_python_api-1.2.0.dist-info/entry_points.txt,sha256=n0leQp_IX2NivoWuUcaR9ZHwAvl_6yt-NcHWuCJyxNo,62
|
|
10
|
+
karakeep_python_api-1.2.0.dist-info/top_level.txt,sha256=X3VKqh9YAbPQp144db0Ko2C5Q2y6-nwpPgtnuCiSDmU,20
|
|
11
|
+
karakeep_python_api-1.2.0.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
karakeep_python_api/__init__.py,sha256=qk3MeIIvnCNAyYzJrv8dMKVXvA4ejLU3mhB3xFzdLDY,606
|
|
2
|
-
karakeep_python_api/__main__.py,sha256=UfL6R-S3rJB7PH3iqTicNq0cvtumnrG_FVyKK36bqdE,32327
|
|
3
|
-
karakeep_python_api/datatypes.py,sha256=CScuMq5oT4YwvGH8Af_Oo60xGmC622d74f4WPUw7l8Q,3398
|
|
4
|
-
karakeep_python_api/karakeep_api.py,sha256=Fpyc3A4Gph3czirIDztsvzbtnegmkDLmiJNDCdgTt04,84749
|
|
5
|
-
karakeep_python_api/openapi_reference.json,sha256=Ku1cO_N4QPLQc7sK8L7FRt2T68QyPCwTGA7RK-046iU,80379
|
|
6
|
-
karakeep_python_api-1.0.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
7
|
-
karakeep_python_api-1.0.0.dist-info/METADATA,sha256=joWnEpuTHNzpSr4U4EJ-Xb48YK8_YU-rrYHW9HWGsAg,14026
|
|
8
|
-
karakeep_python_api-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
-
karakeep_python_api-1.0.0.dist-info/entry_points.txt,sha256=n0leQp_IX2NivoWuUcaR9ZHwAvl_6yt-NcHWuCJyxNo,62
|
|
10
|
-
karakeep_python_api-1.0.0.dist-info/top_level.txt,sha256=X3VKqh9YAbPQp144db0Ko2C5Q2y6-nwpPgtnuCiSDmU,20
|
|
11
|
-
karakeep_python_api-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
{karakeep_python_api-1.0.0.dist-info → karakeep_python_api-1.2.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{karakeep_python_api-1.0.0.dist-info → karakeep_python_api-1.2.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|