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,69 @@
1
+ """MySQL connection pool management."""
2
+
3
+ import asyncio
4
+ import logging
5
+ import time
6
+
7
+ try:
8
+ import aiomysql
9
+ except ImportError:
10
+ ...
11
+ from ..abstract.types import ContainerSettings, MysqlPoolSettings
12
+
13
+
14
+ log = logging.getLogger("dotorm")
15
+
16
+
17
+ class ContainerMysql:
18
+ """
19
+ MySQL connection pool container.
20
+
21
+ Manages pool lifecycle.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ pool_settings: MysqlPoolSettings,
27
+ container_settings: ContainerSettings,
28
+ ):
29
+ self.pool_settings = pool_settings
30
+ self.container_settings = container_settings
31
+ self.pool: "aiomysql.Pool | None" = None
32
+
33
+ async def create_pool(self) -> "aiomysql.Pool":
34
+ """Create connection pool with retry on failure."""
35
+ try:
36
+ start_time = time.time()
37
+ self.pool = await aiomysql.create_pool(
38
+ **self.pool_settings.model_dump(),
39
+ minsize=5,
40
+ maxsize=15,
41
+ autocommit=True,
42
+ # 15 minutes
43
+ pool_recycle=60 * 15,
44
+ )
45
+
46
+ log.debug(
47
+ "Connection MySQL db: %s, created time: [%0.3fs]",
48
+ self.pool_settings.db,
49
+ time.time() - start_time,
50
+ )
51
+ return self.pool
52
+
53
+ except (ConnectionError, TimeoutError, aiomysql.OperationalError):
54
+ log.exception(
55
+ "MySQL create pool connection lost, reconnect after %d seconds",
56
+ self.container_settings.reconnect_timeout,
57
+ )
58
+ await asyncio.sleep(self.container_settings.reconnect_timeout)
59
+ return await self.create_pool()
60
+ except Exception as e:
61
+ log.exception("MySQL create pool error:")
62
+ raise e
63
+
64
+ async def close_pool(self):
65
+ """Close connection pool."""
66
+ if self.pool:
67
+ self.pool.close()
68
+ await self.pool.wait_closed()
69
+ self.pool = None
@@ -0,0 +1,128 @@
1
+ """MySQL session implementations."""
2
+
3
+ from typing import Any, Callable, TYPE_CHECKING
4
+
5
+ from ..abstract.session import SessionAbstract
6
+ from ..abstract.dialect import MySQLDialect, CursorType
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ import aiomysql
11
+
12
+
13
+ # Shared dialect instance
14
+ _dialect = MySQLDialect()
15
+
16
+
17
+ class MysqlSession(SessionAbstract):
18
+ """Base MySQL session."""
19
+
20
+ @staticmethod
21
+ async def _do_execute(
22
+ cursor: "aiomysql.Cursor",
23
+ stmt: str,
24
+ values: Any,
25
+ cursor_type: CursorType,
26
+ ) -> Any:
27
+ """
28
+ Execute query on cursor (shared logic).
29
+
30
+ Args:
31
+ cursor: aiomysql cursor
32
+ stmt: SQL with %s placeholders
33
+ values: Query values
34
+ cursor_type: Cursor type
35
+
36
+ Returns:
37
+ Raw result from aiomysql
38
+ """
39
+ # executemany
40
+ if cursor_type == "executemany":
41
+ if not values:
42
+ raise ValueError("executemany requires values")
43
+ await cursor.executemany(stmt, values)
44
+ return None
45
+
46
+ # Execute query
47
+ if values:
48
+ await cursor.execute(stmt, values)
49
+ else:
50
+ await cursor.execute(stmt)
51
+
52
+ # void - execute only (INSERT/UPDATE/DELETE without return)
53
+ if cursor_type == "void":
54
+ return None
55
+
56
+ # lastrowid special case
57
+ if cursor_type == "lastrowid":
58
+ return cursor.lastrowid
59
+
60
+ # fetch operations
61
+ method = getattr(cursor, _dialect.get_cursor_method(cursor_type))
62
+ return await method()
63
+
64
+
65
+ class TransactionSession(MysqlSession):
66
+ """
67
+ Session for transactional queries.
68
+ Uses single connection within transaction context.
69
+ """
70
+
71
+ def __init__(
72
+ self, connection: "aiomysql.Connection", cursor: "aiomysql.Cursor"
73
+ ) -> None:
74
+ self.connection = connection
75
+ self.cursor = cursor
76
+
77
+ async def execute(
78
+ self,
79
+ stmt: str,
80
+ values: Any = None,
81
+ *,
82
+ prepare: Callable | None = None,
83
+ cursor: CursorType = "fetchall",
84
+ ) -> Any:
85
+ stmt = _dialect.convert_placeholders(stmt)
86
+ result = await self._do_execute(self.cursor, stmt, values, cursor)
87
+ result = _dialect.convert_result(result, cursor)
88
+
89
+ if prepare and result:
90
+ return prepare(result)
91
+ return result
92
+
93
+
94
+ class NoTransactionSession(MysqlSession):
95
+ """
96
+ Session for non-transactional queries.
97
+ Acquires connection from pool per query.
98
+ """
99
+
100
+ default_pool: "aiomysql.Pool | None" = None
101
+
102
+ def __init__(self, pool: "aiomysql.Pool | None" = None) -> None:
103
+ if pool is None:
104
+ assert self.default_pool is not None
105
+ self.pool = self.default_pool
106
+ else:
107
+ self.pool = pool
108
+
109
+ async def execute(
110
+ self,
111
+ stmt: str,
112
+ values: Any = None,
113
+ *,
114
+ prepare: Callable | None = None,
115
+ cursor: CursorType = "fetchall",
116
+ ) -> Any:
117
+ import aiomysql
118
+
119
+ stmt = _dialect.convert_placeholders(stmt)
120
+
121
+ async with self.pool.acquire() as conn:
122
+ async with conn.cursor(aiomysql.DictCursor) as cur:
123
+ result = await self._do_execute(cur, stmt, values, cursor)
124
+ result = _dialect.convert_result(result, cursor)
125
+
126
+ if prepare and result:
127
+ return prepare(result)
128
+ return result
@@ -0,0 +1,39 @@
1
+ """MySQL transaction management."""
2
+
3
+ try:
4
+ import aiomysql
5
+ except ImportError:
6
+ aiomysql = None # type: ignore
7
+
8
+ from .session import TransactionSession
9
+
10
+
11
+ class ContainerTransaction:
12
+ """
13
+ Transaction context manager for MySQL.
14
+
15
+ Acquires connection, executes queries,
16
+ commits on success, rollbacks on exception.
17
+ """
18
+
19
+ def __init__(self, pool: "aiomysql.Pool"):
20
+ self.pool = pool
21
+
22
+ async def __aenter__(self):
23
+ connection: "aiomysql.Connection" = await self.pool.acquire()
24
+ cursor: "aiomysql.Cursor" = await connection.cursor(
25
+ aiomysql.DictCursor
26
+ )
27
+ self.session = TransactionSession(connection, cursor)
28
+ return self.session
29
+
30
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
31
+ if exc_type is not None:
32
+ # Выпало исключение вызвать ролбек
33
+ await self.session.connection.rollback()
34
+ else:
35
+ # Не выпало исключение вызвать комит
36
+ await self.session.connection.commit()
37
+ await self.session.cursor.close()
38
+ # В любом случае закрыть соединение и курсор
39
+ self.pool.release(self.session.connection)
@@ -0,0 +1,23 @@
1
+ """PostgreSQL database support."""
2
+
3
+ from .pool import ContainerPostgres
4
+ from .session import (
5
+ PostgresSession,
6
+ TransactionSession,
7
+ NoTransactionSession,
8
+ NoTransactionNoPoolSession,
9
+ )
10
+ from .transaction import ContainerTransaction, get_current_session
11
+ from ..abstract.dialect import CursorType, PostgresDialect
12
+
13
+ __all__ = [
14
+ "ContainerPostgres",
15
+ "PostgresSession",
16
+ "TransactionSession",
17
+ "NoTransactionSession",
18
+ "NoTransactionNoPoolSession",
19
+ "ContainerTransaction",
20
+ "get_current_session",
21
+ "CursorType",
22
+ "PostgresDialect",
23
+ ]
@@ -0,0 +1,133 @@
1
+ """PostgreSQL connection pool management."""
2
+
3
+ import logging
4
+ import asyncio
5
+ import time
6
+
7
+ try:
8
+ import asyncpg
9
+ except ImportError:
10
+ ...
11
+
12
+ from .transaction import ContainerTransaction
13
+ from ..abstract.types import ContainerSettings, PostgresPoolSettings
14
+ from .session import NoTransactionNoPoolSession
15
+
16
+
17
+ log = logging.getLogger("dotorm")
18
+
19
+
20
+ class ContainerPostgres:
21
+ """
22
+ PostgreSQL connection pool container.
23
+
24
+ Manages pool lifecycle and provides utilities
25
+ for database and table creation.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ pool_settings: PostgresPoolSettings,
31
+ container_settings: ContainerSettings,
32
+ ):
33
+ self.pool_settings = pool_settings
34
+ self.container_settings = container_settings
35
+ self.pool: "asyncpg.Pool | None" = None
36
+
37
+ async def create_pool(self) -> "asyncpg.Pool":
38
+ """Create connection pool with retry on failure."""
39
+ try:
40
+ start_time = time.time()
41
+ pool = await asyncpg.create_pool(
42
+ **self.pool_settings.model_dump(),
43
+ min_size=5,
44
+ max_size=15,
45
+ command_timeout=60,
46
+ # 15 minutes
47
+ # max_inactive_connection_lifetime
48
+ # pool_recycle=60 * 15,
49
+ )
50
+ assert pool is not None
51
+ self.pool = pool
52
+
53
+ log.debug(
54
+ "Connection PostgreSQL db: %s, created time: [%0.3fs]",
55
+ self.pool_settings.database,
56
+ time.time() - start_time,
57
+ )
58
+ return self.pool
59
+ except (
60
+ asyncpg.InvalidCatalogNameError,
61
+ asyncpg.ConnectionDoesNotExistError,
62
+ ):
63
+ # БД не существует — пробрасываем наверх для создания
64
+ raise
65
+ except (
66
+ ConnectionError,
67
+ TimeoutError,
68
+ asyncpg.exceptions.ConnectionFailureError,
69
+ ):
70
+ log.exception(
71
+ "Postgres create pool connection lost, reconnect after %d seconds",
72
+ self.container_settings.reconnect_timeout,
73
+ )
74
+ await asyncio.sleep(self.container_settings.reconnect_timeout)
75
+ return await self.create_pool()
76
+ except Exception as e:
77
+ log.exception("Postgres create pool error:")
78
+ raise e
79
+
80
+ async def close_pool(self):
81
+ """Close connection pool."""
82
+ if self.pool:
83
+ # await self.pool.close()
84
+ self.pool.terminate()
85
+ self.pool = None
86
+
87
+ async def create_database(self):
88
+ """Create database if it doesn't exist."""
89
+ db_to_create = self.pool_settings.database
90
+ # Connect to default postgres database
91
+ temp_settings = self.pool_settings.model_copy()
92
+ temp_settings.database = "postgres"
93
+
94
+ conn = await NoTransactionNoPoolSession.get_connection(temp_settings)
95
+ sql = f'CREATE DATABASE "{db_to_create}"'
96
+ try:
97
+ await conn.execute(sql)
98
+ finally:
99
+ await conn.close()
100
+
101
+ async def create_and_update_tables(self, models: list):
102
+ """
103
+ Create/update tables for given models.
104
+
105
+ Args:
106
+ models: List of DotModel classes
107
+ """
108
+ stmt_foreign_keys: list[tuple[str, str]] = []
109
+
110
+ async with ContainerTransaction(self.pool) as session:
111
+ # создаем модели в БД, без FK
112
+ for model in models:
113
+ foreign_keys = await model.__create_table__(session)
114
+ stmt_foreign_keys += foreign_keys
115
+
116
+ if not stmt_foreign_keys:
117
+ return
118
+
119
+ # Дедупликация по имени FK (M2M таблицы могут дублироваться)
120
+ unique_fks = {
121
+ fk_name: fk_sql for fk_name, fk_sql in stmt_foreign_keys
122
+ }
123
+
124
+ # Получаем существующие FK одним запросом
125
+ existing_fk_result = await session.execute(
126
+ "SELECT conname FROM pg_constraint WHERE contype = 'f'"
127
+ )
128
+ existing_fk_names = {row["conname"] for row in existing_fk_result}
129
+
130
+ # Создаём только отсутствующие FK
131
+ for fk_name, fk_sql in unique_fks.items():
132
+ if fk_name not in existing_fk_names:
133
+ await session.execute(fk_sql)
@@ -0,0 +1,174 @@
1
+ """PostgreSQL session implementations."""
2
+
3
+ from typing import Any, Callable, TYPE_CHECKING
4
+
5
+ from ..abstract.types import PostgresPoolSettings
6
+ from ..abstract.session import SessionAbstract
7
+ from ..abstract.dialect import PostgresDialect, CursorType
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ import asyncpg
12
+ from asyncpg.transaction import Transaction
13
+
14
+
15
+ # Shared dialect instance
16
+ _dialect = PostgresDialect()
17
+
18
+
19
+ class PostgresSession(SessionAbstract):
20
+ """Base PostgreSQL session."""
21
+
22
+ @staticmethod
23
+ async def _do_execute(
24
+ conn: "asyncpg.Connection",
25
+ stmt: str,
26
+ values: Any,
27
+ cursor: CursorType,
28
+ ) -> Any:
29
+ """
30
+ Execute query on connection (shared logic).
31
+
32
+ Args:
33
+ conn: asyncpg connection
34
+ stmt: SQL with $1, $2... placeholders (already converted)
35
+ values: Query values
36
+ cursor: Cursor type
37
+
38
+ Returns:
39
+ Raw result from asyncpg
40
+ """
41
+ # executemany
42
+ if cursor == "executemany":
43
+ if not values:
44
+ raise ValueError("executemany requires values")
45
+ # Handle [[v1, v2], [v3, v4]] or [[[v1, v2], [v3, v4]]] format
46
+ rows = (
47
+ values[0]
48
+ if isinstance(values[0], list)
49
+ and values[0]
50
+ and isinstance(values[0][0], (list, tuple))
51
+ else values
52
+ )
53
+ for row in rows:
54
+ await conn.execute(stmt, *row)
55
+ return None
56
+
57
+ # void - execute only (INSERT/UPDATE/DELETE without return)
58
+ if cursor == "void":
59
+ if values:
60
+ await conn.execute(stmt, *values)
61
+ else:
62
+ await conn.execute(stmt)
63
+ return None
64
+
65
+ # fetch operations
66
+ method = getattr(conn, _dialect.get_cursor_method(cursor))
67
+ if values:
68
+ return await method(stmt, *values)
69
+ return await method(stmt)
70
+
71
+
72
+ class TransactionSession(PostgresSession):
73
+ """
74
+ Session for transactional queries.
75
+ Uses single connection within transaction context.
76
+ """
77
+
78
+ def __init__(
79
+ self, connection: "asyncpg.Connection", transaction: "Transaction"
80
+ ) -> None:
81
+ self.connection = connection
82
+ self.transaction = transaction
83
+
84
+ async def execute(
85
+ self,
86
+ stmt: str,
87
+ values: Any = None,
88
+ *,
89
+ prepare: Callable | None = None,
90
+ cursor: CursorType = "fetchall",
91
+ ) -> Any:
92
+ stmt = _dialect.convert_placeholders(stmt)
93
+ result = await self._do_execute(self.connection, stmt, values, cursor)
94
+ result = _dialect.convert_result(result, cursor)
95
+
96
+ if prepare and result:
97
+ return prepare(result)
98
+ return result
99
+
100
+
101
+ class NoTransactionSession(PostgresSession):
102
+ """
103
+ Session for non-transactional queries.
104
+ Acquires connection from pool per query.
105
+ """
106
+
107
+ default_pool: "asyncpg.Pool | None" = None
108
+
109
+ def __init__(self, pool: "asyncpg.Pool | None" = None) -> None:
110
+ if pool is None:
111
+ assert self.default_pool is not None
112
+ self.pool = self.default_pool
113
+ else:
114
+ self.pool = pool
115
+
116
+ async def execute(
117
+ self,
118
+ stmt: str,
119
+ values: Any = None,
120
+ *,
121
+ prepare: Callable | None = None,
122
+ cursor: CursorType = "fetchall",
123
+ ) -> Any:
124
+ stmt = _dialect.convert_placeholders(stmt)
125
+
126
+ async with self.pool.acquire() as conn:
127
+ result = await self._do_execute(conn, stmt, values, cursor)
128
+ result = _dialect.convert_result(result, cursor)
129
+
130
+ if prepare and result:
131
+ return prepare(result)
132
+ return result
133
+
134
+
135
+ class NoTransactionNoPoolSession(PostgresSession):
136
+ """
137
+ Session without pool.
138
+ Opens connection, executes, closes. For admin tasks.
139
+ """
140
+
141
+ @classmethod
142
+ async def get_connection(
143
+ cls, settings: PostgresPoolSettings
144
+ ) -> "asyncpg.Connection":
145
+ """Create new connection without pool."""
146
+ import asyncpg
147
+
148
+ return await asyncpg.connect(**settings.model_dump())
149
+
150
+ @classmethod
151
+ async def execute(
152
+ cls,
153
+ settings: PostgresPoolSettings,
154
+ stmt: str,
155
+ values: Any = None,
156
+ *,
157
+ prepare: Callable | None = None,
158
+ cursor: str = "execute",
159
+ ) -> Any:
160
+ conn = await cls.get_connection(settings)
161
+
162
+ try:
163
+ if values:
164
+ await conn.execute(stmt, values)
165
+ else:
166
+ await conn.execute(stmt)
167
+
168
+ rows = await getattr(conn, cursor)()
169
+
170
+ if prepare:
171
+ return prepare(rows)
172
+ return rows
173
+ finally:
174
+ await conn.close()
@@ -0,0 +1,82 @@
1
+ """PostgreSQL transaction management."""
2
+
3
+ from contextvars import ContextVar
4
+
5
+ try:
6
+ import asyncpg
7
+ from asyncpg.transaction import Transaction
8
+ except ImportError:
9
+ asyncpg = None # type: ignore
10
+ Transaction = None # type: ignore
11
+
12
+ from .session import TransactionSession
13
+
14
+
15
+ # Context variable для хранения текущей сессии транзакции
16
+ _current_session: ContextVar["TransactionSession | None"] = ContextVar(
17
+ "current_session", default=None
18
+ )
19
+
20
+
21
+ def get_current_session() -> "TransactionSession | None":
22
+ """Получить текущую сессию из контекста (если есть активная транзакция)."""
23
+ return _current_session.get()
24
+
25
+
26
+ class ContainerTransaction:
27
+ """
28
+ Transaction context manager for PostgreSQL.
29
+
30
+ Acquires connection, starts transaction, executes queries,
31
+ commits on success, rollbacks on exception.
32
+
33
+ Автоматически устанавливает текущую сессию в contextvars,
34
+ так что методы ORM могут использовать её без явной передачи.
35
+
36
+ Example:
37
+ async with ContainerTransaction(pool) as session:
38
+ await session.execute("INSERT INTO users ...")
39
+ # Или без явной передачи session:
40
+ await User.create(payload=user) # session подставится из контекста
41
+ # Commits on exit
42
+ """
43
+
44
+ default_pool: "asyncpg.Pool | None" = None
45
+
46
+ def __init__(self, pool: "asyncpg.Pool | None" = None):
47
+ self.session_factory = TransactionSession
48
+ if pool is None:
49
+ assert self.default_pool is not None
50
+ self.pool = self.default_pool
51
+ else:
52
+ self.pool = pool
53
+ self._token = None
54
+
55
+ async def __aenter__(self):
56
+ connection: "asyncpg.Connection" = await self.pool.acquire()
57
+ transaction = connection.transaction()
58
+
59
+ assert isinstance(transaction, Transaction)
60
+ assert isinstance(connection, asyncpg.Connection)
61
+
62
+ await transaction.start()
63
+ self.session = self.session_factory(connection, transaction)
64
+
65
+ # Устанавливаем текущую сессию в контекст
66
+ self._token = _current_session.set(self.session)
67
+
68
+ return self.session
69
+
70
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
71
+ # Сбрасываем контекст
72
+ if self._token is not None:
73
+ _current_session.reset(self._token)
74
+
75
+ if exc_type is not None:
76
+ # Выпало исключение вызвать ролбек
77
+ await self.session.transaction.rollback()
78
+ else:
79
+ # Не выпало исключение вызвать комит
80
+ await self.session.transaction.commit()
81
+ # В любом случае вернуть соединение в пул
82
+ await self.pool.release(self.session.connection)