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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Generator
|
|
4
|
+
from contextlib import ContextDecorator, contextmanager
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from plain.postgres.db import DatabaseError, Error, ProgrammingError, get_connection
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TransactionManagementError(ProgrammingError):
|
|
11
|
+
"""Transaction management is used improperly."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@contextmanager
|
|
17
|
+
def mark_for_rollback_on_error() -> Generator[None]:
|
|
18
|
+
"""
|
|
19
|
+
Internal low-level utility to mark a transaction as "needs rollback" when
|
|
20
|
+
an exception is raised while not enforcing the enclosed block to be in a
|
|
21
|
+
transaction. This is needed by Model.save() and friends to avoid starting a
|
|
22
|
+
transaction when in autocommit mode and a single query is executed.
|
|
23
|
+
|
|
24
|
+
It's equivalent to:
|
|
25
|
+
|
|
26
|
+
if get_connection().get_autocommit():
|
|
27
|
+
yield
|
|
28
|
+
else:
|
|
29
|
+
with transaction.atomic(savepoint=False):
|
|
30
|
+
yield
|
|
31
|
+
|
|
32
|
+
but it uses low-level utilities to avoid performance overhead.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
yield
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
conn = get_connection()
|
|
38
|
+
if conn.in_atomic_block:
|
|
39
|
+
conn.needs_rollback = True
|
|
40
|
+
conn.rollback_exc = exc
|
|
41
|
+
raise
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def on_commit(func: Callable[[], Any], robust: bool = False) -> None:
|
|
45
|
+
"""
|
|
46
|
+
Register `func` to be called when the current transaction is committed.
|
|
47
|
+
If the current transaction is rolled back, `func` will not be called.
|
|
48
|
+
"""
|
|
49
|
+
get_connection().on_commit(func, robust)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
#################################
|
|
53
|
+
# Decorators / context managers #
|
|
54
|
+
#################################
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Atomic(ContextDecorator):
|
|
58
|
+
"""
|
|
59
|
+
Guarantee the atomic execution of a given block.
|
|
60
|
+
|
|
61
|
+
An instance can be used either as a decorator or as a context manager.
|
|
62
|
+
|
|
63
|
+
When it's used as a decorator, __call__ wraps the execution of the
|
|
64
|
+
decorated function in the instance itself, used as a context manager.
|
|
65
|
+
|
|
66
|
+
When it's used as a context manager, __enter__ creates a transaction or a
|
|
67
|
+
savepoint, depending on whether a transaction is already in progress, and
|
|
68
|
+
__exit__ commits the transaction or releases the savepoint on normal exit,
|
|
69
|
+
and rolls back the transaction or to the savepoint on exceptions.
|
|
70
|
+
|
|
71
|
+
It's possible to disable the creation of savepoints if the goal is to
|
|
72
|
+
ensure that some code runs within a transaction without creating overhead.
|
|
73
|
+
|
|
74
|
+
A stack of savepoints identifiers is maintained as an attribute of the
|
|
75
|
+
connection. None denotes the absence of a savepoint.
|
|
76
|
+
|
|
77
|
+
This allows reentrancy even if the same AtomicWrapper is reused. For
|
|
78
|
+
example, it's possible to define `oa = atomic('other')` and use `@oa` or
|
|
79
|
+
`with oa:` multiple times.
|
|
80
|
+
|
|
81
|
+
Since database connections are stored per-context (ContextVar), this is thread-safe.
|
|
82
|
+
|
|
83
|
+
An atomic block can be tagged as durable. In this case, raise a
|
|
84
|
+
RuntimeError if it's nested within another atomic block. This guarantees
|
|
85
|
+
that database changes in a durable block are committed to the database when
|
|
86
|
+
the block exists without error.
|
|
87
|
+
|
|
88
|
+
This is a private API.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, savepoint: bool, durable: bool) -> None:
|
|
92
|
+
self.savepoint = savepoint
|
|
93
|
+
self.durable = durable
|
|
94
|
+
self._from_testcase = False
|
|
95
|
+
|
|
96
|
+
def __enter__(self) -> None:
|
|
97
|
+
conn = get_connection()
|
|
98
|
+
if (
|
|
99
|
+
self.durable
|
|
100
|
+
and conn.atomic_blocks
|
|
101
|
+
and not conn.atomic_blocks[-1]._from_testcase
|
|
102
|
+
):
|
|
103
|
+
raise RuntimeError(
|
|
104
|
+
"A durable atomic block cannot be nested within another atomic block."
|
|
105
|
+
)
|
|
106
|
+
if not conn.in_atomic_block:
|
|
107
|
+
# Reset state when entering an outermost atomic block.
|
|
108
|
+
conn.needs_rollback = False
|
|
109
|
+
if conn.in_atomic_block:
|
|
110
|
+
# We're already in a transaction; create a savepoint, unless we
|
|
111
|
+
# were told not to or we're already waiting for a rollback. The
|
|
112
|
+
# second condition avoids creating useless savepoints and prevents
|
|
113
|
+
# overwriting needs_rollback until the rollback is performed.
|
|
114
|
+
if self.savepoint and not conn.needs_rollback:
|
|
115
|
+
sid = conn.savepoint()
|
|
116
|
+
conn.savepoint_ids.append(sid)
|
|
117
|
+
else:
|
|
118
|
+
conn.savepoint_ids.append(None)
|
|
119
|
+
else:
|
|
120
|
+
conn.set_autocommit(False)
|
|
121
|
+
conn.in_atomic_block = True
|
|
122
|
+
|
|
123
|
+
if conn.in_atomic_block:
|
|
124
|
+
conn.atomic_blocks.append(self)
|
|
125
|
+
|
|
126
|
+
def __exit__(
|
|
127
|
+
self,
|
|
128
|
+
exc_type: type[BaseException] | None,
|
|
129
|
+
exc_value: BaseException | None,
|
|
130
|
+
traceback: Any,
|
|
131
|
+
) -> None:
|
|
132
|
+
conn = get_connection()
|
|
133
|
+
if conn.in_atomic_block:
|
|
134
|
+
conn.atomic_blocks.pop()
|
|
135
|
+
|
|
136
|
+
if conn.savepoint_ids:
|
|
137
|
+
sid = conn.savepoint_ids.pop()
|
|
138
|
+
else:
|
|
139
|
+
# Prematurely unset this flag to allow using commit or rollback.
|
|
140
|
+
conn.in_atomic_block = False
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
if conn.closed_in_transaction:
|
|
144
|
+
# The database will perform a rollback by itself.
|
|
145
|
+
# Wait until we exit the outermost block.
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
elif exc_type is None and not conn.needs_rollback:
|
|
149
|
+
if conn.in_atomic_block:
|
|
150
|
+
# Release savepoint if there is one
|
|
151
|
+
if sid is not None:
|
|
152
|
+
try:
|
|
153
|
+
conn.savepoint_commit(sid)
|
|
154
|
+
except DatabaseError:
|
|
155
|
+
try:
|
|
156
|
+
conn.savepoint_rollback(sid)
|
|
157
|
+
# The savepoint won't be reused. Release it to
|
|
158
|
+
# minimize overhead for the database server.
|
|
159
|
+
conn.savepoint_commit(sid)
|
|
160
|
+
except Error:
|
|
161
|
+
# If rolling back to a savepoint fails, mark for
|
|
162
|
+
# rollback at a higher level and avoid shadowing
|
|
163
|
+
# the original exception.
|
|
164
|
+
conn.needs_rollback = True
|
|
165
|
+
raise
|
|
166
|
+
else:
|
|
167
|
+
# Commit transaction
|
|
168
|
+
try:
|
|
169
|
+
conn.commit()
|
|
170
|
+
except DatabaseError:
|
|
171
|
+
try:
|
|
172
|
+
conn.rollback()
|
|
173
|
+
except Error:
|
|
174
|
+
# An error during rollback means that something
|
|
175
|
+
# went wrong with the connection. Drop it.
|
|
176
|
+
conn.close()
|
|
177
|
+
raise
|
|
178
|
+
else:
|
|
179
|
+
# This flag will be set to True again if there isn't a savepoint
|
|
180
|
+
# allowing to perform the rollback at this level.
|
|
181
|
+
conn.needs_rollback = False
|
|
182
|
+
if conn.in_atomic_block:
|
|
183
|
+
# Roll back to savepoint if there is one, mark for rollback
|
|
184
|
+
# otherwise.
|
|
185
|
+
if sid is None:
|
|
186
|
+
conn.needs_rollback = True
|
|
187
|
+
else:
|
|
188
|
+
try:
|
|
189
|
+
conn.savepoint_rollback(sid)
|
|
190
|
+
# The savepoint won't be reused. Release it to
|
|
191
|
+
# minimize overhead for the database server.
|
|
192
|
+
conn.savepoint_commit(sid)
|
|
193
|
+
except Error:
|
|
194
|
+
# If rolling back to a savepoint fails, mark for
|
|
195
|
+
# rollback at a higher level and avoid shadowing
|
|
196
|
+
# the original exception.
|
|
197
|
+
conn.needs_rollback = True
|
|
198
|
+
else:
|
|
199
|
+
# Roll back transaction
|
|
200
|
+
try:
|
|
201
|
+
conn.rollback()
|
|
202
|
+
except Error:
|
|
203
|
+
# An error during rollback means that something
|
|
204
|
+
# went wrong with the connection. Drop it.
|
|
205
|
+
conn.close()
|
|
206
|
+
|
|
207
|
+
finally:
|
|
208
|
+
# Outermost block exit when autocommit was enabled.
|
|
209
|
+
if not conn.in_atomic_block:
|
|
210
|
+
if conn.closed_in_transaction:
|
|
211
|
+
conn.connection = None
|
|
212
|
+
else:
|
|
213
|
+
conn.set_autocommit(True)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def atomic[F: Callable[..., Any]](
|
|
217
|
+
func: F | None = None, *, savepoint: bool = True, durable: bool = False
|
|
218
|
+
) -> F | Atomic:
|
|
219
|
+
"""Create an atomic transaction context or decorator."""
|
|
220
|
+
if callable(func):
|
|
221
|
+
return Atomic(savepoint, durable)(func)
|
|
222
|
+
return Atomic(savepoint, durable)
|
plain/postgres/types.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Typed field imports for better IDE and type checker support.
|
|
3
|
+
|
|
4
|
+
This module provides the same field classes as plain.postgres.fields,
|
|
5
|
+
but with a companion .pyi stub file that makes type checkers interpret
|
|
6
|
+
field assignments as their primitive Python types.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from plain.postgres import types
|
|
10
|
+
|
|
11
|
+
@postgres.register_model
|
|
12
|
+
class User(postgres.Model):
|
|
13
|
+
email: str = types.EmailField()
|
|
14
|
+
age: int = types.IntegerField()
|
|
15
|
+
is_active: bool = types.BooleanField(default=True)
|
|
16
|
+
|
|
17
|
+
This is optional - you can continue using untyped field definitions.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Re-export scalar field types
|
|
21
|
+
from plain.postgres.fields import (
|
|
22
|
+
BigIntegerField,
|
|
23
|
+
BinaryField,
|
|
24
|
+
BooleanField,
|
|
25
|
+
CharField,
|
|
26
|
+
DateField,
|
|
27
|
+
DateTimeField,
|
|
28
|
+
DecimalField,
|
|
29
|
+
DurationField,
|
|
30
|
+
EmailField,
|
|
31
|
+
FloatField,
|
|
32
|
+
GenericIPAddressField,
|
|
33
|
+
IntegerField,
|
|
34
|
+
PositiveBigIntegerField,
|
|
35
|
+
PositiveIntegerField,
|
|
36
|
+
PositiveSmallIntegerField,
|
|
37
|
+
PrimaryKeyField,
|
|
38
|
+
SmallIntegerField,
|
|
39
|
+
TextField,
|
|
40
|
+
TimeField,
|
|
41
|
+
URLField,
|
|
42
|
+
UUIDField,
|
|
43
|
+
)
|
|
44
|
+
from plain.postgres.fields.encrypted import (
|
|
45
|
+
EncryptedJSONField,
|
|
46
|
+
EncryptedTextField,
|
|
47
|
+
)
|
|
48
|
+
from plain.postgres.fields.json import JSONField
|
|
49
|
+
from plain.postgres.fields.related import ForeignKeyField, ManyToManyField
|
|
50
|
+
from plain.postgres.fields.related_managers import (
|
|
51
|
+
ManyToManyManager,
|
|
52
|
+
ReverseForeignKeyManager,
|
|
53
|
+
)
|
|
54
|
+
from plain.postgres.fields.reverse_descriptors import (
|
|
55
|
+
ReverseForeignKey,
|
|
56
|
+
ReverseManyToMany,
|
|
57
|
+
)
|
|
58
|
+
from plain.postgres.fields.timezones import TimeZoneField
|
|
59
|
+
|
|
60
|
+
__all__ = [
|
|
61
|
+
"BigIntegerField",
|
|
62
|
+
"BinaryField",
|
|
63
|
+
"BooleanField",
|
|
64
|
+
"CharField",
|
|
65
|
+
"DateField",
|
|
66
|
+
"DateTimeField",
|
|
67
|
+
"DecimalField",
|
|
68
|
+
"DurationField",
|
|
69
|
+
"EmailField",
|
|
70
|
+
"EncryptedJSONField",
|
|
71
|
+
"EncryptedTextField",
|
|
72
|
+
"FloatField",
|
|
73
|
+
"ForeignKeyField",
|
|
74
|
+
"GenericIPAddressField",
|
|
75
|
+
"IntegerField",
|
|
76
|
+
"JSONField",
|
|
77
|
+
"ManyToManyField",
|
|
78
|
+
"ManyToManyManager",
|
|
79
|
+
"ReverseForeignKey",
|
|
80
|
+
"ReverseForeignKeyManager",
|
|
81
|
+
"ReverseManyToMany",
|
|
82
|
+
"PositiveBigIntegerField",
|
|
83
|
+
"PositiveIntegerField",
|
|
84
|
+
"PositiveSmallIntegerField",
|
|
85
|
+
"PrimaryKeyField",
|
|
86
|
+
"SmallIntegerField",
|
|
87
|
+
"TextField",
|
|
88
|
+
"TimeField",
|
|
89
|
+
"TimeZoneField",
|
|
90
|
+
"URLField",
|
|
91
|
+
"UUIDField",
|
|
92
|
+
]
|