## 2026-03-30 - US-013
- What was implemented
  - Updated `src/db_connector/__init__.py` to export all public symbols: `AsyncClient`, `SyncClient`, all 6 error classes, `ConnectionConfig`, `connection_config_from_env`, `connection_config_from_mapping`, `generate_iam_token`, `raise_mapped_connector_error`
  - Defined `__all__` for a clean, explicit public API surface
  - Created `README.md` documenting: install, quick-start (sync + async), env var config table, IAM auth, connection pooling (sync + async), transactions (sync + async), error hierarchy with catch examples, dev setup commands
- Files changed
  - `src/db_connector/__init__.py` (updated), `README.md` (new), `scripts/ralph/prd.json` (US-013 passes=true)
- **Learnings for future iterations:**
  - `__init__.py` imports from submodules trigger mypy checks on the whole package — ensure all submodules are already typecheck-clean before wiring them up
  - `raise_mapped_connector_error` is worth exporting as public API — advanced callers may want to use the same mapping logic in their own exception handlers
---
## 2026-03-30 - US-012
- What was implemented
  - Created `tests/test_async_pool.py` (TDD first) with 7 tests: pool created with correct min/max_size, getconn called, putconn+close called on close(), references cleared, no-op close without connect, pool=False skips AsyncConnectionPool, conn kwargs forwarded
  - Added `import psycopg_pool` to `src/db_connector/async_client.py`
  - Added `_pool_obj: psycopg_pool.AsyncConnectionPool[psycopg.AsyncConnection[Any]] | None = None` field
  - Split `connect()` to create `AsyncConnectionPool` + call `await getconn()` when `pool=True`
  - Split `close()` to call `await putconn()` + `await pool.close()` when `pool=True`
- Files changed
  - `src/db_connector/async_client.py` (updated), `tests/test_async_pool.py` (new), `scripts/ralph/prd.json` (US-012 passes=true)
- **Learnings for future iterations:**
  - `psycopg_pool.AsyncConnectionPool` mirrors `ConnectionPool` API but all methods are async: `await pool.getconn()`, `await pool.putconn(conn)`, `await pool.close()`
  - Mock async pool methods with `AsyncMock` (e.g. `mock_pool.getconn = AsyncMock(return_value=mock_conn)`)
  - `_pool_obj` type is `psycopg_pool.AsyncConnectionPool[psycopg.AsyncConnection[Any]] | None`
---
## 2026-03-30 - US-011
- What was implemented
  - Added 4 tests to `tests/test_async_client.py` (TDD first): commit on clean exit, rollback+reraise on exception, nested transaction raises ConnectorError, flag reset after exit
  - Added `_in_transaction: bool = False` field to `AsyncClient.__init__`
  - Added `transaction()` async context manager using `@asynccontextmanager` + `AsyncGenerator[None, None]`
  - Imported `AsyncGenerator` from `collections.abc`, `asynccontextmanager` from `contextlib`, `ConnectorError` from `db_connector.errors`
- Files changed
  - `src/db_connector/async_client.py` (updated), `tests/test_async_client.py` (updated), `scripts/ralph/prd.json` (US-011 passes=true)
- **Learnings for future iterations:**
  - Use `@asynccontextmanager` + `AsyncGenerator[None, None]` return type for async context managers in this codebase (mirrors sync `@contextmanager` + `Generator[None, None, None]`)
  - `make_mock_conn()` helper in test_async_client.py already has `commit = AsyncMock()` and `rollback = AsyncMock()` — no changes needed to the helper
---
## 2026-03-30 - US-010
- What was implemented
  - Created `tests/test_async_client.py` (TDD first) with 16 tests: password auth, IAM auth, token-as-password, sslmode passthrough, connect error mapping, close(), close-without-connect, execute, fetchall, fetchone, fetchone-returns-None, fetchmany, fetchmany-default-size, execute-maps-exception, async context manager connects/closes, async context manager closes on exception
  - Created `src/db_connector/async_client.py` with `AsyncClient`: `__init__`, `async connect()`, `async close()`, `async execute()`, `async fetchall()`, `async fetchone()`, `async fetchmany()`, `__aenter__`/`__aexit__`
  - Uses `psycopg.AsyncConnection.connect(**kwargs)` for connection; `async with conn.cursor() as cur:` for queries
  - All query methods use parameterised queries; exceptions routed through `raise_mapped_connector_error`
