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
18 changes: 9 additions & 9 deletions .github/PULL_REQUEST_TEMPLATE
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
<!-- What is this pull request for? Does it fix any issues? -->

## Checklist
- [ ] If code changes were made, then they have been tested.
- [ ] I have updated the documentation to reflect the changes.
- [ ] I have thought about how this code may affect other services.
- [ ] This PR fixes an issue.
- [ ] This PR add/remove/change unit tests.
- [ ] This PR adds something new (e.g. new method or parameters).
- [ ] This PR is a breaking change (e.g. methods or parameters removed/renamed)
- [ ] This PR is **not** a code change (e.g. documentation, README, ...)
- [ ] If code changes were made, then they have been tested
- [ ] I have updated the documentation to reflect any changes made
- [ ] I have thought about how this code may affect other services
- [ ] This PR fixes an issue
- [ ] This PR is a breaking change (e.g. method, parameters, env variables)
- [ ] This PR adds something new (e.g. method, parameters, env variables)
- [ ] This PR change unit and integration tests
- [ ] This PR is **NOT** a code change (e.g. documentation, packages)

## Reviewer
- [ ] I understand that approving this code, I am also responsible for it going into the codebase.
- [ ] I understand that approving this code, I am also responsible for it going into the codebase
69 changes: 44 additions & 25 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
name: CI/CD Pipeline

'on':
pull_request:
push:
branches: ['**']
tags: ['v*']

env:
LATEST_PYTHON_VERSION: '3.14'

jobs:
lint:
name: Lint (ruff)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/ruff-action@v3
with:
args: "check"

test:
name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.runs-on || matrix.os }}
runs-on: ${{ matrix.os }}
needs: lint
strategy:
fail-fast: false
matrix:
os: ['ubuntu-latest', 'macos-latest', 'macos-14-arm64', 'windows-latest']
python-version: ['3.12', '3.13', '3.14']
include:
- os: 'macos-14-arm64'
runs-on: 'macos-14'
os: ['ubuntu-latest', 'macos-latest', 'windows-latest']
python-version: ['3.10', '3.14']
steps:
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true

- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
Expand All @@ -40,12 +51,12 @@ jobs:
uv venv
if [[ '${{ matrix.os }}' == 'windows-latest' ]]; then
# Skip MySQL on Windows - mysqlclient requires C compilation with MySQL headers
uv pip install -e .[mongodb,mssql,oracle,pgsql,test]
elif [[ '${{ matrix.os }}' == macos* ]]; then
uv sync --locked --all-extras --dev --no-install-package mysqlclient --no-install-package aiomysql
elif [[ '${{ matrix.os }}' == 'macos-latest' ]]; then
export PKG_CONFIG_PATH="$(brew --prefix mysql-client)/lib/pkgconfig"
uv pip install -e .[all,test]
uv sync --locked --all-extras --dev
else
uv pip install -e .[all,test]
uv sync --locked --all-extras --dev
fi
shell: bash

Expand All @@ -54,37 +65,39 @@ jobs:
with:
timeout_minutes: 2
max_attempts: 3
command: uv run pytest tests/unit
command: uv run --no-sync pytest tests/unit
shell: bash

- name: Upload coverage to Codecov
if: matrix.python-version == '3.14' && matrix.os == 'ubuntu-latest'
if: matrix.python-version == env.LATEST_PYTHON_VERSION && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v5

- name: Upload test results to Codecov
if: matrix.python-version == '3.14' && matrix.os == 'ubuntu-latest'
if: matrix.python-version == env.LATEST_PYTHON_VERSION && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v5
with:
report_type: test_results


integration-test:
name: Integration Tests
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v6

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true

- name: Set up Python 3.14
run: uv python install 3.14
- name: Set up Python ${{ env.LATEST_PYTHON_VERSION }}
run: uv python install ${{ env.LATEST_PYTHON_VERSION }}

- name: Install MySQL client libraries
run: sudo apt-get update && sudo apt-get install -y default-libmysqlclient-dev pkg-config

- name: Install dependencies
run: uv sync --all-extras
run: uv sync --locked --all-extras --dev
shell: bash

- name: Install ODBC driver for MSSQL
Expand All @@ -102,11 +115,11 @@ jobs:
with:
timeout_minutes: 3
max_attempts: 3
command: uv run pytest tests/integration --no-cov
command: uv run --no-sync pytest tests/integration --no-cov
shell: bash

build:
name: Build package
name: Build Package
runs-on: ubuntu-latest
needs: [test, integration-test]
if: startsWith(github.ref, 'refs/tags/v')
Expand All @@ -115,25 +128,32 @@ jobs:

- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true

