Use when building reusable PyPI package with pgdbm - provides complete pattern for libraries that work standalone or embedded without needing examples
Provides complete dual-mode library pattern for PyPI packages using pgdbm. Triggers when building reusable libraries that need to work standalone or embedded.
/plugin marketplace add juanre/pgdbm/plugin install pgdbm@juanre-ai-toolsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Core Principle: Accept connection_string OR db_manager, always run own migrations, clean up conditionally.
This pattern makes your library work both standalone (creates own pool) AND embedded (uses shared pool) without code changes.
From pgdbm:choosing-pattern skill - use when:
from typing import Optional
from pathlib import Path
from pgdbm import AsyncDatabaseManager, DatabaseConfig, AsyncMigrationManager
class MyLibrary:
"""Library supporting dual-mode operation.
Standalone: MyLibrary(connection_string='postgresql://...')
Shared pool: MyLibrary(db_manager=shared_db)
"""
def __init__(
self,
connection_string: Optional[str] = None,
db_manager: Optional[AsyncDatabaseManager] = None,
schema: Optional[str] = None,
):
# Validation
if not connection_string and not db_manager:
raise ValueError("Either connection_string or db_manager required")
# Track ownership
self._external_db = db_manager is not None
self.db = db_manager
self._connection_string = connection_string
self._schema = schema or "mylib"
async def initialize(self):
# Create connection only if not provided
if not self._external_db:
config = DatabaseConfig(
connection_string=self._connection_string,
min_connections=5,
max_connections=20,
)
self.db = AsyncDatabaseManager(config)
await self.db.connect()
# ALWAYS run migrations (both modes!)
migrations_path = Path(__file__).parent / "migrations"
migrations = AsyncMigrationManager(
self.db,
migrations_path=str(migrations_path),
module_name=f"mylib_{self._schema}", # Unique per schema
)
await migrations.apply_pending_migrations()
async def close(self):
# Only close if we created the connection
if self.db and not self._external_db:
await self.db.disconnect()
# Library methods (always use {{tables.}})
async def create_record(self, data: dict):
return await self.db.fetch_one(
"INSERT INTO {{tables.records}} (data) VALUES ($1) RETURNING *",
data
)
def __init__(self, connection_string=None, db_manager=None):
if not connection_string and not db_manager:
raise ValueError(
"Either connection_string or db_manager required.\n\n"
"Standalone: MyLib(connection_string='postgresql://...')\n"
"Shared pool: MyLib(db_manager=shared_db)"
)
Why: Catch misconfiguration immediately with helpful error message.
self._external_db = db_manager is not None
Why: Determines who owns lifecycle. If True, caller owns pool. If False, we own it.
async def initialize(self):
if not self._external_db:
# We need to create connection
self.db = AsyncDatabaseManager(config)
await self.db.connect()
# ALWAYS run migrations (external or not)
await self._apply_migrations()
Why: Don't create pool if one was provided. But ALWAYS run migrations.
module_name = f"mylib_{self._schema}" # "mylib_schema1", "mylib_schema2"
Why: Same library can be used multiple times with different schemas. Each needs unique migration tracking.
async def close(self):
if self.db and not self._external_db:
await self.db.disconnect()
Why: Only close connections we created. External connections belong to caller.
from mylib import MyLibrary
# User provides connection string
lib = MyLibrary(connection_string="postgresql://localhost/mydb")
await lib.initialize()
# Use library
result = await lib.create_record({"key": "value"})
# Clean up
await lib.close()
from pgdbm import AsyncDatabaseManager, DatabaseConfig
from mylib import MyLibrary
# App creates shared pool
config = DatabaseConfig(connection_string="postgresql://localhost/app")
shared_pool = await AsyncDatabaseManager.create_shared_pool(config)
# Create schema-isolated manager for library
lib_db = AsyncDatabaseManager(pool=shared_pool, schema="mylib")
# Pass to library
lib = MyLibrary(db_manager=lib_db)
await lib.initialize()
# Use library
result = await lib.create_record({"key": "value"})
# Don't call lib.close() - app owns the pool
await shared_pool.close()
# Parent app using multiple dual-mode libraries
from mylib import MyLibrary
from otherlib import OtherLibrary
# One shared pool
pool = await AsyncDatabaseManager.create_shared_pool(config)
# Each library gets own schema
mylib_db = AsyncDatabaseManager(pool=pool, schema="mylib")
otherlib_db = AsyncDatabaseManager(pool=pool, schema="otherlib")
# Initialize both
mylib = MyLibrary(db_manager=mylib_db)
otherlib = OtherLibrary(db_manager=otherlib_db)
await mylib.initialize() # Runs mylib migrations in mylib schema
await otherlib.initialize() # Runs otherlib migrations in otherlib schema
# Libraries can reference each other's data if needed
# (but each owns its own tables in separate schemas)
Always use {{tables.}} syntax:
-- mylib/migrations/001_initial.sql
CREATE TABLE IF NOT EXISTS {{tables.records}} (
id SERIAL PRIMARY KEY,
data JSONB NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS records_created
ON {{tables.records}} (created_at);
Why templates matter:
{{tables.records}} → records{{tables.records}} → "mylib".recordsSame migration file works in both modes.
# WRONG: Forces standalone mode only
def __init__(self, connection_string: str):
self.db = AsyncDatabaseManager(DatabaseConfig(connection_string=connection_string))
Fix: Accept both connection_string and db_manager.
# WRONG: Assumes migrations already run
async def initialize(self):
if self._external_db:
return # Skip migrations!
Fix: ALWAYS run migrations. Your module owns its schema/tables.
# WRONG: Closes external pool
await lib.close() # Disconnects pool owned by parent app!
Fix: Check _external_db flag in close() method.
# WRONG: Only works in one schema
await db.execute("INSERT INTO mylib.records ...")
Fix: Use {{tables.records}} - works in any schema.
# WRONG: Conflicts if library used twice
module_name = "mylib"
Fix: Include schema: module_name = f"mylib_{schema}".
For libraries providing FastAPI apps:
from contextlib import asynccontextmanager
from fastapi import FastAPI
def create_app(
db_manager: Optional[AsyncDatabaseManager] = None,
connection_string: Optional[str] = None,
) -> FastAPI:
"""Create app supporting both modes."""
@asynccontextmanager
async def lifespan(app: FastAPI):
# Initialize library
lib = MyLibrary(
connection_string=connection_string,
db_manager=db_manager,
)
await lib.initialize()
app.state.lib = lib
yield
# Clean up only if we own it
await lib.close()
return FastAPI(lifespan=lifespan)
# Standalone usage
app = create_app(connection_string="postgresql://...")
# Parent app usage
parent = FastAPI()
lib_app = create_app(db_manager=shared_db)
parent.mount("/lib", lib_app)
# tests/conftest.py
import pytest
from mylib import MyLibrary
@pytest.fixture(params=["standalone", "shared"])
async def lib(request, test_db_factory):
"""Test both modes."""
mode = request.param
if mode == "standalone":
lib = MyLibrary(connection_string="postgresql://localhost/test")
else:
db = await test_db_factory.create_db(schema="mylib")
lib = MyLibrary(db_manager=db)
await lib.initialize()
yield lib
await lib.close()
# tests/test_lib.py
async def test_create_record(lib):
# This test runs in BOTH modes
result = await lib.create_record({"key": "value"})
assert result["id"] is not None
mylib/
├── pyproject.toml
├── README.md
├── src/
│ └── mylib/
│ ├── __init__.py # Export MyLibrary
│ ├── core.py # Implementation
│ └── migrations/
│ ├── 001_initial.sql
│ └── 002_indexes.sql
└── tests/
├── conftest.py
└── test_mylib.py
# pyproject.toml
[project]
name = "mylib"
dependencies = ["pgdbm>=0.2.0"]
[project.optional-dependencies]
dev = ["pytest>=7.0", "pytest-asyncio>=0.21"]
Problem: Users have different deployment contexts:
connection_string setupSolution: Dual-mode pattern handles all cases with same code.
Benefits:
Before publishing dual-mode library:
connection_string and db_manager parameters_external_db flag{{tables.}} in all SQLmodule_name including schemafrom typing import Optional
from pathlib import Path
from pgdbm import AsyncDatabaseManager, DatabaseConfig, AsyncMigrationManager
class MyLibrary:
def __init__(self, connection_string=None, db_manager=None):
if not connection_string and not db_manager:
raise ValueError("Either connection_string or db_manager required")
self._external_db = db_manager is not None
self.db = db_manager
self._connection_string = connection_string
async def initialize(self):
if not self._external_db:
config = DatabaseConfig(connection_string=self._connection_string)
self.db = AsyncDatabaseManager(config)
await self.db.connect()
migrations = AsyncMigrationManager(
self.db,
str(Path(__file__).parent / "migrations"),
module_name="mylib"
)
await migrations.apply_pending_migrations()
async def close(self):
if self.db and not self._external_db:
await self.db.disconnect()
That's the complete dual-mode pattern.
pgdbm:choosing-patternpgdbm:using-pgdbmpgdbm:shared-pool-patternpgdbm:testing-database-code