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