databricks-tellr-app 0.4.3.dev1__tar.gz → 0.4.3.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/PKG-INFO +1 -1
  2. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/databricks_tellr_app.egg-info/PKG-INFO +1 -1
  3. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/pyproject.toml +1 -1
  4. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/middleware/security_headers.py +22 -0
  5. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/images.py +27 -45
  6. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/schemas/requests.py +2 -2
  7. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/services/chat_service.py +9 -12
  8. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/database.py +0 -40
  9. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/image.py +1 -15
  10. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/image_service.py +9 -9
  11. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/image_tools.py +2 -2
  12. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/utils/image_utils.py +4 -5
  13. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/README.md +0 -0
  14. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/databricks_tellr_app/__init__.py +0 -0
  15. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/databricks_tellr_app/run.py +0 -0
  16. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/databricks_tellr_app.egg-info/SOURCES.txt +0 -0
  17. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
  18. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/databricks_tellr_app.egg-info/requires.txt +0 -0
  19. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/databricks_tellr_app.egg-info/top_level.txt +0 -0
  20. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/setup.cfg +0 -0
  21. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/setup.py +0 -0
  22. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/__init__.py +0 -0
  23. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/__init__.py +0 -0
  24. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/_otel_bootstrap.py +0 -0
  25. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/fixtures/tour_demo_deck.json +0 -0
  26. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/main.py +0 -0
  27. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/mcp_auth.py +0 -0
  28. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/mcp_server.py +0 -0
  29. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/middleware/__init__.py +0 -0
  30. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/middleware/csrf.py +0 -0
  31. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/middleware/request_logging.py +0 -0
  32. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/__init__.py +0 -0
  33. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/_authz.py +0 -0
  34. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/admin.py +0 -0
  35. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/admin_usage.py +0 -0
  36. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/agent_config.py +0 -0
  37. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/chat.py +0 -0
  38. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/deck_contributors.py +0 -0
  39. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/export.py +0 -0
  40. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/feedback.py +0 -0
  41. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/google_slides.py +0 -0
  42. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/local_version.py +0 -0
  43. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/profiles.py +0 -0
  44. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/sessions.py +0 -0
  45. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/settings/__init__.py +0 -0
  46. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/settings/contributors.py +0 -0
  47. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/settings/deck_prompts.py +0 -0
  48. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/settings/design_systems.py +0 -0
  49. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/settings/identities.py +0 -0
  50. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/settings/slide_styles.py +0 -0
  51. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/setup.py +0 -0
  52. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/slides.py +0 -0
  53. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/tools.py +0 -0
  54. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/tour.py +0 -0
  55. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/verification.py +0 -0
  56. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/routes/version.py +0 -0
  57. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/schemas/__init__.py +0 -0
  58. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/schemas/agent_config.py +0 -0
  59. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/schemas/feedback.py +0 -0
  60. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/schemas/responses.py +0 -0
  61. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/schemas/settings/__init__.py +0 -0
  62. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/schemas/settings/requests.py +0 -0
  63. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/schemas/settings/responses.py +0 -0
  64. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/schemas/streaming.py +0 -0
  65. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/services/__init__.py +0 -0
  66. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/services/export_job_queue.py +0 -0
  67. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/services/feedback_service.py +0 -0
  68. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/services/job_queue.py +0 -0
  69. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/services/session_manager.py +0 -0
  70. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/services/session_naming.py +0 -0
  71. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/services/usage_events.py +0 -0
  72. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/services/usage_service.py +0 -0
  73. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/utils/__init__.py +0 -0
  74. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/api/utils/validation.py +0 -0
  75. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/__init__.py +0 -0
  76. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/config_loader.py +0 -0
  77. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/context_utils.py +0 -0
  78. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/databricks_client.py +0 -0
  79. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/defaults.py +0 -0
  80. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/encryption.py +0 -0
  81. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/init_default_profile.py +0 -0
  82. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/lakebase.py +0 -0
  83. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/migrate_profiles_to_agent_config.py +0 -0
  84. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/mlflow_agent_spans.py +0 -0
  85. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/mlflow_tracing.py +0 -0
  86. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/otel_logging.py +0 -0
  87. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/permission_context.py +0 -0
  88. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/prompt_modules.py +0 -0
  89. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/settings_db.py +0 -0
  90. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/core/user_context.py +0 -0
  91. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/__init__.py +0 -0
  92. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/__init__.py +0 -0
  93. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/deck_contributor.py +0 -0
  94. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/design_system.py +0 -0
  95. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/encryption_key.py +0 -0
  96. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/feedback.py +0 -0
  97. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/genie_space.py +0 -0
  98. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/google_global_credentials.py +0 -0
  99. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/google_oauth_token.py +0 -0
  100. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/identity.py +0 -0
  101. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/profile.py +0 -0
  102. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/profile_contributor.py +0 -0
  103. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/prompts.py +0 -0
  104. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/request_log.py +0 -0
  105. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/session.py +0 -0
  106. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/slide_deck_prompt.py +0 -0
  107. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/slide_style_library.py +0 -0
  108. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/usage_event.py +0 -0
  109. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/models/user_preference.py +0 -0
  110. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/database/types.py +0 -0
  111. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/domain/__init__.py +0 -0
  112. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/domain/slide.py +0 -0
  113. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/domain/slide_deck.py +0 -0
  114. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/__init__.py +0 -0
  115. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/agent.py +0 -0
  116. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/agent_factory.py +0 -0
  117. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/config_service.py +0 -0
  118. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/config_validator.py +0 -0
  119. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/converter_jail/__init__.py +0 -0
  120. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/converter_jail/ast_guard.py +0 -0
  121. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/converter_jail/codeprep.py +0 -0
  122. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/converter_jail/gslides_runner.py +0 -0
  123. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/converter_jail/jail.py +0 -0
  124. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/converter_jail/pptx_runner.py +0 -0
  125. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/converter_jail/protocol.py +0 -0
  126. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/design_system_compiler.py +0 -0
  127. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/design_system_service.py +0 -0
  128. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/design_system_templates.py +0 -0
  129. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/drive_uploader.py +0 -0
  130. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/evaluation/__init__.py +0 -0
  131. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/evaluation/llm_judge.py +0 -0
  132. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/genie_service.py +0 -0
  133. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/google_slides_auth.py +0 -0
  134. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/google_slides_prompts_defaults.py +0 -0
  135. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/html_to_google_slides.py +0 -0
  136. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/html_to_pptx.py +0 -0
  137. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/identity_provider.py +0 -0
  138. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/identity_providers/__init__.py +0 -0
  139. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/identity_providers/account_provider.py +0 -0
  140. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/identity_providers/local_provider.py +0 -0
  141. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/identity_providers/workspace_provider.py +0 -0
  142. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/permission_service.py +0 -0
  143. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/pptx_from_html_huashu.py +0 -0
  144. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/pptx_from_records.py +0 -0
  145. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/pptx_prompts_defaults.py +0 -0
  146. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/profile_service.py +0 -0
  147. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/streaming_callback.py +0 -0
  148. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/tools/__init__.py +0 -0
  149. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/tools/agent_bricks_tool.py +0 -0
  150. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/tools/ds_asset_tool.py +0 -0
  151. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/tools/genie_tool.py +0 -0
  152. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/tools/mcp_tool.py +0 -0
  153. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/tools/model_endpoint_tool.py +0 -0
  154. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/tools/vector_tool.py +0 -0
  155. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/services/validator.py +0 -0
  156. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/utils/__init__.py +0 -0
  157. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/utils/css_utils.py +0 -0
  158. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/utils/ds_asset_utils.py +0 -0
  159. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/utils/error_handling.py +0 -0
  160. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/utils/html_safety.py +0 -0
  161. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/utils/html_utils.py +0 -0
  162. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/utils/js_validator.py +0 -0
  163. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/utils/logging_config.py +0 -0
  164. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/utils/pi_filter.py +0 -0
  165. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/utils/slide_hash.py +0 -0
  166. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/utils/spotlight.py +0 -0
  167. {databricks_tellr_app-0.4.3.dev1 → databricks_tellr_app-0.4.3.dev2}/src/utils/text_caps.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks-tellr-app
