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.
Files changed (74) hide show
  1. {harlequin-2.2.1 → harlequin-2.4.0}/CHANGELOG.md +19 -1
  2. {harlequin-2.2.1 → harlequin-2.4.0}/PKG-INFO +20 -15
  3. {harlequin-2.2.1 → harlequin-2.4.0}/pyproject.toml +42 -28
  4. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/app.py +42 -19
  5. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/app.tcss +6 -2
  6. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/autocomplete/completers.py +1 -1
  7. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/cli.py +4 -6
  8. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/code_editor.py +22 -10
  9. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/data_catalog/__init__.py +5 -5
  10. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/data_catalog/database_tree.py +4 -13
  11. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/debug_info.py +1 -1
  12. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/error_modal.py +1 -1
  13. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/export.py +2 -2
  14. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/keys_app.py +1 -1
  15. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/plugins.py +1 -6
  16. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_duckdb/adapter.py +1 -0
  17. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_sqlite/adapter.py +1 -1
  18. {harlequin-2.2.1 → harlequin-2.4.0}/.gitignore +0 -0
  19. {harlequin-2.2.1 → harlequin-2.4.0}/LICENSE +0 -0
  20. {harlequin-2.2.1 → harlequin-2.4.0}/README.md +0 -0
  21. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/__init__.py +0 -0
  22. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/__main__.py +0 -0
  23. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/actions.py +0 -0
  24. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/adapter.py +0 -0
  25. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/app_base.py +0 -0
  26. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/autocomplete/__init__.py +0 -0
  27. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/autocomplete/completion.py +0 -0
  28. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/autocomplete/constants.py +0 -0
  29. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/bindings.py +0 -0
  30. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/cache.py +0 -0
  31. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/catalog.py +0 -0
  32. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/catalog_cache.py +0 -0
  33. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/colors.py +0 -0
  34. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/__init__.py +0 -0
  35. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/confirm_modal.py +0 -0
  36. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/data_catalog/file_tree.py +0 -0
  37. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/data_catalog/s3_tree.py +0 -0
  38. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/data_catalog/tree.py +0 -0
  39. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/export_screen.py +0 -0
  40. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/help_screen.md +0 -0
  41. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/help_screen.py +0 -0
  42. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/history_screen.py +0 -0
  43. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/results_viewer.py +0 -0
  44. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/components/run_query_bar.py +0 -0
  45. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/config.py +0 -0
  46. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/config_wizard.py +0 -0
  47. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/copy_formats.py +0 -0
  48. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/copy_widgets.py +0 -0
  49. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/driver.py +0 -0
  50. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/editor_cache.py +0 -0
  51. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/exception.py +0 -0
  52. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/global.tcss +0 -0
  53. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/history.py +0 -0
  54. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/keymap.py +0 -0
  55. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/keys_app.tcss +0 -0
  56. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/locale_manager.py +0 -0
  57. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/messages.py +0 -0
  58. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/options.py +0 -0
  59. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/py.typed +0 -0
  60. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/transaction_mode.py +0 -0
  61. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin/windows_timezone.py +0 -0
  62. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_duckdb/__init__.py +0 -0
  63. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_duckdb/catalog.py +0 -0
  64. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_duckdb/cli_options.py +0 -0
  65. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_duckdb/completions.py +0 -0
  66. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_duckdb/interactions.py +0 -0
  67. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_duckdb/py.typed +0 -0
  68. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_sqlite/__init__.py +0 -0
  69. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_sqlite/catalog.py +0 -0
  70. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_sqlite/cli_options.py +0 -0
  71. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_sqlite/completions.py +0 -0
  72. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_sqlite/interactions.py +0 -0
  73. {harlequin-2.2.1 → harlequin-2.4.0}/src/harlequin_sqlite/py.typed +0 -0
  74. {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.2.1...HEAD
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.2.1
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
- Requires-Python: <3.14,>=3.9
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: importlib-metadata>=8.0; python_version < '3.10'
18
- Requires-Dist: numpy<2.1.0,>=2.0.0; python_version < '3.10'
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.19.0
25
- Requires-Dist: textual-fastdatatable==0.12.0
26
- Requires-Dist: textual-textarea==0.15.0
27
- Requires-Dist: textual==0.89.1
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.2.1"
5
- requires-python = ">=3.9,<3.14"
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==0.89.1",
16
- "textual-fastdatatable==0.12.0",
17
- "textual-textarea==0.15.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.19.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
- # tree-sitter-sql 0.3.8 seems incompatible with tree-sitter 0.24.0
31
- "tree-sitter-sql==0.3.7",
32
- "tree-sitter>=0.23.0,<0.25.0",
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 = ["harlequin-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.1; python_version < '3.12'",
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.6,<0.10",
70
- "mypy>=1.11.0,<2",
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.1.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 = "py39"
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.9"
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.post_message(
330
- QuerySubmitted(
331
- queries=self._get_selected_queries(),
332
- limit=self.run_query_bar.limit_value,
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.post_message(
378
- QuerySubmitted(
379
- queries=self._get_selected_queries(),
380
- limit=self.run_query_bar.limit_value,
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
- if self._validate_selection():
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.post_message(
526
- QuerySubmitted(
527
- queries=self._get_selected_queries(),
528
- limit=self.run_query_bar.limit_value,
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({ep.name: ep.dist.version})
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.css.query import NoMatches
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
- semicolons = self._query_semicolons()
44
+ if ";" not in self.text:
45
+ return [self.text]
45
46
 
46
- if not semicolons:
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 [*semicolons, self.text_input.document.end]:
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 _query_semicolons(self) -> list[tuple[int, int]]:
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: TextType,
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.css.query import NoMatches
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: TextType,
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
- "pip install harlequin[s3]"
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 directory tree will be repopulated using
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
- ) -> AwaitComplete:
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[Path] = []
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
- await self._add_to_load_queue(node, priority=0) # type: ignore[arg-type]
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 in ("escape", "esc"):
296
+ if event.key == "escape":
297
297
  self.app.pop_screen()
298
298
  event.stop()
299
299
  elif event.key == "pageup":
@@ -13,7 +13,7 @@ class ClickableStatic(Static):
13
13
  def on_click(self, message: events.Click) -> None:
14
14
  message.stop()
15
15
  try:
16
- pyperclip.copy(str(self.renderable))
16
+ pyperclip.copy(str(self.content))
17
17
  except pyperclip.PyperclipException:
18
18
  pass
19
19
  else:
@@ -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) # type: ignore[arg-type]
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) # type: ignore[arg-type]
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 sys
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")
@@ -35,6 +35,7 @@ class DuckDbCursor(HarlequinCursor):
35
35
  zip(
36
36
  self.relation.columns,
37
37
  map(self.conn._short_column_type, self.relation.dtypes),
38
+ strict=False,
38
39
  )
39
40
  )
40
41
 
@@ -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