Skip to content

Database class

edgy.Database

Database(
    url=None,
    *,
    force_rollback=None,
    config=None,
    full_isolation=None,
    poll_interval=None,
    **options,
)

An abstraction on the top of the EncodeORM databases.Database object.

This object allows to pass also a configuration dictionary in the format of

DATABASEZ_CONFIG = { "connection": { "credentials": { "scheme": 'sqlite', "postgres"... "host": ..., "port": ..., "user": ..., "password": ..., "database": ..., "options": { "driver": ... "ssl": ... } } } }

Source code in databasez/core/database.py
def __init__(
    self,
    url: str | DatabaseURL | URL | Database | None = None,
    *,
    force_rollback: bool | None = None,
    config: DictAny | None = None,
    full_isolation: bool | None = None,
    # for custom poll intervals
    poll_interval: float | None = None,
    **options: Any,
):
    init()
    assert config is None or url is None, "Use either 'url' or 'config', not both."
    if isinstance(url, Database):
        assert not options, "Cannot specify options when copying a Database object."
        self.backend = url.backend.__copy__()
        self.url = url.url
        self.options = url.options
        self._call_hooks = url._call_hooks
        if poll_interval is None:
            poll_interval = url.poll_interval
        if force_rollback is None:
            force_rollback = bool(url.force_rollback)
        if full_isolation is None:
            full_isolation = bool(url._full_isolation)
    else:
        url = DatabaseURL(url)
        if config and "connection" in config:
            connection_config = config["connection"]
            if "credentials" in connection_config:
                connection_config = connection_config["credentials"]
                url = url.replace(**connection_config)
        self.backend, self.url, self.options = self.apply_database_url_and_options(
            url, **options
        )
        if force_rollback is None:
            force_rollback = False
        if full_isolation is None:
            full_isolation = False
        if poll_interval is None:
            # when not using utils...., the constant cannot be changed at runtime
            poll_interval = utils.DATABASEZ_POLL_INTERVAL
    self.poll_interval = poll_interval
    self._full_isolation = full_isolation
    self._force_rollback = ForceRollback(force_rollback)
    self.backend.owner = self
    self._connection_map = weakref.WeakKeyDictionary()
    self._databases_map = {}

    # When `force_rollback=True` is used, we use a single global
    # connection, within a transaction that always rolls back.
    self._global_connection: Connection | None = None

    self.ref_counter: int = 0
    self.ref_lock: asyncio.Lock = asyncio.Lock()

_connection_map instance-attribute

_connection_map = WeakKeyDictionary()

_databases_map instance-attribute

_databases_map = {}

_loop class-attribute instance-attribute

_loop = None

backend instance-attribute

backend

url instance-attribute

url

options instance-attribute

options

is_connected class-attribute instance-attribute

is_connected = False

_call_hooks class-attribute instance-attribute

_call_hooks = True

_remove_global_connection class-attribute instance-attribute

_remove_global_connection = True

_full_isolation class-attribute instance-attribute

_full_isolation = full_isolation

poll_interval instance-attribute

poll_interval = poll_interval

_force_rollback instance-attribute

_force_rollback = ForceRollback(force_rollback)

force_rollback class-attribute instance-attribute

force_rollback = ForceRollbackDescriptor()

async_helper class-attribute instance-attribute

async_helper = AsyncHelperDatabase

_global_connection instance-attribute

_global_connection = None

ref_counter instance-attribute

ref_counter = 0

ref_lock instance-attribute

ref_lock = Lock()

_current_task property

_current_task

_connection property writable

_connection

engine property

engine

__copy__

__copy__()
Source code in databasez/core/database.py
def __copy__(self) -> Database:
    return self.__class__(self)

inc_refcount async

inc_refcount()

Internal method to bump the ref_count.

Return True if ref_count is 0, False otherwise.

Should not be used outside of tests. Use connect and hooks instead. Not multithreading safe!

Source code in databasez/core/database.py
async def inc_refcount(self) -> bool:
    """
    Internal method to bump the ref_count.

    Return True if ref_count is 0, False otherwise.

    Should not be used outside of tests. Use connect and hooks instead.
    Not multithreading safe!
    """
    async with self.ref_lock:
        self.ref_counter += 1
        # on the first call is count is 1 because of the former +1
        if self.ref_counter == 1:
            return True
    return False

decr_refcount async

decr_refcount()

Internal method to decrease the ref_count.

Return True if ref_count drops to 0, False otherwise.

