pg0-embedded 0.1.1__tar.gz

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,5 @@
1
+ /target
2
+ Cargo.lock
3
+ .idea/
4
+ node_modules/
5
+ .env
@@ -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,123 @@
1
+ # pg0 - Embedded PostgreSQL for Python
2
+
3
+ Zero-config PostgreSQL with pgvector support. Just `pip install` and go.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install pg0-embedded
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```python
14
+ from pg0 import Pg0
15
+
16
+ # Start PostgreSQL (auto-installs on first run)
17
+ pg = Pg0()
18
+ pg.start()
19
+
20
+ print(pg.uri) # postgresql://postgres:postgres@localhost:5432/postgres
21
+
22
+ pg.stop()
23
+ ```
24
+
25
+ ## Context Manager
26
+
27
+ ```python
28
+ from pg0 import Pg0
29
+
30
+ with Pg0() as pg:
31
+ print(pg.uri)
32
+ pg.execute("CREATE EXTENSION IF NOT EXISTS vector")
33
+ pg.execute("SELECT version()")
34
+ # Automatically stopped
35
+ ```
36
+
37
+ ## Custom Configuration
38
+
39
+ ```python
40
+ from pg0 import Pg0
41
+
42
+ pg = Pg0(
43
+ port=5433,
44
+ username="myuser",
45
+ password="mypass",
46
+ database="mydb",
47
+ config={
48
+ "shared_buffers": "512MB",
49
+ "maintenance_work_mem": "1GB",
50
+ }
51
+ )
52
+
53
+ with pg:
54
+ print(pg.uri)
55
+ ```
56
+
57
+ ## Multiple Instances
58
+
59
+ ```python
60
+ from pg0 import Pg0, list_instances
61
+
62
+ # Run multiple PostgreSQL instances
63
+ app = Pg0(name="app", port=5432)
64
+ test = Pg0(name="test", port=5433)
65
+
66
+ app.start()
67
+ test.start()
68
+
69
+ for instance in list_instances():
70
+ print(f"{instance.name}: {instance.uri}")
71
+
72
+ app.stop()
73
+ test.stop()
74
+ ```
75
+
76
+ ## API Reference
77
+
78
+ ### Pg0 Class
79
+
80
+ ```python
81
+ pg = Pg0(
82
+ name="default", # Instance name
83
+ port=5432, # Port
84
+ username="postgres", # Username
85
+ password="postgres", # Password
86
+ database="postgres", # Database
87
+ data_dir=None, # Custom data directory
88
+ config={}, # PostgreSQL config options
89
+ )
90
+
91
+ pg.start() # Start PostgreSQL -> InstanceInfo
92
+ pg.stop() # Stop PostgreSQL
93
+ pg.info() # Get instance info -> InstanceInfo
94
+ pg.uri # Connection URI (property)
95
+ pg.running # Is running (property)
96
+ pg.execute(sql) # Execute SQL -> str
97
+ pg.psql(*args) # Run psql command
98
+ ```
99
+
100
+ ### Module Functions
101
+
102
+ ```python
103
+ import pg0
104
+
105
+ pg0.start(port=5432, ...) # Start instance -> InstanceInfo
106
+ pg0.stop(name="default") # Stop instance
107
+ pg0.info(name="default") # Get info -> InstanceInfo
108
+ pg0.list_instances() # List all -> [InstanceInfo]
109
+ pg0.install(version=None) # Install pg0 binary
110
+ ```
111
+
112
+ ### InstanceInfo
113
+
114
+ ```python
115
+ info.name # Instance name
116
+ info.running # Is running
117
+ info.pid # Process ID
118
+ info.port # Port
119
+ info.uri # Connection URI
120
+ info.username # Username
121
+ info.database # Database
122
+ info.data_dir # Data directory
123
+ ```
@@ -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
+ ]
File without changes
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "pg0-embedded"
3
+ version = "0.1.1"
4
+ description = "Python API for pg0 - embedded PostgreSQL"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.8"
8
+ keywords = ["postgresql", "database", "embedded", "pgvector"]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.8",
15
+ "Programming Language :: Python :: 3.9",
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/vectorize-io/pg0"
23
+ Repository = "https://github.com/vectorize-io/pg0"
24
+
25
+ [dependency-groups]
26
+ dev = ["pytest>=8.0.0"]
27
+
28
+ [build-system]
29
+ requires = ["hatchling"]
30
+ build-backend = "hatchling.build"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["pg0"]
File without changes
@@ -0,0 +1,195 @@
1
+ """Tests for pg0 Python client."""
2
+
3
+ import pytest
4
+ import pg0
5
+ from pg0 import Pg0, InstanceInfo, Pg0NotRunningError, Pg0AlreadyRunningError
6
+
7
+
8
+ # Use a unique port to avoid conflicts
9
+ TEST_PORT = 15432
10
+ TEST_NAME = "pytest-test"
11
+
12
+
13
+ @pytest.fixture
14
+ def clean_instance():
15
+ """Ensure test instance is stopped before and after test."""
16
+ # Cleanup before
17
+ try:
18
+ pg0.stop(TEST_NAME)
19
+ except Pg0NotRunningError:
20
+ pass
21
+
22
+ yield
23
+
24
+ # Cleanup after
25
+ try:
26
+ pg0.stop(TEST_NAME)
27
+ except Pg0NotRunningError:
28
+ pass
29
+
30
+
31
+ class TestPg0:
32
+ """Tests for Pg0 class."""
33
+
34
+ def test_start_stop(self, clean_instance):
35
+ """Test starting and stopping Pg0."""
36
+ pg = Pg0(name=TEST_NAME, port=TEST_PORT)
37
+
38
+ # Start
39
+ info = pg.start()
40
+ assert info.running is True
41
+ assert info.port == TEST_PORT
42
+ assert info.uri is not None
43
+ assert f":{TEST_PORT}/" in info.uri
44
+
45
+ # Stop
46
+ pg.stop()
47
+ info = pg.info()
48
+ assert info.running is False
49
+
50
+ def test_context_manager(self, clean_instance):
51
+ """Test using Pg0 as context manager."""
52
+ with Pg0(name=TEST_NAME, port=TEST_PORT) as pg:
53
+ assert pg.running is True
54
+ assert pg.uri is not None
55
+
56
+ # Should be stopped after exiting context
57
+ info = pg0.info(TEST_NAME)
58
+ assert info.running is False
59
+
60
+ def test_execute_sql(self, clean_instance):
61
+ """Test executing SQL commands."""
62
+ pg = Pg0(name=TEST_NAME, port=TEST_PORT)
63
+ pg.start()
64
+
65
+ try:
66
+ # Execute a simple query
67
+ result = pg.execute("SELECT 1 as num;")
68
+ assert "1" in result
69
+
70
+ # Create and query a table
71
+ pg.execute("CREATE TABLE test_table (id serial, name text);")
72
+ pg.execute("INSERT INTO test_table (name) VALUES ('hello');")
73
+ result = pg.execute("SELECT name FROM test_table;")
74
+ assert "hello" in result
75
+ finally:
76
+ pg.stop()
77
+
78
+ def test_custom_credentials(self, clean_instance):
79
+ """Test custom username, password, database."""
80
+ pg = Pg0(
81
+ name=TEST_NAME,
82
+ port=TEST_PORT,
83
+ username="testuser",
84
+ password="testpass",
85
+ database="testdb",
86
+ )
87
+ info = pg.start()
88
+
89
+ try:
90
+ assert "testuser" in info.uri
91
+ assert "testpass" in info.uri
92
+ assert "testdb" in info.uri
93
+ finally:
94
+ pg.stop()
95
+
96
+ def test_custom_config(self, clean_instance):
97
+ """Test custom Pg0 configuration."""
98
+ pg = Pg0(
99
+ name=TEST_NAME,
100
+ port=TEST_PORT,
101
+ config={"work_mem": "128MB"},
102
+ )
103
+ pg.start()
104
+
105
+ try:
106
+ result = pg.execute("SHOW work_mem;")
107
+ assert "128MB" in result
108
+ finally:
109
+ pg.stop()
110
+
111
+ def test_already_running_error(self, clean_instance):
112
+ """Test that starting twice raises error."""
113
+ pg = Pg0(name=TEST_NAME, port=TEST_PORT)
114
+ pg.start()
115
+
116
+ try:
117
+ with pytest.raises(Pg0AlreadyRunningError):
118
+ pg.start()
119
+ finally:
120
+ pg.stop()
121
+
122
+ def test_not_running_error(self, clean_instance):
123
+ """Test that stopping when not running raises error."""
124
+ pg = Pg0(name=TEST_NAME, port=TEST_PORT)
125
+
126
+ with pytest.raises(Pg0NotRunningError):
127
+ pg.stop()
128
+
129
+ def test_info_when_not_running(self, clean_instance):
130
+ """Test getting info when not running."""
131
+ pg = Pg0(name=TEST_NAME, port=TEST_PORT)
132
+ info = pg.info()
133
+
134
+ assert info.running is False
135
+ assert info.uri is None
136
+
137
+
138
+ class TestConvenienceFunctions:
139
+ """Tests for module-level convenience functions."""
140
+
141
+ def test_start_stop_info(self, clean_instance):
142
+ """Test start, stop, info functions."""
143
+ info = pg0.start(name=TEST_NAME, port=TEST_PORT)
144
+ assert info.running is True
145
+
146
+ info = pg0.info(TEST_NAME)
147
+ assert info.running is True
148
+ assert info.port == TEST_PORT
149
+
150
+ pg0.stop(TEST_NAME)
151
+ info = pg0.info(TEST_NAME)
152
+ assert info.running is False
153
+
154
+ def test_list_instances(self, clean_instance):
155
+ """Test listing instances."""
156
+ # Start an instance
157
+ pg0.start(name=TEST_NAME, port=TEST_PORT)
158
+
159
+ try:
160
+ instances = pg0.list_instances()
161
+ names = [i.name for i in instances]
162
+ assert TEST_NAME in names
163
+ finally:
164
+ pg0.stop(TEST_NAME)
165
+
166
+
167
+ class TestInstanceInfo:
168
+ """Tests for InstanceInfo dataclass."""
169
+
170
+ def test_from_dict(self):
171
+ """Test creating InstanceInfo from dict."""
172
+ data = {
173
+ "name": "test",
174
+ "running": True,
175
+ "pid": 1234,
176
+ "port": 5432,
177
+ "uri": "postgresql://localhost:5432/test",
178
+ }
179
+ info = InstanceInfo.from_dict(data)
180
+
181
+ assert info.name == "test"
182
+ assert info.running is True
183
+ assert info.pid == 1234
184
+ assert info.port == 5432
185
+ assert info.uri == "postgresql://localhost:5432/test"
186
+
187
+ def test_from_dict_minimal(self):
188
+ """Test creating InstanceInfo from minimal dict."""
189
+ data = {"running": False}
190
+ info = InstanceInfo.from_dict(data)
191
+
192
+ assert info.name == "default"
193
+ assert info.running is False
194
+ assert info.pid is None
195
+ assert info.uri is None
@@ -0,0 +1,251 @@
1
+ version = 1
2
+ revision = 1
3
+ requires-python = ">=3.8"
4
+ resolution-markers = [
5
+ "python_full_version >= '3.10'",
6
+ "python_full_version == '3.9.*'",
7
+ "python_full_version < '3.9'",
8
+ ]
9
+
10
+ [[package]]
11
+ name = "colorama"
12
+ version = "0.4.6"
13
+ source = { registry = "https://pypi.org/simple" }
14
+ sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
15
+ wheels = [
16
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
17
+ ]
18
+
19
+ [[package]]
20
+ name = "exceptiongroup"
21
+ version = "1.3.1"
22
+ source = { registry = "https://pypi.org/simple" }
23
+ dependencies = [
24
+ { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
25
+ { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" },
26
+ ]
27
+ sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 }
28
+ wheels = [
29
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 },
30
+ ]
31
+
32
+ [[package]]
33
+ name = "iniconfig"
34
+ version = "2.1.0"
35
+ source = { registry = "https://pypi.org/simple" }
36
+ resolution-markers = [
37
+ "python_full_version == '3.9.*'",
38
+ "python_full_version < '3.9'",
39
+ ]
40
+ sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 }
41
+ wheels = [
42
+ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 },
43
+ ]
44
+
45
+ [[package]]
46
+ name = "iniconfig"
47
+ version = "2.3.0"
48
+ source = { registry = "https://pypi.org/simple" }
49
+ resolution-markers = [
50
+ "python_full_version >= '3.10'",
51
+ ]
52
+ sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 }
53
+ wheels = [
54
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 },
55
+ ]
56
+
57
+ [[package]]
58
+ name = "packaging"
59
+ version = "25.0"
60
+ source = { registry = "https://pypi.org/simple" }
61
+ sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
62
+ wheels = [
63
+ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
64
+ ]
65
+
66
+ [[package]]
67
+ name = "pg0"
68
+ version = "0.1.0"
69
+ source = { editable = "." }
70
+
71
+ [package.dev-dependencies]
72
+ dev = [
73
+ { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
74
+ { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
75
+ { name = "pytest", version = "9.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
76
+ ]
77
+
78
+ [package.metadata]
79
+
80
+ [package.metadata.requires-dev]
81
+ dev = [{ name = "pytest", specifier = ">=8.0.0" }]
82
+
83
+ [[package]]
84
+ name = "pluggy"
85
+ version = "1.5.0"
86
+ source = { registry = "https://pypi.org/simple" }
87
+ resolution-markers = [
88
+ "python_full_version < '3.9'",
89
+ ]
90
+ sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 }
91
+ wheels = [
92
+ { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 },
93
+ ]
94
+
95
+ [[package]]
96
+ name = "pluggy"
97
+ version = "1.6.0"
98
+ source = { registry = "https://pypi.org/simple" }
99
+ resolution-markers = [
100
+ "python_full_version >= '3.10'",
101
+ "python_full_version == '3.9.*'",
102
+ ]
103
+ sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 }
104
+ wheels = [
105
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 },
106
+ ]
107
+
108
+ [[package]]
109
+ name = "pygments"
110
+ version = "2.19.2"
111
+ source = { registry = "https://pypi.org/simple" }
112
+ sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 }
113
+ wheels = [
114
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
115
+ ]
116
+
117
+ [[package]]
118
+ name = "pytest"
119
+ version = "8.3.5"
120
+ source = { registry = "https://pypi.org/simple" }
121
+ resolution-markers = [
122
+ "python_full_version < '3.9'",
123
+ ]
124
+ dependencies = [
125
+ { name = "colorama", marker = "python_full_version < '3.9' and sys_platform == 'win32'" },
126
+ { name = "exceptiongroup", marker = "python_full_version < '3.9'" },
127
+ { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
128
+ { name = "packaging", marker = "python_full_version < '3.9'" },
129
+ { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.9'" },
130
+ { name = "tomli", marker = "python_full_version < '3.9'" },
131
+ ]
132
+ sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 }
133
+ wheels = [
134
+ { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 },
135
+ ]
136
+
137
+ [[package]]
138
+ name = "pytest"
139
+ version = "8.4.2"
140
+ source = { registry = "https://pypi.org/simple" }
141
+ resolution-markers = [
142
+ "python_full_version == '3.9.*'",
143
+ ]
144
+ dependencies = [
145
+ { name = "colorama", marker = "python_full_version == '3.9.*' and sys_platform == 'win32'" },
146
+ { name = "exceptiongroup", marker = "python_full_version == '3.9.*'" },
147
+ { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
148
+ { name = "packaging", marker = "python_full_version == '3.9.*'" },
149
+ { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" },
150
+ { name = "pygments", marker = "python_full_version == '3.9.*'" },
151
+ { name = "tomli", marker = "python_full_version == '3.9.*'" },
152
+ ]
153
+ sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618 }
154
+ wheels = [
155
+ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750 },
156
+ ]
157
+
158
+ [[package]]
159
+ name = "pytest"
160
+ version = "9.0.1"
161
+ source = { registry = "https://pypi.org/simple" }
162
+ resolution-markers = [
163
+ "python_full_version >= '3.10'",
164
+ ]
165
+ dependencies = [
166
+ { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
167
+ { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" },
168
+ { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
169
+ { name = "packaging", marker = "python_full_version >= '3.10'" },
170
+ { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
171
+ { name = "pygments", marker = "python_full_version >= '3.10'" },
172
+ { name = "tomli", marker = "python_full_version == '3.10.*'" },
173
+ ]
174
+ sdist = { url = "https://files.pythonhosted.org/packages/07/56/f013048ac4bc4c1d9be45afd4ab209ea62822fb1598f40687e6bf45dcea4/pytest-9.0.1.tar.gz", hash = "sha256:3e9c069ea73583e255c3b21cf46b8d3c56f6e3a1a8f6da94ccb0fcf57b9d73c8", size = 1564125 }
175
+ wheels = [
176
+ { url = "https://files.pythonhosted.org/packages/0b/8b/6300fb80f858cda1c51ffa17075df5d846757081d11ab4aa35cef9e6258b/pytest-9.0.1-py3-none-any.whl", hash = "sha256:67be0030d194df2dfa7b556f2e56fb3c3315bd5c8822c6951162b92b32ce7dad", size = 373668 },
177
+ ]
178
+
179
+ [[package]]
180
+ name = "tomli"
181
+ version = "2.3.0"
182
+ source = { registry = "https://pypi.org/simple" }
183
+ sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392 }
184
+ wheels = [
185
+ { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236 },
186
+ { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084 },
187
+ { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832 },
188
+ { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052 },
189
+ { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555 },
190
+ { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128 },
191
+ { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445 },
192
+ { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165 },
193
+ { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891 },
194
+ { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796 },
195
+ { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121 },
196
+ { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070 },
197
+ { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859 },
198
+ { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296 },
199
+ { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124 },
200
+ { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698 },
201
+ { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819 },
202
+ { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766 },
203
+ { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771 },
204
+ { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586 },
205
+ { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792 },
206
+ { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909 },
207
+ { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946 },
208
+ { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705 },
209
+ { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244 },
210
+ { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637 },
211
+ { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925 },
212
+ { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045 },
213
+ { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835 },
214
+ { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109 },
215
+ { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930 },
216
+ { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964 },
217
+ { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065 },
218
+ { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088 },
219
+ { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193 },
220
+ { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488 },
221
+ { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669 },
222
+ { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709 },
223
+ { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563 },
224
+ { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756 },
225
+ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408 },
226
+ ]
227
+
228
+ [[package]]
229
+ name = "typing-extensions"
230
+ version = "4.13.2"
231
+ source = { registry = "https://pypi.org/simple" }
232
+ resolution-markers = [
233
+ "python_full_version < '3.9'",
234
+ ]
235
+ sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
236
+ wheels = [
237
+ { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
238
+ ]
239
+
240
+ [[package]]
241
+ name = "typing-extensions"
242
+ version = "4.15.0"
243
+ source = { registry = "https://pypi.org/simple" }
244
+ resolution-markers = [
245
+ "python_full_version >= '3.10'",
246
+ "python_full_version == '3.9.*'",
247
+ ]
248
+ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 }
249
+ wheels = [
250
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 },
251
+ ]