dotorm 2.0.8__py3-none-any.whl

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.
Files changed (50) hide show
  1. dotorm/__init__.py +87 -0
  2. dotorm/access.py +151 -0
  3. dotorm/builder/__init__.py +0 -0
  4. dotorm/builder/builder.py +72 -0
  5. dotorm/builder/helpers.py +63 -0
  6. dotorm/builder/mixins/__init__.py +11 -0
  7. dotorm/builder/mixins/crud.py +246 -0
  8. dotorm/builder/mixins/m2m.py +110 -0
  9. dotorm/builder/mixins/relations.py +96 -0
  10. dotorm/builder/protocol.py +63 -0
  11. dotorm/builder/request_builder.py +144 -0
  12. dotorm/components/__init__.py +18 -0
  13. dotorm/components/dialect.py +99 -0
  14. dotorm/components/filter_parser.py +195 -0
  15. dotorm/databases/__init__.py +13 -0
  16. dotorm/databases/abstract/__init__.py +25 -0
  17. dotorm/databases/abstract/dialect.py +134 -0
  18. dotorm/databases/abstract/pool.py +10 -0
  19. dotorm/databases/abstract/session.py +67 -0
  20. dotorm/databases/abstract/types.py +36 -0
  21. dotorm/databases/clickhouse/__init__.py +8 -0
  22. dotorm/databases/clickhouse/pool.py +60 -0
  23. dotorm/databases/clickhouse/session.py +100 -0
  24. dotorm/databases/mysql/__init__.py +13 -0
  25. dotorm/databases/mysql/pool.py +69 -0
  26. dotorm/databases/mysql/session.py +128 -0
  27. dotorm/databases/mysql/transaction.py +39 -0
  28. dotorm/databases/postgres/__init__.py +23 -0
  29. dotorm/databases/postgres/pool.py +133 -0
  30. dotorm/databases/postgres/session.py +174 -0
  31. dotorm/databases/postgres/transaction.py +82 -0
  32. dotorm/decorators.py +379 -0
  33. dotorm/exceptions.py +9 -0
  34. dotorm/fields.py +604 -0
  35. dotorm/integrations/__init__.py +0 -0
  36. dotorm/integrations/pydantic.py +275 -0
  37. dotorm/model.py +802 -0
  38. dotorm/orm/__init__.py +15 -0
  39. dotorm/orm/mixins/__init__.py +13 -0
  40. dotorm/orm/mixins/access.py +67 -0
  41. dotorm/orm/mixins/ddl.py +250 -0
  42. dotorm/orm/mixins/many2many.py +175 -0
  43. dotorm/orm/mixins/primary.py +218 -0
  44. dotorm/orm/mixins/relations.py +513 -0
  45. dotorm/orm/protocol.py +147 -0
  46. dotorm/orm/utils.py +39 -0
  47. dotorm-2.0.8.dist-info/METADATA +1240 -0
  48. dotorm-2.0.8.dist-info/RECORD +50 -0
  49. dotorm-2.0.8.dist-info/WHEEL +4 -0
  50. dotorm-2.0.8.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,195 @@
