databricks-tellr-app 0.4.3.dev40__tar.gz → 0.4.3.dev41__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 (168) hide show
  1. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/PKG-INFO +1 -1
  2. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/databricks_tellr_app.egg-info/PKG-INFO +1 -1
  3. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/databricks_tellr_app.egg-info/SOURCES.txt +2 -0
  4. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/pyproject.toml +1 -1
  5. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/main.py +2 -19
  6. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/export.py +8 -2
  7. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/services/export_job_queue.py +0 -31
  8. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/design_system_service.py +15 -0
  9. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/image_service.py +15 -0
  10. databricks_tellr_app-0.4.3.dev41/src/services/svg_sanitizer.py +403 -0
  11. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/README.md +0 -0
  12. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/databricks_tellr_app/__init__.py +0 -0
  13. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/databricks_tellr_app/run.py +0 -0
  14. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
  15. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/databricks_tellr_app.egg-info/requires.txt +0 -0
  16. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/databricks_tellr_app.egg-info/top_level.txt +0 -0
  17. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/setup.cfg +0 -0
  18. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/setup.py +0 -0
  19. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/__init__.py +0 -0
  20. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/__init__.py +0 -0
  21. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/_otel_bootstrap.py +0 -0
  22. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/fixtures/tour_demo_deck.json +0 -0
  23. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/mcp_auth.py +0 -0
  24. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/mcp_server.py +0 -0
  25. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/middleware/__init__.py +0 -0
  26. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/middleware/csrf.py +0 -0
  27. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/middleware/request_logging.py +0 -0
  28. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/middleware/security_headers.py +0 -0
  29. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/__init__.py +0 -0
  30. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/_authz.py +0 -0
  31. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/admin.py +0 -0
  32. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/admin_usage.py +0 -0
  33. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/agent_config.py +0 -0
  34. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/chat.py +0 -0
  35. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/deck_contributors.py +0 -0
  36. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/feedback.py +0 -0
  37. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/google_slides.py +0 -0
  38. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/images.py +0 -0
  39. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/local_version.py +0 -0
  40. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/profiles.py +0 -0
  41. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/sessions.py +0 -0
  42. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/settings/__init__.py +0 -0
  43. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/settings/contributors.py +0 -0
  44. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/settings/deck_prompts.py +0 -0
  45. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/settings/design_systems.py +0 -0
  46. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/settings/identities.py +0 -0
  47. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/settings/slide_styles.py +0 -0
  48. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/setup.py +0 -0
  49. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/slides.py +0 -0
  50. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/tools.py +0 -0
  51. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/tour.py +0 -0
  52. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/verification.py +0 -0
  53. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/routes/version.py +0 -0
  54. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/schemas/__init__.py +0 -0
  55. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/schemas/agent_config.py +0 -0
  56. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/schemas/feedback.py +0 -0
  57. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/schemas/requests.py +0 -0
  58. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/schemas/responses.py +0 -0
  59. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/schemas/settings/__init__.py +0 -0
  60. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/schemas/settings/requests.py +0 -0
  61. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/schemas/settings/responses.py +0 -0
  62. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/schemas/streaming.py +0 -0
  63. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/services/__init__.py +0 -0
  64. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/services/chat_service.py +0 -0
  65. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/services/feedback_service.py +0 -0
  66. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/services/job_queue.py +0 -0
  67. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/services/session_manager.py +0 -0
  68. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/services/session_naming.py +0 -0
  69. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/services/usage_events.py +0 -0
  70. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/services/usage_service.py +0 -0
  71. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/utils/__init__.py +0 -0
  72. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/api/utils/validation.py +0 -0
  73. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/__init__.py +0 -0
  74. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/config_loader.py +0 -0
  75. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/context_utils.py +0 -0
  76. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/database.py +0 -0
  77. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/databricks_client.py +0 -0
  78. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/defaults.py +0 -0
  79. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/encryption.py +0 -0
  80. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/init_default_profile.py +0 -0
  81. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/lakebase.py +0 -0
  82. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/migrate_profiles_to_agent_config.py +0 -0
  83. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/mlflow_agent_spans.py +0 -0
  84. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/mlflow_tracing.py +0 -0
  85. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/otel_logging.py +0 -0
  86. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/permission_context.py +0 -0
  87. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/prompt_modules.py +0 -0
  88. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/settings_db.py +0 -0
  89. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/core/user_context.py +0 -0
  90. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/__init__.py +0 -0
  91. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/__init__.py +0 -0
  92. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/deck_contributor.py +0 -0
  93. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/design_system.py +0 -0
  94. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/encryption_key.py +0 -0
  95. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/feedback.py +0 -0
  96. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/genie_space.py +0 -0
  97. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/google_global_credentials.py +0 -0
  98. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/google_oauth_token.py +0 -0
  99. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/identity.py +0 -0
  100. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/image.py +0 -0
  101. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/profile.py +0 -0
  102. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/profile_contributor.py +0 -0
  103. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/prompts.py +0 -0
  104. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/request_log.py +0 -0
  105. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/session.py +0 -0
  106. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/slide_deck_prompt.py +0 -0
  107. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/slide_style_library.py +0 -0
  108. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/usage_event.py +0 -0
  109. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/models/user_preference.py +0 -0
  110. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/database/types.py +0 -0
  111. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/domain/__init__.py +0 -0
  112. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/domain/slide.py +0 -0
  113. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/domain/slide_deck.py +0 -0
  114. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/__init__.py +0 -0
  115. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/agent.py +0 -0
  116. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/agent_factory.py +0 -0
  117. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/config_service.py +0 -0
  118. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/config_validator.py +0 -0
  119. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/converter_jail/__init__.py +0 -0
  120. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/converter_jail/ast_guard.py +0 -0
  121. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/converter_jail/codeprep.py +0 -0
  122. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/converter_jail/gslides_runner.py +0 -0
  123. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/converter_jail/jail.py +0 -0
  124. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/converter_jail/pptx_runner.py +0 -0
  125. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/converter_jail/protocol.py +0 -0
  126. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/design_system_compiler.py +0 -0
  127. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/design_system_templates.py +0 -0
  128. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/drive_uploader.py +0 -0
  129. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/evaluation/__init__.py +0 -0
  130. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/evaluation/llm_judge.py +0 -0
  131. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/genie_service.py +0 -0
  132. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/google_slides_auth.py +0 -0
  133. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/google_slides_prompts_defaults.py +0 -0
  134. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/html_to_google_slides.py +0 -0
  135. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/html_to_pptx.py +0 -0
  136. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/identity_provider.py +0 -0
  137. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/identity_providers/__init__.py +0 -0
  138. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/identity_providers/account_provider.py +0 -0
  139. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/identity_providers/local_provider.py +0 -0
  140. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/identity_providers/workspace_provider.py +0 -0
  141. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/image_tools.py +0 -0
  142. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/permission_service.py +0 -0
  143. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/pptx_from_html_huashu.py +0 -0
  144. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/pptx_from_records.py +0 -0
  145. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/pptx_prompts_defaults.py +0 -0
  146. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/profile_service.py +0 -0
  147. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/streaming_callback.py +0 -0
  148. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/tools/__init__.py +0 -0
  149. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/tools/agent_bricks_tool.py +0 -0
  150. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/tools/ds_asset_tool.py +0 -0
  151. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/tools/genie_tool.py +0 -0
  152. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/tools/mcp_tool.py +0 -0
  153. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/tools/model_endpoint_tool.py +0 -0
  154. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/tools/vector_tool.py +0 -0
  155. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/services/validator.py +0 -0
  156. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/utils/__init__.py +0 -0
  157. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/utils/css_utils.py +0 -0
  158. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/utils/ds_asset_utils.py +0 -0
  159. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/utils/error_handling.py +0 -0
  160. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/utils/html_safety.py +0 -0
  161. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/utils/html_utils.py +0 -0
  162. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/utils/image_utils.py +0 -0
  163. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/utils/js_validator.py +0 -0
  164. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/utils/logging_config.py +0 -0
  165. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/utils/pi_filter.py +0 -0
  166. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/utils/slide_hash.py +0 -0
  167. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/src/utils/spotlight.py +0 -0
  168. {databricks_tellr_app-0.4.3.dev40 → databricks_tellr_app-0.4.3.dev41}/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.dev40
