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():
|
||||
"""List all credential status (without sensitive data)."""
|
||||
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))
|
||||
credentials = result.scalars().all()
|
||||
|
||||
@@ -48,7 +47,7 @@ async def upload_credentials():
|
||||
if extension_key:
|
||||
# Look up API key from database
|
||||
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).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
|
||||
|
||||
async with current_app.db_engine.connect() as conn:
|
||||
session = AsyncSession(bind=conn)
|
||||
|
||||
async with AsyncSession(bind=conn) as session:
|
||||
# Check for existing credential
|
||||
result = await session.execute(
|
||||
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
|
||||
|
||||
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).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
|
||||
|
||||
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).where(Credential.platform == platform)
|
||||
)
|
||||
|
||||
@@ -28,8 +28,7 @@ async def list_downloads():
|
||||
per_page = min(int(request.args.get("per_page", 50)), 100)
|
||||
|
||||
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
|
||||
query = select(Download).options(
|
||||
selectinload(Download.source).selectinload(Source.subscription)
|
||||
@@ -89,8 +88,7 @@ async def list_downloads():
|
||||
async def get_download(download_id: int):
|
||||
"""Get download details."""
|
||||
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))
|
||||
download = result.scalar()
|
||||
|
||||
@@ -120,8 +118,7 @@ async def get_download(download_id: int):
|
||||
async def retry_download(download_id: int):
|
||||
"""Retry a failed download."""
|
||||
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))
|
||||
download = result.scalar()
|
||||
|
||||
@@ -162,8 +159,7 @@ async def recent_activity():
|
||||
cutoff = utcnow() - timedelta(days=days)
|
||||
|
||||
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)
|
||||
# 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
|
||||
@@ -286,8 +282,7 @@ async def get_stats():
|
||||
period = request.args.get("period", "week") # day, week, month
|
||||
|
||||
async with current_app.db_engine.connect() as conn:
|
||||
session = AsyncSession(bind=conn)
|
||||
|
||||
async with AsyncSession(bind=conn) as session:
|
||||
# Total counts by status
|
||||
status_query = select(
|
||||
Download.status,
|
||||
@@ -356,8 +351,7 @@ async def export_failed_logs():
|
||||
cutoff_date = utcnow() - timedelta(days=days)
|
||||
|
||||
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)
|
||||
query = select(Download).options(selectinload(Download.source)).where(
|
||||
and_(
|
||||
|
||||
@@ -81,8 +81,7 @@ async def get_setting_value(session: AsyncSession, key: str, default=None):
|
||||
async def get_all_settings():
|
||||
"""Get all application settings."""
|
||||
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))
|
||||
settings = result.scalars().all()
|
||||
|
||||
@@ -110,8 +109,7 @@ async def update_settings():
|
||||
return jsonify({"error": "No settings provided"}), 400
|
||||
|
||||
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():
|
||||
# Check if setting exists
|
||||
result = await session.execute(select(Setting).where(Setting.key == key))
|
||||
@@ -142,7 +140,7 @@ async def update_settings():
|
||||
async def get_extension_api_key():
|
||||
"""Get the extension API key."""
|
||||
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)
|
||||
return jsonify({"api_key": api_key})
|
||||
|
||||
@@ -151,8 +149,7 @@ async def get_extension_api_key():
|
||||
async def regenerate_extension_api_key():
|
||||
"""Regenerate the extension API key."""
|
||||
async with current_app.db_engine.connect() as conn:
|
||||
session = AsyncSession(bind=conn)
|
||||
|
||||
async with AsyncSession(bind=conn) as session:
|
||||
# Find existing key
|
||||
result = await session.execute(
|
||||
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)
|
||||
|
||||
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
|
||||
query = select(Download).order_by(desc(Download.created_at)).limit(limit)
|
||||
result = await session.execute(query)
|
||||
@@ -333,8 +329,7 @@ async def debug_schedule_interval():
|
||||
the worker would read.
|
||||
"""
|
||||
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
|
||||
result = await session.execute(
|
||||
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)
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
async with current_app.db_engine.connect() as conn:
|
||||
session = AsyncSession(bind=conn)
|
||||
|
||||
async with AsyncSession(bind=conn) as session:
|
||||
# Check subscription exists
|
||||
sub_result = await session.execute(
|
||||
select(Subscription).where(Subscription.id == data["subscription_id"])
|
||||
@@ -129,8 +127,7 @@ async def create_source():
|
||||
async def get_source(source_id: int):
|
||||
"""Get a single source by ID."""
|
||||
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(Source)
|
||||
.options(selectinload(Source.subscription))
|
||||
@@ -150,8 +147,7 @@ async def update_source(source_id: int):
|
||||
data = await request.get_json()
|
||||
|
||||
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(Source)
|
||||
.options(selectinload(Source.subscription))
|
||||
@@ -182,8 +178,7 @@ async def update_source(source_id: int):
|
||||
async def delete_source(source_id: int):
|
||||
"""Delete a source."""
|
||||
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(Source)
|
||||
.options(selectinload(Source.subscription))
|
||||
@@ -206,8 +201,7 @@ async def delete_source(source_id: int):
|
||||
async def trigger_check(source_id: int):
|
||||
"""Trigger an immediate download check for a source."""
|
||||
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(Source)
|
||||
.options(selectinload(Source.subscription))
|
||||
|
||||
@@ -24,8 +24,7 @@ async def list_subscriptions():
|
||||
per_page = min(int(request.args.get("per_page", 50)), 100)
|
||||
|
||||
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
|
||||
query = select(Subscription).options(selectinload(Subscription.sources))
|
||||
|
||||
@@ -70,8 +69,7 @@ async def create_subscription():
|
||||
return jsonify({"error": "Name is required"}), 400
|
||||
|
||||
async with current_app.db_engine.connect() as conn:
|
||||
session = AsyncSession(bind=conn)
|
||||
|
||||
async with AsyncSession(bind=conn) as session:
|
||||
# Check for duplicate name
|
||||
existing = await session.execute(
|
||||
select(Subscription).where(Subscription.name == data["name"])
|
||||
@@ -118,8 +116,7 @@ async def create_subscription():
|
||||
async def get_subscription(subscription_id: int):
|
||||
"""Get a single subscription by ID with its sources."""
|
||||
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(Subscription)
|
||||
.options(selectinload(Subscription.sources))
|
||||
@@ -139,8 +136,7 @@ async def update_subscription(subscription_id: int):
|
||||
data = await request.get_json()
|
||||
|
||||
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(Subscription)
|
||||
.options(selectinload(Subscription.sources))
|
||||
@@ -179,8 +175,7 @@ async def update_subscription(subscription_id: int):
|
||||
async def delete_subscription(subscription_id: int):
|
||||
"""Delete a subscription and all its sources."""
|
||||
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(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):
|
||||
"""List all sources for a subscription."""
|
||||
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(Subscription)
|
||||
.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
|
||||
|
||||
async with current_app.db_engine.connect() as conn:
|
||||
session = AsyncSession(bind=conn)
|
||||
|
||||
async with AsyncSession(bind=conn) as session:
|
||||
# Check subscription exists
|
||||
result = await session.execute(
|
||||
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):
|
||||
"""Trigger download check for all enabled sources in a subscription."""
|
||||
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(Subscription)
|
||||
.options(selectinload(Subscription.sources))
|
||||
|
||||
Reference in New Issue
Block a user