Skip to content

Commit d1a38e8

Browse files
authored
v3.0.11 (#40)
* v3.0.11 * v3.0.11 * v3.0.11 * v3.0.11 * v3.0.11 * v3.0.11 * v3.0.11 * v3.0.11 * v3.0.11 * v3.0.11 --------- Co-authored-by: ddc <ddc@users.noreply.github.com>
1 parent 241d4e2 commit d1a38e8

15 files changed

Lines changed: 544 additions & 438 deletions

File tree

.github/PULL_REQUEST_TEMPLATE

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
11
## Summary
2+
<!-- Brief description of what this PR does and why -->
23

3-
<!-- What is this pull request for? Does it fix any issues? -->
4+
## Changes Made
5+
<!-- List the key changes -->
6+
7+
## Type of Change
8+
- [ ] Bug fix
9+
- [ ] New feature
10+
- [ ] Breaking change (existing functionality affected)
11+
- [ ] Refactoring (no functional changes)
12+
- [ ] Documentation
13+
- [ ] CI/CD or build configuration
14+
- [ ] Dependencies update
15+
16+
## Testing
17+
- [ ] Unit tests added/updated
18+
- [ ] Integration tests added/updated
19+
- [ ] All existing tests pass
20+
- [ ] Manual testing performed
421

522
## Checklist
6-
- [ ] If code changes were made, then they have been tested
7-
- [ ] I have updated the documentation to reflect any changes made
8-
- [ ] I have thought about how this code may affect other services
9-
- [ ] This PR fixes an issue
10-
- [ ] This PR is a breaking change (e.g. method, parameters, env variables)
11-
- [ ] This PR adds something new (e.g. method, parameters, env variables)
12-
- [ ] This PR change unit and integration tests
13-
- [ ] This PR is **NOT** a code change (e.g. documentation, packages)
23+
- [ ] Code follows the project's style and conventions
24+
- [ ] Documentation updated (if applicable)
25+
- [ ] No new warnings or linter errors introduced
26+
- [ ] I have considered how this change may affect other services
1427

1528
## Reviewer
16-
- [ ] I understand that approving this code, I am also responsible for it going into the codebase
29+
- [ ] I understand that by approving this PR, I share responsibility for these changes

.github/workflows/workflow.yml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ jobs:
5454
uv sync --locked --all-extras --dev --no-install-package mysqlclient --no-install-package aiomysql
5555
elif [[ '${{ matrix.os }}' == 'macos-latest' ]]; then
5656
export PKG_CONFIG_PATH="$(brew --prefix mysql-client)/lib/pkgconfig"
57-
uv sync --locked --all-extras --dev
57+
uv sync --locked --all-extras --group dev
5858
else
59-
uv sync --locked --all-extras --dev
59+
uv sync --locked --all-extras --group dev
6060
fi
6161
shell: bash
6262

@@ -65,7 +65,7 @@ jobs:
6565
with:
6666
timeout_minutes: 2
6767
max_attempts: 3
68-
command: uv run --no-sync pytest tests/unit
68+
command: uv run --no-sync coverage run -m pytest tests/unit && uv run --no-sync coverage report && uv run --no-sync coverage xml
6969
shell: bash
7070

7171
- name: Upload coverage to Codecov
@@ -97,8 +97,7 @@ jobs:
9797
run: sudo apt-get update && sudo apt-get install -y default-libmysqlclient-dev pkg-config
9898

9999
- name: Install dependencies
100-
run: uv sync --locked --all-extras --dev
101-
shell: bash
100+
run: uv sync --locked --all-extras --group dev
102101

103102
- name: Install ODBC driver for MSSQL
104103
run: |
@@ -115,7 +114,7 @@ jobs:
115114
with:
116115
timeout_minutes: 3
117116
max_attempts: 3
118-
command: uv run --no-sync pytest tests/integration --no-cov
117+
command: uv run --no-sync pytest tests/integration
119118
shell: bash
120119

121120
build:

README.md

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@
88
<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>
99
<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>
1010
<br>
11-
<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>
12-
<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>
13-
<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>
14-
<br>
15-
<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>
1611
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=plastic" alt="License: MIT"/></a>
1712
<a href="https://pepy.tech/projects/ddcDatabases"><img src="https://static.pepy.tech/badge/ddcDatabases?style=plastic" alt="PyPI Downloads"/></a>
1813
<a href="https://pypi.python.org/pypi/ddcDatabases"><img src="https://img.shields.io/pypi/v/ddcDatabases.svg?style=plastic&logo=python&cacheSeconds=3600" alt="PyPi"/></a>
1914
<br>
15+
<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>
16+
<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>
17+
<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>
18+
<br>
2019
<a href="https://github.com/ddc/ddcDatabases/issues"><img src="https://img.shields.io/github/issues/ddc/ddcDatabases?style=plastic" alt="issues"/></a>
2120
<a href="https://codecov.io/gh/ddc/ddcDatabases"><img src="https://codecov.io/gh/ddc/ddcDatabases/graph/badge.svg?token=XWB53034GI&style=plastic" alt="codecov"/></a>
2221
<a href="https://sonarcloud.io/dashboard?id=ddc_ddcDatabases"><img src="https://sonarcloud.io/api/project_badges/measure?project=ddc_ddcDatabases&metric=alert_status&style=plastic" alt="Quality Gate Status"/></a>
@@ -48,7 +47,7 @@
4847
- [Database Utilities](#database-utilities)
4948
- [Available Methods](#available-methods)
5049
- [Logging](#logging)
51-
- [Development](#development)
50+
- [Development and Testing](#development-and-testing)
5251
- [Create DEV Environment and Running Tests](#create-dev-environment-and-running-tests)
5352
- [Update DEV Environment Packages](#update-dev-environment-packages)
5453
- [Building Wheel](#building-wheel)
@@ -89,13 +88,13 @@
8988

9089
Database classes use structured configuration dataclasses instead of flat keyword arguments:
9190

92-
| Class | Purpose | Fields |
93-
|------------------------------|---------------------------------|-------------------------------------------------------------------------------------|
94-
| `{DB}PoolConfig` | Connection pool settings | `pool_size`, `max_overflow`, `pool_recycle`, `connection_timeout` |
95-
| `{DB}SessionConfig` | SQLAlchemy session settings | `echo`, `autoflush`, `expire_on_commit`, `autocommit` |
96-
| `{DB}ConnectionRetryConfig` | Connection-level retry settings | `enable_retry`, `max_retries`, `initial_retry_delay`, `max_retry_delay` |
97-
| `{DB}OperationRetryConfig` | Operation-level retry settings | `enable_retry`, `max_retries`, `initial_retry_delay`, `max_retry_delay`, `jitter` |
98-
| `PersistentConnectionConfig` | Persistent connection settings | `idle_timeout`, `health_check_interval`, `auto_reconnect` |
91+
| Class | Purpose | Fields |
92+
|------------------------------|---------------------------------|-----------------------------------------------------------------------------------|
93+
| `{DB}PoolConfig` | Connection pool settings | `pool_size`, `max_overflow`, `pool_recycle`, `connection_timeout` |
94+
| `{DB}SessionConfig` | SQLAlchemy session settings | `echo`, `autoflush`, `expire_on_commit`, `autocommit` |
95+
| `{DB}ConnectionRetryConfig` | Connection-level retry settings | `enable_retry`, `max_retries`, `initial_retry_delay`, `max_retry_delay` |
96+
| `{DB}OperationRetryConfig` | Operation-level retry settings | `enable_retry`, `max_retries`, `initial_retry_delay`, `max_retry_delay`, `jitter` |
97+
| `PersistentConnectionConfig` | Persistent connection settings | `idle_timeout`, `health_check_interval`, `auto_reconnect` |
9998

10099
**Note:** Replace `{DB}` with the database prefix: `PostgreSQL`, `MySQL`, `MSSQL`, `Oracle`, `MongoDB`, or `Sqlite`.
101100

@@ -689,18 +688,15 @@ logging.getLogger("ddcDatabases").addHandler(logging.StreamHandler())
689688
```
690689

691690

692-
# Development
691+
# Development and Testing
693692

694693
Must have [UV](https://uv.run/docs/getting-started/installation) installed.
695694

696695
## Create DEV Environment and Running Tests
697696

698-
> **Note:** All poe tasks automatically run ruff linter along with Black formatting
699-
700697
```shell
701698
uv sync --all-extras --all-groups
702-
poe test
703-
poe test-integration
699+
poe tests
704700
```
705701

706702
## Update DEV Environment Packages

ddcDatabases/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ POSTGRESQL_PORT=5432
2222
POSTGRESQL_USER=postgres
2323
POSTGRESQL_PASSWORD=password
2424
POSTGRESQL_DATABASE=postgres
25+
# Comma-separated for multiple schemas (e.g. public,schema2)
2526
POSTGRESQL_SCHEMA=public
2627
# Session settings
2728
POSTGRESQL_ECHO=false

ddcDatabases/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@
195195
__all__ = tuple(__all__)
196196
__title__ = "ddcDatabases"
197197
__author__ = "Daniel Costa"
198-
__email__ = "ddcsoftwares@proton.me"
198+
__email__ = "daniel@ddcsoftwares.com"
199199
__license__ = "MIT"
200200
__copyright__ = "Copyright 2024-present DDC Softwares"
201201
__version__ = version(__title__)

ddcDatabases/core/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class SettingsMessages:
7171
USERNAME_DESCRIPTION = "Database username"
7272
PASSWORD_DESCRIPTION = "Database password"
7373
NAME_DESCRIPTION = "Database name"
74-
SCHEMA_DESCRIPTION = "Database schema"
74+
SCHEMA_DESCRIPTION = "Database schema (comma-separated for multiple schemas, e.g. 'gw2,public')"
7575
ASYNC_DATABASE_DRIVER_DESCRIPTION = "Async database driver"
7676
SYNC_DATABASE_DRIVER_DESCRIPTION = "Sync database driver"
7777

ddcDatabases/core/persistent.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -719,6 +719,7 @@ def __new__(
719719
user: str | None = None,
720720
password: str | None = None,
721721
database: str | None = None,
722+
schema: str | None = None,
722723
async_mode: Literal[False] = False,
723724
config: PersistentConnectionConfig | None = None,
724725
connection_retry_config: BaseRetryConfig | None = None,
@@ -735,6 +736,7 @@ def __new__(
735736
user: str | None = None,
736737
password: str | None = None,
737738
database: str | None = None,
739+
schema: str | None = None,
738740
*,
739741
async_mode: Literal[True],
740742
config: PersistentConnectionConfig | None = None,
@@ -751,6 +753,7 @@ def __new__(
751753
user: str | None = None,
752754
password: str | None = None,
753755
database: str | None = None,
756+
schema: str | None = None,
754757
async_mode: bool = False,
755758
config: PersistentConnectionConfig | None = None,
756759
connection_retry_config: BaseRetryConfig | None = None,
@@ -765,7 +768,10 @@ def __new__(
765768
user = user or _settings.user
766769
password = password or _settings.password
767770
database = database or _settings.database
771+
schema = schema if schema is not None else _settings.schema
768772
connection_key = f"postgresql://{user}@{host}:{port}/{database}" # NOSONAR
773+
if schema and schema != "public":
774+
connection_key += f"?schema={schema}"
769775

770776
# Build config from settings, allowing partial overrides
771777
_cfg = config or PersistentConnectionConfig()
@@ -820,6 +826,9 @@ def __new__(
820826
else:
821827
async_connect_args["ssl"] = ssl_mode
822828

829+
if schema and schema != "public":
830+
async_connect_args["server_settings"] = {"search_path": schema}
831+
823832
merged_kwargs = {**engine_kwargs}
824833
if async_connect_args:
825834
existing_connect_args = merged_kwargs.get("connect_args", {})
@@ -855,6 +864,9 @@ def __new__(
855864
if ssl_client_key_path:
856865
sync_connect_args["sslkey"] = ssl_client_key_path
857866

867+
if schema and schema != "public":
868+
sync_connect_args["options"] = f"-c search_path={schema}"
869+
858870
merged_kwargs = {**engine_kwargs}
859871
if sync_connect_args:
860872
existing_connect_args = merged_kwargs.get("connect_args", {})

ddcDatabases/core/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from pydantic_settings import BaseSettings, SettingsConfigDict
88
from typing import TypeVar
99

10-
warnings.filterwarnings("ignore", message="Field name \"schema\".*shadows an attribute in parent")
10+
warnings.filterwarnings("ignore", message='Field name "schema".*shadows an attribute in parent')
1111

1212
# Type variable for generic settings factory
1313
T = TypeVar("T", bound=BaseSettings)

pyproject.toml

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,14 @@ packages = ["ddcDatabases"]
1313

1414
[project]
1515
name = "ddcDatabases"
16-
version = "3.0.10"
16+
version = "3.0.11"
1717
description = "Simplified database ORM connections with support for multiple database engines"
1818
urls.Repository = "https://github.com/ddc/ddcDatabases"
1919
urls.Homepage = "https://pypi.org/project/ddcDatabases"
2020
license = {text = "MIT"}
2121
readme = "README.md"
2222
authors = [
23-
{name = "Daniel Costa", email = "ddcsoftwares@proton.me"},
23+
{name = "Daniel Costa", email = "daniel@ddcsoftwares.com"},
2424
]
2525
maintainers = [
2626
{name = "Daniel Costa"},
@@ -55,41 +55,40 @@ classifiers = [
5555
requires-python = ">=3.10"
5656
dependencies = [
5757
"pydantic-settings>=2.11.0",
58-
"sqlalchemy[asyncio]>=2.0.46",
58+
"sqlalchemy[asyncio]>=2.0.47",
5959
]
6060

6161
[project.optional-dependencies]
6262
mongodb = ["motor>=3.7.1"]
6363
oracle = ["oracledb>=3.4.2"]
6464
mssql = ["pyodbc>=5.3.0", "aioodbc>=0.5.0"]
6565
mysql = ["mysqlclient>=2.2.8", "aiomysql>=0.3.2"]
66-
postgres = ["psycopg[binary]>=3.3.2", "asyncpg>=0.31.0"]
66+
postgres = ["psycopg[binary]>=3.3.3", "asyncpg>=0.31.0"]
6767
pgsql = ["ddcDatabases[postgres]"]
6868
mariadb = ["ddcDatabases[mysql]"]
6969

7070
[dependency-groups]
7171
dev = [
72+
"coverage>=7.13.4",
73+
"poethepoet>=0.42.0",
7274
"pytest-asyncio>=1.3.0",
73-
"pytest-cov>=7.0.0",
75+
"ruff>=0.15.2",
7476
"testcontainers[postgres,mysql,mssql,mongodb,oracle]>=4.14.1",
75-
"poethepoet>=0.41.0",
76-
"ruff>=0.15.0",
77-
"black>=26.1.0",
7877
]
7978

8079
[tool.poe.tasks]
81-
linter.shell = "uv run ruff check --fix . && uv run black ."
82-
profile.sequence = ["linter", {shell = "uv run python -m cProfile -o cprofile_unit.prof -m pytest tests/unit --no-cov"}]
83-
profile-integration.sequence = ["linter", {shell = "uv run python -m cProfile -o cprofile_integration.prof -m pytest tests/integration --no-cov"}]
84-
test.sequence = ["linter", {shell = "uv run pytest"}]
85-
test-integration.sequence = ["linter", {shell = "uv run pytest tests/integration --no-cov"}]
80+
linter.shell = "uv run ruff check --fix . && uv run ruff format ."
81+
profile = "uv run python -m cProfile -o cprofile_unit.prof -m pytest tests/unit"
82+
profile-integration = "uv run python -m cProfile -o cprofile_integration.prof -m pytest tests/integration"
83+
test.sequence = [{ shell = "uv run coverage run -m pytest tests/unit" }, { shell = "uv run coverage report" }, { shell = "uv run coverage xml" }]
84+
test-integration = "uv run pytest tests/integration"
85+
tests.sequence = ["linter", "test", "test-integration"]
8686
updatedev.sequence = ["linter", {shell = "uv lock --upgrade && uv sync --all-extras --group dev"}]
87-
build.sequence = ["updatedev", "test", "test-integration", {shell = "uv build --wheel"}]
87+
build.sequence = ["updatedev", "tests", {shell = "uv build --wheel"}]
8888

8989
[tool.pytest.ini_options]
90-
addopts = "-v --cov --cov-report=term --cov-report=xml --junitxml=junit.xml"
90+
addopts = "-v --import-mode=importlib --junitxml=junit.xml"
9191
junit_family = "legacy"
92-
testpaths = ["tests/unit"]
9392
asyncio_mode = "strict"
9493
asyncio_default_fixture_loop_scope = "function"
9594
markers = [
@@ -120,10 +119,6 @@ exclude_lines = [
120119
"@(abc\\.)?abstractmethod",
121120
]
122121

123-
[tool.black]
124-
line-length = 120
125-
skip-string-normalization = true
126-
127122
[tool.ruff]
128123
line-length = 120
129124
target-version = "py310"

tests/integration/conftest.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import pytest
2+
import time
23
from sqlalchemy import Boolean, Identity, String
34
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
45

56
# Testcontainer image versions
6-
POSTGRES_IMAGE = "postgres:18-alpine"
7-
MYSQL_IMAGE = "mysql:9"
7+
POSTGRES_IMAGE = "postgres:latest"
8+
MYSQL_IMAGE = "mysql:latest"
9+
MONGODB_IMAGE = "mongo:8.0"
10+
MARIADB_IMAGE = "mariadb:latest"
11+
ORACLE_IMAGE = "gvenzl/oracle-free:slim-faststart"
812
MSSQL_IMAGE = "mcr.microsoft.com/mssql/server:2022-latest"
9-
MONGODB_IMAGE = "mongo:8"
10-
MARIADB_IMAGE = "mariadb:12"
11-
ORACLE_IMAGE = "gvenzl/oracle-free:23-slim"
1213

1314

1415
class Base(DeclarativeBase):
@@ -50,8 +51,21 @@ def mssql_container():
5051
def mongodb_container():
5152
from testcontainers.mongodb import MongoDbContainer
5253

53-
with MongoDbContainer(MONGODB_IMAGE) as mongo:
54-
yield mongo
54+
max_attempts = 3
55+
last_exc = None
56+
for attempt in range(max_attempts):
57+
try:
58+
container = MongoDbContainer(MONGODB_IMAGE)
59+
container.start()
60+
break
61+
except Exception as exc:
62+
last_exc = exc
63+
if attempt < max_attempts - 1:
64+
time.sleep(2)
65+
else:
66+
raise last_exc
67+
yield container
68+
container.stop()
5569

5670

5771
@pytest.fixture(scope="session")

0 commit comments

Comments
 (0)