From f45db389ba651c80b14d5c51821ee86632433f15 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Fri, 3 Jul 2026 00:16:12 -0400 Subject: [PATCH] docs: compress prose/module docstrings, em-dash->hyphen (de-bloat wave 1) Signed-off-by: disqualifier --- README.md | 6 +-- pyproject.toml | 2 +- src/mysql/__init__.py | 4 +- src/mysql/mysql.py | 106 +++++++++++------------------------------- 4 files changed, 31 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 1d77af6..70da3ac 100644 --- a/README.md +++ b/README.md @@ -11,18 +11,18 @@ wire-compatible and share the driver, so this covers both. `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: ```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`). -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 diff --git a/pyproject.toml b/pyproject.toml index 9c0442e..45148e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] 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" requires-python = ">=3.10" dependencies = [ diff --git a/src/mysql/__init__.py b/src/mysql/__init__.py index d2af735..79935b7 100644 --- a/src/mysql/__init__.py +++ b/src/mysql/__init__.py @@ -1,5 +1,3 @@ from .mysql import MysqlDB -__version__ = "0.1.4" - -__all__ = ["MysqlDB", "__version__"] +__all__ = ["MysqlDB"] diff --git a/src/mysql/mysql.py b/src/mysql/mysql.py index 1ac5f5e..1434b45 100644 --- a/src/mysql/mysql.py +++ b/src/mysql/mysql.py @@ -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 app.db = await MysqlDB(host="localhost", port=3306, db="app", user="root", password="secret").connect() await app.db.insert("users", {"name": "ada", "active": True}) rows = await app.db.get("users", {"active": True}) # [{"name": "ada", ...}] - await app.db.close() # on shutdown + await app.db.close() -context manager: - async with MysqlDB(db="app", user="root") as db: - await db.execute("CREATE TABLE ...") +layer 1 is IDENTICAL to the `psql` lib's surface (swap psql<->mysql with zero call-site +changes); layer 2 is the raw `%s`-placeholder escape hatch. see README for the full API +and mysql/psql dialect differences. -lifecycle: - construction is sync and opens no socket. connect() builds the aiomysql pool and - 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. +a literal `%` in layer-2 SQL text must be written `%%` (PyMySQL builds the query via +`query % args` whenever params are passed); layer 1 escapes `%` in identifiers for you. -two-layer API (IDENTICAL layer-1 surface to the `psql` lib — swap psql<->mysql with zero -call-site changes; only the dialect below differs, hidden inside the wrapper): - LAYER 1 — friendly portable verbs for simple single-table CRUD: create_database, - 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). +errors are FAIL LOUD (unlike the mongo lib's swallow-and-default): every method catches +the driver error, logs via getLogger(__name__), and re-raises. a None/[] return is only +ever a real result, never a swallowed failure. for anything not wrapped, use `.pool`. """ import asyncio @@ -65,25 +33,12 @@ _DRIVER_ERRORS = (MySQLError, OSError) def _first_value(row) -> Any: - """return the first column's value from a dict or sequence row - - 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 the first column's value from a dict or sequence row (either cursorclass)""" return next(iter(row.values())) if isinstance(row, dict) else row[0] def _quote_ident(identifier: str) -> str: - """backtick-quote a sql identifier (table/column), escaping backticks and `%` - - 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). - """ + """backtick-quote a sql identifier, doubling embedded backticks and `%` (see module `%%` note)""" return "`" + identifier.replace("`", "``").replace("%", "%%") + "`" @@ -107,10 +62,10 @@ class MysqlDB: 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, ...). 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() 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 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 @@ -118,7 +73,7 @@ class MysqlDB: """ if pool_kwargs.get("autocommit") is False: 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() " "for an atomic multi-statement block instead" ) @@ -219,7 +174,7 @@ class MysqlDB: 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: """run a statement (INSERT/UPDATE/DELETE/DDL); return the affected rowcount @@ -262,20 +217,15 @@ class MysqlDB: return _first_value(row) 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 aiomysql connection (autocommit disabled for the block). """ 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: """CREATE DATABASE IF NOT EXISTS name""" @@ -313,7 +263,7 @@ class MysqlDB: """INSERT one row from {column: value}; return the new row id (lastrowid) 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). """ cols = list(values.keys()) @@ -330,7 +280,7 @@ class MysqlDB: async def get(self, table: str, conditions: Optional[Dict[str, Any]] = None) -> List[dict]: """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(). """ where, params = _where(conditions) @@ -377,16 +327,12 @@ class MysqlDB: return bool(_first_value(row)) if row else False 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 - on ANY unique-key clash (it does not name the constraint the way postgres does), so - `conflict` is accepted for call-signature parity with psql but the constraint is - whatever unique/pk key the row violates. returns the affected rowcount. emits the - `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. + `conflict` is accepted for call-signature parity with psql, but mysql upserts on + ANY unique/pk clash (it does not name the constraint like postgres). returns the + affected rowcount. emits the `col = VALUES(col)` update form, deprecated since + MySQL 8.0.20 (warning 1287, still functional); unaffected on MariaDB. """ cols = list(values.keys()) placeholders = ", ".join(["%s"] * len(cols)) @@ -421,7 +367,7 @@ class _Transaction: await self._conn.begin() except BaseException: # __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) self._conn = None raise