fix: wrap AsyncSession in context manager across all API blueprints
Replace all bare `session = AsyncSession(bind=conn)` assignments with `async with AsyncSession(bind=conn) as session:` in downloads.py, credentials.py, settings.py, sources.py, and subscriptions.py to ensure sessions are properly closed on all code paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,8 +19,7 @@ VALID_PLATFORMS = ["patreon", "subscribestar", "hentaifoundry", "discord", "pixi
|
|||||||
async def list_credentials():
|
async def list_credentials():
|
||||||
"""List all credential status (without sensitive data)."""
|
"""List all credential status (without sensitive data)."""
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
result = await session.execute(select(Credential))
|
result = await session.execute(select(Credential))
|
||||||
credentials = result.scalars().all()
|
credentials = result.scalars().all()
|
||||||
|
|
||||||
@@ -48,7 +47,7 @@ async def upload_credentials():
|
|||||||
if extension_key:
|
if extension_key:
|
||||||
# Look up API key from database
|
# Look up API key from database
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Setting).where(Setting.key == EXTENSION_API_KEY_SETTING)
|
select(Setting).where(Setting.key == EXTENSION_API_KEY_SETTING)
|
||||||
)
|
)
|
||||||
@@ -81,8 +80,7 @@ async def upload_credentials():
|
|||||||
return jsonify({"error": "Invalid credential type. Must be 'cookies' or 'token'"}), 400
|
return jsonify({"error": "Invalid credential type. Must be 'cookies' or 'token'"}), 400
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
# Check for existing credential
|
# Check for existing credential
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Credential).where(Credential.platform == platform)
|
select(Credential).where(Credential.platform == platform)
|
||||||
@@ -127,8 +125,7 @@ async def delete_credentials(platform: str):
|
|||||||
return jsonify({"error": f"Invalid platform. Must be one of: {', '.join(VALID_PLATFORMS)}"}), 400
|
return jsonify({"error": f"Invalid platform. Must be one of: {', '.join(VALID_PLATFORMS)}"}), 400
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Credential).where(Credential.platform == platform)
|
select(Credential).where(Credential.platform == platform)
|
||||||
)
|
)
|
||||||
@@ -160,8 +157,7 @@ async def verify_credentials():
|
|||||||
return jsonify({"error": f"Invalid platform. Must be one of: {', '.join(VALID_PLATFORMS)}"}), 400
|
return jsonify({"error": f"Invalid platform. Must be one of: {', '.join(VALID_PLATFORMS)}"}), 400
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Credential).where(Credential.platform == platform)
|
select(Credential).where(Credential.platform == platform)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,8 +28,7 @@ async def list_downloads():
|
|||||||
per_page = min(int(request.args.get("per_page", 50)), 100)
|
per_page = min(int(request.args.get("per_page", 50)), 100)
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
# Build query - eager load source and subscription for labeling
|
# Build query - eager load source and subscription for labeling
|
||||||
query = select(Download).options(
|
query = select(Download).options(
|
||||||
selectinload(Download.source).selectinload(Source.subscription)
|
selectinload(Download.source).selectinload(Source.subscription)
|
||||||
@@ -89,8 +88,7 @@ async def list_downloads():
|
|||||||
async def get_download(download_id: int):
|
async def get_download(download_id: int):
|
||||||
"""Get download details."""
|
"""Get download details."""
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
result = await session.execute(select(Download).where(Download.id == download_id))
|
result = await session.execute(select(Download).where(Download.id == download_id))
|
||||||
download = result.scalar()
|
download = result.scalar()
|
||||||
|
|
||||||
@@ -120,8 +118,7 @@ async def get_download(download_id: int):
|
|||||||
async def retry_download(download_id: int):
|
async def retry_download(download_id: int):
|
||||||
"""Retry a failed download."""
|
"""Retry a failed download."""
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
result = await session.execute(select(Download).where(Download.id == download_id))
|
result = await session.execute(select(Download).where(Download.id == download_id))
|
||||||
download = result.scalar()
|
download = result.scalar()
|
||||||
|
|
||||||
@@ -162,8 +159,7 @@ async def recent_activity():
|
|||||||
cutoff = utcnow() - timedelta(days=days)
|
cutoff = utcnow() - timedelta(days=days)
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
# Get failed downloads OR completed with files (noteworthy activity)
|
# Get failed downloads OR completed with files (noteworthy activity)
|
||||||
# Exclude failed downloads that have been superseded by a later success
|
# Exclude failed downloads that have been superseded by a later success
|
||||||
# for the same source - those are stale failures the user doesn't need to see
|
# for the same source - those are stale failures the user doesn't need to see
|
||||||
@@ -286,8 +282,7 @@ async def get_stats():
|
|||||||
period = request.args.get("period", "week") # day, week, month
|
period = request.args.get("period", "week") # day, week, month
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
# Total counts by status
|
# Total counts by status
|
||||||
status_query = select(
|
status_query = select(
|
||||||
Download.status,
|
Download.status,
|
||||||
@@ -356,8 +351,7 @@ async def export_failed_logs():
|
|||||||
cutoff_date = utcnow() - timedelta(days=days)
|
cutoff_date = utcnow() - timedelta(days=days)
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
# Build query for failed downloads (eager load source for platform)
|
# Build query for failed downloads (eager load source for platform)
|
||||||
query = select(Download).options(selectinload(Download.source)).where(
|
query = select(Download).options(selectinload(Download.source)).where(
|
||||||
and_(
|
and_(
|
||||||
|
|||||||
@@ -81,8 +81,7 @@ async def get_setting_value(session: AsyncSession, key: str, default=None):
|
|||||||
async def get_all_settings():
|
async def get_all_settings():
|
||||||
"""Get all application settings."""
|
"""Get all application settings."""
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
result = await session.execute(select(Setting))
|
result = await session.execute(select(Setting))
|
||||||
settings = result.scalars().all()
|
settings = result.scalars().all()
|
||||||
|
|
||||||
@@ -110,8 +109,7 @@ async def update_settings():
|
|||||||
return jsonify({"error": "No settings provided"}), 400
|
return jsonify({"error": "No settings provided"}), 400
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
# Check if setting exists
|
# Check if setting exists
|
||||||
result = await session.execute(select(Setting).where(Setting.key == key))
|
result = await session.execute(select(Setting).where(Setting.key == key))
|
||||||
@@ -142,7 +140,7 @@ async def update_settings():
|
|||||||
async def get_extension_api_key():
|
async def get_extension_api_key():
|
||||||
"""Get the extension API key."""
|
"""Get the extension API key."""
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
api_key = await get_or_create_extension_api_key(session)
|
api_key = await get_or_create_extension_api_key(session)
|
||||||
return jsonify({"api_key": api_key})
|
return jsonify({"api_key": api_key})
|
||||||
|
|
||||||
@@ -151,8 +149,7 @@ async def get_extension_api_key():
|
|||||||
async def regenerate_extension_api_key():
|
async def regenerate_extension_api_key():
|
||||||
"""Regenerate the extension API key."""
|
"""Regenerate the extension API key."""
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
# Find existing key
|
# Find existing key
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Setting).where(Setting.key == EXTENSION_API_KEY_SETTING)
|
select(Setting).where(Setting.key == EXTENSION_API_KEY_SETTING)
|
||||||
@@ -246,8 +243,7 @@ async def get_logs():
|
|||||||
limit = min(int(request.args.get("limit", 50)), 200)
|
limit = min(int(request.args.get("limit", 50)), 200)
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
# Get recent downloads with output logs
|
# Get recent downloads with output logs
|
||||||
query = select(Download).order_by(desc(Download.created_at)).limit(limit)
|
query = select(Download).order_by(desc(Download.created_at)).limit(limit)
|
||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
@@ -333,8 +329,7 @@ async def debug_schedule_interval():
|
|||||||
the worker would read.
|
the worker would read.
|
||||||
"""
|
"""
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
# Query the raw setting from database
|
# Query the raw setting from database
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Setting).where(Setting.key == "download.schedule_interval")
|
select(Setting).where(Setting.key == "download.schedule_interval")
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ async def list_sources():
|
|||||||
per_page = min(int(request.args.get("per_page", 50)), 100)
|
per_page = min(int(request.args.get("per_page", 50)), 100)
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
# Build query with subscription eager loading
|
# Build query with subscription eager loading
|
||||||
query = select(Source).options(selectinload(Source.subscription))
|
query = select(Source).options(selectinload(Source.subscription))
|
||||||
|
|
||||||
@@ -83,8 +82,7 @@ async def create_source():
|
|||||||
return jsonify({"error": f"Invalid platform. Must be one of: {', '.join(valid_platforms)}"}), 400
|
return jsonify({"error": f"Invalid platform. Must be one of: {', '.join(valid_platforms)}"}), 400
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
# Check subscription exists
|
# Check subscription exists
|
||||||
sub_result = await session.execute(
|
sub_result = await session.execute(
|
||||||
select(Subscription).where(Subscription.id == data["subscription_id"])
|
select(Subscription).where(Subscription.id == data["subscription_id"])
|
||||||
@@ -129,8 +127,7 @@ async def create_source():
|
|||||||
async def get_source(source_id: int):
|
async def get_source(source_id: int):
|
||||||
"""Get a single source by ID."""
|
"""Get a single source by ID."""
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Source)
|
select(Source)
|
||||||
.options(selectinload(Source.subscription))
|
.options(selectinload(Source.subscription))
|
||||||
@@ -150,8 +147,7 @@ async def update_source(source_id: int):
|
|||||||
data = await request.get_json()
|
data = await request.get_json()
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Source)
|
select(Source)
|
||||||
.options(selectinload(Source.subscription))
|
.options(selectinload(Source.subscription))
|
||||||
@@ -182,8 +178,7 @@ async def update_source(source_id: int):
|
|||||||
async def delete_source(source_id: int):
|
async def delete_source(source_id: int):
|
||||||
"""Delete a source."""
|
"""Delete a source."""
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Source)
|
select(Source)
|
||||||
.options(selectinload(Source.subscription))
|
.options(selectinload(Source.subscription))
|
||||||
@@ -206,8 +201,7 @@ async def delete_source(source_id: int):
|
|||||||
async def trigger_check(source_id: int):
|
async def trigger_check(source_id: int):
|
||||||
"""Trigger an immediate download check for a source."""
|
"""Trigger an immediate download check for a source."""
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Source)
|
select(Source)
|
||||||
.options(selectinload(Source.subscription))
|
.options(selectinload(Source.subscription))
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ async def list_subscriptions():
|
|||||||
per_page = min(int(request.args.get("per_page", 50)), 100)
|
per_page = min(int(request.args.get("per_page", 50)), 100)
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
# Build query with eager loading of sources
|
# Build query with eager loading of sources
|
||||||
query = select(Subscription).options(selectinload(Subscription.sources))
|
query = select(Subscription).options(selectinload(Subscription.sources))
|
||||||
|
|
||||||
@@ -70,8 +69,7 @@ async def create_subscription():
|
|||||||
return jsonify({"error": "Name is required"}), 400
|
return jsonify({"error": "Name is required"}), 400
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
# Check for duplicate name
|
# Check for duplicate name
|
||||||
existing = await session.execute(
|
existing = await session.execute(
|
||||||
select(Subscription).where(Subscription.name == data["name"])
|
select(Subscription).where(Subscription.name == data["name"])
|
||||||
@@ -118,8 +116,7 @@ async def create_subscription():
|
|||||||
async def get_subscription(subscription_id: int):
|
async def get_subscription(subscription_id: int):
|
||||||
"""Get a single subscription by ID with its sources."""
|
"""Get a single subscription by ID with its sources."""
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Subscription)
|
select(Subscription)
|
||||||
.options(selectinload(Subscription.sources))
|
.options(selectinload(Subscription.sources))
|
||||||
@@ -139,8 +136,7 @@ async def update_subscription(subscription_id: int):
|
|||||||
data = await request.get_json()
|
data = await request.get_json()
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Subscription)
|
select(Subscription)
|
||||||
.options(selectinload(Subscription.sources))
|
.options(selectinload(Subscription.sources))
|
||||||
@@ -179,8 +175,7 @@ async def update_subscription(subscription_id: int):
|
|||||||
async def delete_subscription(subscription_id: int):
|
async def delete_subscription(subscription_id: int):
|
||||||
"""Delete a subscription and all its sources."""
|
"""Delete a subscription and all its sources."""
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Subscription).where(Subscription.id == subscription_id)
|
select(Subscription).where(Subscription.id == subscription_id)
|
||||||
)
|
)
|
||||||
@@ -201,8 +196,7 @@ async def delete_subscription(subscription_id: int):
|
|||||||
async def list_subscription_sources(subscription_id: int):
|
async def list_subscription_sources(subscription_id: int):
|
||||||
"""List all sources for a subscription."""
|
"""List all sources for a subscription."""
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Subscription)
|
select(Subscription)
|
||||||
.options(selectinload(Subscription.sources))
|
.options(selectinload(Subscription.sources))
|
||||||
@@ -237,8 +231,7 @@ async def add_source_to_subscription(subscription_id: int):
|
|||||||
return jsonify({"error": f"Invalid platform. Must be one of: {', '.join(valid_platforms)}"}), 400
|
return jsonify({"error": f"Invalid platform. Must be one of: {', '.join(valid_platforms)}"}), 400
|
||||||
|
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
# Check subscription exists
|
# Check subscription exists
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Subscription).where(Subscription.id == subscription_id)
|
select(Subscription).where(Subscription.id == subscription_id)
|
||||||
@@ -290,8 +283,7 @@ async def add_source_to_subscription(subscription_id: int):
|
|||||||
async def trigger_subscription_check(subscription_id: int):
|
async def trigger_subscription_check(subscription_id: int):
|
||||||
"""Trigger download check for all enabled sources in a subscription."""
|
"""Trigger download check for all enabled sources in a subscription."""
|
||||||
async with current_app.db_engine.connect() as conn:
|
async with current_app.db_engine.connect() as conn:
|
||||||
session = AsyncSession(bind=conn)
|
async with AsyncSession(bind=conn) as session:
|
||||||
|
|
||||||
result = await session.execute(
|
result = await session.execute(
|
||||||
select(Subscription)
|
select(Subscription)
|
||||||
.options(selectinload(Subscription.sources))
|
.options(selectinload(Subscription.sources))
|
||||||
|
|||||||
Reference in New Issue
Block a user