ginkgo-report 0.4.0__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.
- ginkgo_report-0.4.0/.gitignore +58 -0
- ginkgo_report-0.4.0/PKG-INFO +15 -0
- ginkgo_report-0.4.0/pyproject.toml +21 -0
- ginkgo_report-0.4.0/src/ginkgo_report/__init__.py +21 -0
- ginkgo_report-0.4.0/src/ginkgo_report/command.py +186 -0
- ginkgo_report-0.4.0/src/ginkgo_report/model.py +1159 -0
- ginkgo_report-0.4.0/src/ginkgo_report/notebook_page.py +265 -0
- ginkgo_report-0.4.0/src/ginkgo_report/render.py +391 -0
- ginkgo_report-0.4.0/src/ginkgo_report/sizing.py +59 -0
- ginkgo_report-0.4.0/src/ginkgo_report/static/fonts/commissioner.woff2 +0 -0
- ginkgo_report-0.4.0/src/ginkgo_report/static/fonts/jetbrains-mono.woff2 +0 -0
- ginkgo_report-0.4.0/src/ginkgo_report/static/images/ginkgo-leaf.png +0 -0
- ginkgo_report-0.4.0/src/ginkgo_report/static/islands.js +137 -0
- ginkgo_report-0.4.0/src/ginkgo_report/static/report.css +791 -0
- ginkgo_report-0.4.0/src/ginkgo_report/templates/_environment.html.j2 +8 -0
- ginkgo_report-0.4.0/src/ginkgo_report/templates/_failure.html.j2 +49 -0
- ginkgo_report-0.4.0/src/ginkgo_report/templates/_footer.html.j2 +5 -0
- ginkgo_report-0.4.0/src/ginkgo_report/templates/_graph.html.j2 +31 -0
- ginkgo_report-0.4.0/src/ginkgo_report/templates/_masthead.html.j2 +22 -0
- ginkgo_report-0.4.0/src/ginkgo_report/templates/_outputs.html.j2 +22 -0
- ginkgo_report-0.4.0/src/ginkgo_report/templates/_pages.html.j2 +35 -0
- ginkgo_report-0.4.0/src/ginkgo_report/templates/_parameters.html.j2 +7 -0
- ginkgo_report-0.4.0/src/ginkgo_report/templates/_retries.html.j2 +15 -0
- ginkgo_report-0.4.0/src/ginkgo_report/templates/_section.html.j2 +4 -0
- ginkgo_report-0.4.0/src/ginkgo_report/templates/_sidebar.html.j2 +36 -0
- ginkgo_report-0.4.0/src/ginkgo_report/templates/_summary.html.j2 +21 -0
- ginkgo_report-0.4.0/src/ginkgo_report/templates/_tasks.html.j2 +41 -0
- ginkgo_report-0.4.0/src/ginkgo_report/templates/index.html.j2 +58 -0
- ginkgo_report-0.4.0/tests/test_reporting.py +818 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Local environment and tool caches
|
|
2
|
+
.pixi/*
|
|
3
|
+
!.pixi/config.toml
|
|
4
|
+
.pytest_cache/
|
|
5
|
+
.ruff_cache/
|
|
6
|
+
ui/node_modules/
|
|
7
|
+
ui/.vite/
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
|
|
11
|
+
*__pycache__*
|
|
12
|
+
# Ginkgo runtime state
|
|
13
|
+
.ginkgo/
|
|
14
|
+
examples/**/logs/
|
|
15
|
+
examples/**/results/
|
|
16
|
+
benchmarks/results/
|
|
17
|
+
|
|
18
|
+
# Agent / local-instructions files
|
|
19
|
+
AGENTS.md
|
|
20
|
+
# ...but not the one `ginkgo init` scaffolds into new projects.
|
|
21
|
+
!src/ginkgo/templates/init/agents/AGENTS.md
|
|
22
|
+
CLAUDE.md
|
|
23
|
+
.claude/
|
|
24
|
+
.agents/
|
|
25
|
+
|
|
26
|
+
# uncoded symbol index (regenerated locally via `uncoded sync`)
|
|
27
|
+
.uncoded/
|
|
28
|
+
|
|
29
|
+
# Local planning / design / worklog docs
|
|
30
|
+
plan.md
|
|
31
|
+
*plan.local.md
|
|
32
|
+
worklog.md
|
|
33
|
+
*worklog*.md
|
|
34
|
+
ginkgo-design-doc.md
|
|
35
|
+
example-workflow.py
|
|
36
|
+
docs/_build/
|
|
37
|
+
docs/site/node_modules/
|
|
38
|
+
docs/site/dist/
|
|
39
|
+
docs/site/.astro/
|
|
40
|
+
|
|
41
|
+
# OS/editor noise
|
|
42
|
+
.DS_Store
|
|
43
|
+
|
|
44
|
+
*.lock
|
|
45
|
+
# Env lockfiles that are part of a fixture or example are inputs, not build
|
|
46
|
+
# output: an example whose lock is missing re-solves on first use, which
|
|
47
|
+
# changes its env identity between runs and silently breaks cache reuse.
|
|
48
|
+
!tests/**/pixi.lock
|
|
49
|
+
!plugins/**/pixi.lock
|
|
50
|
+
!examples/**/pixi.lock
|
|
51
|
+
|
|
52
|
+
# Maintainer working documents (CLAUDE.md: plans are not committed)
|
|
53
|
+
docs/*-plan.md
|
|
54
|
+
docs/user-testing-*.md
|
|
55
|
+
docs/oke-validation.md
|
|
56
|
+
docs/*.pptx
|
|
57
|
+
scratchpad/
|
|
58
|
+
.vscode/
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ginkgo-report
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Static HTML run reports for ginkgo: the ginkgo report command
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: ginkgo-workflow==0.4.0
|
|
8
|
+
Requires-Dist: jinja2>=3.1
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# ginkgo-report
|
|
12
|
+
|
|
13
|
+
Static HTML run reports for ginkgo: the ginkgo report command.
|
|
14
|
+
|
|
15
|
+
A plugin for [Ginkgo](https://sanjaynagi.github.io/ginkgo/). Install it with `pip install "ginkgo-workflow[report]"`, and see the [plugins guide](https://sanjaynagi.github.io/ginkgo/getting-started/installation/#plugins).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ginkgo-report"
|
|
3
|
+
version = "0.4.0"
|
|
4
|
+
description = "Static HTML run reports for ginkgo: the ginkgo report command"
|
|
5
|
+
requires-python = ">= 3.11"
|
|
6
|
+
readme = { text = "# ginkgo-report\n\nStatic HTML run reports for ginkgo: the ginkgo report command.\n\nA plugin for [Ginkgo](https://sanjaynagi.github.io/ginkgo/). Install it with `pip install \"ginkgo-workflow[report]\"`, and see the [plugins guide](https://sanjaynagi.github.io/ginkgo/getting-started/installation/#plugins).", content-type = "text/markdown" }
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"ginkgo-workflow==0.4.0",
|
|
10
|
+
"jinja2>=3.1",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[project.entry-points."ginkgo.plugins"]
|
|
14
|
+
report = "ginkgo_report:PLUGIN"
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
build-backend = "hatchling.build"
|
|
18
|
+
requires = ["hatchling"]
|
|
19
|
+
|
|
20
|
+
[tool.hatch.build.targets.wheel]
|
|
21
|
+
packages = ["src/ginkgo_report"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""ginkgo-report: the ``ginkgo report`` command and its static HTML report."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ginkgo.plugins.api import CliCommand, Plugin
|
|
6
|
+
|
|
7
|
+
from ginkgo_report.command import add_report_arguments, command_report
|
|
8
|
+
|
|
9
|
+
__all__ = ["PLUGIN"]
|
|
10
|
+
|
|
11
|
+
PLUGIN = Plugin(
|
|
12
|
+
name="ginkgo-report",
|
|
13
|
+
contributions=(
|
|
14
|
+
CliCommand(
|
|
15
|
+
name="report",
|
|
16
|
+
help="Generate HTML run reports",
|
|
17
|
+
add_arguments=add_report_arguments,
|
|
18
|
+
run=command_report,
|
|
19
|
+
),
|
|
20
|
+
),
|
|
21
|
+
)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""``ginkgo report`` — export a static HTML report for a completed run."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import webbrowser
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
|
|
13
|
+
from ginkgo.cli.common import console, open_run
|
|
14
|
+
from ginkgo.formatting import format_bytes
|
|
15
|
+
from ginkgo.workspace_layout import WorkspaceLayout
|
|
16
|
+
from ginkgo_report.render import export_report
|
|
17
|
+
from ginkgo_report.sizing import SizingPolicy
|
|
18
|
+
|
|
19
|
+
#: A single-file report above this size is too large to pass around comfortably,
|
|
20
|
+
#: usually because a notebook page embeds large figures or tables.
|
|
21
|
+
LARGE_SINGLE_FILE_BYTES = 25 * 1024 * 1024
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def add_report_arguments(parser: argparse.ArgumentParser) -> None:
|
|
25
|
+
"""Declare ``ginkgo report``'s arguments."""
|
|
26
|
+
parser.add_argument("run_id", nargs="?")
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--out",
|
|
29
|
+
default=None,
|
|
30
|
+
help=(
|
|
31
|
+
"Where to write the report. A bundle is written into this directory "
|
|
32
|
+
"(default: <workspace>/.ginkgo/reports/<run-id>/). With --single-file, "
|
|
33
|
+
"a path ending in .html is the file itself and any other path is a "
|
|
34
|
+
"directory to write index.html into, and any other extension is refused "
|
|
35
|
+
"(default: <workspace>/.ginkgo/reports/<run-id>.html)."
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--force",
|
|
40
|
+
action="store_true",
|
|
41
|
+
help=(
|
|
42
|
+
"Write into an --out destination that holds files ginkgo did not write, "
|
|
43
|
+
"replacing only the report's own files. Without this, such a "
|
|
44
|
+
"destination is left untouched."
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--single-file",
|
|
49
|
+
action="store_true",
|
|
50
|
+
help=(
|
|
51
|
+
"Emit one HTML file that opens offline, with CSS, fonts, script and "
|
|
52
|
+
"rendered pages inlined; a marimo page is inlined as a static copy of "
|
|
53
|
+
"its outputs. Logs are linked but not included."
|
|
54
|
+
),
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--hide-code",
|
|
58
|
+
action="store_true",
|
|
59
|
+
help=(
|
|
60
|
+
"Leave each cell's code out of the static copies of notebook pages. "
|
|
61
|
+
"By default it is shown, folded above the cell's output."
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--max-log-lines",
|
|
66
|
+
type=int,
|
|
67
|
+
default=80,
|
|
68
|
+
help="Trailing log lines to retain for failed tasks (default: 80).",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--open",
|
|
72
|
+
dest="open",
|
|
73
|
+
action="store_true",
|
|
74
|
+
default=False,
|
|
75
|
+
help="Open the report in the default browser once it is written.",
|
|
76
|
+
)
|
|
77
|
+
parser.add_argument(
|
|
78
|
+
"--no-open",
|
|
79
|
+
dest="open",
|
|
80
|
+
action="store_false",
|
|
81
|
+
help="Do not open the report (the default).",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def command_report(args: argparse.Namespace) -> int:
|
|
86
|
+
"""Handle ``ginkgo report``."""
|
|
87
|
+
rich_console = console(sys.stdout)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
with open_run(args.run_id) as (reader, run_id):
|
|
91
|
+
summary = reader.run(run_id)
|
|
92
|
+
measured_runs = reader.measured_executions(
|
|
93
|
+
task.cache_key
|
|
94
|
+
for task in summary.tasks
|
|
95
|
+
if task.cache_label == "hit" and task.cache_key
|
|
96
|
+
)
|
|
97
|
+
except FileNotFoundError as exc:
|
|
98
|
+
rich_console.print(Text.assemble(("✖ ", "red"), str(exc)))
|
|
99
|
+
return 1
|
|
100
|
+
run_dir = summary.run_dir
|
|
101
|
+
policy = SizingPolicy(log_lines=int(args.max_log_lines) if args.max_log_lines else 80)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
out = _resolve_destination(run_dir=run_dir, out=args.out, single_file=args.single_file)
|
|
105
|
+
result = export_report(
|
|
106
|
+
summary=summary,
|
|
107
|
+
out=out,
|
|
108
|
+
policy=policy,
|
|
109
|
+
single_file=bool(args.single_file),
|
|
110
|
+
managed_destination=args.out is None,
|
|
111
|
+
force=bool(args.force),
|
|
112
|
+
measured_runs=measured_runs,
|
|
113
|
+
include_code=not args.hide_code,
|
|
114
|
+
)
|
|
115
|
+
except ValueError as exc:
|
|
116
|
+
rich_console.print(Text.assemble(("✖ ", "red"), str(exc)))
|
|
117
|
+
return 1
|
|
118
|
+
except FileExistsError as exc:
|
|
119
|
+
rich_console.print(Text.assemble(("✖ ", "red"), str(exc)))
|
|
120
|
+
rich_console.print(" Pass --force to write into it anyway, or choose another --out.")
|
|
121
|
+
return 1
|
|
122
|
+
|
|
123
|
+
rich_console.print("[bold green]🌿 ginkgo report[/]\n")
|
|
124
|
+
rich_console.print(f"[cyan]Run:[/] [bold]{run_dir.name}[/]")
|
|
125
|
+
rich_console.print(f"[cyan]Output:[/] [bold]{result.index_path}[/]", soft_wrap=True)
|
|
126
|
+
if result.omitted_links:
|
|
127
|
+
listed = ", ".join(result.omitted_links)
|
|
128
|
+
rich_console.print(
|
|
129
|
+
f"[yellow]⚠[/] The single-file report links {len(result.omitted_links)} "
|
|
130
|
+
f"file(s) it does not include, so those links will not open: {listed}. "
|
|
131
|
+
"Export without --single-file to include them."
|
|
132
|
+
)
|
|
133
|
+
size = result.index_path.stat().st_size
|
|
134
|
+
if result.single_file and size > LARGE_SINGLE_FILE_BYTES:
|
|
135
|
+
rich_console.print(
|
|
136
|
+
f"[yellow]⚠[/] The single-file report is {format_bytes(size)}, mostly "
|
|
137
|
+
"the rendered pages it embeds. Export without --single-file to keep "
|
|
138
|
+
"them as separate files."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if args.open:
|
|
142
|
+
opened = _open_path(result.index_path)
|
|
143
|
+
if not opened:
|
|
144
|
+
rich_console.print("[yellow]⚠[/] Could not open a browser automatically.")
|
|
145
|
+
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _resolve_destination(*, run_dir: Path, out: str | None, single_file: bool) -> Path:
|
|
150
|
+
"""Resolve where the report is written: the bundle's directory or the single file.
|
|
151
|
+
|
|
152
|
+
A single-file report defaults to ``.ginkgo/reports/<run-id>.html``, beside
|
|
153
|
+
rather than inside the run's bundle directory, so neither replaces the other.
|
|
154
|
+
|
|
155
|
+
Raises
|
|
156
|
+
------
|
|
157
|
+
ValueError
|
|
158
|
+
When a single-file ``out`` names a file that is not HTML, such as
|
|
159
|
+
``report.txt``, rather than silently making it a directory.
|
|
160
|
+
"""
|
|
161
|
+
if out is not None:
|
|
162
|
+
path = Path(out).resolve()
|
|
163
|
+
if not single_file or path.suffix.lower() in {".html", ".htm"}:
|
|
164
|
+
return path
|
|
165
|
+
if path.suffix and not path.is_dir():
|
|
166
|
+
raise ValueError(
|
|
167
|
+
f"--out {out} names a {path.suffix} file, but a single-file report is "
|
|
168
|
+
"HTML: name it with .html, or give a directory to write index.html into"
|
|
169
|
+
)
|
|
170
|
+
return path / "index.html"
|
|
171
|
+
workspace = run_dir.parents[1] if len(run_dir.parents) >= 2 else run_dir.parent
|
|
172
|
+
reports_root = WorkspaceLayout(root=workspace).reports
|
|
173
|
+
if single_file:
|
|
174
|
+
return (reports_root / f"{run_dir.name}.html").resolve()
|
|
175
|
+
return (reports_root / run_dir.name).resolve()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _open_path(path: Path) -> bool:
|
|
179
|
+
"""Open a local file path in the user's default browser."""
|
|
180
|
+
url = path.resolve().as_uri()
|
|
181
|
+
if sys.platform == "darwin":
|
|
182
|
+
result = subprocess.run( # noqa: S603 - local OS launcher
|
|
183
|
+
["open", url], check=False, capture_output=True, text=True
|
|
184
|
+
)
|
|
185
|
+
return result.returncode == 0
|
|
186
|
+
return bool(webbrowser.open(url, new=2))
|