zop-cli 0.2.1__tar.gz → 0.2.2__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.
- {zop_cli-0.2.1 → zop_cli-0.2.2}/PKG-INFO +1 -1
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/_version.py +1 -1
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/adapters/zotero_api.py +7 -1
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/services/collections.py +10 -5
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_collections.py +34 -7
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_zotero_api.py +18 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/.gitattributes +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/.github/workflows/ci.yml +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/.gitignore +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/CONTRIBUTING.md +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/LICENSE +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/README.md +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/docs/ARCHITECTURE.md +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/pyproject.toml +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/skills/zop/README.md +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/skills/zop/SKILL.md +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/__init__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/__main__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/adapters/__init__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/adapters/sqlite_reader.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/cli.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/commands/__init__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/commands/collection.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/commands/export.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/commands/item.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/commands/library.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/commands/note.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/commands/pdf.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/commands/tag.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/core/__init__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/core/concurrency.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/core/config.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/core/envelope.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/core/errors.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/models/__init__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/models/collection.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/models/common.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/models/envelope.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/models/item.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/services/__init__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/services/export.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/services/items.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/services/library.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/services/notes.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/services/pdf.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/src/zop/services/tags.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/__init__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/conftest.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/fixtures/test_plan.json +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_cli_collection.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_cli_export.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_cli_item.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_cli_library.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_cli_main.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_cli_note.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_cli_plan.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_cli_tag.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_config.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_envelope.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_export.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_items_service.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_library.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_notes_service.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_pdf.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tests/test_tags_service.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tools/inspect_sqlite.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/tools/inspect_tables.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.2}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: zop-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: High-throughput Zotero CLI focused on batch operations and automation
|
|
5
5
|
Project-URL: Homepage, https://github.com/anomalyco/zop
|
|
6
6
|
Project-URL: Issues, https://github.com/anomalyco/zop/issues
|
|
@@ -172,6 +172,11 @@ class ZoteroApi:
|
|
|
172
172
|
parent_key: New parent key. Pass ``None`` or ``False`` to detach to
|
|
173
173
|
top-level. Default (sentinel) leaves parent unchanged.
|
|
174
174
|
version: If-Unmodified-Since-Version for optimistic locking.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The updated collection dict, or an empty dict on an empty response.
|
|
178
|
+
Zotero answers single-object PATCHes with ``204 No Content``, so
|
|
179
|
+
callers must not subscript the result to read fields.
|
|
175
180
|
"""
|
|
176
181
|
payload: dict[str, Any] = {}
|
|
177
182
|
if name is not None:
|
|
@@ -182,7 +187,8 @@ class ZoteroApi:
|
|
|
182
187
|
if version is not None:
|
|
183
188
|
headers["If-Unmodified-Since-Version"] = str(version)
|
|
184
189
|
resp = await self._client.patch(self._coll_url(key), json=payload, headers=headers)
|
|
185
|
-
|
|
190
|
+
result = self._check(resp)
|
|
191
|
+
return result if isinstance(result, dict) else {}
|
|
186
192
|
|
|
187
193
|
async def delete_collection(self, key: str, *, version: int | None = None) -> None:
|
|
188
194
|
"""Delete a collection. CASCADE: deletes all subcollections."""
|
|
@@ -305,12 +305,17 @@ class CollectionsService:
|
|
|
305
305
|
if version is None:
|
|
306
306
|
current = await api.get_collection(key)
|
|
307
307
|
version = current["version"]
|
|
308
|
-
|
|
308
|
+
# Zotero PATCH /collections/{key} returns 204 No Content (no body),
|
|
309
|
+
# so the patch response carries no fields to read. Re-read the
|
|
310
|
+
# collection to return its true updated state (new version, name,
|
|
311
|
+
# parent) — subscripting the patch response crashes (BUG-11).
|
|
312
|
+
await api.update_collection(key, parent_key=parent_key, version=version)
|
|
313
|
+
updated = await api.get_collection(key)
|
|
309
314
|
return Collection(
|
|
310
|
-
key=
|
|
311
|
-
name=
|
|
312
|
-
parent_key=
|
|
313
|
-
version=
|
|
315
|
+
key=updated["key"],
|
|
316
|
+
name=updated["data"]["name"],
|
|
317
|
+
parent_key=updated["data"].get("parentCollection") or None,
|
|
318
|
+
version=updated["version"],
|
|
314
319
|
)
|
|
315
320
|
|
|
316
321
|
async def move_items(
|
|
@@ -222,23 +222,50 @@ async def test_reparent_fetches_version_when_not_provided(
|
|
|
222
222
|
monkeypatch: pytest.MonkeyPatch,
|
|
223
223
|
) -> None:
|
|
224
224
|
fake_api.get_collection.return_value = {
|
|
225
|
-
"key": "
|
|
225
|
+
"key": "KEY00001",
|
|
226
226
|
"version": 5,
|
|
227
227
|
"data": {"name": "n", "parentCollection": False},
|
|
228
228
|
}
|
|
229
|
-
fake_api.update_collection.return_value = {
|
|
229
|
+
fake_api.update_collection.return_value = {} # 204 No Content
|
|
230
|
+
monkeypatch.setattr(CollectionsService, "_require_api", lambda self: fake_api)
|
|
231
|
+
svc = CollectionsService(db_path=fake_db, creds=creds)
|
|
232
|
+
|
|
233
|
+
await svc.reparent("KEY00001", None) # detach to top-level
|
|
234
|
+
|
|
235
|
+
# get_collection called twice: once to read the version, once to re-read
|
|
236
|
+
# the updated state (PATCH /collections/{key} returns no body — BUG-11).
|
|
237
|
+
assert fake_api.get_collection.await_count == 2
|
|
238
|
+
assert all(c.args == ("KEY00001",) for c in fake_api.get_collection.call_args_list)
|
|
239
|
+
assert fake_api.update_collection.await_count == 1
|
|
240
|
+
assert fake_api.update_collection.call_args.kwargs["version"] == 5
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
async def test_reparent_returns_updated_state_after_empty_patch(
|
|
244
|
+
fake_db: Path,
|
|
245
|
+
creds: ApiCreds,
|
|
246
|
+
fake_api: AsyncMock,
|
|
247
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
248
|
+
) -> None:
|
|
249
|
+
"""BUG-11: PATCH /collections/{key} returns 204 No Content → update_collection
|
|
250
|
+
yields {}. reparent must NOT subscript the patch response; it re-reads the
|
|
251
|
+
collection to build the result, so it returns a Collection instead of
|
|
252
|
+
raising ``TypeError: 'NoneType' object is not subscriptable``.
|
|
253
|
+
"""
|
|
254
|
+
fake_api.update_collection.return_value = {} # real 204 behavior
|
|
255
|
+
fake_api.get_collection.return_value = {
|
|
230
256
|
"key": "KEY00001",
|
|
231
257
|
"version": 6,
|
|
232
|
-
"data": {"name": "
|
|
258
|
+
"data": {"name": "ExistingParent", "parentCollection": "PARENT01"},
|
|
233
259
|
}
|
|
234
260
|
monkeypatch.setattr(CollectionsService, "_require_api", lambda self: fake_api)
|
|
235
261
|
svc = CollectionsService(db_path=fake_db, creds=creds)
|
|
236
262
|
|
|
237
|
-
|
|
263
|
+
# "ExistingParent" resolves locally to PARENT01; reparent KEY00001 under it.
|
|
264
|
+
result = await svc.reparent("KEY00001", "ExistingParent")
|
|
238
265
|
|
|
239
|
-
|
|
240
|
-
assert
|
|
241
|
-
assert
|
|
266
|
+
assert result.key == "KEY00001"
|
|
267
|
+
assert result.parent_key == "PARENT01"
|
|
268
|
+
assert result.version == 6
|
|
242
269
|
|
|
243
270
|
|
|
244
271
|
# ---- create: --parent accepts KEY + API fallback (BUG-7) ----
|
|
@@ -178,6 +178,24 @@ async def test_get_collection_gets_single_collection(creds: ApiCreds) -> None:
|
|
|
178
178
|
assert result["version"] == 9
|
|
179
179
|
|
|
180
180
|
|
|
181
|
+
# ---- update_collection ----
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
async def test_update_collection_empty_body_returns_empty_dict(creds: ApiCreds) -> None:
|
|
185
|
+
"""BUG-11: Zotero PATCH /collections/{key} returns 204 No Content (empty
|
|
186
|
+
body). update_collection must yield {} (like update_item), not None — a
|
|
187
|
+
``None`` return crashes any caller that subscripts the result (reparent).
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
191
|
+
return httpx.Response(204) # no content
|
|
192
|
+
|
|
193
|
+
async with ZoteroApi(creds, transport=httpx.MockTransport(handler)) as api:
|
|
194
|
+
result = await api.update_collection("K", parent_key="P", version=5)
|
|
195
|
+
|
|
196
|
+
assert result == {}
|
|
197
|
+
|
|
198
|
+
|
|
181
199
|
# ---- _check status-code mapping ----
|
|
182
200
|
|
|
183
201
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|