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,513 @@
1
+ """Relations ORM operations mixin."""
2
+
3
+ from typing import TYPE_CHECKING, Any, Literal, Self, TypeVar
4
+
5
+ from ...components.filter_parser import FilterExpression
6
+ from ...decorators import hybridmethod
7
+ from ...access import Operation
8
+ from ...builder.request_builder import (
9
+ RequestBuilderForm,
10
+ )
11
+ from ...fields import (
12
+ AttachmentMany2one,
13
+ AttachmentOne2many,
14
+ Many2many,
15
+ Many2one,
16
+ One2many,
17
+ One2one,
18
+ )
19
+ from ..utils import execute_maybe_parallel
20
+
21
+ if TYPE_CHECKING:
22
+ from ..protocol import DotModelProtocol
23
+ from ...model import DotModel
24
+
25
+ _Base = DotModelProtocol
26
+ else:
27
+ _Base = object
28
+
29
+ # TypeVar for generic payload - accepts any DotModel subclass
30
+ _M = TypeVar("_M", bound="DotModel")
31
+
32
+
33
+ class OrmRelationsMixin(_Base):
34
+ """
35
+ Mixin providing ORM operations for relations.
36
+
37
+ Provides:
38
+ - search - search records with relation loading
39
+ - search_count - count records matching filter
40
+ - exists - have one record or not
41
+ - get_with_relations - get single record with relations
42
+ - update_with_relations - update record with relations
43
+
44
+ Expects DotModel to provide:
45
+ - _get_db_session()
46
+ - _builder
47
+ - _dialect
48
+ - __table__
49
+ - get_fields(), get_store_fields(), get_relation_fields()
50
+ - get_relation_fields_m2m_o2m(), get_relation_fields_attachment()
51
+ - prepare_list_ids()
52
+ - get_many2many(), link_many2many(), unlink_many2many()
53
+ - _records_list_get_relation()
54
+ - update()
55
+ """
56
+
57
+ @hybridmethod
58
+ async def search(
59
+ self,
60
+ fields: list[str] = ["id"],
61
+ start: int | None = None,
62
+ end: int | None = None,
63
+ limit: int = 1000,
64
+ order: Literal["DESC", "ASC", "desc", "asc"] = "DESC",
65
+ sort: str = "id",
66
+ filter: FilterExpression | None = None,
67
+ raw: bool = False,
68
+ session=None,
69
+ ) -> list[Self]:
70
+ cls = self.__class__
71
+
72
+ # Access check + apply domain filter
73
+ filter = await cls._check_access(Operation.READ, filter=filter)
74
+
75
+ session = cls._get_db_session(session)
76
+
77
+ # Use dialect from class
78
+ dialect = cls._dialect
79
+
80
+ stmt, values = cls._builder.build_search(
81
+ fields, start, end, limit, order, sort, filter
82
+ )
83
+ prepare = cls.prepare_list_ids if not raw else None
84
+ records: list[Self] = await session.execute(
85
+ stmt, values, prepare=prepare
86
+ )
87
+
88
+ # если есть хоть одна запись и вообще нужно читать поля связей
89
+ fields_relation = [
90
+ (name, field)
91
+ for name, field in cls.get_relation_fields()
92
+ if name in fields
93
+ ]
94
+ if records and fields_relation:
95
+ await cls._records_list_get_relation(
96
+ session, fields_relation, records
97
+ )
98
+
99
+ return records
100
+
101
+ @hybridmethod
102
+ async def search_count(
103
+ self,
104
+ filter: FilterExpression | None = None,
105
+ session=None,
106
+ ) -> int:
107
+ """
108
+ Count records matching the filter.
109
+
110
+ Args:
111
+ filter: Filter expression
112
+ session: Database session
113
+
114
+ Returns:
115
+ Number of matching records
116
+ """
117
+ cls = self.__class__
118
+ session = cls._get_db_session(session)
119
+
120
+ stmt, values = cls._builder.build_search_count(filter)
121
+ result = await session.execute(stmt, values)
122
+
123
+ if result and len(result) > 0:
124
+ return result[0].get("count", 0)
125
+ return 0
126
+
127
+ @hybridmethod
128
+ async def exists(
129
+ self,
130
+ filter: FilterExpression | None = None,
131
+ session=None,
132
+ ) -> bool:
133
+ """
134
+ Check if any record matches the filter.
135
+
136
+ More efficient than search_count for existence checks.
137
+
138
+ Args:
139
+ filter: Filter expression
140
+ session: Database session
141
+
142
+ Returns:
143
+ True if at least one record exists
144
+ """
145
+ cls = self.__class__
146
+ session = cls._get_db_session(session)
147
+
148
+ stmt, values = cls._builder.build_exists(filter)
149
+ result = await session.execute(stmt, values)
150
+
151
+ return bool(result)
152
+
153
+ @classmethod
154
+ async def get_with_relations(
155
+ cls,
156
+ id,
157
+ fields=None,
158
+ fields_info={},
159
+ session=None,
160
+ ) -> Self:
161
+ """Get record with relations loaded."""
162
+ if not fields:
163
+ fields = []
164
+ session = cls._get_db_session(session)
165
+
166
+ dialect = cls._dialect
167
+
168
+ # защита, оставить только те поля, которые действительно хранятся в базе
169
+ fields_store = [
170
+ name for name in cls.get_store_fields() if name in fields
171
+ ]
172
+ # если вдруг они не заданы, или таких нет, взять все
173
+ if not fields_store:
174
+ fields_store = [name for name in cls.get_store_fields()]
175
+ if "id" not in fields_store:
176
+ fields_store.append("id")
177
+
178
+ stmt, values = cls._builder.build_get(id, fields_store)
179
+ record_raw: list[Any] = await session.execute(stmt, values)
180
+ if not record_raw:
181
+ raise ValueError("Record not found")
182
+ record = cls(**record_raw[0])
183
+
184
+ # защита, оставить только те поля, которые являются отношениями (m2m, o2m, m2o)
185
+ # добавлена информаци о вложенных полях
186
+ fields_relation = [
187
+ (name, field, fields_info.get(name))
188
+ for name, field in cls.get_relation_fields()
189
+ if name in fields
190
+ ]
191
+
192
+ # если есть хоть одна запись и вообще нужно читать поля связей
193
+ if record and fields_relation:
194
+ request_list = []
195
+ execute_list = []
196
+
197
+ # добавить запрос на o2m
198
+ for name, field, fields_nested in fields_relation:
199
+ relation_table = field.relation_table
200
+ relation_table_field = field.relation_table_field
201
+
202
+ if not fields_nested and relation_table:
203
+ fields_select = ["id"]
204
+ if relation_table.get_fields().get("name"):
205
+ fields_select.append("name")
206
+ if isinstance(field, AttachmentMany2one):
207
+ fields_select = (
208
+ relation_table.get_store_fields_omit_m2o()
209
+ )
210
+ else:
211
+ fields_select = fields_nested
212
+
213
+ if (
214
+ isinstance(field, (Many2one, AttachmentMany2one))
215
+ and relation_table
216
+ ):
217
+ m2o_id = getattr(record, name)
218
+ stmt, val = relation_table._builder.build_get(
219
+ m2o_id, fields=fields_select
220
+ )
221
+ req = RequestBuilderForm(
222
+ stmt=stmt,
223
+ value=val,
224
+ field_name=name,
225
+ field=field,
226
+ fields=fields_select,
227
+ )
228
+ request_list.append(req)
229
+ execute_list.append(
230
+ session.execute(
231
+ req.stmt,
232
+ req.value,
233
+ prepare=req.function_prepare,
234
+ cursor=req.function_cursor,
235
+ )
236
+ )
237
+ # если m2m или o2m необходимо посчитать длину, для пагинации
238
+ if isinstance(field, Many2many):
239
+ params = {
240
+ "id": record.id,
241
+ "comodel": relation_table,
242
+ "relation": field.many2many_table,
243
+ "column1": field.column1,
244
+ "column2": field.column2,
245
+ "fields": fields_select,
246
+ "order": "desc",
247
+ "start": 0,
248
+ "end": 40,
249
+ "sort": "id",
250
+ "limit": 40,
251
+ }
252
+ # records
253
+ execute_list.append(cls.get_many2many(**params))
254
+ params["fields"] = ["id"]
255
+ params["start"] = None
256
+ params["end"] = None
257
+ params["limit"] = None
258
+ # len
259
+ execute_list.append(cls.get_many2many(**params))
260
+ req = RequestBuilderForm(
261
+ stmt=None,
262
+ value=None,
263
+ field_name=name,
264
+ field=field,
265
+ fields=fields_select,
266
+ )
267
+ request_list.append(req)
268
+
269
+ if isinstance(field, One2many) and relation_table:
270
+ params = {
271
+ "start": 0,
272
+ "end": 40,
273
+ "limit": 40,
274
+ "fields": fields_select,
275
+ "filter": [(relation_table_field, "=", record.id)],
276
+ }
277
+ execute_list.append(relation_table.search(**params))
278
+ params["fields"] = ["id"]
279
+ params["start"] = None
280
+ params["end"] = None
281
+ params["limit"] = 1000
282
+ execute_list.append(relation_table.search(**params))
283
+ req = RequestBuilderForm(
284
+ stmt=None,
285
+ value=None,
286
+ field_name=name,
287
+ field=field,
288
+ fields=fields_select,
289
+ )
290
+ request_list.append(req)
291
+
292
+ if isinstance(field, AttachmentOne2many) and relation_table:
293
+ params = {
294
+ "start": 0,
295
+ "end": 40,
296
+ "limit": 40,
297
+ "fields": relation_table.get_store_fields_omit_m2o(),
298
+ "filter": [
299
+ ("res_id", "=", record.id),
300
+ ("res_model", "=", record.__table__),
301
+ ],
302
+ }
303
+ execute_list.append(relation_table.search(**params))
304
+ params["fields"] = ["id"]
305
+ params["start"] = None
306
+ params["end"] = None
307
+ params["limit"] = 1000
308
+ execute_list.append(relation_table.search(**params))
309
+ req = RequestBuilderForm(
310
+ stmt=None,
311
+ value=None,
312
+ field_name=name,
313
+ field=field,
314
+ fields=relation_table.get_store_fields_omit_m2o(),
315
+ )
316
+ request_list.append(req)
317
+
318
+ if isinstance(field, One2one) and relation_table:
319
+ params = {
320
+ "limit": 1,
321
+ "fields": fields_select,
322
+ "filter": [(relation_table_field, "=", record.id)],
323
+ }
324
+ execute_list.append(relation_table.search(**params))
325
+ req = RequestBuilderForm(
326
+ stmt=None,
327
+ value=None,
328
+ field_name=name,
329
+ field=field,
330
+ fields=fields_select,
331
+ )
332
+ request_list.append(req)
333
+
334
+ # выполняем последовательно в транзакции, параллельно вне транзакции
335
+ results = await execute_maybe_parallel(execute_list)
336
+
337
+ # добавляем атрибуты к исходному объекту,
338
+ # получая удобное обращение через дот-нотацию
339
+ i = 0
340
+ for request_builder in request_list:
341
+ result = results[i]
342
+
343
+ if isinstance(
344
+ request_builder.field,
345
+ (Many2one, AttachmentMany2one, One2one),
346
+ ):
347
+ # m2o нужно распаковать так как он тоже в списке
348
+ # если пустой список, то установить None
349
+ result = result[0] if result else None
350
+
351
+ if isinstance(
352
+ request_builder.field,
353
+ (Many2many, One2many, AttachmentOne2many),
354
+ ):
355
+ # если m2m или o2m необбзодимо взять два результатата
356
+ # так как один из них это число всех строк таблицы
357
+ # для пагинации
358
+ fields_info = request_builder.field.relation_table.get_fields_info_list(
359
+ request_builder.fields
360
+ )
361
+ result = {
362
+ "data": result,
363
+ "fields": fields_info,
364
+ "total": len(results[i + 1]),
365
+ }
366
+ i += 1
367
+
368
+ setattr(record, request_builder.field_name, result)
369
+ i += 1
370
+
371
+ return record
372
+
373
+ async def update_with_relations(
374
+ self, payload: _M, fields=[], session=None
375
+ ):
376
+ """Update record with relations."""
377
+ session = self._get_db_session(session)
378
+
379
+ # Handle attachments
380
+ fields_attachments = [
381
+ (name, field)
382
+ for name, field in self.get_relation_fields_attachment()
383
+ if name in fields
384
+ ]
385
+
386
+ if fields_attachments:
387
+ for name, field in fields_attachments:
388
+ if isinstance(field, AttachmentMany2one):
389
+ field_obj = getattr(payload, name)
390
+ if field_obj and field.relation_table:
391
+ # TODO: всегда создавать новую строку аттачмент с файлом
392
+ # также надо продумать механизм обновления уже существующего файла
393
+ # надо ли? или проще удалять
394
+ field_obj["res_id"] = self.id
395
+ # Оборачиваем dict в объект модели
396
+ attachment_payload = field.relation_table(**field_obj)
397
+ attachment_id = await field.relation_table.create(
398
+ attachment_payload, session
399
+ )
400
+ setattr(payload, name, attachment_id)
401
+
402
+ # Update stored fields
403
+ fields_store = [
404
+ name for name in self.get_store_fields() if name in fields
405
+ ]
406
+ # Обновление сущности в базе без связей
407
+ if fields_store:
408
+ record_raw = await self.update(payload, fields, session)
409
+
410
+ # защита, оставить только те поля, которые являются отношениями (m2m, o2m)
411
+ # добавлена информаци о вложенных полях
412
+ fields_relation = [
413
+ (name, field)
414
+ for name, field in self.get_relation_fields_m2m_o2m()
415
+ if name in fields
416
+ ]
417
+
418
+ if fields_relation:
419
+ request_list = []
420
+ field_list = []
421
+
422
+ for name, field in fields_relation:
423
+ field_obj = getattr(payload, name)
424
+
425
+ if isinstance(field, One2one):
426
+ params = {
427
+ "limit": 1,
428
+ "fields": ["id"],
429
+ "filter": [(field.relation_table_field, "=", self.id)],
430
+ }
431
+ record = await field.relation_table.search(**params)
432
+ if len(record):
433
+ request_list.append(record[0].update(field_obj))
434
+
435
+ if isinstance(field, (One2many, AttachmentOne2many)):
436
+ field_list.append(field)
437
+ # заменить в связанных полях виртуальный ид на вновь созданный
438
+ for obj in field_obj["created"]:
439
+ for k, v in obj.items():
440
+ f = getattr(field.relation_table, k)
441
+ if (
442
+ isinstance(f, (Many2one, AttachmentMany2one))
443
+ and v == "VirtualId"
444
+ ):
445
+ obj[k] = self.id
446
+
447
+ data_created = [
448
+ field.relation_table(**obj)
449
+ for obj in field_obj["created"]
450
+ ]
451
+
452
+ if isinstance(field, AttachmentOne2many):
453
+ for obj in data_created:
454
+ obj.res_id = self.id
455
+
456
+ if field_obj["created"]:
457
+ request_list.append(
458
+ field.relation_table.create_bulk(data_created)
459
+ )
460
+ if field_obj["deleted"]:
461
+ request_list.append(
462
+ field.relation_table.delete_bulk(
463
+ field_obj["deleted"]
464
+ )
465
+ )
466
+
467
+ if isinstance(field, Many2many):
468
+ field_list.append(field)
469
+
470
+ # Replace virtual ID
471
+ for obj in field_obj["created"]:
472
+ for k, v in obj.items():
473
+ f = getattr(field.relation_table, k)
474
+ if (
475
+ isinstance(f, (Many2one, AttachmentMany2one))
476
+ and v == "VirtualId"
477
+ ):
478
+ obj[k] = self.id
479
+
480
+ data_created = [
481
+ field.relation_table(**obj)
482
+ for obj in field_obj["created"]
483
+ ]
484
+
485
+ if field_obj["created"]:
486
+ created_ids = await field.relation_table.create_bulk(
487
+ data_created
488
+ )
489
+ if "selected" not in field_obj:
490
+ field_obj["selected"] = []
491
+ field_obj["selected"] += [
492
+ rec["id"] for rec in created_ids
493
+ ]
494
+
495
+ if field_obj.get("selected"):
496
+ data_selected = [
497
+ (self.id, id) for id in field_obj["selected"]
498
+ ]
499
+ request_list.append(
500
+ self.link_many2many(field, data_selected)
501
+ )
502
+
503
+ if field_obj.get("unselected"):
504
+ request_list.append(
505
+ self.unlink_many2many(
506
+ field, field_obj["unselected"]
507
+ )
508
+ )
509
+
510
+ # выполняем последовательно
511
+ results = await execute_maybe_parallel(request_list)
512
+
513
+ return record_raw
dotorm/orm/protocol.py ADDED
@@ -0,0 +1,147 @@
1
+ """Protocols defining what ORM mixins expect from the model class."""
2
+
3
+ from typing import (
4
+ TYPE_CHECKING,
5
+ Any,
6
+ ClassVar,
7
+ Protocol,
8
+ Self,
9
+ Type,
10
+ Union,
11
+ runtime_checkable,
12
+ )
13
+
14
+ if TYPE_CHECKING:
15
+ from ..builder.builder import Builder
16
+ from ..components.dialect import Dialect
17
+ from ..fields import Field
18
+ from ..access import Operation
19
+ import aiomysql
20
+ import asyncpg
21
+
22
+
23
+ @runtime_checkable
24
+ class DotModelProtocol(Protocol):
25
+ """
26
+ Base protocol that all ORM mixins expect.
27
+
28
+ Defines the full interface that DotModel provides.
29
+ All mixins inherit from this protocol for type checking.
30
+ """
31
+
32
+ __table__: ClassVar[str]
33
+ __auto_create__: ClassVar[bool] = True
34
+ _pool: ClassVar[Union["aiomysql.Pool", "asyncpg.Pool"]]
35
+ _no_transaction: ClassVar[Type]
36
+ _dialect: ClassVar["Dialect"]
37
+ _builder: ClassVar["Builder"]
38
+
39
+ id: int
40
+
41
+ # Session
42
+ @classmethod
43
+ def _get_db_session(cls, session=None) -> Any: ...
44
+
45
+ # Access control (from AccessMixin)
46
+ @classmethod
47
+ async def _check_access(
48
+ cls,
49
+ operation: "Operation",
50
+ record_ids: list[int] | None = None,
51
+ filter: list | None = None,
52
+ ) -> list | None: ...
53
+
54
+ # Field introspection
55
+ @classmethod
56
+ def get_fields(cls) -> dict[str, "Field"]: ...
57
+
58
+ @classmethod
59
+ def get_store_fields(cls) -> list[str]: ...
60
+
61
+ @classmethod
62
+ def get_store_fields_omit_m2o(cls) -> list[str]: ...
63
+
64
+ @classmethod
65
+ def get_relation_fields(cls) -> list[tuple[str, "Field"]]: ...
66
+
67
+ @classmethod
68
+ def get_relation_fields_m2m_o2m(cls) -> list[tuple[str, "Field"]]: ...
69
+
70
+ @classmethod
71
+ def get_relation_fields_attachment(cls) -> list[tuple[str, "Field"]]: ...
72
+
73
+ # Serialization
74
+ @classmethod
75
+ def prepare_list_ids(cls, rows: list[dict]) -> list[Self]: ...
76
+
77
+ @classmethod
78
+ def prepare_form_id(cls, r: list) -> Self | None: ...
79
+
80
+ def json(
81
+ self,
82
+ include: Any = ...,
83
+ exclude: Any = ...,
84
+ exclude_none: bool = ...,
85
+ exclude_unset: bool = ...,
86
+ only_store: Any = ...,
87
+ mode: Any = ...,
88
+ ) -> dict[str, Any]: ...
89
+
90
+ @classmethod
91
+ def get_none_update_fields_set(cls) -> set[str]: ...
92
+
93
+ def __init__(self, **kwargs: Any) -> None: ...
94
+
95
+ # From OrmPrimaryMixin
96
+ async def update(
97
+ self,
98
+ payload: Self | None = None,
99
+ fields: Any = None,
100
+ session: Any = None,
101
+ ) -> Any: ...
102
+
103
+ # From OrmMany2manyMixin
104
+ @classmethod
105
+ async def get_many2many(
106
+ cls,
107
+ id: int,
108
+ comodel: Any,
109
+ relation: str,
110
+ column1: str,
111
+ column2: str,
112
+ fields: list[str] | None = None,
113
+ order: str = "desc",
114
+ start: int | None = None,
115
+ end: int | None = None,
116
+ sort: str = "id",
117
+ limit: int | None = 10,
118
+ session: Any = None,
119
+ ) -> list[Any]: ...
120
+
121
+ @classmethod
122
+ async def link_many2many(
123
+ cls,
124
+ field: Any,
125
+ values: list,
126
+ session: Any = None,
127
+ ) -> Any: ...
128
+
129
+ @classmethod
130
+ async def unlink_many2many(
131
+ cls,
132
+ field: Any,
133
+ ids: list,
134
+ session: Any = None,
135
+ ) -> Any: ...
136
+
137
+ @classmethod
138
+ async def _records_list_get_relation(
139
+ cls,
140
+ session: Any,
141
+ fields_relation: list[tuple[str, "Field"]],
142
+ records: list[Any],
143
+ ) -> None: ...
144
+
145
+ # From DDLMixin
146
+ @staticmethod
147
+ def format_default_value(value: Any) -> str: ...
dotorm/orm/utils.py ADDED
@@ -0,0 +1,39 @@
1
+ """Utility functions for dotorm."""
2
+
3
+ import asyncio
4
+ from typing import Any, Coroutine, Sequence
5
+
6
+
7
+ async def execute_maybe_parallel(
8
+ coroutines: Sequence[Coroutine[Any, Any, Any]],
9
+ ) -> list[Any]:
10
+ """
11
+ Execute coroutines in parallel or sequentially depending on transaction context.
12
+
13
+ If inside a transaction (single connection), executes sequentially to avoid
14
+ asyncpg "another operation is in progress" error.
15
+
16
+ If outside transaction (pool), executes in parallel for better performance.
17
+
18
+ Args:
19
+ coroutines: List of coroutines to execute
20
+
21
+ Returns:
22
+ List of results in the same order as input coroutines
23
+ """
24
+ from ..databases.postgres.transaction import get_current_session
25
+
26
+ if not coroutines:
27
+ return []
28
+
29
+ # Check if we're inside a transaction
30
+ if get_current_session() is not None:
31
+ # Inside transaction - execute sequentially
32
+ results = []
33
+ for coro in coroutines:
34
+ result = await coro
35
+ results.append(result)
36
+ return results
37
+ else:
38
+ # Outside transaction - execute in parallel
39
+ return list(await asyncio.gather(*coroutines))