karakeep-python-api 1.1.0__tar.gz → 1.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {karakeep_python_api-1.1.0/karakeep_python_api.egg-info → karakeep_python_api-1.2.0}/PKG-INFO +8 -5
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/README.md +7 -4
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/karakeep_python_api/datatypes.py +9 -2
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/karakeep_python_api/karakeep_api.py +192 -43
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/karakeep_python_api/openapi_reference.json +105 -13
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0/karakeep_python_api.egg-info}/PKG-INFO +8 -5
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/setup.py +1 -1
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/tests/test_karakeep_api.py +86 -1
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/LICENSE +0 -0
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/MANIFEST.in +0 -0
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/karakeep_python_api/__init__.py +0 -0
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/karakeep_python_api/__main__.py +0 -0
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/karakeep_python_api.egg-info/SOURCES.txt +0 -0
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/karakeep_python_api.egg-info/dependency_links.txt +0 -0
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/karakeep_python_api.egg-info/entry_points.txt +0 -0
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/karakeep_python_api.egg-info/requires.txt +0 -0
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/karakeep_python_api.egg-info/top_level.txt +0 -0
- {karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/setup.cfg +0 -0
{karakeep_python_api-1.1.0/karakeep_python_api.egg-info → karakeep_python_api-1.2.0}/PKG-INFO
RENAMED
|
@@ -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
|
|
|
@@ -246,14 +248,15 @@ Community Scripts are a bunch of scripts made to solve specific issues. They are
|
|
|
246
248
|
|
|
247
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.
|
|
248
250
|
|
|
249
|
-
| Community Script | Description
|
|
250
|
-
|
|
251
|
+
| Community Script | Description | Documentation |
|
|
252
|
+
|----------------|--------------------------------------------------------------------------------------------------------------|---------------|
|
|
251
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) |
|
|
252
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) |
|
|
253
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) |
|
|
254
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) |
|
|
255
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) |
|
|
256
|
-
| **
|
|
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) |
|
|
257
260
|
|
|
258
261
|
## Development
|
|
259
262
|
|
|
@@ -46,7 +46,7 @@ Methods or CLI commands marked with ❌ should be used with caution as their beh
|
|
|
46
46
|
| Method Name | Pytest | CLI | Remarks |
|
|
47
47
|
| -------------------------------- | :----: | :--: | -------------------------------------------- |
|
|
48
48
|
| `get_all_bookmarks` | ✅ | ✅ | Tested with pagination. |
|
|
49
|
-
| `create_a_new_bookmark` | ✅ | ❌ | Pytest for `type="link"` via fixture. CLI not directly tested. |
|
|
49
|
+
| `create_a_new_bookmark` | ✅ | ❌ | Pytest for `type="link"` via fixture and `type="asset"` via PDF test. CLI not directly tested. |
|
|
50
50
|
| `search_bookmarks` | ✅ | ✅ | Seems to be nondeterministic and fails if using more than 3 words |
|
|
51
51
|
| `get_a_single_bookmark` | ✅ | ❌ | |
|
|
52
52
|
| `delete_a_bookmark` | ✅ | ❌ | |
|
|
@@ -77,6 +77,8 @@ Methods or CLI commands marked with ❌ should be used with caution as their beh
|
|
|
77
77
|
| `get_a_single_highlight` | ❌ | ❌ | |
|
|
78
78
|
| `delete_a_highlight` | ❌ | ❌ | Works from the CLI; not yet added to Pytest. |
|
|
79
79
|
| `update_a_highlight` | ❌ | ❌ | |
|
|
80
|
+
| `upload_a_new_asset` | ✅ | ❌ | Tested in PDF asset lifecycle test. |
|
|
81
|
+
| `get_a_single_asset` | ✅ | ❌ | Tested in PDF asset lifecycle test. |
|
|
80
82
|
| `get_current_user_info` | ✅ | ❌ | Pytest: Tested indirectly during client init. CLI not directly tested. |
|
|
81
83
|
| `get_current_user_stats` | ✅ | ✅ | |
|
|
82
84
|
|
|
@@ -211,14 +213,15 @@ Community Scripts are a bunch of scripts made to solve specific issues. They are
|
|
|
211
213
|
|
|
212
214
|
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.
|
|
213
215
|
|
|
214
|
-
| Community Script | Description
|
|
215
|
-
|
|
216
|
+
| Community Script | Description | Documentation |
|
|
217
|
+
|----------------|--------------------------------------------------------------------------------------------------------------|---------------|
|
|
216
218
|
| **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) |
|
|
217
219
|
| **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) |
|
|
218
220
|
| **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) |
|
|
219
221
|
| **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) |
|
|
220
222
|
| **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) |
|
|
221
|
-
| **
|
|
223
|
+
| **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) |
|
|
224
|
+
| **Freshrss-To-Karakeep** | Syncs some links from Freshrss to Karakeep | [`Link`](https://github.com/thiswillbeyourgithub/freshrss_to_karakeep) |
|
|
222
225
|
|
|
223
226
|
## Development
|
|
224
227
|
|
|
@@ -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):
|
|
@@ -85,7 +85,7 @@ 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,
|
|
@@ -105,7 +105,7 @@ 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
|
-
api_endpoint: Override the
|
|
108
|
+
api_endpoint: Override the endpoint for the API. Must be provided either as an argument
|
|
109
109
|
or via the KARAKEEP_PYTHON_API_ENDPOINT environment variable.
|
|
110
110
|
Example: 'https://karakeep.domain.com/api/v1/'
|
|
111
111
|
openapi_spec_path: Path to the OpenAPI JSON specification file.
|
|
@@ -131,47 +131,47 @@ class KarakeepAPI:
|
|
|
131
131
|
self.api_key = resolved_api_key
|
|
132
132
|
logger.debug("API Key loaded successfully.")
|
|
133
133
|
|
|
134
|
-
# ---
|
|
134
|
+
# --- Endpoint Validation ---
|
|
135
135
|
env_endpoint = os.environ.get("KARAKEEP_PYTHON_API_ENDPOINT")
|
|
136
136
|
logger.debug(
|
|
137
137
|
f"Checked KARAKEEP_PYTHON_API_ENDPOINT environment variable, found: '{env_endpoint}'"
|
|
138
138
|
)
|
|
139
|
-
logger.debug(f"
|
|
139
|
+
logger.debug(f"Endpoint provided as argument: '{api_endpoint}'")
|
|
140
140
|
|
|
141
141
|
if api_endpoint:
|
|
142
142
|
self.api_endpoint = api_endpoint
|
|
143
|
-
logger.info(f"Using provided
|
|
143
|
+
logger.info(f"Using provided endpoint: {self.api_endpoint}")
|
|
144
144
|
elif env_endpoint:
|
|
145
145
|
self.api_endpoint = env_endpoint
|
|
146
146
|
logger.info(
|
|
147
|
-
f"Using
|
|
147
|
+
f"Using endpoint from KARAKEEP_PYTHON_API_ENDPOINT: {self.api_endpoint}"
|
|
148
148
|
)
|
|
149
149
|
else:
|
|
150
150
|
# No api_endpoint from arg or env var - raise error as per requirement
|
|
151
151
|
raise ValueError(
|
|
152
|
-
"API
|
|
152
|
+
"API endpoint is required. Provide 'api_endpoint' argument or set KARAKEEP_PYTHON_API_ENDPOINT environment variable."
|
|
153
153
|
)
|
|
154
154
|
|
|
155
|
-
# Ensure
|
|
155
|
+
# Ensure endpoint ends with /api/v1/
|
|
156
156
|
resolved_url = self.api_endpoint # Use a temporary variable for checks
|
|
157
157
|
if resolved_url.endswith("/api/v1"):
|
|
158
158
|
# Ends with /api/v1, needs a slash
|
|
159
159
|
self.api_endpoint = resolved_url + "/"
|
|
160
160
|
logger.info(
|
|
161
|
-
f"Appended trailing slash to
|
|
161
|
+
f"Appended trailing slash to endpoint ending in /api/v1: {self.api_endpoint}"
|
|
162
162
|
)
|
|
163
163
|
elif resolved_url.endswith("/api/v1/"):
|
|
164
164
|
# Already ends correctly, do nothing
|
|
165
|
-
logger.debug(f"
|
|
165
|
+
logger.debug(f"Endpoint already ends with /api/v1/: {self.api_endpoint}")
|
|
166
166
|
else:
|
|
167
167
|
# Doesn't end with /api/v1 or /api/v1/, append /api/v1/
|
|
168
168
|
# First, remove any existing trailing slash to avoid //api/v1/
|
|
169
169
|
if resolved_url.endswith("/"):
|
|
170
170
|
resolved_url = resolved_url[:-1]
|
|
171
171
|
self.api_endpoint = resolved_url + "/api/v1/"
|
|
172
|
-
logger.info(f"Appended /api/v1/ to
|
|
172
|
+
logger.info(f"Appended /api/v1/ to endpoint: {self.api_endpoint}")
|
|
173
173
|
|
|
174
|
-
logger.debug(f"Final API
|
|
174
|
+
logger.debug(f"Final API Endpoint after /api/v1/ check: {self.api_endpoint}")
|
|
175
175
|
|
|
176
176
|
# --- Load and Parse OpenAPI Spec ---
|
|
177
177
|
if openapi_spec_path is None:
|
|
@@ -258,7 +258,7 @@ class KarakeepAPI:
|
|
|
258
258
|
# self.verbose is still used for conditional logging within the class methods.
|
|
259
259
|
|
|
260
260
|
logger.debug("KarakeepAPI client initialized.")
|
|
261
|
-
logger.debug(f"
|
|
261
|
+
logger.debug(f" Endpoint: {self.api_endpoint}")
|
|
262
262
|
logger.debug(f" Verify SSL: {self.verify_ssl}")
|
|
263
263
|
logger.debug(f" Verbose: {self.verbose}")
|
|
264
264
|
logger.debug(
|
|
@@ -295,26 +295,29 @@ class KarakeepAPI:
|
|
|
295
295
|
data: Optional[
|
|
296
296
|
Union[BaseModel, dict, list, str, bytes]
|
|
297
297
|
] = None, # More specific type hint
|
|
298
|
+
files: Optional[Dict[str, Any]] = None,
|
|
298
299
|
extra_headers: Optional[Dict[str, str]] = None,
|
|
299
|
-
) -> Union[Dict[str, Any], List[Any], None]:
|
|
300
|
+
) -> Union[Dict[str, Any], List[Any], None, bytes]:
|
|
300
301
|
"""
|
|
301
302
|
Internal method to make an HTTP call to the Karakeep API. Handles authentication,
|
|
302
303
|
request formatting, response parsing, and error handling.
|
|
303
304
|
|
|
304
305
|
Args:
|
|
305
306
|
method: HTTP method ('GET', 'POST', 'PUT', 'PATCH', 'DELETE').
|
|
306
|
-
endpoint: API endpoint path relative to the
|
|
307
|
+
endpoint: API endpoint path relative to the endpoint (e.g., 'bookmarks' or 'bookmarks/some_id').
|
|
307
308
|
Path parameters (like {bookmarkId}) MUST be substituted *before* calling _call.
|
|
308
309
|
params: Dictionary of URL query parameters. Values should be primitive types suitable for URLs.
|
|
309
310
|
data: Request body data. Can be a Pydantic model, dict, list, bytes, or str.
|
|
310
311
|
- Pydantic models, dicts, and lists will be automatically JSON-encoded
|
|
311
312
|
with 'Content-Type: application/json' unless overridden in extra_headers.
|
|
312
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.
|
|
313
315
|
extra_headers: Additional headers to include or override default headers.
|
|
314
316
|
|
|
315
317
|
Returns:
|
|
316
|
-
The parsed JSON response from the API as a dict or list,
|
|
317
|
-
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.
|
|
318
321
|
|
|
319
322
|
Raises:
|
|
320
323
|
AuthenticationError: If authentication fails (401).
|
|
@@ -346,7 +349,16 @@ class KarakeepAPI:
|
|
|
346
349
|
# Determine Content-Type, prioritizing extra_headers
|
|
347
350
|
content_type = headers.get("Content-Type")
|
|
348
351
|
|
|
349
|
-
|
|
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:
|
|
350
362
|
if isinstance(data, BaseModel):
|
|
351
363
|
# Serialize Pydantic model to JSON bytes
|
|
352
364
|
request_body_arg = data.model_dump_json(
|
|
@@ -471,6 +483,7 @@ class KarakeepAPI:
|
|
|
471
483
|
url=url,
|
|
472
484
|
params=request_params, # Use params with stringified booleans
|
|
473
485
|
data=request_body_arg, # Serialized data (bytes or str)
|
|
486
|
+
files=files, # File uploads for multipart/form-data
|
|
474
487
|
headers=headers,
|
|
475
488
|
verify=self.verify_ssl,
|
|
476
489
|
timeout=60, # Increased default timeout
|
|
@@ -515,28 +528,42 @@ class KarakeepAPI:
|
|
|
515
528
|
logger.debug(" Body: None (204 No Content or empty response body)")
|
|
516
529
|
return None
|
|
517
530
|
|
|
518
|
-
#
|
|
519
|
-
|
|
520
|
-
|
|
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
|
|
521
560
|
if self.verbose:
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
# specific Pydantic models should happen in the calling wrapper method.
|
|
529
|
-
return result
|
|
530
|
-
except json.JSONDecodeError as e:
|
|
531
|
-
# Handle cases where the response is successful (2xx) but not valid JSON
|
|
532
|
-
logger.error(
|
|
533
|
-
f"API Error: Failed to decode JSON response from {method} {url}. Status: {response.status_code}. Content: {response.text[:500]}..."
|
|
534
|
-
)
|
|
535
|
-
# Raise APIError as the response format is unexpected
|
|
536
|
-
raise APIError(
|
|
537
|
-
message=f"Failed to parse successful API response JSON from {url}: {e}. Response text: {response.text[:200]}...",
|
|
538
|
-
status_code=response.status_code,
|
|
539
|
-
) 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
|
|
540
567
|
|
|
541
568
|
except requests.exceptions.HTTPError as e:
|
|
542
569
|
# Handle 4xx/5xx errors raised by response.raise_for_status()
|
|
@@ -1180,7 +1207,7 @@ class KarakeepAPI:
|
|
|
1180
1207
|
"precrawledArchive",
|
|
1181
1208
|
"unknown",
|
|
1182
1209
|
],
|
|
1183
|
-
) -> Union[datatypes.
|
|
1210
|
+
) -> Union[datatypes.BookmarkAsset, Dict[str, Any], List[Any]]:
|
|
1184
1211
|
"""
|
|
1185
1212
|
Attach a new asset to a bookmark. Corresponds to POST /bookmarks/{bookmarkId}/assets.
|
|
1186
1213
|
|
|
@@ -1191,7 +1218,7 @@ class KarakeepAPI:
|
|
|
1191
1218
|
"bannerImage", "fullPageArchive", "video", "bookmarkAsset", "precrawledArchive", "unknown".
|
|
1192
1219
|
|
|
1193
1220
|
Returns:
|
|
1194
|
-
datatypes.
|
|
1221
|
+
datatypes.BookmarkAsset: The attached asset object.
|
|
1195
1222
|
If response validation is disabled, returns the raw API response (dict/list).
|
|
1196
1223
|
|
|
1197
1224
|
Raises:
|
|
@@ -1208,8 +1235,8 @@ class KarakeepAPI:
|
|
|
1208
1235
|
logger.debug("Skipping response validation as requested.")
|
|
1209
1236
|
return response_data
|
|
1210
1237
|
else:
|
|
1211
|
-
# Response should match
|
|
1212
|
-
return datatypes.
|
|
1238
|
+
# Response should match BookmarkAsset schema
|
|
1239
|
+
return datatypes.BookmarkAsset.model_validate(response_data)
|
|
1213
1240
|
|
|
1214
1241
|
@optional_typecheck
|
|
1215
1242
|
def replace_asset(self, bookmark_id: str, asset_id: str, new_asset_id: str) -> None:
|
|
@@ -1943,3 +1970,125 @@ class KarakeepAPI:
|
|
|
1943
1970
|
response_data = self._call("GET", "users/me/stats")
|
|
1944
1971
|
# No Pydantic validation applied here as the spec defines a simple dict response
|
|
1945
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)
|
{karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/karakeep_python_api/openapi_reference.json
RENAMED
|
@@ -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": {
|
|
@@ -488,17 +488,33 @@
|
|
|
488
488
|
"highlights",
|
|
489
489
|
"nextCursor"
|
|
490
490
|
]
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
+
}
|
|
497
507
|
},
|
|
498
|
-
"required":
|
|
499
|
-
|
|
500
|
-
|
|
508
|
+
"required": [
|
|
509
|
+
"assetId",
|
|
510
|
+
"contentType",
|
|
511
|
+
"size",
|
|
512
|
+
"fileName"
|
|
513
|
+
]
|
|
501
514
|
},
|
|
515
|
+
"File to be uploaded": {}
|
|
516
|
+
},
|
|
517
|
+
"parameters": {
|
|
502
518
|
"BookmarkId": {
|
|
503
519
|
"schema": {
|
|
504
520
|
"$ref": "#/components/schemas/BookmarkId"
|
|
@@ -530,6 +546,14 @@
|
|
|
530
546
|
"required": true,
|
|
531
547
|
"name": "highlightId",
|
|
532
548
|
"in": "path"
|
|
549
|
+
},
|
|
550
|
+
"AssetId": {
|
|
551
|
+
"schema": {
|
|
552
|
+
"$ref": "#/components/schemas/AssetId"
|
|
553
|
+
},
|
|
554
|
+
"required": true,
|
|
555
|
+
"name": "assetId",
|
|
556
|
+
"in": "path"
|
|
533
557
|
}
|
|
534
558
|
}
|
|
535
559
|
},
|
|
@@ -3031,6 +3055,74 @@
|
|
|
3031
3055
|
}
|
|
3032
3056
|
}
|
|
3033
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
|
+
}
|
|
3034
3126
|
}
|
|
3035
3127
|
}
|
|
3036
3128
|
}
|
{karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0/karakeep_python_api.egg-info}/PKG-INFO
RENAMED
|
@@ -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
|
|
|
@@ -246,14 +248,15 @@ Community Scripts are a bunch of scripts made to solve specific issues. They are
|
|
|
246
248
|
|
|
247
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.
|
|
248
250
|
|
|
249
|
-
| Community Script | Description
|
|
250
|
-
|
|
251
|
+
| Community Script | Description | Documentation |
|
|
252
|
+
|----------------|--------------------------------------------------------------------------------------------------------------|---------------|
|
|
251
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) |
|
|
252
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) |
|
|
253
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) |
|
|
254
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) |
|
|
255
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) |
|
|
256
|
-
| **
|
|
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) |
|
|
257
260
|
|
|
258
261
|
## Development
|
|
259
262
|
|
|
@@ -7,7 +7,7 @@ with open("README.md", "r") as readme:
|
|
|
7
7
|
|
|
8
8
|
setup(
|
|
9
9
|
name="karakeep_python_api",
|
|
10
|
-
version="1.
|
|
10
|
+
version="1.2.0",
|
|
11
11
|
description="Community python client for the Karakeep API.", # Simplified description
|
|
12
12
|
long_description=long_description,
|
|
13
13
|
long_description_content_type="text/markdown",
|
|
@@ -237,7 +237,7 @@ def test_openapi_spec_accessible(karakeep_client: KarakeepAPI):
|
|
|
237
237
|
# The closest CLI equivalent is dumping the spec file content
|
|
238
238
|
try:
|
|
239
239
|
logger.info("\n Running CLI equivalent: --dump-openapi-specification")
|
|
240
|
-
# This command doesn't require API key or
|
|
240
|
+
# This command doesn't require API key or endpoint
|
|
241
241
|
subprocess.run(
|
|
242
242
|
"python -m karakeep_python_api --dump-openapi-specification",
|
|
243
243
|
shell=True,
|
|
@@ -803,4 +803,89 @@ def test_get_current_user_stats(karakeep_client: KarakeepAPI):
|
|
|
803
803
|
pytest.fail(f"An unexpected error occurred running the CLI command: {e}")
|
|
804
804
|
|
|
805
805
|
|
|
806
|
+
def test_asset_lifecycle_with_pdf(karakeep_client: KarakeepAPI):
|
|
807
|
+
"""Test creating a PDF bookmark, verifying its asset, and deleting it."""
|
|
808
|
+
pdf_file_path = "tests/PDF Bookmark Sample.pdf"
|
|
809
|
+
uploaded_asset_id = None
|
|
810
|
+
created_bookmark_id = None
|
|
811
|
+
|
|
812
|
+
# Generate unique title to avoid collisions
|
|
813
|
+
timestamp = int(time.time())
|
|
814
|
+
random_suffix = "".join(random.choices(string.ascii_lowercase + string.digits, k=6))
|
|
815
|
+
bookmark_title = f"Test PDF Bookmark {timestamp}-{random_suffix}"
|
|
816
|
+
|
|
817
|
+
try:
|
|
818
|
+
# 1. Upload the PDF asset
|
|
819
|
+
logger.info(f"\nUploading PDF asset from: {pdf_file_path}")
|
|
820
|
+
uploaded_asset = karakeep_client.upload_a_new_asset(file=pdf_file_path)
|
|
821
|
+
assert isinstance(uploaded_asset, datatypes.Asset)
|
|
822
|
+
assert uploaded_asset.assetId, "Uploaded asset must have an ID"
|
|
823
|
+
assert "pdf" in uploaded_asset.contentType.lower(), "Asset should be PDF type"
|
|
824
|
+
assert (
|
|
825
|
+
uploaded_asset.fileName == "PDF Bookmark Sample.pdf"
|
|
826
|
+
), "Asset filename should match the original file"
|
|
827
|
+
uploaded_asset_id = uploaded_asset.assetId
|
|
828
|
+
logger.info(f"✓ PDF uploaded with asset ID: {uploaded_asset_id}")
|
|
829
|
+
|
|
830
|
+
# 2. Create a PDF bookmark using the uploaded asset
|
|
831
|
+
logger.info(f"\nCreating PDF bookmark with title: '{bookmark_title}'")
|
|
832
|
+
bookmark = karakeep_client.create_a_new_bookmark(
|
|
833
|
+
type="asset",
|
|
834
|
+
asset_type="pdf",
|
|
835
|
+
assetId=uploaded_asset_id,
|
|
836
|
+
title=bookmark_title,
|
|
837
|
+
fileName="PDF Bookmark Sample.pdf",
|
|
838
|
+
)
|
|
839
|
+
assert isinstance(bookmark, datatypes.Bookmark)
|
|
840
|
+
assert bookmark.id, "Created bookmark must have an ID"
|
|
841
|
+
assert bookmark.title == bookmark_title, "Bookmark title should match"
|
|
842
|
+
created_bookmark_id = bookmark.id
|
|
843
|
+
logger.info(f"✓ PDF bookmark created with ID: {created_bookmark_id}")
|
|
844
|
+
|
|
845
|
+
# 3. Verify the bookmark has the correct asset
|
|
846
|
+
logger.info(f"\nVerifying bookmark {created_bookmark_id} has the PDF asset")
|
|
847
|
+
retrieved_bookmark = karakeep_client.get_a_single_bookmark(
|
|
848
|
+
bookmark_id=created_bookmark_id
|
|
849
|
+
)
|
|
850
|
+
assert isinstance(retrieved_bookmark, datatypes.Bookmark)
|
|
851
|
+
assert (
|
|
852
|
+
len(retrieved_bookmark.assets) > 0
|
|
853
|
+
), "Bookmark should have at least one asset"
|
|
854
|
+
|
|
855
|
+
# Check that our uploaded asset is among the bookmark's assets
|
|
856
|
+
asset_ids = [asset.id for asset in retrieved_bookmark.assets]
|
|
857
|
+
assert (
|
|
858
|
+
uploaded_asset_id in asset_ids
|
|
859
|
+
), f"Uploaded asset {uploaded_asset_id} should be attached to bookmark"
|
|
860
|
+
logger.info(f"✓ Verified bookmark contains the PDF asset {uploaded_asset_id}")
|
|
861
|
+
|
|
862
|
+
# 4. Retrieve and verify the asset content
|
|
863
|
+
logger.info(f"\nRetrieving asset content for ID: {uploaded_asset_id}")
|
|
864
|
+
asset_content = karakeep_client.get_a_single_asset(asset_id=uploaded_asset_id)
|
|
865
|
+
assert isinstance(asset_content, bytes), "Asset content should be bytes"
|
|
866
|
+
assert len(asset_content) > 0, "Asset content should not be empty"
|
|
867
|
+
assert asset_content.startswith(b"%PDF"), "PDF should start with PDF header"
|
|
868
|
+
logger.info(f"✓ Retrieved PDF asset content ({len(asset_content)} bytes)")
|
|
869
|
+
|
|
870
|
+
except FileNotFoundError:
|
|
871
|
+
pytest.skip(f"PDF test file not found: {pdf_file_path}")
|
|
872
|
+
except (APIError, AuthenticationError) as e:
|
|
873
|
+
pytest.fail(f"API error during PDF asset test: {e}")
|
|
874
|
+
except Exception as e:
|
|
875
|
+
pytest.fail(f"Unexpected error during PDF asset test: {e}")
|
|
876
|
+
finally:
|
|
877
|
+
# 5. Clean up: Delete the bookmark
|
|
878
|
+
if created_bookmark_id:
|
|
879
|
+
logger.info(f"\nCleaning up: Deleting bookmark {created_bookmark_id}")
|
|
880
|
+
try:
|
|
881
|
+
karakeep_client.delete_a_bookmark(bookmark_id=created_bookmark_id)
|
|
882
|
+
logger.info(f"✓ Successfully deleted bookmark {created_bookmark_id}")
|
|
883
|
+
except Exception as e:
|
|
884
|
+
logger.info(
|
|
885
|
+
f" Error during cleanup - failed to delete bookmark {created_bookmark_id}: {e}"
|
|
886
|
+
)
|
|
887
|
+
else:
|
|
888
|
+
logger.info("\nNo bookmark to clean up")
|
|
889
|
+
|
|
890
|
+
|
|
806
891
|
# --- End of Tests ---
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/karakeep_python_api.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/karakeep_python_api.egg-info/requires.txt
RENAMED
|
File without changes
|
{karakeep_python_api-1.1.0 → karakeep_python_api-1.2.0}/karakeep_python_api.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|