databricks-tellr-app 0.4.2.dev9__tar.gz → 0.4.2.dev11__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.
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/PKG-INFO +1 -1
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/databricks_tellr_app.egg-info/PKG-INFO +1 -1
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/pyproject.toml +1 -1
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/settings/design_systems.py +37 -2
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/design_system_service.py +255 -46
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/design_system_templates.py +31 -3
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/README.md +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/databricks_tellr_app/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/databricks_tellr_app/run.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/databricks_tellr_app.egg-info/SOURCES.txt +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/databricks_tellr_app.egg-info/requires.txt +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/databricks_tellr_app.egg-info/top_level.txt +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/setup.cfg +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/setup.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/_otel_bootstrap.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/fixtures/tour_demo_deck.json +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/main.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/mcp_auth.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/mcp_server.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/middleware/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/middleware/csrf.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/middleware/request_logging.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/middleware/security_headers.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/_authz.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/admin.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/admin_usage.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/agent_config.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/chat.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/deck_contributors.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/export.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/feedback.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/google_slides.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/images.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/local_version.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/profiles.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/sessions.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/settings/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/settings/contributors.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/settings/deck_prompts.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/settings/identities.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/settings/slide_styles.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/setup.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/slides.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/tools.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/tour.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/verification.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/version.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/schemas/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/schemas/agent_config.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/schemas/feedback.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/schemas/requests.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/schemas/responses.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/schemas/settings/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/schemas/settings/requests.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/schemas/settings/responses.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/schemas/streaming.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/services/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/services/chat_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/services/export_job_queue.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/services/feedback_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/services/job_queue.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/services/session_manager.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/services/session_naming.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/services/usage_events.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/services/usage_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/utils/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/utils/validation.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/config_loader.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/context_utils.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/database.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/databricks_client.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/defaults.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/encryption.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/init_default_profile.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/lakebase.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/migrate_profiles_to_agent_config.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/mlflow_agent_spans.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/mlflow_tracing.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/otel_logging.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/permission_context.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/prompt_modules.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/settings_db.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/core/user_context.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/deck_contributor.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/design_system.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/encryption_key.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/feedback.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/genie_space.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/google_global_credentials.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/google_oauth_token.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/identity.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/image.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/profile.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/profile_contributor.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/prompts.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/request_log.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/session.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/slide_deck_prompt.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/slide_style_library.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/usage_event.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/models/user_preference.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/database/types.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/domain/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/domain/slide.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/domain/slide_deck.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/agent.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/agent_factory.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/config_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/config_validator.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/converter_jail/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/converter_jail/ast_guard.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/converter_jail/codeprep.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/converter_jail/gslides_runner.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/converter_jail/jail.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/converter_jail/pptx_runner.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/converter_jail/protocol.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/design_system_compiler.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/drive_uploader.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/evaluation/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/evaluation/llm_judge.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/genie_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/google_slides_auth.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/google_slides_prompts_defaults.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/html_to_google_slides.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/html_to_pptx.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/identity_provider.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/identity_providers/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/identity_providers/account_provider.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/identity_providers/local_provider.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/identity_providers/workspace_provider.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/image_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/image_tools.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/permission_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/pptx_from_html_huashu.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/pptx_from_records.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/pptx_prompts_defaults.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/profile_service.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/streaming_callback.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/tools/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/tools/agent_bricks_tool.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/tools/ds_asset_tool.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/tools/genie_tool.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/tools/mcp_tool.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/tools/model_endpoint_tool.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/tools/vector_tool.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/services/validator.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/utils/__init__.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/utils/css_utils.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/utils/ds_asset_utils.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/utils/error_handling.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/utils/html_safety.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/utils/html_utils.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/utils/image_utils.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/utils/js_validator.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/utils/logging_config.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/utils/pi_filter.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/utils/slide_hash.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/utils/spotlight.py +0 -0
- {databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/utils/text_caps.py +0 -0
|
@@ -172,6 +172,28 @@ class DesignSystemDetail(DesignSystemSummary):
|
|
|
172
172
|
assets: List[AssetOut]
|
|
173
173
|
|
|
174
174
|
|
|
175
|
+
class ImportWarningOut(BaseModel):
|
|
176
|
+
"""One bundle entry the import ignored, and why."""
|
|
177
|
+
path: str
|
|
178
|
+
reason: str
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class DesignSystemImportResult(DesignSystemDetail):
|
|
182
|
+
"""The /import response: the imported detail PLUS anything that was ignored.
|
|
183
|
+
|
|
184
|
+
Additive by design. Some entries are individually unusable without making the
|
|
185
|
+
bundle unusable (an unsniffable template screenshot, a non-UTF-8 token source);
|
|
186
|
+
those are dropped so one junk file cannot cost the whole upload. Reporting them
|
|
187
|
+
only to the server log told the user the import had succeeded and left them to
|
|
188
|
+
discover a missing thumbnail themselves.
|
|
189
|
+
|
|
190
|
+
A NON-FATAL list on a 201, and its own model rather than a field on
|
|
191
|
+
:class:`DesignSystemDetail`, so the GET/create/update responses are byte-for-byte
|
|
192
|
+
unchanged and an existing /import client simply ignores a field it does not read.
|
|
193
|
+
"""
|
|
194
|
+
warnings: List[ImportWarningOut] = []
|
|
195
|
+
|
|
196
|
+
|
|
175
197
|
class DesignSystemCreate(BaseModel):
|
|
176
198
|
"""Structured (in-app) create — thin. Assets arrive via /import."""
|
|
177
199
|
# UNCAPPED, matching the unbounded ``design_system.name`` column; ``min_length``
|
|
@@ -496,7 +518,11 @@ def list_design_systems(
|
|
|
496
518
|
)
|
|
497
519
|
|
|
498
520
|
|
|
499
|
-
@router.post(
|
|
521
|
+
@router.post(
|
|
522
|
+
"/import",
|
|
523
|
+
response_model=DesignSystemImportResult,
|
|
524
|
+
status_code=status.HTTP_201_CREATED,
|
|
525
|
+
)
|
|
500
526
|
async def import_design_system(
|
|
501
527
|
file: UploadFile = File(...),
|
|
502
528
|
name: Optional[str] = Form(None),
|
|
@@ -517,14 +543,23 @@ async def import_design_system(
|
|
|
517
543
|
detail=f"Upload exceeds the maximum bundle size of {MAX_BUNDLE_SIZE_BYTES} bytes",
|
|
518
544
|
)
|
|
519
545
|
try:
|
|
546
|
+
# Filled with the entries the import dropped without failing, so the caller
|
|
547
|
+
# learns about them instead of only the server log doing so.
|
|
548
|
+
dropped: list[design_system_service.BundleImportWarning] = []
|
|
520
549
|
ds = design_system_service.import_bundle(
|
|
521
550
|
db,
|
|
522
551
|
zip_bytes=content,
|
|
523
552
|
user=_current_user(),
|
|
524
553
|
name_override=name,
|
|
525
554
|
source_filename=file.filename,
|
|
555
|
+
warnings=dropped,
|
|
556
|
+
)
|
|
557
|
+
return DesignSystemImportResult(
|
|
558
|
+
**_detail(ds).model_dump(),
|
|
559
|
+
warnings=[
|
|
560
|
+
ImportWarningOut(path=w.path, reason=w.reason) for w in dropped
|
|
561
|
+
],
|
|
526
562
|
)
|
|
527
|
-
return _detail(ds)
|
|
528
563
|
except DesignSystemNameConflictError as e:
|
|
529
564
|
db.rollback()
|
|
530
565
|
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(e))
|
|
@@ -11,9 +11,13 @@ Lakebase rows:
|
|
|
11
11
|
custom properties are parsed as an ADDITIONAL token source.
|
|
12
12
|
- ``fonts/**`` and ``assets/**`` — binary files stored as ``design_system_asset``
|
|
13
13
|
rows (bytes in-DB, following the ``image_assets`` pattern). ``preview*`` files
|
|
14
|
-
under ``assets/`` are reference-only and skipped; a template folder's
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
under ``assets/`` are reference-only and skipped; a template folder's screenshot
|
|
15
|
+
IS stored (kind ``template_shot``, v1 Phase 4) so the template picker can serve
|
|
16
|
+
thumbnails. Both shipped shapes count: ``templates/<folder>/preview.<ext>`` and
|
|
17
|
+
the DOT-PREFIXED, EXTENSION-LESS ``templates/<folder>/.thumbnail`` a real export
|
|
18
|
+
writes — the latter is allowlisted past the general dotfile skip and has its
|
|
19
|
+
media type SNIFFED from its magic bytes (unknown content is refused, never
|
|
20
|
+
stored under a guessed type).
|
|
17
21
|
|
|
18
22
|
v1 Phase 1 ("import foundation") extends the importer WITHOUT changing the
|
|
19
23
|
generation seam:
|
|
@@ -55,7 +59,7 @@ import logging
|
|
|
55
59
|
import mimetypes
|
|
56
60
|
import re
|
|
57
61
|
import zipfile
|
|
58
|
-
from typing import Any, Optional
|
|
62
|
+
from typing import Any, NamedTuple, Optional
|
|
59
63
|
|
|
60
64
|
from sqlalchemy.orm import Session, defer
|
|
61
65
|
|
|
@@ -74,6 +78,22 @@ logger = logging.getLogger(__name__)
|
|
|
74
78
|
MANIFEST_FILENAME = "_ds_manifest.json"
|
|
75
79
|
DEFAULT_CSS_TOKEN_SOURCE = "colors_and_type.css"
|
|
76
80
|
|
|
81
|
+
|
|
82
|
+
class BundleImportWarning(NamedTuple):
|
|
83
|
+
"""A bundle entry the import DROPPED without failing the upload.
|
|
84
|
+
|
|
85
|
+
Some entries are individually unusable without making the bundle unusable — an
|
|
86
|
+
unsniffable template screenshot, a CSS token source that is not UTF-8. Dropping
|
|
87
|
+
just that entry is right (one junk file must not cost the whole upload), but the
|
|
88
|
+
caller was then told the import succeeded with nothing to say a file had been
|
|
89
|
+
ignored, so the only record was a server-side log the user cannot see. These ride
|
|
90
|
+
back on the import response instead.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
path: str
|
|
94
|
+
reason: str
|
|
95
|
+
|
|
96
|
+
|
|
77
97
|
# Color sub-groups the compiler renders as :root vars. A token name whose first
|
|
78
98
|
# segment is one of these carries its group in the name (e.g. --brand-accents-lava).
|
|
79
99
|
_COLOR_SUBGROUPS = frozenset(("core", "accents", "ink", "tints"))
|
|
@@ -276,21 +296,89 @@ def _infer_asset_kind(rel_path: str) -> str:
|
|
|
276
296
|
# raster-image extension) — stored as a ``template_shot`` asset so the Phase 4
|
|
277
297
|
# template picker can serve thumbnails. Checked BEFORE ``_should_skip`` (which
|
|
278
298
|
# excludes ``templates/**`` from generic asset storage).
|
|
299
|
+
#
|
|
300
|
+
# Anchored with ``\Z``, NOT ``$``: in Python ``$`` matches at the end of the string
|
|
301
|
+
# OR immediately before a single trailing newline, so a ``$``-anchored allowlist on
|
|
302
|
+
# a PATH accepts ``<allowed shape>\n`` — ``templates/x/preview.png\n`` was matched,
|
|
303
|
+
# stored, and served. ``\Z`` is the true end of string. Applies to every allowlist
|
|
304
|
+
# regex in this module that judges a path or a filename.
|
|
279
305
|
_TEMPLATE_PREVIEW_RE = re.compile(
|
|
280
|
-
r"^templates/[^/]+/preview[^/]*\.(png|jpe?g|gif|webp)
|
|
306
|
+
r"^templates/[^/]+/preview[^/]*\.(png|jpe?g|gif|webp)\Z", re.IGNORECASE
|
|
281
307
|
)
|
|
282
308
|
|
|
309
|
+
# The SAME screenshot as it actually ships in a real export: DOT-PREFIXED, named
|
|
310
|
+
# ``thumbnail`` rather than ``preview``, and carrying NO file extension —
|
|
311
|
+
# ``templates/<folder>/.thumbnail``. Every one of those was discarded, so a real
|
|
312
|
+
# bundle imported with no thumbnail at all and a NULL ``thumbnail_url``.
|
|
313
|
+
#
|
|
314
|
+
# Deliberately the NARROWEST path shape that describes them: exactly one folder
|
|
315
|
+
# segment under ``templates/``, an optional single leading dot, one of the two
|
|
316
|
+
# known basenames, and NOTHING else — no extension, no suffix. It is this
|
|
317
|
+
# tightness that makes it safe for :func:`_iter_safe_entries` to exempt a match
|
|
318
|
+
# from the general dotfile skip; a dotfile anywhere else stays skipped. Because
|
|
319
|
+
# a match carries no extension, its media type is NOT guessable from the name and
|
|
320
|
+
# is sniffed from the content instead (:func:`_sniff_raster_mime`).
|
|
321
|
+
# ``\Z`` for the same reason as _TEMPLATE_PREVIEW_RE: with ``$`` this accepted
|
|
322
|
+
# ``templates/x/.thumbnail\n``, which was then stored AND linked as a template's
|
|
323
|
+
# thumbnail_url.
|
|
324
|
+
_TEMPLATE_THUMBNAIL_RE = re.compile(
|
|
325
|
+
r"^templates/[^/]+/\.?(thumbnail|preview)\Z", re.IGNORECASE
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Magic-byte signatures for the raster formats a template thumbnail may be. An
|
|
329
|
+
# extension-less file is stored ONLY if its bytes match one of these; unknown
|
|
330
|
+
# content is refused rather than persisted under a guessed content type. SVG is
|
|
331
|
+
# absent on purpose (it can carry inline script and has no magic number).
|
|
332
|
+
_PNG_MAGIC = b"\x89PNG\r\n\x1a\n"
|
|
333
|
+
_JPEG_MAGIC = b"\xff\xd8\xff"
|
|
334
|
+
_GIF_MAGICS = (b"GIF87a", b"GIF89a")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _sniff_raster_mime(data: bytes) -> Optional[str]:
|
|
338
|
+
"""MIME type from an image's MAGIC BYTES, or ``None`` if not a known raster.
|
|
339
|
+
|
|
340
|
+
Content-based detection for files whose name carries no extension. WebP is a
|
|
341
|
+
RIFF container, so ``RIFF`` alone is not enough — the form type at bytes 8:12
|
|
342
|
+
must spell ``WEBP``, otherwise a RIFF/WAVE file would be stored as an image.
|
|
343
|
+
"""
|
|
344
|
+
if data.startswith(_PNG_MAGIC):
|
|
345
|
+
return "image/png"
|
|
346
|
+
if data.startswith(_JPEG_MAGIC):
|
|
347
|
+
return "image/jpeg"
|
|
348
|
+
if data.startswith(_GIF_MAGICS):
|
|
349
|
+
return "image/gif"
|
|
350
|
+
if data[:4] == b"RIFF" and data[8:12] == b"WEBP":
|
|
351
|
+
return "image/webp"
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _is_template_thumbnail(rel_path: str) -> bool:
|
|
356
|
+
"""True for an EXTENSION-LESS template screenshot (``templates/x/.thumbnail``).
|
|
357
|
+
|
|
358
|
+
Distinguished from :func:`_is_template_preview` because the absence of an
|
|
359
|
+
extension is what forces content sniffing.
|
|
360
|
+
"""
|
|
361
|
+
return bool(_TEMPLATE_THUMBNAIL_RE.match(rel_path.lower()))
|
|
362
|
+
|
|
283
363
|
|
|
284
364
|
def _is_template_preview(rel_path: str) -> bool:
|
|
285
|
-
|
|
365
|
+
"""True for a template folder's screenshot in EITHER shipped shape."""
|
|
366
|
+
low = rel_path.lower()
|
|
367
|
+
return bool(_TEMPLATE_PREVIEW_RE.match(low)) or bool(
|
|
368
|
+
_TEMPLATE_THUMBNAIL_RE.match(low)
|
|
369
|
+
)
|
|
286
370
|
|
|
287
371
|
|
|
288
372
|
def _should_skip(rel_path: str) -> bool:
|
|
289
373
|
"""Only ``assets/**`` and ``fonts/**`` files are stored; skip everything else.
|
|
290
374
|
|
|
291
375
|
Directories, OS junk, dotfiles, template screenshots and ``preview*`` files
|
|
292
|
-
(reference-only material) are excluded. Template-folder
|
|
293
|
-
|
|
376
|
+
(reference-only material) are excluded. Template-folder screenshots are the
|
|
377
|
+
exception — the caller checks :func:`_is_template_preview` first.
|
|
378
|
+
|
|
379
|
+
NOTE: this is not the FIRST gate an entry passes. ``_iter_safe_entries`` runs
|
|
380
|
+
upstream and applies its own dotfile skip, so a dot-prefixed screenshot has to
|
|
381
|
+
be allowlisted THERE as well or it never reaches this function at all.
|
|
294
382
|
"""
|
|
295
383
|
low = rel_path.lower()
|
|
296
384
|
base = _basename(low)
|
|
@@ -500,12 +588,20 @@ def import_bundle(
|
|
|
500
588
|
user: Optional[str],
|
|
501
589
|
name_override: Optional[str] = None,
|
|
502
590
|
source_filename: Optional[str] = None,
|
|
591
|
+
warnings: Optional[list[BundleImportWarning]] = None,
|
|
503
592
|
) -> DesignSystem:
|
|
504
593
|
"""Import a ``.zip`` design-system bundle into Lakebase and compile it.
|
|
505
594
|
|
|
506
595
|
Returns the persisted :class:`DesignSystem` (committed, with tokens + assets +
|
|
507
596
|
``compiled_style_content``).
|
|
508
597
|
|
|
598
|
+
Args:
|
|
599
|
+
warnings: optional collector. Pass a list to receive an
|
|
600
|
+
:class:`BundleImportWarning` for every entry the import DROPPED without
|
|
601
|
+
failing — the caller can then tell the user which files were ignored and
|
|
602
|
+
why. Omitting it keeps the previous signature working for the many
|
|
603
|
+
callers that only want the design system.
|
|
604
|
+
|
|
509
605
|
Raises:
|
|
510
606
|
DesignSystemImportError: bundle is not a zip, missing/invalid manifest, or
|
|
511
607
|
violates a size limit (HTTP 400).
|
|
@@ -563,8 +659,10 @@ def import_bundle(
|
|
|
563
659
|
# Read the DECLARED CSS token sources ONCE (budgeted); the same bytes are
|
|
564
660
|
# reused for both token parsing and source-file retention (no double-charge).
|
|
565
661
|
css_sources = _read_css_sources(zf, root_prefix, manifest, budget)
|
|
566
|
-
tokens = _collect_tokens(manifest, _decode_css_texts(css_sources))
|
|
567
|
-
assets, files = _collect_assets_and_files(
|
|
662
|
+
tokens = _collect_tokens(manifest, _decode_css_texts(css_sources, warnings))
|
|
663
|
+
assets, files = _collect_assets_and_files(
|
|
664
|
+
zf, root_prefix, budget, css_sources, warnings
|
|
665
|
+
)
|
|
568
666
|
|
|
569
667
|
# Stored VERBATIM; the strip decides only whether the brand wrote a description
|
|
570
668
|
# at all (a whitespace-only one describes nothing, so it stays NULL). Same split
|
|
@@ -913,7 +1011,10 @@ def _read_css_sources(
|
|
|
913
1011
|
return result
|
|
914
1012
|
|
|
915
1013
|
|
|
916
|
-
def _decode_css_texts(
|
|
1014
|
+
def _decode_css_texts(
|
|
1015
|
+
css_sources: "dict[str, bytes]",
|
|
1016
|
+
warnings: "Optional[list[BundleImportWarning]]" = None,
|
|
1017
|
+
) -> list[str]:
|
|
917
1018
|
"""Decode the pre-read CSS source bytes to UTF-8 text (skip undecodable)."""
|
|
918
1019
|
texts: list[str] = []
|
|
919
1020
|
for path, raw in css_sources.items():
|
|
@@ -921,31 +1022,98 @@ def _decode_css_texts(css_sources: "dict[str, bytes]") -> list[str]:
|
|
|
921
1022
|
texts.append(raw.decode("utf-8"))
|
|
922
1023
|
except UnicodeDecodeError:
|
|
923
1024
|
logger.warning("Skipping non-UTF-8 CSS token source: %s", path)
|
|
1025
|
+
if warnings is not None:
|
|
1026
|
+
warnings.append(
|
|
1027
|
+
BundleImportWarning(
|
|
1028
|
+
path,
|
|
1029
|
+
"not valid UTF-8; its design tokens were not read.",
|
|
1030
|
+
)
|
|
1031
|
+
)
|
|
924
1032
|
return texts
|
|
925
1033
|
|
|
926
1034
|
|
|
927
1035
|
def _safe_relpath(rel_path: str) -> Optional[str]:
|
|
928
|
-
"""
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1036
|
+
"""The CANONICAL form of a bundle-relative FILE path, or ``None`` to REFUSE it.
|
|
1037
|
+
|
|
1038
|
+
VALIDATION, not laundering: a path is already canonical or it is refused. It is
|
|
1039
|
+
never rewritten into an acceptable form, because rewriting changes the very
|
|
1040
|
+
thing later rules are about to judge. ``assets/innocent/.`` used to normalize to
|
|
1041
|
+
``assets/innocent`` — losing the ``.`` basename that made it junk — and the
|
|
1042
|
+
entry was stored under a path that was not its own name. Two entries could also
|
|
1043
|
+
collapse onto ONE identity (``assets/./x``, ``assets//x`` and ``assets/x`` all
|
|
1044
|
+
became ``assets/x``), leaving which bytes win to zip ordering.
|
|
1045
|
+
|
|
1046
|
+
Refused: an empty name, a C0/DEL control character (``\\n`` and NUL among them,
|
|
1047
|
+
so no rule downstream has to cope with one), an absolute path (POSIX ``/`` or a
|
|
1048
|
+
Windows drive), and any ``.``, ``..`` or EMPTY segment. Backslashes are still
|
|
1049
|
+
folded to ``/`` first, which is the one tolerance kept deliberately: zips written
|
|
1050
|
+
on Windows use them as separators, and folding cannot erase a segment or change a
|
|
1051
|
+
basename's junk-ness the way the collapses above did.
|
|
1052
|
+
|
|
1053
|
+
A trailing-slash DIRECTORY marker is not a file path and is refused here; callers
|
|
1054
|
+
reach the directory case through :func:`_classify_bundle_entry`, which strips the
|
|
1055
|
+
marker and validates the path it names.
|
|
1056
|
+
|
|
1057
|
+
This is the contract the file browser already relies on: stored
|
|
1058
|
+
``design_system_file.path`` values are canonical by construction, so
|
|
1059
|
+
``_validated_file_path`` can reject a non-canonical REQUEST instead of
|
|
1060
|
+
normalizing it into an accepted one.
|
|
935
1061
|
"""
|
|
936
1062
|
if not rel_path:
|
|
937
1063
|
return None
|
|
938
1064
|
normalized = rel_path.replace("\\", "/")
|
|
1065
|
+
if any(ord(ch) < 0x20 or ord(ch) == 0x7F for ch in normalized):
|
|
1066
|
+
return None
|
|
939
1067
|
if normalized.startswith("/") or re.match(r"^[A-Za-z]:", normalized):
|
|
940
1068
|
return None
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1069
|
+
if any(segment in ("", ".", "..") for segment in normalized.split("/")):
|
|
1070
|
+
return None
|
|
1071
|
+
return normalized
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
# The verdict :func:`_classify_bundle_entry` reaches for one zip entry.
|
|
1075
|
+
_ENTRY_REFUSE = "refuse" # unsafe — reject the whole bundle
|
|
1076
|
+
_ENTRY_SKIP = "skip" # legitimate but never stored (directory, OS junk, dotfile)
|
|
1077
|
+
_ENTRY_FILE = "file" # store it, under the canonical path returned alongside
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
def _classify_bundle_entry(rel_path: str) -> tuple[str, Optional[str]]:
|
|
1081
|
+
"""THE decision for one bundle entry: refuse it, skip it, or store it.
|
|
1082
|
+
|
|
1083
|
+
ONE place that canonicalizes and then applies EVERY rule. Both gates that judge
|
|
1084
|
+
bundle paths call this — the up-front whole-bundle scan
|
|
1085
|
+
(:func:`_assert_bundle_paths_safe`) and the per-entry iterator
|
|
1086
|
+
(:func:`_iter_safe_entries`) — so they cannot disagree about a path shape, and a
|
|
1087
|
+
new rule cannot be added to one and forgotten in the other. They used to decide
|
|
1088
|
+
separately and DID disagree: the scan refused ``assets/innocent/.`` on its raw
|
|
1089
|
+
basename while the iterator accepted the same entry on its normalized one, and
|
|
1090
|
+
the permissive gate was the one that reached storage.
|
|
1091
|
+
|
|
1092
|
+
Returns ``(verdict, canonical_path)``; the path is set only for ``_ENTRY_FILE``.
|
|
1093
|
+
"""
|
|
1094
|
+
if rel_path.endswith("/"):
|
|
1095
|
+
# A directory marker. It is never stored, but the path it NAMES still has to
|
|
1096
|
+
# be canonical, so '../evil/' is refused rather than skipped.
|
|
1097
|
+
return (
|
|
1098
|
+
(_ENTRY_SKIP, None)
|
|
1099
|
+
if _safe_relpath(rel_path[:-1]) is not None
|
|
1100
|
+
else (_ENTRY_REFUSE, None)
|
|
1101
|
+
)
|
|
1102
|
+
canonical = _safe_relpath(rel_path)
|
|
1103
|
+
if canonical is None:
|
|
1104
|
+
return _ENTRY_REFUSE, None
|
|
1105
|
+
low = canonical.lower()
|
|
1106
|
+
if low.startswith("__macosx/") or "/__macosx/" in low:
|
|
1107
|
+
return _ENTRY_SKIP, None
|
|
1108
|
+
base = _basename(low)
|
|
1109
|
+
# The ONE dot-prefixed shape the importer stores is a template folder's
|
|
1110
|
+
# screenshot; every other dotfile stays skipped. Tested against the CANONICAL
|
|
1111
|
+
# path — the same string the recognizer sees downstream.
|
|
1112
|
+
if (base == ".ds_store" or base.startswith(".")) and not _is_template_preview(
|
|
1113
|
+
canonical
|
|
1114
|
+
):
|
|
1115
|
+
return _ENTRY_SKIP, None
|
|
1116
|
+
return _ENTRY_FILE, canonical
|
|
949
1117
|
|
|
950
1118
|
|
|
951
1119
|
def _is_symlink(info: zipfile.ZipInfo) -> bool:
|
|
@@ -966,8 +1134,11 @@ def _assert_bundle_paths_safe(zf: zipfile.ZipFile) -> None:
|
|
|
966
1134
|
root-prefix scoping — so a malicious entry that falls OUTSIDE the bundle root
|
|
967
1135
|
(e.g. ``../evil.png`` when the manifest sits under ``safe/``) is REJECTED, not
|
|
968
1136
|
silently skipped. EVERY entry is checked (no exemption for empty or ``.``-only
|
|
969
|
-
names): absolute paths, ``..`` traversal, and empty/invalid
|
|
970
|
-
and symlink entries are refused before any bytes are read.
|
|
1137
|
+
names): absolute paths, ``..`` traversal, control characters, and empty/invalid
|
|
1138
|
+
names are refused, and symlink entries are refused before any bytes are read.
|
|
1139
|
+
|
|
1140
|
+
The path verdict comes from :func:`_classify_bundle_entry`, the SAME classifier
|
|
1141
|
+
:func:`_iter_safe_entries` uses, so the two gates cannot disagree about a shape.
|
|
971
1142
|
"""
|
|
972
1143
|
for info in zf.infolist():
|
|
973
1144
|
name = info.filename
|
|
@@ -975,12 +1146,12 @@ def _assert_bundle_paths_safe(zf: zipfile.ZipFile) -> None:
|
|
|
975
1146
|
raise DesignSystemImportError(
|
|
976
1147
|
f"Bundle entry '{name}' is a symlink; refusing to import."
|
|
977
1148
|
)
|
|
978
|
-
# No ``if name`` guard: an empty (or ``.``-only) name also
|
|
979
|
-
#
|
|
980
|
-
if
|
|
1149
|
+
# No ``if name`` guard: an empty (or ``.``-only) name is also refused, not
|
|
1150
|
+
# skipped.
|
|
1151
|
+
if _classify_bundle_entry(name)[0] == _ENTRY_REFUSE:
|
|
981
1152
|
raise DesignSystemImportError(
|
|
982
|
-
f"Bundle contains an unsafe path '{name}' (empty, absolute,
|
|
983
|
-
"parent-directory traversal); refusing to import."
|
|
1153
|
+
f"Bundle contains an unsafe path '{name}' (empty, absolute, "
|
|
1154
|
+
"non-canonical, or parent-directory traversal); refusing to import."
|
|
984
1155
|
)
|
|
985
1156
|
|
|
986
1157
|
|
|
@@ -990,26 +1161,34 @@ def _iter_safe_entries(zf: zipfile.ZipFile, root_prefix: str):
|
|
|
990
1161
|
Skips directories, OS junk (``__MACOSX``, ``.DS_Store``) and dotfiles. Raises
|
|
991
1162
|
:class:`DesignSystemImportError` on a zip-slip path (absolute or ``..``) so a
|
|
992
1163
|
malicious bundle is rejected rather than silently stored.
|
|
1164
|
+
|
|
1165
|
+
This is the ACTUAL first gate every entry passes, so the ONE dot-prefixed shape
|
|
1166
|
+
the importer stores — a template folder's ``.thumbnail`` screenshot
|
|
1167
|
+
(:func:`_is_template_preview`) — is allowlisted HERE too. Without that a fix to
|
|
1168
|
+
the recognizer alone reads as working in a unit test and still drops every real
|
|
1169
|
+
thumbnail, because the entry never reaches the recognizer.
|
|
1170
|
+
|
|
1171
|
+
Every path rule lives in :func:`_classify_bundle_entry` — the same classifier the
|
|
1172
|
+
up-front scan uses — so an entry is judged on its CANONICAL path and neither gate
|
|
1173
|
+
can be patched into disagreeing with the other. Stripping ``root_prefix`` is the
|
|
1174
|
+
only work done here, and it is bookkeeping rather than a path rule.
|
|
993
1175
|
"""
|
|
994
1176
|
for info in zf.infolist():
|
|
995
1177
|
name = info.filename
|
|
996
1178
|
if not name.startswith(root_prefix):
|
|
997
1179
|
continue
|
|
998
1180
|
rel_raw = name[len(root_prefix):]
|
|
999
|
-
if not rel_raw
|
|
1000
|
-
continue # directory entry
|
|
1001
|
-
|
|
1002
|
-
if
|
|
1003
|
-
continue
|
|
1004
|
-
base = _basename(low)
|
|
1005
|
-
if base == ".ds_store" or base.startswith("."):
|
|
1006
|
-
continue
|
|
1007
|
-
safe = _safe_relpath(rel_raw)
|
|
1008
|
-
if safe is None:
|
|
1181
|
+
if not rel_raw:
|
|
1182
|
+
continue # the root-prefix directory entry itself
|
|
1183
|
+
verdict, safe = _classify_bundle_entry(rel_raw)
|
|
1184
|
+
if verdict == _ENTRY_REFUSE:
|
|
1009
1185
|
raise DesignSystemImportError(
|
|
1010
|
-
f"Bundle contains an unsafe path '{rel_raw}' (absolute path
|
|
1011
|
-
"parent-directory traversal); refusing to
|
|
1186
|
+
f"Bundle contains an unsafe path '{rel_raw}' (absolute path, "
|
|
1187
|
+
"non-canonical path, or parent-directory traversal); refusing to "
|
|
1188
|
+
"import."
|
|
1012
1189
|
)
|
|
1190
|
+
if safe is None: # _ENTRY_SKIP
|
|
1191
|
+
continue
|
|
1013
1192
|
yield info, safe
|
|
1014
1193
|
|
|
1015
1194
|
|
|
@@ -1128,6 +1307,7 @@ def _collect_assets_and_files(
|
|
|
1128
1307
|
root_prefix: str,
|
|
1129
1308
|
budget: "_SizeBudget",
|
|
1130
1309
|
css_sources: "dict[str, bytes]",
|
|
1310
|
+
warnings: "Optional[list[BundleImportWarning]]" = None,
|
|
1131
1311
|
) -> tuple[list[DesignSystemAsset], list[DesignSystemFile]]:
|
|
1132
1312
|
"""Read a bundle into asset rows + file rows in one safety-checked pass.
|
|
1133
1313
|
|
|
@@ -1176,8 +1356,37 @@ def _collect_assets_and_files(
|
|
|
1176
1356
|
# Size-checked read: the declared size is validated BEFORE
|
|
1177
1357
|
# materialisation (bomb guard).
|
|
1178
1358
|
data = budget.read_info(zf, info)
|
|
1179
|
-
mime = _guess_mime(rel)
|
|
1180
1359
|
kind = "template_shot" if _is_template_preview(rel) else _infer_asset_kind(rel)
|
|
1360
|
+
if _is_template_thumbnail(rel):
|
|
1361
|
+
# No extension to guess from, so the type comes from the CONTENT.
|
|
1362
|
+
# Unrecognized bytes are REFUSED rather than stored as
|
|
1363
|
+
# application/octet-stream: a thumbnail is served back to the
|
|
1364
|
+
# browser, so its declared type has to be one we actually verified.
|
|
1365
|
+
# Only THIS entry is dropped — a bundle import is one request, and
|
|
1366
|
+
# one junk screenshot must not cost the whole upload.
|
|
1367
|
+
sniffed = _sniff_raster_mime(data)
|
|
1368
|
+
if sniffed is None:
|
|
1369
|
+
logger.warning(
|
|
1370
|
+
"Bundle entry '%s' is not a PNG/JPEG/GIF/WebP image "
|
|
1371
|
+
"(%d bytes); not stored as a template thumbnail",
|
|
1372
|
+
rel,
|
|
1373
|
+
len(data),
|
|
1374
|
+
)
|
|
1375
|
+
# A server-side log alone left the user believing the import was
|
|
1376
|
+
# complete; the same fact rides back on the response.
|
|
1377
|
+
if warnings is not None:
|
|
1378
|
+
warnings.append(
|
|
1379
|
+
BundleImportWarning(
|
|
1380
|
+
rel,
|
|
1381
|
+
"not a PNG, JPEG, GIF or WebP image "
|
|
1382
|
+
f"({len(data)} bytes); this template's thumbnail "
|
|
1383
|
+
"was not stored.",
|
|
1384
|
+
)
|
|
1385
|
+
)
|
|
1386
|
+
continue
|
|
1387
|
+
mime = sniffed
|
|
1388
|
+
else:
|
|
1389
|
+
mime = _guess_mime(rel)
|
|
1181
1390
|
width, height = _image_dimensions(data, mime)
|
|
1182
1391
|
asset = DesignSystemAsset(
|
|
1183
1392
|
kind=kind,
|
|
@@ -606,9 +606,37 @@ def _entry_path_candidates(entry: dict) -> list[str]:
|
|
|
606
606
|
return out
|
|
607
607
|
|
|
608
608
|
|
|
609
|
+
#: Basenames a template folder's screenshot ships under, with or without a leading
|
|
610
|
+
#: dot and with or without an extension: the documented ``preview.png`` and the
|
|
611
|
+
#: ``.thumbnail`` a real export actually writes. Matching only ``preview*`` left
|
|
612
|
+
#: every real bundle's ``thumbnail_url`` NULL even once the importer stored the
|
|
613
|
+
#: bytes — the importer and this lookup are two separate gates on the same file.
|
|
614
|
+
_THUMBNAIL_BASENAME_PREFIXES = ("preview", "thumbnail")
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def _is_thumbnail_basename(basename: str) -> bool:
|
|
618
|
+
"""True for a screenshot basename, with or without a leading dot.
|
|
619
|
+
|
|
620
|
+
A PREFIX match on purpose, matching the breadth of the importer's
|
|
621
|
+
``preview[^/]*\\.<ext>`` shape. A C0/DEL control character is refused outright:
|
|
622
|
+
the prefix test alone says nothing about what TRAILS the basename, and this is
|
|
623
|
+
the third gate on the same file — the importer now refuses such a path, so this
|
|
624
|
+
is defence in depth that no longer depends on the importer for the property.
|
|
625
|
+
"""
|
|
626
|
+
if any(ord(ch) < 0x20 or ord(ch) == 0x7F for ch in basename):
|
|
627
|
+
return False
|
|
628
|
+
return basename.lstrip(".").startswith(_THUMBNAIL_BASENAME_PREFIXES)
|
|
629
|
+
|
|
630
|
+
|
|
609
631
|
def _find_thumbnail_asset_id(files: list[Any], template_dir: str) -> Optional[int]:
|
|
610
|
-
"""The template folder's
|
|
611
|
-
|
|
632
|
+
"""The template folder's screenshot: the smallest-path image reference row
|
|
633
|
+
under ``template_dir`` whose basename names a preview or a thumbnail.
|
|
634
|
+
|
|
635
|
+
Smallest path is the tie-break when a folder ships more than one, so the choice
|
|
636
|
+
is deterministic. The ``image/*`` requirement is what keeps a non-image file
|
|
637
|
+
named ``thumbnail`` out: the importer only records a verified, sniffed media
|
|
638
|
+
type on these rows.
|
|
639
|
+
"""
|
|
612
640
|
previews: list[tuple[str, int]] = []
|
|
613
641
|
for ds_file in files:
|
|
614
642
|
asset_id = getattr(ds_file, "asset_id", None)
|
|
@@ -619,7 +647,7 @@ def _find_thumbnail_asset_id(files: list[Any], template_dir: str) -> Optional[in
|
|
|
619
647
|
continue
|
|
620
648
|
basename = path.rsplit("/", 1)[-1].lower()
|
|
621
649
|
mime = (getattr(ds_file, "mime", "") or "").lower()
|
|
622
|
-
if basename
|
|
650
|
+
if _is_thumbnail_basename(basename) and mime.startswith("image/"):
|
|
623
651
|
previews.append((path, asset_id))
|
|
624
652
|
return min(previews)[1] if previews else None
|
|
625
653
|
|
|
File without changes
|
|
File without changes
|
{databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/databricks_tellr_app/run.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/_otel_bootstrap.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/middleware/__init__.py
RENAMED
|
File without changes
|
{databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/middleware/csrf.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/__init__.py
RENAMED
|
File without changes
|
{databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/_authz.py
RENAMED
|
File without changes
|
{databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/admin.py
RENAMED
|
File without changes
|
{databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/admin_usage.py
RENAMED
|
File without changes
|
{databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/agent_config.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/export.py
RENAMED
|
File without changes
|
{databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/feedback.py
RENAMED
|
File without changes
|
{databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/google_slides.py
RENAMED
|
File without changes
|
{databricks_tellr_app-0.4.2.dev9 → databricks_tellr_app-0.4.2.dev11}/src/api/routes/images.py
RENAMED
|
File without changes
|