codexLB-up 1.25.0b10__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.
- app/__init__.py +10 -0
- app/admin_cli.py +494 -0
- app/cli.py +339 -0
- app/codex_sessions_retag.py +487 -0
- app/core/__init__.py +0 -0
- app/core/audit/__init__.py +0 -0
- app/core/audit/service.py +216 -0
- app/core/audit/types.py +94 -0
- app/core/auth/__init__.py +236 -0
- app/core/auth/api_key_cache.py +65 -0
- app/core/auth/dashboard_access.py +429 -0
- app/core/auth/dashboard_mode.py +171 -0
- app/core/auth/dashboard_session_ttl.py +74 -0
- app/core/auth/dashboard_users_cache.py +120 -0
- app/core/auth/dependencies.py +710 -0
- app/core/auth/external_identity.py +78 -0
- app/core/auth/guardian.py +374 -0
- app/core/auth/models.py +49 -0
- app/core/auth/providers/__init__.py +129 -0
- app/core/auth/providers/oidc.py +838 -0
- app/core/auth/providers/registry.py +118 -0
- app/core/auth/refresh.py +394 -0
- app/core/auth/step_up.py +117 -0
- app/core/auth/totp.py +81 -0
- app/core/balancer/__init__.py +71 -0
- app/core/balancer/logic.py +1612 -0
- app/core/balancer/rendezvous_hash.py +21 -0
- app/core/balancer/types.py +21 -0
- app/core/bootstrap.py +172 -0
- app/core/cache/invalidation.py +426 -0
- app/core/clients/__init__.py +0 -0
- app/core/clients/codex.py +996 -0
- app/core/clients/codex_version.py +187 -0
- app/core/clients/codex_version_snapshot.py +2 -0
- app/core/clients/files.py +534 -0
- app/core/clients/headers.py +29 -0
- app/core/clients/http.py +511 -0
- app/core/clients/model_fetcher.py +276 -0
- app/core/clients/native_egress.py +1304 -0
- app/core/clients/oauth.py +477 -0
- app/core/clients/proxy.py +6089 -0
- app/core/clients/proxy_websocket.py +1411 -0
- app/core/clients/rate_limit_reset_credits.py +412 -0
- app/core/clients/stream_errors.py +12 -0
- app/core/clients/thread_cache_identity.py +347 -0
- app/core/clients/usage.py +467 -0
- app/core/clock.py +170 -0
- app/core/config/__init__.py +0 -0
- app/core/config/background_jobs.py +73 -0
- app/core/config/context_window_overrides.py +166 -0
- app/core/config/dashboard_overrides.py +145 -0
- app/core/config/inheritable.py +47 -0
- app/core/config/key_fingerprint.py +115 -0
- app/core/config/settings.py +872 -0
- app/core/config/settings_cache.py +100 -0
- app/core/config/spool_retention.py +217 -0
- app/core/config/tiers.py +174 -0
- app/core/conversation_archive.py +568 -0
- app/core/crypto.py +37 -0
- app/core/draining.py +11 -0
- app/core/errors.py +365 -0
- app/core/exceptions.py +155 -0
- app/core/handlers/__init__.py +3 -0
- app/core/handlers/exceptions.py +478 -0
- app/core/http_protocol.py +176 -0
- app/core/http_protocol_httptools.py +204 -0
- app/core/ingress_limits.py +17 -0
- app/core/metadata_store.py +18 -0
- app/core/metrics/__init__.py +0 -0
- app/core/metrics/middleware.py +74 -0
- app/core/metrics/prometheus.py +604 -0
- app/core/middleware/__init__.py +25 -0
- app/core/middleware/api_firewall.py +111 -0
- app/core/middleware/app_version.py +37 -0
- app/core/middleware/dashboard_auth_proxy.py +55 -0
- app/core/middleware/dashboard_csrf.py +177 -0
- app/core/middleware/dashboard_gzip.py +41 -0
- app/core/middleware/dashboard_overrides.py +59 -0
- app/core/middleware/firewall_cache.py +91 -0
- app/core/middleware/inflight.py +126 -0
- app/core/middleware/multipart_content_encoding.py +129 -0
- app/core/middleware/path_rewrite.py +148 -0
- app/core/middleware/request_body_limit.py +196 -0
- app/core/middleware/request_decompression.py +209 -0
- app/core/middleware/request_id.py +72 -0
- app/core/middleware/required_capability_http.py +116 -0
- app/core/middleware/trusted_proxy_headers.py +36 -0
- app/core/multipart.py +237 -0
- app/core/multipart_fields.py +91 -0
- app/core/openai/__init__.py +0 -0
- app/core/openai/chat_requests.py +493 -0
- app/core/openai/chat_responses.py +931 -0
- app/core/openai/contracts.py +46 -0
- app/core/openai/exceptions.py +24 -0
- app/core/openai/host_models.py +14 -0
- app/core/openai/images.py +413 -0
- app/core/openai/message_coercion.py +376 -0
- app/core/openai/model_refresh_scheduler.py +423 -0
- app/core/openai/model_registry.py +997 -0
- app/core/openai/model_registry_store.py +548 -0
- app/core/openai/models.py +172 -0
- app/core/openai/parsing.py +97 -0
- app/core/openai/requests.py +1980 -0
- app/core/openai/strict_schema.py +199 -0
- app/core/openai/tool_call_safety.py +99 -0
- app/core/openai/v1_requests.py +187 -0
- app/core/plan_types.py +90 -0
- app/core/prestop.py +202 -0
- app/core/rate_limiter/__init__.py +0 -0
- app/core/rate_limiter/db_rate_limiter.py +215 -0
- app/core/request_locality.py +473 -0
- app/core/resilience/__init__.py +0 -0
- app/core/resilience/backpressure.py +57 -0
- app/core/resilience/bulkhead.py +202 -0
- app/core/resilience/circuit_breaker.py +150 -0
- app/core/resilience/degradation.py +55 -0
- app/core/resilience/loop_lag_monitor.py +60 -0
- app/core/resilience/memory_monitor.py +137 -0
- app/core/resilience/network_recovery.py +256 -0
- app/core/resilience/overload.py +98 -0
- app/core/resilience/toggles.py +114 -0
- app/core/retention/__init__.py +0 -0
- app/core/retention/job.py +240 -0
- app/core/retention/scheduler.py +69 -0
- app/core/runtime_logging.py +601 -0
- app/core/scheduling/__init__.py +0 -0
- app/core/scheduling/leader_election.py +993 -0
- app/core/scheduling/leader_election_handle.py +25 -0
- app/core/server.py +140 -0
- app/core/shutdown.py +326 -0
- app/core/socket_peer.py +22 -0
- app/core/startup.py +41 -0
- app/core/timeout_invariants.py +347 -0
- app/core/tracing/__init__.py +0 -0
- app/core/tracing/otel.py +207 -0
- app/core/types.py +6 -0
- app/core/upstream_proxy/__init__.py +13 -0
- app/core/upstream_proxy/cache.py +130 -0
- app/core/upstream_proxy/resolver.py +189 -0
- app/core/upstream_proxy/types.py +104 -0
- app/core/usage/__init__.py +407 -0
- app/core/usage/depletion.py +109 -0
- app/core/usage/live_hub.py +41 -0
- app/core/usage/live_snapshots.py +174 -0
- app/core/usage/logs.py +212 -0
- app/core/usage/metadata_scheduler.py +118 -0
- app/core/usage/models.py +54 -0
- app/core/usage/pricing.py +576 -0
- app/core/usage/pricing_catalog.py +241 -0
- app/core/usage/pricing_snapshot.json +821 -0
- app/core/usage/quota.py +133 -0
- app/core/usage/refresh_policy.py +21 -0
- app/core/usage/refresh_scheduler.py +713 -0
- app/core/usage/reset_credits_refresh_scheduler.py +435 -0
- app/core/usage/types.py +168 -0
- app/core/utils/__init__.py +0 -0
- app/core/utils/json_guards.py +18 -0
- app/core/utils/locks.py +33 -0
- app/core/utils/masking.py +20 -0
- app/core/utils/proxy_env.py +104 -0
- app/core/utils/request_id.py +65 -0
- app/core/utils/retry.py +77 -0
- app/core/utils/shared_future.py +172 -0
- app/core/utils/sse.py +286 -0
- app/core/utils/time.py +24 -0
- app/db/__init__.py +0 -0
- app/db/account_identity_lock.py +56 -0
- app/db/alembic/__init__.py +1 -0
- app/db/alembic/env.py +70 -0
- app/db/alembic/revision_ids.py +43 -0
- app/db/alembic/versions/20260213_000000_base_schema.py +248 -0
- app/db/alembic/versions/20260213_000100_normalize_account_plan_types.py +45 -0
- app/db/alembic/versions/20260213_000200_add_request_logs_reasoning_effort.py +45 -0
- app/db/alembic/versions/20260213_000300_add_accounts_reset_at.py +45 -0
- app/db/alembic/versions/20260213_000400_add_accounts_chatgpt_account_id.py +45 -0
- app/db/alembic/versions/20260213_000500_add_dashboard_settings.py +70 -0
- app/db/alembic/versions/20260213_000600_add_dashboard_settings_totp.py +64 -0
- app/db/alembic/versions/20260213_000700_add_dashboard_settings_password.py +59 -0
- app/db/alembic/versions/20260213_000800_add_api_keys.py +87 -0
- app/db/alembic/versions/20260214_000000_add_api_key_limits.py +143 -0
- app/db/alembic/versions/20260215_000000_add_idx_logs_requested_at.py +38 -0
- app/db/alembic/versions/20260218_000000_add_api_key_usage_reservations.py +117 -0
- app/db/alembic/versions/20260218_000100_add_import_without_overwrite_and_drop_accounts_email_unique.py +198 -0
- app/db/alembic/versions/20260225_000000_add_dashboard_settings_routing_strategy.py +53 -0
- app/db/alembic/versions/20260228_020000_align_api_key_limit_enum_types.py +104 -0
- app/db/alembic/versions/20260228_030000_add_api_firewall_allowlist.py +42 -0
- app/db/alembic/versions/20260306_000000_add_request_logs_service_tier.py +45 -0
- app/db/alembic/versions/20260307_000000_add_api_key_enforcement_fields.py +56 -0
- app/db/alembic/versions/20260308_000000_add_sqlite_performance_indexes.py +41 -0
- app/db/alembic/versions/20260309_000000_add_additional_usage_history.py +71 -0
- app/db/alembic/versions/20260309_000000_request_logs_nullable_account_id.py +28 -0
- app/db/alembic/versions/20260310_000000_add_request_logs_transport.py +45 -0
- app/db/alembic/versions/20260310_000000_fix_postgresql_enum_value_casing.py +79 -0
- app/db/alembic/versions/20260310_120000_add_sticky_session_kinds_and_affinity_ttl.py +110 -0
- app/db/alembic/versions/20260312_000000_add_additional_usage_quota_key.py +193 -0
- app/db/alembic/versions/20260312_000000_split_sticky_sessions_primary_key_by_kind.py +96 -0
- app/db/alembic/versions/20260312_010000_merge_additional_usage_and_sticky_session_heads.py +25 -0
- app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_feature_heads.py +25 -0
- app/db/alembic/versions/20260312_020000_merge_request_logs_transport_and_sticky_heads.py +25 -0
- app/db/alembic/versions/20260312_120000_add_dashboard_upstream_stream_transport.py +62 -0
- app/db/alembic/versions/20260313_024500_merge_request_log_transport_heads.py +27 -0
- app/db/alembic/versions/20260319_100937_increase_prompt_cache_ttl_to_1800s.py +73 -0
- app/db/alembic/versions/20260319_111500_merge_prompt_cache_ttl_and_upstream_transport_heads.py +25 -0
- app/db/alembic/versions/20260319_183000_normalize_sqlite_account_status_casing.py +48 -0
- app/db/alembic/versions/20260320_000000_add_request_log_requested_actual_tiers.py +51 -0
- app/db/alembic/versions/20260321_190000_tighten_dashboard_db_indexes.py +64 -0
- app/db/alembic/versions/20260321_210000_merge_request_log_tiers_and_dashboard_index_heads.py +25 -0
- app/db/alembic/versions/20260325_000000_add_request_log_cost.py +193 -0
- app/db/alembic/versions/20260328_000000_add_rate_limit_attempts.py +48 -0
- app/db/alembic/versions/20260328_120000_add_request_log_ttft.py +37 -0
- app/db/alembic/versions/20260328_130000_add_audit_logs_table.py +45 -0
- app/db/alembic/versions/20260328_140000_add_scheduler_leader_table.py +39 -0
- app/db/alembic/versions/20260330_000000_add_cache_locality_settings.py +69 -0
- app/db/alembic/versions/20260330_010000_merge_scheduler_leader_and_cache_locality_heads.py +25 -0
- app/db/alembic/versions/20260330_020000_add_bridge_ring_members.py +46 -0
- app/db/alembic/versions/20260401_000000_add_api_key_enforced_service_tier.py +52 -0
- app/db/alembic/versions/20260401_000000_add_cache_invalidation.py +41 -0
- app/db/alembic/versions/20260401_020000_merge_cache_invalidation_and_api_key_service_tier_heads.py +22 -0
- app/db/alembic/versions/20260402_000000_switch_dashboard_routing_default_to_capacity_weighted.py +66 -0
- app/db/alembic/versions/20260403_000000_add_credit_api_key_limit_values.py +44 -0
- app/db/alembic/versions/20260406_000000_add_api_key_account_assignments.py +57 -0
- app/db/alembic/versions/20260406_010000_add_api_key_assignment_scope_flag.py +75 -0
- app/db/alembic/versions/20260407_000000_add_bridge_gateway_safe_mode.py +51 -0
- app/db/alembic/versions/20260407_010000_merge_api_key_assignment_and_bridge_gateway_heads.py +25 -0
- app/db/alembic/versions/20260408_000000_switch_import_without_overwrite_default_to_true.py +73 -0
- app/db/alembic/versions/20260408_010000_merge_import_without_overwrite_and_assignment_heads.py +25 -0
- app/db/alembic/versions/20260409_000000_add_http_bridge_sessions.py +130 -0
- app/db/alembic/versions/20260409_000000_switch_sticky_threads_and_prefer_earlier_reset_defaults_to_true.py +89 -0
- app/db/alembic/versions/20260409_010000_add_dashboard_settings_bootstrap_token.py +57 -0
- app/db/alembic/versions/20260409_010000_merge_http_bridge_and_import_without_overwrite_heads.py +25 -0
- app/db/alembic/versions/20260409_020000_fix_http_bridge_last_seen_index.py +46 -0
- app/db/alembic/versions/20260410_000000_backfill_pristine_dashboard_settings_defaults.py +77 -0
- app/db/alembic/versions/20260410_030000_restore_import_without_overwrite_default_true.py +73 -0
- app/db/alembic/versions/20260410_040000_merge_dashboard_defaults_and_import_default_heads.py +25 -0
- app/db/alembic/versions/20260413_000000_add_accounts_blocked_at.py +44 -0
- app/db/alembic/versions/20260415_160000_add_automations_tables.py +189 -0
- app/db/alembic/versions/20260415_160000_add_request_logs_response_lookup_index.py +67 -0
- app/db/alembic/versions/20260417_000000_add_request_log_plan_type.py +44 -0
- app/db/alembic/versions/20260418_190000_add_automation_runs_cycle_key.py +87 -0
- app/db/alembic/versions/20260419_000000_add_automation_run_cycle_metadata.py +56 -0
- app/db/alembic/versions/20260419_020000_add_automation_run_cycles_snapshot_tables.py +342 -0
- app/db/alembic/versions/20260421_000000_add_dashboard_session_ttl_setting.py +51 -0
- app/db/alembic/versions/20260421_120000_merge_request_log_lookup_and_plan_type_heads.py +25 -0
- app/db/alembic/versions/20260421_130000_merge_automation_and_request_log_heads.py +25 -0
- app/db/alembic/versions/20260422_103000_normalize_legacy_automation_run_cycle_keys.py +648 -0
- app/db/alembic/versions/20260423_120000_add_api_key_limit_reset_at_index.py +43 -0
- app/db/alembic/versions/20260424_000000_merge_dashboard_session_ttl_and_request_log_heads.py +25 -0
- app/db/alembic/versions/20260426_000000_add_dashboard_relative_availability_settings.py +68 -0
- app/db/alembic/versions/20260427_130000_add_warmup_model_and_request_kind.py +75 -0
- app/db/alembic/versions/20260506_000000_add_account_routing_policy.py +37 -0
- app/db/alembic/versions/20260509_000000_add_api_key_traffic_class.py +37 -0
- app/db/alembic/versions/20260509_010000_add_additional_quota_routing_policies.py +37 -0
- app/db/alembic/versions/20260513_000000_add_accounts_alias.py +44 -0
- app/db/alembic/versions/20260513_000000_add_api_key_apply_to_codex_model.py +59 -0
- app/db/alembic/versions/20260514_000000_add_request_logs_api_key_time_index.py +43 -0
- app/db/alembic/versions/20260515_000000_add_prefer_earlier_reset_window.py +47 -0
- app/db/alembic/versions/20260515_000000_soft_delete_request_logs_on_account_delete.py +83 -0
- app/db/alembic/versions/20260515_020000_add_split_sticky_budget_thresholds.py +62 -0
- app/db/alembic/versions/20260516_000000_add_sqlite_hot_path_indexes.py +75 -0
- app/db/alembic/versions/20260517_000000_merge_sqlite_hot_path_and_request_log_delete_heads.py +24 -0
- app/db/alembic/versions/20260518_000000_add_http_bridge_durable_input_prefix.py +44 -0
- app/db/alembic/versions/20260518_010000_merge_http_bridge_and_sqlite_recovery_heads.py +25 -0
- app/db/alembic/versions/20260518_120000_add_dashboard_guest_access.py +57 -0
- app/db/alembic/versions/20260520_000000_merge_api_key_and_http_bridge_heads.py +26 -0
- app/db/alembic/versions/20260520_010000_add_request_logs_api_key_account_index.py +45 -0
- app/db/alembic/versions/20260520_010000_merge_automation_and_api_key_heads.py +26 -0
- app/db/alembic/versions/20260520_030000_add_quota_planner.py +194 -0
- app/db/alembic/versions/20260520_040000_merge_quota_planner_and_request_log_index_heads.py +24 -0
- app/db/alembic/versions/20260521_000000_add_account_security_work_authorized.py +46 -0
- app/db/alembic/versions/20260521_000000_merge_dashboard_guest_and_api_key_heads.py +26 -0
- app/db/alembic/versions/20260521_000000_merge_prefer_reset_window_head.py +26 -0
- app/db/alembic/versions/20260521_000000_merge_warmup_and_request_log_index_heads.py +25 -0
- app/db/alembic/versions/20260522_000000_add_limit_warmup_trigger.py +195 -0
- app/db/alembic/versions/20260524_000000_merge_accounts_alias_and_reset_window_heads.py +24 -0
- app/db/alembic/versions/20260525_000000_add_usage_raw_window_latest_index.py +31 -0
- app/db/alembic/versions/20260526_000000_add_request_log_failure_metadata.py +100 -0
- app/db/alembic/versions/20260531_000000_add_account_workspace_identity.py +39 -0
- app/db/alembic/versions/20260531_000000_add_single_account_routing.py +42 -0
- app/db/alembic/versions/20260601_000000_merge_relative_availability_and_usage_raw_heads.py +24 -0
- app/db/alembic/versions/20260601_010000_add_api_key_usage_sections.py +58 -0
- app/db/alembic/versions/20260601_010000_add_routing_policy_persistence.py +67 -0
- app/db/alembic/versions/20260601_010000_merge_security_work_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_010000_merge_split_sticky_and_relative_usage_heads.py +25 -0
- app/db/alembic/versions/20260601_010000_merge_warmup_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_additional_quota_routing_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_quota_planner_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_reset_window_and_relative_availability_heads.py +25 -0
- app/db/alembic/versions/20260601_020000_merge_routing_policy_and_split_threshold_heads.py +24 -0
- app/db/alembic/versions/20260601_020000_merge_warmup_and_request_log_failure_heads.py +25 -0
- app/db/alembic/versions/20260601_030000_merge_single_account_and_routing_quota_heads.py +29 -0
- app/db/alembic/versions/20260601_040000_merge_security_work_and_routing_consolidation_heads.py +25 -0
- app/db/alembic/versions/20260601_050000_merge_quota_planner_and_routing_consolidation_heads.py +25 -0
- app/db/alembic/versions/20260602_050000_add_upstream_proxy_routing.py +164 -0
- app/db/alembic/versions/20260602_060000_merge_account_workspace_and_failure_heads.py +25 -0
- app/db/alembic/versions/20260602_070000_add_request_log_useragent_fields.py +86 -0
- app/db/alembic/versions/20260602_070000_merge_quota_planner_and_account_workspace_heads.py +25 -0
- app/db/alembic/versions/20260602_080000_merge_upstream_proxy_and_quota_planner_heads.py +25 -0
- app/db/alembic/versions/20260602_080000_merge_useragent_and_upstream_proxy_heads.py +24 -0
- app/db/alembic/versions/20260603_000000_add_weekly_pace_working_days.py +52 -0
- app/db/alembic/versions/20260603_000000_free_account_monthly_window.py +60 -0
- app/db/alembic/versions/20260604_000000_add_reauth_required_account_status.py +159 -0
- app/db/alembic/versions/20260604_010000_merge_automations_and_reauth_status_heads.py +25 -0
- app/db/alembic/versions/20260604_010000_merge_dashboard_guest_and_reauth_status_heads.py +25 -0
- app/db/alembic/versions/20260604_010000_merge_useragent_and_reauth_heads.py +24 -0
- app/db/alembic/versions/20260607_000000_merge_weekly_monthly_useragent_heads.py +28 -0
- app/db/alembic/versions/20260608_000000_add_hide_upstream_quota_from_api_keys.py +50 -0
- app/db/alembic/versions/20260611_000000_add_request_log_archive_request_id.py +47 -0
- app/db/alembic/versions/20260611_000000_merge_automations_and_weekly_monthly_useragent_heads.py +26 -0
- app/db/alembic/versions/20260611_000000_merge_dashboard_guest_and_weekly_useragent_heads.py +26 -0
- app/db/alembic/versions/20260611_010000_add_request_log_client_ip.py +57 -0
- app/db/alembic/versions/20260611_020000_merge_request_log_archive_and_client_ip_heads.py +26 -0
- app/db/alembic/versions/20260611_030000_merge_dashboard_guest_and_request_log_client_ip_heads.py +26 -0
- app/db/alembic/versions/20260613_000000_add_accounts_codex_installation_id.py +57 -0
- app/db/alembic/versions/20260613_010000_merge_installation_id_and_dashboard_weekly_heads.py +26 -0
- app/db/alembic/versions/20260615_000000_merge_automations_and_dashboard_guest_heads.py +26 -0
- app/db/alembic/versions/20260623_000000_add_limit_warmup_exhausted_threshold.py +51 -0
- app/db/alembic/versions/20260626_000000_add_http_downstream_transport_policy.py +46 -0
- app/db/alembic/versions/20260626_010000_add_request_logs_upstream_transport.py +31 -0
- app/db/alembic/versions/20260629_000000_add_dashboard_query_hot_path_indexes.py +53 -0
- app/db/alembic/versions/20260630_000000_add_limit_warmup_staggered_idle.py +47 -0
- app/db/alembic/versions/20260630_000000_merge_request_log_client_ip_and_dashboard_hot_path_heads.py +26 -0
- app/db/alembic/versions/20260630_010000_merge_warmup_and_request_log_dashboard_heads.py +26 -0
- app/db/alembic/versions/20260630_020000_merge_automations_and_main_heads.py +26 -0
- app/db/alembic/versions/20260630_020000_merge_warmup_threshold_and_main_heads.py +26 -0
- app/db/alembic/versions/20260630_030000_add_automation_run_model_snapshot.py +59 -0
- app/db/alembic/versions/20260630_040000_merge_automation_snapshot_and_warmup_threshold_heads.py +26 -0
- app/db/alembic/versions/20260630_050000_add_automation_run_prompt_snapshot.py +52 -0
- app/db/alembic/versions/20260701_000000_add_weekly_pace_smoothing_minutes.py +51 -0
- app/db/alembic/versions/20260702_000000_add_model_sources.py +176 -0
- app/db/alembic/versions/20260705_000000_add_model_source_audio_transcriptions.py +50 -0
- app/db/alembic/versions/20260705_000000_harden_dashboard_session_ttl.py +62 -0
- app/db/alembic/versions/20260706_000000_add_model_source_audio_per_minute.py +43 -0
- app/db/alembic/versions/20260707_000000_merge_model_sources_and_session_ttl.py +24 -0
- app/db/alembic/versions/20260707_010000_merge_automations_and_model_sources_heads.py +24 -0
- app/db/alembic/versions/20260707_020000_add_automation_job_account_scope.py +125 -0
- app/db/alembic/versions/20260709_000000_add_ttft_phase_observability.py +66 -0
- app/db/alembic/versions/20260711_000000_add_dashboard_prohibit_fast_mode.py +42 -0
- app/db/alembic/versions/20260711_010000_add_account_chatgpt_user_id.py +40 -0
- app/db/alembic/versions/20260711_020000_add_dashboard_account_concurrency_caps.py +71 -0
- app/db/alembic/versions/20260711_030000_add_limit_warmup_idle_threshold.py +51 -0
- app/db/alembic/versions/20260712_010000_add_account_usage_rollups.py +56 -0
- app/db/alembic/versions/20260712_020000_add_api_key_usage_rollups.py +53 -0
- app/db/alembic/versions/20260713_020000_add_model_registry_snapshot.py +42 -0
- app/db/alembic/versions/20260713_040000_add_account_refresh_claims.py +42 -0
- app/db/alembic/versions/20260713_040000_add_replica_guardrails.py +64 -0
- app/db/alembic/versions/20260713_070000_add_reset_credit_redeem_tables.py +58 -0
- app/db/alembic/versions/20260714_000000_add_oauth_flow_states.py +63 -0
- app/db/alembic/versions/20260715_000000_add_request_log_queue_latency.py +44 -0
- app/db/alembic/versions/20260716_000000_add_oauth_device_flow_slots.py +41 -0
- app/db/alembic/versions/20260716_010000_add_dashboard_retention_settings.py +48 -0
- app/db/alembic/versions/20260716_010000_add_reset_credit_display_settings.py +73 -0
- app/db/alembic/versions/20260717_000000_add_http_bridge_retry_circuits.py +47 -0
- app/db/alembic/versions/20260717_000000_merge_retention_and_reset_credit_display_heads.py +26 -0
- app/db/alembic/versions/20260717_000000_optimize_dashboard_hot_path_indexes.py +162 -0
- app/db/alembic/versions/20260717_000001_merge_retry_circuits_and_dashboard_indexes.py +22 -0
- app/db/alembic/versions/20260720_000000_add_request_log_conversation_id.py +57 -0
- app/db/alembic/versions/20260722_000000_backfill_request_log_useragent_families.py +40 -0
- app/db/alembic/versions/20260724_000000_add_request_usage_time_rollups.py +176 -0
- app/db/alembic/versions/20260724_000000_merge_request_log_schema_heads.py +33 -0
- app/db/alembic/versions/20260725_000000_add_http_bridge_pending_tool_calls.py +43 -0
- app/db/alembic/versions/20260726_000000_add_account_plan_downgrade_observations.py +44 -0
- app/db/alembic/versions/20260726_000000_repair_request_usage_rollups_after_merge.py +37 -0
- app/db/alembic/versions/20260727_000000_add_sticky_session_continuity_abandoned_at.py +45 -0
- app/db/alembic/versions/20260728_000000_merge_pending_tool_calls_and_rollup_repair_heads.py +31 -0
- app/db/alembic/versions/20260730_000000_add_api_key_fair_share_threshold.py +44 -0
- app/db/alembic/versions/20260730_000000_add_http_bridge_recovery_attempts.py +60 -0
- app/db/alembic/versions/20260731_000000_add_capability_lineage_markers.py +38 -0
- app/db/alembic/versions/20260803_000000_merge_http_bridge_recovery_and_capability_lineage_heads.py +30 -0
- app/db/alembic/versions/20260804_000000_add_http_bridge_operations.py +63 -0
- app/db/alembic/versions/20260804_000001_add_global_http_bridge_operation_fingerprint.py +53 -0
- app/db/alembic/versions/20260804_230000_add_request_log_connection_request_kind.py +43 -0
- app/db/alembic/versions/20260805_000000_add_http_bridge_operation_spool.py +75 -0
- app/db/alembic/versions/20260805_000001_finalize_http_bridge_operation_spool.py +65 -0
- app/db/alembic/versions/20260806_000000_add_additional_usage_alias_probe_indexes.py +84 -0
- app/db/alembic/versions/20260806_000000_add_anonymous_telemetry.py +49 -0
- app/db/alembic/versions/20260806_010000_add_conversation_presence_rollup.py +72 -0
- app/db/alembic/versions/20260806_020000_add_usage_history_bulk_covering_indexes.py +99 -0
- app/db/alembic/versions/20260806_030000_add_api_key_allowed_reasoning_efforts.py +65 -0
- app/db/alembic/versions/20260806_120000_add_http_bridge_owner_process_epoch.py +57 -0
- app/db/alembic/versions/20260807_000000_merge_http_bridge_operation_spool_and_latest_main.py +24 -0
- app/db/alembic/versions/20260808_000000_tune_usage_history_autovacuum.py +54 -0
- app/db/alembic/versions/20260810_000000_add_http_bridge_recovery_dispatch_count.py +44 -0
- app/db/alembic/versions/20260811_000000_add_hourly_rollup_cancelled_count.py +92 -0
- app/db/alembic/versions/20260812_000000_merge_recovery_dispatch_and_hourly_cancelled_heads.py +25 -0
- app/db/alembic/versions/20260812_120000_add_sticky_abandonment_scope.py +43 -0
- app/db/alembic/versions/20260813_000000_add_file_account_pins.py +40 -0
- app/db/alembic/versions/20260816_000000_add_account_pending_deletion.py +81 -0
- app/db/alembic/versions/20260816_000000_add_model_source_embeddings.py +50 -0
- app/db/alembic/versions/20260821_000000_add_retry_circuit_admission_generation.py +49 -0
- app/db/alembic/versions/20260826_000000_add_http_bridge_event_chunks.py +107 -0
- app/db/alembic/versions/20260828_000000_add_accounts_chatgpt_identity_index.py +55 -0
- app/db/alembic/versions/20260830_000000_add_quota_warmup_claim_expiry.py +83 -0
- app/db/alembic/versions/20260908_000000_add_guest_session_generation.py +37 -0
- app/db/alembic/versions/20260908_000000_add_subscription_overflow.py +140 -0
- app/db/alembic/versions/20260908_000000_replace_upstream_stream_transport_default_sentinel.py +50 -0
- app/db/alembic/versions/20260908_020000_merge_overflow_transport_heads.py +27 -0
- app/db/alembic/versions/20260909_000000_add_dashboard_roles.py +72 -0
- app/db/alembic/versions/20260909_010000_add_dashboard_users.py +183 -0
- app/db/alembic/versions/20260909_020000_reproject_compat_admin_credentials.py +105 -0
- app/db/alembic/versions/20260909_030000_add_audit_actor_columns.py +103 -0
- app/db/alembic/versions/20260909_030000_dashboard_resilience_toggle_settings.py +63 -0
- app/db/alembic/versions/20260909_040000_add_dashboard_user_invites.py +54 -0
- app/db/alembic/versions/20260909_040000_dashboard_timeout_settings.py +68 -0
- app/db/alembic/versions/20260909_050000_dashboard_routing_overload_settings.py +65 -0
- app/db/alembic/versions/20260909_060000_add_report_rollup.py +74 -0
- app/db/alembic/versions/20260909_070000_automation_run_claim_budget.py +50 -0
- app/db/alembic/versions/20260909_080000_dashboard_stream_bridge_budgets.py +62 -0
- app/db/alembic/versions/20260909_090000_dashboard_background_job_toggles.py +63 -0
- app/db/alembic/versions/20260909_100000_dashboard_codex_prewarm.py +49 -0
- app/db/alembic/versions/20260909_110000_model_context_window_overrides.py +45 -0
- app/db/alembic/versions/20260909_120000_dashboard_conversation_archive.py +48 -0
- app/db/alembic/versions/20260909_130000_add_request_logs_live_facet_indexes.py +96 -0
- app/db/alembic/versions/20260910_000000_add_totp_required_for_admin_role.py +44 -0
- app/db/alembic/versions/20260910_000000_request_logs_missing_cost_index.py +52 -0
- app/db/alembic/versions/20260910_010000_add_dashboard_auth_providers.py +134 -0
- app/db/alembic/versions/20260910_010000_dashboard_spool_retention.py +52 -0
- app/db/alembic/versions/20260910_020000_add_dashboard_role_mappings.py +74 -0
- app/db/alembic/versions/20260910_143000_request_log_affinity.py +28 -0
- app/db/alembic/versions/20260910_180000_merge_affinity_guest_heads.py +17 -0
- app/db/alembic/versions/20260910_200000_merge_affinity_identity_heads.py +17 -0
- app/db/alembic/versions/20260910_220000_merge_affinity_invite_heads.py +17 -0
- app/db/alembic/versions/20260911_000000_model_source_pins_kind_expires_index.py +43 -0
- app/db/alembic/versions/20260911_010000_merge_pin_index_and_affinity_heads.py +17 -0
- app/db/alembic/versions/20260911_020000_add_http_bridge_terminal_append_phase.py +54 -0
- app/db/alembic/versions/20260911_030000_add_local_login_policy.py +48 -0
- app/db/alembic/versions/20260911_040000_add_thread_cache_identity_mode.py +48 -0
- app/db/alembic/versions/20260911_060000_add_bridge_session_continuity_abandonment.py +40 -0
- app/db/alembic/versions/20260912_000000_merge_thread_cache_and_bridge_retirement_heads.py +22 -0
- app/db/alembic/versions/20260912_010000_drop_legacy_dashboard_credentials.py +140 -0
- app/db/alembic/versions/20260913_000000_add_oidc_provider_flow.py +134 -0
- app/db/alembic/versions/20260914_000000_add_scim_tokens.py +78 -0
- app/db/alembic/versions/20260914_000000_drop_subscription_overflow_schema.py +132 -0
- app/db/alembic/versions/20260918_000000_merge_scim_and_overflow_heads.py +17 -0
- app/db/alembic/versions/__init__.py +1 -0
- app/db/backup.py +64 -0
- app/db/migrate.py +1249 -0
- app/db/migration_lock.py +160 -0
- app/db/migration_url.py +18 -0
- app/db/models.py +3100 -0
- app/db/recover.py +244 -0
- app/db/session.py +1295 -0
- app/db/snapshot.py +46 -0
- app/db/sqlite_lock_retry.py +190 -0
- app/db/sqlite_utils.py +549 -0
- app/dependencies.py +462 -0
- app/main.py +1240 -0
- app/modules/__init__.py +0 -0
- app/modules/accounts/__init__.py +0 -0
- app/modules/accounts/api.py +426 -0
- app/modules/accounts/auth_manager.py +1439 -0
- app/modules/accounts/background_repository.py +160 -0
- app/modules/accounts/deletion.py +540 -0
- app/modules/accounts/mappers.py +654 -0
- app/modules/accounts/refresh_claims.py +352 -0
- app/modules/accounts/repository.py +1679 -0
- app/modules/accounts/schemas.py +308 -0
- app/modules/accounts/service.py +809 -0
- app/modules/accounts/usage_rollup.py +486 -0
- app/modules/accounts/usage_rollup_scheduler.py +75 -0
- app/modules/accounts/usage_time_rollup.py +1037 -0
- app/modules/accounts/usage_time_rollup_read.py +522 -0
- app/modules/api_keys/__init__.py +1 -0
- app/modules/api_keys/api.py +344 -0
- app/modules/api_keys/last_used_coalescer.py +216 -0
- app/modules/api_keys/limit_windows.py +41 -0
- app/modules/api_keys/repository.py +1200 -0
- app/modules/api_keys/reset_scheduler.py +85 -0
- app/modules/api_keys/schemas.py +135 -0
- app/modules/api_keys/service.py +2123 -0
- app/modules/audit/__init__.py +0 -0
- app/modules/audit/api.py +88 -0
- app/modules/audit/repository.py +74 -0
- app/modules/audit/schemas.py +33 -0
- app/modules/audit/service.py +71 -0
- app/modules/auth_providers/__init__.py +0 -0
- app/modules/auth_providers/api.py +127 -0
- app/modules/auth_providers/config.py +343 -0
- app/modules/auth_providers/repository.py +101 -0
- app/modules/auth_providers/schemas.py +88 -0
- app/modules/auth_providers/seed.py +105 -0
- app/modules/auth_providers/service.py +289 -0
- app/modules/automations/__init__.py +1 -0
- app/modules/automations/api.py +369 -0
- app/modules/automations/repository.py +1896 -0
- app/modules/automations/scheduler.py +107 -0
- app/modules/automations/schemas.py +158 -0
- app/modules/automations/service.py +2548 -0
- app/modules/cache_isolation_probe/__init__.py +0 -0
- app/modules/cache_isolation_probe/api.py +188 -0
- app/modules/cache_isolation_probe/prefix.py +144 -0
- app/modules/cache_isolation_probe/schemas.py +94 -0
- app/modules/cache_isolation_probe/sender.py +229 -0
- app/modules/cache_isolation_probe/service.py +525 -0
- app/modules/conversation_archive/__init__.py +1 -0
- app/modules/conversation_archive/api.py +101 -0
- app/modules/conversation_archive/schemas.py +28 -0
- app/modules/conversation_archive/service.py +165 -0
- app/modules/dashboard/__init__.py +1 -0
- app/modules/dashboard/api.py +106 -0
- app/modules/dashboard/builders.py +107 -0
- app/modules/dashboard/repository.py +304 -0
- app/modules/dashboard/schemas.py +148 -0
- app/modules/dashboard/service.py +534 -0
- app/modules/dashboard/timeframes.py +19 -0
- app/modules/dashboard/weekly_pace.py +617 -0
- app/modules/dashboard_auth/__init__.py +1 -0
- app/modules/dashboard_auth/api.py +1344 -0
- app/modules/dashboard_auth/oidc_api.py +753 -0
- app/modules/dashboard_auth/oidc_flows.py +330 -0
- app/modules/dashboard_auth/repository.py +452 -0
- app/modules/dashboard_auth/schemas.py +232 -0
- app/modules/dashboard_auth/service.py +1384 -0
- app/modules/dashboard_roles/__init__.py +0 -0
- app/modules/dashboard_roles/api.py +64 -0
- app/modules/dashboard_roles/repository.py +54 -0
- app/modules/dashboard_roles/schemas.py +29 -0
- app/modules/dashboard_roles/seed.py +80 -0
- app/modules/dashboard_roles/service.py +142 -0
- app/modules/dashboard_users/__init__.py +0 -0
- app/modules/dashboard_users/api.py +278 -0
- app/modules/dashboard_users/break_glass.py +169 -0
- app/modules/dashboard_users/credentials.py +18 -0
- app/modules/dashboard_users/identity_resolver.py +665 -0
- app/modules/dashboard_users/repository.py +725 -0
- app/modules/dashboard_users/schemas.py +117 -0
- app/modules/dashboard_users/service.py +1054 -0
- app/modules/firewall/__init__.py +1 -0
- app/modules/firewall/api.py +78 -0
- app/modules/firewall/repository.py +51 -0
- app/modules/firewall/schemas.py +28 -0
- app/modules/firewall/service.py +93 -0
- app/modules/fleet/__init__.py +1 -0
- app/modules/fleet/api.py +177 -0
- app/modules/fleet/mappers.py +56 -0
- app/modules/fleet/observability.py +372 -0
- app/modules/fleet/schemas.py +119 -0
- app/modules/health/__init__.py +0 -0
- app/modules/health/api.py +246 -0
- app/modules/health/schemas.py +27 -0
- app/modules/limit_warmup/__init__.py +1 -0
- app/modules/limit_warmup/repository.py +159 -0
- app/modules/limit_warmup/service.py +1001 -0
- app/modules/model_sources/__init__.py +1 -0
- app/modules/model_sources/api.py +99 -0
- app/modules/model_sources/catalog.py +307 -0
- app/modules/model_sources/forwarding.py +1435 -0
- app/modules/model_sources/projection.py +55 -0
- app/modules/model_sources/repository.py +201 -0
- app/modules/model_sources/schemas.py +79 -0
- app/modules/model_sources/selection.py +166 -0
- app/modules/model_sources/service.py +236 -0
- app/modules/oauth/__init__.py +0 -0
- app/modules/oauth/api.py +101 -0
- app/modules/oauth/repository.py +238 -0
- app/modules/oauth/schemas.py +49 -0
- app/modules/oauth/service.py +1196 -0
- app/modules/oauth/templates/oauth_success.html +122 -0
- app/modules/proxy/__init__.py +0 -0
- app/modules/proxy/_load_balancer/__init__.py +1 -0
- app/modules/proxy/_load_balancer/error_rate.py +116 -0
- app/modules/proxy/_load_balancer/latency_cohort.py +154 -0
- app/modules/proxy/_load_balancer/model_eligibility.py +250 -0
- app/modules/proxy/_load_balancer/opportunistic_admission.py +350 -0
- app/modules/proxy/_load_balancer/overload_backoff.py +394 -0
- app/modules/proxy/_load_balancer/sticky_selection.py +2417 -0
- app/modules/proxy/_load_balancer/throughput_cohort.py +233 -0
- app/modules/proxy/_load_balancer/ttft_cohort.py +214 -0
- app/modules/proxy/_load_balancer/tunables.py +97 -0
- app/modules/proxy/_load_balancer/types.py +96 -0
- app/modules/proxy/_load_balancer/unbound_selection.py +534 -0
- app/modules/proxy/_service/__init__.py +6 -0
- app/modules/proxy/_service/api_key_usage.py +745 -0
- app/modules/proxy/_service/clock_budget.py +38 -0
- app/modules/proxy/_service/codex_control.py +611 -0
- app/modules/proxy/_service/compact.py +2211 -0
- app/modules/proxy/_service/file_ops.py +749 -0
- app/modules/proxy/_service/http_bridge/__init__.py +3 -0
- app/modules/proxy/_service/http_bridge/accepted_replay.py +331 -0
- app/modules/proxy/_service/http_bridge/account_sessions.py +57 -0
- app/modules/proxy/_service/http_bridge/activity.py +178 -0
- app/modules/proxy/_service/http_bridge/helpers.py +3959 -0
- app/modules/proxy/_service/http_bridge/mixin.py +2435 -0
- app/modules/proxy/_service/http_bridge/owner_forwarding.py +609 -0
- app/modules/proxy/_service/http_bridge/protocol.py +167 -0
- app/modules/proxy/_service/http_bridge/proxy_failover.py +58 -0
- app/modules/proxy/_service/http_bridge/quarantine.py +443 -0
- app/modules/proxy/_service/http_bridge/request_submit.py +4582 -0
- app/modules/proxy/_service/http_bridge/retry_circuit.py +2442 -0
- app/modules/proxy/_service/http_bridge/service_stubs.py +492 -0
- app/modules/proxy/_service/http_bridge/session_registry.py +778 -0
- app/modules/proxy/_service/http_bridge/streaming.py +5328 -0
- app/modules/proxy/_service/http_bridge/upstream_events.py +4538 -0
- app/modules/proxy/_service/observability.py +410 -0
- app/modules/proxy/_service/rate_limit.py +361 -0
- app/modules/proxy/_service/realtime_live.py +655 -0
- app/modules/proxy/_service/refresh.py +154 -0
- app/modules/proxy/_service/request_log.py +585 -0
- app/modules/proxy/_service/response_create.py +930 -0
- app/modules/proxy/_service/streaming/__init__.py +3 -0
- app/modules/proxy/_service/streaming/helpers.py +1317 -0
- app/modules/proxy/_service/streaming/mixin.py +1100 -0
- app/modules/proxy/_service/streaming/protocol.py +32 -0
- app/modules/proxy/_service/streaming/retry.py +3762 -0
- app/modules/proxy/_service/support.py +2122 -0
- app/modules/proxy/_service/transcribe.py +434 -0
- app/modules/proxy/_service/warmup.py +483 -0
- app/modules/proxy/_service/websocket/__init__.py +3 -0
- app/modules/proxy/_service/websocket/helpers.py +2360 -0
- app/modules/proxy/_service/websocket/mixin.py +7285 -0
- app/modules/proxy/_service/websocket/protocol.py +74 -0
- app/modules/proxy/account_cache.py +229 -0
- app/modules/proxy/account_eligibility.py +87 -0
- app/modules/proxy/additional_model_limits.py +40 -0
- app/modules/proxy/affinity.py +830 -0
- app/modules/proxy/affinity_observation.py +101 -0
- app/modules/proxy/api.py +10254 -0
- app/modules/proxy/api_key_usage.py +113 -0
- app/modules/proxy/cap_partitioning.py +280 -0
- app/modules/proxy/capability_lineage.py +65 -0
- app/modules/proxy/capability_lineage_repository.py +82 -0
- app/modules/proxy/capability_routing.py +253 -0
- app/modules/proxy/continuity.py +112 -0
- app/modules/proxy/downstream_delivery.py +277 -0
- app/modules/proxy/durable_bridge_coordinator.py +1217 -0
- app/modules/proxy/durable_bridge_repository.py +4516 -0
- app/modules/proxy/durable_bridge_runtime.py +9 -0
- app/modules/proxy/durable_bridge_transcript_codec.py +122 -0
- app/modules/proxy/fair_share.py +131 -0
- app/modules/proxy/file_pin_repository.py +210 -0
- app/modules/proxy/helpers.py +439 -0
- app/modules/proxy/http_bridge_event_batcher.py +613 -0
- app/modules/proxy/http_bridge_forwarding.py +797 -0
- app/modules/proxy/http_continuation.py +76 -0
- app/modules/proxy/images_observability.py +62 -0
- app/modules/proxy/images_service.py +1054 -0
- app/modules/proxy/load_balancer.py +3016 -0
- app/modules/proxy/rate_limit_cache.py +46 -0
- app/modules/proxy/replay_safety.py +1063 -0
- app/modules/proxy/repo_bundle.py +31 -0
- app/modules/proxy/request_policy.py +1054 -0
- app/modules/proxy/ring_membership.py +251 -0
- app/modules/proxy/schemas.py +345 -0
- app/modules/proxy/selection_errors.py +41 -0
- app/modules/proxy/service.py +2600 -0
- app/modules/proxy/source_admission.py +125 -0
- app/modules/proxy/source_dispatch.py +999 -0
- app/modules/proxy/sticky_repository.py +831 -0
- app/modules/proxy/thread_anchors.py +491 -0
- app/modules/proxy/tool_call_dedupe.py +802 -0
- app/modules/proxy/types.py +54 -0
- app/modules/proxy/work_admission.py +121 -0
- app/modules/quota_planner/__init__.py +1 -0
- app/modules/quota_planner/api.py +276 -0
- app/modules/quota_planner/logic.py +743 -0
- app/modules/quota_planner/repository.py +722 -0
- app/modules/quota_planner/scheduler.py +212 -0
- app/modules/quota_planner/schemas.py +102 -0
- app/modules/quota_planner/warmup.py +693 -0
- app/modules/rate_limit_reset_credits/__init__.py +0 -0
- app/modules/rate_limit_reset_credits/api.py +601 -0
- app/modules/rate_limit_reset_credits/redeem_coordination.py +274 -0
- app/modules/rate_limit_reset_credits/store.py +114 -0
- app/modules/reports/__init__.py +1 -0
- app/modules/reports/api.py +134 -0
- app/modules/reports/cache.py +74 -0
- app/modules/reports/filters.py +27 -0
- app/modules/reports/repository.py +525 -0
- app/modules/reports/rollup.py +150 -0
- app/modules/reports/rollup_read.py +148 -0
- app/modules/reports/schemas.py +123 -0
- app/modules/reports/service.py +287 -0
- app/modules/reports/thread_identity.py +256 -0
- app/modules/request_logs/__init__.py +0 -0
- app/modules/request_logs/api.py +224 -0
- app/modules/request_logs/cost_backfill.py +154 -0
- app/modules/request_logs/mappers.py +97 -0
- app/modules/request_logs/repository.py +1765 -0
- app/modules/request_logs/schemas.py +145 -0
- app/modules/request_logs/service.py +348 -0
- app/modules/role_mappings/__init__.py +0 -0
- app/modules/role_mappings/api.py +206 -0
- app/modules/role_mappings/matching.py +55 -0
- app/modules/role_mappings/repository.py +106 -0
- app/modules/role_mappings/schemas.py +67 -0
- app/modules/role_mappings/service.py +334 -0
- app/modules/runtime/__init__.py +1 -0
- app/modules/runtime/api.py +18 -0
- app/modules/runtime/schemas.py +14 -0
- app/modules/runtime/service.py +218 -0
- app/modules/scim/__init__.py +1 -0
- app/modules/scim/api.py +194 -0
- app/modules/scim/dependencies.py +112 -0
- app/modules/scim/errors.py +77 -0
- app/modules/scim/management_api.py +156 -0
- app/modules/scim/repository.py +212 -0
- app/modules/scim/schemas.py +145 -0
- app/modules/scim/service.py +786 -0
- app/modules/scim/tokens.py +43 -0
- app/modules/settings/__init__.py +1 -0
- app/modules/settings/api.py +1677 -0
- app/modules/settings/repository.py +606 -0
- app/modules/settings/schemas.py +496 -0
- app/modules/settings/service.py +769 -0
- app/modules/shared/__init__.py +0 -0
- app/modules/shared/schemas.py +22 -0
- app/modules/sticky_sessions/__init__.py +1 -0
- app/modules/sticky_sessions/api.py +118 -0
- app/modules/sticky_sessions/cleanup_scheduler.py +487 -0
- app/modules/sticky_sessions/schemas.py +78 -0
- app/modules/sticky_sessions/service.py +196 -0
- app/modules/telemetry/__init__.py +5 -0
- app/modules/telemetry/api.py +115 -0
- app/modules/telemetry/clients.py +62 -0
- app/modules/telemetry/consent.py +115 -0
- app/modules/telemetry/scheduler.py +85 -0
- app/modules/telemetry/schemas.py +163 -0
- app/modules/telemetry/sender.py +221 -0
- app/modules/telemetry/snapshot.py +490 -0
- app/modules/usage/__init__.py +0 -0
- app/modules/usage/additional_quota_keys.py +379 -0
- app/modules/usage/api.py +45 -0
- app/modules/usage/background_repository.py +147 -0
- app/modules/usage/builders.py +559 -0
- app/modules/usage/depletion_service.py +277 -0
- app/modules/usage/live_ingest.py +356 -0
- app/modules/usage/mappers.py +29 -0
- app/modules/usage/plan_downgrade_observations.py +488 -0
- app/modules/usage/repository.py +1890 -0
- app/modules/usage/schemas.py +68 -0
- app/modules/usage/service.py +102 -0
- app/modules/usage/updater.py +1488 -0
- app/static/assets/Trans-Cj-J9mVA.js +1 -0
- app/static/assets/access-page-DHzZiXZi.js +1 -0
- app/static/assets/account-cost-donut-DGhfA4U8.js +1 -0
- app/static/assets/account-identifiers-CxbdLshZ.js +1 -0
- app/static/assets/account-status-DIsU5sUS.js +1 -0
- app/static/assets/account-trend-chart-BptlfwwQ.js +1 -0
- app/static/assets/accounts-page-Bbai8ift.js +3 -0
- app/static/assets/api-Dwa7XxPe.js +1 -0
- app/static/assets/api-client-dugX1Pti.js +55 -0
- app/static/assets/api-key-create-dialog-DnRm0oYN.js +1 -0
- app/static/assets/api-key-created-dialog-B6jkDG9f.js +1 -0
- app/static/assets/api-key-edit-dialog-Ck3Bnc3V.js +1 -0
- app/static/assets/api-trend-chart-BchCw7Hj.js +1 -0
- app/static/assets/apis-page-BvXz9c6i.js +2 -0
- app/static/assets/arrow-up-Dqh_KzJo.js +1 -0
- app/static/assets/arrow-up-down-LBRgVDGY.js +1 -0
- app/static/assets/automations-page-DQgNAJOR.js +3 -0
- app/static/assets/badge-DqMb9es0.js +1 -0
- app/static/assets/blur-email-HEvKagG2.js +1 -0
- app/static/assets/button-BjVmmiGW.js +1 -0
- app/static/assets/checkbox-apsl9vzk.js +1 -0
- app/static/assets/chevron-left-1d_pHVa5.js +1 -0
- app/static/assets/circle-alert-BS0xd_o1.js +1 -0
- app/static/assets/clsx-DB0hHKMi.js +1 -0
- app/static/assets/colors-BBy8GEtr.js +1 -0
- app/static/assets/copy-button-D7nAvOKR.js +1 -0
- app/static/assets/cost-per-day-chart-YdqlkZVM.js +1 -0
- app/static/assets/createLucideIcon-DBLzLNh2.js +1 -0
- app/static/assets/dashboard-page-CSdhBy0f.js +2 -0
- app/static/assets/distribution-metric-format-yZTy1ZEu.js +1 -0
- app/static/assets/donut-chart-Bxc69cra.js +1 -0
- app/static/assets/download-kQteIcsB.js +1 -0
- app/static/assets/ellipsis-B_5OgIre.js +1 -0
- app/static/assets/empty-state-JcA7tc8m.js +1 -0
- app/static/assets/es2015-bMH8YKZT.js +41 -0
- app/static/assets/es6-B264ni6W.js +69 -0
- app/static/assets/external-link-UVAOgMQ0.js +1 -0
- app/static/assets/i18n-BWqZ9Ua2.js +1 -0
- app/static/assets/inbox-B_RM2s1J.js +1 -0
- app/static/assets/index-DIpd264Z.js +23 -0
- app/static/assets/index-DMeLKRJx.css +2 -0
- app/static/assets/inherit-badge-QhTYVaU6.js +1 -0
- app/static/assets/invite-dialog-0CYh0GiQ.js +1 -0
- app/static/assets/iso-Bpe5Ihbb.js +1 -0
- app/static/assets/label-CGaLmLNf.js +1 -0
- app/static/assets/lazy-recharts-Pv8p9ArM.js +2 -0
- app/static/assets/loading-overlay-CzKQfHLI.js +1 -0
- app/static/assets/mini-quota-bar-cGT_t8Ae.js +1 -0
- app/static/assets/model-distribution-donut-BKjAfT48.js +1 -0
- app/static/assets/model-source-multi-select-D2d06Vc7.js +1 -0
- app/static/assets/multi-select-filter-KIA5qU6Z.js +1 -0
- app/static/assets/oauth-dialog-XeaGGDjS.js +1 -0
- app/static/assets/pagination-controls-D1Ypd0RU.js +1 -0
- app/static/assets/payload-CIYnGZz9.js +1 -0
- app/static/assets/queue-wait-chart-VoV7Yg9L.js +1 -0
- app/static/assets/report-chart-card-BXDliwcc.js +1 -0
- app/static/assets/reports-page-CGCJeMHG.js +3 -0
- app/static/assets/reset-credit-confirm-dialog-QOifTnVB.js +1 -0
- app/static/assets/rolldown-runtime-hePW80VL.js +1 -0
- app/static/assets/search-HfhqwERt.js +1 -0
- app/static/assets/select-Cl33bz-g.js +1 -0
- app/static/assets/settings-page-mZj01HYl.js +2 -0
- app/static/assets/shim-CZwyM4Q7.js +1 -0
- app/static/assets/skeleton-C_EUc4Rp.js +1 -0
- app/static/assets/sparkline-chart-DdNNsddM.js +1 -0
- app/static/assets/status-badge-CHHA5X6c.js +1 -0
- app/static/assets/switch-CzxgokcM.js +1 -0
- app/static/assets/table-DXaNSQ0G.js +1 -0
- app/static/assets/time-to-first-token-chart--68L0u_v.js +1 -0
- app/static/assets/tokens-per-day-chart-Cs82BFuE.js +1 -0
- app/static/assets/tokens-per-second-chart-AD2dfOn6.js +1 -0
- app/static/assets/totp-settings-CRHtxQ7j.js +1 -0
- app/static/assets/trash-D3KyWr1h.js +1 -0
- app/static/assets/use-account-quota-display-BPmrTBTC.js +1 -0
- app/static/assets/use-accounts-PWlVGw3z.js +1 -0
- app/static/assets/use-auth-CuDlW3iC.js +1 -0
- app/static/assets/use-chart-colors-DIu6tClI.js +1 -0
- app/static/assets/use-model-sources-Bul24JBu.js +1 -0
- app/static/assets/use-models-DUCPUuZg.js +1 -0
- app/static/assets/use-reduced-motion-hAm5r6_d.js +1 -0
- app/static/assets/useTranslation--56nHhlH.js +1 -0
- app/static/assets/useragent-distribution-donut-Dup6sf_n.js +1 -0
- app/static/assets/users-uIC3TIlE.js +1 -0
- app/static/assets/vendor-query-_bHZuxEx.js +1 -0
- app/static/assets/vendor-react-BVL0YZPW.js +9 -0
- app/static/favicon.svg +10 -0
- app/static/fonts/GeistSans-Variable.woff2 +0 -0
- app/static/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- app/static/fonts/jetbrains-mono-latin.woff2 +0 -0
- app/static/index.html +29 -0
- codexlb_up-1.25.0b10.dist-info/METADATA +124 -0
- codexlb_up-1.25.0b10.dist-info/RECORD +827 -0
- codexlb_up-1.25.0b10.dist-info/WHEEL +4 -0
- codexlb_up-1.25.0b10.dist-info/entry_points.txt +3 -0
- codexlb_up-1.25.0b10.dist-info/licenses/LICENSE +21 -0
- config/additional_quota_registry.json +11 -0
app/db/sqlite_utils.py
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sqlite3
|
|
6
|
+
import tempfile
|
|
7
|
+
import urllib.parse
|
|
8
|
+
from collections.abc import Iterator
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TypedDict, cast
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(slots=True)
|
|
17
|
+
class IntegrityCheck:
|
|
18
|
+
ok: bool
|
|
19
|
+
details: str | None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SqliteIntegrityCheckMode(str, Enum):
|
|
23
|
+
QUICK = "quick"
|
|
24
|
+
FULL = "full"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SqliteRunState(str, Enum):
|
|
28
|
+
"""How the previous process left the SQLite store."""
|
|
29
|
+
|
|
30
|
+
RUNNING = "running"
|
|
31
|
+
CLEAN = "clean"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(slots=True, frozen=True)
|
|
35
|
+
class SqliteFileIdentity:
|
|
36
|
+
"""The filesystem identity captured by a run-state transition."""
|
|
37
|
+
|
|
38
|
+
dev: int
|
|
39
|
+
ino: int
|
|
40
|
+
size: int
|
|
41
|
+
mtime_ns: int
|
|
42
|
+
ctime_ns: int
|
|
43
|
+
|
|
44
|
+
def as_payload(self) -> SqliteFileIdentityPayload:
|
|
45
|
+
return {
|
|
46
|
+
"dev": self.dev,
|
|
47
|
+
"ino": self.ino,
|
|
48
|
+
"size": self.size,
|
|
49
|
+
"mtime_ns": self.mtime_ns,
|
|
50
|
+
"ctime_ns": self.ctime_ns,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_payload(cls, payload: object) -> SqliteFileIdentity | None:
|
|
55
|
+
if not isinstance(payload, dict) or payload.keys() != _SQLITE_FILE_IDENTITY_KEYS:
|
|
56
|
+
return None
|
|
57
|
+
typed_payload = cast(SqliteFileIdentityPayload, cast(object, payload))
|
|
58
|
+
values = (
|
|
59
|
+
typed_payload["dev"],
|
|
60
|
+
typed_payload["ino"],
|
|
61
|
+
typed_payload["size"],
|
|
62
|
+
typed_payload["mtime_ns"],
|
|
63
|
+
typed_payload["ctime_ns"],
|
|
64
|
+
)
|
|
65
|
+
if any(type(value) is not int for value in values):
|
|
66
|
+
return None
|
|
67
|
+
return cls(
|
|
68
|
+
dev=typed_payload["dev"],
|
|
69
|
+
ino=typed_payload["ino"],
|
|
70
|
+
size=typed_payload["size"],
|
|
71
|
+
mtime_ns=typed_payload["mtime_ns"],
|
|
72
|
+
ctime_ns=typed_payload["ctime_ns"],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SqliteFileIdentityPayload(TypedDict):
|
|
77
|
+
dev: int
|
|
78
|
+
ino: int
|
|
79
|
+
size: int
|
|
80
|
+
mtime_ns: int
|
|
81
|
+
ctime_ns: int
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_SQLITE_FILE_IDENTITY_KEYS = frozenset({"dev", "ino", "size", "mtime_ns", "ctime_ns"})
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(slots=True, frozen=True)
|
|
88
|
+
class SqliteRunStateRecord:
|
|
89
|
+
"""A run-state transition and the database identity captured with it."""
|
|
90
|
+
|
|
91
|
+
state: SqliteRunState
|
|
92
|
+
identity: SqliteFileIdentity | None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@contextmanager
|
|
96
|
+
def sqlite_connection(path: str | Path) -> Iterator[sqlite3.Connection]:
|
|
97
|
+
connection = sqlite3.connect(str(path))
|
|
98
|
+
try:
|
|
99
|
+
with connection:
|
|
100
|
+
yield connection
|
|
101
|
+
finally:
|
|
102
|
+
connection.close()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _sqlite_path_uses_sqlalchemy_windows_escapes(path: str) -> bool:
|
|
106
|
+
lower_path = path.lower()
|
|
107
|
+
if (
|
|
108
|
+
len(lower_path) >= 5
|
|
109
|
+
and lower_path[1:4] == "%3a"
|
|
110
|
+
and lower_path[0].isalpha()
|
|
111
|
+
and (lower_path[4:7] in ("%5c", "%2f") or lower_path[4] in ("\\", "/"))
|
|
112
|
+
):
|
|
113
|
+
return True
|
|
114
|
+
return lower_path.startswith("%5c%5c")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _sqlite_path_is_raw_windows_drive(path: str) -> bool:
|
|
118
|
+
return len(path) >= 3 and path[1] == ":" and path[0].isalpha() and path[2] in ("\\", "/")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _sqlite_path_is_raw_windows_unc(path: str) -> bool:
|
|
122
|
+
return path.startswith("\\\\")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _decode_sqlalchemy_windows_sqlite_path(path: str) -> str:
|
|
126
|
+
if not _sqlite_path_uses_sqlalchemy_windows_escapes(path):
|
|
127
|
+
return path
|
|
128
|
+
return urllib.parse.unquote(path)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _sqlite_uri_query(url: str, path_start: int) -> dict[str, list[str]]:
|
|
132
|
+
"""Return decoded query values from a SQLite URL's URI suffix."""
|
|
133
|
+
query_start = url.find("?", path_start)
|
|
134
|
+
if query_start < 0:
|
|
135
|
+
return {}
|
|
136
|
+
fragment_start = url.find("#", query_start + 1)
|
|
137
|
+
query_end = len(url) if fragment_start < 0 else fragment_start
|
|
138
|
+
return urllib.parse.parse_qs(url[query_start + 1 : query_end], keep_blank_values=True)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _sqlite_uri_query_value_is_true(value: str) -> bool:
|
|
142
|
+
"""Match the truthy values accepted by SQLAlchemy's SQLite dialect."""
|
|
143
|
+
return value.strip().lower() in {"1", "t", "true", "y", "yes", "on"}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _sqlite_uri_enabled(url: str, path_start: int) -> bool:
|
|
147
|
+
query = _sqlite_uri_query(url, path_start)
|
|
148
|
+
return any(_sqlite_uri_query_value_is_true(value) for value in query.get("uri", []))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _sqlite_uri_is_memory(url: str, path_start: int, path: str) -> bool:
|
|
152
|
+
"""Return whether a SQLite URI denotes an in-memory database.
|
|
153
|
+
|
|
154
|
+
A ``file:`` name is a filesystem path until the SQLite dialect is put in
|
|
155
|
+
URI mode. Only then do ``mode=memory`` and SQLite's ``file::memory:`` URI
|
|
156
|
+
have their in-memory meaning; otherwise the path retains its normal
|
|
157
|
+
file-backed semantics for sidecars and startup checks.
|
|
158
|
+
"""
|
|
159
|
+
if not path.startswith("file:") or not _sqlite_uri_enabled(url, path_start):
|
|
160
|
+
return False
|
|
161
|
+
query = _sqlite_uri_query(url, path_start)
|
|
162
|
+
if any(value.strip().lower() == "memory" for value in query.get("mode", [])):
|
|
163
|
+
return True
|
|
164
|
+
return urllib.parse.unquote(path) == "file::memory:"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _sqlite_uri_file_path(path: str) -> str | None:
|
|
168
|
+
"""Resolve a supported file-backed SQLite URI to its filesystem path."""
|
|
169
|
+
parsed = urllib.parse.urlsplit(path)
|
|
170
|
+
if parsed.scheme != "file":
|
|
171
|
+
return path
|
|
172
|
+
if parsed.netloc not in {"", "localhost"}:
|
|
173
|
+
return None
|
|
174
|
+
resolved = urllib.parse.unquote(parsed.path)
|
|
175
|
+
# SQLite treats the tilde in ``file:~/store.db`` literally; unlike a
|
|
176
|
+
# normal application path it is not a shell shorthand for the home
|
|
177
|
+
# directory. Likewise, ``file:///C:/...`` uses one leading slash for the
|
|
178
|
+
# authority-less URI syntax rather than as part of the Windows drive path.
|
|
179
|
+
if len(resolved) >= 4 and resolved[0] == "/" and resolved[1].isalpha() and resolved[2] == ":":
|
|
180
|
+
resolved = resolved[1:]
|
|
181
|
+
return resolved
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def sqlite_db_path_from_url(url: str) -> Path | None:
|
|
185
|
+
if not (url.startswith("sqlite+aiosqlite:") or url.startswith("sqlite:")):
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
marker = ":///"
|
|
189
|
+
marker_index = url.find(marker)
|
|
190
|
+
if marker_index < 0:
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
path_start = marker_index + len(marker)
|
|
194
|
+
path = url[path_start:]
|
|
195
|
+
if _sqlite_path_is_raw_windows_drive(path) or _sqlite_path_is_raw_windows_unc(path):
|
|
196
|
+
# Raw Windows drive and UNC paths are filesystem paths, not URL-encoded
|
|
197
|
+
# forms: a `#` is a legal path character there (e.g. the decoded output
|
|
198
|
+
# of `normalize_sqlite_url()`), so it must not be stripped as a URL
|
|
199
|
+
# fragment separator.
|
|
200
|
+
path = path.partition("?")[0]
|
|
201
|
+
else:
|
|
202
|
+
path = path.partition("?")[0]
|
|
203
|
+
path = path.partition("#")[0]
|
|
204
|
+
|
|
205
|
+
# SQLAlchemy's `URL.render_as_string()` percent-encodes Windows drive and
|
|
206
|
+
# UNC SQLite paths (e.g. `sqlite:///C%3A%5CUsers%5C...%5Cstore.db`). Decode
|
|
207
|
+
# those recognizable rendered Windows forms before opening the filesystem
|
|
208
|
+
# path. Do not unquote arbitrary `%xx` sequences here: settings builds the
|
|
209
|
+
# default SQLite URL directly from `data_dir`, so a valid literal path such
|
|
210
|
+
# as `/var/lib/codex%20lb/store.db` must remain literal.
|
|
211
|
+
path = _decode_sqlalchemy_windows_sqlite_path(path)
|
|
212
|
+
|
|
213
|
+
if not path or path == ":memory:" or _sqlite_uri_is_memory(url, path_start, path):
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
uri_file_path = path.startswith("file:") and _sqlite_uri_enabled(url, path_start)
|
|
217
|
+
if uri_file_path:
|
|
218
|
+
path = _sqlite_uri_file_path(path)
|
|
219
|
+
if not path:
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
# URI paths are already the exact SQLite target. In particular, a leading
|
|
223
|
+
# ``~`` is literal in SQLite's ``file:`` URI syntax and must not be
|
|
224
|
+
# expanded as it would be for a regular filesystem path.
|
|
225
|
+
return Path(path) if uri_file_path else Path(path).expanduser()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def sqlite_url_is_memory(url: str) -> bool:
|
|
229
|
+
"""Return whether a SQLite URL opens an in-memory database."""
|
|
230
|
+
if not (url.startswith("sqlite+aiosqlite:") or url.startswith("sqlite:")):
|
|
231
|
+
return False
|
|
232
|
+
|
|
233
|
+
marker = ":///"
|
|
234
|
+
marker_index = url.find(marker)
|
|
235
|
+
if marker_index < 0:
|
|
236
|
+
# SQLAlchemy's ``sqlite:///``-less form (``sqlite://``) is its
|
|
237
|
+
# in-memory URL.
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
path_start = marker_index + len(marker)
|
|
241
|
+
path = url[path_start:]
|
|
242
|
+
if _sqlite_path_is_raw_windows_drive(path) or _sqlite_path_is_raw_windows_unc(path):
|
|
243
|
+
path = path.partition("?")[0]
|
|
244
|
+
else:
|
|
245
|
+
path = path.partition("?")[0].partition("#")[0]
|
|
246
|
+
path = _decode_sqlalchemy_windows_sqlite_path(path)
|
|
247
|
+
return not path or path == ":memory:" or _sqlite_uri_is_memory(url, path_start, path)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def normalize_sqlite_url(url: str) -> str:
|
|
251
|
+
if not (url.startswith("sqlite+aiosqlite:") or url.startswith("sqlite:")):
|
|
252
|
+
return url
|
|
253
|
+
|
|
254
|
+
marker = ":///"
|
|
255
|
+
marker_index = url.find(marker)
|
|
256
|
+
if marker_index < 0:
|
|
257
|
+
return url
|
|
258
|
+
|
|
259
|
+
path_start = marker_index + len(marker)
|
|
260
|
+
suffix_index = len(url)
|
|
261
|
+
for separator in ("?", "#"):
|
|
262
|
+
separator_index = url.find(separator, path_start)
|
|
263
|
+
if separator_index >= 0:
|
|
264
|
+
suffix_index = min(suffix_index, separator_index)
|
|
265
|
+
|
|
266
|
+
path = url[path_start:suffix_index]
|
|
267
|
+
if not path or path == ":memory:":
|
|
268
|
+
return url
|
|
269
|
+
|
|
270
|
+
decoded_path = _decode_sqlalchemy_windows_sqlite_path(path)
|
|
271
|
+
return f"{url[:path_start]}{decoded_path}{url[suffix_index:]}"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _integrity_check_pragma(mode: SqliteIntegrityCheckMode) -> str:
|
|
275
|
+
if mode == SqliteIntegrityCheckMode.QUICK:
|
|
276
|
+
return "PRAGMA quick_check;"
|
|
277
|
+
return "PRAGMA integrity_check;"
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def check_sqlite_integrity(
|
|
281
|
+
path: Path,
|
|
282
|
+
*,
|
|
283
|
+
mode: SqliteIntegrityCheckMode = SqliteIntegrityCheckMode.FULL,
|
|
284
|
+
) -> IntegrityCheck:
|
|
285
|
+
if not path.exists():
|
|
286
|
+
return IntegrityCheck(ok=True, details=None)
|
|
287
|
+
|
|
288
|
+
try:
|
|
289
|
+
with sqlite_connection(path) as conn:
|
|
290
|
+
cursor = conn.execute(_integrity_check_pragma(mode))
|
|
291
|
+
rows = [row[0] for row in cursor.fetchall()]
|
|
292
|
+
except sqlite3.DatabaseError as exc:
|
|
293
|
+
return IntegrityCheck(ok=False, details=str(exc))
|
|
294
|
+
|
|
295
|
+
if len(rows) == 1 and rows[0] == "ok":
|
|
296
|
+
return IntegrityCheck(ok=True, details=None)
|
|
297
|
+
|
|
298
|
+
if not rows:
|
|
299
|
+
return IntegrityCheck(ok=False, details=f"{mode.value}_check returned no rows")
|
|
300
|
+
|
|
301
|
+
details = "; ".join(str(row) for row in rows)
|
|
302
|
+
return IntegrityCheck(ok=False, details=details)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def integrity_check_pragma_name(mode: SqliteIntegrityCheckMode) -> str:
|
|
306
|
+
return "quick_check" if mode == SqliteIntegrityCheckMode.QUICK else "integrity_check"
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def sqlite_runstate_path(db_path: Path) -> Path:
|
|
310
|
+
"""Sidecar file recording how the previous process left ``db_path``."""
|
|
311
|
+
return db_path.with_name(f"{db_path.name}.runstate")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def sqlite_runstate_lock_path(db_path: Path) -> Path:
|
|
315
|
+
"""Persistent SQLite sentinel used to fence one process onto ``db_path``."""
|
|
316
|
+
return db_path.with_name(f"{db_path.name}.runstate.lock")
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
class SqliteRunStateLockError(RuntimeError):
|
|
320
|
+
"""The process could not obtain exclusive ownership of a SQLite store."""
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
class SqliteRunStateDurabilityError(OSError):
|
|
324
|
+
"""The failed run-state marker could not be durably invalidated."""
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def acquire_sqlite_runstate_lock(db_path: Path) -> sqlite3.Connection:
|
|
328
|
+
"""Hold an exclusive sentinel transaction for the lifetime of one process.
|
|
329
|
+
|
|
330
|
+
The sentinel is deliberately never unlinked. A ``BEGIN IMMEDIATE`` on its
|
|
331
|
+
own SQLite file is portable, vanishes automatically when the process dies,
|
|
332
|
+
and cannot be confused with the main database's application transactions.
|
|
333
|
+
"""
|
|
334
|
+
lock_path = sqlite_runstate_lock_path(db_path)
|
|
335
|
+
connection: sqlite3.Connection | None = None
|
|
336
|
+
try:
|
|
337
|
+
connection = sqlite3.connect(str(lock_path), timeout=0, isolation_level=None)
|
|
338
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
339
|
+
except (OSError, sqlite3.Error) as exc:
|
|
340
|
+
if connection is not None:
|
|
341
|
+
connection.close()
|
|
342
|
+
raise SqliteRunStateLockError(
|
|
343
|
+
f"Could not acquire the SQLite lifetime lock {lock_path}; "
|
|
344
|
+
"another process may already be using this database"
|
|
345
|
+
) from exc
|
|
346
|
+
return connection
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def release_sqlite_runstate_lock(connection: sqlite3.Connection) -> None:
|
|
350
|
+
"""Release a lifetime sentinel lock without deleting its persistent file."""
|
|
351
|
+
try:
|
|
352
|
+
connection.rollback()
|
|
353
|
+
except sqlite3.Error:
|
|
354
|
+
# Closing still releases the lock when a rollback is interrupted or
|
|
355
|
+
# the underlying sentinel has become unavailable.
|
|
356
|
+
pass
|
|
357
|
+
finally:
|
|
358
|
+
connection.close()
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _sqlite_file_identity(db_path: Path) -> SqliteFileIdentity | None:
|
|
362
|
+
"""Identify the database file well enough to detect that it was replaced.
|
|
363
|
+
|
|
364
|
+
Size and mtime alone are not enough: a restore that preserves timestamps
|
|
365
|
+
(``tar -x``, ``cp -p``, ``rsync -a``) can reproduce both. The inode and
|
|
366
|
+
device catch the replacement itself, and ctime catches an in-place
|
|
367
|
+
metadata change. Any of these shifting for a benign reason only costs an
|
|
368
|
+
extra scan, which is the safe direction.
|
|
369
|
+
"""
|
|
370
|
+
try:
|
|
371
|
+
stat_result = db_path.stat()
|
|
372
|
+
except OSError:
|
|
373
|
+
return None
|
|
374
|
+
return SqliteFileIdentity(
|
|
375
|
+
dev=stat_result.st_dev,
|
|
376
|
+
ino=stat_result.st_ino,
|
|
377
|
+
size=stat_result.st_size,
|
|
378
|
+
mtime_ns=stat_result.st_mtime_ns,
|
|
379
|
+
ctime_ns=stat_result.st_ctime_ns,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# Windows cannot obtain a directory handle through ``os.open``: the underlying
|
|
384
|
+
# CreateFileW call refuses a directory and the failure surfaces as EACCES,
|
|
385
|
+
# which errno cannot tell apart from an ordinary permission denial. Decide by
|
|
386
|
+
# platform instead, so that on POSIX every open failure can be treated as the
|
|
387
|
+
# real failure it is.
|
|
388
|
+
_DIRECTORY_FSYNC_SUPPORTED = os.name == "posix"
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _fsync_directory(directory: Path) -> bool:
|
|
392
|
+
"""Persist a directory entry so a rename survives power loss.
|
|
393
|
+
|
|
394
|
+
Returns ``False`` whenever a sync was expected and did not happen, so the
|
|
395
|
+
caller can refuse to leave behind a record whose durability is unproven.
|
|
396
|
+
A missing path, a permission denial, descriptor exhaustion, and an I/O
|
|
397
|
+
error all count.
|
|
398
|
+
|
|
399
|
+
Where a directory handle is not obtainable at all the sync is not
|
|
400
|
+
attempted and this reports success. There is nothing this code can verify
|
|
401
|
+
on such a platform, and failing closed would mean no Windows deployment
|
|
402
|
+
could ever record a clean shutdown; rename durability there is the
|
|
403
|
+
platform's guarantee to make.
|
|
404
|
+
"""
|
|
405
|
+
if not _DIRECTORY_FSYNC_SUPPORTED:
|
|
406
|
+
return True
|
|
407
|
+
try:
|
|
408
|
+
directory_fd = os.open(directory, os.O_RDONLY)
|
|
409
|
+
except OSError:
|
|
410
|
+
return False
|
|
411
|
+
try:
|
|
412
|
+
os.fsync(directory_fd)
|
|
413
|
+
except OSError:
|
|
414
|
+
return False
|
|
415
|
+
finally:
|
|
416
|
+
os.close(directory_fd)
|
|
417
|
+
return True
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def read_sqlite_runstate(db_path: Path) -> SqliteRunState | None:
|
|
421
|
+
"""Return the recorded run state, or ``None`` when it cannot be trusted.
|
|
422
|
+
|
|
423
|
+
``None`` means "unknown" and callers MUST treat it as potentially
|
|
424
|
+
unclean. A missing sidecar covers both a first run and an upgrade from a
|
|
425
|
+
build that never wrote one, so the conservative reading is the only safe
|
|
426
|
+
one.
|
|
427
|
+
|
|
428
|
+
A ``clean`` record is honoured only while the database file still matches
|
|
429
|
+
the device, inode, size, mtime, and ctime captured when the record was
|
|
430
|
+
written. Restoring a backup or swapping the file in by hand therefore
|
|
431
|
+
reads back as unknown rather than inheriting the previous file's clean
|
|
432
|
+
record. Startup callers must acquire the matching lifetime lock before
|
|
433
|
+
using this result to skip a check.
|
|
434
|
+
"""
|
|
435
|
+
record = read_sqlite_runstate_record(db_path)
|
|
436
|
+
return record.state if record is not None else None
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def read_sqlite_runstate_record(db_path: Path) -> SqliteRunStateRecord | None:
|
|
440
|
+
"""Read the run state and its captured database identity.
|
|
441
|
+
|
|
442
|
+
Startup uses the complete record to compare the prior ``clean`` identity
|
|
443
|
+
with the newly persisted ``running`` identity. A clean record without a
|
|
444
|
+
verifiable identity is unknown; a running record remains readable so the
|
|
445
|
+
startup decision can fail closed on a missing identity.
|
|
446
|
+
"""
|
|
447
|
+
try:
|
|
448
|
+
raw = sqlite_runstate_path(db_path).read_text(encoding="utf-8")
|
|
449
|
+
except (OSError, UnicodeError):
|
|
450
|
+
return None
|
|
451
|
+
try:
|
|
452
|
+
payload = json.loads(raw)
|
|
453
|
+
state = SqliteRunState(payload["state"])
|
|
454
|
+
identity = SqliteFileIdentity.from_payload(payload.get("identity"))
|
|
455
|
+
except (ValueError, TypeError, KeyError, AttributeError, RecursionError):
|
|
456
|
+
return None
|
|
457
|
+
if state is SqliteRunState.CLEAN:
|
|
458
|
+
current_identity = _sqlite_file_identity(db_path)
|
|
459
|
+
if identity is None or current_identity is None or identity != current_identity:
|
|
460
|
+
return None
|
|
461
|
+
return SqliteRunStateRecord(state=state, identity=identity)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _invalidate_failed_sqlite_runstate(target: Path, tmp: Path | None) -> None:
|
|
465
|
+
"""Remove an untrusted run-state write and persist that removal.
|
|
466
|
+
|
|
467
|
+
A failed replacement is safe to recover from only when both the temporary
|
|
468
|
+
file and any previous target are gone and the parent directory sync
|
|
469
|
+
confirms those removals. Raise a distinct error when that proof is
|
|
470
|
+
unavailable so startup cannot continue while an older ``clean`` marker
|
|
471
|
+
might still be trusted after a power loss.
|
|
472
|
+
"""
|
|
473
|
+
cleanup_error: OSError | None = None
|
|
474
|
+
for cleanup in (tmp, target):
|
|
475
|
+
if cleanup is None:
|
|
476
|
+
continue
|
|
477
|
+
try:
|
|
478
|
+
cleanup.unlink(missing_ok=True)
|
|
479
|
+
except OSError as exc:
|
|
480
|
+
cleanup_error = cleanup_error or exc
|
|
481
|
+
if cleanup_error is not None:
|
|
482
|
+
raise SqliteRunStateDurabilityError(
|
|
483
|
+
f"could not remove failed SQLite run-state files for {target}"
|
|
484
|
+
) from cleanup_error
|
|
485
|
+
try:
|
|
486
|
+
directory_synced = _fsync_directory(target.parent)
|
|
487
|
+
except OSError as exc:
|
|
488
|
+
raise SqliteRunStateDurabilityError(
|
|
489
|
+
f"could not persist removal of failed SQLite run-state files for {target}"
|
|
490
|
+
) from exc
|
|
491
|
+
if not directory_synced:
|
|
492
|
+
raise SqliteRunStateDurabilityError(f"could not persist removal of failed SQLite run-state files for {target}")
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def write_sqlite_runstate(db_path: Path, state: SqliteRunState) -> bool:
|
|
496
|
+
"""Record ``state`` atomically. Returns ``False`` if it could not be recorded.
|
|
497
|
+
|
|
498
|
+
The payload and the directory entry are both fsynced, so a power loss
|
|
499
|
+
cannot retain an earlier ``clean`` record while losing the ``running``
|
|
500
|
+
transition that replaced it. A directory sync that is attempted and fails
|
|
501
|
+
is treated as a failed write, because a record whose durability could not
|
|
502
|
+
be established must not be trusted. In WAL mode the main database file can keep
|
|
503
|
+
its size and mtime across a long run, so the sidecar cannot rely on the
|
|
504
|
+
file identity alone to invalidate a lost transition.
|
|
505
|
+
|
|
506
|
+
A failed write must never leave a stale ``clean`` sidecar behind, because
|
|
507
|
+
that would tell the next startup to skip the integrity check for a store
|
|
508
|
+
this process may have left mid-write. The fallback is to remove the
|
|
509
|
+
temporary and target entries, then sync the directory. If that
|
|
510
|
+
invalidation cannot be confirmed, ``SqliteRunStateDurabilityError`` is
|
|
511
|
+
raised so a startup caller can fail closed instead of trusting the marker.
|
|
512
|
+
The caller is responsible for holding the lifetime lock around state
|
|
513
|
+
changes.
|
|
514
|
+
"""
|
|
515
|
+
target = sqlite_runstate_path(db_path)
|
|
516
|
+
tmp: Path | None = None
|
|
517
|
+
tmp_fd: int | None = None
|
|
518
|
+
identity = _sqlite_file_identity(db_path)
|
|
519
|
+
payload = json.dumps({"state": state.value, "identity": identity.as_payload() if identity else None})
|
|
520
|
+
try:
|
|
521
|
+
tmp_fd, tmp_name = tempfile.mkstemp(prefix=f"{target.name}.", suffix=".tmp", dir=target.parent)
|
|
522
|
+
tmp = Path(tmp_name)
|
|
523
|
+
with os.fdopen(tmp_fd, "w", encoding="utf-8") as handle:
|
|
524
|
+
tmp_fd = None
|
|
525
|
+
handle.write(payload)
|
|
526
|
+
handle.flush()
|
|
527
|
+
os.fsync(handle.fileno())
|
|
528
|
+
os.replace(tmp, target)
|
|
529
|
+
if not _fsync_directory(target.parent):
|
|
530
|
+
raise OSError("could not sync the run-state directory entry")
|
|
531
|
+
return True
|
|
532
|
+
except OSError:
|
|
533
|
+
if tmp_fd is not None:
|
|
534
|
+
try:
|
|
535
|
+
os.close(tmp_fd)
|
|
536
|
+
except OSError:
|
|
537
|
+
pass
|
|
538
|
+
for cleanup in (tmp, target):
|
|
539
|
+
if cleanup is None:
|
|
540
|
+
continue
|
|
541
|
+
try:
|
|
542
|
+
cleanup.unlink(missing_ok=True)
|
|
543
|
+
except OSError:
|
|
544
|
+
pass
|
|
545
|
+
# The target cleanup can remove an older clean marker even when the
|
|
546
|
+
# replacement itself failed. A caller must distinguish a durably
|
|
547
|
+
# invalidated marker from cleanup whose durability is unknown.
|
|
548
|
+
_invalidate_failed_sqlite_runstate(target, tmp)
|
|
549
|
+
return False
|