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,313 @@
|
|
|
1
|
+
"""Does this Monte Carlo forecast still describe the current plan? (#3140)
|
|
2
|
+
|
|
3
|
+
A forecast is a statement about a plan at a moment. The plan moves; the forecast does
|
|
4
|
+
not. Every surface that renders a forecast therefore has to answer "is this still
|
|
5
|
+
about the plan I am looking at?" — and until now each one answered it for itself.
|
|
6
|
+
|
|
7
|
+
The web forecast bar answered it with a ``useState(0)`` counter bumped from exactly one
|
|
8
|
+
mutation path, which meant the answer was a fact about one component instance's
|
|
9
|
+
lifetime rather than about the data: it missed inline edits, estimate edits (the actual
|
|
10
|
+
Monte Carlo inputs), a collaborator's write over the WebSocket, and it reset to zero on
|
|
11
|
+
every reload. Once #3132 gated the *Rerun action* on that predicate, the state a user
|
|
12
|
+
most needs the button in — a plan edited before the page was opened — was precisely the
|
|
13
|
+
state that read as "nothing to do".
|
|
14
|
+
|
|
15
|
+
Per ADR-0599 the authoritative classification of a server-computed outcome is itself
|
|
16
|
+
server-computed, so the classification lives here and ships on the payload. No client
|
|
17
|
+
re-derives it.
|
|
18
|
+
|
|
19
|
+
**The plan-version fact is reused, not invented.** ``Project.last_sync_version``
|
|
20
|
+
(ADR-0686 / #2491) is the per-project monotonic sequence every synced row write draws
|
|
21
|
+
its ``sync_seq`` from (:mod:`trueppm_api.apps.sync.sequence`). It is atomic, monotonic
|
|
22
|
+
and authoritative. :mod:`.risk_premium` still carries a comment saying the real stale
|
|
23
|
+
trigger "needs a cheap per-project mutation counter that does not exist yet"; that
|
|
24
|
+
comment predates ADR-0686 and is corrected there.
|
|
25
|
+
|
|
26
|
+
Three properties make it the right counter, and all three are load-bearing:
|
|
27
|
+
|
|
28
|
+
* **It over-approximates, and that is the safe direction.** It bumps for *any* synced
|
|
29
|
+
write in the project — a description edit moves it without moving the plan. So the
|
|
30
|
+
forecast can read stale when it is not. It can never read fresh when it is not, and
|
|
31
|
+
since the defect being fixed is an *unreachable action*, false-stale costs a button
|
|
32
|
+
nobody needed while false-fresh costs the action entirely.
|
|
33
|
+
* **A CPM recompute does not bump it.** CPM output writes go through ``bulk_update``
|
|
34
|
+
(ADR-0091) — no ``save()``, no allocation — so a recalc cannot manufacture staleness
|
|
35
|
+
on a plan nobody touched.
|
|
36
|
+
* **A Monte Carlo run does not bump it.** ``MonteCarloRun`` is a plain ``models.Model``
|
|
37
|
+
outside the sync union, so a run can never make itself stale.
|
|
38
|
+
|
|
39
|
+
**The second property also opens the only false-fresh hole, which is why there is a second
|
|
40
|
+
term.** A run's ``cpm_finish`` is read from *persisted CPM output* (``max(early_finish)``),
|
|
41
|
+
and a recalc rewrites that output without touching the counter. So: an edit bumps the
|
|
42
|
+
counter to N+1 and queues a recalc; the user reruns the forecast before the recalc lands,
|
|
43
|
+
recording ``plan_version = N+1`` against pre-recalc dates; the recalc then lands and moves
|
|
44
|
+
every ``early_finish``. Comparing N+1 to N+1 says ``current`` while the run's
|
|
45
|
+
``cpm_finish`` — and therefore its ``delta_vs_cpm`` and its whole risk-premium family —
|
|
46
|
+
describes a schedule that no longer exists. The window is not exotic: Rerun sits beside
|
|
47
|
+
the drag that queues the recalc.
|
|
48
|
+
|
|
49
|
+
``Project.recalculated_at`` (ADR-0114) closes it for free. It is stamped when the recalc
|
|
50
|
+
task succeeds, so a run older than it provably predates the current CPM output, and every
|
|
51
|
+
caller already holds the project row — no extra query.
|
|
52
|
+
|
|
53
|
+
**What is stored where, because the split is not the obvious one.** ``plan_version`` is an
|
|
54
|
+
*observation* made at run time, so it rides both the ``mc_latest`` cache entry and the
|
|
55
|
+
persisted ``MonteCarloRun`` row — it cannot be recovered later from either the project or
|
|
56
|
+
the clock. Only ``plan_version_current`` and the classification itself are derived per
|
|
57
|
+
response. Carrying it on the cache entry is not belt-and-braces: a run triggered by a
|
|
58
|
+
**Member** refreshes the cache but persists no attributed row (#1502), so the cache is the
|
|
59
|
+
*only* place that run's plan version exists.
|
|
60
|
+
|
|
61
|
+
Publishing ``plan_version_current`` is not new disclosure. ``Project.last_sync_version``
|
|
62
|
+
is already returned verbatim to the same Viewer+ audience as the ``timestamp`` cursor of
|
|
63
|
+
every ``GET /api/v1/sync/projects/<pk>/`` response
|
|
64
|
+
(:meth:`trueppm_api.apps.sync.views.ProjectSyncView._watermark`); the field comment on the
|
|
65
|
+
model says "excluded from the sync serializer", which is true of the row serializer and
|
|
66
|
+
easy to misread as "never leaves the server".
|
|
67
|
+
|
|
68
|
+
The audience is not *identical*, though, and the difference should not be lost: the sync
|
|
69
|
+
pull is not an MCP-readable view, while all three surfaces carrying these keys are. So an
|
|
70
|
+
``mcp:read`` token can now read this counter where none could before. That is a strict
|
|
71
|
+
subset of its owner's own access — the mixin confines tokens to safe methods behind the
|
|
72
|
+
same ``IsProjectMember`` gate — but "no new audience" would be the wrong summary.
|
|
73
|
+
|
|
74
|
+
This module is now a *consumer* of the allocator's monotonicity invariant, whose
|
|
75
|
+
writer-side precondition is tracked in #2617 — a change to the allocator changes what
|
|
76
|
+
"stale" means here.
|
|
77
|
+
|
|
78
|
+
**Composes with, and does not supersede, ``risk_premium_state``.** That enum answers a
|
|
79
|
+
different question — whether the *added-time number* is still worth reporting — and its
|
|
80
|
+
``stale`` occupies the same slot as ``premium``/``zero``/``negative``, so folding plan
|
|
81
|
+
drift into it would destroy the Overview card's premium classification. This module
|
|
82
|
+
therefore shares :data:`~.risk_premium.STALE_AFTER_DAYS` (one age threshold in the
|
|
83
|
+
codebase, so ``aged`` and ``risk_premium_state == "stale"`` can never disagree about the
|
|
84
|
+
age term) and *adds* the plan-drift term that age alone cannot see. Age is still needed
|
|
85
|
+
on its own: ADR-0132 floors a null status date at today, so an untouched project's
|
|
86
|
+
percentiles genuinely drift with the calendar even though its plan version has not moved.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
from __future__ import annotations
|
|
90
|
+
|
|
91
|
+
import datetime
|
|
92
|
+
from collections.abc import Mapping
|
|
93
|
+
from typing import Any, Literal, TypedDict
|
|
94
|
+
|
|
95
|
+
from .risk_premium import STALE_AFTER_DAYS
|
|
96
|
+
|
|
97
|
+
#: The forecast-vs-plan discriminant. Never ``None`` on the wire — a client renders from
|
|
98
|
+
#: this value alone and must never have to decide what an absent key means.
|
|
99
|
+
#:
|
|
100
|
+
#: * ``current`` — the run was computed against the version the project is on now, and it
|
|
101
|
+
#: is not old enough for the calendar to have moved underneath it.
|
|
102
|
+
#: * ``project_changed`` — the project has been written to since the run, or its CPM
|
|
103
|
+
#: output has been recomputed since the run.
|
|
104
|
+
#: * ``aged`` — nothing has been written, but the run is older than
|
|
105
|
+
#: :data:`~.risk_premium.STALE_AFTER_DAYS`, so the data date has advanced under it
|
|
106
|
+
#: (ADR-0132) and the percentiles no longer describe the same remaining work.
|
|
107
|
+
#: * ``unknown`` — the run recorded no plan version (it predates #3140), so no comparison
|
|
108
|
+
#: is possible. Deliberately distinct from ``current``: "we cannot tell" is not "it is
|
|
109
|
+
#: fine", and conflating them is how the original defect read.
|
|
110
|
+
#:
|
|
111
|
+
#: **``project_changed``, not ``plan_changed``, and the name is the honest part.** The
|
|
112
|
+
#: counter behind it — ``Project.last_sync_version`` — advances on *any* synced write in
|
|
113
|
+
#: the project, so a description edit or a label moves it without moving a single date.
|
|
114
|
+
#: It is therefore a sound *trigger* ("the inputs may have moved; offer the recompute")
|
|
115
|
+
#: and would be an unsound *claim* ("your plan changed"). A name that overstates its
|
|
116
|
+
#: evidence is worse here than one that understates it, because the value goes to LLM and
|
|
117
|
+
#: MCP callers that will narrate it as fact and have nothing else in the payload to check
|
|
118
|
+
#: it against. Any consumer wanting the stronger claim must corroborate it separately.
|
|
119
|
+
ForecastStaleness = Literal["current", "project_changed", "aged", "unknown"]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class ForecastStalenessFacts(TypedDict):
|
|
123
|
+
"""The staleness slice as it appears on a Monte Carlo payload."""
|
|
124
|
+
|
|
125
|
+
forecast_staleness: ForecastStaleness
|
|
126
|
+
#: The project's ``last_sync_version`` when the run was computed. ``None`` for runs
|
|
127
|
+
#: recorded before #3140 (nullable with no backfill, the same discipline
|
|
128
|
+
#: ``distribution``/``diagnostic``/``status_date`` already follow on ``MonteCarloRun``).
|
|
129
|
+
plan_version: int | None
|
|
130
|
+
#: The project's ``last_sync_version`` as of *this response*. Carried beside the run's
|
|
131
|
+
#: own version so a reader — including an MCP client — can state the finding
|
|
132
|
+
#: ("computed against 412, plan is at 419") rather than only its verdict.
|
|
133
|
+
plan_version_current: int | None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def classify_forecast_staleness(
|
|
137
|
+
*,
|
|
138
|
+
plan_version: int | None,
|
|
139
|
+
plan_version_current: int | None,
|
|
140
|
+
age_days: int | None,
|
|
141
|
+
recalculated_after_run: bool = False,
|
|
142
|
+
) -> ForecastStaleness:
|
|
143
|
+
"""Classify one run against the plan it is being read beside.
|
|
144
|
+
|
|
145
|
+
Evaluated in a fixed order, and the order is the safety property: **``current`` is
|
|
146
|
+
unreachable without a recorded plan version.** Every run in every install is
|
|
147
|
+
version-less on the day this ships (nullable, no backfill), and a decision order that
|
|
148
|
+
let one of them fall through to ``current`` because it also happened to be younger
|
|
149
|
+
than the age threshold would hide the Rerun button across the entire fleet — the
|
|
150
|
+
original defect, reintroduced by its own fix. So the missing-input branch is decided
|
|
151
|
+
first, exactly as ``unmeasurable`` is decided before the zero branch in
|
|
152
|
+
:func:`~.risk_premium._premium_state`.
|
|
153
|
+
|
|
154
|
+
Within that branch the age rule still runs, because it answers a *narrower* question
|
|
155
|
+
the timestamp alone can support: a version-less run past
|
|
156
|
+
:data:`~.risk_premium.STALE_AFTER_DAYS` is provably ``aged``, which is strictly more
|
|
157
|
+
useful to a reader than ``unknown`` and equally non-``current``.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
plan_version: The project's plan version when the run was computed, or ``None``
|
|
161
|
+
for a run recorded before #3140.
|
|
162
|
+
plan_version_current: The project's plan version now, or ``None`` when the
|
|
163
|
+
project row could not be read.
|
|
164
|
+
age_days: Whole days between the run and today, or ``None`` when the run carries
|
|
165
|
+
no timestamp.
|
|
166
|
+
recalculated_after_run: Whether the project's CPM output was rewritten after this
|
|
167
|
+
run was taken (``Project.recalculated_at > taken_at``). Closes the one
|
|
168
|
+
false-fresh path the version counter cannot see — see the module docstring.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The discriminant a client renders from.
|
|
172
|
+
"""
|
|
173
|
+
aged = age_days is not None and age_days > STALE_AFTER_DAYS
|
|
174
|
+
|
|
175
|
+
if plan_version is None or plan_version_current is None:
|
|
176
|
+
# No comparison is possible. Report what the timestamp *can* prove, and otherwise
|
|
177
|
+
# report that the run is unplaceable — never that it is fine. The client keeps the
|
|
178
|
+
# action reachable on both and withholds the stale claim it cannot support.
|
|
179
|
+
if recalculated_after_run:
|
|
180
|
+
return "project_changed"
|
|
181
|
+
return "aged" if aged else "unknown"
|
|
182
|
+
|
|
183
|
+
# Compared with `!=`, not `<`, deliberately: a restored or rolled-back database whose
|
|
184
|
+
# counter went backwards is exactly as unplaceable as one that moved forward, and
|
|
185
|
+
# `<` would call that case `current`.
|
|
186
|
+
if plan_version != plan_version_current or recalculated_after_run:
|
|
187
|
+
return "project_changed"
|
|
188
|
+
|
|
189
|
+
if aged:
|
|
190
|
+
return "aged"
|
|
191
|
+
|
|
192
|
+
return "current"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def forecast_staleness_facts(
|
|
196
|
+
*,
|
|
197
|
+
plan_version: int | None,
|
|
198
|
+
plan_version_current: int | None,
|
|
199
|
+
taken_at: datetime.datetime | None,
|
|
200
|
+
recalculated_at: datetime.datetime | None,
|
|
201
|
+
today: datetime.date,
|
|
202
|
+
) -> ForecastStalenessFacts:
|
|
203
|
+
"""Build the staleness slice for one forecast response.
|
|
204
|
+
|
|
205
|
+
The single entry point — the live run, the cached read and the from-history fallback
|
|
206
|
+
all reach :func:`classify_forecast_staleness` through here, so the platform has one
|
|
207
|
+
derivation of what "stale" means for a forecast.
|
|
208
|
+
|
|
209
|
+
Derived per response and never stored, for the same reason the risk premium is
|
|
210
|
+
(ADR-0698 §2): both of its inputs move underneath a 24-hour cache entry. The plan
|
|
211
|
+
version advances every time anyone writes to the project, and ``today`` advances on
|
|
212
|
+
its own.
|
|
213
|
+
"""
|
|
214
|
+
age_days = (today - taken_at.date()).days if taken_at is not None else None
|
|
215
|
+
# Both timestamps are timezone-aware in practice, but a legacy cache entry can carry a
|
|
216
|
+
# naive one; comparing the two would raise TypeError and 500 a read path. Treat an
|
|
217
|
+
# unusable pair as "no evidence of a recalc" rather than letting it decide anything.
|
|
218
|
+
recalculated_after_run = False
|
|
219
|
+
if recalculated_at is not None and taken_at is not None:
|
|
220
|
+
naive = (recalculated_at.tzinfo is None) != (taken_at.tzinfo is None)
|
|
221
|
+
recalculated_after_run = not naive and recalculated_at > taken_at
|
|
222
|
+
return ForecastStalenessFacts(
|
|
223
|
+
forecast_staleness=classify_forecast_staleness(
|
|
224
|
+
plan_version=plan_version,
|
|
225
|
+
plan_version_current=plan_version_current,
|
|
226
|
+
age_days=age_days,
|
|
227
|
+
recalculated_after_run=recalculated_after_run,
|
|
228
|
+
),
|
|
229
|
+
plan_version=plan_version,
|
|
230
|
+
plan_version_current=plan_version_current,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def forecast_staleness_from_run(
|
|
235
|
+
run: Any | None,
|
|
236
|
+
*,
|
|
237
|
+
plan_version_current: int | None,
|
|
238
|
+
recalculated_at: datetime.datetime | None,
|
|
239
|
+
today: datetime.date,
|
|
240
|
+
) -> ForecastStalenessFacts:
|
|
241
|
+
"""Derive the staleness slice from a persisted :class:`.models.MonteCarloRun`.
|
|
242
|
+
|
|
243
|
+
``run`` is ``None`` when the project has never been simulated, which classifies as
|
|
244
|
+
``unknown`` — there is no run to place against a plan. Thin adapter over
|
|
245
|
+
:func:`forecast_staleness_facts`, which owns the derivation, and the counterpart to
|
|
246
|
+
:func:`~.risk_premium.build_risk_premium`.
|
|
247
|
+
|
|
248
|
+
Kept value-shaped rather than request- or response-bound on purpose: an Enterprise
|
|
249
|
+
drift digest (``trueppm-enterprise#192``) has to be able to read this OSS fact by
|
|
250
|
+
importing it — enterprise → core, the one sanctioned direction — instead of
|
|
251
|
+
re-deriving its own answer beside ours and drifting from it.
|
|
252
|
+
"""
|
|
253
|
+
if run is None:
|
|
254
|
+
return forecast_staleness_facts(
|
|
255
|
+
plan_version=None,
|
|
256
|
+
plan_version_current=plan_version_current,
|
|
257
|
+
taken_at=None,
|
|
258
|
+
recalculated_at=recalculated_at,
|
|
259
|
+
today=today,
|
|
260
|
+
)
|
|
261
|
+
return forecast_staleness_facts(
|
|
262
|
+
plan_version=run.plan_version,
|
|
263
|
+
plan_version_current=plan_version_current,
|
|
264
|
+
taken_at=run.taken_at,
|
|
265
|
+
recalculated_at=recalculated_at,
|
|
266
|
+
today=today,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _parse_int(value: Any) -> int | None:
|
|
271
|
+
"""Best-effort int; ``None`` for anything that is not one.
|
|
272
|
+
|
|
273
|
+
The cache entry is a plain dict written by a possibly older deploy, so its
|
|
274
|
+
``plan_version`` is only an int by convention. A ``bool`` is rejected explicitly —
|
|
275
|
+
it is an ``int`` subclass, and letting ``True`` through would compare as ``1`` and
|
|
276
|
+
silently classify a forecast against plan version one.
|
|
277
|
+
"""
|
|
278
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
279
|
+
return None
|
|
280
|
+
return int(value)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def forecast_staleness_for_payload(
|
|
284
|
+
payload: Mapping[str, Any],
|
|
285
|
+
*,
|
|
286
|
+
plan_version_current: int | None,
|
|
287
|
+
recalculated_at: datetime.datetime | None,
|
|
288
|
+
today: datetime.date,
|
|
289
|
+
) -> ForecastStalenessFacts:
|
|
290
|
+
"""Derive the staleness slice from a cached Monte Carlo forecast dict.
|
|
291
|
+
|
|
292
|
+
Absence is tolerated rather than trusted, mirroring
|
|
293
|
+
:func:`~.risk_premium.risk_premium_for_forecast_payload`: a ``plan_version`` key that
|
|
294
|
+
is missing, ``None`` or not an int resolves to ``unknown`` — "we do not know" —
|
|
295
|
+
instead of inventing a comparison from a partial payload.
|
|
296
|
+
"""
|
|
297
|
+
taken_at = payload.get("last_run_at")
|
|
298
|
+
parsed_at: datetime.datetime | None = None
|
|
299
|
+
if isinstance(taken_at, datetime.datetime):
|
|
300
|
+
parsed_at = taken_at
|
|
301
|
+
elif isinstance(taken_at, str):
|
|
302
|
+
try:
|
|
303
|
+
parsed_at = datetime.datetime.fromisoformat(taken_at)
|
|
304
|
+
except ValueError:
|
|
305
|
+
parsed_at = None
|
|
306
|
+
|
|
307
|
+
return forecast_staleness_facts(
|
|
308
|
+
plan_version=_parse_int(payload.get("plan_version")),
|
|
309
|
+
plan_version_current=plan_version_current,
|
|
310
|
+
taken_at=parsed_at,
|
|
311
|
+
recalculated_at=recalculated_at,
|
|
312
|
+
today=today,
|
|
313
|
+
)
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Reusable dependency-graph validation for bulk / agent-authored task writes.
|
|
2
|
+
|
|
3
|
+
The human write path (``DependencySerializer._check_no_cycle``) runs
|
|
4
|
+
self-reference and cycle detection on every single proposed edge before it is
|
|
5
|
+
persisted. Bulk and agent write paths — the MS Project importer, the offline
|
|
6
|
+
Jira importer (#1664), the spreadsheet importer, and any future non-interactive
|
|
7
|
+
writer — build whole ``Dependency`` graphs with ``bulk_create`` and therefore
|
|
8
|
+
**bypass** that gate entirely.
|
|
9
|
+
|
|
10
|
+
Every importer named above now calls this guard before its write. The seed
|
|
11
|
+
importer (``apps/seed/importer.py``) still does not — tracked in #2589.
|
|
12
|
+
|
|
13
|
+
That gap is benign only while no bulk writer accepts an untrusted graph. The
|
|
14
|
+
moment a prospect's messy Jira export can be imported (#1664), a cyclic or
|
|
15
|
+
self-referential link would persist an infeasible dependency network that then
|
|
16
|
+
crashes the CPM / what-if engine on the imported data. The differentiator's
|
|
17
|
+
whole claim is that human and agent principals are governed identically; a
|
|
18
|
+
validation gate that only the human path enforces violates "hybrid by
|
|
19
|
+
construction."
|
|
20
|
+
|
|
21
|
+
This module is the shared gate. It wraps the *same* ``trueppm_scheduler``
|
|
22
|
+
``find_cycle`` algorithm the serializer uses — so the human and agent paths
|
|
23
|
+
detect the identical set of cycles — plus the trivial self-reference identity
|
|
24
|
+
check. Callers pass the *complete* edge set they are about to persist (they
|
|
25
|
+
already hold it in memory, so no DB round-trip is needed) and run this guard
|
|
26
|
+
**before** the write and before ``enqueue_recalculate``.
|
|
27
|
+
|
|
28
|
+
The guard operates on opaque string node ids, so callers may validate in their
|
|
29
|
+
own external-id space (Jira issue keys, MS Project uids) *before* creating any
|
|
30
|
+
row — the cycle/self-reference structure is invariant under relabeling, so a bad
|
|
31
|
+
graph is rejected before a single task is written.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import re
|
|
37
|
+
from collections.abc import Mapping
|
|
38
|
+
|
|
39
|
+
from trueppm_scheduler import find_cycle
|
|
40
|
+
|
|
41
|
+
#: Longest one node label may run in a labelled message before it is elided.
|
|
42
|
+
MAX_LABEL_CHARS = 40
|
|
43
|
+
#: Most labels rendered along a cycle path; the rest collapse to "… (N more)".
|
|
44
|
+
#:
|
|
45
|
+
#: A cycle is a *set* of mutually-blocking tasks, so naming three or four of its
|
|
46
|
+
#: members and counting the rest identifies the loop as well as listing all of
|
|
47
|
+
#: them would — and a 200-task cycle must not produce a 200-name sentence.
|
|
48
|
+
MAX_CYCLE_LABELS = 4
|
|
49
|
+
#: Hard ceiling the two caps above are chosen to keep the whole sentence under.
|
|
50
|
+
#:
|
|
51
|
+
#: Not decoration: the classification popover's ``presentable()``
|
|
52
|
+
#: (``useClassificationPopover.ts``) *discards* a server message longer than 300
|
|
53
|
+
#: characters and shows its generic fallback instead, so an unbounded sentence
|
|
54
|
+
#: would silently undo this whole message. Asserted in the guard's tests.
|
|
55
|
+
#:
|
|
56
|
+
#: **Measured in UTF-16 code units, not code points**, because that is what the
|
|
57
|
+
#: check on the other side measures: JavaScript's ``String.length`` counts an
|
|
58
|
+
#: astral character (an emoji in a task name — ordinary, not adversarial) as two.
|
|
59
|
+
#: A code-point budget would pass here and be discarded there, which is the exact
|
|
60
|
+
#: failure this cap exists to prevent.
|
|
61
|
+
MAX_MESSAGE_CHARS = 300
|
|
62
|
+
|
|
63
|
+
#: C0 and C1 control characters, plus DEL.
|
|
64
|
+
_CONTROL_CHARS = re.compile(r"[\x00-\x1f\x7f-\x9f]")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _utf16_len(text: str) -> int:
|
|
68
|
+
"""Length as JavaScript's ``String.length`` would report it."""
|
|
69
|
+
return sum(2 if ord(ch) > 0xFFFF else 1 for ch in text)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _elide(text: str, limit: int) -> str:
|
|
73
|
+
"""Trim ``text`` to at most ``limit`` UTF-16 units, marking the cut with "…".
|
|
74
|
+
|
|
75
|
+
Iterates code points, so the cut can never land between the halves of a
|
|
76
|
+
surrogate pair and produce a lone surrogate.
|
|
77
|
+
"""
|
|
78
|
+
if _utf16_len(text) <= limit:
|
|
79
|
+
return text
|
|
80
|
+
kept: list[str] = []
|
|
81
|
+
width = 0
|
|
82
|
+
for ch in text:
|
|
83
|
+
char_width = 2 if ord(ch) > 0xFFFF else 1
|
|
84
|
+
if width + char_width > limit - 1:
|
|
85
|
+
break
|
|
86
|
+
kept.append(ch)
|
|
87
|
+
width += char_width
|
|
88
|
+
return "".join(kept).rstrip() + "…"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _labelled(node: str, labels: Mapping[str, str]) -> str:
|
|
92
|
+
"""A node's human label, falling back to its raw id when it has none.
|
|
93
|
+
|
|
94
|
+
A per-node fallback rather than an all-or-nothing one: a cycle whose members
|
|
95
|
+
are mostly resolvable is still worth naming, and a row deleted between the
|
|
96
|
+
refusal and the lookup must not cost the caller the other names.
|
|
97
|
+
|
|
98
|
+
Control characters are dropped and whitespace collapsed first. A label is
|
|
99
|
+
built from a task name, which is caller-controlled text that ``CharField``
|
|
100
|
+
does not stop from holding a newline, a tab, or an ANSI escape — and this
|
|
101
|
+
string is now *prose*, which a CLI or agent client is far likelier to print
|
|
102
|
+
raw than a JSON field. A name carrying its own line breaks could otherwise
|
|
103
|
+
fake the end of the message and append a sentence of its own.
|
|
104
|
+
"""
|
|
105
|
+
printable = _CONTROL_CHARS.sub(" ", labels.get(node, node))
|
|
106
|
+
return _elide(" ".join(printable.split()), MAX_LABEL_CHARS)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def nodes_to_label(offending: list[str]) -> list[str]:
|
|
110
|
+
"""The node ids a labelled message will actually name.
|
|
111
|
+
|
|
112
|
+
Exposed so a caller can resolve *only* the ids it is going to print. A cycle
|
|
113
|
+
is capped at :data:`MAX_CYCLE_LABELS` names, so resolving all of a 5000-task
|
|
114
|
+
loop would materialize 5000 rows to render four.
|
|
115
|
+
|
|
116
|
+
Deliberately lives beside :func:`_render_cycle` rather than being reproduced
|
|
117
|
+
at the call site: the two have to agree about which positions survive the
|
|
118
|
+
elision, and a caller re-deriving that slice locally is how they drift apart.
|
|
119
|
+
"""
|
|
120
|
+
if len(offending) <= MAX_CYCLE_LABELS:
|
|
121
|
+
return list(offending)
|
|
122
|
+
return [*offending[: MAX_CYCLE_LABELS - 1], offending[-1]]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _render_cycle(offending: list[str], labels: Mapping[str, str]) -> str:
|
|
126
|
+
"""The cycle path as an arrow chain, elided in the middle when it is long.
|
|
127
|
+
|
|
128
|
+
``find_cycle`` repeats the first node at the end, and that repetition is what
|
|
129
|
+
makes the sentence read as a loop — so the last element is always kept and
|
|
130
|
+
the elision is taken out of the middle.
|
|
131
|
+
"""
|
|
132
|
+
if len(offending) <= MAX_CYCLE_LABELS:
|
|
133
|
+
return " → ".join(_labelled(node, labels) for node in offending)
|
|
134
|
+
head = offending[: MAX_CYCLE_LABELS - 1]
|
|
135
|
+
hidden = len(offending) - MAX_CYCLE_LABELS
|
|
136
|
+
parts = [_labelled(node, labels) for node in head]
|
|
137
|
+
parts.append(f"… ({hidden} more)")
|
|
138
|
+
parts.append(_labelled(offending[-1], labels))
|
|
139
|
+
return " → ".join(parts)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _build_message(reason: str, offending: list[str], labels: Mapping[str, str] | None) -> str:
|
|
143
|
+
"""The exception's message, human-facing when the caller supplied labels.
|
|
144
|
+
|
|
145
|
+
Without labels the original domain-signal message is kept verbatim. That is
|
|
146
|
+
what the importers validate in — they run the guard in their own external-id
|
|
147
|
+
space (Jira keys, MS Project uids) *before* any row exists, so there is no
|
|
148
|
+
task to name yet, and each already renders its own sentence from ``reason``
|
|
149
|
+
and ``offending`` (``msproject.importer.describe_bad_graph``).
|
|
150
|
+
|
|
151
|
+
Both branches tolerate an empty ``offending``. Neither is reachable from
|
|
152
|
+
:func:`validate_task_graph`, which always blames at least one node — but the
|
|
153
|
+
message builder must not be the thing that turns a refusal into a 500, or a
|
|
154
|
+
dangling separator, if a later caller constructs one.
|
|
155
|
+
"""
|
|
156
|
+
if labels is None:
|
|
157
|
+
return f"Infeasible task graph ({reason}): {offending}"
|
|
158
|
+
if reason == "self_reference":
|
|
159
|
+
node = _labelled(offending[0], labels) if offending else "A task"
|
|
160
|
+
return (
|
|
161
|
+
f"Task {node} lists itself as its own predecessor. "
|
|
162
|
+
"Remove that link to schedule this plan."
|
|
163
|
+
)
|
|
164
|
+
named = _render_cycle(offending, labels) if offending else "these tasks"
|
|
165
|
+
return f"Circular dependency: {named}. Remove one of those links to schedule this plan."
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class InfeasibleGraphError(Exception):
|
|
169
|
+
"""A proposed task-dependency graph cannot be persisted.
|
|
170
|
+
|
|
171
|
+
Deliberately not a DRF ``ValidationError``: this is a domain signal raised
|
|
172
|
+
from service / importer code that has no request context. Callers translate
|
|
173
|
+
it to whatever their surface needs — an importer marks its outbox row DEAD
|
|
174
|
+
and records the reason on the TaskRunTracker; a view would map it to a 400.
|
|
175
|
+
|
|
176
|
+
Attributes:
|
|
177
|
+
reason: A stable machine code — ``"self_reference"`` or
|
|
178
|
+
``"cyclic_dependency"`` — so callers can branch (quarantine a
|
|
179
|
+
self-loop vs. reject a whole import) without string-matching the
|
|
180
|
+
message.
|
|
181
|
+
offending: The node ids implicated. For ``"self_reference"`` the single
|
|
182
|
+
offending node; for ``"cyclic_dependency"`` the ordered cycle path
|
|
183
|
+
(first id repeated at the end, e.g. ``["A", "B", "A"]``), matching
|
|
184
|
+
:func:`trueppm_scheduler.find_cycle`.
|
|
185
|
+
labels: ``{node_id: human label}`` when a caller has resolved the ids to
|
|
186
|
+
something the person reading the refusal can find in their plan, or
|
|
187
|
+
``None`` when the message is still in id space. Only the *message*
|
|
188
|
+
reads this — ``offending`` stays the raw id list either way, because
|
|
189
|
+
that is what a client branches or highlights rows on.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
def __init__(
|
|
193
|
+
self,
|
|
194
|
+
reason: str,
|
|
195
|
+
offending: list[str],
|
|
196
|
+
*,
|
|
197
|
+
labels: Mapping[str, str] | None = None,
|
|
198
|
+
) -> None:
|
|
199
|
+
self.reason = reason
|
|
200
|
+
self.offending = offending
|
|
201
|
+
self.labels: dict[str, str] | None = dict(labels) if labels is not None else None
|
|
202
|
+
super().__init__(_build_message(reason, offending, self.labels))
|
|
203
|
+
|
|
204
|
+
def with_labels(self, labels: Mapping[str, str]) -> InfeasibleGraphError:
|
|
205
|
+
"""This same refusal, re-worded to name its nodes rather than list their ids.
|
|
206
|
+
|
|
207
|
+
Returns a new instance rather than mutating in place so the raised object
|
|
208
|
+
stays immutable and the original stays available as the ``__cause__`` of
|
|
209
|
+
whatever the caller re-raises.
|
|
210
|
+
|
|
211
|
+
The guard itself operates on opaque node ids and cannot resolve them, so
|
|
212
|
+
labelling belongs to the caller that knows the id space. Doing it *after*
|
|
213
|
+
the refusal rather than passing labels in is deliberate: only the ids the
|
|
214
|
+
guard actually blamed need resolving, and the feasible path — every
|
|
215
|
+
request that is not refused — pays nothing at all.
|
|
216
|
+
"""
|
|
217
|
+
return InfeasibleGraphError(self.reason, self.offending, labels=labels)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def validate_task_graph(
|
|
221
|
+
edges: list[tuple[str, str]],
|
|
222
|
+
*,
|
|
223
|
+
children_map: dict[str, list[str]] | None = None,
|
|
224
|
+
) -> None:
|
|
225
|
+
"""Reject a self-referential or cyclic dependency graph before it is written.
|
|
226
|
+
|
|
227
|
+
Runs the same self-reference and :func:`trueppm_scheduler.find_cycle` checks
|
|
228
|
+
the interactive ``DependencySerializer`` path runs, so bulk / agent writers
|
|
229
|
+
are governed identically to the human write path.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
edges: The complete ``(predecessor_id, successor_id)`` set the caller is
|
|
233
|
+
about to persist, as opaque string node ids. Callers that want to
|
|
234
|
+
validate before minting DB rows may pass their external-id space
|
|
235
|
+
(e.g. Jira issue keys) — cycle/self-reference detection is invariant
|
|
236
|
+
under relabeling.
|
|
237
|
+
children_map: Optional ``{summary_id: [child_id, ...]}`` mapping so that
|
|
238
|
+
summary→leaf logical cycles are expanded and caught, matching the
|
|
239
|
+
serializer's summary handling. Omit (``None``) for flat graphs with
|
|
240
|
+
no summary tasks (the minimal-import case), where direct edge cycle
|
|
241
|
+
detection is sufficient.
|
|
242
|
+
|
|
243
|
+
Raises:
|
|
244
|
+
InfeasibleGraphError: With ``reason="self_reference"`` if any edge links
|
|
245
|
+
a task to itself, or ``reason="cyclic_dependency"`` if the graph
|
|
246
|
+
contains a cycle. The message and ``offending`` path let the caller
|
|
247
|
+
surface an actionable error rather than crash the CPM engine.
|
|
248
|
+
InvalidScheduleInput: Re-raised unchanged if the graph is *malformed*
|
|
249
|
+
(a pathological summary fan-out beyond the engine's cap, or a
|
|
250
|
+
children_map that is itself cyclic) — a distinct failure from a
|
|
251
|
+
cycle in the edges. Callers treat it the same as a rejection.
|
|
252
|
+
"""
|
|
253
|
+
# Self-reference is a cheap identity check that also gives the caller a
|
|
254
|
+
# precise offending node (find_cycle would report it as a length-1 loop,
|
|
255
|
+
# but a dedicated code lets importers *quarantine* a self-loop — skip it
|
|
256
|
+
# with a warning — rather than reject the whole import).
|
|
257
|
+
for predecessor, successor in edges:
|
|
258
|
+
if predecessor == successor:
|
|
259
|
+
raise InfeasibleGraphError("self_reference", [predecessor])
|
|
260
|
+
|
|
261
|
+
result = find_cycle(edges, children_map=children_map)
|
|
262
|
+
if result.cycle is not None:
|
|
263
|
+
raise InfeasibleGraphError("cyclic_dependency", result.cycle)
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Apply the tiered retention curve to project forecast snapshots (ADR-0154, #388).
|
|
2
|
+
|
|
3
|
+
Operator-facing wrapper around the same ``_do_prune_forecast_snapshots`` logic the
|
|
4
|
+
nightly ``scheduling.prune_forecast_snapshots`` Celery Beat task runs. Per
|
|
5
|
+
``settings.FORECAST_SNAPSHOT_RETENTION``: keep all rows younger than ``daily_days``
|
|
6
|
+
(default 90); keep one-per-ISO-week up to ``weekly_days`` (default 365); keep
|
|
7
|
+
one-per-calendar-month beyond that (kept forever).
|
|
8
|
+
|
|
9
|
+
Idempotent: a second run deletes nothing new (the keepers are already the only rows
|
|
10
|
+
left in each bucket).
|
|
11
|
+
|
|
12
|
+
Usage::
|
|
13
|
+
|
|
14
|
+
python manage.py prune_forecast_snapshots # apply
|
|
15
|
+
python manage.py prune_forecast_snapshots --dry-run # report only, no delete
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from django.core.management.base import BaseCommand
|
|
24
|
+
|
|
25
|
+
from trueppm_api.apps.scheduling.models import ProjectForecastSnapshot
|
|
26
|
+
from trueppm_api.apps.scheduling.tasks import _do_prune_forecast_snapshots
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Command(BaseCommand):
|
|
32
|
+
help = "Prune project forecast snapshots per the tiered retention policy (ADR-0154)."
|
|
33
|
+
|
|
34
|
+
def add_arguments(self, parser: Any) -> None:
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--dry-run",
|
|
37
|
+
action="store_true",
|
|
38
|
+
help="Report how many rows would be deleted without deleting them.",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def handle(self, *args: Any, **options: Any) -> None:
|
|
42
|
+
if options["dry_run"]:
|
|
43
|
+
# Mirror the task's bucketing in a counting-only pass: total - keepers.
|
|
44
|
+
# Re-using the live function would delete, so we report the current count
|
|
45
|
+
# and let the operator run without --dry-run to apply.
|
|
46
|
+
total = ProjectForecastSnapshot.objects.count()
|
|
47
|
+
self.stdout.write(
|
|
48
|
+
f"{total} forecast snapshot(s) on record; run without --dry-run to prune."
|
|
49
|
+
)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
deleted = _do_prune_forecast_snapshots()
|
|
53
|
+
self.stdout.write(self.style.SUCCESS(f"Pruned {deleted} forecast snapshot(s)."))
|