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.
Files changed (27) hide show
  1. {sqlitexplorer-1.1.2/sqlitexplorer.egg-info → sqlitexplorer-1.1.3}/PKG-INFO +1 -1
  2. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/pyproject.toml +1 -1
  3. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/charts.py +58 -29
  4. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3/sqlitexplorer.egg-info}/PKG-INFO +1 -1
  5. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/tests/test_charts.py +62 -0
  6. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/LICENSE +0 -0
  7. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/MANIFEST.in +0 -0
  8. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/README.md +0 -0
  9. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/setup.cfg +0 -0
  10. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/__init__.py +0 -0
  11. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/__main__.py +0 -0
  12. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/cli.py +0 -0
  13. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/completion.py +0 -0
  14. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/core.py +0 -0
  15. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/render.py +0 -0
  16. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer/shell.py +0 -0
  17. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer.egg-info/SOURCES.txt +0 -0
  18. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer.egg-info/dependency_links.txt +0 -0
  19. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer.egg-info/entry_points.txt +0 -0
  20. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer.egg-info/requires.txt +0 -0
  21. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/sqlitexplorer.egg-info/top_level.txt +0 -0
  22. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/tests/conftest.py +0 -0
  23. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/tests/test_cli.py +0 -0
  24. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/tests/test_completion.py +0 -0
  25. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/tests/test_core.py +0 -0
  26. {sqlitexplorer-1.1.2 → sqlitexplorer-1.1.3}/tests/test_render.py +0 -0
  27. {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.2
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.2"
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 taken by the Y axis (ticks, label and separator) next to the canvas.
37
- AXIS_WIDTH = 12
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(10, width - AXIS_WIDTH)
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
- figure = plotille.Figure()
244
- figure.width = _canvas_width(width)
245
- figure.height = max(3, height)
246
- figure.with_colors = color
247
- figure.color_mode = "names"
248
- figure.x_label = x_label
249
- figure.y_label = y_label[:AXIS_LABEL_WIDTH]
250
- for index, item in enumerate(series):
251
- line_color = PALETTE[index % len(PALETTE)] if color else None
252
- if kind is ChartKind.SCATTER:
253
- figure.scatter(item.x, item.y, lc=line_color, label=item.label)
254
- else:
255
- figure.plot(item.x, item.y, lc=line_color, label=item.label)
256
- with _color_environment(color):
257
- return figure.show(legend=len(series) > 1)
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
- with _color_environment(color):
272
- return plotille.histogram(
273
- list(values),
274
- bins=bins,
275
- width=_canvas_width(width),
276
- height=max(3, height),
277
- X_label=x_label,
278
- Y_label=y_label[:AXIS_LABEL_WIDTH],
279
- lc=PALETTE[0] if color else None,
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.2
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