sqlitexplorer 1.1.2__tar.gz → 1.1.3__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.
- {sqlitexplorer-1.1.2/sqlitexplorer.egg-info → sqlitexplorer-1.1.3}/PKG-INFO +1 -1
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/pyproject.toml +1 -1
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/charts.py +58 -29
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3/sqlitexplorer.egg-info}/PKG-INFO +1 -1
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/tests/test_charts.py +62 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/LICENSE +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/MANIFEST.in +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/README.md +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/setup.cfg +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/__init__.py +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/__main__.py +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/cli.py +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/completion.py +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/core.py +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/render.py +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/shell.py +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer.egg-info/SOURCES.txt +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer.egg-info/dependency_links.txt +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer.egg-info/entry_points.txt +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer.egg-info/requires.txt +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer.egg-info/top_level.txt +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/tests/conftest.py +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/tests/test_cli.py +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/tests/test_completion.py +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/tests/test_core.py +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/tests/test_render.py +0 -0
- {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/tests/test_shell.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlitexplorer
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.3
|
|
4
4
|
Summary: Explore SQLite databases from the terminal: tables, schema, stats, search, queries, charts, export/import and an interactive shell.
|
|
5
5
|
Author-email: "Carlos A. Planchón" <carlosandresplanchonprestes@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlitexplorer"
|
|
7
|
-
version = "1.1.
|
|
7
|
+
version = "1.1.3"
|
|
8
8
|
description = "Explore SQLite databases from the terminal: tables, schema, stats, search, queries, charts, export/import and an interactive shell."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -7,7 +7,7 @@ other column is a numeric series. Histograms use the first column only.
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import os
|
|
10
|
-
from collections.abc import Iterator, Sequence
|
|
10
|
+
from collections.abc import Callable, Iterator, Sequence
|
|
11
11
|
from contextlib import contextmanager
|
|
12
12
|
from dataclasses import dataclass
|
|
13
13
|
from datetime import datetime
|
|
@@ -18,6 +18,7 @@ import plotille
|
|
|
18
18
|
import plotilleresample
|
|
19
19
|
|
|
20
20
|
from sqlitexplorer.core import ExplorerError, ResultSet, RowStream
|
|
21
|
+
from sqlitexplorer.render import strip_ansi
|
|
21
22
|
|
|
22
23
|
__all__ = [
|
|
23
24
|
"ChartKind",
|
|
@@ -33,8 +34,11 @@ __all__ = [
|
|
|
33
34
|
PALETTE = ("red", "green", "yellow", "blue", "magenta", "cyan")
|
|
34
35
|
# plotille reserves this many characters for the Y axis label.
|
|
35
36
|
AXIS_LABEL_WIDTH = 8
|
|
36
|
-
# Characters
|
|
37
|
-
|
|
37
|
+
# Characters the Y axis takes next to the canvas: ten for the tick, then
|
|
38
|
+
# " | ". plotille writes past the canvas too, which _fit takes care of.
|
|
39
|
+
AXIS_WIDTH = 13
|
|
40
|
+
# Narrowest canvas worth drawing on when the labels ask for too much room.
|
|
41
|
+
MIN_CANVAS = 10
|
|
38
42
|
# A tuple, not bool | int | float: the union would be rebuilt on every call,
|
|
39
43
|
# and this runs once per value of the result.
|
|
40
44
|
_NUMERIC = (bool, int, float)
|
|
@@ -226,7 +230,23 @@ def _color_environment(enabled: bool) -> Iterator[None]:
|
|
|
226
230
|
|
|
227
231
|
|
|
228
232
|
def _canvas_width(width: int) -> int:
|
|
229
|
-
return max(
|
|
233
|
+
return max(MIN_CANVAS, width - AXIS_WIDTH)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _fit(draw: Callable[[int], str], width: int) -> str:
|
|
237
|
+
"""Draw on the widest canvas whose longest line still fits in *width*.
|
|
238
|
+
|
|
239
|
+
plotille writes the X label and the tick numbers past the end of the
|
|
240
|
+
canvas, by an amount that depends on both, so the only way to know the
|
|
241
|
+
room they take is to draw and measure.
|
|
242
|
+
"""
|
|
243
|
+
canvas = _canvas_width(width)
|
|
244
|
+
while True:
|
|
245
|
+
drawing = draw(canvas)
|
|
246
|
+
excess = max(len(strip_ansi(line)) for line in drawing.splitlines()) - width
|
|
247
|
+
if excess <= 0 or canvas <= MIN_CANVAS:
|
|
248
|
+
return drawing
|
|
249
|
+
canvas = max(MIN_CANVAS, canvas - excess)
|
|
230
250
|
|
|
231
251
|
|
|
232
252
|
def render_chart(
|
|
@@ -240,21 +260,25 @@ def render_chart(
|
|
|
240
260
|
y_label: str,
|
|
241
261
|
) -> str:
|
|
242
262
|
"""Draw *series* as a line chart or scatter plot, with a legend when there are several."""
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
263
|
+
|
|
264
|
+
def draw(canvas: int) -> str:
|
|
265
|
+
figure = plotille.Figure()
|
|
266
|
+
figure.width = canvas
|
|
267
|
+
figure.height = max(3, height)
|
|
268
|
+
figure.with_colors = color
|
|
269
|
+
figure.color_mode = "names"
|
|
270
|
+
figure.x_label = x_label
|
|
271
|
+
figure.y_label = y_label[:AXIS_LABEL_WIDTH]
|
|
272
|
+
for index, item in enumerate(series):
|
|
273
|
+
line_color = PALETTE[index % len(PALETTE)] if color else None
|
|
274
|
+
if kind is ChartKind.SCATTER:
|
|
275
|
+
figure.scatter(item.x, item.y, lc=line_color, label=item.label)
|
|
276
|
+
else:
|
|
277
|
+
figure.plot(item.x, item.y, lc=line_color, label=item.label)
|
|
278
|
+
with _color_environment(color):
|
|
279
|
+
return figure.show(legend=len(series) > 1)
|
|
280
|
+
|
|
281
|
+
return _fit(draw, width)
|
|
258
282
|
|
|
259
283
|
|
|
260
284
|
def render_histogram(
|
|
@@ -268,13 +292,18 @@ def render_histogram(
|
|
|
268
292
|
y_label: str,
|
|
269
293
|
) -> str:
|
|
270
294
|
"""Draw the distribution of *values*."""
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
295
|
+
numbers = list(values)
|
|
296
|
+
|
|
297
|
+
def draw(canvas: int) -> str:
|
|
298
|
+
with _color_environment(color):
|
|
299
|
+
return plotille.histogram(
|
|
300
|
+
numbers,
|
|
301
|
+
bins=bins,
|
|
302
|
+
width=canvas,
|
|
303
|
+
height=max(3, height),
|
|
304
|
+
X_label=x_label,
|
|
305
|
+
Y_label=y_label[:AXIS_LABEL_WIDTH],
|
|
306
|
+
lc=PALETTE[0] if color else None,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return _fit(draw, width)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlitexplorer
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.3
|
|
4
4
|
Summary: Explore SQLite databases from the terminal: tables, schema, stats, search, queries, charts, export/import and an interactive shell.
|
|
5
5
|
Author-email: "Carlos A. Planchón" <carlosandresplanchonprestes@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -7,7 +7,10 @@ from datetime import datetime
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
9
|
from sqlitexplorer.charts import (
|
|
10
|
+
AXIS_WIDTH,
|
|
11
|
+
MIN_CANVAS,
|
|
10
12
|
ChartKind,
|
|
13
|
+
Series,
|
|
11
14
|
histogram_values,
|
|
12
15
|
render_chart,
|
|
13
16
|
render_histogram,
|
|
@@ -16,6 +19,7 @@ from sqlitexplorer.charts import (
|
|
|
16
19
|
stream_series,
|
|
17
20
|
)
|
|
18
21
|
from sqlitexplorer.core import ExplorerError, ResultSet, RowStream
|
|
22
|
+
from sqlitexplorer.render import strip_ansi
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
def braille(text: str) -> bool:
|
|
@@ -227,3 +231,61 @@ def test_series_from_result_can_allow_an_empty_result() -> None:
|
|
|
227
231
|
series, skipped = series_from_result(ResultSet(columns=("x", "y")), require_rows=False)
|
|
228
232
|
assert [item.label for item in series] == ["y"]
|
|
229
233
|
assert series[0].x == [] and skipped == 0
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _widest(drawing: str) -> int:
|
|
237
|
+
return max(len(strip_ansi(line)) for line in drawing.splitlines())
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@pytest.mark.parametrize("width", [60, 80, 96, 120, 200])
|
|
241
|
+
@pytest.mark.parametrize("x_label", ["t", "taken_at", "placed_at_utc"])
|
|
242
|
+
def test_render_chart_never_draws_wider_than_the_terminal(width: int, x_label: str) -> None:
|
|
243
|
+
xs = [float(i) for i in range(400)]
|
|
244
|
+
series = [
|
|
245
|
+
Series(label="celsius", x=xs, y=[(i % 17) * 1.37 for i in range(400)]),
|
|
246
|
+
Series(label="humidity", x=xs, y=[60 - (i % 11) for i in range(400)]),
|
|
247
|
+
]
|
|
248
|
+
for color in (False, True):
|
|
249
|
+
for one in (series[:1], series):
|
|
250
|
+
drawing = render_chart(
|
|
251
|
+
one,
|
|
252
|
+
kind=ChartKind.LINE,
|
|
253
|
+
width=width,
|
|
254
|
+
height=9,
|
|
255
|
+
color=color,
|
|
256
|
+
x_label=x_label,
|
|
257
|
+
y_label="value",
|
|
258
|
+
)
|
|
259
|
+
assert _widest(drawing) <= width, (width, x_label, color, len(one))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@pytest.mark.parametrize("width", [40, 60, 80, 96, 120, 200])
|
|
263
|
+
def test_render_histogram_never_draws_wider_than_the_terminal(width: int) -> None:
|
|
264
|
+
drawing = render_histogram(
|
|
265
|
+
[float(i % 97) for i in range(2000)],
|
|
266
|
+
bins=10,
|
|
267
|
+
width=width,
|
|
268
|
+
height=6,
|
|
269
|
+
color=False,
|
|
270
|
+
x_label="celsius",
|
|
271
|
+
y_label="count",
|
|
272
|
+
)
|
|
273
|
+
assert _widest(drawing) <= width
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_render_chart_stops_narrowing_at_the_floor() -> None:
|
|
277
|
+
# A label wider than the terminal cannot be made to fit, so the canvas
|
|
278
|
+
# narrows to MIN_CANVAS and stops there instead of shrinking to nothing.
|
|
279
|
+
series = [Series(label="v", x=[0.0, 1.0, 2.0], y=[0.0, 1.0, 0.5])]
|
|
280
|
+
drawing = render_chart(
|
|
281
|
+
series,
|
|
282
|
+
kind=ChartKind.LINE,
|
|
283
|
+
width=40,
|
|
284
|
+
height=5,
|
|
285
|
+
color=False,
|
|
286
|
+
x_label="a_label_far_too_long_for_this_terminal",
|
|
287
|
+
y_label="v",
|
|
288
|
+
)
|
|
289
|
+
body = [line for line in drawing.splitlines() if braille(line)]
|
|
290
|
+
assert body, drawing
|
|
291
|
+
assert max(len(line) for line in body) == MIN_CANVAS + AXIS_WIDTH
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|