Skip to content

Commit

Permalink
feat(cli): neatly handle invalid directory
Browse files Browse the repository at this point in the history
Raise a user friendly CLick error
if user passes a directory that doesn't exist to the read command.

Closes #5
  • Loading branch information
jonbiemond committed Mar 12, 2024
1 parent d6980f6 commit b10c891
Show file tree
Hide file tree
Showing 4 changed files with 28 additions and 14 deletions.
12 changes: 7 additions & 5 deletions heave/cli.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Heave CLI."""

from pathlib import Path

import click
from sqlalchemy import create_engine
Expand Down Expand Up @@ -102,7 +102,7 @@ def cli(


@cli.command()
@click.argument("path", type=click.Path(exists=True))
@click.argument("path", type=click.Path(exists=True, readable=True, path_type=Path))
@click.option("-t", "--table", required=True, help="Table to insert into.")
@click.option("-s", "--schema", help="Table schema name.")
@click.option(
Expand All @@ -112,7 +112,7 @@ def cli(
help="Handle conflict errors.",
)
@click.pass_obj
def insert(obj, path: str, table: str, schema: str | None, on_conflict: str | None):
def insert(obj, path: Path, table: str, schema: str | None, on_conflict: str | None):
"""Insert data from a file into a table."""
data = file.read_csv(path)
sql_table = sql.reflect_table(obj, table, schema)
Expand All @@ -121,12 +121,14 @@ def insert(obj, path: str, table: str, schema: str | None, on_conflict: str | No


@cli.command()
@click.argument("path", type=click.Path(exists=False))
@click.argument("path", type=click.Path(exists=False, writable=True, path_type=Path))
@click.option("-t", "--table", required=True, help="Table to read.")
@click.option("-s", "--schema", help="Table schema name.")
@click.pass_obj
def read(obj, path: str, table: str, schema: str | None):
def read(obj, path: Path, table: str, schema: str | None):
"""Read data from a table and write it to a file."""
if not (d := path.parent).exists():
raise click.ClickException(f"No such directory: '{d}'")
sql_table = sql.reflect_table(obj, table, schema)
data = sql.read(obj, sql_table)
file.write_csv(data, path)
Expand Down
5 changes: 3 additions & 2 deletions heave/file.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Extract tabular data into a custom Table object."""
import csv
from pathlib import Path


class Table:
Expand All @@ -24,15 +25,15 @@ def __eq__(self, other):
return self._data == other._data


def read_csv(file: str) -> Table:
def read_csv(file: Path) -> Table:
"""Read a csv file and return a Table."""
with open(file, newline="") as f:
reader = csv.reader(f)
data = [tuple(row) for row in reader]
return Table(data)


def write_csv(data: Table, file: str) -> None:
def write_csv(data: Table, file: Path) -> None:
"""Write a Table to a csv file."""
with open(file, "w", newline="") as f:
writer = csv.writer(f)
Expand Down
22 changes: 16 additions & 6 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test CLI."""

import os
from pathlib import Path
from unittest.mock import ANY, Mock

import pytest
Expand Down Expand Up @@ -117,9 +118,9 @@ def test_insert_schema(self, runner, monkeypatch):
mock_reflect_table = Mock()
monkeypatch.setattr("heave.sql.reflect_table", mock_reflect_table)
runner.invoke(
cli, ["insert", "--schema", "time", "--table", "clock", self.test_file]
cli, ["insert", "--schema", "sales", "--table", "record", self.test_file]
)
mock_reflect_table.assert_called_with(ANY, "clock", "time")
mock_reflect_table.assert_called_with(ANY, "record", "sales")

def test_insert_error(self, runner, monkeypatch):
"""Test that changes are rolled back on error."""
Expand All @@ -136,7 +137,7 @@ def test_insert_error(self, runner, monkeypatch):
assert result.exit_code == 1
result = runner.invoke(cli, ["read", "--table", "user", self.test_file])
assert result.exit_code == 0
table = file.read_csv(self.test_file)
table = file.read_csv(Path(self.test_file))
for row in table.rows:
assert row[1] != "jane.doe"

Expand All @@ -158,7 +159,10 @@ def test_insert_conflict_invalid(self, runner):
cli, ["insert", "--table", "user", "--on-conflict", "foo", self.test_file]
)
assert result.exit_code == 2
assert "Error: Invalid value for '--on-conflict': 'foo' is not one of 'nothing', 'update'."
assert (
"Error: Invalid value for '-oc' / '--on-conflict': 'foo' is not one of 'nothing', 'update'."
in result.output
)

def test_read(self, runner, monkeypatch):
"""Test the read command."""
Expand All @@ -174,6 +178,12 @@ def test_read_schema(self, runner, monkeypatch):
mock_reflect_table = Mock()
monkeypatch.setattr("heave.sql.reflect_table", mock_reflect_table)
runner.invoke(
cli, ["read", "--schema", "time", "--table", "clock", self.test_file]
cli, ["read", "--schema", "sales", "--table", "record", self.test_file]
)
mock_reflect_table.assert_called_with(ANY, "clock", "time")
mock_reflect_table.assert_called_with(ANY, "record", "sales")

def test_read_invalid_directory(self, runner):
"""Test that Click error is raised for a non-existant directory."""
result = runner.invoke(cli, ["read", "--table", "user", "invalid/myfile.csv"])
assert result.exit_code == 1
assert "Error: No such directory: 'invalid'" in result.output
3 changes: 2 additions & 1 deletion tests/test_file.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Tests for the extract module."""
import os
from pathlib import Path

import pytest

Expand All @@ -21,7 +22,7 @@ def test_init(self):
class TestCsv:
"""Test the read_csv function."""

test_file = "temp.csv"
test_file = Path("temp.csv")

@pytest.fixture(autouse=True)
def temp_file(self):
Expand Down

0 comments on commit b10c891

Please sign in to comment.