harlequin 2.5.2__tar.gz → 2.6.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.
- {harlequin-2.5.2 → harlequin-2.6.0}/CHANGELOG.md +12 -1
- {harlequin-2.5.2 → harlequin-2.6.0}/PKG-INFO +1 -1
- {harlequin-2.5.2 → harlequin-2.6.0}/pyproject.toml +8 -3
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/app.py +25 -4
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/autocomplete/completers.py +20 -13
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/code_editor.py +2 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/.gitignore +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/LICENSE +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/README.md +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/__init__.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/__main__.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/actions.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/adapter.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/app.tcss +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/app_base.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/autocomplete/__init__.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/autocomplete/completion.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/autocomplete/constants.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/bindings.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/cache.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/catalog.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/catalog_cache.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/cli.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/colors.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/__init__.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/confirm_modal.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/data_catalog/__init__.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/data_catalog/database_tree.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/data_catalog/file_tree.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/data_catalog/s3_tree.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/data_catalog/tree.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/debug_info.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/error_modal.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/export_screen.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/help_screen.md +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/help_screen.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/history_screen.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/results_viewer.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/components/run_query_bar.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/config.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/config_wizard.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/copy_formats.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/copy_widgets.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/driver.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/editor_cache.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/exception.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/export.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/global.tcss +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/history.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/keymap.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/keys_app.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/keys_app.tcss +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/locale_manager.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/messages.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/options.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/plugins.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/py.typed +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/transaction_mode.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin/windows_timezone.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_duckdb/__init__.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_duckdb/adapter.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_duckdb/catalog.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_duckdb/cli_options.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_duckdb/completions.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_duckdb/interactions.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_duckdb/py.typed +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_sqlite/__init__.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_sqlite/adapter.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_sqlite/catalog.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_sqlite/cli_options.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_sqlite/completions.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_sqlite/interactions.py +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_sqlite/py.typed +0 -0
- {harlequin-2.5.2 → harlequin-2.6.0}/src/harlequin_vscode/__init__.py +0 -0
|
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.6.0] - 2026-08-03
|
|
8
|
+
|
|
9
|
+
### Performance
|
|
10
|
+
|
|
11
|
+
- Autocomplete no longer re-sorts the full completion list every time the Data Catalog lazy-loads a node's children; new completions are staged and merged at most once per second. This keeps the UI responsive while the catalog loads in the background, especially against high-latency (remote) databases.
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
- Refreshes the file tree in the Data Catalog after Harlequin writes a file, either by saving from the editor or exporting data, so newly written files appear without a manual refresh ([#871](https://github.com/tconbeer/harlequin/issues/871)).
|
|
16
|
+
|
|
7
17
|
## [2.5.2] - 2026-03-25
|
|
8
18
|
|
|
9
19
|
### Bug Fixes
|
|
@@ -873,7 +883,8 @@ All notable changes to this project will be documented in this file.
|
|
|
873
883
|
|
|
874
884
|
- Use the DuckDB CLI.
|
|
875
885
|
|
|
876
|
-
[unreleased]: https://github.com/tconbeer/harlequin/compare/2.
|
|
886
|
+
[unreleased]: https://github.com/tconbeer/harlequin/compare/2.6.0...HEAD
|
|
887
|
+
[2.6.0]: https://github.com/tconbeer/harlequin/compare/2.5.2...2.6.0
|
|
877
888
|
[2.5.2]: https://github.com/tconbeer/harlequin/compare/2.5.1...2.5.2
|
|
878
889
|
[2.5.1]: https://github.com/tconbeer/harlequin/compare/2.5.0...2.5.1
|
|
879
890
|
[2.5.0]: https://github.com/tconbeer/harlequin/compare/2.4.1...2.5.0
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
|
|
3
3
|
name = "harlequin"
|
|
4
|
-
version = "2.
|
|
4
|
+
version = "2.6.0"
|
|
5
5
|
requires-python = ">=3.10"
|
|
6
6
|
description = "The SQL IDE for Your Terminal."
|
|
7
7
|
authors = [
|
|
@@ -68,14 +68,15 @@ dev = [
|
|
|
68
68
|
]
|
|
69
69
|
static = [
|
|
70
70
|
"ruff>=0.14.1",
|
|
71
|
-
"mypy==
|
|
71
|
+
"mypy==2.0.0,<3",
|
|
72
72
|
"pandas-stubs>=2,<3",
|
|
73
73
|
"boto3-stubs>=1.34.23,<2",
|
|
74
74
|
]
|
|
75
75
|
test = [
|
|
76
76
|
"pytest>=9.0.2,<9.1.0",
|
|
77
77
|
"pytest-asyncio==1.3.0",
|
|
78
|
-
"pytest-
|
|
78
|
+
"pytest-xdist>=3.6",
|
|
79
|
+
"pytest-textual-snapshot @ git+https://github.com/tconbeer/pytest-textual-snapshot.git@a2c6763b45283f76757a123342900accefcf04e8",
|
|
79
80
|
"boto3==1.34.22",
|
|
80
81
|
# extension tests require consistent version of duckdb.
|
|
81
82
|
"duckdb==1.5.1",
|
|
@@ -186,6 +187,10 @@ strict_equality = true
|
|
|
186
187
|
|
|
187
188
|
[tool.pytest.ini_options]
|
|
188
189
|
|
|
190
|
+
# Tests run in parallel, one worker per CPU. Pass -n0 to run them in a single
|
|
191
|
+
# process, e.g. when using a debugger or -s.
|
|
192
|
+
addopts = ["-n", "auto"]
|
|
193
|
+
|
|
189
194
|
markers = [
|
|
190
195
|
"online: requeries internet connection (deselect with '-m \"not online\"')",
|
|
191
196
|
"use_cache: override the autouse fixture the disables the buffer cache",
|
|
@@ -27,6 +27,7 @@ from textual.lazy import Lazy
|
|
|
27
27
|
from textual.message import Message
|
|
28
28
|
from textual.reactive import reactive
|
|
29
29
|
from textual.screen import Screen, ScreenResultCallbackType, ScreenResultType
|
|
30
|
+
from textual.timer import Timer
|
|
30
31
|
from textual.types import CSSPathType
|
|
31
32
|
from textual.widget import AwaitMount, Widget
|
|
32
33
|
from textual.widgets import Button, Footer, Input
|
|
@@ -229,6 +230,7 @@ class Harlequin(AppBase):
|
|
|
229
230
|
self.query_timer: Union[float, None] = None
|
|
230
231
|
self.connection: HarlequinConnection | None = None
|
|
231
232
|
self.harlequin_driver = HarlequinDriver(app=self)
|
|
233
|
+
self._completer_merge_timer: Timer | None = None
|
|
232
234
|
|
|
233
235
|
if keymap_names is None:
|
|
234
236
|
keymap_names = ("vscode",)
|
|
@@ -889,11 +891,15 @@ class Harlequin(AppBase):
|
|
|
889
891
|
if table is None:
|
|
890
892
|
show_export_error(error=ValueError("You must execute a query first."))
|
|
891
893
|
return
|
|
892
|
-
|
|
894
|
+
|
|
895
|
+
def on_export_success() -> None:
|
|
896
|
+
self.notify("Data exported successfully.")
|
|
897
|
+
self.data_catalog.update_file_tree()
|
|
898
|
+
|
|
893
899
|
callback = partial(
|
|
894
900
|
export_callback,
|
|
895
901
|
table=table,
|
|
896
|
-
success_callback=
|
|
902
|
+
success_callback=on_export_success,
|
|
897
903
|
error_callback=show_export_error,
|
|
898
904
|
)
|
|
899
905
|
self.app.push_screen(
|
|
@@ -1168,12 +1174,27 @@ class Harlequin(AppBase):
|
|
|
1168
1174
|
self.editor_collection.word_completer is not None
|
|
1169
1175
|
and self.editor_collection.member_completer is not None
|
|
1170
1176
|
):
|
|
1177
|
+
# appending to the catalog is cheap, but merging the full
|
|
1178
|
+
# completion list is O(n log n), so when the Data Catalog's lazy
|
|
1179
|
+
# loader delivers a stream of items (one message per node), we
|
|
1180
|
+
# defer the merge and run it at most once per second.
|
|
1171
1181
|
self.editor_collection.word_completer.extend_catalog(
|
|
1172
|
-
parent=parent, items=items
|
|
1182
|
+
parent=parent, items=items, defer_merge=True
|
|
1173
1183
|
)
|
|
1174
1184
|
self.editor_collection.member_completer.extend_catalog(
|
|
1175
|
-
parent=parent, items=items
|
|
1185
|
+
parent=parent, items=items, defer_merge=True
|
|
1176
1186
|
)
|
|
1187
|
+
if self._completer_merge_timer is None:
|
|
1188
|
+
self._completer_merge_timer = self.set_timer(
|
|
1189
|
+
1.0, self._merge_completers
|
|
1190
|
+
)
|
|
1191
|
+
|
|
1192
|
+
def _merge_completers(self) -> None:
|
|
1193
|
+
self._completer_merge_timer = None
|
|
1194
|
+
if self.editor_collection.word_completer is not None:
|
|
1195
|
+
self.editor_collection.word_completer.merge()
|
|
1196
|
+
if self.editor_collection.member_completer is not None:
|
|
1197
|
+
self.editor_collection.member_completer.merge()
|
|
1177
1198
|
|
|
1178
1199
|
def update_completers(self, catalog: Catalog) -> None:
|
|
1179
1200
|
if self.connection is None:
|
|
@@ -25,12 +25,8 @@ class WordCompleter:
|
|
|
25
25
|
self._function_completions = function_completions
|
|
26
26
|
self._catalog_completions = catalog_completions
|
|
27
27
|
self._extra_completions = extra_completions or []
|
|
28
|
-
self.completions: list[HarlequinCompletion] =
|
|
29
|
-
|
|
30
|
-
self._function_completions,
|
|
31
|
-
self._catalog_completions,
|
|
32
|
-
self._extra_completions,
|
|
33
|
-
)
|
|
28
|
+
self.completions: list[HarlequinCompletion] = []
|
|
29
|
+
self.merge()
|
|
34
30
|
|
|
35
31
|
def __call__(self, prefix: str) -> list[tuple[tuple[str, str], str]]:
|
|
36
32
|
"""
|
|
@@ -64,18 +60,29 @@ class WordCompleter:
|
|
|
64
60
|
|
|
65
61
|
def update_catalog(self, catalog: Catalog) -> None:
|
|
66
62
|
self._catalog_completions = build_catalog_completions(catalog=catalog)
|
|
67
|
-
self.
|
|
68
|
-
self._keyword_completions,
|
|
69
|
-
self._function_completions,
|
|
70
|
-
self._catalog_completions,
|
|
71
|
-
self._extra_completions,
|
|
72
|
-
)
|
|
63
|
+
self.merge()
|
|
73
64
|
|
|
74
|
-
def extend_catalog(
|
|
65
|
+
def extend_catalog(
|
|
66
|
+
self,
|
|
67
|
+
parent: CatalogItem,
|
|
68
|
+
items: list[CatalogItem],
|
|
69
|
+
defer_merge: bool = False,
|
|
70
|
+
) -> None:
|
|
75
71
|
# TODO: dedupe/merge on the parent's unique key, so we can load items from
|
|
76
72
|
# a cache and update them later when they are lazy-loaded.
|
|
77
73
|
new_completions = _build_children_completions(items=items, context=parent.label)
|
|
78
74
|
self._catalog_completions.extend(new_completions)
|
|
75
|
+
if not defer_merge:
|
|
76
|
+
self.merge()
|
|
77
|
+
|
|
78
|
+
def merge(self) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Rebuild the merged, sorted completions list. This is O(n log n) in the
|
|
81
|
+
total number of completions, so callers that extend the catalog many
|
|
82
|
+
times in quick succession (e.g., the Data Catalog's lazy loader) should
|
|
83
|
+
pass defer_merge=True to extend_catalog and call this once, on a
|
|
84
|
+
debounce.
|
|
85
|
+
"""
|
|
79
86
|
self.completions = self._merge_completions(
|
|
80
87
|
self._keyword_completions,
|
|
81
88
|
self._function_completions,
|
|
@@ -81,6 +81,8 @@ class CodeEditor(TextEditor, inherit_bindings=False):
|
|
|
81
81
|
|
|
82
82
|
def on_text_area_saved(self, message: TextAreaSaved) -> None:
|
|
83
83
|
self.app.notify(f"Editor contents saved to {message.path}")
|
|
84
|
+
if hasattr(self.app, "data_catalog"):
|
|
85
|
+
self.app.data_catalog.update_file_tree()
|
|
84
86
|
|
|
85
87
|
def on_text_area_clipboard_error(self) -> None:
|
|
86
88
|
if not self.has_shown_clipboard_error:
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|