Skip to content

Commit fefff23

Browse files
feat(db): Add async versions for all database modules (#37)
* feat(db): Add async versions for all database modules - Add aloha.db.postgres_aio using asyncpg - Add aloha.db.mysql_aio using aiomysql - Add aloha.db.redis_aio using redis.asyncio - Add aloha.db.mongo_aio using motor (async pymongo) - Add aloha.db.elasticsearch_aio using AsyncElasticsearch - Add aloha.db.kafka_aio using aiokafka - Add aloha.db.sqlite_aio using aiosqlite - Add aloha.db.duckdb_aio using aioduckdb - Add aloha.db.oracle_aio using oracledb async support - Update pkg/aloha/db/__init__.py to export both sync and async modules - Update pyproject.toml with new 'aio' optional dependencies The async modules maintain similar interfaces to their sync counterparts, allowing minimal code changes when migrating existing applications. Co-authored-by: openhands <openhands@all-hands.dev> * update kafka code * fix: Update license format to SPDX expression Change license from {text = "Apache-2.0"} to "Apache-2.0" to fix setuptools deprecation warning. Co-authored-by: openhands <openhands@all-hands.dev> * fix ver * code clean * remov default import in module * code ql --------- Signed-off-by: Bibo Hao <haobibo@users.noreply.github.com> Co-authored-by: openhands <openhands@all-hands.dev>
1 parent fd8d6b2 commit fefff23

16 files changed

Lines changed: 1807 additions & 33 deletions

‎AGENTS.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ When writing code or configurations for this project, you **MUST** strictly adhe
2424

2525
### A. Python Coding & Naming Standards
2626

27-
- **Primary Type/First-Class Identity Prefix**: Place the variable's type, role, or primary characteristics first in its name.
28-
- _Correct_: `name_service`, `port_service`, `svc_ingress`, `cfg_postgres`, `db_mysql`.
29-
- _Incorrect_: `service_name`, `service_port`, `ingress_service`, `postgres_config`, `mysql_db`.
27+
- **Primary Type/First-Class Identity Prefix**: Place the variable's type, role, or primary characteristics first in its name. If multiple variables in a segment of code belong to the same category, type, or semantic group, place the common semantic prefix first.
28+
- _Correct_: `name_service`, `port_service`, `svc_ingress`, `cfg_postgres`, `db_mysql`, `msg_err`, `msg_info`.
29+
- _Incorrect_: `service_name`, `service_port`, `ingress_service`, `postgres_config`, `mysql_db`, `err_msg`, `info_msg`.
3030
- **Logger Naming**: Use lowercase with underscores for logger names, e.g., `db_sync`, `api_router`.
3131
- **Import Conventions**: Use relative imports if possible, especially inside a package.
3232

‎doc/skills/aloha_python/SKILL.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ This skill provides coding standards, modular application structures, and usage
1313

1414
When developing Python code in this codebase, adhere to the following naming conventions:
1515

16-
- **Primary Type/First-Class Identity Prefix**: Place the variable's type, role, or primary characteristics first in its name.
17-
- _Correct_: `name_service`, `port_service`, `svc_ingress`, `cfg_postgres`.
18-
- _Incorrect_: `service_name`, `service_port`, `ingress_service`, `postgres_config`.
16+
- **Primary Type/First-Class Identity Prefix**: Place the variable's type, role, or primary characteristics first in its name. If multiple variables in a segment of code belong to the same category, type, or semantic group, place the common semantic prefix first.
17+
- _Correct_: `name_service`, `port_service`, `svc_ingress`, `cfg_postgres`, `db_mysql`, `msg_err`, `msg_info`.
18+
- _Incorrect_: `service_name`, `service_port`, `ingress_service`, `postgres_config`, `mysql_db`, `err_msg`, `info_msg`.
1919
- **Logger naming**: Use lowercase with underscores for logger names, e.g., `db_sync`.
2020

2121
---

‎pkg/aloha/db/__init__.py‎

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"""
2+
aloha.db package - Database and middleware connection helpers.
3+
4+
Sync modules (blocking):
5+
from aloha.db import PostgresOperator, MySqlOperator, RedisOperator, ...
6+
from aloha.db.postgres import PostgresOperator
7+
from aloha.db.mysql import MySqlOperator
8+
from aloha.db.redis import RedisOperator
9+
from aloha.db.mongo import MongoOperator
10+
from aloha.db.elasticsearch import ElasticSearchOperator
11+
from aloha.db.kafka import KafkaOperator
12+
from aloha.db.sqlite import SqliteOperator
13+
from aloha.db.duckdb import DuckOperator
14+
from aloha.db.oracle import OracledbOperator
15+
16+
Async modules (non-blocking):
17+
from aloha.db import PostgresOperator as PostgresOperatorAio, ...
18+
from aloha.db.postgres_aio import PostgresOperator
19+
from aloha.db.mysql_aio import MySqlOperator
20+
from aloha.db.redis_aio import RedisOperator
21+
from aloha.db.mongo_aio import MongoOperator
22+
from aloha.db.elasticsearch_aio import ElasticSearchOperator
23+
from aloha.db.kafka_aio import KafkaOperator
24+
from aloha.db.sqlite_aio import SqliteOperator
25+
from aloha.db.duckdb_aio import DuckOperator
26+
from aloha.db.oracle_aio import OracledbOperator
27+
28+
Base utilities:
29+
from aloha.db.base import PasswordVault
30+
from aloha.db.base_aio import PasswordVault # async version
31+
32+
Usage example (sync):
33+
from aloha.db.postgres import PostgresOperator
34+
35+
op = PostgresOperator(db_config)
36+
result = op.execute_query("SELECT * FROM users")
37+
for row in result:
38+
print(row)
39+
40+
Usage example (async):
41+
from aloha.db.postgres_aio import PostgresOperator
42+
43+
async def main():
44+
op = PostgresOperator(db_config)
45+
result = await op.execute_query("SELECT * FROM users")
46+
async for row in op.execute_query_scalars("SELECT * FROM users"):
47+
print(row)
48+
await op.close()
49+
50+
import asyncio
51+
asyncio.run(main())
52+
53+
54+
from .base import PasswordVault
55+
56+
try:
57+
from .postgres import PostgresOperator
58+
except (ImportError, ModuleNotFoundError):
59+
pass
60+
61+
try:
62+
from .mysql import MySqlOperator
63+
except (ImportError, ModuleNotFoundError):
64+
pass
65+
66+
try:
67+
from .redis import RedisOperator
68+
except (ImportError, ModuleNotFoundError):
69+
pass
70+
71+
try:
72+
from .mongo import MongoOperator
73+
except (ImportError, ModuleNotFoundError):
74+
pass
75+
76+
try:
77+
from .elasticsearch import ElasticSearchOperator
78+
except (ImportError, ModuleNotFoundError):
79+
pass
80+
81+
try:
82+
from .kafka import ConsumedMessage, KafkaOperator
83+
except (ImportError, ModuleNotFoundError):
84+
pass
85+
86+
try:
87+
from .sqlite import SqliteOperator
88+
except (ImportError, ModuleNotFoundError):
89+
pass
90+
91+
try:
92+
from .duckdb import DuckOperator
93+
except (ImportError, ModuleNotFoundError):
94+
pass
95+
96+
try:
97+
from .oracle import OracledbOperator
98+
except (ImportError, ModuleNotFoundError):
99+
pass
100+
101+
102+
# Async modules (importable as aliases for easy switching)
103+
from .base_aio import PasswordVault as PasswordVaultAio
104+
105+
try:
106+
from .postgres_aio import PostgresOperator as PostgresOperatorAio
107+
except (ImportError, ModuleNotFoundError):
108+
pass
109+
110+
try:
111+
from .mysql_aio import MySqlOperator as MySqlOperatorAio
112+
except (ImportError, ModuleNotFoundError):
113+
pass
114+
115+
try:
116+
from .redis_aio import RedisOperator as RedisOperatorAio
117+
except (ImportError, ModuleNotFoundError):
118+
pass
119+
120+
try:
121+
from .mongo_aio import MongoOperator as MongoOperatorAio
122+
except (ImportError, ModuleNotFoundError):
123+
pass
124+
125+
try:
126+
from .elasticsearch_aio import ElasticSearchOperator as ElasticSearchOperatorAio
127+
except (ImportError, ModuleNotFoundError):
128+
pass
129+
130+
try:
131+
from .kafka_aio import ConsumedMessage as ConsumedMessageAio
132+
from .kafka_aio import KafkaOperator as KafkaOperatorAio
133+
except (ImportError, ModuleNotFoundError):
134+
pass
135+
136+
try:
137+
from .sqlite_aio import SqliteOperator as SqliteOperatorAio
138+
except (ImportError, ModuleNotFoundError):
139+
pass
140+
141+
try:
142+
from .duckdb_aio import DuckOperator as DuckOperatorAio
143+
except (ImportError, ModuleNotFoundError):
144+
pass
145+
146+
try:
147+
from .oracle_aio import OracledbOperator as OracledbOperatorAio
148+
except (ImportError, ModuleNotFoundError):
149+
pass
150+
151+
__all__ = (
152+
"ConsumedMessage",
153+
"ConsumedMessageAio",
154+
"DuckOperator",
155+
"DuckOperatorAio",
156+
"ElasticSearchOperator",
157+
"ElasticSearchOperatorAio",
158+
"KafkaOperator",
159+
"KafkaOperatorAio",
160+
"MongoOperator",
161+
"MongoOperatorAio",
162+
"MySqlOperator",
163+
"MySqlOperatorAio",
164+
"OracledbOperator",
165+
"OracledbOperatorAio",
166+
"PasswordVault",
167+
"PasswordVaultAio",
168+
# Sync operators
169+
"PostgresOperator",
170+
# Async operators (aliased)
171+
"PostgresOperatorAio",
172+
"RedisOperator",
173+
"RedisOperatorAio",
174+
"SqliteOperator",
175+
"SqliteOperatorAio",
176+
)
177+
"""

‎pkg/aloha/db/duckdb_aio.py‎

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""
2+
Async DuckDB connection helpers.
3+
"""
4+
5+
from pathlib import Path
6+
7+
import duckdb
8+
import duckdb_engine
9+
from sqlalchemy import text
10+
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
11+
12+
from aloha.logger import LOG
13+
14+
__all__ = ("DuckOperator",)
15+
16+
LOG.debug("duckdb_aio version = %s, duckdb_engine = %s (async)", (duckdb.__version__, duckdb_engine.__version__))
17+
18+
19+
class DuckOperator:
20+
"""Create and use an async DuckDB connection through SQLAlchemy."""
21+
22+
def __init__(self, db_config, **kwargs):
23+
"""Build an async DuckDB engine, creating the database file if necessary."""
24+
"""db_config example:
25+
{
26+
"path": "/path/to/db.duckdb", # file path of duckdb, use ":memory:" for in-memory mode
27+
"schema": "sales", # optional, 'main' by default
28+
"read_only": True, # optional, False by default, (will set to False if in in-memory mode)
29+
"config": {"memory_limit": "500mb"}, # optional, duckdb connection configs
30+
}
31+
"""
32+
self._config = {
33+
"path": db_config.get("path", ":memory:"),
34+
"schema": db_config.get("schema", "main"),
35+
"read_only": bool(db_config.get("read_only", False)),
36+
"config": db_config.get("config", {}),
37+
"auto_commit": db_config.get("auto_commit", True),
38+
}
39+
40+
if not self._config["path"] or self._config["path"] == ":memory:":
41+
self._config["path"] = ":memory:"
42+
43+
if self._config["read_only"]:
44+
LOG.warning("In-memory database cannot be read-only. Setting read_only=False.")
45+
self._config["read_only"] = False
46+
47+
else:
48+
self._prepare_database()
49+
50+
try:
51+
str_connection = f"duckdb+aioduckdb:///{self._config['path']}"
52+
self.engine: AsyncEngine = create_async_engine(
53+
str_connection,
54+
connect_args={"read_only": self._config["read_only"], "config": self._config["config"]},
55+
**kwargs,
56+
)
57+
58+
LOG.debug("DuckDB (async) connected: {path} [schema={schema}, read_only={read_only}]".format(**self._config))
59+
except Exception as e:
60+
LOG.exception(e)
61+
raise RuntimeError("Failed to connect to DuckDB (async)") from e
62+
63+
def _prepare_database(self):
64+
"""Prepare the database file and its parent directory."""
65+
path = self._config["path"]
66+
path_obj = Path(path)
67+
68+
parent_dir = path_obj.parent
69+
if not parent_dir.exists():
70+
if self._config["read_only"]:
71+
raise RuntimeError(f"Directory '{parent_dir}' does not exist and read_only=True")
72+
try:
73+
parent_dir.mkdir(parents=True, exist_ok=True)
74+
LOG.debug(f"Created directory: {parent_dir}")
75+
except Exception as e:
76+
raise RuntimeError(f"Failed to create directory '{parent_dir}': {e}") from e
77+
78+
if not path_obj.exists():
79+
if self._config["read_only"]:
80+
raise RuntimeError(f"DuckDB file '{path}' does not exist and read_only=True")
81+
try:
82+
LOG.debug(f"Database file not found, creating: {path}")
83+
duckdb.connect(path).close()
84+
except Exception as e:
85+
raise RuntimeError(f"Failed to create database file '{path}': {e}") from e
86+
87+
@property
88+
def connection(self):
89+
return self.engine
90+
91+
@property
92+
def conn(self):
93+
"""Alias for connection property."""
94+
return self.engine
95+
96+
async def execute_query(self, sql, *args, **kwargs):
97+
"""Execute a SQL statement asynchronously and return the cursor result."""
98+
async with self.engine.connect() as conn:
99+
cur = await conn.execute(text(sql), *args, **kwargs)
100+
if self._config.get("auto_commit", True):
101+
await conn.commit()
102+
return cur
103+
104+
async def execute_query_scalars(self, sql, *args, **kwargs):
105+
"""Execute a SQL statement and return all scalar results."""
106+
async with self.engine.connect() as conn:
107+
cur = await conn.stream_scalars(text(sql), *args, **kwargs)
108+
async for row in cur:
109+
yield row
110+
111+
@property
112+
def connection_str(self) -> str:
113+
"""Return a human-readable connection string."""
114+
return (
115+
f"duckdb:///{self._config['path']} [schema={self._config['schema']}, read_only={self._config['read_only']}] (async)"
116+
)
117+
118+
async def close(self):
119+
"""Close the async engine and all connections."""
120+
await self.engine.dispose()
121+
122+
async def __aenter__(self):
123+
"""Async context manager entry."""
124+
return self
125+
126+
async def __aexit__(self, exc_type, exc_val, exc_tb):
127+
"""Async context manager exit."""
128+
await self.close()

0 commit comments

Comments
 (0)