Skip to content

Commit

Permalink
Merge pull request #53 from ploomber/closing
Browse files Browse the repository at this point in the history
closing a bunch of issues
  • Loading branch information
idomic authored Jan 2, 2023
2 parents 9367c22 + 647bea7 commit 482d312
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 19 deletions.
47 changes: 47 additions & 0 deletions doc/connecting.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,53 @@ a flag with (-a|--connection_arguments)the connection string as a JSON string. S
%sql -a '{"timeout":10, "mode":"ro"}' sqlite:// SELECT * from work;
```

+++

## Connecting securely

**It is highly recommended** that you do not pass plain credentials.

```{code-cell} ipython3
%load_ext sql
```

### Building connection string

One option is to use `getpass`, type your password, build your connection string and pass it to `%sql`:

+++

```python
from getpass import getpass

password = getpass()
connection_string = f"postgresql://user:{password}@localhost/database"
%sql $connection_string
```

+++

### Using `DATABASE_URL`

+++

You may also set the `DATABASE_URL` environment variable, and `%sql` will automatically load it from there. You can do it either by setting the environment variable from your terminal or in your notebook:

```python
from getpass import getpass
from os import environ

password = getpass()
environ["DATABASE_URL"] = f"postgresql://user:{password}@localhost/database"
```

```python
# without any args, %sql reads from DATABASE_URL
%sql
```

+++

## DSN connections

Alternately, you can store connection info in a configuration file, under a section name chosen to refer to your database.
Expand Down
39 changes: 39 additions & 0 deletions doc/howto.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,42 @@ Pass existing engines to `%sql`
%%sql
SELECT MYSUM(1, 2)
```

## Connect to a SQLite database with spaces

Currently, due to a limitation in the argument parser, it's not possible to directly connect to SQLite databases whose path contains spaces; however, you can do it by creating the engine first.

### Setup

```{code-cell} ipython3
%pip install jupysql --quiet
```

```{code-cell} ipython3
%load_ext sql
```

## Connect to db

```{code-cell} ipython3
from sqlalchemy import create_engine
engine = create_engine("sqlite:///my database.db")
```

Add some sample data:

```{code-cell} ipython3
import pandas as pd
_ = pd.DataFrame({"x": range(5)}).to_sql("numbers", engine)
```

```{code-cell} ipython3
%sql engine
```

```{code-cell} ipython3
%%sql
SELECT * FROM numbers
```
2 changes: 1 addition & 1 deletion src/sql/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ def connection_list(cls):
template = " * {}"
else:
template = " {}"
result.append(template.format(engine_url.__repr__()))
result.append(template.format(repr(engine_url)))
return "\n".join(result)

@classmethod
Expand Down
1 change: 1 addition & 0 deletions src/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def ip():
"CREATE TABLE author (first_name, last_name, year_of_death)",
"INSERT INTO author VALUES ('William', 'Shakespeare', 1616)",
"INSERT INTO author VALUES ('Bertold', 'Brecht', 1956)",
"CREATE TABLE empty_table (column INT, another INT)",
],
)
yield ip_session
Expand Down
4 changes: 3 additions & 1 deletion src/tests/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def sql_magic(ip):
("sqlite://", "", "", "sqlite://", None),
("SELECT * FROM TABLE", "", "SELECT * FROM TABLE\n", "", None),
("SELECT * FROM", "TABLE", "SELECT * FROM\nTABLE", "", None),
("my_var << SELECT * FROM table", "", "SELECT * FROM table\n", "", "my_var"),
("my_var << SELECT *", "FROM table", "SELECT *\nFROM table", "", "my_var"),
("[db]", "", "", "sqlite://", None),
],
Expand All @@ -26,7 +27,8 @@ def sql_magic(ip):
"connection-string",
"sql-query",
"sql-query-in-line-and-cell",
"parsed-var",
"parsed-var-single-line",
"parsed-var-multi-line",
"config",
],
)
Expand Down
15 changes: 15 additions & 0 deletions src/tests/test_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import sys
from unittest.mock import Mock

from sqlalchemy.engine import Engine

from sql.connection import Connection


def test_password_isnt_displayed(monkeypatch):
monkeypatch.setitem(sys.modules, "psycopg2", Mock())
monkeypatch.setattr(Engine, "connect", Mock())

