DefPixel-PostgreSQL 0.1.6__91-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.
- DefPixel/DataAccessObject/Implements/PostgreSQL/Adapters/Array.py +16 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Adapters/JavaScriptObjectNotation.py +74 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Adapters/__init__.py +51 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Column.py +34 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Compiler.py +161 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Configuration.py +116 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Connection.py +138 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Constraint.py +18 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/Check.py +23 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/Columns.py +24 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/ForeignKey.py +56 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/PrimaryKey.py +21 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/Unique.py +25 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Constraints/__init__.py +13 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Context.py +48 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Cursor.py +123 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Create.py +181 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Delete.py +31 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Drop.py +53 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Get.py +24 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Insert.py +98 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Query.py +37 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Select.py +122 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/Update.py +57 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Executors/__init__.py +19 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Expr.py +11 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/Common.py +180 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/Compare.py +87 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/Condition.py +155 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/JavaScriptObjectNotation.py +53 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/Operator.py +42 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/String.py +48 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Exprs/__init__.py +75 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Function.py +11 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Functions/Common.py +142 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Functions/DateTime.py +219 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Functions/JavaScriptObjectNotation.py +100 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Functions/Sequence.py +55 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Functions/__init__.py +91 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Index.py +135 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/FullOuter.py +15 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/Inner.py +15 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/Join.py +41 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/LeftOuter.py +15 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/RightOuter.py +15 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Joins/__init__.py +15 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Mapping.py +359 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Orders.py +59 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Pool.py +84 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Schema.py +28 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Sequence.py +55 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Session.py +84 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Type.py +113 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/Array.py +61 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/Binary.py +34 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/Bool.py +29 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/DateTime.py +120 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/JavaScriptObjectNotation.py +52 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/Numeric.py +151 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/Size.py +13 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/String.py +80 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/Types/__init__.py +76 -0
- DefPixel/DataAccessObject/Implements/PostgreSQL/__init__.py +85 -0
- defpixel_postgresql-0.1.6.dist-info/METADATA +184 -0
- defpixel_postgresql-0.1.6.dist-info/RECORD +68 -0
- defpixel_postgresql-0.1.6.dist-info/WHEEL +6 -0
- defpixel_postgresql-0.1.6.dist-info/licenses/LICENSE +674 -0
- defpixel_postgresql-0.1.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from typing import Sequence
|
|
4
|
+
|
|
5
|
+
from psycopg.types.array import ListDumper
|
|
6
|
+
|
|
7
|
+
__all__ = (
|
|
8
|
+
'ArrayDumper',
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ArrayDumper(ListDumper):
|
|
13
|
+
"""Dump tuples as PostgreSQL arrays in the same way as lists."""
|
|
14
|
+
|
|
15
|
+
def dump(self, o: Sequence):
|
|
16
|
+
return super().dump(list(o))
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from collections.abc import (
|
|
4
|
+
Mapping,
|
|
5
|
+
Set,
|
|
6
|
+
)
|
|
7
|
+
from dataclasses import (
|
|
8
|
+
fields,
|
|
9
|
+
is_dataclass,
|
|
10
|
+
)
|
|
11
|
+
from datetime import (
|
|
12
|
+
date,
|
|
13
|
+
datetime,
|
|
14
|
+
time,
|
|
15
|
+
timedelta,
|
|
16
|
+
)
|
|
17
|
+
from decimal import Decimal
|
|
18
|
+
from json import dumps
|
|
19
|
+
from typing import Any
|
|
20
|
+
from uuid import UUID
|
|
21
|
+
|
|
22
|
+
from psycopg.adapt import Dumper
|
|
23
|
+
|
|
24
|
+
__all__ = (
|
|
25
|
+
'encode',
|
|
26
|
+
'JavaScriptObjectNotationDumper',
|
|
27
|
+
'DataclassDumper',
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def encode(o: Any) -> Any:
|
|
32
|
+
"""Convert values that the standard JSON encoder cannot serialize."""
|
|
33
|
+
if is_dataclass(o) and not isinstance(o, type):
|
|
34
|
+
return {member.name: getattr(o, member.name) for member in fields(o)}
|
|
35
|
+
if isinstance(o, (datetime, date, time)):
|
|
36
|
+
return o.isoformat()
|
|
37
|
+
if isinstance(o, timedelta):
|
|
38
|
+
return o.total_seconds()
|
|
39
|
+
if isinstance(o, (Decimal, UUID)):
|
|
40
|
+
return str(o)
|
|
41
|
+
if isinstance(o, Set):
|
|
42
|
+
return list(o)
|
|
43
|
+
if isinstance(o, Mapping):
|
|
44
|
+
return dict(o)
|
|
45
|
+
if isinstance(o, tuple):
|
|
46
|
+
return list(o)
|
|
47
|
+
if isinstance(o, (bytes, bytearray, memoryview)):
|
|
48
|
+
raise TypeError('Binary values cannot be encoded as JSON')
|
|
49
|
+
raise TypeError('{} cannot be encoded as JSON'.format(o.__class__.__name__))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def serialize(o: Any) -> bytes:
|
|
53
|
+
return dumps(
|
|
54
|
+
o,
|
|
55
|
+
default=encode,
|
|
56
|
+
ensure_ascii=False,
|
|
57
|
+
allow_nan=False,
|
|
58
|
+
).encode('utf-8')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class JavaScriptObjectNotationDumper(Dumper):
|
|
62
|
+
"""Dump mappings as JSON text and let PostgreSQL infer json or jsonb from the context."""
|
|
63
|
+
|
|
64
|
+
def dump(self, o: Any) -> bytes:
|
|
65
|
+
return serialize(o)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class DataclassDumper(Dumper):
|
|
69
|
+
"""Dump dataclass instances as JSON objects; reject other unadaptable objects."""
|
|
70
|
+
|
|
71
|
+
def dump(self, o: Any) -> bytes:
|
|
72
|
+
if not is_dataclass(o) or isinstance(o, type):
|
|
73
|
+
raise TypeError('cannot adapt type {!r}'.format(o.__class__.__name__))
|
|
74
|
+
return serialize(o)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from typing import (
|
|
4
|
+
Mapping as TypingMapping,
|
|
5
|
+
Optional,
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
from psycopg.adapt import (
|
|
9
|
+
AdaptersMap,
|
|
10
|
+
Dumper,
|
|
11
|
+
Loader,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .Array import ArrayDumper
|
|
15
|
+
from .JavaScriptObjectNotation import (
|
|
16
|
+
DataclassDumper,
|
|
17
|
+
JavaScriptObjectNotationDumper,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = (
|
|
21
|
+
'ArrayDumper',
|
|
22
|
+
'DataclassDumper',
|
|
23
|
+
'JavaScriptObjectNotationDumper',
|
|
24
|
+
'DUMPERS',
|
|
25
|
+
'register',
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
DUMPERS = {
|
|
29
|
+
dict: JavaScriptObjectNotationDumper,
|
|
30
|
+
tuple: ArrayDumper,
|
|
31
|
+
object: DataclassDumper,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def register(
|
|
36
|
+
adapters: AdaptersMap,
|
|
37
|
+
dumpers: Optional[TypingMapping] = None,
|
|
38
|
+
loaders: Optional[TypingMapping] = None,
|
|
39
|
+
) -> AdaptersMap:
|
|
40
|
+
"""Register DefPixel defaults first, then user dumpers and loaders that may override them."""
|
|
41
|
+
for cls, dumper in DUMPERS.items():
|
|
42
|
+
adapters.register_dumper(cls, dumper)
|
|
43
|
+
for cls, dumper in (dumpers or {}).items():
|
|
44
|
+
if not isinstance(dumper, type) or not issubclass(dumper, Dumper):
|
|
45
|
+
raise TypeError('Dumpers must be psycopg Dumper classes')
|
|
46
|
+
adapters.register_dumper(cls, dumper)
|
|
47
|
+
for oid, loader in (loaders or {}).items():
|
|
48
|
+
if not isinstance(loader, type) or not issubclass(loader, Loader):
|
|
49
|
+
raise TypeError('Loaders must be psycopg Loader classes')
|
|
50
|
+
adapters.register_loader(oid, loader)
|
|
51
|
+
return adapters
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from .Compiler import (
|
|
6
|
+
Compilable,
|
|
7
|
+
Compiler,
|
|
8
|
+
identifier,
|
|
9
|
+
typename,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = (
|
|
13
|
+
'Column',
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Column(Compilable):
|
|
18
|
+
"""Unqualified column reference with an optional type cast."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, name: str):
|
|
21
|
+
identifier(name)
|
|
22
|
+
self.name = name
|
|
23
|
+
self.typecast: Optional[str] = None
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
def cast(self, type) -> 'Column':
|
|
27
|
+
self.typecast = typename(type)
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
def compile(self, compiler: Compiler) -> str:
|
|
31
|
+
return '{}{}'.format(
|
|
32
|
+
identifier(self.name),
|
|
33
|
+
'::{}'.format(self.typecast) if self.typecast else '',
|
|
34
|
+
)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from abc import (
|
|
4
|
+
ABC,
|
|
5
|
+
abstractmethod,
|
|
6
|
+
)
|
|
7
|
+
import re
|
|
8
|
+
from typing import (
|
|
9
|
+
Any,
|
|
10
|
+
Dict,
|
|
11
|
+
Optional,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from psycopg import (
|
|
15
|
+
adapters as default_adapters,
|
|
16
|
+
sql,
|
|
17
|
+
)
|
|
18
|
+
from psycopg.adapt import AdaptersMap
|
|
19
|
+
|
|
20
|
+
from .Adapters import register
|
|
21
|
+
|
|
22
|
+
__all__ = (
|
|
23
|
+
'Compilable',
|
|
24
|
+
'Compiler',
|
|
25
|
+
'identifier',
|
|
26
|
+
'qualified',
|
|
27
|
+
'quote',
|
|
28
|
+
'typename',
|
|
29
|
+
'valuetype',
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def quote(name: str) -> str:
|
|
34
|
+
"""Quote an identifier for use inside a bound text value such as a regclass name."""
|
|
35
|
+
if not isinstance(name, str) or not name or '\x00' in name:
|
|
36
|
+
raise ValueError('SQL identifiers must be nonempty strings without null characters')
|
|
37
|
+
return '"{}"'.format(name.replace('"', '""'))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def identifier(name: str) -> str:
|
|
41
|
+
"""Quote an identifier and escape percent signs for parameterized execution."""
|
|
42
|
+
return quote(name).replace('%', '%%')
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def qualified(
|
|
46
|
+
name: str,
|
|
47
|
+
schema: Optional[str] = None,
|
|
48
|
+
) -> str:
|
|
49
|
+
if schema is None:
|
|
50
|
+
return identifier(name)
|
|
51
|
+
return '{}.{}'.format(identifier(schema), identifier(name))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def typename(
|
|
55
|
+
value,
|
|
56
|
+
escape: bool = True,
|
|
57
|
+
) -> str:
|
|
58
|
+
"""Resolve a SQL type declaration from a string, Type class, or Type instance."""
|
|
59
|
+
from .Type import Type
|
|
60
|
+
if isinstance(value, str):
|
|
61
|
+
if not value.strip() or '\x00' in value or ';' in value or '--' in value or '/*' in value:
|
|
62
|
+
raise ValueError('SQL type names must be nonempty strings without null characters, semicolons, or comments')
|
|
63
|
+
text = value
|
|
64
|
+
elif isinstance(value, Type) or (isinstance(value, type) and issubclass(value, Type)):
|
|
65
|
+
text = value.typestr
|
|
66
|
+
if not text:
|
|
67
|
+
raise ValueError('{} has no SQL type'.format(value.__name__ if isinstance(value, type) else type(value).__name__))
|
|
68
|
+
else:
|
|
69
|
+
raise TypeError('A SQL type must be a string or a PostgreSQL Type')
|
|
70
|
+
return text.replace('%', '%%') if escape else text
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
UNBOUNDED = {
|
|
74
|
+
'CHAR': 'BPCHAR',
|
|
75
|
+
'CHARACTER': 'BPCHAR',
|
|
76
|
+
'BIT': 'VARBIT',
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def valuetype(value) -> str:
|
|
81
|
+
"""Resolve a cast for bound column values without length or precision modifiers.
|
|
82
|
+
|
|
83
|
+
An explicit cast such as ::VARCHAR(10) silently truncates, so values are cast to the
|
|
84
|
+
unbounded type and PostgreSQL applies its assignment and comparison rules instead.
|
|
85
|
+
"""
|
|
86
|
+
text = re.sub(r'\s*\(\s*\d+\s*(?:,\s*-?\d+\s*)?\)', '', typename(value, escape=False))
|
|
87
|
+
text = re.sub(
|
|
88
|
+
r'^(CHARACTER|CHAR|BIT)(?=$|\[)',
|
|
89
|
+
lambda match: UNBOUNDED[match.group(1).upper()],
|
|
90
|
+
text,
|
|
91
|
+
flags=re.IGNORECASE,
|
|
92
|
+
)
|
|
93
|
+
return text
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class LiteralContext:
|
|
97
|
+
"""Adaptation context used to render DDL literals with DefPixel dumpers."""
|
|
98
|
+
|
|
99
|
+
def __init__(self):
|
|
100
|
+
self.adapters = AdaptersMap(default_adapters)
|
|
101
|
+
self.connection = None
|
|
102
|
+
register(self.adapters)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Compilable(ABC):
|
|
107
|
+
"""Object that renders itself into SQL through a Compiler."""
|
|
108
|
+
|
|
109
|
+
@abstractmethod
|
|
110
|
+
def compile(self, compiler: 'Compiler') -> str:
|
|
111
|
+
raise NotImplementedError('{} must be implemented compile'.format(self.__class__.__name__))
|
|
112
|
+
|
|
113
|
+
def __str__(self) -> str:
|
|
114
|
+
return self.compile(Compiler())
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Compiler:
|
|
118
|
+
"""Collect bound parameters, or render literals for DDL that cannot take parameters.
|
|
119
|
+
|
|
120
|
+
qualify=False renders mapped columns without their table name, as required inside CREATE TABLE and CREATE INDEX.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
context = None
|
|
124
|
+
|
|
125
|
+
def __init__(
|
|
126
|
+
self,
|
|
127
|
+
literal: bool = False,
|
|
128
|
+
qualify: bool = True,
|
|
129
|
+
):
|
|
130
|
+
self.literal = literal
|
|
131
|
+
self.qualify = qualify
|
|
132
|
+
self.args: Dict[str, Any] = {}
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
def bind(
|
|
136
|
+
self,
|
|
137
|
+
value: Any,
|
|
138
|
+
cast=None,
|
|
139
|
+
) -> str:
|
|
140
|
+
if isinstance(value, Compilable):
|
|
141
|
+
raise TypeError('Compilable objects must be compiled, not bound')
|
|
142
|
+
if self.literal:
|
|
143
|
+
if Compiler.context is None:
|
|
144
|
+
Compiler.context = LiteralContext()
|
|
145
|
+
text = sql.Literal(value).as_string(Compiler.context).replace('%', '%%')
|
|
146
|
+
else:
|
|
147
|
+
name = 'p{}'.format(len(self.args))
|
|
148
|
+
self.args[name] = value
|
|
149
|
+
text = '%({})s'.format(name)
|
|
150
|
+
if cast is not None:
|
|
151
|
+
text = '{}::{}'.format(text, typename(cast))
|
|
152
|
+
return text
|
|
153
|
+
|
|
154
|
+
def compile(
|
|
155
|
+
self,
|
|
156
|
+
value: Any,
|
|
157
|
+
cast=None,
|
|
158
|
+
) -> str:
|
|
159
|
+
if isinstance(value, Compilable):
|
|
160
|
+
return value.compile(self)
|
|
161
|
+
return self.bind(value, cast)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
Dict,
|
|
7
|
+
Mapping as TypingMapping,
|
|
8
|
+
Optional,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
from DefPixel.DataAccessObject import Configuration as BaseConfiguration
|
|
12
|
+
from psycopg.adapt import (
|
|
13
|
+
Dumper,
|
|
14
|
+
Loader,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__all__ = (
|
|
18
|
+
'Configuration',
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def text(
|
|
23
|
+
name: str,
|
|
24
|
+
value: Any,
|
|
25
|
+
required: bool,
|
|
26
|
+
) -> Optional[str]:
|
|
27
|
+
if value is None and not required:
|
|
28
|
+
return None
|
|
29
|
+
if not isinstance(value, str) or not value or '\x00' in value:
|
|
30
|
+
raise ValueError('{} must be a nonempty string without null characters'.format(name))
|
|
31
|
+
return value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def integer(name: str, value: Any) -> int:
|
|
35
|
+
if type(value) is not int:
|
|
36
|
+
raise TypeError('{} must be an integer'.format(name))
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def flag(name: str, value: Any) -> bool:
|
|
41
|
+
if type(value) is not bool:
|
|
42
|
+
raise TypeError('{} must be a bool'.format(name))
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class Configuration(BaseConfiguration):
|
|
47
|
+
"""Configuration for PostgreSQL connections and optional connection pools."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
host: str,
|
|
52
|
+
port: int = 5432,
|
|
53
|
+
database: Optional[str] = None,
|
|
54
|
+
username: Optional[str] = None,
|
|
55
|
+
password: Optional[str] = None,
|
|
56
|
+
autocommit: bool = False,
|
|
57
|
+
pool: bool = False,
|
|
58
|
+
min: int = 0,
|
|
59
|
+
max: int = 0,
|
|
60
|
+
dumpers: Optional[TypingMapping] = None,
|
|
61
|
+
loaders: Optional[TypingMapping] = None,
|
|
62
|
+
timezone: Optional[str] = None,
|
|
63
|
+
):
|
|
64
|
+
self.host = text('host', host, True)
|
|
65
|
+
self.database = text('database', database, True)
|
|
66
|
+
self.port = integer('port', port)
|
|
67
|
+
if not 1 <= self.port <= 65535:
|
|
68
|
+
raise ValueError('port must be between 1 and 65535')
|
|
69
|
+
self.username = text('username', username, False)
|
|
70
|
+
self.password = text('password', password, False)
|
|
71
|
+
self.autocommit = flag('autocommit', autocommit)
|
|
72
|
+
self.pool = flag('pool', pool)
|
|
73
|
+
self.min = integer('min', min)
|
|
74
|
+
self.max = integer('max', max)
|
|
75
|
+
if self.pool and (self.max < 1 or not 0 <= self.min <= self.max):
|
|
76
|
+
raise ValueError('A connection pool requires 0 <= min <= max and max >= 1')
|
|
77
|
+
for name, value, base in (('dumpers', dumpers, Dumper), ('loaders', loaders, Loader)):
|
|
78
|
+
if value is None:
|
|
79
|
+
continue
|
|
80
|
+
if not isinstance(value, Mapping):
|
|
81
|
+
raise TypeError('{} must be a mapping'.format(name))
|
|
82
|
+
for adapter in value.values():
|
|
83
|
+
if not isinstance(adapter, type) or not issubclass(adapter, base):
|
|
84
|
+
raise TypeError('{} must map to psycopg {} classes'.format(name, base.__name__))
|
|
85
|
+
self.dumpers = dict(dumpers or {})
|
|
86
|
+
self.loaders = dict(loaders or {})
|
|
87
|
+
self.timezone = text('timezone', timezone, False)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
def __repr__(self) -> str:
|
|
91
|
+
return '{}(host={!r}, port={!r}, database={!r}, username={!r}, autocommit={!r}, pool={!r})'.format(
|
|
92
|
+
self.__class__.__name__,
|
|
93
|
+
self.host,
|
|
94
|
+
self.port,
|
|
95
|
+
self.database,
|
|
96
|
+
self.username,
|
|
97
|
+
self.autocommit,
|
|
98
|
+
self.pool,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def parameters(self) -> Dict[str, Any]:
|
|
102
|
+
"""Return driver keyword arguments; secrets never pass through a connection string."""
|
|
103
|
+
kwargs = {
|
|
104
|
+
'host': self.host,
|
|
105
|
+
'port': self.port,
|
|
106
|
+
'dbname': self.database,
|
|
107
|
+
}
|
|
108
|
+
if self.username is not None:
|
|
109
|
+
kwargs['user'] = self.username
|
|
110
|
+
if self.password is not None:
|
|
111
|
+
kwargs['password'] = self.password
|
|
112
|
+
if self.timezone is not None:
|
|
113
|
+
kwargs['options'] = '-c TimeZone={}'.format(
|
|
114
|
+
self.timezone.replace('\\', '\\\\').replace(' ', '\\ '),
|
|
115
|
+
)
|
|
116
|
+
return kwargs
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from itertools import count
|
|
4
|
+
|
|
5
|
+
from psycopg import connect
|
|
6
|
+
from psycopg.pq import TransactionStatus
|
|
7
|
+
from psycopg.rows import dict_row
|
|
8
|
+
|
|
9
|
+
from DefPixel.DataAccessObject import Connection as BaseConnection
|
|
10
|
+
from DefPixel.DataAccessObject.Properties.Database import (
|
|
11
|
+
Database,
|
|
12
|
+
Run,
|
|
13
|
+
)
|
|
14
|
+
from .Adapters import register
|
|
15
|
+
from .Configuration import Configuration
|
|
16
|
+
from .Context import Context
|
|
17
|
+
from .Cursor import Cursor
|
|
18
|
+
from .Pool import Pool
|
|
19
|
+
from .Session import Session
|
|
20
|
+
|
|
21
|
+
__all__ = (
|
|
22
|
+
'Connection',
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Connection(BaseConnection, Database, Run):
|
|
27
|
+
"""PostgreSQL connection with explicit lifecycle and transaction management."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, conf: Configuration):
|
|
30
|
+
if not isinstance(conf, Configuration):
|
|
31
|
+
raise TypeError('Connection requires a PostgreSQL Configuration')
|
|
32
|
+
self.conf = conf
|
|
33
|
+
self.connection = None
|
|
34
|
+
self.savepoints = count()
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
def connect(self):
|
|
38
|
+
if self.connection is not None:
|
|
39
|
+
raise RuntimeError('The PostgreSQL connection is already open')
|
|
40
|
+
if self.conf.pool:
|
|
41
|
+
self.connection = Pool.get(self.conf)
|
|
42
|
+
return
|
|
43
|
+
connection = connect(
|
|
44
|
+
autocommit=self.conf.autocommit,
|
|
45
|
+
row_factory=dict_row,
|
|
46
|
+
**self.conf.parameters(),
|
|
47
|
+
)
|
|
48
|
+
try:
|
|
49
|
+
register(connection.adapters, self.conf.dumpers, self.conf.loaders)
|
|
50
|
+
except BaseException:
|
|
51
|
+
connection.close()
|
|
52
|
+
raise
|
|
53
|
+
self.connection = connection
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
def active(self):
|
|
57
|
+
if self.connection is None or self.connection.closed:
|
|
58
|
+
raise RuntimeError('The PostgreSQL connection is not open')
|
|
59
|
+
return self.connection
|
|
60
|
+
|
|
61
|
+
def close(self):
|
|
62
|
+
"""Close or release the connection; an open transaction is rolled back, never committed."""
|
|
63
|
+
if self.connection is None:
|
|
64
|
+
return
|
|
65
|
+
connection = self.connection
|
|
66
|
+
self.connection = None
|
|
67
|
+
try:
|
|
68
|
+
if not connection.closed and connection.info.transaction_status != TransactionStatus.IDLE:
|
|
69
|
+
connection.rollback()
|
|
70
|
+
if not connection.closed and connection.autocommit != self.conf.autocommit:
|
|
71
|
+
connection.autocommit = self.conf.autocommit
|
|
72
|
+
finally:
|
|
73
|
+
if self.conf.pool:
|
|
74
|
+
Pool.release(self.conf, connection)
|
|
75
|
+
else:
|
|
76
|
+
connection.close()
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
def autocommit(self, operation: str):
|
|
80
|
+
if self.conf.autocommit:
|
|
81
|
+
raise RuntimeError('{} is not available in autocommit mode; use session() for a transaction'.format(operation))
|
|
82
|
+
return self.active()
|
|
83
|
+
|
|
84
|
+
def begin(self):
|
|
85
|
+
connection = self.autocommit('begin')
|
|
86
|
+
if connection.info.transaction_status != TransactionStatus.IDLE:
|
|
87
|
+
raise RuntimeError('A transaction is already in progress')
|
|
88
|
+
# An empty statement makes psycopg open the transaction without a server warning.
|
|
89
|
+
connection.execute('')
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
def execute(
|
|
93
|
+
self,
|
|
94
|
+
sql: str,
|
|
95
|
+
*args,
|
|
96
|
+
**kwargs,
|
|
97
|
+
) -> Context:
|
|
98
|
+
return self.cursor().execute(sql, *args, **kwargs)
|
|
99
|
+
|
|
100
|
+
def executes(
|
|
101
|
+
self,
|
|
102
|
+
sql: str,
|
|
103
|
+
*args,
|
|
104
|
+
) -> Context:
|
|
105
|
+
return self.cursor().executes(sql, *args)
|
|
106
|
+
|
|
107
|
+
def run(
|
|
108
|
+
self,
|
|
109
|
+
executor,
|
|
110
|
+
filter=None,
|
|
111
|
+
fetch=None,
|
|
112
|
+
):
|
|
113
|
+
cursor = self.cursor()
|
|
114
|
+
try:
|
|
115
|
+
return cursor.run(
|
|
116
|
+
executor,
|
|
117
|
+
filter=filter,
|
|
118
|
+
fetch=fetch,
|
|
119
|
+
)
|
|
120
|
+
finally:
|
|
121
|
+
cursor.close()
|
|
122
|
+
|
|
123
|
+
def cursor(self) -> Cursor:
|
|
124
|
+
return Cursor(self.active().cursor())
|
|
125
|
+
|
|
126
|
+
def session(self) -> Session:
|
|
127
|
+
return Session(
|
|
128
|
+
self.active(),
|
|
129
|
+
lambda: 'defpixel_savepoint_{}'.format(next(self.savepoints)),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def commit(self):
|
|
133
|
+
self.autocommit('commit').commit()
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
def rollback(self):
|
|
137
|
+
self.autocommit('rollback').rollback()
|
|
138
|
+
return
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
|
|
5
|
+
from .Compiler import identifier
|
|
6
|
+
|
|
7
|
+
__all__ = (
|
|
8
|
+
'Constraint',
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Constraint(ABC):
|
|
13
|
+
"""Named table constraint."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, name: str):
|
|
16
|
+
identifier(name)
|
|
17
|
+
self.name = name
|
|
18
|
+
return
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from ..Compiler import Compilable
|
|
4
|
+
from ..Constraint import Constraint
|
|
5
|
+
|
|
6
|
+
__all__ = (
|
|
7
|
+
'Check',
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Check(Constraint):
|
|
12
|
+
"""Named check constraint; values in the expression are rendered as DDL literals."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
name: str,
|
|
17
|
+
expr,
|
|
18
|
+
):
|
|
19
|
+
super().__init__(name)
|
|
20
|
+
if not isinstance(expr, Compilable):
|
|
21
|
+
raise TypeError('Check requires a SQL expression')
|
|
22
|
+
self.expr = expr
|
|
23
|
+
return
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
from ..Column import Column
|
|
6
|
+
from ..Type import Type
|
|
7
|
+
|
|
8
|
+
__all__ = (
|
|
9
|
+
'columns',
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def columns(values, name: str = 'cols') -> list:
|
|
14
|
+
"""Normalize constraint columns given as names, Column objects, or column specifications."""
|
|
15
|
+
if isinstance(values, Sequence) and not isinstance(values, str):
|
|
16
|
+
values = list(values)
|
|
17
|
+
else:
|
|
18
|
+
values = [values]
|
|
19
|
+
if not values:
|
|
20
|
+
raise ValueError('{} requires at least one column'.format(name))
|
|
21
|
+
for value in values:
|
|
22
|
+
if not isinstance(value, (str, Column, Type)):
|
|
23
|
+
raise TypeError('{} must contain column names, Column objects, or column specifications'.format(name))
|
|
24
|
+
return values
|