roadmap-cli 0.1.1__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.
- roadmap/__init__.py +27 -0
- roadmap/adapters/__init__.py +1 -0
- roadmap/adapters/base_paginated_adapter.py +117 -0
- roadmap/adapters/cli/__init__.py +249 -0
- roadmap/adapters/cli/analysis/__init__.py +5 -0
- roadmap/adapters/cli/analysis/commands.py +370 -0
- roadmap/adapters/cli/analysis/presenter.py +125 -0
- roadmap/adapters/cli/archive_operations.py +86 -0
- roadmap/adapters/cli/cli_command_helpers.py +109 -0
- roadmap/adapters/cli/cli_confirmations.py +74 -0
- roadmap/adapters/cli/cli_error_handlers.py +146 -0
- roadmap/adapters/cli/cli_validators.py +39 -0
- roadmap/adapters/cli/commands/sync_status.py +308 -0
- roadmap/adapters/cli/comment/__init__.py +5 -0
- roadmap/adapters/cli/comment/commands.py +106 -0
- roadmap/adapters/cli/config/__init__.py +5 -0
- roadmap/adapters/cli/config/commands.py +198 -0
- roadmap/adapters/cli/console_exports.py +10 -0
- roadmap/adapters/cli/core.py +26 -0
- roadmap/adapters/cli/crud/__init__.py +47 -0
- roadmap/adapters/cli/crud/base_archive.py +208 -0
- roadmap/adapters/cli/crud/base_create.py +154 -0
- roadmap/adapters/cli/crud/base_delete.py +155 -0
- roadmap/adapters/cli/crud/base_restore.py +283 -0
- roadmap/adapters/cli/crud/base_update.py +193 -0
- roadmap/adapters/cli/crud/crud_helpers.py +206 -0
- roadmap/adapters/cli/crud/crud_utils.py +152 -0
- roadmap/adapters/cli/crud/entity_builders.py +374 -0
- roadmap/adapters/cli/data/__init__.py +8 -0
- roadmap/adapters/cli/data/commands.py +149 -0
- roadmap/adapters/cli/decorators.py +208 -0
- roadmap/adapters/cli/dtos/__init__.py +155 -0
- roadmap/adapters/cli/exception_handler.py +105 -0
- roadmap/adapters/cli/git/__init__.py +8 -0
- roadmap/adapters/cli/git/commands.py +475 -0
- roadmap/adapters/cli/git/handlers/__init__.py +1 -0
- roadmap/adapters/cli/git/handlers/git_authentication_handler.py +124 -0
- roadmap/adapters/cli/git/handlers/git_branch_handler.py +238 -0
- roadmap/adapters/cli/git/handlers/git_connectivity_handler.py +122 -0
- roadmap/adapters/cli/git/handlers/git_hooks_handler.py +105 -0
- roadmap/adapters/cli/git/hooks_config.py +327 -0
- roadmap/adapters/cli/git/status_display.py +158 -0
- roadmap/adapters/cli/health/__init__.py +8 -0
- roadmap/adapters/cli/health/db_integrity.py +204 -0
- roadmap/adapters/cli/health/enhancer.py +227 -0
- roadmap/adapters/cli/health/fixer.py +295 -0
- roadmap/adapters/cli/health/fixers/__init__.py +10 -0
- roadmap/adapters/cli/health/fixers/corrupted_comments_fixer.py +231 -0
- roadmap/adapters/cli/health/fixers/data_integrity_fixer.py +137 -0
- roadmap/adapters/cli/health/fixers/duplicate_issues_fixer.py +138 -0
- roadmap/adapters/cli/health/fixers/folder_structure_fixer.py +114 -0
- roadmap/adapters/cli/health/fixers/label_normalization_fixer.py +162 -0
- roadmap/adapters/cli/health/fixers/milestone_name_normalization_fixer.py +181 -0
- roadmap/adapters/cli/health/fixers/milestone_naming_compliance_fixer.py +244 -0
- roadmap/adapters/cli/health/fixers/milestone_parse_error_fixer.py +162 -0
- roadmap/adapters/cli/health/fixers/milestone_validation_fixer.py +211 -0
- roadmap/adapters/cli/health/fixers/old_backups_fixer.py +135 -0
- roadmap/adapters/cli/health/fixers/orphaned_issues_fixer.py +252 -0
- roadmap/adapters/cli/health/formatter.py +250 -0
- roadmap/adapters/cli/health/formatters.py +427 -0
- roadmap/adapters/cli/health/scan.py +293 -0
- roadmap/adapters/cli/init/__init__.py +5 -0
- roadmap/adapters/cli/init/commands.py +587 -0
- roadmap/adapters/cli/init.py +9 -0
- roadmap/adapters/cli/issues/__init__.py +70 -0
- roadmap/adapters/cli/issues/archive.py +137 -0
- roadmap/adapters/cli/issues/archive_class.py +253 -0
- roadmap/adapters/cli/issues/block.py +39 -0
- roadmap/adapters/cli/issues/close.py +216 -0
- roadmap/adapters/cli/issues/comment.py +178 -0
- roadmap/adapters/cli/issues/create.py +162 -0
- roadmap/adapters/cli/issues/delete.py +31 -0
- roadmap/adapters/cli/issues/deps.py +161 -0
- roadmap/adapters/cli/issues/issue_status_helpers.py +111 -0
- roadmap/adapters/cli/issues/link.py +308 -0
- roadmap/adapters/cli/issues/list.py +402 -0
- roadmap/adapters/cli/issues/lookup.py +96 -0
- roadmap/adapters/cli/issues/progress.py +102 -0
- roadmap/adapters/cli/issues/restore.py +96 -0
- roadmap/adapters/cli/issues/restore_class.py +57 -0
- roadmap/adapters/cli/issues/start.py +165 -0
- roadmap/adapters/cli/issues/sync_status.py +374 -0
- roadmap/adapters/cli/issues/unblock.py +43 -0
- roadmap/adapters/cli/issues/unlink.py +104 -0
- roadmap/adapters/cli/issues/update.py +120 -0
- roadmap/adapters/cli/issues/view.py +36 -0
- roadmap/adapters/cli/layout.py +220 -0
- roadmap/adapters/cli/mappers.py +200 -0
- roadmap/adapters/cli/milestones/__init__.py +52 -0
- roadmap/adapters/cli/milestones/archive.py +125 -0
- roadmap/adapters/cli/milestones/archive_class.py +236 -0
- roadmap/adapters/cli/milestones/assign.py +62 -0
- roadmap/adapters/cli/milestones/close.py +177 -0
- roadmap/adapters/cli/milestones/create.py +131 -0
- roadmap/adapters/cli/milestones/delete.py +27 -0
- roadmap/adapters/cli/milestones/kanban.py +123 -0
- roadmap/adapters/cli/milestones/list.py +87 -0
- roadmap/adapters/cli/milestones/recalculate.py +79 -0
- roadmap/adapters/cli/milestones/restore.py +87 -0
- roadmap/adapters/cli/milestones/restore_class.py +99 -0
- roadmap/adapters/cli/milestones/update.py +61 -0
- roadmap/adapters/cli/milestones/view.py +142 -0
- roadmap/adapters/cli/output_manager.py +319 -0
- roadmap/adapters/cli/presentation/base_presenter.py +121 -0
- roadmap/adapters/cli/presentation/cleanup_presenter.py +235 -0
- roadmap/adapters/cli/presentation/core_initialization_presenter.py +277 -0
- roadmap/adapters/cli/presentation/crud_presenter.py +134 -0
- roadmap/adapters/cli/presentation/daily_summary_presenter.py +266 -0
- roadmap/adapters/cli/presentation/issue_presenter.py +258 -0
- roadmap/adapters/cli/presentation/milestone_list_presenter.py +135 -0
- roadmap/adapters/cli/presentation/milestone_presenter.py +297 -0
- roadmap/adapters/cli/presentation/project_presenter.py +233 -0
- roadmap/adapters/cli/presentation/project_status_presenter.py +186 -0
- roadmap/adapters/cli/presentation/table_builders.py +95 -0
- roadmap/adapters/cli/projects/__init__.py +42 -0
- roadmap/adapters/cli/projects/archive.py +127 -0
- roadmap/adapters/cli/projects/archive_class.py +96 -0
- roadmap/adapters/cli/projects/close.py +94 -0
- roadmap/adapters/cli/projects/create.py +50 -0
- roadmap/adapters/cli/projects/delete.py +27 -0
- roadmap/adapters/cli/projects/list.py +227 -0
- roadmap/adapters/cli/projects/restore.py +87 -0
- roadmap/adapters/cli/projects/restore_class.py +56 -0
- roadmap/adapters/cli/projects/update.py +57 -0
- roadmap/adapters/cli/projects/view.py +157 -0
- roadmap/adapters/cli/services/__init__.py +1 -0
- roadmap/adapters/cli/services/daily_summary_service.py +313 -0
- roadmap/adapters/cli/services/export_manager.py +268 -0
- roadmap/adapters/cli/services/milestone_list_service.py +266 -0
- roadmap/adapters/cli/services/project_initialization_service.py +29 -0
- roadmap/adapters/cli/services/project_status_service.py +462 -0
- roadmap/adapters/cli/services/sync_service.py +62 -0
- roadmap/adapters/cli/status.py +408 -0
- roadmap/adapters/cli/styling.py +21 -0
- roadmap/adapters/cli/sync.py +1117 -0
- roadmap/adapters/cli/sync_context.py +432 -0
- roadmap/adapters/cli/sync_handlers/__init__.py +77 -0
- roadmap/adapters/cli/sync_handlers/apply_ops.py +522 -0
- roadmap/adapters/cli/sync_handlers/baseline_ops.py +366 -0
- roadmap/adapters/cli/sync_handlers/conflict_ops.py +161 -0
- roadmap/adapters/cli/sync_handlers/dry_run_display.py +237 -0
- roadmap/adapters/cli/sync_handlers/interactive_resolver.py +396 -0
- roadmap/adapters/cli/sync_handlers/progress_tracker.py +128 -0
- roadmap/adapters/cli/sync_metrics_command.py +190 -0
- roadmap/adapters/cli/sync_presenter.py +58 -0
- roadmap/adapters/cli/sync_validation.py +295 -0
- roadmap/adapters/cli/today.py +52 -0
- roadmap/adapters/cli/utils/click_options.py +38 -0
- roadmap/adapters/git/__init__.py +1 -0
- roadmap/adapters/git/git.py +208 -0
- roadmap/adapters/git/git_branch_manager.py +400 -0
- roadmap/adapters/git/git_command_executor.py +56 -0
- roadmap/adapters/git/git_commit_analyzer.py +334 -0
- roadmap/adapters/git/git_hooks.py +22 -0
- roadmap/adapters/git/git_hooks_manager.py +455 -0
- roadmap/adapters/git/git_repository_info.py +65 -0
- roadmap/adapters/git/hook_installer.py +106 -0
- roadmap/adapters/git/hook_registry.py +97 -0
- roadmap/adapters/git/hook_script_generator.py +58 -0
- roadmap/adapters/git/sync_monitor.py +409 -0
- roadmap/adapters/git/workflow_automation.py +286 -0
- roadmap/adapters/github/__init__.py +1 -0
- roadmap/adapters/github/github.py +360 -0
- roadmap/adapters/github/handlers/__init__.py +17 -0
- roadmap/adapters/github/handlers/base.py +337 -0
- roadmap/adapters/github/handlers/collaborators.py +165 -0
- roadmap/adapters/github/handlers/comments.py +118 -0
- roadmap/adapters/github/handlers/issues.py +150 -0
- roadmap/adapters/github/handlers/labels.py +164 -0
- roadmap/adapters/github/handlers/milestones.py +104 -0
- roadmap/adapters/persistence/__init__.py +35 -0
- roadmap/adapters/persistence/conflict_resolver.py +190 -0
- roadmap/adapters/persistence/database_manager.py +492 -0
- roadmap/adapters/persistence/entity_sync_coordinators.py +672 -0
- roadmap/adapters/persistence/file_locking.py +348 -0
- roadmap/adapters/persistence/file_parser.py +92 -0
- roadmap/adapters/persistence/file_synchronizer.py +107 -0
- roadmap/adapters/persistence/focused_managers.py +140 -0
- roadmap/adapters/persistence/git_history.py +330 -0
- roadmap/adapters/persistence/parser/__init__.py +20 -0
- roadmap/adapters/persistence/parser/frontmatter.py +152 -0
- roadmap/adapters/persistence/parser/issue.py +245 -0
- roadmap/adapters/persistence/parser/milestone.py +146 -0
- roadmap/adapters/persistence/parser/project.py +106 -0
- roadmap/adapters/persistence/persistence.py +288 -0
- roadmap/adapters/persistence/repositories/__init__.py +15 -0
- roadmap/adapters/persistence/repositories/issue_repository.py +223 -0
- roadmap/adapters/persistence/repositories/milestone_repository.py +140 -0
- roadmap/adapters/persistence/repositories/project_repository.py +168 -0
- roadmap/adapters/persistence/repositories/remote_link_repository.py +355 -0
- roadmap/adapters/persistence/repositories/sync_state_repository.py +66 -0
- roadmap/adapters/persistence/storage/__init__.py +36 -0
- roadmap/adapters/persistence/storage/conflicts.py +101 -0
- roadmap/adapters/persistence/storage/connection_manager.py +79 -0
- roadmap/adapters/persistence/storage/issue_storage.py +89 -0
- roadmap/adapters/persistence/storage/milestone_storage.py +80 -0
- roadmap/adapters/persistence/storage/project_storage.py +102 -0
- roadmap/adapters/persistence/storage/queries.py +227 -0
- roadmap/adapters/persistence/storage/state_manager.py +652 -0
- roadmap/adapters/persistence/storage/sync_state_storage.py +207 -0
- roadmap/adapters/persistence/sync_metrics_repository.py +342 -0
- roadmap/adapters/persistence/sync_orchestrator.py +293 -0
- roadmap/adapters/persistence/sync_state_tracker.py +86 -0
- roadmap/adapters/persistence/yaml_repositories.py +711 -0
- roadmap/adapters/sync/__init__.py +21 -0
- roadmap/adapters/sync/backend_factory.py +108 -0
- roadmap/adapters/sync/backends/__init__.py +15 -0
- roadmap/adapters/sync/backends/converters.py +209 -0
- roadmap/adapters/sync/backends/github_backend_helpers.py +380 -0
- roadmap/adapters/sync/backends/github_client.py +99 -0
- roadmap/adapters/sync/backends/github_sync_backend.py +792 -0
- roadmap/adapters/sync/backends/github_sync_ops.py +908 -0
- roadmap/adapters/sync/backends/services/__init__.py +1 -0
- roadmap/adapters/sync/backends/services/github_authentication_service.py +105 -0
- roadmap/adapters/sync/backends/services/github_issue_delete_service.py +423 -0
- roadmap/adapters/sync/backends/services/github_issue_dependency_service.py +63 -0
- roadmap/adapters/sync/backends/services/github_issue_fetch_service.py +158 -0
- roadmap/adapters/sync/backends/services/github_label_sync_service.py +126 -0
- roadmap/adapters/sync/backends/services/github_local_issue_sync_service.py +229 -0
- roadmap/adapters/sync/backends/services/github_milestone_fetch_service.py +204 -0
- roadmap/adapters/sync/backends/vanilla_git_sync_backend.py +198 -0
- roadmap/adapters/sync/services/__init__.py +11 -0
- roadmap/adapters/sync/services/baseline_state_handler.py +83 -0
- roadmap/adapters/sync/services/conflict_converter.py +85 -0
- roadmap/adapters/sync/services/issue_persistence_service.py +263 -0
- roadmap/adapters/sync/services/issue_state_service.py +243 -0
- roadmap/adapters/sync/services/local_change_filter.py +129 -0
- roadmap/adapters/sync/services/pull_result_processor.py +71 -0
- roadmap/adapters/sync/services/remote_issue_creation_service.py +112 -0
- roadmap/adapters/sync/services/sync_analysis_service.py +250 -0
- roadmap/adapters/sync/services/sync_authentication_service.py +57 -0
- roadmap/adapters/sync/services/sync_data_fetch_service.py +212 -0
- roadmap/adapters/sync/services/sync_linking_service.py +329 -0
- roadmap/adapters/sync/services/sync_report_service.py +62 -0
- roadmap/adapters/sync/services/sync_state_update_service.py +51 -0
- roadmap/adapters/sync/sync_cache_orchestrator.py +454 -0
- roadmap/adapters/sync/sync_merge_engine.py +416 -0
- roadmap/adapters/sync/sync_merge_orchestrator.py +1313 -0
- roadmap/adapters/sync/sync_retrieval_orchestrator.py +810 -0
- roadmap/application/services/__init__.py +1 -0
- roadmap/application/services/deduplicate_service.py +517 -0
- roadmap/application/use_cases/__init__.py +1 -0
- roadmap/common/__init__.py +308 -0
- roadmap/common/cache.py +209 -0
- roadmap/common/cli_errors.py +147 -0
- roadmap/common/configuration/__init__.py +33 -0
- roadmap/common/configuration/config_loader.py +264 -0
- roadmap/common/configuration/config_manager.py +164 -0
- roadmap/common/configuration/config_schema.py +145 -0
- roadmap/common/configuration/github/__init__.py +9 -0
- roadmap/common/configuration/github/config_manager.py +154 -0
- roadmap/common/configuration/github/token_resolver.py +84 -0
- roadmap/common/console.py +184 -0
- roadmap/common/constants.py +134 -0
- roadmap/common/datetime_parser.py +319 -0
- roadmap/common/error_formatter.py +84 -0
- roadmap/common/errors/__init__.py +143 -0
- roadmap/common/errors/error_base.py +73 -0
- roadmap/common/errors/error_file.py +82 -0
- roadmap/common/errors/error_git.py +67 -0
- roadmap/common/errors/error_handler.py +166 -0
- roadmap/common/errors/error_network.py +90 -0
- roadmap/common/errors/error_security.py +78 -0
- roadmap/common/errors/error_standards.py +561 -0
- roadmap/common/errors/error_validation.py +111 -0
- roadmap/common/errors/exceptions.py +432 -0
- roadmap/common/formatters/__init__.py +29 -0
- roadmap/common/formatters/base_table_formatter.py +96 -0
- roadmap/common/formatters/export/__init__.py +7 -0
- roadmap/common/formatters/export/issue_exporter.py +97 -0
- roadmap/common/formatters/helpers.py +28 -0
- roadmap/common/formatters/kanban/__init__.py +9 -0
- roadmap/common/formatters/kanban/layout.py +48 -0
- roadmap/common/formatters/kanban/organizer.py +81 -0
- roadmap/common/formatters/output/__init__.py +15 -0
- roadmap/common/formatters/tables/__init__.py +11 -0
- roadmap/common/formatters/tables/column_factory.py +312 -0
- roadmap/common/formatters/tables/issue_table.py +276 -0
- roadmap/common/formatters/tables/milestone_table.py +247 -0
- roadmap/common/formatters/tables/project_table.py +195 -0
- roadmap/common/formatters/text/__init__.py +57 -0
- roadmap/common/formatters/text/basic.py +217 -0
- roadmap/common/formatters/text/display.py +42 -0
- roadmap/common/formatters/text/duration.py +36 -0
- roadmap/common/formatters/text/operations.py +325 -0
- roadmap/common/formatters/text/status_badges.py +57 -0
- roadmap/common/initialization/github/__init__.py +11 -0
- roadmap/common/initialization/github/setup_service.py +238 -0
- roadmap/common/initialization/github/setup_validator.py +78 -0
- roadmap/common/logging/__init__.py +113 -0
- roadmap/common/logging/audit_logging.py +5 -0
- roadmap/common/logging/correlation.py +45 -0
- roadmap/common/logging/decorators.py +267 -0
- roadmap/common/logging/error_logging.py +217 -0
- roadmap/common/logging/formatters.py +124 -0
- roadmap/common/logging/loggers.py +68 -0
- roadmap/common/logging/performance_tracking.py +267 -0
- roadmap/common/logging/utils.py +335 -0
- roadmap/common/models/__init__.py +44 -0
- roadmap/common/models/cli_models.py +118 -0
- roadmap/common/models/config_models.py +199 -0
- roadmap/common/models/output_models.py +381 -0
- roadmap/common/observability/__init__.py +28 -0
- roadmap/common/observability/instrumentation.py +103 -0
- roadmap/common/observability/observability.py +76 -0
- roadmap/common/observability/otel_init.py +109 -0
- roadmap/common/output_formatter.py +393 -0
- roadmap/common/progress.py +379 -0
- roadmap/common/result.py +396 -0
- roadmap/common/security/__init__.py +48 -0
- roadmap/common/security/exceptions.py +13 -0
- roadmap/common/security/export_cleanup.py +79 -0
- roadmap/common/security/file_operations.py +104 -0
- roadmap/common/security/filename_sanitization.py +54 -0
- roadmap/common/security/logging.py +27 -0
- roadmap/common/security/path_validation.py +137 -0
- roadmap/common/security/temp_files.py +41 -0
- roadmap/common/services/__init__.py +65 -0
- roadmap/common/services/decorators.py +183 -0
- roadmap/common/services/logging_utils.py +208 -0
- roadmap/common/services/metrics.py +161 -0
- roadmap/common/services/performance.py +219 -0
- roadmap/common/services/profiling.py +288 -0
- roadmap/common/services/retry.py +288 -0
- roadmap/common/status_style_manager.py +114 -0
- roadmap/common/union_find.py +88 -0
- roadmap/common/update_constants.py +13 -0
- roadmap/common/utils/__init__.py +65 -0
- roadmap/common/utils/cli_helpers.py +381 -0
- roadmap/common/utils/file_utils.py +440 -0
- roadmap/common/utils/path_utils.py +38 -0
- roadmap/common/utils/status_utils.py +130 -0
- roadmap/common/utils/timezone_utils.py +480 -0
- roadmap/common/validation/__init__.py +37 -0
- roadmap/common/validation/field_validator.py +104 -0
- roadmap/common/validation/result.py +37 -0
- roadmap/common/validation/roadmap_validator.py +310 -0
- roadmap/common/validation/schema_validator.py +36 -0
- roadmap/common/validation/validators.py +50 -0
- roadmap/core/__init__.py +1 -0
- roadmap/core/domain/__init__.py +30 -0
- roadmap/core/domain/comment.py +24 -0
- roadmap/core/domain/health.py +15 -0
- roadmap/core/domain/issue.py +274 -0
- roadmap/core/domain/milestone.py +173 -0
- roadmap/core/domain/ports/__init__.py +1 -0
- roadmap/core/domain/ports/baseline_repository.py +24 -0
- roadmap/core/domain/ports/issue_repository.py +34 -0
- roadmap/core/domain/ports/remote_backend_port.py +31 -0
- roadmap/core/domain/project.py +130 -0
- roadmap/core/interfaces/__init__.py +109 -0
- roadmap/core/interfaces/assignee_validator.py +40 -0
- roadmap/core/interfaces/backend_factory.py +175 -0
- roadmap/core/interfaces/github.py +102 -0
- roadmap/core/interfaces/parsers.py +140 -0
- roadmap/core/interfaces/persistence.py +45 -0
- roadmap/core/interfaces/repositories.py +174 -0
- roadmap/core/interfaces/state_managers.py +111 -0
- roadmap/core/interfaces/state_storage.py +64 -0
- roadmap/core/interfaces/sync_backend.py +275 -0
- roadmap/core/interfaces/sync_services.py +119 -0
- roadmap/core/interfaces/sync_validators.py +180 -0
- roadmap/core/models/__init__.py +31 -0
- roadmap/core/models/sync_models.py +162 -0
- roadmap/core/models.py +115 -0
- roadmap/core/observability/__init__.py +17 -0
- roadmap/core/observability/sync_metrics.py +699 -0
- roadmap/core/repositories.py +200 -0
- roadmap/core/services/__init__.py +313 -0
- roadmap/core/services/baseline/__init__.py +1 -0
- roadmap/core/services/baseline/baseline_builder_progress.py +274 -0
- roadmap/core/services/baseline/baseline_retriever.py +63 -0
- roadmap/core/services/baseline/baseline_selector.py +248 -0
- roadmap/core/services/baseline/baseline_state_retriever.py +284 -0
- roadmap/core/services/baseline/optimized_baseline_builder.py +425 -0
- roadmap/core/services/comment/__init__.py +1 -0
- roadmap/core/services/comment/comment_service.py +277 -0
- roadmap/core/services/git/__init__.py +1 -0
- roadmap/core/services/git/git_hook_auto_sync_service.py +403 -0
- roadmap/core/services/github/__init__.py +1 -0
- roadmap/core/services/github/github_change_detector.py +162 -0
- roadmap/core/services/github/github_config_validator.py +148 -0
- roadmap/core/services/github/github_conflict_detector.py +179 -0
- roadmap/core/services/github/github_entity_classifier.py +51 -0
- roadmap/core/services/github/github_integration_service.py +439 -0
- roadmap/core/services/github/github_issue_client.py +346 -0
- roadmap/core/services/health/__init__.py +1 -0
- roadmap/core/services/health/backup_cleanup_service.py +202 -0
- roadmap/core/services/health/data_integrity_validator_service.py +90 -0
- roadmap/core/services/health/entity_health_scanner.py +460 -0
- roadmap/core/services/health/file_repair_service.py +193 -0
- roadmap/core/services/health/health_check_service.py +166 -0
- roadmap/core/services/health/health_models.py +80 -0
- roadmap/core/services/health/infrastructure_validator_service.py +328 -0
- roadmap/core/services/health/issue_health_scanner.py +263 -0
- roadmap/core/services/helpers/__init__.py +18 -0
- roadmap/core/services/helpers/status_change_helpers.py +88 -0
- roadmap/core/services/initialization/__init__.py +19 -0
- roadmap/core/services/initialization/utils.py +104 -0
- roadmap/core/services/initialization/validator.py +78 -0
- roadmap/core/services/initialization/workflow.py +182 -0
- roadmap/core/services/initialization_service.py +111 -0
- roadmap/core/services/issue/__init__.py +1 -0
- roadmap/core/services/issue/assignee_validation_service.py +312 -0
- roadmap/core/services/issue/issue_creation_service.py +309 -0
- roadmap/core/services/issue/issue_filter_service.py +302 -0
- roadmap/core/services/issue/issue_matching_service.py +127 -0
- roadmap/core/services/issue/issue_service.py +765 -0
- roadmap/core/services/issue/issue_update_service.py +144 -0
- roadmap/core/services/issue/start_issue_service.py +174 -0
- roadmap/core/services/issue_helpers/__init__.py +18 -0
- roadmap/core/services/issue_helpers/issue_filters.py +296 -0
- roadmap/core/services/milestone_service.py +438 -0
- roadmap/core/services/project/__init__.py +1 -0
- roadmap/core/services/project/project_service.py +434 -0
- roadmap/core/services/project/project_status_service.py +125 -0
- roadmap/core/services/project_init/__init__.py +22 -0
- roadmap/core/services/project_init/context_detection.py +146 -0
- roadmap/core/services/project_init/creation.py +76 -0
- roadmap/core/services/project_init/detection.py +51 -0
- roadmap/core/services/project_init/template.py +228 -0
- roadmap/core/services/status_change_service.py +88 -0
- roadmap/core/services/sync/__init__.py +1 -0
- roadmap/core/services/sync/batch_processor.py +290 -0
- roadmap/core/services/sync/conflict_resolver.py +154 -0
- roadmap/core/services/sync/dependency_resolver.py +367 -0
- roadmap/core/services/sync/duplicate_detector.py +771 -0
- roadmap/core/services/sync/duplicate_resolver.py +315 -0
- roadmap/core/services/sync/error_classification.py +470 -0
- roadmap/core/services/sync/performance.py +263 -0
- roadmap/core/services/sync/sync_change_computer.py +211 -0
- roadmap/core/services/sync/sync_checkpoint.py +348 -0
- roadmap/core/services/sync/sync_conflict_detector.py +147 -0
- roadmap/core/services/sync/sync_conflict_resolver.py +448 -0
- roadmap/core/services/sync/sync_errors.py +383 -0
- roadmap/core/services/sync/sync_key_normalizer.py +222 -0
- roadmap/core/services/sync/sync_metadata_service.py +361 -0
- roadmap/core/services/sync/sync_plan.py +171 -0
- roadmap/core/services/sync/sync_plan_executor.py +447 -0
- roadmap/core/services/sync/sync_report.py +296 -0
- roadmap/core/services/sync/sync_state.py +173 -0
- roadmap/core/services/sync/sync_state_comparator.py +690 -0
- roadmap/core/services/sync/sync_state_manager.py +431 -0
- roadmap/core/services/sync/sync_state_normalizer.py +98 -0
- roadmap/core/services/sync/sync_three_way.py +78 -0
- roadmap/core/services/sync/three_way_merger.py +178 -0
- roadmap/core/services/utils/__init__.py +1 -0
- roadmap/core/services/utils/configuration_service.py +113 -0
- roadmap/core/services/utils/critical_path_calculator.py +418 -0
- roadmap/core/services/utils/dependency_analyzer.py +360 -0
- roadmap/core/services/utils/field_conflict_detector.py +126 -0
- roadmap/core/services/utils/remote_fetcher.py +202 -0
- roadmap/core/services/utils/remote_state_normalizer.py +110 -0
- roadmap/core/services/utils/retry_policy.py +51 -0
- roadmap/core/services/validator_base.py +130 -0
- roadmap/core/services/validators/__init__.py +28 -0
- roadmap/core/services/validators/_utils.py +20 -0
- roadmap/core/services/validators/archivable_issues_validator.py +71 -0
- roadmap/core/services/validators/archivable_milestones_validator.py +69 -0
- roadmap/core/services/validators/backup_validator.py +89 -0
- roadmap/core/services/validators/data_integrity_validator.py +84 -0
- roadmap/core/services/validators/duplicate_issues_validator.py +75 -0
- roadmap/core/services/validators/duplicate_milestones_validator.py +141 -0
- roadmap/core/services/validators/folder_structure_validator.py +165 -0
- roadmap/core/services/validators/health_status_utils.py +28 -0
- roadmap/core/services/validators/milestone_naming_validator.py +146 -0
- roadmap/core/services/validators/missing_headlines_validator.py +97 -0
- roadmap/core/services/validators/orphaned_issues_validator.py +142 -0
- roadmap/core/services/validators/orphaned_milestones_validator.py +85 -0
- roadmap/core/utils/git_remote_parser.py +83 -0
- roadmap/core/utils/github_urls.py +66 -0
- roadmap/domain/validation.py +30 -0
- roadmap/infrastructure/__init__.py +55 -0
- roadmap/infrastructure/coordination/__init__.py +8 -0
- roadmap/infrastructure/coordination/coordinator_params.py +82 -0
- roadmap/infrastructure/coordination/core.py +446 -0
- roadmap/infrastructure/coordination/git_coordinator.py +160 -0
- roadmap/infrastructure/coordination/initialization.py +348 -0
- roadmap/infrastructure/coordination/issue_coordinator.py +154 -0
- roadmap/infrastructure/coordination/issue_operations.py +456 -0
- roadmap/infrastructure/coordination/milestone_coordinator.py +135 -0
- roadmap/infrastructure/coordination/milestone_operations.py +201 -0
- roadmap/infrastructure/coordination/project_coordinator.py +113 -0
- roadmap/infrastructure/coordination/project_operations.py +144 -0
- roadmap/infrastructure/coordination/team_coordinator.py +107 -0
- roadmap/infrastructure/coordination/user_operations.py +150 -0
- roadmap/infrastructure/coordination/validation_coordinator.py +249 -0
- roadmap/infrastructure/coordination_gateway.py +152 -0
- roadmap/infrastructure/git/__init__.py +6 -0
- roadmap/infrastructure/git/git_integration_ops.py +234 -0
- roadmap/infrastructure/git_gateway.py +35 -0
- roadmap/infrastructure/github_gateway.py +52 -0
- roadmap/infrastructure/maintenance/__init__.py +5 -0
- roadmap/infrastructure/maintenance/cleanup.py +483 -0
- roadmap/infrastructure/observability/__init__.py +7 -0
- roadmap/infrastructure/observability/health.py +196 -0
- roadmap/infrastructure/observability/health_formatter.py +73 -0
- roadmap/infrastructure/observability/specialized_health_checkers.py +188 -0
- roadmap/infrastructure/persistence_gateway.py +164 -0
- roadmap/infrastructure/security/__init__.py +1 -0
- roadmap/infrastructure/security/credentials.py +489 -0
- roadmap/infrastructure/sync_gateway.py +94 -0
- roadmap/infrastructure/validation/__init__.py +7 -0
- roadmap/infrastructure/validation/file_enumeration.py +219 -0
- roadmap/infrastructure/validation/github_assignee_validator.py +47 -0
- roadmap/infrastructure/validation/github_validator.py +101 -0
- roadmap/infrastructure/validation/milestone_consistency_validator.py +96 -0
- roadmap/infrastructure/validation/vanilla_assignee_validator.py +37 -0
- roadmap/infrastructure/validation_gateway.py +96 -0
- roadmap/presentation/formatters/sync_metrics_formatter.py +355 -0
- roadmap/settings.py +345 -0
- roadmap/templates/github_workflows/ci-cd-roadmap.yml +241 -0
- roadmap/templates/github_workflows/issue-lifecycle.yml +236 -0
- roadmap/templates/github_workflows/roadmap-integration.yml +137 -0
- roadmap/templates/github_workflows/roadmap-starter.yml +59 -0
- roadmap/version.py +381 -0
- roadmap_cli-0.1.1.dist-info/METADATA +448 -0
- roadmap_cli-0.1.1.dist-info/RECORD +521 -0
- roadmap_cli-0.1.1.dist-info/WHEEL +4 -0
- roadmap_cli-0.1.1.dist-info/entry_points.txt +2 -0
- roadmap_cli-0.1.1.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""Base handler for GitHub API operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, TypedDict
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from roadmap.adapters.base_paginated_adapter import BasePaginatedAdapter
|
|
8
|
+
from roadmap.common.logging import get_logger, log_external_service_error
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GitHubAPIError(Exception):
|
|
12
|
+
"""Exception raised for GitHub API errors."""
|
|
13
|
+
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _ExternalLogKwargs(TypedDict):
|
|
18
|
+
service_name: str
|
|
19
|
+
operation: str
|
|
20
|
+
retry_count: int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BaseGitHubHandler(BasePaginatedAdapter):
|
|
24
|
+
"""Base class for GitHub API handlers.
|
|
25
|
+
|
|
26
|
+
Inherits pagination support from BasePaginatedAdapter for handling
|
|
27
|
+
GitHub's paginated API responses.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
BASE_URL = "https://api.github.com"
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
session: requests.Session,
|
|
35
|
+
owner: str | None = None,
|
|
36
|
+
repo: str | None = None,
|
|
37
|
+
):
|
|
38
|
+
"""Initialize handler with GitHub API session.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
session: Requests session with authentication headers
|
|
42
|
+
owner: Repository owner (username or organization)
|
|
43
|
+
repo: Repository name
|
|
44
|
+
"""
|
|
45
|
+
self.session = session
|
|
46
|
+
self.owner = owner
|
|
47
|
+
self.repo = repo
|
|
48
|
+
|
|
49
|
+
def set_repository(self, owner: str, repo: str) -> None:
|
|
50
|
+
"""Set the target repository."""
|
|
51
|
+
self.owner = owner
|
|
52
|
+
self.repo = repo
|
|
53
|
+
|
|
54
|
+
def _check_repository(self) -> None:
|
|
55
|
+
"""Check if repository is set."""
|
|
56
|
+
if not self.owner or not self.repo:
|
|
57
|
+
raise GitHubAPIError(
|
|
58
|
+
"Repository not set. Use set_repository() or provide owner/repo in constructor."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
def _raise_http_error(
|
|
62
|
+
self,
|
|
63
|
+
status_code: int,
|
|
64
|
+
response: requests.Response,
|
|
65
|
+
method: str,
|
|
66
|
+
endpoint: str,
|
|
67
|
+
e: requests.exceptions.HTTPError,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Log and raise a GitHubAPIError for the given HTTP status code."""
|
|
70
|
+
retry_count = getattr(self.session, "retry_count", 0)
|
|
71
|
+
if not isinstance(retry_count, int):
|
|
72
|
+
retry_count = 0
|
|
73
|
+
|
|
74
|
+
log_kwargs: _ExternalLogKwargs = {
|
|
75
|
+
"service_name": "GitHub API",
|
|
76
|
+
"operation": f"{method} {endpoint}",
|
|
77
|
+
"retry_count": retry_count,
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
handlers: dict[int, Any] = {
|
|
81
|
+
400: self._handle_400,
|
|
82
|
+
401: self._handle_401,
|
|
83
|
+
403: self._handle_403,
|
|
84
|
+
404: self._handle_404,
|
|
85
|
+
410: self._handle_410,
|
|
86
|
+
422: self._handle_422,
|
|
87
|
+
429: self._handle_429,
|
|
88
|
+
}
|
|
89
|
+
handler = handlers.get(status_code)
|
|
90
|
+
if handler:
|
|
91
|
+
handler(status_code, response, method, endpoint, e, log_kwargs)
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
if 500 <= status_code < 600:
|
|
95
|
+
self._handle_5xx(status_code, method, endpoint, e, log_kwargs)
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
self._handle_unknown(status_code, method, endpoint, e, log_kwargs)
|
|
99
|
+
|
|
100
|
+
def _handle_400(
|
|
101
|
+
self,
|
|
102
|
+
status_code: int,
|
|
103
|
+
_response: requests.Response,
|
|
104
|
+
method: str,
|
|
105
|
+
endpoint: str,
|
|
106
|
+
error: requests.exceptions.HTTPError,
|
|
107
|
+
log_kwargs: _ExternalLogKwargs,
|
|
108
|
+
) -> None:
|
|
109
|
+
log_external_service_error(error=error, **log_kwargs)
|
|
110
|
+
get_logger().warning(
|
|
111
|
+
"github_api_bad_request",
|
|
112
|
+
status_code=status_code,
|
|
113
|
+
operation=f"{method} {endpoint}",
|
|
114
|
+
severity="data_error",
|
|
115
|
+
)
|
|
116
|
+
raise GitHubAPIError("Bad Request: Invalid request payload") from error
|
|
117
|
+
|
|
118
|
+
def _handle_401(
|
|
119
|
+
self,
|
|
120
|
+
status_code: int,
|
|
121
|
+
_response: requests.Response,
|
|
122
|
+
method: str,
|
|
123
|
+
endpoint: str,
|
|
124
|
+
error: requests.exceptions.HTTPError,
|
|
125
|
+
log_kwargs: _ExternalLogKwargs,
|
|
126
|
+
) -> None:
|
|
127
|
+
_ = method, endpoint
|
|
128
|
+
log_external_service_error(error=error, **log_kwargs)
|
|
129
|
+
get_logger().warning(
|
|
130
|
+
"github_api_authentication_failed",
|
|
131
|
+
status_code=status_code,
|
|
132
|
+
severity="config",
|
|
133
|
+
)
|
|
134
|
+
raise GitHubAPIError(
|
|
135
|
+
"Authentication failed. Check your GitHub token."
|
|
136
|
+
) from error
|
|
137
|
+
|
|
138
|
+
def _extract_forbidden_response_message(
|
|
139
|
+
self, response: requests.Response
|
|
140
|
+
) -> str | None:
|
|
141
|
+
"""Best-effort parsing of GitHub forbidden response message."""
|
|
142
|
+
try:
|
|
143
|
+
if response.content:
|
|
144
|
+
payload = response.json()
|
|
145
|
+
if isinstance(payload, dict):
|
|
146
|
+
message = payload.get("message")
|
|
147
|
+
return str(message) if message is not None else None
|
|
148
|
+
except Exception as parse_error:
|
|
149
|
+
get_logger().warning(
|
|
150
|
+
"github_api_forbidden_response_parse_failed",
|
|
151
|
+
error=str(parse_error),
|
|
152
|
+
severity="operational",
|
|
153
|
+
)
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def _handle_403(
|
|
157
|
+
self,
|
|
158
|
+
status_code: int,
|
|
159
|
+
response: requests.Response,
|
|
160
|
+
method: str,
|
|
161
|
+
endpoint: str,
|
|
162
|
+
error: requests.exceptions.HTTPError,
|
|
163
|
+
_log_kwargs: _ExternalLogKwargs,
|
|
164
|
+
) -> None:
|
|
165
|
+
response_message = self._extract_forbidden_response_message(response)
|
|
166
|
+
get_logger().warning(
|
|
167
|
+
"github_api_access_forbidden",
|
|
168
|
+
status_code=status_code,
|
|
169
|
+
operation=f"{method} {endpoint}",
|
|
170
|
+
owner=self.owner,
|
|
171
|
+
repo=self.repo,
|
|
172
|
+
rate_limit_remaining=response.headers.get("X-RateLimit-Remaining"),
|
|
173
|
+
rate_limit_reset=response.headers.get("X-RateLimit-Reset"),
|
|
174
|
+
response_message=response_message,
|
|
175
|
+
severity="config",
|
|
176
|
+
)
|
|
177
|
+
raise GitHubAPIError(
|
|
178
|
+
"Access forbidden. Check repository permissions and token scopes."
|
|
179
|
+
) from error
|
|
180
|
+
|
|
181
|
+
def _handle_404(
|
|
182
|
+
self,
|
|
183
|
+
status_code: int,
|
|
184
|
+
_response: requests.Response,
|
|
185
|
+
method: str,
|
|
186
|
+
endpoint: str,
|
|
187
|
+
error: requests.exceptions.HTTPError,
|
|
188
|
+
log_kwargs: _ExternalLogKwargs,
|
|
189
|
+
) -> None:
|
|
190
|
+
log_external_service_error(error=error, **log_kwargs)
|
|
191
|
+
get_logger().warning(
|
|
192
|
+
"github_api_resource_not_found",
|
|
193
|
+
status_code=status_code,
|
|
194
|
+
operation=f"{method} {endpoint}",
|
|
195
|
+
severity="operational",
|
|
196
|
+
)
|
|
197
|
+
raise GitHubAPIError("Repository or resource not found.") from error
|
|
198
|
+
|
|
199
|
+
def _handle_410(
|
|
200
|
+
self,
|
|
201
|
+
status_code: int,
|
|
202
|
+
_response: requests.Response,
|
|
203
|
+
method: str,
|
|
204
|
+
endpoint: str,
|
|
205
|
+
error: requests.exceptions.HTTPError,
|
|
206
|
+
_log_kwargs: _ExternalLogKwargs,
|
|
207
|
+
) -> None:
|
|
208
|
+
get_logger().info(
|
|
209
|
+
"github_api_resource_gone",
|
|
210
|
+
status_code=status_code,
|
|
211
|
+
operation=f"{method} {endpoint}",
|
|
212
|
+
severity="operational",
|
|
213
|
+
)
|
|
214
|
+
raise GitHubAPIError("Resource has been deleted (410 Gone)") from error
|
|
215
|
+
|
|
216
|
+
def _handle_422(
|
|
217
|
+
self,
|
|
218
|
+
status_code: int,
|
|
219
|
+
response: requests.Response,
|
|
220
|
+
method: str,
|
|
221
|
+
endpoint: str,
|
|
222
|
+
error: requests.exceptions.HTTPError,
|
|
223
|
+
_log_kwargs: _ExternalLogKwargs,
|
|
224
|
+
) -> None:
|
|
225
|
+
_ = method, endpoint
|
|
226
|
+
error_data = response.json() if response.content else {}
|
|
227
|
+
error_details = [
|
|
228
|
+
f"{err.get('field', 'unknown')}:{err.get('code', 'unknown')} {err.get('message', '')}"
|
|
229
|
+
for err in error_data.get("errors", [])
|
|
230
|
+
if isinstance(err, dict)
|
|
231
|
+
]
|
|
232
|
+
error_msg = error_data.get("message", "Validation failed")
|
|
233
|
+
if error_details:
|
|
234
|
+
error_msg = f"{error_msg} - {'; '.join(error_details)}"
|
|
235
|
+
get_logger().warning(
|
|
236
|
+
"github_api_validation_error",
|
|
237
|
+
status_code=status_code,
|
|
238
|
+
validation_errors=error_details,
|
|
239
|
+
severity="data_error",
|
|
240
|
+
)
|
|
241
|
+
raise GitHubAPIError(f"Validation error: {error_msg}") from error
|
|
242
|
+
|
|
243
|
+
def _handle_429(
|
|
244
|
+
self,
|
|
245
|
+
status_code: int,
|
|
246
|
+
response: requests.Response,
|
|
247
|
+
method: str,
|
|
248
|
+
endpoint: str,
|
|
249
|
+
error: requests.exceptions.HTTPError,
|
|
250
|
+
_log_kwargs: _ExternalLogKwargs,
|
|
251
|
+
) -> None:
|
|
252
|
+
_ = method, endpoint
|
|
253
|
+
retry_after = response.headers.get("Retry-After", "unknown")
|
|
254
|
+
get_logger().warning(
|
|
255
|
+
"github_api_rate_limited",
|
|
256
|
+
status_code=status_code,
|
|
257
|
+
retry_after=retry_after,
|
|
258
|
+
severity="operational",
|
|
259
|
+
)
|
|
260
|
+
raise GitHubAPIError("Rate limit exceeded. Please try again later.") from error
|
|
261
|
+
|
|
262
|
+
def _handle_5xx(
|
|
263
|
+
self,
|
|
264
|
+
status_code: int,
|
|
265
|
+
method: str,
|
|
266
|
+
endpoint: str,
|
|
267
|
+
error: requests.exceptions.HTTPError,
|
|
268
|
+
log_kwargs: _ExternalLogKwargs,
|
|
269
|
+
) -> None:
|
|
270
|
+
log_external_service_error(error=error, **log_kwargs)
|
|
271
|
+
get_logger().warning(
|
|
272
|
+
"github_api_server_error",
|
|
273
|
+
status_code=status_code,
|
|
274
|
+
operation=f"{method} {endpoint}",
|
|
275
|
+
severity="infrastructure",
|
|
276
|
+
)
|
|
277
|
+
raise GitHubAPIError(f"GitHub API server error ({status_code})") from error
|
|
278
|
+
|
|
279
|
+
def _handle_unknown(
|
|
280
|
+
self,
|
|
281
|
+
status_code: int,
|
|
282
|
+
method: str,
|
|
283
|
+
endpoint: str,
|
|
284
|
+
error: requests.exceptions.HTTPError,
|
|
285
|
+
log_kwargs: _ExternalLogKwargs,
|
|
286
|
+
) -> None:
|
|
287
|
+
log_external_service_error(error=error, **log_kwargs)
|
|
288
|
+
get_logger().warning(
|
|
289
|
+
"github_api_unknown_error",
|
|
290
|
+
status_code=status_code,
|
|
291
|
+
operation=f"{method} {endpoint}",
|
|
292
|
+
severity="operational",
|
|
293
|
+
)
|
|
294
|
+
raise GitHubAPIError(f"GitHub API error ({status_code}): {error}") from error
|
|
295
|
+
|
|
296
|
+
def _make_request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
|
|
297
|
+
"""Make a request to the GitHub API.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
method: HTTP method (GET, POST, etc.)
|
|
301
|
+
endpoint: API endpoint path
|
|
302
|
+
**kwargs: Additional arguments to pass to requests
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
Response object
|
|
306
|
+
|
|
307
|
+
Raises:
|
|
308
|
+
GitHubAPIError: If the request fails
|
|
309
|
+
"""
|
|
310
|
+
url = f"{self.BASE_URL}{endpoint}"
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
response = self.session.request(method, url, **kwargs)
|
|
314
|
+
response.raise_for_status()
|
|
315
|
+
return response
|
|
316
|
+
except requests.exceptions.HTTPError as e:
|
|
317
|
+
self._raise_http_error(response.status_code, response, method, endpoint, e)
|
|
318
|
+
raise # unreachable but satisfies type checkers
|
|
319
|
+
except requests.exceptions.RequestException as e:
|
|
320
|
+
log_external_service_error(
|
|
321
|
+
error=e,
|
|
322
|
+
service_name="GitHub API",
|
|
323
|
+
operation=f"{method} {endpoint}",
|
|
324
|
+
retry_count=0,
|
|
325
|
+
)
|
|
326
|
+
raise GitHubAPIError(f"Request failed: {e}") from e
|
|
327
|
+
|
|
328
|
+
def test_authentication(self) -> dict[str, Any]:
|
|
329
|
+
"""Test authentication and get user info."""
|
|
330
|
+
response = self._make_request("GET", "/user")
|
|
331
|
+
return response.json()
|
|
332
|
+
|
|
333
|
+
def test_repository_access(self) -> dict[str, Any]:
|
|
334
|
+
"""Test repository access."""
|
|
335
|
+
self._check_repository()
|
|
336
|
+
response = self._make_request("GET", f"/repos/{self.owner}/{self.repo}")
|
|
337
|
+
return response.json()
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""GitHub Collaborators handler."""
|
|
2
|
+
|
|
3
|
+
from structlog import get_logger
|
|
4
|
+
|
|
5
|
+
from roadmap.adapters.github.handlers.base import BaseGitHubHandler
|
|
6
|
+
|
|
7
|
+
logger = get_logger()
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CollaboratorsHandler(BaseGitHubHandler):
|
|
11
|
+
"""Handler for GitHub team and collaborators API operations."""
|
|
12
|
+
|
|
13
|
+
def get_current_user(self) -> str:
|
|
14
|
+
"""Get the current authenticated user's login."""
|
|
15
|
+
response = self._make_request("GET", "/user")
|
|
16
|
+
return response.json()["login"]
|
|
17
|
+
|
|
18
|
+
def get_repository_collaborators(self) -> list[str]:
|
|
19
|
+
"""Get all collaborators for the repository.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
List of usernames of repository collaborators
|
|
23
|
+
"""
|
|
24
|
+
self._check_repository()
|
|
25
|
+
|
|
26
|
+
# Get direct collaborators
|
|
27
|
+
collaborators = []
|
|
28
|
+
page = 1
|
|
29
|
+
per_page = 100
|
|
30
|
+
|
|
31
|
+
while True:
|
|
32
|
+
response = self._make_request(
|
|
33
|
+
"GET",
|
|
34
|
+
f"/repos/{self.owner}/{self.repo}/collaborators",
|
|
35
|
+
params={"page": page, "per_page": per_page},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
page_collaborators = response.json()
|
|
39
|
+
if not page_collaborators:
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
collaborators.extend([collab["login"] for collab in page_collaborators])
|
|
43
|
+
|
|
44
|
+
if len(page_collaborators) < per_page:
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
page += 1
|
|
48
|
+
|
|
49
|
+
return sorted(collaborators)
|
|
50
|
+
|
|
51
|
+
def get_repository_contributors(self) -> list[str]:
|
|
52
|
+
"""Get all contributors to the repository.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
List of usernames of repository contributors
|
|
56
|
+
"""
|
|
57
|
+
self._check_repository()
|
|
58
|
+
|
|
59
|
+
contributors = []
|
|
60
|
+
page = 1
|
|
61
|
+
per_page = 100
|
|
62
|
+
|
|
63
|
+
while True:
|
|
64
|
+
response = self._make_request(
|
|
65
|
+
"GET",
|
|
66
|
+
f"/repos/{self.owner}/{self.repo}/contributors",
|
|
67
|
+
params={"page": page, "per_page": per_page},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
page_contributors = response.json()
|
|
71
|
+
if not page_contributors:
|
|
72
|
+
break
|
|
73
|
+
|
|
74
|
+
contributors.extend([contrib["login"] for contrib in page_contributors])
|
|
75
|
+
|
|
76
|
+
if len(page_contributors) < per_page:
|
|
77
|
+
break
|
|
78
|
+
|
|
79
|
+
page += 1
|
|
80
|
+
|
|
81
|
+
return sorted(contributors)
|
|
82
|
+
|
|
83
|
+
def get_team_members(self) -> list[str]:
|
|
84
|
+
"""Get all team members (collaborators and contributors combined).
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
List of unique usernames from collaborators and contributors
|
|
88
|
+
"""
|
|
89
|
+
collaborators = self.get_repository_collaborators()
|
|
90
|
+
contributors = self.get_repository_contributors()
|
|
91
|
+
|
|
92
|
+
# Combine and deduplicate
|
|
93
|
+
team_members = list(set(collaborators + contributors))
|
|
94
|
+
return sorted(team_members)
|
|
95
|
+
|
|
96
|
+
def validate_user_exists(self, username: str) -> bool:
|
|
97
|
+
"""Check if a GitHub user exists.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
username: GitHub username to validate
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if user exists, False otherwise
|
|
104
|
+
"""
|
|
105
|
+
try:
|
|
106
|
+
response = self._make_request("GET", f"/users/{username}")
|
|
107
|
+
return response.status_code == 200
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.error(
|
|
110
|
+
"user_validation_failed",
|
|
111
|
+
username=username,
|
|
112
|
+
error=str(e),
|
|
113
|
+
severity="infrastructure",
|
|
114
|
+
)
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
def validate_user_has_repository_access(self, username: str) -> bool:
|
|
118
|
+
"""Check if a user has access to the repository (is collaborator or contributor).
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
username: GitHub username to validate
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
True if user has repository access, False otherwise
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
team_members = self.get_team_members()
|
|
128
|
+
return username in team_members
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error(
|
|
131
|
+
"access_validation_failed",
|
|
132
|
+
username=username,
|
|
133
|
+
error=str(e),
|
|
134
|
+
severity="infrastructure",
|
|
135
|
+
)
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def validate_assignee(self, assignee: str) -> tuple[bool, str]:
|
|
139
|
+
"""Validate an assignee for issue assignment.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
assignee: Username to validate
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Tuple of (is_valid, error_message)
|
|
146
|
+
- (True, "") if valid
|
|
147
|
+
- (False, error_message) if invalid
|
|
148
|
+
"""
|
|
149
|
+
if not assignee or not assignee.strip():
|
|
150
|
+
return False, "Assignee cannot be empty"
|
|
151
|
+
|
|
152
|
+
assignee = assignee.strip()
|
|
153
|
+
|
|
154
|
+
# First check if user exists on GitHub
|
|
155
|
+
if not self.validate_user_exists(assignee):
|
|
156
|
+
return False, f"GitHub user '{assignee}' does not exist"
|
|
157
|
+
|
|
158
|
+
# Then check if they have repository access
|
|
159
|
+
if not self.validate_user_has_repository_access(assignee):
|
|
160
|
+
return (
|
|
161
|
+
False,
|
|
162
|
+
f"User '{assignee}' does not have access to repository {self.owner}/{self.repo}",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
return True, ""
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""GitHub Comments handler."""
|
|
2
|
+
|
|
3
|
+
from roadmap.adapters.github.handlers.base import BaseGitHubHandler
|
|
4
|
+
from roadmap.common.datetime_parser import parse_github_datetime
|
|
5
|
+
from roadmap.core.domain import Comment
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CommentsHandler(BaseGitHubHandler):
|
|
9
|
+
"""Handler for GitHub Comments API operations."""
|
|
10
|
+
|
|
11
|
+
def _create_comment_from_response(
|
|
12
|
+
self, comment_data: dict, issue_id: str = ""
|
|
13
|
+
) -> Comment:
|
|
14
|
+
"""Create a Comment object from GitHub API response data.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
comment_data: GitHub API comment response
|
|
18
|
+
issue_id: Issue ID (may be empty for standalone comment endpoints)
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Comment object
|
|
22
|
+
"""
|
|
23
|
+
return Comment(
|
|
24
|
+
id=comment_data["id"],
|
|
25
|
+
issue_id=issue_id,
|
|
26
|
+
author=comment_data["user"]["login"],
|
|
27
|
+
body=comment_data["body"],
|
|
28
|
+
created_at=parse_github_datetime(comment_data["created_at"]),
|
|
29
|
+
updated_at=parse_github_datetime(comment_data["updated_at"]),
|
|
30
|
+
github_url=comment_data["html_url"],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def get_issue_comments(self, issue_number: int) -> list[Comment]:
|
|
34
|
+
"""Get all comments for a specific issue.
|
|
35
|
+
|
|
36
|
+
Handles pagination automatically - returns all comments.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
issue_number: GitHub issue number
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
List of all Comment objects for the issue
|
|
43
|
+
"""
|
|
44
|
+
self._check_repository()
|
|
45
|
+
|
|
46
|
+
comments_data = self._paginate_request(
|
|
47
|
+
"GET", f"/repos/{self.owner}/{self.repo}/issues/{issue_number}/comments"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
comments = []
|
|
51
|
+
for comment_data in comments_data:
|
|
52
|
+
comment = self._create_comment_from_response(
|
|
53
|
+
comment_data, issue_id=str(issue_number)
|
|
54
|
+
)
|
|
55
|
+
comments.append(comment)
|
|
56
|
+
|
|
57
|
+
return comments
|
|
58
|
+
|
|
59
|
+
def create_issue_comment(self, issue_number: int, body: str) -> Comment:
|
|
60
|
+
"""Create a new comment on an issue.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
issue_number: GitHub issue number
|
|
64
|
+
body: Comment content (markdown)
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Created Comment object
|
|
68
|
+
"""
|
|
69
|
+
self._check_repository()
|
|
70
|
+
|
|
71
|
+
data = {"body": body}
|
|
72
|
+
|
|
73
|
+
response = self._make_request(
|
|
74
|
+
"POST",
|
|
75
|
+
f"/repos/{self.owner}/{self.repo}/issues/{issue_number}/comments",
|
|
76
|
+
json=data,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
comment_data = response.json()
|
|
80
|
+
return self._create_comment_from_response(
|
|
81
|
+
comment_data, issue_id=str(issue_number)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def update_issue_comment(self, comment_id: int, body: str) -> Comment:
|
|
85
|
+
"""Update an existing comment.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
comment_id: GitHub comment ID
|
|
89
|
+
body: Updated comment content (markdown)
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Updated Comment object
|
|
93
|
+
"""
|
|
94
|
+
self._check_repository()
|
|
95
|
+
|
|
96
|
+
data = {"body": body}
|
|
97
|
+
|
|
98
|
+
response = self._make_request(
|
|
99
|
+
"PATCH",
|
|
100
|
+
f"/repos/{self.owner}/{self.repo}/issues/comments/{comment_id}",
|
|
101
|
+
json=data,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
comment_data = response.json()
|
|
105
|
+
# We don't get issue number from this endpoint
|
|
106
|
+
return self._create_comment_from_response(comment_data, issue_id="")
|
|
107
|
+
|
|
108
|
+
def delete_issue_comment(self, comment_id: int) -> None:
|
|
109
|
+
"""Delete a comment.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
comment_id: GitHub comment ID
|
|
113
|
+
"""
|
|
114
|
+
self._check_repository()
|
|
115
|
+
|
|
116
|
+
self._make_request(
|
|
117
|
+
"DELETE", f"/repos/{self.owner}/{self.repo}/issues/comments/{comment_id}"
|
|
118
|
+
)
|