databricks-tellr-app 0.3.13.dev1__tar.gz → 0.3.13.dev3__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 (155) hide show
  1. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/PKG-INFO +1 -1
  2. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/databricks_tellr_app.egg-info/PKG-INFO +1 -1
  3. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/databricks_tellr_app.egg-info/SOURCES.txt +14 -0
  4. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/pyproject.toml +1 -1
  5. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/chat_service.py +9 -70
  6. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/export_job_queue.py +10 -23
  7. databricks_tellr_app-0.3.13.dev3/src/services/converter_jail/__init__.py +10 -0
  8. databricks_tellr_app-0.3.13.dev3/src/services/converter_jail/ast_guard.py +58 -0
  9. databricks_tellr_app-0.3.13.dev3/src/services/converter_jail/codeprep.py +220 -0
  10. databricks_tellr_app-0.3.13.dev3/src/services/converter_jail/gslides_runner.py +78 -0
  11. databricks_tellr_app-0.3.13.dev3/src/services/converter_jail/jail.py +230 -0
  12. databricks_tellr_app-0.3.13.dev3/src/services/converter_jail/pptx_runner.py +124 -0
  13. databricks_tellr_app-0.3.13.dev3/src/services/converter_jail/protocol.py +41 -0
  14. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/google_slides_prompts_defaults.py +28 -18
  15. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/html_to_google_slides.py +246 -185
  16. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/html_to_pptx.py +73 -453
  17. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/README.md +0 -0
  18. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/databricks_tellr_app/__init__.py +0 -0
  19. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/databricks_tellr_app/run.py +0 -0
  20. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
  21. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/databricks_tellr_app.egg-info/requires.txt +0 -0
  22. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/databricks_tellr_app.egg-info/top_level.txt +0 -0
  23. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/setup.cfg +0 -0
  24. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/setup.py +0 -0
  25. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/__init__.py +0 -0
  26. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/__init__.py +0 -0
  27. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/_otel_bootstrap.py +0 -0
  28. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/fixtures/tour_demo_deck.json +0 -0
  29. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/main.py +0 -0
  30. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/mcp_auth.py +0 -0
  31. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/mcp_server.py +0 -0
  32. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/middleware/__init__.py +0 -0
  33. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/middleware/request_logging.py +0 -0
  34. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/__init__.py +0 -0
  35. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/admin.py +0 -0
  36. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/admin_usage.py +0 -0
  37. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/agent_config.py +0 -0
  38. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/chat.py +0 -0
  39. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/deck_contributors.py +0 -0
  40. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/export.py +0 -0
  41. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/feedback.py +0 -0
  42. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/google_slides.py +0 -0
  43. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/images.py +0 -0
  44. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/local_version.py +0 -0
  45. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/profiles.py +0 -0
  46. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/sessions.py +0 -0
  47. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/settings/__init__.py +0 -0
  48. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/settings/contributors.py +0 -0
  49. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/settings/deck_prompts.py +0 -0
  50. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/settings/identities.py +0 -0
  51. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/settings/slide_styles.py +0 -0
  52. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/setup.py +0 -0
  53. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/slides.py +0 -0
  54. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/tools.py +0 -0
  55. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/tour.py +0 -0
  56. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/verification.py +0 -0
  57. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/routes/version.py +0 -0
  58. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/__init__.py +0 -0
  59. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/agent_config.py +0 -0
  60. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/feedback.py +0 -0
  61. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/requests.py +0 -0
  62. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/responses.py +0 -0
  63. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/settings/__init__.py +0 -0
  64. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/settings/requests.py +0 -0
  65. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/settings/responses.py +0 -0
  66. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/schemas/streaming.py +0 -0
  67. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/__init__.py +0 -0
  68. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/feedback_service.py +0 -0
  69. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/job_queue.py +0 -0
  70. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/session_manager.py +0 -0
  71. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/session_naming.py +0 -0
  72. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/usage_events.py +0 -0
  73. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/services/usage_service.py +0 -0
  74. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/utils/__init__.py +0 -0
  75. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/api/utils/validation.py +0 -0
  76. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/__init__.py +0 -0
  77. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/config_loader.py +0 -0
  78. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/context_utils.py +0 -0
  79. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/database.py +0 -0
  80. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/databricks_client.py +0 -0
  81. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/defaults.py +0 -0
  82. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/encryption.py +0 -0
  83. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/init_default_profile.py +0 -0
  84. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/lakebase.py +0 -0
  85. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/migrate_profiles_to_agent_config.py +0 -0
  86. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/mlflow_agent_spans.py +0 -0
  87. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/mlflow_tracing.py +0 -0
  88. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/otel_logging.py +0 -0
  89. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/permission_context.py +0 -0
  90. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/prompt_modules.py +0 -0
  91. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/settings_db.py +0 -0
  92. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/core/user_context.py +0 -0
  93. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/__init__.py +0 -0
  94. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/__init__.py +0 -0
  95. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/deck_contributor.py +0 -0
  96. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/feedback.py +0 -0
  97. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/genie_space.py +0 -0
  98. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/google_global_credentials.py +0 -0
  99. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/google_oauth_token.py +0 -0
  100. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/identity.py +0 -0
  101. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/image.py +0 -0
  102. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/profile.py +0 -0
  103. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/profile_contributor.py +0 -0
  104. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/prompts.py +0 -0
  105. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/request_log.py +0 -0
  106. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/session.py +0 -0
  107. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/slide_deck_prompt.py +0 -0
  108. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/slide_style_library.py +0 -0
  109. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/usage_event.py +0 -0
  110. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/database/models/user_preference.py +0 -0
  111. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/domain/__init__.py +0 -0
  112. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/domain/slide.py +0 -0
  113. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/domain/slide_deck.py +0 -0
  114. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/__init__.py +0 -0
  115. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/agent.py +0 -0
  116. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/agent_factory.py +0 -0
  117. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/config_service.py +0 -0
  118. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/config_validator.py +0 -0
  119. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/drive_uploader.py +0 -0
  120. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/evaluation/__init__.py +0 -0
  121. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/evaluation/llm_judge.py +0 -0
  122. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/genie_service.py +0 -0
  123. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/google_slides_auth.py +0 -0
  124. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/identity_provider.py +0 -0
  125. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/identity_providers/__init__.py +0 -0
  126. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/identity_providers/account_provider.py +0 -0
  127. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/identity_providers/local_provider.py +0 -0
  128. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/identity_providers/workspace_provider.py +0 -0
  129. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/image_service.py +0 -0
  130. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/image_tools.py +0 -0
  131. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/permission_service.py +0 -0
  132. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/pptx_from_html_huashu.py +0 -0
  133. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/pptx_from_records.py +0 -0
  134. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/pptx_prompts_defaults.py +0 -0
  135. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/profile_service.py +0 -0
  136. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/streaming_callback.py +0 -0
  137. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/tools/__init__.py +0 -0
  138. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/tools/agent_bricks_tool.py +0 -0
  139. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/tools/genie_tool.py +0 -0
  140. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/tools/mcp_tool.py +0 -0
  141. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/tools/model_endpoint_tool.py +0 -0
  142. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/tools/vector_tool.py +0 -0
  143. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/services/validator.py +0 -0
  144. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/__init__.py +0 -0
  145. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/css_utils.py +0 -0
  146. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/error_handling.py +0 -0
  147. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/html_safety.py +0 -0
  148. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/html_utils.py +0 -0
  149. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/image_utils.py +0 -0
  150. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/js_validator.py +0 -0
  151. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/logging_config.py +0 -0
  152. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/pi_filter.py +0 -0
  153. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/slide_hash.py +0 -0
  154. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/src/utils/spotlight.py +0 -0
  155. {databricks_tellr_app-0.3.13.dev1 → databricks_tellr_app-0.3.13.dev3}/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.3.13.dev1
