reydb 1.1.52__py3-none-any.whl → 1.1.54__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.
reydb/rbase.py CHANGED
@@ -9,19 +9,28 @@
9
9
  """
10
10
 
11
11
 
12
- from typing import Any, TypedDict, Literal, overload
12
+ from typing import Any, TypedDict, Literal
13
+ from enum import EnumType
14
+ from sqlalchemy import text as sqlalchemy_text
13
15
  from sqlalchemy.engine.base import Engine, Connection
14
16
  from sqlalchemy.engine.url import URL
15
17
  from sqlalchemy.sql.elements import TextClause
16
18
  from reykit.rbase import Base, throw
17
19
  from reykit.rre import search
18
20
 
21
+ from reykit.rdata import to_json
22
+ from reykit.rre import findall
23
+
19
24
 
20
25
  __all__ = (
21
26
  'DatabaseBase',
27
+ 'handle_sql',
28
+ 'handle_data',
22
29
  'extract_url',
23
30
  'extract_engine',
24
- 'extract_path'
31
+ 'extract_path',
32
+ 'get_syntax',
33
+ 'is_multi_sql'
25
34
  )
26
35
 
27
36
 
@@ -47,6 +56,85 @@ class DatabaseBase(Base):
47
56
  """
48
57
 
49
58
 
59
+ def handle_sql(sql: str | TextClause) -> TextClause:
60
+ """
61
+ Handle SQL.
62
+
63
+ Parameters
64
+ ----------
65
+ sql : SQL in method `sqlalchemy.text` format, or TextClause object.
66
+
67
+ Returns
68
+ -------
69
+ TextClause instance.
70
+ """
71
+
72
+ # Handle parameter.
73
+ if type(sql) == TextClause:
74
+ sql = sql.text
75
+
76
+ # Handle.
77
+ sql = sql.strip()
78
+ if sql[-1] != ';':
79
+ sql += ';'
80
+ sql = sqlalchemy_text(sql)
81
+
82
+ return sql
83
+
84
+
85
+ def handle_data(data: list[dict], sql: str | TextClause) -> list[dict]:
86
+ """
87
+ Handle data based on the content of SQL.
88
+
89
+ Parameters
90
+ ----------
91
+ data : Data set for filling.
92
+ sql : SQL in method `sqlalchemy.text` format, or TextClause object.
93
+
94
+ Returns
95
+ -------
96
+ Filled data.
97
+ """
98
+
99
+ # Handle parameter.
100
+ if type(sql) == TextClause:
101
+ sql = sql.text
102
+
103
+ # Extract keys.
104
+ pattern = '(?<!\\\\):(\\w+)'
105
+ sql_keys = findall(pattern, sql)
106
+
107
+ # Extract keys of syntax "in".
108
+ pattern = '[iI][nN]\\s+(?<!\\\\):(\\w+)'
109
+ sql_keys_in = findall(pattern, sql)
110
+
111
+ # Loop.
112
+ for row in data:
113
+ if row == {}:
114
+ continue
115
+ for key in sql_keys:
116
+ value = row.get(key)
117
+
118
+ # Empty string.
119
+ if value == '':
120
+ value = None
121
+
122
+ # Convert.
123
+ elif (
124
+ type(value) in (list, dict)
125
+ and key not in sql_keys_in
126
+ ):
127
+ value = to_json(value)
128
+
129
+ # Enum.
130
+ elif isinstance(type(value), EnumType):
131
+ value = value.value
132
+
133
+ row[key] = value
134
+
135
+ return data
136
+
137
+
50
138
  def extract_url(url: str | URL) -> URLParameters:
51
139
  """
52
140
  Extract parameters from URL of string.
reydb/rconfig.py CHANGED
@@ -17,7 +17,7 @@ from datetime import (
17
17
  time as Time,
18
18
  timedelta as Timedelta
19
19
  )
20
- from reykit.rbase import null, throw
20
+ from reykit.rbase import Null, throw
21
21
 
22
22
  from .rdb import Database
23
23
 
@@ -445,10 +445,10 @@ class DatabaseConfig(object):
445
445
  """
446
446
 
447
447
  # Get.
448
- value = self.get(key, null)
448
+ value = self.get(key, Null)
449
449
 
450
450
  # Check.
451
- if value == null:
451
+ if value == Null:
452
452
  throw(KeyError, key)
453
453
 
454
454
  return value
reydb/rconn.py CHANGED
@@ -10,7 +10,8 @@
10
10
 
