Skip to content

Commit

Permalink
Add sqlite adapter; improve --help and --version (#324)
Browse files Browse the repository at this point in the history
* feat: add sqlite adapter

* refactor: reorg adapters under src directory

* feat: style cli help option with rich-click

* feat: show version of adapters, close #317
  • Loading branch information
tconbeer committed Nov 15, 2023
1 parent 52c58d6 commit 5d0ec1f
Show file tree
Hide file tree
Showing 29 changed files with 8,892 additions and 2,609 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*.json
*.parquet
*.sql
*.sqlite
!tests/data/**/*.db
!tests/data/**/*.csv
!tests/data/**/*.json
Expand Down
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ repos:
- shandy-sqlfmt[jinjafmt]
- textual>=0.40.0
- textual-textarea>=0.7.2
- textual-fastdatatable>=0.2.0
- textual-fastdatatable>=0.4.0
- pytest
- types-pygments
- rich-click>=1.7.1
args:
- "--disallow-untyped-calls"
- "--disallow-untyped-defs"
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Features

- Harlequin now ships with an experimental SQLite adapter and can be used to query any SQLite database (including an in-memory database). You can select the adapter by starting Harlequin with `harlequin -a sqlite` (for an in-memory session) or `harlequin -a sqlite my.db`.
- `harlequin --help` is all-new, with a glow-up provided by [`rich-click`](https://github.com/ewels/rich-click). Options for each adapter are separated into their own panels.
- `harlequin --version` now shows the versions of installed database adapters ([#317](https://github.com/tconbeer/harlequin/issues/317)).

### Refactoring

- The code for the DuckDB adapter has been moved from `/plugins/harlequin_duckdb` to `/src/harlequin_duckdb`.

## [1.3.1] - 2023-11-13

### Bug Fixes
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ lint:
serve:
textual run --dev -c harlequin f1.db

.PHONY: sqlite
sqlite:
textual run --dev -c harlequin -a sqlite

marketing: $(wildcard static/themes/*.svg) static/harlequin.gif

static/themes/%.svg: pyproject.toml
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/harlequin)
![Runs on Linux | MacOS | Windows](https://img.shields.io/badge/runs%20on-Linux%20%7C%20MacOS%20%7C%20Windows-blue)

The DuckDB IDE for Your Terminal.
The SQL IDE for Your Terminal.

![Harlequin Demo Video](static/harlequin.gif)

Expand Down
27 changes: 23 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
[tool.poetry]
name = "harlequin"
version = "1.3.1"
description = "The DuckDB IDE for Your Terminal."
description = "The SQL IDE for Your Terminal."
authors = ["Ted Conbeer <tconbeer@users.noreply.github.com>"]
license = "MIT"
homepage = "https://harlequin.sh"
repository = "https://github.com/tconbeer/harlequin"
readme = "README.md"
packages = [
{ include = "harlequin", from = "src" },
{ include = "harlequin_duckdb", from = "plugins" },
{ include = "harlequin_duckdb", from = "src" },
{ include = "harlequin_sqlite", from = "src" },
]

[build-system]
Expand All @@ -19,14 +20,15 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry.dependencies]
python = ">=3.8.1,<4.0.0"
textual = "==0.41.0"
textual-fastdatatable = "==0.3.0"
textual-fastdatatable = "==0.4.0"
textual-textarea = "==0.7.3"
click = "^8.1.3"
duckdb = ">=0.8.0"
shandy-sqlfmt = ">=0.19.0"
platformdirs = "^3.10.0"
pyperclip = "^1.8.2"
importlib_metadata = { version = ">=4.6.0", python = "<3.10.0" }
rich-click = "^1.7.1"

[tool.poetry.group.dev.dependencies]
pre-commit = "^3.3.1"
Expand All @@ -50,6 +52,7 @@ harlequin = "harlequin.cli:harlequin"

[tool.poetry.plugins."harlequin.adapter"]
duckdb = "harlequin_duckdb:DuckDbAdapter"
sqlite = "harlequin_sqlite:HarlequinSqliteAdapter"

[tool.ruff]
select = ["A", "B", "E", "F", "I"]
Expand All @@ -58,7 +61,7 @@ target-version = "py38"
[tool.mypy]
python_version = "3.8"
files = [
"src/harlequin/**/*.py",
"src/**/*.py",
"tests/**/*.py",
]
mypy_path = "src:stubs"
Expand Down
8 changes: 5 additions & 3 deletions src/harlequin/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,19 @@ def set_limit(self, limit: int) -> "HarlequinCursor":
pass

@abstractmethod
def fetchall(self) -> AutoBackendType:
def fetchall(self) -> AutoBackendType | None:
"""
Returns data from the cursor's result set. Can return any type supported
by textual-fastdatatable. If set_limit is called prior to fetchall,
this method only returns the limited number of records.
this method only returns the limited number of records. If the query returns
no rows, fetchall should return None.
Returns:
pyarrow.Table |
pyarrow.Record Batch |
Sequence[Iterable[Any]] |
Mapping[str, Sequence[Any]]
Mapping[str, Sequence[Any]] |
None
"""
pass

Expand Down
4 changes: 2 additions & 2 deletions src/harlequin/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ async def _set_result_viewer_data(
await self.results_viewer.push_table(
table_id=id_,
column_labels=cur.columns(),
data=cur_data,
data=cur_data, # type: ignore
)
if errors:
header = getattr(
Expand Down Expand Up @@ -509,6 +509,6 @@ def _validate_selection(self) -> str:
try:
return self.connection.validate_sql(selection)
except NotImplementedError:
return ""
return selection
else:
return ""
95 changes: 86 additions & 9 deletions src/harlequin/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,83 @@
import sys
from typing import Any, Callable

import click
import rich_click as click

from harlequin import Harlequin
from harlequin.adapter import HarlequinAdapter

if sys.version_info < (3, 10):
from importlib_metadata import entry_points
from importlib_metadata import entry_points, version
else:
from importlib.metadata import entry_points
from importlib.metadata import entry_points, version

# configure the rich click interface (mostly --help options)
DOCS_URL = "https://harlequin.sh/docs/getting-started"
GREEN = "#45FFCA"
YELLOW = "#FEFFAC"
PINK = "#FFB6D9"
PURPLE = "#D67BFF"

# general
click.rich_click.USE_RICH_MARKUP = True
click.rich_click.COLOR_SYSTEM = "truecolor"

click.rich_click.STYLE_OPTIONS_TABLE_LEADING = 1
click.rich_click.STYLE_OPTIONS_TABLE_BOX = "SIMPLE"
click.rich_click.STYLE_OPTIONS_PANEL_BORDER = YELLOW
click.rich_click.STYLE_USAGE = f"bold {YELLOW}"
click.rich_click.STYLE_USAGE_COMMAND = "regular"
click.rich_click.STYLE_HELPTEXT = "regular"
click.rich_click.STYLE_OPTION = PINK
click.rich_click.STYLE_ARGUMENT = PINK
click.rich_click.STYLE_COMMAND = PINK
click.rich_click.STYLE_SWITCH = GREEN

# metavars
click.rich_click.SHOW_METAVARS_COLUMN = False
click.rich_click.APPEND_METAVARS_HELP = True
click.rich_click.STYLE_METAVAR_APPEND = PURPLE
click.rich_click.STYLE_METAVAR_SEPARATOR = PURPLE

# errors
click.rich_click.STYLE_ERRORS_SUGGESTION = "italic"
click.rich_click.ERRORS_SUGGESTION = "Try 'harlequin --help' to view available options."
click.rich_click.ERRORS_EPILOGUE = (
f"To learn more, visit [link={DOCS_URL}]{DOCS_URL}[/link]"
)

# define main option group (adapter options added to own groups below)
click.rich_click.OPTION_GROUPS = {
"harlequin": [
{
"name": "Harlequin Options",
"options": ["--adapter", "--theme", "--limit", "--version", "--help"],
},
]
}


def _version_option() -> str:
"""
Build the string printed by harlequin --version
"""
harlequin_version = version("harlequin")
adapter_eps = entry_points(group="harlequin.adapter")
adapter_versions: dict[str, str] = {}
for ep in adapter_eps:
adapter_versions.update({ep.name: ep.dist.version})

adapter_output = "\n".join(
[f" - {name}, version {version}" for name, version in adapter_versions.items()]
)

output = (
f"harlequin, version {harlequin_version}\n\n"
"Installed Adapters:\n"
f"{adapter_output}"
)

return output


def build_cli() -> click.Command:
Expand All @@ -34,7 +102,7 @@ def build_cli() -> click.Command:
)

@click.command()
@click.version_option(package_name="harlequin")
@click.version_option(package_name="harlequin", message=_version_option())
@click.argument(
"conn_str",
nargs=-1,
Expand Down Expand Up @@ -83,13 +151,17 @@ def inner_cli(
"""
This command starts the Harlequin IDE.
conn_str TEXT: One or more connection strings (or paths to local db files)
for databases to open with Harlequin.
[bold #FFB6D9]CONN_STR[/] [#D67BFF](TEXT MULTIPLE)[/][dim]: One or more
connection strings (or paths to local db files) for databases to open with
Harlequin.[/]
"""
# prune the kwargs to only those that don't have their default arguments
params = list(kwargs.keys())
for k in params:
if ctx.get_parameter_source(k) == click.core.ParameterSource.DEFAULT:
if (
ctx.get_parameter_source(k)
== click.core.ParameterSource.DEFAULT # type: ignore
):
kwargs.pop(k)

# load and instantiate the adapter
Expand All @@ -108,14 +180,19 @@ def inner_cli(
# we load the options into a dict keyed by their name to de-dupe options
# that may be passed by multiple adapters.
options: dict[str, Callable[[click.Command], click.Command]] = {}
for adapter_cls in adapters.values():
for adapter_name, adapter_cls in adapters.items():
option_name_list: list[str] = []
if adapter_cls.ADAPTER_OPTIONS is not None:
for option in adapter_cls.ADAPTER_OPTIONS:
options.update({option.name: option.to_click()})
option_name_list.append(f"--{option.name}")
click.rich_click.OPTION_GROUPS["harlequin"].append(
{"name": f"{adapter_name} Adapter Options", "options": option_name_list}
)

fn = inner_cli
for click_opt in options.values():
fn = click_opt(fn)
fn = click_opt(fn) # type: ignore

return fn

Expand Down
1 change: 1 addition & 0 deletions src/harlequin/components/results_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ async def push_table(
self._format_column_label(col_name, col_type)
for col_name, col_type in column_labels
]
self.app.log(f"LABELS: {formatted_labels}")
table = ResultsTable(
id=table_id,
column_labels=formatted_labels, # type: ignore
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def set_limit(self, limit: int) -> HarlequinCursor:
pass
return self

def fetchall(self) -> AutoBackendType:
def fetchall(self) -> AutoBackendType | None:
try:
result = self.relation.fetch_arrow_table()
except duckdb.Error as e:
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 2 additions & 0 deletions src/harlequin_sqlite/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from harlequin_sqlite.adapter import HarlequinSqliteAdapter as HarlequinSqliteAdapter
from harlequin_sqlite.cli_options import SQLITE_OPTIONS as SQLITE_OPTIONS
Loading

0 comments on commit 5d0ec1f

Please sign in to comment.