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
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ PyMongoSQL implements the DB API 2.0 interfaces to provide SQL-like access to Mo
- **JMESPath** (JSON/Dict Path Query)
- jmespath >= 1.0.0

- **Tenacity** (Transient Failure Retry)
- tenacity >= 9.0.0

### Optional Dependencies

- **Tenacity** (Transient Failure Retry)
- tenacity >= 9.0.0
- Install with: `pip install pymongosql[retry]`

- **SQLAlchemy** (for ORM/Core support)
- sqlalchemy >= 1.4.0 (SQLAlchemy 1.4+ and 2.0+ supported)
- Install with: `pip install pymongosql[sqlalchemy]`

## Installation

Expand Down Expand Up @@ -212,12 +214,12 @@ Parameters are substituted into the MongoDB filter during execution, providing p

### Retry on Transient System Errors

PyMongoSQL supports retrying transient, system-level MongoDB failures (for example connection timeout and reconnect errors) using Tenacity.
PyMongoSQL supports retrying transient, system-level MongoDB failures (for example connection timeout and reconnect errors) using [Tenacity](https://github.com/jd/tenacity). This feature requires the optional `tenacity` package — install it with `pip install pymongosql[retry]`. If retry is enabled but tenacity is not installed, operations will proceed without retry.

```python
connection = connect(
host="mongodb://localhost:27017/database",
retry_enabled=True, # default: True
retry_enabled=False, # default: False
retry_attempts=3, # default: 3
retry_wait_min=0.1, # default: 0.1 seconds
retry_wait_max=1.0, # default: 1.0 seconds
Expand Down
2 changes: 1 addition & 1 deletion pymongosql/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
if TYPE_CHECKING:
from .connection import Connection

__version__: str = "0.4.8"
__version__: str = "0.5.0"

# Globals https://www.python.org/dev/peps/pep-0249/#globals
apilevel: str = "2.0"
Expand Down
19 changes: 16 additions & 3 deletions pymongosql/retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
from typing import Any, Callable, Optional, Tuple, TypeVar

from pymongo.errors import AutoReconnect, ConnectionFailure, NetworkTimeout, PyMongoError, ServerSelectionTimeoutError
from tenacity import Retrying, retry_if_exception_type, stop_after_attempt, wait_exponential

try:
from tenacity import Retrying, retry_if_exception_type, stop_after_attempt, wait_exponential

_has_tenacity = True
except ImportError:
_has_tenacity = False

_logger = logging.getLogger(__name__)
_T = TypeVar("_T")
Expand All @@ -19,14 +25,14 @@

@dataclass(frozen=True)
class RetryConfig:
enabled: bool = True
enabled: bool = False
attempts: int = 3
wait_min: float = 0.1
wait_max: float = 1.0

@classmethod
def from_kwargs(cls, kwargs: dict) -> "RetryConfig":
enabled = bool(kwargs.pop("retry_enabled", True))
enabled = bool(kwargs.pop("retry_enabled", False))
attempts = int(kwargs.pop("retry_attempts", 3))
wait_min = float(kwargs.pop("retry_wait_min", 0.1))
wait_max = float(kwargs.pop("retry_wait_max", 1.0))
Expand Down Expand Up @@ -57,6 +63,13 @@ def execute_with_retry(
if not config.enabled or config.attempts <= 1:
return operation()

if not _has_tenacity:
_logger.warning(
"Retry is enabled but 'tenacity' package is not installed. "
"Falling back to no-retry. Install it with: pip install pymongosql[retry]"
)
return operation()

def _before_sleep(retry_state: Any) -> None:
error = retry_state.outcome.exception() if retry_state.outcome else None
_logger.warning(
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ dependencies = [
"pymongo>=4.15.0",
"antlr4-python3-runtime>=4.13.0",
"jmespath>=1.0.0",
"tenacity>=9.0.0",
]

[project.optional-dependencies]
retry = ["tenacity>=9.0.0"]
sqlalchemy = ["sqlalchemy>=1.4.0"]
dev = [
"pytest>=7.0.0",
Expand Down
3 changes: 3 additions & 0 deletions requirements-optional.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# Retry support (optional)
tenacity>=9.0.0

# SQLAlchemy support (optional) - supports 1.4+ and 2.x
sqlalchemy>=1.4.0,<3.0.0
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
antlr4-python3-runtime>=4.13.0
pymongo>=4.15.0
jmespath>=1.0.0
tenacity>=9.0.0
jmespath>=1.0.0
42 changes: 42 additions & 0 deletions tests/test_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,45 @@ def command(self, command_payload):
assert row is not None
assert row[0] == "retry-user"
assert state["calls"] == 3


def test_execute_with_retry_works_without_tenacity(monkeypatch):
"""When tenacity is not installed, retry-enabled calls fall back to no-retry."""
monkeypatch.setattr("pymongosql.retry._has_tenacity", False)

state = {"calls": 0}

def operation():
state["calls"] += 1
return "ok"

result = execute_with_retry(
operation,
RetryConfig(enabled=True, attempts=3, wait_min=0.0, wait_max=0.0),
"no-tenacity fallback",
)

assert result == "ok"
assert state["calls"] == 1 # no retry, just one direct call


def test_retry_module_imports_without_tenacity(monkeypatch):
"""The retry module can be imported even if tenacity is absent."""
import importlib
import sys

monkeypatch.setitem(sys.modules, "tenacity", None) # block tenacity import

import pymongosql.retry

importlib.reload(pymongosql.retry)

assert pymongosql.retry._has_tenacity is False

# Still usable with retry disabled
result = pymongosql.retry.execute_with_retry(
lambda: 42,
pymongosql.retry.RetryConfig(enabled=False),
"import test",
)
assert result == 42
Loading