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.
Files changed (93) hide show
  1. plain/postgres/CHANGELOG.md +1028 -0
  2. plain/postgres/README.md +925 -0
  3. plain/postgres/__init__.py +120 -0
  4. plain/postgres/agents/.claude/rules/plain-postgres.md +78 -0
  5. plain/postgres/aggregates.py +236 -0
  6. plain/postgres/backups/__init__.py +0 -0
  7. plain/postgres/backups/cli.py +148 -0
  8. plain/postgres/backups/clients.py +94 -0
  9. plain/postgres/backups/core.py +172 -0
  10. plain/postgres/base.py +1415 -0
  11. plain/postgres/cli/__init__.py +3 -0
  12. plain/postgres/cli/db.py +142 -0
  13. plain/postgres/cli/migrations.py +1085 -0
  14. plain/postgres/config.py +18 -0
  15. plain/postgres/connection.py +1331 -0
  16. plain/postgres/connections.py +77 -0
  17. plain/postgres/constants.py +13 -0
  18. plain/postgres/constraints.py +495 -0
  19. plain/postgres/database_url.py +94 -0
  20. plain/postgres/db.py +59 -0
  21. plain/postgres/default_settings.py +38 -0
  22. plain/postgres/deletion.py +475 -0
  23. plain/postgres/dialect.py +640 -0
  24. plain/postgres/entrypoints.py +4 -0
  25. plain/postgres/enums.py +103 -0
  26. plain/postgres/exceptions.py +217 -0
  27. plain/postgres/expressions.py +1912 -0
  28. plain/postgres/fields/__init__.py +2118 -0
  29. plain/postgres/fields/encrypted.py +354 -0
  30. plain/postgres/fields/json.py +413 -0
  31. plain/postgres/fields/mixins.py +30 -0
  32. plain/postgres/fields/related.py +1192 -0
  33. plain/postgres/fields/related_descriptors.py +290 -0
  34. plain/postgres/fields/related_lookups.py +223 -0
  35. plain/postgres/fields/related_managers.py +661 -0
  36. plain/postgres/fields/reverse_descriptors.py +229 -0
  37. plain/postgres/fields/reverse_related.py +328 -0
  38. plain/postgres/fields/timezones.py +143 -0
  39. plain/postgres/forms.py +773 -0
  40. plain/postgres/functions/__init__.py +189 -0
  41. plain/postgres/functions/comparison.py +127 -0
  42. plain/postgres/functions/datetime.py +454 -0
  43. plain/postgres/functions/math.py +140 -0
  44. plain/postgres/functions/mixins.py +59 -0
  45. plain/postgres/functions/text.py +282 -0
  46. plain/postgres/functions/window.py +125 -0
  47. plain/postgres/indexes.py +286 -0
  48. plain/postgres/lookups.py +758 -0
  49. plain/postgres/meta.py +584 -0
  50. plain/postgres/migrations/__init__.py +53 -0
  51. plain/postgres/migrations/autodetector.py +1379 -0
  52. plain/postgres/migrations/exceptions.py +54 -0
  53. plain/postgres/migrations/executor.py +188 -0
  54. plain/postgres/migrations/graph.py +364 -0
  55. plain/postgres/migrations/loader.py +377 -0
  56. plain/postgres/migrations/migration.py +180 -0
  57. plain/postgres/migrations/operations/__init__.py +34 -0
  58. plain/postgres/migrations/operations/base.py +139 -0
  59. plain/postgres/migrations/operations/fields.py +373 -0
  60. plain/postgres/migrations/operations/models.py +798 -0
  61. plain/postgres/migrations/operations/special.py +184 -0
  62. plain/postgres/migrations/optimizer.py +74 -0
  63. plain/postgres/migrations/questioner.py +340 -0
  64. plain/postgres/migrations/recorder.py +119 -0
  65. plain/postgres/migrations/serializer.py +378 -0
  66. plain/postgres/migrations/state.py +882 -0
  67. plain/postgres/migrations/utils.py +147 -0
  68. plain/postgres/migrations/writer.py +302 -0
  69. plain/postgres/options.py +207 -0
  70. plain/postgres/otel.py +231 -0
  71. plain/postgres/preflight.py +336 -0
  72. plain/postgres/query.py +2242 -0
  73. plain/postgres/query_utils.py +456 -0
  74. plain/postgres/registry.py +217 -0
  75. plain/postgres/schema.py +1885 -0
  76. plain/postgres/sql/__init__.py +40 -0
  77. plain/postgres/sql/compiler.py +1869 -0
  78. plain/postgres/sql/constants.py +22 -0
  79. plain/postgres/sql/datastructures.py +222 -0
  80. plain/postgres/sql/query.py +2947 -0
  81. plain/postgres/sql/where.py +374 -0
  82. plain/postgres/test/__init__.py +0 -0
  83. plain/postgres/test/pytest.py +117 -0
  84. plain/postgres/test/utils.py +18 -0
  85. plain/postgres/transaction.py +222 -0
  86. plain/postgres/types.py +92 -0
  87. plain/postgres/types.pyi +751 -0
  88. plain/postgres/utils.py +345 -0
  89. plain_postgres-0.84.0.dist-info/METADATA +937 -0
  90. plain_postgres-0.84.0.dist-info/RECORD +93 -0
  91. plain_postgres-0.84.0.dist-info/WHEEL +4 -0
  92. plain_postgres-0.84.0.dist-info/entry_points.txt +5 -0
  93. plain_postgres-0.84.0.dist-info/licenses/LICENSE +61 -0
