diff --git a/pyproject.toml b/pyproject.toml index 4e98505..1691d9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,12 @@ bigquery = ["ibis-framework[bigquery]>=11.0.0"] pyspark = ["setuptools", "ibis-framework[pyspark]>=11.0.0"] clickhouse = ["ibis-framework[clickhouse]>=11.0.0"] databricks = ["databricks-sql-connector>=4", "ibis-framework[databricks]>=11.0.0"] +singlestoredb = ["singlestoredb>=1.0", "ibis-framework[singlestoredb]>=11.0.0"] +exasol = ["pyexasol>=0.25.2", "ibis-framework[exasol]>=11.0.0"] +impala = ["impyla>=0.17", "ibis-framework[impala]>=11.0.0"] +materialize = ["psycopg>=3.2.0", "ibis-framework[materialize]>=11.0.0"] +risingwave = ["psycopg2>=2.8.4", "ibis-framework[risingwave]>=11.0.0"] +druid = ["pydruid>=0.6.7", "ibis-framework[druid]>=11.0.0"] trino = ["ibis-framework[trino]>=11.0.0"] @@ -94,7 +100,3 @@ tests = ["tests", "*/mountainash-data/tests"] [tool.coverage.report] exclude_lines = ["no cov", "if __name__ == .__main__:", "if TYPE_CHECKING:"] - -[tool.pyright] -venvPath = "/home/nathanielramm/git/mountainash-ide/mountainash-dev-local/.venv" -venv = ".venv" diff --git a/src/mountainash_data/backends/ibis/dialects/_registry.py b/src/mountainash_data/backends/ibis/dialects/_registry.py index 229297f..2258a18 100644 --- a/src/mountainash_data/backends/ibis/dialects/_registry.py +++ b/src/mountainash_data/backends/ibis/dialects/_registry.py @@ -430,6 +430,177 @@ def _build_databricks_connection(**config: t.Any) -> t.Any: return ibis.databricks.connect(**kwargs) +def _build_singlestoredb_connection(**config: t.Any) -> t.Any: + """Build a SingleStoreDB ibis connection. + + Uses ibis.singlestoredb.connect() with kwargs: host, port, user, password, + database, driver, autocommit, local_infile. + """ + import ibis + + host = config.get("host", "localhost") + port = config.get("port", 3306) + user = config.get("user", config.get("username", None)) + password = config.get("password", None) + database = config.get("database", None) + driver = config.get("driver", None) + autocommit = config.get("autocommit", True) + local_infile = config.get("local_infile", True) + + known = {"host", "port", "user", "username", "password", "database", + "driver", "autocommit", "local_infile", "connection_string"} + extra = {k: v for k, v in config.items() if k not in known} + + kwargs: dict[str, t.Any] = { + "host": host, + "port": port, + "autocommit": autocommit, + "local_infile": local_infile, + } + if user is not None: + kwargs["user"] = user + if password is not None: + kwargs["password"] = password + if database is not None: + kwargs["database"] = database + if driver is not None: + kwargs["driver"] = driver + + kwargs.update(extra) + return ibis.singlestoredb.connect(**kwargs) + + +def _build_exasol_connection(**config: t.Any) -> t.Any: + """Build an Exasol ibis connection.""" + import ibis + + user = config.get("user", config.get("username", None)) + password = config.get("password", None) + host = config.get("host", "localhost") + port = config.get("port", 8563) + timezone = config.get("timezone", "UTC") + + known = {"host", "port", "user", "username", "password", "timezone", + "connection_string"} + extra = {k: v for k, v in config.items() if k not in known} + + kwargs: dict[str, t.Any] = {"host": host, "port": port, "timezone": timezone} + if user is not None: + kwargs["user"] = user + if password is not None: + kwargs["password"] = password + kwargs.update(extra) + return ibis.exasol.connect(**kwargs) + + +def _build_impala_connection(**config: t.Any) -> t.Any: + """Build an Impala ibis connection.""" + import ibis + + host = config.get("host", "localhost") + port = config.get("port", 21050) + database = config.get("database", "default") + timeout = config.get("timeout", 45) + use_ssl = config.get("use_ssl", False) + ca_cert = config.get("ca_cert", None) + user = config.get("user", config.get("username", None)) + password = config.get("password", None) + auth_mechanism = config.get("auth_mechanism", "NOSASL") + kerberos_service_name = config.get("kerberos_service_name", "impala") + + known = {"host", "port", "database", "timeout", "use_ssl", "ca_cert", + "user", "username", "password", "auth_mechanism", + "kerberos_service_name", "connection_string"} + extra = {k: v for k, v in config.items() if k not in known} + + kwargs: dict[str, t.Any] = { + "host": host, "port": port, "database": database, + "timeout": timeout, "use_ssl": use_ssl, + "auth_mechanism": auth_mechanism, + "kerberos_service_name": kerberos_service_name, + } + if ca_cert is not None: + kwargs["ca_cert"] = ca_cert + if user is not None: + kwargs["user"] = user + if password is not None: + kwargs["password"] = password + kwargs.update(extra) + return ibis.impala.connect(**kwargs) + + +def _build_materialize_connection(**config: t.Any) -> t.Any: + """Build a Materialize ibis connection.""" + import ibis + + host = config.get("host", None) + port = config.get("port", 6875) + user = config.get("user", config.get("username", None)) + password = config.get("password", None) + database = config.get("database", None) + schema = config.get("schema", None) + autocommit = config.get("autocommit", True) + cluster = config.get("cluster", None) + + known = {"host", "port", "user", "username", "password", "database", + "schema", "autocommit", "cluster", "connection_string"} + extra = {k: v for k, v in config.items() if k not in known} + + kwargs: dict[str, t.Any] = {"port": port, "autocommit": autocommit} + if host is not None: + kwargs["host"] = host + if user is not None: + kwargs["user"] = user + if password is not None: + kwargs["password"] = password + if database is not None: + kwargs["database"] = database + if schema is not None: + kwargs["schema"] = schema + if cluster is not None: + kwargs["cluster"] = cluster + kwargs.update(extra) + return ibis.materialize.connect(**kwargs) + + +def _build_risingwave_connection(**config: t.Any) -> t.Any: + """Build a RisingWave ibis connection.""" + import ibis + + host = config.get("host", None) + port = config.get("port", 5432) + user = config.get("user", config.get("username", None)) + password = config.get("password", None) + database = config.get("database", None) + schema = config.get("schema", None) + + known = {"host", "port", "user", "username", "password", "database", + "schema", "connection_string"} + extra = {k: v for k, v in config.items() if k not in known} + + kwargs: dict[str, t.Any] = {"port": port} + if host is not None: + kwargs["host"] = host + if user is not None: + kwargs["user"] = user + if password is not None: + kwargs["password"] = password + if database is not None: + kwargs["database"] = database + if schema is not None: + kwargs["schema"] = schema + kwargs.update(extra) + return ibis.risingwave.connect(**kwargs) + + +def _build_druid_connection(**config: t.Any) -> t.Any: + """Build a Druid ibis connection.""" + import ibis + + extra = {k: v for k, v in config.items() if k != "connection_string"} + return ibis.druid.connect(**extra) + + def _build_pyspark_connection(**config: t.Any) -> t.Any: """Build a PySpark ibis connection. @@ -567,6 +738,42 @@ def _build_pyspark_connection(**config: t.Any) -> t.Any: connection_string_scheme="", connection_builder=_build_databricks_connection, ), + "singlestoredb": DialectSpec( + ibis_backend_name="singlestoredb", + connection_mode=_KWARGS, + connection_string_scheme="singlestoredb://", + connection_builder=_build_singlestoredb_connection, + ), + "exasol": DialectSpec( + ibis_backend_name="exasol", + connection_mode=_KWARGS, + connection_string_scheme="exasol://", + connection_builder=_build_exasol_connection, + ), + "impala": DialectSpec( + ibis_backend_name="impala", + connection_mode=_KWARGS, + connection_string_scheme="impala://", + connection_builder=_build_impala_connection, + ), + "materialize": DialectSpec( + ibis_backend_name="materialize", + connection_mode=_KWARGS, + connection_string_scheme="materialize://", + connection_builder=_build_materialize_connection, + ), + "risingwave": DialectSpec( + ibis_backend_name="risingwave", + connection_mode=_KWARGS, + connection_string_scheme="risingwave://", + connection_builder=_build_risingwave_connection, + ), + "druid": DialectSpec( + ibis_backend_name="druid", + connection_mode=_KWARGS, + connection_string_scheme="druid://", + connection_builder=_build_druid_connection, + ), "pyspark": DialectSpec( ibis_backend_name="pyspark", connection_mode=_CONNECTION_STRING, diff --git a/src/mountainash_data/core/constants.py b/src/mountainash_data/core/constants.py index 0134209..90bb391 100644 --- a/src/mountainash_data/core/constants.py +++ b/src/mountainash_data/core/constants.py @@ -25,6 +25,12 @@ class CONST_DB_PROVIDER_TYPE(Enum): ORACLE = auto() CLICKHOUSE = auto() DATABRICKS = auto() + SINGLESTOREDB = auto() + EXASOL = auto() + IMPALA = auto() + MATERIALIZE = auto() + RISINGWAVE = auto() + DRUID = auto() PYSPARK = auto() @@ -112,6 +118,12 @@ class CONST_DB_BACKEND(StrEnum): MYSQL = "MYSQL" CLICKHOUSE = "CLICKHOUSE" MOTHERDUCK = "MOTHERDUCK" + SINGLESTOREDB = "SINGLESTOREDB" + EXASOL = "EXASOL" + IMPALA = "IMPALA" + MATERIALIZE = "MATERIALIZE" + RISINGWAVE = "RISINGWAVE" + DRUID = "DRUID" PYICEBERG = "PYICEBERG" # POLARS = "POLARS" # PANDAS = "PANDAS" @@ -147,6 +159,12 @@ class CONST_DB_BACKEND_IBIS_PREFIX(StrEnum): MSSQL = "mssql:" CLICKHOUSE = "clickhouse:" MYSQL = "mysql:" + SINGLESTOREDB = "singlestoredb:" + EXASOL = "exasol:" + IMPALA = "impala:" + MATERIALIZE = "materialize:" + RISINGWAVE = "risingwave:" + DRUID = "druid:" class CONST_DB_BACKEND_CAPABILITIES(Enum): """ diff --git a/src/mountainash_data/core/settings/__init__.py b/src/mountainash_data/core/settings/__init__.py index c4b7bcf..5b53466 100644 --- a/src/mountainash_data/core/settings/__init__.py +++ b/src/mountainash_data/core/settings/__init__.py @@ -41,12 +41,18 @@ class body is a two-line shell (``__descriptor__`` + ``__adapter__``). from .clickhouse import ClickHouseAuthSettings from .databricks import DatabricksAuthSettings from .mysql import MySQLAuthSettings +from .singlestoredb import SingleStoreDBAuthSettings from .mssql import MSSQLAuthSettings from .snowflake import SnowflakeAuthSettings from .bigquery import BigQueryAuthSettings from .redshift import RedshiftAuthSettings from .pyspark import PySparkAuthSettings from .trino import TrinoAuthSettings +from .exasol import ExasolAuthSettings +from .impala import ImpalaAuthSettings +from .materialize import MaterializeAuthSettings +from .risingwave import RisingWaveAuthSettings +from .druid import DruidAuthSettings from .pyiceberg_rest import PyIcebergRestAuthSettings __all__ = [ @@ -61,7 +67,11 @@ class body is a two-line shell (``__descriptor__`` + ``__adapter__``). # backends "SQLiteAuthSettings", "DuckDBAuthSettings", "MotherDuckAuthSettings", "PostgreSQLAuthSettings", "ClickHouseAuthSettings", - "DatabricksAuthSettings", "MySQLAuthSettings", "MSSQLAuthSettings", + "DatabricksAuthSettings", "MySQLAuthSettings", "SingleStoreDBAuthSettings", + "MSSQLAuthSettings", "SnowflakeAuthSettings", "BigQueryAuthSettings", "RedshiftAuthSettings", - "PySparkAuthSettings", "TrinoAuthSettings", "PyIcebergRestAuthSettings", + "PySparkAuthSettings", "TrinoAuthSettings", + "ExasolAuthSettings", "ImpalaAuthSettings", "MaterializeAuthSettings", + "RisingWaveAuthSettings", "DruidAuthSettings", + "PyIcebergRestAuthSettings", ] diff --git a/src/mountainash_data/core/settings/druid.py b/src/mountainash_data/core/settings/druid.py new file mode 100644 index 0000000..4ac1273 --- /dev/null +++ b/src/mountainash_data/core/settings/druid.py @@ -0,0 +1,40 @@ +"""Druid backend settings. + +Driver: https://github.com/druid-io/pydruid +Ibis: ``ibis.druid.connect(**kwargs)`` + +Druid's ibis backend accepts fully dynamic kwargs passed through to pydruid. +Core parameters are host, port, and path for the Druid broker endpoint. +""" + +from __future__ import annotations + +from ..constants import CONST_DB_PROVIDER_TYPE +from mountainash_settings.auth import NoAuth, PasswordAuth +from .descriptor import BackendDescriptor, ParameterSpec +from .profile import ConnectionProfile +from .registry import register + + +DRUID_DESCRIPTOR = BackendDescriptor( + name="druid", + provider_type=CONST_DB_PROVIDER_TYPE.DRUID, + default_port=8082, + connection_string_scheme="druid://", + ibis_dialect="druid", + auth_modes=[PasswordAuth, NoAuth], + parameters=[ + ParameterSpec(name="HOST", type=str, tier="core", driver_key="host"), + ParameterSpec(name="PORT", type=int, tier="core", default=8082, + driver_key="port"), + ParameterSpec(name="ENDPOINT_PATH", type=str, tier="core", + default="/druid/v2/sql", driver_key="path"), + ParameterSpec(name="SCHEME", type=str, tier="core", + default="http", driver_key="scheme"), + ], +) + + +@register(DRUID_DESCRIPTOR) +class DruidAuthSettings(ConnectionProfile): + __descriptor__ = DRUID_DESCRIPTOR diff --git a/src/mountainash_data/core/settings/exasol.py b/src/mountainash_data/core/settings/exasol.py new file mode 100644 index 0000000..5f29415 --- /dev/null +++ b/src/mountainash_data/core/settings/exasol.py @@ -0,0 +1,36 @@ +"""Exasol backend settings. + +Driver: https://github.com/exasol/pyexasol +Ibis: ``ibis.exasol.connect(user, password, host, port, timezone, + websocket_sslopt, **kwargs)`` +""" + +from __future__ import annotations + +from ..constants import CONST_DB_PROVIDER_TYPE +from mountainash_settings.auth import PasswordAuth +from .descriptor import BackendDescriptor, ParameterSpec +from .profile import ConnectionProfile +from .registry import register + + +EXASOL_DESCRIPTOR = BackendDescriptor( + name="exasol", + provider_type=CONST_DB_PROVIDER_TYPE.EXASOL, + default_port=8563, + connection_string_scheme="exasol://", + ibis_dialect="exasol", + auth_modes=[PasswordAuth], + parameters=[ + ParameterSpec(name="HOST", type=str, tier="core", driver_key="host"), + ParameterSpec(name="PORT", type=int, tier="core", default=8563, + driver_key="port"), + ParameterSpec(name="TIMEZONE", type=str, tier="core", + default="UTC", driver_key="timezone"), + ], +) + + +@register(EXASOL_DESCRIPTOR) +class ExasolAuthSettings(ConnectionProfile): + __descriptor__ = EXASOL_DESCRIPTOR diff --git a/src/mountainash_data/core/settings/impala.py b/src/mountainash_data/core/settings/impala.py new file mode 100644 index 0000000..b7b0b29 --- /dev/null +++ b/src/mountainash_data/core/settings/impala.py @@ -0,0 +1,59 @@ +"""Impala backend settings. + +Driver: https://github.com/cloudera/impyla +Ibis: ``ibis.impala.connect(host, port, database, timeout, use_ssl, ca_cert, + user, password, auth_mechanism, kerberos_service_name, **params)`` +""" + +from __future__ import annotations + +import typing as t +from enum import StrEnum +from pathlib import Path + +from ..constants import CONST_DB_PROVIDER_TYPE +from mountainash_settings.auth import NoAuth, PasswordAuth +from .descriptor import BackendDescriptor, ParameterSpec +from .profile import ConnectionProfile +from .registry import register + + +class ImpalaAuthMechanism(StrEnum): + NOSASL = "NOSASL" + PLAIN = "PLAIN" + GSSAPI = "GSSAPI" + LDAP = "LDAP" + + +IMPALA_DESCRIPTOR = BackendDescriptor( + name="impala", + provider_type=CONST_DB_PROVIDER_TYPE.IMPALA, + default_port=21050, + connection_string_scheme="impala://", + ibis_dialect="impala", + auth_modes=[PasswordAuth, NoAuth], + parameters=[ + ParameterSpec(name="HOST", type=str, tier="core", driver_key="host"), + ParameterSpec(name="PORT", type=int, tier="core", default=21050, + driver_key="port"), + ParameterSpec(name="DATABASE", type=str, tier="core", + default="default", driver_key="database"), + ParameterSpec(name="TIMEOUT", type=int, tier="advanced", + default=45, driver_key="timeout"), + ParameterSpec(name="USE_SSL", type=bool, tier="core", + default=False, driver_key="use_ssl"), + ParameterSpec(name="CA_CERT", type=t.Optional[Path], tier="advanced", + default=None, driver_key="ca_cert", + transform=lambda p: str(p)), + ParameterSpec(name="AUTH_MECHANISM", type=ImpalaAuthMechanism, + tier="core", default=ImpalaAuthMechanism.NOSASL, + driver_key="auth_mechanism"), + ParameterSpec(name="KERBEROS_SERVICE_NAME", type=str, tier="advanced", + default="impala", driver_key="kerberos_service_name"), + ], +) + + +@register(IMPALA_DESCRIPTOR) +class ImpalaAuthSettings(ConnectionProfile): + __descriptor__ = IMPALA_DESCRIPTOR diff --git a/src/mountainash_data/core/settings/materialize.py b/src/mountainash_data/core/settings/materialize.py new file mode 100644 index 0000000..50dd900 --- /dev/null +++ b/src/mountainash_data/core/settings/materialize.py @@ -0,0 +1,44 @@ +"""Materialize backend settings. + +Driver: https://materialize.com/docs/integrations/python/ +Ibis: ``ibis.materialize.connect(host, user, password, port, database, + schema, autocommit, cluster, **kwargs)`` +""" + +from __future__ import annotations + +import typing as t + +from ..constants import CONST_DB_PROVIDER_TYPE +from mountainash_settings.auth import NoAuth, PasswordAuth +from .descriptor import BackendDescriptor, ParameterSpec +from .profile import ConnectionProfile +from .registry import register + + +MATERIALIZE_DESCRIPTOR = BackendDescriptor( + name="materialize", + provider_type=CONST_DB_PROVIDER_TYPE.MATERIALIZE, + default_port=6875, + connection_string_scheme="materialize://", + ibis_dialect="materialize", + auth_modes=[PasswordAuth, NoAuth], + parameters=[ + ParameterSpec(name="HOST", type=str, tier="core", driver_key="host"), + ParameterSpec(name="PORT", type=int, tier="core", default=6875, + driver_key="port"), + ParameterSpec(name="DATABASE", type=t.Optional[str], tier="core", + default=None, driver_key="database"), + ParameterSpec(name="SCHEMA", type=t.Optional[str], tier="core", + default=None, driver_key="schema"), + ParameterSpec(name="AUTOCOMMIT", type=bool, tier="core", + default=True, driver_key="autocommit"), + ParameterSpec(name="CLUSTER", type=t.Optional[str], tier="core", + default=None, driver_key="cluster"), + ], +) + + +@register(MATERIALIZE_DESCRIPTOR) +class MaterializeAuthSettings(ConnectionProfile): + __descriptor__ = MATERIALIZE_DESCRIPTOR diff --git a/src/mountainash_data/core/settings/risingwave.py b/src/mountainash_data/core/settings/risingwave.py new file mode 100644 index 0000000..00e0866 --- /dev/null +++ b/src/mountainash_data/core/settings/risingwave.py @@ -0,0 +1,40 @@ +"""RisingWave backend settings. + +Driver: https://docs.risingwave.com/docs/current/install-psycopg2/ +Ibis: ``ibis.risingwave.connect(host, user, password, port, database, + schema)`` +""" + +from __future__ import annotations + +import typing as t + +from ..constants import CONST_DB_PROVIDER_TYPE +from mountainash_settings.auth import NoAuth, PasswordAuth +from .descriptor import BackendDescriptor, ParameterSpec +from .profile import ConnectionProfile +from .registry import register + + +RISINGWAVE_DESCRIPTOR = BackendDescriptor( + name="risingwave", + provider_type=CONST_DB_PROVIDER_TYPE.RISINGWAVE, + default_port=5432, + connection_string_scheme="risingwave://", + ibis_dialect="risingwave", + auth_modes=[PasswordAuth, NoAuth], + parameters=[ + ParameterSpec(name="HOST", type=str, tier="core", driver_key="host"), + ParameterSpec(name="PORT", type=int, tier="core", default=5432, + driver_key="port"), + ParameterSpec(name="DATABASE", type=t.Optional[str], tier="core", + default=None, driver_key="database"), + ParameterSpec(name="SCHEMA", type=t.Optional[str], tier="core", + default=None, driver_key="schema"), + ], +) + + +@register(RISINGWAVE_DESCRIPTOR) +class RisingWaveAuthSettings(ConnectionProfile): + __descriptor__ = RISINGWAVE_DESCRIPTOR diff --git a/src/mountainash_data/core/settings/singlestoredb.py b/src/mountainash_data/core/settings/singlestoredb.py new file mode 100644 index 0000000..e85d754 --- /dev/null +++ b/src/mountainash_data/core/settings/singlestoredb.py @@ -0,0 +1,51 @@ +"""SingleStoreDB backend settings. + +Driver: https://singlestoredb-python.labs.singlestore.com/ +Ibis: ``ibis.singlestoredb.connect(host, user, password, port, database, + driver, autocommit, local_infile, **kwargs)`` +""" + +from __future__ import annotations + +import typing as t +from enum import StrEnum + +from ..constants import CONST_DB_PROVIDER_TYPE +from mountainash_settings.auth import NoAuth, PasswordAuth +from .descriptor import BackendDescriptor, ParameterSpec +from .profile import ConnectionProfile +from .registry import register + + +class SingleStoreDriver(StrEnum): + MYSQL = "mysql" + HTTP = "http" + HTTPS = "https" + + +SINGLESTOREDB_DESCRIPTOR = BackendDescriptor( + name="singlestoredb", + provider_type=CONST_DB_PROVIDER_TYPE.SINGLESTOREDB, + default_port=3306, + connection_string_scheme="singlestoredb://", + ibis_dialect="singlestoredb", + auth_modes=[PasswordAuth, NoAuth], + parameters=[ + ParameterSpec(name="HOST", type=str, tier="core", driver_key="host"), + ParameterSpec(name="PORT", type=int, tier="core", default=3306, + driver_key="port"), + ParameterSpec(name="DATABASE", type=t.Optional[str], tier="core", + default=None, driver_key="database"), + ParameterSpec(name="DRIVER", type=t.Optional[SingleStoreDriver], + tier="core", default=None, driver_key="driver"), + ParameterSpec(name="AUTOCOMMIT", type=bool, tier="core", + default=True, driver_key="autocommit"), + ParameterSpec(name="LOCAL_INFILE", type=bool, tier="advanced", + default=True, driver_key="local_infile"), + ], +) + + +@register(SINGLESTOREDB_DESCRIPTOR) +class SingleStoreDBAuthSettings(ConnectionProfile): + __descriptor__ = SINGLESTOREDB_DESCRIPTOR diff --git a/tests/test_unit/backends/ibis/test_dialect_spec.py b/tests/test_unit/backends/ibis/test_dialect_spec.py index 7040c60..b9f3335 100644 --- a/tests/test_unit/backends/ibis/test_dialect_spec.py +++ b/tests/test_unit/backends/ibis/test_dialect_spec.py @@ -33,11 +33,12 @@ def fake_index_sql(table_name, index_name): assert spec.get_index_exists_sql("users", "idx_users_id") == "SELECT 1 FROM users" -def test_registry_contains_all_14_backends(): +def test_registry_contains_all_20_backends(): expected = { "sqlite", "duckdb", "motherduck", "postgres", "mysql", "mssql", "oracle", "snowflake", "bigquery", "redshift", "trino", "pyspark", - "clickhouse", "databricks", + "clickhouse", "databricks", "singlestoredb", + "exasol", "impala", "materialize", "risingwave", "druid", } assert set(DIALECTS.keys()) == expected diff --git a/tests/test_unit/core/settings/backends/test_druid.py b/tests/test_unit/core/settings/backends/test_druid.py new file mode 100644 index 0000000..d66a044 --- /dev/null +++ b/tests/test_unit/core/settings/backends/test_druid.py @@ -0,0 +1,50 @@ +# tests/test_unit/core/settings/backends/test_druid.py +from __future__ import annotations + +import pytest +from pydantic import SecretStr + +from mountainash_data.core.constants import CONST_DB_PROVIDER_TYPE +from mountainash_data.core.settings.auth import NoAuth, PasswordAuth +from mountainash_data.core.settings.druid import DruidAuthSettings + + +@pytest.mark.unit +class TestDruidAuthSettings: + def _minimal(self, **extra): + return DruidAuthSettings( + HOST="druid.example.com", + auth=NoAuth(), + **extra, + ) + + def test_provider_type(self): + assert self._minimal().provider_type == CONST_DB_PROVIDER_TYPE.DRUID + + def test_default_port(self): + assert self._minimal().PORT == 8082 + + def test_default_path(self): + assert self._minimal().ENDPOINT_PATH == "/druid/v2/sql" + + def test_default_scheme(self): + assert self._minimal().SCHEME == "http" + + def test_to_driver_kwargs(self): + kwargs = self._minimal(PORT=8888, SCHEME="https").to_driver_kwargs() + assert kwargs["host"] == "druid.example.com" + assert kwargs["port"] == 8888 + assert kwargs["path"] == "/druid/v2/sql" + assert kwargs["scheme"] == "https" + + def test_with_password_auth(self): + s = DruidAuthSettings( + HOST="druid.example.com", + auth=PasswordAuth(username="admin", password=SecretStr("pass")), + ) + kwargs = s.to_driver_kwargs() + assert kwargs["user"] == "admin" + assert kwargs["password"] == "pass" + + def test_ibis_dialect(self): + assert self._minimal().backend == "druid" diff --git a/tests/test_unit/core/settings/backends/test_exasol.py b/tests/test_unit/core/settings/backends/test_exasol.py new file mode 100644 index 0000000..e048a9b --- /dev/null +++ b/tests/test_unit/core/settings/backends/test_exasol.py @@ -0,0 +1,39 @@ +# tests/test_unit/core/settings/backends/test_exasol.py +from __future__ import annotations + +import pytest +from pydantic import SecretStr + +from mountainash_data.core.constants import CONST_DB_PROVIDER_TYPE +from mountainash_data.core.settings.auth import PasswordAuth +from mountainash_data.core.settings.exasol import ExasolAuthSettings + + +@pytest.mark.unit +class TestExasolAuthSettings: + def _minimal(self, **extra): + return ExasolAuthSettings( + HOST="exasol.example.com", + auth=PasswordAuth(username="sys", password=SecretStr("s3cret")), + **extra, + ) + + def test_provider_type(self): + assert self._minimal().provider_type == CONST_DB_PROVIDER_TYPE.EXASOL + + def test_default_port(self): + assert self._minimal().PORT == 8563 + + def test_default_timezone(self): + assert self._minimal().TIMEZONE == "UTC" + + def test_to_driver_kwargs(self): + kwargs = self._minimal().to_driver_kwargs() + assert kwargs["host"] == "exasol.example.com" + assert kwargs["port"] == 8563 + assert kwargs["timezone"] == "UTC" + assert kwargs["user"] == "sys" + assert kwargs["password"] == "s3cret" + + def test_ibis_dialect(self): + assert self._minimal().backend == "exasol" diff --git a/tests/test_unit/core/settings/backends/test_impala.py b/tests/test_unit/core/settings/backends/test_impala.py new file mode 100644 index 0000000..989dd8b --- /dev/null +++ b/tests/test_unit/core/settings/backends/test_impala.py @@ -0,0 +1,61 @@ +# tests/test_unit/core/settings/backends/test_impala.py +from __future__ import annotations + +import pytest +from pydantic import SecretStr, ValidationError + +from mountainash_data.core.constants import CONST_DB_PROVIDER_TYPE +from mountainash_data.core.settings.auth import NoAuth, PasswordAuth +from mountainash_data.core.settings.impala import ( + ImpalaAuthSettings, + ImpalaAuthMechanism, +) + + +@pytest.mark.unit +class TestImpalaAuthSettings: + def _minimal(self, **extra): + return ImpalaAuthSettings( + HOST="impala.example.com", + auth=NoAuth(), + **extra, + ) + + def test_provider_type(self): + assert self._minimal().provider_type == CONST_DB_PROVIDER_TYPE.IMPALA + + def test_default_port(self): + assert self._minimal().PORT == 21050 + + def test_default_database(self): + assert self._minimal().DATABASE == "default" + + def test_default_auth_mechanism(self): + assert self._minimal().AUTH_MECHANISM == ImpalaAuthMechanism.NOSASL + + def test_auth_mechanism_enum_enforced(self): + with pytest.raises(ValidationError): + self._minimal(AUTH_MECHANISM="nonsense") + + def test_gssapi_mechanism(self): + s = self._minimal(AUTH_MECHANISM=ImpalaAuthMechanism.GSSAPI) + kwargs = s.to_driver_kwargs() + assert kwargs["auth_mechanism"] == "GSSAPI" + + def test_ldap_with_password(self): + s = ImpalaAuthSettings( + HOST="impala.example.com", + auth=PasswordAuth(username="user", password=SecretStr("pass")), + AUTH_MECHANISM=ImpalaAuthMechanism.LDAP, + ) + kwargs = s.to_driver_kwargs() + assert kwargs["auth_mechanism"] == "LDAP" + assert kwargs["user"] == "user" + assert kwargs["password"] == "pass" + + def test_to_driver_kwargs_plumbs_ssl(self): + kwargs = self._minimal(USE_SSL=True).to_driver_kwargs() + assert kwargs["use_ssl"] is True + + def test_ibis_dialect(self): + assert self._minimal().backend == "impala" diff --git a/tests/test_unit/core/settings/backends/test_materialize.py b/tests/test_unit/core/settings/backends/test_materialize.py new file mode 100644 index 0000000..77b24bc --- /dev/null +++ b/tests/test_unit/core/settings/backends/test_materialize.py @@ -0,0 +1,49 @@ +# tests/test_unit/core/settings/backends/test_materialize.py +from __future__ import annotations + +import pytest +from pydantic import SecretStr + +from mountainash_data.core.constants import CONST_DB_PROVIDER_TYPE +from mountainash_data.core.settings.auth import NoAuth, PasswordAuth +from mountainash_data.core.settings.materialize import MaterializeAuthSettings + + +@pytest.mark.unit +class TestMaterializeAuthSettings: + def _minimal(self, **extra): + return MaterializeAuthSettings( + HOST="materialize.example.com", + auth=PasswordAuth(username="mz", password=SecretStr("s3cret")), + **extra, + ) + + def test_provider_type(self): + assert self._minimal().provider_type == CONST_DB_PROVIDER_TYPE.MATERIALIZE + + def test_default_port(self): + assert self._minimal().PORT == 6875 + + def test_default_autocommit(self): + assert self._minimal().AUTOCOMMIT is True + + def test_cluster_param(self): + s = self._minimal(CLUSTER="quickstart") + kwargs = s.to_driver_kwargs() + assert kwargs["cluster"] == "quickstart" + + def test_to_driver_kwargs(self): + kwargs = self._minimal(DATABASE="mydb", SCHEMA="public").to_driver_kwargs() + assert kwargs["host"] == "materialize.example.com" + assert kwargs["port"] == 6875 + assert kwargs["database"] == "mydb" + assert kwargs["schema"] == "public" + assert kwargs["user"] == "mz" + assert kwargs["password"] == "s3cret" + + def test_no_auth(self): + s = MaterializeAuthSettings(HOST="mz.local", auth=NoAuth()) + assert s.HOST == "mz.local" + + def test_ibis_dialect(self): + assert self._minimal().backend == "materialize" diff --git a/tests/test_unit/core/settings/backends/test_risingwave.py b/tests/test_unit/core/settings/backends/test_risingwave.py new file mode 100644 index 0000000..21b4f74 --- /dev/null +++ b/tests/test_unit/core/settings/backends/test_risingwave.py @@ -0,0 +1,41 @@ +# tests/test_unit/core/settings/backends/test_risingwave.py +from __future__ import annotations + +import pytest +from pydantic import SecretStr + +from mountainash_data.core.constants import CONST_DB_PROVIDER_TYPE +from mountainash_data.core.settings.auth import NoAuth, PasswordAuth +from mountainash_data.core.settings.risingwave import RisingWaveAuthSettings + + +@pytest.mark.unit +class TestRisingWaveAuthSettings: + def _minimal(self, **extra): + return RisingWaveAuthSettings( + HOST="rw.example.com", + auth=PasswordAuth(username="root", password=SecretStr("s3cret")), + **extra, + ) + + def test_provider_type(self): + assert self._minimal().provider_type == CONST_DB_PROVIDER_TYPE.RISINGWAVE + + def test_default_port(self): + assert self._minimal().PORT == 5432 + + def test_to_driver_kwargs(self): + kwargs = self._minimal(DATABASE="dev", SCHEMA="public").to_driver_kwargs() + assert kwargs["host"] == "rw.example.com" + assert kwargs["port"] == 5432 + assert kwargs["database"] == "dev" + assert kwargs["schema"] == "public" + assert kwargs["user"] == "root" + assert kwargs["password"] == "s3cret" + + def test_no_auth(self): + s = RisingWaveAuthSettings(HOST="rw.local", auth=NoAuth()) + assert s.HOST == "rw.local" + + def test_ibis_dialect(self): + assert self._minimal().backend == "risingwave" diff --git a/tests/test_unit/core/settings/backends/test_singlestoredb.py b/tests/test_unit/core/settings/backends/test_singlestoredb.py new file mode 100644 index 0000000..6780383 --- /dev/null +++ b/tests/test_unit/core/settings/backends/test_singlestoredb.py @@ -0,0 +1,74 @@ +# tests/test_unit/core/settings/backends/test_singlestoredb.py +from __future__ import annotations + +import pytest +from pydantic import SecretStr, ValidationError + +from mountainash_data.core.constants import CONST_DB_PROVIDER_TYPE +from mountainash_data.core.settings.auth import NoAuth, PasswordAuth +from mountainash_data.core.settings.singlestoredb import ( + SingleStoreDBAuthSettings, + SingleStoreDriver, +) + + +@pytest.mark.unit +class TestSingleStoreDBAuthSettings: + def _minimal(self, **extra): + return SingleStoreDBAuthSettings( + HOST="svc-123.svc.singlestore.com", + auth=PasswordAuth(username="admin", password=SecretStr("s3cret")), + **extra, + ) + + def test_provider_type_is_singlestoredb(self): + s = self._minimal() + assert s.provider_type == CONST_DB_PROVIDER_TYPE.SINGLESTOREDB + + def test_default_port(self): + s = self._minimal() + assert s.PORT == 3306 + + def test_custom_port(self): + s = self._minimal(PORT=3307) + assert s.PORT == 3307 + + def test_driver_enum_enforced(self): + with pytest.raises(ValidationError): + self._minimal(DRIVER="nonsense") + + def test_driver_mysql(self): + s = self._minimal(DRIVER=SingleStoreDriver.MYSQL) + assert s.DRIVER == SingleStoreDriver.MYSQL + + def test_driver_https(self): + s = self._minimal(DRIVER=SingleStoreDriver.HTTPS) + kwargs = s.to_driver_kwargs() + assert kwargs["driver"] == "https" + + def test_autocommit_default_true(self): + s = self._minimal() + assert s.AUTOCOMMIT is True + + def test_local_infile_default_true(self): + s = self._minimal() + assert s.LOCAL_INFILE is True + + def test_no_auth(self): + s = SingleStoreDBAuthSettings( + HOST="svc-123.svc.singlestore.com", auth=NoAuth(), + ) + assert s.HOST == "svc-123.svc.singlestore.com" + + def test_to_driver_kwargs_plumbs_core_fields(self): + s = self._minimal(PORT=3307, DATABASE="mydb") + kwargs = s.to_driver_kwargs() + assert kwargs["host"] == "svc-123.svc.singlestore.com" + assert kwargs["port"] == 3307 + assert kwargs["database"] == "mydb" + assert kwargs["user"] == "admin" + assert kwargs["password"] == "s3cret" + + def test_ibis_dialect(self): + s = self._minimal() + assert s.backend == "singlestoredb"