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.
- pgsqlasync2fast_fastapi/__init__.py +49 -0
- pgsqlasync2fast_fastapi/__version__.py +1 -0
- pgsqlasync2fast_fastapi/connection.py +196 -0
- pgsqlasync2fast_fastapi/database.py +262 -0
- pgsqlasync2fast_fastapi/dependencies.py +147 -0
- pgsqlasync2fast_fastapi/settings.py +198 -0
- pgsqlasync2fast_fastapi-0.1.0.dist-info/METADATA +311 -0
- pgsqlasync2fast_fastapi-0.1.0.dist-info/RECORD +11 -0
- pgsqlasync2fast_fastapi-0.1.0.dist-info/WHEEL +5 -0
- pgsqlasync2fast_fastapi-0.1.0.dist-info/licenses/LICENSE +21 -0
- pgsqlasync2fast_fastapi-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -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,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
|