3
+ Version: 0.3.13.dev3
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.3.13.dev1
3
+ Version: 0.3.13.dev3
4
4
  Summary: Tellr application package for Databricks Apps
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -115,6 +115,13 @@ setup.py
115
115
  ./src/services/profile_service.py
116
116
  ./src/services/streaming_callback.py
117
117
  ./src/services/validator.py
118
+ ./src/services/converter_jail/__init__.py
119
+ ./src/services/converter_jail/ast_guard.py
120
+ ./src/services/converter_jail/codeprep.py
121
+ ./src/services/converter_jail/gslides_runner.py
122
+ ./src/services/converter_jail/jail.py
123
+ ./src/services/converter_jail/pptx_runner.py
124
+ ./src/services/converter_jail/protocol.py
118
125
  ./src/services/evaluation/__init__.py
119
126
  ./src/services/evaluation/llm_judge.py
120
127
  ./src/services/identity_providers/__init__.py
@@ -258,6 +265,13 @@ src/services/pptx_prompts_defaults.py
258
265
  src/services/profile_service.py
259
266
  src/services/streaming_callback.py
260
267
  src/services/validator.py
268
+ src/services/converter_jail/__init__.py
269
+ src/services/converter_jail/ast_guard.py
270
+ src/services/converter_jail/codeprep.py
271
+ src/services/converter_jail/gslides_runner.py
272
+ src/services/converter_jail/jail.py
273
+ src/services/converter_jail/pptx_runner.py
274
+ src/services/converter_jail/protocol.py
261
275
  src/services/evaluation/__init__.py
