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,837 @@
1
+ """User-scoped external task sources (ADR-0097 §1).
2
+
3
+ A second ``ProviderRegistry``, ``EXTERNAL_TASK_SOURCES``, distinct from
4
+ ``TASK_LINK_PROVIDERS`` (ADR-0049). The two solve different problems and must
5
+ not collide:
6
+
7
+ - ``TASK_LINK_PROVIDERS`` — "paste a URL on a task, fetch its status" (git-aware
8
+ tasks). ``jira`` stays reserved for Enterprise *there*.
9
+ - ``EXTERNAL_TASK_SOURCES`` (this module) — "pull the issues assigned to *me*
10
+ from my personal account into My Work." OSS owns ``jira`` **here**, narrowly,
11
+ for read-only personal pull.
12
+
13
+ The ABC below is the entire stable cross-repo surface: OSS registers ``jira``
14
+ in ``IntegrationsConfig.ready()``; Enterprise registers richer sources
15
+ (``servicenow``, ``azure_devops``) against the same registry from its own
16
+ ``AppConfig.ready()`` with **no** ``trueppm_enterprise`` import in OSS. Adding a
17
+ source key is additive; renaming/removing a key or changing the ABC signature
18
+ is a major-version bump.
19
+
20
+ Security posture (ADR-0097 §Threat Model → Resolution):
21
+ - The source never sees ciphertext or the DB row — the caller decrypts the PAT
22
+ once at the boundary and passes the plaintext ``secret`` in. This keeps the
23
+ encryption surface inside ``apps/integrations`` and out of every source impl.
24
+ - ``base_url`` is Jira-Cloud-allow-listed by the connection endpoint
25
+ (``providers.assert_base_url_allowed``) *before* the token is ever put on the
26
+ wire; every fetch additionally routes through the SSRF-guarded ``http`` helper.
27
+ - DTOs returned by a source are **untrusted**: field lengths are capped and
28
+ ``external_url`` is forced to an ``https?://`` scheme at the registry boundary
29
+ (:meth:`ExternalWorkItemDTO.sanitized`), so a hostile provider response cannot
30
+ smuggle an over-long title or a ``javascript:`` link into the cache.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import abc
36
+ import base64
37
+ import email.utils
38
+ import re
39
+ import urllib.parse
40
+ from dataclasses import dataclass
41
+ from datetime import UTC, date, datetime
42
+ from typing import Any, ClassVar
43
+
44
+ from . import http
45
+ from .registry import ProviderRegistry, VerifyResult
46
+
47
+ # Display buckets a source maps its native status onto, for grouping in My Work.
48
+ # Deliberately coarse (three states) — a lossy projection of the provider's
49
+ # workflow, consistent across sources so the My Work section renders uniformly.
50
+ BUCKET_TODO = "todo"
51
+ BUCKET_IN_PROGRESS = "in_progress"
52
+ BUCKET_DONE = "done"
53
+ DISPLAY_BUCKETS: tuple[str, ...] = (BUCKET_TODO, BUCKET_IN_PROGRESS, BUCKET_DONE)
54
+
55
+ # Field-length caps enforced at the registry boundary on untrusted provider data
56
+ # (ADR-0097 §Resolution #4). Match the ``ExternalWorkItem`` column widths so a
57
+ # sanitized DTO always fits the model without a DB-level truncation error.
58
+ _MAX_EXTERNAL_ID = 255
59
+ _MAX_TITLE = 512
60
+ _MAX_STATUS = 64
61
+ _MAX_URL = 2000
62
+
63
+ # Per-(user, source) fetch caps (ADR-0097 §Decision #4 "Bounded growth"). The
64
+ # single-page fetch here is bounded to one page; the multi-page walk + 500-row
65
+ # cache cap live in the #1419 sync worker that persists these DTOs.
66
+ _FETCH_PAGE_SIZE = 100
67
+
68
+
69
+ @dataclass(frozen=True)
70
+ class ExternalWorkItemDTO:
71
+ """One remote work item pulled from an external source (read-only).
72
+
73
+ A transport object between a source's :meth:`ExternalTaskSource.fetch_assigned_items`
74
+ and the #1419 sync worker that upserts it into ``ExternalWorkItem``. It is
75
+ **not** a ``Task`` and never becomes one — the read-only invariant (ADR-0097
76
+ §2) is what keeps this feature OSS.
77
+
78
+ Attributes:
79
+ external_id: Provider-side identifier — the human key for Jira (``"RIV-482"``).
80
+ external_url: Deep link to the item in the provider (``https?://`` only).
81
+ title: Item summary.
82
+ external_status: Raw status name from the provider (``"In Review"``).
83
+ display_bucket: One of :data:`DISPLAY_BUCKETS`, mapped by the source.
84
+ due_date: Optional due date, or ``None``.
85
+ """
86
+
87
+ external_id: str
88
+ external_url: str
89
+ title: str
90
+ external_status: str
91
+ display_bucket: str
92
+ due_date: date | None = None
93
+
94
+ def sanitized(self) -> ExternalWorkItemDTO:
95
+ """Return a copy with untrusted fields length-capped and URL scheme-checked.
96
+
97
+ Called at the registry boundary on every DTO a source returns, so a
98
+ hostile or buggy provider cannot overflow a column or land a
99
+ ``javascript:``/``data:`` URL in the cache (ADR-0097 §Resolution #4). An
100
+ ``external_url`` that is not ``http(s)`` is dropped to an empty string
101
+ rather than raising — one bad link should not fail the whole pull.
102
+ """
103
+ url = self.external_url.strip()
104
+ scheme = urllib.parse.urlparse(url).scheme.lower()
105
+ if scheme not in ("http", "https"):
106
+ url = ""
107
+ bucket = self.display_bucket if self.display_bucket in DISPLAY_BUCKETS else BUCKET_TODO
108
+ return ExternalWorkItemDTO(
109
+ external_id=self.external_id[:_MAX_EXTERNAL_ID],
110
+ external_url=url[:_MAX_URL],
111
+ title=self.title[:_MAX_TITLE],
112
+ external_status=self.external_status[:_MAX_STATUS],
113
+ display_bucket=bucket,
114
+ due_date=self.due_date,
115
+ )
116
+
117
+
118
+ @dataclass(frozen=True)
119
+ class ExternalFetchResult:
120
+ """One pull's items **plus** what the source knows about what it left behind.
121
+
122
+ The page of DTOs alone cannot answer "is this all of it?" — which is exactly
123
+ the question a truncated My Work needs answered (#2925). This carries the two
124
+ facts that make truncation decidable by the caller:
125
+
126
+ Attributes:
127
+ items: The sanitized DTOs this call returned (a single page).
128
+ total_available: How many items the provider says match the query in
129
+ total, or ``None`` when the provider did not say. ``None`` means
130
+ "unknown", never "zero" — a caller must not render a count from it.
131
+ page_size: The most items this call *could* have returned, or ``None``
132
+ when the source is unbounded / does not paginate. Combined with
133
+ ``len(items)`` it is the fallback truncation signal for a provider
134
+ that reports no total: a full page means there is probably more.
135
+ """
136
+
137
+ items: list[ExternalWorkItemDTO]
138
+ total_available: int | None = None
139
+ page_size: int | None = None
140
+
141
+
142
+ class ExternalTaskSource(abc.ABC):
143
+ """Contract for a user-scoped, read-only external work-item source (ADR-0097 §1).
144
+
145
+ The whole stable cross-repo surface. A source is stateless — it is
146
+ instantiated per call and holds no credential. The caller decrypts the PAT
147
+ and passes ``secret`` (plaintext) + ``base_url`` + ``config`` so the source
148
+ never touches the credential store or ciphertext.
149
+ """
150
+
151
+ key: ClassVar[str]
152
+ label: ClassVar[str]
153
+ requires_credential: ClassVar[bool] = True
154
+
155
+ @abc.abstractmethod
156
+ def fetch_assigned_items(
157
+ self, *, base_url: str, secret: str, config: dict[str, Any]
158
+ ) -> list[ExternalWorkItemDTO]:
159
+ """Return the items currently assigned to the credential's owner (read-only).
160
+
161
+ A single SSRF-guarded page of results. The multi-page walk, the 500-row
162
+ cap, ``Retry-After`` backoff, and persistence to ``ExternalWorkItem`` are
163
+ the #1419 sync worker's job — a source is a pure read.
164
+ """
165
+
166
+ def fetch_assigned_items_result(
167
+ self, *, base_url: str, secret: str, config: dict[str, Any]
168
+ ) -> ExternalFetchResult:
169
+ """Same read as :meth:`fetch_assigned_items`, plus the pull's own bounds.
170
+
171
+ Deliberately **not** abstract, and deliberately additive rather than a
172
+ change to the abstract method's return type: an Enterprise source
173
+ registered against this ABC before #2925 implements only the abstract
174
+ method, and must keep working unchanged (the extension point is a
175
+ cross-repo commitment — see CLAUDE.md "OSS / Enterprise boundary rules").
176
+ The default therefore wraps the abstract read and reports *unknown*
177
+ bounds, which the sync worker renders as "no truncation claim" rather
178
+ than as "nothing was truncated". A source that can cheaply report its
179
+ provider's total (Jira's ``search.total``) overrides this.
180
+ """
181
+ items = self.fetch_assigned_items(base_url=base_url, secret=secret, config=config)
182
+ return ExternalFetchResult(items=items)
183
+
184
+ def verify_credential(
185
+ self, *, base_url: str, secret: str, config: dict[str, Any]
186
+ ) -> VerifyResult:
187
+ """Check the credential authenticates against the source.
188
+
189
+ Deliberately **not** abstract: the default accepts the credential
190
+ without a live check (``reason="unverified"``), so an Enterprise source
191
+ registered before this method existed keeps working. Sources that can
192
+ cheaply verify (a ``/myself`` ping) override it.
193
+ """
194
+ return VerifyResult(ok=True, reason="unverified")
195
+
196
+
197
+ # ---------------------------------------------------------------------------
198
+ # OSS Jira source — Cloud + Data Center / Server (ADR-0097 §Decision #1, ADR-0589)
199
+ # ---------------------------------------------------------------------------
200
+
201
+ # ``config["deployment"]`` discriminant (ADR-0589). One ``jira`` registry key
202
+ # spans both Atlassian-hosted Cloud and self-hosted Data Center / Server; the
203
+ # deployment selects the API version + auth shape at call time. A stored row
204
+ # without the key predates the discriminant and is treated as Cloud (the only
205
+ # variant that existed then), so the default is a safe upgrade no-op.
206
+ DEPLOYMENT_CLOUD = "cloud"
207
+ DEPLOYMENT_SERVER = "server"
208
+ JIRA_DEPLOYMENTS: tuple[str, ...] = (DEPLOYMENT_CLOUD, DEPLOYMENT_SERVER)
209
+
210
+ # ``statusCategory.key`` → display bucket. Jira has exactly three status
211
+ # categories and exposes them identically on REST v3 (Cloud) and v2 (Server/DC),
212
+ # so this projection is total, lossless at the category level, and shared across
213
+ # both deployments (the finer per-workflow status is preserved raw in
214
+ # ``external_status``).
215
+ _JIRA_CATEGORY_TO_BUCKET: dict[str, str] = {
216
+ "new": BUCKET_TODO,
217
+ "indeterminate": BUCKET_IN_PROGRESS,
218
+ "done": BUCKET_DONE,
219
+ }
220
+
221
+ # Default JQL: my open work. Overridable per connection via ``config["jql"]``.
222
+ # Valid on both Cloud (v3) and Server/DC (v2) — ``currentUser()`` and
223
+ # ``statusCategory`` are core JQL, not version-specific.
224
+ _DEFAULT_JIRA_JQL = "assignee = currentUser() AND statusCategory != Done ORDER BY updated DESC"
225
+
226
+ # Jira project keys are short tokens: a letter followed by letters, digits or
227
+ # underscores (Jira Cloud caps them at 10 characters; DC/Server is looser, so the
228
+ # ceiling here matches the serializer's ``max_length``).
229
+ #
230
+ # Enforced at *this* boundary, not only at the serializer, for two reasons. A
231
+ # stored ``config`` can predate the serializer's rule, and the key is interpolated
232
+ # into a JQL query — so a key carrying a quote or a parenthesis could rewrite the
233
+ # very filter it is supposed to narrow. An unusable key therefore fails the pull
234
+ # loudly (see :func:`_compose_jql`) rather than being dropped, because dropping it
235
+ # would silently *widen* what leaves Jira, which is the defect #2888 fixed.
236
+ JIRA_PROJECT_KEY_RE = re.compile(r"^[A-Za-z][A-Za-z0-9_]{0,63}$")
237
+
238
+ # JQL's one trailing clause. The project filter has to be ANDed into the WHERE
239
+ # part, *before* the sort — `... ORDER BY updated DESC AND project IN (...)` is a
240
+ # syntax error, so the clause cannot simply be appended.
241
+ _ORDER_BY_RE = re.compile(r"\border\s+by\b", re.IGNORECASE)
242
+
243
+
244
+ class JqlNotWellFormed(ValueError):
245
+ """A JQL query's parentheses or quotes do not balance.
246
+
247
+ Its own exception type because two callers need to react differently to it:
248
+ the connect serializer turns it into an inline 400 on the ``jql`` field, and
249
+ the pull path turns it into an :class:`ExternalSourceError`.
250
+ """
251
+
252
+
253
+ def scan_jql(jql: str) -> tuple[str, str]:
254
+ """Split a JQL string into ``(where_part, order_by_clause)``, validating structure.
255
+
256
+ Two jobs, in one pass, because they need the same quote/paren state:
257
+
258
+ 1. Find the last **top-level** ``ORDER BY`` — outside any quoted string and
259
+ outside any parenthesized group — so a literal in the filter itself
260
+ (``summary ~ "order by rank"``) is not mistaken for the sort clause. Either
261
+ part may be empty: a JQL with no sort yields ``("...", "")``, and a bare
262
+ ``"ORDER BY updated DESC"`` yields ``("", "ORDER BY updated DESC")``.
263
+
264
+ 2. Reject a query whose parentheses or quotes do not balance. This is the
265
+ load-bearing half. :func:`_compose_jql` narrows by wrapping the WHERE part
266
+ in one pair of parentheses and ANDing a project clause after it — and an
267
+ *unbalanced* input turns that wrap into a no-op::
268
+
269
+ jql = 'project = "PUBLIC") OR (project = "SECRET"'
270
+ composed = '(project = "PUBLIC") OR (project = "SECRET") AND project IN ("RIV")'
271
+
272
+ which is valid JQL where ``AND`` binds tighter than ``OR``, so every
273
+ ``PUBLIC`` issue comes back unrestricted by the project filter. The wrap
274
+ only contains an ``OR`` when the thing being wrapped is a single group,
275
+ so structural balance is a precondition of the narrowing, not a nicety.
276
+
277
+ Raises:
278
+ JqlNotWellFormed: unbalanced parentheses, or an unterminated quoted string.
279
+ """
280
+ in_quote: str | None = None
281
+ depth = 0
282
+ index = 0
283
+ split_at = -1
284
+ length = len(jql)
285
+ while index < length:
286
+ char = jql[index]
287
+ if in_quote is not None:
288
+ # JQL escapes a quote inside a string with a backslash; skip the pair
289
+ # so an escaped quote does not look like the end of the string.
290
+ if char == "\\":
291
+ index += 2
292
+ continue
293
+ if char == in_quote:
294
+ in_quote = None
295
+ index += 1
296
+ continue
297
+ if char in ('"', "'"):
298
+ in_quote = char
299
+ index += 1
300
+ continue
301
+ if char == "(":
302
+ depth += 1
303
+ index += 1
304
+ continue
305
+ if char == ")":
306
+ depth -= 1
307
+ if depth < 0:
308
+ raise JqlNotWellFormed("unbalanced ')' in JQL")
309
+ index += 1
310
+ continue
311
+ # ORDER BY is only the sort clause at the top level; inside a group it
312
+ # cannot legally appear, so ignoring it there keeps the split honest.
313
+ if depth == 0:
314
+ match = _ORDER_BY_RE.match(jql, index)
315
+ if match:
316
+ split_at = index
317
+ index = match.end()
318
+ continue
319
+ index += 1
320
+
321
+ if in_quote is not None:
322
+ raise JqlNotWellFormed("unterminated quoted string in JQL")
323
+ if depth != 0:
324
+ raise JqlNotWellFormed("unbalanced '(' in JQL")
325
+
326
+ if split_at < 0:
327
+ return jql.strip(), ""
328
+ return jql[:split_at].strip(), jql[split_at:].strip()
329
+
330
+
331
+ def _compose_jql(jql: str, project_keys: Any) -> str:
332
+ """Narrow a JQL query to the connection's selected Jira projects (#2888).
333
+
334
+ The "Projects" filter on a connection is a **scoping** control: a contributor
335
+ naming two keys to keep a third project out of a shared tool has to get that,
336
+ and the guarantee cannot be one a custom JQL is able to widen. So the keys are
337
+ always ANDed on top of whatever query is in effect — the default one *and* a
338
+ user-supplied ``config["jql"]``::
339
+
340
+ (assignee = currentUser()) AND project IN ("RIV", "BAY") ORDER BY updated DESC
341
+
342
+ The WHERE part is parenthesized so an ``OR`` inside the user's own query cannot
343
+ escape the project constraint (``a OR b AND project IN (…)`` binds ``AND``
344
+ tighter and would pull every ``a``). That wrap is only sound on a structurally
345
+ balanced query, so the query is validated first — see :func:`scan_jql`. No keys
346
+ selected means no clause, no wrap and no validation: the query is returned
347
+ unchanged and Jira's own parser stays the authority ("leave blank for all").
348
+
349
+ Args:
350
+ jql: The effective query — a stored custom one, or :data:`_DEFAULT_JIRA_JQL`.
351
+ project_keys: The connection's ``config["project_keys"]`` (any JSON value;
352
+ a non-list is treated as no selection).
353
+
354
+ Returns:
355
+ The JQL to send to Jira.
356
+
357
+ Raises:
358
+ ExternalSourceError: A stored key is not a syntactically valid Jira project
359
+ key, or the stored JQL is not well-formed enough to narrow safely.
360
+ Failing the pull is deliberate in both cases: proceeding would widen it
361
+ past what the user asked for, and a silent widening of a privacy control
362
+ is worse than a visible failure.
363
+ """
364
+ if not isinstance(project_keys, list | tuple):
365
+ return jql
366
+ seen: dict[str, None] = {}
367
+ for raw in project_keys:
368
+ key = str(raw).strip()
369
+ if not key:
370
+ continue
371
+ if not JIRA_PROJECT_KEY_RE.match(key):
372
+ raise ExternalSourceConfigError(
373
+ "Jira project filter contains an invalid project key; "
374
+ "reconnect the source to correct it."
375
+ )
376
+ seen.setdefault(key.upper(), None)
377
+ if not seen:
378
+ return jql
379
+
380
+ clause = "project IN (" + ", ".join(f'"{key}"' for key in seen) + ")"
381
+ try:
382
+ where, order_by = scan_jql(jql)
383
+ except JqlNotWellFormed as exc:
384
+ raise ExternalSourceConfigError(
385
+ f"Stored Jira filter cannot be scoped to the selected projects ({exc}); "
386
+ "reconnect the source to correct it."
387
+ ) from exc
388
+ narrowed = f"({where}) AND {clause}" if where else clause
389
+ return f"{narrowed} {order_by}" if order_by else narrowed
390
+
391
+
392
+ def _jira_origin(base_url: str) -> str:
393
+ """Return the ``https://host`` origin for a Jira **Cloud** ``base_url``.
394
+
395
+ Defense in depth: the connection endpoint already allow-listed the host to
396
+ ``*.atlassian.net`` before storing it, but this reconstructs the origin from
397
+ the parsed host and forces ``https`` so a stored value can never downgrade
398
+ the scheme or carry a path/query into the request URL. Cloud is always
399
+ root-hosted, so dropping any path is correct here — Server/DC (which is
400
+ commonly deployed under a context path) uses :func:`_jira_server_base`.
401
+
402
+ Raises:
403
+ ValueError: ``base_url`` has no hostname.
404
+ """
405
+ parsed = urllib.parse.urlparse(base_url if "//" in base_url else f"https://{base_url}")
406
+ host = (parsed.hostname or "").lower()
407
+ if not host:
408
+ raise ValueError("Jira base_url must include a hostname")
409
+ return f"https://{host}"
410
+
411
+
412
+ def _jira_server_base(base_url: str) -> str:
413
+ """Return the https API base (origin **+ context path**) for a Jira DC/Server host.
414
+
415
+ Unlike Cloud, a Data Center / Server instance is frequently deployed under a
416
+ context path (``https://jira.corp.example/jira``) and/or a non-standard port
417
+ (``https://jira.corp.example:8443``). Both must be preserved or every REST
418
+ call 404s / connects to the wrong port and the connection looks "connected
419
+ but empty". Forces ``https`` and strips a trailing slash so
420
+ ``{base}/rest/api/2/...`` is well-formed. The host is already
421
+ operator-allow-listed (``providers.assert_base_url_allowed``) before this
422
+ runs, so preserving the port/path does not widen the SSRF surface — the
423
+ allow-list, not this function, is the egress gate.
424
+
425
+ Raises:
426
+ ValueError: ``base_url`` has no hostname.
427
+ """
428
+ parsed = urllib.parse.urlparse(base_url if "//" in base_url else f"https://{base_url}")
429
+ host = (parsed.hostname or "").lower()
430
+ if not host:
431
+ raise ValueError("Jira base_url must include a hostname")
432
+ port = f":{parsed.port}" if parsed.port else ""
433
+ path = parsed.path.rstrip("/")
434
+ return f"https://{host}{port}{path}"
435
+
436
+
437
+ def _jira_auth_header(email: str, api_token: str) -> str:
438
+ """Build the Basic-auth header value for a Jira **Cloud** API token.
439
+
440
+ Jira Cloud authenticates API tokens with HTTP Basic ``email:token`` (Bearer
441
+ is OAuth-3LO only, which is the Enterprise governance path). Kept as a helper
442
+ so the PAT never appears in a log-adjacent f-string at the call site.
443
+ Server/DC instead uses a Personal Access Token as ``Authorization: Bearer``.
444
+ """
445
+ raw = f"{email}:{api_token}".encode()
446
+ return "Basic " + base64.b64encode(raw).decode("ascii")
447
+
448
+
449
+ def _jira_issue_to_dto(base: str, issue: dict[str, Any]) -> ExternalWorkItemDTO:
450
+ """Map one Jira issue JSON object to a sanitized DTO.
451
+
452
+ ``base`` is the deployment's API base (Cloud origin, or Server origin +
453
+ context path) so the ``/browse/`` deep link stays correct under a DC context
454
+ path. The issue JSON shape (``fields.{summary,status,duedate}`` and
455
+ ``status.statusCategory.key``) is identical across REST v2 and v3.
456
+ """
457
+ fields = _as_dict(issue.get("fields"))
458
+ status = _as_dict(fields.get("status"))
459
+ category = _as_dict(status.get("statusCategory"))
460
+ bucket = _JIRA_CATEGORY_TO_BUCKET.get(str(category.get("key", "")).lower(), BUCKET_TODO)
461
+ key = str(issue.get("key", ""))
462
+ return ExternalWorkItemDTO(
463
+ external_id=key,
464
+ external_url=f"{base}/browse/{urllib.parse.quote(key)}" if key else "",
465
+ title=str(fields.get("summary", "")),
466
+ external_status=str(status.get("name", "")),
467
+ display_bucket=bucket,
468
+ due_date=_parse_iso_date(fields.get("duedate")),
469
+ ).sanitized()
470
+
471
+
472
+ class _JiraBackend(abc.ABC):
473
+ """Per-deployment API-shape strategy for the ``jira`` source (ADR-0589).
474
+
475
+ Cloud and Server/DC differ on only three axes — the REST version, the auth
476
+ header, and whether an account email is required — so the request, parse, and
477
+ DTO-mapping logic is shared here and each subclass carries just those deltas.
478
+ Stateless: instantiated per call by :class:`JiraSource`, holds no credential.
479
+ """
480
+
481
+ rest_version: ClassVar[str]
482
+
483
+ @abc.abstractmethod
484
+ def _base(self, base_url: str) -> str:
485
+ """Return the https API base for this deployment (raises ``ValueError`` if no host)."""
486
+
487
+ @abc.abstractmethod
488
+ def _headers(self, secret: str, config: dict[str, Any]) -> dict[str, str]:
489
+ """Auth + Accept headers for this deployment."""
490
+
491
+ def _missing_requirement(self, config: dict[str, Any]) -> str | None:
492
+ """Return a :class:`VerifyResult` reason if the credential is structurally
493
+ incomplete for this deployment (Cloud Basic auth needs an account email),
494
+ else ``None``. Lets ``verify`` fail fast without a network round-trip."""
495
+ return None
496
+
497
+ def verify(self, *, base_url: str, secret: str, config: dict[str, Any]) -> VerifyResult:
498
+ """Ping ``/rest/api/<v>/myself`` to confirm the credential authenticates.
499
+
500
+ A 200 means usable; 401/403 means a wrong/expired token (or wrong email on
501
+ Cloud); 5xx and transport failures degrade to "unreachable" so the user
502
+ can retry rather than assume a dead token.
503
+ """
504
+ reason = self._missing_requirement(config)
505
+ if reason:
506
+ return VerifyResult(ok=False, reason=reason)
507
+ try:
508
+ base = self._base(base_url)
509
+ except ValueError:
510
+ return VerifyResult(ok=False, reason="blocked_host")
511
+ try:
512
+ response = http.get(
513
+ f"{base}/rest/api/{self.rest_version}/myself",
514
+ headers=self._headers(secret, config),
515
+ )
516
+ except http.EgressTimeout:
517
+ return VerifyResult(ok=False, reason="provider_timeout")
518
+ except http.EgressBlocked:
519
+ return VerifyResult(ok=False, reason="blocked_host")
520
+ except http.EgressError:
521
+ return VerifyResult(ok=False, reason="provider_unreachable")
522
+
523
+ if response.status == 200:
524
+ payload = response.json()
525
+ username = payload.get("displayName") if isinstance(payload, dict) else None
526
+ return VerifyResult(ok=True, username=username)
527
+ if response.status >= 500:
528
+ return VerifyResult(ok=False, reason="provider_unreachable")
529
+ return VerifyResult(ok=False, reason="invalid_token")
530
+
531
+ def fetch(
532
+ self, *, base_url: str, secret: str, config: dict[str, Any]
533
+ ) -> list[ExternalWorkItemDTO]:
534
+ """Items-only view of :meth:`fetch_result`, for callers that want the page."""
535
+ return self.fetch_result(base_url=base_url, secret=secret, config=config).items
536
+
537
+ def fetch_result(
538
+ self, *, base_url: str, secret: str, config: dict[str, Any]
539
+ ) -> ExternalFetchResult:
540
+ """Fetch one page of the user's assigned issues as sanitized DTOs.
541
+
542
+ Read-only ``GET /rest/api/<v>/search``. Transport/parse failures raise
543
+ (the #1419 worker maps them to the connection's staleness / auth-failed
544
+ state); an auth failure raises so the worker flips the connection to
545
+ ``auth_failed`` rather than silently returning an empty list that would
546
+ soft-remove every cached item.
547
+
548
+ The connection's ``config["project_keys"]`` is ANDed onto the query here
549
+ (:func:`_compose_jql`), so the "Projects" filter narrows what actually
550
+ leaves Jira instead of only being echoed back to the owner (#2888).
551
+
552
+ Returns the page **and** Jira's own ``total`` for the query, so the sync
553
+ worker can tell the owner their My Work is showing the first N of M
554
+ rather than presenting a truncated page as the whole answer (#2925).
555
+
556
+ Raises:
557
+ ExternalSourceAuthError: 401/403 — token expired or revoked.
558
+ ExternalSourceRateLimited: 429 — carries ``Retry-After`` when Jira
559
+ sent one, so the caller can re-queue instead of dead-lettering.
560
+ ExternalSourceError: any other non-200, a transport failure, or a
561
+ stored project key that is not a valid Jira project key.
562
+ """
563
+ cfg = config or {}
564
+ jql = _compose_jql(
565
+ (cfg.get("jql") or "").strip() or _DEFAULT_JIRA_JQL,
566
+ cfg.get("project_keys"),
567
+ )
568
+ base = self._base(base_url)
569
+ query = urllib.parse.urlencode(
570
+ {
571
+ "jql": jql,
572
+ "fields": "summary,status,duedate",
573
+ "maxResults": str(_FETCH_PAGE_SIZE),
574
+ }
575
+ )
576
+ try:
577
+ response = http.get(
578
+ f"{base}/rest/api/{self.rest_version}/search?{query}",
579
+ headers=self._headers(secret, cfg),
580
+ )
581
+ except http.EgressBlocked as exc:
582
+ raise ExternalSourceError(f"Jira host blocked by egress guard: {exc}") from exc
583
+ except (http.EgressTimeout, http.EgressError) as exc:
584
+ raise ExternalSourceError(f"Jira unreachable: {exc}") from exc
585
+
586
+ if response.status in (401, 403):
587
+ raise ExternalSourceAuthError("Jira rejected the credential (expired or revoked)")
588
+ if response.status == 429:
589
+ # Must precede the generic non-200 arm: a rate limit is the provider
590
+ # telling us when to come back, not a failure to reach it (#2924).
591
+ retry_after = parse_retry_after(response.headers)
592
+ raise ExternalSourceRateLimited("Jira rate-limited the search (HTTP 429)", retry_after)
593
+ if response.status != 200:
594
+ raise ExternalSourceError(f"Jira search returned HTTP {response.status}")
595
+
596
+ payload = response.json()
597
+ if not isinstance(payload, dict):
598
+ raise ExternalSourceError("Jira search returned a non-JSON body")
599
+ issues = payload.get("issues")
600
+ total = _as_non_negative_int(payload.get("total"))
601
+ if not isinstance(issues, list):
602
+ return ExternalFetchResult(items=[], total_available=total, page_size=_FETCH_PAGE_SIZE)
603
+ items = [_jira_issue_to_dto(base, issue) for issue in issues if isinstance(issue, dict)]
604
+ return ExternalFetchResult(items=items, total_available=total, page_size=_FETCH_PAGE_SIZE)
605
+
606
+
607
+ class _JiraCloudBackend(_JiraBackend):
608
+ """Atlassian-hosted Jira Cloud: REST v3 + Basic ``email:token`` auth."""
609
+
610
+ rest_version: ClassVar[str] = "3"
611
+
612
+ def _base(self, base_url: str) -> str:
613
+ return _jira_origin(base_url)
614
+
615
+ def _headers(self, secret: str, config: dict[str, Any]) -> dict[str, str]:
616
+ email = (config or {}).get("account_email", "").strip()
617
+ return {"Authorization": _jira_auth_header(email, secret), "Accept": "application/json"}
618
+
619
+ def _missing_requirement(self, config: dict[str, Any]) -> str | None:
620
+ if not (config or {}).get("account_email", "").strip():
621
+ return "missing_email"
622
+ return None
623
+
624
+
625
+ class _JiraServerBackend(_JiraBackend):
626
+ """Self-hosted Jira Data Center / Server: REST v2 + Personal Access Token.
627
+
628
+ DC/Server PATs (8.14+) authenticate as ``Authorization: Bearer <pat>`` and
629
+ need no account email. Basic ``user:password`` (pre-PAT installs) is a tracked
630
+ follow-up (#2272), not part of this variant.
631
+ """
632
+
633
+ rest_version: ClassVar[str] = "2"
634
+
635
+ def _base(self, base_url: str) -> str:
636
+ return _jira_server_base(base_url)
637
+
638
+ def _headers(self, secret: str, config: dict[str, Any]) -> dict[str, str]:
639
+ return {"Authorization": f"Bearer {secret}", "Accept": "application/json"}
640
+
641
+
642
+ class JiraSource(ExternalTaskSource):
643
+ """Read-only personal pull of a user's assigned Jira issues (ADR-0097, ADR-0589).
644
+
645
+ One registry key, ``jira``, spanning both Atlassian Cloud and self-hosted
646
+ Data Center / Server. ``config["deployment"]`` (``"cloud"`` default |
647
+ ``"server"``) selects the API version + auth shape; ``base_url`` is the
648
+ tenant/host, allow-listed by the connection endpoint before any request is
649
+ made. ``config`` also carries ``{"account_email" (Cloud only), "jql",
650
+ "project_keys"}`` — ``jql`` selects *what* to pull (defaulting to
651
+ :data:`_DEFAULT_JIRA_JQL`) and ``project_keys`` narrows it to named projects,
652
+ ANDed on top of the query rather than replaced by it (#2888).
653
+ """
654
+
655
+ key: ClassVar[str] = "jira"
656
+ label: ClassVar[str] = "Jira"
657
+ requires_credential: ClassVar[bool] = True
658
+
659
+ @staticmethod
660
+ def _backend(config: dict[str, Any]) -> _JiraBackend:
661
+ """Select the deployment strategy from ``config["deployment"]`` (Cloud default)."""
662
+ deployment = (config or {}).get("deployment", DEPLOYMENT_CLOUD)
663
+ return _JiraServerBackend() if deployment == DEPLOYMENT_SERVER else _JiraCloudBackend()
664
+
665
+ def verify_credential(
666
+ self, *, base_url: str, secret: str, config: dict[str, Any]
667
+ ) -> VerifyResult:
668
+ return self._backend(config).verify(base_url=base_url, secret=secret, config=config or {})
669
+
670
+ def fetch_assigned_items(
671
+ self, *, base_url: str, secret: str, config: dict[str, Any]
672
+ ) -> list[ExternalWorkItemDTO]:
673
+ return self._backend(config).fetch(base_url=base_url, secret=secret, config=config or {})
674
+
675
+ def fetch_assigned_items_result(
676
+ self, *, base_url: str, secret: str, config: dict[str, Any]
677
+ ) -> ExternalFetchResult:
678
+ """Override the ABC default: Jira reports a real ``total`` for the query.
679
+
680
+ So truncation is an exact fact here ("the first 100 of 412"), not the
681
+ full-page guess the base implementation would have to fall back on.
682
+ """
683
+ return self._backend(config).fetch_result(
684
+ base_url=base_url, secret=secret, config=config or {}
685
+ )
686
+
687
+
688
+ def _as_dict(value: Any) -> dict[str, Any]:
689
+ """Narrow an untrusted JSON value to a dict (empty if it is not one).
690
+
691
+ Jira nests ``fields.status.statusCategory``; any level can be absent or a
692
+ non-object in a malformed response. Coercing to ``{}`` keeps the mapping
693
+ total without a cascade of ``isinstance`` guards at each ``.get`` site.
694
+ """
695
+ return value if isinstance(value, dict) else {}
696
+
697
+
698
+ def _as_non_negative_int(value: Any) -> int | None:
699
+ """Narrow an untrusted JSON number to a count, or ``None`` if it is not one.
700
+
701
+ ``total`` comes from the provider, so it can be absent, a string, a float, or
702
+ negative. Anything that is not a plain non-negative ``int`` degrades to
703
+ ``None`` ("unknown"), because a wrong number here becomes a wrong sentence in
704
+ the owner's UI — "showing the first 100 of -1" is worse than saying nothing.
705
+ ``bool`` is excluded explicitly: it is an ``int`` subclass, so ``True`` would
706
+ otherwise read as a total of 1.
707
+ """
708
+ if isinstance(value, bool) or not isinstance(value, int):
709
+ return None
710
+ return value if value >= 0 else None
711
+
712
+
713
+ def _parse_iso_date(value: Any) -> date | None:
714
+ """Parse a Jira ``YYYY-MM-DD`` due date, tolerating null / bad shapes."""
715
+ if not isinstance(value, str) or not value:
716
+ return None
717
+ try:
718
+ return date.fromisoformat(value[:10])
719
+ except ValueError:
720
+ return None
721
+
722
+
723
+ def parse_retry_after(headers: dict[str, str], *, now: datetime | None = None) -> float | None:
724
+ """Seconds to wait from a ``Retry-After`` header, or ``None`` if unusable.
725
+
726
+ RFC 9110 permits delta-seconds (``Retry-After: 30``) or an absolute HTTP-date;
727
+ Jira Cloud sends the former and a fronting proxy may rewrite it to the latter,
728
+ so both are read. Header lookup is case-insensitive because ``EgressResponse``
729
+ carries a plain ``dict``, not a case-folding mapping.
730
+
731
+ Total by design: anything unparseable or negative yields ``None``, so a
732
+ malformed header degrades to "no hint" rather than raising inside the failure
733
+ path it is trying to describe.
734
+ """
735
+
736
+ raw = ""
737
+ for key, value in headers.items():
738
+ if key.lower() == "retry-after":
739
+ raw = (value or "").strip()
740
+ break
741
+ if not raw:
742
+ return None
743
+ try:
744
+ seconds = float(int(raw))
745
+ except ValueError:
746
+ # parsedate_to_datetime RAISES on an unparseable value (it does not return
747
+ # None), so this has to be caught or a junk header crashes the 429 path.
748
+ try:
749
+ parsed = email.utils.parsedate_to_datetime(raw)
750
+ except (TypeError, ValueError):
751
+ return None
752
+ if parsed is None: # pragma: no cover — defensive across versions
753
+ return None
754
+ reference = now or datetime.now(UTC)
755
+ try:
756
+ seconds = (parsed - reference).total_seconds()
757
+ except (TypeError, ValueError):
758
+ return None
759
+ return seconds if seconds >= 0 else None
760
+
761
+
762
+ class ExternalSourceError(Exception):
763
+ """A source could not complete a read (transport, non-200, or bad body).
764
+
765
+ The caller keeps the last-good cache and surfaces a staleness note
766
+ (ADR-0097 §5) rather than wiping items on a transient failure.
767
+ """
768
+
769
+
770
+ class ExternalSourceAuthError(ExternalSourceError):
771
+ """The source rejected the credential (401/403).
772
+
773
+ Distinct from :class:`ExternalSourceError` so the caller can short-circuit
774
+ retries and flip the connection to ``auth_failed`` (ADR-0097 §5) instead of
775
+ backing off and retrying a dead token.
776
+ """
777
+
778
+
779
+ class ExternalSourceRateLimited(ExternalSourceError):
780
+ """The source rate-limited the read (HTTP 429) — #2924.
781
+
782
+ Distinct from :class:`ExternalSourceError` because the two need opposite
783
+ handling and conflating them produced the wrong belief. The generic case means
784
+ "the provider is having a moment"; the worker retires the attempt and the user
785
+ (or the next poll) re-triggers. A 429 is not that: it is the provider
786
+ explicitly saying *when* to come back, so the pull is re-queued against that
787
+ clock instead of dead-lettered as "unreachable" — which is what made a
788
+ transient Jira rate limit read to the owner as a broken connection.
789
+
790
+ Attributes:
791
+ retry_after: Seconds until the source will accept another request, parsed
792
+ from ``Retry-After``, or ``None`` when the header was absent or
793
+ unparseable. ``None`` means "no hint" and the caller applies its own
794
+ default backoff — never "do not retry".
795
+ """
796
+
797
+ def __init__(self, message: str, retry_after: float | None = None) -> None:
798
+ super().__init__(message)
799
+ self.retry_after = retry_after
800
+
801
+
802
+ class ExternalSourceConfigError(ExternalSourceError):
803
+ """The connection's stored ``config`` cannot produce a safe query.
804
+
805
+ Distinct from :class:`ExternalSourceError` for the same reason
806
+ :class:`ExternalSourceAuthError` is: the generic case means "the provider is
807
+ having a moment, keep the cache and retry", and retrying fixes nothing here —
808
+ the stored filter will be just as unusable next time. Without the distinction
809
+ a connection carrying a bad filter reports "unreachable" forever while serving
810
+ a cache that was populated under the *unscoped* query, which is the wrong
811
+ belief #2888 exists to prevent, reached by another route. Subclasses
812
+ ``ExternalSourceError`` so any caller that only knows the base class still
813
+ degrades safely.
814
+ """
815
+
816
+
817
+ # ---------------------------------------------------------------------------
818
+ # Registry instance + OSS registration list
819
+ # ---------------------------------------------------------------------------
820
+
821
+ # Distinct from TASK_LINK_PROVIDERS (ADR-0097 §1) — do not merge the two.
822
+ EXTERNAL_TASK_SOURCES = ProviderRegistry("EXTERNAL_TASK_SOURCES", ExternalTaskSource)
823
+
824
+ # Registered against EXTERNAL_TASK_SOURCES in IntegrationsConfig.ready(). OSS
825
+ # owns ``jira`` here (read-only personal pull, Cloud + Data Center / Server);
826
+ # Enterprise appends its own.
827
+ OSS_EXTERNAL_TASK_SOURCES: tuple[type[ExternalTaskSource], ...] = (JiraSource,)
828
+
829
+
830
+ # Fields the field-length caps above were sized against, re-exported so the model
831
+ # module and tests assert against one source of truth rather than magic numbers.
832
+ EXTERNAL_WORK_ITEM_FIELD_CAPS: dict[str, int] = {
833
+ "external_id": _MAX_EXTERNAL_ID,
834
+ "title": _MAX_TITLE,
835
+ "external_status": _MAX_STATUS,
836
+ "external_url": _MAX_URL,
837
+ }