docs: compress prose/module docstrings, em-dash->hyphen (de-bloat wave 1)

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
2026-07-03 00:16:12 -04:00
parent 2e837da7df
commit f45db389ba
4 changed files with 31 additions and 87 deletions
+3 -3
View File
@@ -11,18 +11,18 @@ wire-compatible and share the driver, so this covers both.
`requirements.txt`: `requirements.txt`:
``` ```
mysql @ git+ssh://git@git.rethinkstudios.io/rethink-public/mysql.git@v0.1.4 mysql @ git+ssh://git@git.rethinkstudios.io/rethink-public/mysql.git@v0.1.5
``` ```
Direct: Direct:
```bash ```bash
pip install "mysql @ git+ssh://git@git.rethinkstudios.io/rethink-public/mysql.git@v0.1.4" pip install "mysql @ git+ssh://git@git.rethinkstudios.io/rethink-public/mysql.git@v0.1.5"
``` ```
Pulls `aiomysql` (which pulls `PyMySQL`). Pulls `aiomysql` (which pulls `PyMySQL`).
Drop the `@v0.1.3` suffix from the line above to install the latest unpinned. Drop the `@v0.1.5` suffix from the line above to install the latest unpinned.
## The two-layer API ## The two-layer API
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "mysql" name = "mysql"
version = "0.1.4" version = "0.1.5"
description = "async mysql/mariadb wrapper over aiomysql: two-layer API (friendly verbs + raw escape hatch), fail-loud, config-free" description = "async mysql/mariadb wrapper over aiomysql: two-layer API (friendly verbs + raw escape hatch), fail-loud, config-free"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
+1 -3
View File
@@ -1,5 +1,3 @@
from .mysql import MysqlDB from .mysql import MysqlDB
__version__ = "0.1.4" __all__ = ["MysqlDB"]
__all__ = ["MysqlDB", "__version__"]
+26 -80
View File
@@ -1,55 +1,23 @@
""" """
async mysql / mariadb wrapper over aiomysql two-layer API (friendly verbs + raw hatch) async mysql/mariadb wrapper over aiomysql - two-layer API (friendly verbs + raw hatch)
covers MySQL and MariaDB (wire-compatible, same driver).
object pattern (one pool per process), attach to the app:
from mysql import MysqlDB from mysql import MysqlDB
app.db = await MysqlDB(host="localhost", port=3306, db="app", app.db = await MysqlDB(host="localhost", port=3306, db="app",
user="root", password="secret").connect() user="root", password="secret").connect()
await app.db.insert("users", {"name": "ada", "active": True}) await app.db.insert("users", {"name": "ada", "active": True})
rows = await app.db.get("users", {"active": True}) # [{"name": "ada", ...}] rows = await app.db.get("users", {"active": True}) # [{"name": "ada", ...}]
await app.db.close() # on shutdown await app.db.close()
context manager: layer 1 is IDENTICAL to the `psql` lib's surface (swap psql<->mysql with zero call-site
async with MysqlDB(db="app", user="root") as db: changes); layer 2 is the raw `%s`-placeholder escape hatch. see README for the full API
await db.execute("CREATE TABLE ...") and mysql/psql dialect differences.
lifecycle: a literal `%` in layer-2 SQL text must be written `%%` (PyMySQL builds the query via
construction is sync and opens no socket. connect() builds the aiomysql pool and `query % args` whenever params are passed); layer 1 escapes `%` in identifiers for you.
validates it with `SELECT 1` so a bad host/credentials fails loud immediately rather
than on the first real op, and returns self. close() closes the pool and waits for it.
the pool runs with autocommit=True, so the layer-1 verbs and raw execute() commit on
their own; transaction() opens an explicit transaction for atomic multi-statement work.
two-layer API (IDENTICAL layer-1 surface to the `psql` lib — swap psql<->mysql with zero errors are FAIL LOUD (unlike the mongo lib's swallow-and-default): every method catches
call-site changes; only the dialect below differs, hidden inside the wrapper): the driver error, logs via getLogger(__name__), and re-raises. a None/[] return is only
LAYER 1 — friendly portable verbs for simple single-table CRUD: create_database, ever a real result, never a swallowed failure. for anything not wrapped, use `.pool`.
create_table, drop, insert, get, get_one, delete, exists, upsert.
LAYER 2 — raw escape hatch for the complex ~20% (joins, aggregates, subqueries): you
write the SQL with `%s` placeholders + a params sequence; the wrapper gives pooling,
parameterization, fail-loud errors, and dict rows. execute, fetch, fetchone, fetchval,
transaction. NOTHING in between — no query builder / ORM.
dialect (mysql-specific; visible only in raw layer-2 SQL you write):
- placeholders are `%s` (here) vs `$1` (psql). the `%s` is the DBAPI placeholder the
driver consumes, NOT python `%` string formatting — but PyMySQL builds it via
`query % args` whenever params are passed, so any OTHER literal `%` in the query
text (e.g. `LIKE '%foo%'`) must be written `%%` or it raises. no params -> no
substitution -> a lone `%` is fine. layer 1 escapes `%` in identifiers for you.
- upsert emits `ON DUPLICATE KEY UPDATE` (here) vs `ON CONFLICT` (psql), via the
deprecated `col = VALUES(col)` form (MySQL 8.0.20+ warns; still works, MariaDB is
unaffected). layer-1 upsert() hides both.
rows:
layer 1 and fetch/fetchone return plain dicts via aiomysql's DictCursor (identical
shape to psql). fetchval/exists tolerate a caller-overridden tuple cursorclass too.
errors (FAIL LOUD — unlike the mongo lib's swallow-and-default):
every method catches the driver error (pymysql.err.MySQLError, OSError on connection
loss), logs via getLogger(__name__), and re-raises. a None/[] return is only ever a
real result (no row, empty table) — never a swallowed failure. for anything not
wrapped, use the raw `.pool` property (the aiomysql.Pool).
""" """
import asyncio import asyncio
@@ -65,25 +33,12 @@ _DRIVER_ERRORS = (MySQLError, OSError)
def _first_value(row) -> Any: def _first_value(row) -> Any:
"""return the first column's value from a dict or sequence row """return the first column's value from a dict or sequence row (either cursorclass)"""
fetchval/exists read a single scalar column and work under either cursorclass: the
default DictCursor (dict row) or a caller override to a tuple/sequence cursor via
pool_kwargs — the only two row shapes aiomysql cursors produce.
"""
return next(iter(row.values())) if isinstance(row, dict) else row[0] return next(iter(row.values())) if isinstance(row, dict) else row[0]
def _quote_ident(identifier: str) -> str: def _quote_ident(identifier: str) -> str:
"""backtick-quote a sql identifier (table/column), escaping backticks and `%` """backtick-quote a sql identifier, doubling embedded backticks and `%` (see module `%%` note)"""
identifiers can't be parameterized, so they are interpolated — backtick-quoting +
doubling any embedded backtick is the mysql-safe way to do that for caller names. an
embedded `%` is also doubled: PyMySQL applies `query % args` whenever a call passes
params, and an unescaped `%` inside an interpolated identifier would hit that same
substitution and raise, not just literal `%` written by the caller (see the module
docstring's `%%`-escaping rule).
"""
return "`" + identifier.replace("`", "``").replace("%", "%%") + "`" return "`" + identifier.replace("`", "``").replace("%", "%%") + "`"
@@ -107,10 +62,10 @@ class MysqlDB:
host/port/db/user/password/minsize/maxsize are injected by the caller. extra host/port/db/user/password/minsize/maxsize are injected by the caller. extra
pool_kwargs pass through to aiomysql.create_pool (unix_socket, ssl, charset, ...). pool_kwargs pass through to aiomysql.create_pool (unix_socket, ssl, charset, ...).
rows come back as dicts (DictCursor), overridable via pool_kwargs to a tuple rows come back as dicts (DictCursor), overridable via pool_kwargs to a tuple
cursorclass fetchval/exists work either way, everything else stays dict-shaped cursorclass - fetchval/exists work either way, everything else stays dict-shaped
only under DictCursor. autocommit is always on: layer-1 verbs and raw execute() only under DictCursor. autocommit is always on: layer-1 verbs and raw execute()
rely on it to commit their own writes without an explicit commit, so rely on it to commit their own writes without an explicit commit, so
`autocommit=False` in pool_kwargs is rejected outright silently accepting it `autocommit=False` in pool_kwargs is rejected outright - silently accepting it
would mean every write sits uncommitted until something else commits or the would mean every write sits uncommitted until something else commits or the
connection is returned to the pool and rolled back, losing data with no error. connection is returned to the pool and rolled back, losing data with no error.
use transaction() for an atomic multi-statement block instead (it disables use transaction() for an atomic multi-statement block instead (it disables
@@ -118,7 +73,7 @@ class MysqlDB:
""" """
if pool_kwargs.get("autocommit") is False: if pool_kwargs.get("autocommit") is False:
raise ValueError( raise ValueError(
"mysql: autocommit=False is not supported via pool_kwargs layer-1 verbs and " "mysql: autocommit=False is not supported via pool_kwargs - layer-1 verbs and "
"raw execute() need autocommit to persist their writes; use db.transaction() " "raw execute() need autocommit to persist their writes; use db.transaction() "
"for an atomic multi-statement block instead" "for an atomic multi-statement block instead"
) )
@@ -219,7 +174,7 @@ class MysqlDB:
return await cur.fetchone() return await cur.fetchone()
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# layer 2 raw escape hatch (you write the SQL, %s placeholders) # layer 2 - raw escape hatch (you write the SQL, %s placeholders)
async def execute(self, query: str, params: Optional[Sequence] = None) -> int: async def execute(self, query: str, params: Optional[Sequence] = None) -> int:
"""run a statement (INSERT/UPDATE/DELETE/DDL); return the affected rowcount """run a statement (INSERT/UPDATE/DELETE/DDL); return the affected rowcount
@@ -262,20 +217,15 @@ class MysqlDB:
return _first_value(row) return _first_value(row)
def transaction(self): def transaction(self):
"""async context manager running a block of statements atomically """async context manager running a block of statements atomically (see README)
usage:
async with db.transaction() as conn:
async with conn.cursor() as cur:
await cur.execute("INSERT ...", (a,))
await cur.execute("UPDATE ...", (b,))
commits on clean exit, rolls back and re-raises on any error. `conn` is a raw commits on clean exit, rolls back and re-raises on any error. `conn` is a raw
aiomysql connection (autocommit disabled for the block). aiomysql connection (autocommit disabled for the block).
""" """
return _Transaction(self.pool) return _Transaction(self.pool)
# ------------------------------------------------------------------------- # -------------------------------------------------------------------------
# layer 1 friendly portable verbs (identical to the psql lib) # layer 1 - friendly portable verbs (identical to the psql lib)
async def create_database(self, name: str) -> None: async def create_database(self, name: str) -> None:
"""CREATE DATABASE IF NOT EXISTS name""" """CREATE DATABASE IF NOT EXISTS name"""
@@ -313,7 +263,7 @@ class MysqlDB:
"""INSERT one row from {column: value}; return the new row id (lastrowid) """INSERT one row from {column: value}; return the new row id (lastrowid)
values are parameterized (%s). returns lastrowid for an auto-increment table, or values are parameterized (%s). returns lastrowid for an auto-increment table, or
0 when there is no auto-increment key the portable return shape shared with the 0 when there is no auto-increment key - the portable return shape shared with the
psql lib (which returns the RETURNING rowcount). psql lib (which returns the RETURNING rowcount).
""" """
cols = list(values.keys()) cols = list(values.keys())
@@ -330,7 +280,7 @@ class MysqlDB:
async def get(self, table: str, conditions: Optional[Dict[str, Any]] = None) -> List[dict]: async def get(self, table: str, conditions: Optional[Dict[str, Any]] = None) -> List[dict]:
"""SELECT * rows matching equality `conditions` (col = val AND ...) as dicts """SELECT * rows matching equality `conditions` (col = val AND ...) as dicts
conditions=None/{} returns all rows. simple equality only anything more complex conditions=None/{} returns all rows. simple equality only - anything more complex
goes through raw fetch(). goes through raw fetch().
""" """
where, params = _where(conditions) where, params = _where(conditions)
@@ -377,16 +327,12 @@ class MysqlDB:
return bool(_first_value(row)) if row else False return bool(_first_value(row)) if row else False
async def upsert(self, table: str, values: Dict[str, Any], conflict: Sequence[str]) -> int: async def upsert(self, table: str, values: Dict[str, Any], conflict: Sequence[str]) -> int:
"""INSERT ... ON DUPLICATE KEY UPDATE insert or update on unique/pk clash """INSERT ... ON DUPLICATE KEY UPDATE - insert or update on unique/pk clash
`conflict` is the list of columns forming the unique/pk constraint. mysql upserts `conflict` is accepted for call-signature parity with psql, but mysql upserts on
on ANY unique-key clash (it does not name the constraint the way postgres does), so ANY unique/pk clash (it does not name the constraint like postgres). returns the
`conflict` is accepted for call-signature parity with psql but the constraint is affected rowcount. emits the `col = VALUES(col)` update form, deprecated since
whatever unique/pk key the row violates. returns the affected rowcount. emits the MySQL 8.0.20 (warning 1287, still functional); unaffected on MariaDB.
`col = VALUES(col)` update form, deprecated since MySQL 8.0.20 (warning 1287, still
functional) and unaffected on MariaDB; a future MySQL-only build could probe
`SELECT VERSION()` and switch to alias syntax, not done here as a documented cost/
benefit call, not an oversight.
""" """
cols = list(values.keys()) cols = list(values.keys())
placeholders = ", ".join(["%s"] * len(cols)) placeholders = ", ".join(["%s"] * len(cols))
@@ -421,7 +367,7 @@ class _Transaction:
await self._conn.begin() await self._conn.begin()
except BaseException: except BaseException:
# __aexit__ never runs if __aenter__ raises, so begin() failing after acquire # __aexit__ never runs if __aenter__ raises, so begin() failing after acquire
# would otherwise leak the pooled connection release it here instead. # would otherwise leak the pooled connection - release it here instead.
await self._pool.release(self._conn) await self._pool.release(self._conn)
self._conn = None self._conn = None
raise raise