Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions bot/config/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ class BotConfig:

@dataclass(kw_only=True, frozen=True)
class BotConfigRow:
"""
Columns of the ``bot.bot_config`` table.
"""

discord_token: str

def to_data(self) -> BotConfig:
Expand All @@ -33,6 +37,10 @@ def from_data(data: BotConfig) -> BotConfigRow:


class ConfigStore:
"""
A class responsible for loading/storing :class:`BotConfig` value(s) to the DB, and maintaining an in-memory cache.
"""

__slots__ = "_pool", "_bot_config"
_bot_config: BotConfig | None

Expand All @@ -41,15 +49,18 @@ def __init__(self, pool: asyncpg.Pool, /):
self._bot_config = None

async def get_bot_config(self) -> BotConfig:
"""Raises if no config exists."""
if self._bot_config is None:
self._bot_config = await self._select()
return self._bot_config

async def set_bot_config(self, config: BotConfig, /) -> None:
"""Raises if no config exists."""
await self._update(config)
self._bot_config = config

async def create_initial_config(self, config: BotConfig, /) -> None:
"""Raises if a config already exists."""
await self._insert(config)
self._bot_config = config

Expand Down
5 changes: 5 additions & 0 deletions bot/database/logging.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
"""
A callback that logs raw SQL statements is defined here, so that all raw SQL logs are attributed to this module.
"""

import logging

import asyncpg
Expand All @@ -7,6 +11,7 @@


def log_query(query: asyncpg.connection.LoggedQuery) -> None:
"""A logging callback automatically installed by :func:`bot.database.pool.create_database_pool`."""
kwargs: dict[str, object] = {}
if query.args:
kwargs["args"] = query.args
Expand Down
4 changes: 4 additions & 0 deletions bot/database/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ async def _init_connection(connection: asyncpg.Connection) -> None:


async def create_database_pool(*, database_connection_string: str) -> asyncpg.Pool:
"""
Create a DB connection pool. This should only be called once, as there should only be one :class:`asyncpg.Pool` in
the bot process.
"""
pool = await asyncpg.create_pool(
dsn=database_connection_string,
init=_init_connection,
Expand Down
37 changes: 25 additions & 12 deletions bot/database/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class FieldOrder(Generic[T]):
This class helps convert dataclasses to and from ordered lists of fields, in order to help construct SQL queries in
a way where one does not accidentally mess up the field names or the field order.

An instance of FieldOrder holds onto one or more classes, and all its methods use the same order to talk about the
classes and their fields.
An instance of :class:`FieldOrder` holds onto one or more classes, and all its methods use the same order to talk
about the classes and their fields.

Given a dataclass:

Expand All @@ -33,13 +33,18 @@ class MyClass:

.. code-block:: python

row = await conn.execute(f"SELECT {fields.columns} FROM table")
row = await conn.execute(
f"SELECT {fields.columns} FROM table",
)
value = fields.from_tuple(row)

.. code-block:: python

await conn.execute(
f"INSERT INTO table ({fields.columns}) VALUES ({fields.placeholders})",
f\"\"\"
INSERT INTO table ({fields.columns})
VALUES ({fields.placeholders})
\"\"\",
*fields.to_tuple(value),
)

Expand Down Expand Up @@ -123,7 +128,7 @@ def from_tuple(self, values: Iterable[Any]) -> T:

def tupled(self, cls: type[S], /, *, prefix: str | None = None) -> FieldOrder[tuple[T, S]]:
"""
Construct a FieldOrder referencing more than one class at a time:
Construct a :class:`FieldOrder` referencing more than one class at a time:

.. code-block:: python

Expand All @@ -136,18 +141,26 @@ class OtherClass:
# allFields.columns is "x, y, z"
# allFields.placeholders is "$1, $2, $3"
# allFields.set_list is "x = $1, y = $2, z = $3"
# allFields.to_tuple(arg) is (arg[0].x, arg[0].y, arg[1].z)
# allFields.from_tuple(t) is (MyClass(x=t[0], y=t[1]), OtherClass(z=t[2]))
# allFields.to_tuple(arg) is
# (arg[0].x, arg[0].y, arg[1].z)
# allFields.from_tuple(t) is
# (MyClass(x=t[0], y=t[1]), OtherClass(z=t[2]))

The ``prefix`` arguments can be used to disambiguate column expressions:
The ``prefix`` argument of the constructor can be used to disambiguate column expressions:

.. code-block:: python

taggedFields = FieldOrder(MyClass, prefix="my").tupled(OtherClass, prefix="other")
taggedFields = FieldOrder(MyClass, prefix="my") \
.tupled(OtherClass, prefix="other")

# taggedFields.columns is "my.x, my.y, other.z"

row = await conn.execute(f"SELECT {taggedFields.columns} FROM table AS my, table2 AS other")
row = await conn.execute(
f\"\"\"
SELECT {taggedFields.columns}
FROM table AS my, table2 AS other
\"\"\",
)
my, other = taggedFields.from_tuple(row)

"""
Expand All @@ -165,8 +178,8 @@ class OtherClass:

async def select_single(connection: _Connected, table: str, cls: type[T], condition: str | None = None, /) -> T | None:
"""
Select the first row of the given table that satisfies the given condition, and build the given dataclass out of it.
Returns None if no such row is found.
Select the first row of the given table that satisfies the given condition (SQL expression), and build the given
dataclass out of it. Returns ``None`` if no such row is found.
"""
fields = FieldOrder(cls)
row = await connection.fetchrow(
Expand Down
4 changes: 2 additions & 2 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
sphinx >=3
sphinx-autoapi
sphinx==8.2.3
sphinx-autoapi==3.6.1