"""Render template resolution service. Used from Celery tasks (sync context) to find the best matching .blend template for a given category + output type combination. Cascade priority (first active match wins): 1. Exact: category_key + output_type linked via M2M 2. Category only: category_key + no output_types linked 3. OT only: category_key IS NULL + output_type linked via M2M 4. Global: category_key IS NULL + no output_types linked 5. No template → caller falls back to factory-settings behavior """ import logging from sqlalchemy import create_engine, select, and_, exists from sqlalchemy.orm import Session from app.models.render_template import RenderTemplate from app.models.system_setting import SystemSetting from app.domains.rendering.models import render_template_output_types logger = logging.getLogger(__name__) _engine = None def _get_engine(): global _engine if _engine is None: from app.config import settings as app_settings _engine = create_engine(app_settings.database_url_sync) return _engine def resolve_template_for_session( session: Session, category_key: str | None = None, output_type_id: str | None = None, ) -> RenderTemplate | None: """Find the best matching active render template on an existing sync session.""" active = RenderTemplate.is_active == True # noqa: E712 def _has_ot(ot_id): return exists( select(render_template_output_types.c.template_id).where(and_( render_template_output_types.c.template_id == RenderTemplate.id, render_template_output_types.c.output_type_id == ot_id, )) ) _no_ots = ~exists( select(render_template_output_types.c.template_id).where( render_template_output_types.c.template_id == RenderTemplate.id, ) ) if category_key and output_type_id: row = session.execute( select(RenderTemplate).where(and_( active, RenderTemplate.category_key == category_key, _has_ot(output_type_id), )) ).unique().scalar_one_or_none() if row: return row if category_key: row = session.execute( select(RenderTemplate).where(and_( active, RenderTemplate.category_key == category_key, _no_ots, )) ).unique().scalar_one_or_none() if row: return row if output_type_id: row = session.execute( select(RenderTemplate).where(and_( active, RenderTemplate.category_key.is_(None), _has_ot(output_type_id), )) ).unique().scalar_one_or_none() if row: return row return session.execute( select(RenderTemplate).where(and_( active, RenderTemplate.category_key.is_(None), _no_ots, )) ).scalar_one_or_none() def resolve_template( category_key: str | None = None, output_type_id: str | None = None, ) -> RenderTemplate | None: """Find the best matching active render template. Uses the M2M render_template_output_types table for output type matching. Uses sync SQLAlchemy — safe for Celery tasks. """ engine = _get_engine() with Session(engine) as session: return resolve_template_for_session( session, category_key=category_key, output_type_id=output_type_id, ) def get_material_library_path_for_session(session: Session) -> str | None: """Return the active material library path on an existing sync session.""" from app.domains.materials.models import AssetLibrary row = session.execute( select(AssetLibrary).where(AssetLibrary.is_active == True).limit(1) # noqa: E712 ).scalar_one_or_none() if row and row.blend_file_path: return row.blend_file_path row = session.execute( select(SystemSetting).where(SystemSetting.key == "material_library_path") ).scalar_one_or_none() if row and row.value and row.value.strip(): return row.value.strip() return None def get_material_library_path() -> str | None: """Return the blend_file_path of the first active AssetLibrary. Falls back to the legacy material_library_path system setting. """ engine = _get_engine() with Session(engine) as session: return get_material_library_path_for_session(session)