@@ -0,0 +1,3 @@
1
+ from . import db, migrations
2
+
3
+ __all__ = ["db", "migrations"]
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ import sys
5
+ import time
6
+ from collections import defaultdict
7
+
8
+ import click
9
+
10
+ from plain.cli import register_cli
11
+
12
+ from ..backups.cli import cli as backups_cli
13
+ from ..db import OperationalError, get_connection
14
+ from ..dialect import quote_name
15
+ from ..migrations.recorder import MIGRATION_TABLE_NAME
16
+
17
+
18
+ @register_cli("db")
19
+ @click.group()
20
+ def cli() -> None:
21
+ """Database operations"""
22
+
23
+
24
+ cli.add_command(backups_cli)
25
+
26
+
27
+ @cli.command()
28
+ @click.argument("parameters", nargs=-1)
29
+ def shell(parameters: tuple[str, ...]) -> None:
30
+ """Open an interactive database shell"""
31
+ conn = get_connection()
32
+ try:
33
+ conn.runshell(list(parameters))
34
+ except FileNotFoundError:
35
+ # Note that we're assuming the FileNotFoundError relates to the
36
+ # command missing. It could be raised for some other reason, in
37
+ # which case this error message would be inaccurate. Still, this
38
+ # message catches the common case.
39
+ click.secho(
40
+ f"You appear not to have the {conn.executable_name!r} program installed or on your path.",
41
+ fg="red",
42
+ err=True,
43
+ )
44
+ sys.exit(1)
45
+ except subprocess.CalledProcessError as e:
46
+ click.secho(
47
+ '"{}" returned non-zero exit status {}.'.format(
48
+ " ".join(e.cmd),
49
+ e.returncode,
50
+ ),
51
+ fg="red",
52
+ err=True,
53
+ )
54
+ sys.exit(e.returncode)
55
+
56
+
57
+ @cli.command("drop-unknown-tables")
58
+ @click.option(
59
+ "--yes",
60
+ is_flag=True,
61
+ help="Skip confirmation prompt (for non-interactive use).",
62
+ )
63
+ def drop_unknown_tables(yes: bool) -> None:
64
+ """Drop all tables not associated with a Plain model"""
65
+ conn = get_connection()
66
+ db_tables = set(conn.table_names())
67
+ model_tables = set(conn.plain_table_names())
68
+ unknown_tables = sorted(db_tables - model_tables - {MIGRATION_TABLE_NAME})
69
+
70
+ if not unknown_tables:
71
+ click.echo("No unknown tables found.")
72
+ return
73
+
74
+ unknown_set = set(unknown_tables)
75
+ table_count = len(unknown_tables)
76
+ tables_label = f"{table_count} table{'s' if table_count != 1 else ''}"
77
+
78
+ # Find foreign key constraints from kept tables that reference unknown tables
79
+ cascade_warnings: defaultdict[str, list[tuple[str, str]]] = defaultdict(list)
80
+ with conn.cursor() as cursor:
81
+ for table in unknown_tables:
82
+ cursor.execute(
83
+ """
84
+ SELECT conname, conrelid::regclass
85
+ FROM pg_constraint
86
+ WHERE confrelid = %s::regclass AND contype = 'f'
87
+ """,
88
+ [table],
89
+ )
90
+ for constraint_name, referencing_table in cursor.fetchall():
91
+ if str(referencing_table) not in unknown_set:
92
+ cascade_warnings[table].append(
93
+ (constraint_name, str(referencing_table))
94
+ )
95
+
96
+ click.secho("Unknown tables:", fg="yellow", bold=True)
97
+ for table in unknown_tables:
98
+ click.echo(f" - {table}")
99
+ for constraint_name, referencing_table in cascade_warnings[table]:
100
+ click.secho(
101
+ f" ⚠ CASCADE will drop constraint {constraint_name} on {referencing_table}",
102
+ fg="red",
103
+ )
104
+ click.echo()
105
+
106
+ if not yes:
107
+ if not click.confirm(f"Drop {tables_label} (CASCADE)? This cannot be undone."):
108
+ return
109
+
110
+ with conn.cursor() as cursor:
111
+ for table in unknown_tables:
112
+ click.echo(f" Dropping {table}...", nl=False)
113
+ cursor.execute(f"DROP TABLE IF EXISTS {quote_name(table)} CASCADE")
114
+ click.echo(" OK")
115
+
116
+ click.secho(f"✓ Dropped {tables_label}.", fg="green")
117
+
118
+
119
+ @cli.command()
120
+ def wait() -> None:
121
+ """Wait for the database to be ready"""
122
+ attempts = 0
123
+ while True:
124
+ attempts += 1
125
+ waiting_for = False
126
+
127
+ try:
128
+ get_connection().ensure_connection()
129
+ except OperationalError:
130
+ waiting_for = True
131
+
132
+ if waiting_for:
133
+ if attempts > 1:
134
+ # After the first attempt, start printing them
135
+ click.secho(
136
+ f"Waiting for database (attempt {attempts})",
137
+ fg="yellow",
138
+ )
139
+ time.sleep(1.5)
140
+ else:
141
+ click.secho("✔ Database ready", fg="green")
142
+ break