11
11
 
12
12
  from typing import Self
13
- from sqlalchemy import Transaction
13
+ from sqlalchemy import Connection, Transaction
14
+ from sqlalchemy.ext.asyncio import AsyncConnection, AsyncTransaction
14
15
 
15
16
  from .rbase import DatabaseBase
16
17
  from .rdb import Database
@@ -18,6 +19,7 @@ from .rdb import Database
18
19
 
19
20
  __all__ = (
20
21
  'DatabaseConnection',
22
+ 'DatabaseConnectionAsync'
21
23
  )
22
24
 
23
25
 
@@ -47,11 +49,44 @@ class DatabaseConnection(DatabaseBase):
47
49
  # Build.
48
50
  self.db = db
49
51
  self.autocommit = autocommit
50
- self.conn = db.engine.connect()
51
- self.exec = DatabaseExecute(self)
52
+ self.execute = DatabaseExecute(self)
53
+ self.conn: Connection | None = None
52
54
  self.begin: Transaction | None = None
53
55
 
54
56
 
57
+ def get_conn(self) -> Connection:
58
+ """
59
+ Get `Connection` instance.
60
+
61
+ Returns
62
+ -------
63
+ Instance.
64
+ """
65
+
66
+ # Create.
67
+ if self.conn is None:
68
+ self.conn = self.db.engine.connect()
69
+
70
+ return self.conn
71
+
72
+
73
+ def get_begin(self) -> Transaction:
74
+ """
75
+ Get `Transaction` instance.
76
+
77
+ Returns
78
+ -------
79
+ Instance.
80
+ """
81
+
82
+ # Create.
83
+ if self.begin is None:
84
+ conn = self.get_conn()
85
+ self.begin = conn.begin()
86
+
87
+ return self.begin
88
+
89
+
55
90
  def commit(self) -> None:
56
91
  """
57
92
  Commit cumulative executions.
@@ -80,7 +115,12 @@ class DatabaseConnection(DatabaseBase):
80
115
  """
81
116
 
82
117
  # Close.
83
- self.conn.close()
118
+ if self.begin is not None:
119
+ self.begin.close()
120
+ self.begin = None
121
+ if self.conn is not None:
122
+ self.conn.close()
123
+ self.conn = None
84
124
 
85
125
 
86
126
  def __enter__(self) -> Self:
@@ -113,34 +153,163 @@ class DatabaseConnection(DatabaseBase):
113
153
  self.commit()
114
154
 
115
155
  # Close.
116
- else:
117
- self.close()
156
+ self.close()
118
157
 
119
158
 
120
- __del__ = close
159
+ def insert_id(self) -> int:
160
+ """
161
+ Return last self increasing ID.
121
162
 
163
+ Returns
164
+ -------
165
+ ID.
166
+ """
122
167
 
123
- @property
124
- def execute(self):
168
+ # Get.
169
+ sql = 'SELECT LAST_INSERT_ID()'
170
+ result = self.execute(sql)
171
+ id_ = result.scalar()
172
+
173
+ return id_
174
+
175
+
176
+ class DatabaseConnectionAsync(DatabaseBase):
177
+ """
178
+ Asynchronous database connection type.
179
+ """
180
+
181
+
182
+ def __init__(
183
+ self,
184
+ db: Database,
185
+ autocommit: bool
186
+ ) -> None:
187
+ """
188
+ Build instance attributes.
189
+
190
+ Parameters
191
+ ----------
192
+ db : `DatabaseAsync` instance.
193
+ autocommit: Whether automatic commit execute.
125
194
  """
126
- Build `database execute` instance.
195
+
196
+ # Import.
197
+ from .rexec import DatabaseExecuteAsync
198
+
199
+ # Build.
200
+ self.db = db
201
+ self.autocommit = autocommit
202
+ self.aexecute = DatabaseExecuteAsync(self)
203
+ self.aconn: AsyncConnection | None = None
204
+ self.abegin: AsyncTransaction | None = None
205
+
206
+
207
+ async def get_conn(self) -> AsyncConnection:
208
+ """
209
+ Asynchronous get `Connection` instance.
127
210
 
128
211
  Returns
129
212
  -------
130
213
  Instance.
131
214
  """
132
215
 
133
- # Create transaction.
134
- if self.begin is None:
135
- self.begin = self.conn.begin()
216
+ # Create.
217
+ if self.aconn is None:
218
+ self.aconn = await self.db.aengine.connect()
136
219
 