3
- Version: 0.4.3.dev1
3
+ Version: 0.4.3.dev2
4
4
  Summary: Tellr application package for Databricks Apps
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: databricks-tellr-app
3
- Version: 0.4.3.dev1
3
+ Version: 0.4.3.dev2
4
4
  Summary: Tellr application package for Databricks Apps
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "databricks-tellr-app"
7
- version = "0.4.3.dev1"
7
+ version = "0.4.3.dev2"
8
8
  description = "Tellr application package for Databricks Apps"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -31,6 +31,28 @@ Content-Security-Policy differentiated by response type:
31
31
  'none', no eval) plus the html_safety.py scanner, and any header at
32
32
  all is a strict improvement over the previous no-CSP state.
33
33
 
34
+ SDR-4437 M-CSP — ACCEPTED RESIDUAL (pending SE sign-off). M-CSP asks
35
+ to move ``script-src`` off ``'unsafe-inline'`` to hashes/nonces. That
36
+ is infeasible here and was verified:
37
+ * The SPA's own document has NO inline <script> — index.html loads a
38
+ single external module (``/src/main.tsx``), already covered by
39
+ ``'self'``. So the SPA itself does not need 'unsafe-inline'.
40
+ * The 'unsafe-inline' exists ONLY for the srcdoc slide iframes, whose
41
+ generated HTML contains arbitrary inline <script> (Chart.js init,
42
+ etc.) and which inherit THIS document policy in addition to their
43
+ own SLIDE_CSP meta (both enforce; most restrictive wins).
44
+ * Per CSP2+, adding any nonce or hash to ``script-src`` makes the
45
+ browser IGNORE 'unsafe-inline'. A nonce on the parent would
46
+ therefore disable inline execution for the inherited srcdoc slide
47
+ scripts and break slide rendering app-wide — you cannot have
48
+ "nonce + unsafe-inline" both take effect.
49
+ Removing it would require re-architecting slides away from srcdoc
50
+ (backend-served ``src`` iframes), which the frame-ancestors / no-src
51
+ design above deliberately avoids. The residual is bounded: slides stay
52
+ confined by SLIDE_CSP (an egress boundary) + the html_safety scanner,
53
+ and the DOCUMENT_CSP ⊇ SLIDE_CSP superset invariant is locked by
54
+ tests/unit/test_export_csp.py::test_document_csp_header_is_superset_of_slide_csp.
55
+
34
56
  - Everything else (JSON APIs, assets, SSE) gets the deny-all