3
+ Version: 0.4.3.dev41
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.dev40
3
+ Version: 0.4.3.dev41
4
4
  Summary: Tellr application package for Databricks Apps
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -124,6 +124,7 @@ setup.py
124
124
  ./src/services/pptx_prompts_defaults.py
125
125
  ./src/services/profile_service.py
126
126
  ./src/services/streaming_callback.py
127
+ ./src/services/svg_sanitizer.py
127
128
  ./src/services/validator.py
128
129
  ./src/services/converter_jail/__init__.py
129
130
  ./src/services/converter_jail/ast_guard.py
@@ -286,6 +287,7 @@ src/services/pptx_from_records.py
286
287
  src/services/pptx_prompts_defaults.py
287
288
  src/services/profile_service.py
288
289
  src/services/streaming_callback.py
290
+ src/services/svg_sanitizer.py
289
291
  src/services/validator.py
290
292
  src/services/converter_jail/__init__.py
291
293
  src/services/converter_jail/ast_guard.py
@@ -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.dev40"
7
+ version = "0.4.3.dev41"
8
8
  description = "Tellr application package for Databricks Apps"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -31,10 +31,7 @@ from src.api.routes.settings import (
31
31
  identities_router,
32
32
  slide_styles_router,
33
33
  )
34
- from src.api.services.export_job_queue import (
35
- start_export_cleanup_worker,
36
- start_export_worker,
37
- )
34
+ from src.api.services.export_job_queue import start_export_worker
38
35
  from src.api.services.job_queue import recover_stuck_requests, start_worker
