harlequin 2.2.1__tar.gz → 2.4.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.2.1 → harlequin-2.4.0}/CHANGELOG.md +19 -1
- {harlequin-2.2.1 → harlequin-2.4.0}/PKG-INFO +20 -15
- {harlequin-2.2.1 → harlequin-2.4.0}/pyproject.toml +42 -28
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/app.py +42 -19
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/app.tcss +6 -2
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/autocomplete/completers.py +1 -1
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/cli.py +4 -6
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/code_editor.py +22 -10
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/data_catalog/__init__.py +5 -5
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/data_catalog/database_tree.py +4 -13
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/debug_info.py +1 -1
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/error_modal.py +1 -1
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/export.py +2 -2
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/keys_app.py +1 -1
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/plugins.py +1 -6
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_duckdb/adapter.py +1 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_sqlite/adapter.py +1 -1
- {harlequin-2.2.1 → harlequin-2.4.0}/.gitignore +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/LICENSE +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/README.md +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/__init__.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/__main__.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/actions.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/adapter.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/app_base.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/autocomplete/__init__.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/autocomplete/completion.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/autocomplete/constants.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/bindings.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/cache.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/catalog.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/catalog_cache.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/colors.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/__init__.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/confirm_modal.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/data_catalog/file_tree.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/data_catalog/s3_tree.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/data_catalog/tree.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/export_screen.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/help_screen.md +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/help_screen.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/history_screen.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/results_viewer.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/run_query_bar.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/config.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/config_wizard.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/copy_formats.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/copy_widgets.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/driver.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/editor_cache.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/exception.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/global.tcss +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/history.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/keymap.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/keys_app.tcss +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/locale_manager.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/messages.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/options.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/py.typed +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/transaction_mode.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/windows_timezone.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_duckdb/__init__.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_duckdb/catalog.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_duckdb/cli_options.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_duckdb/completions.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_duckdb/interactions.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_duckdb/py.typed +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_sqlite/__init__.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_sqlite/catalog.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_sqlite/cli_options.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_sqlite/completions.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_sqlite/interactions.py +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_sqlite/py.typed +0 -0
- {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_vscode/__init__.py +0 -0
|
@@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [2.4.0] - 2025-10-29
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- Adds support for Python 3.14 for most Harlequin users ([#852](https://github.com/tconbeer/harlequin/issues/852), [#879](https://github.com/tconbeer/harlequin/pull/879), [#880](https://github.com/tconbeer/harlequin/pull/880) - thank you [@smartinussen](https://github.com/smartinussen) and especially [@branchvincent](https://github.com/branchvincent)!). **NOTE:** The `databricks` extra will not install the Databricks adapter on Python 3.14; Databricks users should continue to use Python 3.10-3.13. uv makes that easy with `uv tool install --python 3.13 'harlequin[databricks]'` See [this issue](https://github.com/alexmalins/harlequin-databricks/issues/23) for more information.
|
|
12
|
+
|
|
13
|
+
## [2.3.0] - 2025-10-24
|
|
14
|
+
|
|
15
|
+
### Breaking Changes
|
|
16
|
+
|
|
17
|
+
- Drops support for Python 3.9.
|
|
18
|
+
|
|
19
|
+
### Bug Fixes
|
|
20
|
+
|
|
21
|
+
- Harlequin will no longer execute an empty query; the Run Query button will appear disabled if the buffer is empty ([#873](https://github.com/tconbeer/harlequin/issues/873)).
|
|
22
|
+
|
|
7
23
|
## [2.2.1] - 2025-10-16
|
|
8
24
|
|
|
9
25
|
### Bug Fixes
|
|
@@ -835,7 +851,9 @@ All notable changes to this project will be documented in this file.
|
|
|
835
851
|
|
|
836
852
|
- Use the DuckDB CLI.
|
|
837
853
|
|
|
838
|
-
[unreleased]: https://github.com/tconbeer/harlequin/compare/2.
|
|
854
|
+
[unreleased]: https://github.com/tconbeer/harlequin/compare/2.4.0...HEAD
|
|
855
|
+
[2.4.0]: https://github.com/tconbeer/harlequin/compare/2.3.0...2.4.0
|
|
856
|
+
[2.3.0]: https://github.com/tconbeer/harlequin/compare/2.2.1...2.3.0
|
|
839
857
|
[2.2.1]: https://github.com/tconbeer/harlequin/compare/2.2.0...2.2.1
|
|
840
858
|
[2.2.0]: https://github.com/tconbeer/harlequin/compare/2.1.3...2.2.0
|
|
841
859
|
[2.1.3]: https://github.com/tconbeer/harlequin/compare/2.1.2...2.1.3
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: harlequin
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: The SQL IDE for Your Terminal.
|
|
5
5
|
Project-URL: Homepage, https://harlequin.sh
|
|
6
6
|
Project-URL: Documentation, https://harlequin.sh/docs/getting-started/index
|
|
@@ -10,24 +10,29 @@ Project-URL: Changelog, https://github.com/tconbeer/harlequin/blob/main/CHANGELO
|
|
|
10
10
|
Author-email: Ted Conbeer <tconbeer@users.noreply.github.com>
|
|
11
11
|
License-Expression: MIT
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Keywords: harlequin,ide,sql,tui
|
|
14
|
-
|
|
13
|
+
Keywords: harlequin,ide,sql,textual,tui
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Requires-Python: >=3.10
|
|
15
24
|
Requires-Dist: click==8.1.8
|
|
16
|
-
Requires-Dist: duckdb>=0.8.0
|
|
17
|
-
Requires-Dist:
|
|
18
|
-
Requires-Dist:
|
|
19
|
-
Requires-Dist: numpy<2.3.0,>=2.1.0; python_version >= '3.10'
|
|
25
|
+
Requires-Dist: duckdb>=0.8.0; python_version < '3.14'
|
|
26
|
+
Requires-Dist: duckdb>=1.4.2.dev0; python_version >= '3.14'
|
|
27
|
+
Requires-Dist: pandas>=2.3; python_version >= '3.14'
|
|
20
28
|
Requires-Dist: platformdirs<5.0,>=3.10
|
|
21
|
-
Requires-Dist: pyarrow>=18.1.0; python_version >= '3.12'
|
|
22
29
|
Requires-Dist: questionary==2.0.1
|
|
23
30
|
Requires-Dist: rich-click==1.8.5
|
|
24
|
-
Requires-Dist: shandy-sqlfmt>=0.
|
|
25
|
-
Requires-Dist: textual-fastdatatable==0.
|
|
26
|
-
Requires-Dist: textual-textarea==0.
|
|
27
|
-
Requires-Dist: textual==
|
|
31
|
+
Requires-Dist: shandy-sqlfmt>=0.28.2
|
|
32
|
+
Requires-Dist: textual-fastdatatable==0.14.0
|
|
33
|
+
Requires-Dist: textual-textarea==0.17.2
|
|
34
|
+
Requires-Dist: textual==6.4.0
|
|
28
35
|
Requires-Dist: tomlkit<0.14.0,>=0.12.5
|
|
29
|
-
Requires-Dist: tree-sitter-sql==0.3.7
|
|
30
|
-
Requires-Dist: tree-sitter<0.25.0,>=0.23.0
|
|
31
36
|
Provides-Extra: adbc
|
|
32
37
|
Requires-Dist: harlequin-adbc; extra == 'adbc'
|
|
33
38
|
Provides-Extra: bigquery
|
|
@@ -35,7 +40,7 @@ Requires-Dist: harlequin-bigquery; extra == 'bigquery'
|
|
|
35
40
|
Provides-Extra: cassandra
|
|
36
41
|
Requires-Dist: harlequin-cassandra; extra == 'cassandra'
|
|
37
42
|
Provides-Extra: databricks
|
|
38
|
-
Requires-Dist: harlequin-databricks; extra == 'databricks'
|
|
43
|
+
Requires-Dist: harlequin-databricks; (python_version < '3.14') and extra == 'databricks'
|
|
39
44
|
Provides-Extra: mysql
|
|
40
45
|
Requires-Dist: harlequin-mysql; extra == 'mysql'
|
|
41
46
|
Provides-Extra: nebulagraph
|
|
@@ -1,45 +1,46 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
|
|
3
3
|
name = "harlequin"
|
|
4
|
-
version = "2.
|
|
5
|
-
requires-python = ">=3.
|
|
4
|
+
version = "2.4.0"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
6
|
description = "The SQL IDE for Your Terminal."
|
|
7
7
|
authors = [
|
|
8
8
|
{ name = "Ted Conbeer", email = "tconbeer@users.noreply.github.com" }
|
|
9
9
|
]
|
|
10
10
|
license = "MIT"
|
|
11
11
|
readme = "README.md"
|
|
12
|
-
keywords = ["sql", "tui", "ide", "harlequin"]
|
|
12
|
+
keywords = ["sql", "tui", "ide", "textual", "harlequin"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Operating System :: MacOS :: MacOS X",
|
|
16
|
+
"Operating System :: Microsoft :: Windows",
|
|
17
|
+
"Operating System :: POSIX :: Linux",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
]
|
|
13
24
|
dependencies = [
|
|
14
25
|
# textual and component libraries
|
|
15
|
-
"textual==
|
|
16
|
-
"textual-fastdatatable==0.
|
|
17
|
-
"textual-textarea==0.
|
|
26
|
+
"textual==6.4.0",
|
|
27
|
+
"textual-fastdatatable==0.14.0",
|
|
28
|
+
"textual-textarea==0.17.2",
|
|
18
29
|
|
|
19
30
|
# click
|
|
20
31
|
"click==8.1.8",
|
|
21
32
|
"rich-click==1.8.5",
|
|
22
33
|
|
|
23
34
|
# other deps
|
|
24
|
-
"duckdb>=0.8.0",
|
|
25
|
-
"shandy-sqlfmt>=0.
|
|
35
|
+
"duckdb>=0.8.0; python_version < '3.14'",
|
|
36
|
+
"shandy-sqlfmt>=0.28.2",
|
|
26
37
|
"platformdirs>=3.10,<5.0",
|
|
27
38
|
"tomlkit>=0.12.5,<0.14.0",
|
|
28
39
|
"questionary==2.0.1",
|
|
29
40
|
|
|
30
|
-
#
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
# backports
|
|
35
|
-
"importlib_metadata>=8.0; python_version < '3.10'",
|
|
36
|
-
|
|
37
|
-
# wheel pins: these packages don't build wheels for all Pythons
|
|
38
|
-
|
|
39
|
-
"numpy>=2.0.0,<2.1.0; python_version < '3.10'",
|
|
40
|
-
"numpy>=2.1.0,<2.3.0; python_version >= '3.10'",
|
|
41
|
-
|
|
42
|
-
"pyarrow>=18.1.0; python_version >= '3.12'",
|
|
41
|
+
# ensure we install from wheels
|
|
42
|
+
"duckdb>=1.4.2.dev0; python_version >= '3.14'",
|
|
43
|
+
"pandas>=2.3; python_version >= '3.14'",
|
|
43
44
|
]
|
|
44
45
|
|
|
45
46
|
|
|
@@ -51,7 +52,11 @@ mysql = ["harlequin-mysql"]
|
|
|
51
52
|
odbc = ["harlequin-odbc"]
|
|
52
53
|
bigquery = ["harlequin-bigquery"]
|
|
53
54
|
trino = ["harlequin-trino"]
|
|
54
|
-
databricks
|
|
55
|
+
# databricks-sql-connector pins numpy to 2.2, which has to be
|
|
56
|
+
# built from source on python 3.14. Rather than forcing all
|
|
57
|
+
# 3.14 users to build numpy from source, make databricks
|
|
58
|
+
# unavailable on 3.14
|
|
59
|
+
databricks = ["harlequin-databricks; python_version < '3.14'"]
|
|
55
60
|
adbc = ["harlequin-adbc"]
|
|
56
61
|
cassandra = ["harlequin-cassandra"]
|
|
57
62
|
nebulagraph = ["harlequin-nebulagraph"]
|
|
@@ -60,14 +65,14 @@ nebulagraph = ["harlequin-nebulagraph"]
|
|
|
60
65
|
[dependency-groups]
|
|
61
66
|
dev = [
|
|
62
67
|
"pre-commit>=3.3,<4",
|
|
63
|
-
"textual-dev==1.0
|
|
68
|
+
"textual-dev==1.8.0",
|
|
64
69
|
"harlequin-postgres>=1.1.1,<2",
|
|
65
70
|
"harlequin-mysql>=1,<2",
|
|
66
71
|
"pyinstrument>=5,<6",
|
|
67
72
|
]
|
|
68
73
|
static = [
|
|
69
|
-
"ruff>=0.
|
|
70
|
-
"mypy
|
|
74
|
+
"ruff>=0.14.1",
|
|
75
|
+
"mypy==1.18.2,<2",
|
|
71
76
|
"pandas-stubs>=2,<3",
|
|
72
77
|
"boto3-stubs>=1.34.23,<2",
|
|
73
78
|
]
|
|
@@ -75,12 +80,20 @@ test = [
|
|
|
75
80
|
"pytest>=7.3.1,<9.0.0",
|
|
76
81
|
"pytest-asyncio==0.21.0",
|
|
77
82
|
"pytest-textual-snapshot @ https://github.com/tconbeer/pytest-textual-snapshot.git#main",
|
|
83
|
+
"boto3==1.34.22",
|
|
78
84
|
# extension tests require consistent version of duckdb.
|
|
79
|
-
"duckdb==1.
|
|
85
|
+
"duckdb==1.4.2.dev23",
|
|
80
86
|
"flaky>=3.8.1",
|
|
81
87
|
]
|
|
82
88
|
|
|
83
89
|
|
|
90
|
+
# For development of related libraries
|
|
91
|
+
# [tool.uv.sources]
|
|
92
|
+
|
|
93
|
+
# textual-fastdatatable = { path = "../textual-fastdatatable" }
|
|
94
|
+
# textual-textarea = { path = "../textual-textarea" }
|
|
95
|
+
|
|
96
|
+
|
|
84
97
|
[project.urls]
|
|
85
98
|
|
|
86
99
|
Homepage = "https://harlequin.sh"
|
|
@@ -134,7 +147,7 @@ only-include = [
|
|
|
134
147
|
|
|
135
148
|
[tool.ruff]
|
|
136
149
|
|
|
137
|
-
target-version = "
|
|
150
|
+
target-version = "py310"
|
|
138
151
|
|
|
139
152
|
|
|
140
153
|
[tool.ruff.lint]
|
|
@@ -144,7 +157,7 @@ select = ["A", "B", "E", "F", "I"]
|
|
|
144
157
|
|
|
145
158
|
[tool.mypy]
|
|
146
159
|
|
|
147
|
-
python_version = "3.
|
|
160
|
+
python_version = "3.10"
|
|
148
161
|
files = [
|
|
149
162
|
"src/**/*.py",
|
|
150
163
|
"tests/**/*.py",
|
|
@@ -180,4 +193,5 @@ strict_equality = true
|
|
|
180
193
|
markers = [
|
|
181
194
|
"online: requeries internet connection (deselect with '-m \"not online\"')",
|
|
182
195
|
"use_cache: override the autouse fixture the disables the buffer cache",
|
|
196
|
+
"py12: Only run on Python 3.12 or higher",
|
|
183
197
|
]
|
|
@@ -326,12 +326,14 @@ class Harlequin(AppBase):
|
|
|
326
326
|
@on(Button.Pressed, "#run_query")
|
|
327
327
|
def submit_query_from_run_query_bar(self, message: Button.Pressed) -> None:
|
|
328
328
|
message.stop()
|
|
329
|
-
self.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
329
|
+
queries = self._get_selected_queries()
|
|
330
|
+
if queries:
|
|
331
|
+
self.post_message(
|
|
332
|
+
QuerySubmitted(
|
|
333
|
+
queries=queries,
|
|
334
|
+
limit=self.run_query_bar.limit_value,
|
|
335
|
+
)
|
|
333
336
|
)
|
|
334
|
-
)
|
|
335
337
|
|
|
336
338
|
@on(Button.Pressed, "#cancel_query")
|
|
337
339
|
def cancel_query(self, message: Button.Pressed) -> None:
|
|
@@ -374,12 +376,14 @@ class Harlequin(AppBase):
|
|
|
374
376
|
@on(CodeEditor.Submitted)
|
|
375
377
|
def submit_query_from_editor(self, message: CodeEditor.Submitted) -> None:
|
|
376
378
|
message.stop()
|
|
377
|
-
self.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
379
|
+
queries = self._get_selected_queries()
|
|
380
|
+
if queries:
|
|
381
|
+
self.post_message(
|
|
382
|
+
QuerySubmitted(
|
|
383
|
+
queries=queries,
|
|
384
|
+
limit=self.run_query_bar.limit_value,
|
|
385
|
+
)
|
|
381
386
|
)
|
|
382
|
-
)
|
|
383
387
|
|
|
384
388
|
@on(DatabaseConnected)
|
|
385
389
|
def initialize_app(self, message: DatabaseConnected) -> None:
|
|
@@ -494,12 +498,14 @@ class Harlequin(AppBase):
|
|
|
494
498
|
self.set_timer(delay=0.1, callback=callback)
|
|
495
499
|
return
|
|
496
500
|
self.editor.focus()
|
|
501
|
+
self._sync_run_button_disabled()
|
|
502
|
+
self._sync_run_button_text()
|
|
503
|
+
|
|
504
|
+
def on_text_area_changed(self) -> None:
|
|
505
|
+
self._sync_run_button_disabled()
|
|
497
506
|
|
|
498
507
|
def on_text_area_selection_changed(self) -> None:
|
|
499
|
-
|
|
500
|
-
self.run_query_bar.run_button.label = "Run Selection"
|
|
501
|
-
else:
|
|
502
|
-
self.run_query_bar.run_button.label = "Run Query"
|
|
508
|
+
self._sync_run_button_text()
|
|
503
509
|
|
|
504
510
|
@on(Input.Changed, "#limit_input")
|
|
505
511
|
def update_limit_tooltip(self, message: Input.Changed) -> None:
|
|
@@ -522,12 +528,14 @@ class Harlequin(AppBase):
|
|
|
522
528
|
and message.validation_result
|
|
523
529
|
and message.validation_result.is_valid
|
|
524
530
|
):
|
|
525
|
-
self.
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
531
|
+
queries = self._get_selected_queries()
|
|
532
|
+
if queries:
|
|
533
|
+
self.post_message(
|
|
534
|
+
QuerySubmitted(
|
|
535
|
+
queries=queries,
|
|
536
|
+
limit=self.run_query_bar.limit_value,
|
|
537
|
+
)
|
|
529
538
|
)
|
|
530
|
-
)
|
|
531
539
|
|
|
532
540
|
@on(DataTable.SelectionCopied)
|
|
533
541
|
def copy_data_to_clipboard(self, message: DataTable.SelectionCopied) -> None:
|
|
@@ -1024,6 +1032,21 @@ class Harlequin(AppBase):
|
|
|
1024
1032
|
self.data_catalog.update_file_tree()
|
|
1025
1033
|
self.data_catalog.update_s3_tree()
|
|
1026
1034
|
|
|
1035
|
+
def _sync_run_button_text(self) -> None:
|
|
1036
|
+
if self._validate_selection():
|
|
1037
|
+
self.run_query_bar.run_button.label = "Run Selection"
|
|
1038
|
+
else:
|
|
1039
|
+
self.run_query_bar.run_button.label = "Run Query"
|
|
1040
|
+
|
|
1041
|
+
def _sync_run_button_disabled(self) -> None:
|
|
1042
|
+
if self.editor is None or self.editor.text_input is None:
|
|
1043
|
+
return
|
|
1044
|
+
|
|
1045
|
+
if self.editor.text.strip():
|
|
1046
|
+
self.run_query_bar.run_button.disabled = False
|
|
1047
|
+
else:
|
|
1048
|
+
self.run_query_bar.run_button.disabled = True
|
|
1049
|
+
|
|
1027
1050
|
@work(
|
|
1028
1051
|
thread=True,
|
|
1029
1052
|
exclusive=True,
|
|
@@ -7,6 +7,7 @@ $border-title-color-focus: $primary;
|
|
|
7
7
|
/* ALL WIDGETS */
|
|
8
8
|
DataCatalog,
|
|
9
9
|
EditorCollection,
|
|
10
|
+
FooterContainer > Input,
|
|
10
11
|
ResultsViewer,
|
|
11
12
|
Lazy {
|
|
12
13
|
border: round $border-color-nofocus;
|
|
@@ -20,6 +21,7 @@ LoadingIndicator {
|
|
|
20
21
|
|
|
21
22
|
DataCatalog:focus-within,
|
|
22
23
|
EditorCollection:focus-within,
|
|
24
|
+
FooterContainer > Input:focus,
|
|
23
25
|
ResultsViewer:focus-within,
|
|
24
26
|
HistoryScreen TextArea:focus,
|
|
25
27
|
HistoryScreen OptionList:focus {
|
|
@@ -97,8 +99,7 @@ DataCatalog * .directory-tree--folder {
|
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
DataCatalog * .directory-tree--hidden {
|
|
100
|
-
text-style: italic;
|
|
101
|
-
color: $text-muted;
|
|
102
|
+
text-style: dim italic;
|
|
102
103
|
}
|
|
103
104
|
|
|
104
105
|
DataCatalog ContextMenu {
|
|
@@ -211,6 +212,9 @@ RunQueryBar Button#run_query {
|
|
|
211
212
|
margin: 0 0 0 4;
|
|
212
213
|
&:hover {
|
|
213
214
|
background: $secondary;
|
|
215
|
+
&:disabled {
|
|
216
|
+
background: $primary;
|
|
217
|
+
}
|
|
214
218
|
}
|
|
215
219
|
&:focus {
|
|
216
220
|
text-style: reverse;
|
|
@@ -140,7 +140,7 @@ class MemberCompleter(WordCompleter):
|
|
|
140
140
|
match_context = context.strip("'`\"").lower()
|
|
141
141
|
separators = SEPARATOR_PROG.findall(prefix)
|
|
142
142
|
value_prefix = "".join(
|
|
143
|
-
f"{w}{sep}" for w, sep in zip([*others, context], separators)
|
|
143
|
+
f"{w}{sep}" for w, sep in zip([*others, context], separators, strict=False)
|
|
144
144
|
)
|
|
145
145
|
|
|
146
146
|
context_completions = [
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
+
from importlib.metadata import entry_points, version
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Any, Sequence
|
|
6
7
|
|
|
@@ -24,11 +25,6 @@ from harlequin.options import AbstractOption
|
|
|
24
25
|
from harlequin.plugins import load_adapter_plugins
|
|
25
26
|
from harlequin.windows_timezone import check_and_install_tzdata
|
|
26
27
|
|
|
27
|
-
if sys.version_info < (3, 10):
|
|
28
|
-
from importlib_metadata import entry_points, version
|
|
29
|
-
else:
|
|
30
|
-
from importlib.metadata import entry_points, version
|
|
31
|
-
|
|
32
28
|
# configure defaults
|
|
33
29
|
DEFAULT_ADAPTER = "duckdb"
|
|
34
30
|
DEFAULT_LIMIT = 100_000
|
|
@@ -106,7 +102,9 @@ def _version_option() -> str:
|
|
|
106
102
|
adapter_eps = entry_points(group="harlequin.adapter")
|
|
107
103
|
adapter_versions: dict[str, str] = {}
|
|
108
104
|
for ep in adapter_eps:
|
|
109
|
-
adapter_versions.update(
|
|
105
|
+
adapter_versions.update(
|
|
106
|
+
{ep.name: ep.dist.version if ep.dist is not None else "unknown"}
|
|
107
|
+
)
|
|
110
108
|
|
|
111
109
|
adapter_output = "\n".join(
|
|
112
110
|
[f" - {name}, version {version}" for name, version in adapter_versions.items()]
|
|
@@ -2,10 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import List, Union
|
|
4
4
|
|
|
5
|
-
from rich.text import TextType
|
|
6
5
|
from sqlfmt.api import Mode, format_string
|
|
7
6
|
from sqlfmt.exception import SqlfmtError
|
|
8
|
-
from textual.
|
|
7
|
+
from textual.content import ContentType
|
|
8
|
+
from textual.css.query import InvalidQueryFormat, NoMatches
|
|
9
9
|
from textual.message import Message
|
|
10
10
|
from textual.reactive import reactive
|
|
11
11
|
from textual.widgets import ContentSwitcher, TabbedContent, TabPane, Tabs
|
|
@@ -38,18 +38,23 @@ class CodeEditor(TextEditor, inherit_bindings=False):
|
|
|
38
38
|
Returns the list of queries that intersect
|
|
39
39
|
with the current selection.
|
|
40
40
|
"""
|
|
41
|
-
if self.text_input is None:
|
|
41
|
+
if self.text_input is None or not self.text.strip():
|
|
42
42
|
return []
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
if ";" not in self.text:
|
|
45
|
+
return [self.text]
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
separators = self._query_separators()
|
|
48
|
+
if not separators:
|
|
49
|
+
# a semicolon could be in a string literal,
|
|
50
|
+
# so there may not be query separators even if
|
|
51
|
+
# there are literal semicolons in the text.
|
|
47
52
|
return [self.text]
|
|
48
53
|
|
|
49
54
|
queries: list[str] = []
|
|
50
55
|
prev_query: str | None = None
|
|
51
56
|
query_start = (0, 0)
|
|
52
|
-
for query_end in [*
|
|
57
|
+
for query_end in [*separators, self.text_input.document.end]:
|
|
53
58
|
if query_start > self.selection.end:
|
|
54
59
|
break
|
|
55
60
|
q = self.text_input.get_text_range(start=query_start, end=query_end).strip()
|
|
@@ -116,7 +121,11 @@ class CodeEditor(TextEditor, inherit_bindings=False):
|
|
|
116
121
|
if hasattr(self.app, "action_focus_data_catalog"):
|
|
117
122
|
self.app.action_focus_data_catalog()
|
|
118
123
|
|
|
119
|
-
def
|
|
124
|
+
def _query_separators(self) -> list[tuple[int, int]]:
|
|
125
|
+
"""
|
|
126
|
+
Return a list of tuples that represent the row and col
|
|
127
|
+
positions of query separators (semicolons) in the buffer text.
|
|
128
|
+
"""
|
|
120
129
|
if self.text_input is None:
|
|
121
130
|
return []
|
|
122
131
|
|
|
@@ -124,6 +133,7 @@ class CodeEditor(TextEditor, inherit_bindings=False):
|
|
|
124
133
|
assert self._semicolon_query is not None
|
|
125
134
|
query_result = self.query_syntax_tree(query=self._semicolon_query)
|
|
126
135
|
return [n.end_point for n in query_result.get("semicolon", [])]
|
|
136
|
+
|
|
127
137
|
else:
|
|
128
138
|
# tree-sitter is not installed. naively split on semicolons and
|
|
129
139
|
# show a warning.
|
|
@@ -138,10 +148,12 @@ class CodeEditor(TextEditor, inherit_bindings=False):
|
|
|
138
148
|
timeout=10,
|
|
139
149
|
)
|
|
140
150
|
self.has_shown_tree_sitter_error = True
|
|
151
|
+
|
|
141
152
|
semicolons: list[tuple[int, int]] = []
|
|
142
153
|
for i, line in enumerate(self.text.splitlines()):
|
|
143
154
|
for pos in [m.span()[1] for m in re.finditer(";", line)]:
|
|
144
155
|
semicolons.append((i, pos))
|
|
156
|
+
|
|
145
157
|
return semicolons
|
|
146
158
|
|
|
147
159
|
|
|
@@ -156,7 +168,7 @@ class EditorCollection(TabbedContent):
|
|
|
156
168
|
|
|
157
169
|
def __init__(
|
|
158
170
|
self,
|
|
159
|
-
*titles:
|
|
171
|
+
*titles: ContentType,
|
|
160
172
|
initial: str = "",
|
|
161
173
|
name: Union[str, None] = None,
|
|
162
174
|
id: Union[str, None] = None, # noqa: A002
|
|
@@ -188,7 +200,7 @@ class EditorCollection(TabbedContent):
|
|
|
188
200
|
try:
|
|
189
201
|
tab_pane = content.query_one(f"#{active_tab_id}", TabPane)
|
|
190
202
|
return tab_pane.query_one(CodeEditor)
|
|
191
|
-
except NoMatches:
|
|
203
|
+
except (NoMatches, InvalidQueryFormat):
|
|
192
204
|
pass
|
|
193
205
|
all_editors = content.query(CodeEditor)
|
|
194
206
|
return all_editors.first(CodeEditor)
|
|
@@ -310,7 +322,7 @@ class EditorCollection(TabbedContent):
|
|
|
310
322
|
tabs = self.query(TabPane)
|
|
311
323
|
next_tabs = tabs[1:]
|
|
312
324
|
next_tabs.append(tabs[0])
|
|
313
|
-
lookup = {t.id: nt.id for t, nt in zip(tabs, next_tabs)}
|
|
325
|
+
lookup = {t.id: nt.id for t, nt in zip(tabs, next_tabs, strict=False)}
|
|
314
326
|
self.active = lookup[active] # type: ignore
|
|
315
327
|
self.post_message(self.EditorSwitched(active_editor=None))
|
|
316
328
|
self.current_editor.focus()
|
|
@@ -4,9 +4,9 @@ from pathlib import Path
|
|
|
4
4
|
from typing import TYPE_CHECKING, ClassVar, Sequence
|
|
5
5
|
|
|
6
6
|
from rich.markup import escape
|
|
7
|
-
from rich.text import TextType
|
|
8
7
|
from textual import on
|
|
9
|
-
from textual.
|
|
8
|
+
from textual.content import ContentType
|
|
9
|
+
from textual.css.query import InvalidQueryFormat, NoMatches
|
|
10
10
|
from textual.message import Message
|
|
11
11
|
from textual.widgets import (
|
|
12
12
|
DirectoryTree,
|
|
@@ -108,7 +108,7 @@ class DataCatalog(TabbedContent, can_focus=True):
|
|
|
108
108
|
|
|
109
109
|
def __init__(
|
|
110
110
|
self,
|
|
111
|
-
*titles:
|
|
111
|
+
*titles: ContentType,
|
|
112
112
|
initial: str = "",
|
|
113
113
|
name: str | None = None,
|
|
114
114
|
id: str | None = None, # noqa: A002
|
|
@@ -150,7 +150,7 @@ class DataCatalog(TabbedContent, can_focus=True):
|
|
|
150
150
|
error=Exception(
|
|
151
151
|
"Could not load s3 catalog because boto3 is not available.\n\n"
|
|
152
152
|
"Re-install harlequin with the s3 extra, like this:\n"
|
|
153
|
-
"
|
|
153
|
+
"uv tool install harlequin[s3]"
|
|
154
154
|
),
|
|
155
155
|
)
|
|
156
156
|
)
|
|
@@ -166,7 +166,7 @@ class DataCatalog(TabbedContent, can_focus=True):
|
|
|
166
166
|
def on_focus(self) -> None:
|
|
167
167
|
try:
|
|
168
168
|
active_widget = self.query_one(f"#{self.active}").children[0]
|
|
169
|
-
except NoMatches:
|
|
169
|
+
except (NoMatches, InvalidQueryFormat):
|
|
170
170
|
self.database_tree.focus()
|
|
171
171
|
else:
|
|
172
172
|
active_widget.focus()
|
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from asyncio import PriorityQueue
|
|
4
4
|
from contextlib import suppress
|
|
5
|
-
from pathlib import Path
|
|
6
5
|
from typing import TYPE_CHECKING, Generator, Iterable, TypeVar
|
|
7
6
|
|
|
8
7
|
from rich.style import Style
|
|
@@ -77,7 +76,7 @@ class DatabaseTree(HarlequinTree[CatalogItem], inherit_bindings=False):
|
|
|
77
76
|
async def watch_catalog(self, catalog: Catalog | None) -> None:
|
|
78
77
|
"""Watch for changes to the `catalog` of the database tree.
|
|
79
78
|
|
|
80
|
-
If the Catalog is changed the
|
|
79
|
+
If the Catalog is changed the database tree will be repopulated using
|
|
81
80
|
the new value as the root.
|
|
82
81
|
"""
|
|
83
82
|
assert isinstance(self.root.data, CatalogItem)
|
|
@@ -91,18 +90,12 @@ class DatabaseTree(HarlequinTree[CatalogItem], inherit_bindings=False):
|
|
|
91
90
|
|
|
92
91
|
def _add_to_load_queue(
|
|
93
92
|
self, node: TreeNode[InteractiveCatalogItem], priority: int = 100
|
|
94
|
-
) ->
|
|
93
|
+
) -> None:
|
|
95
94
|
"""Add the given node to the load priority queue.
|
|
96
95
|
|
|
97
|
-
The return value can optionally be awaited until the queue is empty.
|
|
98
|
-
|
|
99
96
|
Args:
|
|
100
97
|
node: The node to add to the load queue.
|
|
101
98
|
priority: An order for this node to be loaded; lowest first.
|
|
102
|
-
|
|
103
|
-
Returns:
|
|
104
|
-
An optionally awaitable object that can be awaited until the
|
|
105
|
-
load queue has finished processing.
|
|
106
99
|
"""
|
|
107
100
|
assert node.data is not None
|
|
108
101
|
if not node.data.loaded:
|
|
@@ -111,8 +104,6 @@ class DatabaseTree(HarlequinTree[CatalogItem], inherit_bindings=False):
|
|
|
111
104
|
# since TreeNodes do not implement cmp operators.
|
|
112
105
|
self._load_queue.put_nowait((priority, str(node.label), id(node), node))
|
|
113
106
|
|
|
114
|
-
return AwaitComplete(self._load_queue.join())
|
|
115
|
-
|
|
116
107
|
def reload(self) -> AwaitComplete:
|
|
117
108
|
"""Reload the `DirectoryTree` contents.
|
|
118
109
|
|
|
@@ -322,7 +313,7 @@ class DatabaseTree(HarlequinTree[CatalogItem], inherit_bindings=False):
|
|
|
322
313
|
# Get the next node that needs loading off the queue. Note that
|
|
323
314
|
# this blocks if the queue is empty.
|
|
324
315
|
*_, node = await self._load_queue.get()
|
|
325
|
-
content: list[
|
|
316
|
+
content: list[CatalogItem] = []
|
|
326
317
|
async with self.lock:
|
|
327
318
|
try:
|
|
328
319
|
# Spin up a short-lived thread that will load the content of
|
|
@@ -354,7 +345,7 @@ class DatabaseTree(HarlequinTree[CatalogItem], inherit_bindings=False):
|
|
|
354
345
|
return
|
|
355
346
|
if isinstance(node.data, InteractiveCatalogItem) and not node.data.loaded:
|
|
356
347
|
# if this node isn't loaded yet, add it to the front of the queue
|
|
357
|
-
|
|
348
|
+
self._add_to_load_queue(node, priority=0) # type: ignore[arg-type]
|
|
358
349
|
if (
|
|
359
350
|
isinstance(node.data, CatalogItem)
|
|
360
351
|
and node.data.children
|
|
@@ -293,7 +293,7 @@ class DebugInfoScreen(ModalScreen):
|
|
|
293
293
|
self.set_focus(focusables[next_idx])
|
|
294
294
|
|
|
295
295
|
def on_key(self, event: events.Key) -> None:
|
|
296
|
-
if event.key
|
|
296
|
+
if event.key == "escape":
|
|
297
297
|
self.app.pop_screen()
|
|
298
298
|
event.stop()
|
|
299
299
|
elif event.key == "pageup":
|
|
@@ -64,7 +64,7 @@ def _export_csv(
|
|
|
64
64
|
if "header" not in kwargs:
|
|
65
65
|
kwargs["header"] = False
|
|
66
66
|
try:
|
|
67
|
-
relation = duckdb.arrow(data)
|
|
67
|
+
relation = duckdb.arrow(data)
|
|
68
68
|
relation.write_csv(file_name=dest_path, **kwargs)
|
|
69
69
|
except (duckdb.Error, OSError) as e:
|
|
70
70
|
raise HarlequinCopyError(
|
|
@@ -81,7 +81,7 @@ def _export_parquet(
|
|
|
81
81
|
import duckdb
|
|
82
82
|
|
|
83
83
|
try:
|
|
84
|
-
relation = duckdb.arrow(data)
|
|
84
|
+
relation = duckdb.arrow(data)
|
|
85
85
|
relation.write_parquet(
|
|
86
86
|
file_name=dest_path, compression=kwargs.get("compression")
|
|
87
87
|
)
|
|
@@ -522,7 +522,7 @@ class HarlequinKeys(AppBase):
|
|
|
522
522
|
modified_bindings = [
|
|
523
523
|
new_binding
|
|
524
524
|
for new_binding, old_binding in zip(
|
|
525
|
-
self.bindings.values(), self.unmodifed_bindings.values()
|
|
525
|
+
self.bindings.values(), self.unmodifed_bindings.values(), strict=False
|
|
526
526
|
)
|
|
527
527
|
if new_binding != old_binding
|
|
528
528
|
]
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
from importlib.metadata import entry_points
|
|
4
4
|
from typing import Literal, Sequence, overload
|
|
5
5
|
|
|
6
6
|
from harlequin.adapter import HarlequinAdapter
|
|
7
7
|
from harlequin.exception import HarlequinConfigError
|
|
8
8
|
from harlequin.keymap import HarlequinKeyMap
|
|
9
9
|
|
|
10
|
-
if sys.version_info < (3, 10):
|
|
11
|
-
from importlib_metadata import entry_points
|
|
12
|
-
else:
|
|
13
|
-
from importlib.metadata import entry_points
|
|
14
|
-
|
|
15
10
|
|
|
16
11
|
def load_adapter_plugins() -> dict[str, type[HarlequinAdapter]]:
|
|
17
12
|
return _load_plugins(group="harlequin.adapter")
|
|
@@ -374,7 +374,7 @@ class HarlequinSqliteAdapter(HarlequinAdapter):
|
|
|
374
374
|
if hasattr(conn, "autocommit"):
|
|
375
375
|
conn.autocommit = True
|
|
376
376
|
|
|
377
|
-
for uri, name in zip(other_dbs, db_names[1:]):
|
|
377
|
+
for uri, name in zip(other_dbs, db_names[1:], strict=False):
|
|
378
378
|
try:
|
|
379
379
|
conn.execute(f"attach database '{uri}' as {name}")
|
|
380
380
|
except sqlite3.Error as e:
|
|
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
|