vizo 0.0.1__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.
vizo-0.0.1/MANIFEST.in ADDED
@@ -0,0 +1,4 @@
1
+ # The source archive on PyPI is public: only the client goes in it. The staff
2
+ # tools and the test fixtures (consortium metadata) stay in the repository.
3
+ prune tests
4
+ prune tools
vizo-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: vizo
3
+ Version: 0.0.1
4
+ Summary: VIZO command-line tool: download licensed datasets from the VIZO platform
5
+ Author: VIZO
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Homepage, https://vizo-platform.vercel.app
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Topic :: Scientific/Engineering
14
+ Requires-Python: >=3.9
15
+ Description-Content-Type: text/markdown
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=7; extra == "dev"
18
+
19
+ # VIZO CLI
20
+
21
+ `vizo` is the command-line tool for downloading the datasets you are licensed
22
+ for on the VIZO platform. It needs Python 3.9 or newer and nothing beyond the
23
+ standard library.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ python3 -m venv ~/vizo-venv # Python 3.9 or newer; the one macOS ships is fine
29
+ source ~/vizo-venv/bin/activate
30
+ pip3 install vizo
31
+ ```
32
+
33
+ ## Use
34
+
35
+ ```bash
36
+ export VIZO_API_KEY=vz_sk_live_... # from the API Keys page; the only way to authenticate
37
+
38
+ vizo datasets # what you can download, and where to request access
39
+ vizo download ego4d -o ~/vizo-data/ego4d --parts clips annotations --benchmarks FHO --dry-run
40
+ vizo download ego4d -o ~/vizo-data/ego4d --parts clips annotations --benchmarks FHO -y
41
+ vizo verify ~/vizo-data/ego4d --checksum
42
+ ```
43
+
44
+ `python3 -m vizo` works the same as `vizo`.
45
+
46
+ `download` accepts the official selectors (`--parts`/`--datasets`, `--uids`/
47
+ `--video_uids`, `--uid-file`, `--universities`, `--benchmarks`, `--splits`,
48
+ `--views`, `--metadata`), plus `--shard N/M` for splitting one download across
49
+ machines, `--workers`, `--dry-run`, `-y`, and `--refresh-manifest`. A dry run
50
+ fetches nothing but does create the output folder and save the manifest in it,
51
+ so the real run that follows targets the same release. Files land
52
+ in the official layout under `-o`; `.vizo/` inside it holds the saved manifest,
53
+ a log and a done-record per run. Rerunning the same command skips completed
54
+ files; a file that was interrupted starts again from its beginning. Interrupted
55
+ or failed downloads leave `*.part` files, which a run and `verify` report and
56
+ never delete (shards may share a folder); delete them when no run is using the
57
+ folder.
58
+
59
+ Exit codes: 0 done · 1 usage · 2 key problem · 3 not entitled ·
60
+ 4 incomplete/unreachable.
61
+
62
+ The full guide is on the VIZO platform, signed in, under Develop → CLI
63
+ (https://vizo-platform.vercel.app/docs/cli): installation, API keys, choosing
64
+ what to download, each dataset's parts and layout, verifying, large and sharded
65
+ downloads, a command reference and troubleshooting. Its source is `docs/` in
66
+ the source repository; working on the CLI itself is covered in `DEVELOPMENT.md`
67
+ there.
vizo-0.0.1/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # VIZO CLI
2
+
3
+ `vizo` is the command-line tool for downloading the datasets you are licensed
4
+ for on the VIZO platform. It needs Python 3.9 or newer and nothing beyond the
5
+ standard library.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ python3 -m venv ~/vizo-venv # Python 3.9 or newer; the one macOS ships is fine
11
+ source ~/vizo-venv/bin/activate
12
+ pip3 install vizo
13
+ ```
14
+
15
+ ## Use
16
+
17
+ ```bash
18
+ export VIZO_API_KEY=vz_sk_live_... # from the API Keys page; the only way to authenticate
19
+
20
+ vizo datasets # what you can download, and where to request access
21
+ vizo download ego4d -o ~/vizo-data/ego4d --parts clips annotations --benchmarks FHO --dry-run
22
+ vizo download ego4d -o ~/vizo-data/ego4d --parts clips annotations --benchmarks FHO -y
23
+ vizo verify ~/vizo-data/ego4d --checksum
24
+ ```
25
+
26
+ `python3 -m vizo` works the same as `vizo`.
27
+
28
+ `download` accepts the official selectors (`--parts`/`--datasets`, `--uids`/
29
+ `--video_uids`, `--uid-file`, `--universities`, `--benchmarks`, `--splits`,
30
+ `--views`, `--metadata`), plus `--shard N/M` for splitting one download across
31
+ machines, `--workers`, `--dry-run`, `-y`, and `--refresh-manifest`. A dry run
32
+ fetches nothing but does create the output folder and save the manifest in it,
33
+ so the real run that follows targets the same release. Files land
34
+ in the official layout under `-o`; `.vizo/` inside it holds the saved manifest,
35
+ a log and a done-record per run. Rerunning the same command skips completed
36
+ files; a file that was interrupted starts again from its beginning. Interrupted
37
+ or failed downloads leave `*.part` files, which a run and `verify` report and
38
+ never delete (shards may share a folder); delete them when no run is using the
39
+ folder.
40
+
41
+ Exit codes: 0 done · 1 usage · 2 key problem · 3 not entitled ·
42
+ 4 incomplete/unreachable.
43
+
44
+ The full guide is on the VIZO platform, signed in, under Develop → CLI
45
+ (https://vizo-platform.vercel.app/docs/cli): installation, API keys, choosing
46
+ what to download, each dataset's parts and layout, verifying, large and sharded
47
+ downloads, a command reference and troubleshooting. Its source is `docs/` in
48
+ the source repository; working on the CLI itself is covered in `DEVELOPMENT.md`
49
+ there.
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vizo"
7
+ dynamic = ["version"]
8
+ description = "VIZO command-line tool: download licensed datasets from the VIZO platform"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "LicenseRef-Proprietary"
12
+ authors = [{ name = "VIZO" }]
13
+ dependencies = []
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Science/Research",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Topic :: Scientific/Engineering",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://vizo-platform.vercel.app"
25
+
26
+ [project.scripts]
27
+ vizo = "vizo.cli:main"
28
+
29
+ [project.optional-dependencies]
30
+ dev = ["pytest>=7"]
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
34
+
35
+ [tool.setuptools.dynamic]
36
+ version = { attr = "vizo.__version__" }
37
+
38
+ [tool.pytest.ini_options]
39
+ testpaths = ["tests"]
vizo-0.0.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,8 @@
1
+ """VIZO command-line tool and, later, Python SDK.
2
+
3
+ The `vizo` command (datasets, download, verify) is in cli.py; the manifest
4
+ format it reads is in manifest.py. "design §N" in comments refers to
5
+ roadmap_v2/cli_design.md in the vizo-platform repository.
6
+ """
7
+
8
+ __version__ = "0.0.1"
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,221 @@
1
+ """HTTP client for the platform's /api/v1 (design §2-§4).
2
+
3
+ Authenticates with the API key from VIZO_API_KEY, sends the CLI version and the
4
+ run id with every request, and turns refusals into ApiError with the code and
5
+ message the platform returned. Standard library only, so the CLI installs
6
+ anywhere Python does.
7
+ """
8
+
9
+ import json
10
+ import os
11
+ import re
12
+ import socket
13
+ import time
14
+ import urllib.error
15
+ import urllib.parse
16
+ import urllib.request
17
+ from dataclasses import dataclass
18
+ from typing import Any, Dict, List, Optional, Tuple
19
+
20
+ from . import __version__
21
+
22
+ DEFAULT_API_URL = "https://vizo-platform.vercel.app"
23
+ ENV_API_KEY = "VIZO_API_KEY"
24
+ ENV_API_URL = "VIZO_API_URL"
25
+ KEY_PREFIX = "vz_sk_live_"
26
+ # The platform issues the prefix plus base64url characters. Anything else (a
27
+ # line break from `$(cat keyfile)`, a space) must stop here: the HTTP layer
28
+ # would refuse the header with an error that spells the key out.
29
+ KEY_PATTERN = re.compile(re.escape(KEY_PREFIX) + r"[A-Za-z0-9_-]+")
30
+ LOCAL_HOSTS = ("localhost", "127.0.0.1", "::1")
31
+
32
+ RETRIES = 4
33
+ RETRY_STATUSES = {502, 503, 504}
34
+
35
+
36
+ class ApiError(Exception):
37
+ """A refusal or failure from the platform, with the code it gave."""
38
+
39
+ def __init__(self, status: int, code: str, message: str, extra: Optional[Dict[str, Any]] = None):
40
+ super().__init__(message)
41
+ self.status = status
42
+ self.code = code
43
+ self.message = message
44
+ self.extra = extra or {}
45
+
46
+ def __str__(self) -> str:
47
+ hint = ""
48
+ if self.extra.get("license_url"):
49
+ hint = f"\nRequest a license at {self.extra['license_url']}"
50
+ elif self.extra.get("api_keys_url"):
51
+ hint = f"\nManage keys at {self.extra['api_keys_url']}"
52
+ return f"{self.message} [{self.code}]{hint}"
53
+
54
+
55
+ @dataclass
56
+ class Link:
57
+ file_id: str
58
+ url: str
59
+ expires_at: str
60
+
61
+
62
+ class _NoRedirect(urllib.request.HTTPRedirectHandler):
63
+ def redirect_request(self, req, fp, code, msg, headers, newurl): # type: ignore[override]
64
+ return None
65
+
66
+
67
+ def api_key_from_env(api_url: Optional[str] = None) -> str:
68
+ key = os.environ.get(ENV_API_KEY, "").strip()
69
+ if not key:
70
+ raise ApiError(0, "missing_key", f"Set {ENV_API_KEY} to your API key.",
71
+ {"api_keys_url": resolve_api_url(api_url) + "/api-keys"})
72
+ if not key.startswith(KEY_PREFIX):
73
+ raise ApiError(0, "invalid_key", f"{ENV_API_KEY} does not look like a VIZO key (expected {KEY_PREFIX}...).")
74
+ if not KEY_PATTERN.fullmatch(key):
75
+ raise ApiError(0, "invalid_key", f"{ENV_API_KEY} has characters a VIZO key never contains "
76
+ "(a space or a line break inside it?); set it to the key alone.")
77
+ return key
78
+
79
+
80
+ def resolve_api_url(api_url: Optional[str] = None) -> str:
81
+ url = (api_url or os.environ.get(ENV_API_URL) or DEFAULT_API_URL).rstrip("/")
82
+ u = urllib.parse.urlparse(url)
83
+ if u.scheme != "https" and not (u.scheme == "http" and u.hostname in LOCAL_HOSTS):
84
+ raise ValueError(f"the API URL {url} must use https (plain http only for localhost), "
85
+ "or your API key would travel in cleartext")
86
+ return url
87
+
88
+
89
+ class PlatformClient:
90
+ def __init__(self, api_key: str, run_id: str, api_url: Optional[str] = None, timeout: int = 60):
91
+ self.api_key = api_key
92
+ self.run_id = run_id
93
+ self.api_url = resolve_api_url(api_url)
94
+ self.timeout = timeout
95
+ self.key_expires_at: Optional[str] = None
96
+ # Redirects are handled by hand: the manifest endpoint may answer 302 to
97
+ # a presigned R2 URL, which must be fetched WITHOUT our bearer header.
98
+ self._opener = urllib.request.build_opener(_NoRedirect)
99
+
100
+ # -- plumbing ---------------------------------------------------------
101
+
102
+ def _headers(self, extra: Optional[Dict[str, str]] = None) -> Dict[str, str]:
103
+ h = {
104
+ "Authorization": f"Bearer {self.api_key}",
105
+ "X-Vizo-CLI-Version": __version__,
106
+ "X-Vizo-Run-Id": self.run_id,
107
+ "User-Agent": f"vizo-cli/{__version__}",
108
+ "Accept": "application/json, text/csv",
109
+ }
110
+ if extra:
111
+ h.update(extra)
112
+ return h
113
+
114
+ def _request(self, method: str, path: str, body: Optional[Dict[str, Any]] = None) -> Tuple[int, Dict[str, str], bytes]:
115
+ data = json.dumps(body).encode() if body is not None else None
116
+ headers = self._headers({"Content-Type": "application/json"} if data else None)
117
+ return self._open(urllib.request.Request(self.api_url + path, data=data, method=method, headers=headers),
118
+ self.api_url)
119
+
120
+ def _open(self, req: urllib.request.Request, where: str) -> Tuple[int, Dict[str, str], bytes]:
121
+ last_error: Optional[Exception] = None
122
+ for attempt in range(RETRIES):
123
+ try:
124
+ with self._opener.open(req, timeout=self.timeout) as resp:
125
+ return resp.status, dict(resp.headers), resp.read()
126
+ except urllib.error.HTTPError as e:
127
+ payload = e.read()
128
+ if self._retryable(e.code, payload) and attempt < RETRIES - 1:
129
+ last_error = e
130
+ time.sleep(2 ** attempt)
131
+ continue
132
+ if 300 <= e.code < 400:
133
+ return e.code, dict(e.headers), payload
134
+ err = self._api_error(e.code, payload)
135
+ if e.headers.get("Retry-After"):
136
+ err.extra["retry_after"] = e.headers["Retry-After"]
137
+ raise err
138
+ except (urllib.error.URLError, socket.timeout, ConnectionError) as e:
139
+ last_error = e
140
+ if attempt < RETRIES - 1:
141
+ time.sleep(2 ** attempt)
142
+ continue
143
+ raise ApiError(0, "unreachable", f"Could not reach {where}: {last_error}")
144
+
145
+ @classmethod
146
+ def _retryable(cls, status: int, payload: bytes) -> bool:
147
+ if status == 503:
148
+ # the platform marks a transient 503 `unavailable`; one without a JSON error comes from a proxy
149
+ return cls._api_error(status, payload).code in ("unavailable", f"http_{status}")
150
+ return status in RETRY_STATUSES
151
+
152
+ @staticmethod
153
+ def _api_error(status: int, payload: bytes) -> ApiError:
154
+ try:
155
+ err = json.loads(payload.decode("utf-8"))["error"]
156
+ code, message = err.pop("code"), err.pop("message")
157
+ return ApiError(status, code, message, err)
158
+ except (ValueError, KeyError, TypeError):
159
+ return ApiError(status, f"http_{status}", payload.decode("utf-8", "replace")[:300] or f"HTTP {status}")
160
+
161
+ def _note_expiry(self, headers: Dict[str, str]) -> None:
162
+ exp = headers.get("X-Vizo-Key-Expires-At") or headers.get("x-vizo-key-expires-at")
163
+ if exp:
164
+ self.key_expires_at = exp
165
+
166
+ # -- endpoints --------------------------------------------------------
167
+
168
+ @staticmethod
169
+ def _unexpected(status: int, path: str, e: Exception) -> ApiError:
170
+ return ApiError(status, "unexpected_response", f"The platform sent an unexpected response to {path} ({e!r}).")
171
+
172
+ def datasets(self) -> List[Dict[str, Any]]:
173
+ path = "/api/v1/datasets"
174
+ status, headers, body = self._request("GET", path)
175
+ self._note_expiry(headers)
176
+ try:
177
+ data = json.loads(body)
178
+ datasets = data["datasets"]
179
+ if not all(isinstance(d, dict) and "id" in d for d in datasets):
180
+ raise TypeError("datasets entries must be objects with an id")
181
+ self.key_expires_at = data.get("key_expires_at") or self.key_expires_at
182
+ except (ValueError, KeyError, TypeError, AttributeError) as e:
183
+ raise self._unexpected(status, path, e)
184
+ return datasets
185
+
186
+ def manifest(self, dataset: str) -> bytes:
187
+ """The current release's manifest as bytes (gzip or plain CSV; the reader detects which)."""
188
+ path = f"/api/v1/datasets/{urllib.parse.quote(dataset)}/manifest"
189
+ try:
190
+ status, headers, body = self._request("GET", path)
191
+ except ApiError as e:
192
+ if e.code == "manifest_not_published":
193
+ raise ApiError(e.status, e.code, f"The current {dataset} release has not been published yet; "
194
+ "try again once it has.", e.extra)
195
+ raise
196
+ self._note_expiry(headers)
197
+ if status in (301, 302, 303, 307, 308):
198
+ location = headers.get("Location") or headers.get("location")
199
+ if not location:
200
+ raise ApiError(status, "bad_redirect", "The platform redirected without a location.")
201
+ # A presigned URL: no bearer header, or R2 rejects the request.
202
+ req = urllib.request.Request(location, headers={"User-Agent": f"vizo-cli/{__version__}"})
203
+ try:
204
+ status, _, body = self._open(req, "the manifest storage")
205
+ except ApiError as e:
206
+ # status 0: storage being unreachable or refusing says nothing about the key or license
207
+ reason = f"HTTP {e.status}" if e.status else e.message
208
+ raise ApiError(0, "manifest_fetch_failed", f"Fetching the manifest from storage failed: {reason}")
209
+ return body
210
+
211
+ def links(self, dataset: str, file_ids: List[str]) -> Tuple[List[Link], List[str]]:
212
+ path = f"/api/v1/datasets/{urllib.parse.quote(dataset)}/links"
213
+ status, headers, body = self._request("POST", path, {"file_ids": file_ids})
214
+ self._note_expiry(headers)
215
+ try:
216
+ data = json.loads(body)
217
+ links = [Link(l["file_id"], l["url"], l["expires_at"])
218
+ for l in data.get("links", [])]
219
+ return links, list(data.get("unknown_file_ids", []))
220
+ except (ValueError, KeyError, TypeError, AttributeError) as e:
221
+ raise self._unexpected(status, path, e)