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.
- trueppm_api/__init__.py +27 -0
- trueppm_api/apps/access/__init__.py +0 -0
- trueppm_api/apps/access/apps.py +19 -0
- trueppm_api/apps/access/groups.py +518 -0
- trueppm_api/apps/access/management/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/__init__.py +0 -0
- trueppm_api/apps/access/management/commands/create_admin.py +346 -0
- trueppm_api/apps/access/management/commands/migrate_locked.py +96 -0
- trueppm_api/apps/access/management/commands/revoke_api_tokens.py +483 -0
- trueppm_api/apps/access/migrations/0001_initial.py +61 -0
- trueppm_api/apps/access/migrations/0001_squashed_0012_programmembership_role_title.py +184 -0
- trueppm_api/apps/access/migrations/0002_alter_projectmembership_id.py +29 -0
- trueppm_api/apps/access/migrations/0003_projectmembership_soft_delete.py +24 -0
- trueppm_api/apps/access/migrations/0004_alter_projectmembership_role.py +34 -0
- trueppm_api/apps/access/migrations/0005_program_entity_and_membership.py +76 -0
- trueppm_api/apps/access/migrations/0006_role_ordinal_spacing.py +113 -0
- trueppm_api/apps/access/migrations/0007_projectmembership_joined_at_and_more.py +35 -0
- trueppm_api/apps/access/migrations/0008_projectmembership_source_group.py +25 -0
- trueppm_api/apps/access/migrations/0009_projectmembership_pm_proj_serverver_idx.py +20 -0
- trueppm_api/apps/access/migrations/0010_programmembership_joined_at_and_more.py +38 -0
- trueppm_api/apps/access/migrations/0011_alter_programmembership_unique_together_and_more.py +36 -0
- trueppm_api/apps/access/migrations/0012_programmembership_role_title.py +17 -0
- trueppm_api/apps/access/migrations/0013_squashed_0019_release_0_4.py +302 -0
- trueppm_api/apps/access/migrations/0013_userdefinedmentiongroup.py +82 -0
- trueppm_api/apps/access/migrations/0014_programmembership_progm_serverver_idx.py +19 -0
- trueppm_api/apps/access/migrations/0015_programuserdefinedmentiongroup.py +86 -0
- trueppm_api/apps/access/migrations/0016_externalstakeholder.py +67 -0
- trueppm_api/apps/access/migrations/0017_viewer_ordinal_one.py +143 -0
- trueppm_api/apps/access/migrations/0018_programmembership_sync_seq_and_more.py +52 -0
- trueppm_api/apps/access/migrations/0019_programmembership_reinstated_at_and_more.py +22 -0
- trueppm_api/apps/access/migrations/__init__.py +0 -0
- trueppm_api/apps/access/minter_role.py +38 -0
- trueppm_api/apps/access/models.py +588 -0
- trueppm_api/apps/access/permissions.py +3263 -0
- trueppm_api/apps/access/serializers.py +1007 -0
- trueppm_api/apps/access/services.py +878 -0
- trueppm_api/apps/access/signals.py +171 -0
- trueppm_api/apps/access/tasks.py +50 -0
- trueppm_api/apps/access/throttles.py +96 -0
- trueppm_api/apps/access/urls.py +191 -0
- trueppm_api/apps/access/views.py +1965 -0
- trueppm_api/apps/agents/__init__.py +6 -0
- trueppm_api/apps/agents/apps.py +11 -0
- trueppm_api/apps/agents/canonical.py +73 -0
- trueppm_api/apps/agents/deferred.py +108 -0
- trueppm_api/apps/agents/management/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/__init__.py +0 -0
- trueppm_api/apps/agents/management/commands/audit_prune.py +137 -0
- trueppm_api/apps/agents/management/commands/audit_verify.py +101 -0
- trueppm_api/apps/agents/middleware.py +44 -0
- trueppm_api/apps/agents/migrations/0001_initial.py +231 -0
- trueppm_api/apps/agents/migrations/0001_squashed_0003_release_0_4.py +344 -0
- trueppm_api/apps/agents/migrations/0002_agentactioncheckpoint.py +71 -0
- trueppm_api/apps/agents/migrations/0003_agentactionrefusaldetail.py +58 -0
- trueppm_api/apps/agents/migrations/__init__.py +0 -0
- trueppm_api/apps/agents/models.py +371 -0
- trueppm_api/apps/agents/refusal.py +103 -0
- trueppm_api/apps/agents/serializers.py +85 -0
- trueppm_api/apps/agents/services.py +397 -0
- trueppm_api/apps/agents/signals.py +30 -0
- trueppm_api/apps/agents/urls.py +15 -0
- trueppm_api/apps/agents/views.py +109 -0
- trueppm_api/apps/csvimport/__init__.py +12 -0
- trueppm_api/apps/csvimport/apps.py +11 -0
- trueppm_api/apps/csvimport/mapping.py +363 -0
- trueppm_api/apps/csvimport/migrations/0001_initial.py +74 -0
- trueppm_api/apps/csvimport/migrations/0001_squashed_0003_release_0_4.py +90 -0
- trueppm_api/apps/csvimport/migrations/0002_schedule_outline_batch_operations.py +37 -0
- trueppm_api/apps/csvimport/migrations/0003_csvimportrequest_date_order_and_more.py +26 -0
- trueppm_api/apps/csvimport/migrations/__init__.py +0 -0
- trueppm_api/apps/csvimport/models.py +111 -0
- trueppm_api/apps/csvimport/parser.py +1825 -0
- trueppm_api/apps/csvimport/services.py +48 -0
- trueppm_api/apps/csvimport/tasks.py +459 -0
- trueppm_api/apps/csvimport/template.py +37 -0
- trueppm_api/apps/csvimport/urls.py +50 -0
- trueppm_api/apps/csvimport/views.py +793 -0
- trueppm_api/apps/history/__init__.py +0 -0
- trueppm_api/apps/history/apps.py +12 -0
- trueppm_api/apps/history/changelog.py +359 -0
- trueppm_api/apps/history/diff_policy.py +178 -0
- trueppm_api/apps/history/serializers.py +104 -0
- trueppm_api/apps/history/signals.py +82 -0
- trueppm_api/apps/history/tasks.py +105 -0
- trueppm_api/apps/history/urls.py +38 -0
- trueppm_api/apps/history/views.py +443 -0
- trueppm_api/apps/idempotency/__init__.py +0 -0
- trueppm_api/apps/idempotency/apps.py +11 -0
- trueppm_api/apps/idempotency/migrations/0001_initial.py +71 -0
- trueppm_api/apps/idempotency/migrations/__init__.py +0 -0
- trueppm_api/apps/idempotency/mixins.py +226 -0
- trueppm_api/apps/idempotency/models.py +75 -0
- trueppm_api/apps/idempotency/tasks.py +61 -0
- trueppm_api/apps/integrations/__init__.py +13 -0
- trueppm_api/apps/integrations/apps.py +59 -0
- trueppm_api/apps/integrations/connections.py +806 -0
- trueppm_api/apps/integrations/encryption.py +92 -0
- trueppm_api/apps/integrations/external_sources.py +863 -0
- trueppm_api/apps/integrations/git_automation_services.py +231 -0
- trueppm_api/apps/integrations/git_webhook_auth.py +243 -0
- trueppm_api/apps/integrations/http.py +597 -0
- trueppm_api/apps/integrations/me_work.py +218 -0
- trueppm_api/apps/integrations/migrations/0001_initial.py +64 -0
- trueppm_api/apps/integrations/migrations/0001_squashed_0006_tasklink_description_tasklink_preview_type_and_more.py +187 -0
- trueppm_api/apps/integrations/migrations/0002_tasklink.py +67 -0
- trueppm_api/apps/integrations/migrations/0003_tasklink_tasklink_serverver_idx.py +17 -0
- trueppm_api/apps/integrations/migrations/0004_tasklink_custom_title_tasklink_labels.py +25 -0
- trueppm_api/apps/integrations/migrations/0005_boardautomation.py +56 -0
- trueppm_api/apps/integrations/migrations/0006_tasklink_description_tasklink_preview_type_and_more.py +40 -0
- trueppm_api/apps/integrations/migrations/0007_squashed_0014_release_0_4.py +225 -0
- trueppm_api/apps/integrations/migrations/0007_tasklink_created_at.py +23 -0
- trueppm_api/apps/integrations/migrations/0008_integrationcredential_config_externalworkitem.py +66 -0
- trueppm_api/apps/integrations/migrations/0009_externalsyncrequest_and_more.py +96 -0
- trueppm_api/apps/integrations/migrations/0010_externalworkitem_due_date.py +17 -0
- trueppm_api/apps/integrations/migrations/0011_tasklink_sync_seq_tasklink_tasklink_syncseq_idx.py +22 -0
- trueppm_api/apps/integrations/migrations/0012_boardautomation_last_delivery_at_and_more.py +42 -0
- trueppm_api/apps/integrations/migrations/0013_externalsyncrequest_attempt_count_and_more.py +26 -0
- trueppm_api/apps/integrations/migrations/0014_tasklink_created_by.py +27 -0
- trueppm_api/apps/integrations/migrations/__init__.py +0 -0
- trueppm_api/apps/integrations/models.py +635 -0
- trueppm_api/apps/integrations/notification_channels.py +57 -0
- trueppm_api/apps/integrations/opengraph.py +229 -0
- trueppm_api/apps/integrations/outgoing.py +201 -0
- trueppm_api/apps/integrations/providers.py +528 -0
- trueppm_api/apps/integrations/registry.py +345 -0
- trueppm_api/apps/integrations/serializers.py +453 -0
- trueppm_api/apps/integrations/services.py +171 -0
- trueppm_api/apps/integrations/tasks.py +734 -0
- trueppm_api/apps/integrations/throttles.py +165 -0
- trueppm_api/apps/integrations/urls.py +77 -0
- trueppm_api/apps/integrations/views.py +1158 -0
- trueppm_api/apps/jiraimport/__init__.py +8 -0
- trueppm_api/apps/jiraimport/apps.py +11 -0
- trueppm_api/apps/jiraimport/migrations/0001_initial.py +72 -0
- trueppm_api/apps/jiraimport/migrations/__init__.py +0 -0
- trueppm_api/apps/jiraimport/models.py +70 -0
- trueppm_api/apps/jiraimport/parser.py +350 -0
- trueppm_api/apps/jiraimport/services.py +46 -0
- trueppm_api/apps/jiraimport/tasks.py +280 -0
- trueppm_api/apps/jiraimport/urls.py +15 -0
- trueppm_api/apps/jiraimport/views.py +153 -0
- trueppm_api/apps/msproject/__init__.py +0 -0
- trueppm_api/apps/msproject/apps.py +11 -0
- trueppm_api/apps/msproject/dataclasses.py +153 -0
- trueppm_api/apps/msproject/exporter.py +551 -0
- trueppm_api/apps/msproject/extended_attributes.py +77 -0
- trueppm_api/apps/msproject/importer.py +1095 -0
- trueppm_api/apps/msproject/migrations/0001_import_request.py +77 -0
- trueppm_api/apps/msproject/migrations/0001_squashed_0002_importrequest_creates_project.py +80 -0
- trueppm_api/apps/msproject/migrations/0002_importrequest_creates_project.py +15 -0
- trueppm_api/apps/msproject/migrations/__init__.py +0 -0
- trueppm_api/apps/msproject/models.py +81 -0
- trueppm_api/apps/msproject/parser.py +1276 -0
- trueppm_api/apps/msproject/serializers.py +136 -0
- trueppm_api/apps/msproject/services.py +58 -0
- trueppm_api/apps/msproject/tasks.py +439 -0
- trueppm_api/apps/msproject/urls.py +39 -0
- trueppm_api/apps/msproject/views.py +531 -0
- trueppm_api/apps/notifications/__init__.py +7 -0
- trueppm_api/apps/notifications/apps.py +15 -0
- trueppm_api/apps/notifications/backfill.py +79 -0
- trueppm_api/apps/notifications/categories.py +112 -0
- trueppm_api/apps/notifications/delivery_limits.py +258 -0
- trueppm_api/apps/notifications/digests.py +462 -0
- trueppm_api/apps/notifications/email_backend.py +350 -0
- trueppm_api/apps/notifications/email_health.py +128 -0
- trueppm_api/apps/notifications/migrations/0001_initial.py +207 -0
- trueppm_api/apps/notifications/migrations/0001_squashed_0006_notification_task.py +299 -0
- trueppm_api/apps/notifications/migrations/0002_project_notification_preference.py +67 -0
- trueppm_api/apps/notifications/migrations/0003_projectnotificationpreference_paused.py +17 -0
- trueppm_api/apps/notifications/migrations/0004_clean_unknown_matrix_keys.py +30 -0
- trueppm_api/apps/notifications/migrations/0005_notification_body_notification_event_type_and_more.py +27 -0
- trueppm_api/apps/notifications/migrations/0006_notification_task.py +25 -0
- trueppm_api/apps/notifications/migrations/0007_squashed_0011_release_0_4.py +204 -0
- trueppm_api/apps/notifications/migrations/0007_workspaceemailsettings.py +80 -0
- trueppm_api/apps/notifications/migrations/0008_notification_snoozed_until.py +17 -0
- trueppm_api/apps/notifications/migrations/0009_usernotificationsettings.py +43 -0
- trueppm_api/apps/notifications/migrations/0010_projectnotificationpreference_schema_version.py +20 -0
- trueppm_api/apps/notifications/migrations/0011_usernotificationsettings_digest_hour_and_more.py +96 -0
- trueppm_api/apps/notifications/migrations/__init__.py +0 -0
- trueppm_api/apps/notifications/models.py +1148 -0
- trueppm_api/apps/notifications/receivers.py +155 -0
- trueppm_api/apps/notifications/schema_migrations.py +56 -0
- trueppm_api/apps/notifications/serializers.py +1094 -0
- trueppm_api/apps/notifications/services.py +1489 -0
- trueppm_api/apps/notifications/tasks.py +762 -0
- trueppm_api/apps/notifications/throttles.py +98 -0
- trueppm_api/apps/notifications/urls.py +97 -0
- trueppm_api/apps/notifications/views.py +919 -0
- trueppm_api/apps/observability/__init__.py +0 -0
- trueppm_api/apps/observability/apps.py +31 -0
- trueppm_api/apps/observability/logging.py +213 -0
- trueppm_api/apps/observability/migrations/0001_initial.py +35 -0
- trueppm_api/apps/observability/migrations/0001_squashed_0003_alter_purgerun_trigger.py +154 -0
- trueppm_api/apps/observability/migrations/0002_purgerun_retentionpolicy_retentionschedule.py +131 -0
- trueppm_api/apps/observability/migrations/0003_alter_purgerun_trigger.py +21 -0
- trueppm_api/apps/observability/migrations/0004_alter_retentionpolicy_key.py +28 -0
- trueppm_api/apps/observability/migrations/__init__.py +0 -0
- trueppm_api/apps/observability/models.py +141 -0
- trueppm_api/apps/observability/otel/__init__.py +45 -0
- trueppm_api/apps/observability/otel/attributes.py +247 -0
- trueppm_api/apps/observability/otel/export_health.py +533 -0
- trueppm_api/apps/observability/otel/instrumentation.py +351 -0
- trueppm_api/apps/observability/otel/metrics.py +566 -0
- trueppm_api/apps/observability/otel/provider.py +511 -0
- trueppm_api/apps/observability/otel/request_attributes.py +204 -0
- trueppm_api/apps/observability/purge_registry.py +124 -0
- trueppm_api/apps/observability/retention.py +139 -0
- trueppm_api/apps/observability/selectors.py +993 -0
- trueppm_api/apps/observability/serializers.py +142 -0
- trueppm_api/apps/observability/services.py +407 -0
- trueppm_api/apps/observability/tasks.py +266 -0
- trueppm_api/apps/observability/urls.py +27 -0
- trueppm_api/apps/observability/views.py +537 -0
- trueppm_api/apps/profiles/__init__.py +0 -0
- trueppm_api/apps/profiles/apps.py +15 -0
- trueppm_api/apps/profiles/constants.py +54 -0
- trueppm_api/apps/profiles/migrations/0001_initial.py +55 -0
- trueppm_api/apps/profiles/migrations/0001_squashed_0004_userprofile_role_context.py +125 -0
- trueppm_api/apps/profiles/migrations/0002_userprofile_hidden_views.py +17 -0
- trueppm_api/apps/profiles/migrations/0003_projectvisit.py +63 -0
- trueppm_api/apps/profiles/migrations/0004_userprofile_role_context.py +26 -0
- trueppm_api/apps/profiles/migrations/0005_squashed_0009_release_0_4.py +128 -0
- trueppm_api/apps/profiles/migrations/0005_userprofile_schedule_in_deliver.py +20 -0
- trueppm_api/apps/profiles/migrations/0006_userprofile_date_format_userprofile_timezone.py +36 -0
- trueppm_api/apps/profiles/migrations/0007_userpin.py +91 -0
- trueppm_api/apps/profiles/migrations/0008_remove_userprofile_schedule_in_deliver.py +71 -0
- trueppm_api/apps/profiles/migrations/0009_auth_user_email_upper_idx.py +81 -0
- trueppm_api/apps/profiles/migrations/__init__.py +0 -0
- trueppm_api/apps/profiles/models.py +276 -0
- trueppm_api/apps/profiles/serializers.py +199 -0
- trueppm_api/apps/profiles/services.py +541 -0
- trueppm_api/apps/profiles/views.py +124 -0
- trueppm_api/apps/projects/__init__.py +0 -0
- trueppm_api/apps/projects/actual_date_rules.py +163 -0
- trueppm_api/apps/projects/amend.py +133 -0
- trueppm_api/apps/projects/apps.py +38 -0
- trueppm_api/apps/projects/asset_feed.py +459 -0
- trueppm_api/apps/projects/asset_views.py +317 -0
- trueppm_api/apps/projects/attachment_policy.py +278 -0
- trueppm_api/apps/projects/authentication.py +458 -0
- trueppm_api/apps/projects/backfill.py +459 -0
- trueppm_api/apps/projects/backlog_services.py +324 -0
- trueppm_api/apps/projects/backlog_views.py +302 -0
- trueppm_api/apps/projects/batch_operation_views.py +264 -0
- trueppm_api/apps/projects/blocker_services.py +510 -0
- trueppm_api/apps/projects/board_activity.py +462 -0
- trueppm_api/apps/projects/board_activity_views.py +186 -0
- trueppm_api/apps/projects/board_lanes.py +134 -0
- trueppm_api/apps/projects/bulk_settings.py +194 -0
- trueppm_api/apps/projects/bundled_templates.py +215 -0
- trueppm_api/apps/projects/calendar_settings.py +180 -0
- trueppm_api/apps/projects/ceremony_views.py +244 -0
- trueppm_api/apps/projects/commit_moment.py +212 -0
- trueppm_api/apps/projects/config_notice.py +1330 -0
- trueppm_api/apps/projects/custom_field_values.py +266 -0
- trueppm_api/apps/projects/decisions_services.py +132 -0
- trueppm_api/apps/projects/decisions_views.py +97 -0
- trueppm_api/apps/projects/estimation_scale.py +126 -0
- trueppm_api/apps/projects/export_bundle.py +423 -0
- trueppm_api/apps/projects/fixtures/seeds/atlas-platform-launch.json +4278 -0
- trueppm_api/apps/projects/fixtures/seeds/aurora-mobile-app.json +2001 -0
- trueppm_api/apps/projects/fixtures/seeds/bayside-civic-center.json +1827 -0
- trueppm_api/apps/projects/fixtures/seeds/ga-launch.json +1909 -0
- trueppm_api/apps/projects/fixtures/seeds/helios-crm-replacement.json +1410 -0
- trueppm_api/apps/projects/guardrail_policy_source.py +269 -0
- trueppm_api/apps/projects/inbound_sync.py +555 -0
- trueppm_api/apps/projects/iteration_label.py +85 -0
- trueppm_api/apps/projects/key_views.py +468 -0
- trueppm_api/apps/projects/keys.py +205 -0
- trueppm_api/apps/projects/lifecycle.py +191 -0
- trueppm_api/apps/projects/management/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/__init__.py +0 -0
- trueppm_api/apps/projects/management/commands/backfill_in_progress_status.py +92 -0
- trueppm_api/apps/projects/management/commands/create_demo_share_link.py +368 -0
- trueppm_api/apps/projects/management/commands/export_program.py +50 -0
- trueppm_api/apps/projects/management/commands/import_seed.py +165 -0
- trueppm_api/apps/projects/management/commands/load_sample_project.py +514 -0
- trueppm_api/apps/projects/management/commands/seed_capacity.py +406 -0
- trueppm_api/apps/projects/management/commands/seed_integration_fixtures.py +192 -0
- trueppm_api/apps/projects/management/commands/shift_sample_dates.py +120 -0
- trueppm_api/apps/projects/mcp_settings.py +305 -0
- trueppm_api/apps/projects/methodology.py +178 -0
- trueppm_api/apps/projects/migration_helpers.py +75 -0
- trueppm_api/apps/projects/migrations/0001_initial.py +208 -0
- trueppm_api/apps/projects/migrations/0001_squashed_0094_project_decisions_policy.py +5659 -0
- trueppm_api/apps/projects/migrations/0002_alter_uuid_pk_serialize_false.py +57 -0
- trueppm_api/apps/projects/migrations/0003_rename_task_project_index.py +25 -0
- trueppm_api/apps/projects/migrations/0004_soft_delete_and_dependency_versioned.py +70 -0
- trueppm_api/apps/projects/migrations/0005_task_assignee.py +33 -0
- trueppm_api/apps/projects/migrations/0006_task_utilization_index.py +31 -0
- trueppm_api/apps/projects/migrations/0007_task_is_milestone.py +28 -0
- trueppm_api/apps/projects/migrations/0008_baseline.py +119 -0
- trueppm_api/apps/projects/migrations/0009_historicaldependency_historicalproject_and_more.py +210 -0
- trueppm_api/apps/projects/migrations/0010_risk.py +229 -0
- trueppm_api/apps/projects/migrations/0011_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0012_task_status.py +27 -0
- trueppm_api/apps/projects/migrations/0013_task_planned_start.py +24 -0
- trueppm_api/apps/projects/migrations/0014_short_hex_ids.py +74 -0
- trueppm_api/apps/projects/migrations/0015_backfill_short_ids.py +48 -0
- trueppm_api/apps/projects/migrations/0016_task_actual_dates.py +42 -0
- trueppm_api/apps/projects/migrations/0017_boardcolumnconfig.py +43 -0
- trueppm_api/apps/projects/migrations/0018_estimation_governance.py +62 -0
- trueppm_api/apps/projects/migrations/0019_backfill_wbs_paths.py +28 -0
- trueppm_api/apps/projects/migrations/0020_task_status_5col.py +65 -0
- trueppm_api/apps/projects/migrations/0021_boardsavedview.py +64 -0
- trueppm_api/apps/projects/migrations/0022_task_status_changed_at_priority_rank.py +35 -0
- trueppm_api/apps/projects/migrations/0023_risk_framework_fields.py +96 -0
- trueppm_api/apps/projects/migrations/0024_riskcomment.py +52 -0
- trueppm_api/apps/projects/migrations/0025_sprint.py +352 -0
- trueppm_api/apps/projects/migrations/0026_project_methodology.py +30 -0
- trueppm_api/apps/projects/migrations/0027_sprint_retro.py +89 -0
- trueppm_api/apps/projects/migrations/0028_normalize_notes_field.py +42 -0
- trueppm_api/apps/projects/migrations/0029_complete_implies_full_progress.py +38 -0
- trueppm_api/apps/projects/migrations/0030_review_implies_full_progress.py +37 -0
- trueppm_api/apps/projects/migrations/0031_task_remaining_points.py +22 -0
- trueppm_api/apps/projects/migrations/0032_task_is_subtask_and_sprint_scope_change.py +90 -0
- trueppm_api/apps/projects/migrations/0033_task_my_work_index.py +38 -0
- trueppm_api/apps/projects/migrations/0034_inbound_task_sync.py +313 -0
- trueppm_api/apps/projects/migrations/0035_retro_versioned_and_suggested_assignee.py +173 -0
- trueppm_api/apps/projects/migrations/0036_program_entity_and_membership.py +157 -0
- trueppm_api/apps/projects/migrations/0037_sprint_capacity_points.py +20 -0
- trueppm_api/apps/projects/migrations/0038_taskcomment_commentreaction_commentacknowledgement_and_more.py +233 -0
- trueppm_api/apps/projects/migrations/0039_apitoken_and_more.py +88 -0
- trueppm_api/apps/projects/migrations/0040_program_general_fields.py +119 -0
- trueppm_api/apps/projects/migrations/0041_ceremony_template_and_phase_gate_config.py +210 -0
- trueppm_api/apps/projects/migrations/0041_project_general_fields.py +106 -0
- trueppm_api/apps/projects/migrations/0042_merge_20260522_0410.py +12 -0
- trueppm_api/apps/projects/migrations/0042_program_rollup_config.py +95 -0
- trueppm_api/apps/projects/migrations/0042_workflow_color_and_custom_fields.py +94 -0
- trueppm_api/apps/projects/migrations/0043_merge_20260522_1118.py +10 -0
- trueppm_api/apps/projects/migrations/0044_merge_20260522_rollup.py +10 -0
- trueppm_api/apps/projects/migrations/0044_project_archive_program_close.py +119 -0
- trueppm_api/apps/projects/migrations/0045_program_risk_policy.py +78 -0
- trueppm_api/apps/projects/migrations/0046_merge_20260523_2248.py +12 -0
- trueppm_api/apps/projects/migrations/0047_apitokenauditentry_program_and_more.py +55 -0
- trueppm_api/apps/projects/migrations/0048_historicalprogram_color_program_color.py +32 -0
- trueppm_api/apps/projects/migrations/0049_backlog_item.py +112 -0
- trueppm_api/apps/projects/migrations/0050_backlog_item_trgm_search.py +26 -0
- trueppm_api/apps/projects/migrations/0051_alter_calendar_working_days_alter_dependency_lag_and_more.py +144 -0
- trueppm_api/apps/projects/migrations/0052_recurring_tasks.py +278 -0
- trueppm_api/apps/projects/migrations/0053_dependency_dep_pred_serverver_idx_and_more.py +56 -0
- trueppm_api/apps/projects/migrations/0054_sprintscopechange_goal_impact_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0055_historicaltask_sprint_pending_and_more.py +51 -0
- trueppm_api/apps/projects/migrations/0056_historicalproject_prioritization_model_and_more.py +276 -0
- trueppm_api/apps/projects/migrations/0057_historicalsprint_binding_committed_snapshot_and_more.py +58 -0
- trueppm_api/apps/projects/migrations/0058_historicaltask_delivery_mode_and_more.py +72 -0
- trueppm_api/apps/projects/migrations/0059_forecastsnapshot.py +80 -0
- trueppm_api/apps/projects/migrations/0060_historicalprojectsignalprivacypolicy_and_more.py +90 -0
- trueppm_api/apps/projects/migrations/0061_baselinetask_story_points.py +17 -0
- trueppm_api/apps/projects/migrations/0062_project_is_sample.py +22 -0
- trueppm_api/apps/projects/migrations/0063_historicalsprint_wip_limit_sprint_wip_limit.py +22 -0
- trueppm_api/apps/projects/migrations/0064_sprinttaskoutcome.py +97 -0
- trueppm_api/apps/projects/migrations/0065_historicalsprint_goal_outcome_sprint_goal_outcome.py +32 -0
- trueppm_api/apps/projects/migrations/0066_historicalproject_lead_project_lead.py +38 -0
- trueppm_api/apps/projects/migrations/0067_sprint_exclude_from_velocity.py +22 -0
- trueppm_api/apps/projects/migrations/0068_project_recalculated_at.py +23 -0
- trueppm_api/apps/projects/migrations/0069_iteration_label.py +22 -0
- trueppm_api/apps/projects/migrations/0070_historicalprogram_iteration_label_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0071_backfill_iteration_label_inherit.py +35 -0
- trueppm_api/apps/projects/migrations/0072_pulseresponse_retroboarditem.py +129 -0
- trueppm_api/apps/projects/migrations/0073_sprinttaskoutcome_demo_ready.py +17 -0
- trueppm_api/apps/projects/migrations/0074_historicaltask_blocked_reason_task_blocked_reason.py +30 -0
- trueppm_api/apps/projects/migrations/0075_risk_decimal_short_id.py +42 -0
- trueppm_api/apps/projects/migrations/0076_sprinttaskoutcome_review_polish.py +45 -0
- trueppm_api/apps/projects/migrations/0077_task_blocker_fields.py +111 -0
- trueppm_api/apps/projects/migrations/0078_alter_risktask_unique_together_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0079_historicalproject_status_date_project_status_date.py +22 -0
- trueppm_api/apps/projects/migrations/0080_alter_historicaltask_type_alter_task_type.py +46 -0
- trueppm_api/apps/projects/migrations/0081_historicalprogram_allow_guests_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0082_project_last_sync_version.py +90 -0
- trueppm_api/apps/projects/migrations/0083_historicalprogram_mc_history_attribution_audience_and_more.py +108 -0
- trueppm_api/apps/projects/migrations/0084_tasknote.py +71 -0
- trueppm_api/apps/projects/migrations/0085_sprint_ix_sprint_velocity.py +33 -0
- trueppm_api/apps/projects/migrations/0086_board_card_search_trgm.py +33 -0
- trueppm_api/apps/projects/migrations/0087_historicalprogram_task_duration_change_percent_policy_and_more.py +155 -0
- trueppm_api/apps/projects/migrations/0088_historicalprogram_allowed_attachment_types_and_more.py +61 -0
- trueppm_api/apps/projects/migrations/0089_signalceilingraiseproposal_signalceilingraisevote_and_more.py +157 -0
- trueppm_api/apps/projects/migrations/0090_historicaltask_proj_histdate_index.py +59 -0
- trueppm_api/apps/projects/migrations/0091_project_board_cadence.py +39 -0
- trueppm_api/apps/projects/migrations/0092_program_target_date.py +22 -0
- trueppm_api/apps/projects/migrations/0093_cross_project_dependency_consent.py +58 -0
- trueppm_api/apps/projects/migrations/0094_project_decisions_policy.py +90 -0
- trueppm_api/apps/projects/migrations/0095_poker_session_vote.py +127 -0
- trueppm_api/apps/projects/migrations/0095_squashed_0097_release_0_3.py +225 -0
- trueppm_api/apps/projects/migrations/0096_crossprojectslipconflict.py +91 -0
- trueppm_api/apps/projects/migrations/0097_backlogitem_backlogitem_tags_gin_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0098_historicalproject_show_baselines_and_more.py +52 -0
- trueppm_api/apps/projects/migrations/0099_apitoken_scopes.py +48 -0
- trueppm_api/apps/projects/migrations/0100_task_dep_serverver_idx_concurrent.py +75 -0
- trueppm_api/apps/projects/migrations/0101_dependency_deleted_at_task_deleted_at.py +22 -0
- trueppm_api/apps/projects/migrations/0102_project_deleted_at_project_proj_isdel_deletedat_idx.py +25 -0
- trueppm_api/apps/projects/migrations/0103_taskdurationchangeevent_task_dur_evt_sprint_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0104_historicalproject_stale_task_threshold_days_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0104_trash_restore_1113.py +38 -0
- trueppm_api/apps/projects/migrations/0105_historical_changelog_indexes.py +76 -0
- trueppm_api/apps/projects/migrations/0106_boardsavedview_schema_version.py +20 -0
- trueppm_api/apps/projects/migrations/0106_squashed_0158_release_0_4.py +2357 -0
- trueppm_api/apps/projects/migrations/0107_task_activity_event.py +70 -0
- trueppm_api/apps/projects/migrations/0108_remove_apitoken_api_token_scope_xor_and_more.py +117 -0
- trueppm_api/apps/projects/migrations/0109_projectexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0110_sharelink.py +123 -0
- trueppm_api/apps/projects/migrations/0111_projectcalendarlayer.py +67 -0
- trueppm_api/apps/projects/migrations/0112_sharelink_expires_at_alter_sharelink_content_kind.py +31 -0
- trueppm_api/apps/projects/migrations/0113_alter_historicaltask_percent_complete_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0114_historicalproject_default_member_role_and_more.py +28 -0
- trueppm_api/apps/projects/migrations/0115_historicalproject_end_date_shift_threshold_days_and_more.py +22 -0
- trueppm_api/apps/projects/migrations/0116_alter_taskactivityevent_event_type.py +28 -0
- trueppm_api/apps/projects/migrations/0117_label_tasklabel_label_tasks_and_more.py +126 -0
- trueppm_api/apps/projects/migrations/0118_programexportjob.py +82 -0
- trueppm_api/apps/projects/migrations/0119_historicalprogram_calendar_program_calendar.py +36 -0
- trueppm_api/apps/projects/migrations/0120_alter_backlogitem_item_type.py +29 -0
- trueppm_api/apps/projects/migrations/0121_task_three_point_estimate_ordered.py +64 -0
- trueppm_api/apps/projects/migrations/0122_task_relation.py +173 -0
- trueppm_api/apps/projects/migrations/0123_remove_historicalproject_agile_features_and_more.py +20 -0
- trueppm_api/apps/projects/migrations/0124_dependency_is_driving.py +17 -0
- trueppm_api/apps/projects/migrations/0125_historicalprogram_estimation_scale_and_more.py +68 -0
- trueppm_api/apps/projects/migrations/0126_projectcustomfield_show_on_card_taskcustomfieldvalue.py +80 -0
- trueppm_api/apps/projects/migrations/0127_historicalprogram_mcp_enabled_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0128_acceptancecriterion_sync_seq_apitoken_sync_seq_and_more.py +192 -0
- trueppm_api/apps/projects/migrations/0129_sharelink_show_milestone_dates.py +20 -0
- trueppm_api/apps/projects/migrations/0130_programimportjob.py +85 -0
- trueppm_api/apps/projects/migrations/0131_historicalprogram_sprint_picker_ready_only_default_and_more.py +32 -0
- trueppm_api/apps/projects/migrations/0132_task_scheduled_start.py +17 -0
- trueppm_api/apps/projects/migrations/0133_project_status_date_floor_armed_at.py +17 -0
- trueppm_api/apps/projects/migrations/0134_task_edited_at_task_seeded_at_task_source_id_and_more.py +65 -0
- trueppm_api/apps/projects/migrations/0135_projecttemplate_templateapplication_and_more.py +175 -0
- trueppm_api/apps/projects/migrations/0136_schedule_outline_batch_operations.py +118 -0
- trueppm_api/apps/projects/migrations/0137_collapse_dual_scope_api_tokens.py +54 -0
- trueppm_api/apps/projects/migrations/0138_apitokenauditentry_api_token_audit_owner_idx.py +18 -0
- trueppm_api/apps/projects/migrations/0139_historicaltask_duration_unit_task_duration_unit.py +32 -0
- trueppm_api/apps/projects/migrations/0140_historicaltask_auto_container_and_more.py +113 -0
- trueppm_api/apps/projects/migrations/0141_historicalproject_draft_started_at_and_more.py +44 -0
- trueppm_api/apps/projects/migrations/0142_baseline_calendar_hours_per_day_and_more.py +31 -0
- trueppm_api/apps/projects/migrations/0143_sprintcloserequest_failure_reason_and_more.py +42 -0
- trueppm_api/apps/projects/migrations/0144_projecttemplate_source_project_and_more.py +35 -0
- trueppm_api/apps/projects/migrations/0145_seed_bundled_templates.py +65 -0
- trueppm_api/apps/projects/migrations/0146_historicaltask_board_lane_task_board_lane.py +32 -0
- trueppm_api/apps/projects/migrations/0147_structuraloperation.py +128 -0
- trueppm_api/apps/projects/migrations/0148_task_unique_task_wbs_path_per_project_live.py +76 -0
- trueppm_api/apps/projects/migrations/0149_drop_workshops_tables.py +42 -0
- trueppm_api/apps/projects/migrations/0150_clear_uncommitted_cpm_output.py +130 -0
- trueppm_api/apps/projects/migrations/0151_config_notice_request.py +70 -0
- trueppm_api/apps/projects/migrations/0152_aggregate_task_wbs_name_idx.py +20 -0
- trueppm_api/apps/projects/migrations/0153_program_sample_anchor_date.py +22 -0
- trueppm_api/apps/projects/migrations/0154_object_keys.py +184 -0
- trueppm_api/apps/projects/migrations/0155_task_milestone_at_day_end.py +20 -0
- trueppm_api/apps/projects/migrations/0156_program_import_job_replaced_project_ids.py +17 -0
- trueppm_api/apps/projects/migrations/0157_forecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/0158_baselinetask_finish_at_day_start.py +17 -0
- trueppm_api/apps/projects/migrations/__init__.py +0 -0
- trueppm_api/apps/projects/models.py +9209 -0
- trueppm_api/apps/projects/poker_services.py +154 -0
- trueppm_api/apps/projects/poker_views.py +398 -0
- trueppm_api/apps/projects/product_backlog_services.py +450 -0
- trueppm_api/apps/projects/program_label_services.py +94 -0
- trueppm_api/apps/projects/program_label_views.py +189 -0
- trueppm_api/apps/projects/program_rollup.py +903 -0
- trueppm_api/apps/projects/program_schedule.py +767 -0
- trueppm_api/apps/projects/program_views.py +3445 -0
- trueppm_api/apps/projects/project_templates.py +422 -0
- trueppm_api/apps/projects/receivers.py +234 -0
- trueppm_api/apps/projects/refusal_codes.py +140 -0
- trueppm_api/apps/projects/reorder_services.py +145 -0
- trueppm_api/apps/projects/restructure_hooks.py +54 -0
- trueppm_api/apps/projects/retro_board_services.py +368 -0
- trueppm_api/apps/projects/retro_services.py +285 -0
- trueppm_api/apps/projects/risk_import.py +396 -0
- trueppm_api/apps/projects/row_identity.py +113 -0
- trueppm_api/apps/projects/schema_migrations.py +228 -0
- trueppm_api/apps/projects/schemas/seed_v1.json +492 -0
- trueppm_api/apps/projects/schemas/seed_v2.json +891 -0
- trueppm_api/apps/projects/seed/__init__.py +61 -0
- trueppm_api/apps/projects/seed/demo_landing_overlay.py +532 -0
- trueppm_api/apps/projects/seed/exporter.py +1717 -0
- trueppm_api/apps/projects/seed/forecast_backfill.py +247 -0
- trueppm_api/apps/projects/seed/importer.py +2764 -0
- trueppm_api/apps/projects/seed/reanchor.py +551 -0
- trueppm_api/apps/projects/seed/reldates.py +126 -0
- trueppm_api/apps/projects/seed/replace.py +128 -0
- trueppm_api/apps/projects/seed/replay.py +1695 -0
- trueppm_api/apps/projects/seed/replay_ctx.py +41 -0
- trueppm_api/apps/projects/seed/samples.py +427 -0
- trueppm_api/apps/projects/seed/validation.py +1169 -0
- trueppm_api/apps/projects/serializers.py +12904 -0
- trueppm_api/apps/projects/services.py +7525 -0
- trueppm_api/apps/projects/share_serializers.py +142 -0
- trueppm_api/apps/projects/share_services.py +373 -0
- trueppm_api/apps/projects/share_views.py +395 -0
- trueppm_api/apps/projects/sharing_settings.py +187 -0
- trueppm_api/apps/projects/signal_privacy_services.py +910 -0
- trueppm_api/apps/projects/signal_privacy_views.py +485 -0
- trueppm_api/apps/projects/signals.py +132 -0
- trueppm_api/apps/projects/slip_conflict.py +288 -0
- trueppm_api/apps/projects/sprint_cadence.py +453 -0
- trueppm_api/apps/projects/sprint_picker_settings.py +132 -0
- trueppm_api/apps/projects/standup.py +304 -0
- trueppm_api/apps/projects/standup_views.py +99 -0
- trueppm_api/apps/projects/structural_operation_services.py +727 -0
- trueppm_api/apps/projects/structural_operation_views.py +311 -0
- trueppm_api/apps/projects/surface_visibility.py +126 -0
- trueppm_api/apps/projects/task_batch_services.py +379 -0
- trueppm_api/apps/projects/task_bulk.py +1244 -0
- trueppm_api/apps/projects/task_classification.py +561 -0
- trueppm_api/apps/projects/task_classification_defaults.py +95 -0
- trueppm_api/apps/projects/task_duration_settings.py +142 -0
- trueppm_api/apps/projects/task_grouping.py +754 -0
- trueppm_api/apps/projects/tasks.py +1979 -0
- trueppm_api/apps/projects/template_divergence.py +168 -0
- trueppm_api/apps/projects/template_services.py +160 -0
- trueppm_api/apps/projects/template_tasks.py +261 -0
- trueppm_api/apps/projects/template_views.py +829 -0
- trueppm_api/apps/projects/throttles.py +311 -0
- trueppm_api/apps/projects/urls.py +1016 -0
- trueppm_api/apps/projects/utilization.py +909 -0
- trueppm_api/apps/projects/views.py +21225 -0
- trueppm_api/apps/projects/wbs_paths.py +119 -0
- trueppm_api/apps/resources/__init__.py +0 -0
- trueppm_api/apps/resources/apps.py +8 -0
- trueppm_api/apps/resources/capacity.py +121 -0
- trueppm_api/apps/resources/migrations/0001_initial.py +68 -0
- trueppm_api/apps/resources/migrations/0001_squashed_0013_alter_projectresource_unique_together_and_more.py +281 -0
- trueppm_api/apps/resources/migrations/0002_alter_uuid_pk_serialize_false.py +36 -0
- trueppm_api/apps/resources/migrations/0003_resource_soft_delete.py +30 -0
- trueppm_api/apps/resources/migrations/0004_add_taskresource_resource_index.py +25 -0
- trueppm_api/apps/resources/migrations/0005_resource_job_role.py +20 -0
- trueppm_api/apps/resources/migrations/0006_skill.py +34 -0
- trueppm_api/apps/resources/migrations/0007_resource_skill.py +63 -0
- trueppm_api/apps/resources/migrations/0008_project_resource.py +63 -0
- trueppm_api/apps/resources/migrations/0009_task_skill_requirement.py +60 -0
- trueppm_api/apps/resources/migrations/0010_alter_resourceskill_options_and_more.py +20 -0
- trueppm_api/apps/resources/migrations/0011_alter_projectresource_notes.py +17 -0
- trueppm_api/apps/resources/migrations/0012_add_resource_user_fk.py +26 -0
- trueppm_api/apps/resources/migrations/0013_alter_projectresource_unique_together_and_more.py +53 -0
- trueppm_api/apps/resources/migrations/0014_projectresource_sync_seq_resource_sync_seq_and_more.py +37 -0
- trueppm_api/apps/resources/migrations/0014_squashed_0015_release_0_4.py +47 -0
- trueppm_api/apps/resources/migrations/0015_projectresource_deactivated_with_resource.py +17 -0
- trueppm_api/apps/resources/migrations/__init__.py +0 -0
- trueppm_api/apps/resources/models.py +298 -0
- trueppm_api/apps/resources/serializers.py +426 -0
- trueppm_api/apps/resources/services.py +465 -0
- trueppm_api/apps/resources/urls.py +28 -0
- trueppm_api/apps/resources/views.py +1752 -0
- trueppm_api/apps/scheduling/__init__.py +0 -0
- trueppm_api/apps/scheduling/apps.py +153 -0
- trueppm_api/apps/scheduling/calendars.py +144 -0
- trueppm_api/apps/scheduling/deadletter.py +91 -0
- trueppm_api/apps/scheduling/finish_reading.py +198 -0
- trueppm_api/apps/scheduling/forecast_history_settings.py +215 -0
- trueppm_api/apps/scheduling/forecast_staleness.py +313 -0
- trueppm_api/apps/scheduling/graph_guard.py +263 -0
- trueppm_api/apps/scheduling/management/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/__init__.py +0 -0
- trueppm_api/apps/scheduling/management/commands/prune_forecast_snapshots.py +53 -0
- trueppm_api/apps/scheduling/mc_admission.py +181 -0
- trueppm_api/apps/scheduling/migrations/0001_failed_task.py +62 -0
- trueppm_api/apps/scheduling/migrations/0001_squashed_0007_projectforecastsnapshot.py +309 -0
- trueppm_api/apps/scheduling/migrations/0002_schedulerequest.py +78 -0
- trueppm_api/apps/scheduling/migrations/0003_add_schedule_request_reason.py +27 -0
- trueppm_api/apps/scheduling/migrations/0004_velocity_suggestion.py +94 -0
- trueppm_api/apps/scheduling/migrations/0005_montecarlorun.py +61 -0
- trueppm_api/apps/scheduling/migrations/0006_montecarlorun_distribution.py +17 -0
- trueppm_api/apps/scheduling/migrations/0007_projectforecastsnapshot.py +65 -0
- trueppm_api/apps/scheduling/migrations/0008_alter_schedulerequest_reason.py +28 -0
- trueppm_api/apps/scheduling/migrations/0008_folded_schedulerequest_reason.py +34 -0
- trueppm_api/apps/scheduling/migrations/0009_alter_schedulerequest_reason.py +29 -0
- trueppm_api/apps/scheduling/migrations/0010_failedtask_resolution_note_failedtask_resolved_at_and_more.py +43 -0
- trueppm_api/apps/scheduling/migrations/0010_squashed_0016_release_0_4.py +106 -0
- trueppm_api/apps/scheduling/migrations/0011_failedtask_project.py +26 -0
- trueppm_api/apps/scheduling/migrations/0012_montecarlorun_diagnostic.py +17 -0
- trueppm_api/apps/scheduling/migrations/0013_montecarlorun_status_date.py +17 -0
- trueppm_api/apps/scheduling/migrations/0014_montecarlorun_plan_version.py +17 -0
- trueppm_api/apps/scheduling/migrations/0015_projectforecastsnapshot_cpm_finish_at_day_start.py +17 -0
- trueppm_api/apps/scheduling/migrations/0016_montecarlorun_finish_readings.py +32 -0
- trueppm_api/apps/scheduling/migrations/__init__.py +0 -0
- trueppm_api/apps/scheduling/models.py +542 -0
- trueppm_api/apps/scheduling/receivers.py +160 -0
- trueppm_api/apps/scheduling/risk_premium.py +334 -0
- trueppm_api/apps/scheduling/serializers.py +723 -0
- trueppm_api/apps/scheduling/services.py +1251 -0
- trueppm_api/apps/scheduling/signals.py +56 -0
- trueppm_api/apps/scheduling/tasks.py +2248 -0
- trueppm_api/apps/scheduling/telemetry.py +80 -0
- trueppm_api/apps/scheduling/units.py +35 -0
- trueppm_api/apps/scheduling/urls.py +57 -0
- trueppm_api/apps/scheduling/views.py +2908 -0
- trueppm_api/apps/sso/__init__.py +0 -0
- trueppm_api/apps/sso/apps.py +19 -0
- trueppm_api/apps/sso/extensions.py +130 -0
- trueppm_api/apps/sso/management/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/__init__.py +0 -0
- trueppm_api/apps/sso/management/commands/seed_sso_keycloak.py +172 -0
- trueppm_api/apps/sso/migrations/0001_initial.py +119 -0
- trueppm_api/apps/sso/migrations/0002_remove_oidcprovider_workspace_ssoproviderpolicy_and_more.py +169 -0
- trueppm_api/apps/sso/migrations/__init__.py +0 -0
- trueppm_api/apps/sso/models.py +148 -0
- trueppm_api/apps/sso/serializers.py +446 -0
- trueppm_api/apps/sso/services.py +1473 -0
- trueppm_api/apps/sso/urls.py +47 -0
- trueppm_api/apps/sso/views.py +935 -0
- trueppm_api/apps/sync/__init__.py +0 -0
- trueppm_api/apps/sync/apps.py +15 -0
- trueppm_api/apps/sync/backfill.py +208 -0
- trueppm_api/apps/sync/broadcast.py +369 -0
- trueppm_api/apps/sync/conflict.py +262 -0
- trueppm_api/apps/sync/consumers.py +548 -0
- trueppm_api/apps/sync/migrations/0001_initial.py +59 -0
- trueppm_api/apps/sync/migrations/0001_squashed_0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +77 -0
- trueppm_api/apps/sync/migrations/0002_remove_syncbatch_syncbatch_project_client_batch_uniq_and_more.py +41 -0
- trueppm_api/apps/sync/migrations/0003_boardevent.py +46 -0
- trueppm_api/apps/sync/migrations/0003_squashed_0005_release_0_4.py +77 -0
- trueppm_api/apps/sync/migrations/0004_seed_sync_seq.py +24 -0
- trueppm_api/apps/sync/migrations/0005_program_sync_sequence.py +35 -0
- trueppm_api/apps/sync/migrations/0006_boardevent_per_project_seq.py +77 -0
- trueppm_api/apps/sync/migrations/__init__.py +0 -0
- trueppm_api/apps/sync/models.py +272 -0
- trueppm_api/apps/sync/pagination.py +231 -0
- trueppm_api/apps/sync/sequence.py +488 -0
- trueppm_api/apps/sync/serializers.py +688 -0
- trueppm_api/apps/sync/tasks.py +260 -0
- trueppm_api/apps/sync/throttles.py +175 -0
- trueppm_api/apps/sync/upload.py +556 -0
- trueppm_api/apps/sync/urls.py +17 -0
- trueppm_api/apps/sync/views.py +957 -0
- trueppm_api/apps/sync/ws_auth.py +193 -0
- trueppm_api/apps/taskruns/__init__.py +0 -0
- trueppm_api/apps/taskruns/apps.py +11 -0
- trueppm_api/apps/taskruns/migrations/0001_initial.py +92 -0
- trueppm_api/apps/taskruns/migrations/__init__.py +0 -0
- trueppm_api/apps/taskruns/models.py +68 -0
- trueppm_api/apps/taskruns/serializers.py +92 -0
- trueppm_api/apps/taskruns/tasks.py +60 -0
- trueppm_api/apps/taskruns/tracker.py +286 -0
- trueppm_api/apps/taskruns/urls.py +46 -0
- trueppm_api/apps/taskruns/views.py +195 -0
- trueppm_api/apps/teams/__init__.py +8 -0
- trueppm_api/apps/teams/apps.py +16 -0
- trueppm_api/apps/teams/migrations/0001_initial.py +139 -0
- trueppm_api/apps/teams/migrations/0001_squashed_0002_default_teams.py +159 -0
- trueppm_api/apps/teams/migrations/0002_default_teams.py +62 -0
- trueppm_api/apps/teams/migrations/0003_team_sync_seq_teammembership_sync_seq.py +22 -0
- trueppm_api/apps/teams/migrations/0004_backfill_default_teams_on_upgrade.py +49 -0
- trueppm_api/apps/teams/migrations/__init__.py +0 -0
- trueppm_api/apps/teams/models.py +145 -0
- trueppm_api/apps/teams/permissions.py +149 -0
- trueppm_api/apps/teams/serializers.py +95 -0
- trueppm_api/apps/teams/services.py +440 -0
- trueppm_api/apps/teams/signals.py +61 -0
- trueppm_api/apps/teams/urls.py +34 -0
- trueppm_api/apps/teams/views.py +201 -0
- trueppm_api/apps/timetracking/__init__.py +0 -0
- trueppm_api/apps/timetracking/apps.py +9 -0
- trueppm_api/apps/timetracking/migrations/0001_initial.py +117 -0
- trueppm_api/apps/timetracking/migrations/0001_squashed_0004_release_0_4.py +167 -0
- trueppm_api/apps/timetracking/migrations/0002_timesheetsubmission.py +46 -0
- trueppm_api/apps/timetracking/migrations/0003_timeentry_deleted_at_timeentry_deleted_by.py +31 -0
- trueppm_api/apps/timetracking/migrations/0004_timeentry_sync_seq_and_more.py +24 -0
- trueppm_api/apps/timetracking/migrations/__init__.py +0 -0
- trueppm_api/apps/timetracking/models.py +178 -0
- trueppm_api/apps/timetracking/serializers.py +175 -0
- trueppm_api/apps/timetracking/services.py +211 -0
- trueppm_api/apps/timetracking/urls.py +47 -0
- trueppm_api/apps/timetracking/views.py +597 -0
- trueppm_api/apps/webhooks/__init__.py +0 -0
- trueppm_api/apps/webhooks/apps.py +8 -0
- trueppm_api/apps/webhooks/backfill.py +90 -0
- trueppm_api/apps/webhooks/dispatch.py +279 -0
- trueppm_api/apps/webhooks/migrations/0001_initial.py +146 -0
- trueppm_api/apps/webhooks/migrations/0001_squashed_0007_sprint_lifecycle_events.py +249 -0
- trueppm_api/apps/webhooks/migrations/0002_alter_webhook_events.py +32 -0
- trueppm_api/apps/webhooks/migrations/0003_delivery_status_created_idx.py +17 -0
- trueppm_api/apps/webhooks/migrations/0004_webhook_program_alter_webhook_project_and_more.py +64 -0
- trueppm_api/apps/webhooks/migrations/0005_webhook_delivery_sequence_numbers.py +30 -0
- trueppm_api/apps/webhooks/migrations/0006_webhook_format_alter_webhook_events_and_more.py +61 -0
- trueppm_api/apps/webhooks/migrations/0007_sprint_lifecycle_events.py +63 -0
- trueppm_api/apps/webhooks/migrations/0008_risk_baseline_comment_events.py +73 -0
- trueppm_api/apps/webhooks/migrations/0009_encrypt_webhook_secret_and_failure_counters.py +100 -0
- trueppm_api/apps/webhooks/migrations/0010_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/0011_webhookeditor.py +48 -0
- trueppm_api/apps/webhooks/migrations/0012_alter_webhook_disabled_at.py +21 -0
- trueppm_api/apps/webhooks/migrations/__init__.py +0 -0
- trueppm_api/apps/webhooks/models.py +437 -0
- trueppm_api/apps/webhooks/serializers.py +326 -0
- trueppm_api/apps/webhooks/tasks.py +680 -0
- trueppm_api/apps/webhooks/urls.py +22 -0
- trueppm_api/apps/webhooks/views.py +533 -0
- trueppm_api/apps/workflow_engine/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/apps.py +23 -0
- trueppm_api/apps/workflow_engine/migrations/0001_initial.py +230 -0
- trueppm_api/apps/workflow_engine/migrations/0001_squashed_0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +234 -0
- trueppm_api/apps/workflow_engine/migrations/0002_remove_workflowhistoryevent_workflow_history_lookup_idx.py +16 -0
- trueppm_api/apps/workflow_engine/migrations/0003_workflowoutboxrow_workflow_outbox_recovery_idx.py +21 -0
- trueppm_api/apps/workflow_engine/migrations/__init__.py +0 -0
- trueppm_api/apps/workflow_engine/models.py +265 -0
- trueppm_api/apps/workflow_engine/tasks.py +371 -0
- trueppm_api/apps/workspace/__init__.py +0 -0
- trueppm_api/apps/workspace/apps.py +10 -0
- trueppm_api/apps/workspace/backfill.py +111 -0
- trueppm_api/apps/workspace/export.py +254 -0
- trueppm_api/apps/workspace/migrations/0001_initial.py +326 -0
- trueppm_api/apps/workspace/migrations/0001_squashed_0014_workspacemembership_availability_effective_from_and_more.py +829 -0
- trueppm_api/apps/workspace/migrations/0002_remove_workspace_fiscal_year_start_and_more.py +85 -0
- trueppm_api/apps/workspace/migrations/0003_workspaceexportjob.py +73 -0
- trueppm_api/apps/workspace/migrations/0004_alter_workspaceinvite_status.py +27 -0
- trueppm_api/apps/workspace/migrations/0005_workspace_iteration_label_and_more.py +26 -0
- trueppm_api/apps/workspace/migrations/0006_alter_groupmembership_unique_together_and_more.py +45 -0
- trueppm_api/apps/workspace/migrations/0007_workspace_public_sharing_override_policy.py +21 -0
- trueppm_api/apps/workspace/migrations/0008_workspace_mc_history_attribution_audience_and_more.py +44 -0
- trueppm_api/apps/workspace/migrations/0009_workspace_methodology_and_more.py +186 -0
- trueppm_api/apps/workspace/migrations/0010_workspace_logo.py +38 -0
- trueppm_api/apps/workspace/migrations/0011_historicalworkspace_task_duration_change_percent_override_policy_and_more.py +56 -0
- trueppm_api/apps/workspace/migrations/0012_historicalworkspace_allowed_attachment_types_and_more.py +64 -0
- trueppm_api/apps/workspace/migrations/0013_auditevent.py +70 -0
- trueppm_api/apps/workspace/migrations/0014_workspacemembership_availability_effective_from_and_more.py +39 -0
- trueppm_api/apps/workspace/migrations/0015_squashed_0027_release_0_4.py +199 -0
- trueppm_api/apps/workspace/migrations/0015_trash_restore_1113.py +34 -0
- trueppm_api/apps/workspace/migrations/0016_historicalworkspace_calendar_and_more.py +55 -0
- trueppm_api/apps/workspace/migrations/0017_historicalworkspace_estimation_scale_and_more.py +38 -0
- trueppm_api/apps/workspace/migrations/0018_historicalworkspace_feedback_enabled_and_more.py +32 -0
- trueppm_api/apps/workspace/migrations/0019_historicalworkspace_mcp_enabled_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0020_viewer_ordinal_one.py +34 -0
- trueppm_api/apps/workspace/migrations/0021_group_sync_seq_groupmembership_sync_seq_and_more.py +27 -0
- trueppm_api/apps/workspace/migrations/0022_historicalworkspace_sprint_picker_ready_only_default_and_more.py +22 -0
- trueppm_api/apps/workspace/migrations/0023_alter_auditevent_event_type.py +32 -0
- trueppm_api/apps/workspace/migrations/0024_alter_auditevent_event_type.py +33 -0
- trueppm_api/apps/workspace/migrations/0025_alter_auditevent_event_type.py +36 -0
- trueppm_api/apps/workspace/migrations/0026_alter_auditevent_event_type.py +37 -0
- trueppm_api/apps/workspace/migrations/0027_alter_auditevent_event_type.py +42 -0
- trueppm_api/apps/workspace/migrations/__init__.py +0 -0
- trueppm_api/apps/workspace/models.py +857 -0
- trueppm_api/apps/workspace/permissions.py +187 -0
- trueppm_api/apps/workspace/serializers.py +590 -0
- trueppm_api/apps/workspace/services.py +967 -0
- trueppm_api/apps/workspace/signals.py +37 -0
- trueppm_api/apps/workspace/tasks.py +616 -0
- trueppm_api/apps/workspace/urls.py +113 -0
- trueppm_api/apps/workspace/views.py +1581 -0
- trueppm_api/asgi.py +49 -0
- trueppm_api/celery.py +40 -0
- trueppm_api/core/__init__.py +0 -0
- trueppm_api/core/admin_site.py +206 -0
- trueppm_api/core/auth_views.py +934 -0
- trueppm_api/core/constant_time.py +51 -0
- trueppm_api/core/csp.py +54 -0
- trueppm_api/core/db.py +87 -0
- trueppm_api/core/db_session.py +37 -0
- trueppm_api/core/demo_read_only.py +314 -0
- trueppm_api/core/exception_handlers.py +157 -0
- trueppm_api/core/export_downloads.py +56 -0
- trueppm_api/core/extension_providers.py +56 -0
- trueppm_api/core/extension_signals.py +147 -0
- trueppm_api/core/idempotent.py +260 -0
- trueppm_api/core/metadata.py +42 -0
- trueppm_api/core/middleware.py +51 -0
- trueppm_api/core/openapi.py +1221 -0
- trueppm_api/core/password_reset.py +513 -0
- trueppm_api/core/protect_conflict.py +100 -0
- trueppm_api/core/ratelimit.py +148 -0
- trueppm_api/core/redis_throttle.py +82 -0
- trueppm_api/core/refresh_cookie_policy.py +156 -0
- trueppm_api/core/request_body.py +86 -0
- trueppm_api/core/security_checks.py +1034 -0
- trueppm_api/core/storage_config.py +105 -0
- trueppm_api/core/text_decode.py +168 -0
- trueppm_api/core/throttling.py +514 -0
- trueppm_api/core/valkey.py +214 -0
- trueppm_api/core/valkey_cache.py +66 -0
- trueppm_api/core/valkey_checks.py +104 -0
- trueppm_api/core/valkey_config.py +39 -0
- trueppm_api/core/worker_broker_wait.py +149 -0
- trueppm_api/core/worker_heartbeat.py +134 -0
- trueppm_api/core/worker_selfheal.py +205 -0
- trueppm_api/fields.py +36 -0
- trueppm_api/permissions.py +30 -0
- trueppm_api/py.typed +0 -0
- trueppm_api/routing.py +11 -0
- trueppm_api/settings/__init__.py +0 -0
- trueppm_api/settings/base.py +2605 -0
- trueppm_api/settings/dev.py +296 -0
- trueppm_api/settings/prod.py +265 -0
- trueppm_api/urls.py +217 -0
- trueppm_api/workflows/__init__.py +38 -0
- trueppm_api/workflows/backends/__init__.py +6 -0
- trueppm_api/workflows/backends/default.py +297 -0
- trueppm_api/workflows/consumers/__init__.py +11 -0
- trueppm_api/workflows/consumers/requeue_failed_task.py +65 -0
- trueppm_api/workflows/interface.py +148 -0
- trueppm_api/workflows/registry.py +106 -0
- trueppm_api/workflows/services.py +90 -0
- trueppm_api/wsgi.py +11 -0
- trueppm_api-0.4.0.dist-info/METADATA +273 -0
- trueppm_api-0.4.0.dist-info/RECORD +799 -0
- trueppm_api-0.4.0.dist-info/WHEEL +4 -0
- trueppm_api-0.4.0.dist-info/licenses/LICENSE +201 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICE +7 -0
- trueppm_api-0.4.0.dist-info/licenses/NOTICES +104 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/GPL-3.0.txt +674 -0
- trueppm_api-0.4.0.dist-info/licenses/licenses/LGPL-3.0.txt +165 -0
- 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
|
+
}
|