plain.postgres 0.84.0__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.
- plain/postgres/CHANGELOG.md +1028 -0
- plain/postgres/README.md +925 -0
- plain/postgres/__init__.py +120 -0
- plain/postgres/agents/.claude/rules/plain-postgres.md +78 -0
- plain/postgres/aggregates.py +236 -0
- plain/postgres/backups/__init__.py +0 -0
- plain/postgres/backups/cli.py +148 -0
- plain/postgres/backups/clients.py +94 -0
- plain/postgres/backups/core.py +172 -0
- plain/postgres/base.py +1415 -0
- plain/postgres/cli/__init__.py +3 -0
- plain/postgres/cli/db.py +142 -0
- plain/postgres/cli/migrations.py +1085 -0
- plain/postgres/config.py +18 -0
- plain/postgres/connection.py +1331 -0
- plain/postgres/connections.py +77 -0
- plain/postgres/constants.py +13 -0
- plain/postgres/constraints.py +495 -0
- plain/postgres/database_url.py +94 -0
- plain/postgres/db.py +59 -0
- plain/postgres/default_settings.py +38 -0
- plain/postgres/deletion.py +475 -0
- plain/postgres/dialect.py +640 -0
- plain/postgres/entrypoints.py +4 -0
- plain/postgres/enums.py +103 -0
- plain/postgres/exceptions.py +217 -0
- plain/postgres/expressions.py +1912 -0
- plain/postgres/fields/__init__.py +2118 -0
- plain/postgres/fields/encrypted.py +354 -0
- plain/postgres/fields/json.py +413 -0
- plain/postgres/fields/mixins.py +30 -0
- plain/postgres/fields/related.py +1192 -0
- plain/postgres/fields/related_descriptors.py +290 -0
- plain/postgres/fields/related_lookups.py +223 -0
- plain/postgres/fields/related_managers.py +661 -0
- plain/postgres/fields/reverse_descriptors.py +229 -0
- plain/postgres/fields/reverse_related.py +328 -0
- plain/postgres/fields/timezones.py +143 -0
- plain/postgres/forms.py +773 -0
- plain/postgres/functions/__init__.py +189 -0
- plain/postgres/functions/comparison.py +127 -0
- plain/postgres/functions/datetime.py +454 -0
- plain/postgres/functions/math.py +140 -0
- plain/postgres/functions/mixins.py +59 -0
- plain/postgres/functions/text.py +282 -0
- plain/postgres/functions/window.py +125 -0
- plain/postgres/indexes.py +286 -0
- plain/postgres/lookups.py +758 -0
- plain/postgres/meta.py +584 -0
- plain/postgres/migrations/__init__.py +53 -0
- plain/postgres/migrations/autodetector.py +1379 -0
- plain/postgres/migrations/exceptions.py +54 -0
- plain/postgres/migrations/executor.py +188 -0
- plain/postgres/migrations/graph.py +364 -0
- plain/postgres/migrations/loader.py +377 -0
- plain/postgres/migrations/migration.py +180 -0
- plain/postgres/migrations/operations/__init__.py +34 -0
- plain/postgres/migrations/operations/base.py +139 -0
- plain/postgres/migrations/operations/fields.py +373 -0
- plain/postgres/migrations/operations/models.py +798 -0
- plain/postgres/migrations/operations/special.py +184 -0
- plain/postgres/migrations/optimizer.py +74 -0
- plain/postgres/migrations/questioner.py +340 -0
- plain/postgres/migrations/recorder.py +119 -0
- plain/postgres/migrations/serializer.py +378 -0
- plain/postgres/migrations/state.py +882 -0
- plain/postgres/migrations/utils.py +147 -0
- plain/postgres/migrations/writer.py +302 -0
- plain/postgres/options.py +207 -0
- plain/postgres/otel.py +231 -0
- plain/postgres/preflight.py +336 -0
- plain/postgres/query.py +2242 -0
- plain/postgres/query_utils.py +456 -0
- plain/postgres/registry.py +217 -0
- plain/postgres/schema.py +1885 -0
- plain/postgres/sql/__init__.py +40 -0
- plain/postgres/sql/compiler.py +1869 -0
- plain/postgres/sql/constants.py +22 -0
- plain/postgres/sql/datastructures.py +222 -0
- plain/postgres/sql/query.py +2947 -0
- plain/postgres/sql/where.py +374 -0
- plain/postgres/test/__init__.py +0 -0
- plain/postgres/test/pytest.py +117 -0
- plain/postgres/test/utils.py +18 -0
- plain/postgres/transaction.py +222 -0
- plain/postgres/types.py +92 -0
- plain/postgres/types.pyi +751 -0
- plain/postgres/utils.py +345 -0
- plain_postgres-0.84.0.dist-info/METADATA +937 -0
- plain_postgres-0.84.0.dist-info/RECORD +93 -0
- plain_postgres-0.84.0.dist-info/WHEEL +4 -0
- plain_postgres-0.84.0.dist-info/entry_points.txt +5 -0
- plain_postgres-0.84.0.dist-info/licenses/LICENSE +61 -0
plain/postgres/enums.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import enum
|
|
4
|
+
from types import DynamicClassAttribute
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from plain.utils.functional import Promise
|
|
8
|
+
|
|
9
|
+
__all__ = ["Choices", "IntegerChoices", "TextChoices"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ChoicesMeta(enum.EnumMeta):
|
|
13
|
+
"""A metaclass for creating a enum choices."""
|
|
14
|
+
|
|
15
|
+
def __new__(
|
|
16
|
+
metacls: type,
|
|
17
|
+
classname: str,
|
|
18
|
+
bases: tuple[type, ...],
|
|
19
|
+
classdict: Any,
|
|
20
|
+
**kwds: Any,
|
|
21
|
+
) -> type:
|
|
22
|
+
labels = []
|
|
23
|
+
for key in classdict._member_names:
|
|
24
|
+
value = classdict[key]
|
|
25
|
+
if (
|
|
26
|
+
isinstance(value, list | tuple)
|
|
27
|
+
and len(value) > 1
|
|
28
|
+
and isinstance(value[-1], Promise | str)
|
|
29
|
+
):
|
|
30
|
+
*value, label = value
|
|
31
|
+
value = tuple(value)
|
|
32
|
+
else:
|
|
33
|
+
label = key.replace("_", " ").title()
|
|
34
|
+
labels.append(label)
|
|
35
|
+
# Use dict.__setitem__() to suppress defenses against double
|
|
36
|
+
# assignment in enum's classdict.
|
|
37
|
+
dict.__setitem__(classdict, key, value)
|
|
38
|
+
cls = super().__new__(metacls, classname, bases, classdict, **kwds) # type: ignore[misc]
|
|
39
|
+
for member, label in zip(cls.__members__.values(), labels):
|
|
40
|
+
member._label_ = label
|
|
41
|
+
return enum.unique(cls)
|
|
42
|
+
|
|
43
|
+
def __contains__(cls, member: object) -> bool: # type: ignore[override]
|
|
44
|
+
if not isinstance(member, enum.Enum):
|
|
45
|
+
# Allow non-enums to match against member values.
|
|
46
|
+
return any(x.value == member for x in cls) # type: ignore[attr-defined]
|
|
47
|
+
return super().__contains__(member)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def names(cls) -> list[str]:
|
|
51
|
+
empty = ["__empty__"] if hasattr(cls, "__empty__") else []
|
|
52
|
+
return empty + [member.name for member in cls] # type: ignore[attr-defined]
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def choices(cls) -> list[tuple[Any, str]]:
|
|
56
|
+
empty = [(None, cls.__empty__)] if hasattr(cls, "__empty__") else []
|
|
57
|
+
return empty + [(member.value, member.label) for member in cls] # type: ignore[attr-defined]
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def labels(cls) -> list[str]:
|
|
61
|
+
return [label for _, label in cls.choices]
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def values(cls) -> list[Any]:
|
|
65
|
+
return [value for value, _ in cls.choices]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class Choices(enum.Enum, metaclass=ChoicesMeta):
|
|
69
|
+
"""Class for creating enumerated choices."""
|
|
70
|
+
|
|
71
|
+
# Dynamically set by metaclass
|
|
72
|
+
_label_: str
|
|
73
|
+
|
|
74
|
+
@DynamicClassAttribute
|
|
75
|
+
def label(self) -> str:
|
|
76
|
+
return self._label_
|
|
77
|
+
|
|
78
|
+
def __str__(self) -> str:
|
|
79
|
+
"""
|
|
80
|
+
Use value when cast to str, so that Choices set as model instance
|
|
81
|
+
attributes are rendered as expected in templates and similar contexts.
|
|
82
|
+
"""
|
|
83
|
+
return str(self.value)
|
|
84
|
+
|
|
85
|
+
# A similar format was proposed for Python 3.10.
|
|
86
|
+
def __repr__(self) -> str:
|
|
87
|
+
return f"{self.__class__.__qualname__}.{self._name_}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class IntegerChoices(int, Choices):
|
|
91
|
+
"""Class for creating enumerated integer choices."""
|
|
92
|
+
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TextChoices(str, Choices):
|
|
97
|
+
"""Class for creating enumerated string choices."""
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def _generate_next_value_(
|
|
101
|
+
name: str, start: int, count: int, last_values: list[str]
|
|
102
|
+
) -> str:
|
|
103
|
+
return name
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import TYPE_CHECKING, Any, TypeVar, cast
|
|
5
|
+
|
|
6
|
+
import psycopg
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from plain.postgres.connection import DatabaseConnection
|
|
10
|
+
|
|
11
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
12
|
+
|
|
13
|
+
# MARK: Database Query Exceptions
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EmptyResultSet(Exception):
|
|
17
|
+
"""A database query predicate is impossible."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FullResultSet(Exception):
|
|
23
|
+
"""A database query predicate is matches everything."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# MARK: Model and Field Errors
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class FieldDoesNotExist(Exception):
|
|
32
|
+
"""The requested model field does not exist"""
|
|
33
|
+
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class FieldError(Exception):
|
|
38
|
+
"""Some kind of problem with a model field."""
|
|
39
|
+
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ObjectDoesNotExist(Exception):
|
|
44
|
+
"""The requested object does not exist"""
|
|
45
|
+
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class MultipleObjectsReturned(Exception):
|
|
50
|
+
"""The query returned multiple objects when only one was expected."""
|
|
51
|
+
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# MARK: Model Exception Descriptors
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DoesNotExistDescriptor:
|
|
59
|
+
"""Descriptor that creates a unique DoesNotExist exception class per model."""
|
|
60
|
+
|
|
61
|
+
def __init__(self) -> None:
|
|
62
|
+
self._exceptions_by_class: dict[type, type[ObjectDoesNotExist]] = {}
|
|
63
|
+
|
|
64
|
+
def __get__(self, instance: Any, owner: type | None) -> type[ObjectDoesNotExist]:
|
|
65
|
+
if owner is None:
|
|
66
|
+
return ObjectDoesNotExist # Return base class as fallback
|
|
67
|
+
|
|
68
|
+
# Create a unique exception class for this model if we haven't already
|
|
69
|
+
if owner not in self._exceptions_by_class:
|
|
70
|
+
# type() returns a subclass of ObjectDoesNotExist
|
|
71
|
+
exc_class: type[ObjectDoesNotExist] = cast(
|
|
72
|
+
type[ObjectDoesNotExist],
|
|
73
|
+
type(
|
|
74
|
+
"DoesNotExist",
|
|
75
|
+
(ObjectDoesNotExist,),
|
|
76
|
+
{
|
|
77
|
+
"__module__": owner.__module__,
|
|
78
|
+
"__qualname__": f"{owner.__qualname__}.DoesNotExist",
|
|
79
|
+
},
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
self._exceptions_by_class[owner] = exc_class
|
|
83
|
+
|
|
84
|
+
return self._exceptions_by_class[owner]
|
|
85
|
+
|
|
86
|
+
def __set__(self, instance: Any, value: Any) -> None:
|
|
87
|
+
raise AttributeError("Cannot set DoesNotExist")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class MultipleObjectsReturnedDescriptor:
|
|
91
|
+
"""Descriptor that creates a unique MultipleObjectsReturned exception class per model."""
|
|
92
|
+
|
|
93
|
+
def __init__(self) -> None:
|
|
94
|
+
self._exceptions_by_class: dict[type, type[MultipleObjectsReturned]] = {}
|
|
95
|
+
|
|
96
|
+
def __get__(
|
|
97
|
+
self, instance: Any, owner: type | None
|
|
98
|
+
) -> type[MultipleObjectsReturned]:
|
|
99
|
+
if owner is None:
|
|
100
|
+
return MultipleObjectsReturned # Return base class as fallback
|
|
101
|
+
|
|
102
|
+
# Create a unique exception class for this model if we haven't already
|
|
103
|
+
if owner not in self._exceptions_by_class:
|
|
104
|
+
# type() returns a subclass of MultipleObjectsReturned
|
|
105
|
+
exc_class = cast(
|
|
106
|
+
type[MultipleObjectsReturned],
|
|
107
|
+
type(
|
|
108
|
+
"MultipleObjectsReturned",
|
|
109
|
+
(MultipleObjectsReturned,),
|
|
110
|
+
{
|
|
111
|
+
"__module__": owner.__module__,
|
|
112
|
+
"__qualname__": f"{owner.__qualname__}.MultipleObjectsReturned",
|
|
113
|
+
},
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
self._exceptions_by_class[owner] = exc_class
|
|
117
|
+
|
|
118
|
+
return self._exceptions_by_class[owner]
|
|
119
|
+
|
|
120
|
+
def __set__(self, instance: Any, value: Any) -> None:
|
|
121
|
+
raise AttributeError("Cannot set MultipleObjectsReturned")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# MARK: Database Exceptions (PEP-249)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Error(Exception):
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class InterfaceError(Error):
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class DatabaseError(Error):
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class DataError(DatabaseError):
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class OperationalError(DatabaseError):
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class IntegrityError(DatabaseError):
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class InternalError(DatabaseError):
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class ProgrammingError(DatabaseError):
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class NotSupportedError(DatabaseError):
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class DatabaseErrorWrapper:
|
|
164
|
+
"""
|
|
165
|
+
Context manager and decorator that reraises backend-specific database
|
|
166
|
+
exceptions using Plain's common wrappers.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
def __init__(self, wrapper: DatabaseConnection) -> None:
|
|
170
|
+
"""
|
|
171
|
+
wrapper is a database wrapper.
|
|
172
|
+
|
|
173
|
+
It must have a Database attribute defining PEP-249 exceptions.
|
|
174
|
+
"""
|
|
175
|
+
self.wrapper = wrapper
|
|
176
|
+
|
|
177
|
+
def __enter__(self) -> None:
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
def __exit__(
|
|
181
|
+
self,
|
|
182
|
+
exc_type: type[BaseException] | None,
|
|
183
|
+
exc_value: BaseException | None,
|
|
184
|
+
traceback: Any,
|
|
185
|
+
) -> None:
|
|
186
|
+
if exc_type is None:
|
|
187
|
+
return
|
|
188
|
+
for plain_exc_type in (
|
|
189
|
+
DataError,
|
|
190
|
+
OperationalError,
|
|
191
|
+
IntegrityError,
|
|
192
|
+
InternalError,
|
|
193
|
+
ProgrammingError,
|
|
194
|
+
NotSupportedError,
|
|
195
|
+
DatabaseError,
|
|
196
|
+
InterfaceError,
|
|
197
|
+
Error,
|
|
198
|
+
):
|
|
199
|
+
db_exc_type = getattr(psycopg, plain_exc_type.__name__)
|
|
200
|
+
if issubclass(exc_type, db_exc_type):
|
|
201
|
+
plain_exc_value = (
|
|
202
|
+
plain_exc_type(*exc_value.args) if exc_value else plain_exc_type()
|
|
203
|
+
)
|
|
204
|
+
# Only set the 'errors_occurred' flag for errors that may make
|
|
205
|
+
# the connection unusable.
|
|
206
|
+
if plain_exc_type not in (DataError, IntegrityError):
|
|
207
|
+
self.wrapper.errors_occurred = True
|
|
208
|
+
raise plain_exc_value.with_traceback(traceback) from exc_value
|
|
209
|
+
|
|
210
|
+
def __call__(self, func: F) -> F:
|
|
211
|
+
# Note that we are intentionally not using @wraps here for performance
|
|
212
|
+
# reasons. Refs #21109.
|
|
213
|
+
def inner(*args: Any, **kwargs: Any) -> Any:
|
|
214
|
+
with self:
|
|
215
|
+
return func(*args, **kwargs)
|
|
216
|
+
|
|
217
|
+
return cast(F, inner)
|