1
+ """
2
+ SQL filter expression parser.
3
+
4
+ Converts filter expressions to SQL WHERE clauses.
5
+ Extracted to avoid code duplication in builders.
6
+ """
7
+
8
+ from typing import Any, Literal, Union
9
+
10
+ from .dialect import Dialect
11
+
12
+
13
+ # Type definitions
14
+ SQLOperator = Literal[
15
+ "=",
16
+ ">",
17
+ "<",
18
+ "!=",
19
+ ">=",
20
+ "<=",
21
+ "like",
22
+ "ilike",
23
+ "=like",
24
+ "=ilike",
25
+ "not ilike",
26
+ "not like",
27
+ "in",
28
+ "not in",
29
+ "is null",
30
+ "is not null",
31
+ "between",
32
+ "not between",
33
+ ]
34
+
35
+ FilterTriplet = tuple[str, SQLOperator, Any]
36
+
37
+ FilterExpression = list[
38
+ FilterTriplet
39
+ | tuple[Literal["not"], "FilterExpression"]
40
+ | list[Union["FilterExpression", Literal["and", "or"]]],
41
+ ]
42
+
43
+
44
+ class FilterParser:
45
+ """
46
+ Parses filter expressions into SQL WHERE clauses.
47
+
48
+ Supports:
49
+ - Simple triplets: ("field", "=", value)
50
+ - NOT expressions: ("not", ("field", "=", value))
51
+ - Nested logic: [("a", "=", 1), "or", ("b", "=", 2)]
52
+ - Complex nesting with AND/OR
53
+
54
+ Example:
55
+ parser = FilterParser(POSTGRES)
56
+ clause, values = parser.parse([
57
+ ("active", "=", True),
58
+ "or",
59
+ [("role", "=", "admin"), ("verified", "=", True)]
60
+ ])
61
+ # clause: '"active" = $1 OR ("role" = $2 AND "verified" = $3)'
62
+ # values: (True, "admin", True)
63
+ """
64
+
65
+ def __init__(self, dialect: Dialect):
66
+ self.dialect = dialect
67
+
68
+ def _is_triplet(self, expr: Any) -> bool:
69
+ """Check if expression is a simple triplet."""
70
+ return (
71
+ isinstance(expr, (list, tuple))
72
+ and len(expr) == 3
73
+ and isinstance(expr[0], str)
74
+ )
75
+
76
+ def parse(self, filter_expr: FilterExpression) -> tuple[str, tuple]:
77
+ """Recursively parse filter expression."""
78
+ escape = self.dialect.escape
79
+
80
+ # NOT expression: ("not", expr)
81
+ if (
82
+ isinstance(filter_expr, (list, tuple))
83
+ and len(filter_expr) == 2
84
+ and filter_expr[0] == "not"
85
+ ):
86
+ inner_expr = filter_expr[1]
87
+ clause, values = self.parse(inner_expr) # type: ignore
88
+ return f"NOT ({clause})", values
89
+
90
+ # Simple triplet: ("field", "op", value)
91
+ if self._is_triplet(filter_expr):
92
+ field, op, value = filter_expr
93
+ field = f"{escape}{field}{escape}"
94
+ assert isinstance(op, str)
95
+ op = op.lower()
96
+
97
+ if op in ("in", "not in"):
98
+ if not isinstance(value, (list, tuple)):
99
+ raise ValueError(
100
+ f"Operator '{op}' requires list/tuple value"
101
+ )
102
+ placeholders = ", ".join(["%s"] * len(value))
103
+ clause = f"{field} {op.upper()} ({placeholders})"
104
+ return clause, tuple(value)
105
+
106
+ elif op in (
107
+ "like",
108
+ "ilike",
109
+ "=like",
110
+ "=ilike",
111
+ "not like",
112
+ "not ilike",
113
+ ):
114
+ clause = f"{field} {op.upper()} %s"
115
+ return clause, ("%" + str(value) + "%",)
116
+
117
+ elif op in ("=", "!=", ">", "<", ">=", "<="):
118
+ # None -> IS NULL / IS NOT NULL
119
+ if value is None:
120
+ if op == "=":
121
+ return f"{field} IS NULL", ()
122
+ elif op == "!=":
123
+ return f"{field} IS NOT NULL", ()
124
+ else:
125
+ raise ValueError(
126
+ f"Operator '{op}' cannot be used with None"
127
+ )
128
+ clause = f"{field} {op} %s"
129
+ return clause, (value,)
130
+
131
+ elif op == "is null":
132
+ return f"{field} IS NULL", ()
133
+
134
+ elif op == "is not null":
135
+ return f"{field} IS NOT NULL", ()
136
+
137
+ elif op in ("between", "not between"):
138
+ if not isinstance(value, (list, tuple)) or len(value) != 2:
139
+ raise ValueError(
140
+ f"Operator '{op}' requires list of two values"
141
+ )
142
+ clause = f"{field} {op.upper()} %s AND %s"
143
+ return clause, (value[0], value[1])
144
+
145
+ else:
146
+ raise ValueError(f"Unsupported operator: {op}")
147
+
148
+ # Nested expression: list with possible AND/OR
149
+ elif isinstance(filter_expr, list):
150
+ parts: list[tuple[str, str, bool]] = []
151
+ values: list[Any] = []
152
+
153
+ i = 0
154
+ while i < len(filter_expr):
155
+ item = filter_expr[i]
156
+
157
+ if isinstance(item, (list, tuple)):
158
+ clause, clause_values = self.parse(item) # type: ignore
159
+ wrap = not self._is_triplet(item)
160
+ parts.append(("EXPR", clause, wrap))
161
+ values.extend(clause_values)
162
+ i += 1
163
+
164
+ elif isinstance(item, str) and item.lower() in ("and", "or"):
165
+ parts.append(("OP", item.upper(), False))
166
+ i += 1
167
+
168
+ else:
169
+ raise ValueError(
170
+ f"Invalid filter element at position {i}: {item}"
171
+ )
172
+
173
+ # Auto-insert AND between consecutive expressions
174
+ normalized: list[tuple[str, str, bool]] = []
175
+ for idx, part in enumerate(parts):
176
+ if (
177
+ idx > 0
178
+ and part[0] == "EXPR"
179
+ and parts[idx - 1][0] == "EXPR"
180
+ ):
181
+ normalized.append(("OP", "AND", False))
182
+ normalized.append(part)
183
+
184
+ sql_parts: list[str] = []
185
+ for part in normalized:
186
+ kind, content, wrap = part
187
+ if kind == "EXPR":
188
+ sql_parts.append(f"({content})" if wrap else content)
189
+ elif kind == "OP":
190
+ sql_parts.append(content)
191
+
192
+ return " ".join(sql_parts), tuple(values)
193
+
194
+ else:
195
+ raise ValueError("Unsupported filter expression format")
@@ -0,0 +1,13 @@
1
+ """Database drivers."""
2
+
3
+ from .abstract import (
4
+ ContainerSettings,
5
+ PostgresPoolSettings,
6
+ MysqlPoolSettings,
7
+ )
8
+
9
+ __all__ = [
10
+ "ContainerSettings",
11
+ "PostgresPoolSettings",
12
+ "MysqlPoolSettings",
13
+ ]
@@ -0,0 +1,25 @@
1
+ """Abstract database interfaces."""
2
+
3
+ from .pool import PoolAbstract
4
+ from .session import SessionAbstract
5
+ from .dialect import Dialect, PostgresDialect, MySQLDialect, ClickHouseDialect, CursorType
6
+ from .types import (
7
+ ContainerSettings,
8
+ PostgresPoolSettings,
9
+ MysqlPoolSettings,
10
+ ClickhousePoolSettings,
11
+ )
12
+
13
+ __all__ = [
14
+ "PoolAbstract",
15
+ "SessionAbstract",
16
+ "Dialect",
17
+ "PostgresDialect",
18
+ "MySQLDialect",
19
+ "ClickHouseDialect",
20
+ "CursorType",
21
+ "ContainerSettings",
22
+ "PostgresPoolSettings",
23
+ "MysqlPoolSettings",
24
+ "ClickhousePoolSettings",
25
+ ]
@@ -0,0 +1,134 @@
1
+ """Database dialect abstraction (Strategy pattern)."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Literal
5
+
6
+
7
+ # Unified cursor types for all dialects
8
+ CursorType = Literal[
9
+ "fetchall",
10
+ "fetch",
11
+ "fetchrow",
12
+ "fetchval",
13
+ "executemany",
14
+ "lastrowid", # MySQL-specific
15
+ "void", # Execute without returning results
16
+ ]
17
+
18
+
19
+ class Dialect(ABC):
20
+ """
21
+ Abstract dialect defining database-specific behavior.
22
+
23
+ Each database has its own dialect that handles:
24
+ - Placeholder conversion (%s → $1 for Postgres, %s stays for MySQL)
25
+ - Cursor method mapping (fetchall → fetch for asyncpg)
26
+ - Result conversion (Record → dict)
27
+ """
28
+
29
+ _cursor_map: dict[str, str]
30
+
31
+ def convert_placeholders(self, stmt: str) -> str:
32
+ """Convert %s placeholders to database-specific format."""
33
+ return stmt
34
+
35
+ def get_cursor_method(self, cursor: CursorType) -> str:
36
+ """Map CursorType to actual driver method name."""
37
+ return self._cursor_map.get(cursor, "void")
38
+
39
+ @abstractmethod
40
+ def convert_result(self, rows: Any, cursor: CursorType) -> Any:
41
+ """Convert raw database result to standard format (list of dicts, etc)."""
42
+ ...
43
+
44
+
45
+ class PostgresDialect(Dialect):
46
+ """PostgreSQL dialect for asyncpg driver."""
47
+
48
+ _cursor_map = {
49
+ "fetchall": "fetch",
50
+ "fetch": "fetch",
51
+ "fetchrow": "fetchrow",
52
+ "fetchval": "fetchval",
53
+ }
54
+
55
+ def convert_placeholders(self, stmt: str) -> str:
56
+ """Convert %s to $1, $2, $3..."""
57
+ counter = 1
58
+ while "%s" in stmt:
59
+ stmt = stmt.replace("%s", f"${counter}", 1)
60
+ counter += 1
61
+ return stmt
62
+
63
+ def convert_result(self, rows: Any, cursor: CursorType) -> Any:
64
+ """Convert asyncpg Record objects to dicts."""
65
+ if rows is None or cursor in ("void", "executemany"):
66
+ return rows
67
+
68
+ if cursor == "fetchval":
69
+ return rows # Single value
70
+
71
+ if cursor == "fetchrow":
72
+ return dict(rows) if rows else None
73
+
74
+ # fetchall/fetch
75
+ return [dict(rec) for rec in rows] if rows else []
76
+
77
+
78
+ class MySQLDialect(Dialect):
79
+ """MySQL dialect for aiomysql driver."""
80
+
81
+ _cursor_map = {
82
+ "fetchall": "fetchall",
83
+ "fetch": "fetchall",
84
+ "fetchrow": "fetchone",
85
+ "fetchval": "fetchone",
86
+ }
87
+
88
+ def convert_result(self, rows: Any, cursor: CursorType) -> Any:
89
+ """Convert MySQL results."""
90
+ if rows is None or cursor in ("void", "executemany", "lastrowid"):
91
+ return rows
92
+
93
+ if cursor == "fetchval":
94
+ # fetchone returns tuple, get first element
95
+ return (
96
+ rows[0] if rows and isinstance(rows, (tuple, list)) else rows
97
+ )
98
+
99
+ if cursor == "fetchrow":
100
+ return dict(rows) if rows else None
101
+
102
+ # fetchall/fetch
103
+ return [dict(rec) for rec in rows] if rows else []
104
+
105
+
106
+ class ClickHouseDialect(Dialect):
107
+ """ClickHouse dialect for asynch driver."""
108
+
109
+ _cursor_map = {
110
+ "fetchall": "fetchall",
111
+ "fetch": "fetchall",
112
+ "fetchrow": "fetchone",
113
+ "fetchval": "fetchone",
114
+ }
115
+
116
+ def convert_result(self, rows: Any, cursor: CursorType) -> Any:
117
+ """Convert ClickHouse results to dicts."""
118
+ if rows is None or cursor in ("void", "executemany"):
119
+ return rows
120
+
121
+ if cursor == "fetchval":
122
+ # fetchone returns tuple, get first element
123
+ if rows and isinstance(rows, (tuple, list)):
124
+ return rows[0]
125
+ return rows
126
+
127
+ if cursor == "fetchrow":
128
+ # Single row - convert to dict if tuple with column info
129
+ return dict(rows) if rows and hasattr(rows, "_fields") else rows
130
+
131
+ # fetchall/fetch - list of tuples
132
+ if rows and hasattr(rows[0], "_fields"):
133
+ return [dict(rec._asdict()) for rec in rows]
134
+ return rows if rows else []
@@ -0,0 +1,10 @@
1
+ """Abstract pool interface."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class PoolAbstract(ABC):
7
+ @abstractmethod
8
+ async def create_pool(
9
+ self,
10
+ ): ...
@@ -0,0 +1,67 @@
1
+ """Abstract session interface."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Callable, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from .dialect import CursorType
8
+
9
+
10
+ class SessionAbstract(ABC):
11
+ """
12
+ Abstract database session.
13
+
14
+ Subclasses must implement execute() method.
15
+ Use dialect helper for SQL transformations.
16
+ """
17
+
18
+ @abstractmethod
19
+ async def execute(
20
+ self,
21
+ stmt: str,
22
+ values: Any = None,
23
+ *,
24
+ prepare: Callable | None = None,
25
+ cursor: "CursorType" = "fetchall",
26
+ ) -> Any:
27
+ """
28
+ Execute SQL query.
29
+
30
+ Args:
31
+ stmt: SQL statement with %s placeholders
32
+ values: Query parameters:
33
+ - Tuple/list for single query
34
+ - List of tuples for executemany
35
+ prepare: Optional function to transform results
36
+ cursor: Fetch mode:
37
+ - "fetchall"/"fetch": Return list of dicts
38
+ - "fetchrow": Return single dict or None
39
+ - "fetchval": Return single value or None
40
+ - "executemany": Execute multiple inserts
41
+ - "lastrowid": Return last inserted row ID (MySQL only)
42
+ - "void": Execute without returning rows (INSERT/UPDATE/DELETE)
43
+
44
+ Returns:
45
+ Query results based on cursor mode
46
+
47
+ Example:
48
+ # Fetch all rows
49
+ rows = await session.execute("SELECT * FROM users WHERE active = %s", (True,))
50
+
51
+ # Fetch single row
52
+ user = await session.execute("SELECT * FROM users WHERE id = %s", (1,), cursor="fetchrow")
53
+
54
+ # Fetch single value
55
+ count = await session.execute("SELECT COUNT(*) FROM users", cursor="fetchval")
56
+
57
+ # Execute without return
58
+ await session.execute("UPDATE users SET active = %s", (False,), cursor="void")
59
+
60
+ # Execute many
61
+ await session.execute(
62
+ "INSERT INTO users (name) VALUES (%s)",
63
+ [("Alice",), ("Bob",)],
64
+ cursor="executemany"
65
+ )
66
+ """
67
+ ...
@@ -0,0 +1,36 @@
1
+ """Database configuration types."""
2
+
3
+ from typing import Literal
4
+ from pydantic_settings import BaseSettings
5
+
6
+
7
+ class ContainerSettings(BaseSettings):
8
+ reconnect_timeout: int = 10
9
+ driver: Literal["asynch", "aiomysql", "asyncpg"]
10
+ ssl: str = ""
11
+ sync_db: bool = False
12
+
13
+
14
+ class PostgresPoolSettings(BaseSettings):
15
+ host: str
16
+ port: int
17
+ user: str
18
+ password: str
19
+ database: str
20
+
21
+
22
+ class ClickhousePoolSettings(BaseSettings):
23
+ host: str
24
+ port: int
25
+ user: str
26
+ password: str
27
+ database: str
28
+
29
+
30
+ # настройки драйвера для mysql отличаются
31
+ class MysqlPoolSettings(BaseSettings):
32
+ host: str
33
+ port: int
34
+ user: str
35
+ password: str
36
+ db: str
@@ -0,0 +1,8 @@
1
+ """ClickHouse database support."""
2
+
3
+ from .session import ClickhouseSession, NoTransactionSession
4
+
5
+ __all__ = [
6
+ "ClickhouseSession",
7
+ "NoTransactionSession",
8
+ ]
@@ -0,0 +1,60 @@
1
+ import asyncio
2
+ import logging
3
+ import time
4
+
5
+ try:
6
+ import asynch
7
+ except ImportError:
8
+ ...
9
+
10
+ from ..abstract.types import (
11
+ ClickhousePoolSettings,
12
+ ContainerSettings,
13
+ )
14
+
15
+ log = logging.getLogger(__package__)
16
+
17
+
18
+ class ContainerClickhouse:
19
+ def __init__(
20
+ self,
21
+ pool_settings: ClickhousePoolSettings,
22
+ container_settings: ContainerSettings,
23
+ ):
24
+ self.pool_settings = pool_settings
25
+ self.container_settings = container_settings
26
+
27
+ async def create_pool(self):
28
+ try:
29
+ start_time: float = time.time()
30
+ pool = await asynch.create_pool(
31
+ **self.pool_settings.model_dump(),
32
+ min_size=5,
33
+ max_size=15,
34
+ # command_timeout=60,
35
+ # 15 minutes
36
+ # max_inactive_connection_lifetime
37
+ # pool_recycle=60 * 15,
38
+ )
39
+ assert isinstance(pool, asynch.Pool)
40
+ assert pool is not None
41
+ self.pool = pool
42
+ start_time: float = time.time()
43
+
44
+ log.debug(
45
+ "Connection Clickhouse db: %s, created time: [%0.3fs]",
46
+ self.pool_settings.database,
47
+ time.time() - start_time,
48
+ )
49
+ return self.pool
50
+ except (ConnectionError, TimeoutError):
51
+ # Если не смогли подключиться к базе пробуем переподключиться
52
+ log.exception(
53
+ "Clickhouse create poll connection lost, reconnect after 10 seconds: "
54
+ )
55
+ await asyncio.sleep(self.container_settings.reconnect_timeout)
56
+ await self.create_pool()
57
+ except Exception as e:
58
+ # если ошибка не связанна с сетью, завершаем выполнение программы
59
+ log.exception("Clickhouse create pool error:")
60
+ raise e
@@ -0,0 +1,100 @@
1
+ """ClickHouse session implementations."""
2
+
3
+ from typing import Any, Callable, TYPE_CHECKING
4
+
5
+ from ..abstract.session import SessionAbstract
6
+ from ..abstract.dialect import ClickHouseDialect, CursorType
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ import asynch
11
+ from asynch.cursors import Cursor
12
+
13
+
14
+ # Shared dialect instance
15
+ _dialect = ClickHouseDialect()
16
+
17
+
18
+ class ClickhouseSession(SessionAbstract):
19
+ """Base ClickHouse session."""
20
+
21
+ @staticmethod
22
+ async def _do_execute(
23
+ cursor: "Cursor",
24
+ stmt: str,
25
+ values: Any,
26
+ cursor_type: CursorType,
27
+ ) -> Any:
28
+ """
29
+ Execute query on cursor (shared logic).
30
+
31
+ Args:
32
+ cursor: asynch cursor
33
+ stmt: SQL with %s placeholders
34
+ values: Query values
35
+ cursor_type: Cursor type
36
+
37
+ Returns:
38
+ Raw result from asynch
39
+ """
40
+ # executemany
41
+ if cursor_type == "executemany":
42
+ if not values:
43
+ raise ValueError("executemany requires values")
44
+ for row in values:
45
+ await cursor.execute(stmt, row)
46
+ return None
47
+
48
+ # Execute query
49
+ if values:
50
+ await cursor.execute(stmt, values)
51
+ else:
52
+ await cursor.execute(stmt)
53
+
54
+ # void - execute only (INSERT without return)
55
+ if cursor_type == "void":
56
+ return None
57
+
58
+ # fetch operations
59
+ method_name = _dialect.get_cursor_method(cursor_type)
60
+ if method_name:
61
+ method = getattr(cursor, method_name)
62
+ return await method()
63
+ return None
64
+
65
+
66
+ class NoTransactionSession(ClickhouseSession):
67
+ """
68
+ Session for non-transactional queries.
69
+ Acquires connection from pool per query.
70
+
71
+ Note: ClickHouse doesn't support transactions.
72
+ """
73
+
74
+ default_pool: "asynch.Pool | None" = None
75
+
76
+ def __init__(self, pool: "asynch.Pool | None" = None) -> None:
77
+ if pool is None:
78
+ assert self.default_pool is not None
79
+ self.pool = self.default_pool
80
+ else:
81
+ self.pool = pool
82
+
83
+ async def execute(
84
+ self,
85
+ stmt: str,
86
+ values: Any = None,
87
+ *,
88
+ prepare: Callable | None = None,
89
+ cursor: CursorType = "fetchall",
90
+ ) -> Any:
91
+ stmt = _dialect.convert_placeholders(stmt)
92
+
93
+ async with self.pool.acquire() as conn:
94
+ async with conn.cursor() as cur:
95
+ result = await self._do_execute(cur, stmt, values, cursor)
96
+ result = _dialect.convert_result(result, cursor)
97
+
98
+ if prepare and result:
99
+ return prepare(result)
100
+ return result
@@ -0,0 +1,13 @@
1
+ """MySQL database support."""
2
+
3
+ from .pool import ContainerMysql
4
+ from .session import MysqlSession, TransactionSession, NoTransactionSession
5
+ from .transaction import ContainerTransaction
6
+
7
+ __all__ = [
8
+ "ContainerMysql",
9
+ "MysqlSession",
10
+ "TransactionSession",
11
+ "NoTransactionSession",
12
+ "ContainerTransaction",
13
+ ]