Repository navigation
Expand file tree
/
Copy pathdatabase.py
More file actions
153 lines (121 loc) · 4.78 KB
/
Copy pathdatabase.py
File metadata and controls
153 lines (121 loc) · 4.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
"""
Database initialization and session management using SQLAlchemy, providing functions to create the database engine,
manage sessions, and perform connectivity checks. This module includes logic to handle database connection pooling, to
provide context-managed sessions for use in route handlers and services, and to ensure proper cleanup of database
resources on application shutdown. It also includes a function to initialize the database schema based on defined
SQLAlchemy models.
Copyright (c) 2026 Stefan Kumarasinghe.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
"""
import logging
import os
from collections.abc import Generator, Iterator
from contextlib import contextmanager
from typing import Protocol
from sqlalchemy import create_engine, text
from sqlalchemy.engine import Engine, make_url
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, sessionmaker
from db_models import Base
logger = logging.getLogger(__name__)
class _SessionFactory(Protocol):
def __call__(self) -> Session: ...
def _new_session(factory: _SessionFactory) -> Session:
return factory()
_ENGINE: Engine | None = None
_SESSION_LOCAL: _SessionFactory | None = None
def ensure_database_exists(database_url: str) -> None:
url = make_url(database_url)
drivername = str(getattr(url, "drivername", "postgresql"))
if not drivername.startswith("postgresql"):
return
db_name = url.database
if not db_name:
raise RuntimeError("Database URL must include a database name")
admin_url = url.set(database="postgres")
admin_engine = create_engine(admin_url, pool_pre_ping=True)
try:
with admin_engine.connect().execution_options(isolation_level="AUTOCOMMIT") as conn:
exists = conn.execute(
text("SELECT 1 FROM pg_database WHERE datname = :name"),
{"name": db_name},
).scalar()
if exists:
return
conn.execute(text(f'CREATE DATABASE "{db_name}"'))
logger.info("Created database %s", db_name)
finally:
admin_engine.dispose()
def init_database(
database_url: str,
echo: bool = False,
pool_size: int | None = None,
) -> None:
if _ENGINE is not None and _SESSION_LOCAL is not None:
logger.debug("Database already initialized; skipping re-init.")
return
engine = create_engine(
database_url,
pool_pre_ping=True,
pool_size=pool_size or int(os.getenv("DB_POOL_SIZE", "10")),
max_overflow=int(os.getenv("DB_MAX_OVERFLOW", "20")),
pool_timeout=int(os.getenv("DB_POOL_TIMEOUT", "30")),
pool_recycle=int(os.getenv("DB_POOL_RECYCLE", "1800")),
echo=echo,
)
globals()["_ENGINE"] = engine
globals()["_SESSION_LOCAL"] = sessionmaker(bind=engine, autoflush=False, expire_on_commit=False)
return
def _require_session_factory() -> _SessionFactory:
if _SESSION_LOCAL is None:
raise RuntimeError("Database not initialized. Call init_database() first.")
return _SESSION_LOCAL
@contextmanager
def get_db_session() -> Iterator[Session]:
if _ENGINE is None or _SESSION_LOCAL is None:
raise RuntimeError("Database not initialized. Call init_database() first.")
session: Session = _new_session(_require_session_factory())
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
def get_db() -> Generator[Session, None, None]:
if _ENGINE is None or _SESSION_LOCAL is None:
raise RuntimeError("Database not initialized. Call init_database() first.")
session: Session = _new_session(_require_session_factory())
try:
yield session
session.commit()
except Exception:
session.rollback()
raise
finally:
session.close()
def connection_test() -> bool:
if _ENGINE is None:
return False
try:
with _ENGINE.connect() as conn:
conn.execute(text("SELECT 1"))
return True
except SQLAlchemyError as exc:
logger.debug("DB connection test failed: %s", exc)
return False
def dispose_database() -> None:
globals()["_SESSION_LOCAL"] = None
if _ENGINE is not None:
try:
_ENGINE.dispose()
finally:
globals()["_ENGINE"] = None
def init_db() -> None:
if _ENGINE is None or not hasattr(_ENGINE, "connect"):
raise RuntimeError("Database not initialized. Call init_database() first.")
logger.info("Initializing database tables...")
Base.metadata.create_all(bind=_ENGINE)
logger.info("Database tables created successfully.")