Should not be used outside of tests. Use disconnect and hooks instead. Not multithreading safe!

Source code in databasez/core/database.py
async def decr_refcount(self) -> bool:
    """
    Internal method to decrease the ref_count.

    Return True if ref_count drops to 0, False otherwise.

    Should not be used outside of tests. Use disconnect and hooks instead.
    Not multithreading safe!
    """
    async with self.ref_lock:
        self.ref_counter -= 1
        # on the last call, the count is 0
        if self.ref_counter == 0:
            return True
    return False

connect_hook async

connect_hook()

Refcount protected connect hook. Executed before engine and global connection setup.

Source code in databasez/core/database.py
async def connect_hook(self) -> None:
    """Refcount protected connect hook. Executed before engine and global connection setup."""

connect async

connect()

Establish the connection pool.

Source code in databasez/core/database.py
async def connect(self) -> bool:
    """
    Establish the connection pool.
    """
    loop = asyncio.get_running_loop()
    if self._loop is not None and loop != self._loop:
        if self.poll_interval < 0:
            raise RuntimeError("Subdatabases and polling are disabled")
        # copy when not in map
        if loop not in self._databases_map:
            assert self._global_connection is not None, (
                "global connection should have been set"
            )
            # correctly initialize force_rollback with parent value
            database = self.__class__(
                self, force_rollback=bool(self.force_rollback), full_isolation=False
            )
            # prevent side effects of connect_hook
            database._call_hooks = False
            database._global_connection = self._global_connection
            self._databases_map[loop] = database
        # forward call
        return await self._databases_map[loop].connect()

    if not await self.inc_refcount():
        assert self.is_connected, "ref_count < 0"
        return False
    if self._call_hooks:
        try:
            await self.connect_hook()
        except BaseException as exc:
            await self.decr_refcount()
            raise exc
    self._loop = asyncio.get_event_loop()

    await self.backend.connect(self.url, **self.options)
    self.is_connected = True

    if self._global_connection is None:
        connection = Connection(self, force_rollback=True, full_isolation=self._full_isolation)
        self._global_connection = connection
    else:
        self._remove_global_connection = False
    return True

disconnect_hook async

disconnect_hook()

Refcount protected disconnect hook. Executed after connection, engine cleanup.

Source code in databasez/core/database.py
async def disconnect_hook(self) -> None:
    """Refcount protected disconnect hook. Executed after connection, engine cleanup."""

disconnect async

disconnect(force=False, *, parent_database=None)

Close all connections in the connection pool.

Source code in databasez/core/database.py
@multiloop_protector(True, inject_parent=True)
async def disconnect(
    self, force: bool = False, *, parent_database: Database | None = None
) -> bool:
    """
    Close all connections in the connection pool.
    """
    # parent_database is injected and should not be specified manually
    if not await self.decr_refcount() or force:
        if not self.is_connected:
            logger.debug("Already disconnected, skip disconnecting")
            return False
        if force:
            logger.warning("Force disconnect, despite refcount not 0")
        else:
            return False
    if parent_database is not None:
        loop = asyncio.get_running_loop()
        del parent_database._databases_map[loop]
    if force and self._databases_map:
        assert not self._databases_map, "sub databases still active, force terminate them"
        for sub_database in self._databases_map.values():
            await arun_coroutine_threadsafe(
                sub_database.disconnect(True),
                sub_database._loop,
                self.poll_interval,
            )
        self._databases_map = {}
    assert not self._databases_map, "sub databases still active"

    try:
        assert self._global_connection is not None
        if self._remove_global_connection:
            await self._global_connection.__aexit__()
            self._global_connection = None
        self._connection = None
    finally:
        self.is_connected = False
        await self.backend.disconnect()
        self._loop = None
        if self._call_hooks:
            await self.disconnect_hook()
    return True

fetch_all async

fetch_all(query, values=None, timeout=None)
Source code in databasez/core/database.py
async def fetch_all(
    self,
    query: ClauseElement | str,
    values: dict | None = None,
    timeout: float | None = None,
) -> list[interfaces.Record]:
    async with self.connection() as connection:
        return await connection.fetch_all(query, values, timeout=timeout)

fetch_one async

fetch_one(query, values=None, pos=0, timeout=None)
Source code in databasez/core/database.py
async def fetch_one(
    self,
    query: ClauseElement | str,
    values: dict | None = None,
    pos: int = 0,
    timeout: float | None = None,
) -> interfaces.Record | None:
    async with self.connection() as connection:
        return await connection.fetch_one(query, values, pos=pos, timeout=timeout)

