From f07a8da5a4cce41d8a4522e2d010e1500a17302a Mon Sep 17 00:00:00 2001 From: disqualifier Date: Fri, 3 Jul 2026 00:16:11 -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/psql/psql.py | 68 ++++++++++++++++++------------------------------ 3 files changed, 29 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 164b38d..957e63b 100644 --- a/README.md +++ b/README.md @@ -10,18 +10,18 @@ a sibling of the `mongo` lib. Class is **`PsqlDB`**. `requirements.txt`: ``` -psql @ git+ssh://git@git.rethinkstudios.io/rethink-public/psql.git@v0.1.5 +psql @ git+ssh://git@git.rethinkstudios.io/rethink-public/psql.git@v0.1.6 ``` Direct: ```bash -pip install "psql @ git+ssh://git@git.rethinkstudios.io/rethink-public/psql.git@v0.1.5" +pip install "psql @ git+ssh://git@git.rethinkstudios.io/rethink-public/psql.git@v0.1.6" ``` Pulls `asyncpg`. -Drop the `@v0.1.5` suffix from the line above to install the latest unpinned. +Drop the `@v0.1.6` suffix from the line above to install the latest unpinned. ## The two-layer API diff --git a/pyproject.toml b/pyproject.toml index 9e8f32e..9792361 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "psql" -version = "0.1.5" +version = "0.1.6" description = "async postgres wrapper over asyncpg: two-layer API (friendly verbs + raw escape hatch), fail-loud, config-free" requires-python = ">=3.10" dependencies = [ diff --git a/src/psql/psql.py b/src/psql/psql.py index edf5602..5728a5f 100644 --- a/src/psql/psql.py +++ b/src/psql/psql.py @@ -1,5 +1,5 @@ """ -async postgres wrapper over asyncpg — two-layer API (friendly verbs + raw escape hatch) +async postgres wrapper over asyncpg - two-layer API (friendly verbs + raw escape hatch) object pattern (one pool per process), attach to the app: from psql import PsqlDB @@ -13,44 +13,29 @@ context manager: async with PsqlDB(database="app", user="postgres") as db: await db.execute("CREATE TABLE ...") -lifecycle: - construction is sync, opens no socket. connect() builds the asyncpg pool, validates it - with `SELECT 1` (fail loud on bad host/credentials immediately, not on the first real - op), returns self; concurrent connect() calls are lock-serialized so only one pool is - ever live. close() closes the pool and nulls the reference (a closed instance reports - not-connected). - dsn: - host/port default to None, not "localhost"/5432 — asyncpg only reads a dsn's embedded + host/port default to None, not "localhost"/5432 - asyncpg only reads a dsn's embedded host/port when the host/port kwargs are falsy, so a hardcoded default would silently shadow the dsn's server. pass dsn=... in pool_kwargs alone (no host/port) to let the dsn's own host/port reach asyncpg; the no-dsn path still defaults to localhost:5432. two-layer API: - LAYER 1 — friendly, portable verbs for simple single-table CRUD. these hide the - dialect and are byte-for-byte identical to the `mysql` lib, so a dev swaps psql<->mysql - with zero call-site changes: create_database, create_table, drop, insert, get, - get_one, delete, exists, upsert. - LAYER 2 — raw escape hatch for the complex ~20% (joins, aggregates, CTEs, window - functions): execute, fetch, fetchone, fetchval, transaction. you write the SQL with - `$1, $2` placeholders (asyncpg style) + params; the wrapper still gives pooling, - parameterization, fail-loud errors, and row->dict conversion. raw SQL is psql-specific. - there is deliberately NOTHING in between — no query builder / ORM. a join goes through - raw fetch(), never a chainable .where()/.join(). - -rows: - layer 1 and fetch/fetchone return plain dicts ({column: value}), not asyncpg Record - objects — identical shape to the mysql lib. + LAYER 1 - friendly, portable verbs for simple single-table CRUD, byte-for-byte + identical to the `mysql` lib (zero call-site changes swapping psql<->mysql): + create_database, create_table, drop, insert, get, get_one, delete, exists, upsert. + LAYER 2 - raw escape hatch for the complex ~20% (joins, aggregates, CTEs, window + functions): execute, fetch, fetchone, fetchval, transaction, using `$1, $2` + placeholders (asyncpg style). deliberately nothing in between - no query builder/ORM. placeholders / injection safety: - values are ALWAYS parameterized — layer 1 builds `$1, $2` internally; layer 2 takes + values are ALWAYS parameterized - layer 1 builds `$1, $2` internally; layer 2 takes your `$1` placeholders + *params. never f-string/format a value into SQL. only identifiers (table/column names) are interpolated, and they are quoted. -errors (FAIL LOUD — unlike the mongo lib's swallow-and-default): +errors (FAIL LOUD - unlike the mongo lib's swallow-and-default): every method catches the driver error (asyncpg.PostgresError / InterfaceError, 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 + only ever a real result (no row, empty table) - never a swallowed failure. for anything not wrapped, use the raw `.pool` property (the asyncpg.Pool). """ @@ -68,7 +53,7 @@ _DRIVER_ERRORS = (asyncpg.PostgresError, asyncpg.InterfaceError, OSError) def _quote_ident(identifier: str) -> str: """quote a sql identifier (table/column), escaping embedded double-quotes - identifiers can't be parameterized, so they are interpolated — quoting + doubling any + identifiers can't be parameterized, so they are interpolated - quoting + doubling any embedded quote is the postgres-safe way to do that for caller-supplied names. """ return '"' + identifier.replace('"', '""') + '"' @@ -173,22 +158,20 @@ class PsqlDB: @property def pool(self) -> asyncpg.Pool: - """raw asyncpg.Pool escape hatch; full driver surface, raises - - use for copy/prepare/listen-notify/cursors and anything not wrapped. - """ + """raw asyncpg.Pool escape hatch for copy/prepare/listen-notify/cursors and + anything not wrapped; full driver surface, raises""" if self._pool is None: raise RuntimeError("psql: not connected; call await db.connect() first") return self._pool # ------------------------------------------------------------------------- - # layer 2 — raw escape hatch (you write the SQL, $1 placeholders) + # layer 2 - raw escape hatch (you write the SQL, $1 placeholders) async def execute(self, query: str, *params: Any) -> str: """run a statement (INSERT/UPDATE/DELETE/DDL); return asyncpg's status string - the status string is e.g. "INSERT 0 1" / "UPDATE 3" / "DELETE 2" — parse it or use - the layer-1 verbs (insert/delete) which return structured values instead. + e.g. "INSERT 0 1" / "UPDATE 3" / "DELETE 2" - parse it, or use the layer-1 verbs + (insert/delete) which return structured values instead. """ try: return await self.pool.execute(query, *params) @@ -208,8 +191,7 @@ class PsqlDB: async def fetchone(self, query: str, *params: Any) -> Optional[dict]: """run a query and return the first row as a dict, or None if no rows - named fetchone (not asyncpg's fetchrow) to match the mysql lib's layer-2 surface; - maps to the driver's fetchrow internally. + named fetchone (not asyncpg's fetchrow) to match the mysql lib's layer-2 surface. """ try: row = await self.pool.fetchrow(query, *params) @@ -234,15 +216,15 @@ class PsqlDB: await conn.execute("INSERT ...", a) await conn.execute("UPDATE ...", b) commits on clean exit, rolls back and re-raises on any error. `conn` is a raw - asyncpg connection (use its $1-placeholder execute/fetch/... directly). + asyncpg connection ($1-placeholder execute/fetch/... directly). """ return _Transaction(self.pool) # ------------------------------------------------------------------------- - # layer 1 — friendly portable verbs (identical across psql/mysql) + # layer 1 - friendly portable verbs (identical across psql/mysql) async def create_database(self, name: str) -> None: - """CREATE DATABASE name (raises if it already exists — postgres has no IF NOT + """CREATE DATABASE name (raises if it already exists - postgres has no IF NOT EXISTS for CREATE DATABASE; catch the duplicate error or check first)""" try: await self.pool.execute(f"CREATE DATABASE {_quote_ident(name)}") @@ -278,7 +260,7 @@ class PsqlDB: """INSERT one row from {column: value}; return the inserted rowcount (1) values are parameterized ($1, $2, ...). returns the number of rows inserted (1 on - success) — the portable return shape shared with the mysql lib. + success) - the portable return shape shared with the mysql lib. """ cols = list(values.keys()) placeholders = ", ".join(f"${i + 1}" for i in range(len(cols))) @@ -294,7 +276,7 @@ class PsqlDB: 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) @@ -342,7 +324,7 @@ class PsqlDB: raise async def upsert(self, table: str, values: Dict[str, Any], conflict: Sequence[str]) -> int: - """INSERT ... ON CONFLICT (conflict_cols) DO UPDATE — insert or update on key clash + """INSERT ... ON CONFLICT (conflict_cols) DO UPDATE - insert or update on key clash `conflict` is the list of columns forming the unique/pk constraint to upsert on. the wrapper emits ON CONFLICT here (the mysql lib emits ON DUPLICATE KEY UPDATE for @@ -380,7 +362,7 @@ class _Transaction: await self._tx.start() except BaseException: # start() (or transaction()) failing after acquire would otherwise leak the - # pooled connection — __aexit__ is not called when __aenter__ raises. release + # pooled connection - __aexit__ is not called when __aenter__ raises. release # it and reset so a failed transaction start never burns a pool slot. await self._pool.release(self._conn) self._conn = None