diff --git a/src/pricehist/outputs/gnucashsql.py b/src/pricehist/outputs/gnucashsql.py index 1cb536b..2034754 100644 --- a/src/pricehist/outputs/gnucashsql.py +++ b/src/pricehist/outputs/gnucashsql.py @@ -58,6 +58,7 @@ class GnuCashSQL(BaseOutput): self._warn_about_backslashes( { + "date": fmt.format_date("1970-01-01"), "time": fmt.time, "base": base, "quote": quote, @@ -135,7 +136,7 @@ class GnuCashSQL(BaseOutput): logging.warning( f"Before running this SQL, check the formatting of the " f"{self._english_join(hits)} strings. " - f"SQLite treats backslahes in strings as plain characters, but " + f"SQLite treats backslashes in strings as plain characters, but " f"MariaDB/MySQL and PostgreSQL may interpret them as escape " f"codes." ) diff --git a/tests/pricehist/outputs/__init__.py b/tests/pricehist/outputs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/pricehist/outputs/test_beancount.py b/tests/pricehist/outputs/test_beancount.py new file mode 100644 index 0000000..102fe21 --- /dev/null +++ b/tests/pricehist/outputs/test_beancount.py @@ -0,0 +1,44 @@ +from decimal import Decimal + +import pytest + +from pricehist.format import Format +from pricehist.outputs.beancount import Beancount +from pricehist.price import Price +from pricehist.series import Series + + +@pytest.fixture +def out(): + return Beancount() + + +@pytest.fixture +def series(): + prices = [ + Price("2021-01-01", Decimal("24139.4648")), + Price("2021-01-02", Decimal("26533.576")), + Price("2021-01-03", Decimal("27001.2846")), + ] + return Series("BTC", "EUR", "close", "2021-01-01", "2021-01-03", prices) + + +def test_format_basics(out, series, mocker): + source = mocker.MagicMock() + result = out.format(series, source, Format()) + assert result == ( + "2021-01-01 price BTC 24139.4648 EUR\n" + "2021-01-02 price BTC 26533.576 EUR\n" + "2021-01-03 price BTC 27001.2846 EUR\n" + ) + + +def test_format_custom(out, series, mocker): + source = mocker.MagicMock() + fmt = Format(base="XBT", quote="EURO", thousands=".", decimal=",", datesep="/") + result = out.format(series, source, fmt) + assert result == ( + "2021/01/01 price XBT 24.139,4648 EURO\n" + "2021/01/02 price XBT 26.533,576 EURO\n" + "2021/01/03 price XBT 27.001,2846 EURO\n" + ) diff --git a/tests/pricehist/outputs/test_csv.py b/tests/pricehist/outputs/test_csv.py new file mode 100644 index 0000000..f436f73 --- /dev/null +++ b/tests/pricehist/outputs/test_csv.py @@ -0,0 +1,50 @@ +from decimal import Decimal + +import pytest + +from pricehist.format import Format +from pricehist.outputs.csv import CSV +from pricehist.price import Price +from pricehist.series import Series + + +@pytest.fixture +def out(): + return CSV() + + +@pytest.fixture +def series(): + prices = [ + Price("2021-01-01", Decimal("24139.4648")), + Price("2021-01-02", Decimal("26533.576")), + Price("2021-01-03", Decimal("27001.2846")), + ] + return Series("BTC", "EUR", "close", "2021-01-01", "2021-01-03", prices) + + +def test_format_basics(out, series, mocker): + source = mocker.MagicMock() + source.id = mocker.MagicMock(return_value="sourceid") + result = out.format(series, source, Format()) + assert result == ( + "date,base,quote,amount,source,type\n" + "2021-01-01,BTC,EUR,24139.4648,sourceid,close\n" + "2021-01-02,BTC,EUR,26533.576,sourceid,close\n" + "2021-01-03,BTC,EUR,27001.2846,sourceid,close\n" + ) + + +def test_format_custom(out, series, mocker): + source = mocker.MagicMock() + source.id = mocker.MagicMock(return_value="sourceid") + fmt = Format( + base="XBT", quote="€", thousands=".", decimal=",", datesep="/", csvdelim="/" + ) + result = out.format(series, source, fmt) + assert result == ( + "date/base/quote/amount/source/type\n" + '"2021/01/01"/XBT/€/24.139,4648/sourceid/close\n' + '"2021/01/02"/XBT/€/26.533,576/sourceid/close\n' + '"2021/01/03"/XBT/€/27.001,2846/sourceid/close\n' + ) diff --git a/tests/pricehist/outputs/test_gnucashsql.py b/tests/pricehist/outputs/test_gnucashsql.py new file mode 100644 index 0000000..3b058ad --- /dev/null +++ b/tests/pricehist/outputs/test_gnucashsql.py @@ -0,0 +1,140 @@ +import dataclasses +import logging +import re +from decimal import Decimal + +import pytest + +from pricehist.format import Format +from pricehist.outputs.gnucashsql import GnuCashSQL +from pricehist.price import Price +from pricehist.series import Series + + +@pytest.fixture +def out(): + return GnuCashSQL() + + +@pytest.fixture +def series(): + prices = [ + Price("2021-01-01", Decimal("24139.4648")), + Price("2021-01-02", Decimal("26533.576")), + Price("2021-01-03", Decimal("27001.2846")), + ] + return Series("BTC", "EUR", "close", "2021-01-01", "2021-01-03", prices) + + +@pytest.fixture +def src(mocker): + source = mocker.MagicMock() + source.id = mocker.MagicMock(return_value="coindesk") + return source + + +def test_format_base_and_quote(out, series, src): + result = out.format(series, src, Format()) + base, quote = re.findall(r"WHERE mnemonic = (.*) LIMIT", result, re.MULTILINE) + assert base == "'BTC'" + assert quote == "'EUR'" + + +def test_format_new_price_values(out, series, src): + result = out.format(series, src, Format()) + values = re.search( + r"\(guid, date, base, quote, source, type, " + r"value_num, value_denom\) VALUES\n([^;]*);", + result, + re.MULTILINE, + )[1] + assert values == ( + "('0c4c01bd0a252641b806ce46f716f161', '2021-01-01 00:00:00', " + "'BTC', 'EUR', 'coindesk', 'close', 241394648, 10000),\n" + "('47f895ddfcce18e2421387e0e1b636e9', '2021-01-02 00:00:00', " + "'BTC', 'EUR', 'coindesk', 'close', 26533576, 1000),\n" + "('0d81630c4ac50c1b9b7c8211bf99c94e', '2021-01-03 00:00:00', " + "'BTC', 'EUR', 'coindesk', 'close', 270012846, 10000)\n" + ) + + +def test_format_customized(out, series, src): + fmt = Format( + base="XBT", + quote="EURO", + datesep="/", + time="23:59:59", + ) + result = out.format(series, src, fmt) + base, quote = re.findall(r"WHERE mnemonic = (.*) LIMIT", result, re.MULTILINE) + values = re.search( + r"\(guid, date, base, quote, source, type, " + r"value_num, value_denom\) VALUES\n([^;]*);", + result, + re.MULTILINE, + )[1] + assert base == "'XBT'" + assert quote == "'EURO'" + assert values == ( + "('448173eef5dea23cea9ff9d5e8c7b07e', '2021/01/01 23:59:59', " + "'XBT', 'EURO', 'coindesk', 'close', 241394648, 10000),\n" + "('b6c0f4474c91c50e8f65b47767f874ba', '2021/01/02 23:59:59', " + "'XBT', 'EURO', 'coindesk', 'close', 26533576, 1000),\n" + "('2937c872cf0672863e11b9f46ee41e09', '2021/01/03 23:59:59', " + "'XBT', 'EURO', 'coindesk', 'close', 270012846, 10000)\n" + ) + + +def test_format_escaping_of_strings(out, series, src): + result = out.format(series, src, Format(base="B'tc''n")) + base, quote = re.findall(r"WHERE mnemonic = (.*) LIMIT", result, re.MULTILINE) + assert base == "'B''tc''''n'" + + +def test_format_insert_commented_out_if_no_values(out, series, src): + empty_series = dataclasses.replace(series, prices=[]) + result = out.format(empty_series, src, Format()) + ( + "-- INSERT INTO new_prices (guid, date, base, quote, source, type, " + "value_num, value_denom) VALUES\n" + "-- \n" + "-- ;\n" + ) in result + + +def test_format_warns_about_backslash(out, series, src, caplog): + with caplog.at_level(logging.WARNING): + out.format(series, src, Format(quote="EU\\RO")) + r = caplog.records[0] + assert r.levelname == "WARNING" + assert "backslashes in strings" in r.message + + +def test__english_join_other_cases(out): + assert out._english_join([]) == "" + assert out._english_join(["one"]) == "one" + assert out._english_join(["one", "two"]) == "one and two" + assert out._english_join(["one", "two", "three"]) == "one, two and three" + + +def test_format_warns_about_out_of_range_numbers(out, series, src, caplog): + too_big_numerator = Decimal("9223372036854.775808") + s = dataclasses.replace(series, prices=[Price("2021-01-01", too_big_numerator)]) + with caplog.at_level(logging.WARNING): + out.format(s, src, Format()) + r = caplog.records[0] + assert r.levelname == "WARNING" + assert "outside of the int64 range" in r.message + + +def test__rational_other_exponent_cases(out): + assert out._rational(Decimal("9223372036854e6")) == ( + "9223372036854000000", + "1", + True, + ) + assert out._rational(Decimal("9223372036854e-6")) == ( + "9223372036854", + "1000000", + True, + ) diff --git a/tests/pricehist/outputs/test_ledger.py b/tests/pricehist/outputs/test_ledger.py new file mode 100644 index 0000000..a1a242c --- /dev/null +++ b/tests/pricehist/outputs/test_ledger.py @@ -0,0 +1,52 @@ +from decimal import Decimal + +import pytest + +from pricehist.format import Format +from pricehist.outputs.ledger import Ledger +from pricehist.price import Price +from pricehist.series import Series + + +@pytest.fixture +def out(): + return Ledger() + + +@pytest.fixture +def series(): + prices = [ + Price("2021-01-01", Decimal("24139.4648")), + Price("2021-01-02", Decimal("26533.576")), + Price("2021-01-03", Decimal("27001.2846")), + ] + return Series("BTC", "EUR", "close", "2021-01-01", "2021-01-03", prices) + + +def test_format_basics(out, series, mocker): + source = mocker.MagicMock() + result = out.format(series, source, Format()) + assert result == ( + "P 2021-01-01 00:00:00 BTC 24139.4648 EUR\n" + "P 2021-01-02 00:00:00 BTC 26533.576 EUR\n" + "P 2021-01-03 00:00:00 BTC 27001.2846 EUR\n" + ) + + +def test_format_custom(out, series, mocker): + source = mocker.MagicMock() + fmt = Format( + base="XBT", + quote="€", + time="23:59:59", + thousands=".", + decimal=",", + symbol="left", + datesep="/", + ) + result = out.format(series, source, fmt) + assert result == ( + "P 2021/01/01 23:59:59 XBT €24.139,4648\n" + "P 2021/01/02 23:59:59 XBT €26.533,576\n" + "P 2021/01/03 23:59:59 XBT €27.001,2846\n" + )