fetch_val async

fetch_val(
    query, values=None, column=0, pos=0, timeout=None
)
Source code in databasez/core/database.py
async def fetch_val(
    self,
    query: ClauseElement | str,
    values: dict | None = None,
    column: Any = 0,
    pos: int = 0,
    timeout: float | None = None,
) -> Any:
    async with self.connection() as connection:
        return await connection.fetch_val(
            query,
            values,
            column=column,
            pos=pos,
            timeout=timeout,
        )

execute async

execute(query, values=None, timeout=None)
Source code in databasez/core/database.py
async def execute(
    self,
    query: ClauseElement | str,
    values: Any = None,
    timeout: float | None = None,
) -> interfaces.Record | int:
    async with self.connection() as connection:
        return await connection.execute(query, values, timeout=timeout)

execute_many async

execute_many(query, values=None, timeout=None)
Source code in databasez/core/database.py
async def execute_many(
    self,
    query: ClauseElement | str,
    values: Any = None,
    timeout: float | None = None,
) -> Sequence[interfaces.Record] | int:
    async with self.connection() as connection:
        return await connection.execute_many(query, values, timeout=timeout)

iterate async

iterate(query, values=None, chunk_size=None, timeout=None)
Source code in databasez/core/database.py
async def iterate(
    self,
    query: ClauseElement | str,
    values: dict | None = None,
    chunk_size: int | None = None,
    timeout: float | None = None,
) -> AsyncGenerator[interfaces.Record, None]:
    async with self.connection() as connection:
        async for record in connection.iterate(query, values, chunk_size, timeout=timeout):
            yield record

batched_iterate async

batched_iterate(
    query,
    values=None,
    batch_size=None,
    batch_wrapper=tuple,
    timeout=None,
)
Source code in databasez/core/database.py
async def batched_iterate(
    self,
    query: ClauseElement | str,
    values: dict | None = None,
    batch_size: int | None = None,
    batch_wrapper: BatchCallable = tuple,
    timeout: float | None = None,
) -> AsyncGenerator[BatchCallableResult, None]:
    async with self.connection() as connection:
        async for batch in cast(
            AsyncGenerator["BatchCallableResult", None],
            connection.batched_iterate(
                query,
                values,
                batch_wrapper=batch_wrapper,
                batch_size=batch_size,
                timeout=timeout,
            ),
        ):
            yield batch

transaction

transaction(*, force_rollback=False, **kwargs)
Source code in databasez/core/database.py
def transaction(self, *, force_rollback: bool = False, **kwargs: Any) -> Transaction:
    return Transaction(self.connection, force_rollback=force_rollback, **kwargs)

run_sync async

run_sync(fn, *args, timeout=None, **kwargs)
Source code in databasez/core/database.py
async def run_sync(
    self,
    fn: Callable[..., Any],
    *args: Any,
    timeout: float | None = None,
    **kwargs: Any,
) -> Any:
    async with self.connection() as connection:
        return await connection.run_sync(fn, *args, **kwargs, timeout=timeout)

create_all async

create_all(meta, timeout=None, **kwargs)
Source code in databasez/core/database.py
async def create_all(
    self, meta: MetaData, timeout: float | None = None, **kwargs: Any
) -> None:
    async with self.connection() as connection:
        await connection.create_all(meta, **kwargs, timeout=timeout)

drop_all async

drop_all(meta, timeout=None, **kwargs)
Source code in databasez/core/database.py
async def drop_all(self, meta: MetaData, timeout: float | None = None, **kwargs: Any) -> None:
    async with self.connection() as connection:
        await connection.drop_all(meta, **kwargs, timeout=timeout)

_non_global_connection

_non_global_connection(timeout=None)
Source code in databasez/core/database.py
@multiloop_protector(False)
def _non_global_connection(
    self,
    timeout: (
        float | None
    ) = None,  # stub for type checker, multiloop_protector handles timeout
) -> Connection:
    if self._connection is None:
        _connection = self._connection = Connection(self)
        return _connection
    return self._connection

connection

connection(timeout=None)
Source code in databasez/core/database.py
def connection(self, timeout: float | None = None) -> Connection:
    if not self.is_connected:
        raise RuntimeError("Database is not connected")
    if self.force_rollback:
        return cast(Connection, self._global_connection)
    return self._non_global_connection(timeout=timeout)

