proteworks 0.1.2__tar.gz → 0.1.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.
Files changed (21) hide show
  1. {proteworks-0.1.2 → proteworks-0.1.3}/PKG-INFO +1 -1
  2. {proteworks-0.1.2 → proteworks-0.1.3}/pyproject.toml +1 -1
  3. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks/api.py +8 -4
  4. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks/pull.py +11 -2
  5. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks/push.py +4 -1
  6. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks.egg-info/PKG-INFO +1 -1
  7. {proteworks-0.1.2 → proteworks-0.1.3}/tests/test_cli.py +39 -7
  8. {proteworks-0.1.2 → proteworks-0.1.3}/README.md +0 -0
  9. {proteworks-0.1.2 → proteworks-0.1.3}/setup.cfg +0 -0
  10. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks/__init__.py +0 -0
  11. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks/auth.py +0 -0
  12. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks/cli.py +0 -0
  13. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks/config.py +0 -0
  14. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks/index.py +0 -0
  15. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks/orgs.py +0 -0
  16. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks/redact.py +0 -0
  17. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks.egg-info/SOURCES.txt +0 -0
  18. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks.egg-info/dependency_links.txt +0 -0
  19. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks.egg-info/entry_points.txt +0 -0
  20. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks.egg-info/requires.txt +0 -0
  21. {proteworks-0.1.2 → proteworks-0.1.3}/src/proteworks.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proteworks
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Sync files with Proteworks using file-browser paths.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "proteworks"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "Sync files with Proteworks using file-browser paths."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -88,19 +88,21 @@ class ApiClient:
88
88
  params={"path": path, "offset": offset, "limit": limit},
89
89
  )
90
90
 
91
- def manifest_all(self, path: str) -> dict:
91
+ def manifest_all(self, path: str, *, on_list_progress=None, page_size: int = 500) -> dict:
92
92
  """Follow pages. Returns items plus root_folder_id from the first page."""
93
93
  offset = 0
94
94
  items: list[dict] = []
95
95
  root_folder_id = None
96
96
  total = None
97
97
  while True:
98
- page = self.manifest(path, offset)
98
+ page = self.manifest(path, offset, limit=page_size)
99
99
  root_folder_id = root_folder_id or page.get("root_folder_id")
100
100
  batch = list(page.get("items") or [])
101
101
  items.extend(batch)
102
102
  total = int(page.get("total") or 0)
103
103
  offset += len(batch)
104
+ if on_list_progress is not None:
105
+ on_list_progress(offset, total)
104
106
  if not batch or offset >= total:
105
107
  break
106
108
  return {"items": items, "total": total or 0, "root_folder_id": root_folder_id}
@@ -112,17 +114,19 @@ class ApiClient:
112
114
  params={"offset": offset, "limit": limit},
113
115
  )
114
116
 
115
- def export_all(self) -> dict:
117
+ def export_all(self, *, on_list_progress=None, page_size: int = 500) -> dict:
116
118
  """Follow export pages. Returns every item for the active organization."""
117
119
  offset = 0
118
120
  items: list[dict] = []
119
121
  total = None
120
122
  while True:
121
- page = self.export_page(offset)
123
+ page = self.export_page(offset, limit=page_size)
122
124
  batch = list(page.get("items") or [])
123
125
  items.extend(batch)
124
126
  total = int(page.get("total") or 0)
125
127
  offset += len(batch)
128
+ if on_list_progress is not None:
129
+ on_list_progress(offset, total)
126
130
  if not batch or offset >= total:
127
131
  break
128
132
  return {"items": items, "total": total or 0}