35
57
  ``API_CSP`` — nothing executes in a JSON response.
36
58
 
@@ -1,5 +1,4 @@
1
1
  """Image upload and management API endpoints."""
2
- import base64
3
2
  import logging
4
3
  import os
5
4
  from typing import List, Optional
@@ -21,7 +20,7 @@ router = APIRouter(prefix="/api/images", tags=["images"])
21
20
 
22
21
  class ImageResponse(BaseModel):
23
22
  """Response schema for image metadata."""
24
- id: str # Opaque token (SDR-4437 F-TM-7) — the internal int PK is never exposed.
23
+ id: int
25
24
  filename: str
26
25
  original_filename: str
27
26
  mime_type: str
@@ -47,7 +46,7 @@ class ImageListResponse(BaseModel):
47
46
 
48
47
  class ImageDataResponse(BaseModel):
49
48
  """Response containing image base64 data."""
50
- id: str # Opaque token (SDR-4437 F-TM-7).
49
+ id: int
51
50
  mime_type: str
52
51
  base64_data: str
53
52
  data_uri: str # Ready-to-use: "data:image/png;base64,..."
@@ -81,7 +80,7 @@ def _get_current_user() -> str:
81
80
 
82
81
  def _image_to_response(img: ImageAsset) -> ImageResponse:
83
82
  return ImageResponse(
84
- id=img.token, # Expose the opaque token, never the enumerable int PK.
83
+ id=img.id,
85
84
  filename=img.filename,
86
85
  original_filename=img.original_filename,
87
86
  mime_type=img.mime_type,
@@ -168,75 +167,58 @@ def list_images(
168
167
  )
169
168
 
170
169
 
171
- def _deny_ephemeral_cross_user(image: ImageAsset) -> None:
172
- """Defense-in-depth (SDR-4437 F-TM-7): chat-pasted ("ephemeral") images are
173
- private to their uploader. Library images stay open-read. Raise 404 (not 403)
174
- so a wrong caller cannot even confirm the image exists.
175
- """
176
- if image.category == "ephemeral" and image.uploaded_by != _get_current_user():
177
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Image not found")
178
-
179
-
180
- @router.get("/{token}", response_model=ImageResponse)
181
- def get_image(token: str, db: Session = Depends(get_db)):
182
- """Get image metadata by opaque token."""
170
+ @router.get("/{image_id}", response_model=ImageResponse)
171
+ def get_image(image_id: int, db: Session = Depends(get_db)):
172
+ """Get image metadata by ID."""
183
173
  try:
184
174
  image = db.query(ImageAsset).filter(
185
- ImageAsset.token == token,
175
+ ImageAsset.id == image_id,
186
176
  ImageAsset.is_active == True,
187
177
  ).first()
188
178
  if not image:
189
179
  raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Image not found")
190
- _deny_ephemeral_cross_user(image)
191
180
  return _image_to_response(image)
192
181
  except HTTPException:
193
182
  raise
194
183
  except Exception as e:
195
- logger.error(f"Error getting image: {e}", exc_info=True)
184
+ logger.error(f"Error getting image {image_id}: {e}", exc_info=True)
196
185
  raise HTTPException(
197
186
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
198
187
  detail="Failed to get image",
199
188
  )
200
189
 
201
190
 
202
- @router.get("/{token}/data", response_model=ImageDataResponse)
203
- def get_image_data(token: str, db: Session = Depends(get_db)):
204
- """Get full image as base64 data (for embedding in slides), keyed by token."""
191
+ @router.get("/{image_id}/data", response_model=ImageDataResponse)
192
+ def get_image_data(image_id: int, db: Session = Depends(get_db)):
193
+ """Get full image as base64 data (for embedding in slides)."""
205
194
  try:
206
- image = db.query(ImageAsset).filter(
207
- ImageAsset.token == token,
208
- ImageAsset.is_active == True,
209
- ).first()
210
- if not image:
211
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Image not found")
212
- _deny_ephemeral_cross_user(image)
213
- b64_data = base64.b64encode(image.image_data).decode("utf-8")
195
+ b64_data, mime_type = image_service.get_image_base64(db, image_id)
214
196
  return ImageDataResponse(
215
- id=image.token,
216
- mime_type=image.mime_type,
197
+ id=image_id,
198
+ mime_type=mime_type,
217
199
  base64_data=b64_data,
218
- data_uri=f"data:{image.mime_type};base64,{b64_data}",
200
+ data_uri=f"data:{mime_type};base64,{b64_data}",
219
201
  )
220
- except HTTPException:
221
- raise
202
+ except ValueError as e:
203
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
222
204
  except Exception as e:
223
- logger.error(f"Error getting image data: {e}", exc_info=True)
205
+ logger.error(f"Error getting image data {image_id}: {e}", exc_info=True)
224
206
  raise HTTPException(
225
207
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
226
208
  detail="Failed to get image data",
227
209
  )
228
210
 
229
211
 
230
- @router.put("/{token}", response_model=ImageResponse)
212
+ @router.put("/{image_id}", response_model=ImageResponse)
231
213
  def update_image(
232
- token: str,
214
+ image_id: int,
233
215
  request: ImageUpdateRequest,
234
216
  db: Session = Depends(get_db),
235
217
  ):
236
218
  """Update image metadata (tags, description, category)."""
237
219
  try:
238
220
  image = db.query(ImageAsset).filter(
239
- ImageAsset.token == token,
221
+ ImageAsset.id == image_id,
240
222
  ImageAsset.is_active == True,
241
223
  ).first()
242
224
  if not image:
@@ -264,20 +246,20 @@ def update_image(
264
246
  raise
265
247
  except Exception as e:
266
248
  db.rollback()
267
- logger.error(f"Error updating image: {e}", exc_info=True)
249
+ logger.error(f"Error updating image {image_id}: {e}", exc_info=True)
268
250
  raise HTTPException(
269
251
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
270
252
  detail="Failed to update image",
271
253
  )
272
254
 
273
255
 
274
- @router.delete("/{token}", status_code=status.HTTP_204_NO_CONTENT)
275
- def delete_image(token: str, db: Session = Depends(get_db)):
256
+ @router.delete("/{image_id}", status_code=status.HTTP_204_NO_CONTENT)
257
+ def delete_image(image_id: int, db: Session = Depends(get_db)):
276
258
  """Soft-delete an image."""
277
259
  try:
278
260
  user = _get_current_user()
279
261
  image = db.query(ImageAsset).filter(
280
- ImageAsset.token == token,
262
+ ImageAsset.id == image_id,
281
263
  ImageAsset.is_active == True, # noqa: E712
282
264
  ).first()
283
265
  if not image:
@@ -288,14 +270,14 @@ def delete_image(token: str, db: Session = Depends(get_db)):
288
270
  status_code=status.HTTP_403_FORBIDDEN,
289
271
  detail="You don't have permission to modify this image",
290
272
  )
291
- image_service.delete_image(db, token, user)
273
+ image_service.delete_image(db, image_id, user)
292
274
  except HTTPException:
293
275
  raise
294
276
  except ValueError as e:
295
277
  raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))
