ormlambda 3.34.0__py3-none-any.whl → 3.34.5__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.
- ormlambda/__init__.py +33 -0
- ormlambda/databases/__init__.py +4 -0
- ormlambda/databases/my_sql/__init__.py +3 -0
- ormlambda/{dialects/mysql → databases/my_sql}/caster/caster.py +5 -1
- ormlambda/{dialects/mysql → databases/my_sql}/caster/types/__init__.py +2 -0
- ormlambda/databases/my_sql/caster/types/date.py +34 -0
- ormlambda/databases/my_sql/caster/types/decimal.py +32 -0
- ormlambda/{dialects/mysql → databases/my_sql}/clauses/__init__.py +1 -0
- ormlambda/databases/my_sql/clauses/drop_table.py +26 -0
- ormlambda/{dialects/mysql/repository → databases/my_sql}/repository.py +5 -0
- ormlambda/dialects/mysql/__init__.py +3 -33
- ormlambda/dialects/mysql/base.py +194 -129
- ormlambda/dialects/mysql/types.py +4 -1
- ormlambda/engine/base.py +4 -23
- ormlambda/repository/interfaces/IRepositoryBase.py +7 -0
- ormlambda/sql/clause_info/clause_info.py +13 -0
- ormlambda/sql/column/column.py +26 -7
- ormlambda/sql/compiler.py +2 -243
- ormlambda/sql/ddl.py +4 -18
- ormlambda/sql/foreign_key.py +0 -18
- ormlambda/sql/sqltypes.py +12 -6
- ormlambda/sql/table/table.py +9 -5
- ormlambda/sql/type_api.py +3 -0
- ormlambda/sql/visitors.py +3 -0
- ormlambda/statements/interfaces/IStatements.py +0 -3
- ormlambda/statements/statements.py +1 -7
- ormlambda/util/__init__.py +2 -1
- ormlambda/util/load_module.py +21 -0
- ormlambda/util/module_tree/dynamic_module.py +1 -1
- {ormlambda-3.34.0.dist-info → ormlambda-3.34.5.dist-info}/METADATA +2 -3
- {ormlambda-3.34.0.dist-info → ormlambda-3.34.5.dist-info}/RECORD +59 -54
- {ormlambda-3.34.0.dist-info → ormlambda-3.34.5.dist-info}/WHEEL +1 -1
- ormlambda/dialects/mysql/repository/__init__.py +0 -1
- /ormlambda/{dialects/mysql → databases/my_sql}/caster/__init__.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/caster/types/boolean.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/caster/types/bytes.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/caster/types/datetime.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/caster/types/float.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/caster/types/int.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/caster/types/iterable.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/caster/types/none.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/caster/types/point.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/caster/types/string.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/clauses/ST_AsText.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/clauses/ST_Contains.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/clauses/count.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/clauses/delete.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/clauses/group_by.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/clauses/having.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/clauses/insert.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/clauses/joins.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/clauses/limit.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/clauses/offset.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/clauses/order.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/clauses/update.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/clauses/upsert.py +0 -0
- /ormlambda/{dialects/mysql → databases/my_sql}/clauses/where.py +0 -0
- /ormlambda/{dialects/mysql/repository → databases/my_sql}/pool_types.py +0 -0
- {ormlambda-3.34.0.dist-info → ormlambda-3.34.5.dist-info}/AUTHORS +0 -0
- {ormlambda-3.34.0.dist-info → ormlambda-3.34.5.dist-info}/LICENSE +0 -0
@@ -32,7 +32,7 @@ class _NumericType(_NumericCommonType, sqltypes.Numeric): ...
|
|
32
32
|
class _FloatType(_NumericCommonType, sqltypes.Float):
|
33
33
|
def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
|
34
34
|
if isinstance(self, (REAL, DOUBLE)) and ((precision is None and scale is not None) or (precision is not None and scale is None)):
|
35
|
-
raise AttributeError("You must specify both precision and scale or omit
|
35
|
+
raise AttributeError("You must specify both precision and scale or omit both altogether.")
|
36
36
|
super().__init__(precision=precision, asdecimal=asdecimal, **kw)
|
37
37
|
self.scale = scale
|
38
38
|
|
@@ -42,6 +42,9 @@ class _IntegerType(_NumericCommonType, sqltypes.Integer):
|
|
42
42
|
self.display_width = display_width
|
43
43
|
super().__init__(**kw)
|
44
44
|
|
45
|
+
def __repr__(self):
|
46
|
+
return f"{type(self).__name__}({self.display_width})"
|
47
|
+
|
45
48
|
|
46
49
|
class _StringType(sqltypes.String):
|
47
50
|
"""Base for MySQL string types."""
|
ormlambda/engine/base.py
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
from
|
3
|
-
from typing import TYPE_CHECKING, BinaryIO, Literal, Optional, TextIO
|
2
|
+
from typing import TYPE_CHECKING, Literal
|
4
3
|
from ormlambda.engine import url
|
5
|
-
from ormlambda.sql.ddl import CreateSchema, DropSchema
|
4
|
+
from ormlambda.sql.ddl import CreateSchema, DropSchema
|
5
|
+
|
6
6
|
|
7
7
|
if TYPE_CHECKING:
|
8
8
|
from ormlambda.dialects import Dialect
|
@@ -20,7 +20,7 @@ class Engine:
|
|
20
20
|
if if_exists == "replace":
|
21
21
|
self.drop_schema(schema_name, if_exists)
|
22
22
|
|
23
|
-
sql = CreateSchema(schema=schema_name, if_not_exists=if_exists
|
23
|
+
sql = CreateSchema(schema=schema_name, if_not_exists=if_exists== 'append').compile(self.dialect).string
|
24
24
|
try:
|
25
25
|
self.repository.execute(sql)
|
26
26
|
except Exception:
|
@@ -56,22 +56,3 @@ class Engine:
|
|
56
56
|
def set_database(self, name: str) -> None:
|
57
57
|
self.repository.database = name
|
58
58
|
return None
|
59
|
-
|
60
|
-
def create_backup(
|
61
|
-
self,
|
62
|
-
output: Optional[str | BinaryIO | TextIO] = None,
|
63
|
-
compress: bool = False,
|
64
|
-
backup_dir: str = "backups",
|
65
|
-
**kw,
|
66
|
-
) -> Optional[str | BinaryIO | Path]:
|
67
|
-
return (
|
68
|
-
CreateBackup(self.url)
|
69
|
-
.compile(
|
70
|
-
self.dialect,
|
71
|
-
output=output,
|
72
|
-
compress=compress,
|
73
|
-
backup_dir=backup_dir,
|
74
|
-
**kw,
|
75
|
-
)
|
76
|
-
.string
|
77
|
-
)
|
@@ -9,8 +9,12 @@ from typing import (
|
|
9
9
|
Sequence,
|
10
10
|
Type,
|
11
11
|
Iterable,
|
12
|
+
TYPE_CHECKING,
|
12
13
|
)
|
13
14
|
|
15
|
+
if TYPE_CHECKING:
|
16
|
+
from ormlambda.statements.types import TypeExists
|
17
|
+
|
14
18
|
|
15
19
|
type _DBAPICursorDescription = Sequence[
|
16
20
|
tuple[
|
@@ -138,6 +142,9 @@ class IRepositoryBase(ABC):
|
|
138
142
|
@abstractmethod
|
139
143
|
def execute(self, query: str) -> None: ...
|
140
144
|
|
145
|
+
@abstractmethod
|
146
|
+
def drop_table(self, name: str) -> None: ...
|
147
|
+
|
141
148
|
@abstractmethod
|
142
149
|
def table_exists(self, name: str) -> bool: ...
|
143
150
|
|
@@ -9,6 +9,7 @@ from ormlambda.sql.types import (
|
|
9
9
|
TableType,
|
10
10
|
ColumnType,
|
11
11
|
AliasType,
|
12
|
+
TypeEngine,
|
12
13
|
)
|
13
14
|
from .interface import IClauseInfo
|
14
15
|
from ormlambda.sql import ForeignKey
|
@@ -166,6 +167,18 @@ class ClauseInfo[T: Table](IClauseInfo[T]):
|
|
166
167
|
return self._column
|
167
168
|
return type(self._column)
|
168
169
|
|
170
|
+
@property
|
171
|
+
def dbtype(self)->tp.Optional[TypeEngine]:
|
172
|
+
if self._dtype is not None:
|
173
|
+
return self._dtype
|
174
|
+
|
175
|
+
if isinstance(self._column, Column):
|
176
|
+
return self._column.dbtype
|
177
|
+
|
178
|
+
if isinstance(self._column, type):
|
179
|
+
return self._column
|
180
|
+
return type(self._column)
|
181
|
+
|
169
182
|
def query(self, dialect: Dialect, **kwargs) -> str:
|
170
183
|
return self._create_query(dialect, **kwargs)
|
171
184
|
|
ormlambda/sql/column/column.py
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
import abc
|
3
|
-
from typing import Annotated, Any, Iterable, Type, Optional, TYPE_CHECKING, get_type_hints, overload, get_origin, get_args
|
3
|
+
from typing import Annotated, Any, ClassVar, Iterable, Type, Optional, TYPE_CHECKING, get_type_hints, overload, get_origin, get_args
|
4
4
|
from ormlambda.sql.types import TableType, ComparerType, ColumnType
|
5
5
|
from ormlambda import ConditionType
|
6
|
+
from ormlambda.sql.type_api import TypeEngine
|
6
7
|
|
7
8
|
if TYPE_CHECKING:
|
8
9
|
import re
|
9
10
|
from ormlambda import Table
|
10
11
|
from ormlambda.sql.comparer import Comparer, Regex, Like
|
11
|
-
from ormlambda.sql.type_api import TypeEngine
|
12
12
|
|
13
13
|
|
14
14
|
from ormlambda.types import (
|
@@ -24,10 +24,13 @@ from ormlambda.types import (
|
|
24
24
|
|
25
25
|
|
26
26
|
class Column[TProp]:
|
27
|
-
PRIVATE_CHAR: str = "_"
|
27
|
+
PRIVATE_CHAR: ClassVar[str] = "_"
|
28
|
+
|
29
|
+
_dbtype: Optional[TypeEngine]
|
28
30
|
|
29
31
|
__slots__ = (
|
30
|
-
"
|
32
|
+
"_dtype",
|
33
|
+
"_dbtype",
|
31
34
|
"column_name",
|
32
35
|
"table",
|
33
36
|
"is_primary_key",
|
@@ -111,7 +114,8 @@ class Column[TProp]:
|
|
111
114
|
|
112
115
|
def __set__(self, obj, value):
|
113
116
|
if self._check and value is not None:
|
114
|
-
if not isinstance(
|
117
|
+
type_ = self.dtype if not isinstance(self.dtype, TypeEngine) else self.dtype.python_type
|
118
|
+
if not isinstance(value, type_):
|
115
119
|
raise ValueError(f"The '{self.column_name}' Column from '{self.table.__table_name__}' table expected '{str(self.dtype)}' type. You passed '{type(value).__name__}' type")
|
116
120
|
setattr(obj, self.__private_name, value)
|
117
121
|
|
@@ -132,8 +136,6 @@ class Column[TProp]:
|
|
132
136
|
def _fill_from_annotations[T: Table](self, obj: Type[T], name: str) -> None:
|
133
137
|
"""Read the metada when using Annotated typing class, and set the attributes accordingly"""
|
134
138
|
|
135
|
-
from ormlambda.sql.type_api import TypeEngine
|
136
|
-
|
137
139
|
annotations = get_type_hints(obj, include_extras=True)
|
138
140
|
if name in annotations:
|
139
141
|
annotation = annotations[name]
|
@@ -163,6 +165,23 @@ class Column[TProp]:
|
|
163
165
|
self.is_not_null = True
|
164
166
|
return None
|
165
167
|
|
168
|
+
@property
|
169
|
+
def dtype(self) -> Type[TProp]:
|
170
|
+
return self._dtype
|
171
|
+
|
172
|
+
@dtype.setter
|
173
|
+
def dtype(self, value: Type[TProp] | TypeEngine) -> None:
|
174
|
+
if isinstance(value, TypeEngine):
|
175
|
+
self._dtype = value.python_type
|
176
|
+
self._dbtype = value
|
177
|
+
else:
|
178
|
+
self._dtype = value
|
179
|
+
self._dbtype = None
|
180
|
+
|
181
|
+
@property
|
182
|
+
def dbtype(self) -> TypeEngine[TProp]:
|
183
|
+
return self._dbtype if self._dbtype else self.dtype
|
184
|
+
|
166
185
|
@abc.abstractmethod
|
167
186
|
def __comparer_creator(self, other: ColumnType, compare: ComparerType) -> Comparer:
|
168
187
|
from ormlambda.sql.comparer import Comparer
|
ormlambda/sql/compiler.py
CHANGED
@@ -1,13 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
import abc
|
3
|
-
import
|
4
|
-
import io
|
5
|
-
import logging
|
6
|
-
import os
|
7
|
-
from pathlib import Path
|
8
|
-
import subprocess
|
9
|
-
import sys
|
10
|
-
from typing import Any, BinaryIO, ClassVar, Optional, TYPE_CHECKING, TextIO, Union
|
3
|
+
from typing import Any, ClassVar, Optional, TYPE_CHECKING
|
11
4
|
|
12
5
|
from ormlambda.sql.ddl import CreateColumn
|
13
6
|
from ormlambda.sql.foreign_key import ForeignKey
|
@@ -22,13 +15,7 @@ if TYPE_CHECKING:
|
|
22
15
|
from .visitors import Element
|
23
16
|
from .elements import ClauseElement
|
24
17
|
from ormlambda.dialects import Dialect
|
25
|
-
from ormlambda.sql.ddl import
|
26
|
-
CreateTable,
|
27
|
-
CreateSchema,
|
28
|
-
DropSchema,
|
29
|
-
DropTable,
|
30
|
-
CreateBackup,
|
31
|
-
)
|
18
|
+
from ormlambda.sql.ddl import CreateTable, CreateSchema, DropSchema
|
32
19
|
from .sqltypes import (
|
33
20
|
INTEGER,
|
34
21
|
SMALLINTEGER,
|
@@ -75,12 +62,6 @@ if TYPE_CHECKING:
|
|
75
62
|
)
|
76
63
|
|
77
64
|
|
78
|
-
type customString = Union[str | Path]
|
79
|
-
|
80
|
-
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
|
81
|
-
log = logging.getLogger(__name__)
|
82
|
-
|
83
|
-
|
84
65
|
class Compiled:
|
85
66
|
"""Represent a compiled SQL or DDL expression.
|
86
67
|
|
@@ -268,9 +249,6 @@ class DDLCompiler(Compiled):
|
|
268
249
|
sql += f"\n){table_options};"
|
269
250
|
return sql
|
270
251
|
|
271
|
-
def visit_drop_table(self, drop: DropTable, **kw) -> str:
|
272
|
-
return "DROP TABLE " + drop.element.__table_name__
|
273
|
-
|
274
252
|
def visit_create_column(self, create: CreateColumn, first_pk=False, **kw): # noqa: F821
|
275
253
|
column = create.element
|
276
254
|
return self.get_column_specification(column)
|
@@ -295,225 +273,6 @@ class DDLCompiler(Compiled):
|
|
295
273
|
return None
|
296
274
|
return None
|
297
275
|
|
298
|
-
#TODOH []: refactor in order to improve clarity
|
299
|
-
def visit_create_backup(
|
300
|
-
self,
|
301
|
-
backup: CreateBackup,
|
302
|
-
output: Optional[Union[Path | str, BinaryIO, TextIO]] = None,
|
303
|
-
compress: bool = False,
|
304
|
-
backup_dir: customString = ".",
|
305
|
-
**kw,
|
306
|
-
) -> Optional[str | BinaryIO | Path]:
|
307
|
-
"""
|
308
|
-
Create MySQL backup with flexible output options
|
309
|
-
|
310
|
-
Args:
|
311
|
-
backup: An object containing database connection details (host, user, password, database, port).
|
312
|
-
output: Output destination:
|
313
|
-
- None: Auto-generate file path
|
314
|
-
- str: Custom file path (treated as a path-like object)
|
315
|
-
- Stream object: Write to stream (io.StringIO, io.BytesIO, sys.stdout, etc.)
|
316
|
-
compress: Whether to compress the output using gzip.
|
317
|
-
backup_dir: Directory for auto-generated files if 'output' is None.
|
318
|
-
|
319
|
-
Returns:
|
320
|
-
- File path (str) if output to file.
|
321
|
-
- Backup data as bytes (if output to binary stream) or string (if output to text stream).
|
322
|
-
- None if an error occurs.
|
323
|
-
"""
|
324
|
-
|
325
|
-
host = backup.url.host
|
326
|
-
user = backup.url.username
|
327
|
-
password = backup.url.password
|
328
|
-
database = backup.url.database
|
329
|
-
port = backup.url.port
|
330
|
-
|
331
|
-
if not database:
|
332
|
-
log.error("Error: Database name is required for backup.")
|
333
|
-
return None
|
334
|
-
|
335
|
-
# Build mysqldump command
|
336
|
-
command = [
|
337
|
-
"mysqldump",
|
338
|
-
f"--host={host}",
|
339
|
-
f"--port={port}",
|
340
|
-
f"--user={user}",
|
341
|
-
f"--password={password}",
|
342
|
-
"--single-transaction",
|
343
|
-
"--routines",
|
344
|
-
"--triggers",
|
345
|
-
"--events",
|
346
|
-
"--lock-tables=false", # Often used to avoid locking during backup
|
347
|
-
"--add-drop-table",
|
348
|
-
"--extended-insert",
|
349
|
-
database,
|
350
|
-
]
|
351
|
-
|
352
|
-
def export_to_stream_internal() -> Optional[io.BytesIO]:
|
353
|
-
nonlocal command, compress, database
|
354
|
-
# If streaming, execute mysqldump and capture stdout
|
355
|
-
log.info(f"Backing up database '{database}' to BytesIO...")
|
356
|
-
|
357
|
-
try:
|
358
|
-
if compress:
|
359
|
-
# Start mysqldump process
|
360
|
-
mysqldump_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
361
|
-
|
362
|
-
# Start gzip process, taking input from mysqldump
|
363
|
-
gzip_process = subprocess.Popen(["gzip", "-c"], stdin=mysqldump_process.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
364
|
-
|
365
|
-
# Close mysqldump stdout in parent process - gzip will handle it
|
366
|
-
mysqldump_process.stdout.close()
|
367
|
-
|
368
|
-
# Wait for gzip to complete (which will also wait for mysqldump)
|
369
|
-
gzip_stdout, gzip_stderr = gzip_process.communicate()
|
370
|
-
|
371
|
-
# Wait for mysqldump to finish and get its stderr
|
372
|
-
mysqldump_stderr = mysqldump_process.communicate()[1]
|
373
|
-
|
374
|
-
if mysqldump_process.returncode != 0:
|
375
|
-
log.error(f"mysqldump error: {mysqldump_stderr.decode().strip()}")
|
376
|
-
return None
|
377
|
-
if gzip_process.returncode != 0:
|
378
|
-
log.error(f"gzip error: {gzip_stderr.decode().strip()}")
|
379
|
-
return None
|
380
|
-
|
381
|
-
log.info("Backup successful and compressed to BytesIO.")
|
382
|
-
return io.BytesIO(gzip_stdout)
|
383
|
-
else:
|
384
|
-
# Directly capture mysqldump output
|
385
|
-
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
386
|
-
stdout, stderr = process.communicate()
|
387
|
-
|
388
|
-
if process.returncode != 0:
|
389
|
-
log.error(f"mysqldump error: {stderr.decode().strip()}")
|
390
|
-
return None
|
391
|
-
|
392
|
-
log.info("Backup successful to BytesIO.")
|
393
|
-
return io.BytesIO(stdout)
|
394
|
-
|
395
|
-
except FileNotFoundError as e:
|
396
|
-
log.error(f"Error: '{e.filename}' command not found. Please ensure mysqldump (and gzip if compressing) is installed and in your system's PATH.")
|
397
|
-
return None
|
398
|
-
except Exception as e:
|
399
|
-
log.error(f"An unexpected error occurred during streaming backup: {e}")
|
400
|
-
return None
|
401
|
-
|
402
|
-
def export_to_file_internal(file_path: customString) -> Optional[Path]:
|
403
|
-
nonlocal command, compress, database
|
404
|
-
|
405
|
-
if isinstance(file_path, str):
|
406
|
-
file_path = Path(file_path)
|
407
|
-
|
408
|
-
if not file_path.exists():
|
409
|
-
file_path.parent.mkdir(parents=True, exist_ok=True)
|
410
|
-
file_path.touch()
|
411
|
-
|
412
|
-
try:
|
413
|
-
if compress:
|
414
|
-
# Pipe mysqldump output through gzip to file
|
415
|
-
with open(file_path, "wb") as output_file:
|
416
|
-
# Start mysqldump process
|
417
|
-
mysqldump_process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
418
|
-
|
419
|
-
# Start gzip process, taking input from mysqldump and writing to file
|
420
|
-
gzip_process = subprocess.Popen(["gzip", "-c"], stdin=mysqldump_process.stdout, stdout=output_file, stderr=subprocess.PIPE)
|
421
|
-
|
422
|
-
# Close mysqldump stdout in parent process - gzip will handle it
|
423
|
-
mysqldump_process.stdout.close()
|
424
|
-
|
425
|
-
# Wait for gzip to complete (which will also wait for mysqldump)
|
426
|
-
gzip_stdout, gzip_stderr = gzip_process.communicate()
|
427
|
-
|
428
|
-
# Wait for mysqldump to finish and get its stderr
|
429
|
-
mysqldump_stderr = mysqldump_process.communicate()[1]
|
430
|
-
|
431
|
-
if mysqldump_process.returncode != 0:
|
432
|
-
log.error(f"mysqldump error: {mysqldump_stderr.decode().strip()}")
|
433
|
-
return None
|
434
|
-
if gzip_process.returncode != 0:
|
435
|
-
log.error(f"gzip error: {gzip_stderr.decode().strip()}")
|
436
|
-
return None
|
437
|
-
else:
|
438
|
-
# Directly redirect mysqldump output to file
|
439
|
-
with open(file_path, "wb") as output_file:
|
440
|
-
process = subprocess.Popen(command, stdout=output_file, stderr=subprocess.PIPE)
|
441
|
-
stdout, stderr = process.communicate()
|
442
|
-
|
443
|
-
if process.returncode != 0:
|
444
|
-
log.error(f"mysqldump error: {stderr.decode().strip()}")
|
445
|
-
return None
|
446
|
-
|
447
|
-
log.info(f"Backup completed successfully: {file_path}")
|
448
|
-
return file_path
|
449
|
-
|
450
|
-
except FileNotFoundError as e:
|
451
|
-
log.error(f"Error: '{e.filename}' command not found. Please ensure mysqldump (and gzip if compressing) is installed and in your system's PATH.")
|
452
|
-
return None
|
453
|
-
except Exception as e:
|
454
|
-
log.error(f"An unexpected error occurred during file backup: {e}")
|
455
|
-
return None
|
456
|
-
|
457
|
-
try:
|
458
|
-
if output is None:
|
459
|
-
# Auto-generate file path
|
460
|
-
|
461
|
-
backup_dir = Path(backup_dir)
|
462
|
-
backup_dir.mkdir(exist_ok=True)
|
463
|
-
|
464
|
-
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
465
|
-
file_extension = "sql.gz" if compress else "sql"
|
466
|
-
output_filename = f"{database}_backup_{timestamp}.{file_extension}"
|
467
|
-
output_filepath = os.path.join(backup_dir, output_filename)
|
468
|
-
return export_to_file_internal(output_filepath)
|
469
|
-
|
470
|
-
elif isinstance(output, (io.BytesIO, io.StringIO)):
|
471
|
-
# Output to a stream object
|
472
|
-
stream_result = export_to_stream_internal()
|
473
|
-
if stream_result:
|
474
|
-
# Write the content from the internal BytesIO to the provided output stream
|
475
|
-
if isinstance(output, io.BytesIO):
|
476
|
-
output.write(stream_result.getvalue())
|
477
|
-
return stream_result.getvalue() # Return bytes if it was a BytesIO internally
|
478
|
-
elif isinstance(output, io.StringIO):
|
479
|
-
# Attempt to decode bytes to string if target is StringIO
|
480
|
-
try:
|
481
|
-
decoded_content = stream_result.getvalue().decode("utf-8")
|
482
|
-
output.write(decoded_content)
|
483
|
-
return decoded_content
|
484
|
-
except UnicodeDecodeError:
|
485
|
-
log.error("Error: Cannot decode byte stream to UTF-8 for StringIO output. Consider setting compress=False or ensuring database encoding is compatible.")
|
486
|
-
return None
|
487
|
-
return None
|
488
|
-
|
489
|
-
elif isinstance(output, str | Path):
|
490
|
-
# Output to a custom file path
|
491
|
-
return export_to_file_internal(output)
|
492
|
-
|
493
|
-
elif isinstance(output, (BinaryIO, TextIO)): # Handles sys.stdout, open file objects
|
494
|
-
stream_result = export_to_stream_internal()
|
495
|
-
if stream_result:
|
496
|
-
if "b" in getattr(output, "mode", "") or isinstance(output, BinaryIO): # Check if it's a binary stream
|
497
|
-
output.write(stream_result.getvalue())
|
498
|
-
return stream_result.getvalue()
|
499
|
-
else: # Assume text stream
|
500
|
-
try:
|
501
|
-
decoded_content = stream_result.getvalue().decode("utf-8")
|
502
|
-
output.write(decoded_content)
|
503
|
-
return decoded_content
|
504
|
-
except UnicodeDecodeError:
|
505
|
-
log.error("Error: Cannot decode byte stream to UTF-8 for text stream output. Consider setting compress=False or ensuring database encoding is compatible.")
|
506
|
-
return None
|
507
|
-
return None
|
508
|
-
|
509
|
-
else:
|
510
|
-
log.error(f"Unsupported output type: {type(output)}")
|
511
|
-
return None
|
512
|
-
|
513
|
-
except Exception as e:
|
514
|
-
log.error(f"An unexpected error occurred: {e}")
|
515
|
-
return None
|
516
|
-
|
517
276
|
|
518
277
|
class GenericTypeCompiler(TypeCompiler):
|
519
278
|
"""Generic type compiler
|
ormlambda/sql/ddl.py
CHANGED
@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
|
|
3
3
|
from .elements import ClauseElement
|
4
4
|
|
5
5
|
if TYPE_CHECKING:
|
6
|
-
from ormlambda import URL
|
7
6
|
from ormlambda.dialects.interface.dialect import Dialect
|
8
7
|
from ormlambda import Column
|
9
8
|
from ormlambda import Table
|
@@ -23,22 +22,16 @@ class BaseDDLElement(ClauseElement):
|
|
23
22
|
return dialect.ddl_compiler(dialect, self, **kw)
|
24
23
|
|
25
24
|
|
26
|
-
class
|
27
|
-
def __init__(self, element: Table):
|
28
|
-
self.element = element
|
29
|
-
self.columns = [CreateColumn(c) for c in element.get_columns()]
|
30
|
-
|
31
|
-
|
32
|
-
class CreateTable(CreateDropTable, BaseDDLElement):
|
25
|
+
class CreateTable(BaseDDLElement):
|
33
26
|
"""
|
34
27
|
Class representing a CREATE TABLE statement.
|
35
28
|
"""
|
36
29
|
|
37
30
|
__visit_name__ = "create_table"
|
38
31
|
|
39
|
-
|
40
|
-
|
41
|
-
|
32
|
+
def __init__(self, element: Table):
|
33
|
+
self.element = element
|
34
|
+
self.columns = [CreateColumn(c) for c in element.get_columns()]
|
42
35
|
|
43
36
|
|
44
37
|
class CreateColumn[T](BaseDDLElement):
|
@@ -73,10 +66,3 @@ class SchemaExists(BaseDDLElement):
|
|
73
66
|
|
74
67
|
def __init__(self, schema: str):
|
75
68
|
self.schema = schema
|
76
|
-
|
77
|
-
|
78
|
-
class CreateBackup(BaseDDLElement):
|
79
|
-
__visit_name__ = "create_backup"
|
80
|
-
|
81
|
-
def __init__(self, url: URL):
|
82
|
-
self.url = url
|
ormlambda/sql/foreign_key.py
CHANGED
@@ -36,14 +36,6 @@ class ForeignKeyContext(set["ForeignKey"]):
|
|
36
36
|
class ForeignKey[TLeft: Table, TRight: Table](Element, IQuery):
|
37
37
|
__visit_name__ = "foreign_key"
|
38
38
|
|
39
|
-
__slots__ = (
|
40
|
-
"_tright",
|
41
|
-
"_relationship",
|
42
|
-
"_comparer",
|
43
|
-
"_clause_name",
|
44
|
-
"_keep_alive",
|
45
|
-
)
|
46
|
-
|
47
39
|
stored_calls: ClassVar[ForeignKeyContext] = ForeignKeyContext()
|
48
40
|
|
49
41
|
@overload
|
@@ -157,13 +149,3 @@ class ForeignKey[TLeft: Table, TRight: Table](Element, IQuery):
|
|
157
149
|
comparer.set_context(context)
|
158
150
|
comparer._dialect = dialect
|
159
151
|
return comparer
|
160
|
-
|
161
|
-
def __hash__(self):
|
162
|
-
return hash((
|
163
|
-
self._tleft,
|
164
|
-
self._tright,
|
165
|
-
self._clause_name,
|
166
|
-
))
|
167
|
-
|
168
|
-
def __eq__(self, other: ForeignKey):
|
169
|
-
return hash(other) == hash(self)
|
ormlambda/sql/sqltypes.py
CHANGED
@@ -10,6 +10,7 @@ import ormlambda.util as util
|
|
10
10
|
from uuid import UUID as _python_UUID
|
11
11
|
from shapely import Point as _python_Point
|
12
12
|
|
13
|
+
|
13
14
|
class _NoArg(enum.Enum):
|
14
15
|
NO_ARG = 0
|
15
16
|
|
@@ -391,7 +392,7 @@ class Enum(String, TypeEngine[EnumType]):
|
|
391
392
|
enum_args = get_args(pt)
|
392
393
|
bad_args = [arg for arg in enum_args if not isinstance(arg, str)]
|
393
394
|
if bad_args:
|
394
|
-
raise ValueError(f"Can't create string-based Enum datatype from non-string
|
395
|
+
raise ValueError(f"Can't create string-based Enum datatype from non-string values: {', '.join(repr(x) for x in bad_args)}. Please provide an explicit Enum datatype for this Python type")
|
395
396
|
native_enum = False
|
396
397
|
return enum_args, native_enum
|
397
398
|
|
@@ -407,7 +408,7 @@ class Enum(String, TypeEngine[EnumType]):
|
|
407
408
|
elif util.is_pep695(python_type):
|
408
409
|
value = python_type.__value__
|
409
410
|
if not util.is_literal(value):
|
410
|
-
raise ValueError(f"Can't associate TypeAliasType '{python_type}' to an
|
411
|
+
raise ValueError(f"Can't associate TypeAliasType '{python_type}' to an Enum since it's not a direct alias of a Literal. Only aliases in this form `type my_alias = Literal['a', 'b']` are supported when generating Enums.")
|
411
412
|
enum_args, native_enum = process_literal(value)
|
412
413
|
|
413
414
|
elif isinstance(python_type, type) and issubclass(python_type, enum.Enum):
|
@@ -439,13 +440,15 @@ class Enum(String, TypeEngine[EnumType]):
|
|
439
440
|
|
440
441
|
self._valid_lookup.update([(value, self._valid_lookup[self._object_lookup[value]]) for value in values])
|
441
442
|
|
443
|
+
|
442
444
|
class Point(TypeEngine[_python_Point]):
|
443
|
-
__visit_name__ =
|
445
|
+
__visit_name__ = "point"
|
444
446
|
|
445
447
|
@property
|
446
|
-
def python_type(self)->Type[_python_Point]:
|
448
|
+
def python_type(self) -> Type[_python_Point]:
|
447
449
|
return _python_Point
|
448
|
-
|
450
|
+
|
451
|
+
|
449
452
|
class JSON(TypeEngine[Any]):
|
450
453
|
"""JSON data type."""
|
451
454
|
|
@@ -597,6 +600,7 @@ class VARBINARY(Varbinary):
|
|
597
600
|
class ENUM(Enum):
|
598
601
|
__visit_name__ = "ENUM"
|
599
602
|
|
603
|
+
|
600
604
|
class POINT(Point):
|
601
605
|
__visit_name__ = "POINT"
|
602
606
|
|
@@ -623,13 +627,15 @@ _type_dicc: dict[Any, TypeEngine[Any]] = {
|
|
623
627
|
float: Float(),
|
624
628
|
NoneType: NULLTYPE,
|
625
629
|
dt.datetime: Datetime(timezone=False),
|
630
|
+
dt.date: DATE(),
|
626
631
|
bytes: _BINARY,
|
627
632
|
bytearray: _BINARY,
|
628
633
|
bool: Boolean(),
|
629
634
|
enum.Enum: _ENUM,
|
630
635
|
Literal: _ENUM,
|
631
636
|
_python_UUID: UUID(),
|
632
|
-
_python_Point: POINT()
|
637
|
+
_python_Point: POINT(),
|
638
|
+
decimal.Decimal: DECIMAL(),
|
633
639
|
}
|
634
640
|
# enderegion
|
635
641
|
|
ormlambda/sql/table/table.py
CHANGED
@@ -5,9 +5,10 @@ import json
|
|
5
5
|
from ormlambda.sql import Column
|
6
6
|
from ormlambda.sql import ForeignKey
|
7
7
|
from ormlambda.util.module_tree.dfs_traversal import DFSTraversal
|
8
|
-
from ormlambda.sql.ddl import CreateTable
|
8
|
+
from ormlambda.sql.ddl import CreateTable
|
9
9
|
|
10
10
|
if TYPE_CHECKING:
|
11
|
+
from ormlambda.statements import BaseStatement
|
11
12
|
from ormlambda.dialects import Dialect
|
12
13
|
|
13
14
|
from .table_constructor import __init_constructor__
|
@@ -114,14 +115,17 @@ class Table(metaclass=TableMeta):
|
|
114
115
|
if name == key:
|
115
116
|
return value
|
116
117
|
|
118
|
+
@classmethod
|
119
|
+
def create_table_query(cls, statement: BaseStatement) -> str:
|
120
|
+
"""It's classmethod because of it does not matter the columns values to create the table"""
|
121
|
+
from ormlambda.sql.schema_generator import SchemaGeneratorFactory
|
122
|
+
|
123
|
+
return SchemaGeneratorFactory.get_generator(statement._dialect).create_table(cls)
|
124
|
+
|
117
125
|
@classmethod
|
118
126
|
def create_table(cls, dialect: Dialect) -> str:
|
119
127
|
return CreateTable(cls).compile(dialect).string
|
120
128
|
|
121
|
-
@classmethod
|
122
|
-
def drop_table(cls, dialect:Dialect)->str:
|
123
|
-
return DropTable(cls).compile(dialect).string
|
124
|
-
|
125
129
|
@classmethod
|
126
130
|
def find_dependent_tables(cls) -> tuple["Table", ...]:
|
127
131
|
"""Work in progress"""
|
ormlambda/sql/type_api.py
CHANGED
ormlambda/sql/visitors.py
CHANGED
@@ -37,9 +37,6 @@ class IStatements[T: Table](Element):
|
|
37
37
|
@abstractmethod
|
38
38
|
def create_table(self, if_exists: TypeExists = "fail") -> None: ...
|
39
39
|
|
40
|
-
@abstractmethod
|
41
|
-
def drop_table(self) -> None: ...
|
42
|
-
|
43
40
|
# #TODOL [ ]: We must to implement this mehtod
|
44
41
|
# @abstractmethod
|
45
42
|
# def drop_table(self)->None: ...
|