ydb-sqlalchemy 0.0.1b4__py2.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.
@@ -0,0 +1,304 @@
1
+ import collections.abc
2
+ import dataclasses
3
+ import functools
4
+ import itertools
5
+ import logging
6
+ from typing import Any, Dict, List, Mapping, Optional, Sequence, Union
7
+
8
+ import ydb
9
+ import ydb.aio
10
+ from sqlalchemy import util
11
+
12
+ from .errors import (
13
+ DatabaseError,
14
+ DataError,
15
+ IntegrityError,
16
+ InternalError,
17
+ NotSupportedError,
18
+ OperationalError,
19
+ ProgrammingError,
20
+ )
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def get_column_type(type_obj: Any) -> str:
26
+ return str(ydb.convert.type_to_native(type_obj))
27
+
28
+
29
+ @dataclasses.dataclass
30
+ class YdbQuery:
31
+ yql_text: str
32
+ parameters_types: Dict[str, Union[ydb.PrimitiveType, ydb.AbstractTypeBuilder]] = dataclasses.field(
33
+ default_factory=dict
34
+ )
35
+ is_ddl: bool = False
36
+
37
+
38
+ def _handle_ydb_errors(func):
39
+ @functools.wraps(func)
40
+ def wrapper(*args, **kwargs):
41
+ try:
42
+ return func(*args, **kwargs)
43
+ except (ydb.issues.AlreadyExists, ydb.issues.PreconditionFailed) as e:
44
+ raise IntegrityError(e.message, original_error=e) from e
45
+ except (ydb.issues.Unsupported, ydb.issues.Unimplemented) as e:
46
+ raise NotSupportedError(e.message, original_error=e) from e
47
+ except (ydb.issues.BadRequest, ydb.issues.SchemeError) as e:
48
+ raise ProgrammingError(e.message, original_error=e) from e
49
+ except (
50
+ ydb.issues.TruncatedResponseError,
51
+ ydb.issues.ConnectionError,
52
+ ydb.issues.Aborted,
53
+ ydb.issues.Unavailable,
54
+ ydb.issues.Overloaded,
55
+ ydb.issues.Undetermined,
56
+ ydb.issues.Timeout,
57
+ ydb.issues.Cancelled,
58
+ ydb.issues.SessionBusy,
59
+ ydb.issues.SessionExpired,
60
+ ydb.issues.SessionPoolEmpty,
61
+ ) as e:
62
+ raise OperationalError(e.message, original_error=e) from e
63
+ except ydb.issues.GenericError as e:
64
+ raise DataError(e.message, original_error=e) from e
65
+ except ydb.issues.InternalError as e:
66
+ raise InternalError(e.message, original_error=e) from e
67
+ except ydb.Error as e:
68
+ raise DatabaseError(e.message, original_error=e) from e
69
+ except Exception as e:
70
+ raise DatabaseError("Failed to execute query") from e
71
+
72
+ return wrapper
73
+
74
+
75
+ class Cursor:
76
+ def __init__(
77
+ self,
78
+ session_pool: Union[ydb.SessionPool, ydb.aio.SessionPool],
79
+ tx_mode: ydb.AbstractTransactionModeBuilder,
80
+ tx_context: Optional[ydb.BaseTxContext] = None,
81
+ ):
82
+ self.session_pool = session_pool
83
+ self.tx_mode = tx_mode
84
+ self.tx_context = tx_context
85
+ self.description = None
86
+ self.arraysize = 1
87
+ self.rows = None
88
+ self._rows_prefetched = None
89
+
90
+ @_handle_ydb_errors
91
+ def describe_table(self, abs_table_path: str) -> ydb.TableDescription:
92
+ return self._retry_operation_in_pool(self._describe_table, abs_table_path)
93
+
94
+ def check_exists(self, table_path: str) -> bool:
95
+ try:
96
+ self._retry_operation_in_pool(self._describe_path, table_path)
97
+ return True
98
+ except ydb.SchemeError:
99
+ return False
100
+
101
+ @_handle_ydb_errors
102
+ def get_table_names(self) -> List[str]:
103
+ directory: ydb.Directory = self._retry_operation_in_pool(self._list_directory)
104
+ return [child.name for child in directory.children if child.is_table()]
105
+
106
+ def execute(self, operation: YdbQuery, parameters: Optional[Mapping[str, Any]] = None):
107
+ if operation.is_ddl or not operation.parameters_types:
108
+ query = operation.yql_text
109
+ is_ddl = operation.is_ddl
110
+ else:
111
+ query = ydb.DataQuery(operation.yql_text, operation.parameters_types)
112
+ is_ddl = operation.is_ddl
113
+
114
+ logger.info("execute sql: %s, params: %s", query, parameters)
115
+ if is_ddl:
116
+ chunks = self._execute_ddl(query)
117
+ else:
118
+ chunks = self._execute_dml(query, parameters)
119
+
120
+ rows = self._rows_iterable(chunks)
121
+ # Prefetch the description:
122
+ try:
123
+ first_row = next(rows)
124
+ except StopIteration:
125
+ pass
126
+ else:
127
+ rows = itertools.chain((first_row,), rows)
128
+ if self.rows is not None:
129
+ rows = itertools.chain(self.rows, rows)
130
+
131
+ self.rows = rows
132
+
133
+ @_handle_ydb_errors
134
+ def _execute_dml(
135
+ self, query: Union[ydb.DataQuery, str], parameters: Optional[Mapping[str, Any]] = None
136
+ ) -> ydb.convert.ResultSets:
137
+ prepared_query = query
138
+ if isinstance(query, str) and parameters:
139
+ if self.tx_context:
140
+ prepared_query = self._run_operation_in_session(self._prepare, query)
141
+ else:
142
+ prepared_query = self._retry_operation_in_pool(self._prepare, query)
143
+
144
+ if self.tx_context:
145
+ return self._run_operation_in_tx(self._execute_in_tx, prepared_query, parameters)
146
+
147
+ return self._retry_operation_in_pool(self._execute_in_session, self.tx_mode, prepared_query, parameters)
148
+
149
+ @_handle_ydb_errors
150
+ def _execute_ddl(self, query: str) -> ydb.convert.ResultSets:
151
+ return self._retry_operation_in_pool(self._execute_scheme, query)
152
+
153
+ @staticmethod
154
+ def _execute_scheme(session: ydb.Session, query: str) -> ydb.convert.ResultSets:
155
+ return session.execute_scheme(query)
156
+
157
+ @staticmethod
158
+ def _describe_table(session: ydb.Session, abs_table_path: str) -> ydb.TableDescription:
159
+ return session.describe_table(abs_table_path)
160
+
161
+ @staticmethod
162
+ def _describe_path(session: ydb.Session, table_path: str) -> ydb.SchemeEntry:
163
+ return session._driver.scheme_client.describe_path(table_path)
164
+
165
+ @staticmethod
166
+ def _list_directory(session: ydb.Session) -> ydb.Directory:
167
+ return session._driver.scheme_client.list_directory(session._driver._driver_config.database)
168
+
169
+ @staticmethod
170
+ def _prepare(session: ydb.Session, query: str) -> ydb.DataQuery:
171
+ return session.prepare(query)
172
+
173
+ @staticmethod
174
+ def _execute_in_tx(
175
+ tx_context: ydb.TxContext, prepared_query: ydb.DataQuery, parameters: Optional[Mapping[str, Any]]
176
+ ) -> ydb.convert.ResultSets:
177
+ return tx_context.execute(prepared_query, parameters, commit_tx=False)
178
+
179
+ @staticmethod
180
+ def _execute_in_session(
181
+ session: ydb.Session,
182
+ tx_mode: ydb.AbstractTransactionModeBuilder,
183
+ prepared_query: ydb.DataQuery,
184
+ parameters: Optional[Mapping[str, Any]],
185
+ ) -> ydb.convert.ResultSets:
186
+ return session.transaction(tx_mode).execute(prepared_query, parameters, commit_tx=True)
187
+
188
+ def _run_operation_in_tx(self, callee: collections.abc.Callable, *args, **kwargs):
189
+ return callee(self.tx_context, *args, **kwargs)
190
+
191
+ def _run_operation_in_session(self, callee: collections.abc.Callable, *args, **kwargs):
192
+ return callee(self.tx_context.session, *args, **kwargs)
193
+
194
+ def _retry_operation_in_pool(self, callee: collections.abc.Callable, *args, **kwargs):
195
+ return self.session_pool.retry_operation_sync(callee, None, *args, **kwargs)
196
+
197
+ def _rows_iterable(self, chunks_iterable: ydb.convert.ResultSets):
198
+ try:
199
+ for chunk in chunks_iterable:
200
+ self.description = [
201
+ (
202
+ col.name,
203
+ get_column_type(col.type),
204
+ None,
205
+ None,
206
+ None,
207
+ None,
208
+ None,
209
+ )
210
+ for col in chunk.columns
211
+ ]
212
+ for row in chunk.rows:
213
+ # returns tuple to be compatible with SqlAlchemy and because
214
+ # of this PEP to return a sequence: https://www.python.org/dev/peps/pep-0249/#fetchmany
215
+ yield row[::]
216
+ except ydb.Error as e:
217
+ raise DatabaseError(e.message, original_error=e) from e
218
+
219
+ def _ensure_prefetched(self):
220
+ if self.rows is not None and self._rows_prefetched is None:
221
+ self._rows_prefetched = list(self.rows)
222
+ self.rows = iter(self._rows_prefetched)
223
+ return self._rows_prefetched
224
+
225
+ def executemany(self, operation: YdbQuery, seq_of_parameters: Optional[Sequence[Mapping[str, Any]]]):
226
+ for parameters in seq_of_parameters:
227
+ self.execute(operation, parameters)
228
+
229
+ def executescript(self, script):
230
+ return self.execute(script)
231
+
232
+ def fetchone(self):
233
+ return next(self.rows or [], None)
234
+
235
+ def fetchmany(self, size=None):
236
+ return list(itertools.islice(self.rows, size or self.arraysize))
237
+
238
+ def fetchall(self):
239
+ return list(self.rows)
240
+
241
+ def nextset(self):
242
+ self.fetchall()
243
+
244
+ def setinputsizes(self, sizes):
245
+ pass
246
+
247
+ def setoutputsize(self, column=None):
248
+ pass
249
+
250
+ def close(self):
251
+ self.rows = None
252
+ self._rows_prefetched = None
253
+
254
+ @property
255
+ def rowcount(self):
256
+ return len(self._ensure_prefetched())
257
+
258
+
259
+ class AsyncCursor(Cursor):
260
+ _await = staticmethod(util.await_only)
261
+
262
+ @staticmethod
263
+ async def _describe_table(session: ydb.aio.table.Session, abs_table_path: str) -> ydb.TableDescription:
264
+ return await session.describe_table(abs_table_path)
265
+
266
+ @staticmethod
267
+ async def _describe_path(session: ydb.aio.table.Session, table_path: str) -> ydb.SchemeEntry:
268
+ return await session._driver.scheme_client.describe_path(table_path)
269
+
270
+ @staticmethod
271
+ async def _list_directory(session: ydb.aio.table.Session) -> ydb.Directory:
272
+ return await session._driver.scheme_client.list_directory(session._driver._driver_config.database)
273
+
274
+ @staticmethod
275
+ async def _execute_scheme(session: ydb.aio.table.Session, query: str) -> ydb.convert.ResultSets:
276
+ return await session.execute_scheme(query)
277
+
278
+ @staticmethod
279
+ async def _prepare(session: ydb.aio.table.Session, query: str) -> ydb.DataQuery:
280
+ return await session.prepare(query)
281
+
282
+ @staticmethod
283
+ async def _execute_in_tx(
284
+ tx_context: ydb.aio.table.TxContext, prepared_query: ydb.DataQuery, parameters: Optional[Mapping[str, Any]]
285
+ ) -> ydb.convert.ResultSets:
286
+ return await tx_context.execute(prepared_query, parameters, commit_tx=False)
287
+
288
+ @staticmethod
289
+ async def _execute_in_session(
290
+ session: ydb.aio.table.Session,
291
+ tx_mode: ydb.AbstractTransactionModeBuilder,
292
+ prepared_query: ydb.DataQuery,
293
+ parameters: Optional[Mapping[str, Any]],
294
+ ) -> ydb.convert.ResultSets:
295
+ return await session.transaction(tx_mode).execute(prepared_query, parameters, commit_tx=True)
296
+
297
+ def _run_operation_in_tx(self, callee: collections.abc.Coroutine, *args, **kwargs):
298
+ return self._await(callee(self.tx_context, *args, **kwargs))
299
+
300
+ def _run_operation_in_session(self, callee: collections.abc.Coroutine, *args, **kwargs):
301
+ return self._await(callee(self.tx_context.session, *args, **kwargs))
302
+
303
+ def _retry_operation_in_pool(self, callee: collections.abc.Coroutine, *args, **kwargs):
304
+ return self._await(self.session_pool.retry_operation(callee, *args, **kwargs))
@@ -0,0 +1,103 @@
1
+ from typing import List, Optional
2
+
3
+ import ydb
4
+ from google.protobuf.message import Message
5
+
6
+
7
+ class Warning(Exception):
8
+ pass
9
+
10
+
11
+ class Error(Exception):
12
+ def __init__(
13
+ self,
14
+ message: str,
15
+ original_error: Optional[ydb.Error] = None,
16
+ ):
17
+ super(Error, self).__init__(message)
18
+
19
+ self.original_error = original_error
20
+ if original_error:
21
+ pretty_issues = _pretty_issues(original_error.issues)
22
+ self.issues = original_error.issues
23
+ self.message = pretty_issues or message
24
+ self.status = original_error.status
25
+
26
+
27
+ class InterfaceError(Error):
28
+ pass
29
+
30
+
31
+ class DatabaseError(Error):
32
+ pass
33
+
34
+
35
+ class DataError(DatabaseError):
36
+ pass
37
+
38
+
39
+ class OperationalError(DatabaseError):
40
+ pass
41
+
42
+
43
+ class IntegrityError(DatabaseError):
44
+ pass
45
+
46
+
47
+ class InternalError(DatabaseError):
48
+ pass
49
+
50
+
51
+ class ProgrammingError(DatabaseError):
52
+ pass
53
+
54
+
55
+ class NotSupportedError(DatabaseError):
56
+ pass
57
+
58
+
59
+ def _pretty_issues(issues: List[Message]) -> str:
60
+ if issues is None:
61
+ return None
62
+
63
+ children_messages = [_get_messages(issue, root=True) for issue in issues]
64
+
65
+ if None in children_messages:
66
+ return None
67
+
68
+ return "\n" + "\n".join(children_messages)
69
+
70
+
71
+ def _get_messages(issue: Message, max_depth: int = 100, indent: int = 2, depth: int = 0, root: bool = False) -> str:
72
+ if depth >= max_depth:
73
+ return None
74
+
75
+ margin_str = " " * depth * indent
76
+ pre_message = ""
77
+ children = ""
78
+
79
+ if issue.issues:
80
+ collapsed_messages = []
81
+ while not root and len(issue.issues) == 1:
82
+ collapsed_messages.append(issue.message)
83
+ issue = issue.issues[0]
84
+
85
+ if collapsed_messages:
86
+ pre_message = f"{margin_str}{', '.join(collapsed_messages)}\n"
87
+ depth += 1
88
+ margin_str = " " * depth * indent
89
+
90
+ children_messages = [
91
+ _get_messages(iss, max_depth=max_depth, indent=indent, depth=depth + 1) for iss in issue.issues
92
+ ]
93
+
94
+ if None in children_messages:
95
+ return None
96
+
97
+ children = "\n".join(children_messages)
98
+
99
+ return (
100
+ f"{pre_message}{margin_str}{issue.message}\n{margin_str}"
101
+ f"severity level: {issue.severity}\n{margin_str}"
102
+ f"issue code: {issue.issue_code}\n{children}"
103
+ )