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,731 @@
|
|
|
1
|
+
"""Celery workers for user-scoped external task sync (ADR-0097 §4).
|
|
2
|
+
|
|
3
|
+
Four tasks, all registered under short names for Beat (#319):
|
|
4
|
+
|
|
5
|
+
- ``external_sync`` — pull one ``ExternalSyncRequest``'s connection: fetch the
|
|
6
|
+
owner's assigned items from the source, upsert them into ``ExternalWorkItem``,
|
|
7
|
+
soft-remove any that vanished, and flip the connection to ``connected`` /
|
|
8
|
+
``auth_failed`` / ``invalid_filter``. Idempotent under the row's
|
|
9
|
+
``@idempotent_task`` lock.
|
|
10
|
+
- ``drain_external_sync`` — 300 s outbox drain: dispatch stranded ``PENDING``
|
|
11
|
+
rows and recover orphaned ``DISPATCHED`` ones (ADR-0097 §Durable Execution #2).
|
|
12
|
+
- ``poll_external_sources`` — low-frequency opt-in poll: enqueue a pull for every
|
|
13
|
+
connection whose owner opted in (``config["poll_enabled"]``, set from the
|
|
14
|
+
Connected Accounts toggle). Default-off, so this no-ops until a user turns
|
|
15
|
+
polling on (ADR-0097 §4).
|
|
16
|
+
- ``purge_external_sync`` — nightly cleanup: hard-delete terminal outbox rows and
|
|
17
|
+
long-stale ``ExternalWorkItem`` cache rows (ADR-0097 §Durable Execution #6).
|
|
18
|
+
|
|
19
|
+
Security (ADR-0097 §Resolution #2): the PAT and ``Authorization`` header never
|
|
20
|
+
reach a log line — only the source key, user id, and a scrubbed error class are
|
|
21
|
+
recorded. The pull can **never** mint a ``Task`` (the §2 read-only invariant).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import logging
|
|
27
|
+
from datetime import datetime, timedelta
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from django.db import models, transaction
|
|
31
|
+
from django.utils import timezone
|
|
32
|
+
|
|
33
|
+
from trueppm_api.core.idempotent import idempotent_task
|
|
34
|
+
|
|
35
|
+
from .connections import (
|
|
36
|
+
STATUS_AUTH_FAILED,
|
|
37
|
+
STATUS_CONNECTED,
|
|
38
|
+
STATUS_INVALID_FILTER,
|
|
39
|
+
SYNC_FAILURE_REASONS,
|
|
40
|
+
SYNC_REASON_AUTH_FAILED,
|
|
41
|
+
SYNC_REASON_DECRYPT_FAILED,
|
|
42
|
+
SYNC_REASON_INVALID_FILTER,
|
|
43
|
+
SYNC_REASON_RATE_LIMITED,
|
|
44
|
+
SYNC_REASON_UNREACHABLE,
|
|
45
|
+
)
|
|
46
|
+
from .encryption import decrypt_secret
|
|
47
|
+
from .external_sources import (
|
|
48
|
+
EXTERNAL_TASK_SOURCES,
|
|
49
|
+
ExternalFetchResult,
|
|
50
|
+
ExternalSourceAuthError,
|
|
51
|
+
ExternalSourceConfigError,
|
|
52
|
+
ExternalSourceError,
|
|
53
|
+
ExternalSourceRateLimited,
|
|
54
|
+
ExternalWorkItemDTO,
|
|
55
|
+
)
|
|
56
|
+
from .models import (
|
|
57
|
+
ExternalSyncRequest,
|
|
58
|
+
ExternalSyncRequestReason,
|
|
59
|
+
ExternalSyncRequestStatus,
|
|
60
|
+
ExternalWorkItem,
|
|
61
|
+
IntegrationCredential,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
logger = logging.getLogger(__name__)
|
|
65
|
+
|
|
66
|
+
# Per-(user, source) cache cap (ADR-0097 §Decision #4 "bounded growth"). The
|
|
67
|
+
# frozen source ABC fetches a single ~100-item page, so this is a defensive DB
|
|
68
|
+
# bound rather than a routinely-hit limit; when a page ever exceeds it we keep
|
|
69
|
+
# the first CACHE_ITEM_CAP and drop the rest so the cache stays bounded.
|
|
70
|
+
CACHE_ITEM_CAP = 500
|
|
71
|
+
|
|
72
|
+
# The ``config["last_sync"]["reason"]`` vocabulary is defined in ``connections``
|
|
73
|
+
# — beside the read surface that serves it, so that surface can validate against
|
|
74
|
+
# it without importing back into this module. Re-exported here because this is
|
|
75
|
+
# the only module that *writes* it, and every writer should reach for it by name.
|
|
76
|
+
__all__ = [
|
|
77
|
+
"SYNC_FAILURE_REASONS",
|
|
78
|
+
"SYNC_REASON_AUTH_FAILED",
|
|
79
|
+
"SYNC_REASON_DECRYPT_FAILED",
|
|
80
|
+
"SYNC_REASON_INVALID_FILTER",
|
|
81
|
+
"SYNC_REASON_RATE_LIMITED",
|
|
82
|
+
"SYNC_REASON_UNREACHABLE",
|
|
83
|
+
"drain_external_sync",
|
|
84
|
+
"external_sync",
|
|
85
|
+
"poll_external_sources",
|
|
86
|
+
"purge_external_sync",
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
# Drain floor: a PENDING row younger than this was almost certainly just handed
|
|
90
|
+
# to on_commit dispatch, so the drain skips it to avoid a double-dispatch race
|
|
91
|
+
# (ADR-0097 §Durable Execution #3). Older PENDING rows are genuinely stranded.
|
|
92
|
+
_DRAIN_PENDING_FLOOR = timedelta(minutes=2)
|
|
93
|
+
|
|
94
|
+
# Orphan recovery: a DISPATCHED row this old outlived any real pull (the fetch
|
|
95
|
+
# is HTTP-timeout-bounded to a few seconds), so its worker died — reset it to
|
|
96
|
+
# PENDING for re-dispatch. Matches the scheduling drain's 10-minute window.
|
|
97
|
+
_DRAIN_ORPHAN_CUTOFF = timedelta(minutes=10)
|
|
98
|
+
|
|
99
|
+
# Rate-limit backoff (#2924). Used when a 429 carries no usable ``Retry-After``;
|
|
100
|
+
# a header value always wins. Clamped at the top end so a source answering with an
|
|
101
|
+
# absurd Retry-After (or a clock-skewed HTTP-date) cannot park a pull for hours.
|
|
102
|
+
_RATE_LIMIT_DEFAULT_BACKOFF = timedelta(seconds=60)
|
|
103
|
+
_RATE_LIMIT_MAX_BACKOFF = timedelta(minutes=15)
|
|
104
|
+
|
|
105
|
+
# How many times one pull may be re-queued for rate limiting before it retires.
|
|
106
|
+
# Bounds the loop for a source that 429s indefinitely; the user's next manual
|
|
107
|
+
# refresh (or the poll) starts a fresh row, so this is not a permanent give-up.
|
|
108
|
+
_MAX_RATE_LIMIT_ATTEMPTS = 3
|
|
109
|
+
|
|
110
|
+
# Retention windows for the nightly purge.
|
|
111
|
+
_OUTBOX_RETENTION = timedelta(days=7)
|
|
112
|
+
# Stale (vanished-from-Jira) items linger briefly so My Work can show a just-
|
|
113
|
+
# completed item, then are hard-deleted (ADR-0097 §5).
|
|
114
|
+
_STALE_ITEM_RETENTION = timedelta(days=7)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------------
|
|
118
|
+
# external_sync — pull one connection
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@idempotent_task(
|
|
123
|
+
lock_key_template="external_sync:{0}",
|
|
124
|
+
lock_ttl=60,
|
|
125
|
+
on_contention="skip",
|
|
126
|
+
soft_time_limit=25,
|
|
127
|
+
time_limit=30,
|
|
128
|
+
acks_late=True,
|
|
129
|
+
reject_on_worker_lost=True,
|
|
130
|
+
name="integrations.external_sync",
|
|
131
|
+
)
|
|
132
|
+
def external_sync(self: object, request_id: str) -> None:
|
|
133
|
+
"""Execute one queued external-source pull.
|
|
134
|
+
|
|
135
|
+
Locked per request id so a duplicate dispatch (on_commit + drain racing)
|
|
136
|
+
converges rather than double-fetching; ``on_contention="skip"`` drops the
|
|
137
|
+
loser because the winner already did the work.
|
|
138
|
+
"""
|
|
139
|
+
_do_sync(request_id)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _do_sync(request_id: str) -> None:
|
|
143
|
+
"""Business logic for ``external_sync`` — extracted for direct testing.
|
|
144
|
+
|
|
145
|
+
Loads and locks the outbox row, decrypts the owner's PAT, fetches their
|
|
146
|
+
assigned items, upserts the cache, soft-removes anything that vanished, and
|
|
147
|
+
marks the row ``DONE``/``DEAD``. Terminal rows (already ``DONE``/``DEAD``) are
|
|
148
|
+
a no-op so a re-dispatch is safe.
|
|
149
|
+
"""
|
|
150
|
+
now = timezone.now()
|
|
151
|
+
with transaction.atomic():
|
|
152
|
+
req = ExternalSyncRequest.objects.select_for_update().filter(id=request_id).first()
|
|
153
|
+
if req is None:
|
|
154
|
+
# Purged between dispatch and execution — nothing to do.
|
|
155
|
+
return
|
|
156
|
+
if req.status in (
|
|
157
|
+
ExternalSyncRequestStatus.DONE,
|
|
158
|
+
ExternalSyncRequestStatus.DEAD,
|
|
159
|
+
):
|
|
160
|
+
# Already processed by the winning dispatch — idempotent skip.
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
cred = IntegrationCredential.objects.filter(
|
|
164
|
+
user_id=req.user_id, provider=req.source
|
|
165
|
+
).first()
|
|
166
|
+
source_cls = EXTERNAL_TASK_SOURCES.get(req.source)
|
|
167
|
+
if cred is None or source_cls is None:
|
|
168
|
+
# The connection was deleted (or its source de-registered) after the
|
|
169
|
+
# pull was queued. Nothing to sync; retire the row.
|
|
170
|
+
_mark_dead(req, "connection no longer exists")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
secret = decrypt_secret(cred.secret_ciphertext)
|
|
175
|
+
except Exception:
|
|
176
|
+
# A corrupt/undecryptable ciphertext is not user-recoverable by a
|
|
177
|
+
# retry; retire the row without leaking the plaintext into the log
|
|
178
|
+
# (only user_id/source are interpolated below, never the value).
|
|
179
|
+
logger.warning(
|
|
180
|
+
"external_sync: stored connection data could not be decrypted "
|
|
181
|
+
"for user=%s source=%s",
|
|
182
|
+
req.user_id,
|
|
183
|
+
req.source,
|
|
184
|
+
)
|
|
185
|
+
_record_failed_pull(cred, SYNC_REASON_DECRYPT_FAILED, now)
|
|
186
|
+
_mark_dead(req, "credential could not be decrypted")
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
result = source_cls().fetch_assigned_items_result(
|
|
191
|
+
base_url=cred.base_url, secret=secret, config=cred.config or {}
|
|
192
|
+
)
|
|
193
|
+
except ExternalSourceAuthError:
|
|
194
|
+
# 401/403: the token is dead. Flip the connection to auth_failed so My
|
|
195
|
+
# Work shows "Reconnect", stop using the token, keep the last-good
|
|
196
|
+
# cache (ADR-0097 §5). No retry loop on an auth failure.
|
|
197
|
+
_set_connection_status(
|
|
198
|
+
cred,
|
|
199
|
+
STATUS_AUTH_FAILED,
|
|
200
|
+
last_sync=_failed_outcome(SYNC_REASON_AUTH_FAILED, now),
|
|
201
|
+
)
|
|
202
|
+
_mark_dead(req, "auth_failed")
|
|
203
|
+
logger.info(
|
|
204
|
+
"external_sync: auth failed for user=%s source=%s — connection flagged",
|
|
205
|
+
req.user_id,
|
|
206
|
+
req.source,
|
|
207
|
+
)
|
|
208
|
+
return
|
|
209
|
+
except ExternalSourceConfigError:
|
|
210
|
+
# The stored filter cannot be scoped safely (a project key that is not
|
|
211
|
+
# a project key, or a JQL whose parentheses do not balance — both are
|
|
212
|
+
# reachable on rows written before those values were validated). A
|
|
213
|
+
# retry can never fix it, and the alternative to refusing is pulling
|
|
214
|
+
# wider than the owner selected, so flag the connection with a state
|
|
215
|
+
# whose remedy is the connect wizard and stop. Must precede the generic
|
|
216
|
+
# ExternalSourceError arm below — this is a subclass of it.
|
|
217
|
+
_set_connection_status(
|
|
218
|
+
cred,
|
|
219
|
+
STATUS_INVALID_FILTER,
|
|
220
|
+
last_sync=_failed_outcome(SYNC_REASON_INVALID_FILTER, now),
|
|
221
|
+
)
|
|
222
|
+
_mark_dead(req, "invalid_filter")
|
|
223
|
+
logger.info(
|
|
224
|
+
"external_sync: stored filter unusable for user=%s source=%s — connection flagged",
|
|
225
|
+
req.user_id,
|
|
226
|
+
req.source,
|
|
227
|
+
)
|
|
228
|
+
return
|
|
229
|
+
except ExternalSourceRateLimited as exc:
|
|
230
|
+
# 429: the source told us WHEN to come back, so re-queue against that
|
|
231
|
+
# clock instead of dead-lettering (#2924). Before this the row was
|
|
232
|
+
# marked DEAD "unreachable" and the owner saw a broken connection for
|
|
233
|
+
# what is a routine, self-healing condition. Deliberately does NOT
|
|
234
|
+
# touch the connection status: the credential is valid and the host is
|
|
235
|
+
# reachable, so flagging it would send the user to re-issue a token
|
|
236
|
+
# that was never the problem. Must precede the generic arm below —
|
|
237
|
+
# this is a subclass of it.
|
|
238
|
+
_requeue_rate_limited(req, exc.retry_after, now, cred=cred)
|
|
239
|
+
return
|
|
240
|
+
except ExternalSourceError as exc:
|
|
241
|
+
# Transient (5xx / timeout): keep the last-good cache and retire this
|
|
242
|
+
# attempt. The user (or the next poll) re-triggers; the cooldown keeps
|
|
243
|
+
# the retry cadence sane. No data is lost.
|
|
244
|
+
_record_failed_pull(cred, SYNC_REASON_UNREACHABLE, now)
|
|
245
|
+
_mark_dead(req, f"unreachable: {type(exc).__name__}")
|
|
246
|
+
logger.info(
|
|
247
|
+
"external_sync: source unreachable for user=%s source=%s (%s)",
|
|
248
|
+
req.user_id,
|
|
249
|
+
req.source,
|
|
250
|
+
type(exc).__name__,
|
|
251
|
+
)
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
stored = _apply_pull(req.user_id, req.source, result.items, now)
|
|
255
|
+
|
|
256
|
+
# Successful pull: stamp the connection with its outcome and retire the row.
|
|
257
|
+
cred.last_used_at = now
|
|
258
|
+
_set_connection_status(
|
|
259
|
+
cred,
|
|
260
|
+
STATUS_CONNECTED,
|
|
261
|
+
last_sync=_successful_outcome(result, stored, now),
|
|
262
|
+
extra_save_fields=["last_used_at"],
|
|
263
|
+
)
|
|
264
|
+
req.status = ExternalSyncRequestStatus.DONE
|
|
265
|
+
req.last_error = ""
|
|
266
|
+
req.save(update_fields=["status", "last_error"])
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _apply_pull(
|
|
270
|
+
user_id: int,
|
|
271
|
+
source: str,
|
|
272
|
+
items: list[ExternalWorkItemDTO],
|
|
273
|
+
now: datetime,
|
|
274
|
+
) -> int:
|
|
275
|
+
"""Upsert the fetched DTOs and soft-remove anything that vanished.
|
|
276
|
+
|
|
277
|
+
Items are already sanitized (field caps + URL scheme) at the registry
|
|
278
|
+
boundary. Capped at :data:`CACHE_ITEM_CAP` per (user, source). An
|
|
279
|
+
``external_id`` no longer returned by a *successful* pull is soft-removed
|
|
280
|
+
(``is_stale=True``) — never hard-deleted here — so a transient partial
|
|
281
|
+
response can never wipe the list (ADR-0097 §5).
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
How many rows this pull actually stored (after the cap and after
|
|
285
|
+
duplicate ``external_id``s collapse) — the "showing the first N" the
|
|
286
|
+
owner is told about, so the number they see is the number that landed
|
|
287
|
+
rather than the number the provider sent (#2925).
|
|
288
|
+
"""
|
|
289
|
+
capped = items[:CACHE_ITEM_CAP]
|
|
290
|
+
seen: set[str] = set()
|
|
291
|
+
for raw in capped:
|
|
292
|
+
# Re-sanitize at the persistence boundary (idempotent): the OSS Jira
|
|
293
|
+
# source already returns sanitized DTOs, but making *this* the enforcement
|
|
294
|
+
# point means a future/Enterprise source that forgets ``.sanitized()``
|
|
295
|
+
# still cannot land an over-long field or a ``javascript:``/``data:`` URL
|
|
296
|
+
# in the cache — URLField scheme validation does not run on ``.save()``
|
|
297
|
+
# (ADR-0097 §Resolution #4, defense in depth).
|
|
298
|
+
dto = raw.sanitized()
|
|
299
|
+
seen.add(dto.external_id)
|
|
300
|
+
ExternalWorkItem.objects.update_or_create(
|
|
301
|
+
user_id=user_id,
|
|
302
|
+
source=source,
|
|
303
|
+
external_id=dto.external_id,
|
|
304
|
+
defaults={
|
|
305
|
+
"external_url": dto.external_url,
|
|
306
|
+
"title": dto.title,
|
|
307
|
+
"external_status": dto.external_status,
|
|
308
|
+
"display_bucket": dto.display_bucket,
|
|
309
|
+
"due_date": dto.due_date,
|
|
310
|
+
"last_synced_at": now,
|
|
311
|
+
"is_stale": False,
|
|
312
|
+
},
|
|
313
|
+
)
|
|
314
|
+
# Soft-remove cached rows for this connection that this pull did not return.
|
|
315
|
+
stale_qs = ExternalWorkItem.objects.filter(user_id=user_id, source=source)
|
|
316
|
+
if seen:
|
|
317
|
+
stale_qs = stale_qs.exclude(external_id__in=seen)
|
|
318
|
+
stale_qs.filter(is_stale=False).update(is_stale=True, last_synced_at=now)
|
|
319
|
+
return len(seen)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _iso_z(value: datetime) -> str:
|
|
323
|
+
"""Serialize a timestamp the way DRF does, so one object has one spelling.
|
|
324
|
+
|
|
325
|
+
``config`` is a JSON column, so ``at`` is stored as a string and DRF's
|
|
326
|
+
``DateTimeField.to_representation`` passes a ``str`` straight through. Its
|
|
327
|
+
sibling ``last_synced_at`` is a real ``DateTimeField`` and renders as
|
|
328
|
+
``…Z``, so a bare ``isoformat()`` would put ``…+00:00`` and ``…Z`` in the
|
|
329
|
+
same response object. Both are valid RFC 3339 and no client breaks on it —
|
|
330
|
+
it is just two spellings of one thing in one payload, which is the kind of
|
|
331
|
+
inconsistency an integrator has to write a special case for.
|
|
332
|
+
"""
|
|
333
|
+
return value.isoformat().replace("+00:00", "Z")
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _successful_outcome(result: ExternalFetchResult, stored: int, now: datetime) -> dict[str, Any]:
|
|
337
|
+
"""Describe a completed pull for ``config["last_sync"]`` (#2925).
|
|
338
|
+
|
|
339
|
+
The truncation decision, in priority order, because each rung is a stronger
|
|
340
|
+
claim than the next:
|
|
341
|
+
|
|
342
|
+
1. The cache cap bit — more DTOs came back than we were willing to store.
|
|
343
|
+
Tested against :data:`CACHE_ITEM_CAP` rather than ``stored < fetched``,
|
|
344
|
+
because ``stored`` counts *distinct* ``external_id``s: a page containing a
|
|
345
|
+
duplicate satisfies ``stored < fetched`` with no cap anywhere near, which
|
|
346
|
+
would tell an owner their complete list is partial. De-duplication is not
|
|
347
|
+
truncation.
|
|
348
|
+
2. The provider reported a ``total`` larger than what we stored. Exact.
|
|
349
|
+
3. No total, but the page came back **full**. A guess, and the weakest rung:
|
|
350
|
+
a user with exactly ``page_size`` assigned items is indistinguishable from
|
|
351
|
+
one with more. It is the conservative direction — over-reporting "there
|
|
352
|
+
may be more" costs a line of UI, while under-reporting is the silent
|
|
353
|
+
truncation this issue exists to end.
|
|
354
|
+
|
|
355
|
+
``total_available`` stays ``None`` when the source did not report one. The web
|
|
356
|
+
must render that as "the first N" without a denominator rather than inventing
|
|
357
|
+
a total — ``None`` here means *unknown*, never *zero*.
|
|
358
|
+
"""
|
|
359
|
+
fetched = len(result.items)
|
|
360
|
+
total = result.total_available
|
|
361
|
+
if fetched > CACHE_ITEM_CAP:
|
|
362
|
+
truncated = True
|
|
363
|
+
elif total is not None:
|
|
364
|
+
truncated = total > stored
|
|
365
|
+
else:
|
|
366
|
+
truncated = result.page_size is not None and fetched >= result.page_size
|
|
367
|
+
return {
|
|
368
|
+
"at": _iso_z(now),
|
|
369
|
+
"ok": True,
|
|
370
|
+
"reason": "",
|
|
371
|
+
"fetched": fetched,
|
|
372
|
+
"stored": stored,
|
|
373
|
+
"total_available": total,
|
|
374
|
+
"truncated": truncated,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _failed_outcome(reason: str, now: datetime) -> dict[str, Any]:
|
|
379
|
+
"""Describe a failed pull for ``config["last_sync"]`` (#2925).
|
|
380
|
+
|
|
381
|
+
Counts are zero rather than absent so the shape is uniform for every reader —
|
|
382
|
+
a failure did not store anything, and the last-good cache the owner is still
|
|
383
|
+
being served is described by the *previous* successful outcome, not this one.
|
|
384
|
+
``reason`` is a :data:`SYNC_FAILURE_REASONS` token; see that constant for why
|
|
385
|
+
it is never a formatted message.
|
|
386
|
+
"""
|
|
387
|
+
return {
|
|
388
|
+
"at": _iso_z(now),
|
|
389
|
+
"ok": False,
|
|
390
|
+
"reason": reason,
|
|
391
|
+
"fetched": 0,
|
|
392
|
+
"stored": 0,
|
|
393
|
+
"total_available": None,
|
|
394
|
+
"truncated": False,
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _merge_config(
|
|
399
|
+
cred: IntegrationCredential,
|
|
400
|
+
updates: dict[str, Any],
|
|
401
|
+
*,
|
|
402
|
+
extra_save_fields: list[str] | None = None,
|
|
403
|
+
) -> None:
|
|
404
|
+
"""Merge keys into the credential's ``config`` under a row lock.
|
|
405
|
+
|
|
406
|
+
``config`` is one JSON column holding several independently-owned keys: the
|
|
407
|
+
owner writes ``{jql, project_keys, account_email, deployment}`` from the
|
|
408
|
+
connect wizard, and this worker writes ``{status, last_sync}``. A save from
|
|
409
|
+
either side rewrites the **whole** column, so writing from the snapshot
|
|
410
|
+
``cred`` was loaded with — before an outbound fetch that can take seconds —
|
|
411
|
+
silently reverts anything the owner changed while the pull was in flight.
|
|
412
|
+
|
|
413
|
+
That is not a cosmetic lost update. ``project_keys`` is ANDed into the JQL
|
|
414
|
+
(#2888) precisely so the filter narrows what leaves the provider, so reverting
|
|
415
|
+
it *widens* the next pull past what the owner selected; and reinstating a
|
|
416
|
+
stale ``status`` re-shows "Reconnect" on a connection the owner just repaired.
|
|
417
|
+
|
|
418
|
+
So: re-read the row ``SELECT … FOR UPDATE`` immediately before the write, and
|
|
419
|
+
merge only this module's own keys onto whatever is there now. The lock is
|
|
420
|
+
taken *after* the fetch, never across it, so a slow provider cannot hold it.
|
|
421
|
+
A row deleted mid-pull (the owner disconnected) is a no-op rather than a
|
|
422
|
+
resurrection.
|
|
423
|
+
"""
|
|
424
|
+
fresh = IntegrationCredential.objects.select_for_update().filter(pk=cred.pk).first()
|
|
425
|
+
if fresh is None:
|
|
426
|
+
return
|
|
427
|
+
config = dict(fresh.config or {})
|
|
428
|
+
config.update(updates)
|
|
429
|
+
fresh.config = config
|
|
430
|
+
# Fields the caller already set on its own instance (``last_used_at``) have to
|
|
431
|
+
# be carried onto the locked row, which was loaded before they were assigned.
|
|
432
|
+
for field in extra_save_fields or []:
|
|
433
|
+
setattr(fresh, field, getattr(cred, field))
|
|
434
|
+
fresh.save(update_fields=["config", *(extra_save_fields or [])])
|
|
435
|
+
# Keep the caller's instance consistent with what was persisted, so a later
|
|
436
|
+
# read off ``cred`` in the same call does not see the pre-merge snapshot.
|
|
437
|
+
cred.config = config
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _record_failed_pull(cred: IntegrationCredential, reason: str, now: datetime) -> None:
|
|
441
|
+
"""Stamp a failure onto ``config["last_sync"]`` without touching ``status``.
|
|
442
|
+
|
|
443
|
+
Used by the failure paths that deliberately leave the connection lifecycle
|
|
444
|
+
alone — an unreachable host or an unreadable ciphertext is not ``auth_failed``
|
|
445
|
+
and must not send the owner to re-issue a working token. The outcome is still
|
|
446
|
+
recorded, because "connected, last synced 5 minutes ago" reading identically
|
|
447
|
+
whether the last pull worked or not is the defect (#2925).
|
|
448
|
+
"""
|
|
449
|
+
_merge_config(cred, {"last_sync": _failed_outcome(reason, now)})
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _set_connection_status(
|
|
453
|
+
cred: IntegrationCredential,
|
|
454
|
+
status: str,
|
|
455
|
+
*,
|
|
456
|
+
last_sync: dict[str, Any] | None = None,
|
|
457
|
+
extra_save_fields: list[str] | None = None,
|
|
458
|
+
) -> None:
|
|
459
|
+
"""Write ``config["status"]`` on the credential row (the connection lifecycle).
|
|
460
|
+
|
|
461
|
+
``config`` is the ADR-0097 §2 reuse of ``IntegrationCredential`` — the
|
|
462
|
+
connection's ``{account_email, jql, project_keys, status, last_sync}`` live
|
|
463
|
+
there. Only ``status``, an optional ``last_sync`` outcome, and any
|
|
464
|
+
``extra_save_fields`` the caller already set are persisted — see
|
|
465
|
+
:func:`_merge_config` for why the other keys are merged rather than rewritten.
|
|
466
|
+
"""
|
|
467
|
+
updates: dict[str, Any] = {"status": status}
|
|
468
|
+
if last_sync is not None:
|
|
469
|
+
updates["last_sync"] = last_sync
|
|
470
|
+
_merge_config(cred, updates, extra_save_fields=extra_save_fields)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _requeue_rate_limited(
|
|
474
|
+
req: ExternalSyncRequest,
|
|
475
|
+
retry_after: float | None,
|
|
476
|
+
now: datetime,
|
|
477
|
+
*,
|
|
478
|
+
cred: IntegrationCredential | None = None,
|
|
479
|
+
) -> None:
|
|
480
|
+
"""Put a rate-limited pull back in the PENDING pool behind a retry clock.
|
|
481
|
+
|
|
482
|
+
``Retry-After`` wins when the source sent a usable one; otherwise a default
|
|
483
|
+
backoff applies. Either way the value is clamped to
|
|
484
|
+
:data:`_RATE_LIMIT_MAX_BACKOFF` so a hostile or clock-skewed header cannot park
|
|
485
|
+
the pull indefinitely.
|
|
486
|
+
|
|
487
|
+
The row goes back to PENDING rather than staying DISPATCHED because PENDING is
|
|
488
|
+
what the drain re-dispatches. That is safe against the partial-unique
|
|
489
|
+
``one pending per (user, source)`` constraint: this row *is* that connection's
|
|
490
|
+
live pull — anything else that wanted one adopted it while it was in flight.
|
|
491
|
+
|
|
492
|
+
Once the attempt budget is spent the row retires DEAD, so a source that 429s
|
|
493
|
+
forever cannot cycle forever. That is not a permanent give-up: the owner's next
|
|
494
|
+
manual refresh writes a fresh row.
|
|
495
|
+
|
|
496
|
+
Only that terminal give-up is recorded as a failed outcome on the connection
|
|
497
|
+
(#2925). A re-queue is not an outcome — the pull is still in flight, and
|
|
498
|
+
writing "last pull: rate limited" for a request that succeeds forty seconds
|
|
499
|
+
later would show the owner a failure that did not happen.
|
|
500
|
+
"""
|
|
501
|
+
req.attempt_count += 1
|
|
502
|
+
if req.attempt_count >= _MAX_RATE_LIMIT_ATTEMPTS:
|
|
503
|
+
if cred is not None:
|
|
504
|
+
_record_failed_pull(cred, SYNC_REASON_RATE_LIMITED, now)
|
|
505
|
+
_mark_dead(req, f"rate_limited: retry budget spent after {req.attempt_count} attempt(s)")
|
|
506
|
+
logger.info(
|
|
507
|
+
"external_sync: rate-limit budget spent for user=%s source=%s",
|
|
508
|
+
req.user_id,
|
|
509
|
+
req.source,
|
|
510
|
+
)
|
|
511
|
+
return
|
|
512
|
+
|
|
513
|
+
backoff = _RATE_LIMIT_DEFAULT_BACKOFF
|
|
514
|
+
if retry_after is not None:
|
|
515
|
+
backoff = timedelta(seconds=retry_after)
|
|
516
|
+
backoff = min(backoff, _RATE_LIMIT_MAX_BACKOFF)
|
|
517
|
+
|
|
518
|
+
req.status = ExternalSyncRequestStatus.PENDING
|
|
519
|
+
req.next_attempt_at = now + backoff
|
|
520
|
+
req.celery_task_id = ""
|
|
521
|
+
req.dispatched_at = None
|
|
522
|
+
req.last_error = f"rate_limited: retrying in {int(backoff.total_seconds())}s"[:512]
|
|
523
|
+
req.save(
|
|
524
|
+
update_fields=[
|
|
525
|
+
"status",
|
|
526
|
+
"next_attempt_at",
|
|
527
|
+
"attempt_count",
|
|
528
|
+
"celery_task_id",
|
|
529
|
+
"dispatched_at",
|
|
530
|
+
"last_error",
|
|
531
|
+
]
|
|
532
|
+
)
|
|
533
|
+
logger.info(
|
|
534
|
+
"external_sync: rate limited for user=%s source=%s — re-queued in %ss (attempt %d)",
|
|
535
|
+
req.user_id,
|
|
536
|
+
req.source,
|
|
537
|
+
int(backoff.total_seconds()),
|
|
538
|
+
req.attempt_count,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _mark_dead(req: ExternalSyncRequest, error: str) -> None:
|
|
543
|
+
"""Retire an outbox row as ``DEAD`` with a scrubbed error note."""
|
|
544
|
+
req.status = ExternalSyncRequestStatus.DEAD
|
|
545
|
+
req.last_error = error[:512]
|
|
546
|
+
req.save(update_fields=["status", "last_error"])
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# ---------------------------------------------------------------------------
|
|
550
|
+
# drain_external_sync — outbox recovery
|
|
551
|
+
# ---------------------------------------------------------------------------
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
@idempotent_task(
|
|
555
|
+
lock_key_template="drain_external_sync",
|
|
556
|
+
lock_ttl=60,
|
|
557
|
+
on_contention="skip",
|
|
558
|
+
soft_time_limit=55,
|
|
559
|
+
time_limit=90,
|
|
560
|
+
acks_late=True,
|
|
561
|
+
reject_on_worker_lost=True,
|
|
562
|
+
name="integrations.drain_external_sync",
|
|
563
|
+
)
|
|
564
|
+
def drain_external_sync(self: object) -> None:
|
|
565
|
+
"""Dispatch stranded PENDING rows and recover orphaned DISPATCHED ones.
|
|
566
|
+
|
|
567
|
+
Runs every 300 s. The singleton lock + ``on_contention="skip"`` guarantee at
|
|
568
|
+
most one drain at a time; the next Beat tick covers any dropped trigger.
|
|
569
|
+
"""
|
|
570
|
+
_do_drain()
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _do_drain() -> None:
|
|
574
|
+
"""Business logic for ``drain_external_sync`` — extracted for testability."""
|
|
575
|
+
now = timezone.now()
|
|
576
|
+
|
|
577
|
+
recovered = ExternalSyncRequest.objects.filter(
|
|
578
|
+
status=ExternalSyncRequestStatus.DISPATCHED,
|
|
579
|
+
dispatched_at__lt=now - _DRAIN_ORPHAN_CUTOFF,
|
|
580
|
+
).update(status=ExternalSyncRequestStatus.PENDING, celery_task_id="")
|
|
581
|
+
if recovered:
|
|
582
|
+
logger.warning("drain_external_sync: recovered %d orphaned dispatched row(s)", recovered)
|
|
583
|
+
|
|
584
|
+
# Only dispatch PENDING rows past the floor — younger ones were just handed to
|
|
585
|
+
# on_commit dispatch and would double-fire (the per-request lock would drop
|
|
586
|
+
# the loser, but skipping avoids the churn).
|
|
587
|
+
pending = list(
|
|
588
|
+
ExternalSyncRequest.objects.filter(
|
|
589
|
+
status=ExternalSyncRequestStatus.PENDING,
|
|
590
|
+
requested_at__lt=now - _DRAIN_PENDING_FLOOR,
|
|
591
|
+
).filter(
|
|
592
|
+
# A rate-limited row carries the clock the source itself gave us;
|
|
593
|
+
# re-dispatching before it would just earn another 429 (#2924). Null
|
|
594
|
+
# is the ordinary case and means "eligible now", so this narrows
|
|
595
|
+
# nothing for any row that was never rate-limited.
|
|
596
|
+
models.Q(next_attempt_at__isnull=True) | models.Q(next_attempt_at__lte=now)
|
|
597
|
+
)
|
|
598
|
+
)
|
|
599
|
+
dispatched = 0
|
|
600
|
+
for req in pending:
|
|
601
|
+
try:
|
|
602
|
+
result = external_sync.delay(str(req.id))
|
|
603
|
+
except Exception:
|
|
604
|
+
logger.warning(
|
|
605
|
+
"drain_external_sync: broker unavailable — request %s stays pending",
|
|
606
|
+
req.id,
|
|
607
|
+
)
|
|
608
|
+
continue
|
|
609
|
+
ExternalSyncRequest.objects.filter(
|
|
610
|
+
id=req.id, status=ExternalSyncRequestStatus.PENDING
|
|
611
|
+
).update(
|
|
612
|
+
status=ExternalSyncRequestStatus.DISPATCHED,
|
|
613
|
+
celery_task_id=result.id,
|
|
614
|
+
dispatched_at=now,
|
|
615
|
+
)
|
|
616
|
+
dispatched += 1
|
|
617
|
+
|
|
618
|
+
if dispatched or recovered:
|
|
619
|
+
logger.info("drain_external_sync: dispatched=%d recovered=%d", dispatched, recovered)
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
# ---------------------------------------------------------------------------
|
|
623
|
+
# poll_external_sources — opt-in low-frequency poll
|
|
624
|
+
# ---------------------------------------------------------------------------
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
@idempotent_task(
|
|
628
|
+
lock_key_template="poll_external_sources",
|
|
629
|
+
lock_ttl=60,
|
|
630
|
+
on_contention="skip",
|
|
631
|
+
soft_time_limit=55,
|
|
632
|
+
time_limit=90,
|
|
633
|
+
acks_late=True,
|
|
634
|
+
reject_on_worker_lost=True,
|
|
635
|
+
name="integrations.poll_external_sources",
|
|
636
|
+
)
|
|
637
|
+
def poll_external_sources(self: object) -> None:
|
|
638
|
+
"""Enqueue a pull for every connection whose owner opted into polling.
|
|
639
|
+
|
|
640
|
+
Default-off (ADR-0097 §4): a connection polls only when its ``config`` carries
|
|
641
|
+
``poll_enabled: true`` and it is in neither ``auth_failed`` nor
|
|
642
|
+
``invalid_filter``. Owners opt in from the Connected Accounts page (#3104,
|
|
643
|
+
``PATCH /me/connections/{source}/``), so an install where nobody has turned it
|
|
644
|
+
on still fans out zero pulls.
|
|
645
|
+
"""
|
|
646
|
+
_do_poll()
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def _do_poll() -> None:
|
|
650
|
+
"""Business logic for ``poll_external_sources`` — extracted for testability."""
|
|
651
|
+
from .services import enqueue_external_sync
|
|
652
|
+
|
|
653
|
+
source_keys = set(EXTERNAL_TASK_SOURCES.keys())
|
|
654
|
+
if not source_keys:
|
|
655
|
+
return
|
|
656
|
+
# Push the opt-in gates into the DB so a default-off install scans (and
|
|
657
|
+
# returns) ~zero rows each tick rather than every registered-source
|
|
658
|
+
# credential: poll_enabled must be truthy and the connection must be in
|
|
659
|
+
# neither auth_failed nor invalid_filter — the two states a retry cannot clear
|
|
660
|
+
# without the user reconnecting, so polling them just burns a pull per tick.
|
|
661
|
+
# The Python re-check below is belt-and-suspenders for the rare config shapes
|
|
662
|
+
# a JSON lookup can't express (e.g. poll_enabled stored as a string).
|
|
663
|
+
# ``.iterator()` keeps memory flat if the opted-in set ever grows.
|
|
664
|
+
blocked_statuses = (STATUS_AUTH_FAILED, STATUS_INVALID_FILTER)
|
|
665
|
+
candidates = (
|
|
666
|
+
IntegrationCredential.objects.filter(provider__in=source_keys, config__poll_enabled=True)
|
|
667
|
+
.exclude(config__status__in=blocked_statuses)
|
|
668
|
+
.iterator()
|
|
669
|
+
)
|
|
670
|
+
queued = 0
|
|
671
|
+
for cred in candidates:
|
|
672
|
+
config = cred.config or {}
|
|
673
|
+
if not config.get("poll_enabled"):
|
|
674
|
+
continue
|
|
675
|
+
if config.get("status") in blocked_statuses:
|
|
676
|
+
continue
|
|
677
|
+
try:
|
|
678
|
+
enqueue_external_sync(
|
|
679
|
+
cred.user_id, cred.provider, reason=ExternalSyncRequestReason.POLL
|
|
680
|
+
)
|
|
681
|
+
except ValueError:
|
|
682
|
+
# provider not a live external source (e.g. a plain git PAT) — skip.
|
|
683
|
+
continue
|
|
684
|
+
queued += 1
|
|
685
|
+
if queued:
|
|
686
|
+
logger.info("poll_external_sources: enqueued %d poll(s)", queued)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
# ---------------------------------------------------------------------------
|
|
690
|
+
# purge_external_sync — nightly cleanup
|
|
691
|
+
# ---------------------------------------------------------------------------
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
@idempotent_task(
|
|
695
|
+
lock_key_template="purge_external_sync",
|
|
696
|
+
lock_ttl=120,
|
|
697
|
+
on_contention="skip",
|
|
698
|
+
soft_time_limit=55,
|
|
699
|
+
time_limit=90,
|
|
700
|
+
acks_late=True,
|
|
701
|
+
reject_on_worker_lost=True,
|
|
702
|
+
name="integrations.purge_external_sync",
|
|
703
|
+
)
|
|
704
|
+
def purge_external_sync(self: object) -> None:
|
|
705
|
+
"""Hard-delete terminal outbox rows and long-stale cache rows (nightly)."""
|
|
706
|
+
_do_purge()
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def _do_purge() -> None:
|
|
710
|
+
"""Business logic for ``purge_external_sync`` — extracted for testability."""
|
|
711
|
+
now = timezone.now()
|
|
712
|
+
|
|
713
|
+
outbox_deleted, _ = ExternalSyncRequest.objects.filter(
|
|
714
|
+
status__in=[ExternalSyncRequestStatus.DONE, ExternalSyncRequestStatus.DEAD],
|
|
715
|
+
requested_at__lt=now - _OUTBOX_RETENTION,
|
|
716
|
+
).delete()
|
|
717
|
+
|
|
718
|
+
# Stale items are kept briefly (so a just-completed item can show in My Work),
|
|
719
|
+
# then reaped. Guard on last_synced_at so a row still within the grace window
|
|
720
|
+
# survives.
|
|
721
|
+
items_deleted, _ = ExternalWorkItem.objects.filter(
|
|
722
|
+
is_stale=True,
|
|
723
|
+
last_synced_at__lt=now - _STALE_ITEM_RETENTION,
|
|
724
|
+
).delete()
|
|
725
|
+
|
|
726
|
+
if outbox_deleted or items_deleted:
|
|
727
|
+
logger.info(
|
|
728
|
+
"purge_external_sync: outbox=%d stale_items=%d",
|
|
729
|
+
outbox_deleted,
|
|
730
|
+
items_deleted,
|
|
731
|
+
)
|