table3 0.3.0__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.
table3/__init__.py ADDED
@@ -0,0 +1,153 @@
1
+ """Paged, theme-aware tables in notebook output.
2
+
3
+ ``table3.html(data)`` is a table of up to *n* rows shown ten at a time, with
4
+ Previous / page / Next buttons and, for wide data, as many columns as fit
5
+ the cell plus arrows to the rest: what a package returns from its frames'
6
+ ``_repr_html_`` so they print as R notebooks print data frames.
7
+ ``table3.show(data)`` displays the same table and returns *data* unchanged.
8
+
9
+ *data* is a polars DataFrame, LazyFrame or Series, a pandas DataFrame or
10
+ Series, or any object with a ``__table3__`` method::
11
+
12
+ def __table3__(self, n: int) -> tuple[polars.DataFrame, int | None, str | None]:
13
+ # the first n rows, the whole row count (None if not counted),
14
+ # and a caption shown above the table (None for none)
15
+
16
+ so a package's own frames show without table3 importing that package.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from typing import Any
22
+
23
+ import polars as pl
24
+
25
+ from table3.__version__ import __version__
26
+ from table3._html import table_html
27
+
28
+ __all__ = ["__version__", "html", "show"]
29
+
30
+
31
+ def _from_pandas(df: Any) -> pl.DataFrame:
32
+ try:
33
+ return pl.from_pandas(df)
34
+ except Exception: # no pyarrow, or a dtype polars can't take: show the text
35
+ return pl.DataFrame({str(c): df[c].astype(str).tolist() for c in df.columns})
36
+
37
+
38
+ def _parts(data: Any, n: int) -> tuple[pl.DataFrame, int | None, str | None]:
39
+ if hasattr(type(data), "__table3__"):
40
+ return data.__table3__(n)
41
+ if isinstance(data, pl.Series):
42
+ data = data.to_frame()
43
+ if isinstance(data, pl.LazyFrame):
44
+ head, count = pl.collect_all([data.head(n), data.select(pl.len())])
45
+ return head, int(count.item()), None
46
+ if isinstance(data, pl.DataFrame):
47
+ return data.head(n), data.height, None
48
+ # pandas, duck-typed so table3 never imports it
49
+ if hasattr(data, "to_frame") and hasattr(data, "iloc") and not hasattr(data, "columns"):
50
+ data = data.to_frame()
51
+ if hasattr(data, "columns") and hasattr(data, "iloc"):
52
+ return _from_pandas(data.head(n)), len(data), None
53
+ raise TypeError(
54
+ f"table3 can't show a {type(data).__name__}: pass a polars or pandas "
55
+ "DataFrame or Series, or an object with a __table3__ method"
56
+ )
57
+
58
+
59
+ def _positive_int(value: Any) -> bool:
60
+ return isinstance(value, int) and not isinstance(value, bool) and value > 0
61
+
62
+
63
+ def _check(n: Any, pages: Any, page_size: Any, height: Any, digits: Any) -> None:
64
+ if not isinstance(n, int) or isinstance(n, bool) or n < 0:
65
+ raise ValueError("table3: n must be a non-negative integer")
66
+ if not isinstance(pages, bool):
67
+ raise TypeError("table3: pages must be True or False")
68
+ if not _positive_int(page_size):
69
+ raise ValueError("table3: page_size must be a positive integer")
70
+ if height is not None and not _positive_int(height):
71
+ raise ValueError("table3: height must be a positive integer or None")
72
+ if digits is not None and digits != "full" and not _positive_int(digits):
73
+ raise ValueError('table3: digits must be a positive integer, "full", or None')
74
+
75
+
76
+ def html(
77
+ data: Any,
78
+ *,
79
+ n: int = 1000,
80
+ pages: bool = True,
81
+ page_size: int = 10,
82
+ height: int | None = 300,
83
+ digits: int | str | None = None,
84
+ caption: str | None = None,
85
+ ) -> str:
86
+ """The table's HTML for the first *n* rows of *data*.
87
+
88
+ *page_size* rows show per page. ``pages=False``, or a notebook that
89
+ doesn't run output scripts, shows every row instead in a box *height*
90
+ pixels tall that scrolls; ``height=None`` grows it to fit.
91
+
92
+ Floats print as polars' text preview prints them (up to six decimals,
93
+ following ``pl.Config(float_precision=)``) unless *digits* says
94
+ otherwise: ``digits=6`` keeps six significant digits, as R's
95
+ ``signif()`` does, so small numbers keep their precision (0.000123457);
96
+ ``digits="full"`` shows every stored digit. *caption* replaces the one
97
+ ``__table3__`` gives.
98
+ """
99
+ _check(n, pages, page_size, height, digits)
100
+ df, total, own_caption = _parts(data, n)
101
+ return table_html(
102
+ df,
103
+ total,
104
+ own_caption if caption is None else caption,
105
+ pages=pages,
106
+ page_size=page_size,
107
+ height=height,
108
+ digits=digits,
109
+ )
110
+
111
+
112
+ def _text(data: Any, n: int, digits: int | str | None = None) -> str:
113
+ df, total, caption = _parts(data, n)
114
+ # Polars prints no significant digits; digits=k applies to the table only.
115
+ full = {"fmt_float": "full"} if digits == "full" else {}
116
+ with pl.Config(tbl_rows=max(df.height, 1), tbl_hide_dataframe_shape=True, **full):
117
+ lines = [f"shape: ({'?' if total is None else total}, {df.width})", repr(df)]
118
+ if caption:
119
+ lines.insert(0, caption)
120
+ if total is None or total > df.height:
121
+ lines.append(f"… first {df.height:,} rows")
122
+ return "\n".join(lines)
123
+
124
+
125
+ def show(
126
+ data: Any,
127
+ *,
128
+ n: int = 1000,
129
+ pages: bool = True,
130
+ page_size: int = 10,
131
+ height: int | None = 300,
132
+ digits: int | str | None = None,
133
+ caption: str | None = None,
134
+ ) -> Any:
135
+ """Show up to *n* rows of *data* in a paged table and return *data*.
136
+
137
+ Arguments as in ``html()``. Outside a notebook it prints the rows as
138
+ text. As a cell's last line the notebook also prints *data* itself; end
139
+ the line with ``;`` to hide it.
140
+ """
141
+ _check(n, pages, page_size, height, digits)
142
+ try:
143
+ from IPython import get_ipython
144
+ from IPython.display import HTML, display
145
+ except ImportError:
146
+ get_ipython = None
147
+ if get_ipython is not None and get_ipython() is not None:
148
+ options = {"n": n, "pages": pages, "page_size": page_size, "height": height}
149
+ display(HTML(html(data, digits=digits, caption=caption, **options)))
150
+ else:
151
+ print(_text(data, n, digits))
152
+ return data
153
+
table3/__version__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
table3/_format.py ADDED
@@ -0,0 +1,81 @@
1
+ """Cell text: values, types and floats as polars' text preview prints them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html as _html
6
+
7
+ import polars as pl
8
+
9
+ MAX_CHARS = 40 # longer values are cut, with the full value on hover
10
+
11
+
12
+ def escape(v: object) -> str:
13
+ if v is None:
14
+ return "null"
15
+ return _html.escape(str(v))
16
+
17
+
18
+ def cell(v: object) -> str:
19
+ text = "null" if v is None else str(v)
20
+ if len(text) <= MAX_CHARS:
21
+ return escape(v)
22
+ short = _html.escape(text[: MAX_CHARS - 1]) + "…"
23
+ return f"<span title='{_html.escape(text)}'>{short}</span>"
24
+
25
+
26
+ def dtype_short(t: pl.DataType) -> str:
27
+ """Polars-style short dtype name ("i64"); falls back to str(t)."""
28
+ try:
29
+ from polars._plr import dtype_str_repr
30
+
31
+ return dtype_str_repr(t)
32
+ except Exception:
33
+ return str(t)
34
+
35
+
36
+ def significant(v: float, digits: int) -> str:
37
+ """*v* to *digits* significant digits, as R's ``signif()`` keeps them.
38
+
39
+ Written as polars writes floats: "2.0" not "2", "1.5e-7" not "1.5e-07".
40
+ """
41
+ if v != v:
42
+ return "NaN"
43
+ if v in (float("inf"), float("-inf")):
44
+ return "inf" if v > 0 else "-inf"
45
+ text = format(v, f".{digits}g")
46
+ if "e" in text:
47
+ mantissa, exponent = text.split("e")
48
+ return f"{mantissa}e{int(exponent)}"
49
+ return text if "." in text else text + ".0"
50
+
51
+
52
+ def float_text(s: pl.Series, digits: int | str | None = None) -> list[object]:
53
+ """Floats as polars' text preview prints them ("3.3", "1.2346e8").
54
+
55
+ Read off polars' own printer, so ``pl.Config(float_precision=)`` applies.
56
+ *digits* = k keeps k significant digits instead; "full" every stored digit.
57
+ """
58
+ if isinstance(digits, int):
59
+ return [None if v is None else significant(v, digits) for v in s.to_list()]
60
+ full = {"fmt_float": "full"} if digits == "full" else {}
61
+ with pl.Config(
62
+ tbl_formatting="NOTHING",
63
+ tbl_hide_column_names=True,
64
+ tbl_hide_column_data_types=True,
65
+ tbl_hide_dataframe_shape=True,
66
+ tbl_rows=-1,
67
+ tbl_width_chars=10_000,
68
+ **full,
69
+ ):
70
+ lines = [line.strip() for line in repr(s.to_frame()).splitlines()]
71
+ if len(lines) != len(s):
72
+ return s.to_list()
73
+ return [None if v is None else t for v, t in zip(s.to_list(), lines)]
74
+
75
+
76
+ def display_rows(df: pl.DataFrame, digits: int | str | None = None) -> list[tuple]:
77
+ cols = [
78
+ float_text(s, digits) if s.dtype.is_float() else s.to_list()
79
+ for s in df.get_columns()
80
+ ]
81
+ return list(zip(*cols))
table3/_html.py ADDED
@@ -0,0 +1,125 @@
1
+ """The table's HTML: inline styles only.
2
+
3
+ CRAFT republishes remote output through ``display_pub.publish``, which drops
4
+ ``<style>`` blocks, so every element carries its own style (as in tidy3's
5
+ ``display.py``). The pinned header needs a solid background: SolveIt's card
6
+ colour (``--card``, an HSL triplet) is drawn as a gradient over a plain colour
7
+ taken from VS Code, then JupyterLab, then the browser. Where ``--card`` is
8
+ undefined the gradient drops out alone.
9
+
10
+ Paging is a script over this table. Where output scripts don't run, the
11
+ table stays a box of fixed height that scrolls down and sideways, with the
12
+ column names and types pinned at the top.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import html as _html
18
+ import uuid
19
+
20
+ import polars as pl
21
+
22
+ from table3._format import cell, display_rows, dtype_short, escape
23
+ from table3._pager import PAGER_JS
24
+
25
+ MONO = "12px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace"
26
+ RULE = "border-bottom:1px solid rgba(127,127,127,.45)"
27
+ # The font is set once on the table: a cell's style repeats on up to
28
+ # n x columns cells, and the font list is half its length.
29
+ CELL = "font:inherit;padding:3px 10px;text-align:right;white-space:pre;border:0"
30
+ HEAD_BG = (
31
+ "background-color:var(--vscode-editor-background,"
32
+ "var(--jp-layout-color0,Canvas));"
33
+ "background-image:linear-gradient(hsl(var(--card)),hsl(var(--card)))"
34
+ )
35
+ ZEBRA = "background:rgba(127,127,127,.08)"
36
+ SCROLLBAR = "scrollbar-color:rgba(127,127,127,.5) transparent"
37
+
38
+
39
+ def _plural(k: int) -> str:
40
+ return f"{k:,} row" + ("s" if k != 1 else "")
41
+
42
+
43
+ def rows_label(shown: int, total: int | None) -> str:
44
+ """Footer for the scroll box: "35 rows", or how many of how many show."""
45
+ if total is not None and total <= shown:
46
+ return _plural(total)
47
+ of = "more" if total is None else f"{total:,}"
48
+ return f"showing first {shown:,} of {of} rows"
49
+
50
+
51
+ def pages_label(shown: int, total: int | None) -> str:
52
+ """What the pager counts rows of: "344 rows", or the first 100 of more."""
53
+ if total is not None and total <= shown:
54
+ return _plural(shown)
55
+ of = "more" if total is None else f"{total:,}"
56
+ return f"{_plural(shown)} (first {shown:,} of {of})"
57
+
58
+
59
+ def table_html(
60
+ df: pl.DataFrame,
61
+ total: int | None,
62
+ caption: str | None,
63
+ *,
64
+ pages: bool,
65
+ page_size: int,
66
+ height: int | None,
67
+ digits: int | str | None = None,
68
+ ) -> str:
69
+ """*df* is the first rows; *total* the whole row count, None if not counted."""
70
+
71
+ def _tr(cells: list[str], style: str, row_style: str = "") -> str:
72
+ tds = "".join(f"<td style='{style}'>{c}</td>" for c in cells)
73
+ return f"<tr style='{row_style}'>{tds}</tr>"
74
+
75
+ head = _tr([f"<b>{escape(c)}</b>" for c in df.columns], f"{CELL};{RULE}")
76
+ types = _tr(
77
+ [escape(dtype_short(t)) for t in df.dtypes], f"{CELL};opacity:.55;{RULE}"
78
+ )
79
+ body = [
80
+ _tr([cell(v) for v in row], CELL, ZEBRA if i % 2 else "")
81
+ for i, row in enumerate(display_rows(df, digits))
82
+ ]
83
+
84
+ shape = f"shape: ({'?' if total is None else total}, {df.width})"
85
+ box = f"overflow:auto;{SCROLLBAR}"
86
+ if height is not None:
87
+ box = f"max-height:{height}px;{box}"
88
+ small = f"font:{MONO};opacity:.65"
89
+ cap = ""
90
+ if caption:
91
+ cap = (
92
+ f"<div style='font:{MONO};opacity:.85;margin:0 0 2px 0'>"
93
+ f"{_html.escape(caption)}</div>"
94
+ )
95
+
96
+ root = "<div>"
97
+ pager = script = ""
98
+ if pages:
99
+ uid = f"table3-{uuid.uuid4().hex}"
100
+ label = _html.escape(pages_label(df.height, total))
101
+ root = f"<div id='{uid}' data-page-size='{page_size}' data-label='{label}'>"
102
+ pager = (
103
+ f"<div data-pager style='font:{MONO};margin:4px 0 0 0;display:none'>"
104
+ "</div>"
105
+ )
106
+ script = f"<script>{PAGER_JS.replace('__ID__', uid)}</script>"
107
+ return (
108
+ root
109
+ + cap
110
+ + f"<small style='{small}'>{shape}</small>"
111
+ f"<div data-box style='{box}'>"
112
+ "<table style='border-collapse:separate;border-spacing:0;margin:2px 0;"
113
+ f"font:{MONO}'>"
114
+ f"<thead style='position:sticky;top:0;z-index:1;{HEAD_BG}'>"
115
+ + head
116
+ + types
117
+ + "</thead><tbody>"
118
+ + "".join(body)
119
+ + "</tbody></table></div>"
120
+ f"<small data-foot style='{small}'>"
121
+ f"{rows_label(df.height, total)}</small>"
122
+ + pager
123
+ + "</div>"
124
+ + script
125
+ )
table3/_pager.py ADDED
@@ -0,0 +1,106 @@
1
+ """The script that pages a table3 table in place."""
2
+
3
+ # Column widths are measured once over all rows, so a column page keeps its
4
+ # columns whichever row page is shown. __ID__ is the table's root element.
5
+ PAGER_JS = """
6
+ (function () {
7
+ var root = document.getElementById("__ID__");
8
+ if (!root || root.dataset.paged) return;
9
+ root.dataset.paged = "1";
10
+ var box = root.querySelector("[data-box]");
11
+ var table = box.querySelector("table");
12
+ var head = [].slice.call(table.tHead.rows);
13
+ var body = [].slice.call(table.tBodies[0].rows);
14
+ var foot = root.querySelector("[data-foot]");
15
+ var pager = root.querySelector("[data-pager]");
16
+ var size = +root.dataset.pageSize, label = root.dataset.label;
17
+ var widths = [].map.call(head[0].cells, function (c) { return c.offsetWidth; });
18
+ var page = 0, cpage = 0, groups = [], seenWidth = -1;
19
+ var BTN = "font:inherit;color:inherit;background:none;cursor:pointer;" +
20
+ "border:1px solid transparent;border-radius:4px;padding:1px 7px;margin:0 1px";
21
+
22
+ function group() {
23
+ var avail = box.clientWidth || Infinity, cur = [], w = 0;
24
+ seenWidth = box.clientWidth;
25
+ groups = [];
26
+ widths.forEach(function (x, j) {
27
+ if (cur.length && w + x > avail) { groups.push(cur); cur = []; w = 0; }
28
+ cur.push(j); w += x;
29
+ });
30
+ groups.push(cur);
31
+ cpage = Math.min(cpage, groups.length - 1);
32
+ }
33
+
34
+ function button(text, on, go, current) {
35
+ var b = document.createElement("button");
36
+ b.type = "button"; b.textContent = text; b.style.cssText = BTN;
37
+ if (current) b.style.borderColor = "rgba(127,127,127,.6)";
38
+ if (!on) { b.disabled = true; b.style.opacity = ".35"; b.style.cursor = "default"; }
39
+ else b.onclick = go;
40
+ pager.appendChild(b);
41
+ }
42
+
43
+ function gap(text) {
44
+ var s = document.createElement("span");
45
+ s.textContent = text || "…"; s.style.cssText = "opacity:.55;padding:0 4px";
46
+ pager.appendChild(s);
47
+ }
48
+
49
+ function pageList(n, cur) {
50
+ if (n <= 7) return Array.from({ length: n }, function (_, i) { return i; });
51
+ var keep = [0, n - 1, cur - 1, cur, cur + 1].filter(function (i) {
52
+ return i >= 0 && i < n;
53
+ });
54
+ return keep.filter(function (v, i) { return keep.indexOf(v) === i; })
55
+ .sort(function (a, b) { return a - b; });
56
+ }
57
+
58
+ function render() {
59
+ var npages = Math.max(1, Math.ceil(body.length / size));
60
+ page = Math.min(page, npages - 1);
61
+ var lo = page * size, hi = Math.min(lo + size, body.length);
62
+ var cols = groups[cpage], keep = {};
63
+ cols.forEach(function (j) { keep[j] = 1; });
64
+ body.forEach(function (tr, i) {
65
+ var on = i >= lo && i < hi;
66
+ tr.style.display = on ? "" : "none";
67
+ if (on) tr.style.background = (i - lo) % 2 ? "rgba(127,127,127,.08)" : "";
68
+ });
69
+ head.concat(body.slice(lo, hi)).forEach(function (tr) {
70
+ [].forEach.call(tr.cells, function (td, j) { td.style.display = keep[j] ? "" : "none"; });
71
+ });
72
+
73
+ var text = body.length > size ? (lo + 1) + "–" + hi + " of " + label : label;
74
+ if (groups.length > 1)
75
+ text += " | " + (cols[0] + 1) + "–" + (cols[cols.length - 1] + 1) +
76
+ " of " + widths.length + " columns";
77
+ foot.textContent = text;
78
+
79
+ pager.textContent = "";
80
+ if (groups.length > 1) {
81
+ button("◂ columns", cpage > 0, function () { cpage--; render(); });
82
+ button("columns ▸", cpage < groups.length - 1, function () { cpage++; render(); });
83
+ if (npages > 1) gap("|");
84
+ }
85
+ if (npages > 1) {
86
+ button("Previous", page > 0, function () { page--; render(); });
87
+ var last = -1;
88
+ pageList(npages, page).forEach(function (p) {
89
+ if (p > last + 1) gap();
90
+ button(String(p + 1), p !== page, function () { page = p; render(); }, p === page);
91
+ last = p;
92
+ });
93
+ button("Next", page < npages - 1, function () { page++; render(); });
94
+ }
95
+ pager.style.display = pager.childNodes.length ? "" : "none";
96
+ }
97
+
98
+ group();
99
+ box.style.maxHeight = "none";
100
+ render();
101
+ if (window.ResizeObserver)
102
+ new ResizeObserver(function () {
103
+ if (box.clientWidth !== seenWidth) { group(); render(); }
104
+ }).observe(box);
105
+ })();
106
+ """
table3/py.typed ADDED
File without changes
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: table3
3
+ Version: 0.3.0
4
+ Summary: Paged, theme-aware tables in notebook output: shared by tidy3, tidy3a and stats3
5
+ Author: Rigoberto Leyva Salmeron
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/rleyvasal/table3
8
+ Keywords: notebooks,jupyter,polars,tables,viewer
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: IPython
11
+ Classifier: Framework :: Jupyter
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: polars>=1.39
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=7.0; extra == "dev"
19
+ Requires-Dist: ipython>=8.0; extra == "dev"
20
+ Requires-Dist: pandas>=2.1.4; extra == "dev"
21
+ Requires-Dist: pyarrow>=15.0; extra == "dev"
22
+ Dynamic: license-file
23
+
24
+ # table3
25
+
26
+ Paged, theme-aware tables in notebook output, the way R notebooks print a
27
+ data frame. It is the shared table display of
28
+ [tidy3](https://github.com/rleyvasal/tidy3),
29
+ [tidy3a](https://github.com/rleyvasal/tidy3a) and
30
+ [stats3](https://github.com/rleyvasal/stats3), released on its own so each
31
+ can show tables without depending on the others. It needs only polars.
32
+
33
+ ```python
34
+ import polars as pl
35
+ import table3
36
+
37
+ penguins = pl.read_csv(
38
+ "https://raw.githubusercontent.com/allisonhorst/palmerpenguins/main/inst/extdata/penguins.csv",
39
+ null_values="NA",
40
+ )
41
+ table3.show(penguins); # ten rows a page, every row up to n=1000
42
+ table3.show(penguins, page_size=25); # bigger pages
43
+ table3.show(penguins, pages=False, height=300); # one box that scrolls
44
+ table3.show(penguins, digits=4); # four significant digits
45
+ ```
46
+
47
+ Column names and types sit in the header, missing values show as `null`,
48
+ floats print as polars' text preview prints them (or to `digits=`
49
+ significant digits, or `digits="full"`), and long values are cut with the
50
+ full value on hover. Wide tables show as many columns as fit, with
51
+ arrows to the rest. The table follows the notebook's light or dark theme in
52
+ VS Code, JupyterLab and SolveIt.
53
+
54
+ `show()` returns what it was given, so it can sit in the middle of a pipe.
55
+ As a cell's last line the notebook also prints that value; end the line with
56
+ `;` to hide it.
57
+
58
+ ## Your own frames
59
+
60
+ A package makes its frames print this way by returning `table3.html(self)`
61
+ from `_repr_html_`. An object with a `__table3__` method shows without table3
62
+ importing its package:
63
+
64
+ ```python
65
+ def __table3__(self, n):
66
+ return first_n_rows_as_polars, total_rows_or_None, "caption or None"
67
+
68
+ def _repr_html_(self):
69
+ return table3.html(self, n=100)
70
+ ```
71
+
72
+ ## More
73
+
74
+ [docs/design.md](docs/design.md) records the design and what each package
75
+ adds. table3 was first released as view3 0.1 and 0.2, where `show()` was
76
+ `view()`, and its versions continue from them. The name `view` is kept for a
77
+ data explorer, as R's `View()` opens one.
@@ -0,0 +1,11 @@
1
+ table3/__init__.py,sha256=HuQnXQ5FKPEFe_JpbeCs0II-4FpqRdutcolaGZZMxAo,5814
2
+ table3/__version__.py,sha256=VrXpHDu3erkzwl_WXrqINBm9xWkcyUy53IQOj042dOs,22
3
+ table3/_format.py,sha256=vQ8w6FZHZyS-Z3cuKT10OPnws7Xlf54yv9rIgOHSTFw,2536
4
+ table3/_html.py,sha256=aCDUmSGAu85rfG6BbTbmrhqwgaI2KuWlVp3QfdKnr3s,4406
5
+ table3/_pager.py,sha256=P2FGXTxgclpFBySTqgKeBUFZSbGK99BnosTsgMlKPLc,4107
6
+ table3/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ table3-0.3.0.dist-info/licenses/LICENSE,sha256=sjpfmtLhkcM74AL9VMpifL0CRdeiMoLyUO9OKZ_hg9I,1082
8
+ table3-0.3.0.dist-info/METADATA,sha256=s8TqvQ_jfpaNBpttexj3HOyWGqKO6MRU72p7eNOUYRs,2873
9
+ table3-0.3.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
10
+ table3-0.3.0.dist-info/top_level.txt,sha256=UrtGfSKMg2UGL__F7_pu_MAGsOEj1KtBbmUs2NSgIjY,7
11
+ table3-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rigoberto Leyva Salmeron
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ table3