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 +4 -0
- vizo-0.0.1/PKG-INFO +67 -0
- vizo-0.0.1/README.md +49 -0
- vizo-0.0.1/pyproject.toml +39 -0
- vizo-0.0.1/setup.cfg +4 -0
- vizo-0.0.1/src/vizo/__init__.py +8 -0
- vizo-0.0.1/src/vizo/__main__.py +5 -0
- vizo-0.0.1/src/vizo/api.py +221 -0
- vizo-0.0.1/src/vizo/cli.py +387 -0
- vizo-0.0.1/src/vizo/datasets/__init__.py +28 -0
- vizo-0.0.1/src/vizo/datasets/common.py +60 -0
- vizo-0.0.1/src/vizo/datasets/ego4d.py +214 -0
- vizo-0.0.1/src/vizo/datasets/egoexo4d.py +409 -0
- vizo-0.0.1/src/vizo/download.py +440 -0
- vizo-0.0.1/src/vizo/filters.py +212 -0
- vizo-0.0.1/src/vizo/manifest.py +293 -0
- vizo-0.0.1/src/vizo/state.py +137 -0
- vizo-0.0.1/src/vizo/verify.py +58 -0
- vizo-0.0.1/src/vizo.egg-info/PKG-INFO +67 -0
- vizo-0.0.1/src/vizo.egg-info/SOURCES.txt +22 -0
- vizo-0.0.1/src/vizo.egg-info/dependency_links.txt +1 -0
- vizo-0.0.1/src/vizo.egg-info/entry_points.txt +2 -0
- vizo-0.0.1/src/vizo.egg-info/requires.txt +3 -0
- vizo-0.0.1/src/vizo.egg-info/top_level.txt +1 -0
vizo-0.0.1/MANIFEST.in
ADDED
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,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,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)
|