standard-rdbms 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- standard_rdbms-0.2.3/.gitignore +31 -0
- standard_rdbms-0.2.3/LICENSE +21 -0
- standard_rdbms-0.2.3/PKG-INFO +57 -0
- standard_rdbms-0.2.3/README.md +27 -0
- standard_rdbms-0.2.3/pyproject.toml +50 -0
- standard_rdbms-0.2.3/src/std/rdbms/__init__.py +18 -0
- standard_rdbms-0.2.3/src/std/rdbms/config.py +89 -0
- standard_rdbms-0.2.3/src/std/rdbms/database.py +90 -0
- standard_rdbms-0.2.3/src/std/rdbms/datetime_utils.py +28 -0
- standard_rdbms-0.2.3/src/std/rdbms/mysql.py +34 -0
- standard_rdbms-0.2.3/src/std/rdbms/postgres.py +33 -0
- standard_rdbms-0.2.3/src/std/rdbms/py.typed +0 -0
- standard_rdbms-0.2.3/src/std/rdbms/sqlite.py +59 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Workspace-managed mounts.
|
|
2
|
+
/.agents/
|
|
3
|
+
/.aws/
|
|
4
|
+
/.codex/
|
|
5
|
+
|
|
6
|
+
# Python tooling and generated files.
|
|
7
|
+
/.venv/
|
|
8
|
+
/venv/
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.py[cod]
|
|
11
|
+
*.egg-info/
|
|
12
|
+
/build/
|
|
13
|
+
/dist/
|
|
14
|
+
/.coverage
|
|
15
|
+
/.coverage.*
|
|
16
|
+
/htmlcov/
|
|
17
|
+
/deprecated/
|
|
18
|
+
/.pytest_cache/
|
|
19
|
+
/.mypy_cache/
|
|
20
|
+
/.ruff_cache/
|
|
21
|
+
/.tox/
|
|
22
|
+
|
|
23
|
+
# Documentation output.
|
|
24
|
+
/.cache/
|
|
25
|
+
/site/
|
|
26
|
+
|
|
27
|
+
# Local secrets and environment files.
|
|
28
|
+
.env
|
|
29
|
+
.env.*
|
|
30
|
+
!.env.example
|
|
31
|
+
.kata.local.toml
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Standard contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: standard-rdbms
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: Async relational database helpers for the Standard Python library
|
|
5
|
+
Project-URL: Documentation, https://appunni-m.github.io/python-standard/rdbms/
|
|
6
|
+
Project-URL: Repository, https://github.com/appunni-m/python-standard
|
|
7
|
+
Author: Standard contributors
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: database,mysql,postgresql,sqlalchemy,sqlite,standard
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: aiomysql<1,>=0.2
|
|
23
|
+
Requires-Dist: aiosqlite<1,>=0.19
|
|
24
|
+
Requires-Dist: asyncpg<1,>=0.29
|
|
25
|
+
Requires-Dist: cryptography>=41
|
|
26
|
+
Requires-Dist: pydantic<3,>=2.6
|
|
27
|
+
Requires-Dist: sqlalchemy[asyncio]<3,>=2.0
|
|
28
|
+
Requires-Dist: standard==0.2.3
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# standard-rdbms
|
|
32
|
+
|
|
33
|
+
Async SQLAlchemy helpers for the [`standard`](https://pypi.org/project/standard/)
|
|
34
|
+
Python library. The package provides `DatabaseConfig`, managed async sessions,
|
|
35
|
+
SQLite, PostgreSQL, MySQL, and the `ensure_utc` property decorator under
|
|
36
|
+
`std.rdbms`.
|
|
37
|
+
|
|
38
|
+
After publication, install the integration directly:
|
|
39
|
+
|
|
40
|
+
```console
|
|
41
|
+
python -m pip install standard-rdbms
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
For local development from this repository:
|
|
45
|
+
|
|
46
|
+
```console
|
|
47
|
+
python -m pip install -e packages/standard-rdbms
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from std.rdbms import DatabaseConfig, SQLiteMemDatabase
|
|
52
|
+
|
|
53
|
+
database = SQLiteMemDatabase(DatabaseConfig(driver="sqlite+aiosqlite"))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
See the [RDBMS guide](https://appunni-m.github.io/python-standard/rdbms/)
|
|
57
|
+
for transactions, backend configuration, datetime behavior, and test commands.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# standard-rdbms
|
|
2
|
+
|
|
3
|
+
Async SQLAlchemy helpers for the [`standard`](https://pypi.org/project/standard/)
|
|
4
|
+
Python library. The package provides `DatabaseConfig`, managed async sessions,
|
|
5
|
+
SQLite, PostgreSQL, MySQL, and the `ensure_utc` property decorator under
|
|
6
|
+
`std.rdbms`.
|
|
7
|
+
|
|
8
|
+
After publication, install the integration directly:
|
|
9
|
+
|
|
10
|
+
```console
|
|
11
|
+
python -m pip install standard-rdbms
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
For local development from this repository:
|
|
15
|
+
|
|
16
|
+
```console
|
|
17
|
+
python -m pip install -e packages/standard-rdbms
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from std.rdbms import DatabaseConfig, SQLiteMemDatabase
|
|
22
|
+
|
|
23
|
+
database = SQLiteMemDatabase(DatabaseConfig(driver="sqlite+aiosqlite"))
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
See the [RDBMS guide](https://appunni-m.github.io/python-standard/rdbms/)
|
|
27
|
+
for transactions, backend configuration, datetime behavior, and test commands.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27,<2", "hatch-vcs>=0.4,<1"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "standard-rdbms"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Async relational database helpers for the Standard Python library"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Standard contributors" }]
|
|
14
|
+
keywords = ["database", "sqlalchemy", "sqlite", "postgresql", "mysql", "standard"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: OS Independent",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"standard==0.2.3",
|
|
29
|
+
"pydantic>=2.6,<3",
|
|
30
|
+
"SQLAlchemy[asyncio]>=2.0,<3",
|
|
31
|
+
"aiosqlite>=0.19,<1",
|
|
32
|
+
"asyncpg>=0.29,<1",
|
|
33
|
+
"aiomysql>=0.2,<1",
|
|
34
|
+
"cryptography>=41",
|
|
35
|
+
]
|
|
36
|
+
urls = { Documentation = "https://appunni-m.github.io/python-standard/rdbms/", Repository = "https://github.com/appunni-m/python-standard" }
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
only-include = ["src/std/rdbms"]
|
|
40
|
+
sources = ["src"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
only-include = ["pyproject.toml", "README.md", "LICENSE", "src/std/rdbms"]
|
|
44
|
+
|
|
45
|
+
[tool.hatch.version]
|
|
46
|
+
source = "vcs"
|
|
47
|
+
tag-pattern = "^v(?P<version>\\d+\\.\\d+\\.\\d+(?:(?:a|b|rc)\\d+)?)$"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.version.raw-options]
|
|
50
|
+
search_parent_directories = true
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Async relational database helpers for Standard."""
|
|
2
|
+
|
|
3
|
+
from std.rdbms.config import DatabaseConfig
|
|
4
|
+
from std.rdbms.database import Database
|
|
5
|
+
from std.rdbms.datetime_utils import ensure_utc
|
|
6
|
+
from std.rdbms.mysql import MySQLDatabase
|
|
7
|
+
from std.rdbms.postgres import PostgresDatabase
|
|
8
|
+
from std.rdbms.sqlite import SQLiteDatabase, SQLiteMemDatabase
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Database",
|
|
12
|
+
"DatabaseConfig",
|
|
13
|
+
"MySQLDatabase",
|
|
14
|
+
"PostgresDatabase",
|
|
15
|
+
"SQLiteDatabase",
|
|
16
|
+
"SQLiteMemDatabase",
|
|
17
|
+
"ensure_utc",
|
|
18
|
+
]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Validated settings and SQLAlchemy URL construction for relational databases."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path, PureWindowsPath
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
9
|
+
from sqlalchemy.engine import URL
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DatabaseConfig(BaseModel):
|
|
13
|
+
"""Connection and pool settings shared by the async database managers.
|
|
14
|
+
|
|
15
|
+
The ``url`` property is computed from the current fields. It includes the
|
|
16
|
+
password when one is configured, so treat the returned string as a secret.
|
|
17
|
+
The password is excluded from the model representation and serialization.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
model_config = ConfigDict(validate_assignment=True, extra="forbid")
|
|
21
|
+
|
|
22
|
+
driver: str = Field(default="mysql+aiomysql", description="SQLAlchemy async dialect and driver")
|
|
23
|
+
host: str = Field(default="localhost", description="Database server hostname")
|
|
24
|
+
port: int | None = Field(default=None, ge=1, le=65535, description="Database server port")
|
|
25
|
+
user: str | None = Field(default=None, description="Database username")
|
|
26
|
+
password: str | None = Field(
|
|
27
|
+
default=None,
|
|
28
|
+
repr=False,
|
|
29
|
+
exclude=True,
|
|
30
|
+
description="Database password",
|
|
31
|
+
)
|
|
32
|
+
name: str = Field(default="agent_framework", description="Database name or SQLite path")
|
|
33
|
+
echo: bool = Field(default=False, description="Echo SQL statements for debugging")
|
|
34
|
+
pool_size: int = Field(default=5, ge=0, description="Number of persistent connections")
|
|
35
|
+
max_overflow: int = Field(default=10, ge=-1, description="Connections beyond the pool size")
|
|
36
|
+
pool_timeout: float = Field(default=30.0, ge=0, description="Seconds to wait for a connection")
|
|
37
|
+
pool_recycle: int = Field(
|
|
38
|
+
default=1800, ge=-1, description="Recycle connections after this many seconds"
|
|
39
|
+
)
|
|
40
|
+
connect_args: dict[str, Any] = Field(
|
|
41
|
+
default_factory=dict,
|
|
42
|
+
description="Driver-specific connection arguments",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
@field_validator("driver", "host", "name")
|
|
46
|
+
@classmethod
|
|
47
|
+
def require_non_empty_text(cls, value: str) -> str:
|
|
48
|
+
"""Reject empty strings before they become malformed connection URLs."""
|
|
49
|
+
if not value.strip():
|
|
50
|
+
raise ValueError("must not be empty")
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def url(self) -> str:
|
|
55
|
+
"""Return a SQLAlchemy URL string built with proper credential escaping."""
|
|
56
|
+
if self.driver.startswith("sqlite") and self.name == ":memory:":
|
|
57
|
+
return f"{self.driver}:///:memory:"
|
|
58
|
+
return self._sqlalchemy_url().render_as_string(hide_password=False)
|
|
59
|
+
|
|
60
|
+
def _sqlalchemy_url(self) -> URL:
|
|
61
|
+
"""Build a structured SQLAlchemy URL without hand-escaping credentials."""
|
|
62
|
+
if self.driver.startswith("sqlite"):
|
|
63
|
+
database = self.name
|
|
64
|
+
if database != ":memory:":
|
|
65
|
+
is_absolute = (
|
|
66
|
+
Path(database).is_absolute() or PureWindowsPath(database).is_absolute()
|
|
67
|
+
)
|
|
68
|
+
if not is_absolute and not database.startswith(("./", ".\\")):
|
|
69
|
+
database = f"./{database}"
|
|
70
|
+
return URL.create(drivername=self.driver, database=database)
|
|
71
|
+
|
|
72
|
+
port = self.port
|
|
73
|
+
if port is None:
|
|
74
|
+
if "mysql" in self.driver:
|
|
75
|
+
port = 3306
|
|
76
|
+
elif "postgres" in self.driver:
|
|
77
|
+
port = 5432
|
|
78
|
+
else:
|
|
79
|
+
# Preserve the original fallback for custom network dialects.
|
|
80
|
+
port = 3306
|
|
81
|
+
|
|
82
|
+
return URL.create(
|
|
83
|
+
drivername=self.driver,
|
|
84
|
+
username=self.user or None,
|
|
85
|
+
password=self.password if self.user else None,
|
|
86
|
+
host=self.host,
|
|
87
|
+
port=port,
|
|
88
|
+
database=self.name,
|
|
89
|
+
)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Async SQLAlchemy database lifecycle and transaction management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import AsyncIterator
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
|
|
11
|
+
|
|
12
|
+
from std.rdbms.config import DatabaseConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Database(ABC):
|
|
16
|
+
"""Own an async engine and provide commit-on-success session scopes.
|
|
17
|
+
|
|
18
|
+
Each session is intended for one asyncio task at a time. A successful
|
|
19
|
+
``session()`` scope commits; any exception, including cancellation, rolls
|
|
20
|
+
back and re-raises. The session is closed in either case.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, settings: DatabaseConfig) -> None:
|
|
24
|
+
"""Create the engine and its session factory without opening a connection."""
|
|
25
|
+
self.settings = settings
|
|
26
|
+
self._engine = self._create_engine()
|
|
27
|
+
self._session_factory = async_sessionmaker(
|
|
28
|
+
self._engine,
|
|
29
|
+
class_=AsyncSession,
|
|
30
|
+
expire_on_commit=False,
|
|
31
|
+
autoflush=False,
|
|
32
|
+
)
|
|
33
|
+
self._disposed = False
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def engine(self) -> AsyncEngine:
|
|
37
|
+
"""Return the SQLAlchemy engine for migrations and explicit transactions."""
|
|
38
|
+
return self._engine
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def _create_engine(self) -> AsyncEngine:
|
|
42
|
+
"""Create the backend-specific async SQLAlchemy engine."""
|
|
43
|
+
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
@asynccontextmanager
|
|
46
|
+
async def session(self) -> AsyncIterator[AsyncSession]:
|
|
47
|
+
"""Yield a session that commits on success and rolls back on failure."""
|
|
48
|
+
if self._disposed:
|
|
49
|
+
raise RuntimeError("Database has been disposed")
|
|
50
|
+
|
|
51
|
+
session = self._session_factory()
|
|
52
|
+
try:
|
|
53
|
+
yield session
|
|
54
|
+
await session.commit()
|
|
55
|
+
except BaseException as error:
|
|
56
|
+
try:
|
|
57
|
+
await session.rollback()
|
|
58
|
+
except BaseException as rollback_error:
|
|
59
|
+
error.add_note(
|
|
60
|
+
"Session rollback also failed: "
|
|
61
|
+
f"{type(rollback_error).__name__}: {rollback_error}"
|
|
62
|
+
)
|
|
63
|
+
raise
|
|
64
|
+
finally:
|
|
65
|
+
active_error = sys.exception()
|
|
66
|
+
try:
|
|
67
|
+
await session.close()
|
|
68
|
+
except BaseException as close_error:
|
|
69
|
+
if active_error is None:
|
|
70
|
+
raise
|
|
71
|
+
active_error.add_note(
|
|
72
|
+
f"Session close also failed: {type(close_error).__name__}: {close_error}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
async def dispose(self) -> None:
|
|
76
|
+
"""Close pooled connections; calling this more than once is safe."""
|
|
77
|
+
if self._disposed:
|
|
78
|
+
return
|
|
79
|
+
await self._engine.dispose()
|
|
80
|
+
self._disposed = True
|
|
81
|
+
|
|
82
|
+
async def __aenter__(self) -> Database:
|
|
83
|
+
"""Return this database for use as an async resource context manager."""
|
|
84
|
+
if self._disposed:
|
|
85
|
+
raise RuntimeError("Database has been disposed")
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
async def __aexit__(self, exc_type: object, exc: object, traceback: object) -> None:
|
|
89
|
+
"""Dispose the engine when leaving an async resource context."""
|
|
90
|
+
await self.dispose()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Timezone helpers for database-backed model properties."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from functools import wraps
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def ensure_utc(function: Callable[..., datetime | None]) -> property:
|
|
11
|
+
"""Decorate a property getter so returned datetimes are UTC-aware.
|
|
12
|
+
|
|
13
|
+
Naive values are interpreted as UTC, matching the historical helper
|
|
14
|
+
contract. Aware values are converted to UTC. ``None`` passes through.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@wraps(function)
|
|
18
|
+
def getter(instance: object) -> datetime | None:
|
|
19
|
+
value = function(instance)
|
|
20
|
+
if value is None:
|
|
21
|
+
return None
|
|
22
|
+
if not isinstance(value, datetime):
|
|
23
|
+
raise TypeError("ensure_utc property getter must return datetime or None")
|
|
24
|
+
if value.tzinfo is None or value.utcoffset() is None:
|
|
25
|
+
return value.replace(tzinfo=UTC)
|
|
26
|
+
return value.astimezone(UTC)
|
|
27
|
+
|
|
28
|
+
return property(getter)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Async MySQL database manager using SQLAlchemy's async dialect."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
|
6
|
+
|
|
7
|
+
from std.rdbms.config import DatabaseConfig
|
|
8
|
+
from std.rdbms.database import Database
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MySQLDatabase(Database):
|
|
12
|
+
"""Manage MySQL connections with health checks and configured pooling."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, config: DatabaseConfig) -> None:
|
|
15
|
+
"""Copy settings and provide safe MySQL charset and transaction defaults."""
|
|
16
|
+
settings = config.model_copy(deep=True)
|
|
17
|
+
if not settings.driver.startswith("mysql+"):
|
|
18
|
+
raise ValueError("MySQLDatabase requires a MySQL async driver")
|
|
19
|
+
settings.connect_args.setdefault("charset", "utf8mb4")
|
|
20
|
+
settings.connect_args.setdefault("autocommit", False)
|
|
21
|
+
super().__init__(settings)
|
|
22
|
+
|
|
23
|
+
def _create_engine(self) -> AsyncEngine:
|
|
24
|
+
"""Create a pooled MySQL engine with pre-ping enabled."""
|
|
25
|
+
return create_async_engine(
|
|
26
|
+
self.settings.url,
|
|
27
|
+
echo=self.settings.echo,
|
|
28
|
+
pool_size=self.settings.pool_size,
|
|
29
|
+
max_overflow=self.settings.max_overflow,
|
|
30
|
+
pool_timeout=self.settings.pool_timeout,
|
|
31
|
+
pool_recycle=self.settings.pool_recycle,
|
|
32
|
+
pool_pre_ping=True,
|
|
33
|
+
connect_args=self.settings.connect_args,
|
|
34
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Async PostgreSQL database manager using SQLAlchemy's async dialect."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
|
6
|
+
|
|
7
|
+
from std.rdbms.config import DatabaseConfig
|
|
8
|
+
from std.rdbms.database import Database
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PostgresDatabase(Database):
|
|
12
|
+
"""Manage PostgreSQL connections with health checks and configured pooling."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, config: DatabaseConfig) -> None:
|
|
15
|
+
"""Copy settings and provide a recognizable PostgreSQL application name."""
|
|
16
|
+
settings = config.model_copy(deep=True)
|
|
17
|
+
if not settings.driver.startswith(("postgresql+", "postgres+")):
|
|
18
|
+
raise ValueError("PostgresDatabase requires a PostgreSQL async driver")
|
|
19
|
+
settings.connect_args.setdefault("server_settings", {"application_name": "standard-rdbms"})
|
|
20
|
+
super().__init__(settings)
|
|
21
|
+
|
|
22
|
+
def _create_engine(self) -> AsyncEngine:
|
|
23
|
+
"""Create a pooled PostgreSQL engine with pre-ping enabled."""
|
|
24
|
+
return create_async_engine(
|
|
25
|
+
self.settings.url,
|
|
26
|
+
echo=self.settings.echo,
|
|
27
|
+
pool_size=self.settings.pool_size,
|
|
28
|
+
max_overflow=self.settings.max_overflow,
|
|
29
|
+
pool_timeout=self.settings.pool_timeout,
|
|
30
|
+
pool_recycle=self.settings.pool_recycle,
|
|
31
|
+
pool_pre_ping=True,
|
|
32
|
+
connect_args=self.settings.connect_args,
|
|
33
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Async SQLite managers for persistent files and isolated in-memory databases."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
|
6
|
+
from sqlalchemy.pool import NullPool, StaticPool
|
|
7
|
+
|
|
8
|
+
from std.rdbms.config import DatabaseConfig
|
|
9
|
+
from std.rdbms.database import Database
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _sqlite_settings(config: DatabaseConfig) -> DatabaseConfig:
|
|
13
|
+
"""Copy and validate SQLite settings without mutating the caller's model."""
|
|
14
|
+
settings = config.model_copy(deep=True)
|
|
15
|
+
if not settings.driver.startswith("sqlite+aiosqlite"):
|
|
16
|
+
raise ValueError("SQLite managers require the sqlite+aiosqlite driver")
|
|
17
|
+
settings.connect_args.setdefault("check_same_thread", False)
|
|
18
|
+
return settings
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SQLiteDatabase(Database):
|
|
22
|
+
"""Manage a file-backed SQLite database with a fresh connection per checkout."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, config: DatabaseConfig) -> None:
|
|
25
|
+
"""Create a file-backed database; use ``SQLiteMemDatabase`` for memory."""
|
|
26
|
+
if config.name == ":memory:":
|
|
27
|
+
raise ValueError("Use SQLiteMemDatabase for an in-memory SQLite database")
|
|
28
|
+
super().__init__(_sqlite_settings(config))
|
|
29
|
+
|
|
30
|
+
def _create_engine(self) -> AsyncEngine:
|
|
31
|
+
"""Create a file-backed engine with ``NullPool`` and configured connect args."""
|
|
32
|
+
return create_async_engine(
|
|
33
|
+
self.settings.url,
|
|
34
|
+
echo=self.settings.echo,
|
|
35
|
+
poolclass=NullPool,
|
|
36
|
+
connect_args=self.settings.connect_args,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SQLiteMemDatabase(Database):
|
|
41
|
+
"""Manage one shared in-memory SQLite database for the lifetime of its engine."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, config: DatabaseConfig) -> None:
|
|
44
|
+
"""Force this manager's copied settings to use one shared in-memory database."""
|
|
45
|
+
settings = config.model_copy(deep=True)
|
|
46
|
+
if not settings.driver.startswith("sqlite+aiosqlite"):
|
|
47
|
+
raise ValueError("SQLite managers require the sqlite+aiosqlite driver")
|
|
48
|
+
settings.name = ":memory:"
|
|
49
|
+
settings.connect_args.setdefault("check_same_thread", False)
|
|
50
|
+
super().__init__(settings)
|
|
51
|
+
|
|
52
|
+
def _create_engine(self) -> AsyncEngine:
|
|
53
|
+
"""Create an engine with ``StaticPool`` so sessions see the same memory DB."""
|
|
54
|
+
return create_async_engine(
|
|
55
|
+
self.settings.url,
|
|
56
|
+
echo=self.settings.echo,
|
|
57
|
+
poolclass=StaticPool,
|
|
58
|
+
connect_args=self.settings.connect_args,
|
|
59
|
+
)
|