ipyrowtable 0.1.1__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.
- ipyrowtable/__init__.py +53 -0
- ipyrowtable/columns.py +265 -0
- ipyrowtable/examples/__init__.py +4 -0
- ipyrowtable/examples/conduction.py +340 -0
- ipyrowtable/figure.py +64 -0
- ipyrowtable/formatting.py +40 -0
- ipyrowtable/layout.py +16 -0
- ipyrowtable/persistence.py +160 -0
- ipyrowtable/table.py +785 -0
- ipyrowtable/units.py +71 -0
- ipyrowtable-0.1.1.dist-info/METADATA +303 -0
- ipyrowtable-0.1.1.dist-info/RECORD +14 -0
- ipyrowtable-0.1.1.dist-info/WHEEL +4 -0
- ipyrowtable-0.1.1.dist-info/licenses/LICENSE +21 -0
ipyrowtable/figure.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""LiveFigure: a matplotlib figure kept in sync with a RowTable."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import io
|
|
6
|
+
|
|
7
|
+
import ipywidgets as widgets
|
|
8
|
+
|
|
9
|
+
__all__ = ["LiveFigure"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LiveFigure(widgets.Image):
|
|
13
|
+
"""A matplotlib figure redrawn from a table's results after every change.
|
|
14
|
+
|
|
15
|
+
Pass ``draw=function(fig, results)``, or subclass and implement ``draw(self, fig, results)``.
|
|
16
|
+
The figure is hidden (keeping its space) while the table's inputs are invalid. Errors
|
|
17
|
+
raised while drawing are shown under the table.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
table : RowTable
|
|
22
|
+
draw : callable(matplotlib.figure.Figure, TableResults), optional
|
|
23
|
+
size : (width, height) in inches; shown at 100 px per inch
|
|
24
|
+
dpi : render resolution; 200 is crisp on high-DPI screens
|
|
25
|
+
facecolor : figure background
|
|
26
|
+
tight : grow or trim the canvas to fit everything drawn (e.g. labels outside the axes)
|
|
27
|
+
|
|
28
|
+
Needs matplotlib: ``pip install ipyrowtable[plot]``.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, table, draw=None, size=(6.8, 4.4), dpi=200, facecolor="white",
|
|
32
|
+
tight=True):
|
|
33
|
+
super().__init__(format="png")
|
|
34
|
+
self.add_class("ipyrowtable-figure")
|
|
35
|
+
self.table = table
|
|
36
|
+
self.figsize, self.dpi, self.facecolor, self.tight = size, dpi, facecolor, tight
|
|
37
|
+
if draw is not None:
|
|
38
|
+
self.draw = draw
|
|
39
|
+
table.on_update(self._render)
|
|
40
|
+
|
|
41
|
+
def draw(self, fig, results):
|
|
42
|
+
raise NotImplementedError("Pass draw=... or subclass LiveFigure and implement draw().")
|
|
43
|
+
|
|
44
|
+
def _render(self, results):
|
|
45
|
+
try:
|
|
46
|
+
from matplotlib.figure import Figure # no pyplot: nothing leaks into cell output
|
|
47
|
+
except ImportError as exc:
|
|
48
|
+
raise ImportError(
|
|
49
|
+
"LiveFigure needs matplotlib: pip install ipyrowtable[plot]"
|
|
50
|
+
) from exc
|
|
51
|
+
if results is None:
|
|
52
|
+
self.layout.visibility = "hidden"
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
fig = Figure(figsize=self.figsize, facecolor=self.facecolor)
|
|
56
|
+
self.draw(fig, results)
|
|
57
|
+
buf = io.BytesIO()
|
|
58
|
+
fig.savefig(buf, format="png", dpi=self.dpi, facecolor=self.facecolor,
|
|
59
|
+
bbox_inches="tight" if self.tight else None, pad_inches=0.12)
|
|
60
|
+
png = buf.getvalue()
|
|
61
|
+
width_px = int.from_bytes(png[16:20], "big") # from the PNG header
|
|
62
|
+
self.layout.width = f"{round(width_px * 100 / self.dpi)}px"
|
|
63
|
+
self.value = png
|
|
64
|
+
self.layout.visibility = "visible"
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Number formatting helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
__all__ = ["fmt_sig"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def fmt_sig(x, sig: int = 4) -> str:
|
|
11
|
+
"""Format to `sig` significant figures with thousands separators.
|
|
12
|
+
|
|
13
|
+
Scientific notation is used only for very large or very small magnitudes.
|
|
14
|
+
|
|
15
|
+
>>> fmt_sig(6050.0), fmt_sig(0.8), fmt_sig(1.5e-6)
|
|
16
|
+
('6,050', '0.8000', '1.500e-06')
|
|
17
|
+
"""
|
|
18
|
+
if x == 0 or not np.isfinite(x):
|
|
19
|
+
return f"{x:g}"
|
|
20
|
+
magnitude = int(np.floor(np.log10(abs(x))))
|
|
21
|
+
if -3 <= magnitude < 9:
|
|
22
|
+
return f"{x:,.{max(sig - 1 - magnitude, 0)}f}"
|
|
23
|
+
return f"{x:.{sig - 1}e}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def formatter(fmt):
|
|
27
|
+
"""Turn a format spec (None, a format string like "{:.2f}", or a callable) into a callable."""
|
|
28
|
+
if fmt is None:
|
|
29
|
+
return fmt_sig
|
|
30
|
+
return fmt if callable(fmt) else fmt.format
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def tidy(x: float) -> float:
|
|
34
|
+
"""Round a converted input value to 6 significant figures for display in an input box."""
|
|
35
|
+
return float(f"{x:.6g}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def right_aligned(text: str) -> str:
|
|
39
|
+
"""HTML for a right-aligned output cell."""
|
|
40
|
+
return f"<div style='text-align:right; padding-right:6px'>{text}</div>"
|
ipyrowtable/layout.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Layout helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ipywidgets as widgets
|
|
6
|
+
|
|
7
|
+
__all__ = ["side_by_side"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def side_by_side(*items, gap: str = "24px") -> widgets.HBox:
|
|
11
|
+
"""Lay widgets out in a row that wraps onto new lines when the notebook is narrow."""
|
|
12
|
+
for item in items[1:]:
|
|
13
|
+
item.layout.margin = f"0 0 0 {gap}"
|
|
14
|
+
return widgets.HBox(
|
|
15
|
+
list(items), layout=widgets.Layout(flex_flow="row wrap", align_items="flex-start")
|
|
16
|
+
)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Saving table inputs to a JSON file between sessions.
|
|
2
|
+
|
|
3
|
+
A notebook's kernel can't write the notebook's own metadata (only the front end can, and
|
|
4
|
+
front ends differ), so inputs are kept in a small JSON file instead, by default next to the
|
|
5
|
+
notebook: ``<notebook name>.ipyrowtable.json``. One file holds any number of tables, each
|
|
6
|
+
under its own key:
|
|
7
|
+
|
|
8
|
+
{
|
|
9
|
+
"ipyrowtable": 1,
|
|
10
|
+
"tables": {
|
|
11
|
+
"pipes": {
|
|
12
|
+
"saved": "2026-09-27T13:10:42-07:00",
|
|
13
|
+
"units": "Metric",
|
|
14
|
+
"rows": [{"material": "Commercial steel", "length": 25.0, "diameter": 0.065}],
|
|
15
|
+
"edges": {"pressure": [400000.0, null]},
|
|
16
|
+
"params": {"flow": 0.005556}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Values are in base units (the units your compute function uses), so they don't depend on
|
|
22
|
+
which unit system was showing. Set the environment variable IPYROWTABLE_PERSIST=off to turn
|
|
23
|
+
saving and restoring off everywhere, e.g. for batch runs that must start from known inputs.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import datetime
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import shutil
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
"FORMAT_VERSION",
|
|
36
|
+
"StateFile",
|
|
37
|
+
"StateFileError",
|
|
38
|
+
"default_state_file",
|
|
39
|
+
"notebook_path",
|
|
40
|
+
"persistence_enabled",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
FORMAT_VERSION = 1
|
|
44
|
+
ENV_SWITCH = "IPYROWTABLE_PERSIST"
|
|
45
|
+
_OFF = {"0", "off", "false", "no"}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class StateFileError(Exception):
|
|
49
|
+
"""The state file exists but can't be used."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def persistence_enabled() -> bool:
|
|
53
|
+
"""False when the environment variable IPYROWTABLE_PERSIST is 0, off, false or no."""
|
|
54
|
+
return os.environ.get(ENV_SWITCH, "").strip().lower() not in _OFF
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def notebook_path() -> Path | None:
|
|
58
|
+
"""Best guess at the running notebook's file, or None.
|
|
59
|
+
|
|
60
|
+
VS Code puts the path in the kernel's `__vsc_ipynb_file__` variable; Jupyter Server sets
|
|
61
|
+
the JPY_SESSION_NAME environment variable for the kernels it starts.
|
|
62
|
+
"""
|
|
63
|
+
try:
|
|
64
|
+
from IPython import get_ipython
|
|
65
|
+
|
|
66
|
+
shell = get_ipython()
|
|
67
|
+
if shell is not None:
|
|
68
|
+
path = shell.user_ns.get("__vsc_ipynb_file__")
|
|
69
|
+
if path:
|
|
70
|
+
return Path(path)
|
|
71
|
+
except Exception: # pragma: no cover - IPython missing or unusual
|
|
72
|
+
pass
|
|
73
|
+
session = os.environ.get("JPY_SESSION_NAME")
|
|
74
|
+
if session:
|
|
75
|
+
path = Path(session)
|
|
76
|
+
if path.is_absolute() and path.exists():
|
|
77
|
+
return path
|
|
78
|
+
candidate = Path.cwd() / path.name # the kernel usually runs in the notebook's folder
|
|
79
|
+
if candidate.exists():
|
|
80
|
+
return candidate
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def default_state_file() -> Path:
|
|
85
|
+
"""`<notebook name>.ipyrowtable.json` beside the notebook, or `ipyrowtable.json` in the
|
|
86
|
+
working directory when the notebook can't be identified."""
|
|
87
|
+
notebook = notebook_path()
|
|
88
|
+
if notebook is not None and notebook.suffix == ".ipynb":
|
|
89
|
+
return notebook.with_name(f"{notebook.stem}.ipyrowtable.json")
|
|
90
|
+
return Path.cwd() / "ipyrowtable.json"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class StateFile:
|
|
94
|
+
"""A JSON file holding the saved inputs of one or more tables."""
|
|
95
|
+
|
|
96
|
+
def __init__(self, path):
|
|
97
|
+
self.path = Path(path)
|
|
98
|
+
|
|
99
|
+
def _read(self) -> dict:
|
|
100
|
+
try:
|
|
101
|
+
data = json.loads(self.path.read_text(encoding="utf-8"))
|
|
102
|
+
except FileNotFoundError:
|
|
103
|
+
return {"ipyrowtable": FORMAT_VERSION, "tables": {}}
|
|
104
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
105
|
+
raise StateFileError(f"{self.path.name} can't be read ({exc})") from exc
|
|
106
|
+
if not isinstance(data, dict) or not isinstance(data.get("tables"), dict):
|
|
107
|
+
raise StateFileError(f"{self.path.name} isn't an ipyrowtable file")
|
|
108
|
+
version = data.get("ipyrowtable")
|
|
109
|
+
if not isinstance(version, int) or version > FORMAT_VERSION:
|
|
110
|
+
raise StateFileError(
|
|
111
|
+
f"{self.path.name} was saved in format {version!r}, newer than this "
|
|
112
|
+
f"version of ipyrowtable understands ({FORMAT_VERSION})"
|
|
113
|
+
)
|
|
114
|
+
return data
|
|
115
|
+
|
|
116
|
+
def load(self, key: str) -> dict | None:
|
|
117
|
+
"""The saved state for `key`, or None if there isn't one.
|
|
118
|
+
|
|
119
|
+
Raises StateFileError if the file exists but can't be used.
|
|
120
|
+
"""
|
|
121
|
+
return self._read()["tables"].get(key)
|
|
122
|
+
|
|
123
|
+
def save(self, key: str, state: dict) -> str:
|
|
124
|
+
"""Store `state` under `key`, keeping other tables' entries. Returns the timestamp.
|
|
125
|
+
|
|
126
|
+
An unusable existing file is replaced; call `backup()` first to keep a copy.
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
data = self._read()
|
|
130
|
+
except StateFileError:
|
|
131
|
+
data = {"ipyrowtable": FORMAT_VERSION, "tables": {}}
|
|
132
|
+
saved = datetime.datetime.now().astimezone().isoformat(timespec="seconds")
|
|
133
|
+
data["ipyrowtable"] = FORMAT_VERSION
|
|
134
|
+
data["tables"][key] = {"saved": saved, **state}
|
|
135
|
+
text = json.dumps(data, indent=2, ensure_ascii=False, allow_nan=False)
|
|
136
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
tmp = self.path.with_name(self.path.name + ".tmp")
|
|
138
|
+
tmp.write_text(text + "\n", encoding="utf-8")
|
|
139
|
+
os.replace(tmp, self.path) # atomic: a crash never leaves a half-written file
|
|
140
|
+
return saved
|
|
141
|
+
|
|
142
|
+
def backup(self) -> Path | None:
|
|
143
|
+
"""Copy the file to `<name>.bak` (if it exists) and return the copy's path."""
|
|
144
|
+
if not self.path.exists():
|
|
145
|
+
return None
|
|
146
|
+
target = self.path.with_name(self.path.name + ".bak")
|
|
147
|
+
shutil.copy2(self.path, target)
|
|
148
|
+
return target
|
|
149
|
+
|
|
150
|
+
def remove(self, key: str) -> bool:
|
|
151
|
+
"""Forget the saved state for `key`. Returns True if there was one."""
|
|
152
|
+
data = self._read()
|
|
153
|
+
if key not in data["tables"]:
|
|
154
|
+
return False
|
|
155
|
+
del data["tables"][key]
|
|
156
|
+
text = json.dumps(data, indent=2, ensure_ascii=False, allow_nan=False)
|
|
157
|
+
tmp = self.path.with_name(self.path.name + ".tmp")
|
|
158
|
+
tmp.write_text(text + "\n", encoding="utf-8")
|
|
159
|
+
os.replace(tmp, self.path)
|
|
160
|
+
return True
|