pgsqlasync2fast-fastapi 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ """
2
+ pgsqlasync2fast-fastapi - Simple and fast PostgreSQL async module for FastAPI
3
+
4
+ A comprehensive PostgreSQL async module for FastAPI with multi-database support,
5
+ automatic database creation, and Pydantic settings configuration.
6
+
7
+ Features:
8
+ - Multiple database connection support
9
+ - Async database operations with SQLAlchemy
10
+ - Database creation utilities with superuser support
11
+ - Connection pooling and health checks
12
+ - FastAPI integration with dependencies
13
+ - Lazy engine creation for optimal resource usage
14
+ """
15
+
16
+ from .__version__ import __version__
17
+ from .connection import DatabaseManager, get_manager
18
+ from .database import create_database, database_exists, drop_database, list_databases
19
+ from .dependencies import (
20
+ get_db_engine,
21
+ get_db_manager,
22
+ get_db_session,
23
+ shutdown_database,
24
+ startup_database,
25
+ )
26
+ from .settings import DatabaseConnectionSettings, DatabaseSettings, settings
27
+
28
+ __all__ = [
29
+ # Version
30
+ "__version__",
31
+ # Main classes
32
+ "DatabaseManager",
33
+ "get_manager",
34
+ # Settings
35
+ "DatabaseSettings",
36
+ "DatabaseConnectionSettings",
37
+ "settings",
38
+ # FastAPI dependencies
39
+ "get_db_manager",
40
+ "get_db_engine",
41
+ "get_db_session",
42
+ "startup_database",
43
+ "shutdown_database",
44
+ # Database utilities
45
+ "database_exists",
46
+ "create_database",
47
+ "drop_database",
48
+ "list_databases",
49
+ ]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,196 @@
1
+ """
2
+ Database connection and engine management
3
+ """
4
+
5
+ from typing import AsyncGenerator, Dict, Optional
6
+
7
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
8
+ from sqlalchemy.ext.asyncio import async_sessionmaker
9
+ from sqlalchemy import text
10
+
11
+ from .settings import DatabaseSettings, settings
12
+
13
+
14
+ class DatabaseManager:
15
+ """
16
+ Manages multiple async database engines and sessions.
17
+
18
+ This class provides:
19
+ - Lazy engine creation (engines created only when first accessed)
20
+ - Multiple named connections support
21
+ - Session factory for each connection
22
+ - Connection health checks
23
+ - Cleanup utilities
24
+ """
25
+
26
+ def __init__(self, config: DatabaseSettings):
27
+ """
28
+ Initialize the database manager.
29
+
30
+ Args:
31
+ config: Database settings configuration
32
+ """
33
+ self.config = config
34
+ self._engines: Dict[str, AsyncEngine] = {}
35
+ self._session_makers: Dict[str, async_sessionmaker[AsyncSession]] = {}
36
+
37
+ def get_engine(self, connection_name: Optional[str] = None) -> AsyncEngine:
38
+ """
39
+ Get or create an async engine for a connection.
40
+
41
+ Args:
42
+ connection_name: Name of the connection. If None, uses default_connection.
43
+
44
+ Returns:
45
+ AsyncEngine for the requested connection.
46
+
47
+ Raises:
48
+ ValueError: If connection doesn't exist.
49
+ """
50
+ name = connection_name or self.config.default_connection
51
+
52
+ # Return existing engine if already created
53
+ if name in self._engines:
54
+ return self._engines[name]
55
+
56
+ # Get connection settings
57
+ conn_settings = self.config.get_connection(name)
58
+
59
+ # Build connection URL
60
+ url = self.config.get_connection_url(name)
61
+
62
+ # Determine echo mode (connection-specific overrides global)
63
+ echo = conn_settings.echo if conn_settings.echo else self.config.echo
64
+
65
+ # Create engine with pool settings
66
+ engine = create_async_engine(
67
+ url,
68
+ echo=echo,
69
+ pool_size=conn_settings.pool_size,
70
+ max_overflow=conn_settings.max_overflow,
71
+ pool_timeout=conn_settings.pool_timeout,
72
+ pool_recycle=conn_settings.pool_recycle,
73
+ pool_pre_ping=True, # Verify connections before using them
74
+ )
75
+
76
+ # Store engine
77
+ self._engines[name] = engine
78
+
79
+ # Create session maker for this engine
80
+ self._session_makers[name] = async_sessionmaker(
81
+ engine,
82
+ class_=AsyncSession,
83
+ expire_on_commit=False,
84
+ )
85
+
86
+ return engine
87
+
88
+ def get_session_maker(self, connection_name: Optional[str] = None) -> async_sessionmaker[AsyncSession]:
89
+ """
90
+ Get session maker for a connection.
91
+
92
+ Args:
93
+ connection_name: Name of the connection. If None, uses default_connection.
94
+
95
+ Returns:
96
+ Session maker for the requested connection.
97
+ """
98
+ name = connection_name or self.config.default_connection
99
+
100
+ # Ensure engine and session maker exist
101
+ if name not in self._session_makers:
102
+ self.get_engine(name)
103
+
104
+ return self._session_makers[name]
105
+
106
+ async def get_session(self, connection_name: Optional[str] = None) -> AsyncSession:
107
+ """
108
+ Create a new async session for a connection.
109
+
110
+ Args:
111
+ connection_name: Name of the connection. If None, uses default_connection.
112
+
113
+ Returns:
114
+ New AsyncSession instance.
115
+
116
+ Note:
117
+ Remember to close the session when done, or use it in an async context manager.
118
+ """
119
+ session_maker = self.get_session_maker(connection_name)
120
+ return session_maker()
121
+
122
+ async def close_all(self) -> None:
123
+ """
124
+ Dispose all engines and close all connections.
125
+
126
+ This should be called on application shutdown.
127
+ """
128
+ for engine in self._engines.values():
129
+ await engine.dispose()
130
+
131
+ self._engines.clear()
132
+ self._session_makers.clear()
133
+
134
+ async def health_check(self, connection_name: Optional[str] = None) -> bool:
135
+ """
136
+ Check if a database connection is healthy.
137
+
138
+ Args:
139
+ connection_name: Name of the connection. If None, uses default_connection.
140
+
141
+ Returns:
142
+ True if connection is healthy, False otherwise.
143
+ """
144
+ try:
145
+ engine = self.get_engine(connection_name)
146
+ async with engine.connect() as conn:
147
+ await conn.execute(text("SELECT 1"))
148
+ return True
149
+ except Exception as e:
150
+ print(f"Health check failed for connection '{connection_name}': {e}")
151
+ return False
152
+
153
+ def list_connections(self) -> list[str]:
154
+ """
155
+ List all configured connection names.
156
+
157
+ Returns:
158
+ List of connection names.
159
+ """
160
+ return list(self.config.connections.keys())
161
+
162
+ def is_superuser_connection(self, connection_name: Optional[str] = None) -> bool:
163
+ """
164
+ Check if a connection has superuser privileges.
165
+
166
+ Args:
167
+ connection_name: Name of the connection. If None, uses default_connection.
168
+
169
+ Returns:
170
+ True if connection has superuser privileges.
171
+ """
172
+ conn = self.config.get_connection(connection_name)
173
+ return conn.is_superuser
174
+
175
+
176
+ # Global singleton instance
177
+ _manager: Optional[DatabaseManager] = None
178
+
179
+
180
+ def get_manager(config: Optional[DatabaseSettings] = None) -> DatabaseManager:
181
+ """
182
+ Get the global DatabaseManager singleton.
183
+
184
+ Args:
185
+ config: Optional database settings. If not provided, uses global settings.
186
+
187
+ Returns:
188
+ DatabaseManager singleton instance.
189
+ """
190
+ global _manager
191
+
192
+ if _manager is None:
193
+ cfg = config or settings
194
+ _manager = DatabaseManager(cfg)
195
+
196
+ return _manager
@@ -0,0 +1,262 @@
1
+ """
2
+ Database creation and management utilities
3
+ """
4
+
5
+ from typing import List, Optional
6
+
7
+ from sqlalchemy import text
8
+ from sqlalchemy.ext.asyncio import AsyncEngine
9
+
10
+ from .connection import get_manager
11
+ from .settings import settings
12
+
13
+
14
+ async def database_exists(
15
+ database_name: str,
16
+ connection_name: Optional[str] = None
17
+ ) -> bool:
18
+ """
19
+ Check if a database exists.
20
+
21
+ Args:
22
+ database_name: Name of the database to check.
23
+ connection_name: Name of the superuser connection to use.
24
+ If None, uses the first connection with is_superuser=True.
25
+
26
+ Returns:
27
+ True if database exists, False otherwise.
28
+
29
+ Raises:
30
+ ValueError: If no superuser connection is available.
31
+
32
+ Example:
33
+ exists = await database_exists("my_new_db")
34
+ if not exists:
35
+ await create_database("my_new_db")
36
+ """
37
+ manager = get_manager()
38
+
39
+ # Find superuser connection
40
+ if connection_name is None:
41
+ connection_name = settings.get_superuser_connection_name()
42
+ if connection_name is None:
43
+ raise ValueError(
44
+ "No superuser connection available. "
45
+ "Please configure a connection with is_superuser=true"
46
+ )
47
+
48
+ # Verify the connection has superuser privileges
49
+ if not manager.is_superuser_connection(connection_name):
50
+ raise ValueError(
51
+ f"Connection '{connection_name}' does not have superuser privileges. "
52
+ f"Set is_superuser=true in the connection configuration."
53
+ )
54
+
55
+ engine = manager.get_engine(connection_name)
56
+
57
+ async with engine.connect() as conn:
58
+ # Use isolation_level to allow database operations
59
+ await conn.execution_options(isolation_level="AUTOCOMMIT")
60
+
61
+ result = await conn.execute(
62
+ text("SELECT 1 FROM pg_database WHERE datname = :dbname"),
63
+ {"dbname": database_name}
64
+ )
65
+ return result.scalar() is not None
66
+
67
+
68
+ async def create_database(
69
+ database_name: str,
70
+ owner: Optional[str] = None,
71
+ connection_name: Optional[str] = None
72
+ ) -> bool:
73
+ """
74
+ Create a new database.
75
+
76
+ Args:
77
+ database_name: Name of the database to create.
78
+ owner: Optional owner username for the database.
79
+ connection_name: Name of the superuser connection to use.
80
+ If None, uses the first connection with is_superuser=True.
81
+
82
+ Returns:
83
+ True if database was created, False if it already exists.
84
+
85
+ Raises:
86
+ ValueError: If no superuser connection is available.
87
+ Exception: If database creation fails.
88
+
89
+ Example:
90
+ created = await create_database("my_new_db", owner="myuser")
91
+ if created:
92
+ print("Database created successfully!")
93
+ """
94
+ manager = get_manager()
95
+
96
+ # Find superuser connection
97
+ if connection_name is None:
98
+ connection_name = settings.get_superuser_connection_name()
99
+ if connection_name is None:
100
+ raise ValueError(
101
+ "No superuser connection available. "
102
+ "Please configure a connection with is_superuser=true"
103
+ )
104
+
105
+ # Verify the connection has superuser privileges
106
+ if not manager.is_superuser_connection(connection_name):
107
+ raise ValueError(
108
+ f"Connection '{connection_name}' does not have superuser privileges. "
109
+ f"Set is_superuser=true in the connection configuration."
110
+ )
111
+
112
+ # Check if database already exists
113
+ if await database_exists(database_name, connection_name):
114
+ print(f"⚠️ Database '{database_name}' already exists")
115
+ return False
116
+
117
+ engine = manager.get_engine(connection_name)
118
+
119
+ async with engine.connect() as conn:
120
+ # Use AUTOCOMMIT isolation level for CREATE DATABASE
121
+ await conn.execution_options(isolation_level="AUTOCOMMIT")
122
+
123
+ # Build CREATE DATABASE query
124
+ if owner:
125
+ query = text(f'CREATE DATABASE "{database_name}" OWNER "{owner}"')
126
+ else:
127
+ query = text(f'CREATE DATABASE "{database_name}"')
128
+
129
+ await conn.execute(query)
130
+ print(f"✅ Database '{database_name}' created successfully")
131
+ return True
132
+
133
+
134
+ async def drop_database(
135
+ database_name: str,
136
+ connection_name: Optional[str] = None,
137
+ force: bool = False
138
+ ) -> bool:
139
+ """
140
+ Drop a database.
141
+
142
+ Args:
143
+ database_name: Name of the database to drop.
144
+ connection_name: Name of the superuser connection to use.
145
+ If None, uses the first connection with is_superuser=True.
146
+ force: If True, terminates all connections to the database before dropping.
147
+
148
+ Returns:
149
+ True if database was dropped, False if it doesn't exist.
150
+
151
+ Raises:
152
+ ValueError: If no superuser connection is available or attempting to drop
153
+ a protected database (postgres, template0, template1).
154
+ Exception: If database drop fails.
155
+
156
+ Warning:
157
+ This operation is irreversible! Use with caution.
158
+
159
+ Example:
160
+ dropped = await drop_database("old_db", force=True)
161
+ if dropped:
162
+ print("Database dropped successfully!")
163
+ """
164
+ # Safety check: prevent dropping system databases
165
+ protected_databases = ["postgres", "template0", "template1"]
166
+ if database_name in protected_databases:
167
+ raise ValueError(
168
+ f"Cannot drop protected database '{database_name}'. "
169
+ f"Protected databases: {', '.join(protected_databases)}"
170
+ )
171
+
172
+ manager = get_manager()
173
+
174
+ # Find superuser connection
175
+ if connection_name is None:
176
+ connection_name = settings.get_superuser_connection_name()
177
+ if connection_name is None:
178
+ raise ValueError(
179
+ "No superuser connection available. "
180
+ "Please configure a connection with is_superuser=true"
181
+ )
182
+
183
+ # Verify the connection has superuser privileges
184
+ if not manager.is_superuser_connection(connection_name):
185
+ raise ValueError(
186
+ f"Connection '{connection_name}' does not have superuser privileges. "
187
+ f"Set is_superuser=true in the connection configuration."
188
+ )
189
+
190
+ # Check if database exists
191
+ if not await database_exists(database_name, connection_name):
192
+ print(f"⚠️ Database '{database_name}' does not exist")
193
+ return False
194
+
195
+ engine = manager.get_engine(connection_name)
196
+
197
+ async with engine.connect() as conn:
198
+ # Use AUTOCOMMIT isolation level for DROP DATABASE
199
+ await conn.execution_options(isolation_level="AUTOCOMMIT")
200
+
201
+ # Terminate existing connections if force=True
202
+ if force:
203
+ await conn.execute(
204
+ text(
205
+ "SELECT pg_terminate_backend(pg_stat_activity.pid) "
206
+ "FROM pg_stat_activity "
207
+ "WHERE pg_stat_activity.datname = :dbname "
208
+ "AND pid <> pg_backend_pid()"
209
+ ),
210
+ {"dbname": database_name}
211
+ )
212
+
213
+ # Drop the database
214
+ await conn.execute(text(f'DROP DATABASE "{database_name}"'))
215
+ print(f"✅ Database '{database_name}' dropped successfully")
216
+ return True
217
+
218
+
219
+ async def list_databases(connection_name: Optional[str] = None) -> List[str]:
220
+ """
221
+ List all databases.
222
+
223
+ Args:
224
+ connection_name: Name of the superuser connection to use.
225
+ If None, uses the first connection with is_superuser=True.
226
+
227
+ Returns:
228
+ List of database names.
229
+
230
+ Raises:
231
+ ValueError: If no superuser connection is available.
232
+
233
+ Example:
234
+ databases = await list_databases()
235
+ for db in databases:
236
+ print(f" - {db}")
237
+ """
238
+ manager = get_manager()
239
+
240
+ # Find superuser connection
241
+ if connection_name is None:
242
+ connection_name = settings.get_superuser_connection_name()
243
+ if connection_name is None:
244
+ raise ValueError(
245
+ "No superuser connection available. "
246
+ "Please configure a connection with is_superuser=true"
247
+ )
248
+
249
+ # Verify the connection has superuser privileges
250
+ if not manager.is_superuser_connection(connection_name):
251
+ raise ValueError(
252
+ f"Connection '{connection_name}' does not have superuser privileges. "
253
+ f"Set is_superuser=true in the connection configuration."
254
+ )
255
+
256
+ engine = manager.get_engine(connection_name)
257
+
258
+ async with engine.connect() as conn:
259
+ result = await conn.execute(
260
+ text("SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname")
261
+ )
262
+ return [row[0] for row in result.fetchall()]
@@ -0,0 +1,147 @@
1
+ """
2
+ FastAPI dependencies for database functionality
3
+ """
4
+
5
+ from typing import AsyncGenerator, Optional
6
+
7
+ from fastapi import Depends
8
+ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession
9
+
10
+ from .connection import DatabaseManager, get_manager
11
+ from .settings import DatabaseSettings, settings
12
+
13
+
14
+ def get_db_manager(config: DatabaseSettings = Depends(lambda: settings)) -> DatabaseManager:
15
+ """
16
+ FastAPI dependency for DatabaseManager.
17
+
18
+ Returns singleton instance.
19
+
20
+ Usage:
21
+ @app.get("/items")
22
+ async def get_items(manager: DatabaseManager = Depends(get_db_manager)):
23
+ engine = manager.get_engine("default")
24
+ ...
25
+ """
26
+ return get_manager(config)
27
+
28
+
29
+ def get_db_engine(
30
+ connection_name: str = "default",
31
+ manager: DatabaseManager = Depends(get_db_manager)
32
+ ) -> AsyncEngine:
33
+ """
34
+ FastAPI dependency for AsyncEngine.
35
+
36
+ Args:
37
+ connection_name: Name of the database connection to use.
38
+ manager: DatabaseManager instance (injected).
39
+
40
+ Returns:
41
+ AsyncEngine for the specified connection.
42
+
43
+ Usage:
44
+ @app.get("/items")
45
+ async def get_items(engine: AsyncEngine = Depends(get_db_engine)):
46
+ async with engine.connect() as conn:
47
+ result = await conn.execute(text("SELECT * FROM items"))
48
+ ...
49
+ """
50
+ return manager.get_engine(connection_name)
51
+
52
+
53
+ async def get_db_session(
54
+ connection_name: str = "default",
55
+ manager: DatabaseManager = Depends(get_db_manager)
56
+ ) -> AsyncGenerator[AsyncSession, None]:
57
+ """
58
+ FastAPI dependency for AsyncSession with automatic commit/rollback.
59
+
60
+ Args:
61
+ connection_name: Name of the database connection to use.
62
+ manager: DatabaseManager instance (injected).
63
+
64
+ Yields:
65
+ AsyncSession for the specified connection.
66
+
67
+ Usage:
68
+ @app.get("/items")
69
+ async def get_items(session: AsyncSession = Depends(get_db_session)):
70
+ result = await session.execute(text("SELECT * FROM items"))
71
+ items = result.scalars().all()
72
+ return items
73
+
74
+ # Or with a specific connection:
75
+ from functools import partial
76
+
77
+ get_business_session = partial(get_db_session, connection_name="business")
78
+
79
+ @app.get("/business/items")
80
+ async def get_business_items(session: AsyncSession = Depends(get_business_session)):
81
+ ...
82
+ """
83
+ session = await manager.get_session(connection_name)
84
+ try:
85
+ yield session
86
+ await session.commit()
87
+ except Exception:
88
+ await session.rollback()
89
+ raise
90
+ finally:
91
+ await session.close()
92
+
93
+
94
+ async def startup_database(config: Optional[DatabaseSettings] = None) -> DatabaseManager:
95
+ """
96
+ Initialize database connections on application startup.
97
+
98
+ Usage in FastAPI:
99
+ @app.on_event("startup")
100
+ async def startup():
101
+ await startup_database()
102
+
103
+ Or with lifespan (FastAPI 0.93+):
104
+ from contextlib import asynccontextmanager
105
+
106
+ @asynccontextmanager
107
+ async def lifespan(app: FastAPI):
108
+ # Startup
109
+ await startup_database()
110
+ yield
111
+ # Shutdown
112
+ await shutdown_database()
113
+
114
+ app = FastAPI(lifespan=lifespan)
115
+
116
+ Args:
117
+ config: Database settings (uses global settings if not provided)
118
+
119
+ Returns:
120
+ Initialized DatabaseManager instance
121
+ """
122
+ manager = get_manager(config)
123
+
124
+ # Optionally perform health checks on all connections
125
+ print("🔌 Initializing database connections...")
126
+ for conn_name in manager.list_connections():
127
+ is_healthy = await manager.health_check(conn_name)
128
+ status = "✅" if is_healthy else "❌"
129
+ superuser = " (superuser)" if manager.is_superuser_connection(conn_name) else ""
130
+ print(f" {status} Connection '{conn_name}'{superuser}")
131
+
132
+ return manager
133
+
134
+
135
+ async def shutdown_database() -> None:
136
+ """
137
+ Close all database connections on application shutdown.
138
+
139
+ Usage in FastAPI:
140
+ @app.on_event("shutdown")
141
+ async def shutdown():
142
+ await shutdown_database()
143
+ """
144
+ manager = get_manager()
145
+ print("🔌 Closing database connections...")
146
+ await manager.close_all()
147
+ print(" ✅ All connections closed")
@@ -0,0 +1,198 @@
1
+ """
2
+ Settings module for pgsqlasync2fast-fastapi
3
+ Handles configuration using Pydantic Settings with environment variables
4
+ """
5
+
6
+ import os
7
+ from typing import Dict, Optional
8
+
9
+ from pydantic import BaseModel, Field, SecretStr, field_validator
10
+ from pydantic_settings import BaseSettings, SettingsConfigDict
11
+
12
+ # Look for .env in the current working directory (where the app is running)
13
+ DOTENV_PATH = os.path.join(os.getcwd(), ".env")
14
+
15
+
16
+ class DatabaseConnectionSettings(BaseModel):
17
+ """Configuration for a single database connection."""
18
+
19
+ host: str = Field(..., description="Database server host")
20
+ port: int = Field(default=5432, description="Database server port")
21
+ username: str = Field(..., description="Database username")
22
+ password: SecretStr = Field(..., description="Database password")
23
+ database: str = Field(..., description="Database name")
24
+
25
+ # Superuser flag
26
+ is_superuser: bool = Field(
27
+ default=False,
28
+ description="Whether this connection has superuser privileges for database creation"
29
+ )
30
+
31
+ # Connection pool settings
32
+ pool_size: int = Field(
33
+ default=5,
34
+ description="Number of connections to maintain in the pool"
35
+ )
36
+ max_overflow: int = Field(
37
+ default=10,
38
+ description="Maximum number of connections that can be created beyond pool_size"
39
+ )
40
+ pool_timeout: int = Field(
41
+ default=30,
42
+ description="Timeout in seconds for getting a connection from the pool"
43
+ )
44
+ pool_recycle: int = Field(
45
+ default=3600,
46
+ description="Recycle connections after this many seconds (prevents stale connections)"
47
+ )
48
+
49
+ # SQLAlchemy settings
50
+ echo: bool = Field(
51
+ default=False,
52
+ description="Enable SQLAlchemy echo mode for this connection"
53
+ )
54
+
55
+ @field_validator("port")
56
+ @classmethod
57
+ def validate_port(cls, v: int) -> int:
58
+ """Validate port is in valid range."""
59
+ if not 1 <= v <= 65535:
60
+ raise ValueError("Port must be between 1 and 65535")
61
+ return v
62
+
63
+ @field_validator("pool_size")
64
+ @classmethod
65
+ def validate_pool_size(cls, v: int) -> int:
66
+ """Validate pool size is positive."""
67
+ if v <= 0:
68
+ raise ValueError("pool_size must be greater than 0")
69
+ return v
70
+
71
+ @field_validator("max_overflow")
72
+ @classmethod
73
+ def validate_max_overflow(cls, v: int) -> int:
74
+ """Validate max overflow is non-negative."""
75
+ if v < 0:
76
+ raise ValueError("max_overflow must be non-negative")
77
+ return v
78
+
79
+
80
+ class DatabaseSettings(BaseSettings):
81
+ """Main database configuration."""
82
+
83
+ # Multiple named database connections
84
+ connections: Dict[str, DatabaseConnectionSettings] = Field(
85
+ default_factory=dict,
86
+ description="Dictionary of named database connections (e.g., 'default', 'business')",
87
+ )
88
+
89
+ # Default connection to use when none is specified
90
+ default_connection: str = Field(
91
+ default="default",
92
+ description="Name of default database connection to use"
93
+ )
94
+
95
+ # Global SQLAlchemy settings
96
+ echo: bool = Field(
97
+ default=False,
98
+ description="Global SQLAlchemy echo mode (can be overridden per connection)"
99
+ )
100
+
101
+ model_config = SettingsConfigDict(
102
+ env_file=DOTENV_PATH,
103
+ env_file_encoding="utf-8",
104
+ env_prefix="DB_",
105
+ env_nested_delimiter="__",
106
+ extra="ignore",
107
+ )
108
+
109
+ def get_connection(self, connection_name: Optional[str] = None) -> DatabaseConnectionSettings:
110
+ """
111
+ Get database connection configuration by name.
112
+
113
+ Args:
114
+ connection_name: Name of the connection. If None, uses default_connection.
115
+
116
+ Returns:
117
+ DatabaseConnectionSettings for the requested connection.
118
+
119
+ Raises:
120
+ ValueError: If connection doesn't exist.
121
+ """
122
+ name = connection_name or self.default_connection
123
+
124
+ if name not in self.connections:
125
+ available = ", ".join(self.connections.keys()) if self.connections else "none"
126
+ raise ValueError(
127
+ f"Database connection '{name}' not found. Available connections: {available}"
128
+ )
129
+
130
+ return self.connections[name]
131
+
132
+ def has_connection(self, connection_name: str) -> bool:
133
+ """Check if a database connection exists."""
134
+ return connection_name in self.connections
135
+
136
+ def get_connection_url(self, connection_name: Optional[str] = None) -> str:
137
+ """
138
+ Get the database URL for a connection.
139
+
140
+ Args:
141
+ connection_name: Name of the connection. If None, uses default_connection.
142
+
143
+ Returns:
144
+ PostgreSQL async connection URL.
145
+ """
146
+ conn = self.get_connection(connection_name)
147
+ return (
148
+ f"postgresql+asyncpg://{conn.username}:{conn.password.get_secret_value()}"
149
+ f"@{conn.host}:{conn.port}/{conn.database}"
150
+ )
151
+
152
+ def get_superuser_connection(self) -> Optional[DatabaseConnectionSettings]:
153
+ """
154
+ Get the first connection with superuser privileges.
155
+
156
+ Returns:
157
+ DatabaseConnectionSettings with is_superuser=True, or None if not found.
158
+ """
159
+ for conn in self.connections.values():
160
+ if conn.is_superuser:
161
+ return conn
162
+ return None
163
+
164
+ def get_superuser_connection_name(self) -> Optional[str]:
165
+ """
166
+ Get the name of the first connection with superuser privileges.
167
+
168
+ Returns:
169
+ Name of connection with is_superuser=True, or None if not found.
170
+ """
171
+ for name, conn in self.connections.items():
172
+ if conn.is_superuser:
173
+ return name
174
+ return None
175
+
176
+
177
+ # Initialize settings with error handling
178
+ try:
179
+ settings = DatabaseSettings()
180
+
181
+ # Validate that at least one connection is configured
182
+ if not settings.connections:
183
+ print("⚠️ Warning: No database connections configured. Please add at least one connection.")
184
+ print(" Example: DB_CONNECTIONS__DEFAULT__HOST=localhost")
185
+ print(" Example: DB_CONNECTIONS__DEFAULT__USERNAME=myuser")
186
+ print(" Example: DB_CONNECTIONS__DEFAULT__PASSWORD=mypassword")
187
+ print(" Example: DB_CONNECTIONS__DEFAULT__DATABASE=mydb")
188
+
189
+ except Exception as e:
190
+ import traceback
191
+
192
+ print("🚨 Error loading database configuration:")
193
+ print(e)
194
+ traceback.print_exc()
195
+
196
+ # Fallback to minimal configuration
197
+ settings = DatabaseSettings()
198
+ print("⚠️ Using fallback database configuration (no connections)")
@@ -0,0 +1,311 @@
1
+ Metadata-Version: 2.4
2
+ Name: pgsqlasync2fast-fastapi
3
+ Version: 0.1.0
4
+ Summary: Simple and fast PostgreSQL async module for FastAPI with multi-database support
5
+ Author-email: Angel Daniel Sanchez Castillo <angeldaniel.sanchezcastillo@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Angel Daniel Sanchez Castillo
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/AngelDanielSanchezCastillo/pgsqlasync2fast-fastapi
29
+ Project-URL: Documentation, https://github.com/AngelDanielSanchezCastillo/pgsqlasync2fast-fastapi/tree/main/docs
30
+ Project-URL: Repository, https://github.com/AngelDanielSanchezCastillo/pgsqlasync2fast-fastapi
31
+ Project-URL: Issues, https://github.com/AngelDanielSanchezCastillo/pgsqlasync2fast-fastapi/issues
32
+ Keywords: fastapi,postgresql,async,sqlalchemy,database,multi-database
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: License :: OSI Approved :: MIT License
36
+ Classifier: Programming Language :: Python :: 3
37
+ Classifier: Programming Language :: Python :: 3.10
38
+ Classifier: Programming Language :: Python :: 3.11
39
+ Classifier: Programming Language :: Python :: 3.12
40
+ Classifier: Framework :: FastAPI
41
+ Classifier: Topic :: Database
42
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
43
+ Requires-Python: >=3.10
44
+ Description-Content-Type: text/markdown
45
+ License-File: LICENSE
46
+ Requires-Dist: fastapi>=0.100.0
47
+ Requires-Dist: pydantic>=2.0.0
48
+ Requires-Dist: pydantic-settings>=2.0.0
49
+ Requires-Dist: sqlalchemy>=2.0.0
50
+ Requires-Dist: asyncpg>=0.29.0
51
+ Provides-Extra: dev
52
+ Requires-Dist: pytest>=7.0.0; extra == "dev"
53
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
54
+ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
55
+ Requires-Dist: httpx>=0.24.0; extra == "dev"
56
+ Requires-Dist: mypy>=1.0.0; extra == "dev"
57
+ Dynamic: license-file
58
+
59
+ # pgsqlasync2fast-fastapi
60
+
61
+ Simple and fast PostgreSQL async module for FastAPI with multi-database support and automatic database creation.
62
+
63
+ ## Features
64
+
65
+ - ✅ **Multiple Database Connections**: Configure and manage multiple PostgreSQL databases
66
+ - ✅ **Async Support**: Built on SQLAlchemy 2.0+ async engine with asyncpg
67
+ - ✅ **Database Creation**: Automatic database creation with superuser support
68
+ - ✅ **FastAPI Integration**: Ready-to-use dependencies for seamless FastAPI integration
69
+ - ✅ **Connection Pooling**: Configurable connection pools per database
70
+ - ✅ **Health Checks**: Built-in connection health monitoring
71
+ - ✅ **Lazy Loading**: Engines created only when needed
72
+ - ✅ **Type Safe**: Full Pydantic settings with validation
73
+
74
+ ## Installation
75
+
76
+ ```bash
77
+ pip install pgsqlasync2fast-fastapi
78
+ ```
79
+
80
+ ## Quick Start
81
+
82
+ ### 1. Configure your databases in `.env`
83
+
84
+ ```env
85
+ # Default database connection
86
+ DB_CONNECTIONS__DEFAULT__HOST=localhost
87
+ DB_CONNECTIONS__DEFAULT__PORT=5432
88
+ DB_CONNECTIONS__DEFAULT__USERNAME=myuser
89
+ DB_CONNECTIONS__DEFAULT__PASSWORD=mypassword
90
+ DB_CONNECTIONS__DEFAULT__DATABASE=mydb
91
+
92
+ # Business database connection
93
+ DB_CONNECTIONS__BUSINESS__HOST=localhost
94
+ DB_CONNECTIONS__BUSINESS__PORT=5432
95
+ DB_CONNECTIONS__BUSINESS__USERNAME=business_user
96
+ DB_CONNECTIONS__BUSINESS__PASSWORD=business_password
97
+ DB_CONNECTIONS__BUSINESS__DATABASE=business_db
98
+
99
+ # Admin connection with superuser privileges
100
+ DB_CONNECTIONS__ADMIN__HOST=localhost
101
+ DB_CONNECTIONS__ADMIN__PORT=5432
102
+ DB_CONNECTIONS__ADMIN__USERNAME=postgres
103
+ DB_CONNECTIONS__ADMIN__PASSWORD=postgres_password
104
+ DB_CONNECTIONS__ADMIN__DATABASE=postgres
105
+ DB_CONNECTIONS__ADMIN__IS_SUPERUSER=true
106
+ ```
107
+
108
+ ### 2. Use in FastAPI
109
+
110
+ ```python
111
+ from fastapi import FastAPI, Depends
112
+ from sqlalchemy.ext.asyncio import AsyncSession
113
+ from sqlalchemy import text
114
+ from pgsqlasync2fast_fastapi import (
115
+ get_db_session,
116
+ startup_database,
117
+ shutdown_database
118
+ )
119
+
120
+ app = FastAPI()
121
+
122
+ @app.on_event("startup")
123
+ async def startup():
124
+ await startup_database()
125
+
126
+ @app.on_event("shutdown")
127
+ async def shutdown():
128
+ await shutdown_database()
129
+
130
+ @app.get("/users")
131
+ async def get_users(session: AsyncSession = Depends(get_db_session)):
132
+ result = await session.execute(text("SELECT * FROM users"))
133
+ users = result.fetchall()
134
+ return {"users": users}
135
+ ```
136
+
137
+ ### 3. Use Multiple Databases
138
+
139
+ ```python
140
+ from functools import partial
141
+ from fastapi import Depends
142
+ from sqlalchemy.ext.asyncio import AsyncSession
143
+ from pgsqlasync2fast_fastapi import get_db_session
144
+
145
+ # Create dependency for business database
146
+ get_business_session = partial(get_db_session, connection_name="business")
147
+
148
+ @app.get("/business/data")
149
+ async def get_business_data(session: AsyncSession = Depends(get_business_session)):
150
+ result = await session.execute(text("SELECT * FROM business_data"))
151
+ return {"data": result.fetchall()}
152
+ ```
153
+
154
+ ### 4. Create Databases Programmatically
155
+
156
+ ```python
157
+ from pgsqlasync2fast_fastapi import create_database, database_exists
158
+
159
+ # Create a new database (requires a connection with is_superuser=true)
160
+ if not await database_exists("new_database"):
161
+ await create_database("new_database", owner="myuser")
162
+ print("Database created!")
163
+ ```
164
+
165
+ ## Configuration
166
+
167
+ ### Environment Variables
168
+
169
+ All configuration is done through environment variables with the prefix `DB_`:
170
+
171
+ #### Connection Settings
172
+
173
+ ```env
174
+ DB_CONNECTIONS__<NAME>__HOST=localhost # Database host
175
+ DB_CONNECTIONS__<NAME>__PORT=5432 # Database port
176
+ DB_CONNECTIONS__<NAME>__USERNAME=user # Database username
177
+ DB_CONNECTIONS__<NAME>__PASSWORD=pass # Database password
178
+ DB_CONNECTIONS__<NAME>__DATABASE=dbname # Database name
179
+ DB_CONNECTIONS__<NAME>__IS_SUPERUSER=false # Superuser privileges
180
+ ```
181
+
182
+ #### Pool Settings (Optional)
183
+
184
+ ```env
185
+ DB_CONNECTIONS__<NAME>__POOL_SIZE=5 # Connection pool size
186
+ DB_CONNECTIONS__<NAME>__MAX_OVERFLOW=10 # Max overflow connections
187
+ DB_CONNECTIONS__<NAME>__POOL_TIMEOUT=30 # Pool timeout in seconds
188
+ DB_CONNECTIONS__<NAME>__POOL_RECYCLE=3600 # Recycle connections after seconds
189
+ ```
190
+
191
+ #### Global Settings
192
+
193
+ ```env
194
+ DB_DEFAULT_CONNECTION=default # Default connection name
195
+ DB_ECHO=false # Global SQLAlchemy echo mode
196
+ ```
197
+
198
+ ## API Reference
199
+
200
+ ### Dependencies
201
+
202
+ #### `get_db_session(connection_name: str = "default")`
203
+
204
+ FastAPI dependency that provides an async database session with automatic commit/rollback.
205
+
206
+ ```python
207
+ @app.get("/items")
208
+ async def get_items(session: AsyncSession = Depends(get_db_session)):
209
+ result = await session.execute(text("SELECT * FROM items"))
210
+ return result.fetchall()
211
+ ```
212
+
213
+ #### `get_db_engine(connection_name: str = "default")`
214
+
215
+ FastAPI dependency that provides the async engine for a connection.
216
+
217
+ ```python
218
+ @app.get("/health")
219
+ async def health_check(engine: AsyncEngine = Depends(get_db_engine)):
220
+ async with engine.connect() as conn:
221
+ await conn.execute(text("SELECT 1"))
222
+ return {"status": "healthy"}
223
+ ```
224
+
225
+ #### `get_db_manager()`
226
+
227
+ FastAPI dependency that provides the DatabaseManager singleton.
228
+
229
+ ```python
230
+ @app.get("/connections")
231
+ async def list_connections(manager: DatabaseManager = Depends(get_db_manager)):
232
+ return {"connections": manager.list_connections()}
233
+ ```
234
+
235
+ ### Database Utilities
236
+
237
+ #### `create_database(database_name: str, owner: Optional[str] = None, connection_name: Optional[str] = None) -> bool`
238
+
239
+ Create a new database using a superuser connection.
240
+
241
+ ```python
242
+ created = await create_database("my_new_db", owner="myuser")
243
+ if created:
244
+ print("Database created successfully!")
245
+ ```
246
+
247
+ #### `database_exists(database_name: str, connection_name: Optional[str] = None) -> bool`
248
+
249
+ Check if a database exists.
250
+
251
+ ```python
252
+ if await database_exists("my_db"):
253
+ print("Database exists!")
254
+ ```
255
+
256
+ #### `drop_database(database_name: str, connection_name: Optional[str] = None, force: bool = False) -> bool`
257
+
258
+ Drop a database (requires superuser).
259
+
260
+ ```python
261
+ dropped = await drop_database("old_db", force=True)
262
+ ```
263
+
264
+ #### `list_databases(connection_name: Optional[str] = None) -> List[str]`
265
+
266
+ List all databases.
267
+
268
+ ```python
269
+ databases = await list_databases()
270
+ for db in databases:
271
+ print(f" - {db}")
272
+ ```
273
+
274
+ ### Startup/Shutdown
275
+
276
+ #### `startup_database(config: Optional[DatabaseSettings] = None) -> DatabaseManager`
277
+
278
+ Initialize database connections on application startup.
279
+
280
+ ```python
281
+ @app.on_event("startup")
282
+ async def startup():
283
+ await startup_database()
284
+ ```
285
+
286
+ #### `shutdown_database() -> None`
287
+
288
+ Close all database connections on application shutdown.
289
+
290
+ ```python
291
+ @app.on_event("shutdown")
292
+ async def shutdown():
293
+ await shutdown_database()
294
+ ```
295
+
296
+ ## Examples
297
+
298
+ See the `examples/` directory for complete working examples:
299
+
300
+ - `basic_usage.py` - Basic database connection and queries
301
+ - `multi_database.py` - Using multiple database connections
302
+ - `fastapi_integration.py` - Complete FastAPI application
303
+ - `database_creation.py` - Creating databases programmatically
304
+
305
+ ## License
306
+
307
+ MIT License - see LICENSE file for details.
308
+
309
+ ## Author
310
+
311
+ Angel Daniel Sanchez Castillo - angeldaniel.sanchezcastillo@gmail.com
@@ -0,0 +1,11 @@
1
+ pgsqlasync2fast_fastapi/__init__.py,sha256=TYLh-ERPDG9HGvoQbFcy3Kwj7EyoVlibZC_vAHMEvqY,1351
2
+ pgsqlasync2fast_fastapi/__version__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
3
+ pgsqlasync2fast_fastapi/connection.py,sha256=B06MfPoPCWh0MU4Mu0mxYtzbraDbJ0YTtmksZ26gE_M,6144
4
+ pgsqlasync2fast_fastapi/database.py,sha256=onV5BOdp3xXo_B6l70mxr_XHZkqhzQ7LKn0Rf5raeqk,8903
5
+ pgsqlasync2fast_fastapi/dependencies.py,sha256=C7CVOP06FZbwcJ3p3xhBmOCa8Fi5d2o3MtvA-AkM6Vc,4404
6
+ pgsqlasync2fast_fastapi/settings.py,sha256=9SAmEVtotKJYlbTjI4qkKBjEOghTTtaICAFBBrR-eHk,6678
7
+ pgsqlasync2fast_fastapi-0.1.0.dist-info/licenses/LICENSE,sha256=CkISX1hNEwxxrPTOXet3IYMEH28Bn7SoUyKniRjg68I,1086
8
+ pgsqlasync2fast_fastapi-0.1.0.dist-info/METADATA,sha256=Y5HJoqB6eVxb5WEVpIhyy7TzR8yVHKfSYvk_fjMZofs,10082
9
+ pgsqlasync2fast_fastapi-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
10
+ pgsqlasync2fast_fastapi-0.1.0.dist-info/top_level.txt,sha256=7pllDm9nlFGWKJDtG5zukxqdADqjZddU-GzaQJPLMVc,24
11
+ pgsqlasync2fast_fastapi-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Angel Daniel Sanchez Castillo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ pgsqlasync2fast_fastapi