262
276
  src/services/evaluation/llm_judge.py
263
277
  src/services/identity_providers/__init__.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "databricks-tellr-app"
7
- version = "0.3.13.dev1"
7
+ version = "0.3.13.dev3"
8
8
  description = "Tellr application package for Databricks Apps"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -80,11 +80,6 @@ class ChatService:
80
80
  # This avoids re-parsing HTML on every request
81
81
  self._deck_cache: Dict[str, SlideDeck] = {}
82
82
 
83
- # DB version each cached deck corresponds to. The cache is per-process
84
- # while prod runs multiple uvicorn workers sharing one database, so a
85
- # cache hit is only valid if the DB version hasn't moved on.
86
- self._deck_cache_versions: Dict[str, int] = {}
87
-
88
83
  logger.info("ChatService initialized successfully")
89
84
 
90
85
  def _substitute_images_for_response(self, deck_dict, raw_html=None):
@@ -605,7 +600,7 @@ class ChatService:
605
600
  slide_deck_dict = current_deck.to_dict()
606
601
 
607
602
  try:
608
- save_result = session_manager.save_slide_deck(
603
+ session_manager.save_slide_deck(
609
604
  session_id=session_id,
610
605
  title=current_deck.title,
611
606
  html_content=current_deck.knit(),
@@ -615,7 +610,6 @@ class ChatService:
615
610
  modified_by=_user,
616
611
  expected_version=_deck_version_before_llm,
617
612
  )
618
- self._record_deck_version(session_id, save_result)
619
613
  except VersionConflictError:
620
614
  logger.warning(
621
615
  "Chat save rejected: deck was edited during LLM call, reloading",
@@ -1391,7 +1385,7 @@ class ChatService:
1391
1385
  slide_deck_dict = current_deck.to_dict()
1392
1386
 
1393
1387
  try:
1394
- save_result = session_manager.save_slide_deck(
1388
+ session_manager.save_slide_deck(
1395
1389
  session_id=session_id,
1396
1390
  title=current_deck.title,
1397
1391
  html_content=current_deck.knit(),
@@ -1401,7 +1395,6 @@ class ChatService:
1401
1395
  modified_by=_user,
1402
1396
  expected_version=_deck_version_before_llm,
1403
1397
  )
1404
- self._record_deck_version(session_id, save_result)
1405
1398
  except VersionConflictError:
1406
1399
  logger.warning(
1407
1400
  "Chat save rejected: deck was edited during LLM call, reloading",
@@ -1928,63 +1921,18 @@ class ChatService:
1928
1921
  context = "\n\n[Attached images]\n" + "\n".join(image_descriptions)
1929
1922
  return message + context
1930
1923
 
1931
- def _deck_versions(self) -> Dict[str, int]:
1932
- """Return the cached-deck version map, creating it if needed.
1933
-
1934
- Lazy creation keeps instances built without __init__ (test harnesses
1935
- use ChatService.__new__) working. Callers must hold _cache_lock.
1936
- """
1937
- try:
1938
- return self._deck_cache_versions
1939
- except AttributeError:
1940
- self._deck_cache_versions = {}
1941
- return self._deck_cache_versions
1942
-
1943
- def _record_deck_version(self, session_id: str, save_result: Optional[Dict[str, Any]]) -> None:
1944
- """Remember which DB version the cached deck now corresponds to.
1945
-
1946
- Call after save_slide_deck so the next _get_or_load_deck cache hit
1947
- validates cleanly instead of reloading from the database.
1948
- """
1949
- version = (save_result or {}).get("version")
1950
- with self._cache_lock:
1951
- if version is not None:
1952
- self._deck_versions()[session_id] = version
1953
- else:
1954
- self._deck_versions().pop(session_id, None)
1955
-
1956
1924
  def _get_or_load_deck(self, session_id: str) -> Optional[SlideDeck]:
1957
1925
  """Get deck from cache or load from database.
1958
1926
 
1959
1927
  Thread-safe access to deck cache using _cache_lock.
1960
-
1961
- The cache is per-process while prod runs multiple uvicorn workers
1962
- sharing one database, so a cached deck is only served if its recorded
1963
- version matches the current DB version; otherwise it is reloaded.
1964
-
1928
+
1965
1929
  Uses deck_dict (slides array with individual scripts) when available,
1966
1930
  falling back to from_html_string for legacy data.
1967
1931
  """
1968
1932
  # Check cache first (with lock)
1969
1933
  with self._cache_lock:
1970
- cached_deck = self._deck_cache.get(session_id)
1971
- cached_version = self._deck_versions().get(session_id)
1972
-
1973
- if cached_deck is not None:
1974
- db_version = self._get_deck_version(session_id)
1975
- if db_version is None or cached_version == db_version:
1976
- return cached_deck
1977
- logger.info(
1978
- "Deck cache stale, reloading from database",
1979
- extra={
1980
- "session_id": session_id,
1981
- "cached_version": cached_version,
1982
- "db_version": db_version,
1983
- },
1984
- )
1985
- with self._cache_lock:
1986
- self._deck_cache.pop(session_id, None)
1987
- self._deck_versions().pop(session_id, None)
1934
+ if session_id in self._deck_cache:
1935
+ return self._deck_cache[session_id]
1988
1936
 
1989
1937
  # Try to load from database (outside lock to avoid blocking)
1990
1938
  session_manager = get_session_manager()
@@ -2018,10 +1966,6 @@ class ChatService:
2018
1966
  # Store in cache (with lock)
2019
1967
  with self._cache_lock:
2020
1968
  self._deck_cache[session_id] = deck
2021
- if deck_data.get("version") is not None:
2022
- self._deck_versions()[session_id] = deck_data["version"]
2023
- else:
2024
- self._deck_versions().pop(session_id, None)
2025
1969
  return deck
2026
1970
  except Exception as e:
2027
1971
  logger.warning(f"Failed to load deck from database: {e}")
@@ -2064,7 +2008,6 @@ class ChatService:
2064
2008
  """Remove the cached deck for a session so the next read hits the DB."""
2065
2009
  with self._cache_lock:
2066
2010
  self._deck_cache.pop(session_id, None)
2067
- self._deck_versions().pop(session_id, None)
2068
2011
 
2069
2012
  def _replace_slide_htmls_from_cache(self, session_id: str, slide_context: Dict[str, Any]) -> Dict[str, Any]:
2070
2013
  """Replace frontend-supplied slide_htmls with backend cache versions.
@@ -2597,7 +2540,7 @@ class ChatService:
2597
2540
  # Persist to database
2598
2541
  deck_dict = current_deck.to_dict()
2599
2542
  session_manager = get_session_manager()
2600
- save_result = session_manager.save_slide_deck(
2543
+ session_manager.save_slide_deck(
2601
2544
  session_id=session_id,
2602
2545
  title=current_deck.title,
2603
2546
  html_content=current_deck.knit(),
@@ -2606,7 +2549,6 @@ class ChatService:
2606
2549
  deck_dict=deck_dict,
2607
2550
  expected_version=expected_version,
2608
2551
  )
2609
- self._record_deck_version(session_id, save_result)
2610
2552
 
2611
2553
  # Create save point
2612
2554
  try:
@@ -2674,7 +2616,7 @@ class ChatService:
2674
2616
  # Persist to database
2675
2617
  deck_dict = current_deck.to_dict()
2676
2618
  session_manager = get_session_manager()
2677
- save_result = session_manager.save_slide_deck(
2619
+ session_manager.save_slide_deck(
2678
2620
  session_id=session_id,
2679
2621
  title=current_deck.title,
2680
2622
  html_content=current_deck.knit(),
@@ -2683,7 +2625,6 @@ class ChatService:
2683
2625
  deck_dict=deck_dict,
2684
2626
  expected_version=expected_version,
2685
2627
  )
2686
- self._record_deck_version(session_id, save_result)
2687
2628
 
2688
2629
  # Create save point immediately after persisting
2689
2630
  try:
@@ -2740,7 +2681,7 @@ class ChatService:
2740
2681
  # Persist to database
2741
2682
  deck_dict = current_deck.to_dict()
2742
2683
  session_manager = get_session_manager()
2743
- save_result = session_manager.save_slide_deck(
2684
+ session_manager.save_slide_deck(
2744
2685
  session_id=session_id,
2745
2686
  title=current_deck.title,
2746
2687
  html_content=current_deck.knit(),
@@ -2749,7 +2690,6 @@ class ChatService:
2749
2690
  deck_dict=deck_dict,
2750
2691
  expected_version=expected_version,
2751
2692
  )
2752
- self._record_deck_version(session_id, save_result)
2753
2693
 
2754
2694
  # Create save point
2755
2695
  try:
@@ -2804,7 +2744,7 @@ class ChatService:
2804
2744
  # Persist to database
2805
2745
  deck_dict = current_deck.to_dict()
2806
2746
  session_manager = get_session_manager()
2807
- save_result = session_manager.save_slide_deck(
2747
+ session_manager.save_slide_deck(
2808
2748
  session_id=session_id,
2809
2749
  title=current_deck.title,
2810
2750
  html_content=current_deck.knit(),
@@ -2813,7 +2753,6 @@ class ChatService:
2813
2753
  deck_dict=deck_dict,
2814
2754
  expected_version=expected_version,
2815
2755
  )
2816
- self._record_deck_version(session_id, save_result)
2817
2756
 
2818
2757
  # Create save point
2819
2758
  try:
@@ -307,9 +307,6 @@ async def convert_slides_with_progress(
307
307
  output_path: Path to save PPTX
308
308
  chart_images_per_slide: Chart images per slide
309
309
  """
310
- from pptx import Presentation
311
- from pptx.util import Inches
312
-
313
310
  total_slides = len(slides_html)
314
311
 
315
312
  # -- Phase 1: Prepare assets + parallel LLM code generation ---------------
@@ -336,27 +333,17 @@ async def convert_slides_with_progress(
336
333
  slide_inputs, on_codegen_progress=_on_codegen_progress,
337
334
  )
338
335
 
339
- # -- Phase 2: Sequential execution ----------------------------------------
340
- prs = Presentation()
341
- prs.slide_width = Inches(10)
342
- prs.slide_height = Inches(7.5)
343
-
344
- for i, (code, (html_str, _chart_files, _content_files, assets_dir)) in enumerate(
345
- zip(codes, slide_inputs), 1,
346
- ):
347
- update_export_progress(
348
- job_id, i, total_slides,
349
- status_message=f"Building slide {i}/{total_slides}…",
350
- )
351
-
352
- converter._execute_slide(code, prs, html_str, assets_dir, i)
353
-
354
- logger.debug(
355
- f"Processed slide {i}/{total_slides}",
356
- extra={"job_id": job_id, "slide": i, "total": total_slides},
357
- )
336
+ # -- Phase 2: Sequential execution INSIDE the subprocess jail -------------
337
+ def _relay(current: int, total: int, message: str) -> None:
338
+ update_export_progress(job_id, current, total, status_message=message)
358
339
 
359
- prs.save(output_path)
340
+ # to_thread: keep the long blocking jail wait off the event loop (this
341
+ # coroutine runs on the loop; _relay is fired from the jail's stdout
342
+ # reader thread and update_export_progress opens its own session per call).
343
+ await asyncio.to_thread(
344
+ converter._run_pptx_conversion,
345
+ codes, slide_inputs, output_path, progress_cb=_relay,
346
+ )
360
347
 
361
348
 
362
349
  async def export_worker() -> None:
@@ -0,0 +1,10 @@
1
+ # src/services/converter_jail/__init__.py
2
+ """Subprocess jail for LLM-generated converter code (SDR-4437 HIGH-5)."""
3
+
4
+ from src.services.converter_jail.jail import ( # noqa: F401
5
+ JailError,
6
+ JailResult,
7
+ ResourceLimits,
8
+ run_gslides_jail,
9
+ run_pptx_jail,
10
+ )
@@ -0,0 +1,58 @@
1
+ # src/services/converter_jail/ast_guard.py
2
+ """Best-effort AST import allowlist for generated converter code.
3
+
4
+ Defense-in-depth ONLY — bypassable by construction (e.g. __import__,
5
+ importlib). The security boundary is the subprocess jail, not this check.
6
+ Runs on the trusted host before launch so obviously-hostile code never
7
+ reaches the child.
8
+ """
9
+
10
+ import ast
11
+
12
+
13
+ class DisallowedImport(Exception):
14
+ """Raised when generated code imports a module outside the allowlist."""
15
+
16
+
17
+ # Top-level module names generated converters legitimately need.
18
+ # Keep this GENEROUS for harmless stdlib: a snippet failing this check is
19
+ # downgraded to a blank placeholder slide (has_code=False in the job-dir
20
+ # builders, Tasks 4/6), so a too-narrow list silently blanks legitimate
21
+ # slides. The prompts only show example imports — they do not restrict the
22
+ # LLM to a fixed set — and the jail, not this list, is the security boundary.
23
+ DEFAULT_ALLOWED = frozenset({
24
+ # PPTX path
25
+ "pptx", "os", "re", "math", "json", "uuid", "base64", "io",
26
+ "datetime", "collections", "itertools", "string", "textwrap",
27
+ # harmless stdlib the LLM commonly emits unprompted
28
+ "sys", "time", "typing", "functools", "random", "copy",
29
+ "html", "unicodedata", "decimal",
30
+ # image handling
31
+ "PIL", "lxml",
32
+ # Google Slides path emits data only; googleapiclient is host-side now,
33
+ # but allow the http helper import in case a snippet still references it
34
+ # (host strips its effect; import alone is harmless).
35
+ "googleapiclient",
36
+ "xml",
37
+ })
38
+
39
+
40
+ def _root(name: str) -> str:
41
+ return name.split(".", 1)[0]
42
+
43
+
44
+ def check_imports(code: str, allowed: frozenset = DEFAULT_ALLOWED) -> None:
45
+ """Raise DisallowedImport if *code* imports a non-allowlisted top-level
46
+ module. Raises SyntaxError if *code* does not parse."""
47
+ tree = ast.parse(code)
48
+ for node in ast.walk(tree):
49
+ if isinstance(node, ast.Import):
50
+ for alias in node.names:
51
+ if _root(alias.name) not in allowed:
52
+ raise DisallowedImport(_root(alias.name))
53
+ elif isinstance(node, ast.ImportFrom):
54
+ # Relative imports (node.level > 0) have no module root to check;
55
+ # they cannot reach outside the (nonexistent) package, so allow.
56
+ if node.level == 0 and node.module is not None:
57
+ if _root(node.module) not in allowed:
58
+ raise DisallowedImport(_root(node.module))
@@ -0,0 +1,220 @@
1
+ # src/services/converter_jail/codeprep.py
2
+ """Dependency-light code-prep helpers shared by the jail runners (SDR-4437 PR-5).
3
+
4
+ STDLIB-ONLY BY DESIGN. This module is imported inside the sandboxed child
5
+ (`python -I`), so it must never pull in `databricks`, `google*`, `bs4`,
6
+ `pptx`, or any `src.services.*` converter module — those graphs drag the
7
+ credential-capable SDK into the jail just to call string transformers.
8
+
9
+ Contents:
10
+ - ``sanitize_code`` — moved verbatim from ``HtmlToPptxConverterV3._sanitize_code``.
11
+ - ``_fix_apostrophe_strings`` / ``_convert_single_to_double_quoted`` — moved
12
+ verbatim from ``html_to_google_slides``.
13
+ - ``prepare_requests_code`` — preps generated Google Slides data-out code
14
+ (wraps bare code into ``build_slide_requests``; request-shape fixes).
15
+ """
16
+
17
+ import ast
18
+ import re
19
+
20
+
21
+ def sanitize_code(code: str) -> str:
22
+ """Fix common LLM-generated code issues before execution.
23
+
24
+ Handles smart quotes, em-dashes, and apostrophes inside string literals
25
+ that cause SyntaxError (e.g. ``Anthony's`` inside a single-quoted string).
26
+ """
27
+ # Smart/curly quotes → straight quotes
28
+ for smart, straight in {
29
+ "‘": "'", "’": "'", # single curly quotes
30
+ "“": '"', "”": '"', # double curly quotes
31
+ }.items():
32
+ code = code.replace(smart, straight)
33
+
34
+ # Em-dash / en-dash → regular hyphen (prevents SyntaxError when
35
+ # they appear outside strings due to broken quoting)
36
+ code = code.replace("—", "-") # em-dash
37
+ code = code.replace("–", "-") # en-dash
38
+
39
+ # Fix apostrophes inside single-quoted strings (e.g. 'Anthony's workflow')
40
+ # by converting affected lines to use double-quoted strings.
41
+ for _ in range(20):
42
+ try:
43
+ ast.parse(code)
44
+ break
45
+ except SyntaxError as exc:
46
+ if exc.lineno is None:
47
+ break
48
+ lines = code.splitlines()
49
+ idx = exc.lineno - 1
50
+ if idx < 0 or idx >= len(lines):
51
+ break
52
+ original = lines[idx]
53
+ # Re-quote single-quoted strings that contain apostrophes
54
+ fixed = re.sub(
55
+ r"'([^']*\w'\w[^']*)'",
56
+ lambda m: '"' + m.group(1) + '"',
57
+ original,
58
+ )
59
+ if fixed == original:
60
+ break
61
+ lines[idx] = fixed
62
+ code = "\n".join(lines)
63
+ return code
64
+
65
+
66
+ # ---------------------------------------------------------------------------
67
+ # Apostrophe-in-string syntax fixer
68
+ # ---------------------------------------------------------------------------
69
+
70
+ def _convert_single_to_double_quoted(line: str) -> str:
71
+ """Re-quote single-quoted strings that contain apostrophes as double-quoted.
72
+
73
+ Scans each single-quote-delimited token on *line*. When an apostrophe is
74
+ surrounded by word characters (``\\w'\\w``) it is treated as part of the
75
+ content (not a closing delimiter), so the scanner continues until it finds a
76
+ genuine closing quote. Those strings are then re-emitted with double-quote
77
+ delimiters so Python can parse them correctly.
78
+
79
+ Example::
80
+
81
+ 'We didn't do it' → "We didn't do it"
82
+ """
83
+ result: list = []
84
+ i = 0
85
+ n = len(line)
86
+
87
+ while i < n:
88
+ if line[i] != "'":
89
+ result.append(line[i])
90
+ i += 1
91
+ continue
92
+
93
+ # Scan for the "real" closing quote, treating word-apostrophe-word as content.
94
+ j = i + 1
95
+ while j < n:
96
+ if line[j] == "'":
97
+ before_word = j > 0 and line[j - 1].isalnum()
98
+ after_word = (j + 1) < n and line[j + 1].isalnum()
99
+ if before_word and after_word:
100
+ j += 1 # it's a contraction apostrophe — keep scanning
101
+ continue
102
+ break # genuine closing quote
103
+ j += 1
104
+
105
+ if j >= n:
106
+ # Never found a real closing quote — rest of line is broken content.
107
+ content = line[i + 1:]
108
+ result.append('"' + content.replace('"', '\\"') + '"')
109
+ break
110
+
111
+ content = line[i + 1: j]
112
+ if re.search(r"\w'\w", content):
113
+ # Content has an apostrophe → rewrite as double-quoted string.
114
+ result.append('"' + content.replace('"', '\\"') + '"')
115
+ else:
116
+ result.append(line[i: j + 1])
117
+
118
+ i = j + 1
119
+
120
+ return "".join(result)
121
+
122
+
123
+ def _fix_apostrophe_strings(code: str) -> str:
124
+ """Fix single-quoted literals whose content contains apostrophes (contractions).
125
+
126
+ The LLM often emits ``'text': 'We don't ...'``. The ``'`` in *don't* ends the
127
+ literal early; the parser may report ``unterminated string``, ``eol while
128
+ scanning``, or ``invalid character`` (e.g. em-dash) on the remainder.
129
+
130
+ Repeatedly ``ast.parse`` and rewrite the error line with
131
+ :func:`_convert_single_to_double_quoted` whenever that changes the line.
132
+ """
133
+ for _ in range(20): # guard against infinite loops
134
+ try:
135
+ ast.parse(code)
136
+ return code
137
+ except SyntaxError as exc:
138
+ if exc.lineno is None:
139
+ return code
140
+
141
+ lines = code.splitlines()
142
+ line_idx = exc.lineno - 1
143
+ if line_idx >= len(lines):
144
+ return code
145
+
146
+ original = lines[line_idx]
147
+ fixed = _convert_single_to_double_quoted(original)
148
+ if fixed == original:
149
+ return code # heuristic cannot improve this error
150
+
151
+ lines[line_idx] = fixed
152
+ code = "\n".join(lines)
153
+
154
+ return code
155
+
156
+
157
+ # ---------------------------------------------------------------------------
158
+ # Google Slides data-out contract prep
159
+ # ---------------------------------------------------------------------------
160
+
161
+ _ALPHA_PAT = re.compile(r"""[,\s]*['"]alpha['"]\s*:\s*[\d.]+""")
162
+
163
+
164
+ def prepare_requests_code(code: str) -> str:
165
+ """Prep generated code that defines build_slide_requests(...).
166
+
167
+ Reuses the request-shape fixers from the legacy ``_prepare_code`` but wraps
168
+ bare code into ``build_slide_requests`` (not ``add_slide_to_presentation``)
169
+ and never references services."""
170
+ if "def build_slide_requests" not in code:
171
+ lines = code.split("\n")
172
+ imports, body, helpers = [], [], []
173
+ in_helper = False
174
+ for line in lines:
175
+ stripped = line.strip()
176
+ if stripped.startswith(("import ", "from ")) and not body:
177
+ imports.append(line)
178
+ elif stripped.startswith(("def emu(", "def hex_to_rgb(")):
179
+ in_helper = True
180
+ helpers.append(line)
181
+ elif in_helper:
182
+ if stripped == "" or (line and line[0] == " "):
183
+ helpers.append(line)
184
+ if stripped == "":
185
+ in_helper = False
186
+ else:
187
+ in_helper = False
188
+ body.append(line)
189
+ else:
190
+ body.append(line)
191
+ indented = [(" " + l if l.strip() else "") for l in body]
192
+ parts = []
193
+ if imports:
194
+ parts.append("\n".join(imports) + "\n")
195
+ if helpers:
196
+ parts.append("\n".join(helpers) + "\n")
197
+ parts.append("def build_slide_requests(html_str, assets_dir, page_id):")
198
+ parts.extend(indented)
199
+ parts.append(" return requests")
200
+ code = "\n".join(parts)
201
+
202
+ if "import os" not in code:
203
+ code = "import os\nimport json\nimport uuid\n\n" + code
204
+
205
+ for smart, straight in {"‘": "'", "’": "'", "“": '"', "”": '"'}.items():
206
+ code = code.replace(smart, straight)
207
+
208
+ if "alpha" in code:
209
+ code = _ALPHA_PAT.sub("", code)
210
+ if "'paragraphStyle'" in code:
211
+ code = code.replace("'paragraphStyle'", "'style'")
212
+ if '"paragraphStyle"' in code:
213
+ code = code.replace('"paragraphStyle"', '"style"')
214
+ if "'textStyle'" in code:
215
+ code = code.replace("'textStyle'", "'style'")
216
+ if '"textStyle"' in code:
217
+ code = code.replace('"textStyle"', '"style"')
218
+
219
+ code = _fix_apostrophe_strings(code)
220
+ return code
@@ -0,0 +1,78 @@
1
+ # src/services/converter_jail/gslides_runner.py
2
+ """In-jail trusted runner for Google Slides conversion (SDR-4437 HIGH-5).
3
+
4
+ Runs each generated build_slide_requests(html_str, assets_dir, page_id)
5
+ snippet and collects the returned request lists into a JSON file. NO network,
6
+ NO credentials. The host validates, uploads assets, substitutes placeholder
7
+ URLs, and executes the requests.
8
+
9
+ CLI: python -I gslides_runner.py <src_parent> <job_dir> <requests_out_path>
10
+ (<src_parent> is part of the launcher's CLI contract; this runner does NOT
11
+ put it on sys.path — importing anything via `src.services` would execute that
12
+ package's heavy __init__ and drag the credential-capable SDK graph into the
13
+ jail child. Siblings are imported as top-level modules instead.)
14
+ """
15
+
16
+ import importlib.util
17
+ import json
18
+ import sys
19
+ import tempfile
20
+ from pathlib import Path
21
+
22
+ _HERE = Path(__file__).resolve().parent
23
+
24
+ if __package__ in (None, ""):
25
+ # Script mode (inside the jail child): import siblings as top-level
26
+ # modules so src/services/__init__.py (SDK/agent graph) never executes.
27
+ sys.path.insert(0, str(_HERE))
28
+ import protocol
29
+ from codeprep import prepare_requests_code
30
+ else:
31
+ # Imported on the trusted host (unit tests): normal package imports.
32
+ from src.services.converter_jail import protocol
33
+ from src.services.converter_jail.codeprep import prepare_requests_code
34
+
35
+
36
+ def _exec_snippet(code: str, html_str: str, assets_dir: str, page_id: str):
37
+ """Run one snippet; return (requests_list_or_None, error_or_None)."""
38
+ code = prepare_requests_code(code)
39
+ tmp = Path(tempfile.mktemp(suffix=".py", prefix="jail_gslide_"))
40
+ tmp.write_text(code, encoding="utf-8")
41
+ try:
42
+ spec = importlib.util.spec_from_file_location("jail_gslide", str(tmp))
43
+ module = importlib.util.module_from_spec(spec)
44
+ spec.loader.exec_module(module)
45
+ requests = module.build_slide_requests(html_str, assets_dir, page_id)
46
+ if not isinstance(requests, list):
47
+ return None, f"build_slide_requests returned {type(requests).__name__}"
48
+ return requests, None
49
+ except Exception as exc:
50
+ return None, repr(exc)
51
+ finally:
52
+ if tmp.exists():
53
+ tmp.unlink()
54
+
55
+
56
+ def run(job_dir: str, requests_out_path: str) -> None:
57
+ manifest = json.loads((Path(job_dir) / protocol.MANIFEST_NAME).read_text())
58
+ out = []
59
+ for entry in manifest["slides"]:
60
+ i = entry["index"]
61
+ page_id = entry["page_id"]
62
+ sdir = Path(job_dir) / entry["dir"]
63
+ assets_dir = str(sdir / protocol.ASSETS_DIR)
64
+ if not entry.get("has_code"):
65
+ out.append({"index": i, "page_id": page_id, "requests": None,
66
+ "error": "no_code"})
67
+ continue
68
+ html_str = (sdir / protocol.HTML_NAME).read_text(encoding="utf-8")
69
+ code = (sdir / protocol.CODE_NAME).read_text(encoding="utf-8")
70
+ requests, error = _exec_snippet(code, html_str, assets_dir, page_id)
71
+ out.append({"index": i, "page_id": page_id,
72
+ "requests": requests, "error": error})
73
+ Path(requests_out_path).write_text(json.dumps(out))
74
+
75
+
76
+ if __name__ == "__main__":
77
+ # argv: [runner, src_parent, job_dir, requests_out_path]
78
+ run(sys.argv[2], sys.argv[3])