2929
3030__version__ = "0.1.0"
3131
32- # GitHub repo for pg0 releases
33- PG0_REPO = "vectorize-io/pg0"
34- INSTALL_SCRIPT_URL = f"https://raw.githubusercontent.com/{ PG0_REPO } /main/install.sh"
35-
3632
3733class Pg0Error (Exception ):
3834 """Base exception for pg0 errors."""
@@ -102,72 +98,8 @@ def _get_install_dir() -> Path:
10298 return Path .home () / ".local" / "bin"
10399
104100
105- def install (force : bool = False ) -> Path :
106- """
107- Install the pg0 binary using the official install script.
108-
109- Note: If the package was installed from a platform-specific wheel,
110- the binary is already bundled and this function returns immediately.
111-
112- Args:
113- force: Force reinstall even if already installed
114-
115- Returns:
116- Path to the installed binary
117- """
118- # Check for bundled binary first (from platform-specific wheel)
119- bundled = _get_bundled_binary ()
120- if bundled and not force :
121- return bundled
122-
123- install_dir = _get_install_dir ()
124- binary_name = "pg0.exe" if sys .platform == "win32" else "pg0"
125- binary_path = install_dir / binary_name
126-
127- # Check if already installed externally
128- if binary_path .exists () and not force :
129- return binary_path
130-
131- # Use the official install script which handles:
132- # - Platform detection (including old glibc fallback to musl)
133- # - Intel Mac Rosetta handling
134- # - Proper binary naming
135- if sys .platform == "win32" :
136- # Windows: download directly since bash isn't available
137- raise Pg0NotFoundError (
138- "Auto-install not supported on Windows. "
139- "Please download pg0 manually from https://github.com/vectorize-io/pg0/releases"
140- )
141-
142- print ("Installing pg0 using official install script..." )
143- try :
144- result = subprocess .run (
145- ["bash" , "-c" , f"curl -fsSL { INSTALL_SCRIPT_URL } | bash" ],
146- capture_output = True ,
147- text = True ,
148- timeout = 120 ,
149- )
150- if result .returncode != 0 :
151- raise Pg0NotFoundError (f"Install script failed: { result .stderr } " )
152-
153- # Verify installation
154- if binary_path .exists ():
155- return binary_path
156-
157- # Check if installed to a different location
158- path = shutil .which ("pg0" )
159- if path :
160- return Path (path )
161-
162- raise Pg0NotFoundError ("Install script succeeded but pg0 binary not found" )
163- except subprocess .TimeoutExpired :
164- raise Pg0NotFoundError ("Install script timed out" )
165- except FileNotFoundError :
166- raise Pg0NotFoundError ("bash not found - please install pg0 manually" )
167-
168-
169101def _find_pg0 () -> str :
170- """Find the pg0 binary, installing if necessary ."""
102+ """Find the pg0 binary or raise an error if not found ."""
171103 # Check for bundled binary first (from platform-specific wheel)
172104 bundled = _get_bundled_binary ()
173105 if bundled :
@@ -178,17 +110,20 @@ def _find_pg0() -> str:
178110 if path :
179111 return path
180112
181- # Check our install location
113+ # Check common install location
182114 install_dir = _get_install_dir ()
183115 binary_name = "pg0.exe" if sys .platform == "win32" else "pg0"
184116 binary_path = install_dir / binary_name
185117
186118 if binary_path .exists ():
187119 return str (binary_path )
188120
189- # Auto-install as fallback
190- installed_path = install ()
191- return str (installed_path )
121+ # No binary found
122+ raise Pg0NotFoundError (
123+ "pg0 binary not found. Install it with:\n "
124+ " curl -fsSL https://raw.githubusercontent.com/vectorize-io/pg0/main/install.sh | bash\n "
125+ "Or download from: https://github.com/vectorize-io/pg0/releases"
126+ )
192127
193128
194129def _run_pg0 (* args : str , check : bool = True ) -> subprocess .CompletedProcess :
@@ -219,21 +154,21 @@ class Pg0:
219154
220155 Args:
221156 name: Instance name (allows multiple instances)
222- port: Port to listen on
157+ port: Port to listen on (None = auto-select available port)
223158 username: Database username
224159 password: Database password
225160 database: Database name
226161 data_dir: Custom data directory
227162 config: Dict of PostgreSQL configuration options
228163
229164 Example:
230- # Simple usage
165+ # Simple usage (auto-selects available port)
231166 pg = Pg0()
232167 pg.start()
233168 print(pg.uri)
234169 pg.stop()
235170
236- # Context manager
171+ # Context manager with specific port
237172 with Pg0(port=5433, database="myapp") as pg:
238173 print(pg.uri)
239174
@@ -244,7 +179,7 @@ class Pg0:
244179 def __init__ (
245180 self ,
246181 name : str = "default" ,
247- port : int = 5432 ,
182+ port : Optional [ int ] = None ,
248183 username : str = "postgres" ,
249184 password : str = "postgres" ,
250185 database : str = "postgres" ,
@@ -273,12 +208,14 @@ def start(self) -> InstanceInfo:
273208 args = [
274209 "start" ,
275210 "--name" , self .name ,
276- "--port" , str (self .port ),
277211 "--username" , self .username ,
278212 "--password" , self .password ,
279213 "--database" , self .database ,
280214 ]
281215
216+ if self .port is not None :
217+ args .extend (["--port" , str (self .port )])
218+
282219 if self .data_dir :
283220 args .extend (["--data-dir" , self .data_dir ])
284221
@@ -364,6 +301,25 @@ def execute(self, sql: str) -> str:
364301 result = self .psql ("-c" , sql )
365302 return result .stdout
366303
304+ def logs (self , lines : Optional [int ] = None ) -> str :
305+ """
306+ Get PostgreSQL logs for this instance.
307+
308+ Args:
309+ lines: Number of lines to return (None = all logs)
310+
311+ Returns:
312+ Log content as string
313+
314+ Example:
315+ print(pg.logs(50)) # Last 50 lines
316+ """
317+ args = ["logs" , "--name" , self .name ]
318+ if lines is not None :
319+ args .extend (["-n" , str (lines )])
320+ result = _run_pg0 (* args , check = False )
321+ return result .stdout
322+
367323 def __enter__ (self ) -> "Pg0" :
368324 """Context manager entry - starts PostgreSQL."""
369325 self .start ()
@@ -407,7 +363,7 @@ def list_extensions() -> list[str]:
407363
408364def start (
409365 name : str = "default" ,
410- port : int = 5432 ,
366+ port : Optional [ int ] = None ,
411367 username : str = "postgres" ,
412368 password : str = "postgres" ,
413369 database : str = "postgres" ,
@@ -418,7 +374,7 @@ def start(
418374
419375 Args:
420376 name: Instance name
421- port: Port to listen on
377+ port: Port to listen on (None = auto-select available port)
422378 username: Database username
423379 password: Database password
424380 database: Database name
@@ -428,7 +384,7 @@ def start(
428384 InstanceInfo with connection details
429385
430386 Example:
431- info = pg0.start(port=5433, shared_buffers="512MB")
387+ info = pg0.start(shared_buffers="512MB") # auto-selects port
432388 print(info.uri)
433389 """
434390 pg = Pg0 (
@@ -486,6 +442,24 @@ def info(name: str = "default") -> InstanceInfo:
486442 return InstanceInfo .from_dict (data )
487443
488444
445+ def logs (name : str = "default" , lines : Optional [int ] = None ) -> str :
446+ """
447+ Get PostgreSQL logs for an instance (convenience function).
448+
449+ Args:
450+ name: Instance name
451+ lines: Number of lines to return (None = all logs)
452+
453+ Returns:
454+ Log content as string
455+ """
456+ args = ["logs" , "--name" , name ]
457+ if lines is not None :
458+ args .extend (["-n" , str (lines )])
459+ result = _run_pg0 (* args , check = False )
460+ return result .stdout
461+
462+
489463# Keep PostgreSQL as alias for backwards compatibility
490464PostgreSQL = Pg0
491465
@@ -498,12 +472,12 @@ def info(name: str = "default") -> InstanceInfo:
498472 "Pg0NotFoundError" ,
499473 "Pg0NotRunningError" ,
500474 "Pg0AlreadyRunningError" ,
501- "install" ,
502475 "list_instances" ,
503476 "list_extensions" ,
504477 "start" ,
505478 "stop" ,
506479 "drop" ,
507480 "info" ,
481+ "logs" ,
508482 "_get_bundled_binary" , # for testing
509483]
0 commit comments