| name | dual-mode-library |
| description | Use when building reusable PyPI package with pgdbm - provides complete pattern for libraries that work standalone or embedded without needing examples |
Dual-Mode Library Pattern: Complete Implementation
Overview
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.
When to Use This
From pgdbm:choosing-pattern skill - use when:
- Publishing to PyPI
- Building internal shared library
- Code will be used by other developers
- Library might be used alongside other pgdbm libraries
Complete Implementation Template
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,
):
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
self._schema = schema or "mylib"
async def initialize(self):
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()
migrations_path = Path(__file__).parent / "migrations"
migrations = AsyncMigrationManager(
self.db,
migrations_path=str(migrations_path),
module_name=f"mylib_{self._schema}",
)
await migrations.apply_pending_migrations()
async def close(self):
if self.db and not self._external_db:
await self.db.disconnect()
async def create_record(self, data: dict):
return await self.db.fetch_one(
"INSERT INTO {{tables.records}} (data) VALUES ($1) RETURNING *",
data
)
Critical Implementation Details
1. Parameter Validation
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.
2. Track Ownership
self._external_db = db_manager is not None
Why: Determines who owns lifecycle. If True, caller owns pool. If False, we own it.
3. Conditional Connection Creation
async def initialize(self):
if not self._external_db:
self.db = AsyncDatabaseManager(config)
await self.db.connect()
await self._apply_migrations()
Why: Don't create pool if one was provided. But ALWAYS run migrations.
4. Unique Module Name Per Schema
module_name = f"mylib_{self._schema}"
Why: Same library can be used multiple times with different schemas. Each needs unique migration tracking.
5. Conditional Cleanup
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.
Usage Patterns
Standalone Mode
from mylib import MyLibrary
lib = MyLibrary(connection_string="postgresql://localhost/mydb")
await lib.initialize()
result = await lib.create_record({"key": "value"})
await lib.close()
Shared Pool Mode
from pgdbm import AsyncDatabaseManager, DatabaseConfig
from mylib import MyLibrary
config = DatabaseConfig(connection_string="postgresql://localhost/app")
shared_pool = await AsyncDatabaseManager.create_shared_pool(config)
lib_db = AsyncDatabaseManager(pool=shared_pool, schema="mylib")
lib = MyLibrary(db_manager=lib_db)
await lib.initialize()
result = await lib.create_record({"key": "value"})
await shared_pool.close()
Multi-Library Composition
from mylib import MyLibrary
from otherlib import OtherLibrary
pool = await AsyncDatabaseManager.create_shared_pool(config)
mylib_db = AsyncDatabaseManager(pool=pool, schema="mylib")
otherlib_db = AsyncDatabaseManager(pool=pool, schema="otherlib")
mylib = MyLibrary(db_manager=mylib_db)
otherlib = OtherLibrary(db_manager=otherlib_db)
await mylib.initialize()
await otherlib.initialize()
Migration Files
Always use {{tables.}} syntax:
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:
- Standalone mode:
{{tables.records}} → records
- Shared pool with schema:
{{tables.records}} → "mylib".records
Same migration file works in both modes.
Common Mistakes
❌ Not Supporting db_manager Parameter
def __init__(self, connection_string: str):
self.db = AsyncDatabaseManager(DatabaseConfig(connection_string=connection_string))
Fix: Accept both connection_string and db_manager.
❌ Skipping Migrations in Shared Mode
async def initialize(self):
if self._external_db:
return
Fix: ALWAYS run migrations. Your module owns its schema/tables.
❌ Always Calling close()
await lib.close()
Fix: Check _external_db flag in close() method.
❌ Hardcoding Schema/Table Names
await db.execute("INSERT INTO mylib.records ...")
Fix: Use {{tables.records}} - works in any schema.
❌ Not Including Schema in module_name
module_name = "mylib"
Fix: Include schema: module_name = f"mylib_{schema}".
FastAPI Integration
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):
lib = MyLibrary(
connection_string=connection_string,
db_manager=db_manager,
)
await lib.initialize()
app.state.lib = lib
yield
await lib.close()
return FastAPI(lifespan=lifespan)
app = create_app(connection_string="postgresql://...")
parent = FastAPI()
lib_app = create_app(db_manager=shared_db)
parent.mount("/lib", lib_app)
Testing Both Modes
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()
async def test_create_record(lib):
result = await lib.create_record({"key": "value"})
assert result["id"] is not None
Package Structure for PyPI
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
[project]
name = "mylib"
dependencies = ["pgdbm>=0.2.0"]
[project.optional-dependencies]
dev = ["pytest>=7.0", "pytest-asyncio>=0.21"]
Why This Pattern Exists
Problem: Users have different deployment contexts:
- Solo developer: wants simple
connection_string setup
- Enterprise app: has shared pool, needs schema isolation
- Microservices: each service creates own pool
- Multi-tenant: needs dynamic schema creation
Solution: Dual-mode pattern handles all cases with same code.
Benefits:
- Library works in any context
- No forced architecture decisions
- Efficient resource sharing when possible
- Complete isolation when needed
Checklist
Before publishing dual-mode library:
Complete Minimal Example
from 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.
Related Skills
- For pattern selection:
pgdbm:choosing-pattern
- For mental model:
pgdbm:using-pgdbm
- For shared pool usage:
pgdbm:shared-pool-pattern
- For testing:
pgdbm:testing-database-code