databricks-tellr-app 0.4.2.dev12__tar.gz → 0.4.2.dev13__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.2.dev12 → databricks_tellr_app-0.4.2.dev13}/PKG-INFO +1 -1
  2. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/databricks_tellr_app.egg-info/PKG-INFO +1 -1
  3. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/pyproject.toml +1 -1
  4. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/design_system_service.py +307 -53
  5. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/README.md +0 -0
  6. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/databricks_tellr_app/__init__.py +0 -0
  7. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/databricks_tellr_app/run.py +0 -0
  8. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/databricks_tellr_app.egg-info/SOURCES.txt +0 -0
  9. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
  10. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/databricks_tellr_app.egg-info/requires.txt +0 -0
  11. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/databricks_tellr_app.egg-info/top_level.txt +0 -0
  12. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/setup.cfg +0 -0
  13. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/setup.py +0 -0
  14. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/__init__.py +0 -0
  15. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/__init__.py +0 -0
  16. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/_otel_bootstrap.py +0 -0
  17. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/fixtures/tour_demo_deck.json +0 -0
  18. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/main.py +0 -0
  19. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/mcp_auth.py +0 -0
  20. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/mcp_server.py +0 -0
  21. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/middleware/__init__.py +0 -0
  22. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/middleware/csrf.py +0 -0
  23. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/middleware/request_logging.py +0 -0
  24. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/middleware/security_headers.py +0 -0
  25. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/__init__.py +0 -0
  26. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/_authz.py +0 -0
  27. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/admin.py +0 -0
  28. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/admin_usage.py +0 -0
  29. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/agent_config.py +0 -0
  30. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/chat.py +0 -0
  31. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/deck_contributors.py +0 -0
  32. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/export.py +0 -0
  33. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/feedback.py +0 -0
  34. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/google_slides.py +0 -0
  35. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/images.py +0 -0
  36. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/local_version.py +0 -0
  37. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/profiles.py +0 -0
  38. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/sessions.py +0 -0
  39. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/settings/__init__.py +0 -0
  40. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/settings/contributors.py +0 -0
  41. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/settings/deck_prompts.py +0 -0
  42. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/settings/design_systems.py +0 -0
  43. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/settings/identities.py +0 -0
  44. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/settings/slide_styles.py +0 -0
  45. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/setup.py +0 -0
  46. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/slides.py +0 -0
  47. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/tools.py +0 -0
  48. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/tour.py +0 -0
  49. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/verification.py +0 -0
  50. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/routes/version.py +0 -0
  51. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/schemas/__init__.py +0 -0
  52. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/schemas/agent_config.py +0 -0
  53. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/schemas/feedback.py +0 -0
  54. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/schemas/requests.py +0 -0
  55. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/schemas/responses.py +0 -0
  56. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/schemas/settings/__init__.py +0 -0
  57. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/schemas/settings/requests.py +0 -0
  58. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/schemas/settings/responses.py +0 -0
  59. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/schemas/streaming.py +0 -0
  60. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/services/__init__.py +0 -0
  61. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/services/chat_service.py +0 -0
  62. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/services/export_job_queue.py +0 -0
  63. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/services/feedback_service.py +0 -0
  64. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/services/job_queue.py +0 -0
  65. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/services/session_manager.py +0 -0
  66. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/services/session_naming.py +0 -0
  67. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/services/usage_events.py +0 -0
  68. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/services/usage_service.py +0 -0
  69. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/utils/__init__.py +0 -0
  70. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/api/utils/validation.py +0 -0
  71. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/__init__.py +0 -0
  72. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/config_loader.py +0 -0
  73. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/context_utils.py +0 -0
  74. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/database.py +0 -0
  75. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/databricks_client.py +0 -0
  76. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/defaults.py +0 -0
  77. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/encryption.py +0 -0
  78. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/init_default_profile.py +0 -0
  79. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/lakebase.py +0 -0
  80. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/migrate_profiles_to_agent_config.py +0 -0
  81. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/mlflow_agent_spans.py +0 -0
  82. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/mlflow_tracing.py +0 -0
  83. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/otel_logging.py +0 -0
  84. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/permission_context.py +0 -0
  85. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/prompt_modules.py +0 -0
  86. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/settings_db.py +0 -0
  87. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/core/user_context.py +0 -0
  88. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/__init__.py +0 -0
  89. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/__init__.py +0 -0
  90. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/deck_contributor.py +0 -0
  91. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/design_system.py +0 -0
  92. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/encryption_key.py +0 -0
  93. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/feedback.py +0 -0
  94. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/genie_space.py +0 -0
  95. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/google_global_credentials.py +0 -0
  96. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/google_oauth_token.py +0 -0
  97. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/identity.py +0 -0
  98. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/image.py +0 -0
  99. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/profile.py +0 -0
  100. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/profile_contributor.py +0 -0
  101. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/prompts.py +0 -0
  102. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/request_log.py +0 -0
  103. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/session.py +0 -0
  104. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/slide_deck_prompt.py +0 -0
  105. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/slide_style_library.py +0 -0
  106. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/usage_event.py +0 -0
  107. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/models/user_preference.py +0 -0
  108. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/database/types.py +0 -0
  109. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/domain/__init__.py +0 -0
  110. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/domain/slide.py +0 -0
  111. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/domain/slide_deck.py +0 -0
  112. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/__init__.py +0 -0
  113. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/agent.py +0 -0
  114. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/agent_factory.py +0 -0
  115. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/config_service.py +0 -0
  116. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/config_validator.py +0 -0
  117. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/converter_jail/__init__.py +0 -0
  118. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/converter_jail/ast_guard.py +0 -0
  119. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/converter_jail/codeprep.py +0 -0
  120. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/converter_jail/gslides_runner.py +0 -0
  121. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/converter_jail/jail.py +0 -0
  122. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/converter_jail/pptx_runner.py +0 -0
  123. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/converter_jail/protocol.py +0 -0
  124. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/design_system_compiler.py +0 -0
  125. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/design_system_templates.py +0 -0
  126. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/drive_uploader.py +0 -0
  127. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/evaluation/__init__.py +0 -0
  128. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/evaluation/llm_judge.py +0 -0
  129. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/genie_service.py +0 -0
  130. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/google_slides_auth.py +0 -0
  131. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/google_slides_prompts_defaults.py +0 -0
  132. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/html_to_google_slides.py +0 -0
  133. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/html_to_pptx.py +0 -0
  134. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/identity_provider.py +0 -0
  135. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/identity_providers/__init__.py +0 -0
  136. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/identity_providers/account_provider.py +0 -0
  137. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/identity_providers/local_provider.py +0 -0
  138. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/identity_providers/workspace_provider.py +0 -0
  139. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/image_service.py +0 -0
  140. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/image_tools.py +0 -0
  141. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/permission_service.py +0 -0
  142. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/pptx_from_html_huashu.py +0 -0
  143. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/pptx_from_records.py +0 -0
  144. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/pptx_prompts_defaults.py +0 -0
  145. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/profile_service.py +0 -0
  146. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/streaming_callback.py +0 -0
  147. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/tools/__init__.py +0 -0
  148. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/tools/agent_bricks_tool.py +0 -0
  149. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/tools/ds_asset_tool.py +0 -0
  150. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/tools/genie_tool.py +0 -0
  151. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/tools/mcp_tool.py +0 -0
  152. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/tools/model_endpoint_tool.py +0 -0
  153. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/tools/vector_tool.py +0 -0
  154. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/services/validator.py +0 -0
  155. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/utils/__init__.py +0 -0
  156. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/utils/css_utils.py +0 -0
  157. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/utils/ds_asset_utils.py +0 -0
  158. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/utils/error_handling.py +0 -0
  159. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/utils/html_safety.py +0 -0
  160. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/utils/html_utils.py +0 -0
  161. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/utils/image_utils.py +0 -0
  162. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/utils/js_validator.py +0 -0
  163. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/utils/logging_config.py +0 -0
  164. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/utils/pi_filter.py +0 -0
  165. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/utils/slide_hash.py +0 -0
  166. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/src/utils/spotlight.py +0 -0
  167. {databricks_tellr_app-0.4.2.dev12 → databricks_tellr_app-0.4.2.dev13}/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.2.dev12