Connection.from_connect_str("postgresql://user:topsecret@somedomain.com/db")

assert "topsecret" not in Connection.connection_list()
71 changes: 54 additions & 17 deletions src/tests/test_magic.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pathlib import Path
import os.path
import re
import tempfile
Expand All @@ -6,6 +7,7 @@
import pytest
from sqlalchemy import create_engine

from sql.connection import Connection
from conftest import runsql


Expand Down Expand Up @@ -179,25 +181,25 @@ def test_connection_args_double_quotes(ip):
# assert 'Shakespeare' in str(persisted)


@pytest.mark.parametrize("value", ["None", "0"])
def test_displaylimit_disabled(ip, value):
ip.run_line_magic("config", "SqlMagic.autolimit = None")

ip.run_line_magic("config", f"SqlMagic.displaylimit = {value}")
result = runsql(ip, "SELECT * FROM author;")

assert "Brecht" in result._repr_html_()
assert "Shakespeare" in result._repr_html_()


def test_displaylimit(ip):
ip.run_line_magic("config", "SqlMagic.autolimit = None")
ip.run_line_magic("config", "SqlMagic.displaylimit = None")
result = runsql(
ip,
"SELECT * FROM (VALUES ('apple'), ('banana'), ('cherry')) "
"AS Result ORDER BY 1;",
)
assert "apple" in result._repr_html_()
assert "banana" in result._repr_html_()
assert "cherry" in result._repr_html_()

ip.run_line_magic("config", "SqlMagic.displaylimit = 1")
result = runsql(
ip,
"SELECT * FROM (VALUES ('apple'), ('banana'), ('cherry')) "
"AS Result ORDER BY 1;",
)
assert "apple" in result._repr_html_()
assert "cherry" not in result._repr_html_()
result = runsql(ip, "SELECT * FROM author ORDER BY first_name;")

assert "Brecht" in result._repr_html_()
assert "Shakespeare" not in result._repr_html_()


def test_column_local_vars(ip):
Expand Down Expand Up @@ -368,14 +370,41 @@ def test_json_in_select(ip):
assert result == [('{"greeting": "Farewell sweet {person}"}',)]


def test_close_connection(ip):
def test_closed_connections_are_no_longer_listed(ip):
connections = runsql(ip, "%sql -l")
connection_name = list(connections)[0]
runsql(ip, f"%sql -x {connection_name}")
connections_afterward = runsql(ip, "%sql -l")
assert connection_name not in connections_afterward


def test_close_connection(ip, tmp_empty):
# open two connections
ip.run_cell("%sql sqlite:///one.db")
ip.run_cell("%sql sqlite:///two.db")

# close them
ip.run_cell("%sql -x sqlite:///one.db")
ip.run_cell("%sql --close sqlite:///two.db")

assert "sqlite:///one.db" not in Connection.connections
assert "sqlite:///two.db" not in Connection.connections


def test_column_names_visible(ip, tmp_empty):
res = ip.run_line_magic("sql", "SELECT * FROM empty_table")

assert "<th>column</th>" in res._repr_html_()
assert "<th>another</th>" in res._repr_html_()


@pytest.mark.xfail(reason="known parse @ parser.py error")
def test_sqlite_path_with_spaces(ip, tmp_empty):
ip.run_cell("%sql sqlite:///some database.db")

assert Path("some database.db").is_file()


def test_pass_existing_engine(ip, tmp_empty):
ip.user_global_ns["my_engine"] = create_engine("sqlite:///my.db")
ip.run_line_magic("sql", " my_engine ")
Expand All @@ -396,9 +425,17 @@ def test_pass_existing_engine(ip, tmp_empty):

# theres some weird shared state with this one, moving it to the end
def test_autolimit(ip):
# test table has two rows
ip.run_line_magic("config", "SqlMagic.autolimit = 0")
result = runsql(ip, "SELECT * FROM test;")
assert len(result) == 2

# test table has two rows
ip.run_line_magic("config", "SqlMagic.autolimit = None")
result = runsql(ip, "SELECT * FROM test;")
assert len(result) == 2

# test setting autolimit to 1
ip.run_line_magic("config", "SqlMagic.autolimit = 1")
result = runsql(ip, "SELECT * FROM test;")
assert len(result) == 1

0 comments on commit 482d312

Please sign in to comment.