Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Project Overview

This is a Python library called `db-try` that provides PostgreSQL and SQLAlchemy utilities, specifically focusing on:
This is a Python library called `db-retry` that provides PostgreSQL and SQLAlchemy utilities, specifically focusing on:

1. **Retry decorators** for handling database connection issues and serialization errors
2. **Connection factory builders** for managing PostgreSQL connections with multiple hosts
Expand All @@ -25,7 +25,7 @@ The library is built with modern Python practices (3.13+) and uses type hints ex
## Project Structure

```
db_try/
db_retry/
├── __init__.py # Exports all public APIs
├── connections.py # Connection factory builders
├── dsn.py # DSN parsing and manipulation utilities
Expand Down Expand Up @@ -130,4 +130,4 @@ The library can be configured using environment variables:
The retry behavior is defined in `retry.py` and uses the tenacity library. Modify the `_retry_handler` function to change which exceptions trigger retries.

### Working with Connections
Connection handling is in `connections.py`. The `build_connection_factory` function handles connecting to PostgreSQL with support for multiple hosts and fallback mechanisms.
Connection handling is in `connections.py`. The `build_connection_factory` function handles connecting to PostgreSQL with support for multiple hosts and fallback mechanisms.
6 changes: 4 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ RUN apt update \
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
RUN useradd --no-create-home --gid root runner

ENV UV_PYTHON_PREFERENCE=only-system
ENV UV_NO_CACHE=true
ENV UV_PROJECT_ENVIRONMENT=/code/.venv \
UV_NO_MANAGED_PYTHON=1 \
UV_NO_CACHE=true \
UV_LINK_MODE=copy

WORKDIR /code

Expand Down
14 changes: 8 additions & 6 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@ install:
uv sync --all-extras --all-groups --frozen

lint:
uv run --frozen ruff format
uv run --frozen ruff check --fix
uv run --frozen mypy .
uv run eof-fixer .
uv run ruff format
uv run ruff check --fix
uv run mypy .

lint-ci:
uv run --frozen ruff format --check
uv run --frozen ruff check --no-fix
uv run --frozen mypy .
uv run eof-fixer . --check
uv run ruff format --check
uv run ruff check --no-fix
uv run mypy .

publish:
rm -rf dist
Expand Down
30 changes: 16 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# db-try
# db-retry

A Python library providing robust retry mechanisms, connection utilities, and transaction helpers for PostgreSQL and SQLAlchemy applications.

Expand All @@ -14,13 +14,13 @@ A Python library providing robust retry mechanisms, connection utilities, and tr
### Using uv

```bash
uv add db-try
uv add db-retry
```

### Using pip

```bash
pip install db-try
pip install db-retry
```

## ORM-Based Usage Examples
Expand All @@ -34,15 +34,17 @@ import asyncio
import sqlalchemy as sa
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from db_try import postgres_retry
from db_retry import postgres_retry


class User(DeclarativeBase):
__tablename__ = "users"

id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(sa.String())
email: Mapped[str] = mapped_column(sa.String(), index=True)


# Apply retry logic to ORM operations
@postgres_retry
async def get_user_by_email(session: AsyncSession, email: str) -> User:
Expand All @@ -59,6 +61,7 @@ async def main():
if user:
print(f"Found user: {user.name}")


asyncio.run(main())
```

Expand All @@ -70,8 +73,7 @@ Set up resilient database connections with multiple fallback hosts:
import sqlalchemy as sa
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from db_try import build_connection_factory, build_db_dsn

from db_retry import build_connection_factory, build_db_dsn

