volcano-sdk-python 0.6.7__py3-none-any.whl
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.
- volcano_sdk/__init__.py +91 -0
- volcano_sdk/_function_resolution.py +159 -0
- volcano_sdk/_generated/__init__.py +8 -0
- volcano_sdk/_generated/api/__init__.py +1 -0
- volcano_sdk/_generated/api/anon_keys/__init__.py +1 -0
- volcano_sdk/_generated/api/anon_keys/create_anon_key.py +192 -0
- volcano_sdk/_generated/api/anon_keys/get_anon_key.py +197 -0
- volcano_sdk/_generated/api/anon_keys/list_anon_keys.py +297 -0
- volcano_sdk/_generated/api/anon_keys/regenerate_anon_key.py +193 -0
- volcano_sdk/_generated/api/anon_keys/revoke_anon_key.py +216 -0
- volcano_sdk/_generated/api/anon_keys/set_default_anon_key.py +217 -0
- volcano_sdk/_generated/api/auth_admin/__init__.py +1 -0
- volcano_sdk/_generated/api/auth_admin/ban_auth_user.py +240 -0
- volcano_sdk/_generated/api/auth_admin/delete_all_user_sessions.py +132 -0
- volcano_sdk/_generated/api/auth_admin/delete_auth_user.py +127 -0
- volcano_sdk/_generated/api/auth_admin/delete_user_session.py +139 -0
- volcano_sdk/_generated/api/auth_admin/get_auth_insights.py +301 -0
- volcano_sdk/_generated/api/auth_admin/get_auth_user.py +185 -0
- volcano_sdk/_generated/api/auth_admin/list_auth_users.py +306 -0
- volcano_sdk/_generated/api/auth_admin/list_user_sessions.py +365 -0
- volcano_sdk/_generated/api/auth_admin/unban_auth_user.py +205 -0
- volcano_sdk/_generated/api/auth_configuration/__init__.py +1 -0
- volcano_sdk/_generated/api/auth_configuration/configure_auth_methods.py +143 -0
- volcano_sdk/_generated/api/auth_configuration/create_email_template.py +246 -0
- volcano_sdk/_generated/api/auth_configuration/delete_auth_page_layout.py +219 -0
- volcano_sdk/_generated/api/auth_configuration/delete_auth_page_theme.py +197 -0
- volcano_sdk/_generated/api/auth_configuration/delete_email_template.py +211 -0
- volcano_sdk/_generated/api/auth_configuration/get_auth_config.py +172 -0
- volcano_sdk/_generated/api/auth_configuration/get_auth_hosted_page.py +207 -0
- volcano_sdk/_generated/api/auth_configuration/get_auth_methods.py +184 -0
- volcano_sdk/_generated/api/auth_configuration/get_auth_page_appearance.py +201 -0
- volcano_sdk/_generated/api/auth_configuration/get_default_email_template.py +177 -0
- volcano_sdk/_generated/api/auth_configuration/get_default_email_templates.py +158 -0
- volcano_sdk/_generated/api/auth_configuration/get_email_template.py +191 -0
- volcano_sdk/_generated/api/auth_configuration/get_hosted_login_options.py +221 -0
- volcano_sdk/_generated/api/auth_configuration/hosted_login_check_email.py +231 -0
- volcano_sdk/_generated/api/auth_configuration/list_email_templates.py +180 -0
- volcano_sdk/_generated/api/auth_configuration/preview_auth_page.py +243 -0
- volcano_sdk/_generated/api/auth_configuration/render_auth_page_preview.py +228 -0
- volcano_sdk/_generated/api/auth_configuration/render_default_managed_auth_page.py +261 -0
- volcano_sdk/_generated/api/auth_configuration/render_managed_auth_page.py +215 -0
- volcano_sdk/_generated/api/auth_configuration/test_email_config.py +292 -0
- volcano_sdk/_generated/api/auth_configuration/update_auth_config.py +219 -0
- volcano_sdk/_generated/api/auth_configuration/update_auth_hosted_page.py +243 -0
- volcano_sdk/_generated/api/auth_configuration/update_auth_page_layout.py +242 -0
- volcano_sdk/_generated/api/auth_configuration/update_auth_page_theme.py +227 -0
- volcano_sdk/_generated/api/auth_configuration/update_email_template.py +239 -0
- volcano_sdk/_generated/api/authentication/__init__.py +1 -0
- volcano_sdk/_generated/api/authentication/auth_cancel_email_change.py +158 -0
- volcano_sdk/_generated/api/authentication/auth_confirm_email.py +198 -0
- volcano_sdk/_generated/api/authentication/auth_confirm_email_change.py +215 -0
- volcano_sdk/_generated/api/authentication/auth_convert_anonymous.py +222 -0
- volcano_sdk/_generated/api/authentication/auth_delete_all_my_sessions.py +166 -0
- volcano_sdk/_generated/api/authentication/auth_delete_my_session.py +195 -0
- volcano_sdk/_generated/api/authentication/auth_forgot_password.py +209 -0
- volcano_sdk/_generated/api/authentication/auth_get_my_sessions.py +397 -0
- volcano_sdk/_generated/api/authentication/auth_get_password_policy.py +181 -0
- volcano_sdk/_generated/api/authentication/auth_get_user.py +166 -0
- volcano_sdk/_generated/api/authentication/auth_list_identities.py +178 -0
- volcano_sdk/_generated/api/authentication/auth_list_methods.py +174 -0
- volcano_sdk/_generated/api/authentication/auth_logout.py +138 -0
- volcano_sdk/_generated/api/authentication/auth_promote_method.py +221 -0
- volcano_sdk/_generated/api/authentication/auth_refresh.py +235 -0
- volcano_sdk/_generated/api/authentication/auth_request_email_change.py +226 -0
- volcano_sdk/_generated/api/authentication/auth_resend_confirmation.py +210 -0
- volcano_sdk/_generated/api/authentication/auth_reset_password.py +206 -0
- volcano_sdk/_generated/api/authentication/auth_signin.py +222 -0
- volcano_sdk/_generated/api/authentication/auth_signup.py +254 -0
- volcano_sdk/_generated/api/authentication/auth_signup_anonymous.py +205 -0
- volcano_sdk/_generated/api/authentication/auth_unlink_identity.py +206 -0
- volcano_sdk/_generated/api/authentication/auth_update_user.py +211 -0
- volcano_sdk/_generated/api/database_backups/__init__.py +1 -0
- volcano_sdk/_generated/api/database_backups/create_database_backup.py +265 -0
- volcano_sdk/_generated/api/database_backups/create_database_restore.py +305 -0
- volcano_sdk/_generated/api/database_backups/delete_database_backup.py +247 -0
- volcano_sdk/_generated/api/database_backups/get_database_backup.py +235 -0
- volcano_sdk/_generated/api/database_backups/get_database_backup_schedule.py +226 -0
- volcano_sdk/_generated/api/database_backups/get_database_restore.py +232 -0
- volcano_sdk/_generated/api/database_backups/list_database_backups.py +242 -0
- volcano_sdk/_generated/api/database_backups/list_database_restores.py +223 -0
- volcano_sdk/_generated/api/database_backups/update_database_backup_schedule.py +272 -0
- volcano_sdk/_generated/api/database_branches/__init__.py +1 -0
- volcano_sdk/_generated/api/database_branches/create_database_branch.py +285 -0
- volcano_sdk/_generated/api/database_branches/delete_database_branch.py +249 -0
- volcano_sdk/_generated/api/database_branches/get_database_branch.py +225 -0
- volcano_sdk/_generated/api/database_branches/list_database_branches.py +224 -0
- volcano_sdk/_generated/api/database_branches/reset_database_branch.py +268 -0
- volcano_sdk/_generated/api/database_branches/reset_database_branch_password.py +252 -0
- volcano_sdk/_generated/api/database_branches/update_database_branch.py +267 -0
- volcano_sdk/_generated/api/database_queries/__init__.py +1 -0
- volcano_sdk/_generated/api/database_queries/query_database_branch_delete.py +295 -0
- volcano_sdk/_generated/api/database_queries/query_database_branch_insert.py +299 -0
- volcano_sdk/_generated/api/database_queries/query_database_branch_ping.py +276 -0
- volcano_sdk/_generated/api/database_queries/query_database_branch_select.py +315 -0
- volcano_sdk/_generated/api/database_queries/query_database_branch_update.py +303 -0
- volcano_sdk/_generated/api/database_queries/query_database_delete.py +259 -0
- volcano_sdk/_generated/api/database_queries/query_database_insert.py +263 -0
- volcano_sdk/_generated/api/database_queries/query_database_ping.py +240 -0
- volcano_sdk/_generated/api/database_queries/query_database_select.py +279 -0
- volcano_sdk/_generated/api/database_queries/query_database_update.py +267 -0
- volcano_sdk/_generated/api/databases/__init__.py +1 -0
- volcano_sdk/_generated/api/databases/create_database.py +236 -0
- volcano_sdk/_generated/api/databases/delete_database.py +247 -0
- volcano_sdk/_generated/api/databases/get_database.py +185 -0
- volcano_sdk/_generated/api/databases/get_database_stats.py +292 -0
- volcano_sdk/_generated/api/databases/get_project_database_queries.py +267 -0
- volcano_sdk/_generated/api/databases/list_database_regions.py +167 -0
- volcano_sdk/_generated/api/databases/list_databases.py +314 -0
- volcano_sdk/_generated/api/databases/list_postgres_versions.py +167 -0
- volcano_sdk/_generated/api/databases/reset_database_password.py +250 -0
- volcano_sdk/_generated/api/databases/update_database_type.py +268 -0
- volcano_sdk/_generated/api/frontends/__init__.py +1 -0
- volcano_sdk/_generated/api/frontends/create_frontend.py +369 -0
- volcano_sdk/_generated/api/frontends/create_frontend_custom_domain.py +278 -0
- volcano_sdk/_generated/api/frontends/delete_frontend.py +244 -0
- volcano_sdk/_generated/api/frontends/delete_frontend_custom_domain.py +217 -0
- volcano_sdk/_generated/api/frontends/get_frontend.py +221 -0
- volcano_sdk/_generated/api/frontends/get_frontend_custom_domain.py +234 -0
- volcano_sdk/_generated/api/frontends/get_frontend_usage_history.py +283 -0
- volcano_sdk/_generated/api/frontends/list_frontend_deployments.py +258 -0
- volcano_sdk/_generated/api/frontends/list_frontends.py +329 -0
- volcano_sdk/_generated/api/frontends/list_project_custom_domains.py +318 -0
- volcano_sdk/_generated/api/frontends/redeploy_frontend.py +267 -0
- volcano_sdk/_generated/api/functions/__init__.py +1 -0
- volcano_sdk/_generated/api/functions/create_function.py +320 -0
- volcano_sdk/_generated/api/functions/create_function_scheduler.py +225 -0
- volcano_sdk/_generated/api/functions/create_functions_batch.py +271 -0
- volcano_sdk/_generated/api/functions/delete_function.py +209 -0
- volcano_sdk/_generated/api/functions/delete_function_scheduler.py +130 -0
- volcano_sdk/_generated/api/functions/get_function.py +193 -0
- volcano_sdk/_generated/api/functions/get_function_scheduler.py +198 -0
- volcano_sdk/_generated/api/functions/invoke_function.py +385 -0
- volcano_sdk/_generated/api/functions/list_function_deployments.py +230 -0
- volcano_sdk/_generated/api/functions/list_function_regions.py +171 -0
- volcano_sdk/_generated/api/functions/list_function_runtimes.py +166 -0
- volcano_sdk/_generated/api/functions/list_function_schedulers.py +185 -0
- volcano_sdk/_generated/api/functions/list_functions.py +301 -0
- volcano_sdk/_generated/api/functions/list_project_schedulers.py +289 -0
- volcano_sdk/_generated/api/functions/resolve_function_for_invocation.py +264 -0
- volcano_sdk/_generated/api/functions/update_function.py +220 -0
- volcano_sdk/_generated/api/functions/update_function_scheduler.py +218 -0
- volcano_sdk/_generated/api/git_connections/__init__.py +1 -0
- volcano_sdk/_generated/api/git_connections/delete_git_connection.py +204 -0
- volcano_sdk/_generated/api/git_connections/git_connect_callback.py +247 -0
- volcano_sdk/_generated/api/git_connections/list_git_connections.py +172 -0
- volcano_sdk/_generated/api/git_connections/list_git_installation_repositories.py +237 -0
- volcano_sdk/_generated/api/git_connections/list_git_installations.py +224 -0
- volcano_sdk/_generated/api/git_connections/start_git_connect.py +250 -0
- volcano_sdk/_generated/api/locks/__init__.py +1 -0
- volcano_sdk/_generated/api/locks/acquire_project_lock.py +289 -0
- volcano_sdk/_generated/api/locks/force_release_project_lock.py +254 -0
- volcano_sdk/_generated/api/locks/get_project_lock.py +246 -0
- volcano_sdk/_generated/api/locks/release_project_lock.py +252 -0
- volcano_sdk/_generated/api/locks/renew_project_lock.py +285 -0
- volcano_sdk/_generated/api/logs/__init__.py +1 -0
- volcano_sdk/_generated/api/logs/get_project_log_activity.py +261 -0
- volcano_sdk/_generated/api/logs/search_project_logs.py +265 -0
- volcano_sdk/_generated/api/logs/stream_project_logs.py +345 -0
- volcano_sdk/_generated/api/o_auth_authentication/__init__.py +1 -0
- volcano_sdk/_generated/api/o_auth_authentication/auth_device_authorize.py +266 -0
- volcano_sdk/_generated/api/o_auth_authentication/auth_device_token.py +209 -0
- volcano_sdk/_generated/api/o_auth_authentication/auth_device_verify.py +230 -0
- volcano_sdk/_generated/api/o_auth_authentication/auth_link_o_auth_provider.py +272 -0
- volcano_sdk/_generated/api/o_auth_authentication/auth_list_o_auth_providers.py +166 -0
- volcano_sdk/_generated/api/o_auth_authentication/auth_o_auth_authorize.py +181 -0
- volcano_sdk/_generated/api/o_auth_authentication/auth_o_auth_callback.py +260 -0
- volcano_sdk/_generated/api/o_auth_authentication/auth_o_auth_exchange.py +210 -0
- volcano_sdk/_generated/api/o_auth_authentication/auth_platform_exchange.py +202 -0
- volcano_sdk/_generated/api/o_auth_authentication/auth_unlink_o_auth_provider.py +203 -0
- volcano_sdk/_generated/api/o_auth_authentication/call_o_auth_provider_api.py +309 -0
- volcano_sdk/_generated/api/o_auth_authentication/get_o_auth_provider_token.py +200 -0
- volcano_sdk/_generated/api/o_auth_authentication/refresh_o_auth_provider_token.py +207 -0
- volcano_sdk/_generated/api/o_auth_configuration/__init__.py +1 -0
- volcano_sdk/_generated/api/o_auth_configuration/create_o_auth_config.py +204 -0
- volcano_sdk/_generated/api/o_auth_configuration/delete_o_auth_config.py +142 -0
- volcano_sdk/_generated/api/o_auth_configuration/get_o_auth_config.py +209 -0
- volcano_sdk/_generated/api/o_auth_configuration/list_available_o_auth_providers.py +180 -0
- volcano_sdk/_generated/api/o_auth_configuration/list_o_auth_configs.py +180 -0
- volcano_sdk/_generated/api/o_auth_configuration/update_o_auth_config.py +230 -0
- volcano_sdk/_generated/api/project_imports/__init__.py +1 -0
- volcano_sdk/_generated/api/project_imports/complete_import_connect.py +322 -0
- volcano_sdk/_generated/api/project_imports/delete_import_connection.py +211 -0
- volcano_sdk/_generated/api/project_imports/get_project_import_run.py +216 -0
- volcano_sdk/_generated/api/project_imports/list_import_connections.py +165 -0
- volcano_sdk/_generated/api/project_imports/list_import_sources.py +261 -0
- volcano_sdk/_generated/api/project_imports/preflight_project_import.py +258 -0
- volcano_sdk/_generated/api/project_imports/start_import_connect.py +246 -0
- volcano_sdk/_generated/api/project_imports/start_project_import.py +284 -0
- volcano_sdk/_generated/api/projects/__init__.py +1 -0
- volcano_sdk/_generated/api/projects/apply_project_config.py +342 -0
- volcano_sdk/_generated/api/projects/cancel_project_source_export.py +231 -0
- volcano_sdk/_generated/api/projects/connect_project_git.py +278 -0
- volcano_sdk/_generated/api/projects/create_project.py +205 -0
- volcano_sdk/_generated/api/projects/delete_project.py +196 -0
- volcano_sdk/_generated/api/projects/delete_project_logo.py +213 -0
- volcano_sdk/_generated/api/projects/disconnect_project_git.py +204 -0
- volcano_sdk/_generated/api/projects/export_project_source.py +351 -0
- volcano_sdk/_generated/api/projects/get_project.py +180 -0
- volcano_sdk/_generated/api/projects/get_project_config.py +251 -0
- volcano_sdk/_generated/api/projects/get_project_git_connection.py +201 -0
- volcano_sdk/_generated/api/projects/get_project_git_deploy_settings.py +201 -0
- volcano_sdk/_generated/api/projects/get_project_health.py +221 -0
- volcano_sdk/_generated/api/projects/get_project_logo.py +196 -0
- volcano_sdk/_generated/api/projects/get_project_source_export.py +276 -0
- volcano_sdk/_generated/api/projects/get_project_usage.py +199 -0
- volcano_sdk/_generated/api/projects/list_deployments.py +558 -0
- volcano_sdk/_generated/api/projects/list_project_deployments.py +358 -0
- volcano_sdk/_generated/api/projects/list_projects.py +332 -0
- volcano_sdk/_generated/api/projects/query_project_metrics.py +251 -0
- volcano_sdk/_generated/api/projects/set_project_git_production_branch.py +291 -0
- volcano_sdk/_generated/api/projects/summarize_project_deployments.py +288 -0
- volcano_sdk/_generated/api/projects/update_project.py +221 -0
- volcano_sdk/_generated/api/projects/update_project_git_deploy_settings.py +287 -0
- volcano_sdk/_generated/api/projects/upload_project_logo.py +248 -0
- volcano_sdk/_generated/api/realtime/__init__.py +1 -0
- volcano_sdk/_generated/api/realtime/get_realtime_config.py +195 -0
- volcano_sdk/_generated/api/realtime/get_realtime_stats.py +188 -0
- volcano_sdk/_generated/api/realtime/update_realtime_config.py +219 -0
- volcano_sdk/_generated/api/service_keys/__init__.py +1 -0
- volcano_sdk/_generated/api/service_keys/create_service_key.py +216 -0
- volcano_sdk/_generated/api/service_keys/delete_service_key.py +129 -0
- volcano_sdk/_generated/api/service_keys/get_service_key.py +197 -0
- volcano_sdk/_generated/api/service_keys/list_service_keys.py +285 -0
- volcano_sdk/_generated/api/service_keys/regenerate_service_key.py +201 -0
- volcano_sdk/_generated/api/storage_admin/__init__.py +1 -0
- volcano_sdk/_generated/api/storage_admin/get_storage_stats.py +180 -0
- volcano_sdk/_generated/api/storage_admin/list_storage_objects_admin.py +299 -0
- volcano_sdk/_generated/api/storage_buckets/__init__.py +1 -0
- volcano_sdk/_generated/api/storage_buckets/create_storage_bucket.py +196 -0
- volcano_sdk/_generated/api/storage_buckets/delete_storage_bucket.py +123 -0
- volcano_sdk/_generated/api/storage_buckets/get_storage_bucket.py +189 -0
- volcano_sdk/_generated/api/storage_buckets/list_storage_buckets.py +297 -0
- volcano_sdk/_generated/api/storage_buckets/update_storage_bucket.py +205 -0
- volcano_sdk/_generated/api/storage_objects/__init__.py +1 -0
- volcano_sdk/_generated/api/storage_objects/copy_storage_object.py +203 -0
- volcano_sdk/_generated/api/storage_objects/delete_storage_object.py +252 -0
- volcano_sdk/_generated/api/storage_objects/download_public_file.py +302 -0
- volcano_sdk/_generated/api/storage_objects/download_storage_object.py +283 -0
- volcano_sdk/_generated/api/storage_objects/list_storage_objects.py +235 -0
- volcano_sdk/_generated/api/storage_objects/move_storage_object.py +203 -0
- volcano_sdk/_generated/api/storage_objects/update_storage_object_visibility.py +244 -0
- volcano_sdk/_generated/api/storage_objects/upload_part.py +287 -0
- volcano_sdk/_generated/api/storage_objects/upload_storage_object.py +380 -0
- volcano_sdk/_generated/api/storage_policies/__init__.py +1 -0
- volcano_sdk/_generated/api/storage_policies/create_storage_policy.py +205 -0
- volcano_sdk/_generated/api/storage_policies/delete_storage_policy.py +130 -0
- volcano_sdk/_generated/api/storage_policies/list_storage_policies.py +190 -0
- volcano_sdk/_generated/api/system/__init__.py +1 -0
- volcano_sdk/_generated/api/system/health_check.py +153 -0
- volcano_sdk/_generated/api/variables/__init__.py +1 -0
- volcano_sdk/_generated/api/variables/create_variable.py +212 -0
- volcano_sdk/_generated/api/variables/delete_variable.py +201 -0
- volcano_sdk/_generated/api/variables/get_variable.py +201 -0
- volcano_sdk/_generated/api/variables/list_variables.py +285 -0
- volcano_sdk/_generated/api/variables/update_variable.py +225 -0
- volcano_sdk/_generated/client.py +271 -0
- volcano_sdk/_generated/errors.py +14 -0
- volcano_sdk/_generated/models/__init__.py +1003 -0
- volcano_sdk/_generated/models/anon_key.py +167 -0
- volcano_sdk/_generated/models/auth_cancel_email_change_response_200.py +78 -0
- volcano_sdk/_generated/models/auth_config.py +559 -0
- volcano_sdk/_generated/models/auth_config_allowed_email_domains_mode.py +10 -0
- volcano_sdk/_generated/models/auth_confirm_email_body.py +76 -0
- volcano_sdk/_generated/models/auth_confirm_email_change_body.py +76 -0
- volcano_sdk/_generated/models/auth_confirm_email_change_response_200.py +102 -0
- volcano_sdk/_generated/models/auth_confirm_email_response_200.py +92 -0
- volcano_sdk/_generated/models/auth_confirm_email_response_200_message.py +10 -0
- volcano_sdk/_generated/models/auth_convert_anonymous_body.py +110 -0
- volcano_sdk/_generated/models/auth_convert_anonymous_body_user_metadata.py +65 -0
- volcano_sdk/_generated/models/auth_convert_anonymous_response_200.py +93 -0
- volcano_sdk/_generated/models/auth_device_authorize_body.py +76 -0
- volcano_sdk/_generated/models/auth_device_token_body.py +98 -0
- volcano_sdk/_generated/models/auth_device_token_body_grant_type.py +10 -0
- volcano_sdk/_generated/models/auth_device_verify_body.py +100 -0
- volcano_sdk/_generated/models/auth_device_verify_body_action.py +10 -0
- volcano_sdk/_generated/models/auth_device_verify_response_200.py +87 -0
- volcano_sdk/_generated/models/auth_forgot_password_body.py +76 -0
- volcano_sdk/_generated/models/auth_forgot_password_response_200.py +78 -0
- volcano_sdk/_generated/models/auth_get_my_sessions_response_200.py +190 -0
- volcano_sdk/_generated/models/auth_get_my_sessions_sort.py +10 -0
- volcano_sdk/_generated/models/auth_get_my_sessions_status.py +10 -0
- volcano_sdk/_generated/models/auth_get_user_response_200.py +93 -0
- volcano_sdk/_generated/models/auth_hosted_page.py +188 -0
- volcano_sdk/_generated/models/auth_hosted_page_defaults.py +87 -0
- volcano_sdk/_generated/models/auth_hosted_page_response.py +133 -0
- volcano_sdk/_generated/models/auth_hosted_page_runtime.py +97 -0
- volcano_sdk/_generated/models/auth_identities_response.py +94 -0
- volcano_sdk/_generated/models/auth_identity.py +119 -0
- volcano_sdk/_generated/models/auth_insights_interval.py +10 -0
- volcano_sdk/_generated/models/auth_insights_response.py +128 -0
- volcano_sdk/_generated/models/auth_insights_series_point.py +87 -0
- volcano_sdk/_generated/models/auth_insights_summary.py +67 -0
- volcano_sdk/_generated/models/auth_insights_window.py +87 -0
- volcano_sdk/_generated/models/auth_link_o_auth_provider_provider.py +10 -0
- volcano_sdk/_generated/models/auth_link_o_auth_provider_response_200.py +78 -0
- volcano_sdk/_generated/models/auth_link_o_auth_provider_response_mode.py +10 -0
- volcano_sdk/_generated/models/auth_list_o_auth_providers_response_200.py +100 -0
- volcano_sdk/_generated/models/auth_list_o_auth_providers_response_200_providers_item.py +118 -0
- volcano_sdk/_generated/models/auth_logout_body.py +101 -0
- volcano_sdk/_generated/models/auth_logout_body_session_mode.py +10 -0
- volcano_sdk/_generated/models/auth_method_summary.py +173 -0
- volcano_sdk/_generated/models/auth_method_summary_type.py +10 -0
- volcano_sdk/_generated/models/auth_methods_response.py +94 -0
- volcano_sdk/_generated/models/auth_o_auth_authorize_provider.py +10 -0
- volcano_sdk/_generated/models/auth_o_auth_authorize_response_mode.py +10 -0
- volcano_sdk/_generated/models/auth_o_auth_callback_provider.py +10 -0
- volcano_sdk/_generated/models/auth_o_auth_exchange_body.py +84 -0
- volcano_sdk/_generated/models/auth_page_appearance_defaults.py +97 -0
- volcano_sdk/_generated/models/auth_page_appearance_options.py +207 -0
- volcano_sdk/_generated/models/auth_page_appearance_response.py +148 -0
- volcano_sdk/_generated/models/auth_page_appearance_response_layouts.py +78 -0
- volcano_sdk/_generated/models/auth_page_appearance_response_parked.py +66 -0
- volcano_sdk/_generated/models/auth_page_density.py +10 -0
- volcano_sdk/_generated/models/auth_page_font.py +10 -0
- volcano_sdk/_generated/models/auth_page_layout.py +10 -0
- volcano_sdk/_generated/models/auth_page_radius.py +10 -0
- volcano_sdk/_generated/models/auth_page_scale.py +10 -0
- volcano_sdk/_generated/models/auth_page_theme.py +131 -0
- volcano_sdk/_generated/models/auth_page_theme_colors.py +90 -0
- volcano_sdk/_generated/models/auth_page_theme_version.py +10 -0
- volcano_sdk/_generated/models/auth_password_policy.py +116 -0
- volcano_sdk/_generated/models/auth_platform_exchange_body.py +76 -0
- volcano_sdk/_generated/models/auth_refresh_body.py +101 -0
- volcano_sdk/_generated/models/auth_refresh_body_session_mode.py +10 -0
- volcano_sdk/_generated/models/auth_request_email_change_body.py +76 -0
- volcano_sdk/_generated/models/auth_request_email_change_response_200.py +87 -0
- volcano_sdk/_generated/models/auth_resend_confirmation_body.py +76 -0
- volcano_sdk/_generated/models/auth_resend_confirmation_response_200.py +78 -0
- volcano_sdk/_generated/models/auth_reset_password_body.py +85 -0
- volcano_sdk/_generated/models/auth_reset_password_response_200.py +78 -0
- volcano_sdk/_generated/models/auth_session.py +239 -0
- volcano_sdk/_generated/models/auth_session_provider.py +10 -0
- volcano_sdk/_generated/models/auth_signin_body.py +108 -0
- volcano_sdk/_generated/models/auth_signin_body_session_mode.py +10 -0
- volcano_sdk/_generated/models/auth_signup_anonymous_body.py +94 -0
- volcano_sdk/_generated/models/auth_signup_anonymous_body_user_metadata.py +69 -0
- volcano_sdk/_generated/models/auth_signup_body.py +110 -0
- volcano_sdk/_generated/models/auth_signup_body_user_metadata.py +65 -0
- volcano_sdk/_generated/models/auth_signup_response.py +90 -0
- volcano_sdk/_generated/models/auth_token_response.py +119 -0
- volcano_sdk/_generated/models/auth_unlink_o_auth_provider_provider.py +10 -0
- volcano_sdk/_generated/models/auth_update_user_body.py +105 -0
- volcano_sdk/_generated/models/auth_update_user_body_user_metadata.py +68 -0
- volcano_sdk/_generated/models/auth_update_user_response_200.py +93 -0
- volcano_sdk/_generated/models/auth_user.py +276 -0
- volcano_sdk/_generated/models/auth_user_app_metadata.py +66 -0
- volcano_sdk/_generated/models/auth_user_status.py +10 -0
- volcano_sdk/_generated/models/auth_user_user_metadata.py +66 -0
- volcano_sdk/_generated/models/ban_auth_user_body.py +91 -0
- volcano_sdk/_generated/models/ban_user_response.py +146 -0
- volcano_sdk/_generated/models/ban_user_response_status.py +10 -0
- volcano_sdk/_generated/models/batch_function_deploy_failure.py +128 -0
- volcano_sdk/_generated/models/batch_function_deploy_failure_operation.py +10 -0
- volcano_sdk/_generated/models/batch_function_deploy_response.py +138 -0
- volcano_sdk/_generated/models/call_o_auth_provider_api_body.py +126 -0
- volcano_sdk/_generated/models/call_o_auth_provider_api_body_body.py +66 -0
- volcano_sdk/_generated/models/call_o_auth_provider_api_body_method.py +10 -0
- volcano_sdk/_generated/models/call_o_auth_provider_api_provider.py +10 -0
- volcano_sdk/_generated/models/call_o_auth_provider_api_response_200.py +107 -0
- volcano_sdk/_generated/models/call_o_auth_provider_api_response_200_provider.py +10 -0
- volcano_sdk/_generated/models/complete_upload_session_response.py +94 -0
- volcano_sdk/_generated/models/configure_auth_methods_body.py +118 -0
- volcano_sdk/_generated/models/configure_auth_methods_body_oauth_providers_item.py +87 -0
- volcano_sdk/_generated/models/connect_project_git_request.py +137 -0
- volcano_sdk/_generated/models/create_anon_key_body.py +112 -0
- volcano_sdk/_generated/models/create_anon_key_body_permissions_item.py +10 -0
- volcano_sdk/_generated/models/create_database_backup_request.py +78 -0
- volcano_sdk/_generated/models/create_database_branch_request.py +88 -0
- volcano_sdk/_generated/models/create_database_request.py +134 -0
- volcano_sdk/_generated/models/create_database_request_database_type.py +10 -0
- volcano_sdk/_generated/models/create_database_request_pg_version.py +10 -0
- volcano_sdk/_generated/models/create_database_request_region.py +10 -0
- volcano_sdk/_generated/models/create_database_restore_request.py +109 -0
- volcano_sdk/_generated/models/create_email_template_request.py +109 -0
- volcano_sdk/_generated/models/create_email_template_request_template_type.py +10 -0
- volcano_sdk/_generated/models/create_frontend_body.py +159 -0
- volcano_sdk/_generated/models/create_frontend_body_framework.py +10 -0
- volcano_sdk/_generated/models/create_frontend_custom_domain_request.py +92 -0
- volcano_sdk/_generated/models/create_function_body.py +243 -0
- volcano_sdk/_generated/models/create_function_body_runtime.py +10 -0
- volcano_sdk/_generated/models/create_function_scheduler_request.py +139 -0
- volcano_sdk/_generated/models/create_function_scheduler_request_payload.py +65 -0
- volcano_sdk/_generated/models/create_functions_batch_body.py +125 -0
- volcano_sdk/_generated/models/create_o_auth_config_request.py +128 -0
- volcano_sdk/_generated/models/create_o_auth_config_request_provider.py +10 -0
- volcano_sdk/_generated/models/create_project_request.py +109 -0
- volcano_sdk/_generated/models/create_service_key_body.py +100 -0
- volcano_sdk/_generated/models/create_storage_bucket_request.py +101 -0
- volcano_sdk/_generated/models/create_storage_policy_request.py +98 -0
- volcano_sdk/_generated/models/create_storage_policy_request_operation.py +10 -0
- volcano_sdk/_generated/models/create_upload_session_request.py +103 -0
- volcano_sdk/_generated/models/create_upload_session_response.py +119 -0
- volcano_sdk/_generated/models/create_variable_request.py +84 -0
- volcano_sdk/_generated/models/database.py +255 -0
- volcano_sdk/_generated/models/database_backup.py +142 -0
- volcano_sdk/_generated/models/database_backup_list.py +123 -0
- volcano_sdk/_generated/models/database_backup_schedule.py +96 -0
- volcano_sdk/_generated/models/database_backup_schedule_entry.py +114 -0
- volcano_sdk/_generated/models/database_backup_schedule_entry_frequency.py +10 -0
- volcano_sdk/_generated/models/database_backup_source.py +10 -0
- volcano_sdk/_generated/models/database_branch.py +235 -0
- volcano_sdk/_generated/models/database_branch_list.py +94 -0
- volcano_sdk/_generated/models/database_branch_status.py +10 -0
- volcano_sdk/_generated/models/database_branch_storage.py +99 -0
- volcano_sdk/_generated/models/database_database_type.py +10 -0
- volcano_sdk/_generated/models/database_delete_request.py +103 -0
- volcano_sdk/_generated/models/database_insert_request.py +93 -0
- volcano_sdk/_generated/models/database_insert_request_values.py +69 -0
- volcano_sdk/_generated/models/database_query_filter.py +142 -0
- volcano_sdk/_generated/models/database_query_filter_operator.py +10 -0
- volcano_sdk/_generated/models/database_query_order.py +96 -0
- volcano_sdk/_generated/models/database_query_performance_database.py +88 -0
- volcano_sdk/_generated/models/database_query_performance_item.py +156 -0
- volcano_sdk/_generated/models/database_query_performance_response.py +94 -0
- volcano_sdk/_generated/models/database_query_result.py +111 -0
- volcano_sdk/_generated/models/database_query_result_data_item.py +65 -0
- volcano_sdk/_generated/models/database_restore.py +221 -0
- volcano_sdk/_generated/models/database_restore_kind.py +10 -0
- volcano_sdk/_generated/models/database_restore_list.py +94 -0
- volcano_sdk/_generated/models/database_restore_status.py +10 -0
- volcano_sdk/_generated/models/database_restore_window.py +117 -0
- volcano_sdk/_generated/models/database_select_request.py +171 -0
- volcano_sdk/_generated/models/database_stats.py +211 -0
- volcano_sdk/_generated/models/database_stats_granularity.py +10 -0
- volcano_sdk/_generated/models/database_status.py +10 -0
- volcano_sdk/_generated/models/database_update_request.py +120 -0
- volcano_sdk/_generated/models/database_update_request_values.py +69 -0
- volcano_sdk/_generated/models/delete_database_backup_response_200.py +84 -0
- volcano_sdk/_generated/models/delete_database_branch_response_202.py +84 -0
- volcano_sdk/_generated/models/delete_database_response_202.py +87 -0
- volcano_sdk/_generated/models/delete_email_template_type.py +10 -0
- volcano_sdk/_generated/models/delete_o_auth_config_provider.py +10 -0
- volcano_sdk/_generated/models/deployment_phase.py +145 -0
- volcano_sdk/_generated/models/deployment_phase_name.py +10 -0
- volcano_sdk/_generated/models/deployment_phase_status.py +10 -0
- volcano_sdk/_generated/models/deployment_progress.py +168 -0
- volcano_sdk/_generated/models/deployment_progress_current_phase.py +10 -0
- volcano_sdk/_generated/models/deployment_reference.py +81 -0
- volcano_sdk/_generated/models/device_authorization_response.py +122 -0
- volcano_sdk/_generated/models/email_template.py +186 -0
- volcano_sdk/_generated/models/email_template_template_type.py +10 -0
- volcano_sdk/_generated/models/error.py +86 -0
- volcano_sdk/_generated/models/export_project_source_request.py +58 -0
- volcano_sdk/_generated/models/frontend.py +293 -0
- volcano_sdk/_generated/models/frontend_custom_domain_response.py +203 -0
- volcano_sdk/_generated/models/frontend_custom_domain_response_domain_status.py +10 -0
- volcano_sdk/_generated/models/frontend_custom_domain_response_tls_mode.py +10 -0
- volcano_sdk/_generated/models/frontend_custom_domain_response_verification_status.py +10 -0
- volcano_sdk/_generated/models/frontend_custom_domain_status.py +10 -0
- volcano_sdk/_generated/models/frontend_custom_domain_tls_config.py +108 -0
- volcano_sdk/_generated/models/frontend_custom_domain_tls_config_mode.py +10 -0
- volcano_sdk/_generated/models/frontend_deployment.py +304 -0
- volcano_sdk/_generated/models/frontend_deployment_deploy_source.py +10 -0
- volcano_sdk/_generated/models/frontend_deployment_operation.py +10 -0
- volcano_sdk/_generated/models/frontend_deployment_status.py +10 -0
- volcano_sdk/_generated/models/frontend_domain_routing_record.py +98 -0
- volcano_sdk/_generated/models/frontend_domain_routing_record_record_type.py +10 -0
- volcano_sdk/_generated/models/frontend_domain_verification_record.py +92 -0
- volcano_sdk/_generated/models/frontend_framework.py +10 -0
- volcano_sdk/_generated/models/frontend_status.py +10 -0
- volcano_sdk/_generated/models/frontend_usage_daily_entry.py +107 -0
- volcano_sdk/_generated/models/frontend_usage_data.py +112 -0
- volcano_sdk/_generated/models/frontend_usage_history_response.py +139 -0
- volcano_sdk/_generated/models/function.py +341 -0
- volcano_sdk/_generated/models/function_deployment.py +313 -0
- volcano_sdk/_generated/models/function_deployment_deploy_source.py +10 -0
- volcano_sdk/_generated/models/function_deployment_operation.py +10 -0
- volcano_sdk/_generated/models/function_deployment_status.py +10 -0
- volcano_sdk/_generated/models/function_http_auth_mode.py +10 -0
- volcano_sdk/_generated/models/function_invocation_mode.py +10 -0
- volcano_sdk/_generated/models/function_invocation_request.py +106 -0
- volcano_sdk/_generated/models/function_invocation_request_payload.py +79 -0
- volcano_sdk/_generated/models/function_invocation_response.py +66 -0
- volcano_sdk/_generated/models/function_openapi_spec_type_0.py +66 -0
- volcano_sdk/_generated/models/function_region.py +92 -0
- volcano_sdk/_generated/models/function_runtime_deployment.py +109 -0
- volcano_sdk/_generated/models/function_runtime_option.py +108 -0
- volcano_sdk/_generated/models/function_runtimes_response.py +94 -0
- volcano_sdk/_generated/models/function_scheduler.py +338 -0
- volcano_sdk/_generated/models/function_scheduler_list_response.py +146 -0
- volcano_sdk/_generated/models/function_scheduler_payload.py +65 -0
- volcano_sdk/_generated/models/function_scheduler_schedule_kind.py +10 -0
- volcano_sdk/_generated/models/function_status.py +10 -0
- volcano_sdk/_generated/models/get_auth_methods_response_200.py +158 -0
- volcano_sdk/_generated/models/get_auth_methods_response_200_anonymous.py +96 -0
- volcano_sdk/_generated/models/get_auth_methods_response_200_email_password.py +96 -0
- volcano_sdk/_generated/models/get_auth_methods_response_200_oauth_providers_item.py +129 -0
- volcano_sdk/_generated/models/get_database_stats_granularity.py +10 -0
- volcano_sdk/_generated/models/get_default_email_template_type.py +10 -0
- volcano_sdk/_generated/models/get_default_email_templates_response_200.py +100 -0
- volcano_sdk/_generated/models/get_email_template_type.py +10 -0
- volcano_sdk/_generated/models/get_o_auth_config_provider.py +10 -0
- volcano_sdk/_generated/models/get_o_auth_provider_token_provider.py +10 -0
- volcano_sdk/_generated/models/get_o_auth_provider_token_response_200.py +96 -0
- volcano_sdk/_generated/models/get_project_config_format.py +10 -0
- volcano_sdk/_generated/models/git_connect_start_response.py +76 -0
- volcano_sdk/_generated/models/git_connection.py +147 -0
- volcano_sdk/_generated/models/git_connections_response.py +94 -0
- volcano_sdk/_generated/models/git_installation.py +100 -0
- volcano_sdk/_generated/models/git_installations_response.py +94 -0
- volcano_sdk/_generated/models/git_repositories_response.py +94 -0
- volcano_sdk/_generated/models/git_repository.py +108 -0
- volcano_sdk/_generated/models/hosted_auth_page_type.py +10 -0
- volcano_sdk/_generated/models/hosted_login_email_check_request.py +76 -0
- volcano_sdk/_generated/models/hosted_login_email_check_response.py +78 -0
- volcano_sdk/_generated/models/hosted_login_options_response.py +120 -0
- volcano_sdk/_generated/models/hosted_renderable_page_type.py +10 -0
- volcano_sdk/_generated/models/import_connect_start_response.py +76 -0
- volcano_sdk/_generated/models/import_connection.py +205 -0
- volcano_sdk/_generated/models/import_connections_response.py +94 -0
- volcano_sdk/_generated/models/import_provider.py +10 -0
- volcano_sdk/_generated/models/import_source.py +100 -0
- volcano_sdk/_generated/models/import_sources_response.py +94 -0
- volcano_sdk/_generated/models/list_anon_keys_response_200.py +137 -0
- volcano_sdk/_generated/models/list_auth_users_status.py +10 -0
- volcano_sdk/_generated/models/list_available_o_auth_providers_response_200.py +100 -0
- volcano_sdk/_generated/models/list_available_o_auth_providers_response_200_providers_item.py +102 -0
- volcano_sdk/_generated/models/list_database_regions_response_200_item.py +87 -0
- volcano_sdk/_generated/models/list_databases_status.py +10 -0
- volcano_sdk/_generated/models/list_deployments_operation.py +10 -0
- volcano_sdk/_generated/models/list_deployments_order.py +10 -0
- volcano_sdk/_generated/models/list_deployments_resource_type.py +10 -0
- volcano_sdk/_generated/models/list_deployments_status.py +10 -0
- volcano_sdk/_generated/models/list_email_templates_response_200.py +100 -0
- volcano_sdk/_generated/models/list_o_auth_configs_response_200.py +100 -0
- volcano_sdk/_generated/models/list_postgres_versions_response_200_item.py +105 -0
- volcano_sdk/_generated/models/list_project_deployments_resource_type.py +10 -0
- volcano_sdk/_generated/models/list_projects_include_item.py +10 -0
- volcano_sdk/_generated/models/list_storage_objects_admin_response_200.py +155 -0
- volcano_sdk/_generated/models/list_user_sessions_response_200.py +190 -0
- volcano_sdk/_generated/models/list_user_sessions_sort.py +10 -0
- volcano_sdk/_generated/models/list_user_sessions_status.py +10 -0
- volcano_sdk/_generated/models/live_log_level.py +10 -0
- volcano_sdk/_generated/models/log_activity_bucket.py +116 -0
- volcano_sdk/_generated/models/log_activity_bucket_counts.py +113 -0
- volcano_sdk/_generated/models/log_activity_bucket_counts_levels.py +66 -0
- volcano_sdk/_generated/models/log_activity_bucket_counts_regions.py +66 -0
- volcano_sdk/_generated/models/log_activity_bucket_counts_resource_ids.py +66 -0
- volcano_sdk/_generated/models/log_activity_request.py +168 -0
- volcano_sdk/_generated/models/log_activity_response.py +103 -0
- volcano_sdk/_generated/models/log_database_request_resource.py +94 -0
- volcano_sdk/_generated/models/log_database_request_resource_type.py +10 -0
- volcano_sdk/_generated/models/log_deployment.py +105 -0
- volcano_sdk/_generated/models/log_deployment_request_selector.py +81 -0
- volcano_sdk/_generated/models/log_deployment_stage.py +10 -0
- volcano_sdk/_generated/models/log_event.py +222 -0
- volcano_sdk/_generated/models/log_event_body_type_1.py +65 -0
- volcano_sdk/_generated/models/log_frontend_request_resource.py +117 -0
- volcano_sdk/_generated/models/log_frontend_request_resource_type.py +10 -0
- volcano_sdk/_generated/models/log_function_request_resource.py +117 -0
- volcano_sdk/_generated/models/log_function_request_resource_type.py +10 -0
- volcano_sdk/_generated/models/log_resource.py +105 -0
- volcano_sdk/_generated/models/log_resource_type.py +10 -0
- volcano_sdk/_generated/models/log_search_event.py +212 -0
- volcano_sdk/_generated/models/log_search_request.py +177 -0
- volcano_sdk/_generated/models/log_search_response.py +121 -0
- volcano_sdk/_generated/models/log_stream_request.py +148 -0
- volcano_sdk/_generated/models/metric_usage_data.py +148 -0
- volcano_sdk/_generated/models/o_auth_config.py +201 -0
- volcano_sdk/_generated/models/o_auth_config_provider.py +10 -0
- volcano_sdk/_generated/models/o_auth_error_response.py +86 -0
- volcano_sdk/_generated/models/paginated_auth_users.py +136 -0
- volcano_sdk/_generated/models/paginated_databases.py +155 -0
- volcano_sdk/_generated/models/paginated_frontend_deployments.py +136 -0
- volcano_sdk/_generated/models/paginated_frontends.py +155 -0
- volcano_sdk/_generated/models/paginated_function_deployments.py +136 -0
- volcano_sdk/_generated/models/paginated_functions.py +155 -0
- volcano_sdk/_generated/models/paginated_project_custom_domains.py +136 -0
- volcano_sdk/_generated/models/paginated_project_deployments.py +159 -0
- volcano_sdk/_generated/models/paginated_projects.py +218 -0
- volcano_sdk/_generated/models/paginated_projects_status.py +10 -0
- volcano_sdk/_generated/models/paginated_service_keys.py +155 -0
- volcano_sdk/_generated/models/paginated_storage_buckets.py +140 -0
- volcano_sdk/_generated/models/paginated_variables.py +155 -0
- volcano_sdk/_generated/models/platform_exchange_response.py +109 -0
- volcano_sdk/_generated/models/preview_auth_page_request.py +89 -0
- volcano_sdk/_generated/models/preview_auth_page_response.py +89 -0
- volcano_sdk/_generated/models/project.py +255 -0
- volcano_sdk/_generated/models/project_config.py +288 -0
- volcano_sdk/_generated/models/project_config_apply_result.py +170 -0
- volcano_sdk/_generated/models/project_config_apply_result_entry.py +118 -0
- volcano_sdk/_generated/models/project_config_apply_result_entry_action.py +10 -0
- volcano_sdk/_generated/models/project_config_apply_summary.py +124 -0
- volcano_sdk/_generated/models/project_config_auth.py +296 -0
- volcano_sdk/_generated/models/project_config_auth_cors.py +93 -0
- volcano_sdk/_generated/models/project_config_auth_email.py +132 -0
- volcano_sdk/_generated/models/project_config_auth_email_from.py +69 -0
- volcano_sdk/_generated/models/project_config_auth_email_password_provider.py +60 -0
- volcano_sdk/_generated/models/project_config_auth_email_smtp.py +96 -0
- volcano_sdk/_generated/models/project_config_auth_email_verification.py +70 -0
- volcano_sdk/_generated/models/project_config_auth_managed_pages.py +129 -0
- volcano_sdk/_generated/models/project_config_auth_page_appearance.py +97 -0
- volcano_sdk/_generated/models/project_config_auth_page_layouts.py +174 -0
- volcano_sdk/_generated/models/project_config_auth_password.py +96 -0
- volcano_sdk/_generated/models/project_config_auth_password_reset.py +78 -0
- volcano_sdk/_generated/models/project_config_auth_providers.py +105 -0
- volcano_sdk/_generated/models/project_config_auth_rate_limits.py +88 -0
- volcano_sdk/_generated/models/project_config_auth_redirects.py +93 -0
- volcano_sdk/_generated/models/project_config_auth_sessions.py +69 -0
- volcano_sdk/_generated/models/project_config_auth_signup.py +118 -0
- volcano_sdk/_generated/models/project_config_auth_signup_allowed_email_domains_mode.py +10 -0
- volcano_sdk/_generated/models/project_config_auth_tokens.py +88 -0
- volcano_sdk/_generated/models/project_config_bucket.py +118 -0
- volcano_sdk/_generated/models/project_config_bucket_policy.py +80 -0
- volcano_sdk/_generated/models/project_config_bucket_policy_operation.py +10 -0
- volcano_sdk/_generated/models/project_config_custom_domain.py +88 -0
- volcano_sdk/_generated/models/project_config_database.py +109 -0
- volcano_sdk/_generated/models/project_config_database_database_type.py +10 -0
- volcano_sdk/_generated/models/project_config_database_pg_version.py +10 -0
- volcano_sdk/_generated/models/project_config_email_template.py +78 -0
- volcano_sdk/_generated/models/project_config_email_templates.py +136 -0
- volcano_sdk/_generated/models/project_config_frontend.py +91 -0
- volcano_sdk/_generated/models/project_config_function.py +188 -0
- volcano_sdk/_generated/models/project_config_function_openapi_spec_type_0.py +66 -0
- volcano_sdk/_generated/models/project_config_hosted_page.py +68 -0
- volcano_sdk/_generated/models/project_config_hosted_pages.py +172 -0
- volcano_sdk/_generated/models/project_config_missing_resource.py +90 -0
- volcano_sdk/_generated/models/project_config_missing_resource_type.py +10 -0
- volcano_sdk/_generated/models/project_config_o_auth_provider.py +116 -0
- volcano_sdk/_generated/models/project_config_o_auth_provider_provider.py +10 -0
- volcano_sdk/_generated/models/project_config_project.py +85 -0
- volcano_sdk/_generated/models/project_config_realtime.py +87 -0
- volcano_sdk/_generated/models/project_config_scheduler.py +100 -0
- volcano_sdk/_generated/models/project_config_scheduler_payload.py +65 -0
- volcano_sdk/_generated/models/project_config_skipped_resource.py +98 -0
- volcano_sdk/_generated/models/project_config_skipped_resource_type.py +10 -0
- volcano_sdk/_generated/models/project_config_validation_error.py +94 -0
- volcano_sdk/_generated/models/project_config_validation_error_response.py +103 -0
- volcano_sdk/_generated/models/project_config_variable.py +66 -0
- volcano_sdk/_generated/models/project_config_version.py +10 -0
- volcano_sdk/_generated/models/project_deployment.py +231 -0
- volcano_sdk/_generated/models/project_deployment_deploy_source.py +10 -0
- volcano_sdk/_generated/models/project_deployment_operation.py +10 -0
- volcano_sdk/_generated/models/project_deployment_resource.py +103 -0
- volcano_sdk/_generated/models/project_deployment_resource_type.py +10 -0
- volcano_sdk/_generated/models/project_deployment_status.py +10 -0
- volcano_sdk/_generated/models/project_deployment_summary.py +132 -0
- volcano_sdk/_generated/models/project_frontend_custom_domain.py +218 -0
- volcano_sdk/_generated/models/project_frontend_custom_domain_frontend.py +90 -0
- volcano_sdk/_generated/models/project_git_connection.py +122 -0
- volcano_sdk/_generated/models/project_git_connection_summary.py +121 -0
- volcano_sdk/_generated/models/project_git_deploy_settings.py +119 -0
- volcano_sdk/_generated/models/project_health_category.py +10 -0
- volcano_sdk/_generated/models/project_health_check.py +140 -0
- volcano_sdk/_generated/models/project_health_data_status.py +10 -0
- volcano_sdk/_generated/models/project_health_evidence.py +100 -0
- volcano_sdk/_generated/models/project_health_resource.py +102 -0
- volcano_sdk/_generated/models/project_health_resource_type.py +10 -0
- volcano_sdk/_generated/models/project_health_response.py +176 -0
- volcano_sdk/_generated/models/project_health_scope.py +84 -0
- volcano_sdk/_generated/models/project_health_status.py +10 -0
- volcano_sdk/_generated/models/project_health_summary.py +82 -0
- volcano_sdk/_generated/models/project_import_action.py +110 -0
- volcano_sdk/_generated/models/project_import_action_code.py +10 -0
- volcano_sdk/_generated/models/project_import_destination.py +103 -0
- volcano_sdk/_generated/models/project_import_destination_mode.py +10 -0
- volcano_sdk/_generated/models/project_import_disposition.py +10 -0
- volcano_sdk/_generated/models/project_import_finding.py +134 -0
- volcano_sdk/_generated/models/project_import_impact.py +10 -0
- volcano_sdk/_generated/models/project_import_preflight_request.py +123 -0
- volcano_sdk/_generated/models/project_import_readiness.py +10 -0
- volcano_sdk/_generated/models/project_import_report.py +230 -0
- volcano_sdk/_generated/models/project_import_resource.py +109 -0
- volcano_sdk/_generated/models/project_import_resource_kind.py +10 -0
- volcano_sdk/_generated/models/project_import_run.py +190 -0
- volcano_sdk/_generated/models/project_import_run_status.py +10 -0
- volcano_sdk/_generated/models/project_import_start_request.py +126 -0
- volcano_sdk/_generated/models/project_import_summary.py +116 -0
- volcano_sdk/_generated/models/project_import_target.py +10 -0
- volcano_sdk/_generated/models/project_lock_lease.py +75 -0
- volcano_sdk/_generated/models/project_lock_lease_request.py +62 -0
- volcano_sdk/_generated/models/project_lock_state.py +90 -0
- volcano_sdk/_generated/models/project_metrics_data_status.py +10 -0
- volcano_sdk/_generated/models/project_metrics_dimensions.py +83 -0
- volcano_sdk/_generated/models/project_metrics_dimensions_resource_type.py +10 -0
- volcano_sdk/_generated/models/project_metrics_group_by.py +10 -0
- volcano_sdk/_generated/models/project_metrics_metric.py +10 -0
- volcano_sdk/_generated/models/project_metrics_query.py +95 -0
- volcano_sdk/_generated/models/project_metrics_query_request.py +90 -0
- volcano_sdk/_generated/models/project_metrics_query_response.py +122 -0
- volcano_sdk/_generated/models/project_metrics_query_time_range.py +64 -0
- volcano_sdk/_generated/models/project_metrics_query_time_range_window.py +10 -0
- volcano_sdk/_generated/models/project_metrics_result.py +123 -0
- volcano_sdk/_generated/models/project_metrics_unit.py +10 -0
- volcano_sdk/_generated/models/project_metrics_value.py +74 -0
- volcano_sdk/_generated/models/project_metrics_window.py +92 -0
- volcano_sdk/_generated/models/project_plan.py +10 -0
- volcano_sdk/_generated/models/project_source_export.py +152 -0
- volcano_sdk/_generated/models/project_source_export_omission.py +93 -0
- volcano_sdk/_generated/models/project_source_export_skip.py +93 -0
- volcano_sdk/_generated/models/project_source_export_state.py +171 -0
- volcano_sdk/_generated/models/project_source_export_state_mode.py +10 -0
- volcano_sdk/_generated/models/project_status.py +10 -0
- volcano_sdk/_generated/models/project_usage_response.py +145 -0
- volcano_sdk/_generated/models/realtime_config.py +167 -0
- volcano_sdk/_generated/models/realtime_plan_limits.py +115 -0
- volcano_sdk/_generated/models/realtime_stats.py +132 -0
- volcano_sdk/_generated/models/refresh_o_auth_provider_token_provider.py +10 -0
- volcano_sdk/_generated/models/refresh_o_auth_provider_token_response_200.py +96 -0
- volcano_sdk/_generated/models/render_default_managed_auth_page_action.py +10 -0
- volcano_sdk/_generated/models/reset_database_password_response_200.py +106 -0
- volcano_sdk/_generated/models/resolve_function_response.py +96 -0
- volcano_sdk/_generated/models/resource_reference.py +103 -0
- volcano_sdk/_generated/models/resource_reference_type.py +10 -0
- volcano_sdk/_generated/models/schedule_request.py +101 -0
- volcano_sdk/_generated/models/schedule_request_kind.py +10 -0
- volcano_sdk/_generated/models/service_key.py +183 -0
- volcano_sdk/_generated/models/set_project_git_production_branch_request.py +77 -0
- volcano_sdk/_generated/models/start_git_connect_provider.py +10 -0
- volcano_sdk/_generated/models/storage_bucket.py +223 -0
- volcano_sdk/_generated/models/storage_copy_request.py +84 -0
- volcano_sdk/_generated/models/storage_list_response.py +109 -0
- volcano_sdk/_generated/models/storage_move_request.py +84 -0
- volcano_sdk/_generated/models/storage_object.py +243 -0
- volcano_sdk/_generated/models/storage_object_metadata.py +67 -0
- volcano_sdk/_generated/models/storage_object_with_bucket.py +248 -0
- volcano_sdk/_generated/models/storage_object_with_bucket_metadata.py +65 -0
- volcano_sdk/_generated/models/storage_policy.py +166 -0
- volcano_sdk/_generated/models/storage_policy_operation.py +10 -0
- volcano_sdk/_generated/models/storage_stats.py +93 -0
- volcano_sdk/_generated/models/storage_visibility_request.py +76 -0
- volcano_sdk/_generated/models/summarize_project_deployments_resource_type.py +10 -0
- volcano_sdk/_generated/models/test_email_request.py +115 -0
- volcano_sdk/_generated/models/test_email_response.py +76 -0
- volcano_sdk/_generated/models/unban_user_response.py +111 -0
- volcano_sdk/_generated/models/unban_user_response_status.py +10 -0
- volcano_sdk/_generated/models/update_auth_config_request.py +485 -0
- volcano_sdk/_generated/models/update_auth_config_request_allowed_email_domains_mode.py +10 -0
- volcano_sdk/_generated/models/update_auth_hosted_page_request.py +86 -0
- volcano_sdk/_generated/models/update_auth_page_layout_request.py +64 -0
- volcano_sdk/_generated/models/update_auth_page_theme_request.py +66 -0
- volcano_sdk/_generated/models/update_database_branch_request.py +77 -0
- volcano_sdk/_generated/models/update_database_type_request.py +83 -0
- volcano_sdk/_generated/models/update_database_type_request_database_type.py +10 -0
- volcano_sdk/_generated/models/update_email_template_request.py +96 -0
- volcano_sdk/_generated/models/update_email_template_type.py +10 -0
- volcano_sdk/_generated/models/update_function_request.py +149 -0
- volcano_sdk/_generated/models/update_function_request_openapi_spec_type_0.py +66 -0
- volcano_sdk/_generated/models/update_function_scheduler_request.py +147 -0
- volcano_sdk/_generated/models/update_function_scheduler_request_payload.py +65 -0
- volcano_sdk/_generated/models/update_o_auth_config_provider.py +10 -0
- volcano_sdk/_generated/models/update_o_auth_config_request.py +120 -0
- volcano_sdk/_generated/models/update_project_git_deploy_settings_request.py +105 -0
- volcano_sdk/_generated/models/update_project_request.py +107 -0
- volcano_sdk/_generated/models/update_realtime_config_request.py +107 -0
- volcano_sdk/_generated/models/update_storage_bucket_request.py +93 -0
- volcano_sdk/_generated/models/update_variable_request.py +76 -0
- volcano_sdk/_generated/models/upload_project_logo_body.py +101 -0
- volcano_sdk/_generated/models/upload_session_part.py +97 -0
- volcano_sdk/_generated/models/upload_session_status_response.py +234 -0
- volcano_sdk/_generated/models/upload_session_status_response_status.py +10 -0
- volcano_sdk/_generated/models/upload_storage_object_files_body.py +101 -0
- volcano_sdk/_generated/models/upload_storage_object_x_upload_complete.py +10 -0
- volcano_sdk/_generated/models/usage_data_point.py +90 -0
- volcano_sdk/_generated/models/variable.py +216 -0
- volcano_sdk/_generated/models/variable_deploy_source.py +10 -0
- volcano_sdk/_generated/models/variable_status.py +10 -0
- volcano_sdk/_generated/types.py +53 -0
- volcano_sdk/_lock_guard.py +159 -0
- volcano_sdk/_lock_renewer.py +30 -0
- volcano_sdk/_lock_worker.py +104 -0
- volcano_sdk/_realtime_fetch_worker.py +265 -0
- volcano_sdk/_transport.py +1767 -0
- volcano_sdk/auth.py +1128 -0
- volcano_sdk/client.py +288 -0
- volcano_sdk/connection_string.py +70 -0
- volcano_sdk/database.py +281 -0
- volcano_sdk/errors.py +61 -0
- volcano_sdk/functions.py +277 -0
- volcano_sdk/locks.py +227 -0
- volcano_sdk/logs.py +140 -0
- volcano_sdk/models.py +330 -0
- volcano_sdk/realtime.py +1611 -0
- volcano_sdk/storage.py +750 -0
- volcano_sdk_python-0.6.7.dist-info/METADATA +828 -0
- volcano_sdk_python-0.6.7.dist-info/RECORD +778 -0
- volcano_sdk_python-0.6.7.dist-info/WHEEL +4 -0
- volcano_sdk_python-0.6.7.dist-info/licenses/LICENSE +203 -0
volcano_sdk/realtime.py
ADDED
|
@@ -0,0 +1,1611 @@
|
|
|
1
|
+
"""Realtime broadcast facade."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import importlib
|
|
7
|
+
import inspect
|
|
8
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
9
|
+
from dataclasses import dataclass, field, replace
|
|
10
|
+
from types import MappingProxyType
|
|
11
|
+
from typing import TYPE_CHECKING, Any, Literal, Protocol, TypeAlias, cast
|
|
12
|
+
from urllib.parse import quote, urlsplit, urlunsplit
|
|
13
|
+
|
|
14
|
+
from ._realtime_fetch_worker import (
|
|
15
|
+
PostgresFetchJob,
|
|
16
|
+
PostgresFetchOutcome,
|
|
17
|
+
PostgresFetchWorker,
|
|
18
|
+
)
|
|
19
|
+
from ._transport import (
|
|
20
|
+
AsyncDatabaseSelectTransport,
|
|
21
|
+
Transport,
|
|
22
|
+
invoke_async,
|
|
23
|
+
response_payload,
|
|
24
|
+
)
|
|
25
|
+
from .models import JSONValue, _freeze_json
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from .models import Session
|
|
29
|
+
|
|
30
|
+
MessageCallback = Callable[[Any], Any]
|
|
31
|
+
RealtimeCallback = Callable[[Any], Any]
|
|
32
|
+
UnsubscribeCallback = Callable[[], None]
|
|
33
|
+
ChannelType: TypeAlias = Literal["broadcast", "presence", "postgres"]
|
|
34
|
+
PostgresEvent: TypeAlias = Literal["INSERT", "UPDATE", "DELETE"]
|
|
35
|
+
PostgresListenerEvent: TypeAlias = Literal["INSERT", "UPDATE", "DELETE", "*"]
|
|
36
|
+
PostgresChangeCallback = Callable[["PostgresChange"], Any]
|
|
37
|
+
SUPPORTED_CHANNEL_TYPES = frozenset({"broadcast", "presence", "postgres"})
|
|
38
|
+
POSTGRES_EVENTS = frozenset({"INSERT", "UPDATE", "DELETE"})
|
|
39
|
+
POSTGRES_CHANNEL_SEGMENTS = 3
|
|
40
|
+
POSTGRES_PUBLICATION_SEGMENTS = 5
|
|
41
|
+
CENTRIFUGE_ERROR = cast(
|
|
42
|
+
"type[Exception]",
|
|
43
|
+
importlib.import_module("centrifuge").CentrifugeError,
|
|
44
|
+
)
|
|
45
|
+
CALLBACK_QUEUE_LIMIT = 128
|
|
46
|
+
POSTGRES_QUEUE_LIMIT = 128
|
|
47
|
+
POSTGRES_BATCH_WINDOW_MS = 20
|
|
48
|
+
POSTGRES_MAX_BATCH_SIZE = 50
|
|
49
|
+
NO_PENDING_CALLBACK = object()
|
|
50
|
+
CALLBACK_QUEUE_FULL_MESSAGE = (
|
|
51
|
+
"Volcano realtime callback queue is full; publication dropped"
|
|
52
|
+
)
|
|
53
|
+
CHANNEL_NOT_SUBSCRIBED = "Channel must be subscribed before sending"
|
|
54
|
+
CHANNEL_REMOVAL_IN_PROGRESS = "realtime channel removal is in progress"
|
|
55
|
+
CHANNEL_NOT_MANAGED = "realtime channel is no longer managed"
|
|
56
|
+
PRESENCE_ONLY = "operation is only available for presence channels"
|
|
57
|
+
BROADCAST_ONLY = "send is only available for broadcast channels"
|
|
58
|
+
POSTGRES_ONLY = "operation is only available for postgres channels"
|
|
59
|
+
CALLBACK_NOT_CALLABLE = "callback must be callable"
|
|
60
|
+
SUBSCRIPTION_REGISTRY_UNAVAILABLE = (
|
|
61
|
+
"centrifuge client subscription registry is unavailable"
|
|
62
|
+
)
|
|
63
|
+
NO_ACTIVE_SESSION = "No active session"
|
|
64
|
+
CONNECTION_SESSION_UNAVAILABLE = "Realtime connection has no session binding"
|
|
65
|
+
CONNECTION_SESSION_CHANGED = "Realtime connection session changed"
|
|
66
|
+
POSTGRES_FETCH_FAILED_MESSAGE = "Volcano realtime Postgres row fetch failed"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _empty_presence_data() -> Mapping[str, JSONValue]:
|
|
70
|
+
return MappingProxyType({})
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _consume_presence_result(task: asyncio.Task[Any]) -> None:
|
|
74
|
+
if not task.cancelled():
|
|
75
|
+
task.exception()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _validate_channel_type(channel_type: str) -> ChannelType:
|
|
79
|
+
if channel_type not in SUPPORTED_CHANNEL_TYPES:
|
|
80
|
+
message = f"unsupported realtime channel type: {channel_type}"
|
|
81
|
+
raise ValueError(message)
|
|
82
|
+
return cast("ChannelType", channel_type)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _postgres_route_matches(candidate: str, publication: str) -> bool:
|
|
86
|
+
candidate_parts = candidate.split(":")
|
|
87
|
+
publication_parts = publication.split(":")
|
|
88
|
+
return (
|
|
89
|
+
len(candidate_parts) == POSTGRES_CHANNEL_SEGMENTS
|
|
90
|
+
and candidate_parts[0] == "postgres"
|
|
91
|
+
and len(publication_parts) == POSTGRES_PUBLICATION_SEGMENTS
|
|
92
|
+
and publication_parts[1:4] == candidate_parts
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True, slots=True)
|
|
97
|
+
class RealtimeConnectContext:
|
|
98
|
+
"""Details reported after a realtime transport connects."""
|
|
99
|
+
|
|
100
|
+
client: str | None = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass(frozen=True, slots=True)
|
|
104
|
+
class RealtimeDisconnectContext:
|
|
105
|
+
"""Details reported after a realtime transport disconnects."""
|
|
106
|
+
|
|
107
|
+
code: int | None = None
|
|
108
|
+
reason: str | None = None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True, slots=True)
|
|
112
|
+
class RealtimeErrorContext:
|
|
113
|
+
"""Details reported when the realtime transport emits an error."""
|
|
114
|
+
|
|
115
|
+
code: int | None = None
|
|
116
|
+
message: str | None = None
|
|
117
|
+
error: Exception | None = None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass(frozen=True, slots=True)
|
|
121
|
+
class RealtimePresenceInfo:
|
|
122
|
+
"""Immutable identity and metadata for one present realtime client."""
|
|
123
|
+
|
|
124
|
+
client: str
|
|
125
|
+
user: str | None = None
|
|
126
|
+
data: Mapping[str, JSONValue] = field(
|
|
127
|
+
default_factory=_empty_presence_data,
|
|
128
|
+
hash=False,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def __post_init__(self) -> None:
|
|
132
|
+
"""Defensively freeze nested connection metadata."""
|
|
133
|
+
frozen = _freeze_json(cast("JSONValue", dict(self.data)))
|
|
134
|
+
object.__setattr__(self, "data", cast("Mapping[str, JSONValue]", frozen))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass(frozen=True, slots=True)
|
|
138
|
+
class PostgresChange:
|
|
139
|
+
"""Immutable RLS-scoped Postgres row-change notification."""
|
|
140
|
+
|
|
141
|
+
type: PostgresEvent
|
|
142
|
+
schema: str
|
|
143
|
+
table: str
|
|
144
|
+
record: Mapping[str, JSONValue] | None = field(default=None, hash=False)
|
|
145
|
+
old_record: Mapping[str, JSONValue] | None = field(default=None, hash=False)
|
|
146
|
+
columns: tuple[str, ...] | None = None
|
|
147
|
+
timestamp: str = ""
|
|
148
|
+
id: JSONValue = field(default=None, hash=False)
|
|
149
|
+
mode: Literal["lightweight"] | None = None
|
|
150
|
+
|
|
151
|
+
def __post_init__(self) -> None:
|
|
152
|
+
"""Defensively freeze nested row and identifier values."""
|
|
153
|
+
if self.record is not None:
|
|
154
|
+
frozen_record = _freeze_json(cast("JSONValue", dict(self.record)))
|
|
155
|
+
object.__setattr__(self, "record", frozen_record)
|
|
156
|
+
if self.old_record is not None:
|
|
157
|
+
frozen_old_record = _freeze_json(cast("JSONValue", dict(self.old_record)))
|
|
158
|
+
object.__setattr__(self, "old_record", frozen_old_record)
|
|
159
|
+
object.__setattr__(self, "id", _freeze_json(self.id))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass(frozen=True, slots=True)
|
|
163
|
+
class _PostgresFetchRequest:
|
|
164
|
+
database_name: str
|
|
165
|
+
access_token: str
|
|
166
|
+
table: str
|
|
167
|
+
row_id: JSONValue
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass(frozen=True, slots=True)
|
|
171
|
+
class _PostgresFetchConfig:
|
|
172
|
+
enabled: bool
|
|
173
|
+
batch_window_ms: int
|
|
174
|
+
max_batch_size: int
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def batch_window_seconds(self) -> float:
|
|
178
|
+
return self.batch_window_ms / 1_000
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _postgres_fetch_config(
|
|
182
|
+
*,
|
|
183
|
+
auto_fetch: bool,
|
|
184
|
+
fetch_batch_window_ms: object,
|
|
185
|
+
fetch_max_batch_size: object,
|
|
186
|
+
) -> _PostgresFetchConfig:
|
|
187
|
+
if type(fetch_batch_window_ms) is not int or fetch_batch_window_ms <= 0:
|
|
188
|
+
message = "fetch_batch_window_ms must be a positive integer"
|
|
189
|
+
raise ValueError(message)
|
|
190
|
+
if (
|
|
191
|
+
type(fetch_max_batch_size) is not int
|
|
192
|
+
or not 1 <= fetch_max_batch_size <= POSTGRES_QUEUE_LIMIT
|
|
193
|
+
):
|
|
194
|
+
message = (
|
|
195
|
+
f"fetch_max_batch_size must be an integer between 1 and "
|
|
196
|
+
f"{POSTGRES_QUEUE_LIMIT}"
|
|
197
|
+
)
|
|
198
|
+
raise ValueError(message)
|
|
199
|
+
return _PostgresFetchConfig(
|
|
200
|
+
enabled=auto_fetch,
|
|
201
|
+
batch_window_ms=fetch_batch_window_ms,
|
|
202
|
+
max_batch_size=fetch_max_batch_size,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass(frozen=True, slots=True)
|
|
207
|
+
class _PostgresDeliveryIdentity:
|
|
208
|
+
session_lineage: int
|
|
209
|
+
subscription_epoch: int
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass(frozen=True, slots=True)
|
|
213
|
+
class _PostgresDelivery:
|
|
214
|
+
change: PostgresChange
|
|
215
|
+
identity: _PostgresDeliveryIdentity
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@dataclass(frozen=True, slots=True)
|
|
219
|
+
class _CallbackDelivery:
|
|
220
|
+
event: str
|
|
221
|
+
data: Any
|
|
222
|
+
postgres_identity: _PostgresDeliveryIdentity | None = None
|
|
223
|
+
delivery_epoch: int | None = None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _postgres_change(data: Any) -> PostgresChange | None:
|
|
227
|
+
if not isinstance(data, Mapping):
|
|
228
|
+
return None
|
|
229
|
+
typed_data = cast("Mapping[str, object]", data)
|
|
230
|
+
event = typed_data.get("type")
|
|
231
|
+
schema = typed_data.get("schema")
|
|
232
|
+
table = typed_data.get("table")
|
|
233
|
+
timestamp = typed_data.get("timestamp")
|
|
234
|
+
mode = typed_data.get("mode")
|
|
235
|
+
record = typed_data.get("record")
|
|
236
|
+
old_record = typed_data.get("old_record")
|
|
237
|
+
raw_columns = typed_data.get("columns")
|
|
238
|
+
if (
|
|
239
|
+
event not in POSTGRES_EVENTS
|
|
240
|
+
or not isinstance(schema, str)
|
|
241
|
+
or not isinstance(table, str)
|
|
242
|
+
or not isinstance(timestamp, str)
|
|
243
|
+
or (record is not None and not isinstance(record, Mapping))
|
|
244
|
+
or (old_record is not None and not isinstance(old_record, Mapping))
|
|
245
|
+
):
|
|
246
|
+
return None
|
|
247
|
+
typed_mode: Literal["lightweight"] | None
|
|
248
|
+
if mode is None:
|
|
249
|
+
typed_mode = None
|
|
250
|
+
elif mode == "lightweight":
|
|
251
|
+
typed_mode = "lightweight"
|
|
252
|
+
else:
|
|
253
|
+
return None
|
|
254
|
+
if raw_columns is None:
|
|
255
|
+
columns = None
|
|
256
|
+
elif isinstance(raw_columns, (list, tuple)):
|
|
257
|
+
untyped_columns = cast("list[object] | tuple[object, ...]", raw_columns)
|
|
258
|
+
if not all(isinstance(column, str) for column in untyped_columns):
|
|
259
|
+
return None
|
|
260
|
+
columns = tuple(cast("list[str] | tuple[str, ...]", raw_columns))
|
|
261
|
+
else:
|
|
262
|
+
return None
|
|
263
|
+
return PostgresChange(
|
|
264
|
+
type=cast("PostgresEvent", event),
|
|
265
|
+
schema=schema,
|
|
266
|
+
table=table,
|
|
267
|
+
record=cast("Mapping[str, JSONValue] | None", record),
|
|
268
|
+
old_record=cast("Mapping[str, JSONValue] | None", old_record),
|
|
269
|
+
columns=columns,
|
|
270
|
+
timestamp=timestamp,
|
|
271
|
+
id=cast("JSONValue", typed_data.get("id")),
|
|
272
|
+
mode=typed_mode,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class RealtimeContext(Protocol):
|
|
277
|
+
"""Client capabilities required by realtime connections."""
|
|
278
|
+
|
|
279
|
+
_transport: Transport
|
|
280
|
+
|
|
281
|
+
def _anon_token(self) -> str: ...
|
|
282
|
+
|
|
283
|
+
def _session_token(self) -> str: ...
|
|
284
|
+
|
|
285
|
+
def _capture_session_binding(self) -> tuple[int, int, Session | None]: ...
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class CentrifugeSubscription(Protocol):
|
|
289
|
+
"""Centrifuge subscription operations used by the SDK."""
|
|
290
|
+
|
|
291
|
+
async def subscribe(self) -> None:
|
|
292
|
+
"""Subscribe to the remote channel."""
|
|
293
|
+
...
|
|
294
|
+
|
|
295
|
+
async def ready(self) -> None:
|
|
296
|
+
"""Wait for acknowledgement using the client request timeout."""
|
|
297
|
+
...
|
|
298
|
+
|
|
299
|
+
async def publish(self, data: Any) -> Any:
|
|
300
|
+
"""Publish a payload to the remote channel."""
|
|
301
|
+
...
|
|
302
|
+
|
|
303
|
+
async def unsubscribe(self) -> None:
|
|
304
|
+
"""Unsubscribe from the remote channel."""
|
|
305
|
+
...
|
|
306
|
+
|
|
307
|
+
async def presence(self) -> Any:
|
|
308
|
+
"""Return the clients currently present on the channel."""
|
|
309
|
+
...
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
async def _unsubscribe_native(subscription: CentrifugeSubscription) -> None:
|
|
313
|
+
# Finish the native stop before releasing the connection lock on cancellation.
|
|
314
|
+
task = asyncio.create_task(subscription.unsubscribe())
|
|
315
|
+
cancelled: asyncio.CancelledError | None = None
|
|
316
|
+
while not task.done():
|
|
317
|
+
try:
|
|
318
|
+
await asyncio.shield(task)
|
|
319
|
+
except asyncio.CancelledError as error:
|
|
320
|
+
cancelled = error
|
|
321
|
+
except CENTRIFUGE_ERROR:
|
|
322
|
+
if cancelled is None:
|
|
323
|
+
raise
|
|
324
|
+
if cancelled is not None:
|
|
325
|
+
if not task.cancelled():
|
|
326
|
+
task.exception()
|
|
327
|
+
raise cancelled
|
|
328
|
+
task.result()
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class CentrifugeConnection(Protocol):
|
|
332
|
+
"""Centrifuge connection operations used by the SDK."""
|
|
333
|
+
|
|
334
|
+
state: Any
|
|
335
|
+
|
|
336
|
+
async def connect(self) -> None:
|
|
337
|
+
"""Open the remote connection."""
|
|
338
|
+
...
|
|
339
|
+
|
|
340
|
+
async def disconnect(self) -> None:
|
|
341
|
+
"""Close the remote connection."""
|
|
342
|
+
...
|
|
343
|
+
|
|
344
|
+
def new_subscription(
|
|
345
|
+
self,
|
|
346
|
+
name: str,
|
|
347
|
+
*,
|
|
348
|
+
events: Any,
|
|
349
|
+
join_leave: bool = False,
|
|
350
|
+
recoverable: bool = False,
|
|
351
|
+
) -> CentrifugeSubscription:
|
|
352
|
+
"""Create a subscription for a remote channel."""
|
|
353
|
+
...
|
|
354
|
+
|
|
355
|
+
def remove_subscription(self, subscription: Any) -> None:
|
|
356
|
+
"""Remove an unsubscribed channel from the connection registry."""
|
|
357
|
+
...
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class CentrifugeFactory(Protocol):
|
|
361
|
+
"""Construct a typed Centrifuge connection."""
|
|
362
|
+
|
|
363
|
+
def __call__(
|
|
364
|
+
self,
|
|
365
|
+
address: str,
|
|
366
|
+
*,
|
|
367
|
+
events: Any,
|
|
368
|
+
token: str,
|
|
369
|
+
get_token: Callable[[], Awaitable[str]],
|
|
370
|
+
) -> CentrifugeConnection:
|
|
371
|
+
"""Construct a Centrifuge connection."""
|
|
372
|
+
...
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class CentrifugeConstructor(Protocol):
|
|
376
|
+
"""Describe the dynamically imported Centrifuge client constructor."""
|
|
377
|
+
|
|
378
|
+
def __call__(
|
|
379
|
+
self,
|
|
380
|
+
address: str,
|
|
381
|
+
*,
|
|
382
|
+
events: Any,
|
|
383
|
+
token: str,
|
|
384
|
+
get_token: Callable[[], Awaitable[str]],
|
|
385
|
+
) -> object:
|
|
386
|
+
"""Construct the dynamically imported Centrifuge client."""
|
|
387
|
+
...
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class Publication(Protocol):
|
|
391
|
+
"""Publication payload received from Centrifuge."""
|
|
392
|
+
|
|
393
|
+
data: Any
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class PublicationContext(Protocol):
|
|
397
|
+
"""Centrifuge callback context containing a publication."""
|
|
398
|
+
|
|
399
|
+
pub: Publication
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _centrifuge_client(
|
|
403
|
+
address: str,
|
|
404
|
+
*,
|
|
405
|
+
events: Any,
|
|
406
|
+
token: str,
|
|
407
|
+
get_token: Callable[[], Awaitable[str]],
|
|
408
|
+
) -> CentrifugeConnection:
|
|
409
|
+
module = importlib.import_module("centrifuge")
|
|
410
|
+
constructor = cast("CentrifugeConstructor", module.Client)
|
|
411
|
+
return cast(
|
|
412
|
+
"CentrifugeConnection",
|
|
413
|
+
constructor(address, events=events, token=token, get_token=get_token),
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class _ProjectAwareSubscriptions(dict[str, Any]):
|
|
418
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
419
|
+
subscription = super().get(key)
|
|
420
|
+
if subscription is not None:
|
|
421
|
+
return subscription
|
|
422
|
+
matches = [
|
|
423
|
+
(channel, candidate)
|
|
424
|
+
for channel, candidate in self.items()
|
|
425
|
+
if key.endswith(f":{channel}") or _postgres_route_matches(channel, key)
|
|
426
|
+
]
|
|
427
|
+
return max(matches, key=lambda match: len(match[0]))[1] if matches else default
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class _VolcanoCentrifugeConnection:
|
|
431
|
+
def __init__(self, connection: CentrifugeConnection) -> None:
|
|
432
|
+
self._connection = connection
|
|
433
|
+
state = vars(connection)
|
|
434
|
+
subscriptions = state.get("_subs")
|
|
435
|
+
if not isinstance(subscriptions, dict):
|
|
436
|
+
raise TypeError(SUBSCRIPTION_REGISTRY_UNAVAILABLE)
|
|
437
|
+
typed_subscriptions = cast("dict[str, Any]", subscriptions)
|
|
438
|
+
state["_subs"] = _ProjectAwareSubscriptions(typed_subscriptions)
|
|
439
|
+
|
|
440
|
+
async def connect(self) -> None:
|
|
441
|
+
await self._connection.connect()
|
|
442
|
+
|
|
443
|
+
async def disconnect(self) -> None:
|
|
444
|
+
await self._connection.disconnect()
|
|
445
|
+
|
|
446
|
+
@property
|
|
447
|
+
def is_connected(self) -> bool:
|
|
448
|
+
state = self._connection.state
|
|
449
|
+
return getattr(state, "value", None) == "connected"
|
|
450
|
+
|
|
451
|
+
def new_subscription(
|
|
452
|
+
self,
|
|
453
|
+
name: str,
|
|
454
|
+
*,
|
|
455
|
+
events: Any,
|
|
456
|
+
join_leave: bool = False,
|
|
457
|
+
recoverable: bool = False,
|
|
458
|
+
) -> CentrifugeSubscription:
|
|
459
|
+
return self._connection.new_subscription(
|
|
460
|
+
name,
|
|
461
|
+
events=events,
|
|
462
|
+
join_leave=join_leave,
|
|
463
|
+
recoverable=recoverable,
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
def remove_subscription(self, subscription: CentrifugeSubscription) -> None:
|
|
467
|
+
self._connection.remove_subscription(subscription)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
class _ChannelEvents:
|
|
471
|
+
def __init__(self, channel: Channel) -> None:
|
|
472
|
+
self._channel = channel
|
|
473
|
+
|
|
474
|
+
def _is_current(self) -> bool:
|
|
475
|
+
return self._channel._subscription_events is self
|
|
476
|
+
|
|
477
|
+
async def on_publication(self, ctx: PublicationContext) -> None:
|
|
478
|
+
if (
|
|
479
|
+
not self._is_current()
|
|
480
|
+
or self._channel._paused
|
|
481
|
+
or not self._channel._subscribed
|
|
482
|
+
):
|
|
483
|
+
return
|
|
484
|
+
if self._channel._type == "postgres":
|
|
485
|
+
await self._channel._receive_postgres_change(ctx.pub.data)
|
|
486
|
+
return
|
|
487
|
+
await self._channel._emit("message", ctx.pub.data)
|
|
488
|
+
|
|
489
|
+
async def on_subscribing(self, ctx: Any) -> None:
|
|
490
|
+
del ctx
|
|
491
|
+
if self._is_current():
|
|
492
|
+
await self._channel._transport_lost()
|
|
493
|
+
|
|
494
|
+
async def on_subscribed(self, ctx: Any) -> None:
|
|
495
|
+
del ctx
|
|
496
|
+
if not self._is_current() or self._channel._paused:
|
|
497
|
+
return
|
|
498
|
+
self._channel._subscribed = True
|
|
499
|
+
await self._channel._begin_postgres_epoch()
|
|
500
|
+
if self._channel._type == "presence":
|
|
501
|
+
self._channel._schedule_presence_sync()
|
|
502
|
+
|
|
503
|
+
async def on_unsubscribed(self, ctx: Any) -> None:
|
|
504
|
+
del ctx
|
|
505
|
+
if self._is_current():
|
|
506
|
+
await self._channel._transport_lost()
|
|
507
|
+
|
|
508
|
+
async def on_join(self, ctx: Any) -> None:
|
|
509
|
+
if self._is_current():
|
|
510
|
+
await self._channel._presence_join(getattr(ctx, "info", None))
|
|
511
|
+
|
|
512
|
+
async def on_leave(self, ctx: Any) -> None:
|
|
513
|
+
if self._is_current():
|
|
514
|
+
await self._channel._presence_leave(getattr(ctx, "info", None))
|
|
515
|
+
|
|
516
|
+
async def on_error(self, ctx: Any) -> None:
|
|
517
|
+
del ctx
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
class _ClientEvents:
|
|
521
|
+
def __init__(self, realtime: Realtime) -> None:
|
|
522
|
+
self._realtime = realtime
|
|
523
|
+
|
|
524
|
+
async def on_connecting(self, ctx: Any) -> None:
|
|
525
|
+
del ctx
|
|
526
|
+
|
|
527
|
+
async def on_connected(self, ctx: Any) -> None:
|
|
528
|
+
self._realtime._enqueue_connection_callbacks(
|
|
529
|
+
"connect",
|
|
530
|
+
RealtimeConnectContext(client=getattr(ctx, "client", None)),
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
async def on_disconnected(self, ctx: Any) -> None:
|
|
534
|
+
self._realtime._enqueue_connection_callbacks(
|
|
535
|
+
"disconnect",
|
|
536
|
+
RealtimeDisconnectContext(
|
|
537
|
+
code=getattr(ctx, "code", None),
|
|
538
|
+
reason=getattr(ctx, "reason", None),
|
|
539
|
+
),
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
async def on_error(self, ctx: Any) -> None:
|
|
543
|
+
error = getattr(ctx, "error", None)
|
|
544
|
+
self._realtime._enqueue_connection_callbacks(
|
|
545
|
+
"error",
|
|
546
|
+
RealtimeErrorContext(
|
|
547
|
+
code=getattr(ctx, "code", None),
|
|
548
|
+
message=str(error) if error is not None else None,
|
|
549
|
+
error=error if isinstance(error, Exception) else None,
|
|
550
|
+
),
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
async def on_subscribed(self, ctx: Any) -> None:
|
|
554
|
+
del ctx
|
|
555
|
+
|
|
556
|
+
async def on_subscribing(self, ctx: Any) -> None:
|
|
557
|
+
del ctx
|
|
558
|
+
|
|
559
|
+
async def on_unsubscribed(self, ctx: Any) -> None:
|
|
560
|
+
del ctx
|
|
561
|
+
|
|
562
|
+
async def on_publication(self, ctx: Any) -> None:
|
|
563
|
+
del ctx
|
|
564
|
+
|
|
565
|
+
async def on_join(self, ctx: Any) -> None:
|
|
566
|
+
del ctx
|
|
567
|
+
|
|
568
|
+
async def on_leave(self, ctx: Any) -> None:
|
|
569
|
+
del ctx
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
class Channel:
|
|
573
|
+
"""Realtime broadcast, presence, or Postgres channel."""
|
|
574
|
+
|
|
575
|
+
def __init__(
|
|
576
|
+
self,
|
|
577
|
+
realtime: Realtime,
|
|
578
|
+
name: str,
|
|
579
|
+
channel_type: ChannelType,
|
|
580
|
+
*,
|
|
581
|
+
fetch_config: _PostgresFetchConfig,
|
|
582
|
+
) -> None:
|
|
583
|
+
"""Create a channel managed by a realtime facade."""
|
|
584
|
+
self._realtime = realtime
|
|
585
|
+
self._name = name
|
|
586
|
+
self._type = channel_type
|
|
587
|
+
self._fetch_config = fetch_config
|
|
588
|
+
self._callbacks: dict[str, list[MessageCallback]] = {}
|
|
589
|
+
self._presence_state: dict[str, RealtimePresenceInfo] = {}
|
|
590
|
+
self._presence_events: list[tuple[str, RealtimePresenceInfo]] = []
|
|
591
|
+
self._presence_syncing = False
|
|
592
|
+
self._tracked_state: Mapping[str, JSONValue] = MappingProxyType({})
|
|
593
|
+
self._subscribe_lock = asyncio.Lock()
|
|
594
|
+
self._subscribe_generation = 0
|
|
595
|
+
self._readiness_task: asyncio.Task[None] | None = None
|
|
596
|
+
self._subscription: CentrifugeSubscription | None = None
|
|
597
|
+
self._subscription_events: _ChannelEvents | None = None
|
|
598
|
+
self._subscribed = False
|
|
599
|
+
self._paused = True
|
|
600
|
+
self._delivery_epoch = 0
|
|
601
|
+
self._presence_epoch = 0
|
|
602
|
+
self._presence_lock = asyncio.Lock()
|
|
603
|
+
self._presence_sync_task: asyncio.Task[None] | None = None
|
|
604
|
+
self._presence_sync_pending = False
|
|
605
|
+
self._callback_queue: asyncio.Queue[_CallbackDelivery] = asyncio.Queue(
|
|
606
|
+
maxsize=CALLBACK_QUEUE_LIMIT
|
|
607
|
+
)
|
|
608
|
+
self._callback_task: asyncio.Task[None] | None = None
|
|
609
|
+
self._pending_presence_sync: Any = NO_PENDING_CALLBACK
|
|
610
|
+
self._postgres_epoch = 0
|
|
611
|
+
self._postgres_session_lineage = 0
|
|
612
|
+
self._postgres_lock = asyncio.Lock()
|
|
613
|
+
self._postgres_worker: PostgresFetchWorker[_PostgresDelivery] | None = None
|
|
614
|
+
self._postgres_filters: dict[
|
|
615
|
+
int,
|
|
616
|
+
tuple[PostgresListenerEvent, str, str],
|
|
617
|
+
] = {}
|
|
618
|
+
|
|
619
|
+
@property
|
|
620
|
+
def name(self) -> str:
|
|
621
|
+
"""Return the canonical channel name sent to realtime."""
|
|
622
|
+
return self._name
|
|
623
|
+
|
|
624
|
+
def on(self, event: str, callback: MessageCallback) -> Channel:
|
|
625
|
+
"""Register a callback for messages or presence events."""
|
|
626
|
+
allowed_events = {
|
|
627
|
+
"broadcast": {"message"},
|
|
628
|
+
"presence": {"message", "join", "leave", "presence_sync"},
|
|
629
|
+
"postgres": {"*"},
|
|
630
|
+
}[self._type]
|
|
631
|
+
if event not in allowed_events:
|
|
632
|
+
message = f"unsupported realtime event: {event}"
|
|
633
|
+
raise ValueError(message)
|
|
634
|
+
self._callbacks.setdefault(event, []).append(callback)
|
|
635
|
+
return self
|
|
636
|
+
|
|
637
|
+
def on_postgres_changes(
|
|
638
|
+
self,
|
|
639
|
+
event: PostgresListenerEvent,
|
|
640
|
+
*,
|
|
641
|
+
schema: str,
|
|
642
|
+
table: str,
|
|
643
|
+
callback: PostgresChangeCallback,
|
|
644
|
+
) -> UnsubscribeCallback:
|
|
645
|
+
"""Observe Postgres changes filtered by event, schema, and table."""
|
|
646
|
+
if self._type != "postgres":
|
|
647
|
+
raise ValueError(POSTGRES_ONLY)
|
|
648
|
+
if event not in {*POSTGRES_EVENTS, "*"}:
|
|
649
|
+
message = f"unsupported Postgres change event: {event}"
|
|
650
|
+
raise ValueError(message)
|
|
651
|
+
|
|
652
|
+
def filtered(change: PostgresChange) -> Any:
|
|
653
|
+
if change.schema != schema or change.table != table:
|
|
654
|
+
return None
|
|
655
|
+
if event not in ("*", change.type):
|
|
656
|
+
return None
|
|
657
|
+
return callback(change)
|
|
658
|
+
|
|
659
|
+
self._callbacks.setdefault("*", []).append(filtered)
|
|
660
|
+
self._postgres_filters[id(filtered)] = (event, schema, table)
|
|
661
|
+
|
|
662
|
+
def unsubscribe() -> None:
|
|
663
|
+
callbacks = self._callbacks.get("*", [])
|
|
664
|
+
if filtered in callbacks:
|
|
665
|
+
callbacks.remove(filtered)
|
|
666
|
+
self._postgres_filters.pop(id(filtered), None)
|
|
667
|
+
|
|
668
|
+
return unsubscribe
|
|
669
|
+
|
|
670
|
+
def on_presence_sync(self, callback: MessageCallback) -> UnsubscribeCallback:
|
|
671
|
+
"""Observe immutable snapshots of a presence channel's current state."""
|
|
672
|
+
self._ensure_presence()
|
|
673
|
+
self._callbacks.setdefault("presence_sync", []).append(callback)
|
|
674
|
+
|
|
675
|
+
def unsubscribe() -> None:
|
|
676
|
+
callbacks = self._callbacks.get("presence_sync", [])
|
|
677
|
+
if callback in callbacks:
|
|
678
|
+
callbacks.remove(callback)
|
|
679
|
+
|
|
680
|
+
return unsubscribe
|
|
681
|
+
|
|
682
|
+
async def track(self, state: Mapping[str, JSONValue] | None = None) -> None:
|
|
683
|
+
"""Store local presence state while server identity remains authoritative."""
|
|
684
|
+
self._ensure_presence()
|
|
685
|
+
if not self._subscribed:
|
|
686
|
+
raise RuntimeError(CHANNEL_NOT_SUBSCRIBED)
|
|
687
|
+
frozen = _freeze_json(cast("JSONValue", dict(state or {})))
|
|
688
|
+
self._tracked_state = cast("Mapping[str, JSONValue]", frozen)
|
|
689
|
+
|
|
690
|
+
def get_presence_state(self) -> Mapping[str, RealtimePresenceInfo]:
|
|
691
|
+
"""Return an immutable snapshot of the clients currently present."""
|
|
692
|
+
self._ensure_presence()
|
|
693
|
+
return MappingProxyType(dict(self._presence_state))
|
|
694
|
+
|
|
695
|
+
@property
|
|
696
|
+
def tracked_state(self) -> Mapping[str, JSONValue]:
|
|
697
|
+
"""Return an immutable snapshot of this client's local presence state."""
|
|
698
|
+
self._ensure_presence()
|
|
699
|
+
return MappingProxyType(dict(self._tracked_state))
|
|
700
|
+
|
|
701
|
+
def _ensure_presence(self) -> None:
|
|
702
|
+
if self._type != "presence":
|
|
703
|
+
raise ValueError(PRESENCE_ONLY)
|
|
704
|
+
|
|
705
|
+
def _capture_postgres_delivery_identity(self) -> _PostgresDeliveryIdentity:
|
|
706
|
+
return _PostgresDeliveryIdentity(
|
|
707
|
+
session_lineage=self._postgres_session_lineage,
|
|
708
|
+
subscription_epoch=self._postgres_epoch,
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
async def _begin_postgres_epoch(self) -> None:
|
|
712
|
+
if self._type != "postgres":
|
|
713
|
+
return
|
|
714
|
+
await self._stop_postgres_worker()
|
|
715
|
+
self._postgres_epoch += 1
|
|
716
|
+
self._postgres_session_lineage = self._realtime._connection_lineage()
|
|
717
|
+
|
|
718
|
+
async def _end_postgres_epoch(self) -> None:
|
|
719
|
+
if self._type != "postgres":
|
|
720
|
+
return
|
|
721
|
+
self._postgres_epoch += 1
|
|
722
|
+
await self._stop_postgres_worker()
|
|
723
|
+
|
|
724
|
+
async def _stop_postgres_worker(self) -> None:
|
|
725
|
+
async with self._postgres_lock:
|
|
726
|
+
worker = self._postgres_worker
|
|
727
|
+
self._postgres_worker = None
|
|
728
|
+
if worker is not None:
|
|
729
|
+
await worker.abort()
|
|
730
|
+
|
|
731
|
+
def _postgres_delivery_is_current(
|
|
732
|
+
self,
|
|
733
|
+
identity: _PostgresDeliveryIdentity,
|
|
734
|
+
) -> bool:
|
|
735
|
+
_generation, lineage, _session = (
|
|
736
|
+
self._realtime._client_context._capture_session_binding()
|
|
737
|
+
)
|
|
738
|
+
return (
|
|
739
|
+
self._subscribed
|
|
740
|
+
and identity.subscription_epoch == self._postgres_epoch
|
|
741
|
+
and identity.session_lineage == lineage
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
def _has_postgres_listener(self, change: PostgresChange) -> bool:
|
|
745
|
+
for callback in self._callbacks.get("*", []):
|
|
746
|
+
listener_filter = self._postgres_filters.get(id(callback))
|
|
747
|
+
if listener_filter is None:
|
|
748
|
+
return True
|
|
749
|
+
event, schema, table = listener_filter
|
|
750
|
+
if (
|
|
751
|
+
event in ("*", change.type)
|
|
752
|
+
and schema == change.schema
|
|
753
|
+
and table == change.table
|
|
754
|
+
):
|
|
755
|
+
return True
|
|
756
|
+
return False
|
|
757
|
+
|
|
758
|
+
def _postgres_fetch_request(
|
|
759
|
+
self,
|
|
760
|
+
change: PostgresChange,
|
|
761
|
+
) -> _PostgresFetchRequest | None:
|
|
762
|
+
database_name = self._realtime.database_name
|
|
763
|
+
if (
|
|
764
|
+
not self._fetch_config.enabled
|
|
765
|
+
or change.mode != "lightweight"
|
|
766
|
+
or change.type == "DELETE"
|
|
767
|
+
or change.schema != "public"
|
|
768
|
+
or change.id is None
|
|
769
|
+
or database_name is None
|
|
770
|
+
):
|
|
771
|
+
return None
|
|
772
|
+
return _PostgresFetchRequest(
|
|
773
|
+
database_name=database_name,
|
|
774
|
+
access_token=self._realtime._connection_token(),
|
|
775
|
+
table=change.table,
|
|
776
|
+
row_id=change.id,
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
async def _receive_postgres_change(self, data: Any) -> None:
|
|
780
|
+
change = _postgres_change(data)
|
|
781
|
+
if change is None or not self._has_postgres_listener(change):
|
|
782
|
+
return
|
|
783
|
+
if change.mode == "lightweight" and change.type == "DELETE":
|
|
784
|
+
old_record = change.old_record
|
|
785
|
+
if old_record is None and change.id is not None:
|
|
786
|
+
old_record = {"id": change.id}
|
|
787
|
+
change = replace(change, old_record=old_record, id=None, mode=None)
|
|
788
|
+
identity = self._capture_postgres_delivery_identity()
|
|
789
|
+
if not self._postgres_delivery_is_current(identity):
|
|
790
|
+
return
|
|
791
|
+
delivery = _PostgresDelivery(change=change, identity=identity)
|
|
792
|
+
request = self._postgres_fetch_request(change)
|
|
793
|
+
async with self._postgres_lock:
|
|
794
|
+
if not self._postgres_delivery_is_current(identity):
|
|
795
|
+
return
|
|
796
|
+
worker = self._postgres_worker
|
|
797
|
+
if worker is None:
|
|
798
|
+
worker = PostgresFetchWorker(
|
|
799
|
+
self._realtime._fetch_postgres_rows,
|
|
800
|
+
self._deliver_postgres,
|
|
801
|
+
queue_limit=POSTGRES_QUEUE_LIMIT,
|
|
802
|
+
batch_window_seconds=self._fetch_config.batch_window_seconds,
|
|
803
|
+
max_batch_size=self._fetch_config.max_batch_size,
|
|
804
|
+
)
|
|
805
|
+
self._postgres_worker = worker
|
|
806
|
+
try:
|
|
807
|
+
await worker.enqueue(PostgresFetchJob(request=request, fallback=delivery))
|
|
808
|
+
except RuntimeError:
|
|
809
|
+
if self._postgres_delivery_is_current(identity):
|
|
810
|
+
raise
|
|
811
|
+
|
|
812
|
+
async def _deliver_postgres(
|
|
813
|
+
self,
|
|
814
|
+
outcome: PostgresFetchOutcome[_PostgresDelivery],
|
|
815
|
+
) -> None:
|
|
816
|
+
delivery = outcome.job.fallback
|
|
817
|
+
if not self._postgres_delivery_is_current(delivery.identity):
|
|
818
|
+
return
|
|
819
|
+
change = delivery.change
|
|
820
|
+
if outcome.record is not None:
|
|
821
|
+
change = replace(change, record=outcome.record, id=None, mode=None)
|
|
822
|
+
elif outcome.job.request is not None:
|
|
823
|
+
error = outcome.error
|
|
824
|
+
if error is None:
|
|
825
|
+
identifier = (
|
|
826
|
+
f"{change.schema}.{change.table}:{outcome.job.request.row_id}"
|
|
827
|
+
)
|
|
828
|
+
message = f"Postgres row not found: {identifier}"
|
|
829
|
+
error = LookupError(message)
|
|
830
|
+
asyncio.get_running_loop().call_exception_handler(
|
|
831
|
+
{
|
|
832
|
+
"message": POSTGRES_FETCH_FAILED_MESSAGE,
|
|
833
|
+
"exception": error,
|
|
834
|
+
"channel": self._name,
|
|
835
|
+
}
|
|
836
|
+
)
|
|
837
|
+
if self._postgres_delivery_is_current(delivery.identity):
|
|
838
|
+
await self._emit(
|
|
839
|
+
"*",
|
|
840
|
+
change,
|
|
841
|
+
postgres_identity=delivery.identity,
|
|
842
|
+
)
|
|
843
|
+
|
|
844
|
+
async def subscribe(self) -> None:
|
|
845
|
+
"""Wait until this channel is subscribed and ready for use."""
|
|
846
|
+
await self._realtime._subscribe(self)
|
|
847
|
+
|
|
848
|
+
async def send(self, data: Any) -> None:
|
|
849
|
+
"""Publish a broadcast payload to this channel."""
|
|
850
|
+
await self._realtime._publish(self, data)
|
|
851
|
+
|
|
852
|
+
async def unsubscribe(self) -> None:
|
|
853
|
+
"""Unsubscribe from this channel."""
|
|
854
|
+
await self._realtime._unsubscribe(self)
|
|
855
|
+
|
|
856
|
+
async def _emit(
|
|
857
|
+
self,
|
|
858
|
+
event: str,
|
|
859
|
+
data: Any,
|
|
860
|
+
*,
|
|
861
|
+
postgres_identity: _PostgresDeliveryIdentity | None = None,
|
|
862
|
+
) -> None:
|
|
863
|
+
if not self._callbacks.get(event):
|
|
864
|
+
return
|
|
865
|
+
if event == "presence_sync":
|
|
866
|
+
self._pending_presence_sync = NO_PENDING_CALLBACK
|
|
867
|
+
try:
|
|
868
|
+
self._callback_queue.put_nowait(
|
|
869
|
+
_CallbackDelivery(
|
|
870
|
+
event,
|
|
871
|
+
data,
|
|
872
|
+
postgres_identity,
|
|
873
|
+
self._callback_epoch(event) if postgres_identity is None else None,
|
|
874
|
+
)
|
|
875
|
+
)
|
|
876
|
+
except asyncio.QueueFull:
|
|
877
|
+
if event == "presence_sync":
|
|
878
|
+
self._pending_presence_sync = data
|
|
879
|
+
return
|
|
880
|
+
asyncio.get_running_loop().call_exception_handler(
|
|
881
|
+
{
|
|
882
|
+
"message": CALLBACK_QUEUE_FULL_MESSAGE,
|
|
883
|
+
"channel": self._name,
|
|
884
|
+
}
|
|
885
|
+
)
|
|
886
|
+
task = self._callback_task
|
|
887
|
+
if task is None or task.done():
|
|
888
|
+
self._start_callback_dispatcher()
|
|
889
|
+
|
|
890
|
+
def _start_callback_dispatcher(self) -> None:
|
|
891
|
+
task = asyncio.create_task(self._dispatch_callbacks())
|
|
892
|
+
self._callback_task = task
|
|
893
|
+
# Retain running application work even if its channel is removed.
|
|
894
|
+
self._realtime._callback_tasks.add(task)
|
|
895
|
+
task.add_done_callback(self._callback_dispatcher_finished)
|
|
896
|
+
|
|
897
|
+
def _callback_dispatcher_finished(self, task: asyncio.Task[None]) -> None:
|
|
898
|
+
self._realtime._callback_tasks.discard(task)
|
|
899
|
+
if self._callback_task is task:
|
|
900
|
+
self._callback_task = None
|
|
901
|
+
if not task.cancelled() and (error := task.exception()) is not None:
|
|
902
|
+
asyncio.get_running_loop().call_exception_handler(
|
|
903
|
+
{
|
|
904
|
+
"message": "Volcano realtime callback dispatcher failed",
|
|
905
|
+
"exception": error,
|
|
906
|
+
"channel": self._name,
|
|
907
|
+
}
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
async def _dispatch_callbacks(self) -> None:
|
|
911
|
+
try:
|
|
912
|
+
# Register the worker before user code can re-enter through eager tasks.
|
|
913
|
+
await asyncio.sleep(0)
|
|
914
|
+
while not self._callback_queue.empty():
|
|
915
|
+
delivery = self._callback_queue.get_nowait()
|
|
916
|
+
try:
|
|
917
|
+
if not self._callback_delivery_is_current(delivery):
|
|
918
|
+
continue
|
|
919
|
+
for callback in tuple(self._callbacks.get(delivery.event, [])):
|
|
920
|
+
if not self._callback_delivery_is_current(delivery):
|
|
921
|
+
break
|
|
922
|
+
# Isolate a callback's own cancellation from later delivery.
|
|
923
|
+
(error,) = await asyncio.gather(
|
|
924
|
+
self._run_callback(callback, delivery),
|
|
925
|
+
return_exceptions=True,
|
|
926
|
+
)
|
|
927
|
+
if isinstance(error, BaseException):
|
|
928
|
+
asyncio.get_running_loop().call_exception_handler(
|
|
929
|
+
{
|
|
930
|
+
"message": "Volcano realtime callback failed",
|
|
931
|
+
"exception": error,
|
|
932
|
+
"channel": self._name,
|
|
933
|
+
}
|
|
934
|
+
)
|
|
935
|
+
finally:
|
|
936
|
+
self._callback_queue.task_done()
|
|
937
|
+
self._enqueue_pending_presence_sync()
|
|
938
|
+
finally:
|
|
939
|
+
# Event-loop cancellation must not leave queued delivery to restart.
|
|
940
|
+
self._discard_callbacks()
|
|
941
|
+
|
|
942
|
+
def _callback_delivery_is_current(self, delivery: _CallbackDelivery) -> bool:
|
|
943
|
+
if delivery.delivery_epoch is not None:
|
|
944
|
+
return (
|
|
945
|
+
not self._paused or delivery.event == "presence_sync"
|
|
946
|
+
) and delivery.delivery_epoch == self._callback_epoch(delivery.event)
|
|
947
|
+
identity = delivery.postgres_identity
|
|
948
|
+
return identity is None or self._postgres_delivery_is_current(identity)
|
|
949
|
+
|
|
950
|
+
def _callback_epoch(self, event: str) -> int:
|
|
951
|
+
if event in {"join", "leave", "presence_sync"}:
|
|
952
|
+
return self._presence_epoch
|
|
953
|
+
return self._delivery_epoch
|
|
954
|
+
|
|
955
|
+
def _enqueue_pending_presence_sync(self) -> None:
|
|
956
|
+
pending = self._pending_presence_sync
|
|
957
|
+
if pending is NO_PENDING_CALLBACK or self._callback_queue.full():
|
|
958
|
+
return
|
|
959
|
+
self._pending_presence_sync = NO_PENDING_CALLBACK
|
|
960
|
+
self._callback_queue.put_nowait(
|
|
961
|
+
_CallbackDelivery(
|
|
962
|
+
"presence_sync", pending, delivery_epoch=self._presence_epoch
|
|
963
|
+
)
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
async def _run_callback(
|
|
967
|
+
self,
|
|
968
|
+
callback: MessageCallback,
|
|
969
|
+
delivery: _CallbackDelivery,
|
|
970
|
+
) -> None:
|
|
971
|
+
if not self._callback_delivery_is_current(delivery):
|
|
972
|
+
return
|
|
973
|
+
result = callback(delivery.data)
|
|
974
|
+
if inspect.isawaitable(result):
|
|
975
|
+
await result
|
|
976
|
+
|
|
977
|
+
def _replace_presence(self, clients: Mapping[str, Any]) -> None:
|
|
978
|
+
self._presence_state = {
|
|
979
|
+
client_id: self._presence_info(info) for client_id, info in clients.items()
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
async def _begin_presence_sync(self) -> None:
|
|
983
|
+
async with self._presence_lock:
|
|
984
|
+
self._presence_syncing = True
|
|
985
|
+
self._presence_events.clear()
|
|
986
|
+
|
|
987
|
+
async def _complete_presence_sync(self, clients: Mapping[str, Any]) -> None:
|
|
988
|
+
async with self._presence_lock:
|
|
989
|
+
if not self._subscribed:
|
|
990
|
+
self._discard_presence_sync()
|
|
991
|
+
return
|
|
992
|
+
self._replace_presence(clients)
|
|
993
|
+
for event, presence in self._presence_events:
|
|
994
|
+
self._apply_presence_event(event, presence)
|
|
995
|
+
self._discard_presence_sync()
|
|
996
|
+
await self._emit("presence_sync", self.get_presence_state())
|
|
997
|
+
|
|
998
|
+
async def _abort_presence_sync(self) -> None:
|
|
999
|
+
async with self._presence_lock:
|
|
1000
|
+
self._discard_presence_sync()
|
|
1001
|
+
|
|
1002
|
+
async def _fail_presence_sync(self) -> None:
|
|
1003
|
+
async with self._presence_lock:
|
|
1004
|
+
self._discard_presence_sync()
|
|
1005
|
+
if not self._subscribed:
|
|
1006
|
+
return
|
|
1007
|
+
self._presence_state.clear()
|
|
1008
|
+
await self._emit("presence_sync", self.get_presence_state())
|
|
1009
|
+
|
|
1010
|
+
def _discard_presence_sync(self) -> None:
|
|
1011
|
+
self._presence_syncing = False
|
|
1012
|
+
self._presence_events.clear()
|
|
1013
|
+
|
|
1014
|
+
def _apply_presence_event(
|
|
1015
|
+
self,
|
|
1016
|
+
event: str,
|
|
1017
|
+
presence: RealtimePresenceInfo,
|
|
1018
|
+
) -> None:
|
|
1019
|
+
if event == "join":
|
|
1020
|
+
self._presence_state[presence.client] = presence
|
|
1021
|
+
else:
|
|
1022
|
+
self._presence_state.pop(presence.client, None)
|
|
1023
|
+
|
|
1024
|
+
async def _presence_join(self, info: Any) -> None:
|
|
1025
|
+
if self._type != "presence" or info is None:
|
|
1026
|
+
return
|
|
1027
|
+
async with self._presence_lock:
|
|
1028
|
+
if not self._subscribed:
|
|
1029
|
+
return
|
|
1030
|
+
presence = self._presence_info(info)
|
|
1031
|
+
if self._presence_syncing:
|
|
1032
|
+
self._presence_events.append(("join", presence))
|
|
1033
|
+
self._apply_presence_event("join", presence)
|
|
1034
|
+
await self._emit("join", presence)
|
|
1035
|
+
await self._emit("presence_sync", self.get_presence_state())
|
|
1036
|
+
|
|
1037
|
+
async def _presence_leave(self, info: Any) -> None:
|
|
1038
|
+
if self._type != "presence" or info is None:
|
|
1039
|
+
return
|
|
1040
|
+
async with self._presence_lock:
|
|
1041
|
+
if not self._subscribed:
|
|
1042
|
+
return
|
|
1043
|
+
presence = self._presence_info(info)
|
|
1044
|
+
if self._presence_syncing:
|
|
1045
|
+
self._presence_events.append(("leave", presence))
|
|
1046
|
+
self._apply_presence_event("leave", presence)
|
|
1047
|
+
await self._emit("leave", presence)
|
|
1048
|
+
await self._emit("presence_sync", self.get_presence_state())
|
|
1049
|
+
|
|
1050
|
+
async def _presence_unsubscribed(self) -> None:
|
|
1051
|
+
if self._type != "presence":
|
|
1052
|
+
return
|
|
1053
|
+
await self._cancel_presence_sync()
|
|
1054
|
+
async with self._presence_lock:
|
|
1055
|
+
self._discard_presence_sync()
|
|
1056
|
+
self._presence_state.clear()
|
|
1057
|
+
self._tracked_state = MappingProxyType({})
|
|
1058
|
+
await self._emit("presence_sync", self.get_presence_state())
|
|
1059
|
+
|
|
1060
|
+
def _presence_info(self, info: Any) -> RealtimePresenceInfo:
|
|
1061
|
+
data = getattr(info, "conn_info", None)
|
|
1062
|
+
typed_data = (
|
|
1063
|
+
cast("Mapping[str, JSONValue]", data)
|
|
1064
|
+
if isinstance(data, Mapping)
|
|
1065
|
+
else _empty_presence_data()
|
|
1066
|
+
)
|
|
1067
|
+
return RealtimePresenceInfo(
|
|
1068
|
+
client=str(getattr(info, "client", "")),
|
|
1069
|
+
user=getattr(info, "user", None),
|
|
1070
|
+
data=typed_data,
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1073
|
+
def _schedule_presence_sync(self) -> None:
|
|
1074
|
+
task = self._presence_sync_task
|
|
1075
|
+
if task is not None and not task.done():
|
|
1076
|
+
self._presence_sync_pending = True
|
|
1077
|
+
return
|
|
1078
|
+
self._presence_sync_pending = False
|
|
1079
|
+
self._presence_sync_task = asyncio.create_task(self._run_presence_sync())
|
|
1080
|
+
|
|
1081
|
+
async def _run_presence_sync(self) -> None:
|
|
1082
|
+
try:
|
|
1083
|
+
while self._subscribed:
|
|
1084
|
+
await self._realtime._sync_presence(self)
|
|
1085
|
+
if not self._presence_sync_pending:
|
|
1086
|
+
return
|
|
1087
|
+
self._presence_sync_pending = False
|
|
1088
|
+
finally:
|
|
1089
|
+
if asyncio.current_task() is self._presence_sync_task:
|
|
1090
|
+
self._presence_sync_task = None
|
|
1091
|
+
|
|
1092
|
+
async def _wait_presence_sync(self) -> None:
|
|
1093
|
+
task = self._presence_sync_task
|
|
1094
|
+
if task is not None:
|
|
1095
|
+
await asyncio.shield(task)
|
|
1096
|
+
|
|
1097
|
+
async def _cancel_presence_sync(self) -> None:
|
|
1098
|
+
task = self._presence_sync_task
|
|
1099
|
+
self._presence_sync_task = None
|
|
1100
|
+
self._presence_sync_pending = False
|
|
1101
|
+
if task is None or task.done():
|
|
1102
|
+
return
|
|
1103
|
+
task.cancel()
|
|
1104
|
+
await asyncio.gather(task, return_exceptions=True)
|
|
1105
|
+
|
|
1106
|
+
async def _reset(self) -> None:
|
|
1107
|
+
self._invalidate()
|
|
1108
|
+
await self._end_postgres_epoch()
|
|
1109
|
+
await self._cancel_presence_sync()
|
|
1110
|
+
self._presence_state.clear()
|
|
1111
|
+
self._discard_presence_sync()
|
|
1112
|
+
self._tracked_state = MappingProxyType({})
|
|
1113
|
+
self._subscribed = False
|
|
1114
|
+
|
|
1115
|
+
def _invalidate(self) -> None:
|
|
1116
|
+
if self._readiness_task is not None:
|
|
1117
|
+
self._readiness_task.cancel()
|
|
1118
|
+
self._subscription = None
|
|
1119
|
+
self._subscription_events = None
|
|
1120
|
+
self._pause_delivery()
|
|
1121
|
+
|
|
1122
|
+
def _pause_delivery(self) -> None:
|
|
1123
|
+
self._paused = True
|
|
1124
|
+
self._subscribed = False
|
|
1125
|
+
self._discard_callbacks()
|
|
1126
|
+
|
|
1127
|
+
def _discard_callbacks(self, *, presence_only: bool = False) -> None:
|
|
1128
|
+
self._presence_epoch += 1
|
|
1129
|
+
if not presence_only:
|
|
1130
|
+
self._delivery_epoch += 1
|
|
1131
|
+
# Free capacity before recovered publications arrive behind a slow callback.
|
|
1132
|
+
for _ in range(self._callback_queue.qsize()):
|
|
1133
|
+
delivery = self._callback_queue.get_nowait()
|
|
1134
|
+
if presence_only and delivery.event == "message":
|
|
1135
|
+
# Requeue before task_done so queue.join cannot finish prematurely.
|
|
1136
|
+
self._callback_queue.put_nowait(delivery)
|
|
1137
|
+
self._callback_queue.task_done()
|
|
1138
|
+
self._pending_presence_sync = NO_PENDING_CALLBACK
|
|
1139
|
+
|
|
1140
|
+
async def _transport_lost(self) -> None:
|
|
1141
|
+
self._subscribed = False
|
|
1142
|
+
# Recoverable channels already include queued messages in their offsets.
|
|
1143
|
+
if not self._paused and self._type != "broadcast":
|
|
1144
|
+
self._discard_callbacks(presence_only=self._type == "presence")
|
|
1145
|
+
await self._end_postgres_epoch()
|
|
1146
|
+
await self._presence_unsubscribed()
|
|
1147
|
+
|
|
1148
|
+
|
|
1149
|
+
class Realtime:
|
|
1150
|
+
"""Manage project realtime connections and channels."""
|
|
1151
|
+
|
|
1152
|
+
def __init__(
|
|
1153
|
+
self,
|
|
1154
|
+
client: RealtimeContext,
|
|
1155
|
+
*,
|
|
1156
|
+
api_url: str,
|
|
1157
|
+
client_factory: CentrifugeFactory = _centrifuge_client,
|
|
1158
|
+
) -> None:
|
|
1159
|
+
"""Create a lazily connected realtime facade."""
|
|
1160
|
+
self._client_context = client
|
|
1161
|
+
self._api_url = api_url
|
|
1162
|
+
self._client_factory = client_factory
|
|
1163
|
+
self._connection: _VolcanoCentrifugeConnection | None = None
|
|
1164
|
+
self._connection_session_lineage: int | None = None
|
|
1165
|
+
self._connection_access_token: str | None = None
|
|
1166
|
+
self._connection_lock = asyncio.Lock()
|
|
1167
|
+
self._channels: dict[str, Channel] = {}
|
|
1168
|
+
self._callback_tasks: set[asyncio.Task[None]] = set()
|
|
1169
|
+
self._removing_channels: set[str] = set()
|
|
1170
|
+
self._connection_callbacks: dict[str, dict[int, RealtimeCallback]] = {
|
|
1171
|
+
"connect": {},
|
|
1172
|
+
"disconnect": {},
|
|
1173
|
+
"error": {},
|
|
1174
|
+
}
|
|
1175
|
+
self._next_callback_id = 0
|
|
1176
|
+
self._connection_callback_queue: asyncio.Queue[
|
|
1177
|
+
tuple[str, Any, tuple[int, ...]]
|
|
1178
|
+
] = asyncio.Queue(maxsize=CALLBACK_QUEUE_LIMIT)
|
|
1179
|
+
self._connection_callback_task: asyncio.Task[None] | None = None
|
|
1180
|
+
self._database_name: str | None = None
|
|
1181
|
+
|
|
1182
|
+
@property
|
|
1183
|
+
def database_name(self) -> str | None:
|
|
1184
|
+
"""Return the database bound to lightweight Postgres changes."""
|
|
1185
|
+
return self._database_name
|
|
1186
|
+
|
|
1187
|
+
def set_database_name(self, name: str | None) -> None:
|
|
1188
|
+
"""Bind lightweight Postgres changes to a project database."""
|
|
1189
|
+
self._database_name = name
|
|
1190
|
+
|
|
1191
|
+
async def _fetch_postgres_rows(
|
|
1192
|
+
self,
|
|
1193
|
+
requests: tuple[_PostgresFetchRequest, ...],
|
|
1194
|
+
) -> tuple[dict[str, Any] | None, ...]:
|
|
1195
|
+
first = requests[0]
|
|
1196
|
+
row_ids = [request.row_id for request in requests]
|
|
1197
|
+
transport = cast(
|
|
1198
|
+
"AsyncDatabaseSelectTransport",
|
|
1199
|
+
self._client_context._transport,
|
|
1200
|
+
)
|
|
1201
|
+
response = await invoke_async(
|
|
1202
|
+
transport.query_database_select_async,
|
|
1203
|
+
authorization=first.access_token,
|
|
1204
|
+
database_name=first.database_name,
|
|
1205
|
+
body={
|
|
1206
|
+
"table": first.table,
|
|
1207
|
+
"filters": [{"column": "id", "operator": "in", "value": row_ids}],
|
|
1208
|
+
"limit": len(row_ids),
|
|
1209
|
+
},
|
|
1210
|
+
)
|
|
1211
|
+
payload = response_payload(response, 200)
|
|
1212
|
+
rows = list(payload["data"])
|
|
1213
|
+
return tuple(
|
|
1214
|
+
next(
|
|
1215
|
+
(row for row in rows if row.get("id") == request.row_id),
|
|
1216
|
+
None,
|
|
1217
|
+
)
|
|
1218
|
+
for request in requests
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
def on_connect(self, callback: RealtimeCallback) -> UnsubscribeCallback:
|
|
1222
|
+
"""Register a connection callback and return its unsubscribe function."""
|
|
1223
|
+
return self._register_connection_callback("connect", callback)
|
|
1224
|
+
|
|
1225
|
+
def on_disconnect(self, callback: RealtimeCallback) -> UnsubscribeCallback:
|
|
1226
|
+
"""Register a disconnection callback and return its unsubscribe function."""
|
|
1227
|
+
return self._register_connection_callback("disconnect", callback)
|
|
1228
|
+
|
|
1229
|
+
def on_error(self, callback: RealtimeCallback) -> UnsubscribeCallback:
|
|
1230
|
+
"""Register a transport-error callback and return its unsubscribe function."""
|
|
1231
|
+
return self._register_connection_callback("error", callback)
|
|
1232
|
+
|
|
1233
|
+
def _register_connection_callback(
|
|
1234
|
+
self,
|
|
1235
|
+
event: str,
|
|
1236
|
+
callback: RealtimeCallback,
|
|
1237
|
+
) -> UnsubscribeCallback:
|
|
1238
|
+
if not callable(callback):
|
|
1239
|
+
raise TypeError(CALLBACK_NOT_CALLABLE)
|
|
1240
|
+
self._next_callback_id += 1
|
|
1241
|
+
callback_id = self._next_callback_id
|
|
1242
|
+
self._connection_callbacks[event][callback_id] = callback
|
|
1243
|
+
|
|
1244
|
+
def unsubscribe() -> None:
|
|
1245
|
+
self._connection_callbacks[event].pop(callback_id, None)
|
|
1246
|
+
|
|
1247
|
+
return unsubscribe
|
|
1248
|
+
|
|
1249
|
+
def _enqueue_connection_callbacks(self, event: str, context: Any) -> None:
|
|
1250
|
+
callback_ids = tuple(self._connection_callbacks[event])
|
|
1251
|
+
if not callback_ids:
|
|
1252
|
+
return
|
|
1253
|
+
try:
|
|
1254
|
+
self._connection_callback_queue.put_nowait((event, context, callback_ids))
|
|
1255
|
+
except asyncio.QueueFull:
|
|
1256
|
+
asyncio.get_running_loop().call_exception_handler(
|
|
1257
|
+
{"message": "Volcano realtime connection callback queue is full"}
|
|
1258
|
+
)
|
|
1259
|
+
return
|
|
1260
|
+
task = self._connection_callback_task
|
|
1261
|
+
if task is None or task.done():
|
|
1262
|
+
self._connection_callback_task = asyncio.create_task(
|
|
1263
|
+
self._drain_connection_callbacks()
|
|
1264
|
+
)
|
|
1265
|
+
|
|
1266
|
+
async def _drain_connection_callbacks(self) -> None:
|
|
1267
|
+
while not self._connection_callback_queue.empty():
|
|
1268
|
+
event, context, callback_ids = self._connection_callback_queue.get_nowait()
|
|
1269
|
+
try:
|
|
1270
|
+
callbacks = self._connection_callbacks[event]
|
|
1271
|
+
for callback_id in callback_ids:
|
|
1272
|
+
callback = callbacks.get(callback_id)
|
|
1273
|
+
if callback is None:
|
|
1274
|
+
continue
|
|
1275
|
+
(error,) = await asyncio.gather(
|
|
1276
|
+
self._run_connection_callback(callback, context),
|
|
1277
|
+
return_exceptions=True,
|
|
1278
|
+
)
|
|
1279
|
+
if isinstance(error, BaseException):
|
|
1280
|
+
asyncio.get_running_loop().call_exception_handler(
|
|
1281
|
+
{
|
|
1282
|
+
"message": (
|
|
1283
|
+
"Volcano realtime connection callback failed"
|
|
1284
|
+
),
|
|
1285
|
+
"exception": error,
|
|
1286
|
+
"event": event,
|
|
1287
|
+
}
|
|
1288
|
+
)
|
|
1289
|
+
finally:
|
|
1290
|
+
self._connection_callback_queue.task_done()
|
|
1291
|
+
|
|
1292
|
+
async def _run_connection_callback(
|
|
1293
|
+
self,
|
|
1294
|
+
callback: RealtimeCallback,
|
|
1295
|
+
context: Any,
|
|
1296
|
+
) -> None:
|
|
1297
|
+
result = callback(context)
|
|
1298
|
+
if inspect.isawaitable(result):
|
|
1299
|
+
await result
|
|
1300
|
+
|
|
1301
|
+
def channel(
|
|
1302
|
+
self,
|
|
1303
|
+
name: str,
|
|
1304
|
+
*,
|
|
1305
|
+
channel_type: ChannelType = "broadcast",
|
|
1306
|
+
auto_fetch: bool = True,
|
|
1307
|
+
fetch_batch_window_ms: int = POSTGRES_BATCH_WINDOW_MS,
|
|
1308
|
+
fetch_max_batch_size: int = POSTGRES_MAX_BATCH_SIZE,
|
|
1309
|
+
) -> Channel:
|
|
1310
|
+
"""Return a stable channel facade for a realtime name and configuration."""
|
|
1311
|
+
channel_type = _validate_channel_type(channel_type)
|
|
1312
|
+
fetch_config = _postgres_fetch_config(
|
|
1313
|
+
auto_fetch=auto_fetch,
|
|
1314
|
+
fetch_batch_window_ms=fetch_batch_window_ms,
|
|
1315
|
+
fetch_max_batch_size=fetch_max_batch_size,
|
|
1316
|
+
)
|
|
1317
|
+
wire_name = f"{channel_type}:{name}"
|
|
1318
|
+
if wire_name in self._removing_channels:
|
|
1319
|
+
raise RuntimeError(CHANNEL_REMOVAL_IN_PROGRESS)
|
|
1320
|
+
channel = self._channels.get(wire_name)
|
|
1321
|
+
if channel is None:
|
|
1322
|
+
channel = Channel(
|
|
1323
|
+
self,
|
|
1324
|
+
wire_name,
|
|
1325
|
+
channel_type,
|
|
1326
|
+
fetch_config=fetch_config,
|
|
1327
|
+
)
|
|
1328
|
+
self._channels[wire_name] = channel
|
|
1329
|
+
elif channel._fetch_config != fetch_config:
|
|
1330
|
+
message = (
|
|
1331
|
+
f"channel {wire_name!r} already uses a different fetch configuration"
|
|
1332
|
+
)
|
|
1333
|
+
raise ValueError(message)
|
|
1334
|
+
return channel
|
|
1335
|
+
|
|
1336
|
+
@property
|
|
1337
|
+
def is_connected(self) -> bool:
|
|
1338
|
+
"""Return whether the realtime transport is connected."""
|
|
1339
|
+
return self._connection is not None and self._connection.is_connected
|
|
1340
|
+
|
|
1341
|
+
async def remove_channel(
|
|
1342
|
+
self,
|
|
1343
|
+
name: str,
|
|
1344
|
+
*,
|
|
1345
|
+
channel_type: ChannelType = "broadcast",
|
|
1346
|
+
) -> None:
|
|
1347
|
+
"""Unsubscribe and forget one broadcast or presence channel."""
|
|
1348
|
+
channel_type = _validate_channel_type(channel_type)
|
|
1349
|
+
wire_name = f"{channel_type}:{name}"
|
|
1350
|
+
async with self._connection_lock:
|
|
1351
|
+
channel = self._channels.get(wire_name)
|
|
1352
|
+
if channel is None:
|
|
1353
|
+
return
|
|
1354
|
+
self._removing_channels.add(wire_name)
|
|
1355
|
+
try:
|
|
1356
|
+
await self._remove_channel(channel)
|
|
1357
|
+
self._channels.pop(wire_name, None)
|
|
1358
|
+
finally:
|
|
1359
|
+
self._removing_channels.remove(wire_name)
|
|
1360
|
+
|
|
1361
|
+
async def remove_all_channels(self) -> None:
|
|
1362
|
+
"""Unsubscribe and forget every managed channel."""
|
|
1363
|
+
async with self._connection_lock:
|
|
1364
|
+
first_error: Exception | None = None
|
|
1365
|
+
for wire_name, channel in tuple(self._channels.items()):
|
|
1366
|
+
self._removing_channels.add(wire_name)
|
|
1367
|
+
try:
|
|
1368
|
+
await self._remove_channel(channel)
|
|
1369
|
+
except CENTRIFUGE_ERROR as error:
|
|
1370
|
+
first_error = first_error or error
|
|
1371
|
+
else:
|
|
1372
|
+
if self._channels.get(wire_name) is channel:
|
|
1373
|
+
del self._channels[wire_name]
|
|
1374
|
+
finally:
|
|
1375
|
+
self._removing_channels.remove(wire_name)
|
|
1376
|
+
if first_error is not None:
|
|
1377
|
+
raise first_error
|
|
1378
|
+
|
|
1379
|
+
async def _remove_channel(self, channel: Channel) -> None:
|
|
1380
|
+
channel._subscribe_generation += 1
|
|
1381
|
+
await self._discard_subscription(channel)
|
|
1382
|
+
await channel._reset()
|
|
1383
|
+
|
|
1384
|
+
async def _discard_subscription(self, channel: Channel) -> None:
|
|
1385
|
+
subscription = channel._subscription
|
|
1386
|
+
channel._subscription_events = None
|
|
1387
|
+
channel._pause_delivery()
|
|
1388
|
+
try:
|
|
1389
|
+
if subscription is not None:
|
|
1390
|
+
# Native state must change before any cancellable local cleanup.
|
|
1391
|
+
await _unsubscribe_native(subscription)
|
|
1392
|
+
finally:
|
|
1393
|
+
await channel._transport_lost()
|
|
1394
|
+
if subscription is not None and self._connection is not None:
|
|
1395
|
+
self._connection.remove_subscription(subscription)
|
|
1396
|
+
channel._subscription = None
|
|
1397
|
+
|
|
1398
|
+
async def _token(self) -> str:
|
|
1399
|
+
lineage = self._connection_lineage()
|
|
1400
|
+
session = self._session_for_lineage(lineage)
|
|
1401
|
+
self._connection_access_token = session.access_token
|
|
1402
|
+
return session.access_token
|
|
1403
|
+
|
|
1404
|
+
def _session_for_lineage(self, expected_lineage: int) -> Session:
|
|
1405
|
+
_generation, lineage, session = self._client_context._capture_session_binding()
|
|
1406
|
+
if session is None:
|
|
1407
|
+
raise RuntimeError(NO_ACTIVE_SESSION)
|
|
1408
|
+
if lineage != expected_lineage:
|
|
1409
|
+
raise RuntimeError(CONNECTION_SESSION_CHANGED)
|
|
1410
|
+
return session
|
|
1411
|
+
|
|
1412
|
+
def _connection_lineage(self) -> int:
|
|
1413
|
+
lineage = self._connection_session_lineage
|
|
1414
|
+
if lineage is None:
|
|
1415
|
+
raise RuntimeError(CONNECTION_SESSION_UNAVAILABLE)
|
|
1416
|
+
return lineage
|
|
1417
|
+
|
|
1418
|
+
def _connection_token(self) -> str:
|
|
1419
|
+
token = self._connection_access_token
|
|
1420
|
+
if token is None:
|
|
1421
|
+
raise RuntimeError(CONNECTION_SESSION_UNAVAILABLE)
|
|
1422
|
+
return token
|
|
1423
|
+
|
|
1424
|
+
def _address(self) -> str:
|
|
1425
|
+
parsed = urlsplit(self._api_url)
|
|
1426
|
+
scheme = "wss" if parsed.scheme == "https" else "ws"
|
|
1427
|
+
query = f"apikey={quote(self._client_context._anon_token(), safe='')}"
|
|
1428
|
+
return urlunsplit((scheme, parsed.netloc, "/realtime/v1/websocket", query, ""))
|
|
1429
|
+
|
|
1430
|
+
async def _connect(self) -> _VolcanoCentrifugeConnection:
|
|
1431
|
+
async with self._connection_lock:
|
|
1432
|
+
return await self._connect_locked()
|
|
1433
|
+
|
|
1434
|
+
async def _connect_locked(self) -> _VolcanoCentrifugeConnection:
|
|
1435
|
+
if self._connection is not None:
|
|
1436
|
+
self._session_for_lineage(self._connection_lineage())
|
|
1437
|
+
return self._connection
|
|
1438
|
+
_generation, lineage, session = self._client_context._capture_session_binding()
|
|
1439
|
+
if session is None:
|
|
1440
|
+
raise RuntimeError(NO_ACTIVE_SESSION)
|
|
1441
|
+
connection = _VolcanoCentrifugeConnection(
|
|
1442
|
+
self._client_factory(
|
|
1443
|
+
self._address(),
|
|
1444
|
+
events=_ClientEvents(self),
|
|
1445
|
+
token=session.access_token,
|
|
1446
|
+
get_token=self._token,
|
|
1447
|
+
)
|
|
1448
|
+
)
|
|
1449
|
+
self._connection_session_lineage = lineage
|
|
1450
|
+
self._connection_access_token = session.access_token
|
|
1451
|
+
try:
|
|
1452
|
+
await connection.connect()
|
|
1453
|
+
except BaseException:
|
|
1454
|
+
self._connection_session_lineage = None
|
|
1455
|
+
self._connection_access_token = None
|
|
1456
|
+
raise
|
|
1457
|
+
try:
|
|
1458
|
+
current_session = self._session_for_lineage(lineage)
|
|
1459
|
+
except RuntimeError:
|
|
1460
|
+
self._connection = connection
|
|
1461
|
+
await connection.disconnect()
|
|
1462
|
+
self._connection = None
|
|
1463
|
+
self._connection_session_lineage = None
|
|
1464
|
+
self._connection_access_token = None
|
|
1465
|
+
raise
|
|
1466
|
+
self._connection_access_token = current_session.access_token
|
|
1467
|
+
self._connection = connection
|
|
1468
|
+
return connection
|
|
1469
|
+
|
|
1470
|
+
async def _subscribe(self, channel: Channel) -> None:
|
|
1471
|
+
# A later stop supersedes this request, including time spent waiting for locks.
|
|
1472
|
+
generation = channel._subscribe_generation
|
|
1473
|
+
async with channel._subscribe_lock:
|
|
1474
|
+
subscription = None
|
|
1475
|
+
try:
|
|
1476
|
+
async with self._connection_lock:
|
|
1477
|
+
subscription = await self._prepare_subscription(channel, generation)
|
|
1478
|
+
if channel._subscribed:
|
|
1479
|
+
return
|
|
1480
|
+
channel._paused = False
|
|
1481
|
+
await subscription.subscribe()
|
|
1482
|
+
channel._readiness_task = asyncio.create_task(
|
|
1483
|
+
self._wait_subscription(channel, subscription)
|
|
1484
|
+
)
|
|
1485
|
+
try:
|
|
1486
|
+
await channel._readiness_task
|
|
1487
|
+
finally:
|
|
1488
|
+
channel._readiness_task = None
|
|
1489
|
+
except BaseException as error:
|
|
1490
|
+
if (
|
|
1491
|
+
subscription is None
|
|
1492
|
+
or channel._subscription is not subscription
|
|
1493
|
+
or channel._paused
|
|
1494
|
+
):
|
|
1495
|
+
# An explicit pause or removal owns the newer subscription intent.
|
|
1496
|
+
raise
|
|
1497
|
+
channel._subscribe_generation += 1
|
|
1498
|
+
channel._subscription_events = None
|
|
1499
|
+
channel._pause_delivery()
|
|
1500
|
+
try:
|
|
1501
|
+
async with self._connection_lock:
|
|
1502
|
+
if channel._subscription is subscription:
|
|
1503
|
+
await self._discard_subscription(channel)
|
|
1504
|
+
except CENTRIFUGE_ERROR:
|
|
1505
|
+
error.add_note("Failed to clean up the realtime subscription")
|
|
1506
|
+
raise
|
|
1507
|
+
|
|
1508
|
+
async def _prepare_subscription(
|
|
1509
|
+
self, channel: Channel, generation: int
|
|
1510
|
+
) -> CentrifugeSubscription:
|
|
1511
|
+
if generation != channel._subscribe_generation:
|
|
1512
|
+
raise asyncio.CancelledError
|
|
1513
|
+
if self._channels.get(channel._name) is not channel:
|
|
1514
|
+
raise RuntimeError(CHANNEL_NOT_MANAGED)
|
|
1515
|
+
connection = await self._connect_locked()
|
|
1516
|
+
if channel._subscription is not None and channel._subscription_events is None:
|
|
1517
|
+
await self._discard_subscription(channel)
|
|
1518
|
+
if channel._subscription is None:
|
|
1519
|
+
channel._subscription_events = _ChannelEvents(channel)
|
|
1520
|
+
channel._subscription = connection.new_subscription(
|
|
1521
|
+
channel._name,
|
|
1522
|
+
events=channel._subscription_events,
|
|
1523
|
+
join_leave=channel._type == "presence",
|
|
1524
|
+
recoverable=channel._type != "postgres",
|
|
1525
|
+
)
|
|
1526
|
+
return channel._subscription
|
|
1527
|
+
|
|
1528
|
+
async def _wait_subscription(
|
|
1529
|
+
self, channel: Channel, subscription: CentrifugeSubscription
|
|
1530
|
+
) -> None:
|
|
1531
|
+
await subscription.ready()
|
|
1532
|
+
if channel._type == "presence":
|
|
1533
|
+
await channel._wait_presence_sync()
|
|
1534
|
+
if channel._subscription is not subscription or not channel._subscribed:
|
|
1535
|
+
message = "realtime subscription was interrupted"
|
|
1536
|
+
raise RuntimeError(message)
|
|
1537
|
+
|
|
1538
|
+
async def _sync_presence(self, channel: Channel) -> None:
|
|
1539
|
+
if channel._subscription is None:
|
|
1540
|
+
return
|
|
1541
|
+
await channel._begin_presence_sync()
|
|
1542
|
+
query_succeeded = False
|
|
1543
|
+
try:
|
|
1544
|
+
# Native replies must settle even after the roster refresh is cancelled.
|
|
1545
|
+
query = asyncio.create_task(channel._subscription.presence())
|
|
1546
|
+
query.add_done_callback(_consume_presence_result)
|
|
1547
|
+
result = await asyncio.shield(query)
|
|
1548
|
+
query_succeeded = True
|
|
1549
|
+
except CENTRIFUGE_ERROR as error:
|
|
1550
|
+
await channel._fail_presence_sync()
|
|
1551
|
+
query_succeeded = True
|
|
1552
|
+
self._enqueue_connection_callbacks(
|
|
1553
|
+
"error",
|
|
1554
|
+
RealtimeErrorContext(
|
|
1555
|
+
code=getattr(error, "code", None),
|
|
1556
|
+
message=str(error),
|
|
1557
|
+
error=error,
|
|
1558
|
+
),
|
|
1559
|
+
)
|
|
1560
|
+
return
|
|
1561
|
+
finally:
|
|
1562
|
+
if not query_succeeded:
|
|
1563
|
+
await channel._abort_presence_sync()
|
|
1564
|
+
clients = getattr(result, "clients", None)
|
|
1565
|
+
if isinstance(clients, Mapping):
|
|
1566
|
+
await channel._complete_presence_sync(cast("Mapping[str, Any]", clients))
|
|
1567
|
+
else:
|
|
1568
|
+
await channel._abort_presence_sync()
|
|
1569
|
+
|
|
1570
|
+
async def _publish(self, channel: Channel, data: Any) -> None:
|
|
1571
|
+
async with self._connection_lock:
|
|
1572
|
+
if channel._type != "broadcast":
|
|
1573
|
+
raise ValueError(BROADCAST_ONLY)
|
|
1574
|
+
subscription = channel._subscription
|
|
1575
|
+
if not channel._subscribed or subscription is None:
|
|
1576
|
+
raise RuntimeError(CHANNEL_NOT_SUBSCRIBED)
|
|
1577
|
+
await subscription.publish(data)
|
|
1578
|
+
|
|
1579
|
+
async def _unsubscribe(self, channel: Channel) -> None:
|
|
1580
|
+
async with self._connection_lock:
|
|
1581
|
+
channel._subscribe_generation += 1
|
|
1582
|
+
if not channel._paused:
|
|
1583
|
+
channel._pause_delivery()
|
|
1584
|
+
if channel._subscription is not None:
|
|
1585
|
+
await _unsubscribe_native(channel._subscription)
|
|
1586
|
+
|
|
1587
|
+
async def disconnect(self) -> None:
|
|
1588
|
+
"""Disconnect and reset every channel managed by this facade."""
|
|
1589
|
+
async with self._connection_lock:
|
|
1590
|
+
connection = self._connection
|
|
1591
|
+
self._connection = None
|
|
1592
|
+
channels = tuple(self._channels.values())
|
|
1593
|
+
for channel in channels:
|
|
1594
|
+
channel._subscribe_generation += 1
|
|
1595
|
+
channel._invalidate()
|
|
1596
|
+
cancelled: asyncio.CancelledError | None = None
|
|
1597
|
+
try:
|
|
1598
|
+
for channel in channels:
|
|
1599
|
+
try:
|
|
1600
|
+
await channel._reset()
|
|
1601
|
+
except asyncio.CancelledError as error:
|
|
1602
|
+
cancelled = error
|
|
1603
|
+
finally:
|
|
1604
|
+
try:
|
|
1605
|
+
if connection is not None:
|
|
1606
|
+
await connection.disconnect()
|
|
1607
|
+
finally:
|
|
1608
|
+
self._connection_session_lineage = None
|
|
1609
|
+
self._connection_access_token = None
|
|
1610
|
+
if cancelled is not None:
|
|
1611
|
+
raise cancelled
|