Source code for alchemiq.clickhouse.connection

"""Process-global ClickHouse connection lifecycle (configure / dispose / lazy client)."""

from __future__ import annotations

import asyncio
from typing import Any

from alchemiq.exceptions import ClickHouseNotConfiguredError

_config: dict[str, Any] | None = None
_client: Any = None
_lock = asyncio.Lock()


[docs] def configure_clickhouse( dsn: str | None = None, *, host: str | None = None, port: int | None = None, username: str = "default", password: str = "", database: str = "default", secure: bool = False, settings: dict[str, Any] | None = None, **client_kw: Any, ) -> None: r"""Store ClickHouse connection parameters; the async client connects on first use. Call once at application startup (e.g. inside a FastAPI/FastStream lifespan). Pass either a ``dsn`` URL or individual ``host``/``port`` keyword arguments. The client is created lazily on the first internal client-creation call and is shared across the process. Call :func:`.dispose_clickhouse` on shutdown. E.g.:: from alchemiq.clickhouse import configure_clickhouse, dispose_clickhouse # host/port form: configure_clickhouse(host="localhost", port=8123) # DSN form: configure_clickhouse(dsn="clickhouse://default:@localhost/default") :param dsn: ClickHouse DSN URL (e.g. ``clickhouse://user:pass@host/db``). Mutually optional with ``host``/``port`` - use one or the other. :param host: Server hostname. :param port: Server port (default ``8123`` for HTTP, ``8443`` for HTTPS). :param username: ClickHouse username (default ``"default"``). :param password: ClickHouse password (default ``""``). :param database: Target database name (default ``"default"``). :param secure: Use TLS/HTTPS if ``True`` (default ``False``). :param settings: Extra ClickHouse server settings passed to the client. :param client_kw: Additional keyword arguments forwarded to ``clickhouse_connect.get_async_client``. .. seealso:: :func:`.dispose_clickhouse` - close and reset the client on shutdown. """ global _config, _client cfg: dict[str, Any] = { "username": username, "password": password, "database": database, "secure": secure, } if dsn is not None: cfg["dsn"] = dsn if host is not None: cfg["host"] = host if port is not None: cfg["port"] = port if settings is not None: cfg["settings"] = settings cfg.update(client_kw) _config = cfg _client = None
[docs] def is_clickhouse_configured() -> bool: """Return True if configure_clickhouse() has been called and not yet disposed.""" return _config is not None
async def get_clickhouse_client() -> Any: """Return the process-global async client, creating it lazily on first call.""" global _client if _config is None: raise ClickHouseNotConfiguredError( "ClickHouse is not configured; call configure_clickhouse(...) first" ) if _client is not None: # fast path, no lock return _client async with _lock: if _client is None: # double-checked under lock import clickhouse_connect # ty: ignore[unresolved-import] _client = await clickhouse_connect.get_async_client(**_config) return _client
[docs] async def dispose_clickhouse() -> None: """Close the process-global client and clear all connection state. Safe to call even if the client was never created (no-op in that case). Typically called in a lifespan ``finally`` block or in test teardown. .. seealso:: :func:`.configure_clickhouse` - reconfigure after disposing. """ global _config, _client if _client is not None: await _client.close() _client = None _config = None