137
- return self.exec
220
+ return self.aconn
138
221
 
139
222
 
140
- @property
141
- def insert_id(self) -> int:
223
+ async def get_begin(self) -> AsyncTransaction:
142
224
  """
143
- Return last self increasing ID.
225
+ Asynchronous get `Transaction` instance.
226
+
227
+ Returns
228
+ -------
229
+ Instance.
230
+ """
231
+
232
+ # Create.
233
+ if self.abegin is None:
234
+ conn = await self.get_conn()
235
+ self.abegin = await conn.begin()
236
+
237
+ return self.abegin
238
+
239
+
240
+ async def commit(self) -> None:
241
+ """
242
+ Asynchronous commit cumulative executions.
243
+ """
244
+
245
+ # Commit.
246
+ if self.abegin is not None:
247
+ await self.abegin.commit()
248
+ self.abegin = None
249
+
250
+
251
+ async def rollback(self) -> None:
252
+ """
253
+ Asynchronous rollback cumulative executions.
254
+ """
255
+
256
+ # Rollback.
257
+ if self.abegin is not None:
258
+ await self.abegin.rollback()
259
+ self.abegin = None
260
+
261
+
262
+ async def close(self) -> None:
263
+ """
264
+ Asynchronous close database connection.
265
+ """
266
+
267
+ # Close.
268
+ if self.abegin is not None:
269
+ await self.abegin.close()
270
+ self.abegin = None
271
+ if self.aconn is not None:
272
+ await self.aconn.close()
273
+ self.aconn = None
274
+
275
+
276
+ async def __aenter__(self):
277
+ """
278
+ Asynchronous enter syntax `async with`.
279
+
280
+ Returns
281
+ -------
282
+ Self.
283
+ """
284
+
285
+ return self
286
+
287
+
288
+ async def __aexit__(
289
+ self,
290
+ exc_type: type[BaseException] | None,
291
+ *_
292
+ ) -> None:
293
+ """
294
+ Asynchronous exit syntax `async with`.
295
+
296
+ Parameters
297
+ ----------
298
+ exc_type : Exception type.
299
+ """
300
+
301
+ # Commit.
302
+ if exc_type is None:
303
+ await self.commit()
304
+
305
+ # Close.
306
+ await self.close()
307
+ await self.db.dispose()
308
+
309
+
310
+ async def insert_id(self) -> int:
311
+ """
312
+ Asynchronous return last self increasing ID.
144
313
 
145
314
  Returns
146
315
  -------
@@ -149,7 +318,7 @@ class DatabaseConnection(DatabaseBase):
149
318
 
150
319
  # Get.
151
320
  sql = 'SELECT LAST_INSERT_ID()'
152
- result = self.execute(sql)
321
+ result = await self.aexecute(sql)
153
322
  id_ = result.scalar()
154
323
 
155
324
  return id_
reydb/rdb.py CHANGED
@@ -9,10 +9,12 @@
9
9
  """
10
10
 
11
11
 
12
+ from typing import Literal, Final, overload
12
13
  from urllib.parse import quote as urllib_quote
13
14
  from pymysql.constants.CLIENT import MULTI_STATEMENTS
14
15
  from sqlalchemy import create_engine as sqlalchemy_create_engine
15
16
  from sqlalchemy.engine.base import Engine
17
+ from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine as sqlalchemy_create_async_engine
16
18
  from reykit.rtext import join_data_text
17
19
 
18
20
  from .rbase import DatabaseBase, extract_url
@@ -26,10 +28,6 @@ __all__ = (
26
28
  class Database(DatabaseBase):
27
29
  """
28
30
  Database type, based `MySQL`.
29
-
30
- Attributes
31
- ----------
32
- default_report : Whether default to report execution.
33
31
  """
34
32
 
35
33
  default_report: bool = False
@@ -45,7 +43,7 @@ class Database(DatabaseBase):
45
43
  pool_size: int = 5,
46
44
  max_overflow: int = 10,
47
45
  pool_timeout: float = 30.0,
48
- pool_recycle: int | None = None,
46
+ pool_recycle: int | None = 3600,
49
47
  **query: str
50
48
  ) -> None:
51
49
  """
@@ -62,10 +60,7 @@ class Database(DatabaseBase):
62
60
  max_overflow : Number of connections `allowed overflow`.
63
61
  pool_timeout : Number of seconds `wait create` connection.
64
62
  pool_recycle : Number of seconds `recycle` connection.
65
- - `None`: Automatic select.
66
- When is remote server database, then is database variable `wait_timeout` value.
67
- When is local database file, then is `-1`.
68
- - `Literal[-1]`: No recycle.
63
+ - `None | Literal[-1]`: No recycle.
69
64
  - `int`: Use this value.
70
65
  query : Remote server database parameters.
71
66
  """
@@ -90,14 +85,8 @@ class Database(DatabaseBase):
90
85
  self.query = query
91
86
 
92
87
  # Create engine.
93
- self.engine = self.__create_engine()
94
-
95
- # Server recycle time.
96
- if pool_recycle is None:
97
- wait_timeout = self.variables['wait_timeout']
98
- if wait_timeout is not None:
99
- self.pool_recycle = int(wait_timeout)
100
- self.engine.pool._recycle = self.pool_recycle
88
+ self.engine = self.__create_engine(False)
89
+ self.aengine = self.__create_engine(True)
101
90
 
102
91
 
103
92
  @property
@@ -111,7 +100,8 @@ class Database(DatabaseBase):
111
100
  """
112
101
 
113
102
  # Get.
114
- url_params = extract_url(self.url)
103
+ url = self.url(False)
104
+ url_params = extract_url(url)
115
105
  backend = url_params['backend']
116
106
 
117
107
  return backend
@@ -128,17 +118,57 @@ class Database(DatabaseBase):
128
118
  """
129
119
 
130
120
  # Get.
131
- url_params = extract_url(self.url)
121
+ url = self.url(False)
122
+ url_params = extract_url(url)
132
123
  driver = url_params['driver']
133
124
 
134
125
  return driver
135
126
 
136
127
 
137
128
  @property
138
- def url(self) -> str:
129
+ def abackend(self) -> str:
130
+ """
131
+ Asynchronous database backend name.
132
+
133
+ Returns
134
+ -------
135
+ Name.
136
+ """
137
+
138
+ # Get.
139
+ url = self.url(True)
140
+ url_params = extract_url(url)
141
+ backend = url_params['backend']
142
+
143
+ return backend
144
+
145
+
146
+ @property
147
+ def adriver(self) -> str:
148
+ """
149
+ Asynchronous database driver name.
150
+
151
+ Returns
152
+ -------
153
+ Name.
154
+ """
155
+
156
+ # Get.
157
+ url = self.url(True)
158
+ url_params = extract_url(url)
159
+ driver = url_params['driver']
160
+
161
+ return driver
162
+
163
+
164
+ def url(self, is_async: bool) -> str:
139
165
  """
140
166
  Generate server URL.
141
167
 
168
+ Parameters
169
+ ----------
170
+ is_async : Whether to use asynchronous engine.
171
+
142
172
  Returns
143
173
  -------
144
174
  Server URL.
@@ -146,7 +176,10 @@ class Database(DatabaseBase):
146
176
 
147
177
  # Generate URL.
148
178
  password = urllib_quote(self.password)
149
- url_ = f'mysql+pymysql://{self.username}:{password}@{self.host}:{self.port}/{self.database}'
179
+ if is_async:
180
+ url_ = f'mysql+aiomysql://{self.username}:{password}@{self.host}:{self.port}/{self.database}'
181
+ else:
182
+ url_ = f'mysql+pymysql://{self.username}:{password}@{self.host}:{self.port}/{self.database}'
150
183
 
151
184
  # Add Server parameter.
152
185
  if self.query != {}:
@@ -161,18 +194,29 @@ class Database(DatabaseBase):
161
194
  return url_
162
195
 
163
196
 
164
- def __create_engine(self) -> Engine:
197
+ @overload
198
+ def __create_engine(self, is_async: Literal[False]) -> Engine: ...
199
+
200
+ @overload
201
+ def __create_engine(self, is_async: Literal[True]) -> AsyncEngine: ...
202
+
203
+ def __create_engine(self, is_async: bool) -> Engine | AsyncEngine:
165
204
  """
166
205
  Create database `Engine` object.
167
206
 
207
+ Parameters
208
+ ----------
209
+ is_async : Whether to use asynchronous engine.
210
+
168
211
  Returns
169
212
  -------
170
213
  Engine object.
