trueppm-api 0.4.0b2__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.
- trueppm_api/__init__.py +14 -0
- trueppm_api/apps/access/__init__.py +0 -0
- trueppm_api/apps/access/apps.py +19 -0
- trueppm_api/apps/access/groups.py +518 -0
- trueppm_api/apps/access/management/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/create_admin.py +164 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +209 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +167 -0
- trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
- trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
- trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
- trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
- trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
- trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
- trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
- trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
- trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
- trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
- trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
- trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
- trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
- trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
- trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
- trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +132 -0
- trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
- trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
- trueppm_api/apps/access/migrations/__init__.py +0 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +2837 -0
- trueppm_api/apps/access/serializers.py +946 -0
- trueppm_api/apps/access/services.py +667 -0
- trueppm_api/apps/access/signals.py +103 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +95 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1931 -0
- trueppm_api/apps/agents/__init__.py +6 -0
- trueppm_api/apps/agents/apps.py +11 -0
- trueppm_api/apps/agents/canonical.py +73 -0
- trueppm_api/apps/agents/deferred.py +108 -0
- trueppm_api/apps/agents/management/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
- trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
- trueppm_api/apps/agents/middleware.py +44 -0
- trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
- trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
- trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
- trueppm_api/apps/agents/migrations/__init__.py +0 -0
- trueppm_api/apps/agents/models.py +371 -0
- trueppm_api/apps/agents/refusal.py +103 -0
- trueppm_api/apps/agents/serializers.py +85 -0
- trueppm_api/apps/agents/services.py +397 -0
- trueppm_api/apps/agents/signals.py +30 -0
- trueppm_api/apps/agents/urls.py +15 -0
- trueppm_api/apps/agents/views.py +108 -0
- trueppm_api/apps/csvimport/__init__.py +12 -0
- trueppm_api/apps/csvimport/apps.py +11 -0
- trueppm_api/apps/csvimport/mapping.py +363 -0
- trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
- trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
- trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
- trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
- trueppm_api/apps/csvimport/models.py +111 -0
- trueppm_api/apps/csvimport/parser.py +1825 -0
- trueppm_api/apps/csvimport/services.py +48 -0
- trueppm_api/apps/csvimport/tasks.py +418 -0
- trueppm_api/apps/csvimport/template.py +37 -0
- trueppm_api/apps/csvimport/urls.py +50 -0
- trueppm_api/apps/csvimport/views.py +793 -0
- trueppm_api/apps/history/__init__.py +0 -0
- trueppm_api/apps/history/apps.py +12 -0
- trueppm_api/apps/history/changelog.py +359 -0
- trueppm_api/apps/history/diff_policy.py +177 -0
- trueppm_api/apps/history/serializers.py +104 -0
- trueppm_api/apps/history/signals.py +82 -0
- trueppm_api/apps/history/tasks.py +105 -0
- trueppm_api/apps/history/urls.py +38 -0
- trueppm_api/apps/history/views.py +443 -0
- trueppm_api/apps/idempotency/__init__.py +0 -0
- trueppm_api/apps/idempotency/apps.py +11 -0
- trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
- trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
- trueppm_api/apps/idempotency/mixins.py +226 -0
- trueppm_api/apps/idempotency/models.py +75 -0
- trueppm_api/apps/idempotency/tasks.py +61 -0
- trueppm_api/apps/integrations/__init__.py +13 -0
- trueppm_api/apps/integrations/apps.py +59 -0
- trueppm_api/apps/integrations/connections.py +806 -0
- trueppm_api/apps/integrations/encryption.py +92 -0
- trueppm_api/apps/integrations/external_sources.py +837 -0
- trueppm_api/apps/integrations/git_automation_services.py +231 -0
- trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
- trueppm_api/apps/integrations/http.py +403 -0
- trueppm_api/apps/integrations/me_work.py +218 -0
- trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
- trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
- trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
- trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
- trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
- trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
- trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
- trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
- trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
- trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
- trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
- trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
- trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
- trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +622 -0
- trueppm_api/apps/integrations/notification_channels.py +57 -0
- trueppm_api/apps/integrations/opengraph.py +229 -0
- trueppm_api/apps/integrations/outgoing.py +201 -0
- trueppm_api/apps/integrations/providers.py +519 -0
- trueppm_api/apps/integrations/registry.py +345 -0
- trueppm_api/apps/integrations/serializers.py +453 -0
- trueppm_api/apps/integrations/services.py +171 -0
- trueppm_api/apps/integrations/tasks.py +731 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1106 -0
- trueppm_api/apps/jiraimport/__init__.py +8 -0
- trueppm_api/apps/jiraimport/apps.py +11 -0
- trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
- trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
- trueppm_api/apps/jiraimport/models.py +70 -0
- trueppm_api/apps/jiraimport/parser.py +350 -0
- trueppm_api/apps/jiraimport/services.py +46 -0
- trueppm_api/apps/jiraimport/tasks.py +277 -0
- trueppm_api/apps/jiraimport/urls.py +15 -0
- trueppm_api/apps/jiraimport/views.py +153 -0
- trueppm_api/apps/msproject/__init__.py +0 -0
- trueppm_api/apps/msproject/apps.py +11 -0
- trueppm_api/apps/msproject/dataclasses.py +153 -0
- trueppm_api/apps/msproject/exporter.py +500 -0
- trueppm_api/apps/msproject/extended_attributes.py +77 -0
- trueppm_api/apps/msproject/importer.py +1095 -0
- trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
- trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
- trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
- trueppm_api/apps/msproject/migrations/__init__.py +0 -0
- trueppm_api/apps/msproject/models.py +81 -0
- trueppm_api/apps/msproject/parser.py +1231 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +436 -0
- trueppm_api/apps/msproject/urls.py +39 -0
- trueppm_api/apps/msproject/views.py +531 -0
- trueppm_api/apps/notifications/__init__.py +7 -0
- trueppm_api/apps/notifications/apps.py +15 -0
- trueppm_api/apps/notifications/backfill.py +79 -0
- trueppm_api/apps/notifications/categories.py +112 -0
- trueppm_api/apps/notifications/delivery_limits.py +258 -0
- trueppm_api/apps/notifications/digests.py +447 -0
- trueppm_api/apps/notifications/email_backend.py +330 -0
- trueppm_api/apps/notifications/email_health.py +128 -0
- trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
- trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +294 -0
- trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
- trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
- trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
- trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
- trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
- trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
- trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
- trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
- trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
- trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
- trueppm_api/apps/notifications/migrations/__init__.py +0 -0
- trueppm_api/apps/notifications/models.py +1197 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +961 -0
- trueppm_api/apps/notifications/services.py +1451 -0
- trueppm_api/apps/notifications/tasks.py +711 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +904 -0
- trueppm_api/apps/observability/__init__.py +0 -0
- trueppm_api/apps/observability/apps.py +31 -0
- trueppm_api/apps/observability/logging.py +213 -0
- trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
- trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
- trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
- trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
- trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
- trueppm_api/apps/observability/migrations/__init__.py +0 -0
- trueppm_api/apps/observability/models.py +141 -0
- trueppm_api/apps/observability/otel/__init__.py +45 -0
- trueppm_api/apps/observability/otel/attributes.py +247 -0
- trueppm_api/apps/observability/otel/export_health.py +533 -0
- trueppm_api/apps/observability/otel/instrumentation.py +351 -0
- trueppm_api/apps/observability/otel/metrics.py +551 -0
- trueppm_api/apps/observability/otel/provider.py +511 -0
- trueppm_api/apps/observability/otel/request_attributes.py +204 -0
- trueppm_api/apps/observability/purge_registry.py +124 -0
- trueppm_api/apps/observability/retention.py +139 -0
- trueppm_api/apps/observability/selectors.py +969 -0
- trueppm_api/apps/observability/serializers.py +142 -0
- trueppm_api/apps/observability/services.py +407 -0
- trueppm_api/apps/observability/tasks.py +266 -0
- trueppm_api/apps/observability/urls.py +27 -0
- trueppm_api/apps/observability/views.py +523 -0
- trueppm_api/apps/profiles/__init__.py +0 -0
- trueppm_api/apps/profiles/apps.py +15 -0
- trueppm_api/apps/profiles/constants.py +54 -0
- trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
- trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
- trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
- trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
- trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
- trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
- trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
- trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
- trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
- trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
- trueppm_api/apps/profiles/migrations/__init__.py +0 -0
- trueppm_api/apps/profiles/models.py +276 -0
- trueppm_api/apps/profiles/serializers.py +199 -0
- trueppm_api/apps/profiles/services.py +466 -0
- trueppm_api/apps/profiles/views.py +124 -0
- trueppm_api/apps/projects/__init__.py +0 -0
- trueppm_api/apps/projects/actual_date_rules.py +163 -0
- trueppm_api/apps/projects/amend.py +133 -0
- trueppm_api/apps/projects/apps.py +38 -0
- trueppm_api/apps/projects/asset_feed.py +459 -0
- trueppm_api/apps/projects/asset_views.py +317 -0
- trueppm_api/apps/projects/attachment_policy.py +278 -0
- trueppm_api/apps/projects/authentication.py +449 -0
- trueppm_api/apps/projects/backfill.py +287 -0
- trueppm_api/apps/projects/backlog_services.py +324 -0
- trueppm_api/apps/projects/backlog_views.py +302 -0
- trueppm_api/apps/projects/batch_operation_views.py +264 -0
- trueppm_api/apps/projects/blocker_services.py +510 -0
- trueppm_api/apps/projects/board_activity.py +462 -0
- trueppm_api/apps/projects/board_activity_views.py +186 -0
- trueppm_api/apps/projects/board_lanes.py +134 -0
- trueppm_api/apps/projects/bulk_settings.py +194 -0
- trueppm_api/apps/projects/bundled_templates.py +215 -0
- trueppm_api/apps/projects/calendar_settings.py +180 -0
- trueppm_api/apps/projects/ceremony_views.py +244 -0
- trueppm_api/apps/projects/commit_moment.py +204 -0
- trueppm_api/apps/projects/config_notice.py +1325 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +93 -0
- trueppm_api/apps/projects/decisions_views.py +94 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +353 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4256 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2000 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1825 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1905 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1409 -0
- trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
- trueppm_api/apps/projects/inbound_sync.py +555 -0
- trueppm_api/apps/projects/iteration_label.py +85 -0
- trueppm_api/apps/projects/lifecycle.py +191 -0
- trueppm_api/apps/projects/management/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
- trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
- trueppm_api/apps/projects/management/commands/export_program.py +50 -0
- trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
- trueppm_api/apps/projects/management/commands/load_sample_project.py +247 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +405 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +178 -0
- trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
- trueppm_api/apps/projects/mcp_settings.py +305 -0
- trueppm_api/apps/projects/methodology.py +178 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5595 -0
- trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
- trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
- trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
- trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
- trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
- trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
- trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
- trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
- trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
- trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
- trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
- trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
- trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
- trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
- trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
- trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
- trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
- trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
- trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
- trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
- trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
- trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
- trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
- trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
- trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
- trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
- trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
- trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
- trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
- trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
- trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
- trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
- trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
- trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
- trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
- trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
- trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
- trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
- trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
- trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
- trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
- trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
- trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
- trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
- trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
- trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
- trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
- trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
- trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
- trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
- trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
- trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
- trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
- trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
- trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
- trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
- trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
- trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
- trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
- trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
- trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
- trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
- trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
- trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
- trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
- trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
- trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
- trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
- trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
- trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
- trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
- trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
- trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
- trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
- trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
- trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
- trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
- trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
- trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
- trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
- trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
- trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
- trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
- trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
- trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
- trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
- trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +44 -0
- trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
- trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
- trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
- trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
- trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
- trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
- trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
- trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
- trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +61 -0
- trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
- trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
- trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
- trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +62 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
- trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
- trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
- trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
- trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
- trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
- trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
- trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
- trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
- trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
- trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
- trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
- trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
- trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
- trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
- trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
- trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
- trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
- trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
- trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
- trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
- trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
- trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
- trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
- trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
- trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
- trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
- trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
- trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
- trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
- trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
- trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
- trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
- trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
- trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
- trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
- trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
- trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +8946 -0
- trueppm_api/apps/projects/poker_services.py +154 -0
- trueppm_api/apps/projects/poker_views.py +398 -0
- trueppm_api/apps/projects/product_backlog_services.py +450 -0
- trueppm_api/apps/projects/program_label_services.py +94 -0
- trueppm_api/apps/projects/program_label_views.py +189 -0
- trueppm_api/apps/projects/program_rollup.py +575 -0
- trueppm_api/apps/projects/program_schedule.py +745 -0
- trueppm_api/apps/projects/program_views.py +3220 -0
- trueppm_api/apps/projects/project_templates.py +422 -0
- trueppm_api/apps/projects/receivers.py +234 -0
- trueppm_api/apps/projects/refusal_codes.py +140 -0
- trueppm_api/apps/projects/reorder_services.py +145 -0
- trueppm_api/apps/projects/restructure_hooks.py +54 -0
- trueppm_api/apps/projects/retro_board_services.py +368 -0
- trueppm_api/apps/projects/retro_services.py +285 -0
- trueppm_api/apps/projects/risk_import.py +396 -0
- trueppm_api/apps/projects/row_identity.py +113 -0
- trueppm_api/apps/projects/schema_migrations.py +228 -0
- trueppm_api/apps/projects/schemas/seed_v1.json +493 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +892 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/exporter.py +1644 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2390 -0
- trueppm_api/apps/projects/seed/reanchor.py +551 -0
- trueppm_api/apps/projects/seed/reldates.py +126 -0
- trueppm_api/apps/projects/seed/replace.py +126 -0
- trueppm_api/apps/projects/seed/replay.py +1691 -0
- trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
- trueppm_api/apps/projects/seed/samples.py +427 -0
- trueppm_api/apps/projects/seed/validation.py +1149 -0
- trueppm_api/apps/projects/serializers.py +12310 -0
- trueppm_api/apps/projects/services.py +6966 -0
- trueppm_api/apps/projects/share_serializers.py +132 -0
- trueppm_api/apps/projects/share_services.py +325 -0
- trueppm_api/apps/projects/share_views.py +374 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +909 -0
- trueppm_api/apps/projects/signal_privacy_views.py +476 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +266 -0
- trueppm_api/apps/projects/sprint_cadence.py +453 -0
- trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
- trueppm_api/apps/projects/standup.py +304 -0
- trueppm_api/apps/projects/standup_views.py +99 -0
- trueppm_api/apps/projects/structural_operation_services.py +727 -0
- trueppm_api/apps/projects/structural_operation_views.py +311 -0
- trueppm_api/apps/projects/surface_visibility.py +126 -0
- trueppm_api/apps/projects/task_batch_services.py +379 -0
- trueppm_api/apps/projects/task_bulk.py +1152 -0
- trueppm_api/apps/projects/task_classification.py +561 -0
- trueppm_api/apps/projects/task_classification_defaults.py +95 -0
- trueppm_api/apps/projects/task_duration_settings.py +142 -0
- trueppm_api/apps/projects/task_grouping.py +754 -0
- trueppm_api/apps/projects/tasks.py +1917 -0
- trueppm_api/apps/projects/template_divergence.py +168 -0
- trueppm_api/apps/projects/template_services.py +160 -0
- trueppm_api/apps/projects/template_tasks.py +258 -0
- trueppm_api/apps/projects/template_views.py +799 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1011 -0
- trueppm_api/apps/projects/utilization.py +885 -0
- trueppm_api/apps/projects/views.py +20572 -0
- trueppm_api/apps/projects/wbs_paths.py +119 -0
- trueppm_api/apps/resources/__init__.py +0 -0
- trueppm_api/apps/resources/apps.py +8 -0
- trueppm_api/apps/resources/capacity.py +121 -0
- trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
- trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
- trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
- trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
- trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
- trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
- trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
- trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
- trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
- trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
- trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
- trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
- trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
- trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
- trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
- trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
- trueppm_api/apps/resources/migrations/__init__.py +0 -0
- trueppm_api/apps/resources/models.py +298 -0
- trueppm_api/apps/resources/serializers.py +426 -0
- trueppm_api/apps/resources/services.py +465 -0
- trueppm_api/apps/resources/urls.py +28 -0
- trueppm_api/apps/resources/views.py +1749 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +119 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
- trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
- trueppm_api/apps/scheduling/graph_guard.py +263 -0
- trueppm_api/apps/scheduling/management/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
- trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
- trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
- trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
- trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
- trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
- trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
- trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
- trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
- trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
- trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
- trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
- trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
- trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
- trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
- trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
- trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +516 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +283 -0
- trueppm_api/apps/scheduling/serializers.py +564 -0
- trueppm_api/apps/scheduling/services.py +1182 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +1984 -0
- trueppm_api/apps/scheduling/telemetry.py +80 -0
- trueppm_api/apps/scheduling/units.py +35 -0
- trueppm_api/apps/scheduling/urls.py +57 -0
- trueppm_api/apps/scheduling/views.py +2564 -0
- trueppm_api/apps/sso/__init__.py +0 -0
- trueppm_api/apps/sso/apps.py +19 -0
- trueppm_api/apps/sso/extensions.py +130 -0
- trueppm_api/apps/sso/management/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
- trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
- trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
- trueppm_api/apps/sso/migrations/__init__.py +0 -0
- trueppm_api/apps/sso/models.py +148 -0
- trueppm_api/apps/sso/serializers.py +437 -0
- trueppm_api/apps/sso/services.py +1385 -0
- trueppm_api/apps/sso/urls.py +47 -0
- trueppm_api/apps/sso/views.py +935 -0
- trueppm_api/apps/sync/__init__.py +0 -0
- trueppm_api/apps/sync/apps.py +15 -0
- trueppm_api/apps/sync/backfill.py +208 -0
- trueppm_api/apps/sync/broadcast.py +320 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +471 -0
- trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
- trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
- trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
- trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
- trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
- trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +225 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +371 -0
- trueppm_api/apps/sync/serializers.py +687 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +501 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +930 -0
- trueppm_api/apps/sync/ws_auth.py +193 -0
- trueppm_api/apps/taskruns/__init__.py +0 -0
- trueppm_api/apps/taskruns/apps.py +11 -0
- trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
- trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
- trueppm_api/apps/taskruns/models.py +68 -0
- trueppm_api/apps/taskruns/serializers.py +92 -0
- trueppm_api/apps/taskruns/tasks.py +60 -0
- trueppm_api/apps/taskruns/tracker.py +286 -0
- trueppm_api/apps/taskruns/urls.py +46 -0
- trueppm_api/apps/taskruns/views.py +195 -0
- trueppm_api/apps/teams/__init__.py +8 -0
- trueppm_api/apps/teams/apps.py +16 -0
- trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
- trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +142 -0
- trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
- trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
- trueppm_api/apps/teams/migrations/__init__.py +0 -0
- trueppm_api/apps/teams/models.py +145 -0
- trueppm_api/apps/teams/permissions.py +149 -0
- trueppm_api/apps/teams/serializers.py +95 -0
- trueppm_api/apps/teams/services.py +434 -0
- trueppm_api/apps/teams/signals.py +61 -0
- trueppm_api/apps/teams/urls.py +34 -0
- trueppm_api/apps/teams/views.py +201 -0
- trueppm_api/apps/timetracking/__init__.py +0 -0
- trueppm_api/apps/timetracking/apps.py +9 -0
- trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
- trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
- trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
- trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
- trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
- trueppm_api/apps/timetracking/models.py +178 -0
- trueppm_api/apps/timetracking/serializers.py +175 -0
- trueppm_api/apps/timetracking/services.py +117 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +526 -0
- trueppm_api/apps/webhooks/__init__.py +0 -0
- trueppm_api/apps/webhooks/apps.py +8 -0
- trueppm_api/apps/webhooks/backfill.py +90 -0
- trueppm_api/apps/webhooks/dispatch.py +144 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +243 -0
- trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
- trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
- trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
- trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
- trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
- trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
- trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
- trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +90 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +372 -0
- trueppm_api/apps/webhooks/serializers.py +318 -0
- trueppm_api/apps/webhooks/tasks.py +663 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +383 -0
- trueppm_api/apps/workflow_engine/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/apps.py +23 -0
- trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
- trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
- trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
- trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
- trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/models.py +265 -0
- trueppm_api/apps/workflow_engine/tasks.py +368 -0
- trueppm_api/apps/workspace/__init__.py +0 -0
- trueppm_api/apps/workspace/apps.py +10 -0
- trueppm_api/apps/workspace/backfill.py +111 -0
- trueppm_api/apps/workspace/export.py +242 -0
- trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
- trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
- trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
- trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
- trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
- trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
- trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
- trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
- trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
- trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
- trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
- trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
- trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
- trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
- trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
- trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
- trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
- trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
- trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
- trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
- trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
- trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
- trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
- trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
- trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
- trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
- trueppm_api/apps/workspace/migrations/__init__.py +0 -0
- trueppm_api/apps/workspace/models.py +857 -0
- trueppm_api/apps/workspace/permissions.py +187 -0
- trueppm_api/apps/workspace/serializers.py +590 -0
- trueppm_api/apps/workspace/services.py +919 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +555 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1502 -0
- trueppm_api/asgi.py +42 -0
- trueppm_api/celery.py +31 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +916 -0
- trueppm_api/core/constant_time.py +51 -0
- trueppm_api/core/csp.py +54 -0
- trueppm_api/core/db.py +87 -0
- trueppm_api/core/db_session.py +37 -0
- trueppm_api/core/exception_handlers.py +157 -0
- trueppm_api/core/export_downloads.py +56 -0
- trueppm_api/core/extension_providers.py +56 -0
- trueppm_api/core/extension_signals.py +147 -0
- trueppm_api/core/idempotent.py +260 -0
- trueppm_api/core/metadata.py +42 -0
- trueppm_api/core/middleware.py +51 -0
- trueppm_api/core/openapi.py +949 -0
- trueppm_api/core/password_reset.py +500 -0
- trueppm_api/core/protect_conflict.py +100 -0
- trueppm_api/core/ratelimit.py +148 -0
- trueppm_api/core/redis_throttle.py +82 -0
- trueppm_api/core/refresh_cookie_policy.py +156 -0
- trueppm_api/core/request_body.py +86 -0
- trueppm_api/core/security_checks.py +884 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +344 -0
- trueppm_api/core/valkey.py +214 -0
- trueppm_api/core/valkey_cache.py +66 -0
- trueppm_api/core/valkey_checks.py +104 -0
- trueppm_api/core/valkey_config.py +39 -0
- trueppm_api/core/worker_broker_wait.py +149 -0
- trueppm_api/core/worker_heartbeat.py +134 -0
- trueppm_api/fields.py +36 -0
- trueppm_api/permissions.py +30 -0
- trueppm_api/py.typed +0 -0
- trueppm_api/routing.py +11 -0
- trueppm_api/settings/__init__.py +0 -0
- trueppm_api/settings/base.py +2455 -0
- trueppm_api/settings/dev.py +273 -0
- trueppm_api/settings/prod.py +228 -0
- trueppm_api/urls.py +153 -0
- trueppm_api/workflows/__init__.py +38 -0
- trueppm_api/workflows/backends/__init__.py +6 -0
- trueppm_api/workflows/backends/default.py +297 -0
- trueppm_api/workflows/consumers/__init__.py +11 -0
- trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
- trueppm_api/workflows/interface.py +148 -0
- trueppm_api/workflows/registry.py +106 -0
- trueppm_api/workflows/services.py +90 -0
- trueppm_api/wsgi.py +11 -0
- trueppm_api-0.4.0b2.dist-info/METADATA +70 -0
- trueppm_api-0.4.0b2.dist-info/RECORD +759 -0
- trueppm_api-0.4.0b2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
"""Celery tasks for outbound webhook delivery."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import urllib.error
|
|
10
|
+
import urllib.request
|
|
11
|
+
from datetime import timedelta
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from celery import shared_task
|
|
15
|
+
from django.utils import timezone
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING: # pragma: no cover — import cycle: models imports nothing here
|
|
18
|
+
from trueppm_api.apps.webhooks.models import Webhook, WebhookDelivery
|
|
19
|
+
|
|
20
|
+
from trueppm_api.apps.integrations.encryption import CredentialEncryptionError
|
|
21
|
+
from trueppm_api.apps.integrations.http import (
|
|
22
|
+
EgressBlocked,
|
|
23
|
+
EgressError,
|
|
24
|
+
NoRedirectHandler,
|
|
25
|
+
assert_url_allowed,
|
|
26
|
+
)
|
|
27
|
+
from trueppm_api.core.idempotent import idempotent_task
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
# Deliveries created more than this many minutes ago with attempt_count == 0
|
|
32
|
+
# are considered stranded (the .delay() call was lost) and re-dispatched by the
|
|
33
|
+
# drain. Deliveries created *within* the window are left alone — they may still
|
|
34
|
+
# be inside a transaction.on_commit() pipeline.
|
|
35
|
+
_DRAIN_ORPHAN_MINUTES = 5
|
|
36
|
+
|
|
37
|
+
# Exponential backoff. The countdown is 30 * 2**(attempt_count - 1), and a
|
|
38
|
+
# delivery is terminal once attempt_count reaches _MAX_RETRIES — so only FOUR
|
|
39
|
+
# countdowns are ever scheduled (30, 60, 120, 240) and the five attempts execute at
|
|
40
|
+
# t = 0, 30, 90, 210, 450 seconds. Total span 450s (7.5 min), not the 15.5 min the
|
|
41
|
+
# full 30/60/120/240/480 sequence would imply: the 480s step is unreachable because
|
|
42
|
+
# the attempt it would delay is the one that fails terminally instead.
|
|
43
|
+
_MAX_RETRIES = 5
|
|
44
|
+
_BACKOFF_BASE = 30
|
|
45
|
+
|
|
46
|
+
# Redirect-disabled opener (#808). The default urllib opener follows 3xx
|
|
47
|
+
# responses to arbitrary destinations, which would let a malicious receiver
|
|
48
|
+
# bounce a signed webhook payload at a private/loopback/cloud-metadata host
|
|
49
|
+
# *after* assert_url_allowed has validated the original URL. We share the same
|
|
50
|
+
# no-redirect handler used by the integrations SSRF chokepoint so the guard is
|
|
51
|
+
# implemented once.
|
|
52
|
+
_no_redirect_opener = urllib.request.build_opener(NoRedirectHandler)
|
|
53
|
+
|
|
54
|
+
# 4xx statuses that are nonetheless worth retrying (#2884). Everything else in
|
|
55
|
+
# the 4xx range is a *permanent* client error — the request itself is wrong, so
|
|
56
|
+
# replaying it byte-for-byte cannot succeed. Before this split, a revoked Slack
|
|
57
|
+
# incoming webhook answering 404/410 burned five attempts over ~7.5 minutes for
|
|
58
|
+
# every matching event, forever, with no state change on the subscription.
|
|
59
|
+
# 408 Request Timeout / 425 Too Early — the receiver is asking for a retry.
|
|
60
|
+
# 429 Too Many Requests — rate limited; the backoff is exactly the remedy.
|
|
61
|
+
_RETRYABLE_4XX = frozenset({408, 425, 429})
|
|
62
|
+
|
|
63
|
+
# Clock skew tolerance we recommend consumers apply to X-TruePPM-Signature-V2's
|
|
64
|
+
# timestamp (ADR-0837). Documented here because the value only has meaning as a
|
|
65
|
+
# pair with the retry backoff below: the signature is recomputed on *every*
|
|
66
|
+
# attempt, so the fifth and final attempt — 450s (7.5 min) after the first, well
|
|
67
|
+
# outside this window — still carries a fresh timestamp and is never stale.
|
|
68
|
+
SIGNATURE_TOLERANCE_SECONDS = 300
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class WebhookDeliveryFailed(Exception):
|
|
72
|
+
"""A webhook delivery reached a terminal failure and will not be retried.
|
|
73
|
+
|
|
74
|
+
Carried into ``record_failed_task`` so the delivery shows up in the
|
|
75
|
+
dead-letter queue with a readable cause. Never raised out of the task — the
|
|
76
|
+
delivery row, not an exception, is the record of what happened.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _is_permanent_http_status(status_code: int) -> bool:
|
|
81
|
+
"""Whether ``status_code`` means "do not retry this delivery" (#2884)."""
|
|
82
|
+
return 400 <= status_code < 500 and status_code not in _RETRYABLE_4XX
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _sign(secret: str, body: bytes, timestamp: str) -> tuple[str, str]:
|
|
86
|
+
"""Return the (legacy, timestamped) signature values for one delivery.
|
|
87
|
+
|
|
88
|
+
Two signatures are emitted during the deprecation window (ADR-0837):
|
|
89
|
+
|
|
90
|
+
- **legacy** — ``HMAC-SHA256(secret, body)``, the recipe published since 0.1
|
|
91
|
+
and carried in ``X-TruePPM-Signature`` as ``sha256=<hex>``. It has no
|
|
92
|
+
timestamp, so a captured delivery verifies forever; a receiver that stores
|
|
93
|
+
seen ``X-TruePPM-Delivery`` ids is protected against duplicate *processing*
|
|
94
|
+
but cannot *detect* a replay.
|
|
95
|
+
- **timestamped** — ``HMAC-SHA256(secret, "<unix>.<body>")``, carried in
|
|
96
|
+
``X-TruePPM-Signature-V2`` as ``t=<unix>,v1=<hex>``. Binding the timestamp
|
|
97
|
+
into the signed material is what makes it unforgeable: an attacker replaying
|
|
98
|
+
a captured body cannot move the timestamp forward without the secret, so a
|
|
99
|
+
consumer rejecting timestamps outside ``SIGNATURE_TOLERANCE_SECONDS`` bounds
|
|
100
|
+
the replay window. ``v1`` names the HMAC scheme *inside* the header (Stripe's
|
|
101
|
+
convention, which integrators recognize); the recipe version is the header
|
|
102
|
+
name.
|
|
103
|
+
"""
|
|
104
|
+
key = secret.encode("utf-8")
|
|
105
|
+
legacy = hmac.new(key, body, hashlib.sha256).hexdigest()
|
|
106
|
+
timestamped = hmac.new(
|
|
107
|
+
key,
|
|
108
|
+
timestamp.encode("ascii") + b"." + body,
|
|
109
|
+
hashlib.sha256,
|
|
110
|
+
).hexdigest()
|
|
111
|
+
return legacy, timestamped
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _dead_letter_delivery(
|
|
115
|
+
task: object,
|
|
116
|
+
delivery_id: str,
|
|
117
|
+
project_id: str | None,
|
|
118
|
+
reason: str,
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Park a terminally-failed webhook delivery in the dead-letter queue (#2884).
|
|
121
|
+
|
|
122
|
+
Every terminal branch below marks the delivery FAILED and *returns* — the task
|
|
123
|
+
never raises, deliberately, because a raise would put Celery's retry machinery
|
|
124
|
+
back in charge of something we have already decided not to retry. But
|
|
125
|
+
dead-letter recording keys on Celery permanent failure, so before this existed
|
|
126
|
+
``trueppm_task_dead_letter_parked`` stayed at zero no matter how many
|
|
127
|
+
deliveries died: no metric, no ``dead-letter alert:`` log line, and the only
|
|
128
|
+
evidence purged after 7 days.
|
|
129
|
+
|
|
130
|
+
Calling ``record_failed_task`` directly gives the FAILED state a real consumer
|
|
131
|
+
— a ``FailedTask`` row, the ``celery_task_permanently_failed`` extension signal
|
|
132
|
+
(so the OSS alert receiver logs and Enterprise's PagerDuty receiver fires), and
|
|
133
|
+
the health-endpoint counter — without pretending the task crashed.
|
|
134
|
+
|
|
135
|
+
The ``task_id`` falls back to a delivery-derived key when there is no live
|
|
136
|
+
Celery request (the drain and the test suite call the task synchronously), so
|
|
137
|
+
repeated failures of the same delivery bump ``failure_count`` on one row rather
|
|
138
|
+
than flooding the queue.
|
|
139
|
+
|
|
140
|
+
``task_name`` and ``args`` are a **replay record, not display metadata**: the
|
|
141
|
+
dead-letter admin's requeue action feeds them straight to
|
|
142
|
+
``current_app.send_task(failed.task_name, args=..., kwargs=...)``. So
|
|
143
|
+
``task_name`` must be the *registered* name — ``deliver_webhook.name``, read off
|
|
144
|
+
the task rather than written out, because this module's other two tasks declare
|
|
145
|
+
an explicit short ``name=`` and this one does not — and ``args``/``kwargs`` must
|
|
146
|
+
bind to ``deliver_webhook.run(delivery_id)``. Getting either wrong makes a
|
|
147
|
+
requeue a silent no-op (``NotRegistered``, message dropped) or a worker-side
|
|
148
|
+
``TypeError`` while the endpoint answers 200 and stamps the row ``RETRIED`` —
|
|
149
|
+
which is the same "the diagnostic tool reports success on a guaranteed failure"
|
|
150
|
+
shape #2884 exists to remove. The webhook id therefore travels in ``reason``,
|
|
151
|
+
not as an invented kwarg.
|
|
152
|
+
"""
|
|
153
|
+
from trueppm_api.apps.scheduling.deadletter import record_failed_task
|
|
154
|
+
|
|
155
|
+
request = getattr(task, "request", None)
|
|
156
|
+
task_id = getattr(request, "id", None) or f"webhook-delivery-{delivery_id}"
|
|
157
|
+
try:
|
|
158
|
+
record_failed_task(
|
|
159
|
+
task_name=deliver_webhook.name,
|
|
160
|
+
task_id=str(task_id),
|
|
161
|
+
args=[delivery_id],
|
|
162
|
+
kwargs={},
|
|
163
|
+
exception=WebhookDeliveryFailed(reason),
|
|
164
|
+
project_id=project_id,
|
|
165
|
+
)
|
|
166
|
+
except Exception:
|
|
167
|
+
# Dead-letter recording is observability, not delivery: a failure here
|
|
168
|
+
# must never mask the delivery outcome we just persisted.
|
|
169
|
+
logger.exception("deliver_webhook: could not dead-letter delivery %s", delivery_id)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _fail_delivery(
|
|
173
|
+
task: object,
|
|
174
|
+
delivery: WebhookDelivery,
|
|
175
|
+
webhook: Webhook,
|
|
176
|
+
*,
|
|
177
|
+
reason: str,
|
|
178
|
+
operator_reason: str = "",
|
|
179
|
+
extra_fields: tuple[str, ...] = (),
|
|
180
|
+
count_against_guard: bool = True,
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Mark a delivery terminally failed and run every consequence of that (#2884).
|
|
183
|
+
|
|
184
|
+
One helper for all six terminal branches (SSRF block, unusable signing secret,
|
|
185
|
+
unresolvable host with retries exhausted, network error with retries exhausted,
|
|
186
|
+
permanent 4xx, non-2xx with retries exhausted) so none of them can drift out of
|
|
187
|
+
sync with the others — which is exactly how the dead-letter gap arose.
|
|
188
|
+
|
|
189
|
+
``count_against_guard=False`` still records and parks the failure but keeps it
|
|
190
|
+
out of the consecutive-failure counter. Two branches use it, for the same
|
|
191
|
+
reason: the counter is meant to measure *the receiver's* health under production
|
|
192
|
+
traffic, and neither of those failures is evidence of that.
|
|
193
|
+
|
|
194
|
+
``reason`` lands in ``Webhook.last_failure_reason``, which is readable at
|
|
195
|
+
**Viewer** level, so it must stay free of deployment detail. ``operator_reason``
|
|
196
|
+
overrides it for the dead-letter record only — the Admin/operator surface — the
|
|
197
|
+
same split ``validate_url`` already makes between its curated 400 message and its
|
|
198
|
+
server-side log line.
|
|
199
|
+
"""
|
|
200
|
+
from trueppm_api.apps.webhooks.models import PING_EVENT_TYPE, DeliveryStatus
|
|
201
|
+
|
|
202
|
+
delivery.status = DeliveryStatus.FAILED
|
|
203
|
+
delivery.completed_at = timezone.now()
|
|
204
|
+
delivery.save(update_fields=[*extra_fields, "status", "completed_at"])
|
|
205
|
+
|
|
206
|
+
# A test ping is a human probe, not production traffic. Counting it made the
|
|
207
|
+
# admin's own debugging loop the trip wire: five clicks on Test against a broken
|
|
208
|
+
# receiver deactivated the subscription, which is the opposite of what a
|
|
209
|
+
# diagnostic tool should do.
|
|
210
|
+
is_probe = delivery.event_type == PING_EVENT_TYPE
|
|
211
|
+
auto_disabled = False
|
|
212
|
+
if count_against_guard and not is_probe:
|
|
213
|
+
auto_disabled = webhook.record_delivery_failure(reason)
|
|
214
|
+
|
|
215
|
+
if auto_disabled:
|
|
216
|
+
logger.warning(
|
|
217
|
+
"deliver_webhook: webhook %s auto-deactivated after %d consecutive "
|
|
218
|
+
"failed deliveries (%s)",
|
|
219
|
+
webhook.pk,
|
|
220
|
+
webhook.consecutive_failures,
|
|
221
|
+
reason,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# The deactivation is folded into the dead-letter reason rather than only logged.
|
|
225
|
+
# Once a subscription is inactive the `not is_active` branch above deliberately
|
|
226
|
+
# stops parking its deliveries, so the dead-letter stream — the only push signal
|
|
227
|
+
# an operator has — goes quiet at exactly the moment the integration dies. That
|
|
228
|
+
# silence needs an explanation, and this record is the last one before it: it is
|
|
229
|
+
# what makes "the alerts stopped" readable as "we gave up on this webhook"
|
|
230
|
+
# rather than as "the receiver recovered".
|
|
231
|
+
dead_letter_reason = f"webhook {webhook.pk}: {operator_reason or reason}"
|
|
232
|
+
if auto_disabled:
|
|
233
|
+
dead_letter_reason += (
|
|
234
|
+
" — SUBSCRIPTION DEACTIVATED; no further deliveries will be attempted "
|
|
235
|
+
"or reported until an admin re-enables it"
|
|
236
|
+
)
|
|
237
|
+
_dead_letter_delivery(
|
|
238
|
+
task,
|
|
239
|
+
str(delivery.pk),
|
|
240
|
+
str(webhook.project_id) if webhook.project_id else None,
|
|
241
|
+
dead_letter_reason,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _resolve_signing_secret(webhook: Webhook) -> tuple[str, str, str]:
|
|
246
|
+
"""Return ``(secret, viewer_error, operator_error)``; a non-empty error is terminal.
|
|
247
|
+
|
|
248
|
+
``viewer_error`` is curated for ``Webhook.last_failure_reason`` (Viewer-readable);
|
|
249
|
+
``operator_error`` carries the deployment-level detail for the dead-letter record.
|
|
250
|
+
"""
|
|
251
|
+
try:
|
|
252
|
+
secret = webhook.secret
|
|
253
|
+
except CredentialEncryptionError:
|
|
254
|
+
logger.exception("deliver_webhook: webhook %s signing secret is undecryptable", webhook.pk)
|
|
255
|
+
secret = ""
|
|
256
|
+
# Curated, deliberately vague. `last_failure_reason` is readable at Viewer
|
|
257
|
+
# level, and "your INTEGRATION_ENCRYPTION_KEY changed" is a deployment-level
|
|
258
|
+
# diagnostic — the same reasoning that put the delivery log behind Admin
|
|
259
|
+
# (#903). The operator detail is in the logger.exception above and in the
|
|
260
|
+
# dead-letter record; the Viewer-visible field says only that it is broken.
|
|
261
|
+
secret_error = "Signing secret is unusable — contact an administrator"
|
|
262
|
+
secret_operator_error = (
|
|
263
|
+
"Signing secret could not be decrypted — INTEGRATION_ENCRYPTION_KEY has "
|
|
264
|
+
"changed since this webhook was registered"
|
|
265
|
+
)
|
|
266
|
+
else:
|
|
267
|
+
secret_error = "" if secret else "No signing secret is stored for this webhook"
|
|
268
|
+
secret_operator_error = ""
|
|
269
|
+
return secret, secret_error, secret_operator_error
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _retry_or_dead_letter(
|
|
273
|
+
task: object,
|
|
274
|
+
delivery: WebhookDelivery,
|
|
275
|
+
webhook: Webhook,
|
|
276
|
+
*,
|
|
277
|
+
reason: str,
|
|
278
|
+
fields: tuple[str, ...],
|
|
279
|
+
) -> None:
|
|
280
|
+
"""Dead-letter the delivery when attempts are exhausted, else save it and retry.
|
|
281
|
+
|
|
282
|
+
``fields`` is both the ``update_fields`` for the pre-retry save and the
|
|
283
|
+
``extra_fields`` for the terminal failure write. Raises ``Retry`` (via
|
|
284
|
+
``task.retry``) when attempts remain; returns only after the terminal failure.
|
|
285
|
+
"""
|
|
286
|
+
if delivery.attempt_count >= _MAX_RETRIES:
|
|
287
|
+
_fail_delivery(task, delivery, webhook, reason=reason, extra_fields=fields)
|
|
288
|
+
return
|
|
289
|
+
delivery.save(update_fields=list(fields))
|
|
290
|
+
raise task.retry( # type: ignore[attr-defined]
|
|
291
|
+
countdown=_BACKOFF_BASE * (2 ** (delivery.attempt_count - 1)),
|
|
292
|
+
) from None
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@shared_task( # type: ignore[untyped-decorator]
|
|
296
|
+
bind=True,
|
|
297
|
+
max_retries=_MAX_RETRIES,
|
|
298
|
+
retry_backoff=_BACKOFF_BASE,
|
|
299
|
+
retry_backoff_max=600,
|
|
300
|
+
retry_jitter=True,
|
|
301
|
+
soft_time_limit=30,
|
|
302
|
+
time_limit=60,
|
|
303
|
+
acks_late=True,
|
|
304
|
+
reject_on_worker_lost=True,
|
|
305
|
+
)
|
|
306
|
+
def deliver_webhook(self: object, delivery_id: str) -> None:
|
|
307
|
+
"""POST a webhook payload to the registered URL with HMAC-SHA256 signatures.
|
|
308
|
+
|
|
309
|
+
Retries with exponential backoff on network errors, 5xx responses, and the
|
|
310
|
+
retryable 4xx set. A *permanent* 4xx is not retried (#2884). Every terminal
|
|
311
|
+
outcome marks the delivery failed, counts against the subscription's
|
|
312
|
+
consecutive-failure guard, and is parked in the dead-letter queue.
|
|
313
|
+
"""
|
|
314
|
+
from trueppm_api.apps.webhooks.models import DeliveryStatus, WebhookDelivery
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
delivery = WebhookDelivery.objects.select_related("webhook").get(pk=delivery_id)
|
|
318
|
+
except WebhookDelivery.DoesNotExist:
|
|
319
|
+
logger.warning("deliver_webhook: delivery %s not found, skipping", delivery_id)
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
webhook = delivery.webhook
|
|
323
|
+
if not webhook.is_active:
|
|
324
|
+
# Not counted against the failure guard and not dead-lettered: an inactive
|
|
325
|
+
# subscription is a *deliberate* state (an admin paused it, or the guard
|
|
326
|
+
# already gave up on it), so re-parking every skipped delivery would bury
|
|
327
|
+
# the genuine failures under noise from a webhook nobody expects to fire.
|
|
328
|
+
logger.info("deliver_webhook: webhook %s is inactive, skipping", webhook.pk)
|
|
329
|
+
delivery.status = DeliveryStatus.FAILED
|
|
330
|
+
delivery.completed_at = timezone.now()
|
|
331
|
+
delivery.save(update_fields=["status", "completed_at"])
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
body = json.dumps(delivery.payload, default=str, sort_keys=True).encode("utf-8")
|
|
335
|
+
|
|
336
|
+
# An unusable secret has two shapes and BOTH must be terminal here.
|
|
337
|
+
#
|
|
338
|
+
# Empty ciphertext is unreachable through the API (the serializer requires or
|
|
339
|
+
# generates a secret) but possible for a row whose backfill left it blank.
|
|
340
|
+
# Undecryptable ciphertext is the likelier one: it is what a database restored
|
|
341
|
+
# without its INTEGRATION_ENCRYPTION_KEY, or a key rotated without the matching
|
|
342
|
+
# re-encrypt, produces — and ``secret_ciphertext`` tests non-empty, so
|
|
343
|
+
# ``secret_set`` still reports True.
|
|
344
|
+
#
|
|
345
|
+
# Letting the decryption error propagate would be the worst available outcome:
|
|
346
|
+
# it escapes before ``attempt_count`` is incremented, so the row keeps
|
|
347
|
+
# (PENDING, attempt_count=0) — exactly the selector ``_do_drain_webhooks`` uses
|
|
348
|
+
# — and the drain re-dispatches it every 30 seconds forever. The purge only
|
|
349
|
+
# reclaims terminal rows, the failure guard never counts it, and the API keeps
|
|
350
|
+
# reporting the subscription healthy. Signing with an empty key is no better: a
|
|
351
|
+
# receiver would either skip verification or trust ``b""``.
|
|
352
|
+
secret, secret_error, secret_operator_error = _resolve_signing_secret(webhook)
|
|
353
|
+
|
|
354
|
+
if secret_error:
|
|
355
|
+
delivery.attempt_count += 1
|
|
356
|
+
_fail_delivery(
|
|
357
|
+
self,
|
|
358
|
+
delivery,
|
|
359
|
+
webhook,
|
|
360
|
+
reason=secret_error,
|
|
361
|
+
operator_reason=secret_operator_error,
|
|
362
|
+
extra_fields=("attempt_count",),
|
|
363
|
+
)
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
# Recomputed on every attempt, not once per delivery: the final attempt lands
|
|
367
|
+
# 450s after the first, so a timestamp fixed at first-attempt time would put it
|
|
368
|
+
# outside the recommended 300s tolerance and a consumer would reject a
|
|
369
|
+
# legitimate redelivery as stale.
|
|
370
|
+
timestamp = str(int(timezone.now().timestamp()))
|
|
371
|
+
signature, timestamped_signature = _sign(secret, body, timestamp)
|
|
372
|
+
|
|
373
|
+
delivery.attempt_count += 1
|
|
374
|
+
|
|
375
|
+
# SSRF guard (ADR-0049 §3): Webhook.url is configured by a project admin —
|
|
376
|
+
# an RBAC role, not an infra operator — so outbound delivery is untrusted
|
|
377
|
+
# egress. Re-resolve and reject private / loopback / link-local targets at
|
|
378
|
+
# delivery time (not only at registration) to close the DNS-rebinding
|
|
379
|
+
# window, reusing the same chokepoint as the integrations PAT / git-link
|
|
380
|
+
# calls. Without this, an admin could point a webhook at 169.254.169.254
|
|
381
|
+
# (cloud metadata) or an RFC1918 cluster service.
|
|
382
|
+
try:
|
|
383
|
+
assert_url_allowed(webhook.url)
|
|
384
|
+
except EgressBlocked as exc:
|
|
385
|
+
# Permanent: a private/loopback/bad-scheme URL will never be deliverable.
|
|
386
|
+
logger.warning(
|
|
387
|
+
"deliver_webhook: delivery %s blocked by SSRF guard: %s",
|
|
388
|
+
delivery.pk,
|
|
389
|
+
exc,
|
|
390
|
+
)
|
|
391
|
+
_fail_delivery(
|
|
392
|
+
self,
|
|
393
|
+
delivery,
|
|
394
|
+
webhook,
|
|
395
|
+
reason="Blocked by the SSRF egress guard (URL resolves to a "
|
|
396
|
+
"private, loopback, or link-local address)",
|
|
397
|
+
extra_fields=("attempt_count",),
|
|
398
|
+
# Kept out of the auto-disable counter: assert_url_allowed re-resolves
|
|
399
|
+
# DNS on every delivery, so anyone with influence over how the receiver's
|
|
400
|
+
# hostname resolves — a dynamic-DNS receiver, a poisoned resolver, a
|
|
401
|
+
# momentary RFC1918 answer — could otherwise deactivate an admin's
|
|
402
|
+
# integration in five resolution windows. The block itself still stands
|
|
403
|
+
# (the delivery is refused and parked); it just is not evidence about the
|
|
404
|
+
# receiver's health.
|
|
405
|
+
count_against_guard=False,
|
|
406
|
+
)
|
|
407
|
+
return
|
|
408
|
+
except EgressError as exc:
|
|
409
|
+
# Host did not resolve — transient, treated like any network error: retry.
|
|
410
|
+
logger.warning(
|
|
411
|
+
"deliver_webhook: delivery %s SSRF pre-check could not resolve host: %s "
|
|
412
|
+
"(attempt %d/%d)",
|
|
413
|
+
delivery.pk,
|
|
414
|
+
exc,
|
|
415
|
+
delivery.attempt_count,
|
|
416
|
+
_MAX_RETRIES,
|
|
417
|
+
)
|
|
418
|
+
_retry_or_dead_letter(
|
|
419
|
+
self,
|
|
420
|
+
delivery,
|
|
421
|
+
webhook,
|
|
422
|
+
reason=f"Host did not resolve after {_MAX_RETRIES} attempts",
|
|
423
|
+
fields=("attempt_count",),
|
|
424
|
+
)
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
req = urllib.request.Request(
|
|
428
|
+
webhook.url,
|
|
429
|
+
data=body,
|
|
430
|
+
headers={
|
|
431
|
+
"Content-Type": "application/json",
|
|
432
|
+
# Legacy body-only signature (ADR-0837). Still emitted through the
|
|
433
|
+
# deprecation window so every receiver implementing the 0.1–0.3
|
|
434
|
+
# published recipe keeps verifying without a code change.
|
|
435
|
+
"X-TruePPM-Signature": f"sha256={signature}",
|
|
436
|
+
# Timestamped signature — the recipe 0.4 freezes. `t` is bound into
|
|
437
|
+
# the signed material, so it cannot be moved by a replaying attacker.
|
|
438
|
+
#
|
|
439
|
+
# The timestamp is deliberately NOT also broken out into a standalone
|
|
440
|
+
# header. A convenience copy would be unauthenticated, and a consumer
|
|
441
|
+
# bounding its replay window on it gains nothing: an attacker replaying
|
|
442
|
+
# a capture edits that one header to "now", the freshness check passes,
|
|
443
|
+
# and the HMAC still verifies over the original `t` inside this header.
|
|
444
|
+
# The whole protection would be voided by a one-header edit, so the only
|
|
445
|
+
# timestamp we publish is the signed one (ADR-0837).
|
|
446
|
+
"X-TruePPM-Signature-V2": f"t={timestamp},v1={timestamped_signature}",
|
|
447
|
+
"X-TruePPM-Event": delivery.event_type,
|
|
448
|
+
"X-TruePPM-Delivery": str(delivery.pk),
|
|
449
|
+
# Per-subscription monotonic sequence (#664). Stable across retries —
|
|
450
|
+
# the same delivery row keeps its number. Consumers MAY use it to
|
|
451
|
+
# detect gaps (a missing number signals a lost event) and reorder
|
|
452
|
+
# events that arrive out of order. It is a hint, not a strict-order
|
|
453
|
+
# or exactly-once guarantee.
|
|
454
|
+
"X-TruePPM-Webhook-Sequence": str(delivery.sequence_number),
|
|
455
|
+
},
|
|
456
|
+
method="POST",
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
# Redirect-disabled opener (#808): a webhook receiver returning
|
|
461
|
+
# 302/307 Location: http://169.254.169.254/... (cloud metadata) or an
|
|
462
|
+
# RFC1918 host would otherwise be re-fetched *without* re-running the
|
|
463
|
+
# SSRF guard, with the original signed payload preserved on a 307/308.
|
|
464
|
+
# NoRedirectHandler surfaces the 3xx as-is so callers see it and retry
|
|
465
|
+
# via the existing non-2xx path.
|
|
466
|
+
with _no_redirect_opener.open(req, timeout=10) as resp: # nosec B310 — URL passed the assert_url_allowed() SSRF guard above (ADR-0049 §3); redirects disabled
|
|
467
|
+
status_code = resp.status
|
|
468
|
+
except urllib.error.HTTPError as exc:
|
|
469
|
+
status_code = exc.code
|
|
470
|
+
except (urllib.error.URLError, OSError) as exc:
|
|
471
|
+
# Network error — retry.
|
|
472
|
+
logger.warning(
|
|
473
|
+
"deliver_webhook: delivery %s network error: %s (attempt %d/%d)",
|
|
474
|
+
delivery.pk,
|
|
475
|
+
exc,
|
|
476
|
+
delivery.attempt_count,
|
|
477
|
+
_MAX_RETRIES,
|
|
478
|
+
)
|
|
479
|
+
_retry_or_dead_letter(
|
|
480
|
+
self,
|
|
481
|
+
delivery,
|
|
482
|
+
webhook,
|
|
483
|
+
reason=f"Network error after {_MAX_RETRIES} attempts: {type(exc).__name__}",
|
|
484
|
+
fields=("attempt_count",),
|
|
485
|
+
)
|
|
486
|
+
return
|
|
487
|
+
|
|
488
|
+
delivery.response_status = status_code
|
|
489
|
+
|
|
490
|
+
if 200 <= status_code < 300:
|
|
491
|
+
delivery.status = DeliveryStatus.SUCCESS
|
|
492
|
+
delivery.completed_at = timezone.now()
|
|
493
|
+
delivery.save(update_fields=["status", "response_status", "attempt_count", "completed_at"])
|
|
494
|
+
# Clears the consecutive-failure counter, so a receiver that recovers
|
|
495
|
+
# inside the guard's threshold never gets auto-deactivated.
|
|
496
|
+
webhook.record_delivery_success()
|
|
497
|
+
return
|
|
498
|
+
|
|
499
|
+
if _is_permanent_http_status(status_code):
|
|
500
|
+
# Permanent client error: the receiver rejected the request itself, so
|
|
501
|
+
# replaying identical bytes cannot succeed (#2884). One attempt, then
|
|
502
|
+
# terminal — this is where the "revoked receiver answers 410 and we retry
|
|
503
|
+
# it forever" loop is cut.
|
|
504
|
+
logger.warning(
|
|
505
|
+
"deliver_webhook: delivery %s got permanent HTTP %d from %s — not retrying",
|
|
506
|
+
delivery.pk,
|
|
507
|
+
status_code,
|
|
508
|
+
webhook.url,
|
|
509
|
+
)
|
|
510
|
+
_fail_delivery(
|
|
511
|
+
self,
|
|
512
|
+
delivery,
|
|
513
|
+
webhook,
|
|
514
|
+
reason=f"Permanent client error HTTP {status_code} — not retried",
|
|
515
|
+
extra_fields=("response_status", "attempt_count"),
|
|
516
|
+
)
|
|
517
|
+
return
|
|
518
|
+
|
|
519
|
+
# Retryable non-2xx (5xx, 408/425/429, or a 3xx the no-redirect opener
|
|
520
|
+
# surfaced): retry if attempts remain.
|
|
521
|
+
logger.warning(
|
|
522
|
+
"deliver_webhook: delivery %s got HTTP %d from %s (attempt %d/%d)",
|
|
523
|
+
delivery.pk,
|
|
524
|
+
status_code,
|
|
525
|
+
webhook.url,
|
|
526
|
+
delivery.attempt_count,
|
|
527
|
+
_MAX_RETRIES,
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
_retry_or_dead_letter(
|
|
531
|
+
self,
|
|
532
|
+
delivery,
|
|
533
|
+
webhook,
|
|
534
|
+
reason=f"HTTP {status_code} after {_MAX_RETRIES} attempts",
|
|
535
|
+
fields=("response_status", "attempt_count"),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
# ---------------------------------------------------------------------------
|
|
540
|
+
# Webhook delivery drain
|
|
541
|
+
# ---------------------------------------------------------------------------
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _do_drain_webhooks() -> None:
|
|
545
|
+
"""Dispatch any WebhookDelivery rows that were never enqueued.
|
|
546
|
+
|
|
547
|
+
A delivery row is considered stranded when:
|
|
548
|
+
- status is PENDING (not yet successfully delivered or failed)
|
|
549
|
+
- attempt_count == 0 (the .delay() call after creation was lost)
|
|
550
|
+
- created_at is older than _DRAIN_ORPHAN_MINUTES
|
|
551
|
+
|
|
552
|
+
Deliveries with attempt_count > 0 are inside Celery's built-in retry
|
|
553
|
+
chain and must not be re-dispatched here.
|
|
554
|
+
"""
|
|
555
|
+
from trueppm_api.apps.webhooks.models import DeliveryStatus, WebhookDelivery
|
|
556
|
+
|
|
557
|
+
cutoff = timezone.now() - timedelta(minutes=_DRAIN_ORPHAN_MINUTES)
|
|
558
|
+
stranded = list(
|
|
559
|
+
WebhookDelivery.objects.filter(
|
|
560
|
+
status=DeliveryStatus.PENDING,
|
|
561
|
+
attempt_count=0,
|
|
562
|
+
created_at__lt=cutoff,
|
|
563
|
+
).select_related("webhook")
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
for delivery in stranded:
|
|
567
|
+
if not delivery.webhook.is_active:
|
|
568
|
+
delivery.status = DeliveryStatus.FAILED
|
|
569
|
+
delivery.completed_at = timezone.now()
|
|
570
|
+
delivery.save(update_fields=["status", "completed_at"])
|
|
571
|
+
logger.info(
|
|
572
|
+
"_do_drain_webhooks: marked delivery %s failed — webhook inactive",
|
|
573
|
+
delivery.pk,
|
|
574
|
+
)
|
|
575
|
+
continue
|
|
576
|
+
|
|
577
|
+
try:
|
|
578
|
+
deliver_webhook.delay(str(delivery.pk))
|
|
579
|
+
logger.info(
|
|
580
|
+
"_do_drain_webhooks: re-dispatched stranded delivery %s",
|
|
581
|
+
delivery.pk,
|
|
582
|
+
)
|
|
583
|
+
except Exception:
|
|
584
|
+
logger.warning(
|
|
585
|
+
"_do_drain_webhooks: broker unavailable — delivery %s stays pending",
|
|
586
|
+
delivery.pk,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
@idempotent_task(
|
|
591
|
+
lock_key_template="drain_webhook_queue",
|
|
592
|
+
lock_ttl=60,
|
|
593
|
+
on_contention="skip",
|
|
594
|
+
soft_time_limit=25,
|
|
595
|
+
time_limit=30,
|
|
596
|
+
acks_late=True,
|
|
597
|
+
reject_on_worker_lost=True,
|
|
598
|
+
name="webhooks.drain_webhook_queue",
|
|
599
|
+
)
|
|
600
|
+
def drain_webhook_queue(self: object) -> None:
|
|
601
|
+
"""Beat task: drain stranded PENDING webhook deliveries every 30 seconds."""
|
|
602
|
+
_do_drain_webhooks()
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
# ---------------------------------------------------------------------------
|
|
606
|
+
# Webhook delivery retention purge (ADR-0081)
|
|
607
|
+
# ---------------------------------------------------------------------------
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def _do_webhook_purge(*, dry_run: bool = False, override_value: int | None = None) -> int:
|
|
611
|
+
"""Business logic for purge_old_deliveries — extracted for testability.
|
|
612
|
+
|
|
613
|
+
Deletes only terminal (SUCCESS/FAILED) deliveries: PENDING rows may still be
|
|
614
|
+
re-dispatched by the drain, so they are never purged regardless of age. The
|
|
615
|
+
window comes from ``resolve_retention`` (operator override → settings default,
|
|
616
|
+
ADR-0173); ``None`` disables the purge entirely (unbounded retention).
|
|
617
|
+
|
|
618
|
+
Returns the number of rows deleted, or — when ``dry_run`` — the number that
|
|
619
|
+
*would* be deleted. ``override_value`` forces a hypothetical window (used by the
|
|
620
|
+
impact estimate) regardless of the saved policy.
|
|
621
|
+
"""
|
|
622
|
+
from django.utils import timezone
|
|
623
|
+
|
|
624
|
+
from trueppm_api.apps.observability.retention import resolve_retention
|
|
625
|
+
from trueppm_api.apps.webhooks.models import DeliveryStatus, WebhookDelivery
|
|
626
|
+
|
|
627
|
+
retention_days = (
|
|
628
|
+
override_value
|
|
629
|
+
if override_value is not None
|
|
630
|
+
else resolve_retention("TRUEPPM_WEBHOOK_RETENTION_DAYS")
|
|
631
|
+
)
|
|
632
|
+
if retention_days is None:
|
|
633
|
+
return 0
|
|
634
|
+
|
|
635
|
+
cutoff = timezone.now() - timedelta(days=retention_days)
|
|
636
|
+
qs = WebhookDelivery.objects.filter(
|
|
637
|
+
status__in=[DeliveryStatus.SUCCESS, DeliveryStatus.FAILED],
|
|
638
|
+
created_at__lt=cutoff,
|
|
639
|
+
)
|
|
640
|
+
if dry_run:
|
|
641
|
+
return qs.count()
|
|
642
|
+
deleted, _ = qs.delete()
|
|
643
|
+
logger.info("purge_old_deliveries: deleted %d row(s)", deleted)
|
|
644
|
+
return deleted
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
@idempotent_task(
|
|
648
|
+
lock_key_template="purge_old_deliveries",
|
|
649
|
+
lock_ttl=120,
|
|
650
|
+
on_contention="skip",
|
|
651
|
+
soft_time_limit=55,
|
|
652
|
+
time_limit=90,
|
|
653
|
+
acks_late=True,
|
|
654
|
+
reject_on_worker_lost=True,
|
|
655
|
+
name="webhooks.purge_old_deliveries",
|
|
656
|
+
)
|
|
657
|
+
def purge_old_deliveries(self: object) -> None:
|
|
658
|
+
"""Delete terminal WebhookDelivery rows older than the retention window.
|
|
659
|
+
|
|
660
|
+
Runs nightly via Celery Beat. Keeps the delivery table small so the
|
|
661
|
+
(status, created_at) index scans in the drain stay fast on high-traffic boards.
|
|
662
|
+
"""
|
|
663
|
+
_do_webhook_purge()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""URL patterns for the webhooks app."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from django.urls import URLPattern, URLResolver
|
|
6
|
+
from rest_framework.routers import DefaultRouter
|
|
7
|
+
|
|
8
|
+
from trueppm_api.apps.webhooks.views import ProgramWebhookViewSet, WebhookViewSet
|
|
9
|
+
|
|
10
|
+
router = DefaultRouter()
|
|
11
|
+
router.register(
|
|
12
|
+
r"projects/(?P<project_pk>[^/.]+)/webhooks",
|
|
13
|
+
WebhookViewSet,
|
|
14
|
+
basename="project-webhooks",
|
|
15
|
+
)
|
|
16
|
+
router.register(
|
|
17
|
+
r"programs/(?P<program_pk>[^/.]+)/webhooks",
|
|
18
|
+
ProgramWebhookViewSet,
|
|
19
|
+
basename="program-webhooks",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
urlpatterns: list[URLPattern | URLResolver] = router.urls
|