pg0-embedded 0.1.1__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.
pg0/__init__.py ADDED
@@ -0,0 +1,538 @@
1
+ """
2
+ pg0 - Embedded PostgreSQL for Python
3
+
4
+ Usage:
5
+ from pg0 import Pg0
6
+
7
+ # Start PostgreSQL
8
+ pg = Pg0()
9
+ pg.start()
10
+ print(pg.uri)
11
+ pg.stop()
12
+
13
+ # Or use context manager
14
+ with Pg0() as pg:
15
+ print(pg.uri)
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ import os
22
+ import platform
23
+ import shutil
24
+ import stat
25
+ import subprocess
26
+ import sys
27
+ import tempfile
28
+ import urllib.request
29
+ from dataclasses import dataclass
30
+ from pathlib import Path
31
+ from typing import Optional
32
+
33
+
34
+ __version__ = "0.1.0"
35
+
36
+ # GitHub repo for pg0 releases
37
+ PG0_REPO = "vectorize-io/pg0"
38
+
39
+
40
+ class Pg0Error(Exception):
41
+ """Base exception for pg0 errors."""
42
+ pass
43
+
44
+
45
+ class Pg0NotFoundError(Pg0Error):
46
+ """pg0 binary not found and could not be installed."""
47
+ pass
48
+
49
+
50
+ class Pg0NotRunningError(Pg0Error):
51
+ """PostgreSQL instance is not running."""
52
+ pass
53
+
54
+
55
+ class Pg0AlreadyRunningError(Pg0Error):
56
+ """PostgreSQL instance is already running."""
57
+ pass
58
+
59
+
60
+ @dataclass
61
+ class InstanceInfo:
62
+ """Information about a PostgreSQL instance."""
63
+ name: str
64
+ running: bool
65
+ pid: Optional[int] = None
66
+ port: Optional[int] = None
67
+ version: Optional[str] = None
68
+ username: Optional[str] = None
69
+ database: Optional[str] = None
70
+ data_dir: Optional[str] = None
71
+ uri: Optional[str] = None
72
+
73
+ @classmethod
74
+ def from_dict(cls, data: dict) -> "InstanceInfo":
75
+ return cls(
76
+ name=data.get("name", "default"),
77
+ running=data.get("running", False),
78
+ pid=data.get("pid"),
79
+ port=data.get("port"),
80
+ version=data.get("version"),
81
+ username=data.get("username"),
82
+ database=data.get("database"),
83
+ data_dir=data.get("data_dir"),
84
+ uri=data.get("uri"),
85
+ )
86
+
87
+
88
+ def _get_install_dir() -> Path:
89
+ """Get the directory where pg0 binary should be installed."""
90
+ # Use ~/.local/bin on Unix, or a pg0-specific dir
91
+ if sys.platform == "win32":
92
+ base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
93
+ return base / "pg0" / "bin"
94
+ else:
95
+ return Path.home() / ".local" / "bin"
96
+
97
+
98
+ def _get_platform() -> str:
99
+ """Get the platform string for downloading the correct binary."""
100
+ system = platform.system().lower()
101
+ machine = platform.machine().lower()
102
+
103
+ if system == "darwin":
104
+ # macOS - only Apple Silicon supported, Intel uses Rosetta
105
+ return "darwin-aarch64"
106
+ elif system == "linux":
107
+ # Detect architecture
108
+ if machine in ("x86_64", "amd64"):
109
+ arch_str = "x86_64"
110
+ elif machine in ("aarch64", "arm64"):
111
+ arch_str = "aarch64"
112
+ else:
113
+ raise Pg0NotFoundError(f"Unsupported Linux architecture: {machine}")
114
+
115
+ # Detect libc (musl vs glibc)
116
+ # Check for musl by looking for the musl loader
117
+ import subprocess
118
+ try:
119
+ result = subprocess.run(
120
+ ["ldd", "--version"],
121
+ capture_output=True,
122
+ text=True,
123
+ timeout=5,
124
+ )
125
+ output = result.stdout + result.stderr
126
+ if "musl" in output.lower():
127
+ return f"linux-{arch_str}-musl"
128
+ except (FileNotFoundError, subprocess.TimeoutExpired):
129
+ pass
130
+
131
+ # Check for musl loader file
132
+ musl_loaders = [
133
+ f"/lib/ld-musl-{arch_str}.so.1",
134
+ "/lib/ld-musl-x86_64.so.1",
135
+ "/lib/ld-musl-aarch64.so.1",
136
+ ]
137
+ for loader in musl_loaders:
138
+ if Path(loader).exists():
139
+ return f"linux-{arch_str}-musl"
140
+
141
+ # Default to glibc
142
+ return f"linux-{arch_str}-gnu"
143
+ elif system == "windows":
144
+ return "windows-x86_64"
145
+ else:
146
+ raise Pg0NotFoundError(f"Unsupported platform: {system}")
147
+
148
+
149
+ def _get_latest_version() -> str:
150
+ """Get the latest pg0 version from GitHub."""
151
+ url = f"https://api.github.com/repos/{PG0_REPO}/releases/latest"
152
+ try:
153
+ with urllib.request.urlopen(url, timeout=30) as response:
154
+ data = json.loads(response.read().decode())
155
+ return data["tag_name"]
156
+ except Exception as e:
157
+ raise Pg0NotFoundError(f"Failed to fetch latest version: {e}")
158
+
159
+
160
+ def install(version: Optional[str] = None, force: bool = False) -> Path:
161
+ """
162
+ Install the pg0 binary.
163
+
164
+ Args:
165
+ version: Version to install (default: latest)
166
+ force: Force reinstall even if already installed
167
+
168
+ Returns:
169
+ Path to the installed binary
170
+ """
171
+ install_dir = _get_install_dir()
172
+ binary_name = "pg0.exe" if sys.platform == "win32" else "pg0"
173
+ binary_path = install_dir / binary_name
174
+
175
+ # Check if already installed
176
+ if binary_path.exists() and not force:
177
+ return binary_path
178
+
179
+ # Get version
180
+ if version is None:
181
+ version = _get_latest_version()
182
+
183
+ # Get platform
184
+ plat = _get_platform()
185
+
186
+ # Build download URL
187
+ ext = ".exe" if sys.platform == "win32" else ""
188
+ filename = f"pg0-{plat}{ext}"
189
+ url = f"https://github.com/{PG0_REPO}/releases/download/{version}/{filename}"
190
+
191
+ print(f"Installing pg0 {version}...")
192
+
193
+ # Create install directory
194
+ install_dir.mkdir(parents=True, exist_ok=True)
195
+
196
+ # Download binary
197
+ try:
198
+ with tempfile.NamedTemporaryFile(delete=False) as tmp:
199
+ tmp_path = Path(tmp.name)
200
+
201
+ with urllib.request.urlopen(url, timeout=120) as response:
202
+ tmp_path.write_bytes(response.read())
203
+
204
+ # Move to install location
205
+ shutil.move(str(tmp_path), str(binary_path))
206
+
207
+ # Make executable on Unix
208
+ if sys.platform != "win32":
209
+ binary_path.chmod(binary_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
210
+
211
+ print(f"Installed pg0 to {binary_path}")
212
+ return binary_path
213
+
214
+ except Exception as e:
215
+ # Cleanup
216
+ if tmp_path.exists():
217
+ tmp_path.unlink()
218
+ raise Pg0NotFoundError(f"Failed to install pg0: {e}")
219
+
220
+
221
+ def _find_pg0() -> str:
222
+ """Find the pg0 binary, installing if necessary."""
223
+ # Check PATH first
224
+ path = shutil.which("pg0")
225
+ if path:
226
+ return path
227
+
228
+ # Check our install location
229
+ install_dir = _get_install_dir()
230
+ binary_name = "pg0.exe" if sys.platform == "win32" else "pg0"
231
+ binary_path = install_dir / binary_name
232
+
233
+ if binary_path.exists():
234
+ return str(binary_path)
235
+
236
+ # Auto-install
237
+ installed_path = install(version=None)
238
+ return str(installed_path)
239
+
240
+
241
+ def _run_pg0(*args: str, check: bool = True) -> subprocess.CompletedProcess:
242
+ """Run a pg0 command."""
243
+ pg0_path = _find_pg0()
244
+ try:
245
+ result = subprocess.run(
246
+ [pg0_path, *args],
247
+ capture_output=True,
248
+ text=True,
249
+ )
250
+ if check and result.returncode != 0:
251
+ stderr = result.stderr.strip()
252
+ if "already running" in stderr.lower():
253
+ raise Pg0AlreadyRunningError(stderr)
254
+ elif "no running instance" in stderr.lower() or "not running" in stderr.lower():
255
+ raise Pg0NotRunningError(stderr)
256
+ else:
257
+ raise Pg0Error(stderr or f"pg0 command failed with code {result.returncode}")
258
+ return result
259
+ except FileNotFoundError:
260
+ raise Pg0NotFoundError("pg0 binary not found")
261
+
262
+
263
+ class Pg0:
264
+ """
265
+ Embedded PostgreSQL instance.
266
+
267
+ Args:
268
+ name: Instance name (allows multiple instances)
269
+ port: Port to listen on
270
+ username: Database username
271
+ password: Database password
272
+ database: Database name
273
+ data_dir: Custom data directory
274
+ config: Dict of PostgreSQL configuration options
275
+
276
+ Example:
277
+ # Simple usage
278
+ pg = Pg0()
279
+ pg.start()
280
+ print(pg.uri)
281
+ pg.stop()
282
+
283
+ # Context manager
284
+ with Pg0(port=5433, database="myapp") as pg:
285
+ print(pg.uri)
286
+
287
+ # Custom config
288
+ pg = Pg0(config={"shared_buffers": "512MB"})
289
+ """
290
+
291
+ def __init__(
292
+ self,
293
+ name: str = "default",
294
+ port: int = 5432,
295
+ username: str = "postgres",
296
+ password: str = "postgres",
297
+ database: str = "postgres",
298
+ data_dir: Optional[str] = None,
299
+ config: Optional[dict[str, str]] = None,
300
+ ):
301
+ self.name = name
302
+ self.port = port
303
+ self.username = username
304
+ self.password = password
305
+ self.database = database
306
+ self.data_dir = data_dir
307
+ self.config = config or {}
308
+
309
+ def start(self) -> InstanceInfo:
310
+ """
311
+ Start the PostgreSQL instance.
312
+
313
+ Returns:
314
+ InstanceInfo with connection details
315
+
316
+ Raises:
317
+ Pg0AlreadyRunningError: If instance is already running
318
+ Pg0Error: If start fails
319
+ """
320
+ args = [
321
+ "start",
322
+ "--name", self.name,
323
+ "--port", str(self.port),
324
+ "--username", self.username,
325
+ "--password", self.password,
326
+ "--database", self.database,
327
+ ]
328
+
329
+ if self.data_dir:
330
+ args.extend(["--data-dir", self.data_dir])
331
+
332
+ for key, value in self.config.items():
333
+ args.extend(["-c", f"{key}={value}"])
334
+
335
+ _run_pg0(*args)
336
+ return self.info()
337
+
338
+ def stop(self) -> None:
339
+ """
340
+ Stop the PostgreSQL instance.
341
+
342
+ Note: Does not raise an error if the instance is not running.
343
+ """
344
+ _run_pg0("stop", "--name", self.name, check=False)
345
+
346
+ def drop(self, force: bool = True) -> None:
347
+ """
348
+ Drop the PostgreSQL instance (stop if running, delete all data).
349
+
350
+ Args:
351
+ force: Skip confirmation prompt (default True for programmatic use)
352
+
353
+ Warning:
354
+ This permanently deletes all data for this instance!
355
+ """
356
+ args = ["drop", "--name", self.name]
357
+ if force:
358
+ args.append("--force")
359
+ _run_pg0(*args, check=False)
360
+
361
+ def info(self) -> InstanceInfo:
362
+ """
363
+ Get information about the PostgreSQL instance.
364
+
365
+ Returns:
366
+ InstanceInfo with current status and connection details
367
+ """
368
+ result = _run_pg0("info", "--name", self.name, "-o", "json", check=False)
369
+ data = json.loads(result.stdout)
370
+ return InstanceInfo.from_dict(data)
371
+
372
+ @property
373
+ def uri(self) -> Optional[str]:
374
+ """Get the connection URI if running."""
375
+ return self.info().uri
376
+
377
+ @property
378
+ def running(self) -> bool:
379
+ """Check if the instance is running."""
380
+ return self.info().running
381
+
382
+ def psql(self, *args: str) -> subprocess.CompletedProcess:
383
+ """
384
+ Run psql with the given arguments.
385
+
386
+ Args:
387
+ *args: Arguments to pass to psql (e.g., "-c", "SELECT 1")
388
+
389
+ Returns:
390
+ CompletedProcess with stdout/stderr
391
+
392
+ Example:
393
+ result = pg.psql("-c", "SELECT version();")
394
+ print(result.stdout)
395
+ """
396
+ return _run_pg0("psql", "--name", self.name, *args)
397
+
398
+ def execute(self, sql: str) -> str:
399
+ """
400
+ Execute a SQL command and return the output.
401
+
402
+ Args:
403
+ sql: SQL command to execute
404
+
405
+ Returns:
406
+ Command output as string
407
+
408
+ Example:
409
+ output = pg.execute("SELECT version();")
410
+ """
411
+ result = self.psql("-c", sql)
412
+ return result.stdout
413
+
414
+ def __enter__(self) -> "Pg0":
415
+ """Context manager entry - starts PostgreSQL."""
416
+ self.start()
417
+ return self
418
+
419
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
420
+ """Context manager exit - stops PostgreSQL."""
421
+ try:
422
+ self.stop()
423
+ except Pg0NotRunningError:
424
+ pass
425
+
426
+
427
+ def list_instances() -> list[InstanceInfo]:
428
+ """
429
+ List all pg0 instances.
430
+
431
+ Returns:
432
+ List of InstanceInfo for all known instances
433
+ """
434
+ result = _run_pg0("list", "-o", "json", check=False)
435
+ data = json.loads(result.stdout)
436
+ return [InstanceInfo.from_dict(item) for item in data]
437
+
438
+
439
+ def start(
440
+ name: str = "default",
441
+ port: int = 5432,
442
+ username: str = "postgres",
443
+ password: str = "postgres",
444
+ database: str = "postgres",
445
+ **config: str,
446
+ ) -> InstanceInfo:
447
+ """
448
+ Start a PostgreSQL instance (convenience function).
449
+
450
+ Args:
451
+ name: Instance name
452
+ port: Port to listen on
453
+ username: Database username
454
+ password: Database password
455
+ database: Database name
456
+ **config: PostgreSQL configuration options
457
+
458
+ Returns:
459
+ InstanceInfo with connection details
460
+
461
+ Example:
462
+ info = pg0.start(port=5433, shared_buffers="512MB")
463
+ print(info.uri)
464
+ """
465
+ pg = Pg0(
466
+ name=name,
467
+ port=port,
468
+ username=username,
469
+ password=password,
470
+ database=database,
471
+ config=config,
472
+ )
473
+ return pg.start()
474
+
475
+
476
+ def stop(name: str = "default") -> None:
477
+ """
478
+ Stop a PostgreSQL instance (convenience function).
479
+
480
+ Args:
481
+ name: Instance name to stop
482
+ """
483
+ _run_pg0("stop", "--name", name, check=False)
484
+
485
+
486
+ def drop(name: str = "default", force: bool = True) -> None:
487
+ """
488
+ Drop a PostgreSQL instance (convenience function).
489
+
490
+ Stops the instance if running and deletes all data.
491
+
492
+ Args:
493
+ name: Instance name to drop
494
+ force: Skip confirmation prompt (default True for programmatic use)
495
+
496
+ Warning:
497
+ This permanently deletes all data for this instance!
498
+ """
499
+ args = ["drop", "--name", name]
500
+ if force:
501
+ args.append("--force")
502
+ _run_pg0(*args, check=False)
503
+
504
+
505
+ def info(name: str = "default") -> InstanceInfo:
506
+ """
507
+ Get information about a PostgreSQL instance (convenience function).
508
+
509
+ Args:
510
+ name: Instance name
511
+
512
+ Returns:
513
+ InstanceInfo with current status
514
+ """
515
+ result = _run_pg0("info", "--name", name, "-o", "json", check=False)
516
+ data = json.loads(result.stdout)
517
+ return InstanceInfo.from_dict(data)
518
+
519
+
520
+ # Keep PostgreSQL as alias for backwards compatibility
521
+ PostgreSQL = Pg0
522
+
523
+
524
+ __all__ = [
525
+ "Pg0",
526
+ "PostgreSQL", # alias
527
+ "InstanceInfo",
528
+ "Pg0Error",
529
+ "Pg0NotFoundError",
530
+ "Pg0NotRunningError",
531
+ "Pg0AlreadyRunningError",
532
+ "install",
533
+ "list_instances",
534
+ "start",
535
+ "stop",
536
+ "drop",
537
+ "info",
538
+ ]
pg0/py.typed ADDED
File without changes
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: pg0-embedded
3
+ Version: 0.1.1
4
+ Summary: Python API for pg0 - embedded PostgreSQL
5
+ Project-URL: Homepage, https://github.com/vectorize-io/pg0
6
+ Project-URL: Repository, https://github.com/vectorize-io/pg0
7
+ License-Expression: MIT
8
+ Keywords: database,embedded,pgvector,postgresql
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.8
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+
21
+ # pg0 - Embedded PostgreSQL for Python
22
+
23
+ Zero-config PostgreSQL with pgvector support. Just `pip install` and go.
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install pg0-embedded
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```python
34
+ from pg0 import Pg0
35
+
36
+ # Start PostgreSQL (auto-installs on first run)
37
+ pg = Pg0()
38
+ pg.start()
39
+
40
+ print(pg.uri) # postgresql://postgres:postgres@localhost:5432/postgres
41
+
42
+ pg.stop()
43
+ ```
44
+
45
+ ## Context Manager
46
+
47
+ ```python
48
+ from pg0 import Pg0
49
+
50
+ with Pg0() as pg:
51
+ print(pg.uri)
52
+ pg.execute("CREATE EXTENSION IF NOT EXISTS vector")
53
+ pg.execute("SELECT version()")
54
+ # Automatically stopped
55
+ ```
56
+
57
+ ## Custom Configuration
58
+
59
+ ```python
60
+ from pg0 import Pg0
61
+
62
+ pg = Pg0(
63
+ port=5433,
64
+ username="myuser",
65
+ password="mypass",
66
+ database="mydb",
67
+ config={
68
+ "shared_buffers": "512MB",
69
+ "maintenance_work_mem": "1GB",
70
+ }
71
+ )
72
+
73
+ with pg:
74
+ print(pg.uri)
75
+ ```
76
+
77
+ ## Multiple Instances
78
+
79
+ ```python
80
+ from pg0 import Pg0, list_instances
81
+
82
+ # Run multiple PostgreSQL instances
83
+ app = Pg0(name="app", port=5432)
84
+ test = Pg0(name="test", port=5433)
85
+
86
+ app.start()
87
+ test.start()
88
+
89
+ for instance in list_instances():
90
+ print(f"{instance.name}: {instance.uri}")
91
+
92
+ app.stop()
93
+ test.stop()
94
+ ```
95
+
96
+ ## API Reference
97
+
98
+ ### Pg0 Class
99
+
100
+ ```python
101
+ pg = Pg0(
102
+ name="default", # Instance name
103
+ port=5432, # Port
104
+ username="postgres", # Username
105
+ password="postgres", # Password
106
+ database="postgres", # Database
107
+ data_dir=None, # Custom data directory
108
+ config={}, # PostgreSQL config options
109
+ )
110
+
111
+ pg.start() # Start PostgreSQL -> InstanceInfo
112
+ pg.stop() # Stop PostgreSQL
113
+ pg.info() # Get instance info -> InstanceInfo
114
+ pg.uri # Connection URI (property)
115
+ pg.running # Is running (property)
116
+ pg.execute(sql) # Execute SQL -> str
117
+ pg.psql(*args) # Run psql command
118
+ ```
119
+
120
+ ### Module Functions
121
+
122
+ ```python
123
+ import pg0
124
+
125
+ pg0.start(port=5432, ...) # Start instance -> InstanceInfo
126
+ pg0.stop(name="default") # Stop instance
127
+ pg0.info(name="default") # Get info -> InstanceInfo
128
+ pg0.list_instances() # List all -> [InstanceInfo]
129
+ pg0.install(version=None) # Install pg0 binary
130
+ ```
131
+
132
+ ### InstanceInfo
133
+
134
+ ```python
135
+ info.name # Instance name
136
+ info.running # Is running
137
+ info.pid # Process ID
138
+ info.port # Port
139
+ info.uri # Connection URI
140
+ info.username # Username
141
+ info.database # Database
142
+ info.data_dir # Data directory
143
+ ```
@@ -0,0 +1,5 @@
1
+ pg0/__init__.py,sha256=C8i9Lat5e2c8RD70yuzamyYt_cOZY6urGxSRufZTZ8U,14282
2
+ pg0/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ pg0_embedded-0.1.1.dist-info/METADATA,sha256=pya-8UDnu6xWrLhgrZ9dQfRVi-7AQ3s1fTb_6lQ6hAA,3200
4
+ pg0_embedded-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
5
+ pg0_embedded-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any