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.
Files changed (1417) hide show
  1. moolabs/__init__.py +1487 -0
  2. moolabs/_dx_attribution_context.py +213 -0
  3. moolabs/_dx_attribution_db_time.py +2126 -0
  4. moolabs/_dx_attribution_db_time_transport.py +173 -0
  5. moolabs/_dx_attribution_delta.py +142 -0
  6. moolabs/_dx_attribution_deposits.py +552 -0
  7. moolabs/_dx_attribution_instrument.py +416 -0
  8. moolabs/_dx_attribution_middleware.py +541 -0
  9. moolabs/_dx_attribution_propagate.py +509 -0
  10. moolabs/_dx_attribution_signal_transport.py +232 -0
  11. moolabs/_dx_attribution_signals.py +927 -0
  12. moolabs/_dx_buffer.py +429 -0
  13. moolabs/_dx_client.py +726 -0
  14. moolabs/_dx_exceptions.py +84 -0
  15. moolabs/_dx_namespaces.py +1780 -0
  16. moolabs/_dx_pagination.py +131 -0
  17. moolabs/_dx_routing.py +276 -0
  18. moolabs/_dx_urls.py +629 -0
  19. moolabs/_dx_webhooks.py +82 -0
  20. moolabs/api/__init__.py +121 -0
  21. moolabs/api/account_team_api.py +1444 -0
  22. moolabs/api/accounts_api.py +4950 -0
  23. moolabs/api/acute_analytics_api.py +5750 -0
  24. moolabs/api/acute_integrations_api.py +3449 -0
  25. moolabs/api/admin_api.py +3347 -0
  26. moolabs/api/alerts_api.py +1626 -0
  27. moolabs/api/allocation_rules_api.py +1720 -0
  28. moolabs/api/analytics_api.py +3204 -0
  29. moolabs/api/app_custom_invoicing_api.py +952 -0
  30. moolabs/api/app_stripe_api.py +958 -0
  31. moolabs/api/apps_api.py +3064 -0
  32. moolabs/api/arc_admin_api.py +8773 -0
  33. moolabs/api/arc_communications_api.py +1991 -0
  34. moolabs/api/arc_portal_api.py +1737 -0
  35. moolabs/api/arc_webhooks_api.py +2087 -0
  36. moolabs/api/attribution_allocation_api.py +2345 -0
  37. moolabs/api/attribution_api.py +1717 -0
  38. moolabs/api/attribution_customer_keys_api.py +600 -0
  39. moolabs/api/attribution_db_time_api.py +918 -0
  40. moolabs/api/attribution_opencost_api.py +1372 -0
  41. moolabs/api/attribution_readouts_api.py +393 -0
  42. moolabs/api/attribution_signals_api.py +315 -0
  43. moolabs/api/auto_topup_api.py +3095 -0
  44. moolabs/api/batch_api.py +6714 -0
  45. moolabs/api/billing_api.py +300 -0
  46. moolabs/api/bom_api.py +2769 -0
  47. moolabs/api/budgets_api.py +2112 -0
  48. moolabs/api/buyer_quotes_api.py +2007 -0
  49. moolabs/api/cases_api.py +5075 -0
  50. moolabs/api/cash_credits_api.py +1548 -0
  51. moolabs/api/chat_api.py +3101 -0
  52. moolabs/api/clause_packs_api.py +2785 -0
  53. moolabs/api/cloud_billing_api.py +1747 -0
  54. moolabs/api/contract_editor_api.py +3384 -0
  55. moolabs/api/cost_events_api.py +1732 -0
  56. moolabs/api/credit_memos_api.py +3478 -0
  57. moolabs/api/customers_api.py +3931 -0
  58. moolabs/api/dashboard_api.py +771 -0
  59. moolabs/api/debug_api.py +300 -0
  60. moolabs/api/default_api.py +526 -0
  61. moolabs/api/disputes_api.py +3586 -0
  62. moolabs/api/dunning_templates_api.py +8737 -0
  63. moolabs/api/email_provider_api.py +526 -0
  64. moolabs/api/entitlements_api.py +9846 -0
  65. moolabs/api/escalations_api.py +2541 -0
  66. moolabs/api/events_api.py +1125 -0
  67. moolabs/api/fx_rates_api.py +1452 -0
  68. moolabs/api/governance_api.py +4402 -0
  69. moolabs/api/grants_api.py +1370 -0
  70. moolabs/api/handoffs_api.py +2138 -0
  71. moolabs/api/health_api.py +281 -0
  72. moolabs/api/homepage_api.py +305 -0
  73. moolabs/api/integrations_api.py +3374 -0
  74. moolabs/api/internal_api.py +7145 -0
  75. moolabs/api/internal_credit_memos_api.py +914 -0
  76. moolabs/api/internal_grants_api.py +1000 -0
  77. moolabs/api/internal_period_settlements_api.py +657 -0
  78. moolabs/api/internal_plan_pricing_api.py +326 -0
  79. moolabs/api/internal_subscriptions_api.py +326 -0
  80. moolabs/api/ledger_api.py +1483 -0
  81. moolabs/api/lookup_information_api.py +587 -0
  82. moolabs/api/mapping_rules_api.py +1782 -0
  83. moolabs/api/mappings_api.py +632 -0
  84. moolabs/api/margin_simulation_api.py +595 -0
  85. moolabs/api/margins_api.py +2301 -0
  86. moolabs/api/meter_billing_api.py +7419 -0
  87. moolabs/api/meter_portal_api.py +1368 -0
  88. moolabs/api/meter_subscriptions_api.py +4358 -0
  89. moolabs/api/meters_api.py +3707 -0
  90. moolabs/api/monitoring_api.py +561 -0
  91. moolabs/api/moolabs_api.py +1182 -0
  92. moolabs/api/moometer_proxy_api.py +1357 -0
  93. moolabs/api/netsuite_sync_api.py +2180 -0
  94. moolabs/api/notes_api.py +1425 -0
  95. moolabs/api/notifications_api.py +4490 -0
  96. moolabs/api/ops_api.py +315 -0
  97. moolabs/api/outbox_api.py +653 -0
  98. moolabs/api/payments_api.py +2889 -0
  99. moolabs/api/performance_api.py +2471 -0
  100. moolabs/api/plan_pricing_api.py +3409 -0
  101. moolabs/api/plans_api.py +2197 -0
  102. moolabs/api/pools_api.py +2358 -0
  103. moolabs/api/portal_api.py +1103 -0
  104. moolabs/api/product_catalog_api.py +7644 -0
  105. moolabs/api/promises_api.py +2814 -0
  106. moolabs/api/quote_agents_api.py +2036 -0
  107. moolabs/api/quote_contract_webhooks_api.py +327 -0
  108. moolabs/api/quote_contracts_api.py +2551 -0
  109. moolabs/api/quote_sessions_api.py +881 -0
  110. moolabs/api/quotes_api.py +12880 -0
  111. moolabs/api/rate_cards_api.py +578 -0
  112. moolabs/api/rate_catalog_api.py +2120 -0
  113. moolabs/api/rate_schedules_api.py +1224 -0
  114. moolabs/api/rating_api.py +617 -0
  115. moolabs/api/reconstruction_api.py +1281 -0
  116. moolabs/api/remittances_api.py +1521 -0
  117. moolabs/api/reports_api.py +722 -0
  118. moolabs/api/rollover_api.py +313 -0
  119. moolabs/api/sdk_ingest_api.py +312 -0
  120. moolabs/api/snapshots_api.py +1218 -0
  121. moolabs/api/state_projection_api.py +961 -0
  122. moolabs/api/steering_api.py +1862 -0
  123. moolabs/api/subjects_api.py +1160 -0
  124. moolabs/api/subscription_overrides_api.py +579 -0
  125. moolabs/api/subscriptions_api.py +1200 -0
  126. moolabs/api/tasks_api.py +1509 -0
  127. moolabs/api/tenant_api.py +22395 -0
  128. moolabs/api/tenant_config_api.py +282 -0
  129. moolabs/api/topup_api.py +632 -0
  130. moolabs/api/unpriced_api.py +933 -0
  131. moolabs/api/usage_api.py +999 -0
  132. moolabs/api/variance_api.py +1327 -0
  133. moolabs/api/wallets_api.py +3787 -0
  134. moolabs/api/webhooks_api.py +281 -0
  135. moolabs/api/well_known_api.py +278 -0
  136. moolabs/api/word_addin_api.py +623 -0
  137. moolabs/api_client.py +797 -0
  138. moolabs/api_response.py +21 -0
  139. moolabs/configuration.py +620 -0
  140. moolabs/exceptions.py +199 -0
  141. moolabs/models/__init__.py +1286 -0
  142. moolabs/models/account_create.py +114 -0
  143. moolabs/models/account_filter_options_response.py +97 -0
  144. moolabs/models/account_invoice_list_response.py +95 -0
  145. moolabs/models/account_list_item_response.py +152 -0
  146. moolabs/models/account_list_item_response_subsidiaries_inner.py +152 -0
  147. moolabs/models/account_list_response.py +101 -0
  148. moolabs/models/account_overview_bucket_response.py +91 -0
  149. moolabs/models/account_overview_response.py +106 -0
  150. moolabs/models/account_response.py +132 -0
  151. moolabs/models/account_segment_row.py +91 -0
  152. moolabs/models/account_subsidiary_option.py +89 -0
  153. moolabs/models/account_summary.py +97 -0
  154. moolabs/models/account_team_member_in.py +97 -0
  155. moolabs/models/account_team_member_out.py +101 -0
  156. moolabs/models/account_team_member_patch.py +97 -0
  157. moolabs/models/account_team_role.py +39 -0
  158. moolabs/models/account_update.py +106 -0
  159. moolabs/models/action_kind.py +45 -0
  160. moolabs/models/acute_approve_request.py +87 -0
  161. moolabs/models/acute_demo_seed_lifecycle_request.py +103 -0
  162. moolabs/models/acute_http_validation_error.py +95 -0
  163. moolabs/models/acute_validation_error.py +99 -0
  164. moolabs/models/add_contract_participant_request.py +107 -0
  165. moolabs/models/addon.py +178 -0
  166. moolabs/models/addon1.py +115 -0
  167. moolabs/models/addon2.py +95 -0
  168. moolabs/models/addon_create.py +123 -0
  169. moolabs/models/addon_instance_type.py +37 -0
  170. moolabs/models/addon_order_by.py +40 -0
  171. moolabs/models/addon_paginated_response.py +101 -0
  172. moolabs/models/addon_replace_update.py +105 -0
  173. moolabs/models/addon_status.py +38 -0
  174. moolabs/models/address.py +110 -0
  175. moolabs/models/adjusted_total.py +152 -0
  176. moolabs/models/adjustment_amount.py +152 -0
  177. moolabs/models/aging_bucket_detail.py +93 -0
  178. moolabs/models/aging_report_response.py +106 -0
  179. moolabs/models/alb_bootstrap_response.py +89 -0
  180. moolabs/models/alb_connector_config.py +128 -0
  181. moolabs/models/alb_connector_health_response.py +112 -0
  182. moolabs/models/alb_connector_list_response.py +138 -0
  183. moolabs/models/alb_load_balancer_config.py +111 -0
  184. moolabs/models/alb_route_rule.py +107 -0
  185. moolabs/models/alb_source_config.py +123 -0
  186. moolabs/models/alb_source_health_response.py +93 -0
  187. moolabs/models/alb_status_patch_request.py +94 -0
  188. moolabs/models/alb_status_patch_response.py +94 -0
  189. moolabs/models/alb_target_group_config.py +103 -0
  190. moolabs/models/algorithm_breakdown_item.py +89 -0
  191. moolabs/models/algorithm_info.py +93 -0
  192. moolabs/models/alignment.py +87 -0
  193. moolabs/models/allocate_credits_request.py +95 -0
  194. moolabs/models/allocation_coverage_response.py +157 -0
  195. moolabs/models/allocation_fact_page_response.py +97 -0
  196. moolabs/models/allocation_fact_response.py +146 -0
  197. moolabs/models/allocation_key.py +91 -0
  198. moolabs/models/allocation_rule_create.py +114 -0
  199. moolabs/models/allocation_rule_response.py +113 -0
  200. moolabs/models/allocation_rule_update.py +111 -0
  201. moolabs/models/allocation_run_create_request.py +100 -0
  202. moolabs/models/allocation_run_create_response.py +142 -0
  203. moolabs/models/allocation_run_open_cost_snapshot_response.py +120 -0
  204. moolabs/models/allocation_run_readout_response.py +110 -0
  205. moolabs/models/allocation_run_response.py +133 -0
  206. moolabs/models/allocations_list_response.py +97 -0
  207. moolabs/models/amount.py +152 -0
  208. moolabs/models/amount1.py +152 -0
  209. moolabs/models/api_key_item.py +103 -0
  210. moolabs/models/app.py +164 -0
  211. moolabs/models/app_api_v1_auto_topup_router_trigger_response.py +108 -0
  212. moolabs/models/app_api_v1_fx_rates_router_fx_rate_response.py +104 -0
  213. moolabs/models/app_api_v1_integrations_netsuite_sync_router_trigger_response.py +89 -0
  214. moolabs/models/app_api_v1_internal_subscription_sync_schemas_subscription_sync_request.py +123 -0
  215. moolabs/models/app_api_v1_portal_router_create_portal_token_request.py +92 -0
  216. moolabs/models/app_api_v1_quotes_schemas_quote_settings_response.py +91 -0
  217. moolabs/models/app_api_v1_quotes_schemas_update_quote_settings_request.py +89 -0
  218. moolabs/models/app_api_v1_subscriptions_router_subscription_sync_request.py +125 -0
  219. moolabs/models/app_api_v1_tenant_access_router_create_portal_token_request.py +101 -0
  220. moolabs/models/app_api_v1_tenant_fx_rates_router_fx_rate_response.py +93 -0
  221. moolabs/models/app_api_v1_tenant_quote_settings_router_quote_settings_response.py +89 -0
  222. moolabs/models/app_api_v1_tenant_quote_settings_router_update_quote_settings_request.py +87 -0
  223. moolabs/models/app_capability.py +102 -0
  224. moolabs/models/app_capability_type.py +40 -0
  225. moolabs/models/app_paginated_response.py +101 -0
  226. moolabs/models/app_reference.py +95 -0
  227. moolabs/models/app_replace_update.py +164 -0
  228. moolabs/models/app_status.py +37 -0
  229. moolabs/models/app_type.py +38 -0
  230. moolabs/models/apply_credit_request.py +92 -0
  231. moolabs/models/approval_action_request.py +88 -0
  232. moolabs/models/approval_action_response.py +95 -0
  233. moolabs/models/approval_policy_response.py +91 -0
  234. moolabs/models/approve_request.py +88 -0
  235. moolabs/models/approve_response.py +91 -0
  236. moolabs/models/approver_in.py +95 -0
  237. moolabs/models/arc_demo_seed_lifecycle_request.py +103 -0
  238. moolabs/models/arc_dunning_permission_grant_request.py +96 -0
  239. moolabs/models/arc_dunning_permission_revoke_request.py +90 -0
  240. moolabs/models/arc_dunning_template_admin_grant_request.py +90 -0
  241. moolabs/models/arc_dunning_template_admin_grant_response.py +115 -0
  242. moolabs/models/arc_dunning_template_admin_revoke_request.py +88 -0
  243. moolabs/models/arc_dunning_template_admin_user_dto.py +107 -0
  244. moolabs/models/arc_dunning_template_admin_user_grant_dto.py +105 -0
  245. moolabs/models/arc_dunning_template_admin_users_response.py +110 -0
  246. moolabs/models/arc_http_validation_error.py +95 -0
  247. moolabs/models/arc_transaction_request.py +110 -0
  248. moolabs/models/arc_validation_error.py +99 -0
  249. moolabs/models/ask_customer_request.py +97 -0
  250. moolabs/models/ask_customer_response.py +91 -0
  251. moolabs/models/ask_moo_redline_request.py +98 -0
  252. moolabs/models/attribution_allocation_error_detail.py +94 -0
  253. moolabs/models/attribution_allocation_error_response.py +91 -0
  254. moolabs/models/attribution_completeness_response.py +97 -0
  255. moolabs/models/attribution_signal_conflict_detail.py +94 -0
  256. moolabs/models/attribution_signal_conflict_response.py +91 -0
  257. moolabs/models/attribution_signal_delta.py +98 -0
  258. moolabs/models/attribution_signal_request.py +123 -0
  259. moolabs/models/attribution_signal_response.py +96 -0
  260. moolabs/models/audit_request.py +91 -0
  261. moolabs/models/auto_topup_activity_item.py +104 -0
  262. moolabs/models/auto_topup_activity_response.py +97 -0
  263. moolabs/models/auto_topup_rule_response.py +114 -0
  264. moolabs/models/availability.py +43 -0
  265. moolabs/models/backfill_candidate.py +97 -0
  266. moolabs/models/backfill_malformed_disputes_request.py +87 -0
  267. moolabs/models/backfill_malformed_disputes_response.py +101 -0
  268. moolabs/models/balance_history_window.py +99 -0
  269. moolabs/models/basis.py +37 -0
  270. moolabs/models/batch_ingest_request.py +96 -0
  271. moolabs/models/batch_wallet_state_request.py +105 -0
  272. moolabs/models/batch_wallet_state_request_item.py +91 -0
  273. moolabs/models/batch_wallet_state_response.py +97 -0
  274. moolabs/models/batch_wallets_by_subjects_request.py +88 -0
  275. moolabs/models/billing_customer_profile.py +109 -0
  276. moolabs/models/billing_discount_percentage.py +100 -0
  277. moolabs/models/billing_discount_reason.py +164 -0
  278. moolabs/models/billing_discount_usage.py +107 -0
  279. moolabs/models/billing_discounts.py +97 -0
  280. moolabs/models/billing_import_request.py +105 -0
  281. moolabs/models/billing_import_response.py +104 -0
  282. moolabs/models/billing_invoice_customer_extended_details.py +116 -0
  283. moolabs/models/billing_party.py +110 -0
  284. moolabs/models/billing_party_replace_update.py +106 -0
  285. moolabs/models/billing_party_tax_identity.py +88 -0
  286. moolabs/models/billing_profile.py +140 -0
  287. moolabs/models/billing_profile_app_references.py +107 -0
  288. moolabs/models/billing_profile_apps.py +107 -0
  289. moolabs/models/billing_profile_apps_create.py +113 -0
  290. moolabs/models/billing_profile_apps_or_reference.py +148 -0
  291. moolabs/models/billing_profile_create.py +112 -0
  292. moolabs/models/billing_profile_customer_override.py +116 -0
  293. moolabs/models/billing_profile_customer_override_create.py +98 -0
  294. moolabs/models/billing_profile_customer_override_expand.py +37 -0
  295. moolabs/models/billing_profile_customer_override_order_by.py +40 -0
  296. moolabs/models/billing_profile_customer_override_with_details.py +113 -0
  297. moolabs/models/billing_profile_customer_override_with_details_paginated_response.py +101 -0
  298. moolabs/models/billing_profile_expand.py +36 -0
  299. moolabs/models/billing_profile_order_by.py +39 -0
  300. moolabs/models/billing_profile_paginated_response.py +101 -0
  301. moolabs/models/billing_profile_replace_update_with_workflow.py +106 -0
  302. moolabs/models/billing_tenant_settings_out.py +89 -0
  303. moolabs/models/billing_tenant_settings_update.py +88 -0
  304. moolabs/models/billing_workflow.py +109 -0
  305. moolabs/models/billing_workflow_collection_alignment.py +150 -0
  306. moolabs/models/billing_workflow_collection_alignment_anchored.py +100 -0
  307. moolabs/models/billing_workflow_collection_alignment_subscription.py +94 -0
  308. moolabs/models/billing_workflow_collection_settings.py +93 -0
  309. moolabs/models/billing_workflow_create.py +109 -0
  310. moolabs/models/billing_workflow_invoicing_settings.py +99 -0
  311. moolabs/models/billing_workflow_payment_settings.py +88 -0
  312. moolabs/models/billing_workflow_tax_settings.py +89 -0
  313. moolabs/models/bom_component_in.py +105 -0
  314. moolabs/models/bom_component_out.py +109 -0
  315. moolabs/models/bom_create_request.py +107 -0
  316. moolabs/models/bom_out.py +127 -0
  317. moolabs/models/bom_update_request.py +103 -0
  318. moolabs/models/bom_variance_out.py +115 -0
  319. moolabs/models/breakdown_response.py +124 -0
  320. moolabs/models/breakdown_row.py +121 -0
  321. moolabs/models/breakdown_row_kind.py +39 -0
  322. moolabs/models/budget_alert_response.py +107 -0
  323. moolabs/models/budget_amount.py +152 -0
  324. moolabs/models/budget_amount1.py +152 -0
  325. moolabs/models/budget_create.py +110 -0
  326. moolabs/models/budget_pace_status.py +38 -0
  327. moolabs/models/budget_response.py +113 -0
  328. moolabs/models/budget_status_item.py +105 -0
  329. moolabs/models/budget_update.py +107 -0
  330. moolabs/models/budget_with_utilization.py +119 -0
  331. moolabs/models/bulk_action_request.py +92 -0
  332. moolabs/models/bulk_replay_request.py +92 -0
  333. moolabs/models/buyer_accept_request.py +95 -0
  334. moolabs/models/buyer_otp_request_response.py +91 -0
  335. moolabs/models/buyer_otp_verify_request.py +95 -0
  336. moolabs/models/buyer_otp_verify_response.py +89 -0
  337. moolabs/models/buyer_quote_line_item.py +103 -0
  338. moolabs/models/buyer_quote_pricing.py +93 -0
  339. moolabs/models/buyer_quote_projection.py +133 -0
  340. moolabs/models/buyer_reject_request.py +88 -0
  341. moolabs/models/buyer_request_changes_request.py +94 -0
  342. moolabs/models/buyer_request_changes_response.py +93 -0
  343. moolabs/models/buyer_requested_changes.py +90 -0
  344. moolabs/models/cancel_subscription_request.py +91 -0
  345. moolabs/models/card_type.py +37 -0
  346. moolabs/models/case_create.py +112 -0
  347. moolabs/models/case_escalate_request.py +90 -0
  348. moolabs/models/case_invoice_create.py +127 -0
  349. moolabs/models/case_invoice_response.py +150 -0
  350. moolabs/models/case_list_item_response.py +171 -0
  351. moolabs/models/case_list_response.py +101 -0
  352. moolabs/models/case_pause_request.py +91 -0
  353. moolabs/models/case_response.py +169 -0
  354. moolabs/models/case_update.py +102 -0
  355. moolabs/models/case_write_off_request.py +90 -0
  356. moolabs/models/cash_app_config_update_request.py +96 -0
  357. moolabs/models/cei_response.py +102 -0
  358. moolabs/models/change_item.py +101 -0
  359. moolabs/models/channel_policy_schema.py +112 -0
  360. moolabs/models/channel_preference_create.py +99 -0
  361. moolabs/models/channel_preference_response.py +106 -0
  362. moolabs/models/channel_preference_update.py +95 -0
  363. moolabs/models/channel_preferences_response.py +95 -0
  364. moolabs/models/chat_feedback_in.py +91 -0
  365. moolabs/models/chat_message_in.py +89 -0
  366. moolabs/models/chat_request.py +101 -0
  367. moolabs/models/check_trigger_request.py +88 -0
  368. moolabs/models/checkout_session_custom_text_after_submit_params.py +109 -0
  369. moolabs/models/checkout_session_custom_text_after_submit_params_after_submit.py +88 -0
  370. moolabs/models/checkout_session_custom_text_after_submit_params_shipping_address.py +88 -0
  371. moolabs/models/checkout_session_custom_text_after_submit_params_submit.py +88 -0
  372. moolabs/models/checkout_session_custom_text_after_submit_params_terms_of_service_acceptance.py +88 -0
  373. moolabs/models/checkout_session_ui_mode.py +37 -0
  374. moolabs/models/clause_citations.py +91 -0
  375. moolabs/models/clause_family.py +129 -0
  376. moolabs/models/clause_finding.py +110 -0
  377. moolabs/models/clause_pack.py +118 -0
  378. moolabs/models/clause_position.py +91 -0
  379. moolabs/models/client_app_start_response.py +87 -0
  380. moolabs/models/cloud_cost_row_input.py +105 -0
  381. moolabs/models/cloud_provider.py +38 -0
  382. moolabs/models/cloud_provider_data_through.py +116 -0
  383. moolabs/models/cloud_provider_quality_axis.py +107 -0
  384. moolabs/models/cloud_temporal_coverage_summary.py +114 -0
  385. moolabs/models/collection_method.py +37 -0
  386. moolabs/models/commercial_basis.py +39 -0
  387. moolabs/models/commercial_overrides.py +113 -0
  388. moolabs/models/commercial_overrides_input.py +115 -0
  389. moolabs/models/commercial_overrides_payload.py +113 -0
  390. moolabs/models/communication_create.py +110 -0
  391. moolabs/models/communication_list_response.py +101 -0
  392. moolabs/models/communication_response.py +138 -0
  393. moolabs/models/comparison_mode.py +37 -0
  394. moolabs/models/completeness_response.py +105 -0
  395. moolabs/models/component_coverage_response.py +123 -0
  396. moolabs/models/compute_variance_request.py +96 -0
  397. moolabs/models/compute_variance_response.py +105 -0
  398. moolabs/models/config_upload_gap_out.py +89 -0
  399. moolabs/models/config_upload_response.py +109 -0
  400. moolabs/models/config_upload_summary_out.py +95 -0
  401. moolabs/models/connector_config_response.py +93 -0
  402. moolabs/models/connector_health_response.py +102 -0
  403. moolabs/models/connector_item.py +105 -0
  404. moolabs/models/contact_create.py +100 -0
  405. moolabs/models/contact_list_response.py +101 -0
  406. moolabs/models/contact_response.py +134 -0
  407. moolabs/models/contact_update.py +102 -0
  408. moolabs/models/contract_document_response.py +104 -0
  409. moolabs/models/contract_event_response.py +108 -0
  410. moolabs/models/contract_inbound_message_response.py +106 -0
  411. moolabs/models/contract_participant_response.py +131 -0
  412. moolabs/models/contract_segment_response.py +95 -0
  413. moolabs/models/contract_thread_response.py +115 -0
  414. moolabs/models/contract_thread_snapshot_response.py +121 -0
  415. moolabs/models/contract_upload_detail_response.py +103 -0
  416. moolabs/models/contract_version_response.py +128 -0
  417. moolabs/models/contributor_item.py +123 -0
  418. moolabs/models/contributor_kind.py +38 -0
  419. moolabs/models/contributor_query_request.py +146 -0
  420. moolabs/models/contributor_response.py +174 -0
  421. moolabs/models/cost.py +152 -0
  422. moolabs/models/cost_adjustment_create.py +131 -0
  423. moolabs/models/cost_adjustment_response.py +102 -0
  424. moolabs/models/cost_driver_rule_create_request.py +158 -0
  425. moolabs/models/cost_driver_rule_response.py +126 -0
  426. moolabs/models/cost_driver_rule_update_request.py +100 -0
  427. moolabs/models/cost_event_detail_response.py +132 -0
  428. moolabs/models/cost_event_ingest.py +177 -0
  429. moolabs/models/cost_event_response.py +122 -0
  430. moolabs/models/cost_event_summary_item.py +103 -0
  431. moolabs/models/cost_event_summary_request.py +88 -0
  432. moolabs/models/cost_event_summary_response.py +95 -0
  433. moolabs/models/cost_explorer_response.py +155 -0
  434. moolabs/models/cost_scope.py +37 -0
  435. moolabs/models/coverage_snapshot_response.py +114 -0
  436. moolabs/models/cpu_core_hours.py +152 -0
  437. moolabs/models/cpu_cost_weight.py +152 -0
  438. moolabs/models/create_allocation_run_api422_response.py +147 -0
  439. moolabs/models/create_api_key_request.py +89 -0
  440. moolabs/models/create_api_key_response.py +105 -0
  441. moolabs/models/create_auto_topup_rule_request.py +106 -0
  442. moolabs/models/create_checkout_session_tax_id_collection.py +90 -0
  443. moolabs/models/create_checkout_session_tax_id_collection_required.py +37 -0
  444. moolabs/models/create_contract_thread_request.py +87 -0
  445. moolabs/models/create_credit_request.py +97 -0
  446. moolabs/models/create_fx_rate_request.py +103 -0
  447. moolabs/models/create_grant_request.py +136 -0
  448. moolabs/models/create_mapping_request.py +91 -0
  449. moolabs/models/create_matrix_price_row_request.py +91 -0
  450. moolabs/models/create_matrix_price_row_response.py +89 -0
  451. moolabs/models/create_portal_token_response.py +91 -0
  452. moolabs/models/create_quote_redline_request.py +103 -0
  453. moolabs/models/create_quote_request.py +119 -0
  454. moolabs/models/create_quote_template_request.py +102 -0
  455. moolabs/models/create_rate_card_request.py +105 -0
  456. moolabs/models/create_reconstruction_run_request.py +101 -0
  457. moolabs/models/create_session_response.py +97 -0
  458. moolabs/models/create_snapshot_request.py +98 -0
  459. moolabs/models/create_stripe_checkout_session_billing_address_collection.py +37 -0
  460. moolabs/models/create_stripe_checkout_session_consent_collection.py +97 -0
  461. moolabs/models/create_stripe_checkout_session_consent_collection_payment_method_reuse_agreement.py +88 -0
  462. moolabs/models/create_stripe_checkout_session_consent_collection_payment_method_reuse_agreement_position.py +37 -0
  463. moolabs/models/create_stripe_checkout_session_consent_collection_promotions.py +37 -0
  464. moolabs/models/create_stripe_checkout_session_consent_collection_terms_of_service.py +37 -0
  465. moolabs/models/create_stripe_checkout_session_customer_update.py +92 -0
  466. moolabs/models/create_stripe_checkout_session_customer_update_behavior.py +37 -0
  467. moolabs/models/create_stripe_checkout_session_redirect_on_completion.py +38 -0
  468. moolabs/models/create_stripe_checkout_session_request.py +112 -0
  469. moolabs/models/create_stripe_checkout_session_request_customer.py +163 -0
  470. moolabs/models/create_stripe_checkout_session_request_options.py +147 -0
  471. moolabs/models/create_stripe_checkout_session_result.py +139 -0
  472. moolabs/models/create_stripe_customer_portal_session_params.py +91 -0
  473. moolabs/models/create_threshold_request.py +99 -0
  474. moolabs/models/create_value_recognition_request.py +100 -0
  475. moolabs/models/create_wallet_request.py +106 -0
  476. moolabs/models/credit_config.py +119 -0
  477. moolabs/models/credit_config_payload.py +135 -0
  478. moolabs/models/credit_cost_ratio_item.py +93 -0
  479. moolabs/models/credit_cost_ratio_response.py +101 -0
  480. moolabs/models/credit_memo_apply_request.py +90 -0
  481. moolabs/models/credit_memo_create.py +104 -0
  482. moolabs/models/credit_memo_create_internal.py +114 -0
  483. moolabs/models/credit_memo_create_public.py +108 -0
  484. moolabs/models/credit_memo_item.py +102 -0
  485. moolabs/models/credit_memo_line_in.py +90 -0
  486. moolabs/models/credit_memo_list_response.py +101 -0
  487. moolabs/models/credit_memo_out.py +138 -0
  488. moolabs/models/credit_memo_response.py +124 -0
  489. moolabs/models/credit_memo_update.py +94 -0
  490. moolabs/models/credit_note_original_invoice_ref.py +114 -0
  491. moolabs/models/credit_override.py +87 -0
  492. moolabs/models/credit_override_input.py +101 -0
  493. moolabs/models/credit_override_payload.py +100 -0
  494. moolabs/models/crm_quote_context_request.py +94 -0
  495. moolabs/models/crm_quote_context_response.py +101 -0
  496. moolabs/models/crm_quote_draft_request.py +98 -0
  497. moolabs/models/crm_quote_draft_response.py +107 -0
  498. moolabs/models/crm_quote_record_card_response.py +97 -0
  499. moolabs/models/crm_surface_response.py +97 -0
  500. moolabs/models/currency.py +101 -0
  501. moolabs/models/current.py +152 -0
  502. moolabs/models/custom_invoicing_app.py +142 -0
  503. moolabs/models/custom_invoicing_app_replace_update.py +105 -0
  504. moolabs/models/custom_invoicing_customer_app_data.py +117 -0
  505. moolabs/models/custom_invoicing_draft_synchronized_request.py +91 -0
  506. moolabs/models/custom_invoicing_finalized_invoicing_request.py +91 -0
  507. moolabs/models/custom_invoicing_finalized_payment_request.py +87 -0
  508. moolabs/models/custom_invoicing_finalized_request.py +97 -0
  509. moolabs/models/custom_invoicing_line_discount_external_id_mapping.py +97 -0
  510. moolabs/models/custom_invoicing_line_external_id_mapping.py +97 -0
  511. moolabs/models/custom_invoicing_payment_trigger.py +41 -0
  512. moolabs/models/custom_invoicing_sync_result.py +110 -0
  513. moolabs/models/custom_invoicing_tax_config.py +87 -0
  514. moolabs/models/custom_invoicing_update_payment_status_request.py +92 -0
  515. moolabs/models/custom_plan_input.py +136 -0
  516. moolabs/models/custom_subscription_change.py +119 -0
  517. moolabs/models/custom_subscription_create.py +153 -0
  518. moolabs/models/customer.py +172 -0
  519. moolabs/models/customer_access.py +102 -0
  520. moolabs/models/customer_app_data.py +164 -0
  521. moolabs/models/customer_app_data_create_or_update_item.py +164 -0
  522. moolabs/models/customer_app_data_paginated_response.py +101 -0
  523. moolabs/models/customer_cost_item.py +95 -0
  524. moolabs/models/customer_create.py +120 -0
  525. moolabs/models/customer_expand.py +36 -0
  526. moolabs/models/customer_id.py +95 -0
  527. moolabs/models/customer_item.py +114 -0
  528. moolabs/models/customer_key.py +87 -0
  529. moolabs/models/customer_key_page_response.py +101 -0
  530. moolabs/models/customer_key_put_request.py +103 -0
  531. moolabs/models/customer_key_response.py +94 -0
  532. moolabs/models/customer_margin_item.py +99 -0
  533. moolabs/models/customer_margin_response.py +101 -0
  534. moolabs/models/customer_order_by.py +38 -0
  535. moolabs/models/customer_paginated_response.py +101 -0
  536. moolabs/models/customer_replace_update.py +120 -0
  537. moolabs/models/customer_subscription_order_by.py +37 -0
  538. moolabs/models/customer_upsert_request.py +102 -0
  539. moolabs/models/customer_usage_attribution.py +88 -0
  540. moolabs/models/danger_request.py +89 -0
  541. moolabs/models/dashboard_meta.py +172 -0
  542. moolabs/models/dashboard_overview_response.py +115 -0
  543. moolabs/models/data_through.py +115 -0
  544. moolabs/models/db_time_coverage_response.py +154 -0
  545. moolabs/models/db_time_evidence_batch_readback_response.py +135 -0
  546. moolabs/models/db_time_evidence_batch_request.py +147 -0
  547. moolabs/models/db_time_evidence_batch_response.py +100 -0
  548. moolabs/models/db_time_evidence_conflict_detail.py +94 -0
  549. moolabs/models/db_time_evidence_conflict_response.py +91 -0
  550. moolabs/models/db_time_evidence_not_found_detail.py +94 -0
  551. moolabs/models/db_time_evidence_not_found_response.py +91 -0
  552. moolabs/models/db_time_observation_request.py +131 -0
  553. moolabs/models/db_time_readout_quality_response.py +103 -0
  554. moolabs/models/deal_context.py +97 -0
  555. moolabs/models/deal_context_rule.py +105 -0
  556. moolabs/models/decision_item.py +131 -0
  557. moolabs/models/decision_kind.py +46 -0
  558. moolabs/models/decision_overview_response.py +172 -0
  559. moolabs/models/decision_queue_response.py +115 -0
  560. moolabs/models/decision_reason_code.py +53 -0
  561. moolabs/models/delta.py +152 -0
  562. moolabs/models/delta_pct.py +152 -0
  563. moolabs/models/demo_seed_acute_history_baseline_request.py +106 -0
  564. moolabs/models/demo_seed_acute_tenant_registration_request.py +114 -0
  565. moolabs/models/demo_seed_agent_report_baseline_request.py +106 -0
  566. moolabs/models/demo_seed_arc_invoice_ref.py +107 -0
  567. moolabs/models/demo_seed_arc_portfolio_baseline_request.py +121 -0
  568. moolabs/models/demo_seed_catalog_baseline_request.py +108 -0
  569. moolabs/models/demo_seed_customer_portfolio_baseline_request.py +103 -0
  570. moolabs/models/demo_seed_integration_crm_baseline_request.py +106 -0
  571. moolabs/models/demo_seed_integration_finance_baseline_request.py +106 -0
  572. moolabs/models/demo_seed_integration_netsuite_baseline_request.py +106 -0
  573. moolabs/models/demo_seed_integration_webhook_baseline_request.py +106 -0
  574. moolabs/models/demo_seed_invoice_payment_baseline_request.py +106 -0
  575. moolabs/models/demo_seed_isolation_canary_baseline_request.py +106 -0
  576. moolabs/models/demo_seed_lifecycle_request.py +103 -0
  577. moolabs/models/demo_seed_quote_contract_baseline_request.py +106 -0
  578. moolabs/models/demo_seed_security_credentials_baseline_request.py +106 -0
  579. moolabs/models/demo_seed_settings_baseline_request.py +106 -0
  580. moolabs/models/demo_seed_subscription_baseline_request.py +106 -0
  581. moolabs/models/demo_seed_tenant_config_baseline_request.py +119 -0
  582. moolabs/models/demo_seed_usage_baseline_request.py +106 -0
  583. moolabs/models/derive_request.py +94 -0
  584. moolabs/models/dimension.py +39 -0
  585. moolabs/models/dimension_coverage_response.py +102 -0
  586. moolabs/models/disclosure_policy_response.py +93 -0
  587. moolabs/models/disclosure_policy_update_request.py +102 -0
  588. moolabs/models/discount_percentage.py +87 -0
  589. moolabs/models/discount_reason_maximum_spend.py +94 -0
  590. moolabs/models/discount_reason_ratecard_percentage.py +109 -0
  591. moolabs/models/discount_reason_ratecard_usage.py +116 -0
  592. moolabs/models/discount_usage.py +95 -0
  593. moolabs/models/discounts.py +97 -0
  594. moolabs/models/dismiss_refinement_request.py +87 -0
  595. moolabs/models/dispute_approval_request.py +88 -0
  596. moolabs/models/dispute_list_response.py +101 -0
  597. moolabs/models/dispute_resolve_request.py +92 -0
  598. moolabs/models/dispute_response.py +134 -0
  599. moolabs/models/dispute_summary_response.py +104 -0
  600. moolabs/models/dispute_update_request.py +102 -0
  601. moolabs/models/dns_record.py +119 -0
  602. moolabs/models/domain_response.py +95 -0
  603. moolabs/models/domain_update.py +87 -0
  604. moolabs/models/draft_lock_request.py +88 -0
  605. moolabs/models/draft_lock_response.py +97 -0
  606. moolabs/models/draft_save_request.py +114 -0
  607. moolabs/models/draft_unlock_request.py +90 -0
  608. moolabs/models/driver_item.py +122 -0
  609. moolabs/models/dry_run_request.py +89 -0
  610. moolabs/models/dry_run_response.py +97 -0
  611. moolabs/models/dso_response.py +100 -0
  612. moolabs/models/dynamic_price_with_commitments.py +131 -0
  613. moolabs/models/economics_summary.py +114 -0
  614. moolabs/models/edit_subscription_add_item.py +102 -0
  615. moolabs/models/edit_subscription_add_phase.py +100 -0
  616. moolabs/models/edit_subscription_remove_item.py +98 -0
  617. moolabs/models/edit_subscription_remove_phase.py +99 -0
  618. moolabs/models/edit_subscription_stretch_phase.py +98 -0
  619. moolabs/models/edit_subscription_unschedule_edit.py +94 -0
  620. moolabs/models/effective_permission_grant_dto.py +105 -0
  621. moolabs/models/effective_permissions_response.py +129 -0
  622. moolabs/models/email_config_out.py +188 -0
  623. moolabs/models/email_config_upsert.py +111 -0
  624. moolabs/models/email_context.py +44 -0
  625. moolabs/models/email_message_status.py +42 -0
  626. moolabs/models/email_provider.py +37 -0
  627. moolabs/models/email_provider_identity.py +157 -0
  628. moolabs/models/email_provider_identity_list.py +108 -0
  629. moolabs/models/email_provider_operator_upsert.py +131 -0
  630. moolabs/models/email_provider_route.py +128 -0
  631. moolabs/models/email_provider_route_list.py +108 -0
  632. moolabs/models/enable_tier3_response.py +93 -0
  633. moolabs/models/endpoints_response.py +89 -0
  634. moolabs/models/entitlement.py +164 -0
  635. moolabs/models/entitlement_boolean.py +168 -0
  636. moolabs/models/entitlement_boolean_create_inputs.py +127 -0
  637. moolabs/models/entitlement_boolean_v2.py +170 -0
  638. moolabs/models/entitlement_create_inputs.py +164 -0
  639. moolabs/models/entitlement_grant.py +148 -0
  640. moolabs/models/entitlement_grant_create_input.py +111 -0
  641. moolabs/models/entitlement_grant_create_input_v2.py +113 -0
  642. moolabs/models/entitlement_grant_v2.py +148 -0
  643. moolabs/models/entitlement_metered.py +190 -0
  644. moolabs/models/entitlement_metered_create_inputs.py +143 -0
  645. moolabs/models/entitlement_metered_v2.py +196 -0
  646. moolabs/models/entitlement_metered_v2_create_inputs.py +157 -0
  647. moolabs/models/entitlement_order_by.py +37 -0
  648. moolabs/models/entitlement_override.py +89 -0
  649. moolabs/models/entitlement_override_input.py +95 -0
  650. moolabs/models/entitlement_override_payload.py +93 -0
  651. moolabs/models/entitlement_paginated_response.py +101 -0
  652. moolabs/models/entitlement_payload.py +103 -0
  653. moolabs/models/entitlement_static.py +170 -0
  654. moolabs/models/entitlement_static_create_inputs.py +129 -0
  655. moolabs/models/entitlement_static_v2.py +172 -0
  656. moolabs/models/entitlement_type.py +38 -0
  657. moolabs/models/entitlement_v2.py +164 -0
  658. moolabs/models/entitlement_v2_create_inputs.py +164 -0
  659. moolabs/models/entitlement_v2_paginated_response.py +101 -0
  660. moolabs/models/entitlement_value.py +109 -0
  661. moolabs/models/error_extension.py +110 -0
  662. moolabs/models/escalate_request.py +90 -0
  663. moolabs/models/escalate_response.py +91 -0
  664. moolabs/models/escalation_action_request.py +90 -0
  665. moolabs/models/escalation_action_response.py +91 -0
  666. moolabs/models/escalation_create.py +97 -0
  667. moolabs/models/escalation_dismiss_request.py +90 -0
  668. moolabs/models/escalation_list_response.py +101 -0
  669. moolabs/models/escalation_resolve_request.py +87 -0
  670. moolabs/models/escalation_response.py +112 -0
  671. moolabs/models/escalation_update.py +93 -0
  672. moolabs/models/event.py +115 -0
  673. moolabs/models/event_delivery_attempt_response.py +101 -0
  674. moolabs/models/evidence_create_request.py +96 -0
  675. moolabs/models/evidence_kind.py +39 -0
  676. moolabs/models/evidence_list_response.py +97 -0
  677. moolabs/models/evidence_response.py +106 -0
  678. moolabs/models/evidence_review_request.py +90 -0
  679. moolabs/models/evidence_window.py +37 -0
  680. moolabs/models/exchange_rate.py +152 -0
  681. moolabs/models/expiration_duration.py +40 -0
  682. moolabs/models/expiration_period.py +91 -0
  683. moolabs/models/explorer_alias_hint.py +104 -0
  684. moolabs/models/explorer_alias_hint1.py +110 -0
  685. moolabs/models/explorer_alias_kind.py +39 -0
  686. moolabs/models/explorer_breakdown_page.py +166 -0
  687. moolabs/models/explorer_breakdown_row.py +200 -0
  688. moolabs/models/explorer_cloud_provider_quality_axis.py +107 -0
  689. moolabs/models/explorer_cloud_temporal_coverage_summary.py +115 -0
  690. moolabs/models/explorer_contributor_kind.py +40 -0
  691. moolabs/models/explorer_contributor_query_request.py +190 -0
  692. moolabs/models/explorer_contributor_response.py +201 -0
  693. moolabs/models/explorer_contributor_row.py +177 -0
  694. moolabs/models/explorer_cost_event_status.py +39 -0
  695. moolabs/models/explorer_dimension.py +40 -0
  696. moolabs/models/explorer_effective_sort.py +107 -0
  697. moolabs/models/explorer_effective_sort_field.py +41 -0
  698. moolabs/models/explorer_evidence_kind.py +41 -0
  699. moolabs/models/explorer_evidence_role.py +37 -0
  700. moolabs/models/explorer_excluded_event_count.py +104 -0
  701. moolabs/models/explorer_excluded_event_reason.py +38 -0
  702. moolabs/models/explorer_exposure_summary.py +124 -0
  703. moolabs/models/explorer_measure_source.py +40 -0
  704. moolabs/models/explorer_meta.py +196 -0
  705. moolabs/models/explorer_quality_axis.py +117 -0
  706. moolabs/models/explorer_quality_caveat_code.py +45 -0
  707. moolabs/models/explorer_quality_summary.py +151 -0
  708. moolabs/models/explorer_query_request.py +184 -0
  709. moolabs/models/explorer_request_detail_event.py +134 -0
  710. moolabs/models/explorer_request_detail_line.py +133 -0
  711. moolabs/models/explorer_request_detail_query_request.py +201 -0
  712. moolabs/models/explorer_request_detail_response.py +205 -0
  713. moolabs/models/explorer_request_detail_result_cap.py +125 -0
  714. moolabs/models/explorer_request_distribution.py +109 -0
  715. moolabs/models/explorer_request_query_request.py +190 -0
  716. moolabs/models/explorer_search.py +111 -0
  717. moolabs/models/explorer_selection_result.py +107 -0
  718. moolabs/models/explorer_selection_row.py +185 -0
  719. moolabs/models/explorer_selection_status.py +39 -0
  720. moolabs/models/explorer_sort.py +104 -0
  721. moolabs/models/explorer_sort1.py +116 -0
  722. moolabs/models/explorer_sort_fallback_reason.py +39 -0
  723. moolabs/models/explorer_sort_field.py +40 -0
  724. moolabs/models/explorer_source_scope.py +38 -0
  725. moolabs/models/explorer_summary.py +134 -0
  726. moolabs/models/explorer_trend_point.py +124 -0
  727. moolabs/models/explorer_usage_history_evidence.py +110 -0
  728. moolabs/models/explorer_usage_history_source.py +38 -0
  729. moolabs/models/feature.py +156 -0
  730. moolabs/models/feature_contributor_customer.py +91 -0
  731. moolabs/models/feature_contributor_model.py +93 -0
  732. moolabs/models/feature_contributor_request.py +91 -0
  733. moolabs/models/feature_contributors_response.py +121 -0
  734. moolabs/models/feature_cost_item.py +97 -0
  735. moolabs/models/feature_create_inputs.py +128 -0
  736. moolabs/models/feature_flag_item.py +97 -0
  737. moolabs/models/feature_flag_update.py +87 -0
  738. moolabs/models/feature_margin_item.py +101 -0
  739. moolabs/models/feature_margin_response.py +101 -0
  740. moolabs/models/feature_meta.py +97 -0
  741. moolabs/models/feature_order_by.py +40 -0
  742. moolabs/models/feature_override_request.py +93 -0
  743. moolabs/models/feature_paginated_response.py +101 -0
  744. moolabs/models/feature_pool.py +99 -0
  745. moolabs/models/feature_pool_payload.py +107 -0
  746. moolabs/models/feature_price_request.py +93 -0
  747. moolabs/models/feature_pricing_input.py +120 -0
  748. moolabs/models/filter_id_exact.py +88 -0
  749. moolabs/models/filter_string.py +129 -0
  750. moolabs/models/filter_time.py +114 -0
  751. moolabs/models/finance_summary.py +143 -0
  752. moolabs/models/first_pass_redline_request.py +88 -0
  753. moolabs/models/first_pass_redline_response.py +89 -0
  754. moolabs/models/flag_disputed_request.py +92 -0
  755. moolabs/models/flag_disputed_response.py +97 -0
  756. moolabs/models/flat_price.py +104 -0
  757. moolabs/models/flat_price_with_payment_term.py +107 -0
  758. moolabs/models/flatamount.py +152 -0
  759. moolabs/models/freshness_axis.py +121 -0
  760. moolabs/models/freshness_source.py +40 -0
  761. moolabs/models/fx_rate_to_reporting.py +153 -0
  762. moolabs/models/fx_rates_list_response.py +97 -0
  763. moolabs/models/gate_status_response.py +105 -0
  764. moolabs/models/get_fx_rate_request.py +92 -0
  765. moolabs/models/get_matrix_price_rows_response.py +107 -0
  766. moolabs/models/google_chat_quote_response.py +89 -0
  767. moolabs/models/grant_activation_request.py +122 -0
  768. moolabs/models/grant_burn_down_history_segment.py +125 -0
  769. moolabs/models/grant_order_by.py +38 -0
  770. moolabs/models/grant_paginated_response.py +101 -0
  771. moolabs/models/grant_plan_change_request.py +126 -0
  772. moolabs/models/grant_renewal_request.py +122 -0
  773. moolabs/models/grant_response.py +128 -0
  774. moolabs/models/grant_source_type.py +40 -0
  775. moolabs/models/grant_usage_record.py +97 -0
  776. moolabs/models/grant_v2_paginated_response.py +101 -0
  777. moolabs/models/grants_list_response.py +99 -0
  778. moolabs/models/handoff_create_request.py +92 -0
  779. moolabs/models/helicone_config_request.py +92 -0
  780. moolabs/models/homepage_response.py +107 -0
  781. moolabs/models/hosted_checkout_settlement_ptp_response.py +100 -0
  782. moolabs/models/hosted_checkout_settlement_request.py +109 -0
  783. moolabs/models/hosted_checkout_settlement_response.py +97 -0
  784. moolabs/models/http_validation_error.py +95 -0
  785. moolabs/models/hub_spot_connect_request.py +91 -0
  786. moolabs/models/id_resource.py +97 -0
  787. moolabs/models/identity_scope.py +39 -0
  788. moolabs/models/identity_scope_coverage_response.py +96 -0
  789. moolabs/models/import_batch_detail_response.py +105 -0
  790. moolabs/models/import_batch_detail_row.py +111 -0
  791. moolabs/models/import_batch_request.py +112 -0
  792. moolabs/models/import_batch_response.py +101 -0
  793. moolabs/models/import_batch_summary.py +111 -0
  794. moolabs/models/import_dispute_item.py +122 -0
  795. moolabs/models/import_disputes_request.py +96 -0
  796. moolabs/models/import_invoice_item.py +133 -0
  797. moolabs/models/import_invoices_request.py +96 -0
  798. moolabs/models/import_promise_item.py +155 -0
  799. moolabs/models/import_promises_request.py +96 -0
  800. moolabs/models/inbound_email_attachment.py +91 -0
  801. moolabs/models/inbound_email_payload.py +103 -0
  802. moolabs/models/inbound_email_process_response.py +97 -0
  803. moolabs/models/ingest_events_body.py +150 -0
  804. moolabs/models/ingest_health.py +97 -0
  805. moolabs/models/ingested_event.py +111 -0
  806. moolabs/models/ingested_event_cursor_paginated_response.py +98 -0
  807. moolabs/models/initialamount.py +152 -0
  808. moolabs/models/insight_card.py +111 -0
  809. moolabs/models/install_method.py +38 -0
  810. moolabs/models/integration_update.py +89 -0
  811. moolabs/models/invalidate_portal_token_request.py +89 -0
  812. moolabs/models/invalidate_portal_tokens_request.py +89 -0
  813. moolabs/models/invoice.py +249 -0
  814. moolabs/models/invoice_allocation_out.py +103 -0
  815. moolabs/models/invoice_app_external_ids.py +97 -0
  816. moolabs/models/invoice_available_action_details.py +89 -0
  817. moolabs/models/invoice_available_actions.py +132 -0
  818. moolabs/models/invoice_detailed_line.py +244 -0
  819. moolabs/models/invoice_detailed_line_cost_category.py +37 -0
  820. moolabs/models/invoice_detailed_line_rate_card.py +116 -0
  821. moolabs/models/invoice_document_ref_type.py +36 -0
  822. moolabs/models/invoice_expand.py +38 -0
  823. moolabs/models/invoice_generic_document_ref.py +98 -0
  824. moolabs/models/invoice_item.py +112 -0
  825. moolabs/models/invoice_line.py +296 -0
  826. moolabs/models/invoice_line_amount_discount.py +141 -0
  827. moolabs/models/invoice_line_app_external_ids.py +93 -0
  828. moolabs/models/invoice_line_discounts.py +105 -0
  829. moolabs/models/invoice_line_managed_by.py +38 -0
  830. moolabs/models/invoice_line_out.py +99 -0
  831. moolabs/models/invoice_line_replace_update.py +143 -0
  832. moolabs/models/invoice_line_status.py +38 -0
  833. moolabs/models/invoice_line_subscription_reference.py +115 -0
  834. moolabs/models/invoice_line_tax_behavior.py +37 -0
  835. moolabs/models/invoice_line_tax_item.py +117 -0
  836. moolabs/models/invoice_line_usage_discount.py +155 -0
  837. moolabs/models/invoice_order_by.py +41 -0
  838. moolabs/models/invoice_paginated_response.py +101 -0
  839. moolabs/models/invoice_payment_terms.py +91 -0
  840. moolabs/models/invoice_pending_line_create.py +131 -0
  841. moolabs/models/invoice_pending_line_create_input.py +105 -0
  842. moolabs/models/invoice_pending_line_create_response.py +107 -0
  843. moolabs/models/invoice_pending_lines_action_filters_input.py +88 -0
  844. moolabs/models/invoice_pending_lines_action_input.py +106 -0
  845. moolabs/models/invoice_reference.py +101 -0
  846. moolabs/models/invoice_replace_update.py +117 -0
  847. moolabs/models/invoice_simulation_input.py +107 -0
  848. moolabs/models/invoice_simulation_line.py +164 -0
  849. moolabs/models/invoice_status.py +44 -0
  850. moolabs/models/invoice_status_details.py +103 -0
  851. moolabs/models/invoice_totals.py +163 -0
  852. moolabs/models/invoice_type.py +37 -0
  853. moolabs/models/invoice_upsert_request.py +112 -0
  854. moolabs/models/invoice_usage_based_rate_card.py +116 -0
  855. moolabs/models/invoice_workflow_invoicing_settings_replace_update.py +97 -0
  856. moolabs/models/invoice_workflow_replace_update.py +91 -0
  857. moolabs/models/invoice_workflow_settings.py +111 -0
  858. moolabs/models/invoice_workflow_settings_replace_update.py +97 -0
  859. moolabs/models/issue_after_reset.py +90 -0
  860. moolabs/models/jurisdiction_profile_response.py +102 -0
  861. moolabs/models/jurisdiction_resolution_request.py +92 -0
  862. moolabs/models/kill_switch_request.py +87 -0
  863. moolabs/models/langfuse_config_request.py +92 -0
  864. moolabs/models/lesson_action_request.py +89 -0
  865. moolabs/models/level_in.py +91 -0
  866. moolabs/models/lifecycle_event_request.py +97 -0
  867. moolabs/models/line_item_create.py +118 -0
  868. moolabs/models/line_total.py +153 -0
  869. moolabs/models/link_net_suite_account_request.py +89 -0
  870. moolabs/models/list_auto_topup_rules_response.py +97 -0
  871. moolabs/models/list_connectors200_response_inner.py +148 -0
  872. moolabs/models/list_entitlements_result.py +151 -0
  873. moolabs/models/list_events_v2_filter_param.py +123 -0
  874. moolabs/models/list_features_result.py +151 -0
  875. moolabs/models/list_grants_get200_response.py +151 -0
  876. moolabs/models/mapping_response.py +97 -0
  877. moolabs/models/mapping_rule_create.py +105 -0
  878. moolabs/models/mapping_rule_response.py +118 -0
  879. moolabs/models/mapping_rule_update.py +103 -0
  880. moolabs/models/margin_snapshot_response.py +131 -0
  881. moolabs/models/marketplace_app_api_key_install_request.py +91 -0
  882. moolabs/models/marketplace_install_request_payload.py +89 -0
  883. moolabs/models/marketplace_install_response.py +94 -0
  884. moolabs/models/marketplace_listing.py +105 -0
  885. moolabs/models/marketplace_listing_paginated_response.py +101 -0
  886. moolabs/models/matrix_price_row.py +91 -0
  887. moolabs/models/measure.py +146 -0
  888. moolabs/models/measure_kind.py +41 -0
  889. moolabs/models/measure_usage_from.py +151 -0
  890. moolabs/models/measure_usage_from_preset.py +37 -0
  891. moolabs/models/merge_remittances_request.py +89 -0
  892. moolabs/models/meter.py +176 -0
  893. moolabs/models/meter_aggregation.py +42 -0
  894. moolabs/models/meter_create.py +113 -0
  895. moolabs/models/meter_mapping_item.py +95 -0
  896. moolabs/models/meter_order_by.py +40 -0
  897. moolabs/models/meter_query_request.py +121 -0
  898. moolabs/models/meter_query_result.py +103 -0
  899. moolabs/models/meter_query_row.py +98 -0
  900. moolabs/models/meter_test_event_request.py +87 -0
  901. moolabs/models/meter_test_event_response.py +93 -0
  902. moolabs/models/meter_update.py +94 -0
  903. moolabs/models/meter_usage_summary_item.py +103 -0
  904. moolabs/models/meter_usage_summary_response.py +95 -0
  905. moolabs/models/meter_validation_error.py +101 -0
  906. moolabs/models/migrate_subscription_request.py +105 -0
  907. moolabs/models/model_cost_item.py +95 -0
  908. moolabs/models/net_suite_connect_request.py +103 -0
  909. moolabs/models/net_suite_cutover_guard_request.py +87 -0
  910. moolabs/models/net_suite_m2_m_finalize_request.py +87 -0
  911. moolabs/models/net_suite_m2_m_setup_request.py +101 -0
  912. moolabs/models/net_suite_managed_portal_backfill_cancel_request.py +87 -0
  913. moolabs/models/net_suite_managed_portal_backfill_request.py +89 -0
  914. moolabs/models/net_suite_managed_portal_backfill_run_request.py +90 -0
  915. moolabs/models/net_suite_managed_portal_mapping_delete_request.py +91 -0
  916. moolabs/models/net_suite_managed_portal_mapping_request.py +97 -0
  917. moolabs/models/net_suite_managed_portal_protection_ack_request.py +89 -0
  918. moolabs/models/next_action_type.py +41 -0
  919. moolabs/models/note_create.py +94 -0
  920. moolabs/models/note_list_response.py +101 -0
  921. moolabs/models/note_response.py +104 -0
  922. moolabs/models/notification_channel_meta.py +100 -0
  923. moolabs/models/notification_channel_order_by.py +39 -0
  924. moolabs/models/notification_channel_paginated_response.py +101 -0
  925. moolabs/models/notification_channel_type.py +36 -0
  926. moolabs/models/notification_channel_webhook.py +145 -0
  927. moolabs/models/notification_channel_webhook_create_request.py +117 -0
  928. moolabs/models/notification_event.py +137 -0
  929. moolabs/models/notification_event_balance_threshold_payload.py +121 -0
  930. moolabs/models/notification_event_balance_threshold_payload_data.py +133 -0
  931. moolabs/models/notification_event_delivery_attempt.py +103 -0
  932. moolabs/models/notification_event_delivery_status.py +125 -0
  933. moolabs/models/notification_event_delivery_status_state.py +40 -0
  934. moolabs/models/notification_event_entitlement_value_payload_base.py +125 -0
  935. moolabs/models/notification_event_invoice_created_payload.py +121 -0
  936. moolabs/models/notification_event_invoice_updated_payload.py +121 -0
  937. moolabs/models/notification_event_order_by.py +37 -0
  938. moolabs/models/notification_event_paginated_response.py +101 -0
  939. moolabs/models/notification_event_payload.py +178 -0
  940. moolabs/models/notification_event_resend_request.py +88 -0
  941. moolabs/models/notification_event_reset_payload.py +121 -0
  942. moolabs/models/notification_event_type.py +39 -0
  943. moolabs/models/notification_rule.py +178 -0
  944. moolabs/models/notification_rule_balance_threshold.py +159 -0
  945. moolabs/models/notification_rule_balance_threshold_create_request.py +115 -0
  946. moolabs/models/notification_rule_balance_threshold_value.py +90 -0
  947. moolabs/models/notification_rule_balance_threshold_value_type.py +40 -0
  948. moolabs/models/notification_rule_create_request.py +178 -0
  949. moolabs/models/notification_rule_entitlement_reset.py +149 -0
  950. moolabs/models/notification_rule_entitlement_reset_create_request.py +105 -0
  951. moolabs/models/notification_rule_invoice_created.py +139 -0
  952. moolabs/models/notification_rule_invoice_created_create_request.py +103 -0
  953. moolabs/models/notification_rule_invoice_updated.py +139 -0
  954. moolabs/models/notification_rule_invoice_updated_create_request.py +103 -0
  955. moolabs/models/notification_rule_order_by.py +39 -0
  956. moolabs/models/notification_rule_paginated_response.py +101 -0
  957. moolabs/models/notify_channel.py +89 -0
  958. moolabs/models/o_auth2_authorization_code_grant_error_type.py +42 -0
  959. moolabs/models/observation_out.py +103 -0
  960. moolabs/models/observed_total_cost.py +153 -0
  961. moolabs/models/open_cost_cluster_binding.py +115 -0
  962. moolabs/models/open_cost_dedicated_namespace.py +104 -0
  963. moolabs/models/open_cost_installation_create_request.py +122 -0
  964. moolabs/models/open_cost_installation_create_response.py +116 -0
  965. moolabs/models/open_cost_installation_response.py +114 -0
  966. moolabs/models/open_cost_snapshot_request.py +139 -0
  967. moolabs/models/open_cost_snapshot_response.py +125 -0
  968. moolabs/models/open_cost_trust_policy.py +145 -0
  969. moolabs/models/open_cost_wire_row.py +140 -0
  970. moolabs/models/open_from_scope_request.py +98 -0
  971. moolabs/models/operations_summary.py +120 -0
  972. moolabs/models/opt_out_request.py +90 -0
  973. moolabs/models/opt_out_response.py +93 -0
  974. moolabs/models/org_role.py +152 -0
  975. moolabs/models/original_total.py +152 -0
  976. moolabs/models/otlp_ingest_response.py +91 -0
  977. moolabs/models/overall_freshness.py +116 -0
  978. moolabs/models/overdraft_settings.py +93 -0
  979. moolabs/models/overdraft_settings_payload.py +93 -0
  980. moolabs/models/override_first_ingress_request.py +94 -0
  981. moolabs/models/override_request.py +90 -0
  982. moolabs/models/override_response.py +91 -0
  983. moolabs/models/overview_response.py +109 -0
  984. moolabs/models/pack_provenance.py +101 -0
  985. moolabs/models/package_price_with_commitments.py +137 -0
  986. moolabs/models/paginated_credit_memos.py +97 -0
  987. moolabs/models/paginated_customers.py +99 -0
  988. moolabs/models/paginated_invoices.py +97 -0
  989. moolabs/models/paginated_payments.py +97 -0
  990. moolabs/models/paginated_sync_runs.py +97 -0
  991. moolabs/models/patch_quote_agent_policy_request.py +98 -0
  992. moolabs/models/patch_rows_request.py +95 -0
  993. moolabs/models/patch_session_request.py +107 -0
  994. moolabs/models/payment_allocation_response.py +104 -0
  995. moolabs/models/payment_create.py +109 -0
  996. moolabs/models/payment_due_date.py +124 -0
  997. moolabs/models/payment_failed_request.py +101 -0
  998. moolabs/models/payment_failed_response.py +93 -0
  999. moolabs/models/payment_instructions_response.py +95 -0
  1000. moolabs/models/payment_instructions_update_request.py +90 -0
  1001. moolabs/models/payment_item.py +104 -0
  1002. moolabs/models/payment_link_request.py +105 -0
  1003. moolabs/models/payment_link_response.py +89 -0
  1004. moolabs/models/payment_list_response.py +101 -0
  1005. moolabs/models/payment_match_request.py +90 -0
  1006. moolabs/models/payment_response.py +136 -0
  1007. moolabs/models/payment_reversal_request.py +90 -0
  1008. moolabs/models/payment_succeeded_request.py +103 -0
  1009. moolabs/models/payment_succeeded_response.py +97 -0
  1010. moolabs/models/payment_term_due_date.py +113 -0
  1011. moolabs/models/payment_term_instant.py +102 -0
  1012. moolabs/models/payment_terms.py +148 -0
  1013. moolabs/models/pending_provider_confirmation_reconcile_request.py +88 -0
  1014. moolabs/models/pending_sns_confirmation_out.py +102 -0
  1015. moolabs/models/period.py +90 -0
  1016. moolabs/models/period_settlement_out.py +143 -0
  1017. moolabs/models/period_window.py +103 -0
  1018. moolabs/models/persona.py +39 -0
  1019. moolabs/models/plan.py +191 -0
  1020. moolabs/models/plan_addon.py +126 -0
  1021. moolabs/models/plan_addon_create.py +101 -0
  1022. moolabs/models/plan_addon_order_by.py +40 -0
  1023. moolabs/models/plan_addon_paginated_response.py +101 -0
  1024. moolabs/models/plan_addon_replace_update.py +91 -0
  1025. moolabs/models/plan_amend_request.py +103 -0
  1026. moolabs/models/plan_cost_item.py +97 -0
  1027. moolabs/models/plan_create.py +140 -0
  1028. moolabs/models/plan_create_request.py +103 -0
  1029. moolabs/models/plan_order_by.py +40 -0
  1030. moolabs/models/plan_paginated_response.py +101 -0
  1031. moolabs/models/plan_phase.py +113 -0
  1032. moolabs/models/plan_pricing_input.py +103 -0
  1033. moolabs/models/plan_pricing_sync_request.py +105 -0
  1034. moolabs/models/plan_pricing_view.py +107 -0
  1035. moolabs/models/plan_ref_payload.py +91 -0
  1036. moolabs/models/plan_reference.py +106 -0
  1037. moolabs/models/plan_reference_input.py +97 -0
  1038. moolabs/models/plan_replace_update.py +122 -0
  1039. moolabs/models/plan_status.py +39 -0
  1040. moolabs/models/plan_subscription_change.py +153 -0
  1041. moolabs/models/plan_subscription_create.py +167 -0
  1042. moolabs/models/policy_update_request.py +94 -0
  1043. moolabs/models/pool_notification_config_request.py +87 -0
  1044. moolabs/models/pooloverrides_value.py +152 -0
  1045. moolabs/models/portal_case_response.py +108 -0
  1046. moolabs/models/portal_channel_preference_request.py +100 -0
  1047. moolabs/models/portal_dispute_request.py +94 -0
  1048. moolabs/models/portal_dispute_response.py +102 -0
  1049. moolabs/models/portal_invoice_response.py +124 -0
  1050. moolabs/models/portal_payment_intent_response.py +91 -0
  1051. moolabs/models/portal_payment_request.py +102 -0
  1052. moolabs/models/portal_preferences_request.py +113 -0
  1053. moolabs/models/portal_preferences_response.py +87 -0
  1054. moolabs/models/portal_promise_to_pay_response.py +106 -0
  1055. moolabs/models/portal_ptp_request.py +95 -0
  1056. moolabs/models/portal_token.py +121 -0
  1057. moolabs/models/portal_token_item.py +97 -0
  1058. moolabs/models/preview_resolution_request.py +89 -0
  1059. moolabs/models/preview_resolution_response.py +101 -0
  1060. moolabs/models/preview_template_request.py +102 -0
  1061. moolabs/models/preview_template_response.py +95 -0
  1062. moolabs/models/preview_trace_entry.py +97 -0
  1063. moolabs/models/previous.py +152 -0
  1064. moolabs/models/price_payment_term.py +37 -0
  1065. moolabs/models/price_session_response.py +109 -0
  1066. moolabs/models/price_tier.py +110 -0
  1067. moolabs/models/price_tier_payload.py +103 -0
  1068. moolabs/models/pricing_override_input.py +100 -0
  1069. moolabs/models/pricing_result.py +100 -0
  1070. moolabs/models/pricing_snapshot_response.py +109 -0
  1071. moolabs/models/pricing_tier_input.py +90 -0
  1072. moolabs/models/priority.py +38 -0
  1073. moolabs/models/pro_rating_config.py +90 -0
  1074. moolabs/models/pro_rating_mode.py +36 -0
  1075. moolabs/models/process_value_recognition_request.py +94 -0
  1076. moolabs/models/progress.py +94 -0
  1077. moolabs/models/projection_link_in.py +91 -0
  1078. moolabs/models/projection_links_request.py +98 -0
  1079. moolabs/models/projection_links_response.py +89 -0
  1080. moolabs/models/promise_response.py +112 -0
  1081. moolabs/models/promote_response.py +89 -0
  1082. moolabs/models/provider_billing_reference.py +146 -0
  1083. moolabs/models/provider_billing_reference_kind.py +37 -0
  1084. moolabs/models/provider_blocking_reason.py +115 -0
  1085. moolabs/models/provider_confirmation_repair_request.py +99 -0
  1086. moolabs/models/provider_connection_status.py +42 -0
  1087. moolabs/models/provider_identity_details.py +110 -0
  1088. moolabs/models/provider_identity_purpose.py +38 -0
  1089. moolabs/models/provider_kind.py +37 -0
  1090. moolabs/models/provider_readiness_response.py +97 -0
  1091. moolabs/models/provider_readiness_summary.py +123 -0
  1092. moolabs/models/provider_route_status.py +39 -0
  1093. moolabs/models/provider_safe_test_send_request.py +107 -0
  1094. moolabs/models/provider_safe_test_send_response.py +109 -0
  1095. moolabs/models/provider_summary.py +89 -0
  1096. moolabs/models/provider_warning.py +115 -0
  1097. moolabs/models/ptp_approval_request.py +88 -0
  1098. moolabs/models/ptp_create_request.py +103 -0
  1099. moolabs/models/ptp_fulfill_request.py +90 -0
  1100. moolabs/models/ptp_payment_link_response.py +89 -0
  1101. moolabs/models/ptp_summary_response.py +104 -0
  1102. moolabs/models/publish_plan_request.py +93 -0
  1103. moolabs/models/publish_plan_result.py +93 -0
  1104. moolabs/models/publish_template_request.py +96 -0
  1105. moolabs/models/put_account_segment_request.py +89 -0
  1106. moolabs/models/put_sku_eligibility_request.py +89 -0
  1107. moolabs/models/put_tenant_segments_request.py +89 -0
  1108. moolabs/models/quality_axis.py +116 -0
  1109. moolabs/models/quality_caveat_code.py +40 -0
  1110. moolabs/models/quality_status.py +40 -0
  1111. moolabs/models/quality_summary.py +135 -0
  1112. moolabs/models/quantity.py +153 -0
  1113. moolabs/models/quick_action.py +99 -0
  1114. moolabs/models/quick_books_connect_request.py +93 -0
  1115. moolabs/models/quick_redline_response.py +123 -0
  1116. moolabs/models/quote_agent_evaluation_response.py +106 -0
  1117. moolabs/models/quote_agent_policy_response.py +112 -0
  1118. moolabs/models/quote_agent_provenance_run_response.py +114 -0
  1119. moolabs/models/quote_agent_run_response.py +122 -0
  1120. moolabs/models/quote_approval_decision_request.py +88 -0
  1121. moolabs/models/quote_approval_response.py +107 -0
  1122. moolabs/models/quote_line_item_input.py +144 -0
  1123. moolabs/models/quote_list_response.py +101 -0
  1124. moolabs/models/quote_metrics_response.py +105 -0
  1125. moolabs/models/quote_redline_decision_request.py +88 -0
  1126. moolabs/models/quote_redline_response.py +125 -0
  1127. moolabs/models/quote_response.py +125 -0
  1128. moolabs/models/quote_template_list_response.py +95 -0
  1129. moolabs/models/quote_template_response.py +109 -0
  1130. moolabs/models/quote_version_response.py +129 -0
  1131. moolabs/models/ram_cost_weight.py +152 -0
  1132. moolabs/models/ram_gib_hours.py +152 -0
  1133. moolabs/models/rate_bulk_import_request.py +97 -0
  1134. moolabs/models/rate_bulk_import_response.py +91 -0
  1135. moolabs/models/rate_card.py +150 -0
  1136. moolabs/models/rate_card_boolean_entitlement.py +96 -0
  1137. moolabs/models/rate_card_cost_list_response.py +95 -0
  1138. moolabs/models/rate_card_cost_row.py +103 -0
  1139. moolabs/models/rate_card_discount.py +87 -0
  1140. moolabs/models/rate_card_discount_input.py +103 -0
  1141. moolabs/models/rate_card_discount_payload.py +87 -0
  1142. moolabs/models/rate_card_entitlement.py +164 -0
  1143. moolabs/models/rate_card_flat_fee.py +148 -0
  1144. moolabs/models/rate_card_metered_entitlement.py +107 -0
  1145. moolabs/models/rate_card_payload.py +103 -0
  1146. moolabs/models/rate_card_response.py +106 -0
  1147. moolabs/models/rate_card_static_entitlement.py +98 -0
  1148. moolabs/models/rate_card_usage_based.py +148 -0
  1149. moolabs/models/rate_card_usage_based_price.py +192 -0
  1150. moolabs/models/rate_catalog_create.py +117 -0
  1151. moolabs/models/rate_catalog_response.py +118 -0
  1152. moolabs/models/rate_catalog_update.py +107 -0
  1153. moolabs/models/rate_generation_list_view.py +97 -0
  1154. moolabs/models/rate_generation_view.py +98 -0
  1155. moolabs/models/rate_per_unit.py +153 -0
  1156. moolabs/models/rate_per_unit1.py +153 -0
  1157. moolabs/models/rate_schedule_list_response.py +95 -0
  1158. moolabs/models/rate_schedule_preview_response.py +105 -0
  1159. moolabs/models/rate_schedule_version_out.py +101 -0
  1160. moolabs/models/ratify_family_request.py +91 -0
  1161. moolabs/models/ratify_refinement_request.py +93 -0
  1162. moolabs/models/re_scope_request.py +89 -0
  1163. moolabs/models/readiness_summary.py +89 -0
  1164. moolabs/models/readout_page_response.py +119 -0
  1165. moolabs/models/readout_row_response.py +104 -0
  1166. moolabs/models/readout_snapshot_response.py +116 -0
  1167. moolabs/models/reallocate_request.py +90 -0
  1168. moolabs/models/reallocate_response.py +97 -0
  1169. moolabs/models/reattempt_request.py +88 -0
  1170. moolabs/models/reattempt_response.py +89 -0
  1171. moolabs/models/recall_request.py +87 -0
  1172. moolabs/models/reconcile_request.py +89 -0
  1173. moolabs/models/reconcile_response.py +95 -0
  1174. moolabs/models/reconciliation_basis.py +99 -0
  1175. moolabs/models/reconciliation_basis_out.py +95 -0
  1176. moolabs/models/reconstruction_run_response.py +95 -0
  1177. moolabs/models/record_usage_request.py +115 -0
  1178. moolabs/models/recurring_period.py +96 -0
  1179. moolabs/models/recurring_period_create_input.py +94 -0
  1180. moolabs/models/recurring_period_interval.py +151 -0
  1181. moolabs/models/recurring_period_interval_enum.py +39 -0
  1182. moolabs/models/recurring_period_v2.py +94 -0
  1183. moolabs/models/recurringamount.py +152 -0
  1184. moolabs/models/redline_summary.py +89 -0
  1185. moolabs/models/refund_credit_request.py +90 -0
  1186. moolabs/models/reject_rate_generation_request.py +87 -0
  1187. moolabs/models/remittance_create_request.py +103 -0
  1188. moolabs/models/remittance_list_response.py +101 -0
  1189. moolabs/models/remittance_response.py +120 -0
  1190. moolabs/models/remove_contract_participant_response.py +131 -0
  1191. moolabs/models/remove_phase_shifting.py +37 -0
  1192. moolabs/models/replace_levels_request.py +95 -0
  1193. moolabs/models/replay_dead_letter_request.py +89 -0
  1194. moolabs/models/report_create_request.py +105 -0
  1195. moolabs/models/report_create_response.py +139 -0
  1196. moolabs/models/reporting_line_total.py +153 -0
  1197. moolabs/models/reporting_total_cost.py +153 -0
  1198. moolabs/models/request_cost_distribution.py +127 -0
  1199. moolabs/models/request_evidence_response.py +197 -0
  1200. moolabs/models/request_evidence_row.py +154 -0
  1201. moolabs/models/request_pricing_state.py +40 -0
  1202. moolabs/models/reschedule_request.py +88 -0
  1203. moolabs/models/resend_identity_details.py +104 -0
  1204. moolabs/models/reset_entitlement_usage_input.py +92 -0
  1205. moolabs/models/resolve_unapplied_request.py +87 -0
  1206. moolabs/models/resource_map_create_request.py +103 -0
  1207. moolabs/models/resource_map_response.py +113 -0
  1208. moolabs/models/retry_unpriced_request.py +91 -0
  1209. moolabs/models/retry_unpriced_response.py +91 -0
  1210. moolabs/models/revenue_recognition_request.py +89 -0
  1211. moolabs/models/reversed_settlement_ref.py +93 -0
  1212. moolabs/models/review_quarantined_request.py +95 -0
  1213. moolabs/models/review_quarantined_response.py +95 -0
  1214. moolabs/models/revoke_api_key_request.py +87 -0
  1215. moolabs/models/revoke_portal_token_request.py +87 -0
  1216. moolabs/models/rollover_mode.py +38 -0
  1217. moolabs/models/rollover_request.py +95 -0
  1218. moolabs/models/rollover_response.py +98 -0
  1219. moolabs/models/rollovercapamount.py +152 -0
  1220. moolabs/models/rolloverpercent.py +152 -0
  1221. moolabs/models/rotate_key_request.py +88 -0
  1222. moolabs/models/rotate_key_response.py +92 -0
  1223. moolabs/models/route_update_request.py +110 -0
  1224. moolabs/models/salesforce_connect_request.py +93 -0
  1225. moolabs/models/sample_rate.py +153 -0
  1226. moolabs/models/sandbox_app.py +138 -0
  1227. moolabs/models/sandbox_app_replace_update.py +101 -0
  1228. moolabs/models/sandbox_customer_app_data.py +115 -0
  1229. moolabs/models/save_redline_edit_request.py +95 -0
  1230. moolabs/models/scenario_compare_response.py +97 -0
  1231. moolabs/models/scenario_item.py +117 -0
  1232. moolabs/models/scenario_request.py +96 -0
  1233. moolabs/models/scope_type.py +38 -0
  1234. moolabs/models/sdk_batch_ingest_request.py +96 -0
  1235. moolabs/models/sdk_batch_ingest_response.py +91 -0
  1236. moolabs/models/sdk_event.py +111 -0
  1237. moolabs/models/sdk_span.py +106 -0
  1238. moolabs/models/segment_out.py +95 -0
  1239. moolabs/models/sender_response.py +93 -0
  1240. moolabs/models/sender_update.py +93 -0
  1241. moolabs/models/ses_identity_details.py +127 -0
  1242. moolabs/models/ses_provider_scope_status.py +89 -0
  1243. moolabs/models/set_rate_card_cost_request.py +89 -0
  1244. moolabs/models/severity.py +38 -0
  1245. moolabs/models/shadow_mode_request.py +87 -0
  1246. moolabs/models/shadow_run_list_item.py +101 -0
  1247. moolabs/models/shadow_run_metrics.py +114 -0
  1248. moolabs/models/shadow_run_request.py +92 -0
  1249. moolabs/models/shadow_run_summary.py +95 -0
  1250. moolabs/models/simulate_margin_request.py +100 -0
  1251. moolabs/models/simulate_margin_response.py +115 -0
  1252. moolabs/models/simulate_out.py +103 -0
  1253. moolabs/models/sku_eligibility_list_response.py +95 -0
  1254. moolabs/models/sku_eligibility_row.py +91 -0
  1255. moolabs/models/slack_quote_command_response.py +93 -0
  1256. moolabs/models/snapshot_compute_request.py +104 -0
  1257. moolabs/models/snapshot_compute_response.py +99 -0
  1258. moolabs/models/snapshot_response.py +104 -0
  1259. moolabs/models/sns_confirmation_command_out.py +98 -0
  1260. moolabs/models/sns_confirmation_confirmed_out.py +94 -0
  1261. moolabs/models/sns_confirmation_confirmed_request.py +101 -0
  1262. moolabs/models/sns_confirmation_list_out.py +95 -0
  1263. moolabs/models/sort_direction.py +37 -0
  1264. moolabs/models/sort_order.py +37 -0
  1265. moolabs/models/spend_summary.py +114 -0
  1266. moolabs/models/stage_schema.py +117 -0
  1267. moolabs/models/strategy_create_request.py +130 -0
  1268. moolabs/models/strategy_preview_request.py +87 -0
  1269. moolabs/models/strategy_update_request.py +133 -0
  1270. moolabs/models/stripe_api_key_input.py +87 -0
  1271. moolabs/models/stripe_app.py +150 -0
  1272. moolabs/models/stripe_app_replace_update.py +103 -0
  1273. moolabs/models/stripe_checkout_session_mode.py +36 -0
  1274. moolabs/models/stripe_customer_app_data.py +119 -0
  1275. moolabs/models/stripe_customer_app_data_base.py +89 -0
  1276. moolabs/models/stripe_customer_app_data_create_or_update_item.py +111 -0
  1277. moolabs/models/stripe_customer_portal_session.py +102 -0
  1278. moolabs/models/stripe_tax_config.py +95 -0
  1279. moolabs/models/stripe_webhook_event.py +99 -0
  1280. moolabs/models/stripe_webhook_event_data.py +92 -0
  1281. moolabs/models/stripe_webhook_response.py +118 -0
  1282. moolabs/models/subject.py +122 -0
  1283. moolabs/models/subject_upsert.py +98 -0
  1284. moolabs/models/subscription.py +201 -0
  1285. moolabs/models/subscription_activate_request.py +101 -0
  1286. moolabs/models/subscription_addon.py +167 -0
  1287. moolabs/models/subscription_addon_create.py +106 -0
  1288. moolabs/models/subscription_addon_rate_card.py +94 -0
  1289. moolabs/models/subscription_addon_timeline_segment.py +95 -0
  1290. moolabs/models/subscription_addon_update.py +100 -0
  1291. moolabs/models/subscription_alignment.py +93 -0
  1292. moolabs/models/subscription_bad_request_error_response.py +102 -0
  1293. moolabs/models/subscription_change.py +147 -0
  1294. moolabs/models/subscription_change_response_body.py +97 -0
  1295. moolabs/models/subscription_conflict_error_response.py +102 -0
  1296. moolabs/models/subscription_create.py +147 -0
  1297. moolabs/models/subscription_edit.py +102 -0
  1298. moolabs/models/subscription_edit_operation.py +206 -0
  1299. moolabs/models/subscription_error_extensions.py +95 -0
  1300. moolabs/models/subscription_expanded.py +211 -0
  1301. moolabs/models/subscription_item.py +167 -0
  1302. moolabs/models/subscription_item_included.py +97 -0
  1303. moolabs/models/subscription_item_price_override.py +111 -0
  1304. moolabs/models/subscription_item_price_override_type.py +37 -0
  1305. moolabs/models/subscription_paginated_response.py +101 -0
  1306. moolabs/models/subscription_phase_create.py +109 -0
  1307. moolabs/models/subscription_phase_expanded.py +163 -0
  1308. moolabs/models/subscription_ramp_phase.py +91 -0
  1309. moolabs/models/subscription_status.py +39 -0
  1310. moolabs/models/subscription_timing.py +151 -0
  1311. moolabs/models/subscription_timing_enum.py +37 -0
  1312. moolabs/models/sync_config_request.py +109 -0
  1313. moolabs/models/sync_config_response.py +111 -0
  1314. moolabs/models/sync_invoices_request.py +87 -0
  1315. moolabs/models/sync_run_item.py +102 -0
  1316. moolabs/models/synthetic_request_evidence_row.py +118 -0
  1317. moolabs/models/task_complete_request.py +92 -0
  1318. moolabs/models/task_list_response.py +101 -0
  1319. moolabs/models/task_response.py +130 -0
  1320. moolabs/models/task_update.py +93 -0
  1321. moolabs/models/tax_behavior.py +37 -0
  1322. moolabs/models/tax_config.py +100 -0
  1323. moolabs/models/template_archive_request.py +88 -0
  1324. moolabs/models/template_card_action_request.py +99 -0
  1325. moolabs/models/template_item.py +97 -0
  1326. moolabs/models/template_key_create_request.py +92 -0
  1327. moolabs/models/template_key_list_response.py +116 -0
  1328. moolabs/models/template_key_response.py +126 -0
  1329. moolabs/models/template_meta_response.py +93 -0
  1330. moolabs/models/template_preview_request.py +87 -0
  1331. moolabs/models/template_preview_response.py +101 -0
  1332. moolabs/models/template_test_send_request.py +87 -0
  1333. moolabs/models/template_test_send_response.py +93 -0
  1334. moolabs/models/template_update.py +91 -0
  1335. moolabs/models/template_version_list_response.py +95 -0
  1336. moolabs/models/template_version_response.py +143 -0
  1337. moolabs/models/tenant_config_response.py +95 -0
  1338. moolabs/models/tenant_escalation_config_out.py +97 -0
  1339. moolabs/models/tenant_escalation_config_patch.py +92 -0
  1340. moolabs/models/tenant_provision_request.py +89 -0
  1341. moolabs/models/tenant_segment_entry.py +89 -0
  1342. moolabs/models/tenant_segments_response.py +97 -0
  1343. moolabs/models/tenant_unit_economics.py +97 -0
  1344. moolabs/models/territory_in.py +89 -0
  1345. moolabs/models/test_send_request.py +104 -0
  1346. moolabs/models/test_send_response.py +93 -0
  1347. moolabs/models/tiered_price_mode.py +37 -0
  1348. moolabs/models/tiered_price_with_commitments.py +132 -0
  1349. moolabs/models/tiered_pricing_payload.py +97 -0
  1350. moolabs/models/timestamp_window.py +103 -0
  1351. moolabs/models/top_consumer.py +93 -0
  1352. moolabs/models/topup_defaults_request.py +93 -0
  1353. moolabs/models/total_cost.py +153 -0
  1354. moolabs/models/traces_response.py +95 -0
  1355. moolabs/models/trend_data_point.py +91 -0
  1356. moolabs/models/trend_point.py +116 -0
  1357. moolabs/models/trigger_request.py +87 -0
  1358. moolabs/models/ulidor_external_key.py +153 -0
  1359. moolabs/models/unapplied_cash_response.py +106 -0
  1360. moolabs/models/unavailable_reason.py +66 -0
  1361. moolabs/models/unexpected_problem_response.py +111 -0
  1362. moolabs/models/unified_price_row_input.py +99 -0
  1363. moolabs/models/unified_price_row_out.py +109 -0
  1364. moolabs/models/unit_cost.py +153 -0
  1365. moolabs/models/unit_economics.py +97 -0
  1366. moolabs/models/unit_price.py +104 -0
  1367. moolabs/models/unit_price_with_commitments.py +128 -0
  1368. moolabs/models/unitamount.py +152 -0
  1369. moolabs/models/update_approval_policy_request.py +87 -0
  1370. moolabs/models/update_auto_topup_rule_request.py +102 -0
  1371. moolabs/models/update_credit_pool_request.py +98 -0
  1372. moolabs/models/update_quote_template_request.py +102 -0
  1373. moolabs/models/update_threshold_request.py +97 -0
  1374. moolabs/models/update_wallet_settings_request.py +95 -0
  1375. moolabs/models/update_wallet_thresholds_request.py +92 -0
  1376. moolabs/models/upsert_fx_rate_request.py +92 -0
  1377. moolabs/models/upsert_provider_identity_request.py +115 -0
  1378. moolabs/models/upto.py +152 -0
  1379. moolabs/models/usage_summary_window_size.py +37 -0
  1380. moolabs/models/validate_template_request.py +116 -0
  1381. moolabs/models/validate_template_response.py +89 -0
  1382. moolabs/models/validation_error.py +99 -0
  1383. moolabs/models/validation_error_loc_inner.py +152 -0
  1384. moolabs/models/validation_issue.py +135 -0
  1385. moolabs/models/validation_issue_severity.py +37 -0
  1386. moolabs/models/validation_response.py +105 -0
  1387. moolabs/models/value_recognition_response.py +93 -0
  1388. moolabs/models/variance_decomposition_out.py +117 -0
  1389. moolabs/models/variance_summary_response.py +101 -0
  1390. moolabs/models/verification_status.py +39 -0
  1391. moolabs/models/void_grant_request.py +88 -0
  1392. moolabs/models/void_invoice_action_create.py +93 -0
  1393. moolabs/models/void_invoice_action_create_item.py +93 -0
  1394. moolabs/models/void_invoice_action_input.py +103 -0
  1395. moolabs/models/void_invoice_action_line_override.py +101 -0
  1396. moolabs/models/void_invoice_line_action_create.py +150 -0
  1397. moolabs/models/void_invoice_line_action_create_item.py +150 -0
  1398. moolabs/models/void_invoice_line_discard_action.py +94 -0
  1399. moolabs/models/void_invoice_line_pending_action_create.py +97 -0
  1400. moolabs/models/void_invoice_line_pending_action_create_item.py +97 -0
  1401. moolabs/models/volume_summary.py +114 -0
  1402. moolabs/models/wallet_owner_type.py +43 -0
  1403. moolabs/models/wallet_policy.py +38 -0
  1404. moolabs/models/wallet_response.py +112 -0
  1405. moolabs/models/wallet_state_response.py +105 -0
  1406. moolabs/models/warning_item.py +93 -0
  1407. moolabs/models/webhook_response.py +95 -0
  1408. moolabs/models/webhook_update.py +95 -0
  1409. moolabs/models/window_size.py +39 -0
  1410. moolabs/models/windowed_balance_history.py +105 -0
  1411. moolabs/models/xero_connect_request.py +91 -0
  1412. moolabs/py.typed +0 -0
  1413. moolabs/rest.py +257 -0
  1414. moolabs-0.3.0.dist-info/METADATA +31 -0
  1415. moolabs-0.3.0.dist-info/RECORD +1417 -0
  1416. moolabs-0.3.0.dist-info/WHEEL +5 -0
  1417. 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