zop-cli 0.2.1__tar.gz → 0.2.3__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.3}/PKG-INFO +1 -1
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/_version.py +1 -1
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/adapters/sqlite_reader.py +15 -3
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/adapters/zotero_api.py +7 -1
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/commands/collection.py +3 -3
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/services/collections.py +20 -10
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_collections.py +61 -7
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_library.py +22 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_zotero_api.py +18 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/.gitattributes +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/.github/workflows/ci.yml +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/.gitignore +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/CONTRIBUTING.md +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/LICENSE +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/README.md +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/docs/ARCHITECTURE.md +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/pyproject.toml +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/skills/zop/README.md +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/skills/zop/SKILL.md +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/__init__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/__main__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/adapters/__init__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/cli.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/commands/__init__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/commands/export.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/commands/item.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/commands/library.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/commands/note.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/commands/pdf.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/commands/tag.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/core/__init__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/core/concurrency.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/core/config.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/core/envelope.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/core/errors.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/models/__init__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/models/collection.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/models/common.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/models/envelope.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/models/item.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/services/__init__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/services/export.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/services/items.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/services/library.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/services/notes.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/services/pdf.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/src/zop/services/tags.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/__init__.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/conftest.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/fixtures/test_plan.json +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_cli_collection.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_cli_export.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_cli_item.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_cli_library.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_cli_main.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_cli_note.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_cli_plan.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_cli_tag.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_config.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_envelope.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_export.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_items_service.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_notes_service.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_pdf.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tests/test_tags_service.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tools/inspect_sqlite.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/tools/inspect_tables.py +0 -0
- {zop_cli-0.2.1 → zop_cli-0.2.3}/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.3
|
|
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
|
|
@@ -412,7 +412,7 @@ class SqliteReader:
|
|
|
412
412
|
with self._connect() as con:
|
|
413
413
|
rows = con.execute(
|
|
414
414
|
"""
|
|
415
|
-
SELECT i.key, n.note, i.dateAdded, i.dateModified
|
|
415
|
+
SELECT i.key, n.note, i.dateAdded, i.dateModified, parent.key
|
|
416
416
|
FROM itemNotes n
|
|
417
417
|
JOIN items i ON i.itemID = n.itemID
|
|
418
418
|
JOIN items parent ON parent.itemID = n.parentItemID
|
|
@@ -421,8 +421,20 @@ class SqliteReader:
|
|
|
421
421
|
""",
|
|
422
422
|
(item_key, library_id),
|
|
423
423
|
).fetchall()
|
|
424
|
-
|
|
425
|
-
|
|
424
|
+
# parent_key: the parent item's key. The JOIN to `parent` already existed
|
|
425
|
+
# but the SELECT never read parent.key, so callers couldn't attribute a
|
|
426
|
+
# note to its item (#2).
|
|
427
|
+
return [
|
|
428
|
+
{
|
|
429
|
+
"key": r[0],
|
|
430
|
+
"note": r[1] or "",
|
|
431
|
+
"date_added": str(r[2]) if r[2] else "",
|
|
432
|
+
"date_modified": str(r[3]) if r[3] else "",
|
|
433
|
+
"parent_key": r[4],
|
|
434
|
+
}
|
|
435
|
+
for r in rows
|
|
436
|
+
if r[0]
|
|
437
|
+
]
|
|
426
438
|
|
|
427
439
|
def list_all_tags(self, library_id: int = 1) -> list[dict[str, int | str]]:
|
|
428
440
|
with self._connect() as con:
|
|
@@ -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."""
|
|
@@ -106,13 +106,13 @@ def delete_cmd(ctx: click.Context, key: str, yes: bool) -> None:
|
|
|
106
106
|
|
|
107
107
|
@collection.command("reparent")
|
|
108
108
|
@click.argument("key")
|
|
109
|
-
@click.option("--parent", "
|
|
109
|
+
@click.option("--parent", "parent_ref", default=None, help="New parent KEY or NAME (omit for top-level).")
|
|
110
110
|
@click.pass_context
|
|
111
|
-
def reparent_cmd(ctx: click.Context, key: str,
|
|
111
|
+
def reparent_cmd(ctx: click.Context, key: str, parent_ref: str | None) -> None:
|
|
112
112
|
"""Move a collection under a new parent (or to top-level). Requires API key."""
|
|
113
113
|
try:
|
|
114
114
|
svc = _service(ctx)
|
|
115
|
-
result = asyncio.run(svc.reparent(key,
|
|
115
|
+
result = asyncio.run(svc.reparent(key, parent_ref))
|
|
116
116
|
emit([result.model_dump()], human=_human(), count=1)
|
|
117
117
|
except ZopError as e:
|
|
118
118
|
emit_error(e, human=_human())
|
|
@@ -294,23 +294,33 @@ class CollectionsService:
|
|
|
294
294
|
async def reparent(
|
|
295
295
|
self, key: str, new_parent: str | None, *, version: int | None = None
|
|
296
296
|
) -> Collection:
|
|
297
|
-
"""Move collection under new parent (NAME), or detach if
|
|
297
|
+
"""Move collection under a new parent (KEY or NAME), or detach if None.
|
|
298
298
|
|
|
299
|
+
``new_parent`` may be a collection KEY (8-char) or NAME, resolved the same
|
|
300
|
+
way as ``create``'s ``--parent`` (KEY first, then local name, then the
|
|
301
|
+
Web API for a parent not yet in local SQLite). This matches SKILL.md's
|
|
302
|
+
``--parent "KeyOrName"`` contract shared by all collection write commands.
|
|
303
|
+
"""
|
|
299
304
|
api = self._require_api()
|
|
300
|
-
if new_parent is None:
|
|
301
|
-
parent_key: str | None | object = False # detach
|
|
302
|
-
else:
|
|
303
|
-
parent_key = self.resolve(new_parent).key
|
|
304
305
|
async with api:
|
|
306
|
+
if new_parent is None:
|
|
307
|
+
parent_key: str | None | object = False # detach to top-level
|
|
308
|
+
else:
|
|
309
|
+
parent_key = await self._resolve_parent(api, new_parent)
|
|
305
310
|
if version is None:
|
|
306
311
|
current = await api.get_collection(key)
|
|
307
312
|
version = current["version"]
|
|
308
|
-
|
|
313
|
+
# Zotero PATCH /collections/{key} returns 204 No Content (no body),
|
|
314
|
+
# so the patch response carries no fields to read. Re-read the
|
|
315
|
+
# collection to return its true updated state (new version, name,
|
|
316
|
+
# parent) — subscripting the patch response crashes (BUG-11).
|
|
317
|
+
await api.update_collection(key, parent_key=parent_key, version=version)
|
|
318
|
+
updated = await api.get_collection(key)
|
|
309
319
|
return Collection(
|
|
310
|
-
key=
|
|
311
|
-
name=
|
|
312
|
-
parent_key=
|
|
313
|
-
version=
|
|
320
|
+
key=updated["key"],
|
|
321
|
+
name=updated["data"]["name"],
|
|
322
|
+
parent_key=updated["data"].get("parentCollection") or None,
|
|
323
|
+
version=updated["version"],
|
|
314
324
|
)
|
|
315
325
|
|
|
316
326
|
async def move_items(
|
|
@@ -222,23 +222,77 @@ 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
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
async def test_reparent_accepts_parent_key_directly(
|
|
272
|
+
fake_db: Path,
|
|
273
|
+
creds: ApiCreds,
|
|
274
|
+
fake_api: AsyncMock,
|
|
275
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
276
|
+
) -> None:
|
|
277
|
+
"""#1: reparent --parent must accept an 8-char KEY (like create), not just a NAME.
|
|
278
|
+
Passing a KEY short-circuits name lookup — no list_collections scan. Regression
|
|
279
|
+
for ``reparent <KEY> --parent <父KEY>`` failing with ``No collection named '<KEY>'``.
|
|
280
|
+
"""
|
|
281
|
+
fake_api.update_collection.return_value = {} # 204 No Content
|
|
282
|
+
fake_api.get_collection.return_value = {
|
|
283
|
+
"key": "KEY00001",
|
|
284
|
+
"version": 6,
|
|
285
|
+
"data": {"name": "child", "parentCollection": "PARENT01"},
|
|
286
|
+
}
|
|
287
|
+
monkeypatch.setattr(CollectionsService, "_require_api", lambda self: fake_api)
|
|
288
|
+
svc = CollectionsService(db_path=fake_db, creds=creds)
|
|
289
|
+
|
|
290
|
+
await svc.reparent("KEY00001", "PARENT01") # an 8-char KEY, not a NAME
|
|
291
|
+
|
|
292
|
+
# KEY path must not scan collection names via the API
|
|
293
|
+
fake_api.list_collections.assert_not_awaited()
|
|
294
|
+
# update_collection must receive the raw KEY as parent_key
|
|
295
|
+
assert fake_api.update_collection.call_args.kwargs["parent_key"] == "PARENT01"
|
|
242
296
|
|
|
243
297
|
|
|
244
298
|
# ---- create: --parent accepts KEY + API fallback (BUG-7) ----
|
|
@@ -45,6 +45,7 @@ def fake_db(tmp_path: Path) -> Iterator[Path]:
|
|
|
45
45
|
CREATE TABLE fields (fieldID INTEGER PRIMARY KEY, fieldName TEXT);
|
|
46
46
|
CREATE TABLE itemCreators (itemID INT, creatorID INT, orderIndex INT);
|
|
47
47
|
CREATE TABLE creators (creatorID INTEGER PRIMARY KEY, firstName TEXT, lastName TEXT);
|
|
48
|
+
CREATE TABLE itemNotes (itemID INT, parentItemID INT, note TEXT);
|
|
48
49
|
|
|
49
50
|
-- Deliberately non-standard itemTypeID mapping (exposes BUG-1).
|
|
50
51
|
INSERT INTO itemTypes VALUES
|
|
@@ -73,6 +74,11 @@ def fake_db(tmp_path: Path) -> Iterator[Path]:
|
|
|
73
74
|
INSERT INTO tags VALUES (1, 'cs.AI'), (2, 'to-read');
|
|
74
75
|
INSERT INTO collections VALUES (1, 1, 'COLL0001');
|
|
75
76
|
|
|
77
|
+
-- A child note item (itemID 10) attached to JOURN001 (itemID 1) — for #2.
|
|
78
|
+
INSERT INTO items (itemID, itemTypeID, dateAdded, dateModified, libraryID, key) VALUES
|
|
79
|
+
(10, 28, datetime('now','-1 days'), datetime('now'), 1, 'NOTEX001');
|
|
80
|
+
INSERT INTO itemNotes VALUES (10, 1, 'A note on Paper One');
|
|
81
|
+
|
|
76
82
|
-- Titles (for duplicates by title + recent display).
|
|
77
83
|
INSERT INTO itemDataValues VALUES (10, 'Paper One');
|
|
78
84
|
INSERT INTO itemData VALUES (1, 1, 10);
|
|
@@ -156,3 +162,19 @@ def test_get_item_populates_year(fake_db: Path) -> None:
|
|
|
156
162
|
reader = SqliteReader(fake_db)
|
|
157
163
|
item = reader.get_item("JOURN001")
|
|
158
164
|
assert item.year == 2024
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_get_item_notes_includes_parent_key(fake_db: Path) -> None:
|
|
168
|
+
"""#2: note list must surface parent_key so callers can attribute a note to
|
|
169
|
+
its item. The JOIN to the parent item already exists; the SELECT must include
|
|
170
|
+
parent.key and the returned dict must carry it (not be silently absent).
|
|
171
|
+
"""
|
|
172
|
+
reader = SqliteReader(fake_db)
|
|
173
|
+
notes = reader.get_item_notes("JOURN001")
|
|
174
|
+
assert len(notes) == 1
|
|
175
|
+
note = notes[0]
|
|
176
|
+
assert note["key"] == "NOTEX001"
|
|
177
|
+
assert note["note"] == "A note on Paper One"
|
|
178
|
+
assert note["parent_key"] == "JOURN001" # the fix
|
|
179
|
+
assert note["date_added"] # non-empty
|
|
180
|
+
assert note["date_modified"]
|
|
@@ -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
|