proteworks 0.1.1__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.
- {proteworks-0.1.1 → proteworks-0.1.3}/PKG-INFO +1 -1
- {proteworks-0.1.1 → proteworks-0.1.3}/pyproject.toml +1 -1
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks/api.py +8 -4
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks/auth.py +14 -2
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks/pull.py +11 -2
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks/push.py +4 -1
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks.egg-info/PKG-INFO +1 -1
- {proteworks-0.1.1 → proteworks-0.1.3}/tests/test_cli.py +68 -8
- {proteworks-0.1.1 → proteworks-0.1.3}/README.md +0 -0
- {proteworks-0.1.1 → proteworks-0.1.3}/setup.cfg +0 -0
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks/__init__.py +0 -0
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks/cli.py +0 -0
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks/config.py +0 -0
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks/index.py +0 -0
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks/orgs.py +0 -0
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks/redact.py +0 -0
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks.egg-info/SOURCES.txt +0 -0
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks.egg-info/dependency_links.txt +0 -0
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks.egg-info/entry_points.txt +0 -0
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks.egg-info/requires.txt +0 -0
- {proteworks-0.1.1 → proteworks-0.1.3}/src/proteworks.egg-info/top_level.txt +0 -0
|
@@ -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}
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import base64
|
|
6
6
|
import hashlib
|
|
7
|
+
import os
|
|
7
8
|
import secrets
|
|
8
9
|
import webbrowser
|
|
9
10
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
@@ -65,6 +66,18 @@ def loopback_redirect_uri(port: int = LOOPBACK_PORT) -> str:
|
|
|
65
66
|
return f"http://127.0.0.1:{port}{LOOPBACK_REDIRECT_PATH}"
|
|
66
67
|
|
|
67
68
|
|
|
69
|
+
def open_browser(url: str) -> bool:
|
|
70
|
+
"""Open the sign-in page. The browser's own startup errors stay hidden."""
|
|
71
|
+
saved_stderr = os.dup(2)
|
|
72
|
+
try:
|
|
73
|
+
with open(os.devnull, "wb") as discarded:
|
|
74
|
+
os.dup2(discarded.fileno(), 2)
|
|
75
|
+
return bool(webbrowser.open(url))
|
|
76
|
+
finally:
|
|
77
|
+
os.dup2(saved_stderr, 2)
|
|
78
|
+
os.close(saved_stderr)
|
|
79
|
+
|
|
80
|
+
|
|
68
81
|
def login(settings: Settings, token_store: TokenStore, stderr) -> None:
|
|
69
82
|
"""Open a browser, receive the loopback redirect, and save the access token."""
|
|
70
83
|
verifier, challenge = pkce_pair()
|
|
@@ -77,9 +90,8 @@ def login(settings: Settings, token_store: TokenStore, stderr) -> None:
|
|
|
77
90
|
) from exc
|
|
78
91
|
redirect_uri = loopback_redirect_uri()
|
|
79
92
|
url = authorize_url(settings, redirect_uri, state, challenge)
|
|
80
|
-
print(f"Auth0 must allow this callback URL: {redirect_uri}", file=stderr)
|
|
81
93
|
print("Opening a browser to sign in.", file=stderr)
|
|
82
|
-
opened =
|
|
94
|
+
opened = open_browser(url)
|
|
83
95
|
if not opened:
|
|
84
96
|
print("Open this URL to sign in:", file=stderr)
|
|
85
97
|
print(url, file=stderr)
|
|
@@ -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(
|
|
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
|
|
@@ -5,12 +5,16 @@ from __future__ import annotations
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
7
|
import mimetypes
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
8
11
|
import threading
|
|
9
12
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
10
13
|
from pathlib import Path
|
|
11
14
|
from urllib.parse import parse_qs, urlparse
|
|
12
15
|
|
|
13
|
-
from proteworks.
|
|
16
|
+
from proteworks.api import ApiClient
|
|
17
|
+
from proteworks.auth import loopback_redirect_uri, open_browser
|
|
14
18
|
from proteworks.cli import main
|
|
15
19
|
from proteworks.config import Settings
|
|
16
20
|
from proteworks.push import _iter_local_files
|
|
@@ -88,13 +92,16 @@ class FakeApi(threading.Thread):
|
|
|
88
92
|
return
|
|
89
93
|
if parsed.path == "/api/v1/sync/manifest":
|
|
90
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]
|
|
91
98
|
self._send(
|
|
92
99
|
200,
|
|
93
100
|
{
|
|
94
|
-
"items":
|
|
101
|
+
"items": page,
|
|
95
102
|
"total": len(parent.items),
|
|
96
|
-
"limit":
|
|
97
|
-
"offset":
|
|
103
|
+
"limit": limit,
|
|
104
|
+
"offset": offset,
|
|
98
105
|
"root_folder_id": parent.root_folder_id,
|
|
99
106
|
"cloud_storage_url": GS_URL,
|
|
100
107
|
},
|
|
@@ -107,14 +114,18 @@ class FakeApi(threading.Thread):
|
|
|
107
114
|
status, payload = parent.export_error
|
|
108
115
|
self._send(status, payload)
|
|
109
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])
|
|
110
120
|
exported = parent.export_items or []
|
|
121
|
+
page = exported[offset : offset + limit]
|
|
111
122
|
self._send(
|
|
112
123
|
200,
|
|
113
124
|
{
|
|
114
|
-
"items":
|
|
125
|
+
"items": page,
|
|
115
126
|
"total": len(exported),
|
|
116
|
-
"limit":
|
|
117
|
-
"offset":
|
|
127
|
+
"limit": limit,
|
|
128
|
+
"offset": offset,
|
|
118
129
|
"cloud_storage_url": GS_URL,
|
|
119
130
|
},
|
|
120
131
|
)
|
|
@@ -220,6 +231,31 @@ def test_login_callback_url_is_fixed():
|
|
|
220
231
|
assert loopback_redirect_uri() == "http://127.0.0.1:8765/callback"
|
|
221
232
|
|
|
222
233
|
|
|
234
|
+
def test_open_browser_hides_child_stderr(monkeypatch):
|
|
235
|
+
def noisy_open(_url: str) -> bool:
|
|
236
|
+
subprocess.run(
|
|
237
|
+
[sys.executable, "-c", "import os; os.write(2, b'zygote broken pipe\\n')"],
|
|
238
|
+
check=False,
|
|
239
|
+
)
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
monkeypatch.setattr("proteworks.auth.webbrowser.open", noisy_open)
|
|
243
|
+
read_fd, write_fd = os.pipe()
|
|
244
|
+
saved = os.dup(2)
|
|
245
|
+
os.dup2(write_fd, 2)
|
|
246
|
+
os.close(write_fd)
|
|
247
|
+
try:
|
|
248
|
+
assert open_browser("https://example.test/authorize") is True
|
|
249
|
+
os.write(2, b"still open\n")
|
|
250
|
+
finally:
|
|
251
|
+
os.dup2(saved, 2)
|
|
252
|
+
os.close(saved)
|
|
253
|
+
captured = os.read(read_fd, 1024)
|
|
254
|
+
os.close(read_fd)
|
|
255
|
+
assert b"zygote" not in captured
|
|
256
|
+
assert b"still open" in captured
|
|
257
|
+
|
|
258
|
+
|
|
223
259
|
def test_root_help_lists_commands(capsys):
|
|
224
260
|
try:
|
|
225
261
|
main(["--help"])
|
|
@@ -305,7 +341,29 @@ def test_pull_with_two_regular_orgs_stops_before_manifest(tmp_path, capsys):
|
|
|
305
341
|
assert not any(path == "/api/v1/sync/manifest" for _method, path, _body in server.requests)
|
|
306
342
|
|
|
307
343
|
|
|
308
|
-
def
|
|
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):
|
|
309
367
|
items = []
|
|
310
368
|
for name in ("a.cif", "b.cif", "c.cif"):
|
|
311
369
|
item = _item(f"~/My Files/msa/{name}")
|
|
@@ -327,7 +385,9 @@ def test_pull_mints_download_urls_in_one_batch(tmp_path):
|
|
|
327
385
|
for method, path, body in server.requests
|
|
328
386
|
if method == "POST" and path == "/api/v1/files/content-urls"
|
|
329
387
|
]
|
|
388
|
+
err = capsys.readouterr().err
|
|
330
389
|
assert code == 0
|
|
390
|
+
assert "Listed 3/3" in err
|
|
331
391
|
assert len(posts) == 1
|
|
332
392
|
assert set(posts[0]["file_ids"]) == {"a.cif", "b.cif", "c.cif"}
|
|
333
393
|
assert posts[0]["purpose"] == "download"
|
|
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
|