39
36
  from src.core.database import (
40
37
  is_lakebase_environment,
@@ -52,7 +49,6 @@ IS_TESTING = ENVIRONMENT == "test"
52
49
  # Worker task references for cleanup
53
50
  _worker_task = None
54
51
  _export_worker_task = None
55
- _export_cleanup_task: asyncio.Task | None = None
56
52
  _cleanup_task = None
57
53
  _timeout_task = None
58
54
  _frontend_assets_stack: ExitStack | None = None
@@ -61,8 +57,7 @@ _frontend_assets_stack: ExitStack | None = None
61
57
  @asynccontextmanager
62
58
  async def lifespan(app: FastAPI):
63
59
  """Lifespan context manager for startup/shutdown events."""
64
- global _worker_task, _export_worker_task, _export_cleanup_task
65
- global _cleanup_task, _timeout_task, _frontend_assets_stack
60
+ global _worker_task, _export_worker_task, _cleanup_task, _timeout_task, _frontend_assets_stack
66
61
 
67
62
  # Startup
68
63
  logger.info(f"Starting AI Slide Generator API (environment: {ENVIRONMENT})")
@@ -129,10 +124,6 @@ async def lifespan(app: FastAPI):
129
124
  _export_worker_task = await start_export_worker()
130
125
  logger.info("Export job queue worker started")
131
126
 
132
- # Start the export job TTL cleanup worker
133
- _export_cleanup_task = await start_export_cleanup_worker()
134
- logger.info("Export job cleanup worker started")
135
-
136
127
  # Start the MCP job timeout sweeper
137
128
  from src.api.services.job_queue import mark_timed_out_jobs_loop
138
129
  _timeout_task = asyncio.create_task(mark_timed_out_jobs_loop())
@@ -178,14 +169,6 @@ async def lifespan(app: FastAPI):
178
169
  pass
179
170
  logger.info("Export job queue worker stopped")
180
171
 
181
- if _export_cleanup_task:
182
- _export_cleanup_task.cancel()
183
- try:
184
- await _export_cleanup_task
185
- except asyncio.CancelledError:
186
- pass
187
- logger.info("Export job cleanup worker stopped")
188
-
189
172
  if _cleanup_task:
190
173
  _cleanup_task.cancel()
191
174
  try:
@@ -807,11 +807,13 @@ async def poll_pptx_export(job_id: str):
807
807
 
808
808
 
809
809
  @router.get("/pptx/download/{job_id}")
810
- async def download_pptx_export(job_id: str):
810
+ async def download_pptx_export(job_id: str, background_tasks: BackgroundTasks):
811
811
  """Download completed PPTX export.
812
812
 
813
813
  Args:
814
814
  job_id: Job ID from start_pptx_export_async
815
+ background_tasks: FastAPI background tasks for cleanup
816
+
815
817
  Returns:
816
818
  FileResponse with PPTX file
817
819
 
@@ -822,6 +824,7 @@ async def download_pptx_export(job_id: str):
822
824
  _require_export_job_access(job_id)
823
825
  from src.api.services.export_job_queue import (
824
826
  get_export_job_status,
827
+ cleanup_export_job,
825
828
  )
826
829
 
827
830
  job = get_export_job_status(job_id)
@@ -849,6 +852,9 @@ async def download_pptx_export(job_id: str):
849
852
  extra={"job_id": job_id, "pptx_filename": filename},
850
853
  )
851
854
 
855
+ # Schedule cleanup after download
856
+ background_tasks.add_task(cleanup_export_job, job_id)
857
+
852
858
  return FileResponse(
853
859
  path=output_path,
854
860
  filename=filename,
@@ -1289,4 +1295,4 @@ async def export_pptx_huashu_from_html(request: ExportPPTXEditableRequest):
1289
1295
  content=pptx_bytes,
1290
1296
  media_type="application/vnd.openxmlformats-officedocument.presentationml.presentation",
1291
1297
  headers=headers,
1292
- )
1298
+ )
@@ -29,9 +29,6 @@ logger = logging.getLogger(__name__)
29
29
  # Only needs to work within a single process.
30
30
  export_queue: asyncio.Queue = asyncio.Queue()
31
31
 
32
- EXPORT_CLEANUP_INTERVAL_SECONDS = 300
33
- EXPORT_JOB_MAX_AGE_MINUTES = 30
34
-
35
32
 
36
33
  def generate_job_id() -> str:
37
34
  """Generate a unique job ID."""
@@ -513,34 +510,6 @@ async def start_export_worker() -> asyncio.Task:
513
510
  return asyncio.create_task(export_worker())
514
511
 
515
512
 
516
- async def export_cleanup_loop() -> None:
517
- """Periodically remove terminal export jobs that have exceeded their TTL."""
518
- logger.info("Export job cleanup worker started")
519
- while True:
520
- try:
521
- await asyncio.sleep(EXPORT_CLEANUP_INTERVAL_SECONDS)
522
- cleaned = await asyncio.to_thread(
523
- cleanup_stale_jobs,
524
- EXPORT_JOB_MAX_AGE_MINUTES,
525
- )
526
- if cleaned:
527
- logger.info("Cleaned up %d stale export job(s)", cleaned)
528
- except asyncio.CancelledError:
529
- logger.info("Export job cleanup worker shutting down")
530
- raise
531
- except Exception as e:
532
- logger.warning(
533
- "Export job cleanup iteration failed",
534
- exc_info=True,
535
- extra={"error": str(e)},
536
- )
537
-
538
-
539
- async def start_export_cleanup_worker() -> asyncio.Task:
540
- """Start the background export job cleanup task."""
541
- return asyncio.create_task(export_cleanup_loop())
542
-
543
-
544
513
  def cleanup_export_job(job_id: str) -> None:
545
514
  """Clean up job data and temporary files.
546
515
 
@@ -74,6 +74,7 @@ from src.database.models.design_system import (
74
74
  DesignSystemToken,
75
75
  )
76
76
  from src.services.design_system_compiler import recompute_compiled_style_content
77
+ from src.services.svg_sanitizer import looks_like_svg, sanitize_svg
77
78
 
78
79
  logger = logging.getLogger(__name__)
79
80
 
@@ -2164,6 +2165,20 @@ def _collect_assets_and_files(
2164
2165
  mime = sniffed
2165
2166
  else:
2166
2167
  mime = _guess_mime(rel)
2168
+ if mime == "image/svg+xml":
2169
+ try:
2170
+ data = sanitize_svg(data)
2171
+ except ValueError as exc:
2172
+ raise DesignSystemImportError(
2173
+ f"Bundle entry '{rel}' is not a valid SVG: {exc}"
2174
+ ) from exc
2175
+ elif looks_like_svg(data):
2176
+ # Extension says raster/font/etc. but the bytes are SVG:
2177
+ # never trust the name to skip sanitization.
2178
+ raise DesignSystemImportError(
2179
+ f"Bundle entry '{rel}' contains SVG content but does not "
2180
+ "have an .svg extension"
2181
+ )
2167
2182
  width, height = _image_dimensions(data, mime)
2168
2183
  asset = DesignSystemAsset(
2169
2184
  kind=kind,
@@ -11,6 +11,7 @@ from sqlalchemy.dialects.postgresql import JSONB
11
11
  from sqlalchemy.orm import Query, Session
12
12
 
13
13
  from src.database.models.image import ImageAsset
14
+ from src.services.svg_sanitizer import looks_like_svg, sanitize_svg
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
 
@@ -41,6 +42,20 @@ def upload_image(
41
42
  if len(file_content) > MAX_FILE_SIZE:
42
43
  raise ValueError(f"File too large: {len(file_content)} bytes (max {MAX_FILE_SIZE})")
43
44
 
45
+ # The client-declared MIME type is untrusted: decide on the CONTENT.
46
+ # SVG can carry script, so it is always sanitized; SVG bytes declared as
47
+ # any other type are rejected rather than stored under a raster label.
48
+ if mime_type == "image/svg+xml":
49
+ try:
50
+ file_content = sanitize_svg(file_content)
51
+ except ValueError as exc:
52
+ raise ValueError(f"Invalid SVG upload: {exc}") from exc
53
+ elif looks_like_svg(file_content):
54
+ raise ValueError(
55
+ f"File content is SVG but was declared as {mime_type}; "
56
+ "upload it as image/svg+xml."
57
+ )
58
+
44
59
  # Check for duplicate original_filename among active images (case-insensitive).
45
60
  # Ephemeral images (paste-to-chat) skip this check — they use throwaway names
46
61
  # and should never block each other.
@@ -0,0 +1,403 @@
1
+ """Server-side sanitization for SVG images before they are stored."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from urllib.parse import urlsplit
7
+
8
+ from lxml import etree # type: ignore[import-untyped]
9
+
10
+ SVG_NAMESPACE = "http://www.w3.org/2000/svg"
11
+
12
+ _ALLOWED_ELEMENTS = frozenset(
13
+ {
14
+ "svg",
15
+ "g",
16
+ "defs",
17
+ "symbol",
18
+ "use",
19
+ "title",
20
+ "desc",
21
+ "switch",
22
+ "view",
23
+ "path",
24
+ "rect",
25
+ "circle",
26
+ "ellipse",
27
+ "line",
28
+ "polyline",
29
+ "polygon",
30
+ "text",
31
+ "tspan",
32
+ "textPath",
33
+ "a",
34
+ "image",
35
+ "style",
36
+ "linearGradient",
37
+ "radialGradient",
38
+ "stop",
39
+ "pattern",
40
+ "marker",
41
+ "mask",
42
+ "clipPath",
43
+ "filter",
44
+ "feBlend",
45
+ "feColorMatrix",
46
+ "feComponentTransfer",
47
+ "feComposite",
48
+ "feConvolveMatrix",
49
+ "feDiffuseLighting",
50
+ "feDisplacementMap",
51
+ "feDistantLight",
52
+ "feDropShadow",
53
+ "feFlood",
54
+ "feFuncA",
55
+ "feFuncB",
56
+ "feFuncG",
57
+ "feFuncR",
58
+ "feGaussianBlur",
59
+ "feImage",
60
+ "feMerge",
61
+ "feMergeNode",
62
+ "feMorphology",
63
+ "feOffset",
64
+ "fePointLight",
65
+ "feSpecularLighting",
66
+ "feSpotLight",
67
+ "feTile",
68
+ "feTurbulence",
69
+ "animate",
70
+ "animateMotion",
71
+ "animateTransform",
72
+ "set",
73
+ "mpath",
74
+ }
75
+ )
76
+
77
+ _ALLOWED_ATTRIBUTES = frozenset(
78
+ {
79
+ "id",
80
+ "class",
81
+ "style",
82
+ "transform",
83
+ "transform-origin",
84
+ "clip-path",
85
+ "clip-rule",
86
+ "mask",
87
+ "filter",
88
+ "opacity",
89
+ "fill",
90
+ "fill-opacity",
91
+ "fill-rule",
92
+ "stroke",
93
+ "stroke-dasharray",
94
+ "stroke-dashoffset",
95
+ "stroke-linecap",
96
+ "stroke-linejoin",
97
+ "stroke-miterlimit",
98
+ "stroke-opacity",
99
+ "stroke-width",
100
+ "paint-order",
101
+ "color",
102
+ "color-interpolation",
103
+ "color-interpolation-filters",
104
+ "display",
105
+ "visibility",
106
+ "systemLanguage",
107
+ "requiredFeatures",
108
+ "requiredExtensions",
109
+ "lang",
110
+ "space",
111
+ "role",
112
+ "tabindex",
113
+ "version",
114
+ "baseProfile",
115
+ "width",
116
+ "height",
117
+ "viewBox",
118
+ "preserveAspectRatio",
119
+ "x",
120
+ "y",
121
+ "dx",
122
+ "dy",
123
+ "zoomAndPan",
124
+ "d",
125
+ "points",
126
+ "rx",
127
+ "ry",
128
+ "cx",
129
+ "cy",
130
+ "r",
131
+ "x1",
132
+ "x2",
133
+ "y1",
134
+ "y2",
135
+ "rotate",
136
+ "textLength",
137
+ "lengthAdjust",
138
+ "font-family",
139
+ "font-size",
140
+ "font-style",
141
+ "font-variant",
142
+ "font-weight",
143
+ "letter-spacing",
144
+ "word-spacing",
145
+ "text-decoration",
146
+ "text-anchor",
147
+ "dominant-baseline",
148
+ "alignment-baseline",
149
+ "baseline-shift",
150
+ "writing-mode",
151
+ "gradientUnits",
152
+ "gradientTransform",
153
+ "spreadMethod",
154
+ "href",
155
+ "offset",
156
+ "stop-color",
157
+ "stop-opacity",
158
+ "patternUnits",
159
+ "patternContentUnits",
160
+ "patternTransform",
161
+ "markerWidth",
162
+ "markerHeight",
163
+ "markerUnits",
164
+ "refX",
165
+ "refY",
166
+ "orient",
167
+ "clipPathUnits",
168
+ "maskUnits",
169
+ "maskContentUnits",
170
+ "filterUnits",
171
+ "primitiveUnits",
172
+ "in",
173
+ "in2",
174
+ "result",
175
+ "mode",
176
+ "values",
177
+ "type",
178
+ "tableValues",
179
+ "slope",
180
+ "intercept",
181
+ "amplitude",
182
+ "exponent",
183
+ "k1",
184
+ "k2",
185
+ "k3",
186
+ "k4",
187
+ "operator",
188
+ "radius",
189
+ "stdDeviation",
190
+ "xChannelSelector",
191
+ "yChannelSelector",
192
+ "scale",
193
+ "flood-color",
194
+ "flood-opacity",
195
+ "kernelMatrix",
196
+ "divisor",
197
+ "bias",
198
+ "targetX",
199
+ "targetY",
200
+ "edgeMode",
201
+ "kernelUnitLength",
202
+ "preserveAlpha",
203
+ "lighting-color",
204
+ "surfaceScale",
205
+ "diffuseConstant",
206
+ "specularConstant",
207
+ "specularExponent",
208
+ "pointsAtX",
209
+ "pointsAtY",
210
+ "pointsAtZ",
211
+ "limitingConeAngle",
212
+ "azimuth",
213
+ "elevation",
214
+ "seed",
215
+ "numOctaves",
216
+ "baseFrequency",
217
+ "stitchTiles",
218
+ "attributeName",
219
+ "attributeType",
220
+ "begin",
221
+ "end",
222
+ "dur",
223
+ "repeatCount",
224
+ "repeatDur",
225
+ "from",
226
+ "to",
227
+ "by",
228
+ "keyTimes",
229
+ "keySplines",
230
+ "keyPoints",
231
+ "calcMode",
232
+ "additive",
233
+ "accumulate",
234
+ "path",
235
+ "startOffset",
236
+ "method",
237
+ "spacing",
238
+ "crossorigin",
239
+ "decoding",
240
+ "loading",
241
+ "target",
242
+ "download",
243
+ "rel",
244
+ }
245
+ )
246
+
247
+ _URL_ATTRIBUTES = frozenset({"href"})
248
+ _ANIMATION_ELEMENTS = frozenset({"animate", "animateMotion", "animateTransform", "set"})
249
+ _ANIMATION_FORBIDDEN_TARGETS = frozenset({"href", "style"})
250
+ _SAFE_DATA_IMAGE_TYPES = frozenset({"png", "jpeg", "jpg", "gif", "webp"})
251
+ _UNSAFE_URL_PATTERN = re.compile(
252
+ r"(?:javascript|vbscript|livescript|mocha):|data:image/svg+xml",
253
+ re.IGNORECASE,
254
+ )
255
+
256
+
257
+ def _local_name(value: str) -> str:
258
+ return value.rsplit("}", 1)[-1].rsplit(":", 1)[-1]
259
+
260
+
261
+ def _element_local_name(element: etree._Element) -> str:
262
+ return etree.QName(element).localname
263
+
264
+
265
+ def _is_allowed_element(element: etree._Element) -> bool:
266
+ return _element_local_name(element) in _ALLOWED_ELEMENTS
267
+
268
+
269
+ def _normalized_url(value: str) -> str:
270
+ characters = (character for character in value if character not in "\t\n\r\f\v")
271
+ return "".join(characters).strip()
272
+
273
+
274
+ def _is_safe_url(value: str) -> bool:
275
+ normalized = _normalized_url(value)
276
+ if not normalized or normalized.startswith("#"):
277
+ return True
278
+
279
+ parsed = urlsplit(normalized)
280
+ scheme = parsed.scheme.lower()
281
+ if not scheme or scheme in {"http", "https", "mailto"}:
282
+ return True
283
+ if scheme != "data":
284
+ return False
285
+
286
+ media_type = parsed.path.split(";", 1)[0].lower()
287
+ image_type = media_type.removeprefix("image/")
288
+ return image_type in _SAFE_DATA_IMAGE_TYPES
289
+
290
+
291
+ def _has_external_use_reference(element: etree._Element) -> bool:
292
+ return any(
293
+ _local_name(name) == "href" and not _normalized_url(value).startswith("#")
294
+ for name, value in element.attrib.items()
295
+ )
296
+
297
+
298
+ def _animation_targets_forbidden_attribute(element: etree._Element) -> bool:
299
+ target = element.get("attributeName")
300
+ if target is None:
301
+ return False
302
+ local_target = _local_name(target)
303
+ return local_target.startswith("on") or local_target in _ANIMATION_FORBIDDEN_TARGETS
304
+
305
+
306
+ def _contains_unsafe_url(value: str) -> bool:
307
+ return _UNSAFE_URL_PATTERN.search(_normalized_url(value)) is not None
308
+
309
+
310
+ def _sanitize_attributes(root: etree._Element) -> None:
311
+ for element in root.iter():
312
+ for name, value in list(element.attrib.items()):
313
+ local_name = _local_name(name)
314
+ if local_name not in _ALLOWED_ATTRIBUTES:
315
+ del element.attrib[name]
316
+ continue
317
+ if local_name in _URL_ATTRIBUTES and not _is_safe_url(value):
318
+ del element.attrib[name]
319
+ elif local_name == "style" and _contains_unsafe_url(value):
320
+ del element.attrib[name]
321
+
322
+ if _element_local_name(element) == "style" and _contains_unsafe_url(
323
+ element.text or ""
324
+ ):
325
+ element.text = None
326
+
327
+
328
+ _RASTER_MAGICS = (
329
+ b"\x89PNG\r\n\x1a\n",
330
+ b"\xff\xd8\xff",
331
+ b"GIF87a",
332
+ b"GIF89a",
333
+ )
334
+ _SNIFF_WINDOW = 4096
335
+
336
+
337
+ def looks_like_svg(content: bytes) -> bool:
338
+ """Content sniff: True if the bytes look like an SVG document.
339
+
340
+ Used so a client-declared MIME type (e.g. ``image/png``) is never trusted
341
+ to decide whether sanitization is needed. Content that starts with a known
342
+ raster magic number is not SVG; otherwise an ``<svg`` start tag within the
343
+ first few KB (UTF-8/ASCII or BOM-marked UTF-16) marks it as SVG.
344
+ """
345
+ head = content[:_SNIFF_WINDOW]
346
+ if head.startswith(_RASTER_MAGICS):
347
+ return False
348
+ if head.startswith((b"\xff\xfe", b"\xfe\xff")):
349
+ text = head.decode("utf-16", errors="ignore")
350
+ else:
351
+ text = head.decode("utf-8", errors="ignore")
352
+ return "<svg" in text.lower()
353
+
354
+
355
+ def sanitize_svg(content: bytes) -> bytes:
356
+ """Return a sanitized SVG, rejecting malformed or non-SVG XML input."""
357
+ parser = etree.XMLParser(
358
+ resolve_entities=False,
359
+ no_network=True,
360
+ load_dtd=False,
361
+ huge_tree=False,
362
+ remove_comments=True,
363
+ remove_pis=True,
364
+ )
365
+
366
+ try:
367
+ root = etree.fromstring(content, parser=parser)
368
+ except etree.XMLSyntaxError as exc:
369
+ raise ValueError("SVG must be well-formed XML") from exc
370
+
371
+ if root is None:
372
+ raise ValueError("SVG document is empty")
373
+
374
+ root_qname = etree.QName(root)
375
+ if root_qname.localname != "svg" or root_qname.namespace not in {None, SVG_NAMESPACE}:
376
+ raise ValueError("SVG root element is required")
377
+
378
+ if root.getroottree().docinfo.doctype:
379
+ raise ValueError("SVG DOCTYPE declarations are not allowed")
380
+
381
+ elements_to_remove = []
382
+ for element in root.iter():
383
+ if element is root:
384
+ continue
385
+
386
+ local_name = _element_local_name(element)
387
+ if not _is_allowed_element(element):
388
+ elements_to_remove.append(element)
389
+ elif local_name == "use" and _has_external_use_reference(element):
390
+ elements_to_remove.append(element)
391
+ elif local_name in _ANIMATION_ELEMENTS and _animation_targets_forbidden_attribute(
392
+ element
393
+ ):
394
+ elements_to_remove.append(element)
395
+
396
+ for element in elements_to_remove:
397
+ parent = element.getparent()
398
+ if parent is not None:
399
+ parent.remove(element)
400
+
401
+ _sanitize_attributes(root)
402
+
403
+ return etree.tostring(root, encoding="utf-8", xml_declaration=True)