paperthin 0.1.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.
- paperthin-0.1.0/.github/workflows/publish.yml +51 -0
- paperthin-0.1.0/.gitignore +8 -0
- paperthin-0.1.0/.pre-commit-config.yaml +7 -0
- paperthin-0.1.0/LICENSE +28 -0
- paperthin-0.1.0/PKG-INFO +113 -0
- paperthin-0.1.0/README.md +80 -0
- paperthin-0.1.0/paperthin/__init__.py +5 -0
- paperthin-0.1.0/paperthin/_version.py +24 -0
- paperthin-0.1.0/paperthin/adapters/__init__.py +13 -0
- paperthin-0.1.0/paperthin/adapters/config.py +44 -0
- paperthin-0.1.0/paperthin/adapters/image.py +55 -0
- paperthin-0.1.0/paperthin/adapters/plot.py +45 -0
- paperthin-0.1.0/paperthin/adapters/tabular.py +49 -0
- paperthin-0.1.0/paperthin/adapters/types.py +29 -0
- paperthin-0.1.0/paperthin/adapters/value.py +21 -0
- paperthin-0.1.0/paperthin/components/__init__.py +7 -0
- paperthin-0.1.0/paperthin/components/entry.py +229 -0
- paperthin-0.1.0/paperthin/components/section.py +28 -0
- paperthin-0.1.0/paperthin/components/types.py +5 -0
- paperthin-0.1.0/paperthin/html_helpers.py +122 -0
- paperthin-0.1.0/paperthin/py.typed +0 -0
- paperthin-0.1.0/paperthin/report.py +89 -0
- paperthin-0.1.0/pyproject.toml +55 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
with:
|
|
14
|
+
fetch-depth: 0
|
|
15
|
+
fetch-tags: true
|
|
16
|
+
|
|
17
|
+
- uses: actions/setup-python@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
|
|
21
|
+
- name: Build sdist and wheel
|
|
22
|
+
run: |
|
|
23
|
+
python -m pip install --upgrade build
|
|
24
|
+
python -m build
|
|
25
|
+
|
|
26
|
+
- name: Check distributions
|
|
27
|
+
run: |
|
|
28
|
+
python -m pip install --upgrade twine
|
|
29
|
+
twine check dist/*
|
|
30
|
+
|
|
31
|
+
- uses: actions/upload-artifact@v4
|
|
32
|
+
with:
|
|
33
|
+
name: dist
|
|
34
|
+
path: dist/
|
|
35
|
+
|
|
36
|
+
publish:
|
|
37
|
+
needs: build
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
environment:
|
|
40
|
+
name: pypi
|
|
41
|
+
url: https://pypi.org/p/paperthin
|
|
42
|
+
permissions:
|
|
43
|
+
id-token: write
|
|
44
|
+
steps:
|
|
45
|
+
- uses: actions/download-artifact@v4
|
|
46
|
+
with:
|
|
47
|
+
name: dist
|
|
48
|
+
path: dist/
|
|
49
|
+
|
|
50
|
+
- name: Publish to PyPI
|
|
51
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
paperthin-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Stefano Cretti
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
paperthin-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: paperthin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python library to easily create HTML reports
|
|
5
|
+
Project-URL: Homepage, https://github.com/StefanoCretti/paperthin
|
|
6
|
+
Project-URL: Repository, https://github.com/StefanoCretti/paperthin
|
|
7
|
+
Author-email: Stefano Cretti <stefanocretti99@gmail.com>
|
|
8
|
+
License-Expression: BSD-3-Clause
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.12
|
|
19
|
+
Requires-Dist: ipykernel>=7.3
|
|
20
|
+
Requires-Dist: jupytext>=1.19
|
|
21
|
+
Requires-Dist: matplotlib>=3.11
|
|
22
|
+
Requires-Dist: nbclient>=0.11
|
|
23
|
+
Requires-Dist: nbconvert>=7.17
|
|
24
|
+
Requires-Dist: pandas>=3.0
|
|
25
|
+
Requires-Dist: pillow>=12.3
|
|
26
|
+
Requires-Dist: polars>=1.43
|
|
27
|
+
Requires-Dist: pyyaml>=6.0
|
|
28
|
+
Requires-Dist: traitlets>=5.15
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# Paperthin
|
|
35
|
+
|
|
36
|
+
## About
|
|
37
|
+
Paperthin is a lightweight Python library to easily create HTML reports.
|
|
38
|
+
|
|
39
|
+
The idea is very simple:
|
|
40
|
+
|
|
41
|
+
- You provide various items to add to the report (plots, figures, tables...).
|
|
42
|
+
- Paperthin stitches them into a nicely formatted HTML report.
|
|
43
|
+
- You can then share the report with collaborators (no installations needed on their side).
|
|
44
|
+
|
|
45
|
+
Why use Paperthin over a plain Jupyter Notebook?
|
|
46
|
+
|
|
47
|
+
- As it is plain Python, it is very easy to automate (no papermill variable
|
|
48
|
+
injection, takes care of the rendering).
|
|
49
|
+
- Minimal but clean formatting (sections, collapsible descriptions, rendering
|
|
50
|
+
presets for many types of data).
|
|
51
|
+
- Embeds the data in the report and provides download buttons to retrieve it.
|
|
52
|
+
- Easy to inspect in git diffs (no need to learn Jupytext syntax).
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install paperthin
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Requires Python 3.12+.
|
|
61
|
+
|
|
62
|
+
## Quick start
|
|
63
|
+
|
|
64
|
+
- Create a `Report` object
|
|
65
|
+
- Add `Section`s (graphical dividers) and `Entry` items with `+`
|
|
66
|
+
- Call `report.make_report(path)`.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from matplotlib import pyplot as plt
|
|
70
|
+
from paperthin import Entry, Report, Section
|
|
71
|
+
|
|
72
|
+
fig, ax = plt.subplots()
|
|
73
|
+
ax.plot([1, 2, 3], [1, 4, 9])
|
|
74
|
+
|
|
75
|
+
report = (
|
|
76
|
+
Report("My report")
|
|
77
|
+
+ Section("Plots")
|
|
78
|
+
+ Entry.plot(
|
|
79
|
+
fig,
|
|
80
|
+
title="Squares",
|
|
81
|
+
info="A simple plot with its underlying data attached.",
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
report.make_report("report.html")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This produces a single, self-contained `report.html` file you can open in a
|
|
89
|
+
browser or send to anyone: no Python or Jupyter required on their end.
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
## Entries
|
|
93
|
+
|
|
94
|
+
`Entry` currently provides constructors for the following data types:
|
|
95
|
+
|
|
96
|
+
| Constructor | Source |
|
|
97
|
+
| --------------------- | ------------------------------------------ |
|
|
98
|
+
| `Entry.plot(...)` | matplotlib `Figure` |
|
|
99
|
+
| `Entry.image(...)` | path to a `.png` or `.svg` file |
|
|
100
|
+
| `Entry.tabular(...)` | polars/pandas `DataFrame`, or csv/tsv path |
|
|
101
|
+
| `Entry.value(...)` | a single `str` or `float` |
|
|
102
|
+
| `Entry.config(...)` | `dict`, or path to a yaml/json file |
|
|
103
|
+
|
|
104
|
+
## Documentation
|
|
105
|
+
This README covers the basics. Extended documentation is planned, including
|
|
106
|
+
how to create custom entries, and will be linked here once available.
|
|
107
|
+
|
|
108
|
+
## Citation
|
|
109
|
+
If paperthin is useful in your work, please consider citing the repo:
|
|
110
|
+
https://github.com/StefanoCretti/paperthin
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
BSD-3-Clause. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Paperthin
|
|
2
|
+
|
|
3
|
+
## About
|
|
4
|
+
Paperthin is a lightweight Python library to easily create HTML reports.
|
|
5
|
+
|
|
6
|
+
The idea is very simple:
|
|
7
|
+
|
|
8
|
+
- You provide various items to add to the report (plots, figures, tables...).
|
|
9
|
+
- Paperthin stitches them into a nicely formatted HTML report.
|
|
10
|
+
- You can then share the report with collaborators (no installations needed on their side).
|
|
11
|
+
|
|
12
|
+
Why use Paperthin over a plain Jupyter Notebook?
|
|
13
|
+
|
|
14
|
+
- As it is plain Python, it is very easy to automate (no papermill variable
|
|
15
|
+
injection, takes care of the rendering).
|
|
16
|
+
- Minimal but clean formatting (sections, collapsible descriptions, rendering
|
|
17
|
+
presets for many types of data).
|
|
18
|
+
- Embeds the data in the report and provides download buttons to retrieve it.
|
|
19
|
+
- Easy to inspect in git diffs (no need to learn Jupytext syntax).
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install paperthin
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires Python 3.12+.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
- Create a `Report` object
|
|
32
|
+
- Add `Section`s (graphical dividers) and `Entry` items with `+`
|
|
33
|
+
- Call `report.make_report(path)`.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from matplotlib import pyplot as plt
|
|
37
|
+
from paperthin import Entry, Report, Section
|
|
38
|
+
|
|
39
|
+
fig, ax = plt.subplots()
|
|
40
|
+
ax.plot([1, 2, 3], [1, 4, 9])
|
|
41
|
+
|
|
42
|
+
report = (
|
|
43
|
+
Report("My report")
|
|
44
|
+
+ Section("Plots")
|
|
45
|
+
+ Entry.plot(
|
|
46
|
+
fig,
|
|
47
|
+
title="Squares",
|
|
48
|
+
info="A simple plot with its underlying data attached.",
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
report.make_report("report.html")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This produces a single, self-contained `report.html` file you can open in a
|
|
56
|
+
browser or send to anyone: no Python or Jupyter required on their end.
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## Entries
|
|
60
|
+
|
|
61
|
+
`Entry` currently provides constructors for the following data types:
|
|
62
|
+
|
|
63
|
+
| Constructor | Source |
|
|
64
|
+
| --------------------- | ------------------------------------------ |
|
|
65
|
+
| `Entry.plot(...)` | matplotlib `Figure` |
|
|
66
|
+
| `Entry.image(...)` | path to a `.png` or `.svg` file |
|
|
67
|
+
| `Entry.tabular(...)` | polars/pandas `DataFrame`, or csv/tsv path |
|
|
68
|
+
| `Entry.value(...)` | a single `str` or `float` |
|
|
69
|
+
| `Entry.config(...)` | `dict`, or path to a yaml/json file |
|
|
70
|
+
|
|
71
|
+
## Documentation
|
|
72
|
+
This README covers the basics. Extended documentation is planned, including
|
|
73
|
+
how to create custom entries, and will be linked here once available.
|
|
74
|
+
|
|
75
|
+
## Citation
|
|
76
|
+
If paperthin is useful in your work, please consider citing the repo:
|
|
77
|
+
https://github.com/StefanoCretti/paperthin
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
BSD-3-Clause. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .config import ConfigAdapter
|
|
2
|
+
from .image import ImageAdapter
|
|
3
|
+
from .plot import PlotAdapter
|
|
4
|
+
from .tabular import TabularAdapter
|
|
5
|
+
from .value import ValueAdapter
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ConfigAdapter",
|
|
9
|
+
"ImageAdapter",
|
|
10
|
+
"PlotAdapter",
|
|
11
|
+
"TabularAdapter",
|
|
12
|
+
"ValueAdapter",
|
|
13
|
+
]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pathlib
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from html import escape
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from ..html_helpers import DownloadButton
|
|
9
|
+
from . import types as ct
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ConfigAdapter:
|
|
13
|
+
def __init__(self, source: ct.ConfigSource, output: ct.ConfigOutput):
|
|
14
|
+
|
|
15
|
+
if isinstance(source, str):
|
|
16
|
+
match pathlib.Path(source).suffix:
|
|
17
|
+
case ".json":
|
|
18
|
+
with open(source) as f:
|
|
19
|
+
source = json.load(f)
|
|
20
|
+
case ".yaml" | ".yml":
|
|
21
|
+
with open(source) as f:
|
|
22
|
+
source = yaml.safe_load(f)
|
|
23
|
+
case _:
|
|
24
|
+
raise ValueError(f"{source} is not a valid json or yaml file.")
|
|
25
|
+
|
|
26
|
+
self._data = source
|
|
27
|
+
self._output: ct.ConfigOutput = output
|
|
28
|
+
|
|
29
|
+
def get_display(self) -> str:
|
|
30
|
+
text = yaml.safe_dump(self._data, sort_keys=False, default_flow_style=False)
|
|
31
|
+
return f'<pre class="pt-config">{escape(text)}</pre>'
|
|
32
|
+
|
|
33
|
+
def get_buttons(self, title: str) -> Iterable[DownloadButton]:
|
|
34
|
+
|
|
35
|
+
match self._output:
|
|
36
|
+
case "yaml":
|
|
37
|
+
content = yaml.safe_dump(
|
|
38
|
+
self._data, sort_keys=False, default_flow_style=False
|
|
39
|
+
).encode("utf-8")
|
|
40
|
+
case "json":
|
|
41
|
+
content = json.dumps(self._data, indent=2, default=str).encode("utf-8")
|
|
42
|
+
|
|
43
|
+
button = DownloadButton.from_format(title, content, self._output)
|
|
44
|
+
return (button,)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import io
|
|
3
|
+
import pathlib
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
from ..html_helpers import MIMES, DownloadButton
|
|
9
|
+
from . import types as ct
|
|
10
|
+
from .tabular import TabularAdapter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ImageAdapter:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
source: ct.ImageSource,
|
|
17
|
+
data: ct.TabularSource | None = None,
|
|
18
|
+
scale: float = 1.0,
|
|
19
|
+
):
|
|
20
|
+
path = pathlib.Path(source)
|
|
21
|
+
self._content = path.read_bytes()
|
|
22
|
+
self._data = TabularAdapter(data, "tsv") if data is not None else None
|
|
23
|
+
self._scale = scale
|
|
24
|
+
|
|
25
|
+
self._output: ct.ImageOutput
|
|
26
|
+
match path.suffix:
|
|
27
|
+
case ".png":
|
|
28
|
+
self._output = "png"
|
|
29
|
+
case ".svg":
|
|
30
|
+
self._output = "svg"
|
|
31
|
+
case _:
|
|
32
|
+
raise ValueError(f"{source} is not a valid png or svg file.")
|
|
33
|
+
|
|
34
|
+
def get_display(self) -> str:
|
|
35
|
+
if self._output == "svg":
|
|
36
|
+
return self._content.decode("utf-8")
|
|
37
|
+
|
|
38
|
+
encoded = base64.b64encode(self._content).decode("ascii")
|
|
39
|
+
|
|
40
|
+
width, _height = Image.open(io.BytesIO(self._content)).size
|
|
41
|
+
scaled_width = round(width * self._scale)
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
f'<img width="{scaled_width}" '
|
|
45
|
+
f'src="data:{MIMES[self._output]};base64,{encoded}">'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def get_buttons(self, title: str) -> Iterable[DownloadButton]:
|
|
49
|
+
button = DownloadButton.from_format(title, self._content, self._output)
|
|
50
|
+
buttons = [button]
|
|
51
|
+
|
|
52
|
+
if self._data is not None:
|
|
53
|
+
buttons.extend(self._data.get_buttons(title))
|
|
54
|
+
|
|
55
|
+
return buttons
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
|
|
4
|
+
from ..html_helpers import DownloadButton
|
|
5
|
+
from . import types as ct
|
|
6
|
+
from .tabular import TabularAdapter
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PlotAdapter:
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
source: ct.PlotSource,
|
|
13
|
+
output: ct.PlotOutput,
|
|
14
|
+
data: ct.TabularSource | None = None,
|
|
15
|
+
dpi: int = 300,
|
|
16
|
+
):
|
|
17
|
+
self._fig = source
|
|
18
|
+
self._output: ct.PlotOutput = output
|
|
19
|
+
self._data = TabularAdapter(data, "tsv") if data is not None else None
|
|
20
|
+
self._dpi = dpi
|
|
21
|
+
|
|
22
|
+
# svg is always generated since it is used for display, regardless of output
|
|
23
|
+
buffer = io.BytesIO()
|
|
24
|
+
self._fig.savefig(buffer, format="svg", bbox_inches="tight")
|
|
25
|
+
self._svg = buffer.getvalue()
|
|
26
|
+
|
|
27
|
+
def get_display(self) -> str:
|
|
28
|
+
return self._svg.decode("utf-8")
|
|
29
|
+
|
|
30
|
+
def get_buttons(self, title: str) -> Iterable[DownloadButton]:
|
|
31
|
+
|
|
32
|
+
buttons: list[DownloadButton] = []
|
|
33
|
+
|
|
34
|
+
if self._output in ("svg", "svg+png"):
|
|
35
|
+
buttons.append(DownloadButton.from_format(title, self._svg, "svg"))
|
|
36
|
+
|
|
37
|
+
if self._output in ("png", "svg+png"):
|
|
38
|
+
buffer = io.BytesIO()
|
|
39
|
+
self._fig.savefig(buffer, format="png", bbox_inches="tight", dpi=self._dpi)
|
|
40
|
+
buttons.append(DownloadButton.from_format(title, buffer.getvalue(), "png"))
|
|
41
|
+
|
|
42
|
+
if self._data is not None:
|
|
43
|
+
buttons.extend(self._data.get_buttons(title))
|
|
44
|
+
|
|
45
|
+
return buttons
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from collections.abc import Iterable
|
|
3
|
+
from html import escape
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import polars as pl
|
|
7
|
+
|
|
8
|
+
from ..html_helpers import DownloadButton
|
|
9
|
+
from . import types as ct
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TabularAdapter:
|
|
13
|
+
def __init__(self, source: ct.TabularSource, output: ct.TabularOutput):
|
|
14
|
+
|
|
15
|
+
if isinstance(source, pd.DataFrame):
|
|
16
|
+
source = pl.from_pandas(source)
|
|
17
|
+
|
|
18
|
+
elif isinstance(source, str):
|
|
19
|
+
match pathlib.Path(source).suffix:
|
|
20
|
+
case ".csv":
|
|
21
|
+
source = pl.read_csv(source, separator=",")
|
|
22
|
+
case ".tsv":
|
|
23
|
+
source = pl.read_csv(source, separator="\t")
|
|
24
|
+
case _:
|
|
25
|
+
raise ValueError(f"{source} is not a valid csv or tsv file.")
|
|
26
|
+
|
|
27
|
+
self._data = source
|
|
28
|
+
self._output: ct.TabularOutput = output
|
|
29
|
+
|
|
30
|
+
def get_display(self) -> str:
|
|
31
|
+
|
|
32
|
+
header = "".join(f"<th>{escape(str(col))}</th>" for col in self._data.columns)
|
|
33
|
+
rows = "".join(
|
|
34
|
+
"<tr>"
|
|
35
|
+
+ "".join(f"<td>{escape(str(value))}</td>" for value in row)
|
|
36
|
+
+ "</tr>"
|
|
37
|
+
for row in self._data.iter_rows()
|
|
38
|
+
)
|
|
39
|
+
return f'<table class="dataframe"><thead><tr>{header}</tr></thead><tbody>{rows}</tbody></table>'
|
|
40
|
+
|
|
41
|
+
def get_buttons(self, title: str) -> Iterable[DownloadButton]:
|
|
42
|
+
|
|
43
|
+
separator = "\t" if self._output == "tsv" else ","
|
|
44
|
+
button = DownloadButton.from_format(
|
|
45
|
+
title,
|
|
46
|
+
self._data.write_csv(separator=separator).encode("utf-8"),
|
|
47
|
+
self._output,
|
|
48
|
+
)
|
|
49
|
+
return (button,)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from typing import Literal, Protocol
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import polars as pl
|
|
6
|
+
from matplotlib import figure
|
|
7
|
+
|
|
8
|
+
from ..html_helpers import DownloadButton
|
|
9
|
+
|
|
10
|
+
type TabularSource = pl.DataFrame | pd.DataFrame | str
|
|
11
|
+
type TabularOutput = Literal["csv", "tsv"]
|
|
12
|
+
|
|
13
|
+
type PlotSource = figure.Figure
|
|
14
|
+
type PlotOutput = Literal["png", "svg", "svg+png"]
|
|
15
|
+
|
|
16
|
+
type ConfigSource = dict | str
|
|
17
|
+
type ConfigOutput = Literal["json", "yaml"]
|
|
18
|
+
|
|
19
|
+
type ValueSource = str | float
|
|
20
|
+
type ValueOutput = Literal["tsv"]
|
|
21
|
+
|
|
22
|
+
type ImageSource = str
|
|
23
|
+
type ImageOutput = Literal["png", "svg"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Adapter(Protocol):
|
|
27
|
+
def get_display(self) -> str: ...
|
|
28
|
+
|
|
29
|
+
def get_buttons(self, title: str) -> Iterable[DownloadButton]: ...
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from html import escape
|
|
3
|
+
|
|
4
|
+
import polars as pl
|
|
5
|
+
|
|
6
|
+
from ..html_helpers import DownloadButton
|
|
7
|
+
from . import types as ct
|
|
8
|
+
from .tabular import TabularAdapter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ValueAdapter:
|
|
12
|
+
def __init__(self, source: ct.ValueSource, output: ct.ValueOutput):
|
|
13
|
+
self._value = source
|
|
14
|
+
self._output: ct.ValueOutput = output
|
|
15
|
+
|
|
16
|
+
def get_display(self) -> str:
|
|
17
|
+
return escape(str(self._value))
|
|
18
|
+
|
|
19
|
+
def get_buttons(self, title: str) -> Iterable[DownloadButton]:
|
|
20
|
+
df = pl.DataFrame({"stat": title, "value": self._value})
|
|
21
|
+
return TabularAdapter(df, self._output).get_buttons(title)
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from .. import html_helpers as hh
|
|
2
|
+
from ..adapters import (
|
|
3
|
+
ConfigAdapter,
|
|
4
|
+
ImageAdapter,
|
|
5
|
+
PlotAdapter,
|
|
6
|
+
TabularAdapter,
|
|
7
|
+
ValueAdapter,
|
|
8
|
+
)
|
|
9
|
+
from ..adapters import types as ct
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Entry:
|
|
13
|
+
"""Fundamental unit of a report, corresponding to an individual result.
|
|
14
|
+
|
|
15
|
+
An entry is a standardized way to display a result.
|
|
16
|
+
It is always composed of the same elements:
|
|
17
|
+
- A title (h3 size)
|
|
18
|
+
- A toggleable description of the result
|
|
19
|
+
- Some graphical representation of the result
|
|
20
|
+
- Download buttons for the data
|
|
21
|
+
|
|
22
|
+
Though it is possible to initialize an entry by providing all these
|
|
23
|
+
elements to the main class constructor, in most cases you should
|
|
24
|
+
use one of the data-type specific constructors.
|
|
25
|
+
The base constructor should be used only to create entries for
|
|
26
|
+
custom content types: pass any object implementing `get_display(self)
|
|
27
|
+
-> str` (the rendered HTML) and `get_buttons(self, title: str) ->
|
|
28
|
+
Iterable[DownloadButton]` (the download buttons).
|
|
29
|
+
|
|
30
|
+
See Also
|
|
31
|
+
--------
|
|
32
|
+
config : Create an entry from configs (dict, yaml, json).
|
|
33
|
+
image : Create an entry from an image (png, svg).
|
|
34
|
+
plot : Create an entry from a plot (matplotlib Figure).
|
|
35
|
+
tabular : Create an entry from tabular data (csv, tsv, df).
|
|
36
|
+
value : Create an entry from an individual value (str, float).
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
content : Adapter
|
|
41
|
+
Object responsible for rendering the display and download buttons
|
|
42
|
+
(see above for the required shape).
|
|
43
|
+
title : str
|
|
44
|
+
The title of the entry.
|
|
45
|
+
info : str
|
|
46
|
+
Description of the content to place in the collapsible info section.
|
|
47
|
+
Supports HTML tags for formatting (bold, italics, ...).
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, content: ct.Adapter, *, title: str, info: str):
|
|
52
|
+
self._content = content
|
|
53
|
+
self._title = title
|
|
54
|
+
self._info = info
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def config(
|
|
58
|
+
cls,
|
|
59
|
+
source: ct.ConfigSource,
|
|
60
|
+
*,
|
|
61
|
+
title: str,
|
|
62
|
+
info: str,
|
|
63
|
+
output: ct.ConfigOutput = "yaml",
|
|
64
|
+
) -> "Entry":
|
|
65
|
+
"""Create an entry from a config (dict, yaml, or json).
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
source : dict or str
|
|
70
|
+
The config to display as content. Can be a dict, or a path to a
|
|
71
|
+
yaml or json file.
|
|
72
|
+
title : str
|
|
73
|
+
The title of the entry.
|
|
74
|
+
info : str
|
|
75
|
+
Description of the content to place in the collapsible info section.
|
|
76
|
+
Supports HTML tags for formatting (bold, italics, ...).
|
|
77
|
+
output : {`yaml`, `json`}, optional
|
|
78
|
+
Format used for the download button generated for this data.
|
|
79
|
+
Default is `yaml`.
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
return Entry(ConfigAdapter(source, output), title=title, info=info)
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def image(
|
|
87
|
+
cls,
|
|
88
|
+
source: ct.ImageSource,
|
|
89
|
+
*,
|
|
90
|
+
title: str,
|
|
91
|
+
info: str,
|
|
92
|
+
data: ct.TabularSource | None = None,
|
|
93
|
+
scale: float = 1.0,
|
|
94
|
+
) -> "Entry":
|
|
95
|
+
"""Create an entry from an image (png, svg).
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
source : str
|
|
100
|
+
Path to a png or svg image file.
|
|
101
|
+
title : str
|
|
102
|
+
The title of the entry.
|
|
103
|
+
info : str
|
|
104
|
+
Description of the content to place in the collapsible info section.
|
|
105
|
+
Supports HTML tags for formatting (bold, italics, ...).
|
|
106
|
+
data : Tabular data, optional
|
|
107
|
+
Data used to generate the image. If provided, creates an additional
|
|
108
|
+
download button to fetch it as a tsv.
|
|
109
|
+
scale : float, optional
|
|
110
|
+
Factor applied to the image's natural width when displaying it
|
|
111
|
+
(e.g. `0.5` for half size). Only affects png images; the
|
|
112
|
+
downloaded file is always the original, unscaled source. Default
|
|
113
|
+
is `1.0`.
|
|
114
|
+
|
|
115
|
+
Note
|
|
116
|
+
----
|
|
117
|
+
If trying to embed a matplotlib Figure directly, use the `plot`
|
|
118
|
+
constructor instead.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
return Entry(ImageAdapter(source, data, scale), title=title, info=info)
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def plot(
|
|
125
|
+
cls,
|
|
126
|
+
source: ct.PlotSource,
|
|
127
|
+
*,
|
|
128
|
+
title: str,
|
|
129
|
+
info: str,
|
|
130
|
+
data: ct.TabularSource | None = None,
|
|
131
|
+
output: ct.PlotOutput = "svg+png",
|
|
132
|
+
dpi: int = 300,
|
|
133
|
+
) -> "Entry":
|
|
134
|
+
"""Create an entry from a plot (matplotlib Figure).
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
source : matplotlib Figure
|
|
139
|
+
The plot to display as content.
|
|
140
|
+
title : str
|
|
141
|
+
The title of the entry.
|
|
142
|
+
info : str
|
|
143
|
+
Description of the content to place in the collapsible info section.
|
|
144
|
+
Supports HTML tags for formatting (bold, italics, ...).
|
|
145
|
+
data : Tabular data, optional
|
|
146
|
+
Data used to generate the plot. If provided, creates an additional
|
|
147
|
+
download button to fetch it as a tsv.
|
|
148
|
+
output : {`svg`, `png`, `svg+png`}, optional
|
|
149
|
+
Output formats for which to create a download button.
|
|
150
|
+
Default is `svg+png`.
|
|
151
|
+
dpi : int, optional
|
|
152
|
+
Resolution used when rendering the png download. Default is `300`.
|
|
153
|
+
|
|
154
|
+
Note
|
|
155
|
+
----
|
|
156
|
+
If trying to embed an image file, use the `image` constructor.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
return Entry(PlotAdapter(source, output, data, dpi), title=title, info=info)
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def tabular(
|
|
163
|
+
cls,
|
|
164
|
+
source: ct.TabularSource,
|
|
165
|
+
*,
|
|
166
|
+
title: str,
|
|
167
|
+
info: str,
|
|
168
|
+
output: ct.TabularOutput = "tsv",
|
|
169
|
+
) -> "Entry":
|
|
170
|
+
"""Create an entry from tabular data (e.g. csv, tsv, df).
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
source : Tabular data
|
|
175
|
+
The tabular data to display as content. Can be a polars or
|
|
176
|
+
pandas DataFrame, or a path to a csv or tsv file.
|
|
177
|
+
title : str
|
|
178
|
+
The title of the entry.
|
|
179
|
+
info : str
|
|
180
|
+
Description of the content to place in the collapsible info section.
|
|
181
|
+
Supports HTML tags for formatting (bold, italics, ...).
|
|
182
|
+
output : {`tsv`, `csv`}, optional
|
|
183
|
+
Format used for the download button generated for this data.
|
|
184
|
+
Default is `tsv`.
|
|
185
|
+
|
|
186
|
+
Note
|
|
187
|
+
----
|
|
188
|
+
If trying to attach tabular data to a plot, use the `data` parameter
|
|
189
|
+
of the `plot` constructor instead.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
return Entry(TabularAdapter(source, output), title=title, info=info)
|
|
193
|
+
|
|
194
|
+
@classmethod
|
|
195
|
+
def value(
|
|
196
|
+
cls,
|
|
197
|
+
source: ct.ValueSource,
|
|
198
|
+
*,
|
|
199
|
+
title: str,
|
|
200
|
+
info: str,
|
|
201
|
+
output: ct.ValueOutput = "tsv",
|
|
202
|
+
) -> "Entry":
|
|
203
|
+
"""Create an entry from an individual value (str, float).
|
|
204
|
+
|
|
205
|
+
Parameters
|
|
206
|
+
----------
|
|
207
|
+
source : str or float
|
|
208
|
+
The value to display as content.
|
|
209
|
+
title : str
|
|
210
|
+
The title of the entry.
|
|
211
|
+
info : str
|
|
212
|
+
Description of the content to place in the collapsible info section.
|
|
213
|
+
Supports HTML tags for formatting (bold, italics, ...).
|
|
214
|
+
output : {`tsv`}, optional
|
|
215
|
+
Format used for the download button generated for this data.
|
|
216
|
+
Default is `tsv`.
|
|
217
|
+
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
return Entry(ValueAdapter(source, output), title=title, info=info)
|
|
221
|
+
|
|
222
|
+
def get_content(self) -> str:
|
|
223
|
+
html = hh.get_html(
|
|
224
|
+
self._content.get_display(),
|
|
225
|
+
self._title,
|
|
226
|
+
self._info,
|
|
227
|
+
self._content.get_buttons(self._title),
|
|
228
|
+
)
|
|
229
|
+
return f"# %%\ndisplay(HTML({html!r}))"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class Section:
|
|
2
|
+
"""Separator to group results into sections.
|
|
3
|
+
|
|
4
|
+
Inserts a title (h2) with highlighted background in the report.
|
|
5
|
+
|
|
6
|
+
Parameters
|
|
7
|
+
----------
|
|
8
|
+
title : str
|
|
9
|
+
The title of the section.
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, title: str):
|
|
14
|
+
self._title = title
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def title(self) -> str:
|
|
18
|
+
return self._title
|
|
19
|
+
|
|
20
|
+
def get_content(self) -> str:
|
|
21
|
+
"""Return a markdown cell in jupytext percent format.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
str
|
|
26
|
+
|
|
27
|
+
"""
|
|
28
|
+
return f"# %% [markdown]\n# ## {self.title}"
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import re
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
type DownloadFormat = Literal["png", "svg", "tsv", "csv", "json", "yaml"]
|
|
8
|
+
MIMES: dict[DownloadFormat, str] = {
|
|
9
|
+
"png": "image/png",
|
|
10
|
+
"svg": "image/svg+xml",
|
|
11
|
+
"tsv": "text/tab-separated-values",
|
|
12
|
+
"csv": "text/csv",
|
|
13
|
+
"json": "application/json",
|
|
14
|
+
"yaml": "application/yaml",
|
|
15
|
+
}
|
|
16
|
+
CSS_STYLE: str = (
|
|
17
|
+
"h2 { background: #eef1f5; padding: 8px; border-radius: 6px; }"
|
|
18
|
+
".pt-download-btn {"
|
|
19
|
+
" margin-left: 6px; padding: 2px 10px; border: 1px solid #ccc; background: white;"
|
|
20
|
+
" border-radius: 4px; text-decoration: none; font-size: 0.8em; color: #333;"
|
|
21
|
+
"}"
|
|
22
|
+
".pt-buttons .pt-download-btn:link,"
|
|
23
|
+
".pt-buttons .pt-download-btn:visited,"
|
|
24
|
+
".pt-buttons .pt-download-btn:hover,"
|
|
25
|
+
".pt-buttons .pt-download-btn:focus { text-decoration: none; }"
|
|
26
|
+
".pt-row { margin-bottom: 10px; }"
|
|
27
|
+
".pt-row-top { display: flex; align-items: baseline; gap: 8px; }"
|
|
28
|
+
".pt-row-title { min-width: 0; }"
|
|
29
|
+
".pt-row-title h3 { display: inline; margin: 0; }"
|
|
30
|
+
".pt-info-checkbox { position: absolute; opacity: 0; width: 0; height: 0; }"
|
|
31
|
+
".pt-info-icon {"
|
|
32
|
+
" display: inline-flex; align-items: center; justify-content: center;"
|
|
33
|
+
" vertical-align: middle; margin-left: 8px; cursor: pointer;"
|
|
34
|
+
" width: 17px; height: 17px; border-radius: 50%;"
|
|
35
|
+
" border: 1.3px solid #9aa4b0; color: #6b7280; font-size: 0.68rem;"
|
|
36
|
+
" font-weight: 700; font-family: Georgia, 'Times New Roman', serif;"
|
|
37
|
+
" line-height: 1;"
|
|
38
|
+
"}"
|
|
39
|
+
".pt-info-checkbox:checked + .pt-info-icon {"
|
|
40
|
+
" background: #eef1f5; border-color: #6b7280; color: #1a1d21;"
|
|
41
|
+
"}"
|
|
42
|
+
".pt-buttons { flex-shrink: 0; white-space: nowrap; margin-left: auto; }"
|
|
43
|
+
".pt-desc-body {"
|
|
44
|
+
" display: none; margin-top: 8px; color: #555; font-size: 0.9em; line-height: 1.5;"
|
|
45
|
+
"}"
|
|
46
|
+
".pt-row:has(.pt-info-checkbox:checked) .pt-desc-body { display: block; }"
|
|
47
|
+
# not needed yet: no figures in descriptions currently
|
|
48
|
+
# "/* .pt-desc-body svg { max-width: 100%; height: auto; } */"
|
|
49
|
+
"div.output_subarea { padding-top: 0 !important; max-width: 100% !important; }"
|
|
50
|
+
".output_html svg { display: block; margin: 0 auto; }"
|
|
51
|
+
".output_html img { display: block !important; margin: 0 auto !important; }"
|
|
52
|
+
".output_html table.dataframe { margin-left: auto; margin-right: auto; }"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class DownloadButton:
|
|
58
|
+
label: str
|
|
59
|
+
content: bytes
|
|
60
|
+
mime: str
|
|
61
|
+
file_name: str
|
|
62
|
+
|
|
63
|
+
_EMBED_TEMPLATE = (
|
|
64
|
+
'<a href="data:{mime};base64,{content}" '
|
|
65
|
+
'download="{file_name}" '
|
|
66
|
+
'class="pt-download-btn">'
|
|
67
|
+
"{label}</a>"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def get_embeddable(self) -> str:
|
|
71
|
+
"""Create the raw html string for an individual content download button."""
|
|
72
|
+
|
|
73
|
+
return self._EMBED_TEMPLATE.format(
|
|
74
|
+
mime=self.mime,
|
|
75
|
+
content=base64.b64encode(self.content).decode("ascii"),
|
|
76
|
+
file_name=self.file_name,
|
|
77
|
+
label=self.label,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_format(
|
|
82
|
+
cls,
|
|
83
|
+
title: str,
|
|
84
|
+
content: bytes,
|
|
85
|
+
format: DownloadFormat,
|
|
86
|
+
) -> "DownloadButton":
|
|
87
|
+
"""Build a DownloadButton for the given format."""
|
|
88
|
+
return cls(format.upper(), content, MIMES[format], f"{title}.{format}")
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _slugify(text: str) -> str:
|
|
92
|
+
return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_html(
|
|
96
|
+
display: str,
|
|
97
|
+
title: str,
|
|
98
|
+
info: str,
|
|
99
|
+
buttons: Iterable[DownloadButton],
|
|
100
|
+
) -> str:
|
|
101
|
+
|
|
102
|
+
downloads_div = (
|
|
103
|
+
f'<div class="pt-buttons">{"".join(b.get_embeddable() for b in buttons)}</div>'
|
|
104
|
+
if buttons
|
|
105
|
+
else ""
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
description_bar = (
|
|
109
|
+
'<div class="pt-row">'
|
|
110
|
+
'<div class="pt-row-top">'
|
|
111
|
+
'<div class="pt-row-title">'
|
|
112
|
+
f"<h3>{title}</h3>"
|
|
113
|
+
f'<input type="checkbox" id="pt-info-{_slugify(title)}" class="pt-info-checkbox">'
|
|
114
|
+
f'<label for="pt-info-{_slugify(title)}" class="pt-info-icon" title="Description">i</label>'
|
|
115
|
+
"</div>"
|
|
116
|
+
f"{downloads_div}"
|
|
117
|
+
"</div>"
|
|
118
|
+
f'<div class="pt-desc-body">{info}</div>'
|
|
119
|
+
"</div>"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return f"{description_bar}{display}"
|
|
File without changes
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import jupytext
|
|
4
|
+
from nbclient import NotebookClient
|
|
5
|
+
from nbconvert import HTMLExporter
|
|
6
|
+
from traitlets.config import Config
|
|
7
|
+
|
|
8
|
+
from . import html_helpers as hh
|
|
9
|
+
from .components import types as ct
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Report:
|
|
13
|
+
"""Container object to populate with results.
|
|
14
|
+
|
|
15
|
+
A report can be populated with any provided or user-defined component.
|
|
16
|
+
An object is a component if it implements the `get_content` method,
|
|
17
|
+
which returns a string representing one or more jupyter notebook cells
|
|
18
|
+
in jupytext percent format.
|
|
19
|
+
|
|
20
|
+
Components can be added using the `add` method or the overloaded `__add__`.
|
|
21
|
+
Components are rendered in the order they were added to the report.
|
|
22
|
+
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
title : str
|
|
26
|
+
Title displayed at the top of the report (h1).
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, title: str):
|
|
31
|
+
self._title = title
|
|
32
|
+
self._components: list[ct.Component] = []
|
|
33
|
+
|
|
34
|
+
def add(self, component: ct.Component) -> "Report":
|
|
35
|
+
"""Add a component to the report.
|
|
36
|
+
|
|
37
|
+
Returns self to allow chaining add operations.
|
|
38
|
+
|
|
39
|
+
Parameters
|
|
40
|
+
----------
|
|
41
|
+
component : object implementing `get_content(self) -> str`, see constructor.
|
|
42
|
+
An object to add at the end of the current queue of components.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
Report
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
self._components.append(component)
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
__add__ = add
|
|
54
|
+
|
|
55
|
+
def make_report(self, path: str) -> None:
|
|
56
|
+
"""Generate the rendered HTML report at the provided path.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
path : str
|
|
61
|
+
Path where to generate the report.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
content = "# %%\nfrom IPython.display import HTML\n\n"
|
|
66
|
+
content += f"# %% [markdown]\n# # {self._title}\n\n"
|
|
67
|
+
content += "\n\n".join(c.get_content() for c in self._components)
|
|
68
|
+
nb = jupytext.reads(content, fmt="py:percent")
|
|
69
|
+
|
|
70
|
+
kernel_config = Config()
|
|
71
|
+
if sys.platform != "win32":
|
|
72
|
+
# local unix socket instead of tcp: silences ipykernel's
|
|
73
|
+
# unencrypted-transport warning, since ipc never leaves the machine
|
|
74
|
+
kernel_config.KernelManager.transport = "ipc"
|
|
75
|
+
|
|
76
|
+
NotebookClient(nb, config=kernel_config).execute()
|
|
77
|
+
|
|
78
|
+
config = Config()
|
|
79
|
+
config.HTMLExporter.exclude_input = True
|
|
80
|
+
config.HTMLExporter.exclude_input_prompt = True
|
|
81
|
+
config.HTMLExporter.exclude_output_prompt = True
|
|
82
|
+
config.HTMLExporter.template_name = "classic"
|
|
83
|
+
|
|
84
|
+
exporter = HTMLExporter(config=config)
|
|
85
|
+
html, _ = exporter.from_notebook_node(nb)
|
|
86
|
+
html = html.replace("</head>", f"<style>{hh.CSS_STYLE}</style></head>")
|
|
87
|
+
|
|
88
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
89
|
+
f.write(html)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "paperthin"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python library to easily create HTML reports"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "BSD-3-Clause"
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Stefano Cretti", email = "stefanocretti99@gmail.com" },
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: BSD License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Software Development :: Libraries",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"ipykernel>=7.3",
|
|
27
|
+
"jupytext>=1.19",
|
|
28
|
+
"matplotlib>=3.11",
|
|
29
|
+
"nbclient>=0.11",
|
|
30
|
+
"nbconvert>=7.17",
|
|
31
|
+
"pandas>=3.0",
|
|
32
|
+
"pillow>=12.3",
|
|
33
|
+
"polars>=1.43",
|
|
34
|
+
"pyyaml>=6.0",
|
|
35
|
+
"traitlets>=5.15",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
dev = ["ruff", "pre-commit"]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://github.com/StefanoCretti/paperthin"
|
|
43
|
+
Repository = "https://github.com/StefanoCretti/paperthin"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["paperthin"]
|
|
47
|
+
|
|
48
|
+
[tool.hatch.version]
|
|
49
|
+
source = "vcs"
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.hooks.vcs]
|
|
52
|
+
version-file = "paperthin/_version.py"
|
|
53
|
+
|
|
54
|
+
[tool.ruff]
|
|
55
|
+
target-version = "py312"
|