Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,36 @@ finally:
db.stop()
```

## Development

TinyPG uses [uv](https://docs.astral.sh/uv/) to manage its virtual environment
and development tooling. To contribute to the project locally:

1. Install uv (see the [uv installation guide](https://docs.astral.sh/uv/getting-started/installation/)).
2. Create the virtual environment and install all extras plus development
dependencies:

```bash
uv sync --all-extras --dev
```

This command creates a project-local `.venv/` directory that contains every
dependency required by the test suite and linters.

3. Run the test suite through uv to ensure the managed environment is used:

```bash
uv run pytest
```

4. The continuous integration workflow also runs formatting checks. Reproduce
them locally with:

```bash
uv run black --check .
uv run isort --check-only .
```

## Requirements

- Python 3.8+
Expand Down
3 changes: 2 additions & 1 deletion src/tinypg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@

from .config import TinyPGConfig
from .context import async_database, database, database_pool
from .core import AsyncEphemeralDB, EphemeralDB
from .core import AsyncEphemeralDB, EphemeralDB, PersistentDB
from .exceptions import (
BinaryNotFoundError,
DatabaseStartError,
Expand All @@ -181,6 +181,7 @@
__all__ = [
"EphemeralDB",
"AsyncEphemeralDB",
"PersistentDB",
"ExtensionSpec",
"ExtensionManifest",
"get_available_extension",
Expand Down
32 changes: 31 additions & 1 deletion src/tinypg/binaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,37 @@ def _detect_arch(self) -> str:

def _get_platform_string(self) -> str:
"""Get the platform string for binary downloads."""
return f"{self.os_name}-{self.arch}"
platform = f"{self.os_name}-{self.arch}"

# TODO not returning alpine yet for testing
# if False and self.os_name == "linux" and self._using_musl:
# return f"{platform}-alpine"

return platform

def _detect_musl(self) -> bool:
"""Return ``True`` when running on a musl-based libc such as Alpine."""
if self.os_name != "linux":
return False

libc, _ = platform.libc_ver()
if libc and libc.lower().startswith("musl"):
return True

if Path("/etc/alpine-release").exists():
return True

try:
result = subprocess.run(
["ldd", "--version"], capture_output=True, text=True, check=False
)
combined_output = f"{result.stdout}\n{result.stderr}".lower()
if "musl" in combined_output:
return True
except FileNotFoundError:
pass

return False

@classmethod
def ensure_version(cls, version: str) -> Path:
Expand Down
56 changes: 52 additions & 4 deletions src/tinypg/config.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
"""
TinyPG configuration management.
"""
"""TinyPG configuration management."""

import os
import shutil
import tempfile
from pathlib import Path
from typing import Optional


def _default_cache_dir() -> Path:
"""Compute the default cache directory for downloaded PostgreSQL binaries."""

if hasattr(os, "geteuid") and os.geteuid() == 0:
# When running as root prefer a shared location so that unprivileged
# helper processes can still access the binaries.
return Path(tempfile.gettempdir()) / "tinypg"

return Path.home() / ".tinypg"


class TinyPGConfig:
"""Global configuration for TinyPG."""

# Default PostgreSQL version
default_version: str = "15"

# Cache directory for PostgreSQL binaries
cache_dir: Path = Path.home() / ".tinypg"
cache_dir: Path = _default_cache_dir()

# Automatic cleanup of databases
auto_cleanup: bool = True
Expand All @@ -28,6 +39,10 @@ class TinyPGConfig:
# System temp directory override
system_temp_dir: Optional[str] = None

# Runtime user/group used when dropping privileges for PostgreSQL helpers
runtime_user: Optional[str] = None
runtime_group: Optional[str] = None

@classmethod
def set_cache_dir(cls, path: Path) -> None:
"""Set the directory for caching PostgreSQL binaries."""
Expand All @@ -50,4 +65,37 @@ def get_temp_dir(cls) -> Path:
def get_cache_dir(cls) -> Path:
"""Get the cache directory, creating it if necessary."""
cls.cache_dir.mkdir(parents=True, exist_ok=True)
cls.cache_dir.chmod(0o755)
cls._migrate_legacy_cache()
return cls.cache_dir

@classmethod
def set_runtime_identity(cls, user: str, group: Optional[str] = None) -> None:
"""Configure the user/group used to run PostgreSQL helpers."""

cls.runtime_user = user
cls.runtime_group = group

@classmethod
def _migrate_legacy_cache(cls) -> None:
"""Copy binaries from the legacy cache directory if necessary."""

legacy_dir = Path.home() / ".tinypg"

if legacy_dir == cls.cache_dir or not legacy_dir.exists():
return

try:
for entry in legacy_dir.iterdir():
destination = cls.cache_dir / entry.name

if destination.exists():
continue

if entry.is_dir():
shutil.copytree(entry, destination, dirs_exist_ok=True)
else:
shutil.copy2(entry, destination)
except OSError:
# Migration is best-effort; fallback to downloading if copying fails.
pass
Loading