From pgdbm
Use when building reusable PyPI package with pgdbm - provides complete pattern for libraries that work standalone or embedded without needing examples
How this skill is triggered — by the user, by Claude, or both
Slash command
/pgdbm:dual-mode-libraryThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
**Core Principle:** Accept connection_string OR db_manager, always run own migrations, clean up conditionally.
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-codenpx claudepluginhub juanre/ai-tools --plugin pgdbmProvides SQLite patterns for Python projects: WAL mode connections, context managers, async aiosqlite ops, migrations, gotchas, CLI. For local DB state, caching, concurrency.
Provides SQLAlchemy 2.0 patterns for model definitions, sync/async engines, relationships, query optimization, repository pattern, and transactions in Python apps.
Async SQLAlchemy 2.0+ database patterns for FastAPI including session management, connection pooling, Alembic migrations, relationship loading strategies, and query optimization. Use when implementing database models, configuring async sessions, setting up migrations, optimizing queries, managing relationships, or when user mentions SQLAlchemy, async database, ORM, Alembic, database performance, or connection pooling.