databricks-tellr-app 0.3.13.dev4__tar.gz → 0.3.13.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.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/PKG-INFO +8 -8
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/databricks_tellr_app/run.py +0 -12
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/databricks_tellr_app.egg-info/PKG-INFO +8 -8
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/databricks_tellr_app.egg-info/SOURCES.txt +0 -2
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/databricks_tellr_app.egg-info/requires.txt +7 -7
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/pyproject.toml +8 -10
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/services/chat_service.py +75 -19
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/services/session_manager.py +27 -0
- databricks_tellr_app-0.3.13.dev11/src/core/encryption.py +86 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/lakebase.py +114 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/__init__.py +0 -2
- databricks_tellr_app-0.3.13.dev4/src/core/encryption.py +0 -125
- databricks_tellr_app-0.3.13.dev4/src/database/models/encryption_key.py +0 -30
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/README.md +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/databricks_tellr_app/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/databricks_tellr_app.egg-info/dependency_links.txt +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/databricks_tellr_app.egg-info/top_level.txt +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/setup.cfg +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/setup.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/_otel_bootstrap.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/fixtures/tour_demo_deck.json +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/main.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/mcp_auth.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/mcp_server.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/middleware/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/middleware/request_logging.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/admin.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/admin_usage.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/agent_config.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/chat.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/deck_contributors.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/export.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/feedback.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/google_slides.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/images.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/local_version.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/profiles.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/sessions.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/settings/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/settings/contributors.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/settings/deck_prompts.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/settings/identities.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/settings/slide_styles.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/setup.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/slides.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/tools.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/tour.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/verification.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/routes/version.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/schemas/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/schemas/agent_config.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/schemas/feedback.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/schemas/requests.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/schemas/responses.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/schemas/settings/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/schemas/settings/requests.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/schemas/settings/responses.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/schemas/streaming.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/services/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/services/export_job_queue.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/services/feedback_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/services/job_queue.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/services/session_naming.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/services/usage_events.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/services/usage_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/utils/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/api/utils/validation.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/config_loader.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/context_utils.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/database.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/databricks_client.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/defaults.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/init_default_profile.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/migrate_profiles_to_agent_config.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/mlflow_agent_spans.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/mlflow_tracing.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/otel_logging.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/permission_context.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/prompt_modules.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/settings_db.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/core/user_context.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/deck_contributor.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/feedback.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/genie_space.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/google_global_credentials.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/google_oauth_token.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/identity.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/image.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/profile.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/profile_contributor.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/prompts.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/request_log.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/session.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/slide_deck_prompt.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/slide_style_library.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/usage_event.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/database/models/user_preference.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/domain/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/domain/slide.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/domain/slide_deck.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/agent.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/agent_factory.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/config_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/config_validator.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/drive_uploader.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/evaluation/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/evaluation/llm_judge.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/genie_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/google_slides_auth.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/google_slides_prompts_defaults.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/html_to_google_slides.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/html_to_pptx.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/identity_provider.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/identity_providers/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/identity_providers/account_provider.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/identity_providers/local_provider.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/identity_providers/workspace_provider.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/image_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/image_tools.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/permission_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/pptx_from_html_huashu.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/pptx_from_records.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/pptx_prompts_defaults.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/profile_service.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/streaming_callback.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/tools/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/tools/agent_bricks_tool.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/tools/genie_tool.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/tools/mcp_tool.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/tools/model_endpoint_tool.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/tools/vector_tool.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/services/validator.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/utils/__init__.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/utils/css_utils.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/utils/error_handling.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/utils/html_safety.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/utils/html_utils.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/utils/image_utils.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/utils/js_validator.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/utils/logging_config.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/utils/pi_filter.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/utils/slide_hash.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/utils/spotlight.py +0 -0
- {databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/src/utils/text_caps.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: databricks-tellr-app
|
|
3
|
-
Version: 0.3.13.
|
|
3
|
+
Version: 0.3.13.dev11
|
|
4
4
|
Summary: Tellr application package for Databricks Apps
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -24,7 +24,7 @@ Requires-Dist: tinycss2==1.3.0
|
|
|
24
24
|
Requires-Dist: esprima==4.0.1
|
|
25
25
|
Requires-Dist: sqlalchemy==2.0.36
|
|
26
26
|
Requires-Dist: psycopg2-binary==2.9.10
|
|
27
|
-
Requires-Dist: databricks-sql-connector
|
|
27
|
+
Requires-Dist: databricks-sql-connector>=3.0.0
|
|
28
28
|
Requires-Dist: httpx==0.28.1
|
|
29
29
|
Requires-Dist: jinja2==3.1.2
|
|
30
30
|
Requires-Dist: python-dotenv==1.2.1
|
|
@@ -32,12 +32,12 @@ Requires-Dist: pyyaml==6.0.1
|
|
|
32
32
|
Requires-Dist: pandas==2.3.3
|
|
33
33
|
Requires-Dist: python-pptx==0.6.23
|
|
34
34
|
Requires-Dist: playwright==1.48.0
|
|
35
|
-
Requires-Dist: Pillow
|
|
36
|
-
Requires-Dist: google-api-python-client
|
|
37
|
-
Requires-Dist: google-auth-oauthlib
|
|
38
|
-
Requires-Dist: google-auth-httplib2
|
|
39
|
-
Requires-Dist: cryptography
|
|
40
|
-
Requires-Dist: sse-starlette
|
|
35
|
+
Requires-Dist: Pillow>=10.0.0
|
|
36
|
+
Requires-Dist: google-api-python-client>=2.100.0
|
|
37
|
+
Requires-Dist: google-auth-oauthlib>=1.2.0
|
|
38
|
+
Requires-Dist: google-auth-httplib2>=0.2.0
|
|
39
|
+
Requires-Dist: cryptography>=42.0.0
|
|
40
|
+
Requires-Dist: sse-starlette>=3.0.0
|
|
41
41
|
Requires-Dist: databricks-sdk==0.120.0
|
|
42
42
|
Requires-Dist: mlflow[databricks]==3.14.0
|
|
43
43
|
Requires-Dist: mlflow-skinny==3.14.0
|
{databricks_tellr_app-0.3.13.dev4 → databricks_tellr_app-0.3.13.dev11}/databricks_tellr_app/run.py
RENAMED
|
@@ -62,18 +62,6 @@ def init_database(seed_databricks_defaults: bool = False) -> None:
|
|
|
62
62
|
logger.error(f"Failed to seed defaults: {e}\n{tb}")
|
|
63
63
|
raise SystemExit(1) from e
|
|
64
64
|
|
|
65
|
-
# Seed/load the Fernet master key pre-fork (SDR-4437 CRITICAL-3) so all
|
|
66
|
-
# uvicorn workers find the encryption_keys row already present.
|
|
67
|
-
logger.info("Ensuring encryption key...")
|
|
68
|
-
try:
|
|
69
|
-
from src.core.encryption import ensure_encryption_key
|
|
70
|
-
ensure_encryption_key()
|
|
71
|
-
logger.info("Encryption key ready")
|
|
72
|
-
except Exception as e:
|
|
73
|
-
tb = traceback.format_exc()
|
|
74
|
-
logger.error(f"Failed to ensure encryption key: {e}\n{tb}")
|
|
75
|
-
raise SystemExit(1) from e
|
|
76
|
-
|
|
77
65
|
|
|
78
66
|
def main() -> None:
|
|
79
67
|
"""Start the uvicorn server."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: databricks-tellr-app
|
|
3
|
-
Version: 0.3.13.
|
|
3
|
+
Version: 0.3.13.dev11
|
|
4
4
|
Summary: Tellr application package for Databricks Apps
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -24,7 +24,7 @@ Requires-Dist: tinycss2==1.3.0
|
|
|
24
24
|
Requires-Dist: esprima==4.0.1
|
|
25
25
|
Requires-Dist: sqlalchemy==2.0.36
|
|
26
26
|
Requires-Dist: psycopg2-binary==2.9.10
|
|
27
|
-
Requires-Dist: databricks-sql-connector
|
|
27
|
+
Requires-Dist: databricks-sql-connector>=3.0.0
|
|
28
28
|
Requires-Dist: httpx==0.28.1
|
|
29
29
|
Requires-Dist: jinja2==3.1.2
|
|
30
30
|
Requires-Dist: python-dotenv==1.2.1
|
|
@@ -32,12 +32,12 @@ Requires-Dist: pyyaml==6.0.1
|
|
|
32
32
|
Requires-Dist: pandas==2.3.3
|
|
33
33
|
Requires-Dist: python-pptx==0.6.23
|
|
34
34
|
Requires-Dist: playwright==1.48.0
|
|
35
|
-
Requires-Dist: Pillow
|
|
36
|
-
Requires-Dist: google-api-python-client
|
|
37
|
-
Requires-Dist: google-auth-oauthlib
|
|
38
|
-
Requires-Dist: google-auth-httplib2
|
|
39
|
-
Requires-Dist: cryptography
|
|
40
|
-
Requires-Dist: sse-starlette
|
|
35
|
+
Requires-Dist: Pillow>=10.0.0
|
|
36
|
+
Requires-Dist: google-api-python-client>=2.100.0
|
|
37
|
+
Requires-Dist: google-auth-oauthlib>=1.2.0
|
|
38
|
+
Requires-Dist: google-auth-httplib2>=0.2.0
|
|
39
|
+
Requires-Dist: cryptography>=42.0.0
|
|
40
|
+
Requires-Dist: sse-starlette>=3.0.0
|
|
41
41
|
Requires-Dist: databricks-sdk==0.120.0
|
|
42
42
|
Requires-Dist: mlflow[databricks]==3.14.0
|
|
43
43
|
Requires-Dist: mlflow-skinny==3.14.0
|
|
@@ -76,7 +76,6 @@ setup.py
|
|
|
76
76
|
./src/database/__init__.py
|
|
77
77
|
./src/database/models/__init__.py
|
|
78
78
|
./src/database/models/deck_contributor.py
|
|
79
|
-
./src/database/models/encryption_key.py
|
|
80
79
|
./src/database/models/feedback.py
|
|
81
80
|
./src/database/models/genie_space.py
|
|
82
81
|
./src/database/models/google_global_credentials.py
|
|
@@ -220,7 +219,6 @@ src/core/user_context.py
|
|
|
220
219
|
src/database/__init__.py
|
|
221
220
|
src/database/models/__init__.py
|
|
222
221
|
src/database/models/deck_contributor.py
|
|
223
|
-
src/database/models/encryption_key.py
|
|
224
222
|
src/database/models/feedback.py
|
|
225
223
|
src/database/models/genie_space.py
|
|
226
224
|
src/database/models/google_global_credentials.py
|
|
@@ -18,7 +18,7 @@ tinycss2==1.3.0
|
|
|
18
18
|
esprima==4.0.1
|
|
19
19
|
sqlalchemy==2.0.36
|
|
20
20
|
psycopg2-binary==2.9.10
|
|
21
|
-
databricks-sql-connector
|
|
21
|
+
databricks-sql-connector>=3.0.0
|
|
22
22
|
httpx==0.28.1
|
|
23
23
|
jinja2==3.1.2
|
|
24
24
|
python-dotenv==1.2.1
|
|
@@ -26,12 +26,12 @@ pyyaml==6.0.1
|
|
|
26
26
|
pandas==2.3.3
|
|
27
27
|
python-pptx==0.6.23
|
|
28
28
|
playwright==1.48.0
|
|
29
|
-
Pillow
|
|
30
|
-
google-api-python-client
|
|
31
|
-
google-auth-oauthlib
|
|
32
|
-
google-auth-httplib2
|
|
33
|
-
cryptography
|
|
34
|
-
sse-starlette
|
|
29
|
+
Pillow>=10.0.0
|
|
30
|
+
google-api-python-client>=2.100.0
|
|
31
|
+
google-auth-oauthlib>=1.2.0
|
|
32
|
+
google-auth-httplib2>=0.2.0
|
|
33
|
+
cryptography>=42.0.0
|
|
34
|
+
sse-starlette>=3.0.0
|
|
35
35
|
databricks-sdk==0.120.0
|
|
36
36
|
mlflow[databricks]==3.14.0
|
|
37
37
|
mlflow-skinny==3.14.0
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "databricks-tellr-app"
|
|
7
|
-
version = "0.3.13.
|
|
7
|
+
version = "0.3.13.dev11"
|
|
8
8
|
description = "Tellr application package for Databricks Apps"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -29,7 +29,7 @@ dependencies = [
|
|
|
29
29
|
"esprima==4.0.1",
|
|
30
30
|
"sqlalchemy==2.0.36",
|
|
31
31
|
"psycopg2-binary==2.9.10",
|
|
32
|
-
"databricks-sql-connector
|
|
32
|
+
"databricks-sql-connector>=3.0.0",
|
|
33
33
|
"httpx==0.28.1",
|
|
34
34
|
"jinja2==3.1.2",
|
|
35
35
|
"python-dotenv==1.2.1",
|
|
@@ -37,12 +37,12 @@ dependencies = [
|
|
|
37
37
|
"pandas==2.3.3",
|
|
38
38
|
"python-pptx==0.6.23",
|
|
39
39
|
"playwright==1.48.0",
|
|
40
|
-
"Pillow
|
|
41
|
-
"google-api-python-client
|
|
42
|
-
"google-auth-oauthlib
|
|
43
|
-
"google-auth-httplib2
|
|
44
|
-
"cryptography
|
|
45
|
-
"sse-starlette
|
|
40
|
+
"Pillow>=10.0.0",
|
|
41
|
+
"google-api-python-client>=2.100.0",
|
|
42
|
+
"google-auth-oauthlib>=1.2.0",
|
|
43
|
+
"google-auth-httplib2>=0.2.0",
|
|
44
|
+
"cryptography>=42.0.0",
|
|
45
|
+
"sse-starlette>=3.0.0",
|
|
46
46
|
# --- Resolver-speedup pins: the backtracking-driver cluster ---
|
|
47
47
|
# Databricks Apps installs this wheel in a platform-managed BUILD phase with a
|
|
48
48
|
# ~10-minute budget. Left ranged, the databricks-langchain ->
|
|
@@ -58,8 +58,6 @@ dependencies = [
|
|
|
58
58
|
# via a devloop deploy if these constraints change. Formerly injected at
|
|
59
59
|
# deploy time as _RESOLVER_SPEEDUP_PINS; now carried in the wheel so a bare
|
|
60
60
|
# `pip install databricks-tellr-app` is safe.
|
|
61
|
-
# SDR-4437 MEDIUM-7 (July 2026): the 7 formerly-ranged direct deps above
|
|
62
|
-
# this block are now exact-pinned too; same refresh discipline.
|
|
63
61
|
"databricks-sdk==0.120.0",
|
|
64
62
|
"mlflow[databricks]==3.14.0",
|
|
65
63
|
"mlflow-skinny==3.14.0",
|
|
@@ -80,6 +80,11 @@ class ChatService:
|
|
|
80
80
|
# This avoids re-parsing HTML on every request
|
|
81
81
|
self._deck_cache: Dict[str, SlideDeck] = {}
|
|
82
82
|
|
|
83
|
+
# DB version each cached deck corresponds to. The cache is per-process
|
|
84
|
+
# while prod runs multiple uvicorn workers sharing one database, so a
|
|
85
|
+
# cache hit is only valid if the DB version hasn't moved on.
|
|
86
|
+
self._deck_cache_versions: Dict[str, int] = {}
|
|
87
|
+
|
|
83
88
|
logger.info("ChatService initialized successfully")
|
|
84
89
|
|
|
85
90
|
def _substitute_images_for_response(self, deck_dict, raw_html=None):
|
|
@@ -600,7 +605,7 @@ class ChatService:
|
|
|
600
605
|
slide_deck_dict = current_deck.to_dict()
|
|
601
606
|
|
|
602
607
|
try:
|
|
603
|
-
session_manager.save_slide_deck(
|
|
608
|
+
save_result = session_manager.save_slide_deck(
|
|
604
609
|
session_id=session_id,
|
|
605
610
|
title=current_deck.title,
|
|
606
611
|
html_content=current_deck.knit(),
|
|
@@ -610,6 +615,7 @@ class ChatService:
|
|
|
610
615
|
modified_by=_user,
|
|
611
616
|
expected_version=_deck_version_before_llm,
|
|
612
617
|
)
|
|
618
|
+
self._record_deck_version(session_id, save_result)
|
|
613
619
|
except VersionConflictError:
|
|
614
620
|
logger.warning(
|
|
615
621
|
"Chat save rejected: deck was edited during LLM call, reloading",
|
|
@@ -1385,7 +1391,7 @@ class ChatService:
|
|
|
1385
1391
|
slide_deck_dict = current_deck.to_dict()
|
|
1386
1392
|
|
|
1387
1393
|
try:
|
|
1388
|
-
session_manager.save_slide_deck(
|
|
1394
|
+
save_result = session_manager.save_slide_deck(
|
|
1389
1395
|
session_id=session_id,
|
|
1390
1396
|
title=current_deck.title,
|
|
1391
1397
|
html_content=current_deck.knit(),
|
|
@@ -1395,6 +1401,7 @@ class ChatService:
|
|
|
1395
1401
|
modified_by=_user,
|
|
1396
1402
|
expected_version=_deck_version_before_llm,
|
|
1397
1403
|
)
|
|
1404
|
+
self._record_deck_version(session_id, save_result)
|
|
1398
1405
|
except VersionConflictError:
|
|
1399
1406
|
logger.warning(
|
|
1400
1407
|
"Chat save rejected: deck was edited during LLM call, reloading",
|
|
@@ -1921,18 +1928,63 @@ class ChatService:
|
|
|
1921
1928
|
context = "\n\n[Attached images]\n" + "\n".join(image_descriptions)
|
|
1922
1929
|
return message + context
|
|
1923
1930
|
|
|
1931
|
+
def _deck_versions(self) -> Dict[str, int]:
|
|
1932
|
+
"""Return the cached-deck version map, creating it if needed.
|
|
1933
|
+
|
|
1934
|
+
Lazy creation keeps instances built without __init__ (test harnesses
|
|
1935
|
+
use ChatService.__new__) working. Callers must hold _cache_lock.
|
|
1936
|
+
"""
|
|
1937
|
+
try:
|
|
1938
|
+
return self._deck_cache_versions
|
|
1939
|
+
except AttributeError:
|
|
1940
|
+
self._deck_cache_versions = {}
|
|
1941
|
+
return self._deck_cache_versions
|
|
1942
|
+
|
|
1943
|
+
def _record_deck_version(self, session_id: str, save_result: Optional[Dict[str, Any]]) -> None:
|
|
1944
|
+
"""Remember which DB version the cached deck now corresponds to.
|
|
1945
|
+
|
|
1946
|
+
Call after save_slide_deck so the next _get_or_load_deck cache hit
|
|
1947
|
+
validates cleanly instead of reloading from the database.
|
|
1948
|
+
"""
|
|
1949
|
+
version = (save_result or {}).get("version")
|
|
1950
|
+
with self._cache_lock:
|
|
1951
|
+
if version is not None:
|
|
1952
|
+
self._deck_versions()[session_id] = version
|
|
1953
|
+
else:
|
|
1954
|
+
self._deck_versions().pop(session_id, None)
|
|
1955
|
+
|
|
1924
1956
|
def _get_or_load_deck(self, session_id: str) -> Optional[SlideDeck]:
|
|
1925
1957
|
"""Get deck from cache or load from database.
|
|
1926
1958
|
|
|
1927
1959
|
Thread-safe access to deck cache using _cache_lock.
|
|
1928
|
-
|
|
1960
|
+
|
|
1961
|
+
The cache is per-process while prod runs multiple uvicorn workers
|
|
1962
|
+
sharing one database, so a cached deck is only served if its recorded
|
|
1963
|
+
version matches the current DB version; otherwise it is reloaded.
|
|
1964
|
+
|
|
1929
1965
|
Uses deck_dict (slides array with individual scripts) when available,
|
|
1930
1966
|
falling back to from_html_string for legacy data.
|
|
1931
1967
|
"""
|
|
1932
1968
|
# Check cache first (with lock)
|
|
1933
1969
|
with self._cache_lock:
|
|
1934
|
-
|
|
1935
|
-
|
|
1970
|
+
cached_deck = self._deck_cache.get(session_id)
|
|
1971
|
+
cached_version = self._deck_versions().get(session_id)
|
|
1972
|
+
|
|
1973
|
+
if cached_deck is not None:
|
|
1974
|
+
db_version = self._get_deck_version(session_id)
|
|
1975
|
+
if db_version is None or cached_version == db_version:
|
|
1976
|
+
return cached_deck
|
|
1977
|
+
logger.info(
|
|
1978
|
+
"Deck cache stale, reloading from database",
|
|
1979
|
+
extra={
|
|
1980
|
+
"session_id": session_id,
|
|
1981
|
+
"cached_version": cached_version,
|
|
1982
|
+
"db_version": db_version,
|
|
1983
|
+
},
|
|
1984
|
+
)
|
|
1985
|
+
with self._cache_lock:
|
|
1986
|
+
self._deck_cache.pop(session_id, None)
|
|
1987
|
+
self._deck_versions().pop(session_id, None)
|
|
1936
1988
|
|
|
1937
1989
|
# Try to load from database (outside lock to avoid blocking)
|
|
1938
1990
|
session_manager = get_session_manager()
|
|
@@ -1966,6 +2018,10 @@ class ChatService:
|
|
|
1966
2018
|
# Store in cache (with lock)
|
|
1967
2019
|
with self._cache_lock:
|
|
1968
2020
|
self._deck_cache[session_id] = deck
|
|
2021
|
+
if deck_data.get("version") is not None:
|
|
2022
|
+
self._deck_versions()[session_id] = deck_data["version"]
|
|
2023
|
+
else:
|
|
2024
|
+
self._deck_versions().pop(session_id, None)
|
|
1969
2025
|
return deck
|
|
1970
2026
|
except Exception as e:
|
|
1971
2027
|
logger.warning(f"Failed to load deck from database: {e}")
|
|
@@ -1975,23 +2031,18 @@ class ChatService:
|
|
|
1975
2031
|
def _get_deck_version(self, session_id: str) -> Optional[int]:
|
|
1976
2032
|
"""Read the current deck version from the database.
|
|
1977
2033
|
|
|
1978
|
-
|
|
1979
|
-
optimistic locking
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
On Lakebase/PostgreSQL this is a single-row indexed lookup (~1-2ms).
|
|
2034
|
+
Used on every deck-cache hit (multi-worker cache validation) and
|
|
2035
|
+
before LLM calls (optimistic locking), so it must stay cheap: a
|
|
2036
|
+
single-column indexed lookup, never a full deck fetch.
|
|
1983
2037
|
|
|
1984
2038
|
Returns:
|
|
1985
2039
|
Current deck version number, or None if no deck exists.
|
|
1986
2040
|
"""
|
|
1987
2041
|
session_manager = get_session_manager()
|
|
1988
2042
|
try:
|
|
1989
|
-
|
|
1990
|
-
if deck_data:
|
|
1991
|
-
return deck_data.get("version")
|
|
2043
|
+
return session_manager.get_slide_deck_version(session_id)
|
|
1992
2044
|
except Exception:
|
|
1993
|
-
|
|
1994
|
-
return None
|
|
2045
|
+
return None
|
|
1995
2046
|
|
|
1996
2047
|
@staticmethod
|
|
1997
2048
|
def _reindex_slide_ids(deck: "SlideDeck") -> None:
|
|
@@ -2008,6 +2059,7 @@ class ChatService:
|
|
|
2008
2059
|
"""Remove the cached deck for a session so the next read hits the DB."""
|
|
2009
2060
|
with self._cache_lock:
|
|
2010
2061
|
self._deck_cache.pop(session_id, None)
|
|
2062
|
+
self._deck_versions().pop(session_id, None)
|
|
2011
2063
|
|
|
2012
2064
|
def _replace_slide_htmls_from_cache(self, session_id: str, slide_context: Dict[str, Any]) -> Dict[str, Any]:
|
|
2013
2065
|
"""Replace frontend-supplied slide_htmls with backend cache versions.
|
|
@@ -2540,7 +2592,7 @@ class ChatService:
|
|
|
2540
2592
|
# Persist to database
|
|
2541
2593
|
deck_dict = current_deck.to_dict()
|
|
2542
2594
|
session_manager = get_session_manager()
|
|
2543
|
-
session_manager.save_slide_deck(
|
|
2595
|
+
save_result = session_manager.save_slide_deck(
|
|
2544
2596
|
session_id=session_id,
|
|
2545
2597
|
title=current_deck.title,
|
|
2546
2598
|
html_content=current_deck.knit(),
|
|
@@ -2549,6 +2601,7 @@ class ChatService:
|
|
|
2549
2601
|
deck_dict=deck_dict,
|
|
2550
2602
|
expected_version=expected_version,
|
|
2551
2603
|
)
|
|
2604
|
+
self._record_deck_version(session_id, save_result)
|
|
2552
2605
|
|
|
2553
2606
|
# Create save point
|
|
2554
2607
|
try:
|
|
@@ -2616,7 +2669,7 @@ class ChatService:
|
|
|
2616
2669
|
# Persist to database
|
|
2617
2670
|
deck_dict = current_deck.to_dict()
|
|
2618
2671
|
session_manager = get_session_manager()
|
|
2619
|
-
session_manager.save_slide_deck(
|
|
2672
|
+
save_result = session_manager.save_slide_deck(
|
|
2620
2673
|
session_id=session_id,
|
|
2621
2674
|
title=current_deck.title,
|
|
2622
2675
|
html_content=current_deck.knit(),
|
|
@@ -2625,6 +2678,7 @@ class ChatService:
|
|
|
2625
2678
|
deck_dict=deck_dict,
|
|
2626
2679
|
expected_version=expected_version,
|
|
2627
2680
|
)
|
|
2681
|
+
self._record_deck_version(session_id, save_result)
|
|
2628
2682
|
|
|
2629
2683
|
# Create save point immediately after persisting
|
|
2630
2684
|
try:
|
|
@@ -2681,7 +2735,7 @@ class ChatService:
|
|
|
2681
2735
|
# Persist to database
|
|
2682
2736
|
deck_dict = current_deck.to_dict()
|
|
2683
2737
|
session_manager = get_session_manager()
|
|
2684
|
-
session_manager.save_slide_deck(
|
|
2738
|
+
save_result = session_manager.save_slide_deck(
|
|
2685
2739
|
session_id=session_id,
|
|
2686
2740
|
title=current_deck.title,
|
|
2687
2741
|
html_content=current_deck.knit(),
|
|
@@ -2690,6 +2744,7 @@ class ChatService:
|
|
|
2690
2744
|
deck_dict=deck_dict,
|
|
2691
2745
|
expected_version=expected_version,
|
|
2692
2746
|
)
|
|
2747
|
+
self._record_deck_version(session_id, save_result)
|
|
2693
2748
|
|
|
2694
2749
|
# Create save point
|
|
2695
2750
|
try:
|
|
@@ -2744,7 +2799,7 @@ class ChatService:
|
|
|
2744
2799
|
# Persist to database
|
|
2745
2800
|
deck_dict = current_deck.to_dict()
|
|
2746
2801
|
session_manager = get_session_manager()
|
|
2747
|
-
session_manager.save_slide_deck(
|
|
2802
|
+
save_result = session_manager.save_slide_deck(
|
|
2748
2803
|
session_id=session_id,
|
|
2749
2804
|
title=current_deck.title,
|
|
2750
2805
|
html_content=current_deck.knit(),
|
|
@@ -2753,6 +2808,7 @@ class ChatService:
|
|
|
2753
2808
|
deck_dict=deck_dict,
|
|
2754
2809
|
expected_version=expected_version,
|
|
2755
2810
|
)
|
|
2811
|
+
self._record_deck_version(session_id, save_result)
|
|
2756
2812
|
|
|
2757
2813
|
# Create save point
|
|
2758
2814
|
try:
|
|
@@ -723,6 +723,33 @@ class SessionManager:
|
|
|
723
723
|
"version": deck.version,
|
|
724
724
|
}
|
|
725
725
|
|
|
726
|
+
def get_slide_deck_version(self, session_id: str) -> Optional[int]:
|
|
727
|
+
"""Return only the deck's current version number.
|
|
728
|
+
|
|
729
|
+
Selects a single column — no deck_json parse, verification merge, or
|
|
730
|
+
display-name resolution — so it is cheap enough to run on every
|
|
731
|
+
deck-cache hit (multi-worker cache validation) and before LLM calls
|
|
732
|
+
(optimistic locking).
|
|
733
|
+
|
|
734
|
+
Args:
|
|
735
|
+
session_id: Session to check (contributor sessions resolve to the
|
|
736
|
+
owner's deck)
|
|
737
|
+
|
|
738
|
+
Returns:
|
|
739
|
+
Current version number, or None if the session/deck doesn't exist
|
|
740
|
+
"""
|
|
741
|
+
with get_db_session() as db:
|
|
742
|
+
try:
|
|
743
|
+
session = self._get_session_or_raise(db, session_id)
|
|
744
|
+
except SessionNotFoundError:
|
|
745
|
+
return None
|
|
746
|
+
deck_owner = self._get_deck_owner_session(db, session)
|
|
747
|
+
return (
|
|
748
|
+
db.query(SessionSlideDeck.version)
|
|
749
|
+
.filter(SessionSlideDeck.session_id == deck_owner.id)
|
|
750
|
+
.scalar()
|
|
751
|
+
)
|
|
752
|
+
|
|
726
753
|
def get_slide_deck(self, session_id: str) -> Optional[Dict[str, Any]]:
|
|
727
754
|
"""Get slide deck for a session with verification merged by content hash.
|
|
728
755
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Fernet symmetric encryption for sensitive data (OAuth credentials, tokens).
|
|
2
|
+
|
|
3
|
+
Uses AES-128-CBC with HMAC-SHA256 authentication via the ``cryptography`` library.
|
|
4
|
+
The encryption key is read from the ``GOOGLE_OAUTH_ENCRYPTION_KEY`` environment
|
|
5
|
+
variable. If the variable is not set, a key is auto-generated on first access,
|
|
6
|
+
persisted to ``.encryption_key`` in the project root, and a warning is logged.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from functools import lru_cache
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from cryptography.fernet import Fernet, InvalidToken
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
_ENV_KEY_NAME = "GOOGLE_OAUTH_ENCRYPTION_KEY"
|
|
19
|
+
_KEY_FILE = Path(__file__).resolve().parents[2] / ".encryption_key"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@lru_cache(maxsize=1)
|
|
23
|
+
def get_encryption_key() -> bytes:
|
|
24
|
+
"""Return the Fernet encryption key.
|
|
25
|
+
|
|
26
|
+
Resolution order:
|
|
27
|
+
1. ``GOOGLE_OAUTH_ENCRYPTION_KEY`` env var (production).
|
|
28
|
+
2. ``.encryption_key`` file in project root (persisted across restarts).
|
|
29
|
+
3. Generate a new key → write to the file → set the env var.
|
|
30
|
+
"""
|
|
31
|
+
# 1. Env var takes precedence
|
|
32
|
+
key = os.getenv(_ENV_KEY_NAME)
|
|
33
|
+
if key:
|
|
34
|
+
return key.encode()
|
|
35
|
+
|
|
36
|
+
# Production guard: fail fast if the env var is not set
|
|
37
|
+
if os.getenv("ENVIRONMENT") == "production":
|
|
38
|
+
raise RuntimeError(
|
|
39
|
+
"GOOGLE_OAUTH_ENCRYPTION_KEY environment variable is required in production. "
|
|
40
|
+
'Generate one with: python -c "from cryptography.fernet import Fernet; '
|
|
41
|
+
'print(Fernet.generate_key().decode())"'
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# 2. Read from persisted key file
|
|
45
|
+
if _KEY_FILE.exists():
|
|
46
|
+
key = _KEY_FILE.read_text().strip()
|
|
47
|
+
if key:
|
|
48
|
+
os.environ[_ENV_KEY_NAME] = key
|
|
49
|
+
logger.info("Loaded encryption key from %s", _KEY_FILE)
|
|
50
|
+
return key.encode()
|
|
51
|
+
|
|
52
|
+
# 3. Generate, persist, and warn
|
|
53
|
+
generated = Fernet.generate_key()
|
|
54
|
+
try:
|
|
55
|
+
_KEY_FILE.write_text(generated.decode())
|
|
56
|
+
_KEY_FILE.chmod(0o600) # owner-only read/write
|
|
57
|
+
logger.warning(
|
|
58
|
+
"GOOGLE_OAUTH_ENCRYPTION_KEY not set — auto-generated and saved to %s. "
|
|
59
|
+
"Set the env var in production.",
|
|
60
|
+
_KEY_FILE,
|
|
61
|
+
)
|
|
62
|
+
except OSError:
|
|
63
|
+
logger.warning(
|
|
64
|
+
"GOOGLE_OAUTH_ENCRYPTION_KEY not set — auto-generated a key but "
|
|
65
|
+
"could not persist to %s. Key will be lost on restart.",
|
|
66
|
+
_KEY_FILE,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
os.environ[_ENV_KEY_NAME] = generated.decode()
|
|
70
|
+
return generated
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def encrypt_data(plaintext: str) -> str:
|
|
74
|
+
"""Encrypt *plaintext* and return a base64-encoded ciphertext string."""
|
|
75
|
+
f = Fernet(get_encryption_key())
|
|
76
|
+
return f.encrypt(plaintext.encode()).decode()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def decrypt_data(ciphertext: str) -> str:
|
|
80
|
+
"""Decrypt a base64-encoded *ciphertext* string and return the plaintext.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
cryptography.fernet.InvalidToken: If the key does not match or data is corrupt.
|
|
84
|
+
"""
|
|
85
|
+
f = Fernet(get_encryption_key())
|
|
86
|
+
return f.decrypt(ciphertext.encode()).decode()
|
|
@@ -280,6 +280,120 @@ def get_lakebase_connection_url(
|
|
|
280
280
|
return url
|
|
281
281
|
|
|
282
282
|
|
|
283
|
+
def setup_lakebase_schema(
|
|
284
|
+
instance_name: str,
|
|
285
|
+
schema: str,
|
|
286
|
+
client_id: str,
|
|
287
|
+
user: Optional[str] = None,
|
|
288
|
+
client: Optional[WorkspaceClient] = None,
|
|
289
|
+
endpoint_name: Optional[str] = None,
|
|
290
|
+
host: Optional[str] = None,
|
|
291
|
+
lakebase_type: str = "provisioned",
|
|
292
|
+
) -> None:
|
|
293
|
+
"""
|
|
294
|
+
Set up schema and grant permissions in Lakebase for an app.
|
|
295
|
+
|
|
296
|
+
Supports both provisioned and autoscaling Lakebase.
|
|
297
|
+
|
|
298
|
+
This function connects to the Lakebase instance and:
|
|
299
|
+
1. Creates the schema if it doesn't exist
|
|
300
|
+
2. Grants necessary permissions to the app's service principal (client_id)
|
|
301
|
+
|
|
302
|
+
Note: This should be called after the app is created and the database
|
|
303
|
+
resource is attached, as the client_id comes from the app's service principal.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
instance_name: Name of the Lakebase instance
|
|
307
|
+
schema: Schema name to create
|
|
308
|
+
client_id: App's service principal client ID (becomes Postgres role)
|
|
309
|
+
user: Postgres username for connection (uses default if not provided)
|
|
310
|
+
client: Optional WorkspaceClient
|
|
311
|
+
endpoint_name: Full endpoint resource name (autoscaling only)
|
|
312
|
+
host: Pre-resolved host (autoscaling only)
|
|
313
|
+
lakebase_type: 'autoscaling' or 'provisioned'
|
|
314
|
+
|
|
315
|
+
Raises:
|
|
316
|
+
LakebaseError: If schema setup fails
|
|
317
|
+
"""
|
|
318
|
+
logger.info(
|
|
319
|
+
"Setting up Lakebase schema",
|
|
320
|
+
extra={
|
|
321
|
+
"instance_name": instance_name, "schema": schema,
|
|
322
|
+
"client_id": client_id, "lakebase_type": lakebase_type,
|
|
323
|
+
},
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
# Get connection info
|
|
328
|
+
conn_info = get_lakebase_connection_info(
|
|
329
|
+
instance_name=instance_name,
|
|
330
|
+
user=user,
|
|
331
|
+
client=client,
|
|
332
|
+
endpoint_name=endpoint_name,
|
|
333
|
+
host=host,
|
|
334
|
+
lakebase_type=lakebase_type,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Use psycopg2 directly for DDL operations
|
|
338
|
+
import psycopg2
|
|
339
|
+
|
|
340
|
+
conn = psycopg2.connect(
|
|
341
|
+
host=conn_info["host"],
|
|
342
|
+
port=conn_info["port"],
|
|
343
|
+
user=conn_info["user"],
|
|
344
|
+
password=conn_info["password"],
|
|
345
|
+
dbname=conn_info["database"],
|
|
346
|
+
sslmode="require",
|
|
347
|
+
)
|
|
348
|
+
conn.autocommit = True
|
|
349
|
+
|
|
350
|
+
with conn.cursor() as cur:
|
|
351
|
+
# Create schema if not exists
|
|
352
|
+
cur.execute(f'CREATE SCHEMA IF NOT EXISTS "{schema}"')
|
|
353
|
+
logger.info(f"Schema created or verified: {schema}")
|
|
354
|
+
|
|
355
|
+
# Grant permissions to app's service principal
|
|
356
|
+
# The client_id is automatically available as a Postgres role when
|
|
357
|
+
# the database is added as an app resource
|
|
358
|
+
cur.execute(f'GRANT USAGE ON SCHEMA "{schema}" TO "{client_id}"')
|
|
359
|
+
cur.execute(f'GRANT CREATE ON SCHEMA "{schema}" TO "{client_id}"')
|
|
360
|
+
|
|
361
|
+
# Grant permissions on all tables in schema (for existing tables)
|
|
362
|
+
cur.execute(
|
|
363
|
+
f'GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA "{schema}" TO "{client_id}"'
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Grant permissions on all sequences in schema (for auto-increment columns)
|
|
367
|
+
cur.execute(
|
|
368
|
+
f'GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA "{schema}" TO "{client_id}"'
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
# Set default privileges for future tables
|
|
372
|
+
cur.execute(
|
|
373
|
+
f'ALTER DEFAULT PRIVILEGES IN SCHEMA "{schema}" '
|
|
374
|
+
f'GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "{client_id}"'
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Set default privileges for future sequences
|
|
378
|
+
cur.execute(
|
|
379
|
+
f'ALTER DEFAULT PRIVILEGES IN SCHEMA "{schema}" '
|
|
380
|
+
f'GRANT USAGE, SELECT ON SEQUENCES TO "{client_id}"'
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
logger.info(f"Permissions granted to {client_id} on schema {schema}")
|
|
384
|
+
|
|
385
|
+
conn.close()
|
|
386
|
+
|
|
387
|
+
except ImportError:
|
|
388
|
+
raise LakebaseError(
|
|
389
|
+
"psycopg2 is required for Lakebase schema setup. "
|
|
390
|
+
"Install with: pip install psycopg2-binary"
|
|
391
|
+
)
|
|
392
|
+
except Exception as e:
|
|
393
|
+
logger.error(f"Failed to setup Lakebase schema: {e}", exc_info=True)
|
|
394
|
+
raise LakebaseError(f"Lakebase schema setup failed: {e}") from e
|
|
395
|
+
|
|
396
|
+
|
|
283
397
|
def initialize_lakebase_tables(
|
|
284
398
|
instance_name: str,
|
|
285
399
|
schema: str,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"""Database models."""
|
|
2
2
|
|
|
3
3
|
from src.database.models.deck_contributor import DeckContributor
|
|
4
|
-
from src.database.models.encryption_key import EncryptionKey
|
|
5
4
|
from src.database.models.feedback import FeedbackConversation, SurveyResponse
|
|
6
5
|
from src.database.models.genie_space import ConfigGenieSpace
|
|
7
6
|
from src.database.models.google_global_credentials import GoogleGlobalCredentials
|
|
@@ -35,7 +34,6 @@ __all__ = [
|
|
|
35
34
|
"ChatRequest",
|
|
36
35
|
"ConfigGenieSpace",
|
|
37
36
|
"DeckContributor",
|
|
38
|
-
"EncryptionKey",
|
|
39
37
|
"ConfigProfile",
|
|
40
38
|
"ConfigProfileContributor",
|
|
41
39
|
"ConfigPrompts",
|