docverse 1.0.0b2__py3-none-any.whl
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.
- docverse/__init__.py +22 -0
- docverse/_annotations.py +63 -0
- docverse/_cli.py +413 -0
- docverse/_client.py +584 -0
- docverse/_exceptions.py +47 -0
- docverse/_tar.py +82 -0
- docverse/models/__init__.py +196 -0
- docverse/models/_examples.py +77 -0
- docverse/models/builds.py +363 -0
- docverse/models/credentials.py +143 -0
- docverse/models/dashboard.py +69 -0
- docverse/models/dashboard_template.py +209 -0
- docverse/models/editions.py +407 -0
- docverse/models/infrastructure.py +108 -0
- docverse/models/keeper_sync.py +712 -0
- docverse/models/lifecycle.py +196 -0
- docverse/models/memberships.py +134 -0
- docverse/models/organizations.py +449 -0
- docverse/models/projects.py +432 -0
- docverse/models/queue.py +321 -0
- docverse/models/queue_enums.py +136 -0
- docverse/models/services.py +279 -0
- docverse-1.0.0b2.dist-info/METADATA +20 -0
- docverse-1.0.0b2.dist-info/RECORD +27 -0
- docverse-1.0.0b2.dist-info/WHEEL +5 -0
- docverse-1.0.0b2.dist-info/entry_points.txt +2 -0
- docverse-1.0.0b2.dist-info/top_level.txt +1 -0
docverse/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Docverse client library."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from ._client import DEFAULT_UPDATED_SINCE_OVERLAP, DocverseClient, ProjectList
|
|
6
|
+
from ._exceptions import BuildProcessingError, DocverseClientError
|
|
7
|
+
from ._tar import create_tarball
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"DEFAULT_UPDATED_SINCE_OVERLAP",
|
|
11
|
+
"BuildProcessingError",
|
|
12
|
+
"DocverseClient",
|
|
13
|
+
"DocverseClientError",
|
|
14
|
+
"ProjectList",
|
|
15
|
+
"__version__",
|
|
16
|
+
"create_tarball",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
__version__ = version("docverse")
|
|
21
|
+
except PackageNotFoundError:
|
|
22
|
+
__version__ = "0.0.0"
|
docverse/_annotations.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Auto-detection of build provenance annotations from CI environments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from .models.builds import BuildAnnotations
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"detect_github_actions_annotations",
|
|
11
|
+
"merge_annotations",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def detect_github_actions_annotations() -> BuildAnnotations | None:
|
|
16
|
+
"""Detect build provenance from GitHub Actions environment variables.
|
|
17
|
+
|
|
18
|
+
Returns a populated ``BuildAnnotations`` if running inside GitHub
|
|
19
|
+
Actions (``GITHUB_ACTIONS == "true"``), otherwise ``None``.
|
|
20
|
+
"""
|
|
21
|
+
if os.environ.get("GITHUB_ACTIONS") != "true":
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
server_url = os.environ.get("GITHUB_SERVER_URL", "https://github.com")
|
|
25
|
+
repository = os.environ.get("GITHUB_REPOSITORY")
|
|
26
|
+
run_id = os.environ.get("GITHUB_RUN_ID")
|
|
27
|
+
|
|
28
|
+
run_url: str | None = None
|
|
29
|
+
if repository and run_id:
|
|
30
|
+
run_url = f"{server_url}/{repository}/actions/runs/{run_id}"
|
|
31
|
+
|
|
32
|
+
return BuildAnnotations(
|
|
33
|
+
commit_sha=os.environ.get("GITHUB_SHA"),
|
|
34
|
+
github_repository=repository,
|
|
35
|
+
github_run_id=run_id,
|
|
36
|
+
github_run_url=run_url,
|
|
37
|
+
github_run_attempt=os.environ.get("GITHUB_RUN_ATTEMPT"),
|
|
38
|
+
github_workflow=os.environ.get("GITHUB_WORKFLOW"),
|
|
39
|
+
github_actor=os.environ.get("GITHUB_ACTOR"),
|
|
40
|
+
github_event_name=os.environ.get("GITHUB_EVENT_NAME"),
|
|
41
|
+
ci_platform="github-actions",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def merge_annotations(
|
|
46
|
+
auto: BuildAnnotations | None,
|
|
47
|
+
manual: dict[str, str] | None,
|
|
48
|
+
) -> BuildAnnotations | None:
|
|
49
|
+
"""Merge auto-detected and manual annotations.
|
|
50
|
+
|
|
51
|
+
Manual entries take precedence over auto-detected values. Returns
|
|
52
|
+
``None`` if both inputs are empty/None.
|
|
53
|
+
"""
|
|
54
|
+
result: dict[str, str] = {}
|
|
55
|
+
if auto is not None:
|
|
56
|
+
result.update(
|
|
57
|
+
{k: v for k, v in auto.model_dump().items() if v is not None}
|
|
58
|
+
)
|
|
59
|
+
if manual:
|
|
60
|
+
result.update(manual)
|
|
61
|
+
if not result:
|
|
62
|
+
return None
|
|
63
|
+
return BuildAnnotations.model_validate(result)
|
docverse/_cli.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""Command-line interface for the Docverse client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from ._annotations import detect_github_actions_annotations, merge_annotations
|
|
16
|
+
from ._client import DocverseClient
|
|
17
|
+
from ._exceptions import BuildProcessingError, DocverseClientError
|
|
18
|
+
from ._tar import create_tarball
|
|
19
|
+
from .models.queue_enums import JobStatus
|
|
20
|
+
|
|
21
|
+
__all__ = ["main"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
25
|
+
@click.version_option(message="%(version)s")
|
|
26
|
+
def main() -> None:
|
|
27
|
+
"""Docverse documentation hosting client."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@main.command()
|
|
31
|
+
@click.option(
|
|
32
|
+
"--org",
|
|
33
|
+
envvar="DOCVERSE_ORG",
|
|
34
|
+
show_envvar=True,
|
|
35
|
+
required=True,
|
|
36
|
+
help="Organization slug.",
|
|
37
|
+
)
|
|
38
|
+
@click.option(
|
|
39
|
+
"--project",
|
|
40
|
+
envvar="DOCVERSE_PROJECT",
|
|
41
|
+
show_envvar=True,
|
|
42
|
+
required=True,
|
|
43
|
+
help="Project slug.",
|
|
44
|
+
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"--git-ref",
|
|
47
|
+
envvar="DOCVERSE_GIT_REF",
|
|
48
|
+
show_envvar=True,
|
|
49
|
+
default=None,
|
|
50
|
+
help="Git ref for the build. Defaults to HEAD.",
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
"--dir",
|
|
54
|
+
"source_dir",
|
|
55
|
+
envvar="DOCVERSE_DIR",
|
|
56
|
+
show_envvar=True,
|
|
57
|
+
required=True,
|
|
58
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
59
|
+
help="Directory containing the built documentation.",
|
|
60
|
+
)
|
|
61
|
+
@click.option(
|
|
62
|
+
"--token",
|
|
63
|
+
envvar="DOCVERSE_TOKEN",
|
|
64
|
+
show_envvar=True,
|
|
65
|
+
required=True,
|
|
66
|
+
help="API bearer token.",
|
|
67
|
+
)
|
|
68
|
+
@click.option(
|
|
69
|
+
"--base-url",
|
|
70
|
+
envvar="DOCVERSE_API",
|
|
71
|
+
show_envvar=True,
|
|
72
|
+
default="https://roundtable.lsst.cloud/docverse/api",
|
|
73
|
+
show_default=True,
|
|
74
|
+
help="Docverse API base URL.",
|
|
75
|
+
)
|
|
76
|
+
@click.option(
|
|
77
|
+
"--alternate-name",
|
|
78
|
+
envvar="DOCVERSE_ALTERNATE",
|
|
79
|
+
show_envvar=True,
|
|
80
|
+
default=None,
|
|
81
|
+
help="Alternate deployment name.",
|
|
82
|
+
)
|
|
83
|
+
@click.option(
|
|
84
|
+
"--annotation",
|
|
85
|
+
"-a",
|
|
86
|
+
"annotations",
|
|
87
|
+
multiple=True,
|
|
88
|
+
help="Manual annotation in KEY=VALUE format. Can be repeated.",
|
|
89
|
+
)
|
|
90
|
+
@click.option(
|
|
91
|
+
"--auto-annotations/--no-auto-annotations",
|
|
92
|
+
default=True,
|
|
93
|
+
show_default=True,
|
|
94
|
+
help="Auto-detect annotations from CI environment variables.",
|
|
95
|
+
)
|
|
96
|
+
@click.option(
|
|
97
|
+
"--no-wait",
|
|
98
|
+
is_flag=True,
|
|
99
|
+
default=False,
|
|
100
|
+
help="Exit after upload without waiting for processing.",
|
|
101
|
+
)
|
|
102
|
+
@click.option(
|
|
103
|
+
"--verbose",
|
|
104
|
+
"-v",
|
|
105
|
+
is_flag=True,
|
|
106
|
+
default=False,
|
|
107
|
+
help="Show detailed HTTP request/response information.",
|
|
108
|
+
)
|
|
109
|
+
def upload(
|
|
110
|
+
org: str,
|
|
111
|
+
project: str,
|
|
112
|
+
git_ref: str | None,
|
|
113
|
+
source_dir: Path,
|
|
114
|
+
token: str,
|
|
115
|
+
base_url: str,
|
|
116
|
+
alternate_name: str | None,
|
|
117
|
+
annotations: tuple[str, ...],
|
|
118
|
+
auto_annotations: bool, # noqa: FBT001
|
|
119
|
+
no_wait: bool, # noqa: FBT001
|
|
120
|
+
verbose: bool, # noqa: FBT001
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Upload a documentation build."""
|
|
123
|
+
if git_ref is None:
|
|
124
|
+
git_ref = _detect_git_ref()
|
|
125
|
+
|
|
126
|
+
# Parse manual annotations
|
|
127
|
+
manual: dict[str, str] | None = None
|
|
128
|
+
if annotations:
|
|
129
|
+
manual = {}
|
|
130
|
+
for item in annotations:
|
|
131
|
+
if "=" not in item:
|
|
132
|
+
msg = f"Invalid annotation format (expected KEY=VALUE): {item}"
|
|
133
|
+
raise click.BadParameter(msg, param_hint="'--annotation'")
|
|
134
|
+
key, value = item.split("=", 1)
|
|
135
|
+
manual[key] = value
|
|
136
|
+
|
|
137
|
+
asyncio.run(
|
|
138
|
+
_upload_async(
|
|
139
|
+
org=org,
|
|
140
|
+
project=project,
|
|
141
|
+
git_ref=git_ref,
|
|
142
|
+
source_dir=source_dir,
|
|
143
|
+
token=token,
|
|
144
|
+
base_url=base_url,
|
|
145
|
+
alternate_name=alternate_name,
|
|
146
|
+
auto_annotations=auto_annotations,
|
|
147
|
+
manual_annotations=manual,
|
|
148
|
+
no_wait=no_wait,
|
|
149
|
+
verbose=verbose,
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _stage_worker_source(*, worker_dir: Path, dest_dir: Path) -> None:
|
|
155
|
+
"""Pack the worker source and install it into the deployments repo.
|
|
156
|
+
|
|
157
|
+
Parameters
|
|
158
|
+
----------
|
|
159
|
+
worker_dir
|
|
160
|
+
The monorepo's ``cloudflare-worker/`` directory.
|
|
161
|
+
dest_dir
|
|
162
|
+
The ``worker/`` directory inside the deployments repo.
|
|
163
|
+
|
|
164
|
+
Raises
|
|
165
|
+
------
|
|
166
|
+
click.ClickException
|
|
167
|
+
If packing, unpacking, or dependency installation fails.
|
|
168
|
+
"""
|
|
169
|
+
# Phase 1: npm pack
|
|
170
|
+
click.echo(f"Packing cloudflare worker from {worker_dir}")
|
|
171
|
+
try:
|
|
172
|
+
pack_result = subprocess.run(
|
|
173
|
+
["npm", "pack", "--json"],
|
|
174
|
+
check=True,
|
|
175
|
+
capture_output=True,
|
|
176
|
+
text=True,
|
|
177
|
+
cwd=str(worker_dir),
|
|
178
|
+
)
|
|
179
|
+
except subprocess.CalledProcessError as exc:
|
|
180
|
+
msg = f"npm pack failed: {exc.stderr}"
|
|
181
|
+
raise click.ClickException(msg) from exc
|
|
182
|
+
try:
|
|
183
|
+
tarball_name = json.loads(pack_result.stdout)[0]["filename"]
|
|
184
|
+
except (json.JSONDecodeError, KeyError, IndexError) as exc:
|
|
185
|
+
msg = f"Failed to parse npm pack output: {pack_result.stdout!r}"
|
|
186
|
+
raise click.ClickException(msg) from exc
|
|
187
|
+
|
|
188
|
+
# Phase 2: copy and unpack into deployments repo
|
|
189
|
+
click.echo(f"Unpacking worker into {dest_dir}")
|
|
190
|
+
shutil.copy2(worker_dir / tarball_name, dest_dir / tarball_name)
|
|
191
|
+
try:
|
|
192
|
+
subprocess.run(
|
|
193
|
+
["tar", "xzf", tarball_name, "--strip-components=1"],
|
|
194
|
+
check=True,
|
|
195
|
+
cwd=str(dest_dir),
|
|
196
|
+
)
|
|
197
|
+
except subprocess.CalledProcessError as exc:
|
|
198
|
+
msg = f"Failed to unpack worker tarball: {exc}"
|
|
199
|
+
raise click.ClickException(msg) from exc
|
|
200
|
+
finally:
|
|
201
|
+
for tgz in dest_dir.glob("*.tgz"):
|
|
202
|
+
tgz.unlink()
|
|
203
|
+
(worker_dir / tarball_name).unlink(missing_ok=True)
|
|
204
|
+
|
|
205
|
+
# Phase 2a: copy the lockfile alongside the unpacked source. ``npm
|
|
206
|
+
# pack`` always excludes package-lock.json, so without this the
|
|
207
|
+
# destination keeps whatever lockfile an earlier deploy left behind.
|
|
208
|
+
# Once that stale lockfile disagrees with the freshly packed
|
|
209
|
+
# package.json, npm re-resolves the whole tree and deploys stop being
|
|
210
|
+
# reproducible.
|
|
211
|
+
lockfile = worker_dir / "package-lock.json"
|
|
212
|
+
if not lockfile.is_file():
|
|
213
|
+
msg = f"package-lock.json not found in {worker_dir}"
|
|
214
|
+
raise click.ClickException(msg)
|
|
215
|
+
click.echo("Copying worker lockfile")
|
|
216
|
+
shutil.copy2(lockfile, dest_dir / "package-lock.json")
|
|
217
|
+
|
|
218
|
+
# Phase 2b: install worker runtime dependencies. ``npm ci`` installs
|
|
219
|
+
# exactly what the lockfile pins and wipes node_modules first, so no
|
|
220
|
+
# stale packages survive from an earlier deploy.
|
|
221
|
+
click.echo("Installing worker dependencies")
|
|
222
|
+
try:
|
|
223
|
+
subprocess.run(
|
|
224
|
+
["npm", "ci", "--omit=dev"],
|
|
225
|
+
check=True,
|
|
226
|
+
capture_output=True,
|
|
227
|
+
text=True,
|
|
228
|
+
cwd=str(dest_dir),
|
|
229
|
+
)
|
|
230
|
+
except subprocess.CalledProcessError as exc:
|
|
231
|
+
msg = f"npm ci failed: {exc.stderr}"
|
|
232
|
+
raise click.ClickException(msg) from exc
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
@main.command()
|
|
236
|
+
@click.option(
|
|
237
|
+
"--docverse-repo",
|
|
238
|
+
required=True,
|
|
239
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
240
|
+
help="Path to the docverse monorepo root.",
|
|
241
|
+
)
|
|
242
|
+
@click.option(
|
|
243
|
+
"--deployments-repo",
|
|
244
|
+
required=True,
|
|
245
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
246
|
+
help="Path to local docverse-cloudflare-deployments checkout.",
|
|
247
|
+
)
|
|
248
|
+
@click.option(
|
|
249
|
+
"--env",
|
|
250
|
+
"wrangler_env",
|
|
251
|
+
required=True,
|
|
252
|
+
help="Wrangler environment name (e.g., dev, production).",
|
|
253
|
+
)
|
|
254
|
+
@click.option(
|
|
255
|
+
"--dry-run",
|
|
256
|
+
is_flag=True,
|
|
257
|
+
default=False,
|
|
258
|
+
help="Build the worker bundle without deploying.",
|
|
259
|
+
)
|
|
260
|
+
def deploy_worker(
|
|
261
|
+
*,
|
|
262
|
+
docverse_repo: Path,
|
|
263
|
+
deployments_repo: Path,
|
|
264
|
+
wrangler_env: str,
|
|
265
|
+
dry_run: bool,
|
|
266
|
+
) -> None:
|
|
267
|
+
"""Pack and deploy the Cloudflare Worker."""
|
|
268
|
+
if not re.match(r"^[a-zA-Z0-9_-]+$", wrangler_env):
|
|
269
|
+
msg = (
|
|
270
|
+
f"Invalid environment name {wrangler_env!r}: "
|
|
271
|
+
"must contain only alphanumeric characters, hyphens, "
|
|
272
|
+
"and underscores"
|
|
273
|
+
)
|
|
274
|
+
raise click.ClickException(msg)
|
|
275
|
+
|
|
276
|
+
worker_dir = docverse_repo.resolve() / "cloudflare-worker"
|
|
277
|
+
if not worker_dir.is_dir():
|
|
278
|
+
msg = f"cloudflare-worker/ not found in {docverse_repo.resolve()}"
|
|
279
|
+
raise click.ClickException(msg)
|
|
280
|
+
|
|
281
|
+
deployments_repo = deployments_repo.resolve()
|
|
282
|
+
dest_dir = deployments_repo / "worker"
|
|
283
|
+
dest_dir.mkdir(exist_ok=True)
|
|
284
|
+
|
|
285
|
+
# Phases 1 and 2: pack the worker and stage it in the deployments repo
|
|
286
|
+
_stage_worker_source(worker_dir=worker_dir, dest_dir=dest_dir)
|
|
287
|
+
|
|
288
|
+
# Phase 3: wrangler deploy
|
|
289
|
+
wrangler_cmd = [
|
|
290
|
+
"npx",
|
|
291
|
+
"wrangler",
|
|
292
|
+
"deploy",
|
|
293
|
+
"--env",
|
|
294
|
+
wrangler_env,
|
|
295
|
+
]
|
|
296
|
+
if dry_run:
|
|
297
|
+
outdir = deployments_repo / "dist"
|
|
298
|
+
wrangler_cmd.extend(["--dry-run", f"--outdir={outdir}"])
|
|
299
|
+
click.echo(f"Deploying worker (env={wrangler_env}, dry_run={dry_run})")
|
|
300
|
+
try:
|
|
301
|
+
subprocess.run(
|
|
302
|
+
wrangler_cmd,
|
|
303
|
+
check=True,
|
|
304
|
+
cwd=str(deployments_repo),
|
|
305
|
+
)
|
|
306
|
+
except subprocess.CalledProcessError as exc:
|
|
307
|
+
msg = f"wrangler deploy failed: {exc}"
|
|
308
|
+
raise click.ClickException(msg) from exc
|
|
309
|
+
|
|
310
|
+
click.echo(f"Worker deployed successfully (env={wrangler_env})")
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _detect_git_ref() -> str:
|
|
314
|
+
"""Detect the current git ref from HEAD."""
|
|
315
|
+
try:
|
|
316
|
+
result = subprocess.run(
|
|
317
|
+
["git", "rev-parse", "HEAD"],
|
|
318
|
+
capture_output=True,
|
|
319
|
+
text=True,
|
|
320
|
+
check=True,
|
|
321
|
+
)
|
|
322
|
+
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
|
|
323
|
+
msg = (
|
|
324
|
+
"Could not detect git ref from HEAD. "
|
|
325
|
+
"Provide --git-ref or set DOCVERSE_GIT_REF."
|
|
326
|
+
)
|
|
327
|
+
raise click.UsageError(msg) from exc
|
|
328
|
+
return result.stdout.strip()
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
async def _upload_async(
|
|
332
|
+
*,
|
|
333
|
+
org: str,
|
|
334
|
+
project: str,
|
|
335
|
+
git_ref: str,
|
|
336
|
+
source_dir: Path,
|
|
337
|
+
token: str,
|
|
338
|
+
base_url: str,
|
|
339
|
+
alternate_name: str | None,
|
|
340
|
+
auto_annotations: bool,
|
|
341
|
+
manual_annotations: dict[str, str] | None,
|
|
342
|
+
no_wait: bool,
|
|
343
|
+
verbose: bool,
|
|
344
|
+
) -> None:
|
|
345
|
+
"""Run the upload workflow."""
|
|
346
|
+
tarball_path: Path | None = None
|
|
347
|
+
try:
|
|
348
|
+
click.echo(f"Creating tarball from {source_dir}")
|
|
349
|
+
tarball_path, content_hash = create_tarball(source_dir)
|
|
350
|
+
click.echo(f"Content hash: {content_hash}")
|
|
351
|
+
|
|
352
|
+
# Build annotations from auto-detection and manual entries
|
|
353
|
+
auto = (
|
|
354
|
+
detect_github_actions_annotations() if auto_annotations else None
|
|
355
|
+
)
|
|
356
|
+
merged_annotations = merge_annotations(auto, manual_annotations)
|
|
357
|
+
|
|
358
|
+
async with DocverseClient(base_url, token, verbose=verbose) as client:
|
|
359
|
+
click.echo(f"Creating build for {org}/{project} @ {git_ref}")
|
|
360
|
+
build = await client.create_build(
|
|
361
|
+
org,
|
|
362
|
+
project,
|
|
363
|
+
git_ref=git_ref,
|
|
364
|
+
content_hash=content_hash,
|
|
365
|
+
alternate_name=alternate_name,
|
|
366
|
+
annotations=merged_annotations,
|
|
367
|
+
)
|
|
368
|
+
click.echo(f"Build created: {build.id}")
|
|
369
|
+
|
|
370
|
+
if build.upload_url is None:
|
|
371
|
+
msg = (
|
|
372
|
+
"Server did not return an upload URL. "
|
|
373
|
+
"The organization may not have an object "
|
|
374
|
+
"store configured."
|
|
375
|
+
)
|
|
376
|
+
raise DocverseClientError(msg)
|
|
377
|
+
|
|
378
|
+
click.echo("Uploading tarball")
|
|
379
|
+
await client.upload_tarball(build.upload_url, tarball_path)
|
|
380
|
+
|
|
381
|
+
click.echo("Signalling upload complete")
|
|
382
|
+
build = await client.complete_upload(build.self_url)
|
|
383
|
+
|
|
384
|
+
if no_wait:
|
|
385
|
+
click.echo("Upload complete (--no-wait specified)")
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
if build.job_url is None:
|
|
389
|
+
msg = "Server did not return a job URL after upload"
|
|
390
|
+
raise DocverseClientError(msg)
|
|
391
|
+
|
|
392
|
+
click.echo("Waiting for build processing")
|
|
393
|
+
job = await client.wait_for_job(build.job_url)
|
|
394
|
+
|
|
395
|
+
if job.status == JobStatus.completed_with_errors:
|
|
396
|
+
click.echo(
|
|
397
|
+
f"Build completed with errors (phase={job.phase})",
|
|
398
|
+
err=True,
|
|
399
|
+
)
|
|
400
|
+
sys.exit(2)
|
|
401
|
+
elif job.status == JobStatus.cancelled:
|
|
402
|
+
click.echo("Build processing was cancelled", err=True)
|
|
403
|
+
sys.exit(2)
|
|
404
|
+
else:
|
|
405
|
+
click.echo("Build processing complete")
|
|
406
|
+
|
|
407
|
+
except BuildProcessingError as exc:
|
|
408
|
+
raise click.ClickException(str(exc)) from exc
|
|
409
|
+
except DocverseClientError as exc:
|
|
410
|
+
raise click.ClickException(str(exc)) from exc
|
|
411
|
+
finally:
|
|
412
|
+
if tarball_path is not None:
|
|
413
|
+
tarball_path.unlink(missing_ok=True)
|