asgi

asgi(
    app: None, handle_lifespan: bool = False
) -> Callable[[ASGIApp], ASGIApp]
asgi(
    app: ASGIApp, handle_lifespan: bool = False
) -> ASGIApp
asgi(app=None, handle_lifespan=False)

Return wrapper for asgi integration.

Source code in databasez/core/database.py
def asgi(
    self,
    app: ASGIApp | None = None,
    handle_lifespan: bool = False,
) -> ASGIApp | Callable[[ASGIApp], ASGIApp]:
    """Return wrapper for asgi integration."""

    async def setup() -> contextlib.AsyncExitStack:
        cleanupstack = contextlib.AsyncExitStack()
        await self.connect()
        cleanupstack.push_async_callback(self.disconnect)
        return cleanupstack

    return LifespanHook(app, setup=setup, do_forward=not handle_lifespan)

get_backends classmethod

get_backends(
    scheme="",
    *,
    overwrite_paths=["databasez.overwrites"],
    database_name="Database",
    connection_name="Connection",
    transaction_name="Transaction",
    database_class=None,
    connection_class=None,
    transaction_class=None,
)
Source code in databasez/core/database.py
@classmethod
def get_backends(
    cls,
    # let scheme empty for direct imports
    scheme: str = "",
    *,
    overwrite_paths: Sequence[str] = ["databasez.overwrites"],
    database_name: str = "Database",
    connection_name: str = "Connection",
    transaction_name: str = "Transaction",
    database_class: type[interfaces.DatabaseBackend] | None = None,
    connection_class: type[interfaces.ConnectionBackend] | None = None,
    transaction_class: type[interfaces.TransactionBackend] | None = None,
) -> tuple[
    type[interfaces.DatabaseBackend],
    type[interfaces.ConnectionBackend],
    type[interfaces.TransactionBackend],
]:
    module: Any = None
    # when not using utils...., the constant cannot be changed at runtime for debug purposes
    more_debug = utils.DATABASEZ_OVERWRITE_LOGGING
    for overwrite_path in overwrite_paths:
        imp_path = f"{overwrite_path}.{scheme.replace('+', '_')}" if scheme else overwrite_path
        try:
            module = importlib.import_module(imp_path)
        except ImportError as exc:
            logging.debug(
                f'Could not import "{imp_path}". Continue search.',
                exc_info=exc if more_debug else None,
            )
            if "+" in scheme:
                imp_path = f"{overwrite_path}.{scheme.split('+', 1)[0]}"
                try:
                    module = importlib.import_module(imp_path)
                except ImportError as exc:
                    logging.debug(
                        f'Could not import "{imp_path}". Continue search.',
                        exc_info=exc if more_debug else None,
                    )
        if module is not None:
            break
    if module is None:
        logging.debug(
            "No overwrites found. Use default.",
        )
    database_class = getattr(module, database_name, database_class)
    assert database_class is not None and issubclass(
        database_class, interfaces.DatabaseBackend
    )
    connection_class = getattr(module, connection_name, connection_class)
    assert connection_class is not None and issubclass(
        connection_class, interfaces.ConnectionBackend
    )
    transaction_class = getattr(module, transaction_name, transaction_class)
    assert transaction_class is not None and issubclass(
        transaction_class, interfaces.TransactionBackend
    )
    return database_class, connection_class, transaction_class

apply_database_url_and_options classmethod

apply_database_url_and_options(
    url,
    *,
    overwrite_paths=["databasez.overwrites"],
    **options,
)
Source code in databasez/core/database.py
@classmethod
def apply_database_url_and_options(
    cls,
    url: DatabaseURL | str,
    *,
    overwrite_paths: Sequence[str] = ["databasez.overwrites"],
    **options: Any,
) -> tuple[interfaces.DatabaseBackend, DatabaseURL, dict[str, Any]]:
    url = DatabaseURL(url)
    database_class, connection_class, transaction_class = cls.get_backends(
        url.scheme,
        database_class=default_database,
        connection_class=default_connection,
        transaction_class=default_transaction,
        overwrite_paths=overwrite_paths,
    )

    backend = database_class(
        connection_class=connection_class, transaction_class=transaction_class
    )
    url, options = backend.extract_options(url, **options)
    # check against transformed url
    assert url.sqla_url.get_dialect(True).is_async, f'Dialect: "{url.scheme}" is not async.'

    return backend, url, options