- name: Set up Python 3.14
run: uv python install 3.14
- name: Set up Python ${{ env.LATEST_PYTHON_VERSION }}
run: uv python install ${{ env.LATEST_PYTHON_VERSION }}

- name: Build package
run: uv build

- name: Smoke test (wheel)
run: uv run --isolated --no-project --with dist/*.whl tests/smoke_test.py

- name: Smoke test (sdist)
run: uv run --isolated --no-project --with dist/*.tar.gz tests/smoke_test.py

- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: python-packages
path: dist/
retention-days: 7


release:
name: Create Release
runs-on: ubuntu-latest
needs: [build]
needs: build
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
Expand All @@ -154,7 +174,6 @@ jobs:
prerelease: false
files: release-assets/*


publish:
name: Publish to PyPI
runs-on: ubuntu-latest
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ cython_debug/
# PyCharm
.idea/

# ds store
**/.DS_Store

# Custom
/junit.xml
*.prof
junit.xml
5 changes: 5 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.10.0
hooks:
- id: uv-lock
99 changes: 80 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
<a href="https://www.paypal.com/ncp/payment/6G9Z78QHUD4RJ"><img src="https://img.shields.io/badge/Donate-PayPal-brightgreen.svg?style=plastic" alt="Donate"/></a>
<a href="https://github.com/sponsors/ddc"><img src="https://img.shields.io/static/v1?style=plastic&label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=ff69b4" alt="Sponsor"/></a>
<br>
<a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=plastic" alt="Code style: black"/></a>
<a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json?style=plastic" alt="uv"/></a>
<a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json?style=plastic" alt="Ruff"/></a>
<a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=plastic" alt="Code style: black"/></a>
<br>
<a href="https://www.python.org/downloads"><img src="https://img.shields.io/pypi/pyversions/ddcDatabases.svg?style=plastic&logo=python&cacheSeconds=3600" alt="Python"/></a>
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=plastic" alt="License: MIT"/></a>
Expand Down Expand Up @@ -49,7 +49,9 @@
- [Available Methods](#available-methods)
- [Logging](#logging)
- [Development](#development)
- [Create DEV Environment, Running Tests and Building Wheel](#create-dev-environment-running-tests-and-building-wheel)
- [Create DEV Environment and Running Tests](#create-dev-environment-and-running-tests)
- [Update DEV Environment Packages](#update-dev-environment-packages)
- [Building Wheel](#building-wheel)
- [Optionals](#optionals)
- [License](#license)
- [Support](#support)
Expand Down Expand Up @@ -91,8 +93,8 @@ Database classes use structured configuration dataclasses instead of flat keywor
|------------------------------|---------------------------------|-------------------------------------------------------------------------------------|
| `{DB}PoolConfig` | Connection pool settings | `pool_size`, `max_overflow`, `pool_recycle`, `connection_timeout` |
| `{DB}SessionConfig` | SQLAlchemy session settings | `echo`, `autoflush`, `expire_on_commit`, `autocommit` |
| `{DB}ConnRetryConfig` | Connection-level retry settings | `enable_retry`, `max_retries`, `initial_retry_delay`, `max_retry_delay` |
| `{DB}OpRetryConfig` | Operation-level retry settings | `enable_retry`, `max_retries`, `initial_retry_delay`, `max_retry_delay`, `jitter` |
| `{DB}ConnectionRetryConfig` | Connection-level retry settings | `enable_retry`, `max_retries`, `initial_retry_delay`, `max_retry_delay` |
| `{DB}OperationRetryConfig` | Operation-level retry settings | `enable_retry`, `max_retries`, `initial_retry_delay`, `max_retry_delay`, `jitter` |
| `PersistentConnectionConfig` | Persistent connection settings | `idle_timeout`, `health_check_interval`, `auto_reconnect` |

**Note:** Replace `{DB}` with the database prefix: `PostgreSQL`, `MySQL`, `MSSQL`, `Oracle`, `MongoDB`, or `Sqlite`.
Expand Down Expand Up @@ -120,10 +122,10 @@ Retry with exponential backoff is enabled by default at two levels:

**1. Connection Level** - Retries when establishing database connections:
```python
from ddcDatabases import PostgreSQL, PostgreSQLConnRetryConfig
from ddcDatabases import PostgreSQL, PostgreSQLConnectionRetryConfig

with PostgreSQL(
conn_retry_config=PostgreSQLConnRetryConfig(
connection_retry_config=PostgreSQLConnectionRetryConfig(
enable_retry=True, # Enable/disable retry (default: True)
max_retries=3, # Maximum retry attempts (default: 3)
initial_retry_delay=1.0, # Initial delay in seconds (default: 1.0)
Expand All @@ -136,10 +138,10 @@ with PostgreSQL(

**2. Operation Level** - Retries individual database operations (fetchall, insert, etc.):
```python
from ddcDatabases import DBUtils, PostgreSQL, PostgreSQLOpRetryConfig
from ddcDatabases import DBUtils, PostgreSQL, PostgreSQLOperationRetryConfig

with PostgreSQL(
op_retry_config=PostgreSQLOpRetryConfig(
operation_retry_config=PostgreSQLOperationRetryConfig(
enable_retry=True, # Enable/disable (default: True)
max_retries=3, # Max attempts (default: 3)
initial_retry_delay=1.0, # Initial delay in seconds (default: 1.0)
Expand Down Expand Up @@ -174,6 +176,8 @@ from ddcDatabases import (
MySQLPersistent,
MongoDBPersistent,
PersistentConnectionConfig,
PostgreSQLConnectionRetryConfig,
PostgreSQLOperationRetryConfig,
close_all_persistent_connections,
)

Expand All @@ -188,6 +192,19 @@ conn = PostgreSQLPersistent(
health_check_interval=30, # seconds between health checks (default: 30)
auto_reconnect=True, # auto-reconnect on failure (default: True)
),
connection_retry_config=PostgreSQLConnectionRetryConfig(
enable_retry=True, # enable connection retry (default: True)
max_retries=5, # max connection attempts (default: 5)
initial_retry_delay=1.0, # initial delay in seconds (default: 1.0)
max_retry_delay=30.0, # max delay in seconds (default: 30.0)
),
operation_retry_config=PostgreSQLOperationRetryConfig(
enable_retry=True, # enable operation retry (default: True)
max_retries=3, # max operation attempts (default: 3)
initial_retry_delay=0.5, # initial delay in seconds (default: 0.5)
max_retry_delay=10.0, # max delay in seconds (default: 10.0)
jitter=0.1, # randomization factor (default: 0.1)
),
)

# Use as context manager (doesn't disconnect on exit, just updates last-used time)
Expand All @@ -208,6 +225,36 @@ async with conn as session:
close_all_persistent_connections()
```

### Execute with Retry

The `execute_with_retry` method provides automatic session management with retry logic:

**Synchronous:**
```python
from ddcDatabases import PostgreSQLPersistent

db = PostgreSQLPersistent(logger=logger)
result = db.execute_with_retry(
lambda session: MyDal(session).do_something()
)
```

**Asynchronous:**
```python
from ddcDatabases import PostgreSQLPersistent

db = PostgreSQLPersistent(async_mode=True, logger=logger)
result = await db.execute_with_retry(
lambda session: MyDal(session).do_something()
)
```

The method automatically:
- Connects (or reuses existing connection)
- Executes the operation with the session
- Commits on success, rolls back on failure
- Retries with exponential backoff if `auto_reconnect` is enabled

**Available Persistent Connection Classes:**

- `PostgreSQLPersistent` - PostgreSQL (sync/async)
Expand Down Expand Up @@ -605,7 +652,7 @@ async with PostgreSQL() as session:
results = await db_utils_async.fetchall(stmt)
```

**Note:** Retry logic is configured at the database connection level using `op_retry_config` (see [Retry Logic](#retry-logic) section).
**Note:** Retry logic is configured at the database connection level using `operation_retry_config` (see [Retry Logic](#retry-logic) section).


# Logging
Expand Down Expand Up @@ -638,27 +685,41 @@ logging.getLogger("ddcDatabases").addHandler(logging.StreamHandler())

# Development

Must have [UV](https://uv.run/docs/getting-started/installation),
[Black](https://black.readthedocs.io/en/stable/getting_started.html),
[Ruff](https://docs.astral.sh/ruff/installation/), and
[Poe the Poet](https://poethepoet.naber.dev/installation) installed.
Must have [UV](https://uv.run/docs/getting-started/installation) installed.

## Create DEV Environment and Running Tests

## Create DEV Environment, Running Tests and Building Wheel
> **Note:** All poe tasks automatically run ruff linter along with Black formatting

```shell
uv sync --all-extras
poe linter
uv sync --all-extras --all-groups
poe test
poe test-integration
```

## Update DEV Environment Packages
This will update all packages dependencies

```shell
poe updatedev
```


## Building Wheel
This will update all packages, run linter, both unit and integration tests and finally build the wheel

```shell
poe build
```

## Optionals

```shell
poe profile (create a cprofile_unit.prof file from unit tests)
poe profile-integration (create a cprofile_integration.prof file from integration tests)
```
# create a cprofile_unit.prof file from unit tests
poe profile
# create a cprofile_integration.prof file from integration tests
poe profile-integration
`````


# License
Expand Down
Loading
Loading