deev 2.0.1__tar.gz → 2.0.2__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.
Files changed (96) hide show
  1. {deev-2.0.1/src/deev.egg-info → deev-2.0.2}/PKG-INFO +1 -1
  2. {deev-2.0.1 → deev-2.0.2}/pyproject.toml +1 -1
  3. {deev-2.0.1 → deev-2.0.2}/src/deev/__init__.py +2 -2
  4. {deev-2.0.1 → deev-2.0.2}/src/deev/clickhouse/async_clickhouse_table_adapter.py +0 -2
  5. {deev-2.0.1 → deev-2.0.2}/src/deev/clickhouse/async_clickhouse_transaction_context.py +10 -6
  6. {deev-2.0.1 → deev-2.0.2}/src/deev/clickhouse/clickhouse_table_adapter.py +0 -2
  7. {deev-2.0.1 → deev-2.0.2}/src/deev/clickhouse/clickhouse_transaction_context.py +10 -6
  8. {deev-2.0.1 → deev-2.0.2}/src/deev/common/async_db_transaction_context.py +1 -1
  9. {deev-2.0.1 → deev-2.0.2}/src/deev/common/db_migrator.py +0 -1
  10. {deev-2.0.1 → deev-2.0.2}/src/deev/common/db_transaction_context.py +1 -1
  11. {deev-2.0.1 → deev-2.0.2}/src/deev/common/tlc_parser.py +46 -23
  12. {deev-2.0.1 → deev-2.0.2}/src/deev/mongodb/async_mongo_table_adapter.py +0 -2
  13. {deev-2.0.1 → deev-2.0.2}/src/deev/mongodb/async_mongo_transaction_context.py +11 -7
  14. {deev-2.0.1 → deev-2.0.2}/src/deev/mongodb/mongo_table_adapter.py +0 -2
  15. {deev-2.0.1 → deev-2.0.2}/src/deev/mongodb/mongo_transaction_context.py +11 -7
  16. {deev-2.0.1 → deev-2.0.2}/src/deev/mysql/async_mysql_proxy_cursor.py +2 -4
  17. {deev-2.0.1 → deev-2.0.2}/src/deev/mysql/async_mysql_table_adapter.py +0 -2
  18. {deev-2.0.1 → deev-2.0.2}/src/deev/mysql/async_mysql_transaction_context.py +11 -8
  19. {deev-2.0.1 → deev-2.0.2}/src/deev/mysql/mysql_proxy_cursor.py +2 -4
  20. {deev-2.0.1 → deev-2.0.2}/src/deev/mysql/mysql_table_adapter.py +0 -2
  21. {deev-2.0.1 → deev-2.0.2}/src/deev/mysql/mysql_transaction_context.py +18 -10
  22. {deev-2.0.1 → deev-2.0.2}/src/deev/sqlite/async_sqlite_transaction_context.py +1 -1
  23. {deev-2.0.1 → deev-2.0.2}/src/deev/sqlite/sqlite_proxy_cursor.py +2 -5
  24. {deev-2.0.1 → deev-2.0.2}/src/deev/sqlite/sqlite_table_adapter.py +0 -2
  25. {deev-2.0.1 → deev-2.0.2}/src/deev/sqlite/sqlite_transaction_context.py +16 -15
  26. {deev-2.0.1 → deev-2.0.2}/src/deev/utils.py +1 -1
  27. {deev-2.0.1 → deev-2.0.2/src/deev.egg-info}/PKG-INFO +1 -1
  28. {deev-2.0.1 → deev-2.0.2}/LICENSE +0 -0
  29. {deev-2.0.1 → deev-2.0.2}/README.md +0 -0
  30. {deev-2.0.1 → deev-2.0.2}/setup.cfg +0 -0
  31. {deev-2.0.1 → deev-2.0.2}/src/deev/SKILL.md +0 -0
  32. {deev-2.0.1 → deev-2.0.2}/src/deev/_immutable_mixin.py +0 -0
  33. {deev-2.0.1 → deev-2.0.2}/src/deev/_migration_data.py +0 -0
  34. {deev-2.0.1 → deev-2.0.2}/src/deev/clickhouse/__init__.py +0 -0
  35. {deev-2.0.1 → deev-2.0.2}/src/deev/clickhouse/async_clickhouse_proxy_connection.py +0 -0
  36. {deev-2.0.1 → deev-2.0.2}/src/deev/clickhouse/async_clickhouse_proxy_cursor.py +0 -0
  37. {deev-2.0.1 → deev-2.0.2}/src/deev/clickhouse/clickhouse_ddl_generator.py +0 -0
  38. {deev-2.0.1 → deev-2.0.2}/src/deev/clickhouse/clickhouse_proxy_connection.py +0 -0
  39. {deev-2.0.1 → deev-2.0.2}/src/deev/clickhouse/clickhouse_proxy_cursor.py +0 -0
  40. {deev-2.0.1 → deev-2.0.2}/src/deev/clickhouse/clickhouse_type_mapper.py +0 -0
  41. {deev-2.0.1 → deev-2.0.2}/src/deev/clickhouse/utils.py +0 -0
  42. {deev-2.0.1 → deev-2.0.2}/src/deev/common/__init__.py +0 -0
  43. {deev-2.0.1 → deev-2.0.2}/src/deev/common/async_db_adapter.py +0 -0
  44. {deev-2.0.1 → deev-2.0.2}/src/deev/common/async_db_connection.py +0 -0
  45. {deev-2.0.1 → deev-2.0.2}/src/deev/common/async_db_cursor.py +0 -0
  46. {deev-2.0.1 → deev-2.0.2}/src/deev/common/async_db_table_adapter.py +0 -0
  47. {deev-2.0.1 → deev-2.0.2}/src/deev/common/connection_string.py +0 -0
  48. {deev-2.0.1 → deev-2.0.2}/src/deev/common/db_adapter.py +0 -0
  49. {deev-2.0.1 → deev-2.0.2}/src/deev/common/db_connection.py +0 -0
  50. {deev-2.0.1 → deev-2.0.2}/src/deev/common/db_context.py +0 -0
  51. {deev-2.0.1 → deev-2.0.2}/src/deev/common/db_cursor.py +0 -0
  52. {deev-2.0.1 → deev-2.0.2}/src/deev/common/db_error.py +0 -0
  53. {deev-2.0.1 → deev-2.0.2}/src/deev/common/db_parameters.py +0 -0
  54. {deev-2.0.1 → deev-2.0.2}/src/deev/common/db_table_adapter.py +0 -0
  55. {deev-2.0.1 → deev-2.0.2}/src/deev/common/db_type_mapper.py +0 -0
  56. {deev-2.0.1 → deev-2.0.2}/src/deev/common/description_field.py +0 -0
  57. {deev-2.0.1 → deev-2.0.2}/src/deev/common/noop_cursor.py +0 -0
  58. {deev-2.0.1 → deev-2.0.2}/src/deev/db_migrate.py +0 -0
  59. {deev-2.0.1 → deev-2.0.2}/src/deev/entities/__init__.py +0 -0
  60. {deev-2.0.1 → deev-2.0.2}/src/deev/entities/entity_field_spec.py +0 -0
  61. {deev-2.0.1 → deev-2.0.2}/src/deev/entities/entity_spec.py +0 -0
  62. {deev-2.0.1 → deev-2.0.2}/src/deev/entities/index_options.py +0 -0
  63. {deev-2.0.1 → deev-2.0.2}/src/deev/entities/index_order.py +0 -0
  64. {deev-2.0.1 → deev-2.0.2}/src/deev/entities/utils.py +0 -0
  65. {deev-2.0.1 → deev-2.0.2}/src/deev/mongodb/__init__.py +0 -0
  66. {deev-2.0.1 → deev-2.0.2}/src/deev/mongodb/async_mongo_proxy_connection.py +0 -0
  67. {deev-2.0.1 → deev-2.0.2}/src/deev/mongodb/async_mongo_proxy_cursor.py +0 -0
  68. {deev-2.0.1 → deev-2.0.2}/src/deev/mongodb/mongo_proxy_connection.py +0 -0
  69. {deev-2.0.1 → deev-2.0.2}/src/deev/mongodb/mongo_proxy_cursor.py +0 -0
  70. {deev-2.0.1 → deev-2.0.2}/src/deev/mongodb/mongo_type_mapper.py +0 -0
  71. {deev-2.0.1 → deev-2.0.2}/src/deev/mongodb/utils.py +0 -0
  72. {deev-2.0.1 → deev-2.0.2}/src/deev/mysql/__init__.py +0 -0
  73. {deev-2.0.1 → deev-2.0.2}/src/deev/mysql/async_mysql_proxy_connection.py +0 -0
  74. {deev-2.0.1 → deev-2.0.2}/src/deev/mysql/mysql_ddl_generator.py +0 -0
  75. {deev-2.0.1 → deev-2.0.2}/src/deev/mysql/mysql_proxy_connection.py +0 -0
  76. {deev-2.0.1 → deev-2.0.2}/src/deev/mysql/mysql_type_mapper.py +0 -0
  77. {deev-2.0.1 → deev-2.0.2}/src/deev/py.typed +0 -0
  78. {deev-2.0.1 → deev-2.0.2}/src/deev/sqlite/__init__.py +0 -0
  79. {deev-2.0.1 → deev-2.0.2}/src/deev/sqlite/async_sqlite_proxy_connection.py +0 -0
  80. {deev-2.0.1 → deev-2.0.2}/src/deev/sqlite/async_sqlite_proxy_cursor.py +0 -0
  81. {deev-2.0.1 → deev-2.0.2}/src/deev/sqlite/async_sqlite_table_adapter.py +0 -0
  82. {deev-2.0.1 → deev-2.0.2}/src/deev/sqlite/sqlite_ddl_generator.py +0 -0
  83. {deev-2.0.1 → deev-2.0.2}/src/deev/sqlite/sqlite_proxy_connection.py +0 -0
  84. {deev-2.0.1 → deev-2.0.2}/src/deev/sqlite/sqlite_type_mapper.py +0 -0
  85. {deev-2.0.1 → deev-2.0.2}/src/deev/translation/__init__.py +0 -0
  86. {deev-2.0.1 → deev-2.0.2}/src/deev/translation/deev_json_decoder.py +0 -0
  87. {deev-2.0.1 → deev-2.0.2}/src/deev/translation/deev_json_encoder.py +0 -0
  88. {deev-2.0.1 → deev-2.0.2}/src/deev/translation/utils.py +0 -0
  89. {deev-2.0.1 → deev-2.0.2}/src/deev/validation/__init__.py +0 -0
  90. {deev-2.0.1 → deev-2.0.2}/src/deev/validation/utils.py +0 -0
  91. {deev-2.0.1 → deev-2.0.2}/src/deev/validation/validation_error.py +0 -0
  92. {deev-2.0.1 → deev-2.0.2}/src/deev.egg-info/SOURCES.txt +0 -0
  93. {deev-2.0.1 → deev-2.0.2}/src/deev.egg-info/dependency_links.txt +0 -0
  94. {deev-2.0.1 → deev-2.0.2}/src/deev.egg-info/entry_points.txt +0 -0
  95. {deev-2.0.1 → deev-2.0.2}/src/deev.egg-info/requires.txt +0 -0
  96. {deev-2.0.1 → deev-2.0.2}/src/deev.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deev
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: ..an entity framework for Python.
5
5
  Author-email: Shaun Wilson <mrshaunwilson@msn.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deev"
