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