3
+ Version: 0.4.2.dev13
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.2.dev12
3
+ Version: 0.4.2.dev13
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.2.dev12"
7
+ version = "0.4.2.dev13"
8
8
  description = "Tellr application package for Databricks Apps"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1083,6 +1083,22 @@ _REASON_CORRUPT_EXTRA = (
1083
1083
  "its Info-ZIP Unicode Path extra field (0x7075) is malformed, so the name the "
1084
1084
  "archive means to declare for this entry cannot be read at all"
1085
1085
  )
1086
+ _REASON_LOCAL_HEADER = (
1087
+ "its local file header — the second place a .zip records an entry's name — cannot "
1088
+ "be read, so whether the archive states one name for this entry or two cannot be "
1089
+ "established"
1090
+ )
1091
+ # Distinct from :data:`_REASON_EMPTY`, which is about a name that IS empty everywhere.
1092
+ # This one is about a name the ARCHIVE leaves empty while the reader resolves a usable
1093
+ # one from an extra field — so the message has to name both halves, or it reads as a
1094
+ # complaint about a name the user can see is not empty.
1095
+ _REASON_EMPTY_RECORDED_NAME = (
1096
+ "the archive records NO name for this entry at all. An empty central-directory "
1097
+ "name is not a legitimate archive member, and it is not harmless either: an "
1098
+ "Info-ZIP Unicode Path extra field can supply a replacement (its checksum matches "
1099
+ "an empty name trivially), so the entry ends up with a name that was never "
1100
+ "recorded for it"
1101
+ )
1086
1102
  _REASON_ABSOLUTE = (
1087
1103
  "it is an absolute path, so it names a location outside the bundle rather than "
1088
1104
  "a file inside it"
@@ -1398,8 +1414,26 @@ def _raw_entry_name(info: zipfile.ZipInfo) -> str:
1398
1414
  ``ZipInfo.__init__`` truncates ``filename`` at the first NUL byte and keeps the
1399
1415
  real central-directory name in ``orig_filename``. Every gate here reads the name
1400
1416
  through this function so validation judges what the archive actually says.
1417
+
1418
+ ABSENT and EMPTY are different answers, and only an explicit ``None`` test tells
1419
+ them apart. ``or`` did not: it fell back to ``filename`` for an ``orig_filename``
1420
+ of ``""`` as readily as for a missing attribute — which is this module's own
1421
+ laundering, performed at the one function whose entire job is to report the raw
1422
+ name. An empty central name is attacker-controlled data with meaning, and CPython
1423
+ will happily REPLACE it: a ``0x7075`` record needs its checksum to match the
1424
+ central name, and the CRC-32 of ``b""`` is 0, so the record is honoured and
1425
+ ``filename`` becomes whatever it declares. The fallback then handed every gate
1426
+ that name, so ``orig_filename != filename`` compared the replacement against
1427
+ itself and the local-header comparison compared against it too. Measured: an entry
1428
+ with an empty central name, a local name of ``slides/benign.bin`` and a record
1429
+ declaring the same passed both and was yielded for storage.
1430
+
1431
+ The empty name is now REPORTED rather than replaced; refusing it is
1432
+ :func:`_entry_identity_refusal`'s call, on :data:`_REASON_EMPTY_RECORDED_NAME`.
1433
+ The ``getattr`` default stays for a ``ZipInfo`` that genuinely lacks the attribute.
1401
1434
  """
1402
- return getattr(info, "orig_filename", None) or info.filename
1435
+ raw = getattr(info, "orig_filename", None)
1436
+ return info.filename if raw is None else raw
1403
1437
 
1404
1438
 
1405
1439
  # Info-ZIP Unicode Path Extra Field. Its whole purpose is to declare a name for an
@@ -1411,11 +1445,20 @@ _UNICODE_PATH_EXTRA_TAG = 0x7075
1411
1445
  # CRC-32 of the central-directory name.
1412
1446
  _UNICODE_PATH_HEADER_LEN = 5
1413
1447
 
1448
+ # Every extra-field record is a 2-byte tag followed by a 2-byte body size. Anything
1449
+ # shorter than that cannot be a record at all.
1450
+ _EXTRA_RECORD_HEADER_LEN = 4
1414
1451
 
1415
- def _extra_field_name_refusal(info: zipfile.ZipInfo) -> Optional[str]:
1452
+
1453
+ def _extra_field_name_refusal(extra: bytes, central: str) -> Optional[str]:
1416
1454
  """Refuse an entry whose EXTRA FIELD declares a different name, else ``None``.
1417
1455
 
1418
- Parsed from the raw ``info.extra`` bytes on purpose. No stdlib-processed attribute
1456
+ Takes the extra-field BYTES rather than a ``ZipInfo`` because an entry has TWO
1457
+ extra fields — one in the central directory, one in its local file header — and
1458
+ both are walked, by :func:`_entry_identity_refusal`. ``ZipInfo.extra`` is only the
1459
+ central copy.
1460
+
1461
+ Parsed from raw bytes on purpose. No stdlib-processed attribute
1419
1462
  can be trusted as an entry's identity, and this record is the proof: in CPython's
1420
1463
  ``ZipInfo._decodeExtra`` a ``0x7075`` record assigns
1421
1464
  ``self.filename = _sanitize_filename(declared_name)`` and never touches
@@ -1443,12 +1486,21 @@ def _extra_field_name_refusal(info: zipfile.ZipInfo) -> Optional[str]:
1443
1486
  and never the filename. The Unicode COMMENT field (``0x6375``) carries a comment,
1444
1487
  not a path. Unknown tags are skipped by both the stdlib and the loop below.
1445
1488
  """
1446
- central = _raw_entry_name(info)
1447
- extra = info.extra or b""
1448
1489
  offset = 0
1449
- while len(extra) - offset >= 4:
1490
+ while offset < len(extra):
1491
+ # A LEFTOVER too short to be a record header is malformed, not padding. The
1492
+ # extra field is a sequence of records that tile it exactly; the format
1493
+ # defines no filler between or after them. Stopping quietly on a trailing
1494
+ # fragment skipped the shortest possible spelling of the very record this
1495
+ # function exists to read: an extra of exactly b"\x75\x70" is the 0x7075 tag
1496
+ # with its size field and body cut off, and CPython — whose own walk has the
1497
+ # same four-byte floor — opens such an archive without complaint. Whatever
1498
+ # name that record meant to declare cannot be read, which is the condition
1499
+ # the overrun and short-body branches below already refuse.
1500
+ if len(extra) - offset < _EXTRA_RECORD_HEADER_LEN:
1501
+ return _REASON_CORRUPT_EXTRA
1450
1502
  tag, size = struct.unpack_from("<HH", extra, offset)
1451
- body_start = offset + 4
1503
+ body_start = offset + _EXTRA_RECORD_HEADER_LEN
1452
1504
  body_end = body_start + size
1453
1505
  if body_end > len(extra):
1454
1506
  return _REASON_CORRUPT_EXTRA
@@ -1466,42 +1518,196 @@ def _extra_field_name_refusal(info: zipfile.ZipInfo) -> Optional[str]:
1466
1518
  return None
1467
1519
 
1468
1520
 
1469
- def _entry_verdict_for_info(
1470
- info: zipfile.ZipInfo, rel_path: Optional[str] = None
1471
- ) -> _EntryVerdict:
1472
- """The verdict for one ZipInfo, judged on the name the archive really records.
1521
+ # Local file header layout (APPNOTE 4.3.7), the fixed part of which is 30 bytes:
1522
+ # signature, version needed, general-purpose bit flag, ... then the two lengths that
1523
+ # say how much variable-length data follows, then the name and the extra field.
1524
+ _LOCAL_HEADER_SIGNATURE = b"PK\x03\x04"
1525
+ _LOCAL_HEADER_LEN = 30
1526
+ _LOCAL_HEADER_FLAGS_OFFSET = 6
1527
+ _LOCAL_HEADER_LENGTHS_OFFSET = 26 # file name length u16, then extra field length u16
1528
+ # General-purpose bit 11: the name and comment are UTF-8 rather than the historical
1529
+ # CP437. Read from the LOCAL header's own flags, because that is the byte a reader
1530
+ # consulting this header would use to decode the name beside it.
1531
+ _UTF8_NAME_FLAG = 0x800
1532
+
1533
+
1534
+ def _local_header_identity(
1535
+ zf: zipfile.ZipFile, info: zipfile.ZipInfo
1536
+ ) -> "Optional[tuple[str, bytes]]":
1537
+ """The ``(name, extra)`` the entry's LOCAL FILE HEADER records, or ``None``.
1538
+
1539
+ A .zip states every entry's identity TWICE — once in the local file header that
1540
+ precedes its bytes, once in the central directory at the end — and NOTHING in the
1541
+ format makes the two agree. CPython builds ``ZipInfo`` purely from the central
1542
+ copy: ``ZipInfo.extra`` is the central extra and the local one is never parsed at
1543
+ all. So a guard that reads ``info.extra`` alone inspects one of the two names an
1544
+ archive can present, and an attacker simply puts the rewrite in the other:
1545
+ measured, an entry with a clean central name, an EMPTY central extra and a local
1546
+ ``0x7075`` record declaring ``templates/x/.thumbnail\\x00.exe`` (version 1,
1547
+ matching CRC — the form CPython would honour had it looked) was accepted and
1548
+ stored as a live template thumbnail, which is precisely the outcome the central
1549
+ check exists to prevent.
1550
+
1551
+ Read straight from the archive rather than through any ``ZipInfo`` attribute, for
1552
+ the same reason the extra field is: the point is what the bytes say. The header is
1553
+ located by ``info.header_offset`` — an absolute file offset, adjusted by CPython
1554
+ for any prepended data — and parsed at the documented offsets rather than guessed
1555
+ at.
1556
+
1557
+ THE OFFSET IS THE ARCHIVE'S, NOT THE READER'S. ``header_offset`` looks like a value
1558
+ the zip reader vouched for and is nothing of the kind: a central directory record
1559
+ may carry the legacy sentinel ``0xffffffff`` in its 32-bit offset field plus a ZIP64
1560
+ extra field holding a 64-bit replacement, and ``ZipInfo._decodeExtra`` substitutes
1561
+ that verbatim without ever comparing it to the size of the file. A 146-byte archive
1562
+ can therefore claim its entry's local header begins at ``2**64 - 1``. So the offset
1563
+ is bound-checked against the archive's real size BEFORE the seek, and an offset that
1564
+ cannot hold a header is unreadable like any other — which also makes "past the end
1565
+ of the file" and "too large to seek to at all" one case instead of two.
1566
+
1567
+ PRECONDITION on sharing ``zf.fp``, stated rather than assumed. This seeks a file
1568
+ pointer it does not own, and it does so WITHOUT taking ``ZipFile._lock``, so it is
1569
+ not safe in general — a truly concurrent read, or a file object shared with anything
1570
+ outside this ``ZipFile``, could be moved out from under mid-read. It is safe for
1571
+ THIS importer's usage, and only because of four properties that hold together:
1572
+ :func:`import_bundle` wraps the upload in a private :class:`io.BytesIO` that nothing
1573
+ else holds a reference to; the whole-bundle scan runs to completion before any entry
1574
+ is read; ``ZipFile.open``'s reader re-seeks to its own position before every read, so
1575
+ a moved pointer cannot corrupt an open stream; and the asset reads are synchronous
1576
+ ``zf.read`` calls with no interleaving. The position is restored afterwards anyway —
1577
+ it costs one seek and removes the need for a reader of this function to know any of
1578
+ the above.
1579
+
1580
+ ``None`` means the header could not be read as a local file header at all, which
1581
+ callers treat as a refusal (:data:`_REASON_LOCAL_HEADER`) rather than as
1582
+ permission: an entry whose second name is unknown is not an entry with one name.
1583
+ """
1584
+ fp = getattr(zf, "fp", None)
1585
+ if fp is None:
1586
+ return None
1587
+ try:
1588
+ resume_at = fp.tell()
1589
+ try:
1590
+ fp.seek(0, io.SEEK_END)
1591
+ archive_size = fp.tell()
1592
+ offset = info.header_offset
1593
+ # Negative is nonsense and at/after EOF means the header is not there. Both
1594
+ # are checked BEFORE the seek, because a large enough offset does not fail
1595
+ # the seek gracefully — it cannot be converted to a C ssize_t and raises
1596
+ # OverflowError, which is not an OSError and is not a ValueError.
1597
+ if offset < 0 or offset + _LOCAL_HEADER_LEN > archive_size:
1598
+ return None
1599
+ fp.seek(offset)
1600
+ header = fp.read(_LOCAL_HEADER_LEN)
1601
+ if (
1602
+ len(header) != _LOCAL_HEADER_LEN
1603
+ or header[:4] != _LOCAL_HEADER_SIGNATURE
1604
+ ):
1605
+ return None
1606
+ name_len, extra_len = struct.unpack_from(
1607
+ "<HH", header, _LOCAL_HEADER_LENGTHS_OFFSET
1608
+ )
1609
+ name_bytes = fp.read(name_len)
1610
+ extra = fp.read(extra_len)
1611
+ finally:
1612
+ fp.seek(resume_at)
1613
+ except (OSError, ValueError, OverflowError):
1614
+ # A seek on a detached/closed stream raises, and the bound check above cannot
1615
+ # cover an arithmetic surprise nobody has thought of yet — OverflowError is
1616
+ # here as the backstop for the next one, since the whole point of this helper
1617
+ # is that its input is hostile. Unreadable, like any other unparseable header.
1618
+ #
1619
+ # NOT bare ``Exception``: a genuine bug in the parsing this guards must still
1620
+ # crash loudly rather than be laundered into a path refusal.
1621
+ return None
1622
+ if len(name_bytes) != name_len or len(extra) != extra_len:
1623
+ return None
1624
+ (flags,) = struct.unpack_from("<H", header, _LOCAL_HEADER_FLAGS_OFFSET)
1625
+ encoding = "utf-8" if flags & _UTF8_NAME_FLAG else "cp437"
1626
+ try:
1627
+ return name_bytes.decode(encoding), extra
1628
+ except UnicodeDecodeError:
1629
+ return None
1473
1630
 
1474
- ``rel_path`` is the name to CLASSIFY when it differs from the archive's — the
1475
- root-prefix-stripped path, for the in-scope iterator. It has to be a separate
1476
- argument because the template-thumbnail allowlist is anchored at ``templates/``:
1477
- a wrapped bundle's ``safe/templates/x/.thumbnail`` only matches once ``safe/`` is
1478
- off. The identity checks below always apply to the FULL recorded name, which is
1479
- the string a rewrite would have altered.
1480
1631
 
1481
- TWO identity checks, because there are two independent ways an entry can end up
1482
- with more than one name, and each is invisible to the other:
1632
+ def _entry_identity_refusal(
1633
+ zf: zipfile.ZipFile, info: zipfile.ZipInfo
1634
+ ) -> Optional[str]:
1635
+ """Refuse an entry that the archive gives more than ONE name, else ``None``.
1636
+
1637
+ THE place every name-rewriting route is collected, so closing one cannot leave
1638
+ another open. There are four, and each is invisible to the others:
1483
1639
 
1484
1640
  1. ``orig_filename != filename`` — the reader rewrote the central name. A NUL
1485
1641
  byte does this: ``templates/x/.thumbnail\\x00.exe`` is truncated to
1486
1642
  ``templates/x/.thumbnail``, which matches the thumbnail allowlist.
1487
- 2. :func:`_extra_field_name_refusal` — an extra field declares a different name.
1488
- This one DEFEATS check 1 entirely, because the stdlib sanitizes the declared
1489
- name before assigning it, leaving the two attributes equal.
1490
-
1491
- Validating the raw name alone would refuse case 1's entry but leave the deeper
1492
- problem: ``zf.read``/``namelist`` and the rest of the importer key off
1493
- ``filename``, so raw-name validation and by-name retrieval would be talking about
1494
- different files. Refusing any divergence keeps one name per entry everywhere,
1495
- which is the same property the backslash refusal buys (see
1643
+ 2. A ``0x7075`` record in the CENTRAL extra declaring another name. This defeats
1644
+ route 1 entirely, because the stdlib sanitizes the declared name before
1645
+ assigning it, leaving the two attributes equal.
1646
+ 3. The LOCAL file header's name disagreeing with the central directory's.
1647
+ CPython does compare these, but only inside ``ZipFile.open`` — i.e. only for
1648
+ entries whose bytes are actually read, and only as a ``BadZipFile`` rather
1649
+ than as something the user can act on. Refusing here covers every entry and
1650
+ says what is wrong.
1651
+ 4. A ``0x7075`` record in the LOCAL extra. This defeats routes 1-3 together: the
1652
+ central directory is entirely clean, so nothing CPython parses is out of
1653
+ place, while a reader that trusts local headers sees the other name.
1654
+
1655
+ Checked in that order, so the reported reason names the shallowest defect the
1656
+ entry has rather than the deepest.
1657
+
1658
+ All four presuppose that the archive records a name at all, which is why the EMPTY
1659
+ recorded name is refused ahead of them: an entry with no central-directory name has
1660
+ no identity for this function to establish, and every comparison below would be
1661
+ against ``""``. It is reported as the empty name it is rather than as a mismatch
1662
+ with whatever CPython resolved, because that is the fact the user can act on.
1663
+ """
1664
+ central = _raw_entry_name(info)
1665
+ if not central:
1666
+ return _REASON_EMPTY_RECORDED_NAME
1667
+ if central != info.filename:
1668
+ return _REASON_NAME_MISMATCH
1669
+ reason = _extra_field_name_refusal(info.extra or b"", central)
1670
+ if reason is not None:
1671
+ return reason
1672
+ local = _local_header_identity(zf, info)
1673
+ if local is None:
1674
+ return _REASON_LOCAL_HEADER
1675
+ local_name, local_extra = local
1676
+ if local_name != central:
1677
+ return _REASON_NAME_MISMATCH
1678
+ return _extra_field_name_refusal(local_extra, central)
1679
+
1680
+
1681
+ def _entry_verdict_for_info(
1682
+ zf: zipfile.ZipFile, info: zipfile.ZipInfo
1683
+ ) -> _EntryVerdict:
1684
+ """The verdict for one ZipInfo, judged on the name the archive really records.
1685
+
1686
+ The whole-bundle scan's composition of the two shared steps, over the FULL recorded
1687
+ name. :func:`_iter_safe_entries` applies the very same two steps in the very same
1688
+ order but cannot call this, because it has to interleave its root-prefix scoping
1689
+ between them: identity is judged for every member, scoping then decides which
1690
+ entries this import stores, and only those are classified — on the STRIPPED path,
1691
+ because the template-thumbnail allowlist is anchored at ``templates/``. Identity is
1692
+ judged on the full recorded name in both gates, that being the string a rewrite
1693
+ would have altered.
1694
+
1695
+ IDENTITY FIRST, then the path rules. :func:`_entry_identity_refusal` establishes
1696
+ that the entry has exactly ONE name — across both of the archive's copies of it —
1697
+ and only then is that name classified. The order is the point: validating a path
1698
+ that the archive states twice validates whichever copy this reader happened to
1699
+ resolve, while ``zf.read``/``namelist`` and the rest of the importer key off
1700
+ ``filename``. Refusing any divergence keeps one name per entry everywhere, which
1701
+ is the same property the backslash refusal buys (see
1496
1702
  :func:`_entry_path_verdict`).
1703
+
1704
+ ``zf`` is needed because half of what an archive says about an entry lives in the
1705
+ local file header, which no ``ZipInfo`` attribute exposes.
1497
1706
  """
1498
- raw = _raw_entry_name(info)
1499
- if raw != info.filename:
1500
- return _EntryVerdict(_ENTRY_REFUSE, reason=_REASON_NAME_MISMATCH)
1501
- extra_reason = _extra_field_name_refusal(info)
1502
- if extra_reason is not None:
1503
- return _EntryVerdict(_ENTRY_REFUSE, reason=extra_reason)
1504
- return _classify_bundle_entry(raw if rel_path is None else rel_path)
1707
+ identity_reason = _entry_identity_refusal(zf, info)
1708
+ if identity_reason is not None:
1709
+ return _EntryVerdict(_ENTRY_REFUSE, reason=identity_reason)
1710
+ return _classify_bundle_entry(_raw_entry_name(info))
1505
1711
 
1506
1712
 
1507
1713
  def _is_symlink(info: zipfile.ZipInfo) -> bool:
@@ -1558,15 +1764,22 @@ def _assert_bundle_paths_safe(zf: zipfile.ZipFile) -> None:
1558
1764
 
1559
1765
  THIS GATE ESTABLISHES THE INVARIANT THE REST OF THE IMPORTER RELIES ON. It runs
1560
1766
  before root discovery and before any read (see :func:`import_bundle`), and it
1561
- refuses any entry whose recorded name differs from the name Python reports or
1562
- which contains a ``\\``. Everything downstream — :func:`_locate_root_prefix`,
1563
- the ``startswith(root_prefix)`` scoping and slicing in
1564
- :func:`_iter_safe_entries`, and ``zf.read``-by-name — therefore works on names
1565
- that are exactly what validation judged. Weakening either refusal reintroduces
1566
- two path identities in one importer.
1567
-
1568
- The verdict comes from :func:`_entry_verdict_for_info`, the SAME judgement
1569
- :func:`_iter_safe_entries` uses, so the two gates cannot disagree about an entry.
1767
+ refuses any entry the archive gives more than one name — whether the divergence is
1768
+ between ``orig_filename`` and ``filename``, between the two headers that record
1769
+ the entry, or in a name-rewriting extra field in either of them
1770
+ (:func:`_entry_identity_refusal`) — as well as any name containing a ``\\``.
1771
+ Everything downstream — :func:`_locate_root_prefix`, the
1772
+ ``startswith(root_prefix)`` scoping and slicing in :func:`_iter_safe_entries`, and
1773
+ ``zf.read``-by-name — therefore works on names that are exactly what validation
1774
+ judged. Weakening any of those refusals reintroduces two path identities in one
1775
+ importer.
1776
+
1777
+ The verdict comes from :func:`_entry_verdict_for_info`, which composes the SAME two
1778
+ shared steps :func:`_iter_safe_entries` applies —
1779
+ :func:`_entry_identity_refusal` then :func:`_classify_bundle_entry` — so the two
1780
+ gates cannot disagree about an entry. The iterator runs them either side of its own
1781
+ root-prefix scoping, which is why it composes them itself rather than calling this
1782
+ gate's helper; the classifiers are shared, so there is still exactly one of each.
1570
1783
  """
1571
1784
  claimed: dict[str, str] = {}
1572
1785
  for info in zf.infolist():
@@ -1579,7 +1792,7 @@ def _assert_bundle_paths_safe(zf: zipfile.ZipFile) -> None:
1579
1792
  f"Bundle entry {name!r} is a symlink; refusing to import."
1580
1793
  )
1581
1794
  # No ``if name`` guard: an empty name is refused, not skipped.
1582
- verdict = _entry_verdict_for_info(info)
1795
+ verdict = _entry_verdict_for_info(zf, info)
1583
1796
  if verdict.kind == _ENTRY_REFUSE:
1584
1797
  raise DesignSystemImportError(_path_refusal_message(name, verdict.reason))
1585
1798
  if verdict.kind == _ENTRY_FILE:
@@ -1594,7 +1807,8 @@ def _iter_safe_entries(
1594
1807
  """Yield ``(ZipInfo, safe_rel_path)`` for every in-scope bundle FILE entry.
1595
1808
 
1596
1809
  Skips directories, OS junk (``__MACOSX``, ``.DS_Store``) and dotfiles. Raises
1597
- :class:`DesignSystemImportError` on a zip-slip path (absolute or ``..``) so a
1810
+ :class:`DesignSystemImportError` on an in-scope zip-slip path (absolute or ``..``),
1811
+ and on ANY entry the archive gives more than one name wherever it sits, so a
1598
1812
  malicious bundle is rejected rather than silently stored.
1599
1813
 
1600
1814
  This is the ACTUAL first gate every entry passes, so the ONE dot-prefixed shape
@@ -1603,12 +1817,19 @@ def _iter_safe_entries(
1603
1817
  the recognizer alone reads as working in a unit test and still drops every real
1604
1818
  thumbnail, because the entry never reaches the recognizer.
1605
1819
 
1606
- Every path rule lives in :func:`_classify_bundle_entry`, reached through
1607
- :func:`_entry_verdict_for_info` — the same judgement the up-front scan uses — so
1608
- neither gate can be patched into disagreeing with the other. Stripping
1609
- ``root_prefix`` is the only work done here, and it is bookkeeping rather than a
1610
- path rule: it is a raw slice, which is sound precisely because no validated name
1611
- is ever a rewritten one (no ``\\`` fold, no stdlib truncation).
1820
+ Judged in TWO STEPS, in the same order :func:`_entry_verdict_for_info` applies them
1821
+ for the up-front scan, with the scoping filter interleaved between the two:
1822
+ :func:`_entry_identity_refusal` for every member, then
1823
+ :func:`_classify_bundle_entry` for the ones this import is scoped to. Both halves
1824
+ are the SHARED helpers, so neither gate can be patched into disagreeing with the
1825
+ other. Interleaving is what keeps the two halves' different natures straight: an
1826
+ entry outside the root is legitimately none of this import's business, while an
1827
+ entry the archive names twice is hostile wherever it sits — see the comments at the
1828
+ two steps.
1829
+
1830
+ Stripping ``root_prefix`` is the only work done here, and it is bookkeeping rather
1831
+ than a path rule: it is a raw slice, which is sound precisely because no validated
1832
+ name is ever a rewritten one (no ``\\`` fold, no stdlib truncation).
1612
1833
 
1613
1834
  Two in-scope entries claiming ONE path are refused here as well as by the up-front
1614
1835
  scan, so every path this yields is DISTINCT and callers do not need a first-wins
@@ -1625,12 +1846,45 @@ def _iter_safe_entries(
1625
1846
  claimed: dict[str, str] = {}
1626
1847
  for info in zf.infolist():
1627
1848
  name = _raw_entry_name(info)
1849
+ # IDENTITY BEFORE SCOPE, for EVERY member. Whether the archive gives this entry
1850
+ # ONE name is a fact about the entry, true wherever it sits, so it is settled
1851
+ # before the scoping below decides whether this import has any interest in it.
1852
+ # Ordering these the other way round left the iterator judging only what the
1853
+ # scope filter let through, and an entry it dropped was never judged at all: an
1854
+ # EMPTY recorded name is "in scope" of every prefix and strips to nothing, so
1855
+ # ``not rel_raw`` SKIPPED it — the one shape that is hostile precisely because
1856
+ # the archive records no name for it.
1857
+ #
1858
+ # Nothing was exploitable: :func:`_assert_bundle_paths_safe` runs first on every
1859
+ # production path and refuses such an entry before root discovery or any read.
1860
+ # What was lost is that this gate no longer refused INDEPENDENTLY, which is the
1861
+ # entire value of having two.
1862
+ identity_reason = _entry_identity_refusal(zf, info)
1863
+ if identity_reason is not None:
1864
+ raise DesignSystemImportError(
1865
+ _path_refusal_message(name, identity_reason)
1866
+ )
1867
+ # SCOPE, unchanged and still SILENT. An entry outside the discovered root is
1868
+ # not one of the entries this import stores, so it is passed over rather than
1869
+ # refused — including one whose PATH is unsafe, which the whole-bundle scan
1870
+ # already refuses GLOBALLY (see :func:`_assert_bundle_paths_safe`). Raising here
1871
+ # on an out-of-scope path would widen this generator from "the entries this
1872
+ # import stores" to "every entry in the archive", which is a different contract
1873
+ # from the one its caller has. Identity is not that: it is judged above because
1874
+ # no root prefix makes a second name acceptable.
1628
1875
  if not name.startswith(root_prefix):
1629
1876
  continue
1630
1877
  rel_raw = name[len(root_prefix):]
1631
1878
  if not rel_raw:
1632
1879
  continue # the root-prefix directory entry itself
1633
- verdict = _entry_verdict_for_info(info, rel_raw)
1880
+ # The STRIPPED path is what gets classified — the reason this is
1881
+ # :func:`_classify_bundle_entry` directly rather than
1882
+ # :func:`_entry_verdict_for_info`, whose identity half has already run above.
1883
+ # The template-thumbnail allowlist is anchored at ``templates/``, so a wrapped
1884
+ # bundle's ``safe/templates/x/.thumbnail`` only matches once ``safe/`` is off;
1885
+ # the identity check above deliberately used the FULL recorded name instead,
1886
+ # which is the string a rewrite would have altered.
1887
+ verdict = _classify_bundle_entry(rel_raw)
1634
1888
  if verdict.kind == _ENTRY_REFUSE:
1635
1889
  raise DesignSystemImportError(
1636
1890
  _path_refusal_message(rel_raw, verdict.reason)