171
214
  """
172
215
 
173
216
  # Handle parameter.
217
+ url = self.url(is_async)
174
218
  engine_params = {
175
- 'url': self.url,
219
+ 'url': url,
176
220
  'pool_size': self.pool_size,
177
221
  'max_overflow': self.max_overflow,
178
222
  'pool_timeout': self.pool_timeout,
@@ -181,23 +225,44 @@ class Database(DatabaseBase):
181
225
  }
182
226
 
183
227
  # Create Engine.
184
- engine = sqlalchemy_create_engine(**engine_params)
228
+ if is_async:
229
+ engine = sqlalchemy_create_async_engine(**engine_params)
230
+ else:
231
+ engine = sqlalchemy_create_engine(**engine_params)
185
232
 
186
233
  return engine
187
234
 
188
235
 
189
- @property
190
- def count_conn(self) -> tuple[int, int]:
236
+ async def dispose(self) -> None:
237
+ """
238
+ Dispose asynchronous connections.
239
+ """
240
+
241
+ # Dispose.
242
+ await self.aengine.dispose()
243
+
244
+
245
+ def __conn_count(self, is_async: bool) -> tuple[int, int]:
191
246
  """
192
247
  Count number of keep open and allowed overflow connection.
193
248
 
249
+ Parameters
250
+ ----------
251
+ is_async : Whether to use asynchronous engine.
252
+
194
253
  Returns
195
254
  -------
196
255
  Number of keep open and allowed overflow connection.
197
256
  """
198
257
 
258
+ # Handle parameter.
259
+ if is_async:
260
+ engine = self.aengine
261
+ else:
262
+ engine = self.engine
263
+
199
264
  # Count.
200
- _overflow = self.engine.pool._overflow
265
+ _overflow: int = engine.pool._overflow
201
266
  if _overflow < 0:
202
267
  keep_n = self.pool_size + _overflow
203
268
  overflow_n = 0
@@ -208,6 +273,38 @@ class Database(DatabaseBase):
208
273
  return keep_n, overflow_n
209
274
 
210
275
 
276
+ @property
277
+ def conn_count(self) -> tuple[int, int]:
278
+ """
279
+ Count number of keep open and allowed overflow connection.
280
+
281
+ Returns
282
+ -------
283
+ Number of keep open and allowed overflow connection.
284
+ """
285
+
286
+ # Count.
287
+ keep_n, overflow_n = self.__conn_count(False)
288
+
289
+ return keep_n, overflow_n
290
+
291
+
292
+ @property
293
+ def aconn_count(self) -> tuple[int, int]:
294
+ """
295
+ Count number of keep open and allowed overflow asynchronous connection.
296
+
297
+ Returns
298
+ -------
299
+ Number of keep open and allowed overflow asynchronous connection.
300
+ """
301
+
302
+ # Count.
303
+ keep_n, overflow_n = self.__conn_count(True)
304
+
305
+ return keep_n, overflow_n
306
+
307
+
211
308
  def schema(self, filter_default: bool = True) -> dict[str, dict[str, list[str]]]:
212
309
  """
213
310
  Get schemata of databases and tables and columns.
@@ -317,6 +414,45 @@ class Database(DatabaseBase):
317
414
  return exec
318
415
 
319
416
 
417
+ def aconnect(self, autocommit: bool = False):
418
+ """
419
+ Build `DatabaseConnectionAsync` instance.
420
+
421
+ Parameters
422
+ ----------
423
+ autocommit: Whether automatic commit execute.
424
+
425
+ Returns
426
+ -------
427
+ Database connection instance.
428
+ """
429
+
430
+ # Import.
431
+ from .rconn import DatabaseConnectionAsync
432
+
433
+ # Build.
434
+ conn = DatabaseConnectionAsync(self, autocommit)
435
+
436
+ return conn
437
+
438
+
439
+ @property
440
+ def aexecute(self):
441
+ """
442
+ Build `DatabaseConnectionAsync` instance.
443
+
444
+ Returns
445
+ -------
446
+ Instance.
447
+ """
448
+
449
+ # Build.
450
+ dbconn = self.aconnect(True)
451
+ exec = dbconn.aexecute
452
+
453
+ return exec
454
+
455
+
320
456
  @property
321
457
  def orm(self):
322
458
  """
@@ -546,7 +682,8 @@ class Database(DatabaseBase):
546
682
  for key, value in self.__dict__.items()
547
683
  if key not in filter_key
548
684
  }
549
- info['count'] = self.count_conn
685
+ info['conn_count'] = self.conn_count
686
+ info['aconn_count'] = self.aconn_count
550
687
  text = join_data_text(info)
551
688
 
552
689
  return text