# Configure multiple database hosts for high availability
multi_host_dsn = (
Expand All @@ -91,7 +93,7 @@ dsn = build_db_dsn(

# Create connection factory with timeout
connection_factory = build_connection_factory(
url=dsn,
url=dsn,
timeout=5.0 # 5 second connection timeout
)

Expand All @@ -109,7 +111,7 @@ import datetime
import typing

from schemas import AnalyticsEventCreate, AnalyticsEvent
from db_try import Transaction, postgres_retry
from db_retry import Transaction, postgres_retry

from your_service_name.database.tables import EventsTable
from your_service_name.producers.analytics_service_events_producer import AnalyticsEventsProducer
Expand All @@ -125,8 +127,8 @@ class CreateEventUseCase:

@postgres_retry
async def __call__(
self,
event_create_data: AnalyticsEventCreate,
self,
event_create_data: AnalyticsEventCreate,
) -> AnalyticsEvent:
async with self.transaction:
model: typing.Final = EventsTable(
Expand All @@ -147,12 +149,12 @@ Use serializable isolation level to prevent race conditions with ORM:

```python
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from db_try import Transaction
from db_retry import Transaction


async def main():
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")

async with AsyncSession(engine) as session:
strict_transaction = Transaction(
session=session,
Expand All @@ -167,7 +169,7 @@ The library can be configured using environment variables:

| Variable | Description | Default |
|-------------------------|--------------------------------------------------|---------|
| `DB_TRY_RETRIES_NUMBER` | Number of retry attempts for database operations | 3 |
| `DB_RETRY_RETRIES_NUMBER` | Number of retry attempts for database operations | 3 |

Example:
```bash
Expand Down
13 changes: 13 additions & 0 deletions db_retry/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from db_retry.connections import build_connection_factory
from db_retry.dsn import build_db_dsn, is_dsn_multihost
from db_retry.retry import postgres_retry
from db_retry.transaction import Transaction


__all__ = [
"Transaction",
"build_connection_factory",
"build_db_dsn",
"is_dsn_multihost",
"postgres_retry",
]
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions db_try/retry.py → db_retry/retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import tenacity
from sqlalchemy.exc import DBAPIError

from db_try import settings
from db_retry import settings


logger = logging.getLogger(__name__)
Expand All @@ -29,7 +29,7 @@ def postgres_retry[**P, T](
func: typing.Callable[P, typing.Coroutine[None, None, T]],
) -> typing.Callable[P, typing.Coroutine[None, None, T]]:
@tenacity.retry(
stop=tenacity.stop_after_attempt(settings.DB_TRY_RETRIES_NUMBER),
stop=tenacity.stop_after_attempt(settings.DB_RETRY_RETRIES_NUMBER),
wait=tenacity.wait_exponential_jitter(),
retry=tenacity.retry_if_exception(_retry_handler),
reraise=True,
Expand Down
5 changes: 5 additions & 0 deletions db_retry/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import os
import typing


DB_RETRY_RETRIES_NUMBER: typing.Final = int(os.getenv("DB_RETRY_RETRIES_NUMBER", "3"))
File renamed without changes.
13 changes: 0 additions & 13 deletions db_try/__init__.py

This file was deleted.

5 changes: 0 additions & 5 deletions db_try/settings.py

This file was deleted.

3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ services:
dockerfile: ./Dockerfile
restart: always
volumes:
- .:/srv/www/
- .:/code
- /code/.venv
depends_on:
db:
condition: service_healthy
Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[project]
name = "db-try"
name = "db-retry"
description = "PostgreSQL and SQLAlchemy Tools"
authors = [
{ name = "community-of-python" },
Expand Down Expand Up @@ -35,17 +35,18 @@ dev = [
"pytest-asyncio",
]
lint = [
"asyncpg-stubs",
"ruff",
"mypy",
"eof-fixer",
"asyncpg-stubs",
]

[build-system]
requires = ["uv_build"]
build-backend = "uv_build"

[tool.uv.build-backend]
module-name = "db_try"
module-name = "db_retry"
module-root = ""

[tool.mypy]
Expand Down
2 changes: 1 addition & 1 deletion tests/test_connection_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import sqlalchemy
from sqlalchemy.ext import asyncio as sa_async

from db_try.connections import build_connection_factory
from db_retry.connections import build_connection_factory


async def test_connection_factory_success() -> None:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_dsn.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import typing

from db_try import build_db_dsn, is_dsn_multihost
from db_retry import build_db_dsn, is_dsn_multihost


def test_build_db_dsn() -> None:
Expand Down
2 changes: 1 addition & 1 deletion tests/test_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from sqlalchemy.exc import DBAPIError
from sqlalchemy.ext import asyncio as sa_async

from db_try.retry import postgres_retry
from db_retry.retry import postgres_retry


@pytest.mark.parametrize(
Expand Down
2 changes: 1 addition & 1 deletion tests/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest
from sqlalchemy.ext import asyncio as sa_async

from db_try import Transaction
from db_retry import Transaction


@pytest.fixture
Expand Down
Loading