296
278
  except Exception as e:
297
279
  db.rollback()
298
- logger.error(f"Error deleting image: {e}", exc_info=True)
280
+ logger.error(f"Error deleting image {image_id}: {e}", exc_info=True)
299
281
  raise HTTPException(
300
282
  status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
301
283
  detail="Failed to delete image",
@@ -74,9 +74,9 @@ class ChatRequest(BaseModel):
74
74
  default=None,
75
75
  description="Optional context containing the slides to edit",
76
76
  )
77
- image_ids: Optional[list[str]] = Field(
77
+ image_ids: Optional[list[int]] = Field(
78
78
  default=None,
79
- description="Opaque tokens of images attached to this message (SDR-4437 F-TM-7)",
79
+ description="IDs of images attached to this message (from upload or paste)",
80
80
  )
81
81
  agent_config: Optional[dict] = Field(
82
82
  default=None,
@@ -345,7 +345,7 @@ class ChatService:
345
345
  session_id: str,
346
346
  message: str,
347
347
  slide_context: Optional[Dict[str, Any]] = None,
348
- image_ids: Optional[List[str]] = None,
348
+ image_ids: Optional[List[int]] = None,
349
349
  ) -> Dict[str, Any]:
350
350
  """Send a message to the agent and get response.
351
351
 
@@ -828,7 +828,7 @@ class ChatService:
828
828
  message: str,
829
829
  slide_context: Optional[Dict[str, Any]] = None,
830
830
  request_id: Optional[str] = None,
831
- image_ids: Optional[List[str]] = None,
831
+ image_ids: Optional[List[int]] = None,
832
832
  is_first_message_override: Optional[bool] = None,
833
833
  ) -> Generator[StreamEvent, None, None]:
834
834
  """Send a message and yield streaming events.
@@ -1993,30 +1993,27 @@ class ChatService:
1993
1993
 
1994
1994
  return ([], None)
1995
1995
 
1996
- def _inject_image_context(self, message: str, image_ids: List[str]) -> str:
1997
- """Append image metadata to user message so the agent knows about attached images.
1998
-
1999
- ``image_ids`` are opaque image tokens (SDR-4437 F-TM-7), not int PKs.
2000
- """
1996
+ def _inject_image_context(self, message: str, image_ids: List[int]) -> str:
1997
+ """Append image metadata to user message so the agent knows about attached images."""
2001
1998
  from src.core.database import get_db_session
2002
1999
  from src.services import image_service
2003
2000
 
2004
2001
  image_descriptions = []
2005
2002
  with get_db_session() as db:
2006
- for img_token in image_ids:
2003
+ for img_id in image_ids:
2007
2004
  try:
2008
2005
  img = db.query(image_service.ImageAsset).filter(
2009
- image_service.ImageAsset.token == img_token,
2006
+ image_service.ImageAsset.id == img_id,
2010
2007
  image_service.ImageAsset.is_active == True,
2011
2008
  ).first()
2012
2009
  if img:
2013
2010
  image_descriptions.append(
2014
- f'- Image ID {img.token}: "{img.original_filename}" '
2011
+ f'- Image ID {img.id}: "{img.original_filename}" '
2015
2012
  f'({img.description or "no description"}). '
2016
- f'Use: <img src="{{{{image:{img.token}}}}}" alt="{img.description or img.original_filename}" />'
2013
+ f'Use: <img src="{{{{image:{img.id}}}}}" alt="{img.description or img.original_filename}" />'
2017
2014
  )
2018
2015
  except Exception as e:
2019
- logger.warning(f"Failed to load image {img_token} for context injection: {e}")
2016
+ logger.warning(f"Failed to load image {img_id} for context injection: {e}")
2020
2017
 
2021
2018
  if not image_descriptions:
2022
2019
  return message
@@ -524,8 +524,6 @@ def _run_migrations(engine, schema: str | None = None):
524
524
  # --- image_assets.tags: json → jsonb (PostgreSQL) for native @> queries ---
525
525
  _migrate_image_assets_tags_json_to_jsonb(conn, schema, _qual, is_sqlite)
526
526
 
527
- # --- image_assets.token: unguessable external id + backfill (SDR-4437 F-TM-7) ---
528
- _migrate_image_assets_add_token(conn, inspector, schema, _qual, is_sqlite)
529
527
  # --- agent_config: style precedence in the DB, for the raw-SQL writers no
530
528
  # --- Python-side bind hook can see (PostgreSQL only; no-op on SQLite) ---
531
529
  _migrate_agent_config_precedence_trigger(
@@ -1060,44 +1058,6 @@ def _ensure_llm_judge_backend_default(
1060
1058
  )
1061
1059
 
1062
1060
 
1063
- def _migrate_image_assets_add_token(conn, inspector, schema, _qual, is_sqlite) -> None:
1064
- """Add the unguessable ``token`` column to image_assets and backfill (SDR-4437 F-TM-7).
1065
-
1066
- The int PK is enumerable, so image reads now key on an opaque token. Fresh
1067
- installs get the column via create_all(); existing tables need this ALTER +
1068
- per-row backfill. Idempotent: gated on the column's absence.
1069
- """
1070
- import secrets
1071
-
1072
- from sqlalchemy import text
1073
-
1074
- table = "image_assets"
1075
- try:
1076
- cols = {c["name"] for c in inspector.get_columns(table, schema=schema)}
1077
- except Exception:
1078
- return
1079
- if not cols or "token" in cols:
1080
- return
1081
-
1082
- q = _qual(table)
1083
- logger.info(f"Migration: adding token column to {table} (SDR-4437 F-TM-7)")
1084
- # Add nullable first so the ALTER succeeds on a populated table.
1085
- conn.execute(text(f"ALTER TABLE {q} ADD COLUMN token VARCHAR(64)"))
1086
-
1087
- # Backfill every existing row with a distinct unguessable token.
1088
- row_ids = [r[0] for r in conn.execute(text(f"SELECT id FROM {q} WHERE token IS NULL")).fetchall()]
1089
- for row_id in row_ids:
1090
- conn.execute(
1091
- text(f"UPDATE {q} SET token = :tok WHERE id = :id"),
1092
- {"tok": secrets.token_urlsafe(32), "id": row_id},
1093
- )
1094
-
1095
- # Enforce uniqueness (and non-null on PostgreSQL, now that every row has a token).
1096
- conn.execute(text(
1097
- f"CREATE UNIQUE INDEX IF NOT EXISTS ix_image_assets_token ON {q} (token)"
1098
- ))
1099
- if not is_sqlite:
1100
- conn.execute(text(f"ALTER TABLE {q} ALTER COLUMN token SET NOT NULL"))
1101
1061
  #: The one place the precedence rule is written in SQL. ``$$``-quoted so the body
1102
1062
  #: needs no escaping, and named after the rule rather than the tables, since both
1103
1063
  #: ``agent_config`` columns share it.
@@ -1,5 +1,4 @@
1
1
  """Image asset model — metadata and binary data stored together in Lakebase."""
2
- import secrets
3
2
  from datetime import datetime
4
3
 
5
4
  from sqlalchemy import Boolean, Column, DateTime, Integer, JSON, LargeBinary, String, Text
@@ -11,17 +10,6 @@ from src.core.database import Base
11
10
  _TagsColumn = JSON().with_variant(JSONB(), "postgresql")
12
11
 
13
12
 
14
- def _new_image_token() -> str:
15
- """Unguessable external identifier (SDR-4437 F-TM-7).
16
-
17
- The autoincrement int PK is enumerable, so it is never exposed. This
18
- opaque token is the only identifier that leaves the server — it is what
19
- the API returns, what chat sends in ``image_ids``, and what goes in
20
- ``{{image:...}}`` slide placeholders.
21
- """
22
- return secrets.token_urlsafe(32)
23
-
24
-
25
13
  class ImageAsset(Base):
26
14
  """Uploaded image with binary data stored directly in PostgreSQL.
27
15
 
@@ -32,9 +20,7 @@ class ImageAsset(Base):
32
20
 
33
21
  __tablename__ = "image_assets"
34
22
 
35
- id = Column(Integer, primary_key=True) # Internal only — never serialized.
36
- # Unguessable external identifier (SDR-4437 F-TM-7). token_urlsafe(32) → 43 chars.
37
- token = Column(String(64), unique=True, nullable=False, index=True, default=_new_image_token)
23
+ id = Column(Integer, primary_key=True)
38
24
  filename = Column(String(255), nullable=False) # Generated: {uuid}.{ext}
39
25
  original_filename = Column(String(255), nullable=False) # User's original filename
40
26
  mime_type = Column(String(50), nullable=False) # image/png, image/jpeg, image/gif, image/svg+xml
@@ -51,7 +51,7 @@ def upload_image(
51
51
  ).first()
52
52
  if existing:
53
53
  raise ValueError(
54
- f"An image named '{original_filename}' already exists. "
54
+ f"An image named '{original_filename}' already exists (id={existing.id}). "
55
55
  "Please rename the file or delete the existing image first."
56
56
  )
57
57
 
@@ -86,19 +86,19 @@ def upload_image(
86
86
  return image
87
87
 
88
88
 
89
- def get_image_base64(db: Session, token: str) -> tuple[str, str]:
89
+ def get_image_base64(db: Session, image_id: int) -> tuple[str, str]:
90
90
  """
91
- Get full image as base64 string, keyed by the opaque token (SDR-4437 F-TM-7).
91
+ Get full image as base64 string.
92
92
 
93
93
  Returns:
94
94
  Tuple of (base64_data, mime_type)
95
95
  """
96
96
  image = db.query(ImageAsset).filter(
97
- ImageAsset.token == token,
97
+ ImageAsset.id == image_id,
98
98
  ImageAsset.is_active == True,
99
99
  ).first()
100
100
  if not image:
101
- raise ValueError(f"Image {token} not found")
101
+ raise ValueError(f"Image {image_id} not found")
102
102
 
103
103
  b64 = base64.b64encode(image.image_data).decode("utf-8")
104
104
  return b64, image.mime_type
@@ -175,11 +175,11 @@ def search_images(
175
175
  ).all()
176
176
 
177
177
 
178
- def delete_image(db: Session, token: str, user: str) -> None:
179
- """Soft-delete an image, keyed by the opaque token (SDR-4437 F-TM-7)."""
180
- image = db.query(ImageAsset).filter(ImageAsset.token == token).first()
178
+ def delete_image(db: Session, image_id: int, user: str) -> None:
179
+ """Soft-delete an image."""
180
+ image = db.query(ImageAsset).filter(ImageAsset.id == image_id).first()
181
181
  if not image:
182
- raise ValueError(f"Image {token} not found")
182
+ raise ValueError(f"Image {image_id} not found")
183
183
 
184
184
  image.is_active = False
185
185
  image.updated_by = user
@@ -53,13 +53,13 @@ def search_images(
53
53
  # Must build results inside session scope to avoid DetachedInstanceError
54
54
  results = [
55
55
  {
56
- "id": img.token, # Opaque token (SDR-4437 F-TM-7), never the int PK.
56
+ "id": img.id,
57
57
  "filename": img.original_filename,
58
58
  "description": img.description,
59
59
  "tags": img.tags,
60
60
  "category": img.category,
61
61
  "mime_type": img.mime_type,
62
- "usage": f'<img src="{{{{image:{img.token}}}}}" alt="{img.description or img.original_filename}" />',
62
+ "usage": f'<img src="{{{{image:{img.id}}}}}" alt="{img.description or img.original_filename}" />',
63
63
  }
64
64
  for img in images
65
65
  ]
@@ -8,8 +8,7 @@ from src.services import image_service
8
8
 
9
9
  logger = logging.getLogger(__name__)
10
10
 
11
- # Tokens are secrets.token_urlsafe(...) → [A-Za-z0-9_-] (SDR-4437 F-TM-7).
12
- IMAGE_PLACEHOLDER_PATTERN = re.compile(r"\{\{image:([A-Za-z0-9_-]+)\}\}")
11
+ IMAGE_PLACEHOLDER_PATTERN = re.compile(r"\{\{image:(\d+)\}\}")
13
12
 
14
13
 
15
14
  def substitute_image_placeholders(html: str, db: Session) -> str:
@@ -23,12 +22,12 @@ def substitute_image_placeholders(html: str, db: Session) -> str:
23
22
  return html
24
23
 
25
24
  def replace_match(match):
26
- image_token = match.group(1)
25
+ image_id = int(match.group(1))
27
26
  try:
28
- b64_data, mime_type = image_service.get_image_base64(db, image_token)
27
+ b64_data, mime_type = image_service.get_image_base64(db, image_id)
29
28
  return f"data:{mime_type};base64,{b64_data}"
30
29
  except Exception as e:
31
- logger.warning(f"Failed to resolve image placeholder {{{{image:{image_token}}}}}: {e}")
30
+ logger.warning(f"Failed to resolve image placeholder {{{{image:{image_id}}}}}: {e}")
32
31
  return match.group(0) # Leave placeholder if image not found
33
32
 
34
33
  return IMAGE_PLACEHOLDER_PATTERN.sub(replace_match, html)