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