moolabs 0.3.0__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.
- moolabs/__init__.py +1487 -0
- moolabs/_dx_attribution_context.py +213 -0
- moolabs/_dx_attribution_db_time.py +2126 -0
- moolabs/_dx_attribution_db_time_transport.py +173 -0
- moolabs/_dx_attribution_delta.py +142 -0
- moolabs/_dx_attribution_deposits.py +552 -0
- moolabs/_dx_attribution_instrument.py +416 -0
- moolabs/_dx_attribution_middleware.py +541 -0
- moolabs/_dx_attribution_propagate.py +509 -0
- moolabs/_dx_attribution_signal_transport.py +232 -0
- moolabs/_dx_attribution_signals.py +927 -0
- moolabs/_dx_buffer.py +429 -0
- moolabs/_dx_client.py +726 -0
- moolabs/_dx_exceptions.py +84 -0
- moolabs/_dx_namespaces.py +1780 -0
- moolabs/_dx_pagination.py +131 -0
- moolabs/_dx_routing.py +276 -0
- moolabs/_dx_urls.py +629 -0
- moolabs/_dx_webhooks.py +82 -0
- moolabs/api/__init__.py +121 -0
- moolabs/api/account_team_api.py +1444 -0
- moolabs/api/accounts_api.py +4950 -0
- moolabs/api/acute_analytics_api.py +5750 -0
- moolabs/api/acute_integrations_api.py +3449 -0
- moolabs/api/admin_api.py +3347 -0
- moolabs/api/alerts_api.py +1626 -0
- moolabs/api/allocation_rules_api.py +1720 -0
- moolabs/api/analytics_api.py +3204 -0
- moolabs/api/app_custom_invoicing_api.py +952 -0
- moolabs/api/app_stripe_api.py +958 -0
- moolabs/api/apps_api.py +3064 -0
- moolabs/api/arc_admin_api.py +8773 -0
- moolabs/api/arc_communications_api.py +1991 -0
- moolabs/api/arc_portal_api.py +1737 -0
- moolabs/api/arc_webhooks_api.py +2087 -0
- moolabs/api/attribution_allocation_api.py +2345 -0
- moolabs/api/attribution_api.py +1717 -0
- moolabs/api/attribution_customer_keys_api.py +600 -0
- moolabs/api/attribution_db_time_api.py +918 -0
- moolabs/api/attribution_opencost_api.py +1372 -0
- moolabs/api/attribution_readouts_api.py +393 -0
- moolabs/api/attribution_signals_api.py +315 -0
- moolabs/api/auto_topup_api.py +3095 -0
- moolabs/api/batch_api.py +6714 -0
- moolabs/api/billing_api.py +300 -0
- moolabs/api/bom_api.py +2769 -0
- moolabs/api/budgets_api.py +2112 -0
- moolabs/api/buyer_quotes_api.py +2007 -0
- moolabs/api/cases_api.py +5075 -0
- moolabs/api/cash_credits_api.py +1548 -0
- moolabs/api/chat_api.py +3101 -0
- moolabs/api/clause_packs_api.py +2785 -0
- moolabs/api/cloud_billing_api.py +1747 -0
- moolabs/api/contract_editor_api.py +3384 -0
- moolabs/api/cost_events_api.py +1732 -0
- moolabs/api/credit_memos_api.py +3478 -0
- moolabs/api/customers_api.py +3931 -0
- moolabs/api/dashboard_api.py +771 -0
- moolabs/api/debug_api.py +300 -0
- moolabs/api/default_api.py +526 -0
- moolabs/api/disputes_api.py +3586 -0
- moolabs/api/dunning_templates_api.py +8737 -0
- moolabs/api/email_provider_api.py +526 -0
- moolabs/api/entitlements_api.py +9846 -0
- moolabs/api/escalations_api.py +2541 -0
- moolabs/api/events_api.py +1125 -0
- moolabs/api/fx_rates_api.py +1452 -0
- moolabs/api/governance_api.py +4402 -0
- moolabs/api/grants_api.py +1370 -0
- moolabs/api/handoffs_api.py +2138 -0
- moolabs/api/health_api.py +281 -0
- moolabs/api/homepage_api.py +305 -0
- moolabs/api/integrations_api.py +3374 -0
- moolabs/api/internal_api.py +7145 -0
- moolabs/api/internal_credit_memos_api.py +914 -0
- moolabs/api/internal_grants_api.py +1000 -0
- moolabs/api/internal_period_settlements_api.py +657 -0
- moolabs/api/internal_plan_pricing_api.py +326 -0
- moolabs/api/internal_subscriptions_api.py +326 -0
- moolabs/api/ledger_api.py +1483 -0
- moolabs/api/lookup_information_api.py +587 -0
- moolabs/api/mapping_rules_api.py +1782 -0
- moolabs/api/mappings_api.py +632 -0
- moolabs/api/margin_simulation_api.py +595 -0
- moolabs/api/margins_api.py +2301 -0
- moolabs/api/meter_billing_api.py +7419 -0
- moolabs/api/meter_portal_api.py +1368 -0
- moolabs/api/meter_subscriptions_api.py +4358 -0
- moolabs/api/meters_api.py +3707 -0
- moolabs/api/monitoring_api.py +561 -0
- moolabs/api/moolabs_api.py +1182 -0
- moolabs/api/moometer_proxy_api.py +1357 -0
- moolabs/api/netsuite_sync_api.py +2180 -0
- moolabs/api/notes_api.py +1425 -0
- moolabs/api/notifications_api.py +4490 -0
- moolabs/api/ops_api.py +315 -0
- moolabs/api/outbox_api.py +653 -0
- moolabs/api/payments_api.py +2889 -0
- moolabs/api/performance_api.py +2471 -0
- moolabs/api/plan_pricing_api.py +3409 -0
- moolabs/api/plans_api.py +2197 -0
- moolabs/api/pools_api.py +2358 -0
- moolabs/api/portal_api.py +1103 -0
- moolabs/api/product_catalog_api.py +7644 -0
- moolabs/api/promises_api.py +2814 -0
- moolabs/api/quote_agents_api.py +2036 -0
- moolabs/api/quote_contract_webhooks_api.py +327 -0
- moolabs/api/quote_contracts_api.py +2551 -0
- moolabs/api/quote_sessions_api.py +881 -0
- moolabs/api/quotes_api.py +12880 -0
- moolabs/api/rate_cards_api.py +578 -0
- moolabs/api/rate_catalog_api.py +2120 -0
- moolabs/api/rate_schedules_api.py +1224 -0
- moolabs/api/rating_api.py +617 -0
- moolabs/api/reconstruction_api.py +1281 -0
- moolabs/api/remittances_api.py +1521 -0
- moolabs/api/reports_api.py +722 -0
- moolabs/api/rollover_api.py +313 -0
- moolabs/api/sdk_ingest_api.py +312 -0
- moolabs/api/snapshots_api.py +1218 -0
- moolabs/api/state_projection_api.py +961 -0
- moolabs/api/steering_api.py +1862 -0
- moolabs/api/subjects_api.py +1160 -0
- moolabs/api/subscription_overrides_api.py +579 -0
- moolabs/api/subscriptions_api.py +1200 -0
- moolabs/api/tasks_api.py +1509 -0
- moolabs/api/tenant_api.py +22395 -0
- moolabs/api/tenant_config_api.py +282 -0
- moolabs/api/topup_api.py +632 -0
- moolabs/api/unpriced_api.py +933 -0
- moolabs/api/usage_api.py +999 -0
- moolabs/api/variance_api.py +1327 -0
- moolabs/api/wallets_api.py +3787 -0
- moolabs/api/webhooks_api.py +281 -0
- moolabs/api/well_known_api.py +278 -0
- moolabs/api/word_addin_api.py +623 -0
- moolabs/api_client.py +797 -0
- moolabs/api_response.py +21 -0
- moolabs/configuration.py +620 -0
- moolabs/exceptions.py +199 -0
- moolabs/models/__init__.py +1286 -0
- moolabs/models/account_create.py +114 -0
- moolabs/models/account_filter_options_response.py +97 -0
- moolabs/models/account_invoice_list_response.py +95 -0
- moolabs/models/account_list_item_response.py +152 -0
- moolabs/models/account_list_item_response_subsidiaries_inner.py +152 -0
- moolabs/models/account_list_response.py +101 -0
- moolabs/models/account_overview_bucket_response.py +91 -0
- moolabs/models/account_overview_response.py +106 -0
- moolabs/models/account_response.py +132 -0
- moolabs/models/account_segment_row.py +91 -0
- moolabs/models/account_subsidiary_option.py +89 -0
- moolabs/models/account_summary.py +97 -0
- moolabs/models/account_team_member_in.py +97 -0
- moolabs/models/account_team_member_out.py +101 -0
- moolabs/models/account_team_member_patch.py +97 -0
- moolabs/models/account_team_role.py +39 -0
- moolabs/models/account_update.py +106 -0
- moolabs/models/action_kind.py +45 -0
- moolabs/models/acute_approve_request.py +87 -0
- moolabs/models/acute_demo_seed_lifecycle_request.py +103 -0
- moolabs/models/acute_http_validation_error.py +95 -0
- moolabs/models/acute_validation_error.py +99 -0
- moolabs/models/add_contract_participant_request.py +107 -0
- moolabs/models/addon.py +178 -0
- moolabs/models/addon1.py +115 -0
- moolabs/models/addon2.py +95 -0
- moolabs/models/addon_create.py +123 -0
- moolabs/models/addon_instance_type.py +37 -0
- moolabs/models/addon_order_by.py +40 -0
- moolabs/models/addon_paginated_response.py +101 -0
- moolabs/models/addon_replace_update.py +105 -0
- moolabs/models/addon_status.py +38 -0
- moolabs/models/address.py +110 -0
- moolabs/models/adjusted_total.py +152 -0
- moolabs/models/adjustment_amount.py +152 -0
- moolabs/models/aging_bucket_detail.py +93 -0
- moolabs/models/aging_report_response.py +106 -0
- moolabs/models/alb_bootstrap_response.py +89 -0
- moolabs/models/alb_connector_config.py +128 -0
- moolabs/models/alb_connector_health_response.py +112 -0
- moolabs/models/alb_connector_list_response.py +138 -0
- moolabs/models/alb_load_balancer_config.py +111 -0
- moolabs/models/alb_route_rule.py +107 -0
- moolabs/models/alb_source_config.py +123 -0
- moolabs/models/alb_source_health_response.py +93 -0
- moolabs/models/alb_status_patch_request.py +94 -0
- moolabs/models/alb_status_patch_response.py +94 -0
- moolabs/models/alb_target_group_config.py +103 -0
- moolabs/models/algorithm_breakdown_item.py +89 -0
- moolabs/models/algorithm_info.py +93 -0
- moolabs/models/alignment.py +87 -0
- moolabs/models/allocate_credits_request.py +95 -0
- moolabs/models/allocation_coverage_response.py +157 -0
- moolabs/models/allocation_fact_page_response.py +97 -0
- moolabs/models/allocation_fact_response.py +146 -0
- moolabs/models/allocation_key.py +91 -0
- moolabs/models/allocation_rule_create.py +114 -0
- moolabs/models/allocation_rule_response.py +113 -0
- moolabs/models/allocation_rule_update.py +111 -0
- moolabs/models/allocation_run_create_request.py +100 -0
- moolabs/models/allocation_run_create_response.py +142 -0
- moolabs/models/allocation_run_open_cost_snapshot_response.py +120 -0
- moolabs/models/allocation_run_readout_response.py +110 -0
- moolabs/models/allocation_run_response.py +133 -0
- moolabs/models/allocations_list_response.py +97 -0
- moolabs/models/amount.py +152 -0
- moolabs/models/amount1.py +152 -0
- moolabs/models/api_key_item.py +103 -0
- moolabs/models/app.py +164 -0
- moolabs/models/app_api_v1_auto_topup_router_trigger_response.py +108 -0
- moolabs/models/app_api_v1_fx_rates_router_fx_rate_response.py +104 -0
- moolabs/models/app_api_v1_integrations_netsuite_sync_router_trigger_response.py +89 -0
- moolabs/models/app_api_v1_internal_subscription_sync_schemas_subscription_sync_request.py +123 -0
- moolabs/models/app_api_v1_portal_router_create_portal_token_request.py +92 -0
- moolabs/models/app_api_v1_quotes_schemas_quote_settings_response.py +91 -0
- moolabs/models/app_api_v1_quotes_schemas_update_quote_settings_request.py +89 -0
- moolabs/models/app_api_v1_subscriptions_router_subscription_sync_request.py +125 -0
- moolabs/models/app_api_v1_tenant_access_router_create_portal_token_request.py +101 -0
- moolabs/models/app_api_v1_tenant_fx_rates_router_fx_rate_response.py +93 -0
- moolabs/models/app_api_v1_tenant_quote_settings_router_quote_settings_response.py +89 -0
- moolabs/models/app_api_v1_tenant_quote_settings_router_update_quote_settings_request.py +87 -0
- moolabs/models/app_capability.py +102 -0
- moolabs/models/app_capability_type.py +40 -0
- moolabs/models/app_paginated_response.py +101 -0
- moolabs/models/app_reference.py +95 -0
- moolabs/models/app_replace_update.py +164 -0
- moolabs/models/app_status.py +37 -0
- moolabs/models/app_type.py +38 -0
- moolabs/models/apply_credit_request.py +92 -0
- moolabs/models/approval_action_request.py +88 -0
- moolabs/models/approval_action_response.py +95 -0
- moolabs/models/approval_policy_response.py +91 -0
- moolabs/models/approve_request.py +88 -0
- moolabs/models/approve_response.py +91 -0
- moolabs/models/approver_in.py +95 -0
- moolabs/models/arc_demo_seed_lifecycle_request.py +103 -0
- moolabs/models/arc_dunning_permission_grant_request.py +96 -0
- moolabs/models/arc_dunning_permission_revoke_request.py +90 -0
- moolabs/models/arc_dunning_template_admin_grant_request.py +90 -0
- moolabs/models/arc_dunning_template_admin_grant_response.py +115 -0
- moolabs/models/arc_dunning_template_admin_revoke_request.py +88 -0
- moolabs/models/arc_dunning_template_admin_user_dto.py +107 -0
- moolabs/models/arc_dunning_template_admin_user_grant_dto.py +105 -0
- moolabs/models/arc_dunning_template_admin_users_response.py +110 -0
- moolabs/models/arc_http_validation_error.py +95 -0
- moolabs/models/arc_transaction_request.py +110 -0
- moolabs/models/arc_validation_error.py +99 -0
- moolabs/models/ask_customer_request.py +97 -0
- moolabs/models/ask_customer_response.py +91 -0
- moolabs/models/ask_moo_redline_request.py +98 -0
- moolabs/models/attribution_allocation_error_detail.py +94 -0
- moolabs/models/attribution_allocation_error_response.py +91 -0
- moolabs/models/attribution_completeness_response.py +97 -0
- moolabs/models/attribution_signal_conflict_detail.py +94 -0
- moolabs/models/attribution_signal_conflict_response.py +91 -0
- moolabs/models/attribution_signal_delta.py +98 -0
- moolabs/models/attribution_signal_request.py +123 -0
- moolabs/models/attribution_signal_response.py +96 -0
- moolabs/models/audit_request.py +91 -0
- moolabs/models/auto_topup_activity_item.py +104 -0
- moolabs/models/auto_topup_activity_response.py +97 -0
- moolabs/models/auto_topup_rule_response.py +114 -0
- moolabs/models/availability.py +43 -0
- moolabs/models/backfill_candidate.py +97 -0
- moolabs/models/backfill_malformed_disputes_request.py +87 -0
- moolabs/models/backfill_malformed_disputes_response.py +101 -0
- moolabs/models/balance_history_window.py +99 -0
- moolabs/models/basis.py +37 -0
- moolabs/models/batch_ingest_request.py +96 -0
- moolabs/models/batch_wallet_state_request.py +105 -0
- moolabs/models/batch_wallet_state_request_item.py +91 -0
- moolabs/models/batch_wallet_state_response.py +97 -0
- moolabs/models/batch_wallets_by_subjects_request.py +88 -0
- moolabs/models/billing_customer_profile.py +109 -0
- moolabs/models/billing_discount_percentage.py +100 -0
- moolabs/models/billing_discount_reason.py +164 -0
- moolabs/models/billing_discount_usage.py +107 -0
- moolabs/models/billing_discounts.py +97 -0
- moolabs/models/billing_import_request.py +105 -0
- moolabs/models/billing_import_response.py +104 -0
- moolabs/models/billing_invoice_customer_extended_details.py +116 -0
- moolabs/models/billing_party.py +110 -0
- moolabs/models/billing_party_replace_update.py +106 -0
- moolabs/models/billing_party_tax_identity.py +88 -0
- moolabs/models/billing_profile.py +140 -0
- moolabs/models/billing_profile_app_references.py +107 -0
- moolabs/models/billing_profile_apps.py +107 -0
- moolabs/models/billing_profile_apps_create.py +113 -0
- moolabs/models/billing_profile_apps_or_reference.py +148 -0
- moolabs/models/billing_profile_create.py +112 -0
- moolabs/models/billing_profile_customer_override.py +116 -0
- moolabs/models/billing_profile_customer_override_create.py +98 -0
- moolabs/models/billing_profile_customer_override_expand.py +37 -0
- moolabs/models/billing_profile_customer_override_order_by.py +40 -0
- moolabs/models/billing_profile_customer_override_with_details.py +113 -0
- moolabs/models/billing_profile_customer_override_with_details_paginated_response.py +101 -0
- moolabs/models/billing_profile_expand.py +36 -0
- moolabs/models/billing_profile_order_by.py +39 -0
- moolabs/models/billing_profile_paginated_response.py +101 -0
- moolabs/models/billing_profile_replace_update_with_workflow.py +106 -0
- moolabs/models/billing_tenant_settings_out.py +89 -0
- moolabs/models/billing_tenant_settings_update.py +88 -0
- moolabs/models/billing_workflow.py +109 -0
- moolabs/models/billing_workflow_collection_alignment.py +150 -0
- moolabs/models/billing_workflow_collection_alignment_anchored.py +100 -0
- moolabs/models/billing_workflow_collection_alignment_subscription.py +94 -0
- moolabs/models/billing_workflow_collection_settings.py +93 -0
- moolabs/models/billing_workflow_create.py +109 -0
- moolabs/models/billing_workflow_invoicing_settings.py +99 -0
- moolabs/models/billing_workflow_payment_settings.py +88 -0
- moolabs/models/billing_workflow_tax_settings.py +89 -0
- moolabs/models/bom_component_in.py +105 -0
- moolabs/models/bom_component_out.py +109 -0
- moolabs/models/bom_create_request.py +107 -0
- moolabs/models/bom_out.py +127 -0
- moolabs/models/bom_update_request.py +103 -0
- moolabs/models/bom_variance_out.py +115 -0
- moolabs/models/breakdown_response.py +124 -0
- moolabs/models/breakdown_row.py +121 -0
- moolabs/models/breakdown_row_kind.py +39 -0
- moolabs/models/budget_alert_response.py +107 -0
- moolabs/models/budget_amount.py +152 -0
- moolabs/models/budget_amount1.py +152 -0
- moolabs/models/budget_create.py +110 -0
- moolabs/models/budget_pace_status.py +38 -0
- moolabs/models/budget_response.py +113 -0
- moolabs/models/budget_status_item.py +105 -0
- moolabs/models/budget_update.py +107 -0
- moolabs/models/budget_with_utilization.py +119 -0
- moolabs/models/bulk_action_request.py +92 -0
- moolabs/models/bulk_replay_request.py +92 -0
- moolabs/models/buyer_accept_request.py +95 -0
- moolabs/models/buyer_otp_request_response.py +91 -0
- moolabs/models/buyer_otp_verify_request.py +95 -0
- moolabs/models/buyer_otp_verify_response.py +89 -0
- moolabs/models/buyer_quote_line_item.py +103 -0
- moolabs/models/buyer_quote_pricing.py +93 -0
- moolabs/models/buyer_quote_projection.py +133 -0
- moolabs/models/buyer_reject_request.py +88 -0
- moolabs/models/buyer_request_changes_request.py +94 -0
- moolabs/models/buyer_request_changes_response.py +93 -0
- moolabs/models/buyer_requested_changes.py +90 -0
- moolabs/models/cancel_subscription_request.py +91 -0
- moolabs/models/card_type.py +37 -0
- moolabs/models/case_create.py +112 -0
- moolabs/models/case_escalate_request.py +90 -0
- moolabs/models/case_invoice_create.py +127 -0
- moolabs/models/case_invoice_response.py +150 -0
- moolabs/models/case_list_item_response.py +171 -0
- moolabs/models/case_list_response.py +101 -0
- moolabs/models/case_pause_request.py +91 -0
- moolabs/models/case_response.py +169 -0
- moolabs/models/case_update.py +102 -0
- moolabs/models/case_write_off_request.py +90 -0
- moolabs/models/cash_app_config_update_request.py +96 -0
- moolabs/models/cei_response.py +102 -0
- moolabs/models/change_item.py +101 -0
- moolabs/models/channel_policy_schema.py +112 -0
- moolabs/models/channel_preference_create.py +99 -0
- moolabs/models/channel_preference_response.py +106 -0
- moolabs/models/channel_preference_update.py +95 -0
- moolabs/models/channel_preferences_response.py +95 -0
- moolabs/models/chat_feedback_in.py +91 -0
- moolabs/models/chat_message_in.py +89 -0
- moolabs/models/chat_request.py +101 -0
- moolabs/models/check_trigger_request.py +88 -0
- moolabs/models/checkout_session_custom_text_after_submit_params.py +109 -0
- moolabs/models/checkout_session_custom_text_after_submit_params_after_submit.py +88 -0
- moolabs/models/checkout_session_custom_text_after_submit_params_shipping_address.py +88 -0
- moolabs/models/checkout_session_custom_text_after_submit_params_submit.py +88 -0
- moolabs/models/checkout_session_custom_text_after_submit_params_terms_of_service_acceptance.py +88 -0
- moolabs/models/checkout_session_ui_mode.py +37 -0
- moolabs/models/clause_citations.py +91 -0
- moolabs/models/clause_family.py +129 -0
- moolabs/models/clause_finding.py +110 -0
- moolabs/models/clause_pack.py +118 -0
- moolabs/models/clause_position.py +91 -0
- moolabs/models/client_app_start_response.py +87 -0
- moolabs/models/cloud_cost_row_input.py +105 -0
- moolabs/models/cloud_provider.py +38 -0
- moolabs/models/cloud_provider_data_through.py +116 -0
- moolabs/models/cloud_provider_quality_axis.py +107 -0
- moolabs/models/cloud_temporal_coverage_summary.py +114 -0
- moolabs/models/collection_method.py +37 -0
- moolabs/models/commercial_basis.py +39 -0
- moolabs/models/commercial_overrides.py +113 -0
- moolabs/models/commercial_overrides_input.py +115 -0
- moolabs/models/commercial_overrides_payload.py +113 -0
- moolabs/models/communication_create.py +110 -0
- moolabs/models/communication_list_response.py +101 -0
- moolabs/models/communication_response.py +138 -0
- moolabs/models/comparison_mode.py +37 -0
- moolabs/models/completeness_response.py +105 -0
- moolabs/models/component_coverage_response.py +123 -0
- moolabs/models/compute_variance_request.py +96 -0
- moolabs/models/compute_variance_response.py +105 -0
- moolabs/models/config_upload_gap_out.py +89 -0
- moolabs/models/config_upload_response.py +109 -0
- moolabs/models/config_upload_summary_out.py +95 -0
- moolabs/models/connector_config_response.py +93 -0
- moolabs/models/connector_health_response.py +102 -0
- moolabs/models/connector_item.py +105 -0
- moolabs/models/contact_create.py +100 -0
- moolabs/models/contact_list_response.py +101 -0
- moolabs/models/contact_response.py +134 -0
- moolabs/models/contact_update.py +102 -0
- moolabs/models/contract_document_response.py +104 -0
- moolabs/models/contract_event_response.py +108 -0
- moolabs/models/contract_inbound_message_response.py +106 -0
- moolabs/models/contract_participant_response.py +131 -0
- moolabs/models/contract_segment_response.py +95 -0
- moolabs/models/contract_thread_response.py +115 -0
- moolabs/models/contract_thread_snapshot_response.py +121 -0
- moolabs/models/contract_upload_detail_response.py +103 -0
- moolabs/models/contract_version_response.py +128 -0
- moolabs/models/contributor_item.py +123 -0
- moolabs/models/contributor_kind.py +38 -0
- moolabs/models/contributor_query_request.py +146 -0
- moolabs/models/contributor_response.py +174 -0
- moolabs/models/cost.py +152 -0
- moolabs/models/cost_adjustment_create.py +131 -0
- moolabs/models/cost_adjustment_response.py +102 -0
- moolabs/models/cost_driver_rule_create_request.py +158 -0
- moolabs/models/cost_driver_rule_response.py +126 -0
- moolabs/models/cost_driver_rule_update_request.py +100 -0
- moolabs/models/cost_event_detail_response.py +132 -0
- moolabs/models/cost_event_ingest.py +177 -0
- moolabs/models/cost_event_response.py +122 -0
- moolabs/models/cost_event_summary_item.py +103 -0
- moolabs/models/cost_event_summary_request.py +88 -0
- moolabs/models/cost_event_summary_response.py +95 -0
- moolabs/models/cost_explorer_response.py +155 -0
- moolabs/models/cost_scope.py +37 -0
- moolabs/models/coverage_snapshot_response.py +114 -0
- moolabs/models/cpu_core_hours.py +152 -0
- moolabs/models/cpu_cost_weight.py +152 -0
- moolabs/models/create_allocation_run_api422_response.py +147 -0
- moolabs/models/create_api_key_request.py +89 -0
- moolabs/models/create_api_key_response.py +105 -0
- moolabs/models/create_auto_topup_rule_request.py +106 -0
- moolabs/models/create_checkout_session_tax_id_collection.py +90 -0
- moolabs/models/create_checkout_session_tax_id_collection_required.py +37 -0
- moolabs/models/create_contract_thread_request.py +87 -0
- moolabs/models/create_credit_request.py +97 -0
- moolabs/models/create_fx_rate_request.py +103 -0
- moolabs/models/create_grant_request.py +136 -0
- moolabs/models/create_mapping_request.py +91 -0
- moolabs/models/create_matrix_price_row_request.py +91 -0
- moolabs/models/create_matrix_price_row_response.py +89 -0
- moolabs/models/create_portal_token_response.py +91 -0
- moolabs/models/create_quote_redline_request.py +103 -0
- moolabs/models/create_quote_request.py +119 -0
- moolabs/models/create_quote_template_request.py +102 -0
- moolabs/models/create_rate_card_request.py +105 -0
- moolabs/models/create_reconstruction_run_request.py +101 -0
- moolabs/models/create_session_response.py +97 -0
- moolabs/models/create_snapshot_request.py +98 -0
- moolabs/models/create_stripe_checkout_session_billing_address_collection.py +37 -0
- moolabs/models/create_stripe_checkout_session_consent_collection.py +97 -0
- moolabs/models/create_stripe_checkout_session_consent_collection_payment_method_reuse_agreement.py +88 -0
- moolabs/models/create_stripe_checkout_session_consent_collection_payment_method_reuse_agreement_position.py +37 -0
- moolabs/models/create_stripe_checkout_session_consent_collection_promotions.py +37 -0
- moolabs/models/create_stripe_checkout_session_consent_collection_terms_of_service.py +37 -0
- moolabs/models/create_stripe_checkout_session_customer_update.py +92 -0
- moolabs/models/create_stripe_checkout_session_customer_update_behavior.py +37 -0
- moolabs/models/create_stripe_checkout_session_redirect_on_completion.py +38 -0
- moolabs/models/create_stripe_checkout_session_request.py +112 -0
- moolabs/models/create_stripe_checkout_session_request_customer.py +163 -0
- moolabs/models/create_stripe_checkout_session_request_options.py +147 -0
- moolabs/models/create_stripe_checkout_session_result.py +139 -0
- moolabs/models/create_stripe_customer_portal_session_params.py +91 -0
- moolabs/models/create_threshold_request.py +99 -0
- moolabs/models/create_value_recognition_request.py +100 -0
- moolabs/models/create_wallet_request.py +106 -0
- moolabs/models/credit_config.py +119 -0
- moolabs/models/credit_config_payload.py +135 -0
- moolabs/models/credit_cost_ratio_item.py +93 -0
- moolabs/models/credit_cost_ratio_response.py +101 -0
- moolabs/models/credit_memo_apply_request.py +90 -0
- moolabs/models/credit_memo_create.py +104 -0
- moolabs/models/credit_memo_create_internal.py +114 -0
- moolabs/models/credit_memo_create_public.py +108 -0
- moolabs/models/credit_memo_item.py +102 -0
- moolabs/models/credit_memo_line_in.py +90 -0
- moolabs/models/credit_memo_list_response.py +101 -0
- moolabs/models/credit_memo_out.py +138 -0
- moolabs/models/credit_memo_response.py +124 -0
- moolabs/models/credit_memo_update.py +94 -0
- moolabs/models/credit_note_original_invoice_ref.py +114 -0
- moolabs/models/credit_override.py +87 -0
- moolabs/models/credit_override_input.py +101 -0
- moolabs/models/credit_override_payload.py +100 -0
- moolabs/models/crm_quote_context_request.py +94 -0
- moolabs/models/crm_quote_context_response.py +101 -0
- moolabs/models/crm_quote_draft_request.py +98 -0
- moolabs/models/crm_quote_draft_response.py +107 -0
- moolabs/models/crm_quote_record_card_response.py +97 -0
- moolabs/models/crm_surface_response.py +97 -0
- moolabs/models/currency.py +101 -0
- moolabs/models/current.py +152 -0
- moolabs/models/custom_invoicing_app.py +142 -0
- moolabs/models/custom_invoicing_app_replace_update.py +105 -0
- moolabs/models/custom_invoicing_customer_app_data.py +117 -0
- moolabs/models/custom_invoicing_draft_synchronized_request.py +91 -0
- moolabs/models/custom_invoicing_finalized_invoicing_request.py +91 -0
- moolabs/models/custom_invoicing_finalized_payment_request.py +87 -0
- moolabs/models/custom_invoicing_finalized_request.py +97 -0
- moolabs/models/custom_invoicing_line_discount_external_id_mapping.py +97 -0
- moolabs/models/custom_invoicing_line_external_id_mapping.py +97 -0
- moolabs/models/custom_invoicing_payment_trigger.py +41 -0
- moolabs/models/custom_invoicing_sync_result.py +110 -0
- moolabs/models/custom_invoicing_tax_config.py +87 -0
- moolabs/models/custom_invoicing_update_payment_status_request.py +92 -0
- moolabs/models/custom_plan_input.py +136 -0
- moolabs/models/custom_subscription_change.py +119 -0
- moolabs/models/custom_subscription_create.py +153 -0
- moolabs/models/customer.py +172 -0
- moolabs/models/customer_access.py +102 -0
- moolabs/models/customer_app_data.py +164 -0
- moolabs/models/customer_app_data_create_or_update_item.py +164 -0
- moolabs/models/customer_app_data_paginated_response.py +101 -0
- moolabs/models/customer_cost_item.py +95 -0
- moolabs/models/customer_create.py +120 -0
- moolabs/models/customer_expand.py +36 -0
- moolabs/models/customer_id.py +95 -0
- moolabs/models/customer_item.py +114 -0
- moolabs/models/customer_key.py +87 -0
- moolabs/models/customer_key_page_response.py +101 -0
- moolabs/models/customer_key_put_request.py +103 -0
- moolabs/models/customer_key_response.py +94 -0
- moolabs/models/customer_margin_item.py +99 -0
- moolabs/models/customer_margin_response.py +101 -0
- moolabs/models/customer_order_by.py +38 -0
- moolabs/models/customer_paginated_response.py +101 -0
- moolabs/models/customer_replace_update.py +120 -0
- moolabs/models/customer_subscription_order_by.py +37 -0
- moolabs/models/customer_upsert_request.py +102 -0
- moolabs/models/customer_usage_attribution.py +88 -0
- moolabs/models/danger_request.py +89 -0
- moolabs/models/dashboard_meta.py +172 -0
- moolabs/models/dashboard_overview_response.py +115 -0
- moolabs/models/data_through.py +115 -0
- moolabs/models/db_time_coverage_response.py +154 -0
- moolabs/models/db_time_evidence_batch_readback_response.py +135 -0
- moolabs/models/db_time_evidence_batch_request.py +147 -0
- moolabs/models/db_time_evidence_batch_response.py +100 -0
- moolabs/models/db_time_evidence_conflict_detail.py +94 -0
- moolabs/models/db_time_evidence_conflict_response.py +91 -0
- moolabs/models/db_time_evidence_not_found_detail.py +94 -0
- moolabs/models/db_time_evidence_not_found_response.py +91 -0
- moolabs/models/db_time_observation_request.py +131 -0
- moolabs/models/db_time_readout_quality_response.py +103 -0
- moolabs/models/deal_context.py +97 -0
- moolabs/models/deal_context_rule.py +105 -0
- moolabs/models/decision_item.py +131 -0
- moolabs/models/decision_kind.py +46 -0
- moolabs/models/decision_overview_response.py +172 -0
- moolabs/models/decision_queue_response.py +115 -0
- moolabs/models/decision_reason_code.py +53 -0
- moolabs/models/delta.py +152 -0
- moolabs/models/delta_pct.py +152 -0
- moolabs/models/demo_seed_acute_history_baseline_request.py +106 -0
- moolabs/models/demo_seed_acute_tenant_registration_request.py +114 -0
- moolabs/models/demo_seed_agent_report_baseline_request.py +106 -0
- moolabs/models/demo_seed_arc_invoice_ref.py +107 -0
- moolabs/models/demo_seed_arc_portfolio_baseline_request.py +121 -0
- moolabs/models/demo_seed_catalog_baseline_request.py +108 -0
- moolabs/models/demo_seed_customer_portfolio_baseline_request.py +103 -0
- moolabs/models/demo_seed_integration_crm_baseline_request.py +106 -0
- moolabs/models/demo_seed_integration_finance_baseline_request.py +106 -0
- moolabs/models/demo_seed_integration_netsuite_baseline_request.py +106 -0
- moolabs/models/demo_seed_integration_webhook_baseline_request.py +106 -0
- moolabs/models/demo_seed_invoice_payment_baseline_request.py +106 -0
- moolabs/models/demo_seed_isolation_canary_baseline_request.py +106 -0
- moolabs/models/demo_seed_lifecycle_request.py +103 -0
- moolabs/models/demo_seed_quote_contract_baseline_request.py +106 -0
- moolabs/models/demo_seed_security_credentials_baseline_request.py +106 -0
- moolabs/models/demo_seed_settings_baseline_request.py +106 -0
- moolabs/models/demo_seed_subscription_baseline_request.py +106 -0
- moolabs/models/demo_seed_tenant_config_baseline_request.py +119 -0
- moolabs/models/demo_seed_usage_baseline_request.py +106 -0
- moolabs/models/derive_request.py +94 -0
- moolabs/models/dimension.py +39 -0
- moolabs/models/dimension_coverage_response.py +102 -0
- moolabs/models/disclosure_policy_response.py +93 -0
- moolabs/models/disclosure_policy_update_request.py +102 -0
- moolabs/models/discount_percentage.py +87 -0
- moolabs/models/discount_reason_maximum_spend.py +94 -0
- moolabs/models/discount_reason_ratecard_percentage.py +109 -0
- moolabs/models/discount_reason_ratecard_usage.py +116 -0
- moolabs/models/discount_usage.py +95 -0
- moolabs/models/discounts.py +97 -0
- moolabs/models/dismiss_refinement_request.py +87 -0
- moolabs/models/dispute_approval_request.py +88 -0
- moolabs/models/dispute_list_response.py +101 -0
- moolabs/models/dispute_resolve_request.py +92 -0
- moolabs/models/dispute_response.py +134 -0
- moolabs/models/dispute_summary_response.py +104 -0
- moolabs/models/dispute_update_request.py +102 -0
- moolabs/models/dns_record.py +119 -0
- moolabs/models/domain_response.py +95 -0
- moolabs/models/domain_update.py +87 -0
- moolabs/models/draft_lock_request.py +88 -0
- moolabs/models/draft_lock_response.py +97 -0
- moolabs/models/draft_save_request.py +114 -0
- moolabs/models/draft_unlock_request.py +90 -0
- moolabs/models/driver_item.py +122 -0
- moolabs/models/dry_run_request.py +89 -0
- moolabs/models/dry_run_response.py +97 -0
- moolabs/models/dso_response.py +100 -0
- moolabs/models/dynamic_price_with_commitments.py +131 -0
- moolabs/models/economics_summary.py +114 -0
- moolabs/models/edit_subscription_add_item.py +102 -0
- moolabs/models/edit_subscription_add_phase.py +100 -0
- moolabs/models/edit_subscription_remove_item.py +98 -0
- moolabs/models/edit_subscription_remove_phase.py +99 -0
- moolabs/models/edit_subscription_stretch_phase.py +98 -0
- moolabs/models/edit_subscription_unschedule_edit.py +94 -0
- moolabs/models/effective_permission_grant_dto.py +105 -0
- moolabs/models/effective_permissions_response.py +129 -0
- moolabs/models/email_config_out.py +188 -0
- moolabs/models/email_config_upsert.py +111 -0
- moolabs/models/email_context.py +44 -0
- moolabs/models/email_message_status.py +42 -0
- moolabs/models/email_provider.py +37 -0
- moolabs/models/email_provider_identity.py +157 -0
- moolabs/models/email_provider_identity_list.py +108 -0
- moolabs/models/email_provider_operator_upsert.py +131 -0
- moolabs/models/email_provider_route.py +128 -0
- moolabs/models/email_provider_route_list.py +108 -0
- moolabs/models/enable_tier3_response.py +93 -0
- moolabs/models/endpoints_response.py +89 -0
- moolabs/models/entitlement.py +164 -0
- moolabs/models/entitlement_boolean.py +168 -0
- moolabs/models/entitlement_boolean_create_inputs.py +127 -0
- moolabs/models/entitlement_boolean_v2.py +170 -0
- moolabs/models/entitlement_create_inputs.py +164 -0
- moolabs/models/entitlement_grant.py +148 -0
- moolabs/models/entitlement_grant_create_input.py +111 -0
- moolabs/models/entitlement_grant_create_input_v2.py +113 -0
- moolabs/models/entitlement_grant_v2.py +148 -0
- moolabs/models/entitlement_metered.py +190 -0
- moolabs/models/entitlement_metered_create_inputs.py +143 -0
- moolabs/models/entitlement_metered_v2.py +196 -0
- moolabs/models/entitlement_metered_v2_create_inputs.py +157 -0
- moolabs/models/entitlement_order_by.py +37 -0
- moolabs/models/entitlement_override.py +89 -0
- moolabs/models/entitlement_override_input.py +95 -0
- moolabs/models/entitlement_override_payload.py +93 -0
- moolabs/models/entitlement_paginated_response.py +101 -0
- moolabs/models/entitlement_payload.py +103 -0
- moolabs/models/entitlement_static.py +170 -0
- moolabs/models/entitlement_static_create_inputs.py +129 -0
- moolabs/models/entitlement_static_v2.py +172 -0
- moolabs/models/entitlement_type.py +38 -0
- moolabs/models/entitlement_v2.py +164 -0
- moolabs/models/entitlement_v2_create_inputs.py +164 -0
- moolabs/models/entitlement_v2_paginated_response.py +101 -0
- moolabs/models/entitlement_value.py +109 -0
- moolabs/models/error_extension.py +110 -0
- moolabs/models/escalate_request.py +90 -0
- moolabs/models/escalate_response.py +91 -0
- moolabs/models/escalation_action_request.py +90 -0
- moolabs/models/escalation_action_response.py +91 -0
- moolabs/models/escalation_create.py +97 -0
- moolabs/models/escalation_dismiss_request.py +90 -0
- moolabs/models/escalation_list_response.py +101 -0
- moolabs/models/escalation_resolve_request.py +87 -0
- moolabs/models/escalation_response.py +112 -0
- moolabs/models/escalation_update.py +93 -0
- moolabs/models/event.py +115 -0
- moolabs/models/event_delivery_attempt_response.py +101 -0
- moolabs/models/evidence_create_request.py +96 -0
- moolabs/models/evidence_kind.py +39 -0
- moolabs/models/evidence_list_response.py +97 -0
- moolabs/models/evidence_response.py +106 -0
- moolabs/models/evidence_review_request.py +90 -0
- moolabs/models/evidence_window.py +37 -0
- moolabs/models/exchange_rate.py +152 -0
- moolabs/models/expiration_duration.py +40 -0
- moolabs/models/expiration_period.py +91 -0
- moolabs/models/explorer_alias_hint.py +104 -0
- moolabs/models/explorer_alias_hint1.py +110 -0
- moolabs/models/explorer_alias_kind.py +39 -0
- moolabs/models/explorer_breakdown_page.py +166 -0
- moolabs/models/explorer_breakdown_row.py +200 -0
- moolabs/models/explorer_cloud_provider_quality_axis.py +107 -0
- moolabs/models/explorer_cloud_temporal_coverage_summary.py +115 -0
- moolabs/models/explorer_contributor_kind.py +40 -0
- moolabs/models/explorer_contributor_query_request.py +190 -0
- moolabs/models/explorer_contributor_response.py +201 -0
- moolabs/models/explorer_contributor_row.py +177 -0
- moolabs/models/explorer_cost_event_status.py +39 -0
- moolabs/models/explorer_dimension.py +40 -0
- moolabs/models/explorer_effective_sort.py +107 -0
- moolabs/models/explorer_effective_sort_field.py +41 -0
- moolabs/models/explorer_evidence_kind.py +41 -0
- moolabs/models/explorer_evidence_role.py +37 -0
- moolabs/models/explorer_excluded_event_count.py +104 -0
- moolabs/models/explorer_excluded_event_reason.py +38 -0
- moolabs/models/explorer_exposure_summary.py +124 -0
- moolabs/models/explorer_measure_source.py +40 -0
- moolabs/models/explorer_meta.py +196 -0
- moolabs/models/explorer_quality_axis.py +117 -0
- moolabs/models/explorer_quality_caveat_code.py +45 -0
- moolabs/models/explorer_quality_summary.py +151 -0
- moolabs/models/explorer_query_request.py +184 -0
- moolabs/models/explorer_request_detail_event.py +134 -0
- moolabs/models/explorer_request_detail_line.py +133 -0
- moolabs/models/explorer_request_detail_query_request.py +201 -0
- moolabs/models/explorer_request_detail_response.py +205 -0
- moolabs/models/explorer_request_detail_result_cap.py +125 -0
- moolabs/models/explorer_request_distribution.py +109 -0
- moolabs/models/explorer_request_query_request.py +190 -0
- moolabs/models/explorer_search.py +111 -0
- moolabs/models/explorer_selection_result.py +107 -0
- moolabs/models/explorer_selection_row.py +185 -0
- moolabs/models/explorer_selection_status.py +39 -0
- moolabs/models/explorer_sort.py +104 -0
- moolabs/models/explorer_sort1.py +116 -0
- moolabs/models/explorer_sort_fallback_reason.py +39 -0
- moolabs/models/explorer_sort_field.py +40 -0
- moolabs/models/explorer_source_scope.py +38 -0
- moolabs/models/explorer_summary.py +134 -0
- moolabs/models/explorer_trend_point.py +124 -0
- moolabs/models/explorer_usage_history_evidence.py +110 -0
- moolabs/models/explorer_usage_history_source.py +38 -0
- moolabs/models/feature.py +156 -0
- moolabs/models/feature_contributor_customer.py +91 -0
- moolabs/models/feature_contributor_model.py +93 -0
- moolabs/models/feature_contributor_request.py +91 -0
- moolabs/models/feature_contributors_response.py +121 -0
- moolabs/models/feature_cost_item.py +97 -0
- moolabs/models/feature_create_inputs.py +128 -0
- moolabs/models/feature_flag_item.py +97 -0
- moolabs/models/feature_flag_update.py +87 -0
- moolabs/models/feature_margin_item.py +101 -0
- moolabs/models/feature_margin_response.py +101 -0
- moolabs/models/feature_meta.py +97 -0
- moolabs/models/feature_order_by.py +40 -0
- moolabs/models/feature_override_request.py +93 -0
- moolabs/models/feature_paginated_response.py +101 -0
- moolabs/models/feature_pool.py +99 -0
- moolabs/models/feature_pool_payload.py +107 -0
- moolabs/models/feature_price_request.py +93 -0
- moolabs/models/feature_pricing_input.py +120 -0
- moolabs/models/filter_id_exact.py +88 -0
- moolabs/models/filter_string.py +129 -0
- moolabs/models/filter_time.py +114 -0
- moolabs/models/finance_summary.py +143 -0
- moolabs/models/first_pass_redline_request.py +88 -0
- moolabs/models/first_pass_redline_response.py +89 -0
- moolabs/models/flag_disputed_request.py +92 -0
- moolabs/models/flag_disputed_response.py +97 -0
- moolabs/models/flat_price.py +104 -0
- moolabs/models/flat_price_with_payment_term.py +107 -0
- moolabs/models/flatamount.py +152 -0
- moolabs/models/freshness_axis.py +121 -0
- moolabs/models/freshness_source.py +40 -0
- moolabs/models/fx_rate_to_reporting.py +153 -0
- moolabs/models/fx_rates_list_response.py +97 -0
- moolabs/models/gate_status_response.py +105 -0
- moolabs/models/get_fx_rate_request.py +92 -0
- moolabs/models/get_matrix_price_rows_response.py +107 -0
- moolabs/models/google_chat_quote_response.py +89 -0
- moolabs/models/grant_activation_request.py +122 -0
- moolabs/models/grant_burn_down_history_segment.py +125 -0
- moolabs/models/grant_order_by.py +38 -0
- moolabs/models/grant_paginated_response.py +101 -0
- moolabs/models/grant_plan_change_request.py +126 -0
- moolabs/models/grant_renewal_request.py +122 -0
- moolabs/models/grant_response.py +128 -0
- moolabs/models/grant_source_type.py +40 -0
- moolabs/models/grant_usage_record.py +97 -0
- moolabs/models/grant_v2_paginated_response.py +101 -0
- moolabs/models/grants_list_response.py +99 -0
- moolabs/models/handoff_create_request.py +92 -0
- moolabs/models/helicone_config_request.py +92 -0
- moolabs/models/homepage_response.py +107 -0
- moolabs/models/hosted_checkout_settlement_ptp_response.py +100 -0
- moolabs/models/hosted_checkout_settlement_request.py +109 -0
- moolabs/models/hosted_checkout_settlement_response.py +97 -0
- moolabs/models/http_validation_error.py +95 -0
- moolabs/models/hub_spot_connect_request.py +91 -0
- moolabs/models/id_resource.py +97 -0
- moolabs/models/identity_scope.py +39 -0
- moolabs/models/identity_scope_coverage_response.py +96 -0
- moolabs/models/import_batch_detail_response.py +105 -0
- moolabs/models/import_batch_detail_row.py +111 -0
- moolabs/models/import_batch_request.py +112 -0
- moolabs/models/import_batch_response.py +101 -0
- moolabs/models/import_batch_summary.py +111 -0
- moolabs/models/import_dispute_item.py +122 -0
- moolabs/models/import_disputes_request.py +96 -0
- moolabs/models/import_invoice_item.py +133 -0
- moolabs/models/import_invoices_request.py +96 -0
- moolabs/models/import_promise_item.py +155 -0
- moolabs/models/import_promises_request.py +96 -0
- moolabs/models/inbound_email_attachment.py +91 -0
- moolabs/models/inbound_email_payload.py +103 -0
- moolabs/models/inbound_email_process_response.py +97 -0
- moolabs/models/ingest_events_body.py +150 -0
- moolabs/models/ingest_health.py +97 -0
- moolabs/models/ingested_event.py +111 -0
- moolabs/models/ingested_event_cursor_paginated_response.py +98 -0
- moolabs/models/initialamount.py +152 -0
- moolabs/models/insight_card.py +111 -0
- moolabs/models/install_method.py +38 -0
- moolabs/models/integration_update.py +89 -0
- moolabs/models/invalidate_portal_token_request.py +89 -0
- moolabs/models/invalidate_portal_tokens_request.py +89 -0
- moolabs/models/invoice.py +249 -0
- moolabs/models/invoice_allocation_out.py +103 -0
- moolabs/models/invoice_app_external_ids.py +97 -0
- moolabs/models/invoice_available_action_details.py +89 -0
- moolabs/models/invoice_available_actions.py +132 -0
- moolabs/models/invoice_detailed_line.py +244 -0
- moolabs/models/invoice_detailed_line_cost_category.py +37 -0
- moolabs/models/invoice_detailed_line_rate_card.py +116 -0
- moolabs/models/invoice_document_ref_type.py +36 -0
- moolabs/models/invoice_expand.py +38 -0
- moolabs/models/invoice_generic_document_ref.py +98 -0
- moolabs/models/invoice_item.py +112 -0
- moolabs/models/invoice_line.py +296 -0
- moolabs/models/invoice_line_amount_discount.py +141 -0
- moolabs/models/invoice_line_app_external_ids.py +93 -0
- moolabs/models/invoice_line_discounts.py +105 -0
- moolabs/models/invoice_line_managed_by.py +38 -0
- moolabs/models/invoice_line_out.py +99 -0
- moolabs/models/invoice_line_replace_update.py +143 -0
- moolabs/models/invoice_line_status.py +38 -0
- moolabs/models/invoice_line_subscription_reference.py +115 -0
- moolabs/models/invoice_line_tax_behavior.py +37 -0
- moolabs/models/invoice_line_tax_item.py +117 -0
- moolabs/models/invoice_line_usage_discount.py +155 -0
- moolabs/models/invoice_order_by.py +41 -0
- moolabs/models/invoice_paginated_response.py +101 -0
- moolabs/models/invoice_payment_terms.py +91 -0
- moolabs/models/invoice_pending_line_create.py +131 -0
- moolabs/models/invoice_pending_line_create_input.py +105 -0
- moolabs/models/invoice_pending_line_create_response.py +107 -0
- moolabs/models/invoice_pending_lines_action_filters_input.py +88 -0
- moolabs/models/invoice_pending_lines_action_input.py +106 -0
- moolabs/models/invoice_reference.py +101 -0
- moolabs/models/invoice_replace_update.py +117 -0
- moolabs/models/invoice_simulation_input.py +107 -0
- moolabs/models/invoice_simulation_line.py +164 -0
- moolabs/models/invoice_status.py +44 -0
- moolabs/models/invoice_status_details.py +103 -0
- moolabs/models/invoice_totals.py +163 -0
- moolabs/models/invoice_type.py +37 -0
- moolabs/models/invoice_upsert_request.py +112 -0
- moolabs/models/invoice_usage_based_rate_card.py +116 -0
- moolabs/models/invoice_workflow_invoicing_settings_replace_update.py +97 -0
- moolabs/models/invoice_workflow_replace_update.py +91 -0
- moolabs/models/invoice_workflow_settings.py +111 -0
- moolabs/models/invoice_workflow_settings_replace_update.py +97 -0
- moolabs/models/issue_after_reset.py +90 -0
- moolabs/models/jurisdiction_profile_response.py +102 -0
- moolabs/models/jurisdiction_resolution_request.py +92 -0
- moolabs/models/kill_switch_request.py +87 -0
- moolabs/models/langfuse_config_request.py +92 -0
- moolabs/models/lesson_action_request.py +89 -0
- moolabs/models/level_in.py +91 -0
- moolabs/models/lifecycle_event_request.py +97 -0
- moolabs/models/line_item_create.py +118 -0
- moolabs/models/line_total.py +153 -0
- moolabs/models/link_net_suite_account_request.py +89 -0
- moolabs/models/list_auto_topup_rules_response.py +97 -0
- moolabs/models/list_connectors200_response_inner.py +148 -0
- moolabs/models/list_entitlements_result.py +151 -0
- moolabs/models/list_events_v2_filter_param.py +123 -0
- moolabs/models/list_features_result.py +151 -0
- moolabs/models/list_grants_get200_response.py +151 -0
- moolabs/models/mapping_response.py +97 -0
- moolabs/models/mapping_rule_create.py +105 -0
- moolabs/models/mapping_rule_response.py +118 -0
- moolabs/models/mapping_rule_update.py +103 -0
- moolabs/models/margin_snapshot_response.py +131 -0
- moolabs/models/marketplace_app_api_key_install_request.py +91 -0
- moolabs/models/marketplace_install_request_payload.py +89 -0
- moolabs/models/marketplace_install_response.py +94 -0
- moolabs/models/marketplace_listing.py +105 -0
- moolabs/models/marketplace_listing_paginated_response.py +101 -0
- moolabs/models/matrix_price_row.py +91 -0
- moolabs/models/measure.py +146 -0
- moolabs/models/measure_kind.py +41 -0
- moolabs/models/measure_usage_from.py +151 -0
- moolabs/models/measure_usage_from_preset.py +37 -0
- moolabs/models/merge_remittances_request.py +89 -0
- moolabs/models/meter.py +176 -0
- moolabs/models/meter_aggregation.py +42 -0
- moolabs/models/meter_create.py +113 -0
- moolabs/models/meter_mapping_item.py +95 -0
- moolabs/models/meter_order_by.py +40 -0
- moolabs/models/meter_query_request.py +121 -0
- moolabs/models/meter_query_result.py +103 -0
- moolabs/models/meter_query_row.py +98 -0
- moolabs/models/meter_test_event_request.py +87 -0
- moolabs/models/meter_test_event_response.py +93 -0
- moolabs/models/meter_update.py +94 -0
- moolabs/models/meter_usage_summary_item.py +103 -0
- moolabs/models/meter_usage_summary_response.py +95 -0
- moolabs/models/meter_validation_error.py +101 -0
- moolabs/models/migrate_subscription_request.py +105 -0
- moolabs/models/model_cost_item.py +95 -0
- moolabs/models/net_suite_connect_request.py +103 -0
- moolabs/models/net_suite_cutover_guard_request.py +87 -0
- moolabs/models/net_suite_m2_m_finalize_request.py +87 -0
- moolabs/models/net_suite_m2_m_setup_request.py +101 -0
- moolabs/models/net_suite_managed_portal_backfill_cancel_request.py +87 -0
- moolabs/models/net_suite_managed_portal_backfill_request.py +89 -0
- moolabs/models/net_suite_managed_portal_backfill_run_request.py +90 -0
- moolabs/models/net_suite_managed_portal_mapping_delete_request.py +91 -0
- moolabs/models/net_suite_managed_portal_mapping_request.py +97 -0
- moolabs/models/net_suite_managed_portal_protection_ack_request.py +89 -0
- moolabs/models/next_action_type.py +41 -0
- moolabs/models/note_create.py +94 -0
- moolabs/models/note_list_response.py +101 -0
- moolabs/models/note_response.py +104 -0
- moolabs/models/notification_channel_meta.py +100 -0
- moolabs/models/notification_channel_order_by.py +39 -0
- moolabs/models/notification_channel_paginated_response.py +101 -0
- moolabs/models/notification_channel_type.py +36 -0
- moolabs/models/notification_channel_webhook.py +145 -0
- moolabs/models/notification_channel_webhook_create_request.py +117 -0
- moolabs/models/notification_event.py +137 -0
- moolabs/models/notification_event_balance_threshold_payload.py +121 -0
- moolabs/models/notification_event_balance_threshold_payload_data.py +133 -0
- moolabs/models/notification_event_delivery_attempt.py +103 -0
- moolabs/models/notification_event_delivery_status.py +125 -0
- moolabs/models/notification_event_delivery_status_state.py +40 -0
- moolabs/models/notification_event_entitlement_value_payload_base.py +125 -0
- moolabs/models/notification_event_invoice_created_payload.py +121 -0
- moolabs/models/notification_event_invoice_updated_payload.py +121 -0
- moolabs/models/notification_event_order_by.py +37 -0
- moolabs/models/notification_event_paginated_response.py +101 -0
- moolabs/models/notification_event_payload.py +178 -0
- moolabs/models/notification_event_resend_request.py +88 -0
- moolabs/models/notification_event_reset_payload.py +121 -0
- moolabs/models/notification_event_type.py +39 -0
- moolabs/models/notification_rule.py +178 -0
- moolabs/models/notification_rule_balance_threshold.py +159 -0
- moolabs/models/notification_rule_balance_threshold_create_request.py +115 -0
- moolabs/models/notification_rule_balance_threshold_value.py +90 -0
- moolabs/models/notification_rule_balance_threshold_value_type.py +40 -0
- moolabs/models/notification_rule_create_request.py +178 -0
- moolabs/models/notification_rule_entitlement_reset.py +149 -0
- moolabs/models/notification_rule_entitlement_reset_create_request.py +105 -0
- moolabs/models/notification_rule_invoice_created.py +139 -0
- moolabs/models/notification_rule_invoice_created_create_request.py +103 -0
- moolabs/models/notification_rule_invoice_updated.py +139 -0
- moolabs/models/notification_rule_invoice_updated_create_request.py +103 -0
- moolabs/models/notification_rule_order_by.py +39 -0
- moolabs/models/notification_rule_paginated_response.py +101 -0
- moolabs/models/notify_channel.py +89 -0
- moolabs/models/o_auth2_authorization_code_grant_error_type.py +42 -0
- moolabs/models/observation_out.py +103 -0
- moolabs/models/observed_total_cost.py +153 -0
- moolabs/models/open_cost_cluster_binding.py +115 -0
- moolabs/models/open_cost_dedicated_namespace.py +104 -0
- moolabs/models/open_cost_installation_create_request.py +122 -0
- moolabs/models/open_cost_installation_create_response.py +116 -0
- moolabs/models/open_cost_installation_response.py +114 -0
- moolabs/models/open_cost_snapshot_request.py +139 -0
- moolabs/models/open_cost_snapshot_response.py +125 -0
- moolabs/models/open_cost_trust_policy.py +145 -0
- moolabs/models/open_cost_wire_row.py +140 -0
- moolabs/models/open_from_scope_request.py +98 -0
- moolabs/models/operations_summary.py +120 -0
- moolabs/models/opt_out_request.py +90 -0
- moolabs/models/opt_out_response.py +93 -0
- moolabs/models/org_role.py +152 -0
- moolabs/models/original_total.py +152 -0
- moolabs/models/otlp_ingest_response.py +91 -0
- moolabs/models/overall_freshness.py +116 -0
- moolabs/models/overdraft_settings.py +93 -0
- moolabs/models/overdraft_settings_payload.py +93 -0
- moolabs/models/override_first_ingress_request.py +94 -0
- moolabs/models/override_request.py +90 -0
- moolabs/models/override_response.py +91 -0
- moolabs/models/overview_response.py +109 -0
- moolabs/models/pack_provenance.py +101 -0
- moolabs/models/package_price_with_commitments.py +137 -0
- moolabs/models/paginated_credit_memos.py +97 -0
- moolabs/models/paginated_customers.py +99 -0
- moolabs/models/paginated_invoices.py +97 -0
- moolabs/models/paginated_payments.py +97 -0
- moolabs/models/paginated_sync_runs.py +97 -0
- moolabs/models/patch_quote_agent_policy_request.py +98 -0
- moolabs/models/patch_rows_request.py +95 -0
- moolabs/models/patch_session_request.py +107 -0
- moolabs/models/payment_allocation_response.py +104 -0
- moolabs/models/payment_create.py +109 -0
- moolabs/models/payment_due_date.py +124 -0
- moolabs/models/payment_failed_request.py +101 -0
- moolabs/models/payment_failed_response.py +93 -0
- moolabs/models/payment_instructions_response.py +95 -0
- moolabs/models/payment_instructions_update_request.py +90 -0
- moolabs/models/payment_item.py +104 -0
- moolabs/models/payment_link_request.py +105 -0
- moolabs/models/payment_link_response.py +89 -0
- moolabs/models/payment_list_response.py +101 -0
- moolabs/models/payment_match_request.py +90 -0
- moolabs/models/payment_response.py +136 -0
- moolabs/models/payment_reversal_request.py +90 -0
- moolabs/models/payment_succeeded_request.py +103 -0
- moolabs/models/payment_succeeded_response.py +97 -0
- moolabs/models/payment_term_due_date.py +113 -0
- moolabs/models/payment_term_instant.py +102 -0
- moolabs/models/payment_terms.py +148 -0
- moolabs/models/pending_provider_confirmation_reconcile_request.py +88 -0
- moolabs/models/pending_sns_confirmation_out.py +102 -0
- moolabs/models/period.py +90 -0
- moolabs/models/period_settlement_out.py +143 -0
- moolabs/models/period_window.py +103 -0
- moolabs/models/persona.py +39 -0
- moolabs/models/plan.py +191 -0
- moolabs/models/plan_addon.py +126 -0
- moolabs/models/plan_addon_create.py +101 -0
- moolabs/models/plan_addon_order_by.py +40 -0
- moolabs/models/plan_addon_paginated_response.py +101 -0
- moolabs/models/plan_addon_replace_update.py +91 -0
- moolabs/models/plan_amend_request.py +103 -0
- moolabs/models/plan_cost_item.py +97 -0
- moolabs/models/plan_create.py +140 -0
- moolabs/models/plan_create_request.py +103 -0
- moolabs/models/plan_order_by.py +40 -0
- moolabs/models/plan_paginated_response.py +101 -0
- moolabs/models/plan_phase.py +113 -0
- moolabs/models/plan_pricing_input.py +103 -0
- moolabs/models/plan_pricing_sync_request.py +105 -0
- moolabs/models/plan_pricing_view.py +107 -0
- moolabs/models/plan_ref_payload.py +91 -0
- moolabs/models/plan_reference.py +106 -0
- moolabs/models/plan_reference_input.py +97 -0
- moolabs/models/plan_replace_update.py +122 -0
- moolabs/models/plan_status.py +39 -0
- moolabs/models/plan_subscription_change.py +153 -0
- moolabs/models/plan_subscription_create.py +167 -0
- moolabs/models/policy_update_request.py +94 -0
- moolabs/models/pool_notification_config_request.py +87 -0
- moolabs/models/pooloverrides_value.py +152 -0
- moolabs/models/portal_case_response.py +108 -0
- moolabs/models/portal_channel_preference_request.py +100 -0
- moolabs/models/portal_dispute_request.py +94 -0
- moolabs/models/portal_dispute_response.py +102 -0
- moolabs/models/portal_invoice_response.py +124 -0
- moolabs/models/portal_payment_intent_response.py +91 -0
- moolabs/models/portal_payment_request.py +102 -0
- moolabs/models/portal_preferences_request.py +113 -0
- moolabs/models/portal_preferences_response.py +87 -0
- moolabs/models/portal_promise_to_pay_response.py +106 -0
- moolabs/models/portal_ptp_request.py +95 -0
- moolabs/models/portal_token.py +121 -0
- moolabs/models/portal_token_item.py +97 -0
- moolabs/models/preview_resolution_request.py +89 -0
- moolabs/models/preview_resolution_response.py +101 -0
- moolabs/models/preview_template_request.py +102 -0
- moolabs/models/preview_template_response.py +95 -0
- moolabs/models/preview_trace_entry.py +97 -0
- moolabs/models/previous.py +152 -0
- moolabs/models/price_payment_term.py +37 -0
- moolabs/models/price_session_response.py +109 -0
- moolabs/models/price_tier.py +110 -0
- moolabs/models/price_tier_payload.py +103 -0
- moolabs/models/pricing_override_input.py +100 -0
- moolabs/models/pricing_result.py +100 -0
- moolabs/models/pricing_snapshot_response.py +109 -0
- moolabs/models/pricing_tier_input.py +90 -0
- moolabs/models/priority.py +38 -0
- moolabs/models/pro_rating_config.py +90 -0
- moolabs/models/pro_rating_mode.py +36 -0
- moolabs/models/process_value_recognition_request.py +94 -0
- moolabs/models/progress.py +94 -0
- moolabs/models/projection_link_in.py +91 -0
- moolabs/models/projection_links_request.py +98 -0
- moolabs/models/projection_links_response.py +89 -0
- moolabs/models/promise_response.py +112 -0
- moolabs/models/promote_response.py +89 -0
- moolabs/models/provider_billing_reference.py +146 -0
- moolabs/models/provider_billing_reference_kind.py +37 -0
- moolabs/models/provider_blocking_reason.py +115 -0
- moolabs/models/provider_confirmation_repair_request.py +99 -0
- moolabs/models/provider_connection_status.py +42 -0
- moolabs/models/provider_identity_details.py +110 -0
- moolabs/models/provider_identity_purpose.py +38 -0
- moolabs/models/provider_kind.py +37 -0
- moolabs/models/provider_readiness_response.py +97 -0
- moolabs/models/provider_readiness_summary.py +123 -0
- moolabs/models/provider_route_status.py +39 -0
- moolabs/models/provider_safe_test_send_request.py +107 -0
- moolabs/models/provider_safe_test_send_response.py +109 -0
- moolabs/models/provider_summary.py +89 -0
- moolabs/models/provider_warning.py +115 -0
- moolabs/models/ptp_approval_request.py +88 -0
- moolabs/models/ptp_create_request.py +103 -0
- moolabs/models/ptp_fulfill_request.py +90 -0
- moolabs/models/ptp_payment_link_response.py +89 -0
- moolabs/models/ptp_summary_response.py +104 -0
- moolabs/models/publish_plan_request.py +93 -0
- moolabs/models/publish_plan_result.py +93 -0
- moolabs/models/publish_template_request.py +96 -0
- moolabs/models/put_account_segment_request.py +89 -0
- moolabs/models/put_sku_eligibility_request.py +89 -0
- moolabs/models/put_tenant_segments_request.py +89 -0
- moolabs/models/quality_axis.py +116 -0
- moolabs/models/quality_caveat_code.py +40 -0
- moolabs/models/quality_status.py +40 -0
- moolabs/models/quality_summary.py +135 -0
- moolabs/models/quantity.py +153 -0
- moolabs/models/quick_action.py +99 -0
- moolabs/models/quick_books_connect_request.py +93 -0
- moolabs/models/quick_redline_response.py +123 -0
- moolabs/models/quote_agent_evaluation_response.py +106 -0
- moolabs/models/quote_agent_policy_response.py +112 -0
- moolabs/models/quote_agent_provenance_run_response.py +114 -0
- moolabs/models/quote_agent_run_response.py +122 -0
- moolabs/models/quote_approval_decision_request.py +88 -0
- moolabs/models/quote_approval_response.py +107 -0
- moolabs/models/quote_line_item_input.py +144 -0
- moolabs/models/quote_list_response.py +101 -0
- moolabs/models/quote_metrics_response.py +105 -0
- moolabs/models/quote_redline_decision_request.py +88 -0
- moolabs/models/quote_redline_response.py +125 -0
- moolabs/models/quote_response.py +125 -0
- moolabs/models/quote_template_list_response.py +95 -0
- moolabs/models/quote_template_response.py +109 -0
- moolabs/models/quote_version_response.py +129 -0
- moolabs/models/ram_cost_weight.py +152 -0
- moolabs/models/ram_gib_hours.py +152 -0
- moolabs/models/rate_bulk_import_request.py +97 -0
- moolabs/models/rate_bulk_import_response.py +91 -0
- moolabs/models/rate_card.py +150 -0
- moolabs/models/rate_card_boolean_entitlement.py +96 -0
- moolabs/models/rate_card_cost_list_response.py +95 -0
- moolabs/models/rate_card_cost_row.py +103 -0
- moolabs/models/rate_card_discount.py +87 -0
- moolabs/models/rate_card_discount_input.py +103 -0
- moolabs/models/rate_card_discount_payload.py +87 -0
- moolabs/models/rate_card_entitlement.py +164 -0
- moolabs/models/rate_card_flat_fee.py +148 -0
- moolabs/models/rate_card_metered_entitlement.py +107 -0
- moolabs/models/rate_card_payload.py +103 -0
- moolabs/models/rate_card_response.py +106 -0
- moolabs/models/rate_card_static_entitlement.py +98 -0
- moolabs/models/rate_card_usage_based.py +148 -0
- moolabs/models/rate_card_usage_based_price.py +192 -0
- moolabs/models/rate_catalog_create.py +117 -0
- moolabs/models/rate_catalog_response.py +118 -0
- moolabs/models/rate_catalog_update.py +107 -0
- moolabs/models/rate_generation_list_view.py +97 -0
- moolabs/models/rate_generation_view.py +98 -0
- moolabs/models/rate_per_unit.py +153 -0
- moolabs/models/rate_per_unit1.py +153 -0
- moolabs/models/rate_schedule_list_response.py +95 -0
- moolabs/models/rate_schedule_preview_response.py +105 -0
- moolabs/models/rate_schedule_version_out.py +101 -0
- moolabs/models/ratify_family_request.py +91 -0
- moolabs/models/ratify_refinement_request.py +93 -0
- moolabs/models/re_scope_request.py +89 -0
- moolabs/models/readiness_summary.py +89 -0
- moolabs/models/readout_page_response.py +119 -0
- moolabs/models/readout_row_response.py +104 -0
- moolabs/models/readout_snapshot_response.py +116 -0
- moolabs/models/reallocate_request.py +90 -0
- moolabs/models/reallocate_response.py +97 -0
- moolabs/models/reattempt_request.py +88 -0
- moolabs/models/reattempt_response.py +89 -0
- moolabs/models/recall_request.py +87 -0
- moolabs/models/reconcile_request.py +89 -0
- moolabs/models/reconcile_response.py +95 -0
- moolabs/models/reconciliation_basis.py +99 -0
- moolabs/models/reconciliation_basis_out.py +95 -0
- moolabs/models/reconstruction_run_response.py +95 -0
- moolabs/models/record_usage_request.py +115 -0
- moolabs/models/recurring_period.py +96 -0
- moolabs/models/recurring_period_create_input.py +94 -0
- moolabs/models/recurring_period_interval.py +151 -0
- moolabs/models/recurring_period_interval_enum.py +39 -0
- moolabs/models/recurring_period_v2.py +94 -0
- moolabs/models/recurringamount.py +152 -0
- moolabs/models/redline_summary.py +89 -0
- moolabs/models/refund_credit_request.py +90 -0
- moolabs/models/reject_rate_generation_request.py +87 -0
- moolabs/models/remittance_create_request.py +103 -0
- moolabs/models/remittance_list_response.py +101 -0
- moolabs/models/remittance_response.py +120 -0
- moolabs/models/remove_contract_participant_response.py +131 -0
- moolabs/models/remove_phase_shifting.py +37 -0
- moolabs/models/replace_levels_request.py +95 -0
- moolabs/models/replay_dead_letter_request.py +89 -0
- moolabs/models/report_create_request.py +105 -0
- moolabs/models/report_create_response.py +139 -0
- moolabs/models/reporting_line_total.py +153 -0
- moolabs/models/reporting_total_cost.py +153 -0
- moolabs/models/request_cost_distribution.py +127 -0
- moolabs/models/request_evidence_response.py +197 -0
- moolabs/models/request_evidence_row.py +154 -0
- moolabs/models/request_pricing_state.py +40 -0
- moolabs/models/reschedule_request.py +88 -0
- moolabs/models/resend_identity_details.py +104 -0
- moolabs/models/reset_entitlement_usage_input.py +92 -0
- moolabs/models/resolve_unapplied_request.py +87 -0
- moolabs/models/resource_map_create_request.py +103 -0
- moolabs/models/resource_map_response.py +113 -0
- moolabs/models/retry_unpriced_request.py +91 -0
- moolabs/models/retry_unpriced_response.py +91 -0
- moolabs/models/revenue_recognition_request.py +89 -0
- moolabs/models/reversed_settlement_ref.py +93 -0
- moolabs/models/review_quarantined_request.py +95 -0
- moolabs/models/review_quarantined_response.py +95 -0
- moolabs/models/revoke_api_key_request.py +87 -0
- moolabs/models/revoke_portal_token_request.py +87 -0
- moolabs/models/rollover_mode.py +38 -0
- moolabs/models/rollover_request.py +95 -0
- moolabs/models/rollover_response.py +98 -0
- moolabs/models/rollovercapamount.py +152 -0
- moolabs/models/rolloverpercent.py +152 -0
- moolabs/models/rotate_key_request.py +88 -0
- moolabs/models/rotate_key_response.py +92 -0
- moolabs/models/route_update_request.py +110 -0
- moolabs/models/salesforce_connect_request.py +93 -0
- moolabs/models/sample_rate.py +153 -0
- moolabs/models/sandbox_app.py +138 -0
- moolabs/models/sandbox_app_replace_update.py +101 -0
- moolabs/models/sandbox_customer_app_data.py +115 -0
- moolabs/models/save_redline_edit_request.py +95 -0
- moolabs/models/scenario_compare_response.py +97 -0
- moolabs/models/scenario_item.py +117 -0
- moolabs/models/scenario_request.py +96 -0
- moolabs/models/scope_type.py +38 -0
- moolabs/models/sdk_batch_ingest_request.py +96 -0
- moolabs/models/sdk_batch_ingest_response.py +91 -0
- moolabs/models/sdk_event.py +111 -0
- moolabs/models/sdk_span.py +106 -0
- moolabs/models/segment_out.py +95 -0
- moolabs/models/sender_response.py +93 -0
- moolabs/models/sender_update.py +93 -0
- moolabs/models/ses_identity_details.py +127 -0
- moolabs/models/ses_provider_scope_status.py +89 -0
- moolabs/models/set_rate_card_cost_request.py +89 -0
- moolabs/models/severity.py +38 -0
- moolabs/models/shadow_mode_request.py +87 -0
- moolabs/models/shadow_run_list_item.py +101 -0
- moolabs/models/shadow_run_metrics.py +114 -0
- moolabs/models/shadow_run_request.py +92 -0
- moolabs/models/shadow_run_summary.py +95 -0
- moolabs/models/simulate_margin_request.py +100 -0
- moolabs/models/simulate_margin_response.py +115 -0
- moolabs/models/simulate_out.py +103 -0
- moolabs/models/sku_eligibility_list_response.py +95 -0
- moolabs/models/sku_eligibility_row.py +91 -0
- moolabs/models/slack_quote_command_response.py +93 -0
- moolabs/models/snapshot_compute_request.py +104 -0
- moolabs/models/snapshot_compute_response.py +99 -0
- moolabs/models/snapshot_response.py +104 -0
- moolabs/models/sns_confirmation_command_out.py +98 -0
- moolabs/models/sns_confirmation_confirmed_out.py +94 -0
- moolabs/models/sns_confirmation_confirmed_request.py +101 -0
- moolabs/models/sns_confirmation_list_out.py +95 -0
- moolabs/models/sort_direction.py +37 -0
- moolabs/models/sort_order.py +37 -0
- moolabs/models/spend_summary.py +114 -0
- moolabs/models/stage_schema.py +117 -0
- moolabs/models/strategy_create_request.py +130 -0
- moolabs/models/strategy_preview_request.py +87 -0
- moolabs/models/strategy_update_request.py +133 -0
- moolabs/models/stripe_api_key_input.py +87 -0
- moolabs/models/stripe_app.py +150 -0
- moolabs/models/stripe_app_replace_update.py +103 -0
- moolabs/models/stripe_checkout_session_mode.py +36 -0
- moolabs/models/stripe_customer_app_data.py +119 -0
- moolabs/models/stripe_customer_app_data_base.py +89 -0
- moolabs/models/stripe_customer_app_data_create_or_update_item.py +111 -0
- moolabs/models/stripe_customer_portal_session.py +102 -0
- moolabs/models/stripe_tax_config.py +95 -0
- moolabs/models/stripe_webhook_event.py +99 -0
- moolabs/models/stripe_webhook_event_data.py +92 -0
- moolabs/models/stripe_webhook_response.py +118 -0
- moolabs/models/subject.py +122 -0
- moolabs/models/subject_upsert.py +98 -0
- moolabs/models/subscription.py +201 -0
- moolabs/models/subscription_activate_request.py +101 -0
- moolabs/models/subscription_addon.py +167 -0
- moolabs/models/subscription_addon_create.py +106 -0
- moolabs/models/subscription_addon_rate_card.py +94 -0
- moolabs/models/subscription_addon_timeline_segment.py +95 -0
- moolabs/models/subscription_addon_update.py +100 -0
- moolabs/models/subscription_alignment.py +93 -0
- moolabs/models/subscription_bad_request_error_response.py +102 -0
- moolabs/models/subscription_change.py +147 -0
- moolabs/models/subscription_change_response_body.py +97 -0
- moolabs/models/subscription_conflict_error_response.py +102 -0
- moolabs/models/subscription_create.py +147 -0
- moolabs/models/subscription_edit.py +102 -0
- moolabs/models/subscription_edit_operation.py +206 -0
- moolabs/models/subscription_error_extensions.py +95 -0
- moolabs/models/subscription_expanded.py +211 -0
- moolabs/models/subscription_item.py +167 -0
- moolabs/models/subscription_item_included.py +97 -0
- moolabs/models/subscription_item_price_override.py +111 -0
- moolabs/models/subscription_item_price_override_type.py +37 -0
- moolabs/models/subscription_paginated_response.py +101 -0
- moolabs/models/subscription_phase_create.py +109 -0
- moolabs/models/subscription_phase_expanded.py +163 -0
- moolabs/models/subscription_ramp_phase.py +91 -0
- moolabs/models/subscription_status.py +39 -0
- moolabs/models/subscription_timing.py +151 -0
- moolabs/models/subscription_timing_enum.py +37 -0
- moolabs/models/sync_config_request.py +109 -0
- moolabs/models/sync_config_response.py +111 -0
- moolabs/models/sync_invoices_request.py +87 -0
- moolabs/models/sync_run_item.py +102 -0
- moolabs/models/synthetic_request_evidence_row.py +118 -0
- moolabs/models/task_complete_request.py +92 -0
- moolabs/models/task_list_response.py +101 -0
- moolabs/models/task_response.py +130 -0
- moolabs/models/task_update.py +93 -0
- moolabs/models/tax_behavior.py +37 -0
- moolabs/models/tax_config.py +100 -0
- moolabs/models/template_archive_request.py +88 -0
- moolabs/models/template_card_action_request.py +99 -0
- moolabs/models/template_item.py +97 -0
- moolabs/models/template_key_create_request.py +92 -0
- moolabs/models/template_key_list_response.py +116 -0
- moolabs/models/template_key_response.py +126 -0
- moolabs/models/template_meta_response.py +93 -0
- moolabs/models/template_preview_request.py +87 -0
- moolabs/models/template_preview_response.py +101 -0
- moolabs/models/template_test_send_request.py +87 -0
- moolabs/models/template_test_send_response.py +93 -0
- moolabs/models/template_update.py +91 -0
- moolabs/models/template_version_list_response.py +95 -0
- moolabs/models/template_version_response.py +143 -0
- moolabs/models/tenant_config_response.py +95 -0
- moolabs/models/tenant_escalation_config_out.py +97 -0
- moolabs/models/tenant_escalation_config_patch.py +92 -0
- moolabs/models/tenant_provision_request.py +89 -0
- moolabs/models/tenant_segment_entry.py +89 -0
- moolabs/models/tenant_segments_response.py +97 -0
- moolabs/models/tenant_unit_economics.py +97 -0
- moolabs/models/territory_in.py +89 -0
- moolabs/models/test_send_request.py +104 -0
- moolabs/models/test_send_response.py +93 -0
- moolabs/models/tiered_price_mode.py +37 -0
- moolabs/models/tiered_price_with_commitments.py +132 -0
- moolabs/models/tiered_pricing_payload.py +97 -0
- moolabs/models/timestamp_window.py +103 -0
- moolabs/models/top_consumer.py +93 -0
- moolabs/models/topup_defaults_request.py +93 -0
- moolabs/models/total_cost.py +153 -0
- moolabs/models/traces_response.py +95 -0
- moolabs/models/trend_data_point.py +91 -0
- moolabs/models/trend_point.py +116 -0
- moolabs/models/trigger_request.py +87 -0
- moolabs/models/ulidor_external_key.py +153 -0
- moolabs/models/unapplied_cash_response.py +106 -0
- moolabs/models/unavailable_reason.py +66 -0
- moolabs/models/unexpected_problem_response.py +111 -0
- moolabs/models/unified_price_row_input.py +99 -0
- moolabs/models/unified_price_row_out.py +109 -0
- moolabs/models/unit_cost.py +153 -0
- moolabs/models/unit_economics.py +97 -0
- moolabs/models/unit_price.py +104 -0
- moolabs/models/unit_price_with_commitments.py +128 -0
- moolabs/models/unitamount.py +152 -0
- moolabs/models/update_approval_policy_request.py +87 -0
- moolabs/models/update_auto_topup_rule_request.py +102 -0
- moolabs/models/update_credit_pool_request.py +98 -0
- moolabs/models/update_quote_template_request.py +102 -0
- moolabs/models/update_threshold_request.py +97 -0
- moolabs/models/update_wallet_settings_request.py +95 -0
- moolabs/models/update_wallet_thresholds_request.py +92 -0
- moolabs/models/upsert_fx_rate_request.py +92 -0
- moolabs/models/upsert_provider_identity_request.py +115 -0
- moolabs/models/upto.py +152 -0
- moolabs/models/usage_summary_window_size.py +37 -0
- moolabs/models/validate_template_request.py +116 -0
- moolabs/models/validate_template_response.py +89 -0
- moolabs/models/validation_error.py +99 -0
- moolabs/models/validation_error_loc_inner.py +152 -0
- moolabs/models/validation_issue.py +135 -0
- moolabs/models/validation_issue_severity.py +37 -0
- moolabs/models/validation_response.py +105 -0
- moolabs/models/value_recognition_response.py +93 -0
- moolabs/models/variance_decomposition_out.py +117 -0
- moolabs/models/variance_summary_response.py +101 -0
- moolabs/models/verification_status.py +39 -0
- moolabs/models/void_grant_request.py +88 -0
- moolabs/models/void_invoice_action_create.py +93 -0
- moolabs/models/void_invoice_action_create_item.py +93 -0
- moolabs/models/void_invoice_action_input.py +103 -0
- moolabs/models/void_invoice_action_line_override.py +101 -0
- moolabs/models/void_invoice_line_action_create.py +150 -0
- moolabs/models/void_invoice_line_action_create_item.py +150 -0
- moolabs/models/void_invoice_line_discard_action.py +94 -0
- moolabs/models/void_invoice_line_pending_action_create.py +97 -0
- moolabs/models/void_invoice_line_pending_action_create_item.py +97 -0
- moolabs/models/volume_summary.py +114 -0
- moolabs/models/wallet_owner_type.py +43 -0
- moolabs/models/wallet_policy.py +38 -0
- moolabs/models/wallet_response.py +112 -0
- moolabs/models/wallet_state_response.py +105 -0
- moolabs/models/warning_item.py +93 -0
- moolabs/models/webhook_response.py +95 -0
- moolabs/models/webhook_update.py +95 -0
- moolabs/models/window_size.py +39 -0
- moolabs/models/windowed_balance_history.py +105 -0
- moolabs/models/xero_connect_request.py +91 -0
- moolabs/py.typed +0 -0
- moolabs/rest.py +257 -0
- moolabs-0.3.0.dist-info/METADATA +31 -0
- moolabs-0.3.0.dist-info/RECORD +1417 -0
- moolabs-0.3.0.dist-info/WHEEL +5 -0
- moolabs-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,2126 @@
|
|
|
1
|
+
"""Redacted SQLAlchemy DB-time attribution instrumentation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import deque
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from decimal import Decimal, InvalidOperation
|
|
10
|
+
from enum import Enum
|
|
11
|
+
import heapq
|
|
12
|
+
import hashlib
|
|
13
|
+
from importlib import metadata
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import os
|
|
17
|
+
import queue
|
|
18
|
+
import re
|
|
19
|
+
import socket
|
|
20
|
+
import sys
|
|
21
|
+
import threading
|
|
22
|
+
import time
|
|
23
|
+
from typing import Any, Literal
|
|
24
|
+
import uuid
|
|
25
|
+
import weakref
|
|
26
|
+
|
|
27
|
+
from opentelemetry import trace
|
|
28
|
+
from opentelemetry.context import Context
|
|
29
|
+
from opentelemetry.sdk.trace import ReadableSpan, SpanProcessor
|
|
30
|
+
from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased
|
|
31
|
+
from opentelemetry.trace import Link, SpanKind, TraceFlags
|
|
32
|
+
from sqlalchemy import event
|
|
33
|
+
from sqlalchemy.engine import Engine
|
|
34
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
35
|
+
|
|
36
|
+
from ._dx_attribution_context import (
|
|
37
|
+
_normalized_dimensions,
|
|
38
|
+
get_attribution_context,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_SPAN_NAME = "moolabs.db_time.query"
|
|
43
|
+
_SCHEMA_VERSION = 1
|
|
44
|
+
_COMMENT_RE = re.compile(
|
|
45
|
+
r"^/\*moolabs_db_time:v1;ob=([0-9a-f]{32})\*/$",
|
|
46
|
+
re.ASCII,
|
|
47
|
+
)
|
|
48
|
+
_POOL_KEY_RE = re.compile(r"^[a-z0-9][a-z0-9_.-]{0,47}$", re.ASCII)
|
|
49
|
+
_UNATTRIBUTED_IDENTITY_PAIR = ("__unattributed__", "__unattributed__")
|
|
50
|
+
_TRANSACTION_ID_KEY = "moolabs.db_time.v1.transaction_id"
|
|
51
|
+
_STATEMENT_STATE_KEY = "_moolabs_db_time_v1_state"
|
|
52
|
+
|
|
53
|
+
_ALLOWED_SPAN_ATTRIBUTES = (
|
|
54
|
+
"db.system",
|
|
55
|
+
"moolabs.db_time.schema_version",
|
|
56
|
+
"moolabs.db_time.sql_comment",
|
|
57
|
+
"moolabs.db_time.observation_id",
|
|
58
|
+
"moolabs.db_time.service",
|
|
59
|
+
"moolabs.db_time.pool_key",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _as_utc(value: datetime) -> datetime:
|
|
64
|
+
if value.tzinfo is None or value.utcoffset() is None:
|
|
65
|
+
raise ValueError("DB-time timestamps must be timezone-aware")
|
|
66
|
+
return value.astimezone(timezone.utc)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _floor_second(value: datetime) -> datetime:
|
|
70
|
+
return _as_utc(value).replace(microsecond=0)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _next_midnight(value: datetime) -> datetime:
|
|
74
|
+
utc = _as_utc(value)
|
|
75
|
+
return (utc + timedelta(days=1)).replace(
|
|
76
|
+
hour=0,
|
|
77
|
+
minute=0,
|
|
78
|
+
second=0,
|
|
79
|
+
microsecond=0,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _window_boundary(window_start: datetime, interval_sec: float) -> datetime:
|
|
84
|
+
return min(
|
|
85
|
+
window_start + timedelta(seconds=interval_sec),
|
|
86
|
+
_next_midnight(window_start),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _format_utc(value: datetime) -> str:
|
|
91
|
+
utc = _as_utc(value)
|
|
92
|
+
timespec = "seconds" if utc.microsecond == 0 else "microseconds"
|
|
93
|
+
return utc.isoformat(timespec=timespec).replace("+00:00", "Z")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass(frozen=True, slots=True)
|
|
97
|
+
class _PendingDbObservation:
|
|
98
|
+
observation_id: uuid.UUID
|
|
99
|
+
transaction_id: uuid.UUID
|
|
100
|
+
request_id_hash: str | None
|
|
101
|
+
thread_id_hash: str | None
|
|
102
|
+
customer_external_key: str
|
|
103
|
+
feature_key: str
|
|
104
|
+
started_at: datetime
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass(frozen=True, slots=True)
|
|
108
|
+
class _CompletedDbObservation:
|
|
109
|
+
pending: _PendingDbObservation
|
|
110
|
+
trace_id: str
|
|
111
|
+
span_id: str
|
|
112
|
+
linked_trace_id: str | None
|
|
113
|
+
duration_ns: int
|
|
114
|
+
outcome: Literal["ok", "error"]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True, slots=True)
|
|
118
|
+
class _InstallConfig:
|
|
119
|
+
service: str
|
|
120
|
+
pool_key: str
|
|
121
|
+
sample_rate: float
|
|
122
|
+
tracer_provider_identity: int
|
|
123
|
+
flush_callback_identity: tuple[int | None, int] | None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@dataclass(slots=True)
|
|
127
|
+
class _StatementState:
|
|
128
|
+
span: Any
|
|
129
|
+
observation_key: str | None
|
|
130
|
+
sampled: bool
|
|
131
|
+
statement_local_transaction: bool
|
|
132
|
+
_ended: bool = False
|
|
133
|
+
_lock: threading.Lock = field(default_factory=threading.Lock)
|
|
134
|
+
|
|
135
|
+
def mark_ended(self) -> bool:
|
|
136
|
+
with self._lock:
|
|
137
|
+
if self._ended:
|
|
138
|
+
return False
|
|
139
|
+
self._ended = True
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class _DbTimeFlushOutcome(str, Enum):
|
|
144
|
+
DELIVERED = "delivered"
|
|
145
|
+
TERMINAL_DROP = "terminal_drop"
|
|
146
|
+
RETRY_EXHAUSTED = "retry_exhausted"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass(frozen=True, slots=True)
|
|
150
|
+
class _FrozenDbTimeBatch:
|
|
151
|
+
body: bytes
|
|
152
|
+
batch_id: uuid.UUID
|
|
153
|
+
batch_seq: int
|
|
154
|
+
pod_instance_id: uuid.UUID
|
|
155
|
+
service: str
|
|
156
|
+
pool_key: str
|
|
157
|
+
pod_id: str
|
|
158
|
+
window_start: str
|
|
159
|
+
attempted_count: int
|
|
160
|
+
sampled_count: int
|
|
161
|
+
dropped_count: int
|
|
162
|
+
observations: tuple[_CompletedDbObservation, ...]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
DbTimeFlushCallback = Callable[[_FrozenDbTimeBatch], _DbTimeFlushOutcome]
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(frozen=True, slots=True)
|
|
169
|
+
class _StartRecord:
|
|
170
|
+
started_at: datetime
|
|
171
|
+
pending: _PendingDbObservation | None
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass(frozen=True, slots=True)
|
|
175
|
+
class _EndRecord:
|
|
176
|
+
observation_id: uuid.UUID
|
|
177
|
+
trace_id: str
|
|
178
|
+
span_id: str
|
|
179
|
+
linked_trace_id: str | None
|
|
180
|
+
duration_ns: int
|
|
181
|
+
outcome: Literal["ok", "error"]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass(frozen=True, slots=True)
|
|
185
|
+
class _DropRecord:
|
|
186
|
+
observation_id: uuid.UUID
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass(frozen=True, slots=True)
|
|
190
|
+
class _FlushRecord:
|
|
191
|
+
force: bool
|
|
192
|
+
done: threading.Event
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass(frozen=True, slots=True)
|
|
196
|
+
class _SyncRecord:
|
|
197
|
+
done: threading.Event
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dataclass(frozen=True, slots=True)
|
|
201
|
+
class _CloseRecord:
|
|
202
|
+
deadline: float
|
|
203
|
+
done: threading.Event
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass(slots=True)
|
|
207
|
+
class _FlushBarrier:
|
|
208
|
+
window_keys: set[int]
|
|
209
|
+
done: threading.Event
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass(slots=True)
|
|
213
|
+
class _OverflowBucket:
|
|
214
|
+
started_at: datetime
|
|
215
|
+
attempted_count: int = 0
|
|
216
|
+
sampled_count: int = 0
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@dataclass(slots=True)
|
|
220
|
+
class _OverflowRecord:
|
|
221
|
+
max_buckets: int
|
|
222
|
+
buckets: dict[datetime, _OverflowBucket] = field(default_factory=dict)
|
|
223
|
+
|
|
224
|
+
def add(self, record: _StartRecord) -> bool:
|
|
225
|
+
started_at = _floor_second(record.started_at)
|
|
226
|
+
bucket = self.buckets.get(started_at)
|
|
227
|
+
if bucket is None:
|
|
228
|
+
if len(self.buckets) >= self.max_buckets:
|
|
229
|
+
return False
|
|
230
|
+
bucket = _OverflowBucket(started_at=started_at)
|
|
231
|
+
self.buckets[started_at] = bucket
|
|
232
|
+
bucket.attempted_count += 1
|
|
233
|
+
if record.pending is not None:
|
|
234
|
+
bucket.sampled_count += 1
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
def snapshot(self) -> tuple[_OverflowBucket, ...]:
|
|
238
|
+
return tuple(self.buckets[key] for key in sorted(self.buckets))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class _BoundedDbTimeInbox:
|
|
242
|
+
"""Bound data records while reserving every admitted sampled completion."""
|
|
243
|
+
|
|
244
|
+
def __init__(self, capacity: int, *, overflow_bucket_capacity: int) -> None:
|
|
245
|
+
self._capacity = capacity
|
|
246
|
+
self._overflow_bucket_capacity = overflow_bucket_capacity
|
|
247
|
+
self._condition = threading.Condition()
|
|
248
|
+
self._items: deque[object] = deque()
|
|
249
|
+
self._data_count = 0
|
|
250
|
+
self._admitted_ids: set[uuid.UUID] = set()
|
|
251
|
+
self._active_overflow: _OverflowRecord | None = None
|
|
252
|
+
self._ineligible_attempted_count = 0
|
|
253
|
+
self._ineligible_sampled_count = 0
|
|
254
|
+
|
|
255
|
+
def put(self, record: object) -> bool:
|
|
256
|
+
with self._condition:
|
|
257
|
+
if isinstance(record, _StartRecord):
|
|
258
|
+
return self._put_start(record)
|
|
259
|
+
if isinstance(record, (_EndRecord, _DropRecord)):
|
|
260
|
+
return self._put_terminal(record)
|
|
261
|
+
self._active_overflow = None
|
|
262
|
+
self._items.append(record)
|
|
263
|
+
self._condition.notify()
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
def _put_start(self, record: _StartRecord) -> bool:
|
|
267
|
+
pending = record.pending
|
|
268
|
+
observation_id = pending.observation_id if pending is not None else None
|
|
269
|
+
required = 2 if observation_id is not None else 1
|
|
270
|
+
used = self._data_count + len(self._admitted_ids)
|
|
271
|
+
if (
|
|
272
|
+
used + required <= self._capacity
|
|
273
|
+
and observation_id not in self._admitted_ids
|
|
274
|
+
):
|
|
275
|
+
self._active_overflow = None
|
|
276
|
+
if observation_id is not None:
|
|
277
|
+
self._admitted_ids.add(observation_id)
|
|
278
|
+
self._items.append(record)
|
|
279
|
+
self._data_count += 1
|
|
280
|
+
self._condition.notify()
|
|
281
|
+
return True
|
|
282
|
+
|
|
283
|
+
overflow = self._active_overflow
|
|
284
|
+
if overflow is None:
|
|
285
|
+
overflow = _OverflowRecord(max_buckets=self._overflow_bucket_capacity)
|
|
286
|
+
self._active_overflow = overflow
|
|
287
|
+
self._items.append(overflow)
|
|
288
|
+
self._condition.notify()
|
|
289
|
+
if not overflow.add(record):
|
|
290
|
+
self._ineligible_attempted_count += 1
|
|
291
|
+
if record.pending is not None:
|
|
292
|
+
self._ineligible_sampled_count += 1
|
|
293
|
+
return False
|
|
294
|
+
|
|
295
|
+
def _put_terminal(self, record: _EndRecord | _DropRecord) -> bool:
|
|
296
|
+
observation_id = record.observation_id
|
|
297
|
+
if observation_id not in self._admitted_ids:
|
|
298
|
+
return False
|
|
299
|
+
self._admitted_ids.remove(observation_id)
|
|
300
|
+
self._items.append(record)
|
|
301
|
+
self._data_count += 1
|
|
302
|
+
self._condition.notify()
|
|
303
|
+
return True
|
|
304
|
+
|
|
305
|
+
def get(self, timeout: float | None = None) -> object:
|
|
306
|
+
with self._condition:
|
|
307
|
+
deadline = None if timeout is None else time.monotonic() + timeout
|
|
308
|
+
while not self._items:
|
|
309
|
+
if deadline is None:
|
|
310
|
+
self._condition.wait()
|
|
311
|
+
continue
|
|
312
|
+
remaining = deadline - time.monotonic()
|
|
313
|
+
if remaining <= 0:
|
|
314
|
+
raise queue.Empty
|
|
315
|
+
self._condition.wait(remaining)
|
|
316
|
+
|
|
317
|
+
record = self._items.popleft()
|
|
318
|
+
if isinstance(record, (_StartRecord, _EndRecord, _DropRecord)):
|
|
319
|
+
self._data_count -= 1
|
|
320
|
+
if record is self._active_overflow:
|
|
321
|
+
self._active_overflow = None
|
|
322
|
+
return record
|
|
323
|
+
|
|
324
|
+
def get_nowait(self) -> object:
|
|
325
|
+
return self.get(timeout=0.0)
|
|
326
|
+
|
|
327
|
+
def discard_admitted(self, observation_id: uuid.UUID) -> None:
|
|
328
|
+
with self._condition:
|
|
329
|
+
self._admitted_ids.discard(observation_id)
|
|
330
|
+
|
|
331
|
+
def qsize(self) -> int:
|
|
332
|
+
with self._condition:
|
|
333
|
+
return self._data_count + len(self._admitted_ids)
|
|
334
|
+
|
|
335
|
+
def admitted_count(self) -> int:
|
|
336
|
+
with self._condition:
|
|
337
|
+
return len(self._admitted_ids)
|
|
338
|
+
|
|
339
|
+
def consume_ineligible_overflow(self) -> tuple[int, int]:
|
|
340
|
+
with self._condition:
|
|
341
|
+
counts = (
|
|
342
|
+
self._ineligible_attempted_count,
|
|
343
|
+
self._ineligible_sampled_count,
|
|
344
|
+
)
|
|
345
|
+
self._ineligible_attempted_count = 0
|
|
346
|
+
self._ineligible_sampled_count = 0
|
|
347
|
+
return counts
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@dataclass(slots=True)
|
|
351
|
+
class _MutableDbWindow:
|
|
352
|
+
key: int
|
|
353
|
+
window_start: datetime
|
|
354
|
+
boundary_at: datetime
|
|
355
|
+
attempted_count: int = 0
|
|
356
|
+
sampled_count: int = 0
|
|
357
|
+
dropped_count: int = 0
|
|
358
|
+
inflight: set[uuid.UUID] = field(default_factory=set)
|
|
359
|
+
observations: dict[uuid.UUID, _CompletedDbObservation] = field(default_factory=dict)
|
|
360
|
+
sealed_at: datetime | None = None
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class _DbTimeWindowOwner:
|
|
364
|
+
"""Single-thread owner for DB-time windows, retention, freeze, and delivery."""
|
|
365
|
+
|
|
366
|
+
def __init__(
|
|
367
|
+
self,
|
|
368
|
+
*,
|
|
369
|
+
service: str,
|
|
370
|
+
pool_key: str,
|
|
371
|
+
pod_id: str,
|
|
372
|
+
sample_rate: float,
|
|
373
|
+
queue_capacity: int,
|
|
374
|
+
overflow_bucket_capacity: int,
|
|
375
|
+
batch_size: int,
|
|
376
|
+
flush_interval_sec: float,
|
|
377
|
+
flush_callback: DbTimeFlushCallback | None,
|
|
378
|
+
logger: Callable[[str, dict[str, Any]], None] | None,
|
|
379
|
+
utc_now: Callable[[], datetime],
|
|
380
|
+
monotonic: Callable[[], float],
|
|
381
|
+
) -> None:
|
|
382
|
+
self.service = service
|
|
383
|
+
self.pool_key = pool_key
|
|
384
|
+
self.pod_id = pod_id
|
|
385
|
+
self.sample_rate = sample_rate
|
|
386
|
+
self.pod_instance_id = uuid.uuid4()
|
|
387
|
+
self._queue_capacity = queue_capacity
|
|
388
|
+
self._batch_size = batch_size
|
|
389
|
+
self._flush_interval_sec = flush_interval_sec
|
|
390
|
+
self._flush_callback = flush_callback
|
|
391
|
+
self._logger = logger
|
|
392
|
+
self._utc_now = utc_now
|
|
393
|
+
self._monotonic = monotonic
|
|
394
|
+
self._records = _BoundedDbTimeInbox(
|
|
395
|
+
queue_capacity,
|
|
396
|
+
overflow_bucket_capacity=overflow_bucket_capacity,
|
|
397
|
+
)
|
|
398
|
+
self._state_lock = threading.RLock()
|
|
399
|
+
self._command_lock = threading.Lock()
|
|
400
|
+
self._windows: dict[int, _MutableDbWindow] = {}
|
|
401
|
+
self._open_window_key: int | None = None
|
|
402
|
+
self._next_window_key = 1
|
|
403
|
+
self._next_window_start_hint: datetime | None = None
|
|
404
|
+
self._pending: dict[
|
|
405
|
+
uuid.UUID,
|
|
406
|
+
tuple[_PendingDbObservation, int],
|
|
407
|
+
] = {}
|
|
408
|
+
self._completed_heap: list[tuple[datetime, str, uuid.UUID, int]] = []
|
|
409
|
+
self._completed_locations: dict[uuid.UUID, int] = {}
|
|
410
|
+
self._batch_seq = 0
|
|
411
|
+
self._closing = False
|
|
412
|
+
self._close_done: threading.Event | None = None
|
|
413
|
+
self._flush_barriers: list[_FlushBarrier] = []
|
|
414
|
+
self._closed = False
|
|
415
|
+
self._stats = {
|
|
416
|
+
"attempted_count": 0,
|
|
417
|
+
"sampled_start_count": 0,
|
|
418
|
+
"completed_count": 0,
|
|
419
|
+
"pending_count": 0,
|
|
420
|
+
"dropped_count": 0,
|
|
421
|
+
"abandoned_inflight": 0,
|
|
422
|
+
"delivered_batches": 0,
|
|
423
|
+
"terminal_dropped_batches": 0,
|
|
424
|
+
"retry_exhausted_batches": 0,
|
|
425
|
+
"callback_failures": 0,
|
|
426
|
+
"overflow_ineligible_count": 0,
|
|
427
|
+
"overflow_ineligible_batches": 0,
|
|
428
|
+
}
|
|
429
|
+
self.thread = threading.Thread(
|
|
430
|
+
target=self._run,
|
|
431
|
+
name="moolabs-db-time-owner",
|
|
432
|
+
daemon=True,
|
|
433
|
+
)
|
|
434
|
+
self.thread.start()
|
|
435
|
+
|
|
436
|
+
def submit(self, record: object) -> None:
|
|
437
|
+
try:
|
|
438
|
+
self._records.put(record)
|
|
439
|
+
except Exception:
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
def flush(self, *, force: bool) -> None:
|
|
443
|
+
if threading.current_thread() is self.thread:
|
|
444
|
+
if force:
|
|
445
|
+
self._seal_open(self._utc_now())
|
|
446
|
+
else:
|
|
447
|
+
self._seal_due(self._utc_now())
|
|
448
|
+
self._deliver_ready_one()
|
|
449
|
+
return
|
|
450
|
+
with self._command_lock:
|
|
451
|
+
with self._state_lock:
|
|
452
|
+
if self._closing:
|
|
453
|
+
return
|
|
454
|
+
done = threading.Event()
|
|
455
|
+
self.submit(_FlushRecord(force=force, done=done))
|
|
456
|
+
while self.thread.is_alive() and not done.wait(timeout=0.1):
|
|
457
|
+
continue
|
|
458
|
+
|
|
459
|
+
def stats(self) -> dict[str, int]:
|
|
460
|
+
if threading.current_thread() is not self.thread and self.thread.is_alive():
|
|
461
|
+
with self._command_lock:
|
|
462
|
+
with self._state_lock:
|
|
463
|
+
closing = self._closing
|
|
464
|
+
if not closing:
|
|
465
|
+
done = threading.Event()
|
|
466
|
+
self.submit(_SyncRecord(done))
|
|
467
|
+
while self.thread.is_alive() and not done.wait(timeout=0.1):
|
|
468
|
+
continue
|
|
469
|
+
with self._state_lock:
|
|
470
|
+
return dict(self._stats)
|
|
471
|
+
|
|
472
|
+
def request_close(self, deadline: float) -> threading.Event:
|
|
473
|
+
if threading.current_thread() is self.thread:
|
|
474
|
+
return self._request_close(deadline)
|
|
475
|
+
with self._command_lock:
|
|
476
|
+
return self._request_close(deadline)
|
|
477
|
+
|
|
478
|
+
def _request_close(self, deadline: float) -> threading.Event:
|
|
479
|
+
with self._state_lock:
|
|
480
|
+
if self._close_done is not None:
|
|
481
|
+
return self._close_done
|
|
482
|
+
self._closing = True
|
|
483
|
+
done = threading.Event()
|
|
484
|
+
self._close_done = done
|
|
485
|
+
self.submit(_CloseRecord(deadline=deadline, done=done))
|
|
486
|
+
return done
|
|
487
|
+
|
|
488
|
+
def _run(self) -> None:
|
|
489
|
+
while True:
|
|
490
|
+
record: object | None = None
|
|
491
|
+
try:
|
|
492
|
+
timeout = self._wait_timeout()
|
|
493
|
+
record = self._records.get(timeout=timeout)
|
|
494
|
+
except queue.Empty:
|
|
495
|
+
pass
|
|
496
|
+
try:
|
|
497
|
+
close_record = self._drain_available(record)
|
|
498
|
+
if close_record is not None:
|
|
499
|
+
try:
|
|
500
|
+
self._finish_close(close_record)
|
|
501
|
+
finally:
|
|
502
|
+
return
|
|
503
|
+
self._sync_ineligible_overflow()
|
|
504
|
+
self._seal_due(self._utc_now())
|
|
505
|
+
self._deliver_ready_one()
|
|
506
|
+
except Exception:
|
|
507
|
+
if isinstance(record, (_FlushRecord, _SyncRecord)):
|
|
508
|
+
record.done.set()
|
|
509
|
+
|
|
510
|
+
def _wait_timeout(self) -> float | None:
|
|
511
|
+
if self._ready_windows():
|
|
512
|
+
return 0.0
|
|
513
|
+
window = self._open_window()
|
|
514
|
+
if window is None:
|
|
515
|
+
return None
|
|
516
|
+
remaining = (window.boundary_at - self._utc_now()).total_seconds()
|
|
517
|
+
return max(0.0, min(remaining, self._flush_interval_sec))
|
|
518
|
+
|
|
519
|
+
def _process(self, record: object) -> None:
|
|
520
|
+
if isinstance(record, _StartRecord):
|
|
521
|
+
self._process_start(record)
|
|
522
|
+
elif isinstance(record, _OverflowRecord):
|
|
523
|
+
self._process_overflow(record)
|
|
524
|
+
elif isinstance(record, _EndRecord):
|
|
525
|
+
self._process_end(record)
|
|
526
|
+
elif isinstance(record, _DropRecord):
|
|
527
|
+
self._process_drop(record.observation_id, abandoned=False)
|
|
528
|
+
elif isinstance(record, _FlushRecord):
|
|
529
|
+
if record.force:
|
|
530
|
+
self._seal_open(self._utc_now())
|
|
531
|
+
else:
|
|
532
|
+
self._seal_due(self._utc_now())
|
|
533
|
+
ready_keys = {window.key for window in self._ready_windows()}
|
|
534
|
+
if ready_keys:
|
|
535
|
+
self._flush_barriers.append(
|
|
536
|
+
_FlushBarrier(window_keys=ready_keys, done=record.done)
|
|
537
|
+
)
|
|
538
|
+
else:
|
|
539
|
+
record.done.set()
|
|
540
|
+
elif isinstance(record, _SyncRecord):
|
|
541
|
+
record.done.set()
|
|
542
|
+
|
|
543
|
+
def _drain_available(self, first_record: object | None) -> _CloseRecord | None:
|
|
544
|
+
record = first_record
|
|
545
|
+
limit = self._queue_capacity + 8
|
|
546
|
+
for _ in range(limit):
|
|
547
|
+
if record is None:
|
|
548
|
+
try:
|
|
549
|
+
record = self._records.get_nowait()
|
|
550
|
+
except queue.Empty:
|
|
551
|
+
return None
|
|
552
|
+
if isinstance(record, _CloseRecord):
|
|
553
|
+
return record
|
|
554
|
+
self._process(record)
|
|
555
|
+
record = None
|
|
556
|
+
return None
|
|
557
|
+
|
|
558
|
+
def _sync_ineligible_overflow(self) -> None:
|
|
559
|
+
attempted_count, sampled_count = self._records.consume_ineligible_overflow()
|
|
560
|
+
if attempted_count == 0:
|
|
561
|
+
return
|
|
562
|
+
self._increment("attempted_count", attempted_count)
|
|
563
|
+
self._increment("sampled_start_count", sampled_count)
|
|
564
|
+
self._increment("dropped_count", sampled_count)
|
|
565
|
+
self._increment("overflow_ineligible_count", attempted_count)
|
|
566
|
+
self._increment("overflow_ineligible_batches")
|
|
567
|
+
# Consume one wire identity so a later accepted batch exposes the loss.
|
|
568
|
+
self._batch_seq += 1
|
|
569
|
+
self._safe_log(
|
|
570
|
+
"moolabs.attribution.db_time.overflow_ineligible",
|
|
571
|
+
{
|
|
572
|
+
"attempted_count": attempted_count,
|
|
573
|
+
"sampled_count": sampled_count,
|
|
574
|
+
"missing_batch_seq": self._batch_seq,
|
|
575
|
+
},
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
def _process_overflow(self, record: _OverflowRecord) -> None:
|
|
579
|
+
for bucket in record.snapshot():
|
|
580
|
+
self._process_overflow_bucket(bucket)
|
|
581
|
+
|
|
582
|
+
def _process_overflow_bucket(self, bucket: _OverflowBucket) -> None:
|
|
583
|
+
started_at = _as_utc(bucket.started_at)
|
|
584
|
+
unsampled_count = bucket.attempted_count - bucket.sampled_count
|
|
585
|
+
if unsampled_count:
|
|
586
|
+
self._advance_for_start(started_at)
|
|
587
|
+
window = self._open_window()
|
|
588
|
+
if window is None:
|
|
589
|
+
window = self._new_window(started_at)
|
|
590
|
+
window.attempted_count += unsampled_count
|
|
591
|
+
self._increment("attempted_count", unsampled_count)
|
|
592
|
+
|
|
593
|
+
sampled_remaining = bucket.sampled_count
|
|
594
|
+
while sampled_remaining:
|
|
595
|
+
self._advance_for_start(started_at)
|
|
596
|
+
window = self._open_window()
|
|
597
|
+
if window is None:
|
|
598
|
+
window = self._new_window(started_at)
|
|
599
|
+
sampled_capacity = self._batch_size - window.sampled_count
|
|
600
|
+
sampled_count = min(sampled_remaining, sampled_capacity)
|
|
601
|
+
window.attempted_count += sampled_count
|
|
602
|
+
window.sampled_count += sampled_count
|
|
603
|
+
window.dropped_count += sampled_count
|
|
604
|
+
self._increment("attempted_count", sampled_count)
|
|
605
|
+
self._increment("sampled_start_count", sampled_count)
|
|
606
|
+
self._increment("dropped_count", sampled_count)
|
|
607
|
+
sampled_remaining -= sampled_count
|
|
608
|
+
if window.sampled_count >= self._batch_size:
|
|
609
|
+
self._seal_window(window, started_at)
|
|
610
|
+
self._open_window_key = None
|
|
611
|
+
self._next_window_start_hint = None
|
|
612
|
+
|
|
613
|
+
def _process_start(self, record: _StartRecord) -> None:
|
|
614
|
+
started_at = _as_utc(record.started_at)
|
|
615
|
+
self._advance_for_start(started_at)
|
|
616
|
+
window = self._open_window()
|
|
617
|
+
if window is None:
|
|
618
|
+
window = self._new_window(started_at)
|
|
619
|
+
window.attempted_count += 1
|
|
620
|
+
self._increment("attempted_count")
|
|
621
|
+
pending = record.pending
|
|
622
|
+
if pending is None:
|
|
623
|
+
return
|
|
624
|
+
observation_id = pending.observation_id
|
|
625
|
+
if (
|
|
626
|
+
observation_id in self._pending
|
|
627
|
+
or observation_id in self._completed_locations
|
|
628
|
+
):
|
|
629
|
+
window.dropped_count += 1
|
|
630
|
+
self._increment("dropped_count")
|
|
631
|
+
return
|
|
632
|
+
window.sampled_count += 1
|
|
633
|
+
window.inflight.add(observation_id)
|
|
634
|
+
self._pending[observation_id] = (pending, window.key)
|
|
635
|
+
self._increment("sampled_start_count")
|
|
636
|
+
self._set_pending_count()
|
|
637
|
+
if window.sampled_count >= self._batch_size:
|
|
638
|
+
self._seal_window(window, started_at)
|
|
639
|
+
self._open_window_key = None
|
|
640
|
+
self._next_window_start_hint = None
|
|
641
|
+
|
|
642
|
+
def _process_end(self, record: _EndRecord) -> None:
|
|
643
|
+
pending_entry = self._pending.pop(record.observation_id, None)
|
|
644
|
+
if pending_entry is None:
|
|
645
|
+
return
|
|
646
|
+
pending, window_key = pending_entry
|
|
647
|
+
window = self._windows.get(window_key)
|
|
648
|
+
if window is None:
|
|
649
|
+
self._set_pending_count()
|
|
650
|
+
return
|
|
651
|
+
window.inflight.discard(record.observation_id)
|
|
652
|
+
if (
|
|
653
|
+
type(record.duration_ns) is not int
|
|
654
|
+
or not 0 <= record.duration_ns <= 86_400_000_000_000
|
|
655
|
+
):
|
|
656
|
+
window.dropped_count += 1
|
|
657
|
+
self._increment("dropped_count")
|
|
658
|
+
self._set_pending_count()
|
|
659
|
+
return
|
|
660
|
+
completed = _CompletedDbObservation(
|
|
661
|
+
pending=pending,
|
|
662
|
+
trace_id=record.trace_id,
|
|
663
|
+
span_id=record.span_id,
|
|
664
|
+
linked_trace_id=record.linked_trace_id,
|
|
665
|
+
duration_ns=record.duration_ns,
|
|
666
|
+
outcome=record.outcome,
|
|
667
|
+
)
|
|
668
|
+
window.observations[record.observation_id] = completed
|
|
669
|
+
self._completed_locations[record.observation_id] = window_key
|
|
670
|
+
heapq.heappush(
|
|
671
|
+
self._completed_heap,
|
|
672
|
+
(
|
|
673
|
+
pending.started_at,
|
|
674
|
+
pending.observation_id.hex,
|
|
675
|
+
pending.observation_id,
|
|
676
|
+
window_key,
|
|
677
|
+
),
|
|
678
|
+
)
|
|
679
|
+
self._increment("completed_count")
|
|
680
|
+
self._set_pending_count()
|
|
681
|
+
self._evict_completed_overflow()
|
|
682
|
+
|
|
683
|
+
def _process_drop(self, observation_id: uuid.UUID, *, abandoned: bool) -> None:
|
|
684
|
+
self._records.discard_admitted(observation_id)
|
|
685
|
+
pending_entry = self._pending.pop(observation_id, None)
|
|
686
|
+
if pending_entry is None:
|
|
687
|
+
return
|
|
688
|
+
_pending, window_key = pending_entry
|
|
689
|
+
window = self._windows.get(window_key)
|
|
690
|
+
if window is not None:
|
|
691
|
+
window.inflight.discard(observation_id)
|
|
692
|
+
window.dropped_count += 1
|
|
693
|
+
self._increment("dropped_count")
|
|
694
|
+
if abandoned:
|
|
695
|
+
self._increment("abandoned_inflight")
|
|
696
|
+
self._set_pending_count()
|
|
697
|
+
|
|
698
|
+
def _evict_completed_overflow(self) -> None:
|
|
699
|
+
while len(self._completed_locations) > self._queue_capacity:
|
|
700
|
+
if not self._completed_heap:
|
|
701
|
+
return
|
|
702
|
+
_started_at, _hex, observation_id, window_key = heapq.heappop(
|
|
703
|
+
self._completed_heap
|
|
704
|
+
)
|
|
705
|
+
current_window_key = self._completed_locations.get(observation_id)
|
|
706
|
+
if current_window_key != window_key:
|
|
707
|
+
continue
|
|
708
|
+
del self._completed_locations[observation_id]
|
|
709
|
+
window = self._windows.get(window_key)
|
|
710
|
+
if window is None or window.observations.pop(observation_id, None) is None:
|
|
711
|
+
continue
|
|
712
|
+
window.dropped_count += 1
|
|
713
|
+
self._increment("dropped_count")
|
|
714
|
+
|
|
715
|
+
def _advance_for_start(self, started_at: datetime) -> None:
|
|
716
|
+
window = self._open_window()
|
|
717
|
+
if window is not None and started_at < window.window_start:
|
|
718
|
+
self._seal_window(window, window.boundary_at)
|
|
719
|
+
self._open_window_key = None
|
|
720
|
+
self._next_window_start_hint = None
|
|
721
|
+
window = None
|
|
722
|
+
if window is not None and started_at >= window.boundary_at:
|
|
723
|
+
self._seal_window(window, window.boundary_at)
|
|
724
|
+
self._open_window_key = None
|
|
725
|
+
self._next_window_start_hint = window.boundary_at
|
|
726
|
+
if self._open_window() is not None:
|
|
727
|
+
return
|
|
728
|
+
start = self._next_window_start_hint
|
|
729
|
+
if start is None or started_at < start:
|
|
730
|
+
start = _floor_second(started_at)
|
|
731
|
+
while started_at >= _window_boundary(start, self._flush_interval_sec):
|
|
732
|
+
start = _window_boundary(start, self._flush_interval_sec)
|
|
733
|
+
self._new_window(start)
|
|
734
|
+
self._next_window_start_hint = None
|
|
735
|
+
|
|
736
|
+
def _new_window(self, window_start: datetime) -> _MutableDbWindow:
|
|
737
|
+
key = self._next_window_key
|
|
738
|
+
self._next_window_key += 1
|
|
739
|
+
window_start = _floor_second(window_start)
|
|
740
|
+
window = _MutableDbWindow(
|
|
741
|
+
key=key,
|
|
742
|
+
window_start=window_start,
|
|
743
|
+
boundary_at=_window_boundary(window_start, self._flush_interval_sec),
|
|
744
|
+
)
|
|
745
|
+
self._windows[key] = window
|
|
746
|
+
self._open_window_key = key
|
|
747
|
+
return window
|
|
748
|
+
|
|
749
|
+
def _open_window(self) -> _MutableDbWindow | None:
|
|
750
|
+
if self._open_window_key is None:
|
|
751
|
+
return None
|
|
752
|
+
return self._windows.get(self._open_window_key)
|
|
753
|
+
|
|
754
|
+
def _seal_due(self, now: datetime) -> None:
|
|
755
|
+
now = _as_utc(now)
|
|
756
|
+
window = self._open_window()
|
|
757
|
+
if window is None or now < window.boundary_at:
|
|
758
|
+
return
|
|
759
|
+
self._seal_window(window, window.boundary_at)
|
|
760
|
+
self._open_window_key = None
|
|
761
|
+
self._next_window_start_hint = window.boundary_at
|
|
762
|
+
|
|
763
|
+
def _seal_open(self, now: datetime) -> None:
|
|
764
|
+
window = self._open_window()
|
|
765
|
+
if window is None:
|
|
766
|
+
return
|
|
767
|
+
self._seal_window(window, min(_as_utc(now), window.boundary_at))
|
|
768
|
+
self._open_window_key = None
|
|
769
|
+
self._next_window_start_hint = None
|
|
770
|
+
|
|
771
|
+
@staticmethod
|
|
772
|
+
def _seal_window(window: _MutableDbWindow, sealed_at: datetime) -> None:
|
|
773
|
+
if window.sealed_at is None:
|
|
774
|
+
window.sealed_at = max(window.window_start, sealed_at)
|
|
775
|
+
|
|
776
|
+
def _ready_windows(self) -> list[_MutableDbWindow]:
|
|
777
|
+
return sorted(
|
|
778
|
+
(
|
|
779
|
+
window
|
|
780
|
+
for window in self._windows.values()
|
|
781
|
+
if window.sealed_at is not None and not window.inflight
|
|
782
|
+
),
|
|
783
|
+
key=lambda item: item.key,
|
|
784
|
+
)
|
|
785
|
+
|
|
786
|
+
def _deliver_ready_one(self) -> bool:
|
|
787
|
+
ready = self._ready_windows()
|
|
788
|
+
if not ready:
|
|
789
|
+
return False
|
|
790
|
+
window = ready[0]
|
|
791
|
+
frozen = self._freeze(window)
|
|
792
|
+
self._remove_window(window)
|
|
793
|
+
self._invoke_callback(frozen)
|
|
794
|
+
self._complete_flush_barriers(window.key)
|
|
795
|
+
return True
|
|
796
|
+
|
|
797
|
+
def _complete_flush_barriers(self, window_key: int) -> None:
|
|
798
|
+
remaining: list[_FlushBarrier] = []
|
|
799
|
+
for barrier in self._flush_barriers:
|
|
800
|
+
barrier.window_keys.discard(window_key)
|
|
801
|
+
if barrier.window_keys:
|
|
802
|
+
remaining.append(barrier)
|
|
803
|
+
else:
|
|
804
|
+
barrier.done.set()
|
|
805
|
+
self._flush_barriers = remaining
|
|
806
|
+
|
|
807
|
+
def _release_flush_barriers(self) -> None:
|
|
808
|
+
for barrier in self._flush_barriers:
|
|
809
|
+
barrier.done.set()
|
|
810
|
+
self._flush_barriers.clear()
|
|
811
|
+
|
|
812
|
+
def _seal_all_windows(self) -> None:
|
|
813
|
+
now = self._utc_now()
|
|
814
|
+
self._seal_open(now)
|
|
815
|
+
for window in self._windows.values():
|
|
816
|
+
if window.sealed_at is None:
|
|
817
|
+
self._seal_window(window, now)
|
|
818
|
+
|
|
819
|
+
def _deliver_all_after_close(self) -> None:
|
|
820
|
+
while True:
|
|
821
|
+
self._drain_after_deadline()
|
|
822
|
+
self._sync_ineligible_overflow()
|
|
823
|
+
self._seal_all_windows()
|
|
824
|
+
if not self._deliver_ready_one():
|
|
825
|
+
return
|
|
826
|
+
|
|
827
|
+
def _finish_close(self, record: _CloseRecord) -> None:
|
|
828
|
+
try:
|
|
829
|
+
while self._pending and self._monotonic() < record.deadline:
|
|
830
|
+
nested_close = self._drain_available(None)
|
|
831
|
+
if nested_close is not None:
|
|
832
|
+
nested_close.done.set()
|
|
833
|
+
self._sync_ineligible_overflow()
|
|
834
|
+
self._seal_due(self._utc_now())
|
|
835
|
+
if self._deliver_ready_one():
|
|
836
|
+
continue
|
|
837
|
+
remaining = max(0.0, record.deadline - self._monotonic())
|
|
838
|
+
try:
|
|
839
|
+
next_record = self._records.get(timeout=remaining)
|
|
840
|
+
except queue.Empty:
|
|
841
|
+
break
|
|
842
|
+
nested_close = self._drain_available(next_record)
|
|
843
|
+
if nested_close is not None:
|
|
844
|
+
nested_close.done.set()
|
|
845
|
+
for observation_id in tuple(self._pending):
|
|
846
|
+
self._process_drop(observation_id, abandoned=True)
|
|
847
|
+
self._deliver_all_after_close()
|
|
848
|
+
with self._state_lock:
|
|
849
|
+
self._closed = True
|
|
850
|
+
finally:
|
|
851
|
+
self._release_flush_barriers()
|
|
852
|
+
record.done.set()
|
|
853
|
+
|
|
854
|
+
def _drain_after_deadline(self) -> None:
|
|
855
|
+
while True:
|
|
856
|
+
try:
|
|
857
|
+
record = self._records.get_nowait()
|
|
858
|
+
except queue.Empty:
|
|
859
|
+
return
|
|
860
|
+
if isinstance(record, _CloseRecord):
|
|
861
|
+
record.done.set()
|
|
862
|
+
continue
|
|
863
|
+
self._process(record)
|
|
864
|
+
|
|
865
|
+
def _freeze(self, window: _MutableDbWindow) -> _FrozenDbTimeBatch:
|
|
866
|
+
self._batch_seq += 1
|
|
867
|
+
batch_seq = self._batch_seq
|
|
868
|
+
batch_id = uuid.uuid5(self.pod_instance_id, str(batch_seq))
|
|
869
|
+
completed = tuple(
|
|
870
|
+
sorted(
|
|
871
|
+
window.observations.values(),
|
|
872
|
+
key=lambda item: item.pending.observation_id.hex,
|
|
873
|
+
)
|
|
874
|
+
)
|
|
875
|
+
observations = [
|
|
876
|
+
{
|
|
877
|
+
"observation_id": str(item.pending.observation_id),
|
|
878
|
+
"transaction_id": str(item.pending.transaction_id),
|
|
879
|
+
"request_id_hash": item.pending.request_id_hash,
|
|
880
|
+
"thread_id_hash": item.pending.thread_id_hash,
|
|
881
|
+
"trace_id": item.trace_id,
|
|
882
|
+
"span_id": item.span_id,
|
|
883
|
+
"linked_trace_id": item.linked_trace_id,
|
|
884
|
+
"customer_external_key": item.pending.customer_external_key,
|
|
885
|
+
"feature_key": item.pending.feature_key,
|
|
886
|
+
"started_at": _format_utc(item.pending.started_at),
|
|
887
|
+
"duration_ns": item.duration_ns,
|
|
888
|
+
"outcome": item.outcome,
|
|
889
|
+
}
|
|
890
|
+
for item in completed
|
|
891
|
+
]
|
|
892
|
+
dropped_count = window.sampled_count - len(observations)
|
|
893
|
+
sealed_at = window.sealed_at or window.window_start
|
|
894
|
+
elapsed = max(0.0, (sealed_at - window.window_start).total_seconds())
|
|
895
|
+
seconds_until_midnight = (
|
|
896
|
+
_next_midnight(window.window_start) - window.window_start
|
|
897
|
+
).total_seconds()
|
|
898
|
+
window_seconds = min(
|
|
899
|
+
300,
|
|
900
|
+
max(1, math.ceil(min(elapsed, seconds_until_midnight))),
|
|
901
|
+
)
|
|
902
|
+
payload = {
|
|
903
|
+
"schema_version": _SCHEMA_VERSION,
|
|
904
|
+
"service": self.service,
|
|
905
|
+
"pool_key": self.pool_key,
|
|
906
|
+
"pod_id": self.pod_id,
|
|
907
|
+
"pod_instance_id": str(self.pod_instance_id),
|
|
908
|
+
"batch_seq": batch_seq,
|
|
909
|
+
"batch_id": str(batch_id),
|
|
910
|
+
"window_start": _format_utc(window.window_start),
|
|
911
|
+
"window_seconds": window_seconds,
|
|
912
|
+
"sample_rate": format(
|
|
913
|
+
Decimal(str(self.sample_rate)).quantize(Decimal("0.00001")),
|
|
914
|
+
"f",
|
|
915
|
+
),
|
|
916
|
+
"attempted_count": window.attempted_count,
|
|
917
|
+
"sampled_count": window.sampled_count,
|
|
918
|
+
"dropped_count": dropped_count,
|
|
919
|
+
"observations": observations,
|
|
920
|
+
}
|
|
921
|
+
body = json.dumps(
|
|
922
|
+
payload,
|
|
923
|
+
ensure_ascii=True,
|
|
924
|
+
separators=(",", ":"),
|
|
925
|
+
sort_keys=True,
|
|
926
|
+
).encode("utf-8")
|
|
927
|
+
return _FrozenDbTimeBatch(
|
|
928
|
+
body=body,
|
|
929
|
+
batch_id=batch_id,
|
|
930
|
+
batch_seq=batch_seq,
|
|
931
|
+
pod_instance_id=self.pod_instance_id,
|
|
932
|
+
service=self.service,
|
|
933
|
+
pool_key=self.pool_key,
|
|
934
|
+
pod_id=self.pod_id,
|
|
935
|
+
window_start=payload["window_start"],
|
|
936
|
+
attempted_count=window.attempted_count,
|
|
937
|
+
sampled_count=window.sampled_count,
|
|
938
|
+
dropped_count=dropped_count,
|
|
939
|
+
observations=completed,
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
def _remove_window(self, window: _MutableDbWindow) -> None:
|
|
943
|
+
self._windows.pop(window.key, None)
|
|
944
|
+
for observation_id in window.observations:
|
|
945
|
+
self._completed_locations.pop(observation_id, None)
|
|
946
|
+
|
|
947
|
+
def _invoke_callback(self, frozen: _FrozenDbTimeBatch) -> None:
|
|
948
|
+
callback = self._flush_callback
|
|
949
|
+
if callback is None:
|
|
950
|
+
outcome = _DbTimeFlushOutcome.DELIVERED
|
|
951
|
+
else:
|
|
952
|
+
callback_failed = False
|
|
953
|
+
try:
|
|
954
|
+
outcome = callback(frozen)
|
|
955
|
+
if not isinstance(outcome, _DbTimeFlushOutcome):
|
|
956
|
+
callback_failed = True
|
|
957
|
+
outcome = _DbTimeFlushOutcome.RETRY_EXHAUSTED
|
|
958
|
+
except Exception:
|
|
959
|
+
callback_failed = True
|
|
960
|
+
outcome = _DbTimeFlushOutcome.RETRY_EXHAUSTED
|
|
961
|
+
if callback_failed:
|
|
962
|
+
self._increment("callback_failures")
|
|
963
|
+
self._safe_log(
|
|
964
|
+
"moolabs.attribution.db_time.callback_failure",
|
|
965
|
+
{"batch_seq": frozen.batch_seq},
|
|
966
|
+
)
|
|
967
|
+
if outcome is _DbTimeFlushOutcome.DELIVERED:
|
|
968
|
+
self._increment("delivered_batches")
|
|
969
|
+
elif outcome is _DbTimeFlushOutcome.TERMINAL_DROP:
|
|
970
|
+
self._increment("terminal_dropped_batches")
|
|
971
|
+
self._safe_log(
|
|
972
|
+
"moolabs.attribution.db_time.terminal_drop",
|
|
973
|
+
{"batch_seq": frozen.batch_seq},
|
|
974
|
+
)
|
|
975
|
+
else:
|
|
976
|
+
self._increment("retry_exhausted_batches")
|
|
977
|
+
self._safe_log(
|
|
978
|
+
"moolabs.attribution.db_time.retry_exhausted",
|
|
979
|
+
{"batch_seq": frozen.batch_seq},
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
def _set_pending_count(self) -> None:
|
|
983
|
+
with self._state_lock:
|
|
984
|
+
self._stats["pending_count"] = len(self._pending)
|
|
985
|
+
|
|
986
|
+
def _increment(self, name: str, amount: int = 1) -> None:
|
|
987
|
+
with self._state_lock:
|
|
988
|
+
self._stats[name] += amount
|
|
989
|
+
|
|
990
|
+
def _safe_log(self, event_name: str, fields: dict[str, Any]) -> None:
|
|
991
|
+
if self._logger is None:
|
|
992
|
+
return
|
|
993
|
+
try:
|
|
994
|
+
self._logger(event_name, fields)
|
|
995
|
+
except Exception:
|
|
996
|
+
return
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
class _DbTimeSpanProcessor(SpanProcessor):
|
|
1000
|
+
def __init__(self, handle: DbTimeDriverHandle) -> None:
|
|
1001
|
+
self._handle_ref = weakref.ref(handle)
|
|
1002
|
+
self._lock = threading.Lock()
|
|
1003
|
+
self._enabled = True
|
|
1004
|
+
|
|
1005
|
+
@property
|
|
1006
|
+
def enabled(self) -> bool:
|
|
1007
|
+
with self._lock:
|
|
1008
|
+
return self._enabled
|
|
1009
|
+
|
|
1010
|
+
def disable(self) -> None:
|
|
1011
|
+
with self._lock:
|
|
1012
|
+
self._enabled = False
|
|
1013
|
+
|
|
1014
|
+
def on_start(
|
|
1015
|
+
self,
|
|
1016
|
+
span: Any,
|
|
1017
|
+
parent_context: Context | None = None,
|
|
1018
|
+
) -> None:
|
|
1019
|
+
del span, parent_context
|
|
1020
|
+
|
|
1021
|
+
def on_end(self, span: ReadableSpan) -> None:
|
|
1022
|
+
try:
|
|
1023
|
+
if not self.enabled or span.name != _SPAN_NAME:
|
|
1024
|
+
return
|
|
1025
|
+
attributes = span.attributes or {}
|
|
1026
|
+
schema_version = attributes.get("moolabs.db_time.schema_version")
|
|
1027
|
+
if type(schema_version) is not int or schema_version != _SCHEMA_VERSION:
|
|
1028
|
+
return
|
|
1029
|
+
comment = attributes.get("moolabs.db_time.sql_comment")
|
|
1030
|
+
if not isinstance(comment, str):
|
|
1031
|
+
return
|
|
1032
|
+
match = _COMMENT_RE.fullmatch(comment)
|
|
1033
|
+
if match is None:
|
|
1034
|
+
return
|
|
1035
|
+
|
|
1036
|
+
handle = self._handle_ref()
|
|
1037
|
+
if handle is None:
|
|
1038
|
+
return
|
|
1039
|
+
outcome = attributes.get("moolabs.db_time.outcome")
|
|
1040
|
+
if outcome not in {"ok", "error"}:
|
|
1041
|
+
return
|
|
1042
|
+
start_time = span.start_time
|
|
1043
|
+
end_time = span.end_time
|
|
1044
|
+
if start_time is None or end_time is None:
|
|
1045
|
+
return
|
|
1046
|
+
|
|
1047
|
+
linked_trace_id: str | None = None
|
|
1048
|
+
for link in span.links:
|
|
1049
|
+
if link.context.is_valid:
|
|
1050
|
+
linked_trace_id = f"{link.context.trace_id:032x}"
|
|
1051
|
+
break
|
|
1052
|
+
handle._enqueue_span_end(
|
|
1053
|
+
observation_id=uuid.UUID(hex=match.group(1)),
|
|
1054
|
+
trace_id=f"{span.context.trace_id:032x}",
|
|
1055
|
+
span_id=f"{span.context.span_id:016x}",
|
|
1056
|
+
linked_trace_id=linked_trace_id,
|
|
1057
|
+
duration_ns=max(0, int(end_time) - int(start_time)),
|
|
1058
|
+
outcome=outcome,
|
|
1059
|
+
)
|
|
1060
|
+
except Exception:
|
|
1061
|
+
return
|
|
1062
|
+
|
|
1063
|
+
def shutdown(self) -> None:
|
|
1064
|
+
self.disable()
|
|
1065
|
+
|
|
1066
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
1067
|
+
del timeout_millis
|
|
1068
|
+
return True
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
class _ProviderDbTimeSpanProcessor(SpanProcessor):
|
|
1072
|
+
"""One provider-owned dispatcher for all live DB-time handles."""
|
|
1073
|
+
|
|
1074
|
+
def __init__(self) -> None:
|
|
1075
|
+
self._process_id = os.getpid()
|
|
1076
|
+
self._lock = threading.RLock()
|
|
1077
|
+
self._processors: weakref.WeakSet[_DbTimeSpanProcessor] = weakref.WeakSet()
|
|
1078
|
+
self._shutdown = False
|
|
1079
|
+
|
|
1080
|
+
def register(self, processor: _DbTimeSpanProcessor) -> None:
|
|
1081
|
+
if self._process_id != os.getpid():
|
|
1082
|
+
raise RuntimeError(
|
|
1083
|
+
"inherited tracer_provider cannot be reused after fork; "
|
|
1084
|
+
"create a child-owned tracer provider"
|
|
1085
|
+
)
|
|
1086
|
+
with self._lock:
|
|
1087
|
+
if self._shutdown:
|
|
1088
|
+
raise RuntimeError("tracer provider DB-time processor is shut down")
|
|
1089
|
+
self._processors.add(processor)
|
|
1090
|
+
|
|
1091
|
+
def unregister(self, processor: _DbTimeSpanProcessor) -> None:
|
|
1092
|
+
if self._process_id != os.getpid():
|
|
1093
|
+
return
|
|
1094
|
+
with self._lock:
|
|
1095
|
+
self._processors.discard(processor)
|
|
1096
|
+
|
|
1097
|
+
def _snapshot(self) -> tuple[_DbTimeSpanProcessor, ...]:
|
|
1098
|
+
if self._process_id != os.getpid():
|
|
1099
|
+
return ()
|
|
1100
|
+
with self._lock:
|
|
1101
|
+
if self._shutdown:
|
|
1102
|
+
return ()
|
|
1103
|
+
return tuple(self._processors)
|
|
1104
|
+
|
|
1105
|
+
def on_start(
|
|
1106
|
+
self,
|
|
1107
|
+
span: Any,
|
|
1108
|
+
parent_context: Context | None = None,
|
|
1109
|
+
) -> None:
|
|
1110
|
+
for processor in self._snapshot():
|
|
1111
|
+
try:
|
|
1112
|
+
processor.on_start(span, parent_context)
|
|
1113
|
+
except Exception:
|
|
1114
|
+
continue
|
|
1115
|
+
|
|
1116
|
+
def on_end(self, span: ReadableSpan) -> None:
|
|
1117
|
+
for processor in self._snapshot():
|
|
1118
|
+
try:
|
|
1119
|
+
processor.on_end(span)
|
|
1120
|
+
except Exception:
|
|
1121
|
+
continue
|
|
1122
|
+
|
|
1123
|
+
def shutdown(self) -> None:
|
|
1124
|
+
if self._process_id != os.getpid():
|
|
1125
|
+
return
|
|
1126
|
+
with self._lock:
|
|
1127
|
+
if self._shutdown:
|
|
1128
|
+
return
|
|
1129
|
+
self._shutdown = True
|
|
1130
|
+
processors = tuple(self._processors)
|
|
1131
|
+
self._processors.clear()
|
|
1132
|
+
for processor in processors:
|
|
1133
|
+
try:
|
|
1134
|
+
processor.shutdown()
|
|
1135
|
+
except Exception:
|
|
1136
|
+
continue
|
|
1137
|
+
|
|
1138
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
1139
|
+
successful = True
|
|
1140
|
+
for processor in self._snapshot():
|
|
1141
|
+
try:
|
|
1142
|
+
successful = processor.force_flush(timeout_millis) and successful
|
|
1143
|
+
except Exception:
|
|
1144
|
+
successful = False
|
|
1145
|
+
return successful
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
class DbTimeDriverHandle:
|
|
1149
|
+
"""Own SQLAlchemy listeners and the local redacted span processor."""
|
|
1150
|
+
|
|
1151
|
+
def __init__(
|
|
1152
|
+
self,
|
|
1153
|
+
sync_engine: Engine,
|
|
1154
|
+
config: _InstallConfig,
|
|
1155
|
+
tracer_provider: Any,
|
|
1156
|
+
*,
|
|
1157
|
+
pod_id: str,
|
|
1158
|
+
queue_capacity: int,
|
|
1159
|
+
overflow_bucket_capacity: int,
|
|
1160
|
+
batch_size: int,
|
|
1161
|
+
flush_interval_sec: float,
|
|
1162
|
+
shutdown_timeout_sec: float,
|
|
1163
|
+
flush_callback: DbTimeFlushCallback | None,
|
|
1164
|
+
logger: Callable[[str, dict[str, Any]], None] | None,
|
|
1165
|
+
utc_now: Callable[[], datetime],
|
|
1166
|
+
monotonic: Callable[[], float],
|
|
1167
|
+
) -> None:
|
|
1168
|
+
self._process_id = os.getpid()
|
|
1169
|
+
self.sync_engine = sync_engine
|
|
1170
|
+
self.service = config.service
|
|
1171
|
+
self.pool_key = config.pool_key
|
|
1172
|
+
self.sample_rate = config.sample_rate
|
|
1173
|
+
self._config = config
|
|
1174
|
+
self._tracer_provider = tracer_provider
|
|
1175
|
+
self._tracer: Any = None
|
|
1176
|
+
self._tracer_lock = threading.Lock()
|
|
1177
|
+
self._shutdown_timeout_sec = shutdown_timeout_sec
|
|
1178
|
+
self._utc_now = utc_now
|
|
1179
|
+
self._monotonic = monotonic
|
|
1180
|
+
self._owner = _DbTimeWindowOwner(
|
|
1181
|
+
service=self.service,
|
|
1182
|
+
pool_key=self.pool_key,
|
|
1183
|
+
pod_id=pod_id,
|
|
1184
|
+
sample_rate=self.sample_rate,
|
|
1185
|
+
queue_capacity=queue_capacity,
|
|
1186
|
+
overflow_bucket_capacity=overflow_bucket_capacity,
|
|
1187
|
+
batch_size=batch_size,
|
|
1188
|
+
flush_interval_sec=flush_interval_sec,
|
|
1189
|
+
flush_callback=flush_callback,
|
|
1190
|
+
logger=logger,
|
|
1191
|
+
utc_now=utc_now,
|
|
1192
|
+
monotonic=monotonic,
|
|
1193
|
+
)
|
|
1194
|
+
self.pod_id = pod_id
|
|
1195
|
+
self.pod_instance_id = self._owner.pod_instance_id
|
|
1196
|
+
self._worker = self._owner.thread
|
|
1197
|
+
self._processor = _DbTimeSpanProcessor(self)
|
|
1198
|
+
self._provider_processor: _ProviderDbTimeSpanProcessor | None = None
|
|
1199
|
+
self._listeners: list[tuple[Any, str, Callable[..., Any]]] = []
|
|
1200
|
+
self._state_lock = threading.RLock()
|
|
1201
|
+
self._start_gate = threading.RLock()
|
|
1202
|
+
self._close_complete = threading.Event()
|
|
1203
|
+
self._closed = False
|
|
1204
|
+
self._accepting_starts = True
|
|
1205
|
+
self._finalizer = weakref.finalize(
|
|
1206
|
+
self,
|
|
1207
|
+
DbTimeDriverHandle._finalize_owner,
|
|
1208
|
+
self._owner,
|
|
1209
|
+
self._monotonic,
|
|
1210
|
+
self._process_id,
|
|
1211
|
+
)
|
|
1212
|
+
|
|
1213
|
+
def _is_current_process(self) -> bool:
|
|
1214
|
+
return self._process_id == os.getpid()
|
|
1215
|
+
|
|
1216
|
+
def _require_current_process(self) -> None:
|
|
1217
|
+
if not self._is_current_process():
|
|
1218
|
+
raise RuntimeError(
|
|
1219
|
+
"DB-time handle belongs to a parent process; install a child-owned handle"
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
@property
|
|
1223
|
+
def closed(self) -> bool:
|
|
1224
|
+
if not self._is_current_process():
|
|
1225
|
+
return True
|
|
1226
|
+
with self._state_lock:
|
|
1227
|
+
return self._closed
|
|
1228
|
+
|
|
1229
|
+
def __enter__(self) -> DbTimeDriverHandle:
|
|
1230
|
+
return self
|
|
1231
|
+
|
|
1232
|
+
def __exit__(self, *exc_info: Any) -> None:
|
|
1233
|
+
del exc_info
|
|
1234
|
+
self.close()
|
|
1235
|
+
|
|
1236
|
+
def stats(self) -> dict[str, int]:
|
|
1237
|
+
self._require_current_process()
|
|
1238
|
+
return self._owner.stats()
|
|
1239
|
+
|
|
1240
|
+
def flush(self) -> None:
|
|
1241
|
+
self._require_current_process()
|
|
1242
|
+
self._owner.flush(force=True)
|
|
1243
|
+
|
|
1244
|
+
def flush_ready_windows(self) -> None:
|
|
1245
|
+
self._require_current_process()
|
|
1246
|
+
self._owner.flush(force=False)
|
|
1247
|
+
|
|
1248
|
+
def close(self) -> None:
|
|
1249
|
+
if not self._is_current_process():
|
|
1250
|
+
return
|
|
1251
|
+
with self._state_lock:
|
|
1252
|
+
if self._closed:
|
|
1253
|
+
close_complete = self._close_complete
|
|
1254
|
+
if threading.current_thread() is self._worker:
|
|
1255
|
+
return
|
|
1256
|
+
else:
|
|
1257
|
+
self._closed = True
|
|
1258
|
+
close_complete = None
|
|
1259
|
+
|
|
1260
|
+
if close_complete is not None:
|
|
1261
|
+
close_complete.wait()
|
|
1262
|
+
if self._worker.is_alive():
|
|
1263
|
+
self._worker.join()
|
|
1264
|
+
return
|
|
1265
|
+
|
|
1266
|
+
with self._start_gate:
|
|
1267
|
+
self._accepting_starts = False
|
|
1268
|
+
|
|
1269
|
+
deadline = self._monotonic() + self._shutdown_timeout_sec
|
|
1270
|
+
done = self._owner.request_close(deadline)
|
|
1271
|
+
current_worker = threading.current_thread() is self._worker
|
|
1272
|
+
if current_worker:
|
|
1273
|
+
threading.Thread(
|
|
1274
|
+
target=self._complete_close,
|
|
1275
|
+
args=(done,),
|
|
1276
|
+
name="moolabs-db-time-close",
|
|
1277
|
+
daemon=True,
|
|
1278
|
+
).start()
|
|
1279
|
+
return
|
|
1280
|
+
self._complete_close(done)
|
|
1281
|
+
|
|
1282
|
+
def _complete_close(self, done: threading.Event) -> None:
|
|
1283
|
+
try:
|
|
1284
|
+
done.wait()
|
|
1285
|
+
if self._worker.is_alive():
|
|
1286
|
+
self._worker.join()
|
|
1287
|
+
self._remove_listeners()
|
|
1288
|
+
provider_processor = self._provider_processor
|
|
1289
|
+
self._provider_processor = None
|
|
1290
|
+
self._processor.disable()
|
|
1291
|
+
if provider_processor is not None:
|
|
1292
|
+
provider_processor.unregister(self._processor)
|
|
1293
|
+
with _INSTALLS_LOCK:
|
|
1294
|
+
installed = _INSTALLS.get(self.sync_engine)
|
|
1295
|
+
if installed is not None and installed() is self:
|
|
1296
|
+
del _INSTALLS[self.sync_engine]
|
|
1297
|
+
self._finalizer.detach()
|
|
1298
|
+
finally:
|
|
1299
|
+
self._close_complete.set()
|
|
1300
|
+
|
|
1301
|
+
def _remove_listeners(self) -> None:
|
|
1302
|
+
for target, identifier, listener in reversed(self._listeners):
|
|
1303
|
+
try:
|
|
1304
|
+
event.remove(target, identifier, listener)
|
|
1305
|
+
except Exception:
|
|
1306
|
+
continue
|
|
1307
|
+
self._listeners.clear()
|
|
1308
|
+
|
|
1309
|
+
@staticmethod
|
|
1310
|
+
def _finalize_owner(
|
|
1311
|
+
owner: _DbTimeWindowOwner,
|
|
1312
|
+
monotonic: Callable[[], float],
|
|
1313
|
+
process_id: int,
|
|
1314
|
+
) -> None:
|
|
1315
|
+
if process_id != os.getpid():
|
|
1316
|
+
return
|
|
1317
|
+
owner.request_close(monotonic())
|
|
1318
|
+
|
|
1319
|
+
def _register_listeners(self) -> None:
|
|
1320
|
+
listeners: tuple[tuple[Any, str, Callable[..., Any], bool], ...] = (
|
|
1321
|
+
(self.sync_engine, "begin", self._on_begin, False),
|
|
1322
|
+
(self.sync_engine, "commit", self._on_commit, False),
|
|
1323
|
+
(self.sync_engine, "rollback", self._on_rollback, False),
|
|
1324
|
+
(self.sync_engine, "savepoint", self._on_savepoint, False),
|
|
1325
|
+
(
|
|
1326
|
+
self.sync_engine,
|
|
1327
|
+
"rollback_savepoint",
|
|
1328
|
+
self._on_rollback_savepoint,
|
|
1329
|
+
False,
|
|
1330
|
+
),
|
|
1331
|
+
(
|
|
1332
|
+
self.sync_engine,
|
|
1333
|
+
"release_savepoint",
|
|
1334
|
+
self._on_release_savepoint,
|
|
1335
|
+
False,
|
|
1336
|
+
),
|
|
1337
|
+
(
|
|
1338
|
+
self.sync_engine,
|
|
1339
|
+
"before_cursor_execute",
|
|
1340
|
+
self._before_cursor_execute,
|
|
1341
|
+
True,
|
|
1342
|
+
),
|
|
1343
|
+
(
|
|
1344
|
+
self.sync_engine,
|
|
1345
|
+
"after_cursor_execute",
|
|
1346
|
+
self._after_cursor_execute,
|
|
1347
|
+
False,
|
|
1348
|
+
),
|
|
1349
|
+
(self.sync_engine, "handle_error", self._handle_error, False),
|
|
1350
|
+
(self.sync_engine, "checkin", self._on_checkin, False),
|
|
1351
|
+
)
|
|
1352
|
+
try:
|
|
1353
|
+
for target, identifier, listener, returns_value in listeners:
|
|
1354
|
+
if returns_value:
|
|
1355
|
+
event.listen(target, identifier, listener, retval=True)
|
|
1356
|
+
else:
|
|
1357
|
+
event.listen(target, identifier, listener)
|
|
1358
|
+
self._listeners.append((target, identifier, listener))
|
|
1359
|
+
except Exception:
|
|
1360
|
+
for target, identifier, listener in reversed(self._listeners):
|
|
1361
|
+
try:
|
|
1362
|
+
event.remove(target, identifier, listener)
|
|
1363
|
+
except Exception:
|
|
1364
|
+
continue
|
|
1365
|
+
raise
|
|
1366
|
+
|
|
1367
|
+
def _get_tracer(self) -> Any:
|
|
1368
|
+
with self._tracer_lock:
|
|
1369
|
+
if self._tracer is None:
|
|
1370
|
+
self._tracer = self._tracer_provider.get_tracer(
|
|
1371
|
+
"moolabs.db_time",
|
|
1372
|
+
"1",
|
|
1373
|
+
)
|
|
1374
|
+
return self._tracer
|
|
1375
|
+
|
|
1376
|
+
def _enqueue_start(
|
|
1377
|
+
self,
|
|
1378
|
+
pending: _PendingDbObservation | None,
|
|
1379
|
+
*,
|
|
1380
|
+
started_at: datetime | None = None,
|
|
1381
|
+
) -> None:
|
|
1382
|
+
if not self._is_current_process():
|
|
1383
|
+
return
|
|
1384
|
+
with self._start_gate:
|
|
1385
|
+
if not self._accepting_starts:
|
|
1386
|
+
return
|
|
1387
|
+
self._owner.submit(
|
|
1388
|
+
_StartRecord(
|
|
1389
|
+
started_at=started_at
|
|
1390
|
+
or (pending.started_at if pending else self._utc_now()),
|
|
1391
|
+
pending=pending,
|
|
1392
|
+
)
|
|
1393
|
+
)
|
|
1394
|
+
|
|
1395
|
+
def _enqueue_span_end(
|
|
1396
|
+
self,
|
|
1397
|
+
*,
|
|
1398
|
+
observation_id: uuid.UUID,
|
|
1399
|
+
trace_id: str,
|
|
1400
|
+
span_id: str,
|
|
1401
|
+
linked_trace_id: str | None,
|
|
1402
|
+
duration_ns: int,
|
|
1403
|
+
outcome: Literal["ok", "error"],
|
|
1404
|
+
) -> None:
|
|
1405
|
+
if not self._is_current_process():
|
|
1406
|
+
return
|
|
1407
|
+
self._owner.submit(
|
|
1408
|
+
_EndRecord(
|
|
1409
|
+
observation_id=observation_id,
|
|
1410
|
+
trace_id=trace_id,
|
|
1411
|
+
span_id=span_id,
|
|
1412
|
+
linked_trace_id=linked_trace_id,
|
|
1413
|
+
duration_ns=duration_ns,
|
|
1414
|
+
outcome=outcome,
|
|
1415
|
+
)
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
def _enqueue_completed(self, completed: _CompletedDbObservation) -> None:
|
|
1419
|
+
self._enqueue_span_end(
|
|
1420
|
+
observation_id=completed.pending.observation_id,
|
|
1421
|
+
trace_id=completed.trace_id,
|
|
1422
|
+
span_id=completed.span_id,
|
|
1423
|
+
linked_trace_id=completed.linked_trace_id,
|
|
1424
|
+
duration_ns=completed.duration_ns,
|
|
1425
|
+
outcome=completed.outcome,
|
|
1426
|
+
)
|
|
1427
|
+
|
|
1428
|
+
def _enqueue_drop(self, observation_key: str | None) -> None:
|
|
1429
|
+
if not self._is_current_process():
|
|
1430
|
+
return
|
|
1431
|
+
if observation_key is None:
|
|
1432
|
+
return
|
|
1433
|
+
try:
|
|
1434
|
+
observation_id = uuid.UUID(hex=observation_key)
|
|
1435
|
+
except (ValueError, AttributeError):
|
|
1436
|
+
return
|
|
1437
|
+
self._owner.submit(_DropRecord(observation_id))
|
|
1438
|
+
|
|
1439
|
+
def _on_begin(self, connection: Any) -> None:
|
|
1440
|
+
if not self._is_current_process():
|
|
1441
|
+
return
|
|
1442
|
+
try:
|
|
1443
|
+
connection.info[_TRANSACTION_ID_KEY] = uuid.uuid4()
|
|
1444
|
+
except Exception:
|
|
1445
|
+
return
|
|
1446
|
+
|
|
1447
|
+
def _on_commit(self, connection: Any) -> None:
|
|
1448
|
+
if not self._is_current_process():
|
|
1449
|
+
return
|
|
1450
|
+
self._clear_transaction(connection)
|
|
1451
|
+
|
|
1452
|
+
def _on_rollback(self, connection: Any) -> None:
|
|
1453
|
+
if not self._is_current_process():
|
|
1454
|
+
return
|
|
1455
|
+
self._clear_transaction(connection)
|
|
1456
|
+
|
|
1457
|
+
def _on_savepoint(self, connection: Any, name: str | None) -> None:
|
|
1458
|
+
del connection, name
|
|
1459
|
+
|
|
1460
|
+
def _on_rollback_savepoint(
|
|
1461
|
+
self,
|
|
1462
|
+
connection: Any,
|
|
1463
|
+
name: str | None,
|
|
1464
|
+
context: Any,
|
|
1465
|
+
) -> None:
|
|
1466
|
+
del connection, name, context
|
|
1467
|
+
|
|
1468
|
+
def _on_release_savepoint(
|
|
1469
|
+
self,
|
|
1470
|
+
connection: Any,
|
|
1471
|
+
name: str | None,
|
|
1472
|
+
context: Any,
|
|
1473
|
+
) -> None:
|
|
1474
|
+
del connection, name, context
|
|
1475
|
+
|
|
1476
|
+
def _on_checkin(self, dbapi_connection: Any, connection_record: Any) -> None:
|
|
1477
|
+
del dbapi_connection
|
|
1478
|
+
if not self._is_current_process():
|
|
1479
|
+
return
|
|
1480
|
+
try:
|
|
1481
|
+
connection_record.info.pop(_TRANSACTION_ID_KEY, None)
|
|
1482
|
+
except Exception:
|
|
1483
|
+
return
|
|
1484
|
+
|
|
1485
|
+
def _clear_transaction(self, connection: Any) -> None:
|
|
1486
|
+
if not self._is_current_process():
|
|
1487
|
+
return
|
|
1488
|
+
try:
|
|
1489
|
+
connection.info.pop(_TRANSACTION_ID_KEY, None)
|
|
1490
|
+
except Exception:
|
|
1491
|
+
return
|
|
1492
|
+
|
|
1493
|
+
def _transaction_id(self, connection: Any) -> tuple[uuid.UUID, bool]:
|
|
1494
|
+
try:
|
|
1495
|
+
current = connection.info.get(_TRANSACTION_ID_KEY)
|
|
1496
|
+
if isinstance(current, uuid.UUID):
|
|
1497
|
+
return current, False
|
|
1498
|
+
statement_local = uuid.uuid4()
|
|
1499
|
+
connection.info[_TRANSACTION_ID_KEY] = statement_local
|
|
1500
|
+
return statement_local, True
|
|
1501
|
+
except Exception:
|
|
1502
|
+
return uuid.uuid4(), True
|
|
1503
|
+
|
|
1504
|
+
def _before_cursor_execute(
|
|
1505
|
+
self,
|
|
1506
|
+
connection: Any,
|
|
1507
|
+
cursor: Any,
|
|
1508
|
+
statement: str,
|
|
1509
|
+
parameters: Any,
|
|
1510
|
+
context: Any,
|
|
1511
|
+
executemany: bool,
|
|
1512
|
+
) -> tuple[str, Any]:
|
|
1513
|
+
del cursor, executemany
|
|
1514
|
+
if not self._is_current_process():
|
|
1515
|
+
return statement, parameters
|
|
1516
|
+
with self._start_gate:
|
|
1517
|
+
if not self._accepting_starts:
|
|
1518
|
+
return statement, parameters
|
|
1519
|
+
return self._instrument_statement_start(
|
|
1520
|
+
connection,
|
|
1521
|
+
statement,
|
|
1522
|
+
parameters,
|
|
1523
|
+
context,
|
|
1524
|
+
)
|
|
1525
|
+
|
|
1526
|
+
def _instrument_statement_start(
|
|
1527
|
+
self,
|
|
1528
|
+
connection: Any,
|
|
1529
|
+
statement: str,
|
|
1530
|
+
parameters: Any,
|
|
1531
|
+
context: Any,
|
|
1532
|
+
) -> tuple[str, Any]:
|
|
1533
|
+
observation_id = uuid.uuid4()
|
|
1534
|
+
observation_key = observation_id.hex
|
|
1535
|
+
comment = f"/*moolabs_db_time:v1;ob={observation_key}*/"
|
|
1536
|
+
prefixed_statement = f"{comment} {statement}"
|
|
1537
|
+
statement_local = False
|
|
1538
|
+
try:
|
|
1539
|
+
started_at = _as_utc(self._utc_now())
|
|
1540
|
+
except Exception:
|
|
1541
|
+
started_at = datetime.now(timezone.utc)
|
|
1542
|
+
|
|
1543
|
+
span: Any = None
|
|
1544
|
+
start_enqueued = False
|
|
1545
|
+
sampled_start_enqueued = False
|
|
1546
|
+
try:
|
|
1547
|
+
transaction_id, statement_local = self._transaction_id(connection)
|
|
1548
|
+
pending = _pending_observation(
|
|
1549
|
+
observation_id,
|
|
1550
|
+
transaction_id,
|
|
1551
|
+
started_at=started_at,
|
|
1552
|
+
)
|
|
1553
|
+
parent = trace.get_current_span().get_span_context()
|
|
1554
|
+
links = [Link(parent)] if parent.is_valid else []
|
|
1555
|
+
span = self._get_tracer().start_span(
|
|
1556
|
+
_SPAN_NAME,
|
|
1557
|
+
context=Context(),
|
|
1558
|
+
kind=SpanKind.CLIENT,
|
|
1559
|
+
links=links,
|
|
1560
|
+
)
|
|
1561
|
+
span_context = span.get_span_context()
|
|
1562
|
+
sampled = bool(span_context.trace_flags & TraceFlags.SAMPLED)
|
|
1563
|
+
state = _StatementState(
|
|
1564
|
+
span=span,
|
|
1565
|
+
observation_key=observation_key if sampled else None,
|
|
1566
|
+
sampled=sampled,
|
|
1567
|
+
statement_local_transaction=statement_local,
|
|
1568
|
+
)
|
|
1569
|
+
if sampled:
|
|
1570
|
+
values = (
|
|
1571
|
+
"postgresql",
|
|
1572
|
+
_SCHEMA_VERSION,
|
|
1573
|
+
comment,
|
|
1574
|
+
observation_key,
|
|
1575
|
+
self.service,
|
|
1576
|
+
self.pool_key,
|
|
1577
|
+
)
|
|
1578
|
+
for attribute, value in zip(
|
|
1579
|
+
_ALLOWED_SPAN_ATTRIBUTES,
|
|
1580
|
+
values,
|
|
1581
|
+
strict=True,
|
|
1582
|
+
):
|
|
1583
|
+
span.set_attribute(attribute, value)
|
|
1584
|
+
setattr(context, _STATEMENT_STATE_KEY, state)
|
|
1585
|
+
self._enqueue_start(pending if sampled else None, started_at=started_at)
|
|
1586
|
+
start_enqueued = True
|
|
1587
|
+
sampled_start_enqueued = sampled
|
|
1588
|
+
except Exception:
|
|
1589
|
+
if not start_enqueued:
|
|
1590
|
+
try:
|
|
1591
|
+
self._enqueue_start(None, started_at=started_at)
|
|
1592
|
+
except Exception:
|
|
1593
|
+
pass
|
|
1594
|
+
elif sampled_start_enqueued:
|
|
1595
|
+
self._enqueue_drop(observation_key)
|
|
1596
|
+
if span is not None:
|
|
1597
|
+
try:
|
|
1598
|
+
span.end()
|
|
1599
|
+
except Exception:
|
|
1600
|
+
pass
|
|
1601
|
+
if statement_local:
|
|
1602
|
+
self._clear_transaction(connection)
|
|
1603
|
+
return prefixed_statement, parameters
|
|
1604
|
+
|
|
1605
|
+
def _after_cursor_execute(
|
|
1606
|
+
self,
|
|
1607
|
+
connection: Any,
|
|
1608
|
+
cursor: Any,
|
|
1609
|
+
statement: str,
|
|
1610
|
+
parameters: Any,
|
|
1611
|
+
context: Any,
|
|
1612
|
+
executemany: bool,
|
|
1613
|
+
) -> None:
|
|
1614
|
+
del cursor, statement, parameters, executemany
|
|
1615
|
+
if not self._is_current_process():
|
|
1616
|
+
return
|
|
1617
|
+
try:
|
|
1618
|
+
self._finish_statement(connection, context, "ok")
|
|
1619
|
+
except Exception:
|
|
1620
|
+
return
|
|
1621
|
+
|
|
1622
|
+
def _handle_error(self, exception_context: Any) -> None:
|
|
1623
|
+
if not self._is_current_process():
|
|
1624
|
+
return None
|
|
1625
|
+
try:
|
|
1626
|
+
self._finish_statement(
|
|
1627
|
+
exception_context.connection,
|
|
1628
|
+
exception_context.execution_context,
|
|
1629
|
+
"error",
|
|
1630
|
+
)
|
|
1631
|
+
except Exception:
|
|
1632
|
+
return None
|
|
1633
|
+
return None
|
|
1634
|
+
|
|
1635
|
+
def _finish_statement(
|
|
1636
|
+
self,
|
|
1637
|
+
connection: Any,
|
|
1638
|
+
context: Any,
|
|
1639
|
+
outcome: Literal["ok", "error"],
|
|
1640
|
+
) -> None:
|
|
1641
|
+
if not self._is_current_process():
|
|
1642
|
+
return
|
|
1643
|
+
state = getattr(context, _STATEMENT_STATE_KEY, None)
|
|
1644
|
+
if not isinstance(state, _StatementState) or not state.mark_ended():
|
|
1645
|
+
return
|
|
1646
|
+
try:
|
|
1647
|
+
if state.sampled:
|
|
1648
|
+
try:
|
|
1649
|
+
state.span.set_attribute("moolabs.db_time.outcome", outcome)
|
|
1650
|
+
except Exception:
|
|
1651
|
+
self._enqueue_drop(state.observation_key)
|
|
1652
|
+
try:
|
|
1653
|
+
state.span.end()
|
|
1654
|
+
except Exception:
|
|
1655
|
+
self._enqueue_drop(state.observation_key)
|
|
1656
|
+
finally:
|
|
1657
|
+
if state.statement_local_transaction:
|
|
1658
|
+
self._clear_transaction(connection)
|
|
1659
|
+
try:
|
|
1660
|
+
delattr(context, _STATEMENT_STATE_KEY)
|
|
1661
|
+
except Exception:
|
|
1662
|
+
pass
|
|
1663
|
+
|
|
1664
|
+
|
|
1665
|
+
_PROCESS_ID = os.getpid()
|
|
1666
|
+
_INSTALLS_LOCK = threading.RLock()
|
|
1667
|
+
_INSTALLS: weakref.WeakKeyDictionary[
|
|
1668
|
+
Engine,
|
|
1669
|
+
weakref.ReferenceType[DbTimeDriverHandle],
|
|
1670
|
+
] = weakref.WeakKeyDictionary()
|
|
1671
|
+
_PROVIDER_PROCESSORS_LOCK = threading.RLock()
|
|
1672
|
+
_PROVIDER_PROCESSORS: weakref.WeakKeyDictionary[
|
|
1673
|
+
Any,
|
|
1674
|
+
_ProviderDbTimeSpanProcessor,
|
|
1675
|
+
] = weakref.WeakKeyDictionary()
|
|
1676
|
+
|
|
1677
|
+
|
|
1678
|
+
def _ensure_process_state() -> None:
|
|
1679
|
+
global _PROCESS_ID
|
|
1680
|
+
global _INSTALLS_LOCK, _INSTALLS
|
|
1681
|
+
global _PROVIDER_PROCESSORS_LOCK
|
|
1682
|
+
|
|
1683
|
+
process_id = os.getpid()
|
|
1684
|
+
if process_id == _PROCESS_ID:
|
|
1685
|
+
return
|
|
1686
|
+
_INSTALLS_LOCK = threading.RLock()
|
|
1687
|
+
_INSTALLS = weakref.WeakKeyDictionary()
|
|
1688
|
+
_PROVIDER_PROCESSORS_LOCK = threading.RLock()
|
|
1689
|
+
_PROCESS_ID = process_id
|
|
1690
|
+
|
|
1691
|
+
|
|
1692
|
+
def _provider_db_time_processor(provider: Any) -> _ProviderDbTimeSpanProcessor:
|
|
1693
|
+
_validate_provider_process(provider)
|
|
1694
|
+
with _PROVIDER_PROCESSORS_LOCK:
|
|
1695
|
+
processor = _PROVIDER_PROCESSORS.get(provider)
|
|
1696
|
+
if processor is not None:
|
|
1697
|
+
return processor
|
|
1698
|
+
_validate_provider_registration_available(provider)
|
|
1699
|
+
processor = _ProviderDbTimeSpanProcessor()
|
|
1700
|
+
provider.add_span_processor(processor)
|
|
1701
|
+
_PROVIDER_PROCESSORS[provider] = processor
|
|
1702
|
+
return processor
|
|
1703
|
+
|
|
1704
|
+
|
|
1705
|
+
def _validate_provider_process(provider: Any) -> None:
|
|
1706
|
+
try:
|
|
1707
|
+
weakref.ref(provider)
|
|
1708
|
+
except TypeError as exc:
|
|
1709
|
+
raise TypeError("tracer_provider must support weak references") from exc
|
|
1710
|
+
|
|
1711
|
+
with _PROVIDER_PROCESSORS_LOCK:
|
|
1712
|
+
processor = _PROVIDER_PROCESSORS.get(provider)
|
|
1713
|
+
if processor is not None and processor._process_id != os.getpid():
|
|
1714
|
+
raise RuntimeError(
|
|
1715
|
+
"inherited tracer_provider cannot be reused after fork; "
|
|
1716
|
+
"create a child-owned tracer provider"
|
|
1717
|
+
)
|
|
1718
|
+
|
|
1719
|
+
|
|
1720
|
+
def _validate_provider_registration_available(provider: Any) -> None:
|
|
1721
|
+
active_processor = getattr(provider, "_active_span_processor", None)
|
|
1722
|
+
registrar_lock = getattr(active_processor, "_lock", None)
|
|
1723
|
+
lock_is_held = getattr(registrar_lock, "locked", None)
|
|
1724
|
+
if not callable(lock_is_held):
|
|
1725
|
+
raise TypeError(
|
|
1726
|
+
"tracer_provider must be an OpenTelemetry SDK TracerProvider with "
|
|
1727
|
+
"a detectable processor registrar"
|
|
1728
|
+
)
|
|
1729
|
+
if lock_is_held():
|
|
1730
|
+
raise RuntimeError(
|
|
1731
|
+
"tracer_provider processor registrar is locked; after fork, create "
|
|
1732
|
+
"a child-owned tracer provider"
|
|
1733
|
+
)
|
|
1734
|
+
|
|
1735
|
+
|
|
1736
|
+
def _python_version() -> tuple[int, int, int]:
|
|
1737
|
+
return sys.version_info[:3]
|
|
1738
|
+
|
|
1739
|
+
|
|
1740
|
+
def _distribution_version(package: str) -> str:
|
|
1741
|
+
return metadata.version(package)
|
|
1742
|
+
|
|
1743
|
+
|
|
1744
|
+
def _version_tuple(value: str) -> tuple[int, int, int]:
|
|
1745
|
+
match = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?", value)
|
|
1746
|
+
if match is None:
|
|
1747
|
+
raise ValueError(f"unrecognized package version {value!r}")
|
|
1748
|
+
return tuple(int(part or 0) for part in match.groups()) # type: ignore[return-value]
|
|
1749
|
+
|
|
1750
|
+
|
|
1751
|
+
def _installed_driver_version(driver: str) -> str:
|
|
1752
|
+
packages = ("psycopg2", "psycopg2-binary") if driver == "psycopg2" else (driver,)
|
|
1753
|
+
for package in packages:
|
|
1754
|
+
try:
|
|
1755
|
+
return _distribution_version(package)
|
|
1756
|
+
except metadata.PackageNotFoundError:
|
|
1757
|
+
continue
|
|
1758
|
+
raise metadata.PackageNotFoundError(driver)
|
|
1759
|
+
|
|
1760
|
+
|
|
1761
|
+
def _validate_runtime_versions(driver: str) -> None:
|
|
1762
|
+
python_version = _python_version()
|
|
1763
|
+
if not (python_version >= (3, 10, 0) and python_version < (3, 13, 0)):
|
|
1764
|
+
actual = ".".join(str(part) for part in python_version)
|
|
1765
|
+
raise RuntimeError(f"unsupported Python {actual}; requires Python >=3.10,<3.13")
|
|
1766
|
+
|
|
1767
|
+
try:
|
|
1768
|
+
sqlalchemy_version = _distribution_version("SQLAlchemy")
|
|
1769
|
+
except metadata.PackageNotFoundError as exc:
|
|
1770
|
+
raise RuntimeError("requires SQLAlchemy >=1.4.54,<2.1") from exc
|
|
1771
|
+
parsed_sqlalchemy = _version_tuple(sqlalchemy_version)
|
|
1772
|
+
if not ((1, 4, 54) <= parsed_sqlalchemy < (2, 1, 0)):
|
|
1773
|
+
raise RuntimeError(
|
|
1774
|
+
f"unsupported SQLAlchemy {sqlalchemy_version}; "
|
|
1775
|
+
"requires SQLAlchemy >=1.4.54,<2.1"
|
|
1776
|
+
)
|
|
1777
|
+
|
|
1778
|
+
try:
|
|
1779
|
+
otel_api = _distribution_version("opentelemetry-api")
|
|
1780
|
+
otel_sdk = _distribution_version("opentelemetry-sdk")
|
|
1781
|
+
except metadata.PackageNotFoundError as exc:
|
|
1782
|
+
raise RuntimeError("requires OpenTelemetry API/SDK 1.27.0") from exc
|
|
1783
|
+
if otel_api != "1.27.0" or otel_sdk != "1.27.0":
|
|
1784
|
+
raise RuntimeError(
|
|
1785
|
+
f"unsupported OpenTelemetry API/SDK {otel_api}/{otel_sdk}; "
|
|
1786
|
+
"requires OpenTelemetry API/SDK 1.27.0"
|
|
1787
|
+
)
|
|
1788
|
+
|
|
1789
|
+
requirements = {
|
|
1790
|
+
"psycopg2": ((2, 9, 9), (3, 0, 0), "psycopg2 >=2.9.9,<3"),
|
|
1791
|
+
"psycopg": ((3, 1, 18), (4, 0, 0), "psycopg >=3.1.18,<4"),
|
|
1792
|
+
"asyncpg": ((0, 29, 0), (1, 0, 0), "asyncpg >=0.29,<1"),
|
|
1793
|
+
}
|
|
1794
|
+
requirement = requirements.get(driver)
|
|
1795
|
+
if requirement is None:
|
|
1796
|
+
raise RuntimeError(
|
|
1797
|
+
"PostgreSQL DB-time instrumentation requires psycopg2, psycopg, or asyncpg"
|
|
1798
|
+
)
|
|
1799
|
+
minimum, maximum, label = requirement
|
|
1800
|
+
try:
|
|
1801
|
+
driver_version = _installed_driver_version(driver)
|
|
1802
|
+
except metadata.PackageNotFoundError as exc:
|
|
1803
|
+
raise RuntimeError(f"requires {label}") from exc
|
|
1804
|
+
if not (minimum <= _version_tuple(driver_version) < maximum):
|
|
1805
|
+
raise RuntimeError(f"unsupported {driver} {driver_version}; requires {label}")
|
|
1806
|
+
|
|
1807
|
+
|
|
1808
|
+
def _validate_text(value: Any, name: str, maximum: int) -> str:
|
|
1809
|
+
if (
|
|
1810
|
+
not isinstance(value, str)
|
|
1811
|
+
or value != value.strip()
|
|
1812
|
+
or not 1 <= len(value) <= maximum
|
|
1813
|
+
or not value.isprintable()
|
|
1814
|
+
):
|
|
1815
|
+
raise ValueError(
|
|
1816
|
+
f"{name} must be a trimmed printable string of 1-{maximum} characters"
|
|
1817
|
+
)
|
|
1818
|
+
return value
|
|
1819
|
+
|
|
1820
|
+
|
|
1821
|
+
def _resolve_pod_id(explicit_pod_id: str | None) -> str:
|
|
1822
|
+
if explicit_pod_id is not None:
|
|
1823
|
+
candidate = explicit_pod_id
|
|
1824
|
+
else:
|
|
1825
|
+
environment_hostname = os.environ.get("HOSTNAME", "").strip()
|
|
1826
|
+
candidate = environment_hostname or socket.gethostname()
|
|
1827
|
+
return _validate_text(candidate, "pod_id", 128)
|
|
1828
|
+
|
|
1829
|
+
|
|
1830
|
+
def _validate_pool_key(value: Any) -> str:
|
|
1831
|
+
if not isinstance(value, str) or _POOL_KEY_RE.fullmatch(value) is None:
|
|
1832
|
+
raise ValueError(
|
|
1833
|
+
"pool_key must match lowercase ASCII [a-z0-9][a-z0-9_.-]{0,47}"
|
|
1834
|
+
)
|
|
1835
|
+
return value
|
|
1836
|
+
|
|
1837
|
+
|
|
1838
|
+
def _validate_sample_rate(value: Any) -> float:
|
|
1839
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
1840
|
+
raise ValueError("sample_rate must be a finite number in (0,1]")
|
|
1841
|
+
normalized = float(value)
|
|
1842
|
+
if not math.isfinite(normalized) or not 0 < normalized <= 1:
|
|
1843
|
+
raise ValueError("sample_rate must be a finite number in (0,1]")
|
|
1844
|
+
try:
|
|
1845
|
+
decimal = Decimal(str(value))
|
|
1846
|
+
except InvalidOperation as exc:
|
|
1847
|
+
raise ValueError("sample_rate must have at most five decimal places") from exc
|
|
1848
|
+
if decimal.as_tuple().exponent < -5:
|
|
1849
|
+
raise ValueError("sample_rate must have at most five decimal places")
|
|
1850
|
+
return normalized
|
|
1851
|
+
|
|
1852
|
+
|
|
1853
|
+
def _validate_integer(
|
|
1854
|
+
value: Any,
|
|
1855
|
+
name: str,
|
|
1856
|
+
*,
|
|
1857
|
+
minimum: int,
|
|
1858
|
+
maximum: int,
|
|
1859
|
+
) -> int:
|
|
1860
|
+
if type(value) is not int or not minimum <= value <= maximum:
|
|
1861
|
+
raise ValueError(f"{name} must be an integer in [{minimum}, {maximum}]")
|
|
1862
|
+
return value
|
|
1863
|
+
|
|
1864
|
+
|
|
1865
|
+
def _validate_seconds(
|
|
1866
|
+
value: Any,
|
|
1867
|
+
name: str,
|
|
1868
|
+
*,
|
|
1869
|
+
positive: bool,
|
|
1870
|
+
maximum: float | None = None,
|
|
1871
|
+
) -> float:
|
|
1872
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
1873
|
+
raise ValueError(f"{name} must be a finite number")
|
|
1874
|
+
normalized = float(value)
|
|
1875
|
+
valid = normalized > 0 if positive else normalized >= 0
|
|
1876
|
+
if not math.isfinite(normalized) or not valid:
|
|
1877
|
+
bound = "positive" if positive else "non-negative"
|
|
1878
|
+
raise ValueError(f"{name} must be finite and {bound}")
|
|
1879
|
+
if maximum is not None and normalized > maximum:
|
|
1880
|
+
raise ValueError(f"{name} must be at most {maximum}")
|
|
1881
|
+
return normalized
|
|
1882
|
+
|
|
1883
|
+
|
|
1884
|
+
def _overflow_bucket_capacity(
|
|
1885
|
+
flush_callback: DbTimeFlushCallback | None,
|
|
1886
|
+
*,
|
|
1887
|
+
flush_interval_sec: float,
|
|
1888
|
+
shutdown_timeout_sec: float,
|
|
1889
|
+
) -> int:
|
|
1890
|
+
blocking_bound = max(flush_interval_sec, shutdown_timeout_sec)
|
|
1891
|
+
callback_bound = getattr(flush_callback, "max_blocking_seconds", None)
|
|
1892
|
+
if (
|
|
1893
|
+
not isinstance(callback_bound, bool)
|
|
1894
|
+
and isinstance(callback_bound, (int, float))
|
|
1895
|
+
and math.isfinite(float(callback_bound))
|
|
1896
|
+
and float(callback_bound) >= 0
|
|
1897
|
+
):
|
|
1898
|
+
blocking_bound = max(blocking_bound, float(callback_bound))
|
|
1899
|
+
return max(2, math.ceil(blocking_bound) + 3)
|
|
1900
|
+
|
|
1901
|
+
|
|
1902
|
+
def _callable_identity(
|
|
1903
|
+
callback: Callable[..., Any] | None,
|
|
1904
|
+
) -> tuple[int | None, int] | None:
|
|
1905
|
+
if callback is None:
|
|
1906
|
+
return None
|
|
1907
|
+
owner = getattr(callback, "__self__", None)
|
|
1908
|
+
function = getattr(callback, "__func__", callback)
|
|
1909
|
+
return (id(owner) if owner is not None else None, id(function))
|
|
1910
|
+
|
|
1911
|
+
|
|
1912
|
+
def _normalize_engine(engine: Any) -> tuple[Engine, bool]:
|
|
1913
|
+
if isinstance(engine, AsyncEngine):
|
|
1914
|
+
return engine.sync_engine, True
|
|
1915
|
+
if isinstance(engine, Engine):
|
|
1916
|
+
return engine, False
|
|
1917
|
+
raise TypeError("engine must be a SQLAlchemy Engine or AsyncEngine")
|
|
1918
|
+
|
|
1919
|
+
|
|
1920
|
+
def _validate_engine(sync_engine: Engine, is_async: bool) -> str:
|
|
1921
|
+
dialect = getattr(sync_engine, "dialect", None)
|
|
1922
|
+
if dialect is None or getattr(dialect, "name", None) != "postgresql":
|
|
1923
|
+
raise ValueError("DB-time instrumentation supports PostgreSQL engines only")
|
|
1924
|
+
driver = getattr(dialect, "driver", None)
|
|
1925
|
+
if not isinstance(driver, str):
|
|
1926
|
+
raise ValueError("PostgreSQL engine driver could not be identified")
|
|
1927
|
+
if is_async and driver != "asyncpg":
|
|
1928
|
+
raise RuntimeError("AsyncEngine DB-time instrumentation requires asyncpg")
|
|
1929
|
+
if not is_async and driver == "asyncpg":
|
|
1930
|
+
raise RuntimeError("asyncpg DB-time instrumentation requires AsyncEngine")
|
|
1931
|
+
return driver
|
|
1932
|
+
|
|
1933
|
+
|
|
1934
|
+
def _validate_provider(provider: Any) -> None:
|
|
1935
|
+
if not callable(getattr(provider, "get_tracer", None)):
|
|
1936
|
+
raise TypeError("tracer_provider must provide get_tracer")
|
|
1937
|
+
if not callable(getattr(provider, "add_span_processor", None)):
|
|
1938
|
+
raise TypeError("tracer_provider must provide add_span_processor")
|
|
1939
|
+
|
|
1940
|
+
|
|
1941
|
+
def _validate_sampler_parity(provider: Any, sample_rate: float) -> None:
|
|
1942
|
+
sampler = getattr(provider, "sampler", None)
|
|
1943
|
+
if isinstance(sampler, ParentBased):
|
|
1944
|
+
sampler = getattr(sampler, "_root", None)
|
|
1945
|
+
|
|
1946
|
+
probability: Any = None
|
|
1947
|
+
if isinstance(sampler, TraceIdRatioBased):
|
|
1948
|
+
probability = getattr(sampler, "_rate", None)
|
|
1949
|
+
elif callable(getattr(sampler, "get_description", None)):
|
|
1950
|
+
if sampler.get_description() == "AlwaysOnSampler":
|
|
1951
|
+
probability = 1.0
|
|
1952
|
+
|
|
1953
|
+
if (
|
|
1954
|
+
isinstance(probability, bool)
|
|
1955
|
+
or not isinstance(probability, (int, float))
|
|
1956
|
+
or not math.isfinite(float(probability))
|
|
1957
|
+
):
|
|
1958
|
+
raise RuntimeError(
|
|
1959
|
+
"root sampler probability cannot be derived; configure "
|
|
1960
|
+
"AlwaysOnSampler or TraceIdRatioBased"
|
|
1961
|
+
)
|
|
1962
|
+
if Decimal(str(probability)) != Decimal(str(sample_rate)):
|
|
1963
|
+
raise RuntimeError(
|
|
1964
|
+
f"root sampler probability {probability} must equal "
|
|
1965
|
+
f"declared sample_rate {sample_rate}"
|
|
1966
|
+
)
|
|
1967
|
+
|
|
1968
|
+
|
|
1969
|
+
def _correlation_hash(kind: Literal["request", "thread"], value: str) -> str:
|
|
1970
|
+
return hashlib.sha256(
|
|
1971
|
+
f"moolabs:db-time:v1:{kind}:{value}".encode("utf-8")
|
|
1972
|
+
).hexdigest()
|
|
1973
|
+
|
|
1974
|
+
|
|
1975
|
+
def _pending_observation(
|
|
1976
|
+
observation_id: uuid.UUID,
|
|
1977
|
+
transaction_id: uuid.UUID,
|
|
1978
|
+
*,
|
|
1979
|
+
started_at: datetime | None = None,
|
|
1980
|
+
) -> _PendingDbObservation:
|
|
1981
|
+
context = get_attribution_context()
|
|
1982
|
+
if context is None:
|
|
1983
|
+
customer_external_key, feature_key = _UNATTRIBUTED_IDENTITY_PAIR
|
|
1984
|
+
request_id_hash = None
|
|
1985
|
+
thread_id_hash = None
|
|
1986
|
+
else:
|
|
1987
|
+
dimensions = _normalized_dimensions(
|
|
1988
|
+
context.customer_external_key,
|
|
1989
|
+
context.feature_key,
|
|
1990
|
+
context.thread_id,
|
|
1991
|
+
)
|
|
1992
|
+
if dimensions is None:
|
|
1993
|
+
customer_external_key, feature_key = _UNATTRIBUTED_IDENTITY_PAIR
|
|
1994
|
+
request_id_hash = None
|
|
1995
|
+
thread_id_hash = None
|
|
1996
|
+
else:
|
|
1997
|
+
customer_external_key, feature_key, thread_id = dimensions
|
|
1998
|
+
request_id_hash = _correlation_hash("request", context.request_id)
|
|
1999
|
+
thread_id_hash = _correlation_hash("thread", thread_id)
|
|
2000
|
+
return _PendingDbObservation(
|
|
2001
|
+
observation_id=observation_id,
|
|
2002
|
+
transaction_id=transaction_id,
|
|
2003
|
+
request_id_hash=request_id_hash,
|
|
2004
|
+
thread_id_hash=thread_id_hash,
|
|
2005
|
+
customer_external_key=customer_external_key,
|
|
2006
|
+
feature_key=feature_key,
|
|
2007
|
+
started_at=_as_utc(started_at or datetime.now(timezone.utc)),
|
|
2008
|
+
)
|
|
2009
|
+
|
|
2010
|
+
|
|
2011
|
+
def install_db_time_driver(
|
|
2012
|
+
engine: Any,
|
|
2013
|
+
*,
|
|
2014
|
+
service: str,
|
|
2015
|
+
pool_key: str,
|
|
2016
|
+
sample_rate: float,
|
|
2017
|
+
pod_id: str | None = None,
|
|
2018
|
+
tracer_provider: Any = None,
|
|
2019
|
+
queue_capacity: int = 5000,
|
|
2020
|
+
batch_size: int = 500,
|
|
2021
|
+
flush_interval_sec: float = 5.0,
|
|
2022
|
+
shutdown_timeout_sec: float = 60.0,
|
|
2023
|
+
flush_callback: DbTimeFlushCallback | None = None,
|
|
2024
|
+
logger: Callable[[str, dict[str, Any]], None] | None = None,
|
|
2025
|
+
_utc_now: Callable[[], datetime] = lambda: datetime.now(timezone.utc),
|
|
2026
|
+
_monotonic: Callable[[], float] = time.monotonic,
|
|
2027
|
+
) -> DbTimeDriverHandle:
|
|
2028
|
+
"""Install one redacted DB-time listener set on a PostgreSQL engine."""
|
|
2029
|
+
_ensure_process_state()
|
|
2030
|
+
normalized_service = _validate_text(service, "service", 128)
|
|
2031
|
+
normalized_pool_key = _validate_pool_key(pool_key)
|
|
2032
|
+
normalized_sample_rate = _validate_sample_rate(sample_rate)
|
|
2033
|
+
normalized_pod_id = _resolve_pod_id(pod_id)
|
|
2034
|
+
normalized_queue_capacity = _validate_integer(
|
|
2035
|
+
queue_capacity,
|
|
2036
|
+
"queue_capacity",
|
|
2037
|
+
minimum=1,
|
|
2038
|
+
maximum=1_000_000,
|
|
2039
|
+
)
|
|
2040
|
+
normalized_batch_size = _validate_integer(
|
|
2041
|
+
batch_size,
|
|
2042
|
+
"batch_size",
|
|
2043
|
+
minimum=1,
|
|
2044
|
+
maximum=500,
|
|
2045
|
+
)
|
|
2046
|
+
normalized_flush_interval = _validate_seconds(
|
|
2047
|
+
flush_interval_sec,
|
|
2048
|
+
"flush_interval_sec",
|
|
2049
|
+
positive=True,
|
|
2050
|
+
maximum=300.0,
|
|
2051
|
+
)
|
|
2052
|
+
normalized_shutdown_timeout = _validate_seconds(
|
|
2053
|
+
shutdown_timeout_sec,
|
|
2054
|
+
"shutdown_timeout_sec",
|
|
2055
|
+
positive=False,
|
|
2056
|
+
)
|
|
2057
|
+
if flush_callback is not None and not callable(flush_callback):
|
|
2058
|
+
raise ValueError("flush_callback must be callable")
|
|
2059
|
+
if logger is not None and not callable(logger):
|
|
2060
|
+
raise ValueError("logger must be callable")
|
|
2061
|
+
if not callable(_utc_now) or not callable(_monotonic):
|
|
2062
|
+
raise ValueError("clock functions must be callable")
|
|
2063
|
+
overflow_bucket_capacity = _overflow_bucket_capacity(
|
|
2064
|
+
flush_callback,
|
|
2065
|
+
flush_interval_sec=normalized_flush_interval,
|
|
2066
|
+
shutdown_timeout_sec=normalized_shutdown_timeout,
|
|
2067
|
+
)
|
|
2068
|
+
|
|
2069
|
+
sync_engine, is_async = _normalize_engine(engine)
|
|
2070
|
+
driver = _validate_engine(sync_engine, is_async)
|
|
2071
|
+
_validate_runtime_versions(driver)
|
|
2072
|
+
provider = tracer_provider or trace.get_tracer_provider()
|
|
2073
|
+
_validate_provider(provider)
|
|
2074
|
+
_validate_provider_process(provider)
|
|
2075
|
+
config = _InstallConfig(
|
|
2076
|
+
service=normalized_service,
|
|
2077
|
+
pool_key=normalized_pool_key,
|
|
2078
|
+
sample_rate=normalized_sample_rate,
|
|
2079
|
+
tracer_provider_identity=id(provider),
|
|
2080
|
+
flush_callback_identity=_callable_identity(flush_callback),
|
|
2081
|
+
)
|
|
2082
|
+
|
|
2083
|
+
with _INSTALLS_LOCK:
|
|
2084
|
+
existing_ref = _INSTALLS.get(sync_engine)
|
|
2085
|
+
existing = existing_ref() if existing_ref is not None else None
|
|
2086
|
+
if existing_ref is not None and existing is None:
|
|
2087
|
+
del _INSTALLS[sync_engine]
|
|
2088
|
+
if existing is not None and not existing.closed:
|
|
2089
|
+
if existing._config == config:
|
|
2090
|
+
return existing
|
|
2091
|
+
raise ValueError(
|
|
2092
|
+
"SQLAlchemy engine is already instrumented with conflicting "
|
|
2093
|
+
"service, pool_key, sample_rate, tracer provider, or transport owner"
|
|
2094
|
+
)
|
|
2095
|
+
|
|
2096
|
+
_validate_sampler_parity(provider, normalized_sample_rate)
|
|
2097
|
+
handle = DbTimeDriverHandle(
|
|
2098
|
+
sync_engine,
|
|
2099
|
+
config,
|
|
2100
|
+
provider,
|
|
2101
|
+
pod_id=normalized_pod_id,
|
|
2102
|
+
queue_capacity=normalized_queue_capacity,
|
|
2103
|
+
overflow_bucket_capacity=overflow_bucket_capacity,
|
|
2104
|
+
batch_size=normalized_batch_size,
|
|
2105
|
+
flush_interval_sec=normalized_flush_interval,
|
|
2106
|
+
shutdown_timeout_sec=normalized_shutdown_timeout,
|
|
2107
|
+
flush_callback=flush_callback,
|
|
2108
|
+
logger=logger,
|
|
2109
|
+
utc_now=_utc_now,
|
|
2110
|
+
monotonic=_monotonic,
|
|
2111
|
+
)
|
|
2112
|
+
provider_processor: _ProviderDbTimeSpanProcessor | None = None
|
|
2113
|
+
try:
|
|
2114
|
+
provider_processor = _provider_db_time_processor(provider)
|
|
2115
|
+
handle._provider_processor = provider_processor
|
|
2116
|
+
provider_processor.register(handle._processor)
|
|
2117
|
+
handle._register_listeners()
|
|
2118
|
+
except Exception:
|
|
2119
|
+
handle._processor.disable()
|
|
2120
|
+
if provider_processor is not None:
|
|
2121
|
+
provider_processor.unregister(handle._processor)
|
|
2122
|
+
handle._provider_processor = None
|
|
2123
|
+
handle.close()
|
|
2124
|
+
raise
|
|
2125
|
+
_INSTALLS[sync_engine] = weakref.ref(handle)
|
|
2126
|
+
return handle
|