trueppm-api 0.4.0__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 (799) hide show
  1. trueppm_api/__init__.py +27 -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 +346 -0
  8. trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
  9. trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -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 +184 -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_squashed_0019_release_0_4.py +302 -0
  24. trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
  25. trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
  26. trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
  27. trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
  28. trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +143 -0
  29. trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
  30. trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
  31. trueppm_api/apps/access/migrations/__init__.py +0 -0
  32. trueppm_api/apps/access/minter_role.py +38 -0
  33. trueppm_api/apps/access/models.py +588 -0
  34. trueppm_api/apps/access/permissions.py +3263 -0
  35. trueppm_api/apps/access/serializers.py +1007 -0
  36. trueppm_api/apps/access/services.py +878 -0
  37. trueppm_api/apps/access/signals.py +171 -0
  38. trueppm_api/apps/access/tasks.py +50 -0
  39. trueppm_api/apps/access/throttles.py +96 -0
  40. trueppm_api/apps/access/urls.py +191 -0
  41. trueppm_api/apps/access/views.py +1965 -0
  42. trueppm_api/apps/agents/__init__.py +6 -0
  43. trueppm_api/apps/agents/apps.py +11 -0
  44. trueppm_api/apps/agents/canonical.py +73 -0
  45. trueppm_api/apps/agents/deferred.py +108 -0
  46. trueppm_api/apps/agents/management/__init__.py +0 -0
  47. trueppm_api/apps/agents/management/commands/__init__.py +0 -0
  48. trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
  49. trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
  50. trueppm_api/apps/agents/middleware.py +44 -0
  51. trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
  52. trueppm_api/apps/agents/migrations/0001_squashed_0003_release_0_4.py +344 -0
  53. trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
  54. trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
  55. trueppm_api/apps/agents/migrations/__init__.py +0 -0
  56. trueppm_api/apps/agents/models.py +371 -0
  57. trueppm_api/apps/agents/refusal.py +103 -0
  58. trueppm_api/apps/agents/serializers.py +85 -0
  59. trueppm_api/apps/agents/services.py +397 -0
  60. trueppm_api/apps/agents/signals.py +30 -0
  61. trueppm_api/apps/agents/urls.py +15 -0
  62. trueppm_api/apps/agents/views.py +109 -0
  63. trueppm_api/apps/csvimport/__init__.py +12 -0
  64. trueppm_api/apps/csvimport/apps.py +11 -0
  65. trueppm_api/apps/csvimport/mapping.py +363 -0
  66. trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
  67. trueppm_api/apps/csvimport/migrations/0001_squashed_0003_release_0_4.py +90 -0
  68. trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
  69. trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
  70. trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
  71. trueppm_api/apps/csvimport/models.py +111 -0
  72. trueppm_api/apps/csvimport/parser.py +1825 -0
  73. trueppm_api/apps/csvimport/services.py +48 -0
  74. trueppm_api/apps/csvimport/tasks.py +459 -0
  75. trueppm_api/apps/csvimport/template.py +37 -0
  76. trueppm_api/apps/csvimport/urls.py +50 -0
  77. trueppm_api/apps/csvimport/views.py +793 -0
  78. trueppm_api/apps/history/__init__.py +0 -0
  79. trueppm_api/apps/history/apps.py +12 -0
  80. trueppm_api/apps/history/changelog.py +359 -0
  81. trueppm_api/apps/history/diff_policy.py +178 -0
  82. trueppm_api/apps/history/serializers.py +104 -0
  83. trueppm_api/apps/history/signals.py +82 -0
  84. trueppm_api/apps/history/tasks.py +105 -0
  85. trueppm_api/apps/history/urls.py +38 -0
  86. trueppm_api/apps/history/views.py +443 -0
  87. trueppm_api/apps/idempotency/__init__.py +0 -0
  88. trueppm_api/apps/idempotency/apps.py +11 -0
  89. trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
  90. trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
  91. trueppm_api/apps/idempotency/mixins.py +226 -0
  92. trueppm_api/apps/idempotency/models.py +75 -0
  93. trueppm_api/apps/idempotency/tasks.py +61 -0
  94. trueppm_api/apps/integrations/__init__.py +13 -0
  95. trueppm_api/apps/integrations/apps.py +59 -0
  96. trueppm_api/apps/integrations/connections.py +806 -0
  97. trueppm_api/apps/integrations/encryption.py +92 -0
  98. trueppm_api/apps/integrations/external_sources.py +863 -0
  99. trueppm_api/apps/integrations/git_automation_services.py +231 -0
  100. trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
  101. trueppm_api/apps/integrations/http.py +597 -0
  102. trueppm_api/apps/integrations/me_work.py +218 -0
  103. trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
  104. trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
  105. trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
  106. trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
  107. trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
  108. trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
  109. trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
  110. trueppm_api/apps/integrations/migrations/0007_squashed_0014_release_0_4.py +225 -0
  111. trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
  112. trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
  113. trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
  114. trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
  115. trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
  116. trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
  117. trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
  118. trueppm_api/apps/integrations/migrations/0014_tasklink_created_by.py +27 -0
  119. trueppm_api/apps/integrations/migrations/__init__.py +0 -0
  120. trueppm_api/apps/integrations/models.py +635 -0
  121. trueppm_api/apps/integrations/notification_channels.py +57 -0
  122. trueppm_api/apps/integrations/opengraph.py +229 -0
  123. trueppm_api/apps/integrations/outgoing.py +201 -0
  124. trueppm_api/apps/integrations/providers.py +528 -0
  125. trueppm_api/apps/integrations/registry.py +345 -0
  126. trueppm_api/apps/integrations/serializers.py +453 -0
  127. trueppm_api/apps/integrations/services.py +171 -0
  128. trueppm_api/apps/integrations/tasks.py +734 -0
  129. trueppm_api/apps/integrations/throttles.py +165 -0
  130. trueppm_api/apps/integrations/urls.py +77 -0
  131. trueppm_api/apps/integrations/views.py +1158 -0
  132. trueppm_api/apps/jiraimport/__init__.py +8 -0
  133. trueppm_api/apps/jiraimport/apps.py +11 -0
  134. trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
  135. trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
  136. trueppm_api/apps/jiraimport/models.py +70 -0
  137. trueppm_api/apps/jiraimport/parser.py +350 -0
  138. trueppm_api/apps/jiraimport/services.py +46 -0
  139. trueppm_api/apps/jiraimport/tasks.py +280 -0
  140. trueppm_api/apps/jiraimport/urls.py +15 -0
  141. trueppm_api/apps/jiraimport/views.py +153 -0
  142. trueppm_api/apps/msproject/__init__.py +0 -0
  143. trueppm_api/apps/msproject/apps.py +11 -0
  144. trueppm_api/apps/msproject/dataclasses.py +153 -0
  145. trueppm_api/apps/msproject/exporter.py +551 -0
  146. trueppm_api/apps/msproject/extended_attributes.py +77 -0
  147. trueppm_api/apps/msproject/importer.py +1095 -0
  148. trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
  149. trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
  150. trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
  151. trueppm_api/apps/msproject/migrations/__init__.py +0 -0
  152. trueppm_api/apps/msproject/models.py +81 -0
  153. trueppm_api/apps/msproject/parser.py +1276 -0
  154. trueppm_api/apps/msproject/serializers.py +136 -0
  155. trueppm_api/apps/msproject/services.py +58 -0
  156. trueppm_api/apps/msproject/tasks.py +439 -0
  157. trueppm_api/apps/msproject/urls.py +39 -0
  158. trueppm_api/apps/msproject/views.py +531 -0
  159. trueppm_api/apps/notifications/__init__.py +7 -0
  160. trueppm_api/apps/notifications/apps.py +15 -0
  161. trueppm_api/apps/notifications/backfill.py +79 -0
  162. trueppm_api/apps/notifications/categories.py +112 -0
  163. trueppm_api/apps/notifications/delivery_limits.py +258 -0
  164. trueppm_api/apps/notifications/digests.py +462 -0
  165. trueppm_api/apps/notifications/email_backend.py +350 -0
  166. trueppm_api/apps/notifications/email_health.py +128 -0
  167. trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
  168. trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +299 -0
  169. trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
  170. trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
  171. trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
  172. trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
  173. trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
  174. trueppm_api/apps/notifications/migrations/0007_squashed_0011_release_0_4.py +204 -0
  175. trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
  176. trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
  177. trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
  178. trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
  179. trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
  180. trueppm_api/apps/notifications/migrations/__init__.py +0 -0
  181. trueppm_api/apps/notifications/models.py +1148 -0
  182. trueppm_api/apps/notifications/receivers.py +155 -0
  183. trueppm_api/apps/notifications/schema_migrations.py +56 -0
  184. trueppm_api/apps/notifications/serializers.py +1094 -0
  185. trueppm_api/apps/notifications/services.py +1489 -0
  186. trueppm_api/apps/notifications/tasks.py +762 -0
  187. trueppm_api/apps/notifications/throttles.py +98 -0
  188. trueppm_api/apps/notifications/urls.py +97 -0
  189. trueppm_api/apps/notifications/views.py +919 -0
  190. trueppm_api/apps/observability/__init__.py +0 -0
  191. trueppm_api/apps/observability/apps.py +31 -0
  192. trueppm_api/apps/observability/logging.py +213 -0
  193. trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
  194. trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
  195. trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
  196. trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
  197. trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
  198. trueppm_api/apps/observability/migrations/__init__.py +0 -0
  199. trueppm_api/apps/observability/models.py +141 -0
  200. trueppm_api/apps/observability/otel/__init__.py +45 -0
  201. trueppm_api/apps/observability/otel/attributes.py +247 -0
  202. trueppm_api/apps/observability/otel/export_health.py +533 -0
  203. trueppm_api/apps/observability/otel/instrumentation.py +351 -0
  204. trueppm_api/apps/observability/otel/metrics.py +566 -0
  205. trueppm_api/apps/observability/otel/provider.py +511 -0
  206. trueppm_api/apps/observability/otel/request_attributes.py +204 -0
  207. trueppm_api/apps/observability/purge_registry.py +124 -0
  208. trueppm_api/apps/observability/retention.py +139 -0
  209. trueppm_api/apps/observability/selectors.py +993 -0
  210. trueppm_api/apps/observability/serializers.py +142 -0
  211. trueppm_api/apps/observability/services.py +407 -0
  212. trueppm_api/apps/observability/tasks.py +266 -0
  213. trueppm_api/apps/observability/urls.py +27 -0
  214. trueppm_api/apps/observability/views.py +537 -0
  215. trueppm_api/apps/profiles/__init__.py +0 -0
  216. trueppm_api/apps/profiles/apps.py +15 -0
  217. trueppm_api/apps/profiles/constants.py +54 -0
  218. trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
  219. trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
  220. trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
  221. trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
  222. trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
  223. trueppm_api/apps/profiles/migrations/0005_squashed_0009_release_0_4.py +128 -0
  224. trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
  225. trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
  226. trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
  227. trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
  228. trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
  229. trueppm_api/apps/profiles/migrations/__init__.py +0 -0
  230. trueppm_api/apps/profiles/models.py +276 -0
  231. trueppm_api/apps/profiles/serializers.py +199 -0
  232. trueppm_api/apps/profiles/services.py +541 -0
  233. trueppm_api/apps/profiles/views.py +124 -0
  234. trueppm_api/apps/projects/__init__.py +0 -0
  235. trueppm_api/apps/projects/actual_date_rules.py +163 -0
  236. trueppm_api/apps/projects/amend.py +133 -0
  237. trueppm_api/apps/projects/apps.py +38 -0
  238. trueppm_api/apps/projects/asset_feed.py +459 -0
  239. trueppm_api/apps/projects/asset_views.py +317 -0
  240. trueppm_api/apps/projects/attachment_policy.py +278 -0
  241. trueppm_api/apps/projects/authentication.py +458 -0
  242. trueppm_api/apps/projects/backfill.py +459 -0
  243. trueppm_api/apps/projects/backlog_services.py +324 -0
  244. trueppm_api/apps/projects/backlog_views.py +302 -0
  245. trueppm_api/apps/projects/batch_operation_views.py +264 -0
  246. trueppm_api/apps/projects/blocker_services.py +510 -0
  247. trueppm_api/apps/projects/board_activity.py +462 -0
  248. trueppm_api/apps/projects/board_activity_views.py +186 -0
  249. trueppm_api/apps/projects/board_lanes.py +134 -0
  250. trueppm_api/apps/projects/bulk_settings.py +194 -0
  251. trueppm_api/apps/projects/bundled_templates.py +215 -0
  252. trueppm_api/apps/projects/calendar_settings.py +180 -0
  253. trueppm_api/apps/projects/ceremony_views.py +244 -0
  254. trueppm_api/apps/projects/commit_moment.py +212 -0
  255. trueppm_api/apps/projects/config_notice.py +1330 -0
  256. trueppm_api/apps/projects/custom_field_values.py +266 -0
  257. trueppm_api/apps/projects/decisions_services.py +132 -0
  258. trueppm_api/apps/projects/decisions_views.py +97 -0
  259. trueppm_api/apps/projects/estimation_scale.py +126 -0
  260. trueppm_api/apps/projects/export_bundle.py +423 -0
  261. trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
  262. trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
  263. trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
  264. trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
  265. trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -0
  266. trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
  267. trueppm_api/apps/projects/inbound_sync.py +555 -0
  268. trueppm_api/apps/projects/iteration_label.py +85 -0
  269. trueppm_api/apps/projects/key_views.py +468 -0
  270. trueppm_api/apps/projects/keys.py +205 -0
  271. trueppm_api/apps/projects/lifecycle.py +191 -0
  272. trueppm_api/apps/projects/management/__init__.py +0 -0
  273. trueppm_api/apps/projects/management/commands/__init__.py +0 -0
  274. trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
  275. trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
  276. trueppm_api/apps/projects/management/commands/export_program.py +50 -0
  277. trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
  278. trueppm_api/apps/projects/management/commands/load_sample_project.py +514 -0
  279. trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
  280. trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -0
  281. trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
  282. trueppm_api/apps/projects/mcp_settings.py +305 -0
  283. trueppm_api/apps/projects/methodology.py +178 -0
  284. trueppm_api/apps/projects/migration_helpers.py +75 -0
  285. trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
  286. trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -0
  287. trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
  288. trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
  289. trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
  290. trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
  291. trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
  292. trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
  293. trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
  294. trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
  295. trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
  296. trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
  297. trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
  298. trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
  299. trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
  300. trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
  301. trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
  302. trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
  303. trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
  304. trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
  305. trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
  306. trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
  307. trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
  308. trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
  309. trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
  310. trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
  311. trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
  312. trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
  313. trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
  314. trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
  315. trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
  316. trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
  317. trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
  318. trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
  319. trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
  320. trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
  321. trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
  322. trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
  323. trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
  324. trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
  325. trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
  326. trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
  327. trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
  328. trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
  329. trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
  330. trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
  331. trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
  332. trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
  333. trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
  334. trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
  335. trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
  336. trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
  337. trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
  338. trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
  339. trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
  340. trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
  341. trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
  342. trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
  343. trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
  344. trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
  345. trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
  346. trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
  347. trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
  348. trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
  349. trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
  350. trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
  351. trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
  352. trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
  353. trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
  354. trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
  355. trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
  356. trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
  357. trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
  358. trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
  359. trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
  360. trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
  361. trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
  362. trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
  363. trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
  364. trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
  365. trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
  366. trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
  367. trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
  368. trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
  369. trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
  370. trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
  371. trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
  372. trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
  373. trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
  374. trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
  375. trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
  376. trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
  377. trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
  378. trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
  379. trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +59 -0
  380. trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
  381. trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
  382. trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
  383. trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
  384. trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
  385. trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
  386. trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
  387. trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
  388. trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
  389. trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
  390. trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +75 -0
  391. trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
  392. trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
  393. trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
  394. trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
  395. trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
  396. trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +76 -0
  397. trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
  398. trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -0
  399. trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
  400. trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
  401. trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
  402. trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
  403. trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
  404. trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
  405. trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
  406. trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
  407. trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
  408. trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
  409. trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
  410. trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
  411. trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
  412. trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
  413. trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
  414. trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
  415. trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
  416. trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
  417. trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
  418. trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
  419. trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
  420. trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
  421. trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
  422. trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
  423. trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
  424. trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
  425. trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
  426. trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
  427. trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
  428. trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
  429. trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
  430. trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
  431. trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
  432. trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
  433. trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
  434. trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
  435. trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
  436. trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
  437. trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
  438. trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
  439. trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
  440. trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
  441. trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
  442. trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
  443. trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
  444. trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
  445. trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
  446. trueppm_api/apps/projects/migrations/0154_object_keys.py +184 -0
  447. trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
  448. trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
  449. trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
  450. trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
  451. trueppm_api/apps/projects/migrations/__init__.py +0 -0
  452. trueppm_api/apps/projects/models.py +9209 -0
  453. trueppm_api/apps/projects/poker_services.py +154 -0
  454. trueppm_api/apps/projects/poker_views.py +398 -0
  455. trueppm_api/apps/projects/product_backlog_services.py +450 -0
  456. trueppm_api/apps/projects/program_label_services.py +94 -0
  457. trueppm_api/apps/projects/program_label_views.py +189 -0
  458. trueppm_api/apps/projects/program_rollup.py +903 -0
  459. trueppm_api/apps/projects/program_schedule.py +767 -0
  460. trueppm_api/apps/projects/program_views.py +3445 -0
  461. trueppm_api/apps/projects/project_templates.py +422 -0
  462. trueppm_api/apps/projects/receivers.py +234 -0
  463. trueppm_api/apps/projects/refusal_codes.py +140 -0
  464. trueppm_api/apps/projects/reorder_services.py +145 -0
  465. trueppm_api/apps/projects/restructure_hooks.py +54 -0
  466. trueppm_api/apps/projects/retro_board_services.py +368 -0
  467. trueppm_api/apps/projects/retro_services.py +285 -0
  468. trueppm_api/apps/projects/risk_import.py +396 -0
  469. trueppm_api/apps/projects/row_identity.py +113 -0
  470. trueppm_api/apps/projects/schema_migrations.py +228 -0
  471. trueppm_api/apps/projects/schemas/seed_v1.json +492 -0
  472. trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
  473. trueppm_api/apps/projects/seed/__init__.py +61 -0
  474. trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
  475. trueppm_api/apps/projects/seed/exporter.py +1717 -0
  476. trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
  477. trueppm_api/apps/projects/seed/importer.py +2764 -0
  478. trueppm_api/apps/projects/seed/reanchor.py +551 -0
  479. trueppm_api/apps/projects/seed/reldates.py +126 -0
  480. trueppm_api/apps/projects/seed/replace.py +128 -0
  481. trueppm_api/apps/projects/seed/replay.py +1695 -0
  482. trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
  483. trueppm_api/apps/projects/seed/samples.py +427 -0
  484. trueppm_api/apps/projects/seed/validation.py +1169 -0
  485. trueppm_api/apps/projects/serializers.py +12904 -0
  486. trueppm_api/apps/projects/services.py +7525 -0
  487. trueppm_api/apps/projects/share_serializers.py +142 -0
  488. trueppm_api/apps/projects/share_services.py +373 -0
  489. trueppm_api/apps/projects/share_views.py +395 -0
  490. trueppm_api/apps/projects/sharing_settings.py +187 -0
  491. trueppm_api/apps/projects/signal_privacy_services.py +910 -0
  492. trueppm_api/apps/projects/signal_privacy_views.py +485 -0
  493. trueppm_api/apps/projects/signals.py +132 -0
  494. trueppm_api/apps/projects/slip_conflict.py +288 -0
  495. trueppm_api/apps/projects/sprint_cadence.py +453 -0
  496. trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
  497. trueppm_api/apps/projects/standup.py +304 -0
  498. trueppm_api/apps/projects/standup_views.py +99 -0
  499. trueppm_api/apps/projects/structural_operation_services.py +727 -0
  500. trueppm_api/apps/projects/structural_operation_views.py +311 -0
  501. trueppm_api/apps/projects/surface_visibility.py +126 -0
  502. trueppm_api/apps/projects/task_batch_services.py +379 -0
  503. trueppm_api/apps/projects/task_bulk.py +1244 -0
  504. trueppm_api/apps/projects/task_classification.py +561 -0
  505. trueppm_api/apps/projects/task_classification_defaults.py +95 -0
  506. trueppm_api/apps/projects/task_duration_settings.py +142 -0
  507. trueppm_api/apps/projects/task_grouping.py +754 -0
  508. trueppm_api/apps/projects/tasks.py +1979 -0
  509. trueppm_api/apps/projects/template_divergence.py +168 -0
  510. trueppm_api/apps/projects/template_services.py +160 -0
  511. trueppm_api/apps/projects/template_tasks.py +261 -0
  512. trueppm_api/apps/projects/template_views.py +829 -0
  513. trueppm_api/apps/projects/throttles.py +311 -0
  514. trueppm_api/apps/projects/urls.py +1016 -0
  515. trueppm_api/apps/projects/utilization.py +909 -0
  516. trueppm_api/apps/projects/views.py +21225 -0
  517. trueppm_api/apps/projects/wbs_paths.py +119 -0
  518. trueppm_api/apps/resources/__init__.py +0 -0
  519. trueppm_api/apps/resources/apps.py +8 -0
  520. trueppm_api/apps/resources/capacity.py +121 -0
  521. trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
  522. trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
  523. trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
  524. trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
  525. trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
  526. trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
  527. trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
  528. trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
  529. trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
  530. trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
  531. trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
  532. trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
  533. trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
  534. trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
  535. trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
  536. trueppm_api/apps/resources/migrations/0014_squashed_0015_release_0_4.py +47 -0
  537. trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
  538. trueppm_api/apps/resources/migrations/__init__.py +0 -0
  539. trueppm_api/apps/resources/models.py +298 -0
  540. trueppm_api/apps/resources/serializers.py +426 -0
  541. trueppm_api/apps/resources/services.py +465 -0
  542. trueppm_api/apps/resources/urls.py +28 -0
  543. trueppm_api/apps/resources/views.py +1752 -0
  544. trueppm_api/apps/scheduling/__init__.py +0 -0
  545. trueppm_api/apps/scheduling/apps.py +153 -0
  546. trueppm_api/apps/scheduling/calendars.py +144 -0
  547. trueppm_api/apps/scheduling/deadletter.py +91 -0
  548. trueppm_api/apps/scheduling/finish_reading.py +198 -0
  549. trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
  550. trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
  551. trueppm_api/apps/scheduling/graph_guard.py +263 -0
  552. trueppm_api/apps/scheduling/management/__init__.py +0 -0
  553. trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
  554. trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
  555. trueppm_api/apps/scheduling/mc_admission.py +181 -0
  556. trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
  557. trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
  558. trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
  559. trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
  560. trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
  561. trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
  562. trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
  563. trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
  564. trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
  565. trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
  566. trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
  567. trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
  568. trueppm_api/apps/scheduling/migrations/0010_squashed_0016_release_0_4.py +106 -0
  569. trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
  570. trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
  571. trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
  572. trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
  573. trueppm_api/apps/scheduling/migrations/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
  574. trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
  575. trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
  576. trueppm_api/apps/scheduling/models.py +542 -0
  577. trueppm_api/apps/scheduling/receivers.py +160 -0
  578. trueppm_api/apps/scheduling/risk_premium.py +334 -0
  579. trueppm_api/apps/scheduling/serializers.py +723 -0
  580. trueppm_api/apps/scheduling/services.py +1251 -0
  581. trueppm_api/apps/scheduling/signals.py +56 -0
  582. trueppm_api/apps/scheduling/tasks.py +2248 -0
  583. trueppm_api/apps/scheduling/telemetry.py +80 -0
  584. trueppm_api/apps/scheduling/units.py +35 -0
  585. trueppm_api/apps/scheduling/urls.py +57 -0
  586. trueppm_api/apps/scheduling/views.py +2908 -0
  587. trueppm_api/apps/sso/__init__.py +0 -0
  588. trueppm_api/apps/sso/apps.py +19 -0
  589. trueppm_api/apps/sso/extensions.py +130 -0
  590. trueppm_api/apps/sso/management/__init__.py +0 -0
  591. trueppm_api/apps/sso/management/commands/__init__.py +0 -0
  592. trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
  593. trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
  594. trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
  595. trueppm_api/apps/sso/migrations/__init__.py +0 -0
  596. trueppm_api/apps/sso/models.py +148 -0
  597. trueppm_api/apps/sso/serializers.py +446 -0
  598. trueppm_api/apps/sso/services.py +1473 -0
  599. trueppm_api/apps/sso/urls.py +47 -0
  600. trueppm_api/apps/sso/views.py +935 -0
  601. trueppm_api/apps/sync/__init__.py +0 -0
  602. trueppm_api/apps/sync/apps.py +15 -0
  603. trueppm_api/apps/sync/backfill.py +208 -0
  604. trueppm_api/apps/sync/broadcast.py +369 -0
  605. trueppm_api/apps/sync/conflict.py +262 -0
  606. trueppm_api/apps/sync/consumers.py +548 -0
  607. trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
  608. trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
  609. trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
  610. trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
  611. trueppm_api/apps/sync/migrations/0003_squashed_0005_release_0_4.py +77 -0
  612. trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
  613. trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
  614. trueppm_api/apps/sync/migrations/0006_boardevent_per_project_seq.py +77 -0
  615. trueppm_api/apps/sync/migrations/__init__.py +0 -0
  616. trueppm_api/apps/sync/models.py +272 -0
  617. trueppm_api/apps/sync/pagination.py +231 -0
  618. trueppm_api/apps/sync/sequence.py +488 -0
  619. trueppm_api/apps/sync/serializers.py +688 -0
  620. trueppm_api/apps/sync/tasks.py +260 -0
  621. trueppm_api/apps/sync/throttles.py +175 -0
  622. trueppm_api/apps/sync/upload.py +556 -0
  623. trueppm_api/apps/sync/urls.py +17 -0
  624. trueppm_api/apps/sync/views.py +957 -0
  625. trueppm_api/apps/sync/ws_auth.py +193 -0
  626. trueppm_api/apps/taskruns/__init__.py +0 -0
  627. trueppm_api/apps/taskruns/apps.py +11 -0
  628. trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
  629. trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
  630. trueppm_api/apps/taskruns/models.py +68 -0
  631. trueppm_api/apps/taskruns/serializers.py +92 -0
  632. trueppm_api/apps/taskruns/tasks.py +60 -0
  633. trueppm_api/apps/taskruns/tracker.py +286 -0
  634. trueppm_api/apps/taskruns/urls.py +46 -0
  635. trueppm_api/apps/taskruns/views.py +195 -0
  636. trueppm_api/apps/teams/__init__.py +8 -0
  637. trueppm_api/apps/teams/apps.py +16 -0
  638. trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
  639. trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +159 -0
  640. trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
  641. trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
  642. trueppm_api/apps/teams/migrations/0004_backfill_default_teams_on_upgrade.py +49 -0
  643. trueppm_api/apps/teams/migrations/__init__.py +0 -0
  644. trueppm_api/apps/teams/models.py +145 -0
  645. trueppm_api/apps/teams/permissions.py +149 -0
  646. trueppm_api/apps/teams/serializers.py +95 -0
  647. trueppm_api/apps/teams/services.py +440 -0
  648. trueppm_api/apps/teams/signals.py +61 -0
  649. trueppm_api/apps/teams/urls.py +34 -0
  650. trueppm_api/apps/teams/views.py +201 -0
  651. trueppm_api/apps/timetracking/__init__.py +0 -0
  652. trueppm_api/apps/timetracking/apps.py +9 -0
  653. trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
  654. trueppm_api/apps/timetracking/migrations/0001_squashed_0004_release_0_4.py +167 -0
  655. trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
  656. trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
  657. trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
  658. trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
  659. trueppm_api/apps/timetracking/models.py +178 -0
  660. trueppm_api/apps/timetracking/serializers.py +175 -0
  661. trueppm_api/apps/timetracking/services.py +211 -0
  662. trueppm_api/apps/timetracking/urls.py +47 -0
  663. trueppm_api/apps/timetracking/views.py +597 -0
  664. trueppm_api/apps/webhooks/__init__.py +0 -0
  665. trueppm_api/apps/webhooks/apps.py +8 -0
  666. trueppm_api/apps/webhooks/backfill.py +90 -0
  667. trueppm_api/apps/webhooks/dispatch.py +279 -0
  668. trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
  669. trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -0
  670. trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
  671. trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
  672. trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
  673. trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
  674. trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
  675. trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
  676. trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
  677. trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +100 -0
  678. trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
  679. trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
  680. trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
  681. trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
  682. trueppm_api/apps/webhooks/models.py +437 -0
  683. trueppm_api/apps/webhooks/serializers.py +326 -0
  684. trueppm_api/apps/webhooks/tasks.py +680 -0
  685. trueppm_api/apps/webhooks/urls.py +22 -0
  686. trueppm_api/apps/webhooks/views.py +533 -0
  687. trueppm_api/apps/workflow_engine/__init__.py +0 -0
  688. trueppm_api/apps/workflow_engine/apps.py +23 -0
  689. trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
  690. trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
  691. trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
  692. trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
  693. trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
  694. trueppm_api/apps/workflow_engine/models.py +265 -0
  695. trueppm_api/apps/workflow_engine/tasks.py +371 -0
  696. trueppm_api/apps/workspace/__init__.py +0 -0
  697. trueppm_api/apps/workspace/apps.py +10 -0
  698. trueppm_api/apps/workspace/backfill.py +111 -0
  699. trueppm_api/apps/workspace/export.py +254 -0
  700. trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
  701. trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
  702. trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
  703. trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
  704. trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
  705. trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
  706. trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
  707. trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
  708. trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
  709. trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
  710. trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
  711. trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
  712. trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
  713. trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
  714. trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
  715. trueppm_api/apps/workspace/migrations/0015_squashed_0027_release_0_4.py +199 -0
  716. trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
  717. trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
  718. trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
  719. trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
  720. trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
  721. trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
  722. trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
  723. trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
  724. trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
  725. trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
  726. trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
  727. trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
  728. trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
  729. trueppm_api/apps/workspace/migrations/__init__.py +0 -0
  730. trueppm_api/apps/workspace/models.py +857 -0
  731. trueppm_api/apps/workspace/permissions.py +187 -0
  732. trueppm_api/apps/workspace/serializers.py +590 -0
  733. trueppm_api/apps/workspace/services.py +967 -0
  734. trueppm_api/apps/workspace/signals.py +37 -0
  735. trueppm_api/apps/workspace/tasks.py +616 -0
  736. trueppm_api/apps/workspace/urls.py +113 -0
  737. trueppm_api/apps/workspace/views.py +1581 -0
  738. trueppm_api/asgi.py +49 -0
  739. trueppm_api/celery.py +40 -0
  740. trueppm_api/core/__init__.py +0 -0
  741. trueppm_api/core/admin_site.py +206 -0
  742. trueppm_api/core/auth_views.py +934 -0
  743. trueppm_api/core/constant_time.py +51 -0
  744. trueppm_api/core/csp.py +54 -0
  745. trueppm_api/core/db.py +87 -0
  746. trueppm_api/core/db_session.py +37 -0
  747. trueppm_api/core/demo_read_only.py +314 -0
  748. trueppm_api/core/exception_handlers.py +157 -0
  749. trueppm_api/core/export_downloads.py +56 -0
  750. trueppm_api/core/extension_providers.py +56 -0
  751. trueppm_api/core/extension_signals.py +147 -0
  752. trueppm_api/core/idempotent.py +260 -0
  753. trueppm_api/core/metadata.py +42 -0
  754. trueppm_api/core/middleware.py +51 -0
  755. trueppm_api/core/openapi.py +1221 -0
  756. trueppm_api/core/password_reset.py +513 -0
  757. trueppm_api/core/protect_conflict.py +100 -0
  758. trueppm_api/core/ratelimit.py +148 -0
  759. trueppm_api/core/redis_throttle.py +82 -0
  760. trueppm_api/core/refresh_cookie_policy.py +156 -0
  761. trueppm_api/core/request_body.py +86 -0
  762. trueppm_api/core/security_checks.py +1034 -0
  763. trueppm_api/core/storage_config.py +105 -0
  764. trueppm_api/core/text_decode.py +168 -0
  765. trueppm_api/core/throttling.py +514 -0
  766. trueppm_api/core/valkey.py +214 -0
  767. trueppm_api/core/valkey_cache.py +66 -0
  768. trueppm_api/core/valkey_checks.py +104 -0
  769. trueppm_api/core/valkey_config.py +39 -0
  770. trueppm_api/core/worker_broker_wait.py +149 -0
  771. trueppm_api/core/worker_heartbeat.py +134 -0
  772. trueppm_api/core/worker_selfheal.py +205 -0
  773. trueppm_api/fields.py +36 -0
  774. trueppm_api/permissions.py +30 -0
  775. trueppm_api/py.typed +0 -0
  776. trueppm_api/routing.py +11 -0
  777. trueppm_api/settings/__init__.py +0 -0
  778. trueppm_api/settings/base.py +2605 -0
  779. trueppm_api/settings/dev.py +296 -0
  780. trueppm_api/settings/prod.py +265 -0
  781. trueppm_api/urls.py +217 -0
  782. trueppm_api/workflows/__init__.py +38 -0
  783. trueppm_api/workflows/backends/__init__.py +6 -0
  784. trueppm_api/workflows/backends/default.py +297 -0
  785. trueppm_api/workflows/consumers/__init__.py +11 -0
  786. trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
  787. trueppm_api/workflows/interface.py +148 -0
  788. trueppm_api/workflows/registry.py +106 -0
  789. trueppm_api/workflows/services.py +90 -0
  790. trueppm_api/wsgi.py +11 -0
  791. trueppm_api-0.4.0.dist-info/METADATA +273 -0
  792. trueppm_api-0.4.0.dist-info/RECORD +799 -0
  793. trueppm_api-0.4.0.dist-info/WHEEL +4 -0
  794. trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
  795. trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
  796. trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
  797. trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
  798. trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
  799. trueppm_api-0.4.0.dist-info/licenses/licenses/MPL-2.0.txt +373 -0
