This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
just install # uv lock --upgrade && uv sync
just lint # ruff format + eof-fixer (auto-fix)
just lint-ci # ruff check only (no fixes)
just build # build docker image
just test # run pytest inside docker (requires postgres)
just # install lint build test (full pipeline)To run tests locally without Docker, set DB_DSN and run:
uv run pytest
uv run pytest tests/test_retry.py::test_postgres_retry # single testThe CI DB_DSN format: postgresql+asyncpg://postgres:postgres@localhost:5432/postgres
The package (db_retry/) exposes five public symbols via __init__.py:
-
postgres_retry(retry.py) — async tenacity decorator that retries onasyncpg.SerializationError(40001) andasyncpg.PostgresConnectionError(08000/08003). Walks the exception chain viaDBAPIError.orig.__cause__to distinguish retriable errors from others likeStatementCompletionUnknownError(40002). Supports bare@postgres_retry(uses default) and@postgres_retry(retries=N)for per-callsite override. -
build_connection_factory(connections.py) — returns an async callable suitable for SQLAlchemy'sasync_engine_from_config. Handles multi-host DSNs by randomizing host order (load balancing) and attempting all hosts on timeout before raisingTargetServerAttributeNotMatched. -
build_db_dsn/is_dsn_multihost(dsn.py) — parse and constructsqlalchemy.URLobjects. Multi-host DSNs encode additional hosts in query parameters. Existingtarget_session_attrsin the DSN is preserved (not overwritten). -
Transaction(transaction.py) — frozen dataclass context manager wrappingAsyncSession. Supports optional isolation level (e.g.,"SERIALIZABLE"). Auto-rolls back on__aexit__if the session is still in a transaction (i.e. no explicit.commit()or.rollback()was called). Usestyping.Self(notyping_extensionsdependency). -
settings.py— exposesget_retries_number()which readsDB_RETRY_RETRIES_NUMBERenv var at call time (default: 3), allowingmonkeypatch.setenvto work in tests.
Ruff is configured with select = ["ALL"] plus specific exclusions. Line length is 120. Run just lint before committing.
Type checking uses ty (not mypy). In code, use ty: ignore for suppression comments (not type: ignore).