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