@@ -0,0 +1,734 @@
1
+ """Celery workers for user-scoped external task sync (ADR-0097 §4).
2
+
3
+ Four tasks, all registered under short names for Beat (#319):
4
+
5
+ - ``external_sync`` — pull one ``ExternalSyncRequest``'s connection: fetch the
6
+ owner's assigned items from the source, upsert them into ``ExternalWorkItem``,
7
+ soft-remove any that vanished, and flip the connection to ``connected`` /
8
+ ``auth_failed`` / ``invalid_filter``. Idempotent under the row's
9
+ ``@idempotent_task`` lock.
10
+ - ``drain_external_sync`` — 300 s outbox drain: dispatch stranded ``PENDING``
11
+ rows and recover orphaned ``DISPATCHED`` ones (ADR-0097 §Durable Execution #2).
12
+ - ``poll_external_sources`` — low-frequency opt-in poll: enqueue a pull for every
13
+ connection whose owner opted in (``config["poll_enabled"]``, set from the
14
+ Connected Accounts toggle). Default-off, so this no-ops until a user turns
15
+ polling on (ADR-0097 §4).
16
+ - ``purge_external_sync`` — nightly cleanup: hard-delete terminal outbox rows and
17
+ long-stale ``ExternalWorkItem`` cache rows (ADR-0097 §Durable Execution #6).
18
+
19
+ Security (ADR-0097 §Resolution #2): the PAT and ``Authorization`` header never
20
+ reach a log line — only the source key, user id, and a scrubbed error class are
21
+ recorded. The pull can **never** mint a ``Task`` (the §2 read-only invariant).
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import logging
27
+ from datetime import datetime, timedelta
28
+ from typing import Any
29
+
30
+ from celery.exceptions import SoftTimeLimitExceeded
31
+ from django.db import models, transaction
32
+ from django.utils import timezone
33
+
34
+ from trueppm_api.core.idempotent import idempotent_task
35
+
36
+ from .connections import (
37
+ STATUS_AUTH_FAILED,
38
+ STATUS_CONNECTED,
39
+ STATUS_INVALID_FILTER,
40
+ SYNC_FAILURE_REASONS,
41
+ SYNC_REASON_AUTH_FAILED,
42
+ SYNC_REASON_DECRYPT_FAILED,
43
+ SYNC_REASON_INVALID_FILTER,
44
+ SYNC_REASON_RATE_LIMITED,
45
+ SYNC_REASON_UNREACHABLE,
46
+ )
47
+ from .encryption import decrypt_secret
48
+ from .external_sources import (
49
+ EXTERNAL_TASK_SOURCES,
50
+ ExternalFetchResult,
51
+ ExternalSourceAuthError,
52
+ ExternalSourceConfigError,
53
+ ExternalSourceError,
54
+ ExternalSourceRateLimited,
55
+ ExternalWorkItemDTO,
56
+ )
57
+ from .models import (
58
+ ExternalSyncRequest,
59
+ ExternalSyncRequestReason,
60
+ ExternalSyncRequestStatus,
61
+ ExternalWorkItem,
62
+ IntegrationCredential,
63
+ )
64
+
65
+ logger = logging.getLogger(__name__)
66
+
67
+ # Per-(user, source) cache cap (ADR-0097 §Decision #4 "bounded growth"). The
68
+ # frozen source ABC fetches a single ~100-item page, so this is a defensive DB
69
+ # bound rather than a routinely-hit limit; when a page ever exceeds it we keep
70
+ # the first CACHE_ITEM_CAP and drop the rest so the cache stays bounded.
71
+ CACHE_ITEM_CAP = 500
72
+
73
+ # The ``config["last_sync"]["reason"]`` vocabulary is defined in ``connections``
74
+ # — beside the read surface that serves it, so that surface can validate against
75
+ # it without importing back into this module. Re-exported here because this is
76
+ # the only module that *writes* it, and every writer should reach for it by name.
77
+ __all__ = [
78
+ "SYNC_FAILURE_REASONS",
79
+ "SYNC_REASON_AUTH_FAILED",
80
+ "SYNC_REASON_DECRYPT_FAILED",
81
+ "SYNC_REASON_INVALID_FILTER",
82
+ "SYNC_REASON_RATE_LIMITED",
83
+ "SYNC_REASON_UNREACHABLE",
84
+ "drain_external_sync",
85
+ "external_sync",
86
+ "poll_external_sources",
87
+ "purge_external_sync",
88
+ ]
89
+
90
+ # Drain floor: a PENDING row younger than this was almost certainly just handed
91
+ # to on_commit dispatch, so the drain skips it to avoid a double-dispatch race
92
+ # (ADR-0097 §Durable Execution #3). Older PENDING rows are genuinely stranded.
93
+ _DRAIN_PENDING_FLOOR = timedelta(minutes=2)
94
+
95
+ # Orphan recovery: a DISPATCHED row this old outlived any real pull (the fetch
96
+ # is HTTP-timeout-bounded to a few seconds), so its worker died — reset it to
97
+ # PENDING for re-dispatch. Matches the scheduling drain's 10-minute window.
98
+ _DRAIN_ORPHAN_CUTOFF = timedelta(minutes=10)
99
+
100
+ # Rate-limit backoff (#2924). Used when a 429 carries no usable ``Retry-After``;
101
+ # a header value always wins. Clamped at the top end so a source answering with an
102
+ # absurd Retry-After (or a clock-skewed HTTP-date) cannot park a pull for hours.
103
+ _RATE_LIMIT_DEFAULT_BACKOFF = timedelta(seconds=60)
104
+ _RATE_LIMIT_MAX_BACKOFF = timedelta(minutes=15)
105
+
106
+ # How many times one pull may be re-queued for rate limiting before it retires.
107
+ # Bounds the loop for a source that 429s indefinitely; the user's next manual
108
+ # refresh (or the poll) starts a fresh row, so this is not a permanent give-up.
109
+ _MAX_RATE_LIMIT_ATTEMPTS = 3
110
+
111
+ # Retention windows for the nightly purge.
112
+ _OUTBOX_RETENTION = timedelta(days=7)
113
+ # Stale (vanished-from-Jira) items linger briefly so My Work can show a just-
114
+ # completed item, then are hard-deleted (ADR-0097 §5).
115
+ _STALE_ITEM_RETENTION = timedelta(days=7)
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # external_sync — pull one connection
120
+ # ---------------------------------------------------------------------------
121
+
122
+
123
+ @idempotent_task(
124
+ lock_key_template="external_sync:{0}",
125
+ lock_ttl=60,
126
+ on_contention="skip",
127
+ soft_time_limit=25,
128
+ time_limit=30,
129
+ acks_late=True,
130
+ reject_on_worker_lost=True,
131
+ name="integrations.external_sync",
132
+ )
133
+ def external_sync(self: object, request_id: str) -> None:
134
+ """Execute one queued external-source pull.
135
+
136
+ Locked per request id so a duplicate dispatch (on_commit + drain racing)
137
+ converges rather than double-fetching; ``on_contention="skip"`` drops the
138
+ loser because the winner already did the work.
139
+ """
140
+ _do_sync(request_id)
141
+
142
+
143
+ def _do_sync(request_id: str) -> None:
144
+ """Business logic for ``external_sync`` — extracted for direct testing.
145
+
146
+ Loads and locks the outbox row, decrypts the owner's PAT, fetches their
147
+ assigned items, upserts the cache, soft-removes anything that vanished, and
148
+ marks the row ``DONE``/``DEAD``. Terminal rows (already ``DONE``/``DEAD``) are
149
+ a no-op so a re-dispatch is safe.
150
+ """
151
+ now = timezone.now()
152
+ with transaction.atomic():
153
+ req = ExternalSyncRequest.objects.select_for_update().filter(id=request_id).first()
154
+ if req is None:
155
+ # Purged between dispatch and execution — nothing to do.
156
+ return
157
+ if req.status in (
158
+ ExternalSyncRequestStatus.DONE,
159
+ ExternalSyncRequestStatus.DEAD,
160
+ ):
161
+ # Already processed by the winning dispatch — idempotent skip.
162
+ return
163
+
164
+ cred = IntegrationCredential.objects.filter(
165
+ user_id=req.user_id, provider=req.source
166
+ ).first()
167
+ source_cls = EXTERNAL_TASK_SOURCES.get(req.source)
168
+ if cred is None or source_cls is None:
169
+ # The connection was deleted (or its source de-registered) after the
170
+ # pull was queued. Nothing to sync; retire the row.
171
+ _mark_dead(req, "connection no longer exists")
172
+ return
173
+
174
+ try:
175
+ secret = decrypt_secret(cred.secret_ciphertext)
176
+ except Exception:
177
+ # A corrupt/undecryptable ciphertext is not user-recoverable by a
178
+ # retry; retire the row without leaking the plaintext into the log
179
+ # (only user_id/source are interpolated below, never the value).
180
+ logger.warning(
181
+ "external_sync: stored connection data could not be decrypted "
182
+ "for user=%s source=%s",
183
+ req.user_id,
184
+ req.source,
185
+ )
186
+ _record_failed_pull(cred, SYNC_REASON_DECRYPT_FAILED, now)
187
+ _mark_dead(req, "credential could not be decrypted")
188
+ return
189
+
190
+ try:
191
+ result = source_cls().fetch_assigned_items_result(
192
+ base_url=cred.base_url, secret=secret, config=cred.config or {}
193
+ )
194
+ except ExternalSourceAuthError:
195
+ # 401/403: the token is dead. Flip the connection to auth_failed so My
196
+ # Work shows "Reconnect", stop using the token, keep the last-good
197
+ # cache (ADR-0097 §5). No retry loop on an auth failure.
198
+ _set_connection_status(
199
+ cred,
200
+ STATUS_AUTH_FAILED,
201
+ last_sync=_failed_outcome(SYNC_REASON_AUTH_FAILED, now),
202
+ )
203
+ _mark_dead(req, "auth_failed")
204
+ logger.info(
205
+ "external_sync: auth failed for user=%s source=%s — connection flagged",
206
+ req.user_id,
207
+ req.source,
208
+ )
209
+ return
210
+ except ExternalSourceConfigError:
211
+ # The stored filter cannot be scoped safely (a project key that is not
212
+ # a project key, or a JQL whose parentheses do not balance — both are
213
+ # reachable on rows written before those values were validated). A
214
+ # retry can never fix it, and the alternative to refusing is pulling
215
+ # wider than the owner selected, so flag the connection with a state
216
+ # whose remedy is the connect wizard and stop. Must precede the generic
217
+ # ExternalSourceError arm below — this is a subclass of it.
218
+ _set_connection_status(
219
+ cred,
220
+ STATUS_INVALID_FILTER,
221
+ last_sync=_failed_outcome(SYNC_REASON_INVALID_FILTER, now),
222
+ )
223
+ _mark_dead(req, "invalid_filter")
224
+ logger.info(
225
+ "external_sync: stored filter unusable for user=%s source=%s — connection flagged",
226
+ req.user_id,
227
+ req.source,
228
+ )
229
+ return
230
+ except ExternalSourceRateLimited as exc:
231
+ # 429: the source told us WHEN to come back, so re-queue against that
232
+ # clock instead of dead-lettering (#2924). Before this the row was
233
+ # marked DEAD "unreachable" and the owner saw a broken connection for
234
+ # what is a routine, self-healing condition. Deliberately does NOT
235
+ # touch the connection status: the credential is valid and the host is
236
+ # reachable, so flagging it would send the user to re-issue a token
237
+ # that was never the problem. Must precede the generic arm below —
238
+ # this is a subclass of it.
239
+ _requeue_rate_limited(req, exc.retry_after, now, cred=cred)
240
+ return
241
+ except ExternalSourceError as exc:
242
+ # Transient (5xx / timeout): keep the last-good cache and retire this
243
+ # attempt. The user (or the next poll) re-triggers; the cooldown keeps
244
+ # the retry cadence sane. No data is lost.
245
+ _record_failed_pull(cred, SYNC_REASON_UNREACHABLE, now)
246
+ _mark_dead(req, f"unreachable: {type(exc).__name__}")
247
+ logger.info(
248
+ "external_sync: source unreachable for user=%s source=%s (%s)",
249
+ req.user_id,
250
+ req.source,
251
+ type(exc).__name__,
252
+ )
253
+ return
254
+
255
+ stored = _apply_pull(req.user_id, req.source, result.items, now)
256
+
257
+ # Successful pull: stamp the connection with its outcome and retire the row.
258
+ cred.last_used_at = now
259
+ _set_connection_status(
260
+ cred,
261
+ STATUS_CONNECTED,
262
+ last_sync=_successful_outcome(result, stored, now),
263
+ extra_save_fields=["last_used_at"],
264
+ )
265
+ req.status = ExternalSyncRequestStatus.DONE
266
+ req.last_error = ""
267
+ req.save(update_fields=["status", "last_error"])
268
+
269
+
270
+ def _apply_pull(
271
+ user_id: int,
272
+ source: str,
273
+ items: list[ExternalWorkItemDTO],
274
+ now: datetime,
275
+ ) -> int:
276
+ """Upsert the fetched DTOs and soft-remove anything that vanished.
277
+
278
+ Items are already sanitized (field caps + URL scheme) at the registry
279
+ boundary. Capped at :data:`CACHE_ITEM_CAP` per (user, source). An
280
+ ``external_id`` no longer returned by a *successful* pull is soft-removed
281
+ (``is_stale=True``) — never hard-deleted here — so a transient partial
282
+ response can never wipe the list (ADR-0097 §5).
283
+
284
+ Returns:
285
+ How many rows this pull actually stored (after the cap and after
286
+ duplicate ``external_id``s collapse) — the "showing the first N" the
287
+ owner is told about, so the number they see is the number that landed
288
+ rather than the number the provider sent (#2925).
289
+ """
290
+ capped = items[:CACHE_ITEM_CAP]
291
+ seen: set[str] = set()
292
+ for raw in capped:
293
+ # Re-sanitize at the persistence boundary (idempotent): the OSS Jira
294
+ # source already returns sanitized DTOs, but making *this* the enforcement
295
+ # point means a future/Enterprise source that forgets ``.sanitized()``
296
+ # still cannot land an over-long field or a ``javascript:``/``data:`` URL
297
+ # in the cache — URLField scheme validation does not run on ``.save()``
298
+ # (ADR-0097 §Resolution #4, defense in depth).
299
+ dto = raw.sanitized()
300
+ seen.add(dto.external_id)
301
+ ExternalWorkItem.objects.update_or_create(
302
+ user_id=user_id,
303
+ source=source,
304
+ external_id=dto.external_id,
305
+ defaults={
306
+ "external_url": dto.external_url,
307
+ "title": dto.title,
308
+ "external_status": dto.external_status,
309
+ "display_bucket": dto.display_bucket,
310
+ "due_date": dto.due_date,
311
+ "last_synced_at": now,
312
+ "is_stale": False,
313
+ },
314
+ )
315
+ # Soft-remove cached rows for this connection that this pull did not return.
316
+ stale_qs = ExternalWorkItem.objects.filter(user_id=user_id, source=source)
317
+ if seen:
318
+ stale_qs = stale_qs.exclude(external_id__in=seen)
319
+ stale_qs.filter(is_stale=False).update(is_stale=True, last_synced_at=now)
320
+ return len(seen)
321
+
322
+
323
+ def _iso_z(value: datetime) -> str:
324
+ """Serialize a timestamp the way DRF does, so one object has one spelling.
325
+
326
+ ``config`` is a JSON column, so ``at`` is stored as a string and DRF's
327
+ ``DateTimeField.to_representation`` passes a ``str`` straight through. Its
328
+ sibling ``last_synced_at`` is a real ``DateTimeField`` and renders as
329
+ ``…Z``, so a bare ``isoformat()`` would put ``…+00:00`` and ``…Z`` in the
330
+ same response object. Both are valid RFC 3339 and no client breaks on it —
331
+ it is just two spellings of one thing in one payload, which is the kind of
332
+ inconsistency an integrator has to write a special case for.
333
+ """
334
+ return value.isoformat().replace("+00:00", "Z")
335
+
336
+
337
+ def _successful_outcome(result: ExternalFetchResult, stored: int, now: datetime) -> dict[str, Any]:
338
+ """Describe a completed pull for ``config["last_sync"]`` (#2925).
339
+
340
+ The truncation decision, in priority order, because each rung is a stronger
341
+ claim than the next:
342
+
343
+ 1. The cache cap bit — more DTOs came back than we were willing to store.
344
+ Tested against :data:`CACHE_ITEM_CAP` rather than ``stored < fetched``,
345
+ because ``stored`` counts *distinct* ``external_id``s: a page containing a
346
+ duplicate satisfies ``stored < fetched`` with no cap anywhere near, which
347
+ would tell an owner their complete list is partial. De-duplication is not
348
+ truncation.
349
+ 2. The provider reported a ``total`` larger than what we stored. Exact.
350
+ 3. No total, but the page came back **full**. A guess, and the weakest rung:
351
+ a user with exactly ``page_size`` assigned items is indistinguishable from
352
+ one with more. It is the conservative direction — over-reporting "there
353
+ may be more" costs a line of UI, while under-reporting is the silent
354
+ truncation this issue exists to end.
355
+
356
+ ``total_available`` stays ``None`` when the source did not report one. The web
357
+ must render that as "the first N" without a denominator rather than inventing
358
+ a total — ``None`` here means *unknown*, never *zero*.
359
+ """
360
+ fetched = len(result.items)
361
+ total = result.total_available
362
+ if fetched > CACHE_ITEM_CAP:
363
+ truncated = True
364
+ elif total is not None:
365
+ truncated = total > stored
366
+ else:
367
+ truncated = result.page_size is not None and fetched >= result.page_size
368
+ return {
369
+ "at": _iso_z(now),
370
+ "ok": True,
371
+ "reason": "",
372
+ "fetched": fetched,
373
+ "stored": stored,
374
+ "total_available": total,
375
+ "truncated": truncated,
376
+ }
377
+
378
+
379
+ def _failed_outcome(reason: str, now: datetime) -> dict[str, Any]:
380
+ """Describe a failed pull for ``config["last_sync"]`` (#2925).
381
+
382
+ Counts are zero rather than absent so the shape is uniform for every reader —
383
+ a failure did not store anything, and the last-good cache the owner is still
384
+ being served is described by the *previous* successful outcome, not this one.
385
+ ``reason`` is a :data:`SYNC_FAILURE_REASONS` token; see that constant for why
386
+ it is never a formatted message.
387
+ """
388
+ return {
389
+ "at": _iso_z(now),
390
+ "ok": False,
391
+ "reason": reason,
392
+ "fetched": 0,
393
+ "stored": 0,
394
+ "total_available": None,
395
+ "truncated": False,
396
+ }
397
+
398
+
399
+ def _merge_config(
400
+ cred: IntegrationCredential,
401
+ updates: dict[str, Any],
402
+ *,
403
+ extra_save_fields: list[str] | None = None,
404
+ ) -> None:
405
+ """Merge keys into the credential's ``config`` under a row lock.
406
+
407
+ ``config`` is one JSON column holding several independently-owned keys: the
408
+ owner writes ``{jql, project_keys, account_email, deployment}`` from the
409
+ connect wizard, and this worker writes ``{status, last_sync}``. A save from
410
+ either side rewrites the **whole** column, so writing from the snapshot
411
+ ``cred`` was loaded with — before an outbound fetch that can take seconds —
412
+ silently reverts anything the owner changed while the pull was in flight.
413
+
414
+ That is not a cosmetic lost update. ``project_keys`` is ANDed into the JQL
415
+ (#2888) precisely so the filter narrows what leaves the provider, so reverting
416
+ it *widens* the next pull past what the owner selected; and reinstating a
417
+ stale ``status`` re-shows "Reconnect" on a connection the owner just repaired.
418
+
419
+ So: re-read the row ``SELECT … FOR UPDATE`` immediately before the write, and
420
+ merge only this module's own keys onto whatever is there now. The lock is
421
+ taken *after* the fetch, never across it, so a slow provider cannot hold it.
422
+ A row deleted mid-pull (the owner disconnected) is a no-op rather than a
423
+ resurrection.
424
+ """
425
+ fresh = IntegrationCredential.objects.select_for_update().filter(pk=cred.pk).first()
426
+ if fresh is None:
427
+ return
428
+ config = dict(fresh.config or {})
429
+ config.update(updates)
430
+ fresh.config = config
431
+ # Fields the caller already set on its own instance (``last_used_at``) have to
432
+ # be carried onto the locked row, which was loaded before they were assigned.
433
+ for field in extra_save_fields or []:
434
+ setattr(fresh, field, getattr(cred, field))
435
+ fresh.save(update_fields=["config", *(extra_save_fields or [])])
436
+ # Keep the caller's instance consistent with what was persisted, so a later
437
+ # read off ``cred`` in the same call does not see the pre-merge snapshot.
438
+ cred.config = config
439
+
440
+
441
+ def _record_failed_pull(cred: IntegrationCredential, reason: str, now: datetime) -> None:
442
+ """Stamp a failure onto ``config["last_sync"]`` without touching ``status``.
443
+
444
+ Used by the failure paths that deliberately leave the connection lifecycle
445
+ alone — an unreachable host or an unreadable ciphertext is not ``auth_failed``
446
+ and must not send the owner to re-issue a working token. The outcome is still
447
+ recorded, because "connected, last synced 5 minutes ago" reading identically
448
+ whether the last pull worked or not is the defect (#2925).
449
+ """
450
+ _merge_config(cred, {"last_sync": _failed_outcome(reason, now)})
451
+
452
+
453
+ def _set_connection_status(
454
+ cred: IntegrationCredential,
455
+ status: str,
456
+ *,
457
+ last_sync: dict[str, Any] | None = None,
458
+ extra_save_fields: list[str] | None = None,
459
+ ) -> None:
460
+ """Write ``config["status"]`` on the credential row (the connection lifecycle).
461
+
462
+ ``config`` is the ADR-0097 §2 reuse of ``IntegrationCredential`` — the
463
+ connection's ``{account_email, jql, project_keys, status, last_sync}`` live
464
+ there. Only ``status``, an optional ``last_sync`` outcome, and any
465
+ ``extra_save_fields`` the caller already set are persisted — see
466
+ :func:`_merge_config` for why the other keys are merged rather than rewritten.
467
+ """
468
+ updates: dict[str, Any] = {"status": status}
469
+ if last_sync is not None:
470
+ updates["last_sync"] = last_sync
471
+ _merge_config(cred, updates, extra_save_fields=extra_save_fields)
472
+
473
+
474
+ def _requeue_rate_limited(
475
+ req: ExternalSyncRequest,
476
+ retry_after: float | None,
477
+ now: datetime,
478
+ *,
479
+ cred: IntegrationCredential | None = None,
480
+ ) -> None:
481
+ """Put a rate-limited pull back in the PENDING pool behind a retry clock.
482
+
483
+ ``Retry-After`` wins when the source sent a usable one; otherwise a default
484
+ backoff applies. Either way the value is clamped to
485
+ :data:`_RATE_LIMIT_MAX_BACKOFF` so a hostile or clock-skewed header cannot park
486
+ the pull indefinitely.
487
+
488
+ The row goes back to PENDING rather than staying DISPATCHED because PENDING is
489
+ what the drain re-dispatches. That is safe against the partial-unique
490
+ ``one pending per (user, source)`` constraint: this row *is* that connection's
491
+ live pull — anything else that wanted one adopted it while it was in flight.
492
+
493
+ Once the attempt budget is spent the row retires DEAD, so a source that 429s
494
+ forever cannot cycle forever. That is not a permanent give-up: the owner's next
495
+ manual refresh writes a fresh row.
496
+
497
+ Only that terminal give-up is recorded as a failed outcome on the connection
498
+ (#2925). A re-queue is not an outcome — the pull is still in flight, and
499
+ writing "last pull: rate limited" for a request that succeeds forty seconds
500
+ later would show the owner a failure that did not happen.
501
+ """
502
+ req.attempt_count += 1
503
+ if req.attempt_count >= _MAX_RATE_LIMIT_ATTEMPTS:
504
+ if cred is not None:
505
+ _record_failed_pull(cred, SYNC_REASON_RATE_LIMITED, now)
506
+ _mark_dead(req, f"rate_limited: retry budget spent after {req.attempt_count} attempt(s)")
507
+ logger.info(
508
+ "external_sync: rate-limit budget spent for user=%s source=%s",
509
+ req.user_id,
510
+ req.source,
511
+ )
512
+ return
513
+
514
+ backoff = _RATE_LIMIT_DEFAULT_BACKOFF
515
+ if retry_after is not None:
516
+ backoff = timedelta(seconds=retry_after)
517
+ backoff = min(backoff, _RATE_LIMIT_MAX_BACKOFF)
518
+
519
+ req.status = ExternalSyncRequestStatus.PENDING
520
+ req.next_attempt_at = now + backoff
521
+ req.celery_task_id = ""
522
+ req.dispatched_at = None
523
+ req.last_error = f"rate_limited: retrying in {int(backoff.total_seconds())}s"[:512]
524
+ req.save(
525
+ update_fields=[
526
+ "status",
527
+ "next_attempt_at",
528
+ "attempt_count",
529
+ "celery_task_id",
530
+ "dispatched_at",
531
+ "last_error",
532
+ ]
533
+ )
534
+ logger.info(
535
+ "external_sync: rate limited for user=%s source=%s — re-queued in %ss (attempt %d)",
536
+ req.user_id,
537
+ req.source,
538
+ int(backoff.total_seconds()),
539
+ req.attempt_count,
540
+ )
541
+
542
+
543
+ def _mark_dead(req: ExternalSyncRequest, error: str) -> None:
544
+ """Retire an outbox row as ``DEAD`` with a scrubbed error note."""
545
+ req.status = ExternalSyncRequestStatus.DEAD
546
+ req.last_error = error[:512]
547
+ req.save(update_fields=["status", "last_error"])
548
+
549
+
550
+ # ---------------------------------------------------------------------------
551
+ # drain_external_sync — outbox recovery
552
+ # ---------------------------------------------------------------------------
553
+
554
+
555
+ @idempotent_task(
556
+ lock_key_template="drain_external_sync",
557
+ lock_ttl=60,
558
+ on_contention="skip",
559
+ soft_time_limit=55,
560
+ time_limit=90,
561
+ acks_late=True,
562
+ reject_on_worker_lost=True,
563
+ name="integrations.drain_external_sync",
564
+ )
565
+ def drain_external_sync(self: object) -> None:
566
+ """Dispatch stranded PENDING rows and recover orphaned DISPATCHED ones.
567
+
568
+ Runs every 300 s. The singleton lock + ``on_contention="skip"`` guarantee at
569
+ most one drain at a time; the next Beat tick covers any dropped trigger.
570
+ """
571
+ _do_drain()
572
+
573
+
574
+ def _do_drain() -> None:
575
+ """Business logic for ``drain_external_sync`` — extracted for testability."""
576
+ now = timezone.now()
577
+
578
+ recovered = ExternalSyncRequest.objects.filter(
579
+ status=ExternalSyncRequestStatus.DISPATCHED,
580
+ dispatched_at__lt=now - _DRAIN_ORPHAN_CUTOFF,
581
+ ).update(status=ExternalSyncRequestStatus.PENDING, celery_task_id="")
582
+ if recovered:
583
+ logger.warning("drain_external_sync: recovered %d orphaned dispatched row(s)", recovered)
584
+
585
+ # Only dispatch PENDING rows past the floor — younger ones were just handed to
586
+ # on_commit dispatch and would double-fire (the per-request lock would drop
587
+ # the loser, but skipping avoids the churn).
588
+ pending = list(
589
+ ExternalSyncRequest.objects.filter(
590
+ status=ExternalSyncRequestStatus.PENDING,
591
+ requested_at__lt=now - _DRAIN_PENDING_FLOOR,
592
+ ).filter(
593
+ # A rate-limited row carries the clock the source itself gave us;
594
+ # re-dispatching before it would just earn another 429 (#2924). Null
595
+ # is the ordinary case and means "eligible now", so this narrows
596
+ # nothing for any row that was never rate-limited.
597
+ models.Q(next_attempt_at__isnull=True) | models.Q(next_attempt_at__lte=now)
598
+ )
599
+ )
600
+ dispatched = 0
601
+ for req in pending:
602
+ try:
603
+ result = external_sync.delay(str(req.id))
604
+ except SoftTimeLimitExceeded:
605
+ raise
606
+ except Exception:
607
+ logger.warning(
608
+ "drain_external_sync: broker unavailable — request %s stays pending",
609
+ req.id,
610
+ )
611
+ continue
612
+ ExternalSyncRequest.objects.filter(
613
+ id=req.id, status=ExternalSyncRequestStatus.PENDING
614
+ ).update(
615
+ status=ExternalSyncRequestStatus.DISPATCHED,
616
+ celery_task_id=result.id,
617
+ dispatched_at=now,
618
+ )
619
+ dispatched += 1
620
+
621
+ if dispatched or recovered:
622
+ logger.info("drain_external_sync: dispatched=%d recovered=%d", dispatched, recovered)
623
+
624
+
625
+ # ---------------------------------------------------------------------------
626
+ # poll_external_sources — opt-in low-frequency poll
627
+ # ---------------------------------------------------------------------------
628
+
629
+
630
+ @idempotent_task(
631
+ lock_key_template="poll_external_sources",
632
+ lock_ttl=60,
633
+ on_contention="skip",
634
+ soft_time_limit=55,
635
+ time_limit=90,
636
+ acks_late=True,
637
+ reject_on_worker_lost=True,
638
+ name="integrations.poll_external_sources",
639
+ )
640
+ def poll_external_sources(self: object) -> None:
641
+ """Enqueue a pull for every connection whose owner opted into polling.
642
+
643
+ Default-off (ADR-0097 §4): a connection polls only when its ``config`` carries
644
+ ``poll_enabled: true`` and it is in neither ``auth_failed`` nor
645
+ ``invalid_filter``. Owners opt in from the Connected Accounts page (#3104,
646
+ ``PATCH /me/connections/{source}/``), so an install where nobody has turned it
647
+ on still fans out zero pulls.
648
+ """
649
+ _do_poll()
650
+
651
+
652
+ def _do_poll() -> None:
653
+ """Business logic for ``poll_external_sources`` — extracted for testability."""
654
+ from .services import enqueue_external_sync
655
+
656
+ source_keys = set(EXTERNAL_TASK_SOURCES.keys())
657
+ if not source_keys:
658
+ return
659
+ # Push the opt-in gates into the DB so a default-off install scans (and
660
+ # returns) ~zero rows each tick rather than every registered-source
661
+ # credential: poll_enabled must be truthy and the connection must be in
662
+ # neither auth_failed nor invalid_filter — the two states a retry cannot clear
663
+ # without the user reconnecting, so polling them just burns a pull per tick.
664
+ # The Python re-check below is belt-and-suspenders for the rare config shapes
665
+ # a JSON lookup can't express (e.g. poll_enabled stored as a string).
666
+ # ``.iterator()` keeps memory flat if the opted-in set ever grows.
667
+ blocked_statuses = (STATUS_AUTH_FAILED, STATUS_INVALID_FILTER)
668
+ candidates = (
669
+ IntegrationCredential.objects.filter(provider__in=source_keys, config__poll_enabled=True)
670
+ .exclude(config__status__in=blocked_statuses)
671
+ .iterator()
672
+ )
673
+ queued = 0
674
+ for cred in candidates:
675
+ config = cred.config or {}
676
+ if not config.get("poll_enabled"):
677
+ continue
678
+ if config.get("status") in blocked_statuses:
679
+ continue
680
+ try:
681
+ enqueue_external_sync(
682
+ cred.user_id, cred.provider, reason=ExternalSyncRequestReason.POLL
683
+ )
684
+ except ValueError:
685
+ # provider not a live external source (e.g. a plain git PAT) — skip.
686
+ continue
687
+ queued += 1
688
+ if queued:
689
+ logger.info("poll_external_sources: enqueued %d poll(s)", queued)
690
+
691
+
692
+ # ---------------------------------------------------------------------------
693
+ # purge_external_sync — nightly cleanup
694
+ # ---------------------------------------------------------------------------
695
+
696
+
697
+ @idempotent_task(
698
+ lock_key_template="purge_external_sync",
699
+ lock_ttl=120,
700
+ on_contention="skip",
701
+ soft_time_limit=55,
702
+ time_limit=90,
703
+ acks_late=True,
704
+ reject_on_worker_lost=True,
705
+ name="integrations.purge_external_sync",
706
+ )
707
+ def purge_external_sync(self: object) -> None:
708
+ """Hard-delete terminal outbox rows and long-stale cache rows (nightly)."""
709
+ _do_purge()
710
+
711
+
712
+ def _do_purge() -> None:
713
+ """Business logic for ``purge_external_sync`` — extracted for testability."""
714
+ now = timezone.now()
715
+
716
+ outbox_deleted, _ = ExternalSyncRequest.objects.filter(
717
+ status__in=[ExternalSyncRequestStatus.DONE, ExternalSyncRequestStatus.DEAD],
718
+ requested_at__lt=now - _OUTBOX_RETENTION,
719
+ ).delete()
720
+
721
+ # Stale items are kept briefly (so a just-completed item can show in My Work),
722
+ # then reaped. Guard on last_synced_at so a row still within the grace window
723
+ # survives.
724
+ items_deleted, _ = ExternalWorkItem.objects.filter(
725
+ is_stale=True,
726
+ last_synced_at__lt=now - _STALE_ITEM_RETENTION,
727
+ ).delete()
728
+
729
+ if outbox_deleted or items_deleted:
730
+ logger.info(
731
+ "purge_external_sync: outbox=%d stale_items=%d",
732
+ outbox_deleted,
733
+ items_deleted,
734
+ )