@@ -20,10 +20,19 @@ from proteworks.index import (
20
20
  _CONTENT_URL_BATCH = 100
21
21
 
22
22
 
23
+ def _list_progress(stderr):
24
+ """Print how many paths the API has returned so far."""
25
+
26
+ def report(listed: int, total: int) -> None:
27
+ print(f"Listed {listed}/{total}", file=stderr)
28
+
29
+ return report
30
+
31
+
23
32
  def pull_path(api: ApiClient, remote: str, dest: Path, *, dry_run: bool, stderr) -> int:
24
33
  """Download readable files for a file-browser path. Returns a process status code."""
25
34
  try:
26
- page = api.manifest_all(remote)
35
+ page = api.manifest_all(remote, on_list_progress=_list_progress(stderr))
27
36
  except ApiError as exc:
28
37
  print(str(exc), file=stderr)
29
38
  return 1
@@ -35,7 +44,7 @@ def pull_path(api: ApiClient, remote: str, dest: Path, *, dry_run: bool, stderr)
35
44
  def export_organization(api: ApiClient, dest: Path, *, dry_run: bool, stderr) -> int:
36
45
  """Download every live file in the active organization. Returns a process status code."""
37
46
  try:
38
- page = api.export_all()
47
+ page = api.export_all(on_list_progress=_list_progress(stderr))
39
48
  except ApiError as exc:
40
49
  print(str(exc), file=stderr)
41
50
  return 1
@@ -20,7 +20,10 @@ def push_directory(api: ApiClient, local_dir: Path, remote: str, *, dry_run: boo
20
20
  print(str(exc), file=stderr)
21
21
  return 2
22
22
  try:
23
- page = api.manifest_all(remote)
23
+ page = api.manifest_all(
24
+ remote,
25
+ on_list_progress=lambda listed, total: print(f"Listed {listed}/{total}", file=stderr),
26
+ )
24
27
  except ApiError as exc:
25
28
  print(str(exc), file=stderr)
26
29
  return 1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proteworks
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Sync files with Proteworks using file-browser paths.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -13,6 +13,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
13
13
  from pathlib import Path
14
14
  from urllib.parse import parse_qs, urlparse
15
15
 
16
+ from proteworks.api import ApiClient
16
17
  from proteworks.auth import loopback_redirect_uri, open_browser
17
18
  from proteworks.cli import main
18
19
  from proteworks.config import Settings
@@ -91,13 +92,16 @@ class FakeApi(threading.Thread):
91
92
  return
92
93
  if parsed.path == "/api/v1/sync/manifest":
93
94
  query = parse_qs(parsed.query)
95
+ offset = int((query.get("offset") or ["0"])[0])
96
+ limit = int((query.get("limit") or ["500"])[0])
97
+ page = parent.items[offset : offset + limit]
94
98
  self._send(
95
99
  200,
96
100
  {
97
- "items": parent.items,
101
+ "items": page,
98
102
  "total": len(parent.items),
99
- "limit": 500,
100
- "offset": 0,
103
+ "limit": limit,
104
+ "offset": offset,
101
105
  "root_folder_id": parent.root_folder_id,
102
106
  "cloud_storage_url": GS_URL,
103
107
  },
@@ -110,14 +114,18 @@ class FakeApi(threading.Thread):
110
114
  status, payload = parent.export_error
111
115
  self._send(status, payload)
112
116
  return
117
+ query = parse_qs(parsed.query)
118
+ offset = int((query.get("offset") or ["0"])[0])
119
+ limit = int((query.get("limit") or ["500"])[0])
113
120
  exported = parent.export_items or []
121
+ page = exported[offset : offset + limit]
114
122
  self._send(
115
123
  200,
116
124
  {
117
- "items": exported,
125
+ "items": page,
118
126
  "total": len(exported),
119
- "limit": 500,
120
- "offset": 0,
127
+ "limit": limit,
128
+ "offset": offset,
121
129
  "cloud_storage_url": GS_URL,
122
130
  },
123
131
  )
@@ -333,7 +341,29 @@ def test_pull_with_two_regular_orgs_stops_before_manifest(tmp_path, capsys):
333
341
  assert not any(path == "/api/v1/sync/manifest" for _method, path, _body in server.requests)
334
342
 
335
343
 
336
- def test_pull_mints_download_urls_in_one_batch(tmp_path):
344
+ def test_manifest_all_reports_listing_progress():
345
+ items = []
346
+ for index in range(5):
347
+ item = _item(f"~/My Files/msa/{index}.cif")
348
+ item["file_id"] = f"f{index}"
349
+ items.append(item)
350
+ server = FakeApi([_org("reg", "Research")], items).start()
351
+ progress = []
352
+ try:
353
+ api = ApiClient(server.url, "token", organization_id="reg")
354
+ page = api.manifest_all(
355
+ "~/My Files/msa/",
356
+ on_list_progress=lambda listed, total: progress.append((listed, total)),
357
+ page_size=2,
358
+ )
359
+ finally:
360
+ api.close()
361
+ server.stop()
362
+ assert len(page["items"]) == 5
363
+ assert progress == [(2, 5), (4, 5), (5, 5)]
364
+
365
+
366
+ def test_pull_mints_download_urls_in_one_batch(tmp_path, capsys):
337
367
  items = []
338
368
  for name in ("a.cif", "b.cif", "c.cif"):
339
369
  item = _item(f"~/My Files/msa/{name}")
@@ -355,7 +385,9 @@ def test_pull_mints_download_urls_in_one_batch(tmp_path):
355
385
  for method, path, body in server.requests
356
386
  if method == "POST" and path == "/api/v1/files/content-urls"
357
387
  ]
388
+ err = capsys.readouterr().err
358
389
  assert code == 0
390
+ assert "Listed 3/3" in err
359
391
  assert len(posts) == 1
360
392
  assert set(posts[0]["file_ids"]) == {"a.cif", "b.cif", "c.cif"}
361
393
  assert posts[0]["purpose"] == "download"
File without changes
File without changes