feat: configure Alembic env.py to dynamically load settings database URL and metadata
Browse files- alembic/env.py +15 -26
- backend/app/config/settings.py +10 -5
- pyproject.toml +1 -0
- uv.lock +16 -0
alembic/env.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
| 1 |
import asyncio
|
|
|
|
| 2 |
from logging.config import fileConfig
|
|
|
|
| 3 |
|
| 4 |
from sqlalchemy import pool
|
| 5 |
from sqlalchemy.engine import Connection
|
|
@@ -7,39 +9,29 @@ from sqlalchemy.ext.asyncio import async_engine_from_config
|
|
| 7 |
|
| 8 |
from alembic import context
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
# this is the Alembic Config object, which provides
|
| 11 |
# access to the values within the .ini file in use.
|
| 12 |
config = context.config
|
| 13 |
|
|
|
|
|
|
|
|
|
|
| 14 |
# Interpret the config file for Python logging.
|
| 15 |
-
# This line sets up loggers basically.
|
| 16 |
if config.config_file_name is not None:
|
| 17 |
fileConfig(config.config_file_name)
|
| 18 |
|
| 19 |
-
#
|
| 20 |
-
|
| 21 |
-
# from myapp import mymodel
|
| 22 |
-
# target_metadata = mymodel.Base.metadata
|
| 23 |
-
target_metadata = None
|
| 24 |
-
|
| 25 |
-
# other values from the config, defined by the needs of env.py,
|
| 26 |
-
# can be acquired:
|
| 27 |
-
# my_important_option = config.get_main_option("my_important_option")
|
| 28 |
-
# ... etc.
|
| 29 |
|
| 30 |
|
| 31 |
def run_migrations_offline() -> None:
|
| 32 |
-
"""Run migrations in 'offline' mode.
|
| 33 |
-
|
| 34 |
-
This configures the context with just a URL
|
| 35 |
-
and not an Engine, though an Engine is acceptable
|
| 36 |
-
here as well. By skipping the Engine creation
|
| 37 |
-
we don't even need a DBAPI to be available.
|
| 38 |
-
|
| 39 |
-
Calls to context.execute() here emit the given string to the
|
| 40 |
-
script output.
|
| 41 |
-
|
| 42 |
-
"""
|
| 43 |
url = config.get_main_option("sqlalchemy.url")
|
| 44 |
context.configure(
|
| 45 |
url=url,
|
|
@@ -62,9 +54,7 @@ def do_run_migrations(connection: Connection) -> None:
|
|
| 62 |
async def run_async_migrations() -> None:
|
| 63 |
"""In this scenario we need to create an Engine
|
| 64 |
and associate a connection with the context.
|
| 65 |
-
|
| 66 |
"""
|
| 67 |
-
|
| 68 |
connectable = async_engine_from_config(
|
| 69 |
config.get_section(config.config_ini_section, {}),
|
| 70 |
prefix="sqlalchemy.",
|
|
@@ -79,11 +69,10 @@ async def run_async_migrations() -> None:
|
|
| 79 |
|
| 80 |
def run_migrations_online() -> None:
|
| 81 |
"""Run migrations in 'online' mode."""
|
| 82 |
-
|
| 83 |
asyncio.run(run_async_migrations())
|
| 84 |
|
| 85 |
|
| 86 |
if context.is_offline_mode():
|
| 87 |
run_migrations_offline()
|
| 88 |
else:
|
| 89 |
-
run_migrations_online()
|
|
|
|
| 1 |
import asyncio
|
| 2 |
+
import sys
|
| 3 |
from logging.config import fileConfig
|
| 4 |
+
from pathlib import Path
|
| 5 |
|
| 6 |
from sqlalchemy import pool
|
| 7 |
from sqlalchemy.engine import Connection
|
|
|
|
| 9 |
|
| 10 |
from alembic import context
|
| 11 |
|
| 12 |
+
# 1. Add the project root to Python Path to allow imports from backend
|
| 13 |
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
| 14 |
+
|
| 15 |
+
from backend.app.config.settings import settings
|
| 16 |
+
from backend.app.database.models import Base
|
| 17 |
+
|
| 18 |
# this is the Alembic Config object, which provides
|
| 19 |
# access to the values within the .ini file in use.
|
| 20 |
config = context.config
|
| 21 |
|
| 22 |
+
# 2. Inject our dynamic database URL from settings into Alembic's configuration
|
| 23 |
+
config.set_main_option("sqlalchemy.url", settings.database_url_async)
|
| 24 |
+
|
| 25 |
# Interpret the config file for Python logging.
|
|
|
|
| 26 |
if config.config_file_name is not None:
|
| 27 |
fileConfig(config.config_file_name)
|
| 28 |
|
| 29 |
+
# 3. Hook up our SQLAlchemy Base metadata to enable schema autogeneration
|
| 30 |
+
target_metadata = Base.metadata
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
def run_migrations_offline() -> None:
|
| 34 |
+
"""Run migrations in 'offline' mode."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
url = config.get_main_option("sqlalchemy.url")
|
| 36 |
context.configure(
|
| 37 |
url=url,
|
|
|
|
| 54 |
async def run_async_migrations() -> None:
|
| 55 |
"""In this scenario we need to create an Engine
|
| 56 |
and associate a connection with the context.
|
|
|
|
| 57 |
"""
|
|
|
|
| 58 |
connectable = async_engine_from_config(
|
| 59 |
config.get_section(config.config_ini_section, {}),
|
| 60 |
prefix="sqlalchemy.",
|
|
|
|
| 69 |
|
| 70 |
def run_migrations_online() -> None:
|
| 71 |
"""Run migrations in 'online' mode."""
|
|
|
|
| 72 |
asyncio.run(run_async_migrations())
|
| 73 |
|
| 74 |
|
| 75 |
if context.is_offline_mode():
|
| 76 |
run_migrations_offline()
|
| 77 |
else:
|
| 78 |
+
run_migrations_online()
|
backend/app/config/settings.py
CHANGED
|
@@ -11,13 +11,18 @@ class Settings(BaseSettings):
|
|
| 11 |
GEMINI_API_KEY: str
|
| 12 |
|
| 13 |
# Optional Razorpay Credentials (defaults to empty string if not set)
|
| 14 |
-
RAZORPAY_KEY_ID: str= Field(default="")
|
| 15 |
-
RAZORPAY_KEY_SECRET: str= Field(default="")
|
| 16 |
-
RAZORPAY_WEBHOOK_SECRET: str= Field(default="")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
# Auth configuration
|
| 19 |
-
SECRET_KEY: str= Field(default="supersecretkeychangeinproduction")
|
| 20 |
-
ACCESS_TOKEN_EXPIRE_MINUTES: int= 60*24*8
|
| 21 |
|
| 22 |
@property
|
| 23 |
def database_url_async(self) -> str:
|
|
|
|
| 11 |
GEMINI_API_KEY: str
|
| 12 |
|
| 13 |
# Optional Razorpay Credentials (defaults to empty string if not set)
|
| 14 |
+
RAZORPAY_KEY_ID: str = Field(default="")
|
| 15 |
+
RAZORPAY_KEY_SECRET: str = Field(default="")
|
| 16 |
+
RAZORPAY_WEBHOOK_SECRET: str = Field(default="")
|
| 17 |
+
|
| 18 |
+
# Optional Cloudinary Credentials (defaults to empty string if not set)
|
| 19 |
+
CLOUDINARY_CLOUD_NAME: str = Field(default="")
|
| 20 |
+
CLOUDINARY_API_KEY: str = Field(default="")
|
| 21 |
+
CLOUDINARY_API_SECRET: str = Field(default="")
|
| 22 |
|
| 23 |
# Auth configuration
|
| 24 |
+
SECRET_KEY: str = Field(default="supersecretkeychangeinproduction")
|
| 25 |
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
|
| 26 |
|
| 27 |
@property
|
| 28 |
def database_url_async(self) -> str:
|
pyproject.toml
CHANGED
|
@@ -21,4 +21,5 @@ dependencies = [
|
|
| 21 |
"prometheus-fastapi-instrumentator>=7.0.0",
|
| 22 |
"razorpay>=1.4.1",
|
| 23 |
"httpx>=0.27.0",
|
|
|
|
| 24 |
]
|
|
|
|
| 21 |
"prometheus-fastapi-instrumentator>=7.0.0",
|
| 22 |
"razorpay>=1.4.1",
|
| 23 |
"httpx>=0.27.0",
|
| 24 |
+
"cloudinary>=1.38.0",
|
| 25 |
]
|
uv.lock
CHANGED
|
@@ -305,6 +305,20 @@ wheels = [
|
|
| 305 |
{ url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
|
| 306 |
]
|
| 307 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
[[package]]
|
| 309 |
name = "colorama"
|
| 310 |
version = "0.4.6"
|
|
@@ -1507,6 +1521,7 @@ source = { virtual = "." }
|
|
| 1507 |
dependencies = [
|
| 1508 |
{ name = "alembic" },
|
| 1509 |
{ name = "asyncpg" },
|
|
|
|
| 1510 |
{ name = "fastapi" },
|
| 1511 |
{ name = "google-generativeai" },
|
| 1512 |
{ name = "httpx" },
|
|
@@ -1528,6 +1543,7 @@ dependencies = [
|
|
| 1528 |
requires-dist = [
|
| 1529 |
{ name = "alembic", specifier = ">=1.13.1" },
|
| 1530 |
{ name = "asyncpg", specifier = ">=0.29.0" },
|
|
|
|
| 1531 |
{ name = "fastapi", specifier = ">=0.110.0" },
|
| 1532 |
{ name = "google-generativeai", specifier = ">=0.4.1" },
|
| 1533 |
{ name = "httpx", specifier = ">=0.27.0" },
|
|
|
|
| 305 |
{ url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
|
| 306 |
]
|
| 307 |
|
| 308 |
+
[[package]]
|
| 309 |
+
name = "cloudinary"
|
| 310 |
+
version = "1.44.2"
|
| 311 |
+
source = { registry = "https://pypi.org/simple" }
|
| 312 |
+
dependencies = [
|
| 313 |
+
{ name = "certifi" },
|
| 314 |
+
{ name = "six" },
|
| 315 |
+
{ name = "urllib3" },
|
| 316 |
+
]
|
| 317 |
+
sdist = { url = "https://files.pythonhosted.org/packages/0c/1b/ae48dbbe3c2ff81b94816dd6645af2d609e135cea4fcd65b1a8a61672cea/cloudinary-1.44.2.tar.gz", hash = "sha256:bc9139c68f2ef6996ba62a5d0300e63c711d4f6564644c0c82aa31bf641ef3c7", size = 188389, upload-time = "2026-04-16T08:18:19.92Z" }
|
| 318 |
+
wheels = [
|
| 319 |
+
{ url = "https://files.pythonhosted.org/packages/b2/8b/0da00a22362c89ef53f9d64af29546d24aba8533ffdb7bbcb19631c7f636/cloudinary-1.44.2-py3-none-any.whl", hash = "sha256:6624d06014c33f50c0c9e740fde1dfe4579d03311025d8bb7183824b7443aa09", size = 147805, upload-time = "2026-04-16T08:18:18.84Z" },
|
| 320 |
+
]
|
| 321 |
+
|
| 322 |
[[package]]
|
| 323 |
name = "colorama"
|
| 324 |
version = "0.4.6"
|
|
|
|
| 1521 |
dependencies = [
|
| 1522 |
{ name = "alembic" },
|
| 1523 |
{ name = "asyncpg" },
|
| 1524 |
+
{ name = "cloudinary" },
|
| 1525 |
{ name = "fastapi" },
|
| 1526 |
{ name = "google-generativeai" },
|
| 1527 |
{ name = "httpx" },
|
|
|
|
| 1543 |
requires-dist = [
|
| 1544 |
{ name = "alembic", specifier = ">=1.13.1" },
|
| 1545 |
{ name = "asyncpg", specifier = ">=0.29.0" },
|
| 1546 |
+
{ name = "cloudinary", specifier = ">=1.38.0" },
|
| 1547 |
{ name = "fastapi", specifier = ">=0.110.0" },
|
| 1548 |
{ name = "google-generativeai", specifier = ">=0.4.1" },
|
| 1549 |
{ name = "httpx", specifier = ">=0.27.0" },
|