- Files changed
  - `src/db_connector/async_client.py` (new), `tests/test_async_client.py` (new), `scripts/ralph/prd.json` (US-010 passes=true)
- **Learnings for future iterations:**
  - Mock `psycopg.AsyncConnection.connect` with `new_callable=AsyncMock` (it's an async classmethod)
  - For async cursor context manager mock: create a `MagicMock` with `__aenter__ = AsyncMock(return_value=mock_cursor)` and `__aexit__ = AsyncMock(return_value=False)`
  - `AsyncClient._connection` typed as `psycopg.AsyncConnection[Any] | None` — same `[Any]` generic pattern as sync
  - `ruff check --fix` handles import sort (I001) automatically
---
## 2026-03-30 - US-009
- What was implemented
  - Created `tests/test_sync_pool.py` (TDD first) with 7 tests: pool created with correct min/max_size, getconn called, putconn+close called on close(), references cleared, no-op close without connect, pool=False skips ConnectionPool, conn kwargs forwarded
  - Updated `src/db_connector/sync_client.py`: imported `psycopg_pool`, added `_pool_obj` field, split `connect()` to create `ConnectionPool` + call `getconn()` when `pool=True`, split `close()` to call `putconn()`+`pool.close()` when `pool=True`
  - Changed `_connection` type annotation from `psycopg.Connection | None` to `psycopg.Connection[Any] | None` for mypy compatibility
- Files changed
  - `tests/test_sync_pool.py` (new), `src/db_connector/sync_client.py` (updated), `scripts/ralph/prd.json` (US-009 passes=true)
- **Learnings for future iterations:**
  - `psycopg_pool.ConnectionPool` takes `conninfo=""` + `kwargs=dict` pattern for keyword-based connection params
  - `_pool_obj` type is `psycopg_pool.ConnectionPool[psycopg.Connection[Any]] | None`
  - `getconn()` returns `psycopg.Connection[Any]`; compatible with `_connection` field after updating its type annotation
---
## 2026-03-30 - US-007
- What was implemented
  - Added 7 tests to `tests/test_sync_client.py` (TDD first): execute, fetchall, fetchone, fetchone-returns-None, fetchmany, fetchmany-default-size, and execute-maps-exception
  - Added `execute()`, `fetchall()`, `fetchone()`, `fetchmany()` methods to `SyncClient` in `src/db_connector/sync_client.py`
  - All methods use parameterised queries via `cursor.execute(query, params)`
  - Exceptions during execution are routed through `raise_mapped_connector_error`
  - Swapped `from typing import Sequence` → `from collections.abc import Sequence` (ruff UP035)
- Files changed
  - `src/db_connector/sync_client.py` (updated), `tests/test_sync_client.py` (updated), `scripts/ralph/prd.json` (US-007 passes=true)
- **Learnings for future iterations:**
  - Use `from collections.abc import Sequence` not `from typing import Sequence` — ruff UP035 flags the latter
  - `cursor()` is used as a context manager with `with self._connection.cursor() as cur:` — need `# type: ignore[union-attr]` since `_connection` can be `None` at type-check time
  - `mypy` return-type inference handles `NoReturn` from `raise_mapped_connector_error` correctly in the except branch, so `fetchall`/`fetchone`/`fetchmany` don't need explicit `return` after the except
---
## 2026-03-30 - US-008
- What was implemented
  - Added 4 tests to `tests/test_sync_client.py` (TDD first): commit on clean exit, rollback + reraise on exception, nested transaction raises ConnectorError, flag reset after exit
  - Added `transaction()` context manager to `SyncClient` using `@contextmanager` decorator
  - Added `_in_transaction: bool = False` state flag to `__init__`
  - Imports added: `Generator` from `collections.abc`, `contextmanager` from `contextlib`, `ConnectorError` from `db_connector.errors`
- Files changed
  - `src/db_connector/sync_client.py` (updated), `tests/test_sync_client.py` (updated), `scripts/ralph/prd.json` (US-008 passes=true)
- **Learnings for future iterations:**
  - `@contextmanager` + `Generator[None, None, None]` return type is the clean pattern for sync context managers in this codebase
  - The nested transaction check must happen before setting `_in_transaction = True` (raise first, then set flag)
  - `ConnectorError` is raised directly (not via `raise_mapped_connector_error`) for application-layer guard conditions like nested transactions
---
## Codebase Patterns
- Package lives under `src/db_connector/`; install with `pip install -e ".[dev]"` from repo root.
- Virtual env: `python-db-connector-env/` at repo root; activate with `source python-db-connector-env/bin/activate`.
- Quality gate: `ruff check src/ tests/`, `ruff format --check src/ tests/`, `mypy src/`, `pytest`.
- Build backend: `setuptools.build_meta` (NOT `setuptools.backends.legacy:build` — that doesn't exist in this env).
- boto3/botocore lack type stubs; suppress mypy errors via `[[tool.mypy.overrides]]` with `ignore_missing_imports = true` in pyproject.toml (NOT inline `# type: ignore` — mypy with overrides active will flag those as unused).
- pytest asyncio_mode = "auto" configured in pyproject.toml.
- Ruff `known-first-party` includes `db_connector`.
- `socket.timeout` is aliased to `TimeoutError` in Python 3.3+; ruff UP041 will auto-replace `socket.timeout` with `TimeoutError` — use `TimeoutError` in implementation checks too.
- `.gitignore` has a blanket `config.py` rule; add `!src/db_connector/config.py` negation to allow library modules named config.py to be committed.
- Use raw strings (`r"..."`) for regex patterns in `pytest.raises(match=...)` to satisfy ruff RUF043.

# Ralph Progress Log
Started: Mon Mar 30 18:13:14 -03 2026
---
## 2026-03-30 - US-001
- What was implemented
  - Added `[project]` table to pyproject.toml: name `python-db-connector`, version `0.1.0`, Python `>=3.10`, runtime deps boto3, psycopg[binary], psycopg_pool
  - Added `[project.optional-dependencies]` dev extras: pytest, pytest-asyncio, ruff, mypy, pre-commit
  - Added `[build-system]` using setuptools>=68.0 and setuptools-scm with `setuptools.build_meta`
  - Added `[tool.mypy]` with strict mode and src layout config; `[tool.pytest.ini_options]` asyncio_mode=auto
  - Created `src/db_connector/__init__.py` (empty), `src/db_connector/py.typed`, `tests/__init__.py`, `tests/conftest.py` (placeholder comment)
  - Created `scripts/ralph/prd.json` with all 13 user stories
- Files changed
  - `pyproject.toml`, `src/db_connector/__init__.py`, `src/db_connector/py.typed`, `tests/__init__.py`, `tests/conftest.py`, `scripts/ralph/prd.json`
- **Learnings for future iterations:**
  - `setuptools.backends.legacy:build` does NOT work in this env; use `setuptools.build_meta`
  - The virtualenv is `python-db-connector-env/` at repo root
  - All quality checks pass with empty src and no tests (pytest exit 5 = no tests collected, which is OK)
---
## 2026-03-30 - US-002
- What was implemented
  - Created `tests/test_errors.py` (TDD first) with 17 tests covering inheritance, message, cause, and catch-all behaviour
  - Created `src/db_connector/errors.py` with: `ConnectorError` (base, stores optional `cause`), `AuthenticationError`, `IAMAuthenticationError`, `DatabaseAuthenticationError`, `ConnectorTimeoutError`, `ConnectorConnectionError`
  - Each class accepts `message: str` and keyword-only `cause: Exception | None = None`; `__str__` returns `str(self.args[0])`
- Files changed
  - `src/db_connector/errors.py` (new), `tests/test_errors.py` (new), `scripts/ralph/prd.json` (US-002 passes=true)
- **Learnings for future iterations:**
  - mypy flags `self.args[0]` as `Any` in `__str__`; wrap with `str(...)` to satisfy `no-any-return`
  - Use `from __future__ import annotations` so `Exception | None` union syntax works for Python <3.10 forward-refs (even though runtime is 3.14)
---
## 2026-03-30 - US-003
- What was implemented
  - Created `tests/test_error_mapping.py` (TDD first) with 9 tests covering all mapped exception types and NoReturn guarantee
  - Created `src/db_connector/error_mapping.py` with `raise_mapped_connector_error(exc: Exception) -> NoReturn`
  - Maps: `socket.timeout`/`TimeoutError` → `ConnectorTimeoutError`; `psycopg.OperationalError`, `socket.gaierror`, `ConnectionRefusedError`, `OSError`, and all others → `ConnectorConnectionError`
  - All mapped errors store original exception as `.cause`
- Files changed
  - `src/db_connector/error_mapping.py` (new), `tests/test_error_mapping.py` (new), `scripts/ralph/prd.json` (US-003 passes=true)
- **Learnings for future iterations:**
  - Ruff UP041 replaces `socket.timeout` with `TimeoutError` (they are the same type in Python 3.3+); write implementation using `TimeoutError` to avoid lint churn
  - `isinstance(exc, OSError)` catches `socket.gaierror`, `ConnectionRefusedError`, and `TimeoutError` since all are OSError subclasses — but order matters: check `TimeoutError` first to route it to `ConnectorTimeoutError` before the broader OSError branch
---
## 2026-03-30 - US-004
- What was implemented
  - Created `tests/test_config.py` (TDD first) with 18 tests covering ConnectionConfig defaults, env-var loading (password and IAM paths), port defaulting, missing var errors, lowercase normalization, and mapping-based construction
  - Created `src/db_connector/config.py` with `ConnectionConfig` dataclass and `connection_config_from_env()` / `connection_config_from_mapping()` functions
  - Created `.env.example` with all variable placeholders for treasury/research/sandbox in dev/prod
  - Added `!src/db_connector/config.py` negation to `.gitignore` (blanket `config.py` rule was blocking the file)
- Files changed
  - `src/db_connector/config.py` (new), `tests/test_config.py` (new), `.env.example` (new), `.gitignore` (updated), `scripts/ralph/prd.json` (US-004 passes=true)
- **Learnings for future iterations:**
  - `.gitignore` has a blanket `config.py` rule for sensitive files; must add `!src/db_connector/config.py` negation
  - Use raw strings (`r"..."`) in `pytest.raises(match=...)` to satisfy ruff RUF043
  - `connection_config_from_mapping` uses `username` key (not `user`) per PRD; maps it to `ConnectionConfig.user`
---
## 2026-03-30 - US-005
- What was implemented
  - Created `tests/test_iam_auth.py` (TDD first) with 7 tests: correct boto3 params, return type, wrapping BotoCoreError/ClientError/NoCredentialsError, error message content, and missing-region guard
  - Created `src/db_connector/iam_auth.py` with `generate_iam_token(config: ConnectionConfig) -> str`
  - Raises `IAMAuthenticationError` when `config.region` is None
  - Wraps all botocore errors (`BotoCoreError`, `ClientError`, `NoCredentialsError`) in `IAMAuthenticationError` with the original as `.cause`
  - Added `[[tool.mypy.overrides]]` in `pyproject.toml` for boto3/botocore to suppress `import-untyped` errors
- Files changed
  - `src/db_connector/iam_auth.py` (new), `tests/test_iam_auth.py` (new), `pyproject.toml` (mypy overrides added), `scripts/ralph/prd.json` (US-005 passes=true)
- **Learnings for future iterations:**
  - boto3/botocore have no type stubs; use `[[tool.mypy.overrides]]` with `ignore_missing_imports = true` in pyproject.toml — do NOT use inline `# type: ignore[import-untyped]` because the overrides will suppress the underlying error making the inline ignore "unused" and triggering another mypy error
  - `botocore.exceptions.NoCredentialsError` is a subclass of `BotoCoreError`; catching both is fine but order doesn't matter since they both map to the same error class here
---
## 2026-03-30 - US-006
- What was implemented
  - Created `tests/test_sync_client.py` (TDD first) with 9 tests covering password auth, IAM auth, sslmode passthrough, connection error mapping, close(), and context manager protocol
  - Created `src/db_connector/sync_client.py` with `SyncClient` class: `__init__`, `connect()`, `close()`, `__enter__`/`__exit__`
  - Password auth uses `config.password` directly; IAM path calls `generate_iam_token()`
  - Connection errors are caught and routed through `raise_mapped_connector_error`
  - `close()` is a no-op when not connected (guards against calling close before connect)
- Files changed
  - `src/db_connector/sync_client.py` (new), `tests/test_sync_client.py` (new), `scripts/ralph/prd.json` (US-006 passes=true)
- **Learnings for future iterations:**
  - `psycopg.Connection` type annotation doesn't need `# type: ignore[type-arg]` — it's generic but mypy handles it fine without the comment (unused ignores cause mypy errors)
  - `from types import TracebackType` is needed for `__exit__` signature type annotations
---
