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.
Files changed (29) hide show
  1. ginkgo_report-0.4.0/.gitignore +58 -0
  2. ginkgo_report-0.4.0/PKG-INFO +15 -0
  3. ginkgo_report-0.4.0/pyproject.toml +21 -0
  4. ginkgo_report-0.4.0/src/ginkgo_report/__init__.py +21 -0
  5. ginkgo_report-0.4.0/src/ginkgo_report/command.py +186 -0
  6. ginkgo_report-0.4.0/src/ginkgo_report/model.py +1159 -0
  7. ginkgo_report-0.4.0/src/ginkgo_report/notebook_page.py +265 -0
  8. ginkgo_report-0.4.0/src/ginkgo_report/render.py +391 -0
  9. ginkgo_report-0.4.0/src/ginkgo_report/sizing.py +59 -0
  10. ginkgo_report-0.4.0/src/ginkgo_report/static/fonts/commissioner.woff2 +0 -0
  11. ginkgo_report-0.4.0/src/ginkgo_report/static/fonts/jetbrains-mono.woff2 +0 -0
  12. ginkgo_report-0.4.0/src/ginkgo_report/static/images/ginkgo-leaf.png +0 -0
  13. ginkgo_report-0.4.0/src/ginkgo_report/static/islands.js +137 -0
  14. ginkgo_report-0.4.0/src/ginkgo_report/static/report.css +791 -0
  15. ginkgo_report-0.4.0/src/ginkgo_report/templates/_environment.html.j2 +8 -0
  16. ginkgo_report-0.4.0/src/ginkgo_report/templates/_failure.html.j2 +49 -0
  17. ginkgo_report-0.4.0/src/ginkgo_report/templates/_footer.html.j2 +5 -0
  18. ginkgo_report-0.4.0/src/ginkgo_report/templates/_graph.html.j2 +31 -0
  19. ginkgo_report-0.4.0/src/ginkgo_report/templates/_masthead.html.j2 +22 -0
  20. ginkgo_report-0.4.0/src/ginkgo_report/templates/_outputs.html.j2 +22 -0
  21. ginkgo_report-0.4.0/src/ginkgo_report/templates/_pages.html.j2 +35 -0
  22. ginkgo_report-0.4.0/src/ginkgo_report/templates/_parameters.html.j2 +7 -0
  23. ginkgo_report-0.4.0/src/ginkgo_report/templates/_retries.html.j2 +15 -0
  24. ginkgo_report-0.4.0/src/ginkgo_report/templates/_section.html.j2 +4 -0
  25. ginkgo_report-0.4.0/src/ginkgo_report/templates/_sidebar.html.j2 +36 -0
  26. ginkgo_report-0.4.0/src/ginkgo_report/templates/_summary.html.j2 +21 -0
  27. ginkgo_report-0.4.0/src/ginkgo_report/templates/_tasks.html.j2 +41 -0
  28. ginkgo_report-0.4.0/src/ginkgo_report/templates/index.html.j2 +58 -0
  29. 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))