From 57c78ffc48aa6e30791bc6002757f9e1b771ee6d Mon Sep 17 00:00:00 2001 From: "Benjamin T. Schwertfeger" Date: Mon, 6 Jan 2025 06:24:22 +0100 Subject: [PATCH] Resolve "Allow for using a local sqlite database" (#30) --- .gitignore | 4 + Makefile | 2 +- pyproject.toml | 6 +- src/kraken_infinity_grid/cli.py | 83 +++++++++++-------- src/kraken_infinity_grid/database.py | 21 ++--- tests/conftest.py | 33 ++++++++ tests/integration/conftest.py | 15 ---- tests/integration/test_integration_DCA.py | 1 + .../integration/test_integration_GridHODL.py | 1 + .../integration/test_integration_GridSell.py | 2 +- tests/integration/test_integration_SWING.py | 1 + tests/test_database.py | 6 +- tests/test_gridbot.py | 14 ---- 13 files changed, 111 insertions(+), 78 deletions(-) create mode 100644 tests/conftest.py diff --git a/.gitignore b/.gitignore index bd63930..bf2d0d2 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,7 @@ docs/_build/ # CSV and pickle files *.csv + +# Other artifacts +*.sqlite +*.sqlite-journal diff --git a/Makefile b/Makefile index ea78da2..2e3b1ee 100644 --- a/Makefile +++ b/Makefile @@ -91,7 +91,7 @@ clean: .vscode \ dist/ \ doc/_build \ - kraken_infinity_grid.egg-info \ + src/kraken_infinity_grid.egg-info \ build/ rm -f \ diff --git a/pyproject.toml b/pyproject.toml index a50ba7e..93c563d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,11 @@ testpaths = ["tests"] [tool.pytest.ini_options] cache_dir = ".cache/pytest" -markers = ["wip: used for wip tests.", "asyncio: used for async tests."] +markers = [ + "asyncio: used for async tests.", + "integration: used for integration tests.", + "wip: used for wip tests.", +] asyncio_default_fixture_loop_scope = "function" [tool.coverage.run] diff --git a/src/kraken_infinity_grid/cli.py b/src/kraken_infinity_grid/cli.py index ca9cf59..294eb02 100644 --- a/src/kraken_infinity_grid/cli.py +++ b/src/kraken_infinity_grid/cli.py @@ -9,7 +9,8 @@ from typing import Any from click import BOOL, FLOAT, INT, STRING, Context, echo, pass_context -from cloup import Choice, HelpFormatter, HelpTheme, Style, group, option +from cloup import Choice, HelpFormatter, HelpTheme, Style, group, option, option_group +from cloup.constraints import If, accept_none, require_all def print_version(ctx: Context, param: Any, value: Any) -> None: # noqa: ANN401, ARG001 @@ -125,6 +126,12 @@ def cli(ctx: Context, **kwargs: dict) -> None: ), ), ) +@option( + "--strategy", + type=Choice(choices=("DCA", "GridHODL", "GridSell", "SWING"), case_sensitive=True), + help="The strategy to run.", + required=True, +) @option( "--name", required=True, @@ -213,43 +220,43 @@ def cli(ctx: Context, **kwargs: dict) -> None: help="A reference number to identify the bots orders with.", ) @option( - "--db-user", - type=STRING, - help="PostgreSQL DB user", - required=True, -) -@option( - "--db-password", + "--sqlite-file", type=STRING, - help="PostgreSQL DB password", - required=True, + help="SQLite file to use as database.", ) @option( "--db-name", type=STRING, default="kraken_infinity_grid", - help="PostgreSQL DB name", - required=True, -) -@option( - "--db-host", - type=STRING, - default="postgresql", - help="PostgreSQL DB host", - required=True, + help="The database name.", ) -@option( - "--db-port", - type=STRING, - default="5432", - help="PostgreSQL DB port", - required=True, -) -@option( - "--strategy", - type=Choice(choices=("DCA", "GridHODL", "GridSell", "SWING"), case_sensitive=True), - help="The strategy to run.", - required=True, +@option_group( + "PostgreSQL Database Options", + option( + "--db-user", + type=STRING, + help="PostgreSQL DB user", + ), + option( + "--db-password", + type=STRING, + help="PostgreSQL DB password", + ), + option( + "--db-host", + type=STRING, + help="PostgreSQL DB host", + ), + option( + "--db-port", + type=STRING, + help="PostgreSQL DB port", + ), + constraint=If( + "sqlite_file", + then=accept_none, + else_=require_all, + ), ) @pass_context def run(ctx: Context, **kwargs: dict) -> None: @@ -261,10 +268,18 @@ def run(ctx: Context, **kwargs: dict) -> None: from kraken_infinity_grid.gridbot import KrakenInfinityGridBot # noqa: PLC0415 ctx.obj |= kwargs - db_config = {key: value for key, value in kwargs.items() if key.startswith("db_")} - for key in db_config: - del kwargs[key] + if ctx.obj["sqlite_file"]: + # FIXME: Maybe use in_memory for dry-run? + db_config = {"sqlite_file": ctx.obj["sqlite_file"]} + else: + db_config = { + "db_user": ctx.obj["db_user"], + "db_password": ctx.obj["db_password"], + "db_host": ctx.obj["db_host"], + "db_port": ctx.obj["db_port"], + "db_name": ctx.obj["db_name"], + } async def main() -> None: # Instantiate the trading algorithm diff --git a/src/kraken_infinity_grid/database.py b/src/kraken_infinity_grid/database.py index 4445a35..d0a5ec4 100644 --- a/src/kraken_infinity_grid/database.py +++ b/src/kraken_infinity_grid/database.py @@ -33,30 +33,31 @@ class DBConnect: - """Class handling the connection to the PostgreSQL database.""" + """Class handling the connection to the PostgreSQL or sqlite database.""" def __init__( # pylint: disable=too-many-positional-arguments self: Self, - db_user: str = "", - db_password: str = "", - db_host: str = "", - db_port: str = "5432", + db_user: str | None = None, + db_password: str | None = None, + db_host: str | None = None, + db_port: str | int | None = None, db_name: str = "kraken_infinity_grid", - db_backend: str = "postgresql", - in_memory: bool = False, # FIXME: Only used for testing + in_memory: bool = False, + sqlite_file: str | None = None, ) -> None: LOG.info("Connecting to the database...") if in_memory: engine = "sqlite:///:memory:" - # if sqlite_file: # FIXME: implement this - # engine = f"sqlite:///{sqlite_file}" + elif sqlite_file: + engine = f"sqlite:///{sqlite_file}" else: - engine = f"{db_backend}://" + engine = "postgresql://" if db_user and db_password: engine += f"{db_user}:{db_password}@" if db_host and db_port: engine += f"{db_host}:{db_port}" engine += f"/{db_name}" + self.engine = create_engine(engine) self.session = sessionmaker(bind=self.engine)() self.metadata = MetaData() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1c5b109 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# Copyright (C) 2025 Benjamin Thomas Schwertfeger +# GitHub: https://github.com/btschwertfeger +# + +from pathlib import Path + +import pytest + + +@pytest.fixture +def sqlite_file(tmp_path: Path) -> Path: + """ + Fixture to create a Path object to the SQLite database file. + + This is used during tests in order to create isolated databases. + """ + Path(tmp_path).mkdir(exist_ok=True) + return tmp_path / "kraken_infinity_grid.sqlite" + + +@pytest.fixture +def db_config(sqlite_file: Path) -> dict: + """Fixture to create a mock database configuration.""" + return { + "db_user": "", + "db_password": "", + "db_host": "", + "db_port": "", + "db_name": "kraken_infinity_grid", + "sqlite_file": sqlite_file, + } diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 01878a9..66963d8 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -7,7 +7,6 @@ import logging from unittest import mock -import pytest import pytest_asyncio from kraken.spot import Market @@ -16,20 +15,6 @@ from .helper import KrakenAPI -@pytest.fixture -def db_config() -> dict: - """Fixture to create a mock database configuration.""" - return { - "db_user": "", - "db_password": "", - "db_host": "", - "db_port": "", - "db_name": "kraken_infinity_grid", - "db_backend": "sqlite", - "in_memory": True, - } - - @pytest_asyncio.fixture async def instance(config: dict, db_config: dict) -> KrakenInfinityGridBot: """Fixture to create a KrakenInfinityGridBot instance for testing.""" diff --git a/tests/integration/test_integration_DCA.py b/tests/integration/test_integration_DCA.py index b3d9695..3634a32 100644 --- a/tests/integration/test_integration_DCA.py +++ b/tests/integration/test_integration_DCA.py @@ -34,6 +34,7 @@ def config() -> dict: } +@pytest.mark.integration @pytest.mark.asyncio @mock.patch("kraken_infinity_grid.order_management.sleep", return_value=None) @mock.patch("kraken_infinity_grid.gridbot.sleep", return_value=None) diff --git a/tests/integration/test_integration_GridHODL.py b/tests/integration/test_integration_GridHODL.py index 62b923e..afe8bf9 100644 --- a/tests/integration/test_integration_GridHODL.py +++ b/tests/integration/test_integration_GridHODL.py @@ -39,6 +39,7 @@ def config() -> dict: } +@pytest.mark.integration @pytest.mark.asyncio @mock.patch("kraken_infinity_grid.order_management.sleep", return_value=None) @mock.patch("kraken_infinity_grid.gridbot.sleep", return_value=None) diff --git a/tests/integration/test_integration_GridSell.py b/tests/integration/test_integration_GridSell.py index 6d800f7..c2fac9d 100644 --- a/tests/integration/test_integration_GridSell.py +++ b/tests/integration/test_integration_GridSell.py @@ -39,7 +39,7 @@ def config() -> dict: } -@pytest.mark.wip +@pytest.mark.integration @pytest.mark.asyncio @mock.patch("kraken_infinity_grid.order_management.sleep", return_value=None) @mock.patch("kraken_infinity_grid.gridbot.sleep", return_value=None) diff --git a/tests/integration/test_integration_SWING.py b/tests/integration/test_integration_SWING.py index 260058b..94c862a 100644 --- a/tests/integration/test_integration_SWING.py +++ b/tests/integration/test_integration_SWING.py @@ -39,6 +39,7 @@ def config() -> dict: } +@pytest.mark.integration @pytest.mark.asyncio @mock.patch("kraken_infinity_grid.order_management.sleep", return_value=None) @mock.patch("kraken_infinity_grid.gridbot.sleep", return_value=None) diff --git a/tests/test_database.py b/tests/test_database.py index e6570d9..bfed528 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -7,6 +7,8 @@ """Unit tests for the database module.""" +from pathlib import Path + import pytest from kraken_infinity_grid.database import ( @@ -19,11 +21,11 @@ @pytest.fixture -def db_connect() -> DBConnect: +def db_connect(sqlite_file: Path) -> DBConnect: """ Fixture to create a DBConnect instance with an in-memory SQLite database. """ - return DBConnect(in_memory=True) + return DBConnect(sqlite_file=sqlite_file) @pytest.fixture diff --git a/tests/test_gridbot.py b/tests/test_gridbot.py index 904ddda..bb5d914 100644 --- a/tests/test_gridbot.py +++ b/tests/test_gridbot.py @@ -40,20 +40,6 @@ def config() -> dict: } -@pytest.fixture -def db_config() -> dict: - """Fixture to create a mock database configuration.""" - return { - "db_user": "", - "db_password": "", - "db_host": "", - "db_port": "", - "db_name": "KrakenInfinityGridBot", - "db_backend": "sqlite", - "in_memory": True, - } - - @pytest_asyncio.fixture async def instance( # noqa: RUF029 config: dict,