Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

display improvements #432

Merged
merged 25 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

## 0.7.7dev

* [Doc] Hiding connection string when passing `--alias` when opening a connection (#432)
* [Doc] Fix `api/magic-sql.md` since it incorrectly stated that listing functions was `--list`, but it's `--connections` (#432)
* [Feature] Clearer message display when executing queries, listing connections and persisting data frames (#432)
* [Feature] `%sql --connections` now displays an HTML table in Jupyter and a text-based table in the terminal

## 0.7.6 (2023-05-29)

* [Feature] Add `%sqlcmd explore` to explore tables interactively ([#330](https://github.com/ploomber/jupysql/issues/330))

* [Feature] Support for printing capture variables using `=<<` syntax (by [@jorisroovers](https://github.com/jorisroovers))

* [Feature] Adds `--persist-replace` argument to replace existing tables when persisting data frames ([#440](https://github.com/ploomber/jupysql/issues/440))

* [Fix] Fix error when checking if custom connection was PEP 249 Compliant ([#517](https://github.com/ploomber/jupysql/issues/517))

* [Doc] documenting how to manage connections with `Connection` object ([#282](https://github.com/ploomber/jupysql/issues/282))

## 0.7.5 (2023-05-24)
Expand Down
4 changes: 2 additions & 2 deletions doc/api/magic-sql.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ To make all subsequent queries to use certain connection, pass the connection na
You can inspect which is the current active connection:

```{code-cell} ipython3
%sql --list
%sql --connections
```

For more details on managing connections, see [Switch connections](../howto.md#switch-connections).
Expand All @@ -121,7 +121,7 @@ For more details on managing connections, see [Switch connections](../howto.md#s
## List connections

```{code-cell} ipython3
%sql --list
%sql --connections
```

## Close connection
Expand Down
21 changes: 21 additions & 0 deletions doc/community/developer-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ Before continuing, ensure you have a working [development environment.](https://

+++

## Displaying messages

```{important}
Use the `sql.display` module instead of `print` for showing feedback to the user.
```

You can use `message` (contextual information) and `message_success` (successful operations) to show feedback to the user. Here's an example:
edublancas marked this conversation as resolved.
Show resolved Hide resolved

```{code-cell} ipython3
from sql.display import message, message_success
```

```{code-cell} ipython3
message("Some information")
edublancas marked this conversation as resolved.
Show resolved Hide resolved
```

```{code-cell} ipython3
message_success("Some operation finished successfully!")
```


## Throwing errors

When writing Python libraries, we often throw errors (and display error tracebacks) to let users know that something went wrong. However, JupySQL is an abstraction for executing SQL queries; hence, Python tracebacks a useless to end-users since they expose JupySQL's internals.
Expand Down
23 changes: 23 additions & 0 deletions doc/howto.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,3 +389,26 @@ import warnings

warnings.filterwarnings("ignore", category=FutureWarning)
```

```{code-cell} ipython3
conns = %sql --connections
conns["db-three"]
```

## Hide connection string

If you want to hide the connection string, pass an alias

```{code-cell} ipython3
%sql --close duckdb://
```

```{code-cell} ipython3
%sql duckdb:// --alias myconnection
```

The alias will be displayed instead of the connection string:

```{code-cell} ipython3
%sql SELECT * FROM 'penguins.csv' LIMIT 3
```
9 changes: 9 additions & 0 deletions src/sql/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ class SQLCommand:
"""

def __init__(self, magic, user_ns, line, cell) -> None:
self._line = line
self._cell = cell

self.args = parse.magic_args(magic.execute, line)
# self.args.line (everything that appears after %sql/%%sql in the first line)
# is split in tokens (delimited by spaces), this checks if we have one arg
Expand Down Expand Up @@ -103,3 +106,9 @@ def return_result_var(self):

def _var_expand(self, sql, user_ns, magic):
return Template(sql).render(user_ns)

def __repr__(self) -> str:
return (
f"{type(self).__name__}(line={self._line!r}, cell={self._cell!r}) -> "
f"({self.sql!r}, {self.sql_original!r})"
)
108 changes: 83 additions & 25 deletions src/sql/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from sql.store import store
from sql.telemetry import telemetry
from sql import exceptions
from sql import exceptions, display
from sql.error_message import detail
from ploomber_core.exceptions import modify_exceptions

Expand Down Expand Up @@ -153,6 +153,30 @@ class Connection:
----------
engine: sqlalchemy.engine.Engine
The SQLAlchemy engine to use

Attributes
----------
alias : str or None
The alias passed in the constructor

engine : sqlalchemy.engine.Engine
The SQLAlchemy engine passed to the constructor

name : str
A name to identify the connection: {user}@{database_name}

metadata : Metadata or None
An SQLAlchemy Metadata object (if using SQLAlchemy 2, this is None),
used to retrieve connection information

url : str
An obfuscated connection string (password hidden)

dialect : sqlalchemy dialect
A SQLAlchemy dialect object

session : sqlalchemy session
A SQLAlchemy session object
"""

# the active connection
Expand All @@ -162,26 +186,26 @@ class Connection:
connections = {}

def __init__(self, engine, alias=None):
self.url = engine.url
self.name = self.assign_name(engine)
self.dialect = self.url.get_dialect()
self.alias = alias
self.engine = engine
self.name = self.assign_name(engine)

if IS_SQLALCHEMY_ONE:
self.metadata = sqlalchemy.MetaData(bind=engine)
else:
self.metadata = None

url = (
self.url = (
repr(sqlalchemy.MetaData(bind=engine).bind.url)
if IS_SQLALCHEMY_ONE
else repr(engine.url)
)

self.session = self._create_session(engine, url)

self.connections[alias or url] = self
self.dialect = engine.url.get_dialect()
self.session = self._create_session(engine, self.url)
edublancas marked this conversation as resolved.
Show resolved Hide resolved

self.connections[alias or self.url] = self
self.connect_args = None
self.alias = alias
Connection.current = self

@classmethod
Expand Down Expand Up @@ -362,8 +386,7 @@ def set(cls, descriptor, displaycon, connect_args=None, creator=None, alias=None
else:
if cls.connections:
if displaycon:
# display list of connections
print(cls.connection_list())
cls.display_current_connection()
elif os.getenv("DATABASE_URL"):
cls.current = Connection.from_connect_str(
connect_str=os.getenv("DATABASE_URL"),
Expand All @@ -382,27 +405,53 @@ def assign_name(cls, engine):
return name

@classmethod
def connection_list(cls):
"""Returns the list of connections, appending '*' to the current one"""
result = []
def _get_connections(cls):
"""
Return a list of dictionaries
"""
connections = []

for key in sorted(cls.connections):
conn = cls.connections[key]

if cls.is_custom_connection(conn):
engine_url = conn.url
else:
engine_url = conn.metadata.bind.url if IS_SQLALCHEMY_ONE else conn.url
current = conn == cls.current

prefix = "* " if conn == cls.current else " "
connections.append(
{
"current": current,
"key": key,
"url": conn.url,
"alias": conn.alias,
"connection": conn,
}
)

if conn.alias:
repr_ = f"{prefix} ({conn.alias}) {engine_url!r}"
else:
repr_ = f"{prefix} {engine_url!r}"
return connections

result.append(repr_)
@classmethod
def display_current_connection(cls):
for conn in cls._get_connections():
if conn["current"]:
alias = conn.get("alias")
if alias:
display.message(f"Running query in {alias!r}")
else:
display.message(f"Running query in {conn['url']!r}")
edublancas marked this conversation as resolved.
Show resolved Hide resolved

return "\n".join(result)
@classmethod
def connections_table(cls):
"""Returns the current connections as a table"""
connections = cls._get_connections()

def map_values(d):
d["current"] = "*" if d["current"] else ""
d["alias"] = d["alias"] if d["alias"] else ""
return d

return display.ConnectionsTable(
headers=["current", "url", "alias"],
rows_maps=[map_values(c) for c in connections],
)

@classmethod
def close(cls, descriptor):
Expand All @@ -426,6 +475,15 @@ def close(cls, descriptor):
)
conn.session.close()

@classmethod
def close_all(cls):
"""Close all active connections"""
connections = Connection.connections.copy()
for key, conn in connections.items():
conn.close(key)

cls.connections = {}

def is_custom_connection(conn=None) -> bool:
"""
Checks if given connection is custom
Expand Down
92 changes: 92 additions & 0 deletions src/sql/display.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""
A module to display confirmation messages and contextual information to the user
"""
import html

from prettytable import PrettyTable
from IPython.display import display


class Table:
"""Provides a txt and html representation of tabular data"""

TITLE = ""

def __init__(self, headers, rows) -> None:
self._headers = headers
self._rows = rows
self._table = PrettyTable()
self._table.field_names = headers

for row in rows:
self._table.add_row(row)

self._table_html = self._table.get_html_string()
self._table_txt = self._table.get_string()

def __repr__(self) -> str:
return self.TITLE + "\n" + self._table_txt

def _repr_html_(self) -> str:
return self.TITLE + "\n" + self._table_html


class ConnectionsTable(Table):
TITLE = "Active connections:"

def __init__(self, headers, rows_maps) -> None:
def get_values(d):
d = {k: v for k, v in d.items() if k not in {"connection", "key"}}
return list(d.values())

rows = [get_values(r) for r in rows_maps]

self._mapping = {}

for row in rows_maps:
self._mapping[row["key"]] = row["connection"]

super().__init__(headers=headers, rows=rows)

def __getitem__(self, key: str):
"""
This method is provided for backwards compatibility. Before
creating ConnectionsTable, `%sql --connections` returned a dictionary,
hence users could retrieve connections using __getitem__. Note that this
was undocumented so we might decide to remove it in the future.
"""
return self._mapping[key]

def __iter__(self):
"""Also provided for backwards compatibility"""
for key in self._mapping:
yield key

def __len__(self):
"""Also provided for backwards compatibility"""
return len(self._mapping)


class Message:
"""Message for the user"""

def __init__(self, message, style=None) -> None:
self._message = message
self._message_html = html.escape(message)
self._style = "" or style

def _repr_html_(self):
return f'<span style="{self._style}">{self._message_html}</span>'

def __repr__(self) -> str:
return self._message


def message(message):
"""Display a generic message"""
display(Message(message))


def message_success(message):
"""Display a success message"""
display(Message(message, style="color: green"))
Loading