trueppm-api 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (799) hide show
  1. trueppm_api/__init__.py +27 -0
  2. trueppm_api/apps/access/__init__.py +0 -0
  3. trueppm_api/apps/access/apps.py +19 -0
  4. trueppm_api/apps/access/groups.py +518 -0
  5. trueppm_api/apps/access/management/__init__.py +0 -0
  6. trueppm_api/apps/access/management/commands/__init__.py +0 -0
  7. trueppm_api/apps/access/management/commands/create_admin.py +346 -0
  8. trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
  9. trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -0
  10. trueppm_api/apps/access/migrations/0001_initial.py +61 -0
  11. trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +184 -0
  12. trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
  13. trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
  14. trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
  15. trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
  16. trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
  17. trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
  18. trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
  19. trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
  20. trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
  21. trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
  22. trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
  23. trueppm_api/apps/access/migrations/0013_squashed_0019_release_0_4.py +302 -0
  24. trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
  25. trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
  26. trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
  27. trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
  28. trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +143 -0
  29. trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
  30. trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
  31. trueppm_api/apps/access/migrations/__init__.py +0 -0
  32. trueppm_api/apps/access/minter_role.py +38 -0
  33. trueppm_api/apps/access/models.py +588 -0
  34. trueppm_api/apps/access/permissions.py +3263 -0
  35. trueppm_api/apps/access/serializers.py +1007 -0
  36. trueppm_api/apps/access/services.py +878 -0
  37. trueppm_api/apps/access/signals.py +171 -0
  38. trueppm_api/apps/access/tasks.py +50 -0
  39. trueppm_api/apps/access/throttles.py +96 -0
  40. trueppm_api/apps/access/urls.py +191 -0
  41. trueppm_api/apps/access/views.py +1965 -0
  42. trueppm_api/apps/agents/__init__.py +6 -0
  43. trueppm_api/apps/agents/apps.py +11 -0
  44. trueppm_api/apps/agents/canonical.py +73 -0
  45. trueppm_api/apps/agents/deferred.py +108 -0
  46. trueppm_api/apps/agents/management/__init__.py +0 -0
  47. trueppm_api/apps/agents/management/commands/__init__.py +0 -0
  48. trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
  49. trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
  50. trueppm_api/apps/agents/middleware.py +44 -0
  51. trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
  52. trueppm_api/apps/agents/migrations/0001_squashed_0003_release_0_4.py +344 -0
  53. trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
  54. trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
  55. trueppm_api/apps/agents/migrations/__init__.py +0 -0
  56. trueppm_api/apps/agents/models.py +371 -0
  57. trueppm_api/apps/agents/refusal.py +103 -0
  58. trueppm_api/apps/agents/serializers.py +85 -0
  59. trueppm_api/apps/agents/services.py +397 -0
  60. trueppm_api/apps/agents/signals.py +30 -0
  61. trueppm_api/apps/agents/urls.py +15 -0
  62. trueppm_api/apps/agents/views.py +109 -0
  63. trueppm_api/apps/csvimport/__init__.py +12 -0
  64. trueppm_api/apps/csvimport/apps.py +11 -0
  65. trueppm_api/apps/csvimport/mapping.py +363 -0
  66. trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
  67. trueppm_api/apps/csvimport/migrations/0001_squashed_0003_release_0_4.py +90 -0
  68. trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
  69. trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
  70. trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
  71. trueppm_api/apps/csvimport/models.py +111 -0
  72. trueppm_api/apps/csvimport/parser.py +1825 -0
  73. trueppm_api/apps/csvimport/services.py +48 -0
  74. trueppm_api/apps/csvimport/tasks.py +459 -0
  75. trueppm_api/apps/csvimport/template.py +37 -0
  76. trueppm_api/apps/csvimport/urls.py +50 -0
  77. trueppm_api/apps/csvimport/views.py +793 -0
  78. trueppm_api/apps/history/__init__.py +0 -0
  79. trueppm_api/apps/history/apps.py +12 -0
  80. trueppm_api/apps/history/changelog.py +359 -0
  81. trueppm_api/apps/history/diff_policy.py +178 -0
  82. trueppm_api/apps/history/serializers.py +104 -0
  83. trueppm_api/apps/history/signals.py +82 -0
  84. trueppm_api/apps/history/tasks.py +105 -0
  85. trueppm_api/apps/history/urls.py +38 -0
  86. trueppm_api/apps/history/views.py +443 -0
  87. trueppm_api/apps/idempotency/__init__.py +0 -0
  88. trueppm_api/apps/idempotency/apps.py +11 -0
  89. trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
  90. trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
  91. trueppm_api/apps/idempotency/mixins.py +226 -0
  92. trueppm_api/apps/idempotency/models.py +75 -0
  93. trueppm_api/apps/idempotency/tasks.py +61 -0
  94. trueppm_api/apps/integrations/__init__.py +13 -0
  95. trueppm_api/apps/integrations/apps.py +59 -0
  96. trueppm_api/apps/integrations/connections.py +806 -0
  97. trueppm_api/apps/integrations/encryption.py +92 -0
  98. trueppm_api/apps/integrations/external_sources.py +863 -0
  99. trueppm_api/apps/integrations/git_automation_services.py +231 -0
  100. trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
  101. trueppm_api/apps/integrations/http.py +597 -0
  102. trueppm_api/apps/integrations/me_work.py +218 -0
  103. trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
  104. trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
  105. trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
  106. trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
  107. trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
  108. trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
  109. trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
  110. trueppm_api/apps/integrations/migrations/0007_squashed_0014_release_0_4.py +225 -0
  111. trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
  112. trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
  113. trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
  114. trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
  115. trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
  116. trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
  117. trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
  118. trueppm_api/apps/integrations/migrations/0014_tasklink_created_by.py +27 -0
  119. trueppm_api/apps/integrations/migrations/__init__.py +0 -0
  120. trueppm_api/apps/integrations/models.py +635 -0
  121. trueppm_api/apps/integrations/notification_channels.py +57 -0
  122. trueppm_api/apps/integrations/opengraph.py +229 -0
  123. trueppm_api/apps/integrations/outgoing.py +201 -0
  124. trueppm_api/apps/integrations/providers.py +528 -0
  125. trueppm_api/apps/integrations/registry.py +345 -0
  126. trueppm_api/apps/integrations/serializers.py +453 -0
  127. trueppm_api/apps/integrations/services.py +171 -0
  128. trueppm_api/apps/integrations/tasks.py +734 -0
  129. trueppm_api/apps/integrations/throttles.py +165 -0
  130. trueppm_api/apps/integrations/urls.py +77 -0
  131. trueppm_api/apps/integrations/views.py +1158 -0
  132. trueppm_api/apps/jiraimport/__init__.py +8 -0
  133. trueppm_api/apps/jiraimport/apps.py +11 -0
  134. trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
  135. trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
  136. trueppm_api/apps/jiraimport/models.py +70 -0
  137. trueppm_api/apps/jiraimport/parser.py +350 -0
  138. trueppm_api/apps/jiraimport/services.py +46 -0
  139. trueppm_api/apps/jiraimport/tasks.py +280 -0
  140. trueppm_api/apps/jiraimport/urls.py +15 -0
  141. trueppm_api/apps/jiraimport/views.py +153 -0
  142. trueppm_api/apps/msproject/__init__.py +0 -0
  143. trueppm_api/apps/msproject/apps.py +11 -0
  144. trueppm_api/apps/msproject/dataclasses.py +153 -0
  145. trueppm_api/apps/msproject/exporter.py +551 -0
  146. trueppm_api/apps/msproject/extended_attributes.py +77 -0
  147. trueppm_api/apps/msproject/importer.py +1095 -0
  148. trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
  149. trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
  150. trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
  151. trueppm_api/apps/msproject/migrations/__init__.py +0 -0
  152. trueppm_api/apps/msproject/models.py +81 -0
  153. trueppm_api/apps/msproject/parser.py +1276 -0
  154. trueppm_api/apps/msproject/serializers.py +136 -0
  155. trueppm_api/apps/msproject/services.py +58 -0
  156. trueppm_api/apps/msproject/tasks.py +439 -0
  157. trueppm_api/apps/msproject/urls.py +39 -0
  158. trueppm_api/apps/msproject/views.py +531 -0
  159. trueppm_api/apps/notifications/__init__.py +7 -0
  160. trueppm_api/apps/notifications/apps.py +15 -0
  161. trueppm_api/apps/notifications/backfill.py +79 -0
  162. trueppm_api/apps/notifications/categories.py +112 -0
  163. trueppm_api/apps/notifications/delivery_limits.py +258 -0
  164. trueppm_api/apps/notifications/digests.py +462 -0
  165. trueppm_api/apps/notifications/email_backend.py +350 -0
  166. trueppm_api/apps/notifications/email_health.py +128 -0
  167. trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
  168. trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +299 -0
  169. trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
  170. trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
  171. trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
  172. trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
  173. trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
  174. trueppm_api/apps/notifications/migrations/0007_squashed_0011_release_0_4.py +204 -0
  175. trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
  176. trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
  177. trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
  178. trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
  179. trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
  180. trueppm_api/apps/notifications/migrations/__init__.py +0 -0
  181. trueppm_api/apps/notifications/models.py +1148 -0
  182. trueppm_api/apps/notifications/receivers.py +155 -0
  183. trueppm_api/apps/notifications/schema_migrations.py +56 -0
  184. trueppm_api/apps/notifications/serializers.py +1094 -0
  185. trueppm_api/apps/notifications/services.py +1489 -0
  186. trueppm_api/apps/notifications/tasks.py +762 -0
  187. trueppm_api/apps/notifications/throttles.py +98 -0
  188. trueppm_api/apps/notifications/urls.py +97 -0
  189. trueppm_api/apps/notifications/views.py +919 -0
  190. trueppm_api/apps/observability/__init__.py +0 -0
  191. trueppm_api/apps/observability/apps.py +31 -0
  192. trueppm_api/apps/observability/logging.py +213 -0
  193. trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
  194. trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
  195. trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
  196. trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
  197. trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
  198. trueppm_api/apps/observability/migrations/__init__.py +0 -0
  199. trueppm_api/apps/observability/models.py +141 -0
  200. trueppm_api/apps/observability/otel/__init__.py +45 -0
  201. trueppm_api/apps/observability/otel/attributes.py +247 -0
  202. trueppm_api/apps/observability/otel/export_health.py +533 -0
  203. trueppm_api/apps/observability/otel/instrumentation.py +351 -0
  204. trueppm_api/apps/observability/otel/metrics.py +566 -0
  205. trueppm_api/apps/observability/otel/provider.py +511 -0
  206. trueppm_api/apps/observability/otel/request_attributes.py +204 -0
  207. trueppm_api/apps/observability/purge_registry.py +124 -0
  208. trueppm_api/apps/observability/retention.py +139 -0
  209. trueppm_api/apps/observability/selectors.py +993 -0
  210. trueppm_api/apps/observability/serializers.py +142 -0
  211. trueppm_api/apps/observability/services.py +407 -0
  212. trueppm_api/apps/observability/tasks.py +266 -0
  213. trueppm_api/apps/observability/urls.py +27 -0
  214. trueppm_api/apps/observability/views.py +537 -0
  215. trueppm_api/apps/profiles/__init__.py +0 -0
  216. trueppm_api/apps/profiles/apps.py +15 -0
  217. trueppm_api/apps/profiles/constants.py +54 -0
  218. trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
  219. trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
  220. trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
  221. trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
  222. trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
  223. trueppm_api/apps/profiles/migrations/0005_squashed_0009_release_0_4.py +128 -0
  224. trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
  225. trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
  226. trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
  227. trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
  228. trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
  229. trueppm_api/apps/profiles/migrations/__init__.py +0 -0
  230. trueppm_api/apps/profiles/models.py +276 -0
  231. trueppm_api/apps/profiles/serializers.py +199 -0
  232. trueppm_api/apps/profiles/services.py +541 -0
  233. trueppm_api/apps/profiles/views.py +124 -0
  234. trueppm_api/apps/projects/__init__.py +0 -0
  235. trueppm_api/apps/projects/actual_date_rules.py +163 -0
  236. trueppm_api/apps/projects/amend.py +133 -0
  237. trueppm_api/apps/projects/apps.py +38 -0
  238. trueppm_api/apps/projects/asset_feed.py +459 -0
  239. trueppm_api/apps/projects/asset_views.py +317 -0
  240. trueppm_api/apps/projects/attachment_policy.py +278 -0
  241. trueppm_api/apps/projects/authentication.py +458 -0
  242. trueppm_api/apps/projects/backfill.py +459 -0
  243. trueppm_api/apps/projects/backlog_services.py +324 -0
  244. trueppm_api/apps/projects/backlog_views.py +302 -0
  245. trueppm_api/apps/projects/batch_operation_views.py +264 -0
  246. trueppm_api/apps/projects/blocker_services.py +510 -0
  247. trueppm_api/apps/projects/board_activity.py +462 -0
  248. trueppm_api/apps/projects/board_activity_views.py +186 -0
  249. trueppm_api/apps/projects/board_lanes.py +134 -0
  250. trueppm_api/apps/projects/bulk_settings.py +194 -0
  251. trueppm_api/apps/projects/bundled_templates.py +215 -0
  252. trueppm_api/apps/projects/calendar_settings.py +180 -0
  253. trueppm_api/apps/projects/ceremony_views.py +244 -0
  254. trueppm_api/apps/projects/commit_moment.py +212 -0
  255. trueppm_api/apps/projects/config_notice.py +1330 -0
  256. trueppm_api/apps/projects/custom_field_values.py +266 -0
  257. trueppm_api/apps/projects/decisions_services.py +132 -0
  258. trueppm_api/apps/projects/decisions_views.py +97 -0
  259. trueppm_api/apps/projects/estimation_scale.py +126 -0
  260. trueppm_api/apps/projects/export_bundle.py +423 -0
  261. trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
  262. trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
  263. trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
  264. trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
  265. trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -0
  266. trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
  267. trueppm_api/apps/projects/inbound_sync.py +555 -0
  268. trueppm_api/apps/projects/iteration_label.py +85 -0
  269. trueppm_api/apps/projects/key_views.py +468 -0
  270. trueppm_api/apps/projects/keys.py +205 -0
  271. trueppm_api/apps/projects/lifecycle.py +191 -0
  272. trueppm_api/apps/projects/management/__init__.py +0 -0
  273. trueppm_api/apps/projects/management/commands/__init__.py +0 -0
  274. trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
  275. trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
  276. trueppm_api/apps/projects/management/commands/export_program.py +50 -0
  277. trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
  278. trueppm_api/apps/projects/management/commands/load_sample_project.py +514 -0
  279. trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
  280. trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -0
  281. trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
  282. trueppm_api/apps/projects/mcp_settings.py +305 -0
  283. trueppm_api/apps/projects/methodology.py +178 -0
  284. trueppm_api/apps/projects/migration_helpers.py +75 -0
  285. trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
  286. trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -0
  287. trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
  288. trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
  289. trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
  290. trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
  291. trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
  292. trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
  293. trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
  294. trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
  295. trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
  296. trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
  297. trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
  298. trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
  299. trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
  300. trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
  301. trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
  302. trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
  303. trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
  304. trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
  305. trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
  306. trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
  307. trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
  308. trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
  309. trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
  310. trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
  311. trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
  312. trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
  313. trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
  314. trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
  315. trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
  316. trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
  317. trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
  318. trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
  319. trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
  320. trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
  321. trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
  322. trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
  323. trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
  324. trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
  325. trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
  326. trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
  327. trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
  328. trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
  329. trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
  330. trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
  331. trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
  332. trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
  333. trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
  334. trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
  335. trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
  336. trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
  337. trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
  338. trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
  339. trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
  340. trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
  341. trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
  342. trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
  343. trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
  344. trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
  345. trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
  346. trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
  347. trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
  348. trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
  349. trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
  350. trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
  351. trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
  352. trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
  353. trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
  354. trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
  355. trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
  356. trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
  357. trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
  358. trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
  359. trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
  360. trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
  361. trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
  362. trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
  363. trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
  364. trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
  365. trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
  366. trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
  367. trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
  368. trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
  369. trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
  370. trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
  371. trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
  372. trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
  373. trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
  374. trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
  375. trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
  376. trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
  377. trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
  378. trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
  379. trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +59 -0
  380. trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
  381. trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
  382. trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
  383. trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
  384. trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
  385. trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
  386. trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
  387. trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
  388. trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
  389. trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
  390. trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +75 -0
  391. trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
  392. trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
  393. trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
  394. trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
  395. trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
  396. trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +76 -0
  397. trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
  398. trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -0
  399. trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
  400. trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
  401. trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
  402. trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
  403. trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
  404. trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
  405. trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
  406. trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
  407. trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
  408. trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
  409. trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
  410. trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
  411. trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
  412. trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
  413. trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
  414. trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
  415. trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
  416. trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
  417. trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
  418. trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
  419. trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
  420. trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
  421. trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
  422. trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
  423. trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
  424. trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
  425. trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
  426. trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
  427. trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
  428. trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
  429. trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
  430. trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
  431. trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
  432. trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
  433. trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
  434. trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
  435. trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
  436. trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
  437. trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
  438. trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
  439. trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
  440. trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
  441. trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
  442. trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
  443. trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
  444. trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
  445. trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
  446. trueppm_api/apps/projects/migrations/0154_object_keys.py +184 -0
  447. trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
  448. trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
  449. trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
  450. trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
  451. trueppm_api/apps/projects/migrations/__init__.py +0 -0
  452. trueppm_api/apps/projects/models.py +9209 -0
  453. trueppm_api/apps/projects/poker_services.py +154 -0
  454. trueppm_api/apps/projects/poker_views.py +398 -0
  455. trueppm_api/apps/projects/product_backlog_services.py +450 -0
  456. trueppm_api/apps/projects/program_label_services.py +94 -0
  457. trueppm_api/apps/projects/program_label_views.py +189 -0
  458. trueppm_api/apps/projects/program_rollup.py +903 -0
  459. trueppm_api/apps/projects/program_schedule.py +767 -0
  460. trueppm_api/apps/projects/program_views.py +3445 -0
  461. trueppm_api/apps/projects/project_templates.py +422 -0
  462. trueppm_api/apps/projects/receivers.py +234 -0
  463. trueppm_api/apps/projects/refusal_codes.py +140 -0
  464. trueppm_api/apps/projects/reorder_services.py +145 -0
  465. trueppm_api/apps/projects/restructure_hooks.py +54 -0
  466. trueppm_api/apps/projects/retro_board_services.py +368 -0
  467. trueppm_api/apps/projects/retro_services.py +285 -0
  468. trueppm_api/apps/projects/risk_import.py +396 -0
  469. trueppm_api/apps/projects/row_identity.py +113 -0
  470. trueppm_api/apps/projects/schema_migrations.py +228 -0
  471. trueppm_api/apps/projects/schemas/seed_v1.json +492 -0
  472. trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
  473. trueppm_api/apps/projects/seed/__init__.py +61 -0
  474. trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
  475. trueppm_api/apps/projects/seed/exporter.py +1717 -0
  476. trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
  477. trueppm_api/apps/projects/seed/importer.py +2764 -0
  478. trueppm_api/apps/projects/seed/reanchor.py +551 -0
  479. trueppm_api/apps/projects/seed/reldates.py +126 -0
  480. trueppm_api/apps/projects/seed/replace.py +128 -0
  481. trueppm_api/apps/projects/seed/replay.py +1695 -0
  482. trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
  483. trueppm_api/apps/projects/seed/samples.py +427 -0
  484. trueppm_api/apps/projects/seed/validation.py +1169 -0
  485. trueppm_api/apps/projects/serializers.py +12904 -0
  486. trueppm_api/apps/projects/services.py +7525 -0
  487. trueppm_api/apps/projects/share_serializers.py +142 -0
  488. trueppm_api/apps/projects/share_services.py +373 -0
  489. trueppm_api/apps/projects/share_views.py +395 -0
  490. trueppm_api/apps/projects/sharing_settings.py +187 -0
  491. trueppm_api/apps/projects/signal_privacy_services.py +910 -0
  492. trueppm_api/apps/projects/signal_privacy_views.py +485 -0
  493. trueppm_api/apps/projects/signals.py +132 -0
  494. trueppm_api/apps/projects/slip_conflict.py +288 -0
  495. trueppm_api/apps/projects/sprint_cadence.py +453 -0
  496. trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
  497. trueppm_api/apps/projects/standup.py +304 -0
  498. trueppm_api/apps/projects/standup_views.py +99 -0
  499. trueppm_api/apps/projects/structural_operation_services.py +727 -0
  500. trueppm_api/apps/projects/structural_operation_views.py +311 -0
  501. trueppm_api/apps/projects/surface_visibility.py +126 -0
  502. trueppm_api/apps/projects/task_batch_services.py +379 -0
  503. trueppm_api/apps/projects/task_bulk.py +1244 -0
  504. trueppm_api/apps/projects/task_classification.py +561 -0
  505. trueppm_api/apps/projects/task_classification_defaults.py +95 -0
  506. trueppm_api/apps/projects/task_duration_settings.py +142 -0
  507. trueppm_api/apps/projects/task_grouping.py +754 -0
  508. trueppm_api/apps/projects/tasks.py +1979 -0
  509. trueppm_api/apps/projects/template_divergence.py +168 -0
  510. trueppm_api/apps/projects/template_services.py +160 -0
  511. trueppm_api/apps/projects/template_tasks.py +261 -0
  512. trueppm_api/apps/projects/template_views.py +829 -0
  513. trueppm_api/apps/projects/throttles.py +311 -0
  514. trueppm_api/apps/projects/urls.py +1016 -0
  515. trueppm_api/apps/projects/utilization.py +909 -0
  516. trueppm_api/apps/projects/views.py +21225 -0
  517. trueppm_api/apps/projects/wbs_paths.py +119 -0
  518. trueppm_api/apps/resources/__init__.py +0 -0
  519. trueppm_api/apps/resources/apps.py +8 -0
  520. trueppm_api/apps/resources/capacity.py +121 -0
  521. trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
  522. trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
  523. trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
  524. trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
  525. trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
  526. trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
  527. trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
  528. trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
  529. trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
  530. trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
  531. trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
  532. trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
  533. trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
  534. trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
  535. trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
  536. trueppm_api/apps/resources/migrations/0014_squashed_0015_release_0_4.py +47 -0
  537. trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
  538. trueppm_api/apps/resources/migrations/__init__.py +0 -0
  539. trueppm_api/apps/resources/models.py +298 -0
  540. trueppm_api/apps/resources/serializers.py +426 -0
  541. trueppm_api/apps/resources/services.py +465 -0
  542. trueppm_api/apps/resources/urls.py +28 -0
  543. trueppm_api/apps/resources/views.py +1752 -0
  544. trueppm_api/apps/scheduling/__init__.py +0 -0
  545. trueppm_api/apps/scheduling/apps.py +153 -0
  546. trueppm_api/apps/scheduling/calendars.py +144 -0
  547. trueppm_api/apps/scheduling/deadletter.py +91 -0
  548. trueppm_api/apps/scheduling/finish_reading.py +198 -0
  549. trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
  550. trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
  551. trueppm_api/apps/scheduling/graph_guard.py +263 -0
  552. trueppm_api/apps/scheduling/management/__init__.py +0 -0
  553. trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
  554. trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
  555. trueppm_api/apps/scheduling/mc_admission.py +181 -0
  556. trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
  557. trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
  558. trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
  559. trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
  560. trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
  561. trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
  562. trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
  563. trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
  564. trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
  565. trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
  566. trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
  567. trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
  568. trueppm_api/apps/scheduling/migrations/0010_squashed_0016_release_0_4.py +106 -0
  569. trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
  570. trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
  571. trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
  572. trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
  573. trueppm_api/apps/scheduling/migrations/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
  574. trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
  575. trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
  576. trueppm_api/apps/scheduling/models.py +542 -0
  577. trueppm_api/apps/scheduling/receivers.py +160 -0
  578. trueppm_api/apps/scheduling/risk_premium.py +334 -0
  579. trueppm_api/apps/scheduling/serializers.py +723 -0
  580. trueppm_api/apps/scheduling/services.py +1251 -0
  581. trueppm_api/apps/scheduling/signals.py +56 -0
  582. trueppm_api/apps/scheduling/tasks.py +2248 -0
  583. trueppm_api/apps/scheduling/telemetry.py +80 -0
  584. trueppm_api/apps/scheduling/units.py +35 -0
  585. trueppm_api/apps/scheduling/urls.py +57 -0
  586. trueppm_api/apps/scheduling/views.py +2908 -0
  587. trueppm_api/apps/sso/__init__.py +0 -0
  588. trueppm_api/apps/sso/apps.py +19 -0
  589. trueppm_api/apps/sso/extensions.py +130 -0
  590. trueppm_api/apps/sso/management/__init__.py +0 -0
  591. trueppm_api/apps/sso/management/commands/__init__.py +0 -0
  592. trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
  593. trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
  594. trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
  595. trueppm_api/apps/sso/migrations/__init__.py +0 -0
  596. trueppm_api/apps/sso/models.py +148 -0
  597. trueppm_api/apps/sso/serializers.py +446 -0
  598. trueppm_api/apps/sso/services.py +1473 -0
  599. trueppm_api/apps/sso/urls.py +47 -0
  600. trueppm_api/apps/sso/views.py +935 -0
  601. trueppm_api/apps/sync/__init__.py +0 -0
  602. trueppm_api/apps/sync/apps.py +15 -0
  603. trueppm_api/apps/sync/backfill.py +208 -0
  604. trueppm_api/apps/sync/broadcast.py +369 -0
  605. trueppm_api/apps/sync/conflict.py +262 -0
  606. trueppm_api/apps/sync/consumers.py +548 -0
  607. trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
  608. trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
  609. trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
  610. trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
  611. trueppm_api/apps/sync/migrations/0003_squashed_0005_release_0_4.py +77 -0
  612. trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
  613. trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
  614. trueppm_api/apps/sync/migrations/0006_boardevent_per_project_seq.py +77 -0
  615. trueppm_api/apps/sync/migrations/__init__.py +0 -0
  616. trueppm_api/apps/sync/models.py +272 -0
  617. trueppm_api/apps/sync/pagination.py +231 -0
  618. trueppm_api/apps/sync/sequence.py +488 -0
  619. trueppm_api/apps/sync/serializers.py +688 -0
  620. trueppm_api/apps/sync/tasks.py +260 -0
  621. trueppm_api/apps/sync/throttles.py +175 -0
  622. trueppm_api/apps/sync/upload.py +556 -0
  623. trueppm_api/apps/sync/urls.py +17 -0
  624. trueppm_api/apps/sync/views.py +957 -0
  625. trueppm_api/apps/sync/ws_auth.py +193 -0
  626. trueppm_api/apps/taskruns/__init__.py +0 -0
  627. trueppm_api/apps/taskruns/apps.py +11 -0
  628. trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
  629. trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
  630. trueppm_api/apps/taskruns/models.py +68 -0
  631. trueppm_api/apps/taskruns/serializers.py +92 -0
  632. trueppm_api/apps/taskruns/tasks.py +60 -0
  633. trueppm_api/apps/taskruns/tracker.py +286 -0
  634. trueppm_api/apps/taskruns/urls.py +46 -0
  635. trueppm_api/apps/taskruns/views.py +195 -0
  636. trueppm_api/apps/teams/__init__.py +8 -0
  637. trueppm_api/apps/teams/apps.py +16 -0
  638. trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
  639. trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +159 -0
  640. trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
  641. trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
  642. trueppm_api/apps/teams/migrations/0004_backfill_default_teams_on_upgrade.py +49 -0
  643. trueppm_api/apps/teams/migrations/__init__.py +0 -0
  644. trueppm_api/apps/teams/models.py +145 -0
  645. trueppm_api/apps/teams/permissions.py +149 -0
  646. trueppm_api/apps/teams/serializers.py +95 -0
  647. trueppm_api/apps/teams/services.py +440 -0
  648. trueppm_api/apps/teams/signals.py +61 -0
  649. trueppm_api/apps/teams/urls.py +34 -0
  650. trueppm_api/apps/teams/views.py +201 -0
  651. trueppm_api/apps/timetracking/__init__.py +0 -0
  652. trueppm_api/apps/timetracking/apps.py +9 -0
  653. trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
  654. trueppm_api/apps/timetracking/migrations/0001_squashed_0004_release_0_4.py +167 -0
  655. trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
  656. trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
  657. trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
  658. trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
  659. trueppm_api/apps/timetracking/models.py +178 -0
  660. trueppm_api/apps/timetracking/serializers.py +175 -0
  661. trueppm_api/apps/timetracking/services.py +211 -0
  662. trueppm_api/apps/timetracking/urls.py +47 -0
  663. trueppm_api/apps/timetracking/views.py +597 -0
  664. trueppm_api/apps/webhooks/__init__.py +0 -0
  665. trueppm_api/apps/webhooks/apps.py +8 -0
  666. trueppm_api/apps/webhooks/backfill.py +90 -0
  667. trueppm_api/apps/webhooks/dispatch.py +279 -0
  668. trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
  669. trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -0
  670. trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
  671. trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
  672. trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
  673. trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
  674. trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
  675. trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
  676. trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
  677. trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +100 -0
  678. trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
  679. trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
  680. trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
  681. trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
  682. trueppm_api/apps/webhooks/models.py +437 -0
  683. trueppm_api/apps/webhooks/serializers.py +326 -0
  684. trueppm_api/apps/webhooks/tasks.py +680 -0
  685. trueppm_api/apps/webhooks/urls.py +22 -0
  686. trueppm_api/apps/webhooks/views.py +533 -0
  687. trueppm_api/apps/workflow_engine/__init__.py +0 -0
  688. trueppm_api/apps/workflow_engine/apps.py +23 -0
  689. trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
  690. trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
  691. trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
  692. trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
  693. trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
  694. trueppm_api/apps/workflow_engine/models.py +265 -0
  695. trueppm_api/apps/workflow_engine/tasks.py +371 -0
  696. trueppm_api/apps/workspace/__init__.py +0 -0
  697. trueppm_api/apps/workspace/apps.py +10 -0
  698. trueppm_api/apps/workspace/backfill.py +111 -0
  699. trueppm_api/apps/workspace/export.py +254 -0
  700. trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
  701. trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
  702. trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
  703. trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
  704. trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
  705. trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
  706. trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
  707. trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
  708. trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
  709. trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
  710. trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
  711. trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
  712. trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
  713. trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
  714. trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
  715. trueppm_api/apps/workspace/migrations/0015_squashed_0027_release_0_4.py +199 -0
  716. trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
  717. trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
  718. trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
  719. trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
  720. trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
  721. trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
  722. trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
  723. trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
  724. trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
  725. trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
  726. trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
  727. trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
  728. trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
  729. trueppm_api/apps/workspace/migrations/__init__.py +0 -0
  730. trueppm_api/apps/workspace/models.py +857 -0
  731. trueppm_api/apps/workspace/permissions.py +187 -0
  732. trueppm_api/apps/workspace/serializers.py +590 -0
  733. trueppm_api/apps/workspace/services.py +967 -0
  734. trueppm_api/apps/workspace/signals.py +37 -0
  735. trueppm_api/apps/workspace/tasks.py +616 -0
  736. trueppm_api/apps/workspace/urls.py +113 -0
  737. trueppm_api/apps/workspace/views.py +1581 -0
  738. trueppm_api/asgi.py +49 -0
  739. trueppm_api/celery.py +40 -0
  740. trueppm_api/core/__init__.py +0 -0
  741. trueppm_api/core/admin_site.py +206 -0
  742. trueppm_api/core/auth_views.py +934 -0
  743. trueppm_api/core/constant_time.py +51 -0
  744. trueppm_api/core/csp.py +54 -0
  745. trueppm_api/core/db.py +87 -0
  746. trueppm_api/core/db_session.py +37 -0
  747. trueppm_api/core/demo_read_only.py +314 -0
  748. trueppm_api/core/exception_handlers.py +157 -0
  749. trueppm_api/core/export_downloads.py +56 -0
  750. trueppm_api/core/extension_providers.py +56 -0
  751. trueppm_api/core/extension_signals.py +147 -0
  752. trueppm_api/core/idempotent.py +260 -0
  753. trueppm_api/core/metadata.py +42 -0
  754. trueppm_api/core/middleware.py +51 -0
  755. trueppm_api/core/openapi.py +1221 -0
  756. trueppm_api/core/password_reset.py +513 -0
  757. trueppm_api/core/protect_conflict.py +100 -0
  758. trueppm_api/core/ratelimit.py +148 -0
  759. trueppm_api/core/redis_throttle.py +82 -0
  760. trueppm_api/core/refresh_cookie_policy.py +156 -0
  761. trueppm_api/core/request_body.py +86 -0
  762. trueppm_api/core/security_checks.py +1034 -0
  763. trueppm_api/core/storage_config.py +105 -0
  764. trueppm_api/core/text_decode.py +168 -0
  765. trueppm_api/core/throttling.py +514 -0
  766. trueppm_api/core/valkey.py +214 -0
  767. trueppm_api/core/valkey_cache.py +66 -0
  768. trueppm_api/core/valkey_checks.py +104 -0
  769. trueppm_api/core/valkey_config.py +39 -0
  770. trueppm_api/core/worker_broker_wait.py +149 -0
  771. trueppm_api/core/worker_heartbeat.py +134 -0
  772. trueppm_api/core/worker_selfheal.py +205 -0
  773. trueppm_api/fields.py +36 -0
  774. trueppm_api/permissions.py +30 -0
  775. trueppm_api/py.typed +0 -0
  776. trueppm_api/routing.py +11 -0
  777. trueppm_api/settings/__init__.py +0 -0
  778. trueppm_api/settings/base.py +2605 -0
  779. trueppm_api/settings/dev.py +296 -0
  780. trueppm_api/settings/prod.py +265 -0
  781. trueppm_api/urls.py +217 -0
  782. trueppm_api/workflows/__init__.py +38 -0
  783. trueppm_api/workflows/backends/__init__.py +6 -0
  784. trueppm_api/workflows/backends/default.py +297 -0
  785. trueppm_api/workflows/consumers/__init__.py +11 -0
  786. trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
  787. trueppm_api/workflows/interface.py +148 -0
  788. trueppm_api/workflows/registry.py +106 -0
  789. trueppm_api/workflows/services.py +90 -0
  790. trueppm_api/wsgi.py +11 -0
  791. trueppm_api-0.4.0.dist-info/METADATA +273 -0
  792. trueppm_api-0.4.0.dist-info/RECORD +799 -0
  793. trueppm_api-0.4.0.dist-info/WHEEL +4 -0
  794. trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
  795. trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
  796. trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
  797. trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
  798. trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
  799. trueppm_api-0.4.0.dist-info/licenses/licenses/MPL-2.0.txt +373 -0
@@ -0,0 +1,1221 @@
1
+ """OpenAPI / drf-spectacular customizations for SDK-quality schema output (#1333).
2
+
3
+ Generated clients (openapi-generator, Orval, openapi-typescript) key their method
4
+ names, doc comments, and API-class grouping off three schema facets that
5
+ drf-spectacular leaves thin by default:
6
+
7
+ * ``summary`` — the human label a client renders as the method doc comment. Without
8
+ it, tools fall back to the raw ``operationId`` (``v1_calendars_exceptions_create``),
9
+ which produces unreadable SDK docs.
10
+ * ``tags`` — the grouping key. drf-spectacular tags every ``/api/v1/`` operation
11
+ ``"v1"`` by default, so a generated client collapses the whole API into a single
12
+ ``V1Api`` class. Meaningful tags split it into ``ProjectsApi``, ``SprintsApi``,
13
+ ``SchedulingApi``, … the way an SDK consumer expects.
14
+ * ``security`` — a global default plus explicit ``security: []`` on the public
15
+ endpoints, so a client knows which calls need a credential and which do not.
16
+
17
+ Rather than hand-annotate ~300 view methods (error-prone, and every *new* endpoint
18
+ would silently regress coverage), the mechanical facets are filled by a single
19
+ post-processing hook that derives them from the request path — so coverage is
20
+ complete today and stays complete as endpoints are added. A view can still override
21
+ any facet with an explicit ``@extend_schema(...)``; the hook only fills what is
22
+ missing (summary) or still default (the ``"v1"`` tag).
23
+
24
+ The ``429`` documentation is injected by a custom :class:`AutoSchema` subclass
25
+ because throttling is a *view* attribute (``throttle_classes``) that is not visible
26
+ in the final schema dict — only the schema generator, which holds the view, can see
27
+ it.
28
+
29
+ The same subclass narrows ``security`` per-method on MCP-readable views (#2659):
30
+ ``McpReadableViewMixin`` adds ``projectApiTokenAuth`` at the view level, but the
31
+ project/program-scoped credentials unique to that scheme are refused on the whole
32
+ MCP surface, so the unsafe methods must not advertise it. A ``legacy:full``
33
+ personal token *can* write there since #2877 and is described by
34
+ ``personalApiTokenAuth``, which those operations already carry.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ from copy import deepcopy
40
+ from typing import Any, cast
41
+
42
+ from drf_spectacular.openapi import AutoSchema
43
+ from rest_framework.permissions import SAFE_METHODS
44
+
45
+ from trueppm_api.core.demo_read_only import DEMO_READ_ONLY_CODE
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Tag definitions
49
+ # ---------------------------------------------------------------------------
50
+
51
+ # The top-level ``tags`` block (name + description for each tag below) is declared
52
+ # in ``SPECTACULAR_SETTINGS["TAGS"]`` in settings/base.py — it is defined there
53
+ # rather than imported from here because Django settings must not import
54
+ # DRF-dependent app code. The two must stay in lockstep: every tag this module can
55
+ # assign is described in that block.
56
+
57
+ # Top-level path segment -> tag. Anything not listed falls through to the segment
58
+ # itself (which will already be a sensible resource name).
59
+ _TOP_TAG: dict[str, str] = {
60
+ "acceptance-criteria": "tasks",
61
+ "admin": "meta",
62
+ "auth": "auth",
63
+ "calendars": "calendars",
64
+ "cascade-classification-operations": "projects",
65
+ "dependencies": "tasks",
66
+ "edition": "meta",
67
+ "health": "meta",
68
+ "import-templates": "import-export",
69
+ "readyz": "meta",
70
+ "integrations": "integrations",
71
+ # ADR-1237: project/program key lookups, folded under `projects` rather than
72
+ # minting a top-level tag for two small read-only endpoints.
73
+ "keys": "projects",
74
+ "me": "me",
75
+ "paste-many-operations": "projects",
76
+ "poker": "sprints",
77
+ "programs": "programs",
78
+ "project-resources": "resources",
79
+ "project-templates": "projects",
80
+ "projects": "projects",
81
+ "recurrence-rules": "tasks",
82
+ "resource-skills": "resources",
83
+ "resolve": "projects",
84
+ "resources": "resources",
85
+ "retro-items": "sprints",
86
+ "scope-changes": "sprints",
87
+ "share": "share",
88
+ "skills": "resources",
89
+ "slip-conflicts": "scheduling",
90
+ "sprint-task-outcomes": "sprints",
91
+ "sprints": "sprints",
92
+ # ADR-0880 (#2974): the structural undo ledger, folded under `projects` with its
93
+ # ADR-0810 siblings (`paste-many-operations`, `cascade-classification-operations`)
94
+ # rather than minting a top-level tag for one read-only ledger.
95
+ "structural-operations": "projects",
96
+ "task-resources": "resources",
97
+ "task-runs": "tasks",
98
+ "task-skill-requirements": "resources",
99
+ "tasks": "tasks",
100
+ "teams": "teams",
101
+ "template-applications": "projects",
102
+ "users": "workspace",
103
+ "velocity-suggestions": "sprints",
104
+ "workspace": "workspace",
105
+ "ws": "sync",
106
+ }
107
+
108
+ # Second path segment under /projects/ or /programs/ -> tag, so nested resources
109
+ # land in their own SDK class instead of the umbrella Projects/Programs class.
110
+ _SUB_TAG: dict[str, str] = {
111
+ "tasks": "tasks",
112
+ "task": "tasks",
113
+ "milestones": "tasks",
114
+ "acceptance-results": "tasks",
115
+ "acceptance-criteria": "tasks",
116
+ "baseline": "scheduling",
117
+ "baselines": "scheduling",
118
+ "schedule": "scheduling",
119
+ "scheduler": "scheduling",
120
+ "monte-carlo": "scheduling",
121
+ "forecast": "scheduling",
122
+ "risk": "scheduling",
123
+ "risks": "scheduling",
124
+ "slip-conflicts": "scheduling",
125
+ "critical-path": "scheduling",
126
+ "sprints": "sprints",
127
+ "sprint": "sprints",
128
+ "sprint-health": "sprints",
129
+ "velocity": "sprints",
130
+ "retrospective": "sprints",
131
+ "standup": "sprints",
132
+ "burn": "sprints",
133
+ "board": "sprints",
134
+ "product-backlog": "sprints",
135
+ "queue": "sprints",
136
+ "ceremonies": "sprints",
137
+ "backlog": "sprints",
138
+ "resources": "resources",
139
+ "resource-allocation": "resources",
140
+ "resource-contention": "resources",
141
+ "utilization": "resources",
142
+ "members": "members",
143
+ "teams": "teams",
144
+ "webhooks": "webhooks",
145
+ "integrations": "integrations",
146
+ "integrations-summary": "integrations",
147
+ "import": "import-export",
148
+ "imports": "import-export",
149
+ "export": "import-export",
150
+ "share-links": "share",
151
+ }
152
+
153
+ # ---------------------------------------------------------------------------
154
+ # Security
155
+ # ---------------------------------------------------------------------------
156
+
157
+ # The default requirement advertised at the document root: a caller authenticates
158
+ # with either a bearer JWT or the session cookie. Operations that accept a project
159
+ # API token additionally list ``projectApiTokenAuth`` on their own operation-level
160
+ # ``security`` (emitted by drf-spectacular from the view's authentication classes).
161
+ GLOBAL_SECURITY: list[dict[str, list[str]]] = [{"jwtAuth": []}, {"cookieAuth": []}]
162
+
163
+ # Endpoints that require no credential at all. They must carry an explicit
164
+ # ``security: []`` so a generated client does not attach a (non-existent) token.
165
+ PUBLIC_PATHS: frozenset[str] = frozenset(
166
+ {
167
+ "/api/v1/auth/logout/",
168
+ "/api/v1/auth/oidc/callback/",
169
+ "/api/v1/auth/oidc/discover/",
170
+ "/api/v1/auth/oidc/login/",
171
+ "/api/v1/auth/token/",
172
+ "/api/v1/auth/token/refresh/",
173
+ "/api/v1/edition/",
174
+ "/api/v1/health/",
175
+ "/api/v1/readyz",
176
+ "/api/v1/integrations/projects/{project_pk}/git-webhook/",
177
+ "/api/v1/workspace/invites/accept/",
178
+ }
179
+ )
180
+
181
+ # Paths where only some methods are public. The workspace logo is served
182
+ # anonymously (GET) but uploaded and cleared by an authenticated admin, so
183
+ # marking the whole path public advertised the writes as unauthenticated.
184
+ PUBLIC_METHODS_BY_PATH: dict[str, frozenset[str]] = {
185
+ "/api/v1/workspace/logo/": frozenset({"get"}),
186
+ }
187
+
188
+ _HTTP_METHODS = ("get", "post", "put", "patch", "delete", "head", "options", "trace")
189
+
190
+ # Attribute stamped on a viewset ``@action`` handler to opt its list response out
191
+ # of drf-spectacular's automatic pagination-envelope wrapping (see
192
+ # :func:`suppress_list_pagination` and :meth:`TruePPMAutoSchema._get_paginator`).
193
+ _SUPPRESS_PAGINATION_ATTR = "spectacular_suppress_pagination"
194
+ _JSON_MEDIA_TYPE = "application/json"
195
+
196
+
197
+ def suppress_list_pagination(view_func: Any) -> Any:
198
+ """Mark a viewset ``@action``'s list response as NON-paginated for the schema.
199
+
200
+ drf-spectacular auto-wraps any ``many=True`` response of a view whose
201
+ ``pagination_class`` is set in that paginator's envelope
202
+ (``Paginated…List``). Actions that deliberately return a **bare array** — a
203
+ small, capped, or fixed-size list the client consumes directly — must opt out
204
+ so the generated schema matches the real response body; otherwise the
205
+ committed schema claims a paginated object and every such response fails
206
+ ``response_schema_conformance`` (#2127). Pairs with
207
+ :meth:`TruePPMAutoSchema._get_paginator`.
208
+ """
209
+ setattr(view_func, _SUPPRESS_PAGINATION_ATTR, True)
210
+ return view_func
211
+
212
+
213
+ _IRREGULAR_SINGULAR = {
214
+ "criteria": "criterion",
215
+ "analyses": "analysis",
216
+ "indices": "index",
217
+ "people": "person",
218
+ }
219
+
220
+ # Verbs used for standard CRUD-style detail operations, keyed by HTTP method.
221
+ _DETAIL_VERB = {
222
+ "get": "Retrieve",
223
+ "put": "Update",
224
+ "patch": "Partially update",
225
+ "delete": "Delete",
226
+ "post": "Update",
227
+ }
228
+
229
+
230
+ def _humanize(segment: str) -> str:
231
+ """``acceptance-results`` -> ``acceptance results`` (lower, space-separated)."""
232
+ return segment.replace("-", " ").replace("_", " ").strip().lower()
233
+
234
+
235
+ def _singularize(phrase: str) -> str:
236
+ """Singularize the last word of a humanized phrase (naive, with irregulars)."""
237
+ if not phrase:
238
+ return phrase
239
+ words = phrase.split()
240
+ last = words[-1]
241
+ if last in _IRREGULAR_SINGULAR:
242
+ singular = _IRREGULAR_SINGULAR[last]
243
+ elif last.endswith("ies") and len(last) > 3:
244
+ singular = last[:-3] + "y"
245
+ elif last.endswith(("ses", "xes", "ches", "shes")):
246
+ singular = last[:-2]
247
+ elif last.endswith("s") and not last.endswith("ss"):
248
+ singular = last[:-1]
249
+ else:
250
+ singular = last
251
+ words[-1] = singular
252
+ return " ".join(words)
253
+
254
+
255
+ def _sentence_case(text: str) -> str:
256
+ return text[:1].upper() + text[1:] if text else text
257
+
258
+
259
+ def _path_segments(path: str) -> list[str]:
260
+ """Path parts with the ``api``/``v1`` prefix stripped."""
261
+ return [p for p in path.split("/") if p and p not in ("api", "v1")]
262
+
263
+
264
+ def _derive_tag(path: str) -> str:
265
+ segments = _path_segments(path)
266
+ literals = [p for p in segments if not (p.startswith("{") and p.endswith("}"))]
267
+ if not literals:
268
+ return "meta"
269
+ top = literals[0]
270
+ if top in ("projects", "programs") and len(literals) > 1:
271
+ return _SUB_TAG.get(literals[1], top)
272
+ return _TOP_TAG.get(top, top)
273
+
274
+
275
+ def _derive_summary(path: str, method: str, collection_paths: frozenset[str]) -> str:
276
+ """Build a human ``summary`` from the request path and HTTP method.
277
+
278
+ The path structure disambiguates the three shapes an SDK cares about:
279
+ collection (``List``/``Create``), detail-by-id (``Retrieve``/``Update``/…) and
280
+ custom action (``Accept``/``Close``/…). A trailing literal is a *collection*
281
+ when a child ``.../{id}/`` path exists, otherwise it is treated as an action
282
+ (POST) or a singleton sub-resource (GET/PUT/PATCH/DELETE).
283
+ """
284
+ method = method.lower()
285
+ segments = _path_segments(path)
286
+ if not segments:
287
+ return _sentence_case(method)
288
+
289
+ last = segments[-1]
290
+
291
+ # Detail-by-id: the resource is the collection immediately above the id.
292
+ if last.startswith("{") and last.endswith("}"):
293
+ return _detail_summary(segments, method)
294
+
295
+ # Trailing literal that owns a ``.../{id}/`` route -> a collection.
296
+ normalized = path if path.endswith("/") else path + "/"
297
+ if normalized in collection_paths:
298
+ return _collection_summary(last, method)
299
+
300
+ # Trailing literal with no child id route: a custom action (POST) or a
301
+ # singleton sub-resource (safe/idempotent methods).
302
+ return _action_summary(segments, method)
303
+
304
+
305
+ def _detail_summary(segments: list[str], method: str) -> str:
306
+ """Summary for a detail-by-id path: ``<verb> <singular resource>``."""
307
+ parent = segments[-2] if len(segments) > 1 else segments[-1]
308
+ resource = _singularize(_humanize(parent))
309
+ return f"{_DETAIL_VERB.get(method, 'Retrieve')} {resource}"
310
+
311
+
312
+ def _collection_summary(last: str, method: str) -> str:
313
+ """Summary for a collection path: ``List``/``Create``/``<verb>`` the resource."""
314
+ human = _humanize(last)
315
+ if method == "get":
316
+ return f"List {human}"
317
+ if method == "post":
318
+ return f"Create {_singularize(human)}"
319
+ return f"{_DETAIL_VERB.get(method, 'Update')} {_singularize(human)}"
320
+
321
+
322
+ def _action_summary(segments: list[str], method: str) -> str:
323
+ """Summary for a custom action (POST) or singleton sub-resource (safe methods)."""
324
+ last = segments[-1]
325
+ preceding_literals = [p for p in segments[:-1] if not (p.startswith("{") and p.endswith("}"))]
326
+ owner_segment = preceding_literals[-1] if preceding_literals else last
327
+ has_parent_id = any(p.startswith("{") and p.endswith("}") for p in segments[:-1])
328
+
329
+ if method == "post":
330
+ verb = _sentence_case(_humanize(last))
331
+ owner_human = _humanize(owner_segment)
332
+ owner = _singularize(owner_human) if has_parent_id else owner_human
333
+ if owner_segment == last:
334
+ return verb
335
+ return f"{verb} {owner}"
336
+
337
+ return f"{_DETAIL_VERB.get(method, 'Retrieve')} {_singularize(_humanize(last))}"
338
+
339
+
340
+ def _collection_paths(paths: dict[str, Any]) -> frozenset[str]:
341
+ """Paths that own a child ``.../{param}/`` route (i.e. real collections)."""
342
+ collections: set[str] = set()
343
+ for path in paths:
344
+ segments = path.rstrip("/").split("/")
345
+ if not segments:
346
+ continue
347
+ tail = segments[-1]
348
+ if tail.startswith("{") and tail.endswith("}"):
349
+ parent = "/".join(segments[:-1]) + "/"
350
+ collections.add(parent)
351
+ return frozenset(collections)
352
+
353
+
354
+ def postprocess_openapi(
355
+ result: dict[str, Any],
356
+ generator: Any,
357
+ request: Any,
358
+ public: bool,
359
+ **kwargs: Any,
360
+ ) -> dict[str, Any]:
361
+ """Fill SDK-quality facets the per-view annotations leave thin (#1333).
362
+
363
+ * add a derived ``summary`` to every operation that lacks one;
364
+ * replace the default ``"v1"`` tag with a meaningful resource tag;
365
+ * set a document-level ``security`` default and mark public paths ``security: []``.
366
+ * rewrite every bare ``format: "time"`` to a pattern (see ``_rewrite_time_format``).
367
+ * publish the role ordinal as a banded integer, not a closed enum
368
+ (see ``_publish_role_ordinal``, ADR-1291);
369
+ * mark every enum open or exhaustive (see ``_mark_enum_openness``, ADR-1291).
370
+
371
+ Explicit ``@extend_schema`` values are never overwritten — a non-default tag is
372
+ kept, and an existing summary is left untouched.
373
+ """
374
+ paths = result.get("paths", {})
375
+ collection_paths = _collection_paths(paths)
376
+
377
+ for path, operations in paths.items():
378
+ for method, operation in operations.items():
379
+ if method not in _HTTP_METHODS or not isinstance(operation, dict):
380
+ continue
381
+ is_public = path in PUBLIC_PATHS or method in PUBLIC_METHODS_BY_PATH.get(
382
+ path, frozenset()
383
+ )
384
+ _annotate_operation(operation, path, method, collection_paths, is_public)
385
+
386
+ _register_validation_error_detail(result)
387
+ _register_demo_read_only_error(result)
388
+ _rewrite_time_format(result)
389
+ # Order matters: the role ordinal stops being an enum before the openness pass
390
+ # runs, so it is never marked as one.
391
+ _publish_role_ordinal(result)
392
+ _mark_enum_openness(result)
393
+
394
+ # Document-level default so a client knows the baseline auth scheme.
395
+ result["security"] = list(GLOBAL_SECURITY)
396
+ return result
397
+
398
+
399
+ # RFC 3339 `full-time` (what JSON Schema's `format: "time"` validates against)
400
+ # requires a trailing offset (`09:30:00Z` / `09:30:00+00:00`). Every DRF
401
+ # ``TimeField`` in this API represents a local wall-clock time-of-day (a daily
402
+ # notification slot, a retention run time) rather than a UTC instant, and is
403
+ # serialized as bare ``HH:MM:SS`` with no offset — so drf-spectacular's default
404
+ # ``format: "time"`` mapping (``OpenApiTypes.TIME``) makes a claim the response
405
+ # never satisfies. There is no accurate JSON Schema ``format`` for "local time,
406
+ # no offset"; a pattern matching the real serialization is correct instead
407
+ # (schemathesis job #16501510109 / #3811).
408
+ _TIME_OF_DAY_PATTERN = r"^\d{2}:\d{2}:\d{2}(\.\d{1,6})?$"
409
+
410
+
411
+ def _rewrite_time_format(node: Any) -> None:
412
+ """Replace every ``{"type": "string", "format": "time"}`` schema node in place.
413
+
414
+ Walks the whole document (not just ``components.schemas``) because the same
415
+ property can also appear inline in request/response bodies and parameters.
416
+ """
417
+ if isinstance(node, dict):
418
+ if node.get("type") == "string" and node.get("format") == "time":
419
+ del node["format"]
420
+ node["pattern"] = _TIME_OF_DAY_PATTERN
421
+ for value in node.values():
422
+ _rewrite_time_format(value)
423
+ elif isinstance(node, list):
424
+ for item in node:
425
+ _rewrite_time_format(item)
426
+
427
+
428
+ # ---------------------------------------------------------------------------
429
+ # Enum openness (ADR-1291, #4593)
430
+ # ---------------------------------------------------------------------------
431
+
432
+ #: Vendor extension carried by every ``enum`` node in the published document.
433
+ #: Namespaced rather than reusing Zalando's ``x-extensible-enum`` on purpose: that
434
+ #: convention takes an *array* of values and *replaces* ``enum``, so a tool that
435
+ #: knows it would misread a string flag sitting beside ``enum``. See ADR-1291 for
436
+ #: why ``enum`` is kept instead of replaced.
437
+ ENUM_OPENNESS_KEY = "x-trueppm-enum"
438
+ ENUM_OPEN = "open"
439
+ ENUM_EXHAUSTIVE = "exhaustive"
440
+
441
+ #: The enum components that are genuinely closed. Everything else is open: a
442
+ #: later release may add a value, and a client must tolerate one it does not know
443
+ #: (``stability.md`` § Change classes). Keep this short — every entry is a promise
444
+ #: that adding a value is a Breaking change — and give each one a reason.
445
+ EXHAUSTIVE_ENUMS: dict[str, str] = {
446
+ # drf-spectacular's structural devices for "blank" and "null" on a nullable /
447
+ # blankable choice field (`oneOf: [{X}, {BlankEnum}, {NullEnum}]`). They are not
448
+ # vocabularies; opening them would make every such `oneOf` accept any value.
449
+ "BlankEnum": "drf-spectacular's empty-string branch of a blankable choice field",
450
+ "NullEnum": "drf-spectacular's null branch of a nullable choice field",
451
+ # The four precedence-diagramming relationships are the whole set by
452
+ # definition, and both scheduling engines hard-code exactly four. A fifth would
453
+ # be a new engine contract, which is Breaking whatever the schema says.
454
+ "DepTypeEnum": "FS/SS/FF/SF is the complete set of CPM dependency types",
455
+ # Python's weekday() integers. There is no eighth day.
456
+ "DigestWeekdayEnum": "the seven weekdays, 0 (Monday) to 6 (Sunday)",
457
+ # Discriminator tags. Each names which `oneOf` variant a response is; a client
458
+ # dispatches on it, and a new chart type or engine pass arrives as a new variant
459
+ # with its own tag rather than as a new value of an existing variant's tag.
460
+ "ProjectBurnCombinedChartTypeEnum": "discriminator tag of the ProjectBurnResponse variant",
461
+ "ProjectBurnSeriesChartTypeEnum": "discriminator tag of the ProjectBurnResponse variant",
462
+ "ScheduleCpmDerivationPassEnum": "tags the CPM variant of ScheduleDerivationResponse",
463
+ "ScheduleMonteCarloDerivationPassEnum": (
464
+ "tags the Monte Carlo variant of ScheduleDerivationResponse"
465
+ ),
466
+ }
467
+
468
+
469
+ def _inline_enum_openness(node: dict[str, Any]) -> str:
470
+ """Openness of an enum declared inline (a parameter or an anonymous property).
471
+
472
+ A boolean or null constant (``sent: [false]``) has no room to grow, so it is
473
+ exhaustive; any other inline vocabulary follows the open default.
474
+ """
475
+ if node.get("type") == "boolean" or all(v is None for v in node.get("enum", [])):
476
+ return ENUM_EXHAUSTIVE
477
+ return ENUM_OPEN
478
+
479
+
480
+ def _mark_inline_enums(node: Any) -> None:
481
+ if isinstance(node, dict):
482
+ if "enum" in node and ENUM_OPENNESS_KEY not in node:
483
+ node[ENUM_OPENNESS_KEY] = _inline_enum_openness(node)
484
+ for value in node.values():
485
+ _mark_inline_enums(value)
486
+ elif isinstance(node, list):
487
+ for item in node:
488
+ _mark_inline_enums(item)
489
+
490
+
491
+ def _mark_enum_openness(result: dict[str, Any]) -> None:
492
+ """Mark every ``enum`` node in the document ``open`` or ``exhaustive`` (#4593).
493
+
494
+ Before this every one of the 143 enum components was published closed, while
495
+ ``stability.md`` called a new enum value an Additive change — so the schema
496
+ told strict generated clients (and the e2e schema guard) to reject exactly the
497
+ change the policy promised was safe. The default is now open; the closed ones
498
+ are named in :data:`EXHAUSTIVE_ENUMS` with a reason each.
499
+
500
+ ``enum`` itself is kept rather than replaced, so typed clients still get the
501
+ known values and the reference docs still list them. The marker is what a
502
+ client reads to decide whether an unrecognized value is a bug or a newer
503
+ server; ADR-1291 records the trade-off.
504
+ """
505
+ schemas = result.get("components", {}).get("schemas", {})
506
+ for name, schema in schemas.items():
507
+ if isinstance(schema, dict) and "enum" in schema:
508
+ schema[ENUM_OPENNESS_KEY] = ENUM_EXHAUSTIVE if name in EXHAUSTIVE_ENUMS else ENUM_OPEN
509
+ # Then everything still unmarked: inline property, parameter and response enums.
510
+ _mark_inline_enums(result)
511
+
512
+
513
+ #: The component every role field is published as (ADR-0072 Amendment 2, ADR-1291).
514
+ ROLE_ORDINAL = "RoleOrdinal"
515
+ _ROLE_ENUM_REF = "#/components/schemas/RoleEnum"
516
+ _ROLE_ORDINAL_REF = f"#/components/schemas/{ROLE_ORDINAL}"
517
+
518
+
519
+ def _role_ordinal_schema() -> dict[str, Any]:
520
+ """The banded-integer schema for a role ordinal, derived from ``Role`` itself.
521
+
522
+ Derived rather than written out so the published OSS ordinals cannot drift
523
+ from the model's choices.
524
+ """
525
+ from trueppm_api.apps.access.models import Role # app code: import at call time
526
+
527
+ tiers = ", ".join(f"`{member.value}` {member.name}" for member in Role)
528
+ return {
529
+ "type": "integer",
530
+ "minimum": int(min(Role)),
531
+ "maximum": int(max(Role)),
532
+ "description": (
533
+ "A role ordinal (ADR-0072). This is a banded integer, not a closed set. "
534
+ f"The five OSS roles are {tiers}; their display names depend on the "
535
+ "project or program vocabulary. The ordinals between them (2-99, "
536
+ "101-199, 201-299 and 301-399) are reserved for Enterprise custom "
537
+ "roles, and an Enterprise install may return one. A client must accept "
538
+ "any integer in range: compare with `>=` against the OSS ordinal that "
539
+ "opens a band to decide what a role may do, and render an unrecognized "
540
+ "ordinal by the band it falls in. `0` and anything above the owner "
541
+ "ordinal are never assigned. An OSS server accepts only the five OSS "
542
+ "ordinals on write."
543
+ ),
544
+ }
545
+
546
+
547
+ def _rewrite_ref(node: Any, old: str, new: str) -> None:
548
+ if isinstance(node, dict):
549
+ if node.get("$ref") == old:
550
+ node["$ref"] = new
551
+ for value in node.values():
552
+ _rewrite_ref(value, old, new)
553
+ elif isinstance(node, list):
554
+ for item in node:
555
+ _rewrite_ref(item, old, new)
556
+
557
+
558
+ def _publish_role_ordinal(result: dict[str, Any]) -> None:
559
+ """Publish ``role`` / ``default_member_role`` / ``my_role`` as a banded integer.
560
+
561
+ drf-spectacular derives a closed ``RoleEnum = [1, 100, 200, 300, 400]`` from
562
+ the ``Role`` choices. That enum is closed over the very ordinals ADR-0072
563
+ reserves for Enterprise custom roles, so an Enterprise install returning a
564
+ role at 250 would violate the published OSS contract by design (#4593). The
565
+ component is replaced by :data:`ROLE_ORDINAL` and every reference repointed.
566
+
567
+ Some fields carry a project ``Role`` ordinal as a plain ``integer`` with no
568
+ reference to the role at all: a ``SerializerMethodField`` (``my_role``,
569
+ ``max_project_role``) or a bare ``IntegerField`` whose choices are enforced in
570
+ ``validate`` rather than declared. They are pointed at the same component so
571
+ their bands are documented in the one place every role field reads them from.
572
+ ``my_role`` is matched on every component that declares it; the others are
573
+ named in :data:`_BARE_ROLE_FIELDS`.
574
+ """
575
+ schemas = result.get("components", {}).get("schemas")
576
+ if not schemas or "RoleEnum" not in schemas:
577
+ return
578
+ del schemas["RoleEnum"]
579
+ schemas[ROLE_ORDINAL] = _role_ordinal_schema()
580
+ _rewrite_ref(result, _ROLE_ENUM_REF, _ROLE_ORDINAL_REF)
581
+ for name, schema in schemas.items():
582
+ properties = schema.get("properties", {}) if isinstance(schema, dict) else {}
583
+ for field in ("my_role", *_BARE_ROLE_FIELDS.get(name, ())):
584
+ _point_at_role_ordinal(properties.get(field))
585
+ result["components"]["schemas"] = dict(sorted(schemas.items()))
586
+
587
+
588
+ #: Component -> properties that carry a project ``Role`` ordinal as a bare
589
+ #: ``integer``. Workspace-role fields (``workspace_role``, ``role_value``, the
590
+ #: workspace member/invite ``role``, SSO ``default_role``) are deliberately absent:
591
+ #: they are ``WorkspaceRole`` ordinals, a different scale this component does not
592
+ #: describe.
593
+ _BARE_ROLE_FIELDS: dict[str, tuple[str, ...]] = {
594
+ "ProjectMembershipWriteRequest": ("role",),
595
+ "PatchedProjectMembershipUpdateRequest": ("role",),
596
+ "GroupProjectLink": ("role",),
597
+ "GroupProjectWriteRequest": ("role",),
598
+ "Me": ("max_project_role",),
599
+ }
600
+
601
+
602
+ def _point_at_role_ordinal(prop: Any) -> None:
603
+ """Turn ``{type: integer, ...}`` into ``{allOf: [RoleOrdinal], ...}`` in place."""
604
+ if not isinstance(prop, dict) or prop.get("type") != "integer":
605
+ return
606
+ del prop["type"]
607
+ rest = dict(prop)
608
+ prop.clear()
609
+ prop["allOf"] = [{"$ref": _ROLE_ORDINAL_REF}]
610
+ prop.update(rest)
611
+
612
+
613
+ def _register_validation_error_detail(result: dict[str, Any]) -> None:
614
+ """Register the recursive ``ValidationErrorDetail`` component (#3324).
615
+
616
+ It is defined here rather than through drf-spectacular's component registry
617
+ because nothing in the API is serialized by it — it describes DRF's *error*
618
+ output, which has no serializer to introspect. The ``$ref`` that points at it
619
+ is injected per-operation by :class:`TruePPMAutoSchema`, so registering it in
620
+ the finished document is what makes that ``$ref`` resolve.
621
+
622
+ Re-sorted afterwards because drf-spectacular emits ``components.schemas``
623
+ alphabetically; appending to the end would make the committed
624
+ ``docs/api/openapi.json`` diff against a fresh regenerate on ordering alone.
625
+ """
626
+ schemas = result.setdefault("components", {}).setdefault("schemas", {})
627
+ schemas[VALIDATION_ERROR_DETAIL] = deepcopy(_VALIDATION_ERROR_DETAIL_SCHEMA)
628
+ result["components"]["schemas"] = dict(sorted(schemas.items()))
629
+
630
+
631
+ DEMO_READ_ONLY_ERROR = "DemoReadOnlyError"
632
+
633
+
634
+ def _register_demo_read_only_error(result: dict[str, Any]) -> None:
635
+ """Register the ``403`` body demo read-only mode returns for every refused write.
636
+
637
+ The demo read-only middleware answers before routing, so no operation has a view to
638
+ attach this response to, and stamping a ``403`` onto every write operation would
639
+ describe a response the vast majority of deployments can never return. A single
640
+ named component, which ``api/errors.md`` and the API description point at, is the
641
+ honest shape: any unsafe request may return it, but only when the deployment sets
642
+ ``TRUEPPM_DEMO_READ_ONLY``.
643
+
644
+ Re-sorted for the same reason as :func:`_register_validation_error_detail`: the
645
+ committed ``docs/api/openapi.json`` must not diff against a fresh regenerate on
646
+ ordering alone.
647
+ """
648
+ schemas = result.setdefault("components", {}).setdefault("schemas", {})
649
+ schemas[DEMO_READ_ONLY_ERROR] = {
650
+ "type": "object",
651
+ "description": (
652
+ "Returned with HTTP 403 for every `POST`, `PUT`, `PATCH` or `DELETE` under "
653
+ "`/api/` when the deployment runs in demo read-only mode "
654
+ "(`TRUEPPM_DEMO_READ_ONLY=true`), except sign-in, token refresh and sign-out. "
655
+ "Independent of role, permission class and authentication: an Owner and an "
656
+ "anonymous caller are refused identically, and retrying with more permission "
657
+ "never helps. A normal installation never returns it."
658
+ ),
659
+ "properties": {
660
+ "detail": {
661
+ "type": "string",
662
+ "description": "Human-readable explanation. May be reworded; do not match on it.",
663
+ },
664
+ "code": {
665
+ "type": "string",
666
+ "enum": [DEMO_READ_ONLY_CODE],
667
+ "description": "Stable machine-readable refusal code. Branch on this.",
668
+ },
669
+ },
670
+ "required": ["detail", "code"],
671
+ "additionalProperties": False,
672
+ }
673
+ result["components"]["schemas"] = dict(sorted(schemas.items()))
674
+
675
+
676
+ def _annotate_operation(
677
+ operation: dict[str, Any],
678
+ path: str,
679
+ method: str,
680
+ collection_paths: frozenset[str],
681
+ is_public: bool,
682
+ ) -> None:
683
+ """Fill a single operation's ``summary``/``tags``/``security`` in place.
684
+
685
+ Explicit ``@extend_schema`` values are preserved: an existing summary and a
686
+ non-default tag are left untouched.
687
+ """
688
+ if not operation.get("summary"):
689
+ operation["summary"] = _derive_summary(path, method, collection_paths)
690
+
691
+ tags = operation.get("tags")
692
+ if not tags or tags == ["v1"]:
693
+ operation["tags"] = [_derive_tag(path)]
694
+
695
+ if is_public:
696
+ operation["security"] = []
697
+
698
+
699
+ # ---------------------------------------------------------------------------
700
+ # 429 documentation via a custom AutoSchema
701
+ # ---------------------------------------------------------------------------
702
+
703
+ # The 403 an API-token caller receives on the credential-management surface (#2878) and
704
+ # on SSO provider configuration (#3551). Declared on every operation whose view carries
705
+ # ``IsNotTokenAuthenticated``, so an integrator scripting against a token learns from the
706
+ # contract that this refusal is by design rather than discovering it as an apparent
707
+ # permissions bug. Worded for the whole surface rather than the token routes alone: it is
708
+ # emitted verbatim on SSO operations too, where a token-management-specific sentence
709
+ # would be a confidently wrong diagnosis.
710
+ _TOKEN_REFUSED_RESPONSE = {
711
+ "description": (
712
+ "Refused because the caller authenticated with an API token. Managing "
713
+ "credentials or sign-in configuration requires a session or JWT — a token can "
714
+ "neither mint, list or revoke tokens, nor read or reconfigure SSO providers — "
715
+ "so that revoking a leaked credential is actual containment. The ``refusal`` "
716
+ "envelope carries ``reason: policy`` and ``constraint: capability_scope``. A "
717
+ "token that is revoked, expired, or carries the wrong scope for this surface is "
718
+ "rejected earlier, by the authenticator, and receives ``401`` with "
719
+ "``reason: identity`` instead. This status is also returned to a *session* "
720
+ "caller who lacks the required role; that refusal carries ``detail`` alone, "
721
+ "with no ``refusal`` envelope."
722
+ ),
723
+ "content": {
724
+ _JSON_MEDIA_TYPE: {
725
+ "schema": {
726
+ "type": "object",
727
+ "properties": {
728
+ "detail": {
729
+ "type": "string",
730
+ "example": (
731
+ "API tokens cannot manage credentials or sign-in "
732
+ "configuration. Sign in to perform this action."
733
+ ),
734
+ },
735
+ "refusal": {
736
+ "type": "object",
737
+ "properties": {
738
+ "verdict": {"type": "string", "example": "refused"},
739
+ "reason": {"type": "string", "example": "policy"},
740
+ "constraint": {"type": "string", "example": "capability_scope"},
741
+ },
742
+ },
743
+ },
744
+ }
745
+ }
746
+ },
747
+ }
748
+
749
+ #: Component name for the recursive validation-error value (#3324).
750
+ VALIDATION_ERROR_DETAIL = "ValidationErrorDetail"
751
+ _VALIDATION_ERROR_DETAIL_REF = {"$ref": f"#/components/schemas/{VALIDATION_ERROR_DETAIL}"}
752
+
753
+ #: What one field key in a 400 body can hold (#3324).
754
+ #:
755
+ #: #3286 declared this as ``array<string>`` and the first nightly ``api:fuzz`` run
756
+ #: after it merged failed three operations on ``response_schema_conformance``:
757
+ #: ``POST /projects/``, ``POST /programs/`` and ``PATCH /auth/me/profile/`` all
758
+ #: returned ``{"0": [...], "1": [...]}`` under a field key, which is not an array.
759
+ #: That is not a defect in the response — ``ListField.to_internal_value``
760
+ #: accumulates per-item errors into a dict keyed by ITEM INDEX and raises
761
+ #: ``ValidationError`` on it, and the index is the part a client needs to point at
762
+ #: the offending element. The contract was the thing that was too narrow.
763
+ #:
764
+ #: Four shapes reach a field key, and only the first was declared:
765
+ #:
766
+ #: ``["msg", ...]`` flat field
767
+ #: ``{"0": ["msg"]}`` ``ListField`` — keyed by item index
768
+ #: ``{"sub": ["msg"]}`` nested serializer — keyed by subfield
769
+ #: ``[{}, {"sub": ["msg"]}]`` ``many=True`` — one entry per submitted item
770
+ #: ``{"non_field_errors": ["msg"]}`` a constraint on the list as a whole (#3596)
771
+ #:
772
+ #: The last one keys on neither an index nor a subfield: a length cap, or a value that
773
+ #: is not a list at all, has no failing *item* to point at, so ``ListSerializer`` puts
774
+ #: it under ``non_field_errors``. Declaring only "index or subfield name" sent an
775
+ #: integrator looking for a ``"0"`` key that is never there.
776
+ #:
777
+ #: Recursive rather than an enumeration of those four, because nesting composes: a
778
+ #: ``ListField`` inside a ``many=True`` serializer nests one level deeper than any
779
+ #: of them and would need a fifth branch, then a sixth. The invariant that holds at
780
+ #: every depth is the one declared here — **strings at the leaves**. The three
781
+ #: branches are disjoint by ``type``, so ``oneOf`` is exact rather than merely
782
+ #: permissive: no value matches two of them.
783
+ _VALIDATION_ERROR_DETAIL_SCHEMA = {
784
+ "description": (
785
+ "One field's validation errors. A flat field yields a list of messages. A "
786
+ "list field or nested serializer nests one level further, as an object keyed "
787
+ "by item index or by subfield name; a ``many=True`` serializer yields one "
788
+ "entry per submitted item, ``{}`` where that item validated. A constraint on "
789
+ "the list as a whole — its length, or a value that is not a list — has no "
790
+ "failing item to point at and keys on ``non_field_errors`` instead. Nesting is "
791
+ "arbitrarily deep and the leaves are always message strings."
792
+ ),
793
+ "oneOf": [
794
+ {"type": "string"},
795
+ {"type": "array", "items": _VALIDATION_ERROR_DETAIL_REF},
796
+ {"type": "object", "additionalProperties": _VALIDATION_ERROR_DETAIL_REF},
797
+ ],
798
+ }
799
+
800
+ #: The 400 every operation that accepts a request body can return (#3286).
801
+ #:
802
+ #: Modelled on what the API actually emits rather than on one tidy shape, because
803
+ #: ``docs/api/errors.md`` documents two and DRF contributes a third:
804
+ #:
805
+ #: ``{"detail": "..."}`` Shape 1 — a bare refusal
806
+ #: ``{"code": "...", "detail": "..."}`` Shape 2 — ``code`` is the contract
807
+ #: ``{"name": ["This field is required."]}`` DRF's field-keyed serializer errors
808
+ #:
809
+ #: ``additionalProperties`` carries the third, via the recursive component above so
810
+ #: that its nested forms are described too. Declaring only ``detail`` would be the
811
+ #: more comfortable lie: a generated client would type the uncommon case and
812
+ #: mis-type field validation, which is the majority of real 400s.
813
+ #:
814
+ #: ``detail`` and ``code`` reference that same component rather than declaring
815
+ #: ``string`` (#3347). In JSON Schema ``additionalProperties`` applies only to keys
816
+ #: **not** matched by ``properties``, so the widening #3324 made to the field-keyed
817
+ #: map never reached the two keys named in the envelope — they stayed pinned to
818
+ #: ``string``. That is only sound while each name means one thing, and ``code``
819
+ #: means two: the refusal envelope's stable machine code, and the validation errors
820
+ #: of the serializer field literally *named* ``code`` — ``Program.code`` and
821
+ #: ``Project.code``, reachable on 21 operations. DRF keys the latter by field name
822
+ #: and yields a list, so the 2026-09-04 nightly ``api:fuzz`` failed
823
+ #: ``POST /programs/`` and ``POST /projects/`` on
824
+ #: ``["Ensure this field has no more than 40 characters."] is not of type "string"``.
825
+ #: The response was right and the declaration was narrow, exactly as in #3324.
826
+ #:
827
+ #: ``detail`` is widened with it. No serializer has a writable ``detail`` field
828
+ #: today, so that half is latent — but it is the *same* declaration bug, and the
829
+ #: one-instance fix is what left this open after #3324.
830
+ #:
831
+ #: The reference is exact, not merely permissive: ``ValidationErrorDetail``'s first
832
+ #: ``oneOf`` branch is ``{"type": "string"}``, so the envelope form still validates
833
+ #: against precisely one branch. What a client loses is the guarantee that ``code``
834
+ #: is a string — which it never had. It gains the discriminator: a **string**
835
+ #: ``code`` is a refusal code from the vocabulary in ``docs/api/errors.md``; a list
836
+ #: or object under ``code`` is field validation of the field of that name.
837
+ #:
838
+ #: ``allOf`` wraps the ``$ref`` because this is OpenAPI 3.0.3, where sibling keys
839
+ #: alongside a ``$ref`` are discarded — without it the description and example are
840
+ #: silently dropped from the published schema.
841
+ _VALIDATION_RESPONSE = {
842
+ "description": (
843
+ "The request was rejected. Either a field failed validation — in which case "
844
+ "the body is keyed by field name with a list of messages — or the request was "
845
+ "refused as a whole, in which case it carries ``detail`` and, when the failure "
846
+ "is one a client is expected to handle rather than merely report, a stable "
847
+ "``code``. Branch on ``code`` **when it is a string**; a list or object under "
848
+ "``code`` or ``detail`` is field validation of the field of that name, not the "
849
+ "envelope. ``detail`` is prose and may be reworded at any time. See the error "
850
+ "reference for the code vocabulary."
851
+ ),
852
+ "content": {
853
+ _JSON_MEDIA_TYPE: {
854
+ "schema": {
855
+ "type": "object",
856
+ "properties": {
857
+ "detail": {
858
+ "allOf": [_VALIDATION_ERROR_DETAIL_REF],
859
+ "description": (
860
+ "The refusal's human-readable explanation when the request "
861
+ "was refused as a whole. A list or object here is instead "
862
+ "the validation errors of a serializer field named "
863
+ "``detail``."
864
+ ),
865
+ "example": "Request body must be a JSON object.",
866
+ },
867
+ "code": {
868
+ "allOf": [_VALIDATION_ERROR_DETAIL_REF],
869
+ "description": (
870
+ "The refusal's stable machine code when the request was "
871
+ "refused as a whole — a string, drawn from the vocabulary "
872
+ "in the error reference. A list or object here is instead "
873
+ "the validation errors of the serializer field named "
874
+ "``code`` (``Program.code``, ``Project.code``)."
875
+ ),
876
+ "example": "invalid_body",
877
+ },
878
+ },
879
+ "additionalProperties": _VALIDATION_ERROR_DETAIL_REF,
880
+ }
881
+ }
882
+ },
883
+ }
884
+
885
+
886
+ def validation_envelope_400(description: str) -> Any:
887
+ """Declare a 400 with a site-specific description and the shared error envelope.
888
+
889
+ The #3286 injection uses ``setdefault``, so an operation that declares its own 400
890
+ with a description only *replaces* the envelope body rather than refining it,
891
+ silently dropping the ``application/json`` schema every other 400 carries. Use this
892
+ where an endpoint wants its own wording but the standard ``detail``/``code``/
893
+ field-keyed body.
894
+ """
895
+ from drf_spectacular.utils import OpenApiResponse
896
+
897
+ return OpenApiResponse(
898
+ response=deepcopy(
899
+ cast("dict[str, Any]", _VALIDATION_RESPONSE["content"])[_JSON_MEDIA_TYPE]["schema"]
900
+ ),
901
+ description=description,
902
+ )
903
+
904
+
905
+ # ---------------------------------------------------------------------------
906
+ # #3319 — the 400 a BODYLESS write returns when it refuses on state
907
+ # ---------------------------------------------------------------------------
908
+
909
+ #: The three wire shapes a bodyless write's refusal actually takes.
910
+ #:
911
+ #: Which one an endpoint emits is decided by *how* it raises, not by intent, and
912
+ #: the three are not interchangeable — a client that types the wrong one mis-reads
913
+ #: every refusal that endpoint produces:
914
+ #:
915
+ #: ``detail`` ``Response({"detail": ...}, status=400)`` or
916
+ #: ``ValidationError({"detail": ...})`` — a JSON **object**.
917
+ #: ``messages`` ``ValidationError("a bare string")`` — DRF wraps a string detail
918
+ #: in a list, so the body is a top-level JSON **array**, not an
919
+ #: object. Easy to declare wrongly; verified per site.
920
+ #: ``fields`` ``ValidationError({"field": "..."})`` — DRF's field-keyed object,
921
+ #: described by the recursive ``ValidationErrorDetail`` component.
922
+ _STATE_REFUSAL_SHAPES: dict[str, dict[str, Any]] = {
923
+ "detail": {
924
+ "type": "object",
925
+ "properties": {"detail": {"type": "string"}},
926
+ },
927
+ "messages": {"type": "array", "items": {"type": "string"}},
928
+ "fields": {
929
+ "type": "object",
930
+ "additionalProperties": _VALIDATION_ERROR_DETAIL_REF,
931
+ },
932
+ }
933
+
934
+
935
+ def state_refusal_400(
936
+ description: str,
937
+ *,
938
+ shape: str = "detail",
939
+ codes: tuple[str, ...] = (),
940
+ ) -> Any:
941
+ """Declare the 400 a **bodyless** write returns when it refuses on state (#3319).
942
+
943
+ #3286 injects a 400 wherever an operation has a ``requestBody``, because
944
+ ``requestBody`` present is precisely the condition under which DRF can raise
945
+ ``ValidationError`` — a real equivalence the generator already computed. A
946
+ write that takes no body has no equivalent signal, and blanket-declaring a 400
947
+ across all of them would advertise a refusal most cannot produce: the same
948
+ defect as omitting one, pointing the other way. So every site opts in by hand,
949
+ having been checked against what the handler actually returns.
950
+
951
+ Args:
952
+ description: The state that makes *this* endpoint refuse. Site-specific by
953
+ design — a generic string would make the declaration decoration rather
954
+ than contract, which is the thing this issue exists to avoid.
955
+ shape: Which of :data:`_STATE_REFUSAL_SHAPES` reaches the wire. Read it off
956
+ the ``raise``/``Response`` in the handler, not off the neighbours.
957
+ codes: Stable ``code`` values the refusal really puts in the response
958
+ **body**. Pass one only where the handler writes it into a literal
959
+ response dict — a ``ValidationError(..., code=...)`` keyword never
960
+ reaches the client (#2550), so declaring that would invert this fix.
961
+
962
+ Returns:
963
+ The ``OpenApiResponse`` to hang off ``responses={400: ...}``.
964
+ """
965
+ from drf_spectacular.utils import OpenApiResponse
966
+
967
+ schema = deepcopy(_STATE_REFUSAL_SHAPES[shape])
968
+ if codes:
969
+ # Only reachable on the object shapes; a list body has nowhere to carry it.
970
+ schema["properties"]["code"] = {
971
+ "type": "string",
972
+ "enum": list(codes),
973
+ "description": "Stable refusal code — branch on this, not on ``detail``.",
974
+ }
975
+ return OpenApiResponse(response=schema, description=description)
976
+
977
+
978
+ def ownership_refusal_403(description: str, *, codes: tuple[str, ...] = ()) -> Any:
979
+ """Declare the 403 an object-ownership guard returns (#3365).
980
+
981
+ Six guards used to refuse with ``ValidationError`` and so answered ``400`` for
982
+ what is an authorization decision — "you are not the author / uploader / a
983
+ role above the target", not "the thing you sent is malformed". A client
984
+ branching on status could not tell the two apart, and retrying with a
985
+ corrected body could never succeed because the body was never the problem.
986
+ They now raise ``PermissionDenied``, and this declares what reaches the wire
987
+ so a generated client has a typed branch for it.
988
+
989
+ Args:
990
+ description: Who is refused and why, for *this* endpoint. Site-specific
991
+ for the same reason :func:`state_refusal_400` insists on it — a
992
+ generic string is decoration, not contract.
993
+ codes: Stable ``code`` values the refusal really puts in the response
994
+ body. A guard emits one only by raising ``PermissionDenied`` with a
995
+ **dict** detail (``{"detail": ..., "code": ...}``) — DRF serializes a
996
+ dict detail key-for-key, whereas a ``code=`` keyword never leaves the
997
+ ``ErrorDetail`` object (#2550). Declare only what the handler writes.
998
+
999
+ Returns:
1000
+ The ``OpenApiResponse`` to hang off ``responses={403: ...}``.
1001
+ """
1002
+ from drf_spectacular.utils import OpenApiResponse
1003
+
1004
+ schema: dict[str, Any] = {
1005
+ "type": "object",
1006
+ "properties": {"detail": {"type": "string"}},
1007
+ "required": ["detail"],
1008
+ }
1009
+ if codes:
1010
+ schema["properties"]["code"] = {
1011
+ "type": "string",
1012
+ "enum": list(codes),
1013
+ "description": "Stable refusal code — branch on this, not on ``detail``.",
1014
+ }
1015
+ schema["required"].append("code")
1016
+ return OpenApiResponse(response=schema, description=description)
1017
+
1018
+
1019
+ _THROTTLE_RESPONSE = {
1020
+ "description": (
1021
+ "Rate limit exceeded. The client is issuing requests faster than the "
1022
+ "endpoint's throttle allows; retry after the interval in the "
1023
+ "``Retry-After`` response header."
1024
+ ),
1025
+ "content": {
1026
+ _JSON_MEDIA_TYPE: {
1027
+ "schema": {
1028
+ "type": "object",
1029
+ "properties": {
1030
+ "detail": {
1031
+ "type": "string",
1032
+ "example": "Request was throttled. Expected available in 42 seconds.",
1033
+ }
1034
+ },
1035
+ }
1036
+ }
1037
+ },
1038
+ }
1039
+
1040
+
1041
+ class TruePPMAutoSchema(AutoSchema):
1042
+ """AutoSchema that documents a ``429`` response on rate-limited operations, and
1043
+ keeps the ``projectApiTokenAuth`` security scheme honest on MCP-readable views.
1044
+
1045
+ Throttling is a view attribute (``throttle_classes``) invisible to the final
1046
+ schema dict, so 429 documentation has to happen here — where the generated
1047
+ operation and its view are both in hand — rather than in a post-processing hook.
1048
+ Only statically-declared ``throttle_classes`` are considered; that covers every
1049
+ scoped/dedicated throttle in the codebase (login, refresh, ws-ticket,
1050
+ user-search, credential rotation, Monte Carlo what-if, invite accept/resend,
1051
+ resource catalog, Git webhook, inbound task-sync and acceptance-result reporting).
1052
+ """
1053
+
1054
+ def get_auth(self) -> list[dict[str, Any]]:
1055
+ """Drop ``projectApiTokenAuth`` from unsafe operations on MCP-readable views (#2659).
1056
+
1057
+ ``McpReadableViewMixin.get_authenticators`` prepends
1058
+ ``ProjectApiTokenAuthentication`` at the *view* level (so a token is
1059
+ recognized on every action), but drf-spectacular's default ``get_auth()``
1060
+ reads ``view.get_authenticators()`` with no awareness of *which* method is
1061
+ being documented — so it attached ``projectApiTokenAuth`` to every method on
1062
+ the view, including the unsafe ones no credential unique to that scheme can
1063
+ reach. That made the schema advertise 82 write operations
1064
+ (POST/PUT/PATCH/DELETE) as project/program-token-writable when they are not.
1065
+
1066
+ The rule the filter describes changed shape in #2877 but not in outcome.
1067
+ Before: ``TokenReadOnlyMethods`` refused *every* token an unsafe method on
1068
+ these views. Now a ``legacy:full`` personal access token may write here — but
1069
+ that credential is described by the **``personalApiTokenAuth``** scheme,
1070
+ which these operations already advertise through the default authentication
1071
+ stack. The credentials unique to ``projectApiTokenAuth`` are the project- and
1072
+ program-scoped tokens, and those are still refused on this whole surface by
1073
+ ``TokenIsOwnerScoped`` (401), so the scheme remains unreachable for an unsafe
1074
+ method and must not be advertised. Both schemes are ``http bearer`` with the
1075
+ same ``tppm_<64-hex>`` format, so a client reading the schema is told exactly
1076
+ the header it needs.
1077
+
1078
+ This method and its view are the one place drf-spectacular hands both to the
1079
+ same call, so the filter belongs here rather than in the mixin (which has no
1080
+ reason to know about schema generation) or in a blanket post-processing hook
1081
+ (which would also strip the scheme from ``TaskSyncView`` /
1082
+ ``AcceptanceResultIngestView`` — two views that reference
1083
+ ``ProjectApiTokenAuthentication`` directly to *accept* a token write, and
1084
+ must keep advertising it).
1085
+ """
1086
+ auth = super().get_auth()
1087
+ if self._mcp_scoped_token_write_unreachable():
1088
+ auth = [entry for entry in auth if "projectApiTokenAuth" not in entry]
1089
+ if self._token_callers_refused():
1090
+ auth = [
1091
+ entry
1092
+ for entry in auth
1093
+ if "projectApiTokenAuth" not in entry and "personalApiTokenAuth" not in entry
1094
+ ]
1095
+ return auth
1096
+
1097
+ def _token_callers_refused(self) -> bool:
1098
+ """Whether this view refuses every API token outright (``IsNotTokenAuthenticated``).
1099
+
1100
+ The credential-management surface is session/JWT only since #2878, and SSO
1101
+ provider configuration since #3551, so neither token scheme is reachable on any
1102
+ of their operations and the schema must not advertise them — an integrator
1103
+ reading it would otherwise write a script that cannot possibly work, against the
1104
+ surfaces where the failure looks like a permissions bug rather than a deliberate
1105
+ refusal.
1106
+
1107
+ Read off ``permission_classes`` rather than ``get_permissions()`` because schema
1108
+ generation has no request to resolve per-action permissions against; every view
1109
+ carrying the guard declares it statically, which is also what makes the
1110
+ route-table tripwire in ``tests/apps/access/test_route_table_invariants.py``
1111
+ sound.
1112
+ """
1113
+ from trueppm_api.apps.access.permissions import IsNotTokenAuthenticated
1114
+
1115
+ classes = getattr(self.view, "permission_classes", []) or []
1116
+ return any(cls is IsNotTokenAuthenticated for cls in classes)
1117
+
1118
+ def _mcp_scoped_token_write_unreachable(self) -> bool:
1119
+ """Whether this operation is an unsafe method on an ``McpReadableViewMixin`` view.
1120
+
1121
+ On such an operation the project/program-scoped credentials that
1122
+ ``projectApiTokenAuth`` uniquely describes are refused by
1123
+ ``TokenIsOwnerScoped``, which applies to every ``ApiToken`` on the MCP
1124
+ surface regardless of scope and regardless of method — so the predicate
1125
+ stays a plain "unsafe method on the mixin" and does not need to restate the
1126
+ scope logic (#2877).
1127
+ """
1128
+ from trueppm_api.apps.access.permissions import McpReadableViewMixin
1129
+
1130
+ return isinstance(self.view, McpReadableViewMixin) and self.method not in SAFE_METHODS
1131
+
1132
+ def get_operation(
1133
+ self,
1134
+ path: str,
1135
+ path_regex: str,
1136
+ path_prefix: str,
1137
+ method: str,
1138
+ registry: Any,
1139
+ ) -> dict[str, Any] | None:
1140
+ operation = super().get_operation(path, path_regex, path_prefix, method, registry)
1141
+ if operation is None:
1142
+ return operation
1143
+
1144
+ throttle_classes = getattr(self.view, "throttle_classes", None)
1145
+ if throttle_classes:
1146
+ responses = operation.setdefault("responses", {})
1147
+ responses.setdefault("429", dict(_THROTTLE_RESPONSE))
1148
+ # A 400 is declarable exactly where there is a body to reject (#3286). The
1149
+ # predicate is READ OFF the operation drf-spectacular just built rather than
1150
+ # guessed from the view: `requestBody` is present precisely when the
1151
+ # generator resolved a serializer or a raw-body declaration for this method,
1152
+ # which is the same condition under which DRF can raise ValidationError. An
1153
+ # unsafe method with no body — a POST action that works from the URL alone —
1154
+ # gets nothing, because advertising a refusal an endpoint cannot produce is
1155
+ # the same defect as omitting one it can, pointing the other way.
1156
+ #
1157
+ # `setdefault`, so an operation declaring its own richer 400 (the
1158
+ # refusal-code enums in `refusal_codes.py`, say) keeps it.
1159
+ if method not in SAFE_METHODS and "requestBody" in operation:
1160
+ responses = operation.setdefault("responses", {})
1161
+ responses.setdefault("400", dict(_VALIDATION_RESPONSE))
1162
+ if self._token_callers_refused():
1163
+ # Declared here rather than by decorating each view for the same reason as
1164
+ # the 429 above: the fact lives in a view attribute the schema dict cannot
1165
+ # see, and one injection point cannot drift from the permission class the
1166
+ # way fifteen hand-written `@extend_schema` blocks would (#2878).
1167
+ responses = operation.setdefault("responses", {})
1168
+ responses.setdefault("403", dict(_TOKEN_REFUSED_RESPONSE))
1169
+ return operation
1170
+
1171
+ def _get_paginator(self) -> Any:
1172
+ """Return ``None`` for actions marked with :func:`suppress_list_pagination`.
1173
+
1174
+ drf-spectacular paginates a ``many=True`` response whenever the view has a
1175
+ ``pagination_class``. An ``@action`` that returns a bare array opts out via
1176
+ the marker so its schema is a plain array, not a pagination envelope
1177
+ (#2127). Every other action falls through to the default behaviour.
1178
+ """
1179
+ action = getattr(self.view, "action", None)
1180
+ if action:
1181
+ handler = getattr(self.view, action, None)
1182
+ if handler is not None and getattr(handler, _SUPPRESS_PAGINATION_ATTR, False):
1183
+ return None
1184
+ # drf-spectacular's AutoSchema is untyped; the base return is a paginator or None.
1185
+ return super()._get_paginator() # type: ignore[no-untyped-call]
1186
+
1187
+
1188
+ def undo_summary_schema(description: str, **counts: str) -> dict[str, Any]:
1189
+ """Raw OpenAPI schema for the ``undo`` key a batch-undo action adds to its 200 (#3416).
1190
+
1191
+ All four undo actions build their body as ``data = serializer.data`` followed by
1192
+ ``data["undo"] = summary``. Declaring the bare serializer as the ``200`` therefore
1193
+ publishes a document missing the one value the endpoint exists to report — a
1194
+ generated SDK has no ``undo`` field at all, so a typed client cannot read the
1195
+ outcome of the call it just made. That is the #3399 trap pointed at a different
1196
+ surface: the declaration is self-consistent, ``api:schema-drift`` passes on it,
1197
+ and it is still false.
1198
+
1199
+ The counts differ per action (``deleted``/``kept`` for the two that remove rows,
1200
+ ``reverted``/``kept`` for the cascade, five keys for the structural undo), so each
1201
+ site passes its own. They are spelled out rather than left as an open
1202
+ ``additionalProperties: {type: integer}`` map because the key set is closed and a
1203
+ client branches on the individual names.
1204
+
1205
+ Args:
1206
+ description: What this action's summary counts, in its own vocabulary.
1207
+ **counts: ``field_name="what it counts"`` for every key the summary carries.
1208
+
1209
+ Returns:
1210
+ A schema dict to hang off ``@extend_schema_field`` on the wrapper's field.
1211
+ """
1212
+ return {
1213
+ "type": "object",
1214
+ "description": description,
1215
+ "properties": {
1216
+ name: {"type": "integer", "minimum": 0, "description": text}
1217
+ for name, text in counts.items()
1218
+ },
1219
+ "required": list(counts),
1220
+ "additionalProperties": False,
1221
+ }