sediment-cli 0.1.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.
@@ -0,0 +1,337 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ """Private local PostgreSQL installation and foreground server ownership."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import hashlib
7
+ import os
8
+ import platform
9
+ import signal
10
+ import socket
11
+ import stat
12
+ import subprocess
13
+ import tarfile
14
+ import tempfile
15
+ import time
16
+ import urllib.request
17
+ from contextlib import contextmanager
18
+ from pathlib import Path
19
+
20
+ VERSION = "17.11.0"
21
+ _PACKAGES = {
22
+ ("Darwin", "arm64"): (
23
+ "aarch64-apple-darwin",
24
+ "fd4b62794b160e26973a768a1eef3248aef9d2ff23ebd6d884a4299485e28e57",
25
+ ),
26
+ ("Darwin", "x86_64"): (
27
+ "x86_64-apple-darwin",
28
+ "e43a81b15e1cfe7f9d8fd79c6d4d0366e9001a5f690e322224dca704656602f7",
29
+ ),
30
+ ("Linux", "aarch64"): (
31
+ "aarch64-unknown-linux-gnu",
32
+ "abffda09209280ec1502b73720dc4d254fb7fff9a072e324926c600a5b16c221",
33
+ ),
34
+ ("Linux", "x86_64"): (
35
+ "x86_64-unknown-linux-gnu",
36
+ "b7a1ba6bae6499d8296e3e81b0171eecfd1766ca9aaa0057e41ad3e844e5e2e0",
37
+ ),
38
+ }
39
+ _EXTERNAL = "Set SEDIMENT_BOOTSTRAP_DATABASE_URL to use external PostgreSQL."
40
+
41
+
42
+ @contextmanager
43
+ def private_file(path: Path, flags: int):
44
+ """Open an owned regular file without following links or waiting on a FIFO."""
45
+ fd = os.open(path, flags | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600)
46
+ try:
47
+ info = os.fstat(fd)
48
+ if (
49
+ not stat.S_ISREG(info.st_mode)
50
+ or info.st_nlink != 1
51
+ or info.st_uid != os.getuid()
52
+ ):
53
+ raise ValueError(f"{path.name} must be a regular private file")
54
+ os.fchmod(fd, 0o600)
55
+ yield fd
56
+ finally:
57
+ os.close(fd)
58
+
59
+
60
+ @contextmanager
61
+ def server_root(root: Path):
62
+ """Hold one root for the entire API and PostgreSQL lifetime."""
63
+ if os.name != "posix":
64
+ raise ValueError("sediment server requires macOS or Linux")
65
+ import fcntl
66
+
67
+ if root.is_symlink():
68
+ raise ValueError("server root must be a regular private directory")
69
+ root.mkdir(parents=True, exist_ok=True, mode=0o700)
70
+ if root.stat().st_uid != os.getuid():
71
+ raise ValueError("server root must be a regular private directory")
72
+ root.chmod(0o700)
73
+ with private_file(root / "server.lock", os.O_CREAT | os.O_RDWR) as fd:
74
+ try:
75
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
76
+ except BlockingIOError:
77
+ raise ValueError(f"a server is already running under {root}") from None
78
+ # Uvicorn restores and re-raises signals after its own graceful shutdown.
79
+ # This handler also covers PostgreSQL initialization and provisioning.
80
+ previous = signal.getsignal(signal.SIGTERM)
81
+ signal.signal(signal.SIGTERM, _interrupt)
82
+ try:
83
+ yield
84
+ finally:
85
+ signal.signal(signal.SIGTERM, previous)
86
+
87
+
88
+ def _interrupt(signum, frame):
89
+ raise KeyboardInterrupt
90
+
91
+
92
+ def _install_postgres(root: Path) -> Path:
93
+ package = _PACKAGES.get((platform.system(), platform.machine()))
94
+ if package is None:
95
+ raise ValueError(
96
+ f"managed PostgreSQL isn't available on this platform. {_EXTERNAL}"
97
+ )
98
+ target, checksum = package
99
+ name = f"postgresql-{VERSION}-{target}"
100
+ destination = root / name
101
+ host_ssl = None
102
+ if platform.system() == "Darwin":
103
+ try:
104
+ prefix = subprocess.run(
105
+ ["brew", "--prefix", "openssl@3"],
106
+ check=True,
107
+ capture_output=True,
108
+ text=True,
109
+ timeout=10,
110
+ ).stdout.strip()
111
+ host_ssl = Path(prefix) / "lib"
112
+ if not host_ssl.is_absolute() or not all(
113
+ (host_ssl / name).is_file()
114
+ for name in ("libssl.3.dylib", "libcrypto.3.dylib")
115
+ ):
116
+ raise ValueError
117
+ except (OSError, ValueError, subprocess.SubprocessError):
118
+ raise ValueError(
119
+ "managed PostgreSQL requires Homebrew openssl@3; install the quickstart prerequisites, then retry"
120
+ ) from None
121
+ if destination.is_symlink():
122
+ raise ValueError("PostgreSQL installation must be a regular private directory")
123
+ if destination.exists():
124
+ if not all(
125
+ (destination / "bin" / executable).is_file()
126
+ for executable in ("postgres", "initdb")
127
+ ):
128
+ raise ValueError(f"incomplete PostgreSQL installation at {destination}")
129
+ return destination / "bin"
130
+ print(f"Downloading PostgreSQL {VERSION.removesuffix('.0')} for this machine...")
131
+ url = f"https://github.com/theseus-rs/postgresql-binaries/releases/download/{VERSION}/{name}.tar.gz"
132
+ with tempfile.TemporaryDirectory(
133
+ prefix=".postgres-download-", dir=root
134
+ ) as temporary:
135
+ staging = Path(temporary)
136
+ archive = staging / "postgres.tar.gz"
137
+ digest = hashlib.sha256()
138
+ size = 0
139
+ try:
140
+ with (
141
+ urllib.request.urlopen(url, timeout=30) as response,
142
+ archive.open("wb") as stream,
143
+ ):
144
+ while chunk := response.read(1024 * 1024):
145
+ size += len(chunk)
146
+ if size > 128 * 1024 * 1024:
147
+ raise ValueError("PostgreSQL download exceeds the size limit")
148
+ digest.update(chunk)
149
+ stream.write(chunk)
150
+ except OSError:
151
+ raise ValueError(
152
+ f"PostgreSQL download failed; check your connection and retry. {_EXTERNAL}"
153
+ ) from None
154
+ if digest.hexdigest() != checksum:
155
+ raise ValueError(
156
+ "PostgreSQL download checksum mismatch; no binaries were installed"
157
+ )
158
+ try:
159
+ with tarfile.open(archive, mode="r:gz") as bundle:
160
+
161
+ def installed_file(member, path):
162
+ # Use maintained host OpenSSL; never install the archive's
163
+ # copied libraries. Ordinary loader paths follow the two
164
+ # links below, including initdb's shell-launched children.
165
+ if host_ssl is not None and member.name in {
166
+ f"{name}/lib/libssl.3.dylib",
167
+ f"{name}/lib/libcrypto.3.dylib",
168
+ }:
169
+ return None
170
+ return tarfile.data_filter(member, path)
171
+
172
+ bundle.extractall(staging, filter=installed_file)
173
+ except (tarfile.TarError, OSError):
174
+ raise ValueError(
175
+ "PostgreSQL archive could not be extracted safely"
176
+ ) from None
177
+ installed = staging / name
178
+ if not all(
179
+ (installed / "bin" / executable).is_file()
180
+ for executable in ("postgres", "initdb")
181
+ ):
182
+ raise ValueError("PostgreSQL archive lacks the required executables")
183
+ if host_ssl is not None:
184
+ for library in ("libssl.3.dylib", "libcrypto.3.dylib"):
185
+ (installed / "lib" / library).symlink_to(host_ssl / library)
186
+ installed.rename(destination)
187
+ return destination / "bin"
188
+
189
+
190
+ def _stop(process: subprocess.Popen) -> None:
191
+ """Stop only the child this invocation owns; never act on a saved PID."""
192
+ if process.poll() is None:
193
+ os.killpg(process.pid, signal.SIGINT) # Fast, orderly PostgreSQL shutdown.
194
+ try:
195
+ process.wait(timeout=15)
196
+ except subprocess.TimeoutExpired:
197
+ os.killpg(process.pid, signal.SIGKILL)
198
+ process.wait()
199
+
200
+
201
+ @contextmanager
202
+ def managed_postgres(root: Path, password: str):
203
+ """Yield a bootstrap URL while an owned loopback PostgreSQL process runs."""
204
+ from sqlalchemy.engine import URL
205
+ from sediment_core.postgres_engine import DatabaseOperationError, configure_libpq
206
+
207
+ if os.geteuid() == 0:
208
+ raise ValueError(f"managed PostgreSQL must run as a non-root user. {_EXTERNAL}")
209
+ # Keep the existing maintained host driver; fail before downloading or
210
+ # initializing data if it isn't installed.
211
+ try:
212
+ configure_libpq()
213
+ import psycopg
214
+ except ImportError:
215
+ raise ValueError(
216
+ "PostgreSQL client library unavailable; install libpq (macOS) or libpq5 (Debian/Ubuntu), then retry"
217
+ ) from None
218
+ binaries = _install_postgres(root)
219
+ data = root / "postgres"
220
+ if data.is_symlink():
221
+ raise ValueError("PostgreSQL data must be a regular private directory")
222
+ if data.exists() and (
223
+ not (data / "PG_VERSION").is_file()
224
+ or (data / "PG_VERSION").read_text().strip() != "17"
225
+ ):
226
+ raise ValueError(
227
+ f"existing database at {data} isn't PostgreSQL 17; it was preserved"
228
+ )
229
+ environment = {
230
+ key: value
231
+ for key, value in os.environ.items()
232
+ if not key.startswith(("SEDIMENT_", "PG"))
233
+ }
234
+ environment["LC_ALL"] = "C"
235
+ log = root / "postgres.log"
236
+ with private_file(log, os.O_CREAT | os.O_WRONLY | os.O_APPEND) as log_fd:
237
+ if not data.exists():
238
+ print(f"Creating PostgreSQL database in {data}")
239
+ with tempfile.TemporaryDirectory(
240
+ prefix=".postgres-init-", dir=root
241
+ ) as temporary:
242
+ staging = Path(temporary)
243
+ password_file = staging / "password"
244
+ with private_file(password_file, os.O_CREAT | os.O_WRONLY) as fd:
245
+ os.write(fd, password.encode() + b"\n")
246
+ initializer = subprocess.Popen(
247
+ [
248
+ str(binaries / "initdb"),
249
+ "-D",
250
+ str(staging / "data"),
251
+ "--username=sediment_bootstrap",
252
+ f"--pwfile={password_file}",
253
+ "--auth=scram-sha-256",
254
+ "--encoding=UTF8",
255
+ "--locale=C",
256
+ ],
257
+ env=environment,
258
+ stdin=subprocess.DEVNULL,
259
+ stdout=log_fd,
260
+ stderr=log_fd,
261
+ start_new_session=True,
262
+ )
263
+ try:
264
+ if initializer.wait(timeout=60):
265
+ raise ValueError(f"PostgreSQL initialization failed; see {log}")
266
+ except subprocess.TimeoutExpired:
267
+ raise ValueError(
268
+ f"PostgreSQL initialization timed out; see {log}"
269
+ ) from None
270
+ finally:
271
+ _stop(initializer)
272
+ (staging / "data").rename(data)
273
+ with socket.socket() as listener:
274
+ listener.bind(("127.0.0.1", 0))
275
+ port = listener.getsockname()[1]
276
+ url = URL.create(
277
+ "postgresql+psycopg",
278
+ username="sediment_bootstrap",
279
+ password=password,
280
+ host="127.0.0.1",
281
+ port=port,
282
+ database="sediment",
283
+ )
284
+ process = subprocess.Popen(
285
+ [
286
+ str(binaries / "postgres"),
287
+ "-D",
288
+ str(data),
289
+ "-h",
290
+ "127.0.0.1",
291
+ "-p",
292
+ str(port),
293
+ "-c",
294
+ "unix_socket_directories=",
295
+ "-c",
296
+ "ssl=off",
297
+ ],
298
+ env=environment,
299
+ stdin=subprocess.DEVNULL,
300
+ stdout=log_fd,
301
+ stderr=log_fd,
302
+ start_new_session=True,
303
+ )
304
+ try:
305
+ deadline = time.monotonic() + 30
306
+ while True:
307
+ if process.poll() is not None:
308
+ raise ValueError(f"PostgreSQL startup failed; see {log}")
309
+ try:
310
+ with psycopg.connect(
311
+ host="127.0.0.1",
312
+ port=port,
313
+ user="sediment_bootstrap",
314
+ password=password,
315
+ dbname="postgres",
316
+ connect_timeout=1,
317
+ sslmode="disable",
318
+ autocommit=True,
319
+ ) as connection:
320
+ if not connection.execute(
321
+ "SELECT 1 FROM pg_database WHERE datname = 'sediment'"
322
+ ).fetchone():
323
+ connection.execute("CREATE DATABASE sediment")
324
+ break
325
+ except psycopg.OperationalError:
326
+ if time.monotonic() >= deadline:
327
+ raise ValueError(
328
+ f"PostgreSQL didn't become ready; see {log}"
329
+ ) from None
330
+ time.sleep(0.1)
331
+ except psycopg.Error:
332
+ raise DatabaseOperationError(
333
+ f"create local database failed; see {log}"
334
+ ) from None
335
+ yield url.render_as_string(hide_password=False)
336
+ finally:
337
+ _stop(process)