3
- version = "2.0.1"
3
+ version = "2.0.2"
4
4
  description = "..an entity framework for Python."
5
5
  keywords = ["DB", "database", "entity framework", "mapper", "sqlite", "mysql", "mongodb", "clickhouse"]
6
6
  authors = [
@@ -21,8 +21,8 @@ from .translation import hydrate, splat
21
21
  from .utils import connect
22
22
 
23
23
 
24
- __version__ = '2.0.1'
25
- __commit__ = 'e40c4fd'
24
+ __version__ = '2.0.2'
25
+ __commit__ = 'a337fba'
26
26
  __all__ = [
27
27
  '__version__',
28
28
  '__commit__',
@@ -45,7 +45,6 @@ class AsyncClickHouseTableAdapter(AsyncDbTableAdapter[TEntity]):
45
45
  __logger: logging.Logger
46
46
  __dbtype_mapper: DbTypeMapper
47
47
  __table_name: str | None
48
- __transaction_state: int
49
48
 
50
49
  def __init__(
51
50
  self,
@@ -59,7 +58,6 @@ class AsyncClickHouseTableAdapter(AsyncDbTableAdapter[TEntity]):
59
58
  self.__create_table = create_table is True
60
59
  self.__initialized = False
61
60
  self.__table_name = table_name
62
- self.__transaction_state = 0
63
61
  self.__is_sync_replicas_enabled = sync_replicas is True and getattr(self.__context, 'is_replicated', False) is True
64
62
  self.__logger = hanaro.get_logger()
65
63
 
@@ -94,8 +94,12 @@ class AsyncClickHouseTransactionContext(AsyncDbTransactionContext):
94
94
  For ClickHouse: all TLC keywords return None (scrubbed) since ClickHouse
95
95
  has no transaction support. Depth tracking is for diagnostics/consistency.
96
96
  """
97
- if self.__transaction_depth < 0:
98
- raise DbError('Cannot use a transaction context that has been exited.')
97
+ if self.__transaction_depth == -1:
98
+ raise DbError('Cannot use a transaction context that has exited.')
99
+ elif self.__transaction_depth == -2:
100
+ raise DbError('Cannot use a transaction context that has been committed.')
101
+ elif self.__transaction_depth == -3:
102
+ raise DbError('Cannot use a transaction context that has been rolled back.')
99
103
 
100
104
  sql_upper = sql.lstrip().upper()
101
105
  prefix = sql_upper[:4]
@@ -119,12 +123,12 @@ class AsyncClickHouseTransactionContext(AsyncDbTransactionContext):
119
123
  self.__savepoints.clear()
120
124
  self.__transaction_name = None
121
125
  self.__ambient_transaction_id.set(None)
126
+ self.__transaction_depth = -2
122
127
  return None
123
128
 
124
129
  elif prefix == 'ROLL':
125
130
  if self.__transaction_depth == 0:
126
131
  raise DbError('Cannot rollback, no transaction.')
127
- self.__transaction_depth = 0
128
132
  name = extract_rollback_name(sql)
129
133
 
130
134
  if name:
@@ -141,7 +145,7 @@ class AsyncClickHouseTransactionContext(AsyncDbTransactionContext):
141
145
  else:
142
146
  pass # full rollback, depth reset below
143
147
 
144
- self.__transaction_depth = 0
148
+ self.__transaction_depth = -3
145
149
  self.__savepoints.clear()
146
150
  self.__transaction_name = None
147
151
  self.__ambient_transaction_id.set(None)
@@ -250,8 +254,8 @@ class AsyncClickHouseTransactionContext(AsyncDbTransactionContext):
250
254
  except Exception:
251
255
  return cursor.rowcount
252
256
 
253
- async def execute_script(self, sql: str, raw: str | None = None) -> None:
254
- if raw is not None:
257
+ async def execute_script(self, sql: str, raw: bool = False) -> None:
258
+ if raw is True:
255
259
  await self.execute(sql, raw=True)
256
260
  return
257
261
  lines = sql.split('\n')
@@ -41,7 +41,6 @@ class ClickHouseTableAdapter(Generic[TEntity]):
41
41
  __initialized: bool
42
42
  __logger: logging.Logger
43
43
  __table_name: str | None
44
- __transaction_state: int
45
44
 
46
45
  def __init__(
47
46
  self,
@@ -55,7 +54,6 @@ class ClickHouseTableAdapter(Generic[TEntity]):
55
54
  self.__create_table = create_table is True
56
55
  self.__initialized = False
57
56
  self.__table_name = table_name
58
- self.__transaction_state = 0
59
57
  self.__is_sync_replicas_enabled = sync_replicas is True and getattr(self.__context, 'is_replicated', False) is True
60
58
  self.__logger = hanaro.get_logger()
61
59
 
@@ -91,8 +91,12 @@ class ClickHouseTransactionContext(DbTransactionContext):
91
91
  For ClickHouse: all TLC keywords return None (scrubbed) since ClickHouse
92
92
  has no transaction support. Depth tracking is for diagnostics/consistency.
93
93
  """
94
- if self.__transaction_depth < 0:
95
- raise DbError('Cannot use a transaction context that has been exited.')
94
+ if self.__transaction_depth == -1:
95
+ raise DbError('Cannot use a transaction context that has exited.')
96
+ elif self.__transaction_depth == -2:
97
+ raise DbError('Cannot use a transaction context that has been committed.')
98
+ elif self.__transaction_depth == -3:
99
+ raise DbError('Cannot use a transaction context that has been rolled back.')
96
100
 
97
101
  sql_upper = sql.lstrip().upper()
98
102
  prefix = sql_upper[:4]
@@ -116,12 +120,12 @@ class ClickHouseTransactionContext(DbTransactionContext):
116
120
  self.__savepoints.clear()
117
121
  self.__transaction_name = None
118
122
  self.__ambient_transaction_id.set(None)
123
+ self.__transaction_depth = -2
119
124
  return None
120
125
 
121
126
  elif prefix == 'ROLL':
122
127
  if self.__transaction_depth == 0:
123
128
  raise DbError('Cannot rollback, no transaction.')
124
- self.__transaction_depth = 0
125
129
  name = extract_rollback_name(sql)
126
130
 
127
131
  if name:
@@ -138,7 +142,7 @@ class ClickHouseTransactionContext(DbTransactionContext):
138
142
  else:
139
143
  pass # full rollback, depth reset below
140
144
 
141
- self.__transaction_depth = 0
145
+ self.__transaction_depth = -3
142
146
  self.__savepoints.clear()
143
147
  self.__transaction_name = None
144
148
  self.__ambient_transaction_id.set(None)
@@ -242,8 +246,8 @@ class ClickHouseTransactionContext(DbTransactionContext):
242
246
  except Exception:
243
247
  return cursor.rowcount
244
248
 
245
- def execute_script(self, sql: str, raw: str | None = None) -> None:
246
- if raw is not None:
249
+ def execute_script(self, sql: str, raw: bool = False) -> None:
250
+ if raw is True:
247
251
  self.execute(sql, raw=True)
248
252
  return
249
253
  lines = sql.split('\n')
@@ -64,7 +64,7 @@ class AsyncDbTransactionContext(Protocol):
64
64
  async def execute(self, sql: str, parameters: DbParameters | None = ..., raw: bool = ...) -> AsyncDbCursor:
65
65
  ...
66
66
 
67
- async def execute_script(self, sql: str) -> None:
67
+ async def execute_script(self, sql: str, raw: bool = ...) -> None:
68
68
  ...
69
69
 
70
70
  async def execute_nonquery(self, sql: str, parameters: DbParameters | None = ...) -> None:
@@ -161,7 +161,6 @@ class DbMigrator:
161
161
  if migration_func is not None:
162
162
  with begin_transaction(self.__connectionstring) as db_transaction:
163
163
  migration_func(db_transaction)
164
- db_transaction.commit()
165
164
  # NOTE: callee (migration_func) is responsible for calling commit (or rollback)
166
165
  migrations_table.delete(id=applied_migrations.get(migration_name, 0))
167
166
  migrations_table.commit()
@@ -58,7 +58,7 @@ class DbTransactionContext(Protocol):
58
58
  def execute(self, sql: str, parameters: DbParameters | None = ...) -> DbCursor:
59
59
  ...
60
60
 
61
- def execute_script(self, sql: str) -> None:
61
+ def execute_script(self, sql: str, raw: bool = ...) -> None:
62
62
  ...
63
63
 
64
64
  def execute_nonquery(self, sql: str, parameters: DbParameters | None = ...) -> None:
@@ -17,6 +17,11 @@ _BEGIN_PATTERN = re.compile(
17
17
  re.IGNORECASE,
18
18
  )
19
19
 
20
+ _START_PATTERN = re.compile(
21
+ r'^START\s+TRANSACTION\s+(\S+)',
22
+ re.IGNORECASE,
23
+ )
24
+
20
25
  _SAVEPOINT_PATTERN = re.compile(
21
26
  r'^(?:SAVEPOINT|SAVE\s+(?:TRANSACTION|TRAN))\s+(\S+)',
22
27
  re.IGNORECASE,
@@ -24,14 +29,35 @@ _SAVEPOINT_PATTERN = re.compile(
24
29
 
25
30
  # Handles: ROLLBACK name, ROLLBACK TO name, ROLLBACK TO SAVEPOINT name,
26
31
  # ROLLBACK TRANSACTION name, ROLLBACK TO TRANSACTION name, etc.
27
- # Uses a multi-step parser instead of a single regex to avoid
28
- # backtracking issues with optional keyword groups.
29
-
32
+ # All valid syntaxes are enumerated as explicit alternations so the regex
33
+ # engine never backtracks into capturing bare keywords as names.
30
34
  _ROLLBACK_TO_PREFIX = re.compile(
31
35
  r'^ROLLBACK\s+TO\s',
32
36
  re.IGNORECASE,
33
37
  )
34
38
 
39
+ # Each alternative requires a name; alternatives that match return the
40
+ # captured group. Nothing else matches, so bare keywords return None.
41
+ _ROLLBACK_PATTERN = re.compile(
42
+ r'^ROLLBACK\s+'
43
+ r'(?:'
44
+ r'TO\s+SAVEPOINT\s+TRAN(?=\s|$)\s+(\S+)' # ROLLBACK TO SAVEPOINT TRAN name
45
+ r'|TO\s+SAVEPOINT\s+TRANSACTION\s+(\S+)' # ROLLBACK TO SAVEPOINT TRANSACTION name
46
+ r'|TO\s+SAVEPOINT\s+(\S+)' # ROLLBACK TO SAVEPOINT name
47
+ r'|TO\s+TRANSACTION\s+(\S+)' # ROLLBACK TO TRANSACTION name
48
+ r'|TO\s+TRAN(?=\s|$)\s+(\S+)' # ROLLBACK TO TRAN name
49
+ r'|TO\s+(?!SAVEPOINT\s*$|TRAN\s*$|TRANSACTION\s*$)(\S+)' # ROLLBACK TO name
50
+ r'|TRANSACTION\s+(\S+)' # ROLLBACK TRANSACTION name
51
+ r'|TRAN(?=\s|$)\s+(\S+)' # ROLLBACK TRAN name
52
+ # Reject bare keywords and TO-prefixed bare keywords
53
+ r'|(?!SAVEPOINT\s*$|TRANSACTION\s*$|TRAN\s*$'
54
+ r'|TO\s+(?:SAVEPOINT|TRAN|TRANSACTION)\s*$' # TO SAVEPOINT, TO TRAN, TO TRANSACTION
55
+ r'|TO\s*$)' # bare TO
56
+ r'\s*(\S+)' # ROLLBACK name
57
+ r')',
58
+ re.IGNORECASE,
59
+ )
60
+
35
61
 
36
62
  def extract_begin_name(sql: str) -> str | None:
37
63
  """Extract the transaction name from a ``BEGIN [TRANSACTION|TRAN] name`` statement.
@@ -43,6 +69,16 @@ def extract_begin_name(sql: str) -> str | None:
43
69
  return m.group(1) if m else None
44
70
 
45
71
 
72
+ def extract_start_name(sql: str) -> str | None:
73
+ """Extract the transaction name from a ``START TRANSACTION name`` statement.
74
+
75
+ Returns the name exactly as written in the original SQL, or ``None`` if no
76
+ name is present.
77
+ """
78
+ m = _START_PATTERN.match(sql.lstrip())
79
+ return m.group(1) if m else None
80
+
81
+
46
82
  def extract_savepoint_name(sql: str) -> str | None:
47
83
  """Extract the savepoint name from a ``SAVEPOINT name`` or ``SAVE [TRANSACTION|TRAN] name`` statement.
48
84
 
@@ -59,27 +95,13 @@ def extract_rollback_name(sql: str) -> str | None:
59
95
  Returns the name exactly as written in the original SQL, or ``None`` if no
60
96
  name is present.
61
97
  """
62
- s = sql.lstrip()
63
- if not s.upper().startswith('ROLLBACK'):
98
+ m = _ROLLBACK_PATTERN.match(sql.lstrip())
99
+ if m is None:
64
100
  return None
65
- rest = s[8:].lstrip()
66
-
67
- # Skip optional "TO"
68
- if rest.upper().startswith('TO '):
69
- rest = rest[3:].lstrip()
70
-
71
- # Skip optional "SAVEPOINT"
72
- if rest.upper().startswith('SAVEPOINT'):
73
- rest = rest[9:].lstrip()
74
- # Skip optional "TRANSACTION" or "TRAN"
75
- if rest.upper().startswith('TRANSACTION'):
76
- rest = rest[11:].lstrip()
77
- elif rest.upper().startswith('TRAN '):
78
- rest = rest[5:].lstrip()
79
-
80
- # Extract name
81
- name = rest.split()[0] if rest else None
82
- return name
101
+ for group in m.groups():
102
+ if group is not None:
103
+ return group
104
+ return None
83
105
 
84
106
 
85
107
  def is_rollback_to(sql: str) -> bool:
@@ -93,6 +115,7 @@ def is_rollback_to(sql: str) -> bool:
93
115
 
94
116
  __all__ = [
95
117
  'extract_begin_name',
118
+ 'extract_start_name',
96
119
  'extract_savepoint_name',
97
120
  'extract_rollback_name',
98
121
  'is_rollback_to'
@@ -41,7 +41,6 @@ class AsyncMongoTableAdapter(AsyncDbTableAdapter[TEntity]):
41
41
  __initialized: bool
42
42
  __dbtype_mapper: DbTypeMapper
43
43
  __table_name: str | None
44
- __transaction_state: int
45
44
 
46
45
  def __init__(
47
46
  self,
@@ -55,7 +54,6 @@ class AsyncMongoTableAdapter(AsyncDbTableAdapter[TEntity]):
55
54
  self.__create_table = create_table is True
56
55
  self.__initialized = False
57
56
  self.__table_name = table_name
58
- self.__transaction_state = 0
59
57
 
60
58
  self.__database_name = self.__context.mongo_database_name # type: ignore[missing-attribute, union-attr]
61
59
 
@@ -109,8 +109,12 @@ class AsyncMongoTransactionContext(AsyncDbTransactionContext):
109
109
  For MongoDB: all TLC keywords return None (scrubbed) since MongoDB has
110
110
  no SQL transaction syntax. Side effects happen via mongo_session.
111
111
  """
112
- if self.__transaction_depth < 0:
113
- raise DbError('Cannot use a transaction context that has been exited.')
112
+ if self.__transaction_depth == -1:
113
+ raise DbError('Cannot use a transaction context that has exited.')
114
+ elif self.__transaction_depth == -2:
115
+ raise DbError('Cannot use a transaction context that has been committed.')
116
+ elif self.__transaction_depth == -3:
117
+ raise DbError('Cannot use a transaction context that has been rolled back.')
114
118
 
115
119
  sql_upper = sql.lstrip().upper()
116
120
  prefix = sql_upper[:4]
@@ -136,14 +140,14 @@ class AsyncMongoTransactionContext(AsyncDbTransactionContext):
136
140
  self.__savepoints.clear()
137
141
  self.__transaction_name = None
138
142
  self.__ambient_transaction_id.set(None)
139
- if not self.__delegate_mode and self.__transaction_depth == 0:
143
+ self.__transaction_depth = -2
144
+ if not self.__delegate_mode and self.__transaction_depth == -2:
140
145
  await self.mongo_session.commit_transaction()
141
146
  return None
142
147
 
143
148
  elif prefix == 'ROLL':
144
149
  if self.__transaction_depth == 0:
145
150
  raise DbError('Cannot rollback, no transaction.')
146
- self.__transaction_depth = 0
147
151
  name = extract_rollback_name(sql)
148
152
 
149
153
  if name:
@@ -160,7 +164,7 @@ class AsyncMongoTransactionContext(AsyncDbTransactionContext):
160
164
  else:
161
165
  pass # full rollback, depth reset below
162
166
 
163
- self.__transaction_depth = 0
167
+ self.__transaction_depth = -3
164
168
  self.__savepoints.clear()
165
169
  self.__transaction_name = None
166
170
  self.__ambient_transaction_id.set(None)
@@ -282,8 +286,8 @@ class AsyncMongoTransactionContext(AsyncDbTransactionContext):
282
286
  except Exception:
283
287
  return cursor.rowcount
284
288
 
285
- async def execute_script(self, sql: str, raw: str | None = None) -> None:
286
- if raw is not None:
289
+ async def execute_script(self, sql: str, raw: bool = False) -> None:
290
+ if raw is True:
287
291
  await self.execute(sql, raw=True)
288
292
  return
289
293
  lines = sql.split('\n')
@@ -40,7 +40,6 @@ class MongoTableAdapter(Generic[TEntity]):
40
40
  __initialized: bool
41
41
  __dbtype_mapper: DbTypeMapper
42
42
  __table_name: str | None
43
- __transaction_state: int
44
43
 
45
44
  def __init__(
46
45
  self,
@@ -54,7 +53,6 @@ class MongoTableAdapter(Generic[TEntity]):
54
53
  self.__create_table = create_table is True
55
54
  self.__initialized = False
56
55
  self.__table_name = table_name
57
- self.__transaction_state = 0
58
56
 
59
57
  self.__database_name = self.__context.mongo_database_name # type: ignore[missing-attribute, union-attr]
60
58
 
@@ -111,8 +111,12 @@ class MongoTransactionContext(DbTransactionContext):
111
111
  For MongoDB: all TLC keywords return None (scrubbed) since MongoDB has
112
112
  no SQL transaction syntax. Side effects happen via mongo_session.
113
113
  """
114
- if self.__transaction_depth < 0:
115
- raise DbError('Cannot use a transaction context that has been exited.')
114
+ if self.__transaction_depth == -1:
115
+ raise DbError('Cannot use a transaction context that has exited.')
116
+ elif self.__transaction_depth == -2:
117
+ raise DbError('Cannot use a transaction context that has been committed.')
118
+ elif self.__transaction_depth == -3:
119
+ raise DbError('Cannot use a transaction context that has been rolled back.')
116
120
 
117
121
  sql_upper = sql.lstrip().upper()
118
122
  prefix = sql_upper[:4]
@@ -138,14 +142,14 @@ class MongoTransactionContext(DbTransactionContext):
138
142
  self.__savepoints.clear()
139
143
  self.__transaction_name = None
140
144
  self.__ambient_transaction_id.set(None)
141
- if not self.__delegate_mode and self.__transaction_depth == 0:
145
+ self.__transaction_depth = -2
146
+ if not self.__delegate_mode and self.__transaction_depth == -2:
142
147
  self.mongo_session.commit_transaction()
143
148
  return None
144
149
 
145
150
  elif prefix == 'ROLL':
146
151
  if self.__transaction_depth == 0:
147
152
  raise DbError('Cannot rollback, no transaction.')
148
- self.__transaction_depth = 0
149
153
  name = extract_rollback_name(sql)
150
154
 
151
155
  if name:
@@ -162,7 +166,7 @@ class MongoTransactionContext(DbTransactionContext):
162
166
  else:
163
167
  pass # full rollback, depth reset below
164
168
 
165
- self.__transaction_depth = 0
169
+ self.__transaction_depth = -3
166
170
  self.__savepoints.clear()
167
171
  self.__transaction_name = None
168
172
  self.__ambient_transaction_id.set(None)
@@ -281,8 +285,8 @@ class MongoTransactionContext(DbTransactionContext):
281
285
  except Exception:
282
286
  return cursor.rowcount
283
287
 
284
- def execute_script(self, sql: str, raw: str | None = None) -> None:
285
- if raw is not None:
288
+ def execute_script(self, sql: str, raw: bool = False) -> None:
289
+ if raw is True:
286
290
  self.execute(sql, raw=True)
287
291
  return
288
292
  lines = sql.split('\n')
@@ -39,16 +39,14 @@ class AsyncMySQLProxyCursor(AsyncDbCursor):
39
39
  return self.__cursor.rowcount
40
40
 
41
41
  async def execute(self, operation: str, parameters: DbParameters | None = None) -> None:
42
- operation = operation.replace(self.__sql_arg_expect, self.__sql_arg_subst)
43
- self.__logger.debug(f'execute({operation!r}, {parameters!r})')
44
- if parameters is None:
42
+ if parameters is None or len(parameters) == 0:
45
43
  await self.__cursor.execute(operation)
46
44
  else:
45
+ operation = operation.replace(self.__sql_arg_expect, self.__sql_arg_subst)
47
46
  await self.__cursor.execute(operation, parameters) # type: ignore[arg-type]
48
47
 
49
48
  async def executemany(self, operation: str, seq_of_parameters: Sequence[DbParameters]) -> None:
50
49
  operation = operation.replace(self.__sql_arg_expect, self.__sql_arg_subst)
51
- self.__logger.debug(f'execute({operation!r}, {seq_of_parameters!r})')
52
50
  await self.__cursor.executemany(operation, seq_of_parameters) # type: ignore[arg-type]
53
51
 
54
52
  async def fetchone(self) -> tuple[Any, ...] | None:
@@ -38,7 +38,6 @@ class AsyncMySQLTableAdapter(AsyncDbTableAdapter[TEntity]):
38
38
  __entity_spec: EntitySpec
39
39
  __initialized: bool
40
40
  __table_name: str | None
41
- __transaction_state: int
42
41
 
43
42
  def __init__(
44
43
  self,
@@ -52,7 +51,6 @@ class AsyncMySQLTableAdapter(AsyncDbTableAdapter[TEntity]):
52
51
  self.__create_table = create_table is True
53
52
  self.__initialized = False
54
53
  self.__table_name = table_name
55
- self.__transaction_state = 0
56
54
 
57
55
  async def __deferred_init(self) -> None:
58
56
  if not self.__initialized:
@@ -95,8 +95,12 @@ class AsyncMySQLTransactionContext(AsyncDbTransactionContext):
95
95
 
96
96
  MySQL-specific: strips name from START TRANSACTION (MySQL doesn't support named transactions).
97
97
  """
98
- if self.__transaction_depth < 0:
99
- raise DbError('Cannot use a transaction context that has been exited.')
98
+ if self.__transaction_depth == -1:
99
+ raise DbError('Cannot use a transaction context that has exited.')
100
+ elif self.__transaction_depth == -2:
101
+ raise DbError('Cannot use a transaction context that has been committed.')
102
+ elif self.__transaction_depth == -3:
103
+ raise DbError('Cannot use a transaction context that has been rolled back.')
100
104
 
101
105
  sql_upper = sql.lstrip().upper()
102
106
  prefix = sql_upper[:4]
@@ -123,7 +127,7 @@ class AsyncMySQLTransactionContext(AsyncDbTransactionContext):
123
127
  self.__savepoints.clear()
124
128
  self.__transaction_name = None
125
129
  self.__ambient_transaction_id.set(None)
126
- if self.__transaction_depth == 0:
130
+ self.__transaction_depth = -2
127
131
  return 'COMMIT'
128
132
  else:
129
133
  return None
@@ -131,7 +135,6 @@ class AsyncMySQLTransactionContext(AsyncDbTransactionContext):
131
135
  elif prefix == 'ROLL':
132
136
  if self.__transaction_depth == 0:
133
137
  raise DbError('Cannot rollback, no transaction.')
134
- self.__transaction_depth = 0
135
138
  name = extract_rollback_name(sql)
136
139
 
137
140
  if name:
@@ -140,7 +143,7 @@ class AsyncMySQLTransactionContext(AsyncDbTransactionContext):
140
143
  sp = self.__savepoints.pop()
141
144
  if sp == name:
142
145
  break
143
- return 'ROLLBACK TRANSACTION'
146
+ return f'ROLLBACK TO SAVEPOINT {name}'
144
147
  elif name == self.__transaction_name:
145
148
  pass # full rollback, depth reset below
146
149
  else:
@@ -148,7 +151,7 @@ class AsyncMySQLTransactionContext(AsyncDbTransactionContext):
148
151
  else:
149
152
  pass # full rollback, depth reset below
150
153
 
151
- self.__transaction_depth = 0
154
+ self.__transaction_depth = -3
152
155
  self.__savepoints.clear()
153
156
  self.__transaction_name = None
154
157
  self.__ambient_transaction_id.set(None)
@@ -242,8 +245,8 @@ class AsyncMySQLTransactionContext(AsyncDbTransactionContext):
242
245
  except mysql.connector.ProgrammingError:
243
246
  return cursor.rowcount
244
247
 
245
- async def execute_script(self, sql: str, raw: str | None = None) -> None:
246
- if raw is not None:
248
+ async def execute_script(self, sql: str, raw: bool = False) -> None:
249
+ if raw is True:
247
250
  await self.execute(sql, raw=True)
248
251
  return
249
252
  lines = sql.split('\n')
@@ -41,16 +41,14 @@ class MySQLProxyCursor(DbCursor):
41
41
  return self.__cursor.rowcount
42
42
 
43
43
  def execute(self, operation: str, parameters: DbParameters | None = None) -> None:
44
- operation = operation.replace(self.__sql_arg_expect, self.__sql_arg_subst)
45
- self.__logger.debug(f'execute({operation!r}, {parameters!r})')
46
- if parameters is None:
44
+ if parameters is None or len(parameters) == 0:
47
45
  self.__cursor.execute(operation)
48
46
  else:
47
+ operation = operation.replace(self.__sql_arg_expect, self.__sql_arg_subst)
49
48
  self.__cursor.execute(operation, parameters) # type: ignore[arg-type]
50
49
 
51
50
  def executemany(self, operation: str, seq_of_parameters: Sequence[DbParameters]) -> None:
52
51
  operation = operation.replace(self.__sql_arg_expect, self.__sql_arg_subst)
53
- self.__logger.debug(f'execute({operation!r}, {seq_of_parameters!r})')
54
52
  self.__cursor.executemany(operation, seq_of_parameters)
55
53
 
56
54
  def fetchone(self) -> tuple[Any, ...] | None:
@@ -40,7 +40,6 @@ class MySQLTableAdapter(Generic[TEntity]):
40
40
  __initialized: bool
41
41
  __dbtype_mapper: DbTypeMapper
42
42
  __table_name: str | None
43
- __transaction_state: int
44
43
 
45
44
  def __init__(
46
45
  self,
@@ -54,7 +53,6 @@ class MySQLTableAdapter(Generic[TEntity]):
54
53
  self.__create_table = create_table is True
55
54
  self.__initialized = False
56
55
  self.__table_name = table_name
57
- self.__transaction_state = 0
58
56
 
59
57
  def __deferred_init(self) -> None:
60
58
  if not self.__initialized:
@@ -17,7 +17,7 @@ from ..common.db_cursor import DbCursor
17
17
  from ..common.db_error import DbError
18
18
  from ..common.db_parameters import DbParameters
19
19
  from ..common.db_transaction_context import DbTransactionContext
20
- from ..common.tlc_parser import extract_begin_name, extract_rollback_name, extract_savepoint_name
20
+ from ..common.tlc_parser import extract_begin_name, extract_rollback_name, extract_savepoint_name, extract_start_name
21
21
  from .mysql_proxy_connection import MySQLProxyConnection
22
22
 
23
23
 
@@ -84,8 +84,12 @@ class MySQLTransactionContext(DbTransactionContext):
84
84
 
85
85
  MySQL-specific: strips name from START TRANSACTION (MySQL doesn't support named transactions).
86
86
  """
87
- if self.__transaction_depth < 0:
88
- raise DbError('Cannot use a transaction context that has been exited.')
87
+ if self.__transaction_depth == -1:
88
+ raise DbError('Cannot use a transaction context that has exited.')
89
+ elif self.__transaction_depth == -2:
90
+ raise DbError('Cannot use a transaction context that has been committed.')
91
+ elif self.__transaction_depth == -3:
92
+ raise DbError('Cannot use a transaction context that has been rolled back.')
89
93
 
90
94
  sql_upper = sql.lstrip().upper()
91
95
  prefix = sql_upper[:4]
@@ -94,7 +98,12 @@ class MySQLTransactionContext(DbTransactionContext):
94
98
  if self.__transaction_depth > 0:
95
99
  return None
96
100
  self.__transaction_depth += 1
97
- self.__transaction_name = extract_begin_name(sql)
101
+ if prefix == 'STAR':
102
+ self.__transaction_name = extract_start_name(sql)
103
+ elif sql_upper.startswith('BEGIN WORK') or sql_upper.startswith('BEGIN '):
104
+ self.__transaction_name = None
105
+ else:
106
+ self.__transaction_name = extract_begin_name(sql)
98
107
  self.__ambient_transaction_id.set(self.__transaction_name or self.__transaction_id)
99
108
  return 'START TRANSACTION'
100
109
 
@@ -112,7 +121,7 @@ class MySQLTransactionContext(DbTransactionContext):
112
121
  self.__savepoints.clear()
113
122
  self.__transaction_name = None
114
123
  self.__ambient_transaction_id.set(None)
115
- if self.__transaction_depth == 0:
124
+ self.__transaction_depth = -2
116
125
  return 'COMMIT'
117
126
  else:
118
127
  return None
@@ -120,7 +129,6 @@ class MySQLTransactionContext(DbTransactionContext):
120
129
  elif prefix == 'ROLL':
121
130
  if self.__transaction_depth == 0:
122
131
  raise DbError('Cannot rollback, no transaction.')
123
- self.__transaction_depth = 0
124
132
  name = extract_rollback_name(sql)
125
133
 
126
134
  if name:
@@ -129,7 +137,7 @@ class MySQLTransactionContext(DbTransactionContext):
129
137
  sp = self.__savepoints.pop()
130
138
  if sp == name:
131
139
  break
132
- return 'ROLLBACK'
140
+ return f'ROLLBACK TO SAVEPOINT {name}'
133
141
  elif name == self.__transaction_name:
134
142
  pass # full rollback, depth reset below
135
143
  else:
@@ -137,7 +145,7 @@ class MySQLTransactionContext(DbTransactionContext):
137
145
  else:
138
146
  pass # full rollback, depth reset below
139
147
 
140
- self.__transaction_depth = 0
148
+ self.__transaction_depth = -3
141
149
  self.__savepoints.clear()
142
150
  self.__transaction_name = None
143
151
  self.__ambient_transaction_id.set(None)
@@ -234,8 +242,8 @@ class MySQLTransactionContext(DbTransactionContext):
234
242
  except mysql.connector.ProgrammingError:
235
243
  return cursor.rowcount
236
244
 
237
- def execute_script(self, sql: str, raw: str | None = None) -> None:
238
- if raw is not None:
245
+ def execute_script(self, sql: str, raw: bool = False) -> None:
246
+ if raw is True:
239
247
  self.execute(sql, raw=True)
240
248
  return
241
249
  lines = sql.split('\n')
@@ -126,7 +126,7 @@ class AsyncSQLiteTransactionContext(AsyncDbTransactionContext):
126
126
  async def execute_scalar(self, sql: str, parameters: DbParameters | None = None) -> Any:
127
127
  return self.__get_inner_ctx().execute_scalar(sql, parameters)
128
128
 
129
- async def execute_script(self, sql: str, raw: str | None = None) -> None:
129
+ async def execute_script(self, sql: str, raw: bool = False) -> None:
130
130
  self.__get_inner_ctx().execute_script(sql, raw)
131
131
 
132
132
  async def rollback(self, name: str | None = None) -> None:
@@ -45,17 +45,14 @@ class SQLiteProxyCursor(DbCursor):
45
45
  return self.__cursor.rowcount
46
46
 
47
47
  def execute(self, operation: str, parameters: DbParameters | None = None) -> None:
48
- if parameters is not None:
49
- operation = operation.replace(self.__sql_arg_expect, self.__sql_arg_subst)
50
- self.__logger.debug(f'execute({operation!r}, {parameters!r})')
51
- if parameters is None:
48
+ if parameters is None or len(parameters) == 0:
52
49
  self.__cursor.execute(operation)
53
50
  else:
51
+ operation = operation.replace(self.__sql_arg_expect, self.__sql_arg_subst)
54
52
  self.__cursor.execute(operation, parameters) # type: ignore[arg-type]
55
53
 
56
54
  def executemany(self, operation: str, seq_of_parameters: Sequence[DbParameters]) -> None:
57
55
  operation = operation.replace(self.__sql_arg_expect, self.__sql_arg_subst)
58
- self.__logger.debug(f'execute({operation!r}, {seq_of_parameters!r})')
59
56
  self.__cursor.executemany(operation, seq_of_parameters)
60
57
 
61
58
  def fetchone(self) -> tuple[Any, ...] | None:
@@ -39,7 +39,6 @@ class SQLiteTableAdapter(Generic[TEntity]):
39
39
  __initialized: bool
40
40
  __dbtype_mapper: DbTypeMapper
41
41
  __entity_spec: EntitySpec
42
- __transaction_state: int
43
42
  __table_name: str | None
44
43
 
45
44
  def __init__(
@@ -54,7 +53,6 @@ class SQLiteTableAdapter(Generic[TEntity]):
54
53
  self.__create_table = create_table is True
55
54
  self.__initialized = False
56
55
  self.__table_name = table_name
57
- self.__transaction_state = 0
58
56
 
59
57
  def __deferred_init(self) -> None:
60
58
  if not self.__initialized:
@@ -82,8 +82,12 @@ class SQLiteTransactionContext(DbTransactionContext):
82
82
  - None if scrubbed because the DBMS does not support it.
83
83
  - Raises DbError if operation is invalid.
84
84
  """
85
- if self.__transaction_depth < 0:
86
- raise DbError('Cannot use a transaction context that has been exited.')
85
+ if self.__transaction_depth == -1:
86
+ raise DbError('Cannot use a transaction context that has exited.')
87
+ elif self.__transaction_depth == -2:
88
+ raise DbError('Cannot use a transaction context that has been committed.')
89
+ elif self.__transaction_depth == -3:
90
+ raise DbError('Cannot use a transaction context that has been rolled back.')
87
91
 
88
92
  sql_upper = sql.lstrip().upper()
89
93
  prefix = sql_upper[:4]
@@ -111,7 +115,7 @@ class SQLiteTransactionContext(DbTransactionContext):
111
115
  self.__savepoints.clear()
112
116
  self.__transaction_name = None
113
117
  self.__ambient_transaction_id.set(None)
114
- if self.__transaction_depth == 0:
118
+ self.__transaction_depth = -2
115
119
  return 'COMMIT'
116
120
  else:
117
121
  return None
@@ -122,8 +126,6 @@ class SQLiteTransactionContext(DbTransactionContext):
122
126
  name = extract_rollback_name(sql)
123
127
  is_savepoint_rollback = is_rollback_to(sql)
124
128
 
125
- is_full_rollback = False
126
-
127
129
  if name:
128
130
  if name in self.__savepoints:
129
131
  while self.__savepoints:
@@ -131,21 +133,20 @@ class SQLiteTransactionContext(DbTransactionContext):
131
133
  if sp == name:
132
134
  break
133
135
  elif name == self.__transaction_name:
134
- is_full_rollback = True
136
+ pass # full rollback, depth reset below
135
137
  else:
136
138
  raise DbError('Invalid Transaction Name')
137
139
  else:
138
- is_full_rollback = True
140
+ pass # full rollback, depth reset below
139
141
 
140
- if is_full_rollback:
141
- self.__transaction_depth = 0
142
+ if is_savepoint_rollback:
143
+ return 'ROLLBACK TO SAVEPOINT ' + (name or '')
144
+ else:
145
+ self.__transaction_depth = -3
142
146
  self.__savepoints.clear()
143
147
  self.__transaction_name = None
144
148
  self.__ambient_transaction_id.set(None)
145
-
146
- if is_savepoint_rollback:
147
- return 'ROLLBACK TO SAVEPOINT ' + (name or '')
148
- return 'ROLLBACK TRANSACTION'
149
+ return 'ROLLBACK TRANSACTION'
149
150
 
150
151
  return sql
151
152
 
@@ -239,8 +240,8 @@ class SQLiteTransactionContext(DbTransactionContext):
239
240
  except sqlite3.ProgrammingError:
240
241
  return cursor.rowcount
241
242
 
242
- def execute_script(self, sql: str, raw: str | None = None) -> None:
243
- if raw is not None:
243
+ def execute_script(self, sql: str, raw: bool = False) -> None:
244
+ if raw is True:
244
245
  self.execute(sql, raw=True)
245
246
  return
246
247
  lines = sql.split('\n')
@@ -235,7 +235,7 @@ def resolve_mongodb_auth_source(connectionstring: ConnectionString) -> str:
235
235
  import pymongo
236
236
  from pymongo import errors as _pymongo_errors
237
237
 
238
- cache_key = f"{connectionstring.server}:{connectionstring.database}:{connectionstring.user}"
238
+ cache_key = f'{connectionstring.server}:{connectionstring.database}:{connectionstring.user}'
239
239
  cached = _MONGODB_AUTH_SOURCE_CACHE.get(cache_key)
240
240
  if cached is not None:
241
241
  return cached
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deev
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: ..an entity framework for Python.
5
5
  Author-email: Shaun Wilson <mrshaunwilson@msn.com>
6
6
  License-Expression: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes