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,468 @@
1
+ """Key resolution and key availability endpoints (ADR-1237 §5, §6).
2
+
3
+ ``GET /api/v1/resolve/`` turns whatever a person pasted — a project or program
4
+ key, a retired key, a UUID, or a ``PLAT-T-10`` reference — into the UUIDs every
5
+ other endpoint takes. ``GET /api/v1/keys/`` backs the create form's live key
6
+ suggestion and availability check.
7
+
8
+ The security property of the resolver is that it cannot tell a caller anything
9
+ about an object they cannot read. That is structural, not a check: every lookup
10
+ runs against ONE queryset that is already restricted to what the caller can
11
+ read, intersected with the token's scope, so there is no "found it, now check
12
+ permission" branch whose timing or body could differ. Missing, hidden,
13
+ out-of-token-scope and retired-key-of-a-hidden-project all fall out of the same
14
+ empty queryset and return the same bytes. A future "fast path" that looks an
15
+ ``ObjectKey`` up first and checks access second reintroduces the distinction;
16
+ ``test_resolve_404_is_indistinguishable`` is the tripwire.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import re
22
+ import uuid
23
+ from typing import Any
24
+
25
+ from django.db.models import Exists, OuterRef, Q, QuerySet
26
+ from drf_spectacular.utils import (
27
+ OpenApiParameter,
28
+ OpenApiResponse,
29
+ extend_schema,
30
+ inline_serializer,
31
+ )
32
+ from rest_framework import serializers, status
33
+ from rest_framework.authentication import BaseAuthentication
34
+ from rest_framework.permissions import IsAuthenticated
35
+ from rest_framework.request import Request
36
+ from rest_framework.response import Response
37
+ from rest_framework.throttling import ScopedRateThrottle
38
+ from rest_framework.views import APIView
39
+
40
+ from trueppm_api.apps.access.models import ProgramMembership, ProjectMembership
41
+ from trueppm_api.apps.projects.keys import (
42
+ KIND_PROGRAM,
43
+ KIND_PROJECT,
44
+ KINDS,
45
+ base_key_for,
46
+ is_reserved,
47
+ key_format_error,
48
+ normalize_key,
49
+ )
50
+ from trueppm_api.apps.projects.models import (
51
+ ApiToken,
52
+ ObjectKey,
53
+ Program,
54
+ Project,
55
+ Risk,
56
+ Sprint,
57
+ Task,
58
+ )
59
+
60
+ #: ``<KEY>-<T|SP|R>-<n>``. The greedy ``.+`` makes the match take the RIGHTMOST
61
+ #: marker, so a grandfathered hyphenated key parses: ``GA-SEC-T-10`` is key
62
+ #: ``GA-SEC``, task 10 (ADR-1237 §2).
63
+ _REF_RE = re.compile(r"(?P<key>.+)-(?P<marker>T|SP|R)-(?P<n>[0-9]{1,9})", re.IGNORECASE)
64
+
65
+ #: The one 404 body. Built here, once, and returned verbatim from every
66
+ #: not-found branch so the four cases cannot drift apart byte-wise.
67
+ _NOT_FOUND_BODY = {"detail": "Not found."}
68
+
69
+ _MAX_REF_LENGTH = 128
70
+
71
+ RESOLVE_RESPONSE = inline_serializer(
72
+ "ResolveResponse",
73
+ {
74
+ "type": serializers.ChoiceField(choices=["project", "program", "task", "sprint", "risk"]),
75
+ "id": serializers.UUIDField(),
76
+ "project_id": serializers.UUIDField(allow_null=True),
77
+ "program_id": serializers.UUIDField(allow_null=True),
78
+ "key": serializers.CharField(
79
+ allow_null=True,
80
+ help_text=(
81
+ "The CURRENT key of the project (or program) the ref belongs to — a "
82
+ "retired key resolves, and this is what it was renamed to. Null for a "
83
+ "project that has no key yet (addressable by UUID only)."
84
+ ),
85
+ ),
86
+ "canonical_ref": serializers.CharField(
87
+ help_text=(
88
+ "The current reference form: `PLAT`, `atlas-platform-launch`, "
89
+ "`PLAT-T-10`, `PLAT-SP-3`, `PLAT-R-7`. The UUID when the owning "
90
+ "project has no key."
91
+ )
92
+ ),
93
+ },
94
+ )
95
+
96
+ KEY_SUGGESTION_RESPONSE = inline_serializer(
97
+ "KeySuggestionResponse",
98
+ {
99
+ "available": serializers.BooleanField(
100
+ required=False,
101
+ help_text="Present only when `key` was sent. Whether that key can be used.",
102
+ ),
103
+ "reason": serializers.ChoiceField(
104
+ choices=["taken", "reserved", "invalid"],
105
+ allow_null=True,
106
+ required=False,
107
+ help_text=(
108
+ "Present only when `key` was sent. Why it is unavailable: `taken` "
109
+ "(held now or before by some project/program — never says which), "
110
+ "`reserved` (`new`, `settings`, `trash`, or UUID-shaped), or `invalid` "
111
+ "(does not match the format). Null when available."
112
+ ),
113
+ ),
114
+ "suggestion": serializers.CharField(
115
+ help_text=(
116
+ "A free key: derived from `name`, or the next free suffix of `key` "
117
+ "(`PLAT` → `PLAT2`). Free at the time of the call only."
118
+ )
119
+ ),
120
+ },
121
+ )
122
+
123
+ _ERROR_RESPONSE = inline_serializer("KeyRequestError", {"detail": serializers.CharField()})
124
+
125
+
126
+ def _bad_request(detail: str) -> Response:
127
+ return Response({"detail": detail}, status=status.HTTP_400_BAD_REQUEST)
128
+
129
+
130
+ def _kind(request: Request) -> str | None:
131
+ kind = (request.query_params.get("kind") or "").strip().lower()
132
+ return kind if kind in KINDS else None
133
+
134
+
135
+ def _user(request: Request) -> Any:
136
+ # IsAuthenticated has already run; the Any keeps AnonymousUser out of the lookup type.
137
+ return request.user
138
+
139
+
140
+ def readable_projects(request: Request) -> QuerySet[Project]:
141
+ """Projects the caller can read, intersected with the token's scope (ADR-1237 §5).
142
+
143
+ Readable is the same rule ``ProjectScopedViewSet`` applies: a live
144
+ ``ProjectMembership`` on a live project. A project- or program-scoped
145
+ ``ApiToken`` authenticates *as its minter*, so without the intersection it
146
+ would resolve every project the minter can see — the #1712 blast-radius
147
+ problem on a new surface.
148
+ """
149
+ qs = Project.objects.filter(
150
+ is_deleted=False,
151
+ pk__in=ProjectMembership.objects.filter(user=_user(request), is_deleted=False).values(
152
+ "project_id"
153
+ ),
154
+ )
155
+ token = getattr(request, "auth", None)
156
+ if isinstance(token, ApiToken):
157
+ if token.project_id is not None:
158
+ qs = qs.filter(pk=token.project_id)
159
+ elif token.program_id is not None:
160
+ qs = qs.filter(program_id=token.program_id)
161
+ return qs
162
+
163
+
164
+ def readable_programs(request: Request) -> QuerySet[Program]:
165
+ """Programs the caller can read, intersected with the token's scope.
166
+
167
+ A project-scoped token resolves no program at all: it was minted for one
168
+ project, and the program around it is not part of that grant.
169
+ """
170
+ qs = Program.objects.filter(is_deleted=False).filter(
171
+ Exists(
172
+ ProgramMembership.objects.filter(
173
+ program=OuterRef("pk"), user=_user(request), is_deleted=False
174
+ )
175
+ )
176
+ )
177
+ token = getattr(request, "auth", None)
178
+ if isinstance(token, ApiToken):
179
+ if token.project_id is not None:
180
+ return qs.none()
181
+ if token.program_id is not None:
182
+ qs = qs.filter(pk=token.program_id)
183
+ return qs
184
+
185
+
186
+ def _match_key(qs: QuerySet[Any], kind: str, key: str) -> Any:
187
+ """The object in ``qs`` whose current or retired key is ``key``, or ``None``.
188
+
189
+ ``code`` is matched as well as ``ObjectKey`` so a key written by a pre-0.4 pod
190
+ during the rolling upgrade, which has no row yet, still resolves.
191
+ """
192
+ fk = "program_id" if kind == KIND_PROGRAM else "project_id"
193
+ held = ObjectKey.objects.filter(kind=kind, key__iexact=key).values(fk)
194
+ return qs.filter(Q(code__iexact=key) | Q(pk__in=held)).first()
195
+
196
+
197
+ def _owner_ref(project: Project) -> str | None:
198
+ return project.code or None
199
+
200
+
201
+ def _project_body(project: Project) -> dict[str, Any]:
202
+ return {
203
+ "type": KIND_PROJECT,
204
+ "id": str(project.pk),
205
+ "project_id": str(project.pk),
206
+ "program_id": str(project.program_id) if project.program_id else None,
207
+ "key": _owner_ref(project),
208
+ "canonical_ref": project.code or str(project.pk),
209
+ }
210
+
211
+
212
+ def _program_body(program: Program) -> dict[str, Any]:
213
+ return {
214
+ "type": KIND_PROGRAM,
215
+ "id": str(program.pk),
216
+ "project_id": None,
217
+ "program_id": str(program.pk),
218
+ "key": program.code or None,
219
+ "canonical_ref": program.code or str(program.pk),
220
+ }
221
+
222
+
223
+ def _child_body(project: Project, kind: str, obj: Any, marker: str, number: int) -> dict[str, Any]:
224
+ return {
225
+ "type": kind,
226
+ "id": str(obj.pk),
227
+ "project_id": str(project.pk),
228
+ "program_id": str(project.program_id) if project.program_id else None,
229
+ "key": _owner_ref(project),
230
+ "canonical_ref": f"{project.code}-{marker}-{number}" if project.code else str(obj.pk),
231
+ }
232
+
233
+
234
+ def _resolve_child(project: Project, marker: str, number: int) -> dict[str, Any] | None:
235
+ """Decode a ``T``/``SP``/``R`` number against ``project``.
236
+
237
+ Tasks and sprints share the hex ``object_sequence`` short id, so ``T-10`` is
238
+ ``short_id == "0000000A"`` (see ``format_short_id_display``); risks keep their
239
+ own decimal counter, so ``R-7`` is ``short_id == "7"`` (#929).
240
+ """
241
+ if marker == "R":
242
+ risk = Risk.objects.filter(project=project, short_id=str(number), is_deleted=False).first()
243
+ return _child_body(project, "risk", risk, "R", number) if risk else None
244
+ hex_id = f"{number:08X}"
245
+ if marker == "T":
246
+ task = Task.objects.filter(project=project, short_id=hex_id, is_deleted=False).first()
247
+ return _child_body(project, "task", task, "T", number) if task else None
248
+ sprint = Sprint.objects.filter(project=project, short_id=hex_id, is_deleted=False).first()
249
+ return _child_body(project, "sprint", sprint, "SP", number) if sprint else None
250
+
251
+
252
+ class _TokenReachableView(APIView):
253
+ """Accept a project/program-scoped API token ahead of the default auth stack.
254
+
255
+ ``readable_projects``/``readable_programs`` (above) already intersect a
256
+ project- or program-scoped ``ApiToken`` against its own ``project_id``/
257
+ ``program_id`` — that IS the #1712 confused-deputy defense for this surface.
258
+ Without this override, such a token never reaches that code at all: the
259
+ default auth stack's ``OwnerScopedApiTokenAuthentication`` accepts only
260
+ owner-scoped (personal) tokens and 401s every project/program token before
261
+ the view runs, so ``readable_projects``'/``readable_programs``' token
262
+ branches were dead in production (rbac-check finding, security-review Low).
263
+
264
+ This mirrors ``McpReadableViewMixin.get_authenticators`` — prepend
265
+ ``ProjectApiTokenAuthentication`` so a ``tppm_`` bearer authenticates before
266
+ JWT/the owner-scoped class get a turn — but deliberately WITHOUT that
267
+ mixin's ``TokenIsOwnerScoped`` guard, which *rejects* project/program
268
+ tokens outright for the unscoped MCP collection reads (#1712). Rejecting
269
+ them here would be the opposite bug: ADR-1237 §5 is written for exactly a
270
+ project/program token narrowing the resolver to its own scope, so the
271
+ guard that protects the MCP surface would 401 the very callers this
272
+ endpoint exists to admit.
273
+ """
274
+
275
+ def get_authenticators(self) -> list[BaseAuthentication]:
276
+ from trueppm_api.apps.projects.authentication import ProjectApiTokenAuthentication
277
+
278
+ return [ProjectApiTokenAuthentication(), *super().get_authenticators()]
279
+
280
+
281
+ class ResolveView(_TokenReachableView):
282
+ """Resolve a key, retired key, UUID, or ``PLAT-T-10`` reference to UUIDs (ADR-1237 §5).
283
+
284
+ Read-only, no side effects, no audit event. Anything the caller cannot read
285
+ — or that does not exist — is the same ``404 {"detail": "Not found."}``.
286
+ """
287
+
288
+ permission_classes = [IsAuthenticated] # noqa: RUF012
289
+ throttle_classes = [ScopedRateThrottle] # noqa: RUF012
290
+ throttle_scope = "resolve"
291
+
292
+ @extend_schema(
293
+ summary="Resolve a project/program key or reference to its ids",
294
+ description=(
295
+ "`ref` is a current key, a retired key, a UUID, or `<KEY>-<T|SP|R>-<n>` "
296
+ "(project kind only). A hyphenated legacy key parses on the rightmost "
297
+ "marker: `GA-SEC-T-10` is task 10 of `GA-SEC`. `key` and `canonical_ref` "
298
+ "are always the CURRENT form, so a client holding a retired key learns "
299
+ "the new one.\n\n"
300
+ "Visibility is the lookup: only projects/programs the caller is a live "
301
+ "member of, intersected with a project/program token's scope, can match. "
302
+ 'Everything else — missing, hidden, out of token scope — is `404 {"detail": '
303
+ '"Not found."}`, byte-identical. Throttle scope `resolve`.'
304
+ ),
305
+ parameters=[
306
+ OpenApiParameter("kind", str, OpenApiParameter.QUERY, required=True, enum=list(KINDS)),
307
+ OpenApiParameter("ref", str, OpenApiParameter.QUERY, required=True),
308
+ ],
309
+ responses={
310
+ 200: RESOLVE_RESPONSE,
311
+ 400: OpenApiResponse(
312
+ response=_ERROR_RESPONSE, description="Missing or invalid kind/ref."
313
+ ),
314
+ 404: OpenApiResponse(
315
+ response=_ERROR_RESPONSE,
316
+ description="Nothing the caller can read matches `ref`.",
317
+ ),
318
+ },
319
+ )
320
+ def get(self, request: Request) -> Response:
321
+ kind = _kind(request)
322
+ if kind is None:
323
+ return _bad_request("kind must be 'project' or 'program'.")
324
+ ref = (request.query_params.get("ref") or "").strip()
325
+ if not ref or len(ref) > _MAX_REF_LENGTH:
326
+ return _bad_request("ref is required.")
327
+ body = self._resolve(request, kind, ref)
328
+ if body is None:
329
+ return Response(dict(_NOT_FOUND_BODY), status=status.HTTP_404_NOT_FOUND)
330
+ return Response(body)
331
+
332
+ def _resolve(self, request: Request, kind: str, ref: str) -> dict[str, Any] | None:
333
+ if kind == KIND_PROGRAM:
334
+ programs = readable_programs(request)
335
+ try:
336
+ program = programs.filter(pk=uuid.UUID(ref)).first()
337
+ except ValueError:
338
+ program = _match_key(programs, KIND_PROGRAM, ref)
339
+ return _program_body(program) if program is not None else None
340
+
341
+ projects = readable_projects(request)
342
+ try:
343
+ project = projects.filter(pk=uuid.UUID(ref)).first()
344
+ except ValueError:
345
+ project = None
346
+ else:
347
+ return _project_body(project) if project is not None else None
348
+
349
+ # The whole ref as a key first: a grandfathered code can itself end in
350
+ # something marker-shaped, and addressing the project must win.
351
+ project = _match_key(projects, KIND_PROJECT, ref)
352
+ if project is not None:
353
+ return _project_body(project)
354
+ match = _REF_RE.fullmatch(ref)
355
+ if match is None:
356
+ return None
357
+ project = _match_key(projects, KIND_PROJECT, match["key"])
358
+ if project is None:
359
+ return None
360
+ return _resolve_child(project, match["marker"].upper(), int(match["n"]))
361
+
362
+
363
+ class KeySuggestionView(_TokenReachableView):
364
+ """Suggest a key from a name, or check one key's availability (ADR-1237 §6).
365
+
366
+ **This is an existence oracle, and that is accepted.** A workspace-unique
367
+ namespace cannot refuse a taken key without saying it is taken, so a caller
368
+ learns that *some* project holds ``ACME`` — never which one, its name, or its
369
+ UUID. Scoping this to the caller's membership would silently allow duplicate
370
+ keys; do not "fix" it. Throttled in the same ``resolve`` bucket.
371
+ """
372
+
373
+ permission_classes = [IsAuthenticated] # noqa: RUF012
374
+ throttle_classes = [ScopedRateThrottle] # noqa: RUF012
375
+ throttle_scope = "resolve"
376
+
377
+ @extend_schema(
378
+ summary="Suggest a project/program key, or check one for availability",
379
+ description=(
380
+ "Send `name` to get `{suggestion}` derived from it (`Platform` → `PLAT`, "
381
+ "then `PLAT2` when taken). Send `key` to get `{available, reason, "
382
+ "suggestion}`. A key is taken if any project (or program) holds or ever "
383
+ "held it — keys are never reissued. The response never names the holder. "
384
+ "`object_id` (optional, with `key`) is the project/program being renamed: "
385
+ "its own current and retired keys are reported available, because "
386
+ "renaming back is allowed. Throttle scope `resolve`."
387
+ ),
388
+ parameters=[
389
+ OpenApiParameter("kind", str, OpenApiParameter.QUERY, required=True, enum=list(KINDS)),
390
+ OpenApiParameter("name", str, OpenApiParameter.QUERY, required=False),
391
+ OpenApiParameter("key", str, OpenApiParameter.QUERY, required=False),
392
+ OpenApiParameter("object_id", str, OpenApiParameter.QUERY, required=False),
393
+ ],
394
+ responses={
395
+ 200: KEY_SUGGESTION_RESPONSE,
396
+ 400: OpenApiResponse(
397
+ response=_ERROR_RESPONSE, description="Missing kind, or neither name nor key."
398
+ ),
399
+ },
400
+ )
401
+ def get(self, request: Request) -> Response:
402
+ from trueppm_api.apps.projects.services import (
403
+ KeyAssignmentError,
404
+ derive_key,
405
+ is_key_taken,
406
+ suggest_free_key,
407
+ )
408
+
409
+ kind = _kind(request)
410
+ if kind is None:
411
+ return _bad_request("kind must be 'project' or 'program'.")
412
+ raw_key = (request.query_params.get("key") or "").strip()
413
+ name = (request.query_params.get("name") or "").strip()
414
+ try:
415
+ if raw_key:
416
+ if len(raw_key) > _MAX_REF_LENGTH:
417
+ return _bad_request("key is too long.")
418
+ key = normalize_key(raw_key, kind)
419
+ problem = key_format_error(key, kind)
420
+ if problem is not None:
421
+ reserved = is_reserved(key)
422
+ base = key if reserved else base_key_for(raw_key, kind)
423
+ return Response(
424
+ {
425
+ "available": False,
426
+ "reason": "reserved" if reserved else "invalid",
427
+ "suggestion": suggest_free_key(base, kind),
428
+ }
429
+ )
430
+ exclude = self._renamed_object(request, kind)
431
+ if is_key_taken(kind, key, exclude=exclude):
432
+ return Response(
433
+ {
434
+ "available": False,
435
+ "reason": "taken",
436
+ "suggestion": suggest_free_key(key, kind),
437
+ }
438
+ )
439
+ return Response({"available": True, "reason": None, "suggestion": key})
440
+ if name:
441
+ if len(name) > 255:
442
+ return _bad_request("name is too long.")
443
+ return Response({"suggestion": derive_key(name, kind)})
444
+ except KeyAssignmentError as exc:
445
+ # Namespace-squatting cap (security-review Low, perf-check Low):
446
+ # next_free_key() gives up after MAX_SUFFIX_ATTEMPTS rather than
447
+ # looping forever. A 400 the caller can act on (pick their own key),
448
+ # never a 500.
449
+ return _bad_request(str(exc))
450
+ return _bad_request("Send name or key.")
451
+
452
+ def _renamed_object(self, request: Request, kind: str) -> Any:
453
+ """The object being renamed, when the caller can read it; else ``None``.
454
+
455
+ Resolved through the same readable querysets as the resolver, so naming
456
+ an object you cannot see simply has no effect.
457
+ """
458
+ raw = (request.query_params.get("object_id") or "").strip()
459
+ if not raw:
460
+ return None
461
+ try:
462
+ pk = uuid.UUID(raw)
463
+ except ValueError:
464
+ return None
465
+ qs: QuerySet[Any] = (
466
+ readable_programs(request) if kind == KIND_PROGRAM else readable_projects(request)
467
+ )
468
+ return qs.filter(pk=pk).first()
@@ -0,0 +1,205 @@
1
+ """Pure key rules for project and program keys (ADR-1237).
2
+
3
+ A key is the human address of a project (``PLAT``) or program
4
+ (``atlas-platform-launch``), stored denormalized as ``code`` and, with its whole
5
+ history, as ``ObjectKey`` rows. This module holds only the rules that need no
6
+ database: format, reserved words, normalization, the derivation of a base key
7
+ from a name, and the suffixing that makes a base key free. It is deliberately
8
+ ORM-free so the migration repair (``projects.backfill``) can reuse exactly the
9
+ rules the live service applies — two copies of a derivation rule drift, and a
10
+ backfill that derives ``PLAT`` differently from the create path would leave the
11
+ upgrade and the next create disagreeing about what a name's key is.
12
+
13
+ The database-aware halves (``derive_key``, ``assign_key``) live in
14
+ ``apps.projects.services``.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import re
20
+ import unicodedata
21
+ from collections.abc import Callable
22
+
23
+ KIND_PROJECT = "project"
24
+ KIND_PROGRAM = "program"
25
+ KINDS = (KIND_PROJECT, KIND_PROGRAM)
26
+
27
+ #: New project keys: a letter, then 1-9 letters or digits. No hyphen, so a
28
+ #: reference is ``-``-separated like Jira and the shipped ``PLAT-R-7``.
29
+ PROJECT_KEY_RE = re.compile(r"[A-Z][A-Z0-9]{1,9}")
30
+ PROJECT_KEY_MAX = 10
31
+ #: Pre-ADR-1237 project codes (hyphens allowed, <=12). Grandfathered: a stored
32
+ #: value in this shape is never rewritten and still resolves, but no *new* value
33
+ #: may take it.
34
+ LEGACY_PROJECT_CODE_RE = re.compile(r"[A-Z0-9](?:[A-Z0-9-]*[A-Z0-9])?")
35
+ LEGACY_PROJECT_CODE_MAX = 12
36
+ #: Program keys stay slugs, because seed replace keys on ``program.slug``.
37
+ PROGRAM_KEY_RE = re.compile(r"[a-z0-9](?:[a-z0-9-]*[a-z0-9])?")
38
+ PROGRAM_KEY_MAX = 40
39
+
40
+ #: Route segments a key must never shadow (``/projects/new``, ``/projects/trash``).
41
+ RESERVED_KEYS = frozenset({"new", "settings", "trash"})
42
+ _UUID_SHAPED = re.compile(
43
+ r"[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}", re.IGNORECASE
44
+ )
45
+
46
+ #: The DoS bound on renames (ADR-1237 threat model, D): a rename loop would
47
+ #: otherwise reserve keys forever.
48
+ MAX_RETIRED_KEYS = 10
49
+
50
+ #: Cap on how many candidates :func:`next_free_key` will probe (security-review
51
+ #: Low, perf-check Low). Without a bound, namespace squatting — deliberately
52
+ #: registering every key in a popular name's suffix run (``PLAT``, ``PLAT2``,
53
+ #: ``PLAT3``, …) — turns another user's create or rename into an unbounded
54
+ #: loop of 1-2 queries per candidate. 1000 is far past any real collision run
55
+ #: (the suffix cap already trims the base to fit ``PROJECT_KEY_MAX``/
56
+ #: ``PROGRAM_KEY_MAX``, so exhausting it needs 1000 deliberately-squatted keys
57
+ #: for one base) and still resolves in well under the ``resolve`` throttle's
58
+ #: per-request budget.
59
+ MAX_SUFFIX_ATTEMPTS = 1000
60
+
61
+ # The en dash matches the UX copy (ADR-1237 UX spec §1) the web renders verbatim.
62
+ PROJECT_FORMAT_MESSAGE = "Letters and digits only, starting with a letter, 2–10 characters." # noqa: RUF001
63
+ PROGRAM_FORMAT_MESSAGE = "Lowercase letters, digits and hyphens, up to 40 characters."
64
+ RESERVED_MESSAGE = "That word is reserved."
65
+ IN_USE_MESSAGE = "This key is already in use."
66
+ CAP_MESSAGE = (
67
+ f"This key has been changed {MAX_RETIRED_KEYS} times, the most allowed. "
68
+ "Contact your workspace admin."
69
+ )
70
+
71
+ _PROJECT_FALLBACK = "PROJ"
72
+ _PROGRAM_FALLBACK = "program"
73
+
74
+ SUFFIX_EXHAUSTED_MESSAGE = "Couldn't find a free key from this name — please choose one."
75
+
76
+
77
+ class KeyAssignmentError(ValueError):
78
+ """A key write refused for a caller-correctable reason; always a ``400`` on ``code``.
79
+
80
+ Defined here rather than in ``services.py`` (its main call site and the
81
+ module every existing ``except KeyAssignmentError`` imports it from)
82
+ because :func:`next_free_key` below must be able to raise it, and this
83
+ module is deliberately import-light (see the module docstring) — nothing
84
+ here may depend on ``services.py``. ``services.py`` re-exports this name,
85
+ so every existing ``from trueppm_api.apps.projects.services import
86
+ KeyAssignmentError`` keeps working unchanged.
87
+
88
+ The message never names the object holding a taken key — the existence
89
+ oracle ADR-1237 §6 accepts leaks *that* a key is taken, never *by what*.
90
+ """
91
+
92
+
93
+ def is_uuid_shaped(value: str) -> bool:
94
+ """True when ``value`` would read as a UUID (hyphenated or bare hex)."""
95
+ return bool(_UUID_SHAPED.fullmatch(value))
96
+
97
+
98
+ def normalize_key(key: str, kind: str) -> str:
99
+ """Canonical case for ``kind``: project keys upper, program keys lower.
100
+
101
+ Comparison is case-insensitive either way; normalizing on write means the
102
+ stored form is what a user would have typed into the UI, which uppercases
103
+ (project) or lowercases (program) as they type.
104
+ """
105
+ stripped = (key or "").strip()
106
+ return stripped.upper() if kind == KIND_PROJECT else stripped.lower()
107
+
108
+
109
+ def key_format_error(key: str, kind: str) -> str | None:
110
+ """Why ``key`` cannot be a *new* key of ``kind``, or ``None`` when it can.
111
+
112
+ Expects an already-normalized key. Reserved words and UUID-shaped values are
113
+ checked for both kinds: a key must never shadow a route segment or be
114
+ mistaken for the UUID it stands in for.
115
+ """
116
+ if kind == KIND_PROJECT:
117
+ if not PROJECT_KEY_RE.fullmatch(key):
118
+ return PROJECT_FORMAT_MESSAGE
119
+ elif len(key) > PROGRAM_KEY_MAX or not PROGRAM_KEY_RE.fullmatch(key):
120
+ return PROGRAM_FORMAT_MESSAGE
121
+ if key.lower() in RESERVED_KEYS or is_uuid_shaped(key):
122
+ return RESERVED_MESSAGE
123
+ return None
124
+
125
+
126
+ def is_reserved(key: str) -> bool:
127
+ """True for a reserved word or a UUID-shaped value (either kind)."""
128
+ return key.lower() in RESERVED_KEYS or is_uuid_shaped(key)
129
+
130
+
131
+ def _ascii(value: str) -> str:
132
+ return unicodedata.normalize("NFKD", value or "").encode("ascii", "ignore").decode("ascii")
133
+
134
+
135
+ def base_key_for(name: str, kind: str) -> str:
136
+ """The un-suffixed key a ``name`` suggests. Always a valid new key of ``kind``.
137
+
138
+ Project: the initials of a multi-word name (``Platform Migration`` → ``PM``),
139
+ or the first four characters of a single word (``Platform`` → ``PLAT``). A
140
+ result that would start with a digit gets a ``P`` prefix. Program: the name
141
+ slugified to ``[a-z0-9-]``, capped at 40.
142
+
143
+ Reserved and UUID-shaped results are *not* rejected here — they are valid
144
+ shapes that :func:`next_free_key` steps past with a suffix, the same way it
145
+ steps past a taken key.
146
+ """
147
+ if kind == KIND_PROJECT:
148
+ words = re.findall(r"[A-Za-z0-9]+", _ascii(name))
149
+ if len(words) >= 2:
150
+ base = "".join(word[0] for word in words)
151
+ elif words:
152
+ base = words[0][:4]
153
+ else:
154
+ base = ""
155
+ base = base.upper()
156
+ if base and base[0].isdigit():
157
+ base = f"P{base}"
158
+ base = base[:PROJECT_KEY_MAX]
159
+ if len(base) < 2:
160
+ # One-letter initials ("X") or nothing usable at all.
161
+ base = (base + words[0][1:4].upper()) if words and len(words[0]) > 1 else ""
162
+ base = base[:PROJECT_KEY_MAX]
163
+ if not PROJECT_KEY_RE.fullmatch(base):
164
+ base = _PROJECT_FALLBACK
165
+ return base
166
+ slug = re.sub(r"[^a-z0-9]+", "-", _ascii(name).lower()).strip("-")
167
+ slug = slug[:PROGRAM_KEY_MAX].rstrip("-")
168
+ return slug or _PROGRAM_FALLBACK
169
+
170
+
171
+ def _suffixed(base: str, n: int, kind: str) -> str:
172
+ if kind == KIND_PROJECT:
173
+ tail = str(n)
174
+ return base[: PROJECT_KEY_MAX - len(tail)] + tail
175
+ tail = f"-{n}"
176
+ return base[: PROGRAM_KEY_MAX - len(tail)].rstrip("-") + tail
177
+
178
+
179
+ def next_free_key(base: str, kind: str, is_taken: Callable[[str], bool]) -> str:
180
+ """``base`` if free, else the first free ``base2``/``base3``… (``base-2`` for programs).
181
+
182
+ ``is_taken`` is asked about each candidate; reserved and UUID-shaped
183
+ candidates are skipped without asking. The suffix trims the base rather than
184
+ exceeding the length cap, so ``ABCDEFGHIJ`` steps to ``ABCDEFGHI2``.
185
+
186
+ Raises:
187
+ KeyAssignmentError: after :data:`MAX_SUFFIX_ATTEMPTS` candidates are all
188
+ reserved or taken (security-review Low, perf-check Low). Without a
189
+ bound, namespace squatting — pre-registering every suffix of a
190
+ popular base — turns another caller's create or rename into an
191
+ unbounded loop of 1-2 queries per candidate. Every caller of this
192
+ function already sits behind (or is wrapped to sit behind) a
193
+ ``KeyAssignmentError`` handler that turns it into a ``400`` on
194
+ ``code``, so this is a caller-correctable refusal, not a crash.
195
+ """
196
+ candidate = base
197
+ n = 1
198
+ attempts = 1
199
+ while is_reserved(candidate) or is_taken(candidate):
200
+ if attempts >= MAX_SUFFIX_ATTEMPTS:
201
+ raise KeyAssignmentError(SUFFIX_EXHAUSTED_MESSAGE)
202
+ n += 1
203
+ candidate = _suffixed(base, n, kind)
204
+ attempts += 1
205
+ return candidate